Skip to content

tklHelper Source Code ​

Library version: 1.01.043. Source: tkl-opt/tooling/integration-tools/tklHelper.js; synchronized on 2026-10-12.

Manual and V3 integration examples. This is the helper library source. Platform scripts use the sandbox globals directly; do not paste these module imports into script bodies.

Runtime requirement

Updating this code page does not synchronize the backend helper or deploy the platform. Confirm that the target runtime has loaded 1.01.043 before using V3. The library version and transaction version: 2 / 3 are separate; omitting the transaction version still selects 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
                    }
                }
            }
        };
    }
}