MT-EB Integration Guide
Capability Overview
MT-EB is integrated with ThinkLink through ptl-d01 in edgebus-dtu mode. This guide documents the current deployable artifacts and their source-traceable versions.
| Item | Value |
|---|---|
| Project | mt-eb |
| Vendor | MT |
| Model | MT-EB |
| Business Code | Not applicable |
| Integration Mode | edgebus-dtu |
| Protocol | ptl-d01 |
Model catalog
Search for these models in ThinkLink by name or ID.
| Model type | Name | id_name | Platform model ID |
|---|---|---|---|
| RPC | [EB SET] easy para | eb_easy_para_set | 71718097953755141 |
| RPC | [MT ACT] down data | mt_act_dn_data | 28944881661513733 |
| RPC | [MT ACT] sw data down | mt_act_sw_dn | 41526370722910213 |
| RPC | [MT ACT] transparent | mt_act_transparent | 24232175968718853 |
| RPC | [MT ACT] action | mt_action | 24040650571780101 |
| RPC | [MT SET] app any para | mt_app_any_set | 65090370840563717 |
| RPC | [MT SET] uart para | mt_app_uart_set | 41601231033995269 |
| RPC | [MT SET] Class Mode | mt_class_mode_set | 132443389613207552 |
| RPC | [MT ACT] debug modbus | mt_debug_modbus | 55427921770188805 |
| RPC | [MT GET] any para | mt_get_any_para | 68445253878681605 |
| RPC | [MT SET] broad para | mt_set_broad | 41212245631307781 |
| RPC | [MT SET] eb para | mt_set_eb_para | 64665196043440133 |
| RPC | [MT SET] multi devices | mt_set_multi_device | 65014851788869637 |
| RPC | [MT SET] radio para | mt_set_radio_para | 38534049215549445 |
| RPC | [MT SET] sw paras | mt_sw_set_para | 41239002052825093 |
| RPC | [MT SET] period_heart | mt_up_period_set | 64780121331994629 |
| RPC | [MT EB ADR CONFIG] | mt_eb_adr_config | 130598892310786048 |
| Trigger | MT-EB ADR Trigger | mt_eb_adr_trigger | 130598893220950016 |
| Template | MT-EB-BATTERY | 5 | |
| Template | MT-EB-POWER | 10 | |
| Thing Model | MT-EB | mt_eb | 24220289923551238 |
| Thing Model | MT-EB-Emeter | mt_eb_emeter | 23052087352889349 |
| Thing Model | MT-EB-MULTIDEVICE | mt_eb_multi_device | 24570740992905221 |
Operations Skill
Use mt-eb-operations to query or set EB/UART/radio parameters, change LoRaWAN Class, issue transparent or Modbus actions, or diagnose EB ADR. It distinguishes current from deprecated RPCs and requires device-response or telemetry readback.
Key new and compatibility RPCs
| RPC | Key parameters | Behavior and verification |
|---|---|---|
mt_class_mode_set | class_mode: ClassA or ClassC | Sends CIO_MS on port 214 and writes shared_attrs.class_mode after queueing. Use only with ManThink firmware that supports CIO_MS; verify the actual class from later uplink/downlink timing. |
mt_eb_adr_config | lorawan_region, dr_min, dr_max | Writes server_attrs.eb_adr only; it does not immediately downlink. Regions are CN470, AS923, EU868, US915, AU915, KR920, and IN865. Later uplinks let mt_eb_adr_trigger call mt_set_eb_para. |
eb_easy_para_get has been removed. eb_easy_para_set and mt_up_period_set are deprecated. Use mt_action/mt_get_any_para for reads, mt_set_eb_para/mt_app_any_set for writes, and the period_heart variant of mt_set_eb_para for heartbeat period.
Features and Application Scope
This project covers MT-EB through ptl-d01. Its capability boundary, fields, and deployable models follow the current descriptor, requirements, and model sources.
EB ADR versus External ADR
- `mt_eb_adr_config` + `mt_eb_adr_trigger` implement EB parameter ADR. For CN470, AS923, and EU868, DR5 is the fixed highest target when `SNR > 0 dB`; the retained legacy `dr_min`/`dr_max` fields no longer cap that target.
- `platform/tkl/mt-adr` implements external LoRaWAN MAC ADR. Its `mt_adr_engine` evaluates link margin and sends `LinkADRReq`; it does not write the EB parameter register.
- ThinkLink owns only the persistent MT-EB configured `DR`; device-native ADR owns the actual per-frame uplink `datr`. A difference between them is not a configuration failure. The trigger reads configured DR from the current thing-model telemetry first and falls back to `shared_attrs.DR`. When configured DR is already 5, it remains silent even if native ADR uses another runtime DR.
- Missing, malformed, or region-invalid configured DR, absent/incomplete trigger configuration, `SNR <= 0 dB`, and unrecognized uplinks fail safe. Echo confirmation and bounded retry remain the responsibility of `mt_set_eb_para`/`paraCheck`.
To enable EB ADR, run `[MT EB ADR CONFIG]` with valid `lorawan_region`, `dr_min`, and `dr_max`, then ensure the template includes `MT-EB ADR Trigger`. This policy applies to CN470, EU868, and AS923; the legacy range does not need to include DR5.
Deployment and Data Acquisition
| DTU | Not applicable | | Power | battery | | LoRaWAN Class | A | | Interface | LoRaWAN | | Baud Rate | Not applicable |
Telemetry and Register Definitions
| Field | Name | Type | Unit | Address/Source | Description |
|---|---|---|---|---|---|
| datr | Uplink Data Rate | string | LoRaWAN uplink metadata | Actual uplink rate selected by device-native ADR; used only to validate a recognized uplink, not to infer configured-DR rollback | |
| snr | SNR | number | dB | LoRaWAN uplink metadata | Current uplink SNR used by EB ADR to decide whether to set DR5 |
| BzType | BzType | uint16LE | port 209/213/201 | Source field: BzType | |
| BzVersion | BzVersion | uint8 | port 209/213/201 | Source field: BzVersion | |
| HwType | HwType | bitLE | port 209/201 | 40=OM422, 51=OM822 | |
| HwVersion | HwVersion | bitLE | port 209/201 | Source field: HwVersion | |
| SwVersion | SwVersion | uint8 | port 209/201 | Source field: SwVersion | |
| chip_temperature | chip_temperature | uint16LE | °C | port 209 | Source field: chip_temperature |
| chip_voltage | chip_voltage | uint8 | V | port 209 | Source field: chip_voltage |
| BaudRate | BaudRate | uint8 | bps | port 201 | Source field: BaudRate |
| DataBits | DataBits | bitLE | port 201 | Source field: DataBits | |
| StopBits | StopBits | bitLE | port 201 | Source field: StopBits | |
| Checkbit | Checkbit | bitLE | port 201 | 0=none, 1=odd, 2=even | |
| Battery | Battery | bitLE | port 201 | Source field: Battery |
Parameter Definitions
| Parameter | Name | Type | Default | Range | Unit | Description |
|---|---|---|---|---|---|---|
| app_params | app_params | mixed | — | Source parameter: app_params | ||
| fw_params | fw_params | mixed | — | Source parameter: fw_params | ||
| cf_params | cf_params | mixed | — | Source parameter: cf_params | ||
| radio_params | radio_params | mixed | — | Source parameter: radio_params | ||
| ds_params | ds_params | mixed | — | Source parameter: ds_params |
Requirements Evidence
The following key facts were extracted from the requirements source; line references support verification.
| Category | Source Fact | Source |
|---|---|---|
| Protocol/Data | - Description: EdgeBus DTU MT-EB DTU 51/201/209/213 214 automatic detection OM422/OM822 | requirement/requirements.md:11 |
| Deployment | - Power: battery | requirement/requirements.md:17 |
| Protocol/Data | - Protocol: ptl-d01 | requirement/requirements.md:19 |
| Protocol/Data | - Interface: LoRaWAN | requirement/requirements.md:20 |
| Protocol/Data | - Baud rate: Not applicable | requirement/requirements.md:21 |
| Protocol/Data | Field Name Type Unit Address/Source Description | requirement/requirements.md:25 |
| Protocol/Data | BzType BzType uint16LE port 209/213/201 Source field: BzType | requirement/requirements.md:27 |
| Protocol/Data | BzVersion BzVersion uint8 port 209/213/201 Source field: BzVersion | requirement/requirements.md:28 |
| Protocol/Data | HwType HwType bitLE port 209/201 40=OM422, 51=OM822 | requirement/requirements.md:29 |
| Protocol/Data | HwVersion HwVersion bitLE port 209/201 Source field: HwVersion | requirement/requirements.md:30 |
| Protocol/Data | SwVersion SwVersion uint8 port 209/201 Source field: SwVersion | requirement/requirements.md:31 |
| Protocol/Data | chip_temperature chip_temperature uint16LE °C port 209 Source field: chip_temperature | requirement/requirements.md:32 |
| Protocol/Data | chip_voltage chip_voltage uint8 V port 209 Source field: chip_voltage | requirement/requirements.md:33 |
| Protocol/Data | BaudRate BaudRate uint8 bps port 201 Source field: BaudRate | requirement/requirements.md:34 |
| Protocol/Data | DataBits DataBits bitLE port 201 Source field: DataBits | requirement/requirements.md:35 |
| Protocol/Data | StopBits StopBits bitLE port 201 Source field: StopBits | requirement/requirements.md:36 |
| Protocol/Data | Checkbit Checkbit bitLE port 201 0=none, 1=odd, 2=even | requirement/requirements.md:37 |
| Protocol/Data | Battery Battery bitLE port 201 Source field: Battery | requirement/requirements.md:38 |
| Protocol/Data | mt_eb port number Port | requirement/requirements.md:45 |
| Protocol/Data | mt_eb BaudRate number Baud Rate | requirement/requirements.md:50 |
| Deployment | mt_eb pwron_delay number Power-on Delay | requirement/requirements.md:56 |
| Protocol/Data | mt_act_dn_data port number Port 12 | requirement/requirements.md:85 |
| Protocol/Data | mt_app_any_set addr number Modbus Address true | requirement/requirements.md:93 |
| Protocol/Data | mt_app_uart_set BaudRate number Baud Rate — bps | requirement/requirements.md:96 |
Thing Model and RPC Code
RPC: [EB SET] easy para
id_name: eb_easy_para_set · ID: 71718097953755141
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef = {}
let paraInput={...params}
let frames=RPCHelper.buildFrame({
paraDef:paraDef,
params:paraInput});
let redoBuffer=RPCHelper.redo()
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length+redoBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
redoBuffer.copy(dnBuffer,frames.writeBuffer.length+frames.readBuffer.length);
logger.info("set easy para")
let msgServerAttrs={
sleepTimeMs: 0,
type:"modifyAttrs",
dnMsg: {
server_attrs: {
eb_easy_para_set: {
ms:Date.now(),
params:params
}
}
}
}
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras,
device:device,
dnBuffer:dnBuffer,
sleepTime:0,
})
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT ACT] down data
id_name: mt_act_dn_data · ID: 28944881661513733
import {BufferTKL} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let datastr=params.data.replaceAll(" ", "")
if ((datastr.length%2)===1) { datastr='0'+datastr;}
let downData=Buffer.from(datastr,'hex').toString("base64");
logger.info("down data")
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
lorawan_down:{
rpc:"mt_act_dn_data",
ms:Date.now(),
port:params.port,
confirmed:params.confirmed,
down_data:downData,
params:params
}
}
}
}
let msg={
sleepTimeMs: 0,
dnMsg: {
"version":"3.0",
"type":params.type,
"if":"loraWAN",
"moteeui": device.eui,
"token": new Date().getTime(),
"userdata":{
"confirmed":params.confirmed,
"fpend":false,
"port":params.port,
"TxUTCtime":"",
"payload":downData,
"dnWaitms":0,
"type":"data",
"intervalms":0,
"specify": {
"gweui": params?.gweui==="auto"?"":params?.gweui
}
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: { data:"01 02 03", port:2, confirmed:true, type:"data" },
alarms: [],
logger: console,
org_params: {}
})RPC: [MT ACT] sw data down
id_name: mt_act_sw_dn · ID: 41526370722910213
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
if (device.shared_attrs?.swBW==undefined || device.shared_attrs?.swSF==undefined ||
device.shared_attrs?.swFreq==undefined || device.shared_attrs?.swPeriod==undefined ){
logger.warn("no sw paras in shared_attrs and read first");
let frame=RPCHelper.paraRead("cf","hexbe8",60);
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras, device:device, dnBuffer:frame, sleepTime:0,
})
return [msg]
}
let rawCmdBuf = Buffer.from(params.data.replaceAll(" ", ""), "hex")
if (rawCmdBuf.length<1) { return null }
logger.info("sw down data",{params:{
swFreq:device.shared_attrs.swFreq,
swPeriod:device.shared_attrs.swPeriod,
swSF:device.shared_attrs.swSF,
swBW:device.shared_attrs.swBW
}})
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.swDown,
device:device,
dnBuffer:rawCmdBuf,
sleepTime:0,
})
if (params.gwEui!=undefined && params.gwEui!="auto"){
msg.dnMsg.userdata.specify.gwEui=params.gwEui
}
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
lora_sw_down:{
rpc:"mt_act_sw_dn",
ms:Date.now(),
port:msg.dnMsg.port,
down_data:msg.dnMsg.userdata.payload,
swFreq:device.shared_attrs.swFreq,
swPeriod:device.shared_attrs.swPeriod,
swSF:device.shared_attrs.swSF,
swBW:device.shared_attrs.swBW,
params:params
}
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT ACT] transparent
id_name: mt_act_transparent · ID: 24232175968718853
import {BufferTKL} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
logger.info("transparent")
let datastr=params.payload.replaceAll(" ", "")
if ((datastr.length%2)===1) { datastr='0'+datastr;}
let downData=Buffer.from(datastr,'hex').toString("base64");
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
transparent:{
rpc:"mt_act_transparent",
ms:Date.now(),
down_data:downData,
params:params
}
}
}
}
let msg={
sleepTimeMs: 0,
dnMsg: {
"version":"3.0",
"type":"data",
"if":"loraWAN",
"moteeui": device.eui,
"token": new Date().getTime(),
"userdata":{
"confirmed":false,
"fpend":false,
"port":51,
"TxUTCtime":"",
"payload":downData,
"dnWaitms":0,
"type":"data",
"intervalms":0
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: { payload:"01 02 03" },
alarms: [],
logger: console,
org_params: {}
})RPC: [MT ACT] action
id_name: mt_action · ID: 24040650571780101
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let frame=""
if (params.action==="swDataDown") {
if (device.shared_attrs?.swBW==undefined || device.shared_attrs?.swSF==undefined ||
device.shared_attrs?.swFreq==undefined || device.shared_attrs?.swPeriod==undefined ){
logger.warn("no sw paras in shared_attrs and read first");
frame=RPCHelper.paraRead("cf","hexbe8",60);
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras, device:device, dnBuffer:frame, sleepTime:0,
})
return [msg]
}
let rawCmdBuf = Buffer.from("C70403123456",'hex');
logger.info("sw down data")
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.swDown,
device:device,
dnBuffer:rawCmdBuf,
sleepTime:0,
})
if (params.gwEui!=undefined && params.gwEui!="auto"){
msg.dnMsg.userdata.specify.gwEui=params.gwEui
}
return [msg]
}else if (params.action==="downBufferClear") {
let msg=RPCHelper.makeDnDataClear()
return [msg]
}
switch(params.action) {
case "swPara": frame=RPCHelper.paraRead("cf","hexbe8",60); break;
case "uartPara": frame=RPCHelper.paraRead("app","hexbe2",12); break;
case "reset": frame=RPCHelper.reset();break;
case "redo" : frame=RPCHelper.redo();break;
case "join" : frame=RPCHelper.join();break;
case "app": frame=RPCHelper.paraRead("app","hexbe70",0); break;
case "radio" :frame=RPCHelper.paraRead("radio","hexbe90",0); break;
case "cf": frame=RPCHelper.paraRead("cf","hexbe68",0); break;
case "ds": frame=RPCHelper.paraRead("ds","hexbe16",0); break;
case "multiDevice":frame=RPCHelper.paraRead("app","hexbe100",60); break;
// case "modbusRead": frame=RPCHelper.buildmodbusFrameRead(params.addr,params.code,params.reg,params.count)
}
logger.info("action", {params: params})
if (frame.length===0) { return null}
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras, device:device, dnBuffer:frame, sleepTime:0,
})
return [msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] app any para
id_name: mt_app_any_set · ID: 65090370840563717
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let frame=""
if (params?.addr==undefined){ return null}
if (params?.type==undefined) { return null}
if (params?.value==undefined) { return null}
let value=Utils.parseVal(params.value)
let writeBuffer=RPCHelper.paraWrite("app",params.type,params.addr,value);
let readBuffer=RPCHelper.paraRead("app",params.type,params.addr,value);
let frameBuffer=Buffer.alloc(writeBuffer.length+readBuffer.length);
writeBuffer.copy(frameBuffer,0)
readBuffer.copy(frameBuffer,writeBuffer.length)
logger.info("set paras", {params: params})
// 下行重发:makeParaSetMSG 记录待设态 + ClassC 预排重发。这是通用低层写(type/addr/value),
// 回读 addr 不映射到固定具名字段,无法 echo 比对,故不接 paraCheck(ClassA 仅单发,ClassC 定时重发)。
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let msgQue=Utils.makeParaSetMSG({
msgType:Utils.msgType.paras,
device:device,
classMode:classMode,
rpcName:"mt_app_any_set",
params:params,
paraDownBuffer:frameBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] uart para
id_name: mt_app_uart_set · ID: 41601231033995269
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let intervalms = classMode === "ClassA" ? 500 : 5000;
let paraDef = {
app_12:{name: "BaudRate", field_name: "BaudRate", unit: "bps", index: 12, type: "uint8", coefficient: 1200},
app_13:{name: "DataBits", field_name: "DataBits", unit: "", index: 13, type: "bitLE0-3"},
app_13_1:{name: "StopBits", field_name: "StopBits", unit: "", index: 13, type: "bitLE4-5"},
app_13_2:{name: "Checkbit", field_name: "Checkbit", unit: "", index: 13, type: "bitLE6-7", options: {0: "none", 1: "odd", 2: "even", 3: "none"}},
};
let writeBuffer=Buffer.from([0xCF,0x04,12,0x02,0x00,0x01])
writeBuffer[4]=params.BaudRate/1200
writeBuffer[5]=params.DataBits|(params.StopBits<<4)|(params.Checkbit<<6);
let readBuffer=Buffer.from([0x8F,0x02,12,0x02]);
//let frames=RPCHelper.buildFrame({paraDef:paraDef,params:params});
logger.info("set uart para")
let resetFrame=RPCHelper.reset();
let dnBuffer=Buffer.alloc(writeBuffer.length+readBuffer.length);
writeBuffer.copy(dnBuffer,0)
readBuffer.copy(dnBuffer,writeBuffer.length)
// 下行重发:SET 帧走 makeParaSetMSG(记录 server_attrs.mt_app_uart_set 待设态 + ClassC 预排重发;
// mt_eb port-214 解析尾部 paraCheck("mt_app_uart_set") 比对回读 BaudRate/DataBits/StopBits/Checkbit
// 解码域——BaudRate 解析器带 coefficient 1200,与入参 bps 同域——命中即停、ClassA 不符重派),随后追加复位帧。
let setParams={
BaudRate:params.BaudRate,
DataBits:params.DataBits,
StopBits:params.StopBits,
Checkbit:params.Checkbit,
}
if (params._retries != null) setParams._retries=params._retries
let msgQue=Utils.makeParaSetMSG({
msgType:Utils.msgType.paras,
device:device,
classMode:classMode,
rpcName:"mt_app_uart_set",
params:setParams,
paraDownBuffer:dnBuffer,
})
let msgRest=RPCHelper.makeMSG({
msgType:Utils.msgType.paras,
device:device,
dnBuffer:resetFrame,
intervalms:intervalms,
sleepTime:0,
})
return [ ...msgQue, msgRest]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT ACT] debug modbus
id_name: mt_debug_modbus · ID: 55427921770188805
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let dnBuffer=RPCHelper.modbusAction(params.addr,params.code,params.regAddr,params.counts)
logger.info("debug")
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.transParent, device:device, dnBuffer:dnBuffer, sleepTime:0,
})
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
transparent:{
rpc:"mt_debug_modbus",
ms:Date.now(),
down_data:msg.dnMsg.userdata.payload,
params:params
}
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: { addr:"01", code:3, regAddr:0, counts:1 },
alarms: [],
logger: console,
org_params: {}
})RPC: [MT GET] any para
id_name: mt_get_any_para · ID: 68445253878681605
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
if (params?.addr==undefined){ return null}
if (params?.type==undefined) { return null}
if (params?.size==undefined) { return null}
let readBuffer=RPCHelper.paraRead(params.type,"hexBE"+params.size.toString(10),params.addr);
logger.info("get paras")
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras, device:device, dnBuffer:readBuffer, sleepTime:0,
})
return [msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] broad para
id_name: mt_set_broad · ID: 41212245631307781
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef = {
cf_2: { name: "mulDevAddr", field_name: "mulDevAddr", unit: "", type: "hexLE4"},
cf_6: { name: "mulNSkey", field_name: "mulNSkey", unit: "", type: "hexBE16" },
cf_22: { name: "mulASkey", field_name: "mulASkey", unit: "", type: "hexBE16"},
};
let frames=RPCHelper.buildFrame({
paraDef:paraDef,params:params});
logger.info("set group board cast", {params: frames.log})
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
// 下行重发:makeParaSetMSG 记录 server_attrs.mt_set_broad 待设态 + ClassC 预排重发,确保组播密钥下发到位。
// 不接 paraCheck echo 校验:组播密钥(mulNSkey/mulASkey)多为只写、回读不返,无法可靠比对(否则每次空跑 maxRetries)。
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let msgQue=Utils.makeParaSetMSG({
msgType:Utils.msgType.paras,
device:device,
classMode:classMode,
rpcName:"mt_set_broad",
params:params,
paraDownBuffer:dnBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] eb para
id_name: mt_set_eb_para · ID: 64665196043440133
import {BufferTKL, RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
const APP14_BITS = [
{ name: "KeepRx", bit: 0 },
{ name: "Battery", bit: 1 },
{ name: "Uart1Used", bit: 2 },
{ name: "TransparentBit", bit: 3 },
{ name: "SwUp", bit: 4 },
{ name: "JoinRst", bit: 5 },
{ name: "PowerCtrl", bit: 6 },
{ name: "Wait60s", bit: 7 },
];
function parseBit(value) {
if (value === true || value === 1 || value === "1" || value === "true") return true;
if (value === false || value === 0 || value === "0" || value === "false") return false;
return null;
}
function buildApp14Value(targetBattery) {
const sharedAttrs = device?.shared_attrs ?? {};
let app14Value = 0;
for (const field of APP14_BITS) {
const currentValue = parseBit(sharedAttrs[field.name]);
if (currentValue == null) {
logger.warn(
"Battery change rejected: refresh EB parameters and ensure the latest value is available for "
+ field.name,
);
return null;
}
const finalValue = field.name === "Battery" ? targetBattery : currentValue;
if (finalValue) app14Value |= (1 << field.bit);
}
return app14Value;
}
// variant 提交为嵌套键值对:params.para = { value: 判别值, <子字段名>: 值 }(子字段名=判别值,KB-28)。
// 下行重发再派发(paraCheck → {method:"mt_set_eb_para", params:{<which>:value}})走扁平入参,故两种都接:
// 有 params.para 走变体;否则取首个非 "_" 键为 which(KB-41:存进 server_attrs 的是扁平标量字段集)。
let which, raw;
if (params?.para?.value != null) {
which = params.para.value;
raw = params.para[which];
} else {
which = Object.keys(params || {}).find(k => !k.startsWith("_"));
raw = which != null ? Reflect.get(params, which) : null;
}
if (which == null || which === "nc") { return null; }
if (raw == null || raw === "nc" || raw === "") { return null; }
let value = which === "Battery"
? parseBit(raw)
: ((typeof raw === "number") ? raw : Utils.parseVal(raw));
if (value == null) { return null; }
function buildFrame(register, type, addr, val) {
let writeBuffer = RPCHelper.paraWrite(register, type, addr, val);
let readBuffer = RPCHelper.paraRead(register, type, addr, val);
let frameBuffer = Buffer.alloc(writeBuffer.length + readBuffer.length);
writeBuffer.copy(frameBuffer, 0);
readBuffer.copy(frameBuffer, writeBuffer.length);
return frameBuffer;
}
let frame = "";
switch (which) {
case "Battery": {
const app14Value = buildApp14Value(value);
if (app14Value == null) return null;
frame = buildFrame("app", "uint8", 14, app14Value);
break;
}
case "ConfirmDuty": frame = buildFrame("app", "uint8", 15, value); break;
case "query_timeout": {
// shared_attrs.query_timeout is already decoded to ms by the PARA parser, and the input unit is ms,
// so the backfilled value and the input are the same ms domain — backfill is safe here like the other params.
let setVal = value;
if (value < 260000) { setVal = value | 0xC000; } else { setVal = value / 1000; }
frame = buildFrame("app", "uint16le", 32, setVal); break;
}
case "retries": frame = buildFrame("app", "uint8", 34, value); break;
case "pwron_delay": frame = buildFrame("app", "uint16le", 38, value); break;
case "period_heart": frame = buildFrame("app", "uint32le", 40, value); break;
case "sub_device_counts": frame = buildFrame("app", "uint8", 60, value); break;
case "dr": frame = buildFrame("ds", "uint8", 0, value); break;
case "swFreq": frame = buildFrame("cf", "uint32le", 62, value); break;
default: return null;
}
if (frame.length === 0) { return null; }
logger.info("set eb para: " + which + "=" + value);
// 下行重发:makeParaSetMSG 记录 server_attrs.mt_set_eb_para 待设态 + ClassC 预排重发;
// mt_eb port-214 解析尾部 paraCheck("mt_set_eb_para") 比对回读 shared_attrs[which](解码域),
// 命中即停、ClassA 不符重派。存扁平 {[which]:value} 对齐解析器字段集(KB-41)。
// 注:query_timeout 因寄存器编码/解码非严格互逆,echo 可能不命中 → 退化为有界重发(maxRetries 后停,不无限)。
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let msgQue = Utils.makeParaSetMSG({
msgType: Utils.msgType.paras,
device: device,
classMode: classMode,
rpcName: "mt_set_eb_para",
params: { [which]: value },
paraDownBuffer: frame,
});
if (msgQue.length === 0) return null;
if (which === "Battery") {
const resetMsg = RPCHelper.makeMSG({
msgType: Utils.msgType.paras,
device: device,
dnBuffer: RPCHelper.reset(),
intervalms: classMode === "ClassA" ? 500 : 5000,
sleepTime: 0,
});
if (resetMsg == null) return null;
return [...msgQue, resetMsg];
}
return msgQue;
}
rpc_script({
device: { server_attrs: { }, shared_attrs: { period_heart: 28800 } },
params: { para: { value: "period_heart", period_heart: 28800 } },
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] multi devices
id_name: mt_set_multi_device · ID: 65014851788869637
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
function processSubAddr(subAddr, subAddrSize, isHex) {
let hexStr=subAddr.replaceAll(" ","")
if (isHex){
if (hexStr.length % 2 === 1) { hexStr = '0' + hexStr; }
}else {
hexStr = parseInt(subAddr, 10).toString(16);
}
const buffer = Buffer.alloc(subAddrSize);
const numBuffer = Buffer.from(hexStr, 'hex');
if (numBuffer.length !==subAddrSize) {
logger.warn('subAddr ' + subAddr + ' exceeds size ' + subAddrSize + ', truncating');
}
numBuffer.copy(buffer, 0, 0,subAddrSize);
return buffer;
}
function build(params) {
let queryIndex = params.query_index || 0;
let subAddrSize = params.sub_addr_size || 1;
let deviceCounts = 0;
let payHex=""
for (let i = 1; i <= 6; i++) {
// 修复 off-by-one:原为 params['sub_addr'+(i+1)] 读 sub_addr2..7,导致模板 sub_addr1
// 永远收不到、sub_addr7 不存在。现读 sub_addr1..6,与模板字段及 shared_attrs 回填对齐。
const subAddr = params['sub_addr' + i];
if(subAddr===undefined) {continue}
const saddr = subAddr.trim().toLowerCase();
if (saddr=== ""||saddr==="nc") { break; }
deviceCounts++;
const payload = processSubAddr(subAddr, subAddrSize, params.isHex);
payHex=payHex+payload.toString("hex")
}
let subDeviceBuffer=Buffer.from(payHex,"hex");
let frameSetSubDevice = Buffer.alloc(6 + subAddrSize * deviceCounts);
frameSetSubDevice[0] = 0xCF;
frameSetSubDevice[1] = 4 + subAddrSize * deviceCounts;
frameSetSubDevice[2] = 65;
frameSetSubDevice[3] = subAddrSize * deviceCounts+2;
frameSetSubDevice.writeUInt8(queryIndex, 4);
frameSetSubDevice.writeUInt8(subAddrSize, 5);
subDeviceBuffer.copy(frameSetSubDevice,6);
let devCountsBuffer = RPCHelper.paraWrite("app","uint8",60,deviceCounts);
let readBuffer=RPCHelper.paraRead("app","hexbe100",60);
let dnBuffer=Buffer.alloc(frameSetSubDevice.length+devCountsBuffer.length+readBuffer.length);
frameSetSubDevice.copy(dnBuffer)
devCountsBuffer.copy(dnBuffer,frameSetSubDevice.length)
readBuffer.copy(dnBuffer,frameSetSubDevice.length+devCountsBuffer.length);
return dnBuffer
}
let dnDataBuffer = build(params);
logger.info("set multi devices", {params: params})
// 下行重发:makeParaSetMSG 记录待设态 + ClassC 预排重发。不接 paraCheck echo 校验:入参含 isHex(表单专用、
// 设备不回读)且 sub_addr 十进制/十六进制与回读零填充格式有歧义,强行比对会每次空跑 maxRetries;
// 故按 ClassC 定时重发为主(与 mt_up_period_set/mt_sw_set_para 同级)。
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let msgQue=Utils.makeParaSetMSG({
msgType:Utils.msgType.paras,
device:device,
classMode:classMode,
rpcName:"mt_set_multi_device",
params:params,
paraDownBuffer:dnDataBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] radio para
id_name: mt_set_radio_para · ID: 38534049215549445
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef = {
radio_0: { order: 0, name: "RX2Frequency", field_name: "dn2Freq", unit: "MHz", type: "uint32le" },
radio_4: { order: 1, name: "DLSetting", field_name: "DLSetting", unit: "", type: "hexbe1" },
radio_5: { order: 2, name: "channelMap", field_name: "channelMap", unit: "", type: "hexle2" },
radio_7: { order: 3, name: "power", field_name: "power", unit: "dBm", type: "int8" },
radio_8: { order: 4, name: "Freq1", field_name: "Freq1", unit: "MHz", type: "uint32le" },
radio_12: { order: 5, name: "DRRange1", field_name: "DRRange1", unit: "", type: "hexbe1" },
radio_13: { order: 6, name: "Freq2", field_name: "Freq2", unit: "MHz", type: "uint32le" },
radio_17: { order: 7, name: "DRRange2", field_name: "DRRange2", unit: "", type: "hexbe1" },
radio_18: { order: 8, name: "Freq3", field_name: "Freq3", unit: "MHz", type: "uint32le" },
radio_22: { order: 9, name: "DRRange3", field_name: "DRRange3", unit: "", type: "hexbe1" },
radio_23: { order: 10, name: "Freq4", field_name: "Freq4", unit: "MHz", type: "uint32le" },
radio_27: { order: 11, name: "DRRange4", field_name: "DRRange4", unit: "", type: "hexbe1" },
radio_28: { order: 12, name: "Freq5", field_name: "Freq5", unit: "MHz", type: "uint32le" },
radio_32: { order: 13, name: "DRRange5", field_name: "DRRange5", unit: "", type: "hexbe1" },
radio_33: { order: 14, name: "Freq6", field_name: "Freq6", unit: "MHz", type: "uint32le" },
radio_37: { order: 15, name: "DRRange6", field_name: "DRRange6", unit: "", type: "hexbe1" },
radio_38: { order: 16, name: "Freq7", field_name: "Freq7", unit: "MHz", type: "uint32le" },
radio_42: { order: 17, name: "DRRange7", field_name: "DRRange7", unit: "", type: "hexbe1" },
radio_43: { order: 18, name: "Freq8", field_name: "Freq8", unit: "MHz", type: "uint32le" },
radio_47: { order: 19, name: "DRRange8", field_name: "DRRange8", unit: "", type: "hexbe1" },
radio_48: { order: 20, name: "Freq9", field_name: "Freq9", unit: "MHz", type: "uint32le" },
radio_52: { order: 21, name: "DRRange9", field_name: "DRRange9", unit: "", type: "hexbe1" },
radio_53: { order: 22, name: "Freq10", field_name: "Freq10", unit: "MHz", type: "uint32le" },
radio_57: { order: 23, name: "DRRange10", field_name: "DRRange10", unit: "", type: "hexbe1" },
radio_58: { order: 24, name: "Freq11", field_name: "Freq11", unit: "MHz", type: "uint32le" },
radio_62: { order: 25, name: "DRRange11", field_name: "DRRange11", unit: "", type: "hexbe1" },
radio_63: { order: 26, name: "Freq12", field_name: "Freq12", unit: "MHz", type: "uint32le" },
radio_67: { order: 27, name: "DRRange12", field_name: "DRRange12", unit: "", type: "hexbe1" },
radio_68: { order: 28, name: "Freq13", field_name: "Freq13", unit: "MHz", type: "uint32le" },
radio_72: { order: 29, name: "DRRange13", field_name: "DRRange13", unit: "", type: "hexbe1" },
radio_73: { order: 30, name: "Freq14", field_name: "Freq14", unit: "MHz", type: "uint32le" },
radio_77: { order: 31, name: "DRRange14", field_name: "DRRange14", unit: "", type: "hexbe1" },
radio_78: { order: 32, name: "Freq15", field_name: "Freq15", unit: "MHz", type: "uint32le" },
radio_82: { order: 33, name: "DRRange15", field_name: "DRRange15", unit: "", type: "hexbe1" },
radio_83: { order: 34, name: "Freq16", field_name: "Freq16", unit: "MHz", type: "uint32le" },
radio_87: { order: 35, name: "DRRange16", field_name: "DRRange16", unit: "", type: "hexbe1" }
};
function getFreq(value){
if (!value) {return null }
let freq=value.replaceAll(" ","").split(",")
if (freq.length!==4){
return null
}
let drRangeL=parseInt(freq[1])
let drRangeH=parseInt(freq[2])
return {
freq: Number(freq[0]*1000000),
drRange: drRangeL|(drRangeH<<4),
enable:JSON.parse(freq[3]),
}
}
const hasEnabledChannel = Object.entries(params).some(([key, val]) => {
if (!key.startsWith("freq")) return false;
const info = getFreq(val);
return info && info.enable;
});
if (!hasEnabledChannel) {
logger.error("set radio para rejected: all channels disabled (channelMap=0)");
return null;
}
// RPCHelper.buildFrame calls RPCHelper._preDealParams (hardcoded, not subclass override),
// so freqN composites and dn2Freq unit conversion must be done here before buildFrame.
const frameParams = {};
let channelMap = 0;
Object.entries(params).forEach(([key, val]) => {
if (!key.startsWith("freq")) return;
const info = getFreq(val);
if (!info) return;
const fIndex = parseInt(key.substring(4));
if (!info.enable) return;
frameParams["Freq" + fIndex.toString()] = info.freq;
frameParams["DRRange" + fIndex.toString()] = info.drRange.toString(16);
channelMap |= 1 << (fIndex - 1);
});
frameParams.channelMap = channelMap.toString(16);
// BUG-058: the device's power register holds dBm+2, and the mt_eb parser undoes it on
// readback (pradio.power -= 2). Write dBm+2 here so the echoed register decodes back to
// params.power → paraCheck (archived params.power vs decoded echo) converges.
if (params.power != null) frameParams.power = params.power + 2;
if (params.dn2Freq != null) frameParams.dn2Freq = Math.round(params.dn2Freq * 1000000); // MHz→Hz
if (params.DLSetting != null) frameParams.DLSetting = params.DLSetting;
let frames=RPCHelper.buildFrame({
paraDef:paraDef,params:frameParams});
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
logger.error("set radio paras downlink", {params: frames.log, hex: dnBuffer.toString("hex").toUpperCase(), len: dnBuffer.length})
// Downlink resend (was a single fire-and-forget makeMSG, no retry): makeParaSetMSG records
// the pending-set state in server_attrs.mt_set_radio_para (counter/params/timeout/classMode)
// and, for ClassC, pre-queues maxRetries retries at intervalms:timeout. The echo-validated
// early-stop + ClassA re-dispatch is driven by Utils.paraCheck("mt_set_radio_para", …) in the
// mt_eb port-214 parser. dnBuffer is write+read, so the device returns a 0x22 radio frame the
// parser decodes; `params` here is the form's freqN composites (already in the decoded
// shared_attrs domain the parser re-synthesizes), so paraCheck's default equality compare
// matches and avoids the KB-41 domain-mismatch trap.
//
// KB-46: Normalize disabled-channel params before storing in server_attrs so paraCheck
// can match the echo. When the form is opened before the device has sent its first
// port-214 frame (e.g. device just powered on), shared_attrs.freqN may be empty and
// the form submits ",,,false" (empty MHz). paraCheck then compares "923.4,0,5,false"
// (echo decoded) vs ",,,false" (stored) → permanent mismatch → EB reinits LoRa on
// every retry → device appears offline.
// Fix: replace empty-MHz disabled channels with the device's actual shared_attrs value;
// if that is also unknown, prefix with _ so paraCheck skips the field entirely.
const normalizedParams = { ...params };
for (const [key, val] of Object.entries(params)) {
if (!key.startsWith("freq")) continue;
const info = getFreq(val);
// info.freq===0 means MHz was empty in the form (e.g. ",,,false")
if (!info || (!info.enable && info.freq === 0)) {
const actual = device?.shared_attrs?.[key];
const actualInfo = getFreq(actual);
if (actualInfo && actualInfo.freq > 0) {
normalizedParams[key] = actual; // use device's real value for paraCheck
} else {
normalizedParams["_" + key] = val; // unknown → skip in paraCheck
delete normalizedParams[key];
}
}
}
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let msgQue=Utils.makeParaSetMSG({
msgType:Utils.msgType.paras,
device:device,
classMode:classMode,
rpcName:"mt_set_radio_para",
params:normalizedParams,
paraDownBuffer:dnBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
// Test 1: empty-MHz disabled channels (KB-46) — should use shared_attrs fallback or _ prefix
const r1 = rpc_script({
device: { server_attrs: {}, shared_attrs: {
class_mode: "ClassA",
freq9: "923.4,0,5,false", freq10: "923.6,0,5,false", freq11: "923.8,0,5,false",
freq12: "924,0,5,false", freq13: "924.2,0,5,false", freq14: "924.4,0,5,false",
freq15: "924.6,0,5,false"
}},
params: {
freq1: "921.8,0,5,true", freq2: "922,0,5,true", freq3: "922.2,0,5,true",
freq4: "922.4,0,5,true", freq5: "922.6,0,5,true", freq6: "922.8,0,5,true",
freq7: "923,0,5,true", freq8: "923.2,0,5,false",
freq9: ",,,false", freq10: ",,,false", freq11: ",,,false", freq12: ",,,false",
freq13: ",,,false", freq14: ",,,false", freq15: ",,,false",
freq16: "924.8,0,5,true",
power: 22, dn2Freq: 923.2, DLSetting: "02"
},
alarms: [], logger: { error: (m,d) => { if(d?.params) { const p=d.params; const bad=Object.entries(p).filter(([k,v])=>k.startsWith('freq')&&v===',,,false'); console.log('KB-46 test1: bad empty-MHz fields remaining:', bad.length===0?'PASS':'FAIL', bad); } }, info:()=>{} }, org_params: {}
});
console.log("KB-46 test1 result:", r1 ? "has output" : "null (FAIL - no enabled channels?)");
// Test 2: normal call
rpc_script({
device: { server_attrs: {}, shared_attrs: { class_mode: "ClassA" } },
params: {
freq1: "921.8,0,5,true", freq2: "922,0,5,true", freq3: "922.2,0,5,true",
freq4: "922.4,0,5,true", freq5: "922.6,0,5,true", freq6: "922.8,0,5,true",
freq7: "923,0,5,true", freq8: "923.2,0,5,false",
freq9: "923.4,0,5,false", freq10: "923.6,0,5,false", freq11: "923.8,0,5,false",
freq12: "924,0,5,false", freq13: "924.2,0,5,false", freq14: "924.4,0,5,false",
freq15: "924.6,0,5,false", freq16: "924.8,0,5,true",
power: 22, dn2Freq: 923.2, DLSetting: "02"
},
alarms: [],
logger: {
error: (msg, data) => {
console.log("[error]", msg);
if (data?.params) console.log(" params:", JSON.stringify(data.params));
if (data?.hex) {
// Verify dn2Freq encoded as 923200000 Hz = 0x370AEB00 → LE: 00 EB 0A 37
const hex = data.hex;
console.log(" hex:", hex);
console.log(" dn2Freq LE bytes in hex:", hex.substring(0, 8), "(expect 00EB0A37 for 923.2MHz)");
console.log(" channelMap 2-byte big-endian in frame (expect bit6=freq7,bit0=freq1 + bit15=freq16)");
}
if (data) console.log(" len:", data.len);
},
info: (msg, data) => console.log("[info]", msg, data)
},
org_params: {}
})RPC: [MT SET] sw paras
id_name: mt_sw_set_para · ID: 41239002052825093
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
const rpcName="mt_sw_set_para";
let paraDef = {
cf_60: { name: "swSF", field_name: "swSF", unit: "", type: "uint8" },
cf_61: { name: "swBW", field_name: "swBW", unit: "", type: "uint8", options: { 6: "62.5kHz", 7: "125kHz", 8: "250kHz", 9: "500kHz" } },
cf_62: { name: "swFreq", field_name: "swFreq", unit: "MHz", type: "uint32le" },
cf_66: { name: "swPeriod", field_name: "swPeriod", unit: "ms",type: "uint8", coefficient: 50 },
};
let paraInput={...params}
switch (params.swBW){
case "62.5kHz": paraInput.swBW=6;break;
case "125kHz": paraInput.swBW=7;break;
case "250kHz": paraInput.swBW=8;break;
case "500kHz": paraInput.swBW=9;break;
}
logger.info("set sw para")
let frames=RPCHelper.buildFrame({
paraDef:paraDef,params:paraInput});
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
let msgQue=Utils.makeParaSetMSG({
device:device,
classMode:classMode,
rpcName:rpcName,
params:params,
paraDownBuffer:dnBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [MT SET] period_heart
id_name: mt_up_period_set · ID: 64780121331994629
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
// rpcName 必须等于注册 id_name "mt_up_period_set"——server_attrs 待设态键、paraCheck 查找键、
// 失败重派 action.method 三者同源;旧值 "mt_period_up_set" 与 id_name 不符会致 ClassA 重派打到不存在的 RPC。
const rpcName="mt_up_period_set"
// app_40 在 mt-eb 上是 period_heart(心跳/周期上报间隔),与解码器 mt_eb.js port-214(app_40→period_heart)
// 及 mt_set_eb_para 的 period_heart 分支同源。曾误标 period_up(沿用自 KS 系列 LoRaWAN 传感器,那里 app_40=上报周期),
// 致 paraCheck 比对回读 dataAfter.period_heart 时找不到 period_up 字段 → echo 永不命中、ClassA 重派至失败(KB-41)。
let paraDef= {
app_40: {name: "period_heart", field_name: "period_heart", unit: "s", type: "uint32LE"},
}
let frames=RPCHelper.buildFrame({
paraDef:paraDef,params:params});
logger.info("set paras")
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
let msgQue=Utils.makeParaSetMSG({
device:device,
classMode:classMode,
rpcName:rpcName,
params:params,
paraDownBuffer:dnBuffer,
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})Thing Model: MT-EB
id_name: mt_eb · ID: 24220289923551238
import {PayloadParser, RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port = msg?.userdata?.port || null;
const uplinkDatr = msg?.moteTx?.datr
?? msg?.userdata?.moteTx?.datr
?? msg?.userdata?.motetx?.datr
?? msg?.userdata?.datr;
let freq=msg?.moteTx?.freq??0
let hwType=device.shared_attrs?.HwType??"OM822"
let swFreq=device.shared_attrs?.swFreq??600000000
let classMode=device.shared_attrs?.class_mode
let share_attrs_change={}
if ( freq<520 && hwType!=="OM422") { share_attrs_change.HwType="OM422" }
if ( freq>520 && hwType!=="OM822") { share_attrs_change.HwType="OM822" }
if ( (msg?.userdata?.class!=undefined)&&(msg?.userdata?.class!==classMode)){
share_attrs_change.class_mode=msg.userdata.class
}
if ( freq<520 && swFreq>520000000 ) {
share_attrs_change.swFreq=477300000
share_attrs_change.swBW="500kHz"
share_attrs_change.swSF=7
share_attrs_change.period=3000
}
if (freq>800 && swFreq<800000000) {
share_attrs_change.swFreq=901300000
share_attrs_change.swBW="500kHz"
share_attrs_change.swSF=7
share_attrs_change.period=3000
}
if (port!==51&&port!==201&&port!==213&&port!==209&&port!==214) {
let checkData=Utils.paraCheck("eb_easy_para_set",device.server_attrs,device.shared_attrs)
if(share_attrs_change=={}) share_attrs_change=null
return {
telemetry_data: uplinkDatr == null ? null : { datr: uplinkDatr },
server_attrs: checkData.sdata,
shared_attrs: share_attrs_change,
actions: checkData.actions,
}
}
let frame213={
port:213, dataLen:28, status:-1, battery:-1, rssi:false,
tagList:[{ index:0, tag:0x81}, { index:1, tag:0}, { index:2, tag:0},]
}
let info213=[
{name: "BzType", field_name: "BzType", unit:"", index: 12, type: "UInt16LE"},
{name: "BzVersion", field_name: "BzVersion", unit:"", index: 14, type: "UInt8"}
]
let frame209={
port:209, dataLen:20, status:-1, battery:-1, rssi:false,
tagList:[{ index:0, tag:0x81}, { index:1, tag:0x21}, { index:2, tag:0x03},]
}
let info209=[
{name: "FuotaVersion", field_name: "FuotaVersion", unit:"", index: 3, type: "bitLE0-3"},
{name: "HwType", field_name: "HwType", unit:"", index: 3, type: "bitLE4-15", options: {40:"OM422",51:"OM822"}},
{name: "HwVersion", field_name: "HwVersion", unit:"", index: 5, type: "bitLE4-7"},
{name: "SwVersion", field_name: "SwVersion", unit:"", index: 6, type: "unit8"},
{name: "BzType", field_name: "BzType", unit:"", index: 7, type: "uint16le"},
{name: "BzVersion", field_name: "BzVersion", unit:"", index: 9, type: "uint8"},
{name: "chip_temperature", field_name: "chip_temperature", unit:"℃", index: 10, type: "uint16le"},
{name: "chip_voltage", field_name: "chip_voltage", unit:"v", index: 12, type: "int8"},
{name: "pwron_delay", field_name: "pwron_delay", unit:"S", index: 13, type: "uint16le"},
{name: "period_heart", field_name: "period_heart", unit:"S", index: 15, type: "uint32le"},
{name: "sub_device_counts", field_name: "sub_device_counts", unit:"", index: 19, type: "uint8"},
]
let frame201={
port:201, dataLen:28, status:-1, battery:-1, rssi:false,
tagList:[{ index:0, tag:0x81}, { index:7, tag:0x00}]
}
let info201 = [
{name: "FuotaVersion", field_name: "FuotaVersion", unit: "", index: 8, type: "bitLE0-3"},
{name: "HwType", field_name: "HwType", unit: "", index: 8, type: "bitLE4-15", options: {40: "OM422", 51: "OM822"}},
{name: "HwVersion", field_name: "HwVersion", unit: "", index: 10, type: "bitLE4-7"},
{name: "SwVersion", field_name: "SwVersion", unit: "", index: 11, type: "uint8"},
{name: "BzType", field_name: "BzType", unit: "", index: 12, type: "uint16le"},
{name: "BzVersion", field_name: "BzVersion", unit: "", index: 14, type: "uint8"},
{name: "FilterMask", field_name: "FilterMask", unit: "", index: 15, type: "bitLE0-2"},
{name: "BaudRate", field_name: "BaudRate", unit: "", index: 16, type: "uint8", coefficient: 1200},
{name: "DataBits", field_name: "DataBits", unit: "", index: 17, type: "bitLE0-3"},
{name: "StopBits", field_name: "StopBits", unit: "", index: 17, type: "bitLE4-5"},
{name: "Checkbit", field_name: "Checkbit", unit: "", index: 17, type: "bitLE6-7"},
{name: "KeepRx", field_name: "KeepRx", unit: "", index: 18, type: "bitLE0-0", options: {0: false, 1: true}},
{name: "Battery", field_name: "Battery", unit: "", index: 18, type: "bitLE1-1", options: {0: false, 1: true}},
{name: "Uart1Used", field_name: "Uart1Used", unit: "", index: 18, type: "bitLE2-2", options: {0: false, 1: true}},
{name: "TransparentBit", field_name: "TransparentBit", unit: "", index: 18, type: "bitLE3-3", options: {0: false, 1: true}},
{name: "SwUp", field_name: "SwUp", unit: "", index: 18, type: "bitLE4-4", options: {0: false, 1: true}},
{name: "JoinRst", field_name: "JoinRst", unit: "", index: 18, type: "bitLE5-5", options: {0: false, 1: true}},
{name: "PowerCtrl", field_name: "PowerCtrl", unit: "", index: 18, type: "bitLE6-6", options: {0: false, 1: true}},
{name: "Wait60s", field_name: "Wait60s", unit: "", index: 18, type: "bitLE7-7", options: {0: false, 1: true}},
{name: "ConfirmDuty", field_name: "ConfirmDuty", unit: "", index: 19, type: "uint8"},
{name: "portPara", field_name: "portPara", unit: "", index: 20, type: "uint8"},
{name: "portTransparent", field_name: "portTransparent", unit: "", index: 21, type: "uint8"},
{name: "RstHours", field_name: "RstHours", unit: "", index: 22, type: "uint16le"},
{name: "TimeOffset", field_name: "TimeOffset", unit: "", index: 24, type: "uint32le"},
];
let radioDefine = {
radio_0: { order: 0, name: "RX2Frequency", field_name: "dn2Freq", unit: "MHz", type: "uint32le" },
radio_4: { order: 1, name: "DLSetting", field_name: "DLSetting", unit: "", type: "hexbe1" },
radio_5: { order: 2, name: "channelMap", field_name: "channelMap", unit: "", type: "hexle2" },
radio_7: { order: 3, name: "power", field_name: "power", unit: "dBm", type: "int8" },
radio_8: { order: 4, name: "Freq1", field_name: "Freq1", unit: "MHz", type: "uint32le" },
radio_12: { order: 5, name: "DRRange1", field_name: "DRRange1", unit: "", type: "hexbe1" },
radio_13: { order: 6, name: "Freq2", field_name: "Freq2", unit: "MHz", type: "uint32le" },
radio_17: { order: 7, name: "DRRange2", field_name: "DRRange2", unit: "", type: "hexbe1" },
radio_18: { order: 8, name: "Freq3", field_name: "Freq3", unit: "MHz", type: "uint32le" },
radio_22: { order: 9, name: "DRRange3", field_name: "DRRange3", unit: "", type: "hexbe1" },
radio_23: { order: 10, name: "Freq4", field_name: "Freq4", unit: "MHz", type: "uint32le" },
radio_27: { order: 11, name: "DRRange4", field_name: "DRRange4", unit: "", type: "hexbe1" },
radio_28: { order: 12, name: "Freq5", field_name: "Freq5", unit: "MHz", type: "uint32le" },
radio_32: { order: 13, name: "DRRange5", field_name: "DRRange5", unit: "", type: "hexbe1" },
radio_33: { order: 14, name: "Freq6", field_name: "Freq6", unit: "MHz", type: "uint32le" },
radio_37: { order: 15, name: "DRRange6", field_name: "DRRange6", unit: "", type: "hexbe1" },
radio_38: { order: 16, name: "Freq7", field_name: "Freq7", unit: "MHz", type: "uint32le" },
radio_42: { order: 17, name: "DRRange7", field_name: "DRRange7", unit: "", type: "hexbe1" },
radio_43: { order: 18, name: "Freq8", field_name: "Freq8", unit: "MHz", type: "uint32le" },
radio_47: { order: 19, name: "DRRange8", field_name: "DRRange8", unit: "", type: "hexbe1" },
radio_48: { order: 20, name: "Freq9", field_name: "Freq9", unit: "MHz", type: "uint32le" },
radio_52: { order: 21, name: "DRRange9", field_name: "DRRange9", unit: "", type: "hexbe1" },
radio_53: { order: 22, name: "Freq10", field_name: "Freq10", unit: "MHz", type: "uint32le" },
radio_57: { order: 23, name: "DRRange10", field_name: "DRRange10", unit: "", type: "hexbe1" },
radio_58: { order: 24, name: "Freq11", field_name: "Freq11", unit: "MHz", type: "uint32le" },
radio_62: { order: 25, name: "DRRange11", field_name: "DRRange11", unit: "", type: "hexbe1" },
radio_63: { order: 26, name: "Freq12", field_name: "Freq12", unit: "MHz", type: "uint32le" },
radio_67: { order: 27, name: "DRRange12", field_name: "DRRange12", unit: "", type: "hexbe1" },
radio_68: { order: 28, name: "Freq13", field_name: "Freq13", unit: "MHz", type: "uint32le" },
radio_72: { order: 29, name: "DRRange13", field_name: "DRRange13", unit: "", type: "hexbe1" },
radio_73: { order: 30, name: "Freq14", field_name: "Freq14", unit: "MHz", type: "uint32le" },
radio_77: { order: 31, name: "DRRange14", field_name: "DRRange14", unit: "", type: "hexbe1" },
radio_78: { order: 32, name: "Freq15", field_name: "Freq15", unit: "MHz", type: "uint32le" },
radio_82: { order: 33, name: "DRRange15", field_name: "DRRange15", unit: "", type: "hexbe1" },
radio_83: { order: 34, name: "Freq16", field_name: "Freq16", unit: "MHz", type: "uint32le" },
radio_87: { order: 35, name: "DRRange16", field_name: "DRRange16", unit: "", type: "hexbe1" }
};
let fwDefine = {
fw_79:{order:1,name: "reTx", field_name: "reTx", unit:"", type: "uint8"},
// fw_80:{order:2,name: "globalDutyRate", field_name: "globalDutyRate", unit:"", type: "uint8"},
// fw_81:{order:3,name: "dutyBand1", field_name: "dutyBand1", unit:"", type: "uint16le"},
// fw_83:{order:4,name: "dutyBand2", field_name: "dutyBand2", unit:"", type: "uint16le"},
// fw_85:{order:5,name: "dutyBand3", field_name: "dutyBand3", unit:"", type: "uint16le"},
// fw_87:{order:6,name: "dutyBand4", field_name: "dutyBand4", unit:"", type: "uint16le"},
// fw_89:{order:7,name: "devTimeReqDuty", field_name: "devTimeReqDuty", unit:"", type: "uint8"},
fw_90:{order:8,name: "swPublic", field_name: "swPublic", unit:"", type: "bitle0-0"},
}
let cfDefine = {
cf_0:{order:0,name: "sleepWakeEnable", field_name: "sleepWakeEnable", unit:"", type: "bitLE3-3"},
cf_0_1:{order:1,name: "standard", field_name: "standard", unit:"", type: "bitLE4-7",
options: {
0: "EU433/CN470/EU868",
1: "US902",
2: "KR923",
3: "AS923",
4: "AU915",
}
},
cf_1: { name: "Tmode", field_name: "Tmode", unit: "", type: "bit0-0", options: { 0: false, 1: true } },
cf_1_1: { name: "SWRXON", field_name: "SWRXON", unit: "",type: "bit2-2", options: { 0: false, 1: true } },
cf_1_2: { name: "SWTXF", field_name: "SWTXF", unit: "", type: "bit3-3", options: { 0: false, 1: true } },
cf_2: { name: "mulDevAddr", field_name: "mulDevAddr", unit: "", type: "hexLE4"},
cf_6: { name: "mulNSkey", field_name: "mulNSkey", unit: "", type: "hexBE16" },
cf_22: { name: "mulASkey", field_name: "mulASkey", unit: "", type: "hexBE16"},
cf_60: { name: "swSF", field_name: "swSF", unit: "", type: "uint8" },
cf_61: { name: "swBW", field_name: "swBW", unit: "", type: "uint8", options: { 6: "62.5kHz", 7: "125kHz", 8: "250kHz", 9: "500kHz" } },
cf_62: { name: "swFreq", field_name: "swFreq", unit: "Hz", type: "uint32le" },
cf_66: { name: "swPeriod", field_name: "swPeriod", unit: "ms",type: "uint8", coefficient: 50 },
cf_67: { name: "upRepeat", field_name: "upRepeat", unit: "", type: "uint8" }
}
let dsDefine = {
ds_0:{order:0,name: "DR", field_name: "DR", unit:"", type: "uint8"},
ds_1:{order:1,name: "BattLevel", field_name: "BattLevel", unit:"v", type: "uint8"},
ds_2:{order:2,name: "RetryCounts", field_name: "RetryCounts", unit:"", type: "uint8"},
ds_3:{order:3,name: "RSSI", field_name: "RSSI", unit:"", type: "uint8"},
ds_4:{order:4,name: "SNR", field_name: "SNR", unit:"", type: "int8"},
ds_5:{order:5,name: "ChanIndex", field_name: "ChanIndex", unit:"", type: "uint8"},
ds_6:{order:6,name: "RxSlot", field_name: "RxSlot", unit:"", type: "uint8"},
ds_7:{order:7,name: "Rsv", field_name: "Rsv", unit:"", type: "uint8"},
ds_8:{order:8,name: "MacReq", field_name: "MacReq", unit:"", type: "uint16LE"},
ds_10:{order:10,name: "DeviceTime", field_name: "DeviceTime", unit:"", type: "uint32LE"},
ds_14:{order:14,name: "linkMargin", field_name: "linkMargin", unit:"", type: "uint8"},
ds_15:{order:15,name: "linkGWCount", field_name: "linkGWCount", unit:"", type: "uint8"},
}
let appDefine= {
app_0:{name: "FuotaVersion", field_name: "FuotaVersion", unit: "", index: 0, type: "bitLE0-3"},
app_0_1:{name: "HwType", field_name: "HwType", unit: "", index: 0, type: "bitLE4-15", options: {40: "OM422", 51: "OM822"}},
app_2:{name: "HwVersion", field_name: "HwVersion", unit: "", index: 2, type: "bitLE4-7"},
app_3:{name: "SwVersion", field_name: "SwVersion", unit: "", index: 3, type: "uint8"},
app_4:{name: "BzType", field_name: "BzType", unit: "", index: 4, type: "uint16le"},
app_6:{name: "BzVersion", field_name: "BzVersion", unit: "", index: 6, type: "uint8"},
app_10:{name: "minorVersion", field_name: "minorVersion", unit: "", index: 10, type: "uint8"},
app_11:{name: "FilterMask", field_name: "FilterMask", unit: "", index: 11, type: "bitLE0-2"},
app_12:{name: "BaudRate", field_name: "BaudRate", unit: "bps", index: 12, type: "uint8", coefficient: 1200},
app_13:{name: "DataBits", field_name: "DataBits", unit: "", index: 13, type: "bitLE0-3"},
app_13_1:{name: "StopBits", field_name: "StopBits", unit: "", index: 13, type: "bitLE4-5"},
app_13_2:{name: "Checkbit", field_name: "Checkbit", unit: "", index: 13, type: "bitLE6-7"},
app_14:{name: "KeepRx", field_name: "KeepRx", unit: "", index: 14, type: "bitLE0-0", options: {0: false, 1: true}},
app_14_1:{name: "Battery", field_name: "Battery", unit: "", index: 14, type: "bitLE1-1", options: {0: false, 1: true}},
app_14_2:{name: "Uart1Used", field_name: "Uart1Used", unit: "", index: 14, type: "bitLE2-2", options: {0: false, 1: true}},
app_14_3:{name: "TransparentBit", field_name: "TransparentBit", unit: "", index: 14, type: "bitLE3-3", options: {0: false, 1: true}},
app_14_4:{name: "SwUp", field_name: "SwUp", unit: "", index: 14, type: "bitLE4-4", options: {0: false, 1: true}},
app_14_5:{name: "JoinRst", field_name: "JoinRst", unit: "", index: 14, type: "bitLE5-5", options: {0: false, 1: true}},
app_14_6:{name: "PowerCtrl", field_name: "PowerCtrl", unit: "", index: 14, type: "bitLE6-6", options: {0: false, 1: true}},
app_14_7:{name: "Wait60s", field_name: "Wait60s", unit: "", index: 14, type: "bitLE7-7", options: {0: false, 1: true}},
app_15:{name: "ConfirmDuty", field_name: "ConfirmDuty", unit: "", index: 15, type: "uint8"},
app_16:{name: "portPara", field_name: "portPara", unit: "", index: 16, type: "uint8"},
app_17:{name: "portTransparent", field_name: "portTransparent", unit: "", index: 17, type: "uint8"},
app_18:{name: "RstHours", field_name: "RstHours", unit: "h", index: 18, type: "uint16le"},
app_20:{name: "TimeOffset", field_name: "TimeOffset", unit: "", index: 20, type: "uint32le"},
app_24: { name: "battery_base", field_name: "battery_base", unit: "v", index: 24, type: "uint8" }, // 实际值 = raw * 0.01 + 2.0?
app_25: { name: "utc_seconds", field_name: "utc_seconds", unit: "s", index: 25, type: "uint32le" },
app_29: { name: "chip_temperature", field_name: "chip_temperature", unit: "°C", index: 29, type: "uint16le"},
app_31: { name: "chip_voltage", field_name: "chip_voltage", unit: "V", index: 31, type: "uint8"},
app_32: { name: "query_timeout", field_name: "query_timeout", unit: "ms", index: 32, type: "uint16le",},
app_34: { name: "retries", field_name: "retries", unit: "", index: 34, type: "uint8" },
app_35: {name: "uart_calibration", field_name: "uart_calibration", unit: "", index: 35, type: "bitLE0-0"},
app_35_1: {name: "calibration_method", field_name: "calibration_method", unit: "", index: 35, type: "bitLE1-2",
options: {
0: "Internal RC", 1: "External Crystal", 2: "GPS PPS", 3: "Reserved"
}
},
app_36: { name: "calibration_groups", field_name: "calibration_groups", unit: "", index: 36, type: "uint8" },
app_37: { name: "join_interval", field_name: "join_interval", unit: "min", index: 37, type: "uint8" },
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_40: { name: "period_heart", field_name: "period_heart", unit: "s", index: 40, type: "uint32le" },
app_60: { name: "sub_device_counts", field_name: "sub_device_counts", unit: "", index: 60, type: "uint8" },
app_65: { name: "query_index", field_name: "query_index", unit: "", index: 65, type: "uint8" },
app_66: { name: "sub_addr_size", field_name: "sub_addr_size", unit: "", index: 66, type: "uint8" }
}
let pdata={}
let frameInfo={}
let appInfo={}
let paraInfo={}
if (port===51) {
let tdata={}
let payBuffer=Buffer.from(msg?.userdata?.payload,"hex")
tdata.port=port
tdata.payload=payBuffer.toString("hex")
tdata.rssi = msg.gwrx[0].rssi;
tdata.snr = msg.gwrx[0].lsnr;
if (uplinkDatr != null) tdata.datr = uplinkDatr;
if(share_attrs_change=={}) share_attrs_change=null
return {
telemetry_data: tdata,
server_attrs: null,
shared_attrs: share_attrs_change,
};
}
if (port===209) {
frameInfo=frame209
appInfo=info209
}else if(port===213){
frameInfo=frame213
appInfo=info213
}else if (port===201) {
frameInfo=frame201
appInfo=info201
}else if (port===214){
paraInfo=appDefine
}else {
return null
}
if (port===214) {
let fwParser = new PayloadParser({
device: device,
msg: msg,
paraInfo: fwDefine,
})
let pfw=fwParser.paras()
let cfParser=new PayloadParser({
device: device,
msg: msg,
paraInfo: cfDefine,
})
let pcf=cfParser.paras()
let radioParser=new PayloadParser({
device: device,
msg: msg,
paraInfo: radioDefine
})
let pradio=radioParser.paras()
if (pradio) {
if (pradio["power"]!==undefined){ pradio["power"] -= 2;}
if (pradio["dn2Freq"]!==undefined){ pradio["dn2Freq"] /= 1000000;}
for(let i=0;i<16;i++) {
if (pradio["Freq"+(i+1).toString(10)]===undefined) { continue}
pradio["Freq"+(i+1).toString(10)]&=0xFFFFFFFC
pradio["Freq"+(i+1).toString(10)]/=1000000
}
// Compose freq1..16 composite strings ("<MHz>,<drRangeL>,<drRangeH>,<enable>") into
// shared_attrs so the [MT SET] radio para RPC form can backfill the device's CURRENT
// channel plan (mt_set_radio_para fields use default_from:shared_attrs + attr_field_name:"freqN";
// getFreq() reverses this exact format). Without it the form falls back to a fixed factory plan
// and an operator who only tweaks power would resend defaults and clobber the live channel plan.
// enable is set for BOTH active and inactive channels (KB-18): an undefined enable would let a
// form resend silently drop active channels.
if (pradio["channelMap"]!==undefined) {
let chMap = parseInt(pradio["channelMap"], 16);
for (let i = 1; i <= 16; i++) {
// KB-46/freq16: a partial SET echo only reads back the channels it wrote
// (up to the highest enabled channel), so higher channels (e.g. Freq16 when
// only ch8-15 were enabled) are absent from this frame. Fall back to the
// device's last-known shared_attrs Freq/DRRange so EVERY channel's composite
// is refreshed from channelMap — otherwise the absent channel keeps a stale
// enable flag (reads back true after being disabled) and the SET's paraCheck
// never converges (compares stored "...,false" vs stale live "...,true").
let mhz = pradio["Freq" + i];
if (mhz === undefined) { mhz = device?.shared_attrs?.["Freq" + i]; }
let drStr = pradio["DRRange" + i];
if (drStr === undefined) { drStr = device?.shared_attrs?.["DRRange" + i]; }
if (mhz === undefined || drStr === undefined) { continue; }
let drRange = parseInt(drStr, 16);
let drL = drRange & 0x0F;
let drH = (drRange >> 4) & 0x0F;
let enable = ((chMap >> (i - 1)) & 1) === 1;
pradio["freq" + i] = mhz + "," + drL + "," + drH + "," + enable;
}
}
}
let dsParser=new PayloadParser({
device: device,
msg: msg,
paraInfo: dsDefine
})
let pds=dsParser.paras()
let appParser=new PayloadParser({
device: device,
msg: msg,
paraInfo: appDefine
})
let papp=appParser.paras()
pdata = {
...(papp ?? {}),
...(pfw ?? {}),
...(pcf ?? {}),
...(pds ?? {}),
...(pradio ?? {}),
};
}else {
let payParser = new PayloadParser({
device: device,
msg: msg,
frameInfo: frameInfo,
appInfo: appInfo,
})
pdata = payParser.telemetry()
}
if(pdata?.chip_temperature!==undefined){ pdata.chip_temperature=((pdata.chip_temperature - 1000) / 10).toFixed(1);}
if(pdata?.chip_voltage!==undefined){pdata.chip_voltage = ((pdata.chip_voltage*1.6)/254 + 2).toFixed(2);}
if(pdata?.DataBits!==undefined){ if (pdata.DataBits>8) pdata.DataBits=8;}
if(pdata?.battery_base!==undefined){}
if (pdata?.query_timeout!==undefined){
let raw=pdata.query_timeout;
let timeout_unit = (raw >> 14) & 0x03;
let timeout_value = raw & 0x3FFF;
switch(timeout_unit) {
case 0: pdata.query_timeout= timeout_value * 1000; break; // 秒 -> ms
case 1: pdata.query_timeout= timeout_value * 60000; break; // 分钟 -> ms
case 2: pdata.query_timeout= timeout_value * 3600000; break; // 小时 -> ms
case 3: pdata.query_timeout= timeout_value * 86400000; break; // 小时 -> ms
default: pdata.query_timeout= timeout_value; // ms
}
}
if (pdata!=undefined){
if (uplinkDatr != null) pdata.datr = uplinkDatr;
pdata.class_mode=msg?.userdata?.class
if (msg?.gwrx?.[0] ) {
pdata.rssi = msg.gwrx[0].rssi;
pdata.snr = msg.gwrx[0].lsnr;
}
}
let dataAfter={
...(device.shared_attrs),
...(pdata ?? {}),
}
// Echo-validated downlink resend for makeParaSetMSG-based EB SET RPCs: each RPC stored its
// pending-set state under server_attrs[rpcName]; paraCheck compares the stored params against
// this read-back (dataAfter = shared_attrs ∪ decoded pdata), advancing the counter, clearing
// the ClassC downlink buffer on success, or re-dispatching the RPC on mismatch until success
// or maxRetries. Run for every resend-capable RPC and merge — only the one with a live counter
// returns sdata/actions. mt_set_radio_para added 2026-06-20; eb_easy_para_set kept for compat.
let serverOut={}; let actionsOut=[];
// 全部 makeParaSetMSG 类 EB SET RPC 都参与 echo 重发。前 5 个回读字段可净比对→命中即停;
// 后 3 个(mt_set_broad 组播密钥可能只写不回读 / mt_set_multi_device 的 sub_addr 在 mt_eb_multi_device
// 模型解码、本模型只见 counts / mt_app_any_set 通用写无固定具名字段)无法保证命中→退化为有界重发
// (存的是完整表单入参,重派可正确重建;比对失败仅令其重发至 maxRetries 后停,绝不误判成功/不无限)。
for (const rpcName of ["mt_set_radio_para","mt_set_eb_para","mt_app_uart_set","mt_up_period_set","mt_sw_set_para","mt_set_multi_device","mt_set_broad","mt_app_any_set","eb_easy_para_set"]) {
let checkData=Utils.paraCheck(rpcName,device.server_attrs,dataAfter)
if (checkData.sdata) Object.assign(serverOut,checkData.sdata)
if (checkData.actions) actionsOut.push(...checkData.actions)
}
let result = {
telemetry_data: pdata,
server_attrs: Object.keys(serverOut).length?serverOut:null,
shared_attrs: {...pdata,...share_attrs_change},
actions: actionsOut.length?actionsOut:null,
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "mt_eb", name: "MT-EB", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{
"if": "loraWAN",
"gwrx": [
{
"eui": "5a53012501030221",
"chan": 0,
"lsnr": 11.5,
"rfch": 0,
"rssi": -35,
"time": "2026-06-23T04:03:19.803673Z",
"tmms": 0,
"tmst": 65565302,
"ftime": 0
}
],
"type": "data",
"token": 4254031,
"moteTx": {
"codr": "4/5",
"datr": "SF12BW125",
"freq": 923.4,
"modu": "LORA",
"macAck": " [down] 6088e00132200e0098b8b8bb",
"macCmd": ""
},
"geoInfo": {
"type": "gw:wifi",
"accuracy": 50,
"altitude": 0,
"latitude": 34.20034,
"longitude": 108.86365
},
"moteeui": "6353012b00018125",
"version": "3.0",
"userdata": {
"adr": false,
"port": 214,
"class": "ClassA",
"seqno": 24,
"payload": "YgEAYgEAIgoACADqBjcCgH8OIiorKADqBjdQQPcJN1CABA03UMAREDdQAB8TN1BALBY3UIA5GTdQwEYcN1A=",
"confirmed": true
}
},
"1",
{ telemetry_data: true },
{}
))Thing Model: MT-EB-Emeter
id_name: mt_eb_emeter · ID: 23052087352889349
import {BufferTKL, RPCHelper} from '#tklHelper';
import {Buffer} from "buffer";
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let payload = Buffer.from(msg?.userdata?.payload, "base64");
let port=msg?.userdata?.port;
let preTelemetry = device?.telemetry_data?.[thingModelId];
function parseTelemetry(payload){
if (port!=12||payload[0]!=0x21||payload[1]!=0x05) { return {sdevice:null,tdata:null}}
if (payload.length <39) {return {sdevice:null,tdata:null}}
if ((payload[38]&0x01)==0x01) {return {sdevice:null,tdata:null}}
let telemetryData={}
let subDevice=payload.readUInt32LE(2).toString(10).padStart(12, '0')
telemetryData.ua=Number((payload.readUInt16LE(10)/10).toFixed(1))
telemetryData.ub=Number((payload.readUInt16LE(12)/10).toFixed(1))
telemetryData.uc=Number((payload.readUInt16LE(14)/10).toFixed(1))
telemetryData.ia=Number((payload.readUInt16LE(16)/10).toFixed(1))
telemetryData.ib=Number((payload.readUInt16LE(18)/10).toFixed(1))
telemetryData.ic=Number((payload.readUInt16LE(20)/10).toFixed(1))
telemetryData.yz=Number((payload.readUInt32LE(22)/10).toFixed(1))
telemetryData.yf=Number((payload.readUInt32LE(26)/10).toFixed(1))
telemetryData.wz=Number((payload.readUInt32LE(30)/10).toFixed(1))
telemetryData.wf=Number((payload.readUInt32LE(34)/10).toFixed(1))
return {
sdevice: subDevice,
tdata: telemetryData
}
}
let appData= parseTelemetry(payload)
let result = {
sub_device: appData.sdevice,
telemetry_data: appData.tdata,
server_attrs: (appData.sdevice===null)?null:{"sub_addr":appData.sdevice},
shared_attrs: null
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "mt_eb_emeter", name: "MT-EB-Emeter", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model: MT-EB-MULTIDEVICE
id_name: mt_eb_multi_device · ID: 24570740992905221
import {BufferTKL, RPCHelper} from '#tklHelper';
import {Buffer} from "buffer";
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let payload = Buffer.from(msg?.userdata?.payload, "base64");
let port=msg?.userdata?.port || null;
function parseSharedAttrs(payload) {
if (port !== 214 || payload[0] !== 0x2F) { return null; }
let pdata = {};
if (payload.length < 6) { return null; }
let regAddress = payload[2];
if (regAddress!==60) { return null; }
pdata.sub_device_counts=payload[4] //60
pdata.query_index=payload[9] //65
pdata.sub_addr_size=payload[10] //66
let size = payload.length - 11;
for (let i = 0; i < 12; i++) {
if (size<i*pdata.sub_addr_size) { break}
const subBuf = payload.subarray(11+i*pdata.sub_addr_size, 11+i*pdata.sub_addr_size+pdata.sub_addr_size);
switch (i) {
case 0: pdata.sub_addr1 = subBuf.toString('hex'); break;
case 1: pdata.sub_addr2 = subBuf.toString('hex'); break;
case 2: pdata.sub_addr3 =subBuf.toString('hex');break;
case 3: pdata.sub_addr4 = subBuf.toString('hex'); break;
case 4: pdata.sub_addr5 = subBuf.toString('hex'); break;
case 5: pdata.sub_addr6 =subBuf.toString('hex');break;
case 6: pdata.sub_addr7 = subBuf.toString('hex'); break;
case 7: pdata.sub_addr8 = subBuf.toString('hex'); break;
case 8: pdata.sub_addr9 =subBuf.toString('hex');break;
case 9: pdata.sub_addr10 = subBuf.toString('hex'); break;
case 10: pdata.sub_addr11 = subBuf.toString('hex'); break;
case 11: pdata.sub_addr12 =subBuf.toString('hex');break;
default: break;
}
}
if (Object.keys(pdata).length < 1) { return null; }
return pdata;
}
let pdata=parseSharedAttrs(payload)
if (pdata==null) { return null; } // 非 port-214 子设备配置帧:解析器对该模型不适用,返回 null(避免对 null 设 class_mode)
pdata.class_mode=msg?.userdata?.class
let result = {
telemetry_data: pdata,
server_attrs: null,
shared_attrs: pdata
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "mt_eb_multi_device", name: "MT-EB-MULTIDEVICE", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model Field Definitions
| Thing Model | Field | Type | Unit | Name |
|---|---|---|---|---|
| mt_eb | datr | string | Uplink Data Rate | |
| mt_eb | snr | number | dB | SNR |
| mt_eb | port | number | Port | |
| mt_eb | rssi | number | dBm | RSSI |
| mt_eb | BzType | number | Business Type | |
| mt_eb | Battery | number | Battery | |
| mt_eb | payload | string | Payload | |
| mt_eb | BaudRate | number | Baud Rate | |
| mt_eb | Checkbit | number | Parity | |
| mt_eb | DataBits | number | Data Bits | |
| mt_eb | StopBits | number | Stop Bits | |
| mt_eb | BzVersion | number | Business Version | |
| mt_eb | ConfirmDuty | number | Confirm Duty Cycle | |
| mt_eb | pwron_delay | number | Power-on Delay | |
| mt_eb | period_heart | number | Heartbeat Period | |
| mt_eb | sub_device_counts | number | Sub-device Count | |
| mt_eb_emeter | ia | number | A | Phase A Current |
| mt_eb_emeter | ib | number | A | Phase B Current |
| mt_eb_emeter | ic | number | A | Phase C Current |
| mt_eb_emeter | ua | number | V | Phase A Voltage |
| mt_eb_emeter | ub | number | V | Phase B Voltage |
| mt_eb_emeter | uc | number | V | Phase C Voltage |
| mt_eb_emeter | wf | number | kvarh | Reverse Reactive Energy |
| mt_eb_emeter | wz | number | kvarh | Forward Reactive Energy |
| mt_eb_emeter | yf | number | kWh | Reverse Active Energy |
| mt_eb_emeter | yz | number | kWh | Forward Active Energy |
RPC Parameter Definitions
| RPC | Parameter | Type | Description | Default | Unit |
|---|---|---|---|---|---|
| mt_act_dn_data | data | string | Data | 1234 | hex |
| mt_act_dn_data | port | number | Port | 12 | |
| mt_act_dn_data | type | string | Type | data | |
| mt_act_dn_data | gweui | string | Gateway EUI | auto | hex |
| mt_act_dn_data | confirmed | boolean | Confirmed | false | |
| mt_act_sw_dn | data | string | Data | C70403123456 | |
| mt_act_sw_dn | gwEui | string | Gateway EUI | auto | |
| mt_act_transparent | payload | string | Payload | 123456 | hex |
| mt_action | action | string | Action | app | |
| mt_app_any_set | addr | number | Modbus Address | true | |
| mt_app_any_set | type | string | Type | uint8 | |
| mt_app_any_set | value | number | Value | 0 | |
| mt_app_uart_set | BaudRate | number | Baud Rate | — | bps |
| mt_app_uart_set | Checkbit | number | Parity | — | |
| mt_app_uart_set | DataBits | number | Data Bits | — | |
| mt_app_uart_set | StopBits | number | Stop Bits | — | |
| mt_debug_modbus | addr | string | Modbus Address | 1 | |
| mt_debug_modbus | code | string | Modbus Function Code | 03 | |
| mt_debug_modbus | counts | number | Register Count | 1 | |
| mt_debug_modbus | regAddr | string | Register Address | 0000 | |
| mt_get_any_para | addr | string | Modbus Address | 0 | |
| mt_get_any_para | size | number | Size | 30 | |
| mt_get_any_para | type | string | Type | app | |
| mt_set_broad | mulASkey | string | Multicast AppSKey | — | |
| mt_set_broad | mulNSkey | string | Multicast NwkSKey | — | |
| mt_set_broad | mulDevAddr | string | Multicast DevAddr | — | |
| mt_set_eb_para | para | variant | Parameter | period_heart | |
| mt_set_multi_device | isHex | boolean | Is Hex | true | |
| mt_set_multi_device | sub_addr1 | string | Sub-address 1 | nc | |
| mt_set_multi_device | sub_addr2 | string | Sub-address 2 | nc | |
| mt_set_multi_device | sub_addr3 | string | Sub-address 3 | nc | |
| mt_set_multi_device | sub_addr4 | string | Sub-address 4 | nc | |
| mt_set_multi_device | sub_addr5 | string | Sub-address 5 | nc | |
| mt_set_multi_device | sub_addr6 | string | Sub-address 6 | nc | |
| mt_set_multi_device | query_index | number | Query Index | — | |
| mt_set_multi_device | sub_addr_size | number | Sub-address Count | — | |
| mt_set_radio_para | freq1 | string | Frequency 1 | — | |
| mt_set_radio_para | freq2 | string | Frequency 2 | — | |
| mt_set_radio_para | freq3 | string | Frequency 3 | — | |
| mt_set_radio_para | freq4 | string | Frequency 4 | — | |
| mt_set_radio_para | freq5 | string | Frequency 5 | — | |
| mt_set_radio_para | freq6 | string | Frequency 6 | — | |
| mt_set_radio_para | freq7 | string | Frequency 7 | — | |
| mt_set_radio_para | freq8 | string | Frequency 8 | — | |
| mt_set_radio_para | freq9 | string | Frequency 9 | — | |
| mt_set_radio_para | power | number | Power | — | dBm |
| mt_set_radio_para | freq10 | string | Frequency 10 | — | |
| mt_set_radio_para | freq11 | string | Frequency 11 | — | |
| mt_set_radio_para | freq12 | string | Frequency 12 | — | |
| mt_set_radio_para | freq13 | string | Frequency 13 | — | |
| mt_set_radio_para | freq14 | string | Frequency 14 | — | |
| mt_set_radio_para | freq15 | string | Frequency 15 | — | |
| mt_set_radio_para | freq16 | string | Frequency 16 | — | |
| mt_set_radio_para | dn2Freq | number | RX2 Frequency | — | MHz |
| mt_set_radio_para | DLSetting | string | Downlink Setting | — | |
| mt_sw_set_para | swBW | string | LoRa Bandwidth | — | |
| mt_sw_set_para | swSF | number | LoRa Spreading Factor | — | |
| mt_sw_set_para | swPeriod | number | Switch Period | — | ms |
| mt_up_period_set | period_heart | number | Heartbeat Period (s) | — |
Trigger Definitions
No project-specific triggers are defined.
Third-Party Platform Subscription
MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}
{
"eui": "6353012af10a9331",
"active_time": "2026-07-13T08:35:48.000Z",
"thingModelId": "Use the platform model ID from the model inventory",
"thingModelIdName": "Use the current telemetry model id_name",
"telemetry_data": {
"snr": 13.5,
"rssi": -51,
"battery": 3.37,
"BzType": 1,
"BzVersion": 2,
"HwType": 3.75,
"HwVersion": 5,
"SwVersion": 5,
"chip_temperature": 6,
"chip_voltage": 7,
"BaudRate": 8
}
}Template Selection and Verification
Search ThinkLink by project name, business code, or the id_name and ID values in the model inventory. After deployment, use real uplinks to verify fields, units, RPCs, and trigger behavior.
Supplementary Notes
Requirements and decisions: requirement/requirements.md.