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