Skip to content

1. RPC 模型

ThinkLink(以下简称 TKL)的 RPC 模型 提供了对 LoRaWAN 设备进行远程控制和参数配置的能力。通过定义标准化的远程过程调用(Remote Procedure Call),用户可以向设备下发指令、设置工作参数或触发特定动作,实现设备的智能化运维管理。

1.1. 新建 RPC

在 TKL 平台中,可通过以下步骤创建一个新的 RPC 命令:

  1. 进入模型管理 → RPC 模型 → 新增
  2. 配置基本信息与脚本逻辑。

1.2. 参数信息

字段说明
字段标识参数在脚本中的变量名,即 params 对象中的键名。例如:period 表示上报周期值,该名称将用于脚本中读取用户输入。
Method通过 MQTT 或其他方式调用时使用的函数名称。
别名在用户界面中显示的提示名称,提升可读性。例如:"修改周期",方便用户理解参数含义。
Inherit是否继承至子设备:
true:设备下的子设备可使用此 RPC;
false:子设备不可使用。

✅ 支持添加多个参数字段,以满足复杂控制需求。

1.3. RPC 脚本

TKL 支持使用 JavaScript 编写自定义编码脚本,将用户输入转化为符合设备通信协议的数据格式,并通过下行链路发送至目标设备。

示例脚本:

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❌ 不支持不能引入任何外部模块。
processpathfs 等 Node 全局对象❌ 不可用沙盒不注入这些对象,调用会抛出 ReferenceError
setTimeout / setInterval❌ 不可用沙盒中无定时器全局函数;多步骤延迟请通过返回数组中的 sleepTimeMs 字段实现。
console❌ 不可用调试请使用注入的 logger 对象(支持 infowarnerror)。
Buffer✅ 可用(受限)注入的是受限版本 SafeBuffer,可用静态方法:allocfromisBufferbyteLengthcompareconcatallocUnsafe / allocUnsafeSlow 被屏蔽;实例读写方法(readUInt8writeUInt16LE 等)正常可用。

沙盒可用全局变量

变量来源说明
device平台注入,只读目标设备对象(vm.freeze)。
params平台注入,只读用户在界面或 API 中传入的参数对象(vm.freeze)。
alarms平台注入,只读当前设备的告警状态映射(vm.freeze)。
logger平台注入RPC 日志对象,用法同 consoleinfo / 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 / RPCHelpertklHelper,只读平台工具库(vm.freeze),详见 tklHelper

注意deviceparamsalarms 及 tklHelper 各类均以 vm.freeze 冻结方式注入,脚本中对其属性赋值不会生效。要修改设备属性,需在返回数组中使用 type: "modifyAttrs" 动作。

1.3.2. 输入参数

  1. device

RPC 脚本中注入的 device目标设备对象vm.freeze,只读)。完整属性如下:

属性类型说明
euistring设备 EUI(16位十六进制字符串,全局唯一)。
namestring设备名称。
device_typestring设备类型:"NORMAL" / "SUB_DEVICE" / "VIRTUAL"
data_fromstring数据来源:"LoRaWAN""OTHER"
parentstring | null父设备 EUI(子设备类型时有值),下行至子设备时需将 target 设为该值。
onlineboolean设备当前是否在线。
active_timestring最近一次上行时间(ISO 8601)。
shared_attrsobject平台与设备双向同步的共享属性。
server_attrsobject仅存储在平台侧的服务端属性(设备不可读)。
mac_attrsobjectLoRaWAN MAC 层属性(频段、ADR 等)。
telemetry_dataobject各物模型最新遥测快照,以 thingModelId 为键。如:device.telemetry_data["45616600866361349"].TP
thing_modelstring[]已挂载的物模型 ID 数组。
rpcstring[]已挂载的 RPC 模型 ID 数组。
tagsstring[]设备标签数组。
tenant_codestring租户标识符。
heart_periodnumber心跳检测周期(秒)。

📌 当通过 MQTT 或 HTTP 调用 RPC 时,需显式传入设备 EUI,字段名为 _eui

  1. params

包含所有用户输入参数的对象,每个参数具有以下属性:

属性名用途说明
变量名用于在 JS 脚本中通过 params.xxx 访问该参数。
序号控制参数在界面上的显示顺序。
别名用户界面上展示的友好名称。
类型支持类型:numberstringbooleanobjectvariant。其中 variant(变体)仅 RPC 入参可用(物模型 / 触发器字段编辑器不提供),详见 §1.3.2.2 变体类型
默认值若未填写,则使用默认值。
单位显示时附加的物理单位(如 smin)。
可选值提供下拉选择项。必须是对象数组,每项含 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每个字段都必须可免填提交,二选一:

  1. default_from:① "fixed" + dftValue(固定默认值);② "shared_attrs" / "server_attrs" + attr_field_name(回填设备当前值,且 rpc_script 在回显 / 下发成功后须闭环写回同一属性键)。
  2. 确实没有任何默认值 / 回填源的参数:保留无默认,并约定 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" };当 operatortrue/false 时仅保留 operator

当参考字段是 variant(变体)类型时,比较自动取其判别值——扁平提交形态下 variant 字段本身就是判别值字符串(如 params.mode 直接是 "auto"),无需在隐藏条件里写 mode.value

1.3.2.2. 变体类型(variant)

variant(变体)是 RPC 入参专用的字段类型:一个下拉框选择「判别值」,每个选项各自携带一组「附加字段」。用户在「执行 RPC」弹窗中选中某个选项后,该选项的附加字段会在下拉框正下方展开供填写;切换到别的选项时,旧选项的附加字段值会被清空。常用于「先选指令类型,再按类型显示对应参数」的场景。

配置方式(在 RPC 模型编辑弹窗中):

  1. 字段 类型variant
  2. 在「可选值」表格中逐行新增选项,填写 名称(显示文本)与 (判别值)。
  3. 每行末尾的「附加字段 [n]」按钮打开「管理附加字段」弹窗,为该选项配置任意数量的子字段(子字段与普通入参一样有变量名、类型、默认值、单位、可选值等)。

提交形态(扁平一层 —— 关键):variant 字段在 params不是嵌套对象,而是扁平展开——params.<variant 字段名> 本身就是判别值字符串,每个附加字段是与之同级的参数 params.<附加字段名>

json
"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 回填。

  1. alarms

保存了对应设备的告警信息。通过 alarms[alarm_name] 可获取指定告警事件是否存在。RPC 代码可根据对应告警状态进行逻辑处理。

  1. logger

RPC 日志工具。logger 的使用方式与 console 一致,用户需要记录的信息须以 Object 形式作为 params 变量传入。logger 支持 infowarnerror 三种等级,方便按等级过滤和查找日志。

示例:

javascript
logger.info("set my paras", { params: paras })
  1. axios

预配置的 Axios 实例(超时 30 秒)。详细用法见 §1.3.4 调用外部 HTTP 接口

  1. org_params

当前组织(租户)级别的"环境变量",由系统管理 → 服务器配置 → 组织参数维护,平台在执行 RPC 时自动注入。

适用于所有 RPC 共享、不希望暴露给调用方的常量——例如第三方机器人 Webhook、外部 API 的 access key、跨环境切换的 base URL 等。换密钥时只改服务器配置一处,所有 RPC 自动生效。

完整的字段约定和安全提示见服务器配置 §1.6 组织参数

函数签名:

javascript
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(...)(需要读取响应)

适用场景:需要先获取外部接口的返回值,再决定下行逻辑。例如:调用第三方鉴权服务、查询业务系统状态、动态计算下行参数。

javascript
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 通知、向外部平台转发遥测数据。该方式由平台在脚本执行结束后异步发起,不占用脚本执行时间,更适合耗时较长的外部调用。

javascript
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 格式示例如下:

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_attrsshared_attrs。以下是修改 server_attrs 的示例:

json
{
    "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" 四种。

示例:

javascript
{
    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 请求配置 的格式编写。

字段说明:

字段说明
methodHTTP 方法:"GET" / "POST" / "PUT" / "DELETE" / "PATCH" 等,默认 "GET"
url完整请求 URL,须包含协议(http://https://)。
headers自定义请求头对象,例如 { "Authorization": "Bearer xxx", "Content-Type": "application/json" }
paramsURL 查询参数对象,会自动序列化拼接到 URL 上。
data请求体内容(用于 POST/PUT/PATCH),可以是 JSON 对象、字符串或 Buffer。
timeout请求超时时间(毫秒),最长 30000(30 秒),超过会自动截断。

示例——当设备上报温度告警时,将设备信息推送给外部业务平台:

javascript
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" 的区别:

维度alarmnotify
是否写入告警库✅ 写入 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 写法):

javascript
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
  • 操作步骤
    1. 在设备详情页点击 RPC 标签。
    2. 点击新增,从下拉列表中选择已创建的 RPC。
    3. 可重复添加多个不同的 RPC 到同一设备。

✅ 支持一个设备挂载多个 RPC,适用于多功能控制场景。

1.5. 执行 RPC

当 RPC 成功挂载后,即可对设备执行远程调用。

  • 操作路径:同上,进入设备详情 → RPC 管理界面。
  • 操作步骤
    1. 找到已挂载的 RPC 条目。
    2. 点击对应操作列的执行按钮。
    3. 弹出输入窗口,填写各参数值(根据"别名"提示输入)。
    4. 确认后,系统将调用脚本生成指令并发送至设备。

📌 执行结果可在日志或设备响应中查看,依赖于设备回传机制与确认模式设置(Confirmed/Unconfirmed)。

通过灵活配置 RPC 模型,TKL 实现了对 LoRaWAN 设备的精细化远程控制能力,为设备调试、配置更新与故障处置提供了高效手段。

1.5.1. 绑定设备执行(定时调度)

手动「执行」是即时的一次性下发。若希望 RPC 按周期自动下发,可将其绑定到一个设备执行任务——设备执行负责「对哪些设备、下发哪个 RPC」,而它绑定的定时任务(cron 触发器)负责「何时下发」。

  • 操作路径运维管理 → 设备管理 → 目标设备 → 详情 → RPC,在「绑定的设备执行」列点击 「+ 添加到设备执行」
  • 新建设备执行:自动带入当前 RPC 与设备 EUI,在抽屉中选择定时任务、执行类型(无限次/固定次数)、设备间发送间隔、是否存储日志等,保存即可。
  • 添加到已有:把当前设备追加进一个已存在的设备执行任务。

「绑定的设备执行」列以标签形式列出当前 RPC 已关联的设备执行任务,点击标签上的下拉菜单可:

  • 详情:打开该设备执行的详情抽屉查看/编辑。
  • 删除:把当前设备从该设备执行的目标设备列表中移除(只移除本设备,不删除整个设备执行任务)。

孤儿 RPC 提示:若某条设备执行引用了一个未挂载到当前设备的 RPC,该 RPC 会在列表中以红色显示并附「该 RPC 未绑定到当前设备」提示,其「执行」按钮不可用。请先在 RPC 列表中将该 RPC 挂载到设备,或从对应设备执行中移除本设备。

绑定定时任务后,平台会在每个 cron 周期自动对目标设备下发该 RPC,无需人工触发;也可在「设备执行」列表页「立即执行」或「停止」。

设备执行与定时任务的完整字段、cron 表达式与执行日志详见 → 设备执行 · 定时任务

1.6. 告警 RPC

ThinkLink 已内置通用告警 RPC 功能 ALARM,使用时需要将 ALARM RPC 挂载到对应的设备或资产上。配置触发联动逻辑后,即可实现告警功能。默认的 ALARM 实现如下:

javascript
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

RPCHelpertklHelper 提供的工具类,封装了 RPC 开发中的常见操作:发送告警事件、构建参数读写帧、组装 LoRaWAN 下行消息以及生成 Modbus 帧。在 RPC 脚本中可直接调用。

1.7.1. 告警 — RPCHelper.makeAlarm()

为 RPC 脚本构造标准告警返回数组,内置去重逻辑:若相同告警(name、title、desc、level 均一致)已存在,则 "new" 动作被抑制,返回 null

javascript
RPCHelper.makeAlarm({
    alarms,          // rpc_script 输入的 alarms 对象
    eui,             // 目标设备 EUI
    name,            // 唯一告警事件名称
    action,          // "new" | "clear"
    group,           // 通知组数组
    title,           // 告警标题
    desc,            // 告警描述
    level            // "low" | "mid" | "high" | "urgent"
})

示例:

javascript
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 参数协议构造单寄存器读写帧。

javascript
// 写入单个寄存器
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()

为多个命名空间的多个寄存器构造合并的写入+读取帧,按地址自动排序,自动处理分段和帧头。

javascript
let result = RPCHelper.buildFrame({
    serverParaDef,  // 数组,每项含 { field_name },指定需保存到 server_attrs 的字段
    paraDef,        // 对象,键为 "命名空间_地址",值为 { field_name, type, ... }
    params          // rpc_script 的 params 对象
});
// 返回: { writeBuffer, readBuffer, serverPara, log }

示例:

javascript
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下行窗口等待时间(毫秒)
javascript
RPCHelper.makeMSG({
    msgType:   "paras",
    device:    device,
    dnBuffer:  writeBuffer,
    confirmed: true,
    dnWaitms:  3000,
    sleepTime: 0
})

1.7.5. 设备控制指令

常用 LoRaWAN 设备操作的快捷 Buffer:

javascript
RPCHelper.reset()  // 复位设备
RPCHelper.redo()   // 重新执行上一条指令
RPCHelper.join()   // 强制重新入网

1.7.6. Modbus 工具方法

javascript
// 构造 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 模型、物模型、触发模型与转发器脚本编辑器均提供相同的「历史版本 / 还原」操作。