Skip to content

tklHelper

tklHelper 是 ThinkLink 平台为简化物模型(Thing Model)和 RPC 配置而提供的工具集。通过 JSON 风格的声明式配置,配合 PayloadParser / MSparser / RPCHelper / Utils / TriggerHelper 等类,可以快速实现上行数据解析、参数下发和告警触发,无需手写底层 Buffer 逻辑。

本说明书内容对照源码 source/tklHelper.js(version 1.01.036)。在物模型脚本、RPC 脚本和触发模型脚本中,tklHelper 暴露的所有类(BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper)均作为 sandbox 全局变量直接使用,无需 import。


1. frameInfo — 数据帧合法性校验

frameInfo 用于在解析前对上行帧做合法性校验,不符合规则的帧直接返回 null

字段类型默认值描述
portNumber-1LoRaWAN 上行端口号。正数时端口不匹配的帧会被丢弃;-1 表示不校验。
dataLenNumber-1预期帧长(字节)。正数时长度不一致的帧会被丢弃;-1 表示不校验。
statusNumber-1帧内设备状态字节偏移。>= 0 时自动把该字节读为 uint8 写入 tdata.status
batteryNumber-1帧内电池电压字节偏移。>= 0 时取 uint8 后按 (vbat * 1.6) / 254 + 2.0 换算为电压(V),保留 2 位小数写入 tdata.battery
rssiBooleanfalsetrue 时从 msg.gwrx[0] 读取网关侧 RSSI 与 SNR,写入 tdata.rssi / tdata.snr
tagListArray[]帧头校验数组,每项 { index, tag }:要求 payload[index] === tag,否则丢弃整帧。
subDeviceObject{ index: -1, type: "uint8" }多子设备场景下标识地址在帧中的位置与类型。index >= 0 时按 type 读取地址写入 tdata.addr;读出 0 则丢弃整帧。

示例:

javascript
let frameInfo = {
    port: 22,
    dataLen: 24,
    rssi: true,
    status: 5,
    battery: 6,
    tagList: [
        { index: 0, tag: 0x83 },
        { index: 1, tag: 0x23 }
    ],
    subDevice: { index: 7, type: "uint8" }
};

2. appInfo — 变量信息标识

appInfo 是数组,每项描述一个要从帧中解析出的应用层变量。

字段类型描述
nameString前端展示用名称。
field_nameString程序与接口调用使用的字段名;也是 tdata 中的键。
unitString单位。
indexNumber变量在帧中的起始字节偏移。
typeString数据类型字符串(见 §3)。
coefficientNumber | String比例系数。数值:解析值乘以该数。字符串:当作另一变量的 field_name,在第一轮解析完成后再做二次乘法(适用于系数本身编码在帧中的场景)。默认 1
decimalNumber保留的小数位数,默认 0
illegalString数据合法性检查,格式 "<操作符><十六进制值>",例如 ">0x80""<0x10""=0xFF"。操作符为 > / < / =,命中时该字段被静默丢弃。检查值会按当前字段的 type 重新读取,十六进制位宽要与 type 对应。
optionsObject值映射表,例如 { 0: "normal", 1: "fault" }。命中时直接替换解析值;在 coefficient / decimal 之后、postProcess 跳过之前应用。
postProcessStringJS 表达式字符串,输入变量名 value。可写多语句,若不含 return 则自动补 return value;。例:"if (value > 8) value = 8; return value"

示例:

javascript
let appInfo = [
    {
        name: "重量1",
        field_name: "weight1",
        unit: "kg",
        index: 6,
        type: "int32be",
        coefficient: "precision1",   // 乘以字段 "precision1" 的解析值
        decimal: 2,
        illegal: ">0x7FFFFFFE"
    },
    {
        name: "状态",
        field_name: "status",
        index: 10,
        type: "uint8",
        options: { 0: "normal", 1: "fault" }
    }
];

3. 数据类型详解

类型字符串大小写不敏感(内部统一 toLowerCase())。

3.1 整数类型

类型字节数说明
int8 / uint81
int16 / int16be / uint16 / uint16be2大端(默认)
int16le / uint16le2小端
int24 / int24be / uint24 / uint24be3大端
int24le / uint24le3小端
int32 / int32be / uint32 / uint32be4大端
int32le / uint32le4小端
int40 / uint40 / int48 / uint48 / int56 / uint56(含 BE / LE 变体)5 / 6 / 7
int64 / uint64(含 BE / LE 变体)8内部使用 BigInt64* 读取后 Number() 转换,超过 2⁵³ 会损失精度。

3.2 浮点类型

类型字节数说明
floatbe / floatle4IEEE 754 单精度浮点,大/小端。
floatcdab4字节序 2-3-0-1,常见于部分 Modbus 设备。
intcdab4有符号 32 位整数,字节序 2-3-0-1。

3.3 编码数值类型

类型说明
bcdbe<N>N 字节大端 BCD。每个 nibble 必须为 0–9,否则抛错。例:bcdbe4
bcdle<N>N 字节小端 BCD。
hexbe<N>N 字节大端 → 小写十六进制字符串(无 0x 前缀)。
hexle<N>N 字节小端 → 小写十六进制字符串。

3.4 位字段类型

格式:bitbe<S>-<E>bitle<S>-<E>
S / E 为起止位索引(含,从 0 起)。

  • bitbe:字节按大端拼成大整数后再 shift/mask。
  • bitle:字节按小端拼成大整数后再 shift/mask。

例:bitle3-9 按小端字节序提取第 3 到 9 位。

3.5 布尔类型

类型返回值
bool<N>N 字节无符号整数 → 非零返回 1,零返回 0
boolean<N>N 字节无符号整数 → 非零返回 true,零返回 false

3.6 字符串类型

类型说明
string<N>N 字节 ASCII 字符串。

4. 物模型展示字段配置

控制平台前端如何渲染设备数据:

json
{
    "fields": {
        "<field_name>": {
            "icon": null,
            "name": "<展示名称>",
            "type": "<number|string|boolean|object>",
            "unit": "<单位>",
            "order": 1,
            "field_name": "<必须与 appInfo.field_name 一致>"
        }
    }
}
字段类型描述
iconNull当前保持为 null
nameString前端显示名。
typeStringnumber / string / boolean / object 之一。
orderNumber展示顺序(升序)。
field_nameString必须与对应 appInfo.field_name 一致。
unitString单位,可为空串。

5. BufferTKL — 底层 Buffer 读写

BufferTKL 封装 Node.js Buffer,提供基于 §3 类型字符串的有类型读写。

javascript
// 读:传入已有 Buffer 构造,然后调用 read(offset, type)
let buf = new BufferTKL(Buffer.from("0102030405", "hex"));
let val = buf.read(0, "uint16be");  // → 258

// 写:静态方法,返回编码后的 Buffer
let outBuf = BufferTKL.write(258, "uint16be");  // → <Buffer 01 02>
方法描述
new BufferTKL(buffer)包装已有 Buffer。若 buffer 为 undefined 或非 Buffer,则包装空 Buffer。
buf.read(offset, type)offset 起按指定类型读取值。越界或类型无效时返回 false
BufferTKL.write(value, type)value 按指定类型编码为新 Buffer。类型无效时返回空 Buffer。
BufferTKL.getTypeInfo(type)返回 { type, dataLen, bitStart?, bitEnd? },用于计算字节长度等。

6. 解析器类

6.1 PayloadParser

负责 LoRaWAN 上行数据的标准解析。构造参数:

javascript
new PayloadParser({
    bufferType,   // 可选,默认 BufferTKL
    device,       // 当前设备对象
    msg,          // 上行 msg(提供 msg.userdata.payload / port、msg.gwrx 等)
    frameInfo,    // 见 §1
    appInfo,      // 见 §2
    paraInfo,     // 见 §6.3,仅 paras() 使用
    extraData     // 透传字段,子类(如 MSparser)使用
})
方法说明
telemetry()解析为遥测数据 tdata。按顺序校验 port / dataLen / tagList / subDevice,然后提取 status / battery / rssi 及各 appInfo 字段,依次应用 illegal / coefficient / decimal / postProcess / options。第二轮处理 coefficient 为字段名字符串的条目。返回对象或 null
paras()解析端口 214 的参数回包。按段标识拆分——0x2F(app) / 0x21(fw) / 0x29(cf) / 0x22(radio) / 0x24(ds) / 0x23(status)——再按 paraInfo 键名 <段名>_<地址>[_<k>] 匹配解析。端口不为 214 时直接返回 null

子类可重写 _preParseTelemetry / _postParseTelemetry / _preParseParas / _postParseParas 注入自定义逻辑。

6.2 MSparser — 多传感器 / 多子设备

PayloadParser 的子类,专用于"一帧含多个子设备数据"的场景。构造时必须传 extraData

javascript
new MSparser({ device, msg, frameInfo, extraData })

帧格式:重复的 [ID (idType 字节) | 字段数据...] 块。每读到一个 ID,MSparserextraData 中键名 user_0x<ID>_<j>(j = 0..3,每个 ID 最多 4 个字段)找到对应 appInfo 项并自动累加偏移。frameInfo.idType 指定 ID 的读取类型(通常 "uint16be")。

6.3 paraInfo 键命名规范

paraInfoparas()RPCHelper.buildFrame() 使用,将寄存器地址映射到具名字段。

键格式含义
app_<addr>APP 段,寄存器地址 <addr>
app_<addr>_<k>同地址多个字段,k = 1..15
cf_<addr> / fw_<addr> / radio_<addr> / ds_<addr> / status_<addr>配置 / 固件 / 射频 / 设备 / 状态段。

值对象结构与 appInfo 项一致(field_name / type / coefficient / decimal / illegal / options / postProcess)。


7. RPCHelper — 参数下发与告警

7.1 设备控制指令

方法返回描述
RPCHelper.reset()Buffer固定字节序列:设备复位。
RPCHelper.redo()Buffer固定字节序列:重做。
RPCHelper.join()Buffer固定字节序列:重入网。

7.2 TKL 协议参数读写

生成 TKL 协议设备的原始参数帧 Buffer。

方法描述
RPCHelper.paraRead(name, type, addr)单寄存器读指令。name 为段名(app / cf / fw / radio / ds / status)。
RPCHelper.paraWrite(name, type, addr, value)单寄存器写指令。
RPCHelper.buildFrame({ serverParaDef, paraDef, params })paraInfo 将多字段打包为一个复合帧;自动合并相邻寄存器,最大写 180 字节 / 最大读 50 字节。返回 { writeBuffer, readBuffer, serverPara, log }
RPCHelper.arrangePara(paraInfo)paraInfo 键按段(app / fw / cf / radio / ds)分组并按寄存器地址排序。buildFrame 内部调用。

7.3 Modbus 帧构造

方法描述
RPCHelper.buildmodbusFrame06({ serverParaDef, paraDef, params })构造 Modbus FC06(写单个寄存器)帧,含 CRC16 Modbus。
RPCHelper.buildmodbusFrame10({ serverParaDef, paraDef, params })构造 Modbus FC10(写多个寄存器)帧。
RPCHelper.buildmodbusFrameRead(addr, code, regStart, count)构造 Modbus 读帧(如 FC03 / FC04)。addr 和寄存器值支持 "0x..." 字符串或十进制数。
RPCHelper.modbusAction(addr, code, regAddr, regVal, valBuffer?)构造通用 Modbus 单写帧;如提供 valBuffer 则追加到帧尾。

7.4 其他协议帧

方法描述
RPCHelper.buildSensorFrame({ serverParaDef, paraDef, params })通用传感器协议帧,帧头须以 paraDef.cmd_header(十六进制字符串)给出。
RPCHelper.cj188reader(addr, model)构造 CJ/T 188 读表请求。addr 解码后必须恰好 7 字节,model 解码后必须恰好 5 字节,否则返回空 Buffer。

7.5 下行消息构造

方法描述
RPCHelper.makeMSG({ msgType, device, dnBuffer, type?, port?, confirmed?, sleepTime?, dnWaitms?, intervalms? })构造下行消息体。msgTypeUtils.msgType.*user(自定义端口)/ paras(port 214)/ transParent(port 51)/ swDown(直发 LoRa SW 帧)。
RPCHelper.makeDnDataClear(eui)构造下行 dataClear 消息(port 214)。
RPCHelper.makeAlarm({ alarms, eui, name, action, group, title, desc, level })构造告警消息;action = "new" 且现存告警的 title / desc / level 完全一致时返回 null(去重)。

8. Utils — 工具函数

8.1 参数下发状态机

方法描述
Utils.makeParaSetMSG({ msgType, checkInfo, device, classMode, rpcName, params, paraDownBuffer, extraAppBuffer, timeout, maxRetries })把"服务端属性更新 + 下行参数包 + 可选重试帧"打包为消息队列数组。classMode = "ClassC" 时按 maxRetries 追加重发帧;extraAppBuffer 用于追加额外 APP 段写包。
Utils.paraCheck(rpc, server_attrs, data)参数下发后上行回包的"校验 + 重试"状态机。未配置 checkInfo 时逐字段比对 paramsdata;配置了 checkInfo 则用其 frameInfo / appInfo 走一遍 PayloadParser.telemetry() 再比对。返回 { sdata, tdata, pdata, actions }。成功时 counter = -1;ClassC 成功还追加 mt_action: downBufferClear;未达 maxRetries 时返回 actions: [{ method: rpc, params }] 触发重发。

makeParaSetMSG 参数说明:

参数默认值说明
msgTypeUtils.msgType.paras下行通道类型。
timeout864000000(10 天)任务总超时(ms)。
maxRetries3最大重试次数。
classMode"ClassA""ClassA" 单发;"ClassC" 追加重试帧。
rpcName必填;作为 server_attrs 中追踪状态的键。
device当前设备对象。
paramsRPC 参数映射。
paraDownBuffer原始下行 Buffer。
extraAppBuffer可选,主包之后追加的额外 APP 段 Buffer。
checkInfo配置后以 PayloadParser 检查代替字段逐一比对。

8.2 Buffer / 值解析工具

方法描述
Utils.parseToBuffer(inputStr)将十六进制字符串(支持 0x 前缀和空格)解析为 Buffer。undefined 返回空 Buffer。
Utils.parseVal(valstr)"0x..." 十六进制字符串或十进制字符串解析为数值。若已是数值则原样返回。

8.3 CRC 计算

方法描述
Utils.crcSum(buf, len, poly?)简单累加校验:buf[0..len-1] 求和,16 位掩码。poly 默认 0
Utils.crc16CCIT(data, len, poly?)CRC-16/CCITT。poly 默认 0x1021
Utils.crc16Modbus(buf, len, poly?)CRC-16/Modbus。poly 默认 0xA001

8.4 枚举常量

常量说明
Utils.ACTION{ no, new, clear }告警动作枚举。
Utils.LEVEL{ low, mid, high, urgent }告警等级枚举。
Utils.msgType{ paras, transParent, swDown, user }下行通道类型枚举。
Utils.paraName{ app, cf, fw, radio, user }参数段名称枚举。

9. TriggerHelper — 告警类触发模型基类

继承 TriggerHelper 并重写 _alarmProcess(info),可避免手写完整的触发返回对象。详见 TriggerModel

构造参数:

参数默认值说明
device当前设备对象。
thingModelId物模型 ID;构造时自动取 device.telemetry_data[thingModelId] 存入 this.tdata
alarmEventdevice.name告警事件名。
title""告警标题,自动加 [device.name] : 前缀。
desc""告警描述。
levelUtils.LEVEL.lowlow / mid / high / urgent
actionUtils.ACTION.newno / new / clear

alarm(info) 行为:

  1. 调用 _alarmProcess(info)
  2. 若返回 falseactionno,返回 null
  3. 否则返回标准触发返回对象:{ delayms: 0, abort_previous_timer: true, actions: [{ method: "alarm", params: {...} }] }

10. BufferTKL 常量

常量说明
BufferTKL.PERIOD_DEFAULT900默认上报周期 15 分钟(秒)。
BufferTKL.PERIOD_DUMB86400 × 3650 ≈ 10 年"事实上禁用周期"的占位值。
BufferTKL.PERIOD_MAX86400 × 365周期上限判定值(1 年,秒)。
BufferTKL.PERIOD_TRIGGER"TRIGGER"表示"由触发驱动,无固定周期"。
BufferTKL.INVALID_NUM-1000000000数值类型的 sentinel(下发时跳过)。
BufferTKL.INVALID_STR"INVALID"字符串类型的 sentinel(下发时跳过)。

11. 完整示例

javascript
function payload_parser({ device, msg, thingModelId, noticeAttrs }) {
  let port = msg?.userdata?.port || null;

  let frameInfo = {
    port: 11, dataLen: 15, rssi: true,
    tagList: [
      { index: 0, tag: 0x21 },
      { index: 1, tag: 0x07 },
      { index: 2, tag: 0x03 }
    ]
  };

  let appInfo = [
    { name: "status",      field_name: "status",      unit: "",   index: 7,  type: "bitle0-0" },
    { name: "leakStatus",  field_name: "leakStatus",  unit: "",   index: 7,  type: "bitle4-4" },
    { name: "temperature", field_name: "temperature", unit: "℃",  index: 8,  type: "uint16le" },
    { name: "humidity",    field_name: "humidity",    unit: "%",  index: 10, type: "uint16le", coefficient: 0.1, decimal: 1 },
    { name: "vbat",        field_name: "vbat",        unit: "v",  index: 12, type: "uint8" }
  ];

  let paraInfo = {
    app_40:  { name: "upPeriod",       field_name: "upPeriod",       unit: "s",  type: "uint32le" },
    app_142: { name: "measurePeriod",  field_name: "measurePeriod",  unit: "s",  type: "uint16le" },
    app_144: { name: "covTemperature", field_name: "covTemperature", unit: "℃", type: "uint8", coefficient: 0.1, decimal: 1 },
    app_145: { name: "covHumidity",    field_name: "covHumidity",    unit: "%",  type: "uint8", coefficient: 0.1, decimal: 1 }
  };

  let payParser = new PayloadParser({ device, msg, frameInfo, appInfo, paraInfo });

  let tdata = null, pdata = null;
  if (port === 214) {
    pdata = payParser.paras();
  } else {
    tdata = payParser.telemetry();
  }

  if (tdata) {
    tdata.status      = tdata.status !== 0 ? "fault" : "normal";
    tdata.temperature = Number(((tdata.temperature - 1000) * 0.1).toFixed(1));
    tdata.vbat        = Number(((tdata.vbat * 1.6) / 254 + 2.0).toFixed(2));
  }

  return {
    telemetry_data: tdata,
    server_attrs:   null,
    shared_attrs:   pdata
  };
}