Skip to content

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.

ItemValue
Projectmt-eb
VendorMT
ModelMT-EB
Business CodeNot applicable
Integration Modeedgebus-dtu
Protocolptl-d01

Model catalog ​

Search for these models in ThinkLink by name or ID.

Model typeNameid_namePlatform model 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

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 ​

RPCKey parametersBehavior and verification
mt_class_mode_setclass_mode: ClassA or ClassCSends 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_configlorawan_region, dr_min, dr_maxWrites 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 ​

FieldNameTypeUnitAddress/SourceDescription
datrUplink Data RatestringLoRaWAN uplink metadataActual uplink rate selected by device-native ADR; used only to validate a recognized uplink, not to infer configured-DR rollback
snrSNRnumberdBLoRaWAN uplink metadataCurrent uplink SNR used by EB ADR to decide whether to set DR5
BzTypeBzTypeuint16LEport 209/213/201Source field: BzType
BzVersionBzVersionuint8port 209/213/201Source field: BzVersion
HwTypeHwTypebitLEport 209/20140=OM422, 51=OM822
HwVersionHwVersionbitLEport 209/201Source field: HwVersion
SwVersionSwVersionuint8port 209/201Source field: SwVersion
chip_temperaturechip_temperatureuint16LE°Cport 209Source field: chip_temperature
chip_voltagechip_voltageuint8Vport 209Source field: chip_voltage
BaudRateBaudRateuint8bpsport 201Source field: BaudRate
DataBitsDataBitsbitLEport 201Source field: DataBits
StopBitsStopBitsbitLEport 201Source field: StopBits
CheckbitCheckbitbitLEport 2010=none, 1=odd, 2=even
BatteryBatterybitLEport 201Source field: Battery

Parameter Definitions ​

ParameterNameTypeDefaultRangeUnitDescription
app_paramsapp_paramsmixed—Source parameter: app_params
fw_paramsfw_paramsmixed—Source parameter: fw_params
cf_paramscf_paramsmixed—Source parameter: cf_params
radio_paramsradio_paramsmixed—Source parameter: radio_params
ds_paramsds_paramsmixed—Source parameter: ds_params

Requirements Evidence ​

The following key facts were extracted from the requirements source; line references support verification.

CategorySource FactSource
Protocol/Data- Description: EdgeBus DTU MT-EB DTU 51/201/209/213 214 automatic detection OM422/OM822requirement/requirements.md:11
Deployment- Power: batteryrequirement/requirements.md:17
Protocol/Data- Protocol: ptl-d01requirement/requirements.md:19
Protocol/Data- Interface: LoRaWANrequirement/requirements.md:20
Protocol/Data- Baud rate: Not applicablerequirement/requirements.md:21
Protocol/DataField Name Type Unit Address/Source Descriptionrequirement/requirements.md:25
Protocol/DataBzType BzType uint16LE port 209/213/201 Source field: BzTyperequirement/requirements.md:27
Protocol/DataBzVersion BzVersion uint8 port 209/213/201 Source field: BzVersionrequirement/requirements.md:28
Protocol/DataHwType HwType bitLE port 209/201 40=OM422, 51=OM822requirement/requirements.md:29
Protocol/DataHwVersion HwVersion bitLE port 209/201 Source field: HwVersionrequirement/requirements.md:30
Protocol/DataSwVersion SwVersion uint8 port 209/201 Source field: SwVersionrequirement/requirements.md:31
Protocol/Datachip_temperature chip_temperature uint16LE °C port 209 Source field: chip_temperaturerequirement/requirements.md:32
Protocol/Datachip_voltage chip_voltage uint8 V port 209 Source field: chip_voltagerequirement/requirements.md:33
Protocol/DataBaudRate BaudRate uint8 bps port 201 Source field: BaudRaterequirement/requirements.md:34
Protocol/DataDataBits DataBits bitLE port 201 Source field: DataBitsrequirement/requirements.md:35
Protocol/DataStopBits StopBits bitLE port 201 Source field: StopBitsrequirement/requirements.md:36
Protocol/DataCheckbit Checkbit bitLE port 201 0=none, 1=odd, 2=evenrequirement/requirements.md:37
Protocol/DataBattery Battery bitLE port 201 Source field: Batteryrequirement/requirements.md:38
Protocol/Datamt_eb port number Portrequirement/requirements.md:45
Protocol/Datamt_eb BaudRate number Baud Raterequirement/requirements.md:50
Deploymentmt_eb pwron_delay number Power-on Delayrequirement/requirements.md:56
Protocol/Datamt_act_dn_data port number Port 12requirement/requirements.md:85
Protocol/Datamt_app_any_set addr number Modbus Address truerequirement/requirements.md:93
Protocol/Datamt_app_uart_set BaudRate number Baud Rate — bpsrequirement/requirements.md:96

Thing Model and RPC Code ​

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

Thing Model Field Definitions ​

Thing ModelFieldTypeUnitName
mt_ebdatrstringUplink Data Rate
mt_ebsnrnumberdBSNR
mt_ebportnumberPort
mt_ebrssinumberdBmRSSI
mt_ebBzTypenumberBusiness Type
mt_ebBatterynumberBattery
mt_ebpayloadstringPayload
mt_ebBaudRatenumberBaud Rate
mt_ebCheckbitnumberParity
mt_ebDataBitsnumberData Bits
mt_ebStopBitsnumberStop Bits
mt_ebBzVersionnumberBusiness Version
mt_ebConfirmDutynumberConfirm Duty Cycle
mt_ebpwron_delaynumberPower-on Delay
mt_ebperiod_heartnumberHeartbeat Period
mt_ebsub_device_countsnumberSub-device Count
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 Parameter Definitions ​

RPCParameterTypeDescriptionDefaultUnit
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 Rate—bps
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_parapowernumberPower—dBm
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 Frequency—MHz
mt_set_radio_paraDLSettingstringDownlink Setting—
mt_sw_set_paraswBWstringLoRa Bandwidth—
mt_sw_set_paraswSFnumberLoRa Spreading Factor—
mt_sw_set_paraswPeriodnumberSwitch Period—ms
mt_up_period_setperiod_heartnumberHeartbeat Period (s)—

Trigger Definitions ​

No project-specific triggers are defined.

Third-Party Platform Subscription ​

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

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.

Base templates and historical RPCs ​

Base templates provide an EdgeBus device skeleton; select the matching protocol application, acquisition parameters and business models. RPCs marked disuse are listed only to identify old bindings, not for new installations. Read actual parameters and inspect the current device template before migration; do not replace bindings in bulk.

TypeNameid_namePlatform model ID
TemplateMT-EB-BATTERY5
TemplateMT-EB-POWER10
Templatedefault1
RPC[MT KS52] get cov paras<disuse>mtget_ks52_paras24014202611961861
RPC[MT APP] get multi device count<disuse>mtget_app_device_count24846903489335301
RPC[MT APP] get multi devices<disuse>mtget_app_multi_device_para24577580598300677
RPC[MT APP] get data period<disuse>mtget_period_data_disuse35431228547010565
RPC[MT APP] configure multi devices<disuse>mtset_dtu_multi_device24597181948235781
RPC[MT APP] set period <disuse>mt_set_period_heart_disuse20932439878668293
RPC[MT APP] get heart period <disuse>mtget_period_heart24574845555576837
RPC[MT RADIO] get frequencies<disuse>mtget_frequencies_disuse38273241294311429