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 manual and the complete source are synchronized to
tklHelper 1.01.043(source:tkl-opt/tooling/integration-tools/tklHelper.js, 2026-10-12). 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.
V3 runtime requirement
V3 is an explicitly selected parameter transaction version; the default remains V2. This documentation update does not synchronize the backend helper or deploy the platform. Confirm that the target runtime has loaded 1.01.043. Migrate the sending RPC and receiving Thing Model together; see the section 12 examples.
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
Library version 1.01.043 is separate from the transaction version. Version arguments accept only numeric 2 / 3; omission or undefined selects 2. Values such as string "3" and null throw. V3 neither falls back to V2 records nor migrates them automatically.
| Method | Description |
|---|---|
Utils.makeParaSetMSG({ version, msgType, checkInfo, device, classMode, rpcName, params, paraDownBuffer, extraAppBuffer, timeout, maxRetries, telemetryFields }) | Returns a queue of attribute persistence, parameter downlink, and optional retry actions. V2 uses server_attrs; V3 stores each complete transaction under process_data.__para_set__<slot>. |
Utils.paraCheck(rpcName, device, data, { version: 3 }) | V3 entry; the second argument is the complete device. Returns the required standard Thing Model fields: process_data / telemetry_data / shared_attrs / server_attrs / actions. Missing or terminal transactions return {}. |
Utils.paraCheck(rpcName, device.server_attrs, data) | Compatible V2 entry returning { sdata, tdata, pdata, actions }; convert these to standard Thing Model fields. |
Utils.getParaSetInfo({ device, rpcName, version, msgType? }) | Returns a detached { version, rpcInfo, checkState } copy, or null if absent. Does not persist or calculate expiration. |
Utils.updateParaSetInfo({ device, rpcName, version, msgType?, updates }) | Validates and returns a patch for persistence. Does not mutate the device, save directly, or resend. See section 12.6 for usage and allowed fields. |
makeParaSetMSG options:
| Option | Default | Description |
|---|---|---|
version | 2 | Explicit numeric 3 enables V3; pass it on every RPC invocation, including automatic retries. |
msgType | Utils.msgType.paras | paras uses port 214; transParent uses port 51. Preserve the device's actual channel. |
timeout | Class A: 864000000; Class C: 5000 | Milliseconds. Transaction confirmation lifetime; also the Class C retry interval when constructing the queue. Section 12.5 adjusts the confirmation window while retaining that interval. |
maxRetries | 3 | Retries beyond the initial send. Class C schedules them at creation; Class A generates retry actions from its mismatch state. |
classMode | device.shared_attrs.class_mode, otherwise "ClassA" | "ClassA" / "ClassC"; successful Class C confirmation returns a queue-clear action. |
rpcName | — | Actual callable RPC identifier; required for V3. Ordinary RPCs have separate slots; transparent requests share transParent. |
device | — | Complete current device object. |
params | — | Parameters to compare with device feedback, retaining actual field names and types. |
paraDownBuffer | — | Raw downlink Buffer from the existing protocol encoder. |
extraAppBuffer | — | Optional additional APP-segment Buffer after the main packet. |
checkInfo | — | Omit to compare parsed parameters directly. An object selects frame checking; V3 requires nonempty appInfo with expected val values. |
telemetryFields | — | V3 telemetry allowlist after confirmation. Explicit checkInfo.telemetry_data takes precedence; null disables telemetry. No mapping or allowlist means no automatic telemetry fill. |
V3 transaction data and the new read/update interfaces require lossless JSON values. Omit optional fields instead of using undefined; nonfinite numbers, functions, BigInt, cycles, sparse arrays, and nonplain objects are rejected. The downlink Buffer still follows the existing protocol input. Transport dnMsg.version: "3.0" / "3.1" is unrelated to transaction versioning.
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
};
}12. V3: RPC and Thing Model Integration
Applies to the local opt helper 1.01.043, dated 2026-10-12. The backend has not been synchronized and the platform has not been released as part of this change. Verify the target script runtime before use. These examples demonstrate integration points, not a complete package for a real device.
Library version 1.01.043 and transaction version: 2 / 3 are separate. Omitting the transaction version still selects V2; only numeric 3 enables V3. The RPC and receiving Thing Model must select the same version.
12.1 What changes
| Location | V2 | V3 |
|---|---|---|
| Sending RPC | Existing makeParaSetMSG() call | Add version: 3 and configure confirmation fills as needed |
| Receiving Thing Model | paraCheck(rpcName, device.server_attrs, data) | paraCheck(rpcName, device, data, { version: 3 }) |
| Thing Model result | Convert sdata/tdata/pdata to standard fields | Return the standard helper result, or merge each result group with existing parsing output |
| Transaction storage | Existing server_attrs slots | process_data.__para_set__<slot> |
| UI feedback | Existing behavior | Consume telemetry generated after confirmation; process data itself does not publish MQTT |
Use the device's actual callable rpcName, not a UI display label. The Thing Model must validate feedback for this device and protocol. The helper adds neither cross-device routing nor generic CRC validation.
Examples below are script bodies or embedded fragments. Save the body that actually executes, not an uncalled function declaration. Preserve existing validation, protocol encoding, device routing, and persistent execution logs.
12.2 RPC: send parameters and record a V3 transaction
This example uses direct comparison of parsed parameters. It assumes the target device, actual rpcName, validated params, and an existing protocol encoder's paraDownBuffer are available. Replace the teaching field target_value with the device's actual field. Do not pass all raw user input, passwords, or fields unrelated to confirmation.
// Pass only parameters that require confirmation from the device.
return Utils.makeParaSetMSG({
version: 3,
device,
rpcName,
msgType: Utils.msgType.paras,
params: { target_value: params.target_value },
paraDownBuffer,
telemetryFields: ['target_value']
});Preserve the actual channel: paras uses port 214, while transParent uses port 51. Section 12.4 covers transparent frame checking.
telemetryFields explicitly allows fields to be filled after confirmation. Declare those fields in the receiving Thing Model with matching names, types, and units. Without an allowlist or explicit mapping, the helper does not copy all parameters into telemetry.
Every RPC entry, including automatic Class A retries, must pass version: 3. Retry actions do not inject the version into business parameters. A generated queue represents pending actions; it does not prove device execution.
12.3 Thing Model: confirm from newly parsed feedback
parsedParams must come from the current valid message, not cached telemetry or the requested target value. Check port, length, address, function code, and applicable checksum before reaching this branch.
// Use the same rpcName as the sending RPC.
// Example: parsedParams = { target_value: 30 }; number/string types must match.
return Utils.paraCheck(rpcName, device, parsedParams, { version: 3 });A branch dedicated to parameter feedback can return this result directly:
| Output field | Purpose |
|---|---|
process_data | Persist the complete new transaction state |
telemetry_data | Fill business values after successful confirmation |
shared_attrs / server_attrs | Fill business attributes according to explicit configuration |
actions | Existing retry actions or Class C queue-clear actions |
Missing or terminal records return {}. Do not extract only telemetry_data; that would lose state or cancellation actions.
If the same model also returns normal samples, merge each result group only where there are no conflicting field names, preserving both action lists in order. Do not overwrite entire telemetry/attribute groups using { ...baseResult, ...checkResult }, or republish cached old samples. The proposed Utils.mergeParaResult() is not implemented and must not be called.
12.4 Transparent feedback: check frames with checkInfo
Real devices must retain validated frameInfo, appInfo, and protocol encoding. This example uses a synthetic two-byte big-endian value on port 51. It is not a Modbus frame and is not suitable for direct deployment to field devices.
Sending RPC:
// Teaching protocol: both downlink and echo contain one uint16be value.
const value = params.target_value;
if (!Number.isInteger(value) || value < 0 || value > 65535) {
throw new TypeError('target_value must be an integer from 0 to 65535');
}
const paraDownBuffer = Buffer.alloc(2);
paraDownBuffer.writeUInt16BE(value, 0);
return Utils.makeParaSetMSG({
version: 3,
device,
rpcName,
msgType: Utils.msgType.transParent,
params: { target_value: value },
paraDownBuffer,
checkInfo: {
frameInfo: { port: 51, dataLen: 2 },
appInfo: [
{ field_name: 'target_value', index: 0, type: 'uint16be', val: value }
]
},
telemetryFields: ['target_value']
});Receiving Thing Model branch:
return Utils.paraCheck(
Utils.msgType.transParent,
device,
{ device, msg },
{ version: 3 }
);msg is the newly received platform message. Its Base64 payload is msg.userdata.payload, and its port is msg.userdata.port. Filter actual sub-device address, device identity, and frame validity according to the device protocol before entering this branch.
Transparent requests continue to share one transParent slot; a new command replaces that slot. Do not use it for multiple independent parallel transactions. The state cannot strictly distinguish a late identical echo from two identical commands.
Fill telemetry and shared/server attributes together
Add explicit mappings to a valid checkInfo, for example:
const checkInfo = {
frameInfo: { port: 51, dataLen: 2 },
appInfo: [
{ field_name: 'target_value', index: 0, type: 'uint16be', val: value }
],
telemetry_data: { target_value: value },
shared_attrs: { target_value: value }
};Pass this object to makeParaSetMSG(). Fills occur only after matching feedback. Configure server_attrs in the same way for business attributes, without using reserved helper state keys.
checkInfo.telemetry_data takes precedence over telemetryFields; null explicitly disables telemetry. Do not use only { telemetry_data: null } as a complete checkInfo: an object selects frame checking and still requires valid nonempty appInfo.
12.5 Class C: retain scheduled retries and cancel on confirmation
Keep the existing mechanism; no timer job or new retry scheduler is required. maxRetries counts retries beyond the first send, so maxRetries: 2 constructs at most three parameter downlink actions.
timeout controls both the retry interval at queue construction and the transaction's confirmation lifetime. To cover later packets, adjust the transaction in the not-yet-submitted queue with the existing update API:
// Choose timings for the actual link; this does not establish radio transmission times.
const queue = Utils.makeParaSetMSG({
version: 3,
device,
rpcName,
msgType: Utils.msgType.paras,
params: { target_value: params.target_value },
paraDownBuffer,
telemetryFields: ['target_value'],
classMode: 'ClassC',
timeout: 10000,
maxRetries: 2
});
const stateAction = queue.find(action => action?.type === 'modifyAttrs');
const patch = Utils.updateParaSetInfo({
device: stateAction.dnMsg,
rpcName,
version: 3,
updates: { rpcInfo: { timeout: 60000 } }
});
stateAction.dnMsg = { ...stateAction.dnMsg, ...patch };
return queue;The repeated actions retain intervalms: 10000, while the confirmation window becomes 60000 ms from record creation. This uses the existing API, not a new independent timeout option. For transparent requests, retain the channel and checking configuration from section 12.4 and adjust the queued record in the same way.
Successful confirmation returns an mt_action call with downBufferClear. The device must have the corresponding RPC binding and permission. The request carries no specific command identifier, so cancellation is not scoped precisely to one command. Already transmitted packets cannot be recalled.
Expiration does not invoke the check automatically. A later paraCheck() call may record EXPIRED; getParaSetInfo() neither calculates nor writes expiration. A reader needing timely timeout display should compare against rpcInfo.ms + rpcInfo.timeout and show an unconfirmed outcome, without claiming cancellation has executed.
12.6 Read and update transaction information
Read within an RPC or Thing Model script:
const info = Utils.getParaSetInfo({ device, rpcName, version: 3 });
// Missing: null. Found: a detached { version, rpcInfo, checkState } copy.
// Useful fields: info.rpcInfo.params, info.rpcInfo.timeout, info.checkState.phase.This is a script helper, not an already-created query RPC. Do not return info as a downlink action array. Omit version or pass 2 to read V2. Historical records without a version are interpreted as V2 without writing them back.
Update and submit a patch from an RPC:
const patch = Utils.updateParaSetInfo({
device,
rpcName,
version: 3,
updates: { rpcInfo: { timeout: 60000 } }
});
return [{ sleepTimeMs: 0, type: 'modifyAttrs', dnMsg: patch }];In a Thing Model, return patch or merge its corresponding groups into the current result. Do not mutate the read-only device. Editing a getParaSetInfo() copy does not save it.
| Editable field | Rules |
|---|---|
rpcInfo.timeout | Nonnegative finite number in milliseconds, still measured from the original ms |
rpcInfo.params | Replaces the entire comparison object; does not resend to the device |
rpcInfo.checkInfo | Replaces the entire checking/mapping object; V3 validation applies |
rpcInfo.classMode | ClassA or ClassC |
rpcInfo.telemetryFields | V3 telemetry allowlist |
checkState.maxRetries | Nonnegative integer |
Version, RPC identity, timestamps, phase, and counters cannot be changed. Missing transactions throw instead of being created. Updating a submitted record does not recall or reschedule queued downlinks. Adjusting a terminal record's timeout does not reactivate it.
12.7 Existing V2 calls
Keep the existing sending RPC:
return Utils.makeParaSetMSG({ device, rpcName, params, paraDownBuffer });Retain the old receiving arguments and result conversion:
const result = Utils.paraCheck(rpcName, device.server_attrs, parsedParams);
return {
server_attrs: result.sdata,
telemetry_data: result.tdata,
shared_attrs: result.pdata,
actions: result.actions
};Legacy transparent calls retain the transParent slot and { device, msg } input. Do not switch only the sender to V3 while keeping a V2 receiver. Version changes do not migrate or clean old transactions; pending commands and existing downlink queues require separate handling.
12.8 Integration checks
- Verify that the runtime has the new APIs and both sending and receiving scripts select the same transaction version.
- Preserve device identity, callable RPC identifiers, channel, encoding, and feedback validation. Declare telemetry fields in the receiving Thing Model.
- Return all helper result fields; ensure Class C queue-clear RPC bindings and permissions work.
- Read back the saved version, target values, and state. Check mismatch, success, duplicate, and expiration branches. Process patches are complete transaction snapshots without locks or atomic compare-and-update.
- Preserve persistent, queryable execution logs for reads, updates, exceptions, unchanged results, and asynchronous confirmation. These snippets introduce no logging service;
process_datais not an execution log. - Verify platform acceptance, actual downlink, real feedback, telemetry/MQTT, and UI completion separately. Local documentation and examples do not replace runtime acceptance.
See RPC Model and Thing Model for platform result formats, and tklHelper source for the complete implementation.