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 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 by tklHelper (BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelper) are available as sandbox globals — no import needed.

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

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 ​

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.

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

OptionDefaultDescription
version2Explicit numeric 3 enables V3; pass it on every RPC invocation, including automatic retries.
msgTypeUtils.msgType.parasparas uses port 214; transParent uses port 51. Preserve the device's actual channel.
timeoutClass A: 864000000; Class C: 5000Milliseconds. Transaction confirmation lifetime; also the Class C retry interval when constructing the queue. Section 12.5 adjusts the confirmation window while retaining that interval.
maxRetries3Retries beyond the initial send. Class C schedules them at creation; Class A generates retry actions from its mismatch state.
classModedevice.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 ​

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
device—Current device object.
thingModelId—Thing 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
  };
}

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 ​

LocationV2V3
Sending RPCExisting makeParaSetMSG() callAdd version: 3 and configure confirmation fills as needed
Receiving Thing ModelparaCheck(rpcName, device.server_attrs, data)paraCheck(rpcName, device, data, { version: 3 })
Thing Model resultConvert sdata/tdata/pdata to standard fieldsReturn the standard helper result, or merge each result group with existing parsing output
Transaction storageExisting server_attrs slotsprocess_data.__para_set__<slot>
UI feedbackExisting behaviorConsume 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.

js
// 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.

js
// 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 fieldPurpose
process_dataPersist the complete new transaction state
telemetry_dataFill business values after successful confirmation
shared_attrs / server_attrsFill business attributes according to explicit configuration
actionsExisting 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:

js
// 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:

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

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

js
// 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:

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

js
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 fieldRules
rpcInfo.timeoutNonnegative finite number in milliseconds, still measured from the original ms
rpcInfo.paramsReplaces the entire comparison object; does not resend to the device
rpcInfo.checkInfoReplaces the entire checking/mapping object; V3 validation applies
rpcInfo.classModeClassA or ClassC
rpcInfo.telemetryFieldsV3 telemetry allowlist
checkState.maxRetriesNonnegative 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:

js
return Utils.makeParaSetMSG({ device, rpcName, params, paraDownBuffer });

Retain the old receiving arguments and result conversion:

js
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 ​

  1. Verify that the runtime has the new APIs and both sending and receiving scripts select the same transaction version.
  2. Preserve device identity, callable RPC identifiers, channel, encoding, and feedback validation. Declare telemetry fields in the receiving Thing Model.
  3. Return all helper result fields; ensure Class C queue-clear RPC bindings and permissions work.
  4. 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.
  5. Preserve persistent, queryable execution logs for reads, updates, exceptions, unchanged results, and asynchronous confirmation. These snippets introduce no logging service; process_data is not an execution log.
  6. 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.