Skip to content

tklHelper 完整代码 ​

库版本:1.01.043。来源:tkl-opt/tooling/integration-tools/tklHelper.js,同步日期:2026-10-12。

使用说明与 V3 接入示例。这是 helper 库源码;平台脚本直接使用 sandbox 中的类,无需粘贴本文件的模块导入。

运行环境要求

本次仅同步文档与代码页,不代表后端 helper 已同步或平台已发布。使用 V3 前确认目标环境已加载 1.01.043。库版本与事务 version: 2 / 3 不同,省略事务版本仍使用 V2。

javascript
import {Buffer} from "buffer";
import * as util from "node:util";
export const version="1.01.043"
export  class BufferTKL {
    static DUBM_PERIOD=86400*3650
    static INVALID_NUM=-1000000000
    static INVALID_STR="INVALID"
    static PERIOD_DEFAULT=900;
    static PERIOD_DUMB=86400*3650;
    static PERIOD_TRIGGER="TRIGGER";
    static PERIOD_MAX =86400*365
    constructor(buffer) {
        if (buffer==undefined) {this.buffer=Buffer.alloc(0);
        }else if (!Buffer.isBuffer(buffer)) {this.buffer=Buffer.alloc(0);
        }else{  this.buffer = buffer;   }
        this.size=this.buffer.length;
    }
    static getTypeInfo(type) {
        const normalizedType = String(type).trim().toLowerCase();
        let dataLen = 0;
        let baseType = normalizedType;
        let bitStart,bitEnd;
        if (normalizedType.length > 3) {
            if (normalizedType.startsWith("hex")) {
                const endian=normalizedType.substring(3,5)
                baseType = "hexbe";
                if (endian==="le"){ baseType = "hexle"}
                if (endian === "be" || endian === "le") {
                    dataLen = parseInt(normalizedType.substring(5), 10);
                } else {
                    dataLen = parseInt(normalizedType.substring(3), 10);
                }
            } else if (normalizedType.startsWith("bcd")) {
                const endian=normalizedType.substring(3,5)
                baseType = "bcdbe";
                if (endian==="le"){ baseType = "bcdle"; }
                if  (normalizedType.includes('e')) { dataLen = parseInt(normalizedType.substring(5), 10);
                }else { dataLen = parseInt(normalizedType.substring(3), 10);}
            }else if (normalizedType.startsWith("bit")) {
                baseType = "bitbe";
                const endian=normalizedType.substring(3,5)
                if (endian==="le"){ baseType = "bitle"}
                let valueList=[]
                if  (normalizedType.includes('e')) { valueList=normalizedType.substring(5).split("-");
                }else { valueList=normalizedType.substring(3).split("-");   }
                bitStart=Number(valueList[0])
                bitEnd=Number(valueList[1])
                dataLen = ((bitEnd/8)|0) +1;
            }else if (normalizedType.startsWith("boolean")) {
                baseType = "boolean";
                dataLen = parseInt(normalizedType.substring(7), 10);
            }else if (normalizedType.startsWith("bool")) {
                baseType = "bool";
                dataLen = parseInt(normalizedType.substring(4), 10);
            }else if (normalizedType.startsWith("string")) {
                baseType = "string";
                dataLen = parseInt(normalizedType.substring(6), 10);
            }
        }
        switch (baseType) {
            case "int8":      return { type: "Int8",      dataLen: 1};
            case "uint8":     return { type: "UInt8",     dataLen: 1 };
            case "int16":
            case "int16be":   return { type: "Int16BE",   dataLen: 2};
            case "uint16":
            case "uint16be":  return { type: "UInt16BE",  dataLen: 2};
            case "int16le":   return { type: "Int16LE",   dataLen: 2};
            case "uint16le":  return { type: "UInt16LE",  dataLen: 2};

            case "int24":
            case "int24be":   return { type: "IntBE",     dataLen: 3}; // Buffer 没有 readInt24,通常用 readIntBE(offset, 3)
            case "uint24":
            case "uint24be":  return { type: "UIntBE",    dataLen: 3};
            case "int24le":   return { type: "IntLE",     dataLen: 3};
            case "uint24le":  return { type: "UIntLE",    dataLen: 3};

            case "int32":
            case "int32be":   return { type: "Int32BE",   dataLen: 4};
            case "uint32":
            case "uint32be":  return { type: "UInt32BE",  dataLen: 4};
            case "int32le":   return { type: "Int32LE",   dataLen: 4};
            case "uint32le":  return { type: "UInt32LE",  dataLen: 4};

            case "int40":
            case "int40be":   return { type: "IntBE",     dataLen: 5};
            case "uint40":
            case "uint40be":  return { type: "UIntBE",    dataLen: 5};
            case "int40le":   return { type: "IntLE",     dataLen: 5};
            case "uint40le":  return { type: "UIntLE",    dataLen: 5};

            case "int48":
            case "int48be":   return { type: "IntBE",     dataLen: 6};
            case "uint48":
            case "uint48be":  return { type: "UIntBE",    dataLen: 6};
            case "int48le":   return { type: "IntLE",     dataLen: 6};
            case "uint48le":  return { type: "UIntLE",    dataLen: 6};

            case "int56":
            case "int56be":   return { type: "IntBE",     dataLen: 7};
            case "uint56":
            case "uint56be":  return { type: "UIntBE",    dataLen: 7};
            case "int56le":   return { type: "IntLE",     dataLen: 7};
            case "uint56le":  return { type: "UIntLE",    dataLen: 7};

            case "int64":
            case "int64be":   return { type: "BigInt64BE",  dataLen: 8};
            case "uint64":
            case "uint64be":  return { type: "BigUInt64BE", dataLen: 8};
            case "int64le":   return { type: "BigInt64LE",  dataLen: 8};
            case "uint64le":  return { type: "BigUInt64LE", dataLen: 8};

            case "floatbe":   return { type: "FloatBE",   dataLen: 4};
            case "floatle":   return { type: "FloatLE",   dataLen: 4};

            case "floatcdab": return { type: "FloatCDAB",     dataLen: 4};
            case "intcdab":   return { type: "IntCDAB",       dataLen: 4};
            case "hexbe":       return { type: "HexBE",           dataLen: dataLen};
            case "hexle":       return { type: "HexLE",           dataLen: dataLen};
            case "bcdbe":       return { type: "BCDBE",           dataLen: dataLen};
            case "bcdle":       return { type: "BCDLE",           dataLen: dataLen};
            case "bitle":       return { type: "BitLE",     dataLen: dataLen,bitStart:bitStart,bitEnd:bitEnd};
            case "bitbe":       return { type: "BitBE",     dataLen: dataLen,bitStart:bitStart,bitEnd:bitEnd};
            case "bool" :       return { type: "Bool" ,     dataLen: dataLen};
            case "boolean" :       return { type: "Boolean" ,     dataLen: dataLen};
            case "string":      return { type: "String",    dataLen: dataLen};

            default:
                return { type: "invalid", dataLen: 0 };
        }
    }
    _preRead (offset,typeInfo) {
        if (!this._ensure(offset,typeInfo.dataLen)) { return false}
        let retBuffer=Buffer.alloc(typeInfo.dataLen);
        this.buffer.copy(retBuffer,0,offset,offset+typeInfo.dataLen);
        return retBuffer;
    }
    read(offset,type) {
        const typeInfo=BufferTKL.getTypeInfo(type);
        if (typeInfo.type==="invalid") {return false}
        let value=0
        let valBuffer=this._preRead(offset,typeInfo)
        if (valBuffer===false) { return false }
        switch (typeInfo.type) {
            case "UIntBE":
            case "IntBE":
            case "UIntLE":
            case "IntLE":  value= valBuffer["read"+typeInfo.type](0, typeInfo.dataLen); break;
            case "FloatCDAB": value=  BufferTKL.readFloatCDAB(valBuffer,0); break;
            case "IntCDAB" : value=  BufferTKL.readIntCDAB(valBuffer,0);break;
            case "HexBE": value=  BufferTKL.readHexBE(valBuffer,0,typeInfo.dataLen); break;
            case "BCDBE": value=  BufferTKL.readBCDBE(valBuffer,0,typeInfo.dataLen);break;
            case "HexLE": value=  BufferTKL.readHexLE(valBuffer,0,typeInfo.dataLen); break;
            case "BCDLE": value=  BufferTKL.readBCDLE(valBuffer,0,typeInfo.dataLen);break;
            case "BitLE": value= BufferTKL.readBitLE(valBuffer,0,typeInfo.dataLen,typeInfo.bitStart,typeInfo.bitEnd);break;
            case "BitBE":value= BufferTKL.readBitBE(valBuffer,0,typeInfo.dataLen,typeInfo.bitStart,typeInfo.bitEnd);break;
            case "Bool":value= BufferTKL.readBool(valBuffer,0,typeInfo.dataLen);break;
            case "Boolean":value= BufferTKL.readBoolean(valBuffer,0,typeInfo.dataLen);break;
            case "String":value= BufferTKL.readString(valBuffer,0,typeInfo.dataLen);break;
            case "BigInt64BE" :
            case "BigUInt64BE":
            case "BigInt64LE" :
            case "BigUInt64LE":
                value= Number( valBuffer["read"+typeInfo.type](0));
                break;
            default:
                value=  valBuffer["read"+typeInfo.type](0);
                break  ;
        }
        return value;
    }
    static write(value,type) {
        const typeInfo=BufferTKL.getTypeInfo(type);
        if (typeInfo.type==="invalid") {return Buffer.alloc(0);}
        let valBuffer=Buffer.alloc(typeInfo.dataLen);
        switch (typeInfo.type) {
            case "UIntBE":
            case "IntBE":
            case "UIntLE":
            case "IntLE":       valBuffer["write"+typeInfo.type](value,0, typeInfo.dataLen); break;
            case "FloatCDAB":   BufferTKL.writeFloatCDAB(valBuffer,value,0); break;
            case "IntCDAB" :    BufferTKL.writeIntCDAB(valBuffer,value,0);break;
            case "HexBE":       BufferTKL.writeHexBE(valBuffer,value,0,typeInfo.dataLen); break;
            case "BCDBE":       BufferTKL.writeBCDBE(valBuffer,value,0,typeInfo.dataLen);break;
            case "HexLE":       BufferTKL.writeHexLE(valBuffer,value,0,typeInfo.dataLen); break;
            case "BCDLE":       BufferTKL.writeBCDLE(valBuffer,value,0,typeInfo.dataLen);break;
            case "BitLE":       BufferTKL.writeBitLE(valBuffer,value,0,typeInfo.dataLen,typeInfo.bitStart,typeInfo.bitEnd);break;
            case "BitBE":       BufferTKL.writeBitBE(valBuffer,value,0,typeInfo.dataLen,typeInfo.bitStart,typeInfo.bitEnd);break;
            case "Bool":       BufferTKL.writeBool(valBuffer,value,0,typeInfo.dataLen);break;
            case "Boolean":       BufferTKL.writeBoolean(valBuffer,value,0,typeInfo.dataLen);break;
            case "String":       BufferTKL.writeString(valBuffer,value,0,typeInfo.dataLen);break;
            case "BigInt64BE":
            case "BigUInt64BE":
            case "BigInt64LE":
            case "BigUInt64LE":
                valBuffer["write" + typeInfo.type](BigInt(value), 0);
                break;
            default:
                valBuffer["write"+typeInfo.type](value,0); break  ;
        }
        return valBuffer;
    }
    _ensure(offset,bytes) {
        if (offset + bytes > this.size) {return false}
        return true;
    }
    static readHexBE(buffer,offset,dataLen) {
        const data = buffer.slice(offset, offset + dataLen);
        return data.toString('hex').toLowerCase();
    }
    static readHexLE(buffer,offset,dataLen) {
        let data=Buffer.alloc(dataLen);
        for (let i=0;i<dataLen;i++) {   data[i]=buffer[offset+dataLen-i-1];}
        return data.toString('hex').toLowerCase();
    }
    static readBCDBE(buffer,offset,dataLen) {
        let value = 0;
        for (let i = 0; i < dataLen; i++) {
            const byte = buffer[offset + i];
            const high = (byte >> 4) & 0x0f;
            const low = byte & 0x0f;
            if (high > 9 || low > 9) {  throw Error("wrong input or type")}
            value = value * 100 + high * 10 + low;
        }
        return value;
    }
    static readBCDLE(buffer,offset,dataLen) {
        let value = 0;
        for (let i = 0; i < dataLen; i++) {
            const byte = buffer[offset + dataLen-i-1];
            const high = (byte >> 4) & 0x0f;
            const low = byte & 0x0f;
            if (high > 9 || low > 9) {  throw Error("bcdle : wrong input or type")}
            value = value * 100 + high * 10 + low;
        }
        return value;
    }
    static readFloatCDAB(buffer,offset,dataLen) {
        const buf = buffer.slice(offset, offset + 4);
        const swapped = Buffer.from([buf[2], buf[3], buf[0], buf[1]]);
        return swapped.readFloatLE(0);
    }
    static readIntCDAB(buffer,offset) {
        const buf = buffer.slice(offset, offset + 4);
        const swapped = Buffer.from([buf[2], buf[3], buf[0], buf[1]]);
        return swapped.readInt32LE(0);
    }
    static readBool(buffer,offset,dataLen) {
        const data = buffer.readUIntBE(offset, dataLen);
        if (data===0) { return 0}
        return 1
    }
    static readBoolean(buffer,offset,dataLen) {
        const data = buffer.readUIntBE(offset, dataLen);
        if (data===0) { return false}
        return true
    }
    static readString(buffer,offset,dataLen) {
        const data = buffer.slice(offset, offset + dataLen);
        return data.toString('ascii');
    }
    static readBitLE(buffer, offset, dataLen, bitStart, bitEnd) {
        if (bitStart < 0 || bitEnd >= dataLen * 8 || bitStart > bitEnd) {
            throw new RangeError(`Invalid bit range: [${bitStart}, ${bitEnd}] for ${dataLen} bytes`);
        }
        const width = bitEnd - bitStart + 1;
        let val = 0n;
        for (let i = 0; i < dataLen; i++) {
            val |= BigInt(buffer[offset + i]) << (8n * BigInt(i));
        }
        const shifted = val >> BigInt(bitStart);
        const mask = (1n << BigInt(width)) - 1n;
        return Number(shifted & mask);
    }

    static readBitBE(buffer, offset, dataLen, bitStart, bitEnd) {
        if (bitStart < 0 || bitEnd >= dataLen * 8 || bitStart > bitEnd) {
            throw new RangeError(`Invalid bit range: [${bitStart}, ${bitEnd}] for ${dataLen} bytes`);
        }
        const width = bitEnd - bitStart + 1;
        let val = 0n;
        for (let i = 0; i < dataLen; i++) {
            val = (val << 8n) | BigInt(buffer[offset + i]);
        }
        const shifted = val >> BigInt(bitStart);
        const mask = (1n << BigInt(width)) - 1n;
        return Number(shifted & mask);
    }
    static writeHexBE(buffer,value,offset, dataLen) {
        let inputstr = String(value).toLowerCase().replaceAll("0x", "").replaceAll(" ", "");
        const hex = inputstr.padStart(dataLen * 2, '0');
        const tempBuf = Buffer.from(hex, 'hex');
        tempBuf.copy(buffer, offset, 0, dataLen);
        return buffer;
    }
    static writeHexLE(buffer,value, offset, dataLen) {
        let inputstr = String(value).toLowerCase().replaceAll("0x", "").replaceAll(" ", "");        const hex = inputstr.padStart(dataLen * 2, '0');
        const tempBuf = Buffer.from(hex, 'hex');
        for (let i = 0; i < dataLen; i++) {
            buffer[offset + dataLen - i - 1] = tempBuf[i];
        }
        return buffer;
    }
    static writeBCDBE(buffer,value,offset, dataLen) {
        let tempValue = value;
        for (let i = dataLen - 1; i >= 0; i--) {
            const low = tempValue % 10;
            tempValue = Math.floor(tempValue / 10);
            const high = tempValue % 10;
            tempValue = Math.floor(tempValue / 10);
            buffer[offset + i] = (high << 4) | low;
        }
        return buffer;
    }
    static writeBCDLE(buffer, value, offset, dataLen) {
        let tempValue = value;
        for (let i = 0; i < dataLen; i++) {
            const low = tempValue % 10;
            tempValue = Math.floor(tempValue / 10);
            const high = tempValue % 10;
            tempValue = Math.floor(tempValue / 10);
            buffer[offset + i] = (high << 4) | low;
        }
        return buffer;
    }
    static writeFloatCDAB(buffer, value, offset) {
        const tempBuf = Buffer.alloc(4);
        tempBuf.writeFloatLE(value, 0);
        // 从 [0,1,2,3] 变换为 [2,3,0,1]
        buffer[offset] = tempBuf[2];
        buffer[offset + 1] = tempBuf[3];
        buffer[offset + 2] = tempBuf[0];
        buffer[offset + 3] = tempBuf[1];
        return buffer;
    }
    static writeIntCDAB(buffer, value, offset) {
        const tempBuf = Buffer.alloc(4);
        tempBuf.writeInt32LE(value, 0);
        // 从 [0,1,2,3] 变换为 [2,3,0,1]
        buffer[offset] = tempBuf[2];
        buffer[offset + 1] = tempBuf[3];
        buffer[offset + 2] = tempBuf[0];
        buffer[offset + 3] = tempBuf[1];
        return buffer;
    }
    static writeBool(buffer, value, offset, dataLen) {
        buffer.writeUIntBE(value ? 1 : 0, offset, dataLen);
        return buffer;
    }
    static writeBoolean(buffer, value, offset, dataLen) {
        buffer.writeUIntBE(value ? 1 : 0, offset, dataLen);
        return buffer;
    }
    static writeString(buffer, value, offset, dataLen) {
        const tempBuf = Buffer.from(value, 'ascii');
        tempBuf.copy(buffer, offset, 0, dataLen);
        return buffer;
    }
    static writeBitLE(buffer, value, offset, dataLen, bitStart, bitEnd) {
        if (bitStart < 0 || bitEnd >= dataLen * 8 || bitStart > bitEnd) {
            throw new RangeError(`Invalid bit range: [${bitStart}, ${bitEnd}] for ${dataLen} bytes`);
        }

        const width = bitEnd - bitStart + 1;
        const mask = (1n << BigInt(width)) - 1n;

        let val = 0n;
        for (let i = 0; i < dataLen; i++) {
            val |= BigInt(buffer[offset + i]) << (8n * BigInt(i));
        }

        const cleanValue = BigInt(value) & mask;
        val = (val & ~(mask << BigInt(bitStart))) | (cleanValue << BigInt(bitStart));

        for (let i = 0; i < dataLen; i++) {
            buffer[offset + i] = Number(val & 0xffn);
            val >>= 8n;
        }

        return buffer;
    }
    static writeBitBE(buffer, value, offset, dataLen, bitStart, bitEnd) {
        if (bitStart < 0 || bitEnd >= dataLen * 8 || bitStart > bitEnd) {
            throw new RangeError(`Invalid bit range: [${bitStart}, ${bitEnd}] for ${dataLen} bytes`);
        }

        const width = bitEnd - bitStart + 1;
        const mask = (1n << BigInt(width)) - 1n;

        let val = 0n;
        for (let i = 0; i < dataLen; i++) {
            val = (val << 8n) | BigInt(buffer[offset + i]);
        }

        const cleanValue = BigInt(value) & mask;
        val = (val & ~(mask << BigInt(bitStart))) | (cleanValue << BigInt(bitStart));

        for (let i = dataLen - 1; i >= 0; i--) {
            buffer[offset + i] = Number(val & 0xffn);
            val >>= 8n;
        }

        return buffer;
    }
}
export class PayloadParser {
    constructor( {
                     bufferType,
                     device,
                     msg,
                     frameInfo,
                     appInfo,
                     paraInfo,
                     extraData
                 }) {
        if (bufferType==undefined){ this.bufferType=BufferTKL
        }else { this.bufferType=bufferType }
        this.device = device
        this.msg=msg;
        this.frameInfo=frameInfo??{}
        this.frameInfo.port=frameInfo?.port??-1
        this.frameInfo.dataLen=frameInfo?.dataLen ?? -1;
        this.frameInfo.status=frameInfo?.status ?? -1;
        this.frameInfo.battery=frameInfo?.battery ?? -1;
        this.frameInfo.rssi=frameInfo?.rssi ?? false;
        this.frameInfo.tagList=frameInfo?.tagList ?? [];
        if(frameInfo?.subDevice==undefined||frameInfo?.subDevice?.index==undefined) {
            this.frameInfo.subDevice={}
            this.frameInfo.subDevice.index=-1
            this.frameInfo.subDevice.type="uint8"
        }
        this.appInfo = appInfo ?? [];
        this.paraInfo = paraInfo ?? {};
        this.extraData=extraData
        this.payload = Buffer.from(msg?.userdata?.payload, "base64");
        this.port = msg?.userdata?.port || -1;
        this.tdata={};
        this.pdata={};
    }
    _preParseTelemetry(){}
    _preParseParas(){}
    _postParseTelemetry() {}
    _postParseParas() {}
    telemetry() {
        let payBuffer = new this.bufferType(this.payload);
        let valid=true
        if (this.frameInfo.port>0&&this.port!==this.frameInfo.port){ return null }
        if(this.frameInfo.dataLen>0&&this.frameInfo.dataLen!==payBuffer.size){ return null }
        this.frameInfo.tagList.forEach(range => {if(this.payload[range.index]!==range.tag){ valid=false}})
        if(valid===false){ return null}
        if (this.frameInfo?.status>=0) { this.tdata.status=payBuffer.read(this.frameInfo.status,"uint8");   }
        if (this.frameInfo?.battery>=0) {
            let vbat=payBuffer.read(this.frameInfo.battery,"uint8");
            this.tdata.battery=Number(((vbat*1.6)/254 +2.0).toFixed(2))
        }
        if (this.frameInfo.rssi&&this.msg?.gwrx?.[0] ) {
            this.tdata.rssi = this.msg.gwrx[0].rssi;
            this.tdata.snr = this.msg.gwrx[0].lsnr;
        }
        this._preParseTelemetry()
        if (this.frameInfo?.subDevice?.index>=0){
            this.tdata.addr=payBuffer.read(this.frameInfo?.subDevice?.index,this.frameInfo?.subDevice?.type);
            if (this.tdata.addr===0){return null}
        }
        const counts=this.appInfo.length;
        for (let i=0; i<counts; i++){
            let val=payBuffer.read(this.appInfo[i].index,this.appInfo[i].type);
            if (val===false){ continue}
            let lastVal
            if (this.appInfo[i]?.illegal!=undefined ) {
                let method=this.appInfo[i]?.illegal.substring(0,1)
                 let checkBuffer= new BufferTKL( Buffer.from(this.appInfo[i]?.illegal.substring(1),"hex"));
                 let checkVal= checkBuffer.read(0,this.appInfo[i].type)
                if (method==='>'&& val>checkVal){ continue}
                if (method==='<'&& val<checkVal){ continue}
                if (method==='='&& val===checkVal){ continue}
            }
            let decimal=this.appInfo[i]?.decimal==undefined?0:this.appInfo[i]?.decimal;
            if (typeof val==="number"){
                if (this.appInfo[i]?.coefficient==undefined){   lastVal=Number(val.toFixed(decimal));
                }else if (typeof this.appInfo[i]?.coefficient!=="number"){   lastVal=Number(val.toFixed(decimal));
                }else { lastVal=Number((val*this.appInfo[i].coefficient).toFixed(decimal));  }
            }else { lastVal=val }
            if (this.appInfo[i].postProcess!=undefined){
                let postFucn=this.appInfo[i].postProcess;
                if(!postFucn.includes("return") ){
                    postFucn= postFucn.concat(" ; return value;");
                }
                const postProcessor = new Function('value',postFucn);
                lastVal = postProcessor(lastVal);
            }
            if (Object.keys(this.appInfo[i].options??{}).length > 0) {
                let rval=lastVal.toString()
               // lastVal=this.appInfo[i].options[Object.keys(this.appInfo[i].options)[0]]
                if (rval in this.appInfo[i].options) {  lastVal= this.appInfo[i].options[rval] }
            }
            this.tdata[this.appInfo[i].field_name]=lastVal
        }
        for (let i=0; i<counts; i++){
            if (this.appInfo[i]?.coefficient==undefined) {continue}
            if (typeof this.appInfo[i].coefficient==="number"){ continue}
            let val=this.tdata[this.appInfo[i].field_name]
            if (val==undefined){ continue}
            let valcof=this.tdata[this.appInfo[i].coefficient];
            if (valcof==undefined){ continue}
            let decimal=this.appInfo[i]?.decimal==undefined?0:this.appInfo[i]?.decimal;
            this.tdata[this.appInfo[i].field_name]=Number((val*valcof).toFixed(decimal));
        }
        this._postParseTelemetry()
        return Object.keys(this.tdata).length? Utils.withForwarderInfo(this.device, this.tdata): null;
    }
    paras() {
        if (this.port !== 214) {  return null }
       // this.pdata.content = this.payload.toString('hex');
        let size=this.payload.length;
        let regAddress=0
        let startAddress=0
        this._preParseParas()
        for (let i = 0; i < size; i++) {
            //if (payload[i]!==0x2F){ return null }
            let paraName="app"
            if ((this.payload[i]&0xFF)===0x2F) { paraName="app"
            }else if((this.payload[i]&0xFF)===0x21) { paraName="fw"
            }else if((this.payload[i]&0xFF)===0x29) { paraName="cf"
            }else if((this.payload[i]&0xFF)===0x22) { paraName="radio"
            }else if((this.payload[i]&0xFF)===0x24) { paraName="ds"
            }else if((this.payload[i]&0xFF)===0x23) { paraName="status"
            }else {
                i+=this.payload[i+1]+1
                continue
            }
            if (size<(i+1+this.payload[i+1]) ){ return null }
            let regcounts;
            let reader;
            if (paraName == "status") {
                regcounts=this.payload[i+1]
                if (size<(i+2+regcounts) ){ return null }
                startAddress=0
                reader= new this.bufferType(this.payload.slice(i+2,i+2+regcounts));
            } else {
                regcounts=this.payload[i+1]-2;
                if (size<(i+4+regcounts) ){ return null }
                startAddress=this.payload[i+2]
                reader= new this.bufferType(this.payload.slice(i+4,i+4+regcounts));
            }


            for (let j = 0; j < regcounts; j++) {
                regAddress=startAddress + j
                let para={}
                para=this.paraInfo[paraName+"_"+regAddress.toString(10)]
                for (let k=0;k<16;k++) {
                    if (k>0) {  para=this.paraInfo[paraName+"_"+regAddress.toString(10)+"_"+k.toString(10)]}
                    if (para==undefined){    continue    }
                    let val=reader.read(j,para.type);
                    if (val===false){ continue}
                    let lastVal
                    if (para.illegal!=undefined ) {
                        let method=para.illegal.substring(0,1)
                        let checkBuffer= new BufferTKL( Buffer.from(para.illegal.substring(1),"hex"));
                        let checkVal= checkBuffer.read(0,para.type)
                        if (method==='>'&& val>checkVal){ continue}
                        if (method==='<'&& val<checkVal){ continue}
                        if (method==='='&& val===checkVal){ continue}
                    }
                    let decimal=para?.decimal==undefined?0:para?.decimal;
                    if (typeof val==="number"){
                        if (para?.coefficient==undefined){   lastVal=Number(val.toFixed(decimal));
                        }else if (typeof para?.coefficient!=="number"){   lastVal=Number(val.toFixed(decimal));
                        }else { lastVal=Number((val*para.coefficient).toFixed(decimal));  }
                    }else { lastVal=val }
                    if (para.postProcess!=undefined){
                        let postFunc=para.postProcess;
                        if(!postFunc.includes("return") ){postFunc= postFunc.concat(" ; return value;");}
                        const postProcessor = new Function('value',postFunc);
                        lastVal = postProcessor(lastVal);
                    }
                    if (Object.keys(para.options??{}).length > 0) {
                        let rval=lastVal.toString()
                        if (rval in para.options) {    lastVal= para.options[rval] }
                    }
                    this.pdata[para.field_name]=lastVal
                }
            }
            i+=regcounts+3;
        }
        this._postParseParas()
        if (Object.keys(this.pdata).length < 1) {
            return null;
        }
        return this.pdata;
    }
}
export class MSparser extends PayloadParser {
    constructor({device, msg, frameInfo, extraData}){
        super({
            device:device, msg:msg, frameInfo:frameInfo, extraData:extraData
        })
        let payload=Buffer.from(msg?.userdata?.payload, "base64")
        let  payBuffer=new BufferTKL(payload);
        this.appInfo=[];
        let index=0
        let newPayload=Buffer.alloc(255);
        for(let i=0; i < payload.length;) {
            let idVal=payBuffer.read(i,frameInfo.idType)
            i+=2;
            for (let j = 0; j < 4; j++) {
                let key="user_0x"+idVal+"_"+j.toString(10)
                let para=this.extraData[key]
                if (para==undefined) { continue}
                let ptypeInfo=BufferTKL.getTypeInfo(para.type)
                if ((i+ptypeInfo.dataLen)>payload.length) {continue}
                para["index"]=index
                payload.copy(newPayload,index,i,i+ptypeInfo.dataLen)
                index+=ptypeInfo.dataLen
                this.appInfo.push(para)
                i+=ptypeInfo.dataLen;
            }
        }
        this.payload=Buffer.alloc(index);
        newPayload.copy(this.payload,0,0,index);
    }
}
export class Utils{
    static paraName = Object.freeze({
        app: 'app',
        cf: 'cf',
        fw: 'fw',
        radio: 'radio',
        user:"user"
    });
    static msgType=Object.freeze({
        paras:"para",
        transParent:"transParent",
        swDown:"swDown",
        user:"user",
    })
    static ACTION = Object.freeze({  no:"no",new: 'new',clear: 'clear'});
    static LEVEL=Object.freeze({low:'low',mid:'mid',high:'high',urgent:'urgent'});
    static parseToBuffer(inputStr){
        if (inputStr==undefined){  return Buffer.alloc(0)}
        let xstr=inputStr.toLowerCase().replaceAll("0x","").replaceAll(" ","")
        return Buffer.from(xstr.length%2===0?xstr:('0'+xstr),"hex")
    }
    static parseVal(valstr){
        if (typeof valstr==="number"){ return valstr; }
        let val=valstr.toLowerCase();
        if (val.includes("0x")){
            val=val.replaceAll("0x","").replaceAll(" ","");
            let cleanedHex =val.length % 2 === 0 ? val : '0' + val;
            return parseInt(cleanedHex, 16);
        }
        return parseInt(val,10)
    }
    static  crcSum( buf, len, poly=0)
    {
        let crc = poly;
        for(let i=0;i<len;i++) {
            crc += buf[i];
            crc&=0xFFFF
        }
        return crc&0xFFFF;
    }
    static crc16CCIT (data, len,poly=0x1021)
    {
        let remainder = 0;
        let polynomial = poly;
        for( let i = 0; i < len; i++ )
        {
            remainder ^=(data[i] << 8);
            for( let bit = 8; bit > 0; bit--)
            {
                if( (remainder & 0x8000) )	 remainder = (remainder << 1) ^ polynomial;
                else 					     remainder <<= 1;
                remainder &= 0xFFFF; // Keep within 16 bits
            }
        }
        return remainder&0xFFFF;
    }
    static crc16Modbus(buf,  len, poly=0xa001)
    {
        let i,j;
        let crc;
        for(i=0,crc=0xffff;i< len;i++)
        {
            crc ^= buf[i];
            for(j=0;j<8;j++)
            {
                if(crc & 0x01)  crc = (crc >> 1) ^ poly;
                else            crc >>= 1;
                crc&=0xFFFF
            }
        }
        return crc&0xFFFF;
    }
    static timingPerformanceCalc(sf, bw, swPeriod) {
        let minSf = 5;
        let maxSf = 12;
        let bwList = [500, 250, 125];
        let baseSymbolTime = 0.064;
        if (sf < minSf) sf = 5;
        if (sf > maxSf) sf = 12;
        if (!bwList.includes(bw)) bw = 500;
        let powerOf2 =  ((sf - minSf) + (bwList.indexOf(bw)))
        let symbolTime = baseSymbolTime * (2 ** powerOf2);
        return Math.ceil((swPeriod + 300) / symbolTime)
    }
     /**
       * 将设备转发配置附加到遥测数据。
       * device.server_attrs._forwarder_info
       * → tdata._forwarder_info
       */
      static withForwarderInfo(device, tdata) {
          const info = device?.server_attrs?._forwarder_info
          if (
              tdata &&
              typeof tdata === "object" &&
              !Array.isArray(tdata) &&
              info &&
              typeof info === "object" &&
              !Array.isArray(info) &&
              Object.keys(info).length
          ) {
              tdata._forwarder_info = { ...info }
          }
          return tdata
      }
    // Omitted version retains the existing version-2 server_attrs contract.
    static _paraVersion(version = 2) {
        if (version !== 2 && version !== 3) {
            throw new TypeError('Unsupported parameter transaction version; expected 2 or 3');
        }
        return version;
    }
    static _paraClone(value) {
        return JSON.parse(JSON.stringify(value));
    }
    static _paraObject(value, name) {
        if (!value || typeof value !== 'object' || Array.isArray(value)) {
            throw new TypeError(`${name} must be an object`);
        }
    }
    static _paraJson(value, name, ancestors = new Set()) {
        if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
        if (typeof value === 'number' && Number.isFinite(value)) return;
        if (!value || typeof value !== 'object') throw new TypeError(`${name} must contain lossless JSON values`);
        if (ancestors.has(value)) throw new TypeError(`${name} cannot contain cycles`);
        const array = Array.isArray(value);
        const prototype = Object.getPrototypeOf(value);
        if (!array && prototype !== null &&
            (Object.getPrototypeOf(prototype) !== null || prototype.constructor?.name !== 'Object')) {
            throw new TypeError(`${name} must contain plain JSON objects`);
        }
        if ('toJSON' in value) throw new TypeError(`${name} cannot use toJSON`);
        const entries = Object.getOwnPropertyDescriptors(value);
        if (array && Object.keys(entries).length !== value.length + 1) {
            throw new TypeError(`${name} cannot contain sparse arrays or extra array properties`);
        }
        ancestors.add(value);
        for (const key of Reflect.ownKeys(entries)) {
            if (array && key === 'length') continue;
            const descriptor = entries[key];
            if (typeof key !== 'string' || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value') ||
                (array && !/^(0|[1-9]\d*)$/.test(key))) {
                throw new TypeError(`${name} must contain enumerable JSON values only`);
            }
            this._paraJson(descriptor.value, `${name}.${key}`, ancestors);
        }
        ancestors.delete(value);
    }
    static _paraName(rpcName) {
        if (typeof rpcName !== 'string' || !rpcName.trim()) {
            throw new TypeError('rpcName must be a non-empty string');
        }
    }
    static _findParaSetInfo({ device, rpcName, version = 2, msgType }) {
        this._paraVersion(version);
        this._paraName(rpcName);
        const slot = msgType === this.msgType.transParent ? this.msgType.transParent : rpcName;
        const slots = msgType === undefined ? [...new Set([slot, this.msgType.transParent])] : [slot];
        for (const key of slots) {
            let record;
            if (version === 2) {
                const attrs = device?.server_attrs;
                if (!attrs || !Object.hasOwn(attrs, key)) continue;
                const rpcInfo = attrs[key];
                if (!rpcInfo || (rpcInfo.rpc !== rpcName && rpcName !== key)) continue;
                this._paraVersion(rpcInfo.version);
                if (rpcInfo.version !== undefined && rpcInfo.version !== 2) {
                    throw new TypeError('server_attrs transaction must use version 2');
                }
                record = {
                    version: 2,
                    rpcInfo: { ...rpcInfo, version: 2 },
                    checkState: attrs[`__para_check__${rpcInfo.rpc ?? key}`] ?? null,
                };
            } else {
                const processKey = `__para_set__${key}`;
                const attrs = device?.process_data;
                if (!attrs || !Object.hasOwn(attrs, processKey)) continue;
                record = attrs[processKey];
                if (!record || (record.rpcInfo?.rpc !== rpcName && rpcName !== key)) continue;
                if (record.version !== 3 || record.rpcInfo?.version !== 3 || record.checkState?.version !== 3) {
                    throw new TypeError('process_data transaction must use version 3');
                }
            }
            this._paraJson(record, 'transaction');
            return { key, record: this._paraClone(record) };
        }
        return null;
    }
    /** Read a detached transaction snapshot from a device or modifyAttrs.dnMsg. */
    static getParaSetInfo(options) {
        return this._findParaSetInfo(options)?.record ?? null;
    }
    static _paraSetPatch(key, record) {
        if (record.version === 3) {
            return { process_data: { [`__para_set__${key}`]: this._paraClone(record) } };
        }
        const server_attrs = { [key]: this._paraClone(record.rpcInfo) };
        if (record.checkState) {
            server_attrs[`__para_check__${record.rpcInfo.rpc ?? key}`] = this._paraClone(record.checkState);
        }
        return { server_attrs };
    }
    /** Return a persistence patch; never mutate the read-only device or send a command. */
    static updateParaSetInfo({ updates, ...options }) {
        const found = this._findParaSetInfo(options);
        if (!found) throw new Error('Parameter transaction not found');
        this._paraObject(updates, 'updates');
        // Validate before cloning; JSON.stringify otherwise drops/changes unsafe values.
        this._paraJson(updates, 'updates');
        const allowed = {
            rpcInfo: ['timeout', 'params', 'checkInfo', 'classMode', 'telemetryFields'],
            checkState: ['maxRetries'],
        };
        for (const [group, patch] of Object.entries(updates)) {
            if (!Object.hasOwn(allowed, group)) throw new TypeError(`Cannot update ${group}`);
            this._paraObject(patch, group);
            for (const key of Object.keys(patch)) {
                if (!allowed[group].includes(key)) throw new TypeError(`Cannot update ${group}.${key}`);
            }
            if (!found.record[group]) throw new Error(`${group} is missing`);
            found.record[group] = { ...found.record[group], ...this._paraClone(patch) };
        }
        const { rpcInfo, checkState } = found.record;
        if (rpcInfo.timeout !== undefined && (!Number.isFinite(rpcInfo.timeout) || rpcInfo.timeout < 0)) {
            throw new TypeError('timeout must be a non-negative number');
        }
        if (checkState?.maxRetries !== undefined && (!Number.isInteger(checkState.maxRetries) || checkState.maxRetries < 0)) {
            throw new TypeError('maxRetries must be a non-negative integer');
        }
        if (rpcInfo.classMode !== undefined && !['ClassA', 'ClassC'].includes(rpcInfo.classMode)) {
            throw new TypeError('classMode must be ClassA or ClassC');
        }
        if (rpcInfo.params !== undefined) this._paraObject(rpcInfo.params, 'params');
        if (found.record.version === 3) this._validateParaV3(rpcInfo);
        return this._paraSetPatch(found.key, found.record);
    }
    static _validateParaV3({ params, checkInfo, telemetryFields }) {
        this._paraObject(params, 'params');
        this._paraJson(params, 'params');
        if (checkInfo !== undefined && checkInfo !== null) {
            this._paraObject(checkInfo, 'checkInfo');
            this._paraJson(checkInfo, 'checkInfo');
            if (!Array.isArray(checkInfo.appInfo) || checkInfo.appInfo.length === 0 ||
                checkInfo.appInfo.some(tag => !tag || typeof tag.field_name !== 'string' || tag.val === undefined)) {
                throw new TypeError('checkInfo.appInfo requires explicit feedback fields and expected values');
            }
            for (const field of ['telemetry_data', 'shared_attrs', 'server_attrs']) {
                if (checkInfo[field] != null) this._paraObject(checkInfo[field], field);
            }
            if (Object.keys(checkInfo.server_attrs ?? {}).some(key => key.startsWith('__para_check__'))) {
                throw new TypeError('Business server_attrs cannot overwrite helper check state');
            }
        } else if (!Object.keys(params).some(key => !key.startsWith('_'))) {
            throw new TypeError('Version 3 requires at least one feedback comparison');
        }
        if (telemetryFields !== undefined) {
            this._paraJson(telemetryFields, 'telemetryFields');
            if (!Array.isArray(telemetryFields) ||
                telemetryFields.some(key => typeof key !== 'string' || key.startsWith('_') || !Object.hasOwn(params, key))) {
                throw new TypeError('telemetryFields must name approved public parameter fields');
            }
        }
        if (checkInfo && !Object.hasOwn(checkInfo, 'telemetry_data') && telemetryFields?.some(field =>
            !checkInfo.appInfo.some(tag => tag.field_name === field && tag.val === params[field]))) {
            throw new TypeError('telemetryFields must be covered by the feedback comparison');
        }
    }
    static makeParaSetMSG(options) {
        const version = this._paraVersion(options.version);
        if (version === 2) {
            const queue = this._makeParaSetMSGLegacy(options);
            if (options.rpcName != undefined && options.device != undefined) {
                const key = options.msgType == this.msgType.transParent ? this.msgType.transParent : options.rpcName;
                queue[0].dnMsg.server_attrs[key].version = 2;
            }
            return queue;
        }
        this._paraName(options.rpcName);
        this._paraObject(options.device, 'device');
        if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 0)) {
            throw new TypeError('timeout must be a non-negative number');
        }
        if (options.maxRetries !== undefined && (!Number.isInteger(options.maxRetries) || options.maxRetries < 0)) {
            throw new TypeError('maxRetries must be a non-negative integer');
        }
        const key = options.msgType === this.msgType.transParent ? this.msgType.transParent : options.rpcName;
        const previous = this.getParaSetInfo({ device: options.device, rpcName: options.rpcName, version: 3, msgType: options.msgType ?? this.msgType.paras });
        const retry = previous?.checkState.phase === 'RETRY_READY';
        const params = options.params ?? {};
        const checkInfo = options.checkInfo ?? (retry ? previous.rpcInfo.checkInfo : undefined);
        const telemetryFields = options.telemetryFields ?? (retry ? previous.rpcInfo.telemetryFields : undefined);
        this._validateParaV3({ params, checkInfo, telemetryFields });
        const server_attrs = previous ? {
            [key]: previous.rpcInfo,
            [`__para_check__${options.rpcName}`]: { ...previous.checkState, version: 2 },
        } : {};
        const queue = this._makeParaSetMSGLegacy({
            ...options, checkInfo,
            device: { ...options.device, server_attrs },
        });
        const written = queue[0].dnMsg.server_attrs;
        const record = {
            version: 3,
            rpcInfo: { ...written[key], version: 3 },
            checkState: { ...written[`__para_check__${options.rpcName}`], version: 3 },
        };
        if (telemetryFields !== undefined) record.rpcInfo.telemetryFields = telemetryFields;
        queue[0].dnMsg = this._paraSetPatch(key, record);
        return queue;
    }
    // Keep legacy action order, defaults, checks and retry semantics isolated.
    static _makeParaSetMSGLegacy({
                              msgType,
                       checkInfo,
                       device,
                       classMode,
                       rpcName,
                       params,
                       paraDownBuffer,
                       extraAppBuffer,
                       timeout,
                       maxRetries,
                   }){
        let msgQue=[]
        if (device==undefined) return msgQue
        if (classMode==undefined) { classMode=device.shared_attrs?.class_mode??"ClassA"; }
        if (timeout==undefined) {
            timeout=864000000 // 10 days
            if(classMode==="ClassC") timeout=5000
        }
        if(msgType==undefined) msgType=this.msgType.paras
        maxRetries=Number.isInteger(maxRetries)&&maxRetries>=0?maxRetries:3;

        // [paraCheck state v2] paraCheck 的控制状态独立于 RPC 业务属性。
        // 普通调用始终重新开始检查;只有 RETRY_READY 表示本次是 paraCheck 发起的重试。
        let paraCheckRetry=false
        if(rpcName!=undefined){
            if (params == undefined) params = {};
            let skey=rpcName
            if (msgType==this.msgType.transParent) skey=this.msgType.transParent
            const checkKey=`__para_check__${rpcName}`
            const previousRpcInfo=device.server_attrs?.[skey]
            const previousCheck=device.server_attrs?.[checkKey]
            paraCheckRetry=previousCheck?.version===2&&previousCheck?.phase==="RETRY_READY"

            if(paraCheckRetry){
                classMode=previousRpcInfo?.classMode??classMode
                timeout=previousRpcInfo?.timeout??timeout
                maxRetries=previousCheck?.maxRetries??maxRetries
                if(checkInfo==undefined) checkInfo=previousRpcInfo?.checkInfo
            }

            const businessParams={...params}
            delete businessParams._retries
            const nextCheck=paraCheckRetry?{
                ...previousCheck,
                phase:"WAIT_ECHO",
                maxRetries:maxRetries,
            }:{
                version:2,
                phase:"WAIT_ECHO",
                counter:1,
                retries:0,
                maxRetries:maxRetries,
            }
            const rpcInfo={
                rpc:rpcName,
                timeout:timeout,
                classMode:classMode,
                date:new Date().toISOString(),
                ms:Date.now(),
                params:businessParams,
            }
            if(checkInfo!=undefined) rpcInfo.checkInfo=checkInfo
            let msgServerAttrs={
                sleepTimeMs: 0,
                type:"modifyAttrs",
                dnMsg: {
                    server_attrs:  {
                        [skey]:rpcInfo,
                        [checkKey]:nextCheck,
                    }
                }
            }
            msgQue.push(msgServerAttrs);
        }
        if (paraDownBuffer!=undefined){
            let msg=RPCHelper.makeMSG({
                msgType:msgType,
                device:device,
                dnBuffer:paraDownBuffer,
                sleepTime:0,
                dnWaitms:0,
            })
            msgQue.push(msg)
            // [paraCheck state v2] Class C 的重试已在首次调用时排队,paraCheck 重入时不能重复排队。
            if (classMode==="ClassC"&&!paraCheckRetry) {
                for(let i=0;i<maxRetries;i++){
                    let msgRetry=RPCHelper.makeMSG({
                        msgType:msgType,
                        device:device,
                        dnBuffer:paraDownBuffer,
                        sleepTime:0,
                        intervalms:timeout,
                        dnWaitms:0,
                    }
                    )
                    msgQue.push(msgRetry)
                }
            }
        }
        if (extraAppBuffer!=undefined){
            let msgExtra=RPCHelper.makeMSG({
                msgType:Utils.msgType.paras,
                device:device,
                dnBuffer:extraAppBuffer,
                sleepTime:0,
                intervalms:classMode==="ClassA"?0:2000
            })
            msgQue.push(msgExtra);
        }
        return msgQue;
    }
    static paraCheck(rpc, source, data, { version = 2 } = {}) {
        this._paraVersion(version);
        if (version === 2) return this._paraCheckLegacy(rpc, source, data);
        const found = this._findParaSetInfo({ device: source, rpcName: rpc, version: 3 });
        if (!found) return {};
        const { key, record } = found;
        const { rpcInfo, checkState } = record;
        if (!['WAIT_ECHO', 'RETRY_READY'].includes(checkState.phase) || checkState.counter < 0) return {};
        if (rpcInfo.timeout !== undefined && Date.now() - rpcInfo.ms > rpcInfo.timeout) {
            record.checkState = { ...checkState, phase: 'EXPIRED', counter: -3 };
            return this._paraSetPatch(key, record);
        }
        this._validateParaV3(rpcInfo);
        const checkKey = `__para_check__${rpcInfo.rpc}`;
        const result = this._paraCheckLegacy(key, {
            [key]: this._paraClone(rpcInfo),
            [checkKey]: { ...checkState, version: 2 },
        }, data);
        if (!result.sdata?.[checkKey]) return {};
        record.checkState = { ...result.sdata[checkKey], version: 3 };
        const output = this._paraSetPatch(key, record);
        const server_attrs = { ...result.sdata };
        delete server_attrs[checkKey];
        if (Object.keys(server_attrs).length) output.server_attrs = server_attrs;
        if (result.tdata != null) output.telemetry_data = this._paraClone(result.tdata);
        if (result.pdata != null) output.shared_attrs = this._paraClone(result.pdata);
        if (result.actions != null) output.actions = this._paraClone(result.actions);
        if (record.checkState.phase === 'SUCCESS') {
            record.checkState.observedAt = new Date().toISOString();
            Object.assign(output, this._paraSetPatch(key, record));
            // Never copy arbitrary params. Callers explicitly approve model fields.
            if (!Object.hasOwn(rpcInfo.checkInfo ?? {}, 'telemetry_data') && rpcInfo.telemetryFields?.length) {
                output.telemetry_data = Object.fromEntries(rpcInfo.telemetryFields.map(field => [field, rpcInfo.params[field]]));
            }
        }
        return output;
    }
    static _paraCheckLegacy(rpc,server_attrs,data){
        let retval={actions:null,sdata:{},tdata:null,pdata:null}
        let success=true
        const nullVal={sdata:null,tdata:null,pdata:null,actions:null }
        if (server_attrs==undefined){ return nullVal }
        let rpcInfo=server_attrs?.[rpc]
        if (rpcInfo==undefined){ return nullVal }

        // [paraCheck state v2] transParent 的业务属性沿用共享槽位,检查状态按真实 rpcName 隔离。
        const actualRpc=rpcInfo?.rpc??rpc
        const checkKey=`__para_check__${actualRpc}`
        const storedCheck=server_attrs?.[checkKey]
        if(storedCheck?.version!==2) return nullVal
        let checkState=JSON.parse(JSON.stringify(storedCheck))
        if(checkState?.counter==undefined||checkState.counter<0) return nullVal
        if(rpcInfo?.params==undefined) return nullVal
        if (rpcInfo.timeout!=undefined){
            let diffms=Date.now()-rpcInfo.ms
            if (diffms>rpcInfo.timeout) return nullVal
        }
        if (rpcInfo?.checkInfo==undefined) {
            success = Object.keys(rpcInfo.params).every(param => {
                    if (param?.startsWith("_")) return true;
                    return rpcInfo.params[param] === data?.[param]
                }
            );
        }else {
            let payParser=new PayloadParser({
                device:data?.device,
                msg:data?.msg,
                frameInfo:rpcInfo.checkInfo?.frameInfo,
                appInfo:rpcInfo.checkInfo?.appInfo,
            })
            let tdata= payParser.telemetry()
            if(tdata==undefined){ success=false
            }else{
            rpcInfo.checkInfo?.appInfo.forEach(tag=>{
                if(tdata[tag.field_name]!==tag.val) success=false
            })
            }
        }
        const FIRST_STATE = 2;
        const retries=Number.isInteger(checkState.retries)?checkState.retries:0
        const maxRetries=Number.isInteger(checkState.maxRetries)&&checkState.maxRetries>=0?
            checkState.maxRetries:3
        if(success){
            checkState.phase="SUCCESS"
            checkState.counter=-1;
            if (rpcInfo.classMode=="ClassC"){
                retval.actions=[
                    {
                        method:"mt_action",
                        params:{
                          action:"downBufferClear",
                        }
                    }
                ];
            }
            if (rpcInfo?.checkInfo?.telemetry_data!=undefined) retval.tdata=rpcInfo?.checkInfo?.telemetry_data
            if (rpcInfo?.checkInfo?.shared_attrs!=undefined) retval.pdata=rpcInfo?.checkInfo?.shared_attrs
            if (rpcInfo?.checkInfo?.server_attrs!=undefined) retval.sdata=rpcInfo?.checkInfo?.server_attrs
            retval.sdata[checkKey]=checkState
        } else if(checkState.phase!=="WAIT_ECHO"){
            return nullVal
        } else if (checkState.counter<FIRST_STATE) {
            checkState.counter++;
            retval.sdata[checkKey]=checkState
        } else if (retries>=maxRetries) {
            checkState.phase="FAILED"
            checkState.counter=-2;
            retval.sdata[checkKey]=checkState
            if(rpcInfo.classMode==="ClassC"){
                retval.actions=[
                    {
                        method:"mt_action",
                        params:{action:"downBufferClear"}
                    }
                ]
            }
        }else {
            checkState.retries=retries+1
            checkState.counter++;
            if(rpcInfo.classMode==="ClassC"){
                checkState.phase="WAIT_ECHO"
            }else{
                checkState.phase="RETRY_READY"
                retval.actions=[
                    {
                        method:actualRpc,
                        params:{...rpcInfo.params}
                    }
                ]
            }
            retval.sdata[checkKey]=checkState
        }
        return retval;
    }
}
export class TriggerHelper{
    constructor({
                    device, thingModelId,alarmEvent,title,desc,level,action
                }) {
        this.device = device;
        this.thingModelId = thingModelId;
        this.alarmEvent = alarmEvent??device?.name;
        this.title="["+device?.name+"] : " +(title??"")
        this.action = action??Utils.ACTION.new;
        this.desc = desc??"";
        this.level = level??Utils.LEVEL.low;
        this.group=device?.server_attrs?.notify ?? [];
        this.tdata=device?.telemetry_data?.[thingModelId]
    }
     _alarmProcess(info){}
     alarm(info){
         let rslt=this._alarmProcess(info);
         if (rslt === false) { return null}
         if (!Object.values(Utils.ACTION).includes(this?.action)) return null;
         if (this?.action===Utils.ACTION.no) return null
        return {
            delayms: 0,
            abort_previous_timer: true,
            actions: [{
                method: "alarm",
                params: {
                    _eui:this.device.eui,
                    action:this.action,
                    name:this.alarmEvent,
                    title:this.title,
                    level:this.level,
                    desc:this.desc,
                    group:this.group
                }
            }]
        }
    }
}
export class RPCHelper {
    constructor(){}
    static reset() {    return Buffer.from("CF03090101", "hex") }
    static redo()  {    return Buffer.from("CF03090102", "hex") }
    static join()  {    return Buffer.from("CF03090104", "hex") }
    static makeAlarm(
        {
            alarms,
            eui,
            name,
            action,
            group,
            title,
            desc,
            level
        }
    ){
        const ACTION = {  no:"no","new": 'new',clear: 'clear'};
        switch (action){
            case ACTION.clear: break;
            case ACTION.new:
                let alarmInfo=alarms[name]
                if (alarmInfo==undefined){break}
                if (alarmInfo.title!==title){break}
                if(alarmInfo.desc!==desc) {break}
                if (alarmInfo.level!==level) {break}
                action = ACTION.no
                break
            default: return null
        }
        if(action===ACTION.no){ return null}
        return [
            {
                sleepTimeMs: 0,
                target: eui,
                type:"alarm",
                dnMsg: {
                    action:action,
                    data:{
                        alarm_name: name,
                        notice_groups: group,
                        title: title,
                        desc:  desc,
                        level: level,
                    }
                }
            }
        ]
    }
    static paraWrite(name,type,addr,value) {
        if(name==="workMode") {
            let wBuffer=Buffer.alloc(4);
            wBuffer[0]=RPCHelper.getFuncReadCode(name)
            wBuffer[1]=2
            let valBuffer=BufferTKL.write(value,type);
            valBuffer.copy(wBuffer,2);
            return wBuffer;
        }
        let typeInfo=BufferTKL.getTypeInfo(type)
        let writeBuffer=Buffer.alloc(typeInfo.dataLen+4);
        writeBuffer[0]=RPCHelper.getFuncWriteCode(name)
        writeBuffer[1]=2+typeInfo.dataLen;
        writeBuffer[2]=Utils.parseVal(addr)
        writeBuffer[3]=typeInfo.dataLen;
        let valBuffer=BufferTKL.write(value,type);
        valBuffer.copy(writeBuffer,4);
        return writeBuffer;
    }
    static paraRead(name,type,addr) {
        if(name==="workMode") {
           let rBuffer=Buffer.alloc(2);
           rBuffer[0]=RPCHelper.getFuncReadCode(name)
            rBuffer[1]=0
            return rBuffer;
        }
        let readBuffer=Buffer.alloc(4);
        let typeInfo=BufferTKL.getTypeInfo(type)
        readBuffer[0]=RPCHelper.getFuncReadCode(name)
        readBuffer[1]=2;
        readBuffer[2]=Utils.parseVal(addr);
        readBuffer[3]=typeInfo.dataLen;
        return readBuffer;
    }
    static getFuncWriteCode(name){
        switch(name){
            case "app": return 0xCF;
            case "cf" : return 0xC9;
            case "fw" : return 0xC1;
            case "radio": return 0xC2;
            case "ds": return 0xC4;
            case "workMode":return 0xc3;

            default: return 0xCF;
        }
    }
    static getFuncReadCode(name){
        switch(name){
            case "app": return 0x8F;
            case "cf" : return 0x89;
            case "fw" : return 0x81;
            case "radio": return 0x82;
            case "ds":return 0x84;
            case "workMode": return 0x83;
            default: return 0x8F;
        }
    }
    static arrangePara(paraInfo) {
        const paraList = {
            app: [],
            fw: [],
            cf: [],
            radio: [],
            ds:[]
        };
        Object.keys(paraInfo).forEach(key => {
            const match=key.split("_")
            let prefix=match[0]
            let regAddr=parseInt(match[1],10)
            if (!paraList[prefix]) return;
            paraList[prefix].push({
                regAddr:regAddr,
                ...paraInfo[key]
            })
        })
        Object.keys(paraList).forEach(type => {
            paraList[type].sort((a, b) => a.regAddr - b.regAddr);
        });
        return paraList;
    }
    static _preDealParams (
        {
            serverParaDef,
            paraDef,
            params
        }) {
        return {
            serverParaDef:serverParaDef,
            paraDef:paraDef,
            params:params
        }
    }
    static _postBuild (
        {
            paraDef,
            writeBuffer,
            readBuffer,
            serverPara,
            log
        }) {
        return {
            writeBuffer:writeBuffer,
            readBuffer:readBuffer,
            serverPara:serverPara,
            log:log
        }
    }
    static _freshPara({
                          funcName,
                          writeBuffer,
                          readBuffer,
                          startIndex,
                          writeIndex,
                          readIndex,
                          regBytes,
                          nextReg
                      }){
        writeBuffer[startIndex+1]=regBytes+2;
        writeBuffer[startIndex+3]=regBytes;
        readBuffer[readIndex]=RPCHelper.getFuncReadCode(funcName);
        readBuffer[readIndex+1]=2;
        readBuffer[readIndex+2]=writeBuffer[startIndex+2];
        readBuffer[readIndex+3]=regBytes;
        readIndex+=4
        startIndex=writeIndex
        writeBuffer[startIndex]=RPCHelper.getFuncWriteCode(funcName);
        writeBuffer[startIndex+2]=nextReg;
        writeIndex+=4
        return {
            startIndex:startIndex,
            writeIndex:writeIndex,
            readIndex:readIndex,
        }
    }
    static buildmodbusFrameRead(addr,code,regStart,count){
        let cmdBuffer=Buffer.from([0x01,0x06,0x03,0x04,0x05,0x06,0x07,0x08]);
        cmdBuffer[0]=Utils.parseVal(addr);
        cmdBuffer[1]=Utils.parseVal(code);
        regStart=Utils.parseVal(regStart);
        cmdBuffer.writeUInt16BE(regStart,2)
        count=Utils.parseVal(count)
        cmdBuffer.writeUInt16BE(count,4)
        let crcSum=Utils.crc16Modbus(cmdBuffer,6)
        cmdBuffer.writeUInt16LE(Number(crcSum),cmdBuffer.length-2)
        return cmdBuffer
    }
    static buildmodbusFrame10({
                                  serverParaDef,
                                  paraDef,
                                  params
                              }){
        const apara=this._preDealParams({
            serverParaDef:serverParaDef,
            paraDef:paraDef,
            params:params});
        serverParaDef=apara.serverParaDef;
        paraDef=apara.paraDef;
        params=apara.params
        let valList=[]
        let log=[]
        let serverPara={}
        if (serverParaDef==undefined) {serverParaDef=[]}
        if (paraDef==undefined) { paraDef={}}
        if (params==undefined) {params={}}
        serverParaDef.forEach(item=>{
            const para=params[item.field_name]
            if (para==undefined) { return }
            serverPara[item.field_name]=para;
        })
        let regAddr=Utils.parseVal(paraDef.paraList[0].index)
        let totalLength=0
        paraDef.paraList.forEach(item=>{
            let logItem={}
            let val=params[item.field_name]
            if (val==undefined) {
                if (item.default==undefined) { return}
                val=item.default
            }
            val=Utils.parseVal(val)
            serverPara[item.field_name]=val
            logItem.valName=item.name
            logItem.field_name=item.field_name;
            logItem.value=val
            if (item.coefficient!=undefined){
                if (typeof item.coefficient ==="number") {  val=(val/item.coefficient)|0}
            }
            log.push(logItem)
            let valBuffer=BufferTKL.write(val,item.type)
            item.valBuffer=valBuffer
            totalLength+=valBuffer.length
        })
        let writeBuffer=Buffer.alloc(totalLength+9)
        writeBuffer[0]=Utils.parseVal(paraDef.addr)
        writeBuffer[1]=0x10
        writeBuffer.writeUInt16BE(regAddr,2)
        writeBuffer.writeUInt16BE(totalLength/2,4)
        writeBuffer.writeUInt8(totalLength,6)
        let index=7
        paraDef.paraList.forEach(item=>{
            if (item.valBuffer==undefined) { return}
            item.valBuffer.copy(writeBuffer,index)
            index+=item.valBuffer.length
        })
        let crcSum=Utils.crc16Modbus(writeBuffer,writeBuffer.length-2)
        writeBuffer.writeUInt16LE(Number(crcSum),writeBuffer.length-2)
        return RPCHelper._postBuild({
            writeBuffer:writeBuffer,
            readBuffer:Buffer.alloc(0),
            serverPara:serverPara,
            log:log
        })
    }
    static buildmodbusFrame06({
                                  serverParaDef,
                                  paraDef,
                                  params
                              }){
        const apara=RPCHelper._preDealParams({
            serverParaDef:serverParaDef,
            paraDef:paraDef,
            params:params});
        serverParaDef=apara.serverParaDef;
        paraDef=apara.paraDef;
        params=apara.params
        let valList=[]
        let log=[]
        let serverPara={}
        if (serverParaDef==undefined) {serverParaDef=[]}
        if (paraDef==undefined) { paraDef={}}
        if (params==undefined) {params={}}
        serverParaDef.forEach(item=>{
            const para=params[item.field_name]
            if (para==undefined) { return }
            serverPara[item.field_name]=para;
        })
        let writeBuffer=Buffer.from([0x01,0x06,0x03,0x04,0x05,0x06,0x07,0x08]);
        writeBuffer[0]=Utils.parseVal(paraDef.addr);
        let para=paraDef.paraList[0]
        let regAddr=Utils.parseVal(para.index)
        writeBuffer.writeUInt16BE(regAddr,2)
        let logItem={}
        let val=params[para.field_name]
        if (val==undefined) {
            if (para.default==undefined) { return}
            val=para.default
        }
        val=Utils.parseVal(val)
        serverPara[para.field_name]=val
        logItem.valName=para.name
        logItem.field_name=para.field_name;
        logItem.value=val
        if (para.coefficient!=undefined){
            if (typeof para.coefficient ==="number") {  val=(val/para.coefficient)|0}
        }
        log.push(logItem)
        let valBuffer=BufferTKL.write(val,para.type)
        valBuffer.copy(writeBuffer,4)
        let crcSum=Utils.crc16Modbus(writeBuffer,6)
        writeBuffer.writeUInt16LE(Number(crcSum),writeBuffer.length-2)
        return RPCHelper._postBuild({
            writeBuffer:writeBuffer,
            readBuffer:Buffer.alloc(0),
            serverPara:serverPara,
            log:log
        })
    }
    static buildSensorFrame({
                                serverParaDef,
                                paraDef,
                                params
                            }){
        const apara=RPCHelper._preDealParams({
            serverParaDef:serverParaDef,
            paraDef:paraDef,
            params:params});
            serverParaDef=apara.serverParaDef;
            paraDef=apara.paraDef;
            params=apara.params
            if (serverParaDef==undefined) {serverParaDef=[]}
            if (paraDef==undefined) { paraDef={}}
            if (params==undefined) {params={}}
            if (paraDef.cmd_header==undefined) { return null}
            paraDef.headerBuffer=Buffer.from(paraDef.cmd_header.replaceAll(" ",""),"hex")
            let valList=[]
            let log=[]
            let serverPara={}
            serverParaDef.forEach(item=>{
                const para=params[item.field_name]
                if (para==undefined) { return }
                serverPara[item.field_name]=para;
            })
            let totalLen=0
            paraDef.paraList.forEach(para => {
                let logItem={}
                let typeInfo=BufferTKL.getTypeInfo(para.type)
                let writeBuffer=Buffer.alloc(typeInfo.dataLen);
                let val=params[para.field_name]
                if (val==undefined) {
                    if (para.default==undefined) { return}
                    val=para.default
                }
                serverPara[para.field_name]=val
                logItem.valName=para.name
                logItem.field_name=para.field_name;
                logItem.value=val
                if (para.coefficient!=undefined){
                    if (typeof para.coefficient ==="number") {  val=(val/para.coefficient)|0}
                }
                let valBuffer=BufferTKL.write(val,para.type)
                para.valBuffer=valBuffer
                totalLen+=para.valBuffer.length;
                valList.push(para)
                log.push(logItem)
            })
            let writeBuffer=Buffer.alloc(totalLen);
            let dataIndex=0
            valList.forEach(para => {   para.valBuffer.copy(writeBuffer,dataIndex);dataIndex+=para.valBuffer.length})
            return RPCHelper._postBuild({
                writeBuffer:writeBuffer,
                readBuffer:Buffer.alloc(0),
                serverPara:serverPara,
                log:log
            })
    }
    static buildFrame({
                          serverParaDef,
                          paraDef,
                          params
                      }){
        const apara=RPCHelper._preDealParams({
            serverParaDef:serverParaDef,
            paraDef:paraDef,
            params:params});
        serverParaDef=apara.serverParaDef;
        paraDef=apara.paraDef;
        params=apara.params
        let readOnly=false
        if(serverParaDef==undefined){ serverParaDef=[]}
        if(paraDef==undefined){ paraDef={}}
        if(params==undefined){params={}}
        const paraList=RPCHelper.arrangePara(paraDef)
        let serverPara={}
        let keys = Object.keys(params)
        if (keys.length===0 || (keys.length === 1 && keys[0] == "_eui")) {readOnly=true}
        serverParaDef.forEach(item=>{
            const para=params[item.field_name]
            if (para==undefined && !readOnly) { return }
            serverPara[item.field_name]=para;
        })
        const maxWriteBytes=180
        const maxReadBytes=50
        let writeBuffer=Buffer.alloc(maxWriteBytes);
        let readBuffer=Buffer.alloc(maxReadBytes);
        let writeIndex=4;
        let readIndex=0;
        let regBytes=0
        let nextReg=-1
        let startIndex=0;
        let totalWriteBytes=0;
        let totalReadBytes=0;
        let log=[]
        Object.entries(paraList).forEach(([funcName,list])=>{
            if (list.length===0) {return}
            list.forEach(para=>{
                let logItem={}
                let val=undefined
                let typeInfo
                if((writeIndex+8)>maxWriteBytes) return
                if((readIndex+4)>maxReadBytes)   return
                if (para?.field_name==undefined) return
                if(!readOnly){
                    if (params==undefined)           return
                    if (params[para.field_name]==undefined)  return
                    val=params[para.field_name];
                    if(val==="nc"||val===BufferTKL.INVALID_STR||val===BufferTKL.INVALID_NUM)  return
                }
                typeInfo=BufferTKL.getTypeInfo(para.type)
                if(typeInfo.type==="invalid") return
                logItem.valName=para.name
                logItem.field_name=para.field_name;
                logItem.value=val
                log.push(logItem)
                if (nextReg===-1) {
                    writeBuffer[startIndex]=RPCHelper.getFuncWriteCode(funcName)
                    nextReg=para.regAddr;
                    writeBuffer[startIndex+2]=nextReg;
                }
                if (nextReg!==para.regAddr){
                    totalWriteBytes+=4+regBytes
                    totalReadBytes+=4
                    const retVal=RPCHelper._freshPara({
                        funcName:funcName,
                        writeBuffer:writeBuffer,
                        readBuffer:readBuffer,
                        startIndex:startIndex,
                        writeIndex:writeIndex,
                        readIndex:readIndex,
                        regBytes:regBytes,
                        nextReg:para.regAddr
                    });
                    nextReg=para.regAddr;
                    startIndex=retVal.startIndex;
                    writeIndex=retVal.writeIndex;
                    readIndex=retVal.readIndex;
                    regBytes=0
                }
                //let payHandler=new BufferTKL(Buffer.alloc(0));
                let valBuffer
                if(val!=undefined){
                    if (para?.coefficient!=undefined && (typeof para?.coefficient)==="number" ){
                        val=val/para.coefficient
                    }
                    valBuffer=BufferTKL.write(val,para.type);
                    valBuffer.copy(writeBuffer,writeIndex);
                }
                writeIndex+=typeInfo.dataLen;
                nextReg+=typeInfo.dataLen;
                regBytes+=typeInfo.dataLen;
            })
            totalWriteBytes+=4+regBytes
            totalReadBytes+=4
            const retv=RPCHelper._freshPara({
                funcName:funcName,
                writeBuffer:writeBuffer,
                readBuffer:readBuffer,
                startIndex:startIndex,
                writeIndex:writeIndex,
                readIndex:readIndex,regBytes:regBytes,
                nextReg:0
            });
            nextReg=-1;
            startIndex=retv.startIndex;
            writeIndex=retv.writeIndex;
            readIndex=retv.readIndex;
            regBytes=0
        })
        if (totalWriteBytes===4) {totalWriteBytes=0;totalReadBytes=0}
        let retWBuffer=Buffer.alloc(totalWriteBytes);
        let retRBuffer=Buffer.alloc(totalReadBytes);
        writeBuffer.copy(retWBuffer,0,0,totalWriteBytes);
        readBuffer.copy(retRBuffer,0,0,totalReadBytes);
        return RPCHelper._postBuild({
            writeBuffer:retWBuffer,
            readBuffer:retRBuffer,
            serverPara:serverPara,
            log:log
        })
    }
    static cj188reader(addr,model) {
        let buffer =Buffer.from("FE FE FE FE FE 68 20 AA AA AA AA AA AA AA 03 03 81 0A 01 C0 16".replaceAll(" ",""),"hex")
        let addrBuffer=Utils.parseToBuffer(addr)
        if (addrBuffer.length!==7) { return Buffer.alloc(0)   }
        for (let i = 0; i < 7; i++) {   buffer[7+i] = addrBuffer[6-i];  }
        let modelBuffer=Utils.parseToBuffer(model);
        if(modelBuffer.length!==5) { return Buffer.alloc(0)}
        modelBuffer.copy(buffer,14,0,5);
        let crc=Utils.crcSum(buffer.slice(5,buffer.length-2),buffer.length-7)
        buffer[buffer.length-2]=crc&0x00FF
        return buffer
    }
    static modbusAction(addr,code,regAddr,regVal,valBuffer=undefined) {
        let baseBuffer=Buffer.from([0x01,0x03,0x01,0x03,0x03,0x04,0x05,0x06])
        let buffer=baseBuffer;
        if (valBuffer!=undefined) {
            buffer=Buffer.alloc(baseBuffer.length+valBuffer.length);
            baseBuffer.copy(buffer);
        }
        buffer[0]= Utils.parseVal(addr)
        buffer[1]=Utils.parseVal(code)
        const regStart=Utils.parseVal(regAddr);
        const regValue=Utils.parseVal(regVal);
        buffer.writeUInt16BE(regStart,2);
        buffer.writeUInt16BE(regValue,4);
        let crcSum=Utils.crc16Modbus(buffer,6)
        buffer.writeUInt16LE(Number(crcSum),buffer.length-2)
        return buffer
    }
    static makeDnDataClear(eui){
        return {
            sleepTimeMs: 0,
            dnMsg: {
                version: "3.0",
                type: "dataClear",
                if: "loraWAN",
                moteeui: eui,
                token: new Date().getTime(),
                userdata: {
                    confirmed: false,
                    fpend: false,
                    port: 214,
                    TxUTCtime: "",
                    payload: "",
                    dnWaitms: 0,
                    type:  "data",
                    intervalms:0,
                }
            }
        }
    }
    static makeMSG(
        {
            msgType,
            device,
            dnBuffer,
            type,
            port,
            confirmed,
            sleepTime,
            dnWaitms,
            intervalms,
        }){
        if(msgType==undefined){msgType=Utils.msgType.user}
        if (msgType===Utils.msgType.swDown) {
            let cmdBuf = Buffer.alloc(5+dnBuffer.length)
            cmdBuf[0]=0xFF;cmdBuf[1]=0xAA
            dnBuffer.copy(cmdBuf,2)
            let crc=Utils.crc16Modbus(cmdBuf,dnBuffer.length+2)
            cmdBuf.writeUInt16LE(crc,dnBuffer.length+2)
            cmdBuf[cmdBuf.length-1]=0x40
            let payBuffer =Buffer.alloc(9+cmdBuf.length);
            payBuffer[0]=0xdc
            let devEuiBuffer=BufferTKL.write(device.eui,"hexLE8")
            devEuiBuffer.copy(payBuffer,1)
            cmdBuf.copy(payBuffer,9);
            let sf=device?.shared_attrs?.swSF
            if (sf==undefined){ return null}
            let bw=device?.shared_attrs?.swBW.replaceAll("kHz","")
            return {
                sleepTimeMs: sleepTime??0,
                dnMsg:{
                version: "3.1",
                type: "data",
                if: "loraSW",
                moteeui: device.eui,
                token: new Date().getTime(),
                userdata: {
                    payload: payBuffer.toString("base64"),
                    dnWaitms: dnWaitms??0,
                    intervalms: intervalms??0,
                    specify: {
                        txpk: {
                            imme: true,
                            tmst: 0,
                            tmms: 0,
                            time: "",
                            freq: device?.shared_attrs?.swFreq / 1000000,  // shared_attrs中获取
                            rfch: 0,
                            powe: 22,
                            modu: "LORA",
                            datr: `SF${sf}BW${bw}`,   // shared_attrs中获取
                            codr: "4/5",
                            fdev: 0,
                            ipol: false,
                            prea: Utils.timingPerformanceCalc(sf, bw, device?.shared_attrs?.swPeriod),
                            ncrc: false
                        }
                    }
                }
                }
            }
        }else if (msgType===Utils.msgType.transParent||msgType===Utils.msgType.paras||msgType===Utils.msgType.user) {
            if (msgType===Utils.msgType.transParent) {
                port=51
                if (dnWaitms==undefined){dnWaitms=2000}
            }else if (msgType===Utils.msgType.paras) {
                port=214
                if (dnWaitms==undefined){dnWaitms=2000}
            }
            return {
                    sleepTimeMs: sleepTime??0,
                    dnMsg: {
                        version: "3.0",
                        type: type??"data",
                        if: "loraWAN",
                        moteeui: device.eui,
                        token: new Date().getTime(),
                        userdata: {
                            confirmed: confirmed??false,
                            fpend: false,
                            port: port,
                            TxUTCtime: "",
                            payload: dnBuffer.toString("base64"),
                            dnWaitms: dnWaitms??0,
                            type:  "data",
                            intervalms: intervalms??0,
                        }
                    }
            }
        }
    }

    /**
     * 物模型通用 rpcHook(设备对接通用规则 / KB-48)—— 只构造并返回 action,不碰 result。
     * 读 device.server_attrs["rpc_hook_<idName>"],为非空字符串则返回一条调用该 RPC 的 action
     * (唯一业务入参 hookPara = { as, name, idName, id, parsed });未配置则返回 null。
     * 是否把返回的 action 加进 result.actions,由物模型 parser 自行判断。
     *
     * 注意:物模型 payload_parser 跑在 vm2 沙箱,本方法定义在沙箱外读不到沙箱全局,
     * 故 device / msg / thingModelId / idName / name / parsed 必须由 parser 显式传入。
     * idName/name 沙箱未注入须传字面量;thingModelId 沙箱已注入(=物模型 id)。
     * parsed 传 parser 的解析结果 { telemetry_data, server_attrs, shared_attrs }。
     *
     * 调用方应做类型检查以兼容未部署本方法的老平台:
     *   if (typeof RPCHelper.buildRpcHookAction === "function") {
     *     const a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName, name, parsed:{...} });
     *     if (a) { result.actions = result.actions || []; result.actions.push(a); }
     *   }
     *
     * @param {object} ctx { device, msg, thingModelId, idName, name, parsed }
     * @returns {object|null} action 对象(命中 hook)或 null(未配置 hook)
     */
    static buildRpcHookAction({ device, msg, thingModelId, idName, name, parsed } = {}) {
        const hookRpc = (device && device.server_attrs) ? device.server_attrs["rpc_hook_" + idName] : null;
        if (typeof hookRpc !== "string" || !hookRpc.trim()) return null;
        const p = parsed || {};
        return {
            method: hookRpc.trim(),
            params: {
                _eui: device ? device.eui : undefined,
                hookPara: {
                    as: msg,
                    name: name ?? null,
                    idName: idName ?? null,
                    id: thingModelId ?? null,
                    parsed: {
                        telemetry_data: p.telemetry_data ?? null,
                        server_attrs: p.server_attrs ?? null,
                        shared_attrs: p.shared_attrs ?? null
                    }
                }
            }
        };
    }
}