Skip to content

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 (version 1.01.036). Inside Thing Model scripts, RPC scripts, and Trigger scripts, all classes exported by tklHelper (BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper) are available as sandbox globals — no import needed.


1. frameInfo — Frame Validation

frameInfo validates each uplink frame before parsing begins. Frames that fail any rule are dropped (parser returns null).

FieldTypeDefaultDescription
portNumber-1LoRaWAN uplink port. Positive value: frames with a mismatched port are dropped. -1 disables the check.
dataLenNumber-1Expected frame length (bytes). Positive value: frames with a different length are dropped. -1 disables the check.
statusNumber-1Byte offset of the device status byte. When >= 0, reads that byte as uint8 and writes it to tdata.status.
batteryNumber-1Byte 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.
rssiBooleanfalseWhen true, reads RSSI and SNR from msg.gwrx[0] and writes them to tdata.rssi / tdata.snr.
tagListArray[]Frame-header validation. Each entry { index, tag } requires payload[index] === tag; otherwise the whole frame is dropped.
subDeviceObject{ 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:

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 — Variable Definitions

appInfo is an array; each entry describes one application-layer variable to extract from the frame.

FieldTypeDescription
nameStringDisplay name shown on the frontend.
field_nameStringCode-facing field name; also the key in tdata.
unitStringUnit string.
indexNumberStarting byte offset in the frame.
typeStringData type string (see §3).
coefficientNumber | StringScaling 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.
decimalNumberNumber of decimal places to retain. Default 0.
illegalStringValidity 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.
optionsObjectValue 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.
postProcessStringJS 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:

javascript
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

TypeBytesNotes
int8 / uint81
int16 / int16be / uint16 / uint16be2Big-endian (default)
int16le / uint16le2Little-endian
int24 / int24be / uint24 / uint24be3Big-endian
int24le / uint24le3Little-endian
int32 / int32be / uint32 / uint32be4Big-endian
int32le / uint32le4Little-endian
int40 / uint40 / int48 / uint48 / int56 / uint56 (BE / LE variants)5 / 6 / 7
int64 / uint64 (BE / LE variants)8Read via BigInt64*, then Number(). Values above 2⁵³ lose precision.

3.2 Float Types

TypeBytesNotes
floatbe / floatle4IEEE 754 float, big/little-endian.
floatcdab4Byte order 2-3-0-1 — common on certain Modbus devices.
intcdab4Signed 32-bit int, byte order 2-3-0-1.

3.3 Encoded-Number Types

TypeNotes
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

TypeReturns
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

TypeNotes
string<N>N-byte ASCII string.

4. Thing Model Display Field Configuration

These fields control how the platform frontend renders device data:

json
{
    "fields": {
        "<field_name>": {
            "icon": null,
            "name": "<display name>",
            "type": "<number|string|boolean|object>",
            "unit": "<unit>",
            "order": 1,
            "field_name": "<must match appInfo.field_name>"
        }
    }
}
FieldTypeDescription
iconNullAlways null for now.
nameStringDisplay name on the frontend.
typeStringOne of number / string / boolean / object.
orderNumberDisplay order (ascending).
field_nameStringMust match the corresponding appInfo.field_name.
unitStringUnit 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.

javascript
// 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>
MethodDescription
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:

javascript
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
})
MethodDescription
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:

javascript
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 formatMeaning
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.1 Device Control Commands

MethodReturnsDescription
RPCHelper.reset()BufferFixed byte sequence: device reset.
RPCHelper.redo()BufferFixed byte sequence: redo.
RPCHelper.join()BufferFixed byte sequence: re-join.

7.2 TKL Parameter Read / Write

These methods produce raw parameter frame Buffers for TKL-protocol devices.

MethodDescription
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

MethodDescription
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

MethodDescription
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.
MethodDescription
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

MethodDescription
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:

ParameterDefaultDescription
msgTypeUtils.msgType.parasDownlink channel type.
timeout864000000 (10 days)Task total timeout (ms).
maxRetries3Maximum retry count.
classMode"ClassA""ClassA": single send. "ClassC": appends retry frames.
rpcNameRequired; used as the key in server_attrs to track state.
deviceCurrent device object.
paramsRPC parameter map.
paraDownBufferThe raw downlink buffer.
extraAppBufferOptional extra APP-segment buffer appended after the main packet.
checkInfoIf set, overrides field-by-field comparison with a PayloadParser-based check.

8.2 Buffer / Value Helpers

MethodDescription
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

MethodDescription
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

ConstantValuesDescription
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:

ParameterDefaultDescription
deviceCurrent device object.
thingModelIdThing Model ID; the constructor reads device.telemetry_data[thingModelId] into this.tdata.
alarmEventdevice.nameAlarm event name.
title""Alarm title; automatically prefixed with [device.name] : .
desc""Alarm description.
levelUtils.LEVEL.lowlow / mid / high / urgent.
actionUtils.ACTION.newno / new / clear.

alarm(info) behaviour:

  1. Calls _alarmProcess(info).
  2. If it returns false, or action is no, returns null.
  3. Otherwise returns the standard trigger return object: { delayms: 0, abort_previous_timer: true, actions: [{ method: "alarm", params: {...} }] }.

10. BufferTKL Constants

ConstantValueDescription
BufferTKL.PERIOD_DEFAULT900Default reporting period: 15 minutes (seconds).
BufferTKL.PERIOD_DUMB86400 × 3650 ≈ 10 yearsSentinel for "effectively disabled" periodic reporting.
BufferTKL.PERIOD_MAX86400 × 365Upper bound for period validation (1 year in seconds).
BufferTKL.PERIOD_TRIGGER"TRIGGER"Indicates trigger-driven reporting with no fixed period.
BufferTKL.INVALID_NUM-1000000000Numeric sentinel value (skipped on downlink).
BufferTKL.INVALID_STR"INVALID"String sentinel value (skipped on downlink).

11. Complete Example

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
  };
}