JavaScript 脚本(适用于 UDP 和 TCP)
本文件介绍了如何通过 JavaScript 脚本在 Aqara Studio 中自定义添加 UDP 和 TCP 设备、功能点及其解析函数。
脚本介绍
在 UDP 和 TCP 集成场景下,Aqara Studio 会通过您编写的 JavaScript 脚本完成以下工作:
- 生成设备发现、点位发现、心跳检测、读写控制等请求报文。
- 解析设备返回的字节数据。
- 将原始字节报文转换为 Aqara Studio 可识别的标准对象结构。
- 解析设备主动上报的点值变化。
根据接入方式和设备协议设计,您通常会在脚本中涉及以下 14 个函数(其中部分可按设备协议选择性实现):
| 函数 | 说明 |
|---|---|
| buildDiscoveryRequest | 用于生成发现设备的请求报文 |
| buildDiscoverPointsRequest | 用于生成查询设备点信息的请求报文 |
| buildProbeRequest | 用于生成检测设备心跳的请求报文 |
| buildReadRequest | 用于生成读取设备点数据的请求报文 |
| buildWriteRequest | 用于生成写入设备点数据的请求报文 |
| buildReportResponse | 用于生成回复设备主动上报报文的响应数据 |
| packetProcessConfig | 用于定义如何从字节流中截取完整报文 |
| dispatch | 用于根据消息内容分发给对应解析函数 |
| parseDiscoveryResponse | 用于解析发现设备的响应报文 |
| parseDiscoverPointsResponse | 用于解析设备点信息响应报文 |
| parseProbeResponse | 用于解析设备心跳检测响应报文 |
| parseReadResponse | 用于解析点数据读取响应报文 |
| parseWriteResponse | 用于解析点数据写入响应报文 |
| parseReportRequest | 用于解析设备主动上报报文 |
请勿更改函数名称和参数定义。build*Request 函数需要返回设备协议报文,可使用 Uint8Array、number[]、十六进制字符串或普通字符串,平台会转换为字节数组后通过 TCP/UDP 发送。
并非所有函数都必须在所有设备场景下实现。例如,如果您的设备不需要主动上报回复,buildReportResponse 可以不实现;如果没有独立的点位发现请求,也可通过 parseDiscoverPointsResponse 直接返回点信息。
脚本全局变量
协议脚本可以使用 globalThis.studioScriptState 在同一个 Context 的多次函数调用之间共享少量临时状态。该变量默认初始化为空对象:
globalThis.studioScriptState = {
requestCount: 0,
lastValue: null
};
请保持该变量为普通对象。对象属性可保存字符串、有限数值、布尔值、null、普通对象和数组,并可使用这些类型相互嵌套。日期应先转换为字符串。不要保存 undefined、函数、Symbol、BigInt、NaN、Infinity、循环引用、Map、Set、Java/Host 对象或自定义类实例;这些值可能被忽略、转换失真或导致序列化失败。
状态大小按序列化后的 UTF-8 JSON 字节数计算,告警阈值为 2 MiB。Studio 在脚本调用后限频检查,首次越界时会同时向服务日志和协议配置页的 Console Content 写入一条 [warn];持续超限不会定期重复打印,也不会自动删除状态或终止本次调用。开发脚本时应确保该变量长期保持在 2 MiB 以内。
Context 被空闲回收、内存压力回收、脚本更新或执行超时后,globalThis.studioScriptState 会重新初始化。请勿用它替代 packetProcessConfig 完成跨报文组帧,也不要保存必须持久化的设备状态、凭据或无限增长的历史消息。
前提条件
在编写脚本前,请先充分了解目标设备的 UDP 或 TCP 协议细节,包括但不限于:
- 设备发现报文格式
- 功能点发现报文格式
- 心跳检测报文格式
- 点位读取与写入报文格式
- 设备主动上报报文格式
- 字节流分包规则、帧头规则和报文长度规则
- 设备返回数据的字段结构和字段含义
只有充分理解上述协议内容,才能确保 JavaScript 脚本按照设备规范正确完成报文构建与数据解析。
buildDiscoveryRequest
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何向设备发送“发现设备”的字节报文。
入参说明
该函数无入参。设备发现不使用 msgIdMatch。
返回值
为确保 Aqara Studio 可以正常向设备发起请求,您需要确保 buildDiscoveryRequest 函数返回的是字节数组,例如:
Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0))
函数示例
function buildDiscoveryRequest() {
return Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0));
}
buildDiscoverPointsRequest
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何向指定设备发送“发现功能点”的报文。如果设备点位是静态的,可以不实现该函数或返回空,平台会调用 parseDiscoverPointsResponse(deviceId, undefined)。
入参说明
建议该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| msgId | String | 请求消息 ID |
| deviceId | String | 设备 ID |
返回值
返回值必须是字节数组,用于表示点位发现请求报文。
函数示例
function buildDiscoverPointsRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#DEVICE_POINT_SCAN#" + deviceId, c => c.charCodeAt(0));
}
buildProbeRequest
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何向设备发送心跳检测报文。
入参说明
建议该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| msgId | String | 请求消息 ID。若启用了基于 msgIdMatch 的响应匹配,则必须传入。 |
| deviceId | String | 设备 ID |
返回值
返回值必须是字节数组,用于表示心跳检测报文。
函数示例
function buildProbeRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#SHAKEHANDS#\r\n", c => c.charCodeAt(0));
}
buildReadRequest
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何向设备发送“读取点值”的报文。
入参说明
建议该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| msgId | String | 请求消息 ID |
| deviceId | String | 设备 ID |
| pointId | String | 点位 ID |
返回值
返回值必须是字节数组,用于表示点位读取报文。
函数示例
function buildReadRequest(msgId, deviceId, pointId) {
return Uint8Array.from(msgId + "#POLL#" + pointId + "\r\n", c => c.charCodeAt(0));
}
buildWriteRequest
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何向设备发送“写入点值”的控制报文。
入参说明
建议该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| msgId | String | 请求消息 ID |
| deviceId | String | 设备 ID |
| pointId | String | 点位 ID |
| value | String / Number / Boolean / Int(enum) | 待写入的控制值 |
返回值
返回值必须是字节数组,用于表示控制报文。
函数示例
function buildWriteRequest(msgId, deviceId, pointId, value) {
return Uint8Array.from(msgId + "#WRITE#" + "#" + pointId + "#" + value, c => c.charCodeAt(0));
}
buildReportResponse
请在该函数中返回一个可转换为字节数组的设备协议报文,用于指示 Aqara Studio 如何回复设备主动上报的报文。
如果您的协议不要求对上报报文进行回复,则可以不实现该函数。
入参说明
建议该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| msgId | String | 请求消息 ID |
| pointId | String | 点位 ID |
| code | Byte | 回复状态码,0 表示成功 |
| errorMsg | String | 错误原因 |
返回值
返回值必须是字节数组,用于表示回复上报的报文。
函数示例
function buildReportResponse(msgId, pointId, code, errorMsg) {
return Uint8Array.from(msgId + "#EVT_REPLY#OK", c => c.charCodeAt(0));
}
packetProcessConfig
请在该函数中返回一个对象,用于定义 Aqara Studio 如何从 UDP/TCP 字节流中截取一条完整报文。
如果您不定义该函数,Aqara Studio 默认每次读取 128 字节作为响应值,这通常只适用于非常简单的报文场景。
返回值
返回值格式如下:
{
"frameStartBytes": [0, 216],
"lengthKnownAfterBytes": 4,
"remainingBytes": 1
}
字段说明如下:
| 字段 | 类型 | 含义 |
|---|---|---|
| frameStartBytes | number[] | 表示帧头字节序列,用于识别一条报文的开始 |
| lengthKnownAfterBytes | number | 表示从帧头之后第几个字节开始可以得知长度 |
| remainingBytes | number | 在已知长度字段之后,仍需继续读取的剩余字节数 |
| totalLength | number | 报文总长度。如果不使用 lengthKnownAfterBytes 和 remainingBytes,则可直接定义总长度 |
frameStartBytes、lengthKnownAfterBytes、remainingBytes 为必填;totalLength 可选。通常 lengthKnownAfterBytes + remainingBytes 与 totalLength 二选一配置即可。具体如何配置,请以设备的帧结构协议为准。
函数示例
function packetProcessConfig() {
return {
frameStartBytes: [0x00, 0xD8],
lengthKnownAfterBytes: 4,
remainingBytes: 1
};
}
dispatch
该函数是 TCP/UDP 脚本的必填函数。
请在该函数中实现“报文分发逻辑”,以便 Aqara Studio 根据设备返回的字节报文判断应调用哪个解析函数。
作用说明
Aqara Studio 收到一条报文后,会先调用该函数。您需要在此函数中决定:
- 当前报文对应哪个请求的响应;
- 或者应交由哪个解析函数处理。
msgId 与 path 相互独立:msgId 用于事务匹配,path 用于校验并选择解析函数。
- 同时返回
msgId和path时,Studio 先按请求 ID 定位事务,再校验解析函数; path为空但msgId不为空时,Studio 匹配事务后,按该请求期望的解析函数处理;path和msgId都为空时,Studio 默认按parseReportRequest处理;path为parseReportRequest时始终按主动上报处理,即使携带msgId也不会占用等待事务;- 其他不受支持的非空
path会被拒绝并记录警告。
协议配置字段 msgIdMatch 是请求 ID 校验开关,默认关闭。关闭时,buildDiscoverPointsRequest、buildProbeRequest、buildReadRequest 和 buildWriteRequest 收到的第一个 msgId 参数为空字符串;开启时,Studio 为每次请求生成非空 ID,脚本应将其写入请求报文,并由 dispatch 从响应中返回相同 ID。Studio 使用该 ID 匹配等待事务及乱序响应;ID 缺失、不一致或已过期时不会完成当前事务,该操作可能重试或超时。此字段不决定请求是否等待响应,也不替代 path,并且不适用于设备发现和 buildReportResponse。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 设备返回的原始字节报文 |
返回值
返回值对象可包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| msgId | String | 从响应中提取的请求消息 ID。启用 msgIdMatch 时,必须与请求报文中的 ID 一致。 |
| path | String | 对应的解析函数名称,例如 parseProbeResponse |
返回值示例
{
"msgId": "msg.1234",
"path": "parseProbeResponse"
}
函数示例
function dispatch(fromAcceptMessage) {
const dataStr = String.fromCharCode.apply(null, fromAcceptMessage)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
const reply = parts[1];
if (reply == "SHAKEHANDS_REPLY") {
return {
msgId: parts[0],
path: "parseProbeResponse"
};
} else if (reply == "EVT_REPLY") {
return {
msgId: parts[0],
path: "parseWriteResponse"
};
} else if (reply == "EVT") {
return {
msgId: parts[0],
path: "parseReportRequest"
};
}
}
parseDiscoveryResponse
请在该函数中解析设备发现响应报文,并返回设备信息对象数组。
如果未定义 buildDiscoveryRequest,Aqara Studio 也可能直接使用该函数返回的数据作为设备发现依据,因此请确保返回结果结构稳定。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 设备发现响应的原始字节报文 |
返回值
返回值格式如下:
[
{
"deviceName": "dn1",
"model": "model-x",
"vendor": "simensA",
"firmwareVersion": "1.1.0",
"deviceId": "uuid1234",
"ip": "设备ip",
"port": 1102,
"localPort": 1103
}
]
字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deviceName | String | 否 | 设备名称 |
| model | String | 否 | 型号 |
| vendor | String | 否 | 厂商 |
| firmwareVersion | String | 否 | 固件版本 |
| ip | String | 是 | 设备 IP 地址 |
| port | Int | 是 | UDP/TCP 端口 |
| localPort | Int | 否 | 仅用于 UDP。Studio 接收响应和主动上报时绑定的本地监听端口。未返回或返回 0 时使用 port;TCP 会忽略该字段。 |
| deviceId | String | 是 | 设备唯一标识 |
localPort 适用于设备远端服务端口与 Studio 接收端口不同、设备要求客户端使用固定源端口,或者多个 UDP 设备需要避免本地监听端口冲突的场景。大多数请求与响应使用同一端口的设备无需返回该字段。
当前每个 UDP 设备独立绑定本地监听端口。请确保同时运行的 UDP 设备使用可用且不冲突的 localPort。如果多个设备必须向 Studio 的同一个本地端口主动上报,当前接入方式不支持共享该监听端口。
函数示例
function parseDiscoveryResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const snMatch = dataStr.match(/SN=([^&]+)/);
const ipMatch = dataStr.match(/IP=([^\r\n&]+)/);
return [
{
deviceId: snMatch ? snMatch[1].trim() : "",
ip: ipMatch ? ipMatch[1].trim() : "",
port: 8820
}
];
}
parseDiscoverPointsResponse
请在该函数中解析设备点信息,并返回点对象数组。
入参说明
该函数接收以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| deviceId | String | 设备 ID,平台固定作为第一个参数传入,必填 |
| fromAcceptMessage | Uint8Array | undefined | 如果定义了 buildDiscoverPointsRequest 并收到设备响应,则作为第二个参数传入;没有独立点位发现请求时可能为空 |
返回值
返回值应为点对象数组,示例如下:
[
{
"pointId": "p1",
"pointName": "pn1",
"devType": "Light",
"functionCode": "Output",
"traitCode": "OnOff"
},
{
"pointId": "p2",
"pointName": "pn2",
"dataType": "numerical",
"min": -20,
"max": 50,
"step": 0.1,
"precision": 0.1,
"decimals": 1,
"access": 3
}
]
字段说明如下:
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| pointId | String | 必填 | 点位唯一 ID,后续读写都会使用 |
| pointName | String | 必填 | 点位名称 |
| devType | String | 条件必填 | 对应 Aqara 物模型规范的 deviceType。基于 Aqara Spec 定义点位时填写,具体可用值请阅读 设备类型。 |
| functionCode | String | 条件必填 | 对应 Aqara 物模型规范的 functionCode。基于 Aqara Spec 定义点位时填写,具体可用值请阅读 功能代码。 |
| traitCode | String | 条件必填 | 对应 Aqara 物模型规范的 traitCode。基于 Aqara Spec 定义点位时填写,具体可用值请阅读 功能点代码。 |
| dataType | String | 条件必填 | 通用点位必填。通用点位的数据类型,支持 enum、bool、numerical、text |
| enumRange | Array | 条件必填 | 枚举选项数组。仅当通用点位的 dataType 为 enum 时必填。数组不能为空;每项包含整数类型的 value 和字符串类型的 label,value 和 label 均不可重复,且 label 不可为空 |
| trueText / falseText | String | 可选 | 布尔型通用点位的显示文案。仅当 dataType 为 bool 时生效;未配置时默认 trueText 为 开,falseText 为 关。仅在需要自定义文案时配置,例如 启用 / 禁用。也可使用 activeText / onText 和 inactiveText / offText 作为别名 |
| min | Number | 可选 | 数值型通用点位的最小值。仅当 dataType 为 numerical 时生效;也可使用 minimum 作为别名 |
| max | Number | 可选 | 数值型通用点位的最大值。仅当 dataType 为 numerical 时生效;也可使用 maximum 作为别名 |
| step | Number | 可选 | 数值型通用点位的步长,必须大于 0。仅当 dataType 为 numerical 时生效;也可使用 resolution 作为别名 |
| precision | Number | 可选 | 数值型通用点位的精度值,必须大于等于 0。仅当 dataType 为 numerical 时生效 |
| decimals | Int | 可选 | 数值型通用点位的小数位数,必须为大于等于 0 的整数。仅当 dataType 为 numerical 时生效 |
| unit / units | String | 可选 | 数值型通用点位的单位。仅当 dataType 为 numerical 时生效;建议填写系统已注册的单位名称,例如 kilowatt hour、watt、volt、ampere、celsius、percent。Studio 会展示对应符号,例如 kWh、W、V、A、°C、%。完整映射请参见 脚本支持的单位名称与符号。系统也可匹配已注册的单位符号 |
| access | Int | 条件必填 | 通用点位必填。访问权限位,bit1 表示读,bit2 表示写,bit3 表示上报 |
对于 numerical 点,Aqara Studio 在写值调用 buildWriteRequest 时会优先依据 decimals 格式化数值;未配置 decimals 时会依据 step / resolution 推导小数位。为了避免 0.3 被二进制浮点误差表现为 0.30000030249357224,建议需要小数写入的点显式配置 decimals 或 step。precision 仅用于描述精度,不作为写值报文的小数位截断规则。
如果数值点会被能耗统计或其他依赖单位的功能使用,请配置 unit 或 units,例如累计电量使用 kilowatt hour(显示 kWh),当前功率使用 watt(显示 W),电压使用 volt(显示 V),电流使用 ampere(显示 A),温度使用 celsius(显示 °C)。单位名称请按 脚本支持的单位名称与符号 中的原文填写,大小写和空格保持一致。
如果您已基于 Aqara Spec 定义了 devType、functionCode、traitCode,则可不再依赖 dataType 和 access 的通用模式;如果未使用 Aqara Spec,则 dataType 和 access 必须提供。
函数示例
function parseDiscoverPointsResponse(deviceId, fromAcceptMessage) {
return [
{
pointId: "sceneWrite",
pointName: "场景执行",
dataType: "text",
access: 2
},
{
pointId: "sceneListen",
pointName: "场景监听",
dataType: "text",
access: 1
},
{
pointId: "setTemperature",
pointName: "设定温度",
dataType: "numerical",
min: -20,
max: 50,
step: 0.1,
precision: 0.1,
decimals: 1,
unit: "celsius",
access: 3
},
{
pointId: "operatingMode",
pointName: "运行模式",
dataType: "enum",
enumRange: [
{ value: 0, label: "关闭" },
{ value: 1, label: "自动" }
],
access: 3
}
];
}
parseProbeResponse
请在该函数中解析设备心跳检测响应,并返回标准状态对象。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 设备返回的心跳响应报文 |
返回值
返回值格式如下:
{
"code": 0,
"errorMsg": ""
}
字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | Byte | 是 | 0 表示成功,其他值表示失败 |
| errorMsg | String | 否 | 错误说明 |
| deviceId | String | 否 | 当未使用 msgId 匹配时,可返回设备 ID |
函数示例
function parseProbeResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts.length == 3 && parts[2] == "SUCCEED") {
return {
code: 0,
errorMsg: ""
};
} else {
return {
code: 1,
errorMsg: "no reason"
};
}
}
parseReadResponse
请在该函数中解析设备点值读取响应,并返回点位值对象。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 读取点值的响应报文 |
返回值
返回值格式如下:
{
"pointId": "p.1234",
"value": 1
}
字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pointId | String | 否 | 当未使用 msgId 匹配时,建议返回点位 ID |
| value | String / Double / Boolean / Int(enum) | 是 | 点位值 |
函数示例
function parseReadResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
pointId: parts[0],
value: parts[1]
};
}
parseWriteResponse
请在该函数中解析设备控制响应,并返回标准写入结果对象。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 写入点值后的响应报文 |
返回值
返回值格式如下:
{
"pointId": "p.1234",
"code": 0,
"errorMsg": ""
}
字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | Byte | 是 | 0 表示成功 |
| errorMsg | String | 是 | 错误说明,若为空表示成功 |
| pointId | String | 否 | 当未使用 msgId 匹配时,建议返回点位 ID |
函数示例
function parseWriteResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts[2] == "OK") {
return {
code: 0,
errorMsg: ""
};
} else {
return {
code: 1,
errorMsg: parts[2]
};
}
}
parseReportRequest
请在该函数中解析设备主动上报的点值报文,并一次返回报文中的所有点位值。每条完整报文只调用一次该函数,返回多个点时不会为每个点重复执行脚本。
前提条件
只有当设备支持主动上报,或者您的接入方案依赖“设备主动推送点值变化”时,才需要重点实现该函数。
入参说明
| 参数 | 类型 | 说明 |
|---|---|---|
| fromAcceptMessage | Uint8Array | 设备主动上报的原始字节报文 |
返回值
返回值必须是包含 points 数组的对象:
{
points: [
{ pointId: "temperature", value: 23.5 },
{ pointId: "humidity", value: 60 }
]
}
points 数组项的字段说明如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pointId | String | 是 | 点位 ID |
| value | String / Double / Boolean / Int(enum) | 是 | 点位值 |
函数示例
function parseReportRequest(msg) {
if (!msg || msg.length === 0) {
return { points: [] };
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
points: [
{
pointId: parts[0],
value: parts[1]
}
]
};
}
完整示例
以下示例展示了一个简化的 UDP/TCP 接入脚本结构,您可以根据设备协议进行调整:
示例中的 globalThis 注册是动态的:脚本实际实现了哪些函数,就只注册哪些函数;未实现的函数不需要注册。
(function(){
// 仅保存可 JSON 序列化的临时状态;Context 重建后会清空。
const scriptState = globalThis.studioScriptState;
scriptState.reportCount = 0;
scriptState.lastReportAt = null;
function buildDiscoveryRequest() {
return Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0));
}
function buildDiscoverPointsRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#DEVICE_POINT_SCAN#" + deviceId, c => c.charCodeAt(0));
}
function buildProbeRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#SHAKEHANDS#\r\n", c => c.charCodeAt(0));
}
function buildReadRequest(msgId, deviceId, pointId) {
return Uint8Array.from(msgId + "#POLL#" + pointId + "\r\n", c => c.charCodeAt(0));
}
function buildWriteRequest(msgId, deviceId, pointId, value) {
return Uint8Array.from(msgId + "#WRITE#" + "#" + pointId + "#" + value, c => c.charCodeAt(0));
}
function buildReportResponse(msgId, pointId, code, errorMsg) {
return Uint8Array.from(msgId + "#EVT_REPLY#OK", c => c.charCodeAt(0));
}
function packetProcessConfig() {
return { frameStartBytes: [0x00, 0xD8], lengthKnownAfterBytes: 4, remainingBytes: 1 };
}
function dispatch(fromAcceptMessage) {
const dataStr = String.fromCharCode.apply(null, fromAcceptMessage)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
const reply = parts[1];
if (reply == "SHAKEHANDS_REPLY") {
return { msgId: parts[0], path: "parseProbeResponse" };
} else if (reply == "EVT_REPLY") {
return { msgId: parts[0], path: "parseWriteResponse" };
} else if (reply == "EVT") {
return { msgId: parts[0], path: "parseReportRequest" };
}
}
function parseDiscoveryResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const snMatch = dataStr.match(/SN=([^&]+)/);
const ipMatch = dataStr.match(/IP=([^\r\n&]+)/);
return [
{
deviceId: snMatch ? snMatch[1].trim() : "",
ip: ipMatch ? ipMatch[1].trim() : "",
port: 8820
}
];
}
function parseDiscoverPointsResponse(deviceId, fromAcceptMessage) {
return [
{
pointId: "sceneWrite",
pointName: "场景执行",
dataType: "text",
access: 2
},
{
pointId: "sceneListen",
pointName: "场景监听",
dataType: "text",
access: 1
}
];
}
function parseProbeResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts.length == 3 && parts[2] == "SUCCEED") {
return { code: 0, errorMsg: "" };
} else {
return { code: 1, errorMsg: "no reason" };
}
}
function parseReadResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
pointId: parts[0],
value: parts[1]
};
}
function parseWriteResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts[2] == "OK") {
return { code: 0, errorMsg: "" };
} else {
return { code: 1, errorMsg: parts[2] };
}
}
function parseReportRequest(msg) {
scriptState.reportCount += 1;
scriptState.lastReportAt = new Date().toISOString();
if (!msg || msg.length === 0) return { points: [] };
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
points: [
{
pointId: parts[0],
value: parts[1]
}
]
};
}
globalThis.buildDiscoveryRequest = buildDiscoveryRequest;
globalThis.buildDiscoverPointsRequest = buildDiscoverPointsRequest;
globalThis.buildProbeRequest = buildProbeRequest;
globalThis.buildReadRequest = buildReadRequest;
globalThis.buildWriteRequest = buildWriteRequest;
globalThis.buildReportResponse = buildReportResponse;
globalThis.packetProcessConfig = packetProcessConfig;
globalThis.dispatch = dispatch;
globalThis.parseDiscoveryResponse = parseDiscoveryResponse;
globalThis.parseDiscoverPointsResponse = parseDiscoverPointsResponse;
globalThis.parseProbeResponse = parseProbeResponse;
globalThis.parseReadResponse = parseReadResponse;
globalThis.parseWriteResponse = parseWriteResponse;
globalThis.parseReportRequest = parseReportRequest;
})()
排查脚本问题
在完成脚本开发后,如果您无法在 Aqara Studio 完成发现设备或功能点等操作,请参考以下步骤检查脚本是否正常运行:
- 使用 控制台日志 检查脚本内各方法是否被正常调用;
- 阅读 脚本检查要点,逐项检查脚本是否满足 Aqara Studio 的校验要求。
- 如果以上步骤都无法解决问题,可能是因为 Aqara Studio 的 运行资源限制 导致脚本无法正确运行。
控制台日志
-
为调试和排查脚本各方法(例如
parseDiscoveryResponse、parseDiscoverPointsResponse等),可在函数内部任意位置添加如下控制台日志方法:console.logconsole.infoconsole.debugconsole.warnconsole.error
提示请勿在循环或高频报文处理函数中无条件输出日志,调试时应使用条件开关或采样输出。
示例代码:
function parseDiscoveryResponse() {
console.info("开始解析发现响应", arguments);
return [
{
deviceName: "xxx",
// ...
}
];
}
提示- 多参数时,日志内容将使用空格拼接输出。
console.warn和console.error的输出分别带有[warn]和[error]前缀。
-
在 Aqara Studio 中重新发起相关操作(如设备发现、功能点发现、功能点读取、写入、上报等),即可触发包含日志的方法。
-
进入 UDP 协议配置 或 TCP 协议配置 页面,查看
Console Content字段是否有日志输出:- 若日志正常显示,说明方法已正确接入;
- 若没有日志,请检查方法实现是否有误。
提示Console Content最多保留 2048 个字符;超出限制时旧内容会被替换。- 每个脚本解析器每秒最多接收 20 条日志,超出部分会被忽略并在下一时间窗口输出汇总提示。
- 单条日志最多保留 512 个字符并转换前 16 个参数。
脚本检查要点
建议您结合 Aqara Studio 各业务节点的校验流程,逐项检查脚本是否满足平台要求,确保各关键节点通过校验,提升脚本可靠性与兼容性。
| 业务节点 | 调用函数 | 主要校验 |
|---|---|---|
| 脚本加载 | 完整脚本 | 脚本不超过 1,048,576 个字符并且能够执行;加载后检查 dispatch 及协议期望函数,校验结果用于诊断,但不会阻止脚本加载。 |
| 报文组帧和分发 | packetProcessConfig / dispatch(bytes) | 传入 JavaScript 的单条完整报文不得超过 64 KiB;dispatch 应返回 path、msgId 或两者。仅返回 msgId 时交给事务匹配,两者都为空时按主动上报处理。 |
| 设备和功能点发现 | parseDiscoveryResponse / parseDiscoverPointsResponse | 发现结果必须是数组;单次最多处理 128 个设备或 512 个功能点;无效条目会被逐项忽略。 |
| 探测、读取和写入 | parseProbeResponse / parseReadResponse / parseWriteResponse | 根据 dispatch.path 选择函数;响应结构、功能点匹配和数值类型必须有效,否则本次操作失败,但不影响其他 Context。 |
| 设备主动上报 | parseReportRequest(bytes) | 函数每条报文只调用一次;返回值必须包含 points 数组,单次最多处理 512 项,并逐项校验 pointId、value、点匹配和类型转换。 |
| 请求报文和上报响应 | build*Request / buildReportResponse | 返回值必须能够转换为字节数组;空值表示本次不发送报文。 |
运行资源限制
- 传入 JavaScript 的单条 TCP/UDP 二进制报文不得超过 64 KiB;超过限制的报文不会交给 JavaScript 处理。
- 单次设备发现最多处理 128 个设备,单次功能点发现最多处理 512 个功能点;超出部分会被忽略。
- 单条主动上报最多处理 512 个功能点值;超出部分会被忽略。
- 脚本 Context 在长时间无调用或系统内存压力较高时可能被回收并在下次调用时重建。请勿依赖全局变量永久保存设备状态,跨报文组帧应由
packetProcessConfig和通信层完成。 - 当脚本执行队列已满或系统内存压力较高时,本次脚本调用可能被拒绝。设备主动上报应具备合理的重试或后续状态补报机制。