1. RPC 模型
ThinkLink(以下简称 TKL)的 RPC 模型 提供了对 LoRaWAN 设备进行远程控制和参数配置的能力。通过定义标准化的远程过程调用(Remote Procedure Call),用户可以向设备下发指令、设置工作参数或触发特定动作,实现设备的智能化运维管理。
1.1. 新建 RPC
在 TKL 平台中,可通过以下步骤创建一个新的 RPC 命令:
- 进入模型管理 → RPC 模型 → 新增。
- 配置基本信息与脚本逻辑。

1.2. 参数信息
| 字段 | 说明 |
|---|---|
| 字段标识 | 参数在脚本中的变量名,即 params 对象中的键名。例如:period 表示上报周期值,该名称将用于脚本中读取用户输入。 |
| Method | 通过 MQTT 或其他方式调用时使用的函数名称。 |
| 别名 | 在用户界面中显示的提示名称,提升可读性。例如:"修改周期",方便用户理解参数含义。 |
| Inherit | 是否继承至子设备: ✅ true:设备下的子设备可使用此 RPC;❌ false:子设备不可使用。 |
✅ 支持添加多个参数字段,以满足复杂控制需求。
1.3. RPC 脚本
TKL 支持使用 JavaScript 编写自定义编码脚本,将用户输入转化为符合设备通信协议的数据格式,并通过下行链路发送至目标设备。
示例脚本:
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let sleepMs = classMode === "ClassA" ? 200 : 10000;
let isClassA = classMode === "ClassA";
function getDevicesInfo() {
let buffer = Buffer.alloc(4);
buffer[0] = 0x8F;
buffer[1] = 2;
buffer[2] = 100;
buffer[3] = 96;
return buffer.toString("base64");
}
function processSubAddr(subAddr, modelHex) {
let addrBuffer;
let laddrBuffer = Buffer.alloc(7);
let substr = subAddr.replaceAll(" ", "");
if (substr === "nc" || substr === "") { return null; }
if (modelHex.length != 10) { return null; }
let subnum = parseInt(substr, 10);
if (subnum === 0) {
for (let i = 0; i < 7; i++) { laddrBuffer[i] = 0; }
} else {
addrBuffer = Buffer.from(substr, 'hex');
if (addrBuffer.length !== 7) { return null; }
for (let i = 0; i < 7; i++) { laddrBuffer[i] = addrBuffer[6 - i]; }
}
let hexStr = laddrBuffer.toString('hex') + modelHex;
const buffer = Buffer.from(hexStr, 'hex');
return buffer;
}
function encode(params) {
let buffer = Buffer.alloc(98);
buffer[0] = 0xCF;
buffer[1] = 76;
buffer[2] = 100;
buffer[3] = 96;
let dataSize = 0;
let serverAttrs = {};
for (let i = 0; i < 6; i++) {
const subAddr = params['sub_addr' + (i + 1)];
if (!subAddr || subAddr === "nc") {
buffer.writeUint32LE(0xFFFF, i*2+4); // 10 年
continue;
}
const modelHex = params['model' + (i + 1)];
if (modelHex === "0000000000") {
buffer.writeUint32LE(0xFFFF, i*2+4);
continue;
}
let period = params['period' + (i + 1)];
let payload = processSubAddr(subAddr, modelHex);
serverAttrs['sub_' + subAddr.replaceAll(" ", "")] = {
addr: subAddr.replaceAll(" ", ""),
model: modelHex,
period: period,
};
serverAttrs['model' + (i + 1)] = modelHex;
serverAttrs['period' + (i + 1)] = period;
if (payload === null) {
buffer.writeUint32LE(0xFFFF, i*2+4);
continue;
}
period = (period) & 0x7FFF;
period |= 0x4000;
buffer.writeUint32LE(period, i*2+4);
payload.copy(buffer, 26 + i*12, 0, 12);
dataSize += 12;
}
if (dataSize === 0) { return null; }
buffer[1] = 24 + dataSize;
buffer[3] = 22 + dataSize;
let retBuffer = Buffer.alloc(26 + dataSize);
buffer.copy(retBuffer, 0, 0, 26 + dataSize);
return {
sAttrs: Object.keys(serverAttrs).length < 1 ? null : serverAttrs,
payload: retBuffer.toString("base64")
};
}
let rdata = encode(params);
if (rdata === null) { return null; }
return [
{
sleepTimeMs: 100,
type: "modifyAttrs",
dnMsg: {
server_attrs: rdata.sAttrs,
}
},
{
sleepTimeMs: 0,
dnMsg: {
"version": "3.0",
"type": "data",
"if": "loraWAN",
"moteeui": device.eui,
"token": new Date().getTime(),
"userdata": {
"confirmed": isClassA,
"fpend": false,
"port": 214,
"TxUTCtime": "",
"payload": rdata.payload,
"dnWaitms": 3000,
"type": "data",
"intervalms": 0
}
}
},
{
sleepTimeMs: sleepMs,
dnMsg: {
"version": "3.0",
"type": "data",
"if": "loraWAN",
"moteeui": device.eui,
"token": new Date().getTime() + 1,
"userdata": {
"confirmed": true,
"fpend": false,
"port": 214,
"TxUTCtime": "",
"payload": getDevicesInfo(),
"dnWaitms": 3000,
"type": "data",
"intervalms": 0
}
}
}
];1.3.1. 沙盒运行环境与限制
RPC 脚本在 vm2 沙盒中以 async 函数方式执行,与 Node.js 主进程完全隔离。
运行限制
| 限制项 | 值 | 说明 |
|---|---|---|
| 执行超时 | 1000 ms | 脚本执行超过 1 秒立即中断,本次 RPC 调用失败。 |
eval | ❌ 禁用 | 不能动态执行字符串代码。 |
| WebAssembly | ❌ 禁用 | 不能加载或执行 .wasm 模块。 |
async / await | ✅ 支持 | RPC 脚本支持异步,可 await axios(...) 等操作。 |
require / import | ❌ 不支持 | 不能引入任何外部模块。 |
process、path、fs 等 Node 全局对象 | ❌ 不可用 | 沙盒不注入这些对象,调用会抛出 ReferenceError。 |
setTimeout / setInterval | ❌ 不可用 | 沙盒中无定时器全局函数;多步骤延迟请通过返回数组中的 sleepTimeMs 字段实现。 |
console | ❌ 不可用 | 调试请使用注入的 logger 对象(支持 info、warn、error)。 |
Buffer | ✅ 可用(受限) | 注入的是受限版本 SafeBuffer,可用静态方法:alloc、from、isBuffer、byteLength、compare、concat;allocUnsafe / allocUnsafeSlow 被屏蔽;实例读写方法(readUInt8、writeUInt16LE 等)正常可用。 |
沙盒可用全局变量
| 变量 | 来源 | 说明 |
|---|---|---|
device | 平台注入,只读 | 目标设备对象(vm.freeze)。 |
params | 平台注入,只读 | 用户在界面或 API 中传入的参数对象(vm.freeze)。 |
alarms | 平台注入,只读 | 当前设备的告警状态映射(vm.freeze)。 |
logger | 平台注入 | RPC 日志对象,用法同 console(info / warn / error)。 |
axios | 平台注入,只读 | 预配置的 Axios 实例(超时 30 秒),用于调用外部 HTTP 接口(vm.freeze)。 |
org_params | 平台注入,只读 | 组织级环境变量(vm.freeze)。 |
env | 平台注入,只读 | 挂载插件声明的环境变量(高级功能)。 |
plugins | 平台注入,只读 | 挂载的插件实例(高级功能)。 |
SafePromise | 平台注入,只读 | 带超时的 Promise 基类;脚本内 class Promise extends SafePromise {} 已自动注入,无需手动使用。 |
BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper | tklHelper,只读 | 平台工具库(vm.freeze),详见 tklHelper。 |
注意:
device、params、alarms及 tklHelper 各类均以vm.freeze冻结方式注入,脚本中对其属性赋值不会生效。要修改设备属性,需在返回数组中使用type: "modifyAttrs"动作。
1.3.2. 输入参数
device
RPC 脚本中注入的 device 是目标设备对象(vm.freeze,只读)。完整属性如下:
| 属性 | 类型 | 说明 |
|---|---|---|
eui | string | 设备 EUI(16位十六进制字符串,全局唯一)。 |
name | string | 设备名称。 |
device_type | string | 设备类型:"NORMAL" / "SUB_DEVICE" / "VIRTUAL"。 |
data_from | string | 数据来源:"LoRaWAN" 或 "OTHER"。 |
parent | string | null | 父设备 EUI(子设备类型时有值),下行至子设备时需将 target 设为该值。 |
online | boolean | 设备当前是否在线。 |
active_time | string | 最近一次上行时间(ISO 8601)。 |
shared_attrs | object | 平台与设备双向同步的共享属性。 |
server_attrs | object | 仅存储在平台侧的服务端属性(设备不可读)。 |
mac_attrs | object | LoRaWAN MAC 层属性(频段、ADR 等)。 |
telemetry_data | object | 各物模型最新遥测快照,以 thingModelId 为键。如:device.telemetry_data["45616600866361349"].TP。 |
thing_model | string[] | 已挂载的物模型 ID 数组。 |
rpc | string[] | 已挂载的 RPC 模型 ID 数组。 |
tags | string[] | 设备标签数组。 |
tenant_code | string | 租户标识符。 |
heart_period | number | 心跳检测周期(秒)。 |
📌 当通过 MQTT 或 HTTP 调用 RPC 时,需显式传入设备 EUI,字段名为 _eui。
params
包含所有用户输入参数的对象,每个参数具有以下属性:
| 属性名 | 用途说明 |
|---|---|
| 变量名 | 用于在 JS 脚本中通过 params.xxx 访问该参数。 |
| 序号 | 控制参数在界面上的显示顺序。 |
| 别名 | 用户界面上展示的友好名称。 |
| 类型 | 支持类型:number、string、boolean、object、variant。其中 variant(变体)仅 RPC 入参可用(物模型 / 触发器字段编辑器不提供),详见 §1.3.2.2 变体类型。 |
| 默认值 | 若未填写,则使用默认值。 |
| 单位 | 显示时附加的物理单位(如 s、min、℃)。 |
| 可选值 | 提供下拉选择项。必须是对象数组,每项含 id/label/value(详见下方示例);留空 [] 则渲染为自由输入框。 |
| 隐藏条件 | 控制该参数在「执行 RPC」输入弹窗中是否显示,详见 §1.3.2.1 参数隐藏条件。 |
可选值(下拉)格式 —— 重要
可选值(即 RPC 模板字段的options)必须是对象数组,每个选项形如{ "id": "<雪花ID>", "label": "<显示文本>", "value": "<提交值>" }。不要写成纯字符串数组(如
["low", "mid", "high"])——平台会忽略它或导致 RPC 弹窗渲染失败, 该字段会退化成自由文本框,用户可输入非法值。json"alarm_level": { "name": "alarm_level", "type": "string", "alias": "告警级别", "order": 5, "options": [ { "id": "<雪花ID>", "label": "low", "value": "low" }, { "id": "<雪花ID>", "label": "mid", "value": "mid" }, { "id": "<雪花ID>", "label": "high", "value": "high" }, { "id": "<雪花ID>", "label": "urgent", "value": "urgent" } ], "default_from": "server_attrs", "attr_field_name": "alarm_level" }
- 每个选项的
id需全局唯一(雪花 ID),并用字符串类型(数值会超出 JS 安全整数精度而丢精度)。- 字段
类型用string(不是enum);options为空[]表示自由输入/数值字段,是合法写法。
⚠️ 执行弹窗每个数据项都是必填的 —— 每个入参都要能「免填提交」
「执行 RPC」弹窗里每个数据项操作员不填就无法提交,因此设计
template时每个字段都必须可免填提交,二选一:
- 配
default_from:①"fixed"+dftValue(固定默认值);②"shared_attrs"/"server_attrs"+attr_field_name(回填设备当前值,且 rpc_script 在回显 / 下发成功后须闭环写回同一属性键)。- 确实没有任何默认值 / 回填源的参数:保留无默认,并约定
null为「用户未输入」的保留语义——rpc_script 必须显式判params.X == null并安全处理(跳过该参数 /return null),绝不能把缺参当0/ 空值写下去(会误下发甚至清零寄存器)。variant 的判别值字段(用
"fixed"+dftValue指定默认选中项)及其每个附加字段同样适用本规则。
1.3.2.1. 参数隐藏条件
每个输入参数都可配置「隐藏条件」,用于在「执行 RPC」输入弹窗中按需显隐该字段——常用于一个开关字段控制另一组字段是否出现。隐藏条件有四种取值:
| 取值 | 行为 |
|---|---|
| 不隐藏(默认) | 字段始终显示。 |
| 始终隐藏 (true) | 字段在弹窗中永久隐藏,但其默认值仍会随指令提交。 |
| 始终显示 (false) | 等价于不隐藏,显式声明字段恒显。 |
| 比较运算符 | 选择 == / === / != / > / >= / < / <=,再选一个同一 RPC 中的其它输入字段作为参考,并填一个比较值。运行时取参考字段的当前值与比较值比较,结果为 true 时隐藏当前字段。 |
⚠️ 被隐藏字段的默认值仍会提交。无论是「始终隐藏」还是比较条件命中而隐藏,该字段都不会从用户输入中消失——平台仍按其默认值下发。因此被隐藏字段务必配置合理的默认值。
示例:参数 mode(下拉:auto / manual),参数 interval(仅在手动模式下才需要填写)。给 interval 配置隐藏条件 == mode auto,即「当 mode 等于 auto 时隐藏 interval」——切到 auto 自动收起 interval,切回 manual 再次出现。
底层以 hidden 对象存储:{ "operator": "==", "field": "mode", "value": "auto" };当 operator 为 true/false 时仅保留 operator。
当参考字段是
variant(变体)类型时,比较自动取其判别值——扁平提交形态下variant字段本身就是判别值字符串(如params.mode直接是"auto"),无需在隐藏条件里写mode.value。
1.3.2.2. 变体类型(variant)
variant(变体)是 RPC 入参专用的字段类型:一个下拉框选择「判别值」,每个选项各自携带一组「附加字段」。用户在「执行 RPC」弹窗中选中某个选项后,该选项的附加字段会在下拉框正下方展开供填写;切换到别的选项时,旧选项的附加字段值会被清空。常用于「先选指令类型,再按类型显示对应参数」的场景。
配置方式(在 RPC 模型编辑弹窗中):
- 字段
类型选variant。 - 在「可选值」表格中逐行新增选项,填写
名称(显示文本)与值(判别值)。 - 每行末尾的「附加字段 [n]」按钮打开「管理附加字段」弹窗,为该选项配置任意数量的子字段(子字段与普通入参一样有变量名、类型、默认值、单位、可选值等)。
提交形态(扁平一层 —— 关键):variant 字段在 params 中不是嵌套对象,而是扁平展开——params.<variant 字段名> 本身就是判别值字符串,每个附加字段是与之同级的参数 params.<附加字段名>:
"msg": {
"cmd": "set_interval",
"interval": 30,
"unit": "s"
}其中 cmd(variant 字段)的值是所选选项的判别值字符串,interval / unit 是该选项附加字段的同级参数。
⚠️ 脚本读法:
const which = params.cmd;(字符串),附加字段读同级params.interval/params.unit。绝不能把params.cmd当对象读.value/.<子字段>——扁平形态下那样取到的是undefined,常导致脚本静默return null。⚠️ 源码镜像可能与线上不一致:
source/tkl-web镜像(rpc-input-modal.tsx/dynamic-filed-input-item.tsx)的表单 name 路径看似会提交嵌套对象{ value, ...附加字段 },且全链路无 flatten 步骤;但线上部署平台实测为扁平字符串形态,说明源码镜像滞后于部署版本。一律以线上实测的扁平形态为准。
约束:
- 附加字段名不能与 variant 字段本身同名(扁平形态下二者同级,同名会互相覆盖)。
- 附加字段不能再嵌套
variant,也不支持隐藏条件(保持结构简单)。 - 附加字段支持属性回填:
default_from可填"shared_attrs"/"server_attrs"解析设备实时值(与普通入参一致),也可填"fixed"+dftValue用固定默认值。回填键走attr_field_name(不是字段name),缺attr_field_name则不回填。 - variant 必须配置「可选值」(用作判别值),否则无法渲染。
- 隐藏的 variant 字段在提交时同样按扁平形态下发(判别值 + 各附加字段同级);若用户未触碰则以默认判别值(
"fixed"+dftValue指定的默认选中项)及各附加字段默认值兜底。
最佳实践(设定点合并范式):把多个同类设定(如「设定湿度 / 设定温度」)合并为一条 variant 时,附加字段名直接取判别值本身(
humidity选项 → 附加字段名humidity),脚本统一用params[params.<variant 字段名>]读取;attr_field_name指向真实属性键(如humidity_set),与字段名解耦。下发成功后由 rpc_script 闭环回写同一属性键,弹窗即可据当前shared_attrs回填。
alarms
保存了对应设备的告警信息。通过 alarms[alarm_name] 可获取指定告警事件是否存在。RPC 代码可根据对应告警状态进行逻辑处理。
logger
RPC 日志工具。logger 的使用方式与 console 一致,用户需要记录的信息须以 Object 形式作为 params 变量传入。logger 支持 info、warn、error 三种等级,方便按等级过滤和查找日志。
示例:
logger.info("set my paras", { params: paras })axios
预配置的 Axios 实例(超时 30 秒)。详细用法见 §1.3.4 调用外部 HTTP 接口。
org_params
当前组织(租户)级别的"环境变量",由系统管理 → 服务器配置 → 组织参数维护,平台在执行 RPC 时自动注入。
适用于所有 RPC 共享、不希望暴露给调用方的常量——例如第三方机器人 Webhook、外部 API 的 access key、跨环境切换的 base URL 等。换密钥时只改服务器配置一处,所有 RPC 自动生效。
完整的字段约定和安全提示见服务器配置 §1.6 组织参数。
函数签名:
function rpc_script({ device, params, alarms, logger, org_params }) {
let webhook = org_params?.wecom_webhook_url;
// ... 用 webhook 发起请求
}⚠️ 不要把
org_params的值写进device.server_attrs或 logger 正文里——那等于把隐藏的密钥又暴露给调用方或日志读者。
1.3.4. 调用外部 HTTP 接口
RPC 脚本提供两种方式调用外部 HTTP/HTTPS 接口,适用于不同场景:
方式一:脚本内联 await axios(...)(需要读取响应)
适用场景:需要先获取外部接口的返回值,再决定下行逻辑。例如:调用第三方鉴权服务、查询业务系统状态、动态计算下行参数。
let resp = await axios({
method: "GET",
url: "https://api.example.com/check",
params: { eui: device.eui },
headers: { Authorization: "Bearer xxxxx" }
});
if (!resp.data?.allowed) {
return null; // 外部系统拒绝时,不下发任何指令
}
return [ /* ... 正常下行动作 ... */ ];请求参数与 Axios 官方 API 一致,单次请求超时上限 30 秒。
方式二:返回 type: "axios" 动作(仅发出请求,不等响应)
适用场景:不关心响应内容,只需把请求发出去。例如:Webhook 通知、向外部平台转发遥测数据。该方式由平台在脚本执行结束后异步发起,不占用脚本执行时间,更适合耗时较长的外部调用。
return [
{
sleepTimeMs: 0,
type: "axios",
dnMsg: {
method: "POST",
url: "https://api.example.com/notify",
headers: {
"Authorization": "Bearer xxxxx",
"Content-Type": "application/json"
},
data: {
eui: device.eui,
name: device.name,
telemetry: device.telemetry_data,
time: new Date().toISOString()
},
timeout: 5000
}
}
];dnMsg 字段按 Axios 请求配置 格式编写;timeout 最大 30000 ms。请求失败(非 2xx、超时、网络错误)会自动写入 RPC 日志。
| 维度 | 内联 await axios(...) | 返回 type: "axios" |
|---|---|---|
| 能读取响应 | ✅ | ❌ |
| 占用脚本执行时间 | ✅(计入 1 s 超时) | ❌(脚本结束后异步执行) |
| 适合耗时请求 | ❌ | ✅ |
| 适合按响应决策 | ✅ | ❌ |
1.3.3. 返回参数
一条 RPC 可支持连续执行多条指令。每条指令遵循如下结构之一:
类型一:发送设备指令(LoRaWAN 或非 LoRaWAN 设备)
适用于通过 Topic 按照标准协议格式下发给设备的消息,Topic 参考:
[CN] PTL-S05 ASP LoRaWAN NS 与应用服务器通信协议 V3.2
LoRaWAN 设备的消息 JSON 格式示例如下:
{
"version": "3.0",
"type": "data",
"if": "loraWAN",
"moteeui": "ABCDEF1234567890",
"token": 1712345678901,
"userdata": {
"confirmed": true,
"fpend": false,
"port": 214,
"TxUTCtime": "",
"payload": "base64_encoded_data",
"dnWaitms": 3000,
"type": "data",
"intervalms": 0
}
}🔗 非 LoRaWAN 设备应按照此消息格式侦听对应 Topic。
类型二:修改设备属性
通过设置 type: "modifyAttrs" 实现对设备属性的更新操作,可修改 server_attrs 和 shared_attrs。以下是修改 server_attrs 的示例:
{
"sleepTimeMs": 100,
"type": "modifyAttrs",
"dnMsg": {
"server_attrs": {
"covtemp": 15
}
}
}该操作会将指定属性写入平台数据库,无需发送到终端设备。
⚠️ 此操作不会向设备发送任何消息,仅更新平台内部状态。
类型三:告警
通过设置 type: "alarm" 实现告警功能。一个设备或资产要启用告警功能,需要配置触发联动逻辑,在触发模型中定义触发条件,通过告警 RPC 实现告警通知。
字段说明:
| 字段 | 说明 |
|---|---|
action | 告警动作类型:"new" 新增一个告警事件;"clear" 清除该告警事件。 |
alarm_name | 对应的告警事件名称,一个名称对应一种告警类型,不同告警事件名称不能重复。 |
notice_groups | 通知组。选中后,告警事件发生时将通知到对应的通知组。 |
title | 告警事件发生时的标题。 |
desc | 告警事件的描述。 |
level | 告警等级,分为 "low"、"mid"、"high"、"urgent" 四种。 |
示例:
{
sleepTimeMs: 0,
target: device.eui,
type: "alarm",
dnMsg: {
action: "new",
data: {
alarm_name: "alarm test",
notice_groups: [],
title: title,
desc: "this is a alarm",
level: "high",
}
}
}类型四:调用外部 HTTP API
通过设置 type: "axios" 让 RPC 在执行过程中调用一个外部 HTTP/HTTPS 接口,可用于对接业务系统、第三方平台、Webhook 等场景。dnMsg 字段按 Axios 请求配置 的格式编写。
字段说明:
| 字段 | 说明 |
|---|---|
method | HTTP 方法:"GET" / "POST" / "PUT" / "DELETE" / "PATCH" 等,默认 "GET"。 |
url | 完整请求 URL,须包含协议(http:// 或 https://)。 |
headers | 自定义请求头对象,例如 { "Authorization": "Bearer xxx", "Content-Type": "application/json" }。 |
params | URL 查询参数对象,会自动序列化拼接到 URL 上。 |
data | 请求体内容(用于 POST/PUT/PATCH),可以是 JSON 对象、字符串或 Buffer。 |
timeout | 请求超时时间(毫秒),最长 30000(30 秒),超过会自动截断。 |
示例——当设备上报温度告警时,将设备信息推送给外部业务平台:
return [
{
sleepTimeMs: 0,
type: "axios",
dnMsg: {
method: "POST",
url: "https://api.example.com/notify",
headers: {
"Authorization": "Bearer xxxxx",
"Content-Type": "application/json"
},
data: {
eui: device.eui,
name: device.name,
telemetry: device.telemetry_data,
time: new Date().toISOString()
},
timeout: 5000
}
}
];📌 与下行指令不同,本类型动作不需要填写
target字段(请求对象是外部 HTTP 服务,与设备 EUI 无关)。请求失败(非 2xx、超时、网络错误等)会自动写入 RPC 日志,方便排查。
类型五:仅发送通知(notify)
通过设置 type: "notify" 让 RPC 仅向选定的通知组发送一条消息,不写入告警库、不更新告警状态、不参与去重。适用于"只想推一条消息"的场景,例如日报推送、运维��件提醒、外部联动结果回执等——既不需要在 ThinkLink 告警中心留痕,也不需要配对的 clear 动作。
与 type: "alarm" 的区别:
| 维度 | alarm | notify |
|---|---|---|
| 是否写入告警库 | ✅ 写入 alarm_latest / alarm_history,可在告警中心查看 | ❌ 不写入,不在告警中心留痕 |
是否需要 action/alarm_name/level | ✅ 需要(用于去重和状态机) | ❌ 不需要 |
| 是否做"相同告警去重" | ✅ 相同 name+title+desc+level 视为重复,new 会被抑制 | ❌ 每次都发 |
是否分发到 notice_groups | ✅ 分发 | ✅ 分发(与 alarm 共用同一套通知组逻辑) |
字段说明:
| 字段 | 说明 |
|---|---|
title | 消息标题;邮件渠道作为邮件主题,企微机器人渠道作为 Markdown 正文首行。 |
desc | 消息正文;格式取决于 notice_groups 中接收方的渠道类型(见下)。 |
notice_groups | 通知组名称数组,按平台系统管理 → 通知组配置的渠道分发。 |
desc 的格式由通知组的渠道类型决定:
| 通知组渠道 | desc 渲染方式 | 应书写的格式 |
|---|---|---|
邮件(email) | 作为邮件 HTML 正文发送 | HTML 片段(<div>、<font>、<table> 等) |
企微机器人(WeComWebHook) | 拼成 title\ndesc 后以 msgtype: "markdown" 推送 | Markdown(# 标题、<font color="red"> 内联色等企微支持的子集) |
如果一个 notify 动作的 notice_groups 同时包含不同渠道(如邮件 + 企微),同一份 desc 会被各渠道按自己的方式渲染——意味着你需要选择一种格式并接受另一渠道展示效果不理想。需要每个渠道都呈现得当,就按渠道拆成多条 type: "notify",各写各的格式。
示例(演示两种 desc 写法):
return [
{
sleepTimeMs: 0,
target: device.eui,
type: "notify",
dnMsg: {
data: {
title: "# 测试",
desc: "<font color=\"red\">测试内容</font>", // 企微 markdown
notice_groups: ["WeCom_test"]
}
}
},
{
sleepTimeMs: 0,
target: device.eui,
type: "notify",
dnMsg: {
data: {
title: "测试",
desc: "<div style=\"color:red\">测试内容</div>", // 邮件 HTML
notice_groups: ["email_test"]
}
}
}
];💡 想要"既留痕又推送"——用
type: "alarm";想要"只推送不留痕"——用type: "notify"。两者共用notice_groups分发,企微/邮件的渲染规则完全一致。
公共字段说明
| 字段 | 说明 |
|---|---|
| sleepTimeMs | 发送前等待时间(毫秒),用于控制多条指令间的延迟。 |
| target | 默认为目标设备的 eui;当操作子设备时,因指令需通过父设备转发,此处应设为 device.parent。 |
| type | 指令类型:default(普通下行消息)、modifyAttrs(修改服务端/共享属性)、alarm(触发或清除告警)、axios(调用外部 HTTP API)、notify(仅向通知组发送消息,不写入告警库)。 |
✅ 最佳实践:在生产环境部署前,务必在开发环境中测试 RPC 脚本。使用内置调试器验证输出 payload,确保编码正确。
1.4. 挂载 RPC
创建完成的 RPC 需要绑定到具体设备才能使用。
- 操作路径:
运维管理 → 设备管理 → 选择目标设备 → 详情 → RPC - 操作步骤:
- 在设备详情页点击 RPC 标签。
- 点击新增,从下拉列表中选择已创建的 RPC。
- 可重复添加多个不同的 RPC 到同一设备。
✅ 支持一个设备挂载多个 RPC,适用于多功能控制场景。
1.5. 执行 RPC
当 RPC 成功挂载后,即可对设备执行远程调用。
- 操作路径:同上,进入设备详情 → RPC 管理界面。
- 操作步骤:
- 找到已挂载的 RPC 条目。
- 点击对应操作列的执行按钮。
- 弹出输入窗口,填写各参数值(根据"别名"提示输入)。
- 确认后,系统将调用脚本生成指令并发送至设备。
📌 执行结果可在日志或设备响应中查看,依赖于设备回传机制与确认模式设置(Confirmed/Unconfirmed)。
通过灵活配置 RPC 模型,TKL 实现了对 LoRaWAN 设备的精细化远程控制能力,为设备调试、配置更新与故障处置提供了高效手段。
1.5.1. 绑定设备执行(定时调度)
手动「执行」是即时的一次性下发。若希望 RPC 按周期自动下发,可将其绑定到一个设备执行任务——设备执行负责「对哪些设备、下发哪个 RPC」,而它绑定的定时任务(cron 触发器)负责「何时下发」。
- 操作路径:
运维管理 → 设备管理 → 目标设备 → 详情 → RPC,在「绑定的设备执行」列点击 「+ 添加到设备执行」。 - 新建设备执行:自动带入当前 RPC 与设备 EUI,在抽屉中选择定时任务、执行类型(无限次/固定次数)、设备间发送间隔、是否存储日志等,保存即可。
- 添加到已有:把当前设备追加进一个已存在的设备执行任务。
「绑定的设备执行」列以标签形式列出当前 RPC 已关联的设备执行任务,点击标签上的下拉菜单可:
- 详情:打开该设备执行的详情抽屉查看/编辑。
- 删除:把当前设备从该设备执行的目标设备列表中移除(只移除本设备,不删除整个设备执行任务)。
孤儿 RPC 提示:若某条设备执行引用了一个未挂载到当前设备的 RPC,该 RPC 会在列表中以红色显示并附「该 RPC 未绑定到当前设备」提示,其「执行」按钮不可用。请先在 RPC 列表中将该 RPC 挂载到设备,或从对应设备执行中移除本设备。
绑定定时任务后,平台会在每个 cron 周期自动对目标设备下发该 RPC,无需人工触发;也可在「设备执行」列表页「立即执行」或「停止」。
1.6. 告警 RPC
ThinkLink 已内置通用告警 RPC 功能 ALARM,使用时需要将 ALARM RPC 挂载到对应的设备或资产上。配置触发联动逻辑后,即可实现告警功能。默认的 ALARM 实现如下:
function rpc_script({device, params, alarms, logger}) {
const ACTION = { no: "no", "new": 'new', clear: 'clear' };
let notify = params?.notify ?? [];
let alarm_name = params?.name ?? "[alarm]";
let action = params?.action ?? ACTION.no;
let title = params?.title ?? "[tile]";
let desc = params?.desc ?? "this is a description of alarm";
let level = params?.level ?? "low";
switch (action) {
case ACTION.clear:
if (!alarms[alarm_name]) action = ACTION.no;
break;
case ACTION.no:
return null;
case ACTION.new:
let alarmInfo = alarms[alarm_name];
if (alarmInfo == undefined) { break; }
if (alarmInfo.title !== title || alarmInfo.desc !== desc || alarmInfo.level !== level) {
break;
}
action = ACTION.no;
break;
default: break;
}
if (action == ACTION.no) return;
return [
{
sleepTimeMs: 0,
target: device.eui,
type: "alarm",
dnMsg: {
action: action,
data: {
alarm_name: alarm_name,
notice_groups: notify,
title: title,
desc: desc,
level: level,
}
}
}
];
}📌 升级要点:参数
group(旧版形如{ notify: [...] })已改为notify(直接的字符串数组[...])。触发模型及调用方需按新形态传参。
1.7. RPCHelper
RPCHelper 是 tklHelper 提供的工具类,封装了 RPC 开发中的常见操作:发送告警事件、构建参数读写帧、组装 LoRaWAN 下行消息以及生成 Modbus 帧。在 RPC 脚本中可直接调用。
1.7.1. 告警 — RPCHelper.makeAlarm()
为 RPC 脚本构造标准告警返回数组,内置去重逻辑:若相同告警(name、title、desc、level 均一致)已存在,则 "new" 动作被抑制,返回 null。
RPCHelper.makeAlarm({
alarms, // rpc_script 输入的 alarms 对象
eui, // 目标设备 EUI
name, // 唯一告警事件名称
action, // "new" | "clear"
group, // 通知组数组
title, // 告警标题
desc, // 告警描述
level // "low" | "mid" | "high" | "urgent"
})示例:
function rpc_script({ device, params, alarms, logger }) {
return RPCHelper.makeAlarm({
alarms: alarms,
eui: device.eui,
name: params.name,
action: params.action,
group: params.notify ?? [],
title: params.title,
desc: params.desc,
level: params.level
});
}1.7.2. LoRaWAN 参数帧 — paraWrite / paraRead
为 ThinkLink 参数协议构造单寄存器读写帧。
// 写入单个寄存器
let buf = RPCHelper.paraWrite(name, type, addr, value);
// name: 寄存器命名空间 — "app" | "cf" | "fw" | "radio" | "ds" | "status"
// type: 数据类型字符串(如 "uint16le")
// addr: 寄存器地址(十进制或 0x 前缀的十六进制)
// value: 要写入的值
// 读取单个寄存器
let buf = RPCHelper.paraRead(name, type, addr);1.7.3. 批量参数帧 — RPCHelper.buildFrame()
为多个命名空间的多个寄存器构造合并的写入+读取帧,按地址自动排序,自动处理分段和帧头。
let result = RPCHelper.buildFrame({
serverParaDef, // 数组,每项含 { field_name },指定需保存到 server_attrs 的字段
paraDef, // 对象,键为 "命名空间_地址",值为 { field_name, type, ... }
params // rpc_script 的 params 对象
});
// 返回: { writeBuffer, readBuffer, serverPara, log }示例:
function rpc_script({ device, params, alarms, logger }) {
let paraDef = {
app_40: { name: "上报周期", field_name: "upPeriod", type: "uint32le" },
app_142: { name: "采集周期", field_name: "measPeriod", type: "uint16le" },
};
let result = RPCHelper.buildFrame({ paraDef: paraDef, params: params });
if (!result || result.writeBuffer.length === 0) { return null; }
return [
{
sleepTimeMs: 100,
type: "modifyAttrs",
dnMsg: { server_attrs: result.serverPara }
},
RPCHelper.makeMSG({
msgType: "paras",
device: device,
dnBuffer: result.writeBuffer,
confirmed: true,
dnWaitms: 3000
})
];
}1.7.4. 下行消息封装 — RPCHelper.makeMSG()
将原始 Buffer 封装为符合 ThinkLink 协议的下行 JSON 对象。
| 参数 | 说明 |
|---|---|
msgType | "user"(自定义端口)、"paras"(端口 214)、"transParent"(端口 51)、"swDown"(LoRa SW) |
device | 设备对象 |
dnBuffer | 含有 payload 数据的 Buffer |
port | 端口号(仅 "user" 类型时有效) |
confirmed | 是否使用确认下行 |
sleepTime | 返回项中的 sleepTimeMs 值 |
dnWaitms | 下行窗口等待时间(毫秒) |
RPCHelper.makeMSG({
msgType: "paras",
device: device,
dnBuffer: writeBuffer,
confirmed: true,
dnWaitms: 3000,
sleepTime: 0
})1.7.5. 设备控制指令
常用 LoRaWAN 设备操作的快捷 Buffer:
RPCHelper.reset() // 复位设备
RPCHelper.redo() // 重新执行上一条指令
RPCHelper.join() // 强制重新入网1.7.6. Modbus 工具方法
// 构造 Modbus FC03/FC06 读帧
RPCHelper.buildmodbusFrameRead(addr, code, regStart, count)
// 构造 Modbus FC06 单寄存器写帧
RPCHelper.buildmodbusFrame06({ serverParaDef, paraDef, params })
// 构造 Modbus FC10 多寄存器写帧
RPCHelper.buildmodbusFrame10({ serverParaDef, paraDef, params })
// 构造通用 Modbus 动作帧
RPCHelper.modbusAction(addr, code, regAddr, regVal)1.8. 历史版本与还原
每次保存 RPC,平台都会记录一个版本快照。在 RPC 编辑器中点击历史版本即可浏览早期版本。每条记录提供:
- 详情 —— 查看该快照内容(并可与相邻版本对比)。
- 还原 —— 将当前值回滚到该快照。还原会生成一个新的版本记录(因此历史不会丢失),并将列表刷新到第一页;确认弹窗会提示「还原会产生一个新版本」。
RPC 模型、物模型、触发模型与转发器脚本编辑器均提供相同的「历史版本 / 还原」操作。