tklHelper
tklHelper is the toolkit ThinkLink provides to simplify Thing Model and RPC configuration. Through JSON-style declarative configuration combined with PayloadParser / MSparser / RPCHelper / Utils / TriggerHelper classes, you can implement uplink parsing, parameter downlink, and alarm dispatch without writing low-level buffer logic.
This document mirrors
source/tklHelper.js(version1.01.036). Inside Thing Model scripts, RPC scripts, and Trigger scripts, all classes exported bytklHelper(BufferTKL/PayloadParser/MSparser/Utils/TriggerHelper/RPCHelper) are available as sandbox globals — noimportneeded.
1. frameInfo — Frame Validation
frameInfo validates each uplink frame before parsing begins. Frames that fail any rule are dropped (parser returns null).
| Field | Type | Default | Description |
|---|---|---|---|
port | Number | -1 | LoRaWAN uplink port. Positive value: frames with a mismatched port are dropped. -1 disables the check. |
dataLen | Number | -1 | Expected frame length (bytes). Positive value: frames with a different length are dropped. -1 disables the check. |
status | Number | -1 | Byte offset of the device status byte. When >= 0, reads that byte as uint8 and writes it to tdata.status. |
battery | Number | -1 | Byte offset of battery voltage. When >= 0, reads that byte as uint8, applies (vbat * 1.6) / 254 + 2.0 (volts, 2 decimal places), and writes to tdata.battery. |
rssi | Boolean | false | When true, reads RSSI and SNR from msg.gwrx[0] and writes them to tdata.rssi / tdata.snr. |
tagList | Array | [] | Frame-header validation. Each entry { index, tag } requires payload[index] === tag; otherwise the whole frame is dropped. |
subDevice | Object | { index: -1, type: "uint8" } | Sub-device address location. When index >= 0, reads the address using type and writes it to tdata.addr. If the address is 0, the frame is dropped. |
Example:
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 — Variable Definitions
appInfo is an array; each entry describes one application-layer variable to extract from the frame.
| Field | Type | Description |
|---|---|---|
name | String | Display name shown on the frontend. |
field_name | String | Code-facing field name; also the key in tdata. |
unit | String | Unit string. |
index | Number | Starting byte offset in the frame. |
type | String | Data type string (see §3). |
coefficient | Number | String | Scaling factor. Number: parsed value is multiplied. String: treated as another variable's field_name; multiplication happens in the second pass after all fields are read (useful when a scale factor is itself encoded in the frame). Default 1. |
decimal | Number | Number of decimal places to retain. Default 0. |
illegal | String | Validity check, format "<op><hex>" — e.g. ">0x80", "<0x10", "=0xFF". Operator is > / < / =. When the condition is met, the field is silently dropped. The hex value is re-read using the same type, so its width must match. |
options | Object | Value lookup table, e.g. { 0: "normal", 1: "fault" }. When the parsed value matches a key, the mapped string replaces it. Applied after coefficient / decimal, before postProcess is skipped. |
postProcess | String | JS expression string. The variable name is value. Multi-statement is allowed; if there is no return, return value; is appended automatically. Example: "if (value > 8) value = 8; return value". |
Example:
let appInfo = [
{
name: "Weight 1",
field_name: "weight1",
unit: "kg",
index: 6,
type: "int32be",
coefficient: "precision1", // multiply by the value of field "precision1"
decimal: 2,
illegal: ">0x7FFFFFFE"
},
{
name: "Status",
field_name: "status",
index: 10,
type: "uint8",
options: { 0: "normal", 1: "fault" }
}
];3. Data Type Reference
Type strings are case-insensitive (internally normalised with toLowerCase()).
3.1 Integer Types
| Type | Bytes | Notes |
|---|---|---|
int8 / uint8 | 1 | |
int16 / int16be / uint16 / uint16be | 2 | Big-endian (default) |
int16le / uint16le | 2 | Little-endian |
int24 / int24be / uint24 / uint24be | 3 | Big-endian |
int24le / uint24le | 3 | Little-endian |
int32 / int32be / uint32 / uint32be | 4 | Big-endian |
int32le / uint32le | 4 | Little-endian |
int40 / uint40 / int48 / uint48 / int56 / uint56 (BE / LE variants) | 5 / 6 / 7 | |
int64 / uint64 (BE / LE variants) | 8 | Read via BigInt64*, then Number(). Values above 2⁵³ lose precision. |
3.2 Float Types
| Type | Bytes | Notes |
|---|---|---|
floatbe / floatle | 4 | IEEE 754 float, big/little-endian. |
floatcdab | 4 | Byte order 2-3-0-1 — common on certain Modbus devices. |
intcdab | 4 | Signed 32-bit int, byte order 2-3-0-1. |
3.3 Encoded-Number Types
| Type | Notes |
|---|---|
bcdbe<N> | N-byte big-endian BCD. Each nibble must be 0–9, otherwise throws. Example: bcdbe4. |
bcdle<N> | N-byte little-endian BCD. |
hexbe<N> | N bytes big-endian → lowercase hex string (no 0x prefix). |
hexle<N> | N bytes little-endian → lowercase hex string. |
3.4 Bit-field Types
Format: bitbe<S>-<E> or bitle<S>-<E>.S / E are the start and end bit indices (inclusive, 0-based).
bitbe: bytes assembled big-endian before shift/mask.bitle: bytes assembled little-endian before shift/mask.
Example: bitle3-9 extracts bits 3 to 9, little-endian byte order.
3.5 Boolean Types
| Type | Returns |
|---|---|
bool<N> | N-byte unsigned int → 1 (non-zero) or 0 (zero). |
boolean<N> | N-byte unsigned int → true (non-zero) or false (zero). |
3.6 String Type
| Type | Notes |
|---|---|
string<N> | N-byte ASCII string. |
4. Thing Model Display Field Configuration
These fields control how the platform frontend renders device data:
{
"fields": {
"<field_name>": {
"icon": null,
"name": "<display name>",
"type": "<number|string|boolean|object>",
"unit": "<unit>",
"order": 1,
"field_name": "<must match appInfo.field_name>"
}
}
}| Field | Type | Description |
|---|---|---|
icon | Null | Always null for now. |
name | String | Display name on the frontend. |
type | String | One of number / string / boolean / object. |
order | Number | Display order (ascending). |
field_name | String | Must match the corresponding appInfo.field_name. |
unit | String | Unit string; may be empty. |
5. BufferTKL — Low-level Buffer Read / Write
BufferTKL wraps a Node.js Buffer and provides typed read/write using the type strings from §3.
// Read: construct with an existing Buffer, then call read(offset, type)
let buf = new BufferTKL(Buffer.from("0102030405", "hex"));
let val = buf.read(0, "uint16be"); // → 258
// Write: static method, returns a Buffer containing the encoded value
let outBuf = BufferTKL.write(258, "uint16be"); // → <Buffer 01 02>| Method | Description |
|---|---|
new BufferTKL(buffer) | Wrap an existing Buffer. If buffer is undefined or not a Buffer, wraps an empty Buffer. |
buf.read(offset, type) | Read a value at offset using the given type string. Returns false if out-of-bounds or the type is invalid. |
BufferTKL.write(value, type) | Encode value into a new Buffer using the given type string. Returns an empty Buffer on invalid type. |
BufferTKL.getTypeInfo(type) | Returns { type, dataLen, bitStart?, bitEnd? } for a type string; useful for computing byte lengths. |
6. Parser Classes
6.1 PayloadParser
Standard LoRaWAN uplink parser. Constructor:
new PayloadParser({
bufferType, // optional; defaults to BufferTKL
device, // current device object
msg, // uplink msg (provides msg.userdata.payload / port, msg.gwrx, ...)
frameInfo, // see §1
appInfo, // see §2
paraInfo, // see §6.3; used by paras() only
extraData // pass-through; used by subclasses such as MSparser
})| Method | Description |
|---|---|
telemetry() | Parses telemetry data into tdata. Validates port / dataLen / tagList / subDevice, then extracts status / battery / rssi and each appInfo field — applying illegal / coefficient / decimal / postProcess / options in order. A second pass handles entries whose coefficient is a field-name string. Returns the object or null. |
paras() | Parses the parameter-reply uplink on port 214. Splits the payload by segment marker — 0x2F (app) / 0x21 (fw) / 0x29 (cf) / 0x22 (radio) / 0x24 (ds) / 0x23 (status) — then maps each register to paraInfo keys of the form <segment>_<addr>[_<k>]. Returns null if the port is not 214. |
Override _preParseTelemetry / _postParseTelemetry / _preParseParas / _postParseParas in a subclass to inject custom logic.
6.2 MSparser — Multi-Sensor / Multi-Sub-device
A subclass of PayloadParser for frames that carry readings from multiple sub-devices. extraData is required:
new MSparser({ device, msg, frameInfo, extraData })Frame layout: repeated [ID (idType bytes) | field data...] blocks. For each block, MSparser looks up extraData keys named user_0x<ID>_<j> (j = 0..3, up to 4 fields per ID) to find matching appInfo entries and advance the internal index. Set frameInfo.idType to match how the ID field is encoded (typically "uint16be").
6.3 paraInfo Key Naming
paraInfo is used by paras() and RPCHelper.buildFrame() to map register addresses to named fields.
| Key format | Meaning |
|---|---|
app_<addr> | APP segment, register <addr>. |
app_<addr>_<k> | Multiple fields at the same register, k = 1..15. |
cf_<addr> / fw_<addr> / radio_<addr> / ds_<addr> / status_<addr> | Configuration / firmware / radio / device / status segments. |
Each value object has the same shape as an appInfo entry (field_name / type / coefficient / decimal / illegal / options / postProcess).
7. RPCHelper — Parameter Downlink & Alarms
7.1 Device Control Commands
| Method | Returns | Description |
|---|---|---|
RPCHelper.reset() | Buffer | Fixed byte sequence: device reset. |
RPCHelper.redo() | Buffer | Fixed byte sequence: redo. |
RPCHelper.join() | Buffer | Fixed byte sequence: re-join. |
7.2 TKL Parameter Read / Write
These methods produce raw parameter frame Buffers for TKL-protocol devices.
| Method | Description |
|---|---|
RPCHelper.paraRead(name, type, addr) | Single-register read command. name = segment (app / cf / fw / radio / ds / status). |
RPCHelper.paraWrite(name, type, addr, value) | Single-register write command. |
RPCHelper.buildFrame({ serverParaDef, paraDef, params }) | Packs multiple read/write fields into one composite frame using paraInfo. Merges adjacent registers; max 180 bytes per write, 50 bytes per read. Returns { writeBuffer, readBuffer, serverPara, log }. |
RPCHelper.arrangePara(paraInfo) | Sorts paraInfo keys into { app, fw, cf, radio, ds } lists ordered by register address. Used internally by buildFrame. |
7.3 Modbus Frame Builders
| Method | Description |
|---|---|
RPCHelper.buildmodbusFrame06({ serverParaDef, paraDef, params }) | Builds a Modbus function-code 06 (write single register) frame, CRC16 Modbus included. |
RPCHelper.buildmodbusFrame10({ serverParaDef, paraDef, params }) | Builds a Modbus function-code 10 (write multiple registers) frame. |
RPCHelper.buildmodbusFrameRead(addr, code, regStart, count) | Builds a Modbus read frame (e.g. FC 03 / 04). addr and register values accept "0x..." strings or decimal numbers. |
RPCHelper.modbusAction(addr, code, regAddr, regVal, valBuffer?) | Builds a generic Modbus single-write frame; appends valBuffer if provided. |
7.4 Other Protocol Builders
| Method | Description |
|---|---|
RPCHelper.buildSensorFrame({ serverParaDef, paraDef, params }) | Generic sensor-protocol frame. Frame header must be provided as paraDef.cmd_header (hex string). |
RPCHelper.cj188reader(addr, model) | Builds a CJ/T 188 meter-read request. addr must decode to exactly 7 bytes; model must decode to exactly 5 bytes. Returns empty Buffer on mismatch. |
7.5 Downlink Message Builders
| Method | Description |
|---|---|
RPCHelper.makeMSG({ msgType, device, dnBuffer, type?, port?, confirmed?, sleepTime?, dnWaitms?, intervalms? }) | Builds a downlink message body. msgType comes from Utils.msgType.*: user (custom port) / paras (port 214) / transParent (port 51) / swDown (raw LoRa SW frame). |
RPCHelper.makeDnDataClear(eui) | Builds a dataClear downlink message (port 214). |
RPCHelper.makeAlarm({ alarms, eui, name, action, group, title, desc, level }) | Builds an alarm message. When action = "new" and an existing alarm with identical title / desc / level is already active, returns null (deduplication). |
8. Utils — Utility Functions
8.1 Parameter State Machine
| Method | Description |
|---|---|
Utils.makeParaSetMSG({ msgType, checkInfo, device, classMode, rpcName, params, paraDownBuffer, extraAppBuffer, timeout, maxRetries }) | Packs server-attr update + downlink buffer + optional retry frames into a message queue. When classMode = "ClassC", maxRetries retry copies are appended. extraAppBuffer adds an extra APP-segment write packet. Returns an array of message queue entries. |
Utils.paraCheck(rpc, server_attrs, data) | "Verify and retry" state machine for parameter-confirm uplinks. Compares received data against the pending RPC params (or runs PayloadParser.telemetry() when checkInfo is configured). Returns { sdata, tdata, pdata, actions }. On success, counter = -1; on ClassC success, appends mt_action: downBufferClear. While under maxRetries, returns actions: [{ method: rpc, params }] to trigger a re-send. |
makeParaSetMSG parameters:
| Parameter | Default | Description |
|---|---|---|
msgType | Utils.msgType.paras | Downlink channel type. |
timeout | 864000000 (10 days) | Task total timeout (ms). |
maxRetries | 3 | Maximum retry count. |
classMode | "ClassA" | "ClassA": single send. "ClassC": appends retry frames. |
rpcName | — | Required; used as the key in server_attrs to track state. |
device | — | Current device object. |
params | — | RPC parameter map. |
paraDownBuffer | — | The raw downlink buffer. |
extraAppBuffer | — | Optional extra APP-segment buffer appended after the main packet. |
checkInfo | — | If set, overrides field-by-field comparison with a PayloadParser-based check. |
8.2 Buffer / Value Helpers
| Method | Description |
|---|---|
Utils.parseToBuffer(inputStr) | Parses a hex string (with or without 0x prefixes / spaces) into a Buffer. Returns an empty Buffer for undefined. |
Utils.parseVal(valstr) | Parses a hex string ("0x...") or decimal string into a number. If valstr is already a number, returns it unchanged. |
8.3 CRC Functions
| Method | Description |
|---|---|
Utils.crcSum(buf, len, poly?) | Simple checksum: sum of buf[0..len-1], masked to 16 bits. poly defaults to 0. |
Utils.crc16CCIT(data, len, poly?) | CRC-16/CCITT. poly defaults to 0x1021. |
Utils.crc16Modbus(buf, len, poly?) | CRC-16/Modbus. poly defaults to 0xA001. |
8.4 Enumerations
| Constant | Values | Description |
|---|---|---|
Utils.ACTION | { no, new, clear } | Alarm action enum. |
Utils.LEVEL | { low, mid, high, urgent } | Alarm severity enum. |
Utils.msgType | { paras, transParent, swDown, user } | Downlink channel type enum. |
Utils.paraName | { app, cf, fw, radio, user } | Parameter segment name enum. |
9. TriggerHelper — Alarm Trigger Base Class
Extend TriggerHelper and override _alarmProcess(info) to return clean alarm trigger objects without boilerplate. See also TriggerModel.
Constructor parameters:
| Parameter | Default | Description |
|---|---|---|
device | — | Current device object. |
thingModelId | — | Thing Model ID; the constructor reads device.telemetry_data[thingModelId] into this.tdata. |
alarmEvent | device.name | Alarm event name. |
title | "" | Alarm title; automatically prefixed with [device.name] : . |
desc | "" | Alarm description. |
level | Utils.LEVEL.low | low / mid / high / urgent. |
action | Utils.ACTION.new | no / new / clear. |
alarm(info) behaviour:
- Calls
_alarmProcess(info). - If it returns
false, oractionisno, returnsnull. - Otherwise returns the standard trigger return object:
{ delayms: 0, abort_previous_timer: true, actions: [{ method: "alarm", params: {...} }] }.
10. BufferTKL Constants
| Constant | Value | Description |
|---|---|---|
BufferTKL.PERIOD_DEFAULT | 900 | Default reporting period: 15 minutes (seconds). |
BufferTKL.PERIOD_DUMB | 86400 × 3650 ≈ 10 years | Sentinel for "effectively disabled" periodic reporting. |
BufferTKL.PERIOD_MAX | 86400 × 365 | Upper bound for period validation (1 year in seconds). |
BufferTKL.PERIOD_TRIGGER | "TRIGGER" | Indicates trigger-driven reporting with no fixed period. |
BufferTKL.INVALID_NUM | -1000000000 | Numeric sentinel value (skipped on downlink). |
BufferTKL.INVALID_STR | "INVALID" | String sentinel value (skipped on downlink). |
11. Complete Example
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
};
}