Skip to content

MT-EB 平台能力对接文档

能力简介

门思科技 EdgeBus DTU 通用内置物模型(MT-EB),处理所有 DTU 上行遥测(端口 51/201/209/213)和参数查询(端口 214),自动识别 OM422/OM822 硬件型号

项目
项目名称mt-eb
厂商门思科技 (ManThink)
型号MT-EB
业务代码Not applicable
对接模式edgebus-dtu
协议ptl-d01

模型清单

可使用下列名称或 ID 在 ThinkLink 中搜索对应模型。

模型类型名称id_name平台模型 ID
RPC[EB SET] easy paraeb_easy_para_set71718097953755141
RPC[MT ACT] down datamt_act_dn_data28944881661513733
RPC[MT ACT] sw data downmt_act_sw_dn41526370722910213
RPC[MT ACT] transparentmt_act_transparent24232175968718853
RPC[MT ACT] actionmt_action24040650571780101
RPC[MT SET] app any paramt_app_any_set65090370840563717
RPC[MT SET] uart paramt_app_uart_set41601231033995269
RPC[MT SET] Class Modemt_class_mode_set132443389613207552
RPC[MT ACT] debug modbusmt_debug_modbus55427921770188805
RPC[MT GET] any paramt_get_any_para68445253878681605
RPC[MT SET] broad paramt_set_broad41212245631307781
RPC[MT SET] eb paramt_set_eb_para64665196043440133
RPC[MT SET] multi devicesmt_set_multi_device65014851788869637
RPC[MT SET] radio paramt_set_radio_para38534049215549445
RPC[MT SET] sw parasmt_sw_set_para41239002052825093
RPC[MT SET] period_heartmt_up_period_set64780121331994629
RPC[MT EB ADR CONFIG]mt_eb_adr_config130598892310786048
TriggerMT-EB ADR Triggermt_eb_adr_trigger130598893220950016
TemplateMT-EB-BATTERY5
TemplateMT-EB-POWER10
Thing ModelMT-EBmt_eb24220289923551238
Thing ModelMT-EB-Emetermt_eb_emeter23052087352889349
Thing ModelMT-EB-MULTIDEVICEmt_eb_multi_device24570740992905221

运维 Skill

查询或设置 EB/UART/射频参数、修改 LoRaWAN Class、执行透明/Modbus 下行或排查 EB ADR 时,使用 mt-eb-operations。该 Skill 明确当前 RPC 与弃用 RPC 的边界, 并要求设备响应或遥测读回,不能只依据派发结果。

关键新增/兼容 RPC

RPC关键参数行为与验证
mt_class_mode_setclass_mode: ClassAClassC通过 214 端口发送 CIO_MS,并在下行入队后写 shared_attrs.class_mode。仅适用于支持 CIO_MS 的门思固件;还要用后续上下行时序验证设备实际工作类别。
mt_eb_adr_configlorawan_regiondr_mindr_max只写 server_attrs.eb_adr,不立即下发设备。支持 CN470、AS923、EU868、US915、AU915、KR920、IN865;后续由 mt_eb_adr_trigger 判断上行并调用 mt_set_eb_para

eb_easy_para_get 已移除;eb_easy_para_setmt_up_period_set 已弃用。读取请用 mt_action/mt_get_any_para,写入请用 mt_set_eb_para/mt_app_any_set; 心跳周期统一使用 mt_set_eb_paraperiod_heart 变体。

产品特点与适用范围

本项目面向“EdgeBus 内置物模型”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。

EB ADR 与外挂 ADR 的区别

  • 本项目的 `mt_eb_adr_config` + `mt_eb_adr_trigger` 属于 EB 参数 ADR:CN470、AS923、EU868 在 `SNR > 0 dB` 时固定以 DR5 为最高目标;兼容保留的 `dr_min`/`dr_max` 不再限制该目标。
  • `platform/tkl/mt-adr` 属于 外挂 LoRaWAN MAC ADR:由 `mt_adr_engine` 计算链路余量并下发 `LinkADRReq`,不走 EB 参数寄存器。
  • 平台只管理 MT-EB 持久配置 `DR`;设备原生 ADR 管理每帧实际空口 `datr`,两者不同不表示配置失败。触发器优先读取本物模型遥测中的 `DR`,缺失时回退 `shared_attrs.DR`;配置 DR 已为 5 时,即使实际上行是其它 DR,也不重复下发。
  • 配置 DR 缺失、格式错误或不属于当前频段,配置 RPC 未执行或属性不完整,`SNR <= 0 dB`,以及协议格式不匹配时均安全静默。回显确认和有界重试继续由 `mt_set_eb_para`/`paraCheck` 负责。

启用 EB ADR:先执行 `[MT EB ADR CONFIG]`,填写合法的 `lorawan_region`、`dr_min`、`dr_max`;再确保模板包含 `MT-EB ADR Trigger`。本触发规则适用于 CN470、EU868、AS923,旧范围无需包含 DR5。

部署与采集信息

| DTU | Not applicable | | 供电 | battery | | LoRaWAN Class | A | | 接口 | LoRaWAN | | 波特率 | Not applicable |

遥测与寄存器定义

字段名称类型单位地址/来源说明
datr上行数据率stringLoRaWAN 上行元数据设备原生 ADR 决定的实际上行速率,仅用于识别有效上行,不用于判断配置 DR 是否回退
snr信噪比numberdBLoRaWAN 上行元数据当前上行信噪比,供 EB ADR 判断是否设置 DR5
BzType业务类型uint16LEport 209/213/201EB业务代码
BzVersion业务版本uint8port 209/213/201EB业务固件版本
HwType硬件型号bitLEport 209/20140=OM422, 51=OM822
HwVersion硬件版本bitLEport 209/201DTU硬件版本号
SwVersion软件版本uint8port 209/201DTU固件版本
chip_temperature芯片温度uint16LE°Cport 209MCU温度, (raw-1000)/10
chip_voltage芯片电压uint8Vport 209MCU供电电压, raw*1.6/254+2
BaudRate波特率uint8bpsport 201串口波特率 raw*1200
DataBits数据位bitLEport 201串口数据位
StopBits停止位bitLEport 201串口停止位
Checkbit校验位bitLEport 2010=none, 1=odd, 2=even
Battery电池供电bitLEport 201是否电池供电

参数定义

参数名称类型默认值范围单位说明
app_params应用参数mixed
fw_params固件参数mixed
cf_paramsLoRaWAN配置参数mixed
radio_params射频参数mixed
ds_params设备状态参数mixed

Requirements 证据

以下关键事实由 requirements 源文件提取;原始行号用于复核。

类别来源事实来源
协议/数据- Description: 门思科技 EdgeBus DTU 通用内置物模型(MT-EB),处理所有 DTU 上行遥测(端口 51/201/209/213)和参数查询(端口 214),自动识别 OM422/OM822 硬件型号requirement/requirements.md:11
部署- Power: batteryrequirement/requirements.md:17
协议/数据- Protocol: ptl-d01requirement/requirements.md:19
协议/数据- Interface: LoRaWANrequirement/requirements.md:20
协议/数据- Baud rate: Not applicablerequirement/requirements.md:21
协议/数据| Field | Name | Type | Unit | Address/Source | Description |requirement/requirements.md:25
协议/数据| BzType | BzType | uint16LE | | port 209/213/201 | Source field: BzType |requirement/requirements.md:27
协议/数据| BzVersion | BzVersion | uint8 | | port 209/213/201 | Source field: BzVersion |requirement/requirements.md:28
协议/数据| HwType | HwType | bitLE | | port 209/201 | 40=OM422, 51=OM822 |requirement/requirements.md:29
协议/数据| HwVersion | HwVersion | bitLE | | port 209/201 | Source field: HwVersion |requirement/requirements.md:30
协议/数据| SwVersion | SwVersion | uint8 | | port 209/201 | Source field: SwVersion |requirement/requirements.md:31
协议/数据| chip_temperature | chip_temperature | uint16LE | °C | port 209 | Source field: chip_temperature |requirement/requirements.md:32
协议/数据| chip_voltage | chip_voltage | uint8 | V | port 209 | Source field: chip_voltage |requirement/requirements.md:33
协议/数据| BaudRate | BaudRate | uint8 | bps | port 201 | Source field: BaudRate |requirement/requirements.md:34
协议/数据| DataBits | DataBits | bitLE | | port 201 | Source field: DataBits |requirement/requirements.md:35
协议/数据| StopBits | StopBits | bitLE | | port 201 | Source field: StopBits |requirement/requirements.md:36
协议/数据| Checkbit | Checkbit | bitLE | | port 201 | 0=none, 1=odd, 2=even |requirement/requirements.md:37
协议/数据| Battery | Battery | bitLE | | port 201 | Source field: Battery |requirement/requirements.md:38
协议/数据| mt_eb | port | number | | Port |requirement/requirements.md:45
协议/数据| mt_eb | BaudRate | number | | Baud Rate |requirement/requirements.md:50
部署| mt_eb | pwron_delay | number | | Power-on Delay |requirement/requirements.md:56
协议/数据| mt_act_dn_data | port | number | Port | 12 | |requirement/requirements.md:85
协议/数据| mt_app_any_set | addr | number | Modbus Address | true | |requirement/requirements.md:93
协议/数据| mt_app_uart_set | BaudRate | number | Baud Rate | — | bps |requirement/requirements.md:96

物模型与 RPC 代码

RPC: [EB SET] easy para

id_name: eb_easy_para_set · ID: 71718097953755141

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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 },
    {}
))

物模型字段定义

物模型字段类型单位名称
mt_ebdatrstring上行数据率
mt_ebsnrnumberdB信噪比
mt_ebportnumber端口
mt_ebrssinumberdBm信号强度
mt_ebBzTypenumber业务类型
mt_ebBatterynumber电池电量
mt_ebpayloadstring原始报文
mt_ebBaudRatenumber波特率
mt_ebCheckbitnumber校验位
mt_ebDataBitsnumber数据位
mt_ebStopBitsnumber停止位
mt_ebBzVersionnumber业务版本
mt_ebConfirmDutynumber确认占空比
mt_ebpwron_delaynumber上电延时
mt_ebperiod_heartnumber心跳周期
mt_ebsub_device_countsnumber子设备数量
mt_eb_emeterianumberAPhase A Current
mt_eb_emeteribnumberAPhase B Current
mt_eb_emetericnumberAPhase C Current
mt_eb_emeteruanumberVPhase A Voltage
mt_eb_emeterubnumberVPhase B Voltage
mt_eb_emeterucnumberVPhase C Voltage
mt_eb_emeterwfnumberkvarhReverse Reactive Energy
mt_eb_emeterwznumberkvarhForward Reactive Energy
mt_eb_emeteryfnumberkWhReverse Active Energy
mt_eb_emeteryznumberkWhForward Active Energy

RPC 参数定义

RPC参数类型说明默认值单位
mt_act_dn_datadatastringData1234hex
mt_act_dn_dataportnumberPort12
mt_act_dn_datatypestringTypedata
mt_act_dn_datagweuistringGateway EUIautohex
mt_act_dn_dataconfirmedbooleanConfirmedfalse
mt_act_sw_dndatastringDataC70403123456
mt_act_sw_dngwEuistringGateway EUIauto
mt_act_transparentpayloadstringPayload123456hex
mt_actionactionstringActionapp
mt_app_any_setaddrnumberModbus Addresstrue
mt_app_any_settypestringTypeuint8
mt_app_any_setvaluenumberValue0
mt_app_uart_setBaudRatenumberBaud Ratebps
mt_app_uart_setCheckbitnumberParity
mt_app_uart_setDataBitsnumberData Bits
mt_app_uart_setStopBitsnumberStop Bits
mt_debug_modbusaddrstringModbus Address1
mt_debug_modbuscodestringModbus Function Code03
mt_debug_modbuscountsnumberRegister Count1
mt_debug_modbusregAddrstringRegister Address0000
mt_get_any_paraaddrstringModbus Address0
mt_get_any_parasizenumberSize30
mt_get_any_paratypestringTypeapp
mt_set_broadmulASkeystringMulticast AppSKey
mt_set_broadmulNSkeystringMulticast NwkSKey
mt_set_broadmulDevAddrstringMulticast DevAddr
mt_set_eb_paraparavariantParameterperiod_heart
mt_set_multi_deviceisHexbooleanIs Hextrue
mt_set_multi_devicesub_addr1stringSub-address 1nc
mt_set_multi_devicesub_addr2stringSub-address 2nc
mt_set_multi_devicesub_addr3stringSub-address 3nc
mt_set_multi_devicesub_addr4stringSub-address 4nc
mt_set_multi_devicesub_addr5stringSub-address 5nc
mt_set_multi_devicesub_addr6stringSub-address 6nc
mt_set_multi_devicequery_indexnumberQuery Index
mt_set_multi_devicesub_addr_sizenumberSub-address Count
mt_set_radio_parafreq1stringFrequency 1
mt_set_radio_parafreq2stringFrequency 2
mt_set_radio_parafreq3stringFrequency 3
mt_set_radio_parafreq4stringFrequency 4
mt_set_radio_parafreq5stringFrequency 5
mt_set_radio_parafreq6stringFrequency 6
mt_set_radio_parafreq7stringFrequency 7
mt_set_radio_parafreq8stringFrequency 8
mt_set_radio_parafreq9stringFrequency 9
mt_set_radio_parapowernumberPowerdBm
mt_set_radio_parafreq10stringFrequency 10
mt_set_radio_parafreq11stringFrequency 11
mt_set_radio_parafreq12stringFrequency 12
mt_set_radio_parafreq13stringFrequency 13
mt_set_radio_parafreq14stringFrequency 14
mt_set_radio_parafreq15stringFrequency 15
mt_set_radio_parafreq16stringFrequency 16
mt_set_radio_paradn2FreqnumberRX2 FrequencyMHz
mt_set_radio_paraDLSettingstringDownlink Setting
mt_sw_set_paraswBWstringLoRa Bandwidth
mt_sw_set_paraswSFnumberLoRa Spreading Factor
mt_sw_set_paraswPeriodnumberSwitch Periodms
mt_up_period_setperiod_heartnumberHeartbeat Period (s)

触发器定义

无项目专用触发器。

第三方平台数据订阅

MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}

json
{
  "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
  }
}

模板选择与验证

在 ThinkLink 平台按项目名称、业务代码或模型清单中的 id_name、ID 搜索并绑定模型。部署后使用真实上行帧核对字段、单位、RPC 和触发器行为。

补充说明

需求与决策依据:requirement/requirements.md