最近项目中用js写了一个关于645协议的解析脚本。其中解析了部分字段,大部分是用于抄读的功能,支持时间和日期的读写。在这里记录。如果是前端的同志,可以忽略,因为这个属于公司项目中的偏业务层面的脚本,感觉跟前端关系不大,只是语言使用了js而已,不过花了挺多时间的,所以想在这里记录留念一下。以后再做这方面的协议解析报文的工作会有经验一点。
解析示例
// 以下为脚本模版,您可以基于以下模版进行脚本编写
/**
* 物模型方法
*/
const METHOD = {
post: 'thing.event.property.post',
get: 'thing.service.property.get',
set: 'thing.service.property.set',
action: 'thing.service.{identifier}',
}
/**
* 设备到云消息解析
* 将设备的自定义格式数据转换为标准协议的数据,设备上报数据到物联网平台时调用
* 模拟执行输入参数示例 {"data":"FE0304229401197EF2","identifier":"Version"}
* @param {string} jsonString "{\"data\": \"FE0304229401197EF2\", \"identifier\": \"Version\"}"
* @param {string} jsonString.data 报文帧
* @param {string} jsonString.identifier 物模型标识符
* @returns {object} result
* @returns {string} result.data 物模型属性值
* @returns {string} result.identifier 物模型标识符
* @returns {string} result.method 物模型方法
* thing.event.property.post (主动上报) | thing.service.property.get (属性获取) | thing.service.property.set (属性设置) | thing.service.${identifier} (动作调用)
*/
function rawDataToProtocol(jsonString) {
const jsonObj = {}
return jsonObj
}
/**
* 云到设备消息解析
* 将标准协议的数据转换为设备能识别的格式数据,物联网平台给设备下发数据时调用
* 模拟执行输入参数,示例 {"address":"1","functionCode":"0x04","params":{"FlowRate":true}, "deviceName": "123456789012"}
* @param {string} jsonString "{\"address\":\"1\",\"functionCode\":\"0x04\",\"params\":{\"FlowRate\":true}, \"deviceName\":\"123456789012\"}"
* @param {string} jsonString.address 从机地址
* @param {string} jsonString.functionCode 功能码
* @param {object} jsonString.params 标识符 key-value 对
* @param {string} jsonString.deviceName 设备名称
* @returns {string} rawdata 设备能识别的格式数据
*/
function protocolToRawData(jsonObj) {
const rawdata = ''
return rawdata
}
645协议部分数据标识解析-js脚本
/**
* 645三相电表脚本
* v1 2024-12-27 17:15
* v2 2025-01-16 10:35 (1)补充函数校验
* (2)base64编码
* (3)FUNCTION_CODE_MAP结构调整
* (4)jsonString入参结构调整
*/
/**
* 前提:此脚本目前仅支持读取数据并且仅支持FUNCTION_CODE_MAP中的数据标识读取,并且仅支持一次读取一个数据标识的情况
* 仅支持时间和日期的写入
*
* 属性的控制码C:读数据 11H(设备接收-读数据)| 91H(设备上报-无后续数据帧)| D1H 异常应答帧
* 写数据 14H(设备接收-写数据)| 94H(设备上报-无后续数据帧)| D4H 异常应答帧
* dataCode:数据标识;顺序是:DI3 DI2 DI1 DI0
* long:设备上报的数据长度(单位:字节),这里long是16进制, 2 => 0x02
* dataFormat:数据格式,保留几位小数
* desc:属性内容
* symbol:0正1负号 (有些数据上报的值需要考虑正负)
* ASCII :是否按照ASCII解析
* dataType:数据类型:字符型(text) | 单精度浮点型(float)| 双精度浮点型(double)
*/
/**
* 物模型方法
* thing.event.property.post (主动上报)
* thing.service.property.get (属性获取)
* thing.service.property.set (属性设置)
* thing.service.${identifier} (动作调用)
*/
const Buffer = require('buffer/').Buffer
const METHOD = {
post: 'thing.event.property.post',
get: 'thing.service.property.get',
set: 'thing.service.property.set',
action: 'thing.service.{identifier}',
}
const FUNCTION_CODE_MAP = {
//485子设备-645单相电表
//485子设备-645三相电表
// 属性抄读
get: {
Ua:{dataCode: "02010100",long:2, dataFormat: 1, desc: 'A相电压',symbol:0},
Ub:{dataCode: "02010200",long:2, dataFormat: 1, desc: 'B相电压',symbol:0},
Uc:{dataCode: "02010300",long:2, dataFormat: 1, desc: 'C相电压',symbol:0},
Ia:{dataCode: "02020100",long:3, dataFormat: 3, desc: 'A相电流',symbol:1},
Ib:{dataCode: "02020200",long:3, dataFormat: 3, desc: 'B相电流',symbol:1},
Ic:{dataCode: "02020300",long:3, dataFormat: 3, desc: 'C相电流',symbol:1},
Pa:{dataCode: "02030100",long:3, dataFormat: 4, desc: 'A相有功功率',symbol:1},
Pb:{dataCode: "02030200",long:3, dataFormat: 4, desc: 'B相有功功率',symbol:1},
Pc:{dataCode: "02030300",long:3, dataFormat: 4, desc: 'C相有功功率',symbol:1},
Qa:{dataCode: "02040100",long:3, dataFormat: 4, desc: 'A相无功功率',symbol:1},
Qb:{dataCode: "02040200",long:3, dataFormat: 4, desc: 'B相无功功率',symbol:1},
Qc:{dataCode: "02040300",long:3, dataFormat: 4, desc: 'C相无功功率',symbol:1},
Sa:{dataCode: "02050100",long:3, dataFormat: 4, desc: 'A相视在功率',symbol:1},
Sb:{dataCode: "02050200",long:3, dataFormat: 4, desc: 'B相视在功率',symbol:1},
Sc:{dataCode: "02050300",long:3, dataFormat: 4, desc: 'C相视在功率',symbol:1},
PFa:{dataCode: "02060100",long:2, dataFormat: 3, desc: 'A相功率因数',symbol:1},
PFb:{dataCode: "02060200",long:2, dataFormat: 3, desc: 'B相功率因数',symbol:1},
PFc:{dataCode: "02060300",long:2, dataFormat: 3, desc: 'C相功率因数',symbol:1},
FPEnergy:{dataCode: "00010000",long:4, dataFormat: 2, desc: '正向有功总电能',symbol:0},
OPEnergy:{dataCode: "00020000",long:4, dataFormat: 2, desc: '反向有功总电能',symbol:0},
FPEnergy1:{dataCode: "00010100",long:4, dataFormat: 2, desc: '尖时段有功总电能',symbol:0},
FPEnergy2:{dataCode: "00010200",long:4, dataFormat: 2, desc: '峰时段有功总电能',symbol:0},
FPEnergy3:{dataCode: "00010300",long:4, dataFormat: 2, desc: '平时段有功总电能',symbol:0},
FPEnergy4:{dataCode: "00010400",long:4, dataFormat: 2, desc: '谷时段有功总电能',symbol:0},
FPEnergyA:{dataCode: "00150000",long:4, dataFormat: 2, desc: 'A相正向有功电能',symbol:0},
FPEnergyB:{dataCode: "00290000",long:4, dataFormat: 2, desc: 'B相正向有功电能',symbol:0},
FPEnergyC:{dataCode: "003D0000",long:4, dataFormat: 2, desc: 'C相正向有功电能',symbol:0},
OPEnergyA:{dataCode: "00160000",long:4, dataFormat: 2, desc: 'A相反向有功电能',symbol:0},
OPEnergyB:{dataCode: "002A0000",long:4, dataFormat: 2, desc: 'B相反向有功电能',symbol:0},
OPEnergyC:{dataCode: "003E0000",long:4, dataFormat: 2, desc: 'C相反向有功电能',symbol:0},
//Pdmd:{dataCode: "02800004",long:3, dataFormat: 4, desc: '有功需量',symbol:1},
//Qdmd:{dataCode: "02800005",long:3, dataFormat: 4, desc: '无功需量',symbol:1},
GridFreq:{dataCode: "02800002",long:2, dataFormat: 2, desc: '频率',symbol:0},
Temp:{dataCode: "02800007",long:2, dataFormat: 1, desc: '温度',symbol:1},
P:{dataCode: "02030000",long:3, dataFormat: 4, desc: '总有功功率',symbol:1},
Q:{dataCode: "02040000",long:3, dataFormat: 4, desc: '总无功功率',symbol:1},
S:{dataCode: "02050000",long:3, dataFormat: 4, desc: '总视在功率',symbol:1},
PF:{dataCode: "02060000",long:2, dataFormat: 3, desc: '总功率因数',symbol:1},
PEnergy:{dataCode: "00000000",long:4, dataFormat: 2, desc: '组合有功总电能',symbol:1},
//新字段
Version:{dataCode: "04800001",long:20, dataFormat: 0, desc: '版本号',symbol:0,ASCII:true, dataType: "text"},
TimeYMDW:{dataCode: "04000101",long:4, dataFormat: 0, desc: '年月日星期',symbol:0,dataType: "text"},
TimeHMS:{dataCode: "04000102",long:3, dataFormat: 0, desc: '时分秒',symbol:0,dataType: "text"},
},
// 属性设置(写)
set: {
TimeYMDW:{dataCode: "04000101",long:4, dataFormat: 0, desc: '年月日星期',symbol:0,dataType: "text"},
TimeHMS:{dataCode: "04000102",long:3, dataFormat: 0, desc: '时分秒',symbol:0,dataType: "text"},
},
// 动作调用
action: {
},
}
/**
* 设备到云消息解析
* 模拟执行输入参数
* 1. Ua
* {"inputConfig":{"deviceName":"010070980000","port":1,"address":1,"identifier":"PF"},"result":{"data":"aAAAmHAAAWiRBjMzOTXMu8sW","port":1}}
* 报文base解码:aAAAmHAAAWiRBjMzOTXMu8sW => 6800009870000168910633333935CCBBCB16
* @param {string} jsonString '{"inputConfig":{"deviceName":"","port":1,"address":1,"identifier":""},"result":{"data":"","port":""}}'
* @param {string} jsonString.inputConfig 输入参数元配置(设备名称、端口号、从机地址、物模型标识符)
* @param {string} jsonString.result 设备返回的数据(16进制报文、端口号、设备名改)
* @returns {object} result
* @returns {string} result.data 物模型属性值
* @returns {string} result.identifier 物模型标识符
* @returns {string} result.method 物模型方法
* thing.event.property.post (主动上报) | thing.service.property.get (属性获取) | thing.service.property.set (属性设置) | thing.service.${identifier} (动作调用)
*/
//设备上报
function rawDataToProtocol(jsonString) {
const jsonData = JSON.parse(jsonString)
//==================================变量部分===================================================//
//设备到云: 设备返回的报文帧
const rawData = base64ToHex(jsonData.result.data)
// 云到设备的设备号(这里用于校验,设备回复报文中的设备号是否正确)
const deviceName = jsonData.inputConfig.deviceName || ""
// 云到设备的数据标识(这里用于校验,设备回复报文中的标识符是否正确)
const identifier = jsonData.inputConfig.identifier || ""
// 定义输出结果:result
let result = {
deviceName:"", //设备号(通讯地址)
data:"", //读写的结果返回值
identifier:"", //读写的标识符
method:"" //物模型方法
};
//////////////////////////////////////////校验部分////////////////////////////////////////////////
// jsonData格式校验
if ((!('inputConfig' in jsonData)) || (!('result' in jsonData))) {
throw new Error(`输入参数格式不正确:(${jsonData})`);
}
// jsonData.inputConfig格式校验
if ((!('deviceName' in jsonData.inputConfig)) || (!('port' in jsonData.inputConfig))|| (!('identifier' in jsonData.inputConfig))) {
throw new Error(`inputConfig格式不正确:(${jsonData.inputConfig})`);
}
// jsonData.result格式校验
if ((!('data' in jsonData.result)) || (!('port' in jsonData.result))) {
throw new Error(`result格式不正确:(${jsonData.result})`);
}
// 端口匹配校验
if (jsonData.inputConfig.port !== jsonData.result.port) {
throw new Error(`inputConfig端口${jsonData.inputConfig.port}与设备上报端口${jsonData.result.port}不匹配`);
}
// 设备号匹配校验-校验放入后面设备抄读91 和 属性设置94(写)的函数中
// 数据标识校验-放在后面的设备抄读91中,属性设置94(写)设备不回复数据标识
// 从站异常应答校验
if (rawData.substring(16, 18) === "D1" || rawData.substring(16, 18)=== "D4") {
throw new Error(`从站异常应答,报文:${rawData}`);
}
// 从站异常应答校验
if (rawData.substring(16, 18) === "D1" || rawData.substring(16, 18)=== "D4") {
throw new Error(`从站异常应答,报文:${rawData}`);
}
// 查找字符串中两个 "68" 的位置 第1-2位 和 第15-16位 必须是"68"
if (rawData.substring(0, 2) !== '68') {
throw new Error('第1-2位必须是"68"');
}
if (rawData.substring(14, 16) !== '68') {
throw new Error('第15-16位必须是"68"');
}
const regex = /^\d+$/;
// 电表号校验
if (!regex.test(rawData.substring(2, 14))) {
throw new Error('电表号deviceName必须是12位数字');
}
// cs校验 判断
if (calculateChecksum(rawData.substring(0, rawData.length -4)) !== rawData.slice(-4, -2)) {
throw new Error('CS校验值不正确');
}
//----------------------------------------解析method-----------------------------------------------------------------//
// 判断一下上报的是thing.service.property.get (属性获取) | thing.service.property.set (属性设置)
result.method = rawData.substring(16, 18)=== "91"? METHOD['get']: rawData.substring(16, 18)=== "94"?METHOD['set']:""
//----------------------------------------解析-电表号-----------------------------------------------------------------//
// 截取deviceName(从第一个"68"(1-2位)到第二个"68"(15-16位)之间的部分(3-14位,12位电表号))
const first68Index = rawData.indexOf("68");
const second68Index = rawData.indexOf("68", first68Index + 1);
const deviceNameChunks = [];
const deviceNameTemp = rawData.substring(first68Index + 2, second68Index)
for (let i = 0; i < deviceNameTemp.length; i += 2) {
deviceNameChunks.push(deviceNameTemp.substring(i, i + 2));
}
// 反转数组,再拼接位字符串
const reversedChunks = deviceNameChunks.reverse().join('');
// 设备号匹配校验
if (jsonData.inputConfig.deviceName !== reversedChunks) {
throw new Error(`inputConfig设备号${jsonData.inputConfig.deviceName}与设备上报设备号${reversedChunks}不匹配`);
}
result.deviceName = reversedChunks; // 设备号
//----------------------------------------先判断是读还是写-----------------------------------------------------------------//
if(rawData.substring(16, 18) === "91"){
//----------------------------------------确认dataCode---------------------------------------------------//
// 控制码C(17-18位): 读数据回复91H | 写数据回复94H
// L = 0x04(一个数据标识长度)+ long (设备上报的数据长度long不定)(19-20位)(L内容不固定,但是仅占用1个字节)
// 所以需要从字符串中第17-20位后开始取L*2位数据:前8位表示:数据标识;后几位表示:设备上报的值
//读数据需要解析数据标识;写数据不用解析数据标识,设备回复94后面 + L =00H即为成功
// 数据标识8位:表示该条指令确定对哪个标识符发出指令和回复
let dataCode = rawData.substring(20, 28);
// console.log("上报的8位数据标识",dataCode)
// 将dataCode字符串每两位分为一组,组成数组groups
let groups = [];
for (let i = 0; i < dataCode.length; i += 2) {
groups.push(dataCode.substring(i, i + 2));
}
// groups每一项减去33H (51十进制) 并转为16进制字符串
let res = groups.map(group => {
let value = parseInt(group, 16); // 转换为十六进制数
let newValue = value - 0x33; // 减去33H
return newValue.toString(16).toUpperCase().padStart(2, '0'); // 转换回16进制并格式化为两位
});
//console.log("res",res)
// 接收方需要反转;反转res数组,res转成字符串,拼接最终结果
dataCode = res.reverse().join('');
// console.log("拼接最终结果",dataCode)
//----------------------------------------根据dataCode-解析数据标识---------------------------------------------------//
// 判断上报数据的正负
let symbol = "";
// 通过dataCode遍历 FUNCTION_CODE_MAP.get,匹配标识符
for (let key in FUNCTION_CODE_MAP.get) {
if (FUNCTION_CODE_MAP.get[key].dataCode === dataCode) {
if (key !== identifier){
throw new Error(`设备上报的数据标识${key}与平台下发的数据标识${identifier}不符`);
}
//根据dataCode-解析数据标识
result.identifier = key
//------------------------------------解析-上报数据------------------------------------//
// long是16进制的数据长度 (具体长度取决于645协议中的定义好的数据长度,这里是在FUNCTION_CODE_MAP.get中准备好,用long表示)
const long = convertNumber(FUNCTION_CODE_MAP.get[key].long,16,10)*2
// 设备上报的数据值
let dataValue = rawData.substring(28, 28+long);
// 按照每两位分隔字符串
const arr = [];
for (let i = 0; i < dataValue.length; i += 2) {
arr.push(dataValue.substring(i, i + 2));
}
// 准备工作:反转数组,并且每个字节都减去33H
const temp = arr.reverse();
const resultArray = temp.map(hex => {
// 将16进制字符串转换为十进制数
const decimalValue = parseInt(hex, 16);
// 减去33H(即51的十进制值)
const result = decimalValue - 0x33;
// 如果需要将结果转换回16进制字符串,可以使用toString(16)
return result.toString(16).toUpperCase().padStart(2, '0');
});
// console.log("反转数组resultArray",resultArray)
//----------------解析上报数据(按照ASCII解析)------------------------//
//判断字段是否需要使用ASCII解析
if(FUNCTION_CODE_MAP.get[key].ASCII){
// 将每个16进制字符串转换为对应的ASCII字符,.trim去除前后空格 返回给result.data
result.data = resultArray.map(hex => {
// 将16进制字符串转换为整数
const decimalValue = parseInt(hex, 16);
// 将整数转换为对应的ASCII字符
return String.fromCharCode(decimalValue);
}).join('').trim();
break;
}else{
//----------------解析上报数据(正常解析)------------------------//
// 将第一项从16进制字符串按照16进制转换(结果为十进制:80H <=> 128)
const firstItemDecimal = parseInt(resultArray[0], 16);
// console.log("firstItemDecimal",firstItemDecimal)
// 判断第一项是否大于等于80H(128 十进制值) && 是需要考虑正负数的标识符 ==> 负号
if (firstItemDecimal >= 0x80 && FUNCTION_CODE_MAP.get[key].symbol ===1) {
//首先可以确认上报数据符号为负号
symbol = "1"
// 用第一项减去80H(十进制值128)之后此时差值已变成十进制,再转成16进制
const result = convertNumber(firstItemDecimal - 0x80,10,16);
// 将结果转换回16进制字符串,返回给resultArray[0],并确保是两位数(结果是一位,前面补0)
resultArray[0] = result.toString(16).toUpperCase().padStart(2, '0');
// console.log('resultArray[0]',result,resultArray[0]); // 输出: '00'
} else {
//这里不用考虑符号,直接取值
// console.log('第一项小于80H');
symbol = "0"
}
const flippedDataValue = resultArray.join('');
//console.log('flippedDataValue',flippedDataValue);
//判断数据正负号:1负 0正
if(symbol === "1"){
//这里数据是负值,必然不是字符型(text)
// 判断保留几位小数(10 ** FUNCTION_CODE_MAP.get[key].dataFormat)))
// parseFloat转成Float型数据
result.data =parseFloat("-" +( flippedDataValue / (10 ** FUNCTION_CODE_MAP.get[key].dataFormat)));
}else{
//这里需要再判断一下数据类型为字符型(text)的情况
if(FUNCTION_CODE_MAP.get[key].dataType === "text"){
result.data =flippedDataValue ;
}else{
// 判断保留几位小数
result.data =parseFloat( flippedDataValue / (10 ** FUNCTION_CODE_MAP.get[key].dataFormat));
}
}
//console.log(temp,resultArray ,flippedDataValue);
break;
}
}
}
}else if(rawData.substring(16, 18) === "94"){
//设备上报,报文是0x94,则认为是写入成功
result.data = "94"
result.identifier = identifier
}
//返回构造的结果
return result
}
/**
*
* 云到设备消息解析
* 模拟执行输入参数
* 1. 抄读数据
*{"type":"get","params":{"identifier":"PF"},"deviceName":"010070980000"}
* 模拟结果:aAAAmHAAAWgRBDMzOTXCFg== (6800009870000168110433333935C216)
* 2. 属性设置
* {"type":"set","params":{"identifier":"TimeYMDW","inputData":{"TimeYMDW":"25021405"}},"deviceName":"112233445561"}
* 模拟结果:aGFVRDMiEWgUEDQ0Mzc1VVVVwMHCwzhHNVhsFg== (686155443322116814103434333735555555C0C1C2C3384735586C16)
* 将标准协议的数据转换为设备能识别的格式数据,物联网平台给设备下发数据时调用
* @param {string} jsonString "{\"type\":\"get\",\"params\":{\"identifier\":Time}}"
* @param {string} jsonString.type 指令类型 get(属性抄读)/set(属性设置)/action(动作调用)
* @param {object} jsonString.params key-value 键值对
* @param {object} jsonString.params.identifier 标识符
* @param {object} jsonString.params.inputData 输入参数(属性设置和动作调用类型使用)
* @param {string} jsonString.deviceName 设备名称
* @returns {string} rawdata 设备能识别的格式数据
*/
//设备接收
function protocolToRawData(jsonString) {
const jsonResult = JSON.parse(jsonString)
//==================================变量部分===================================================//
//类型type
const type = jsonResult.type || '';
//设备号
const deviceName = jsonResult.deviceName || '';
//下发给设备的数据标识
const Key_Real_Name = jsonResult.params.identifier || '';
//set时下发给设备的实际值(目前只支持写 日期 或者 时间)
const Value_Real_Name = type==="get"? "": type==="set" ? (jsonResult.params.inputData[Key_Real_Name] || '') :"";
//==================================校验部分===================================================//
const regex = /^\d+$/;
// 电表号校验
if (deviceName.length !== 12 || !regex.test(deviceName)) {
throw new Error('电表号deviceName必须是12位数字');
}
if ((!('type' in jsonResult)) || (!('params' in jsonResult))) {
throw new Error(` json格式不正确:(${jsonString}),缺少键值`);
}
if (type !== 'get' && type !== 'set' ) {
throw new Error(`类型type:(${type})不支持`);
}
// if ((!('identifier' in jsonResult.params)) || (!('inputData' in jsonResult.params))) {
// throw new Error(` params格式不正确:(${jsonResult.params})`);
// }
//当指令是get的时候:目前只支持读FUNCTION_CODE_MAP['get']中的数据标识,如果是其他的数据标识直接抛错
if (type === 'get' && (!(Key_Real_Name in FUNCTION_CODE_MAP['get']))) {
throw new Error(`此数据标识(${Key_Real_Name})不支持读取`);
}
//当指令是set的时候:目前只支持写FUNCTION_CODE_MAP['set']中的key,如果是写其他的数据标识直接抛错
if (type === 'set' && (!(Key_Real_Name in FUNCTION_CODE_MAP['set']))) {
throw new Error(`此数据标识(${Key_Real_Name})不支持写入`);
}
//当指令是set的时候: identifier的值 和 inputData的数据标识 不匹配直接抛错
if (type === 'set' && (!(Key_Real_Name in jsonResult.params.inputData))) {
throw new Error(`数据标识identifier:${Key_Real_Name}与inputData中数据标识不匹配`);
}
//校验 写功能,日期 的值:类型 长度 是否全部是1-9的数字
if (type === 'set' && Key_Real_Name === "TimeYMDW" && (Value_Real_Name.length !==8 ||(typeof Value_Real_Name !== "string") || !regex.test(Value_Real_Name))) {
throw new Error(`写入日期格式不正确:(${Value_Real_Name})`);
}
//校验 写功能,时间 的值:类型 长度 是否全部是1-9的数字
if (type === 'set' && Key_Real_Name === "TimeHMS" && (Value_Real_Name.length !==6 ||(typeof Value_Real_Name !== "string") || !regex.test(Value_Real_Name))) {
throw new Error(`写入时间格式不正确:(${Value_Real_Name})`);
}
//==========================第1-16位 "68 + 电表号(按每两位反转) + 68"====================//
// Step 1: 拼接起始部分
let rawData = '68'; // 第1-2位 "68"
// 电表号下发需要按照每两位分隔字符串,组成数组然后整体反转数组,在拼成字符串下发(接收也是,反转之后才是电表号)
const deviceNameChunks = [];
for (let i = 0; i < deviceName.length; i += 2) {
deviceNameChunks.push(deviceName.substring(i, i + 2));
}
// 反转数组,再拼接位字符串
const reversedChunks = deviceNameChunks.reverse().join('');
rawData += reversedChunks.padEnd(12, '0'); // 第3-14位 deviceName,若长度不足则用0填充
rawData += '68'; // 第15-16位
//==========================第17-20位 控制码 + 标识符位数===============================//
// Step 2: 读:11 写:14
const functionCode = type === 'get' ? '11' : type === 'set'? '14':"";
rawData += functionCode;
//控制码:读11 ,那么直接跟数据长度L = 04
if( functionCode === '11'){ // 读
rawData += "04"
//===================读=======第21-28位 数据标识符=========================================//
if (FUNCTION_CODE_MAP.get[Key_Real_Name]) {
let dataCode = FUNCTION_CODE_MAP.get[Key_Real_Name].dataCode;
let dataIdentifier = calculateDataIdentifier(dataCode);
rawData += dataIdentifier;
} else {
throw new Error(`FUNCTION_CODE_MAP中不支持读取此数据标识(${Key_Real_Name})`);
}
}else if(functionCode === '14'){ // 写
//控制码:写14 ,那么数据长度L =04H+04H(密码:等级+密码)+04H(操作者代码)+m(数据长度)
//以日期及星期(YYMMDDWW,数据长度为4个字节)为例 L = 04H + 04H + 04H +04H = 10
//以时间(hhmmss,数据长度为3个字节)为例 L = 04H + 04H + 04H +03H = 0F
//密码4个字节:这里取02222222(02是操作等级,后面是密码222222)
//操作者代码4个字节:这里取C0C1C2C3 (这里的8位随便取,给个默认值)
//使用convertNumber将十进制=>16进制,再转成大写,结果是一位的话再前面补0
let long = convertNumber(( 4 + FUNCTION_CODE_MAP.set[Key_Real_Name].long + 4 + 4),10,16).toUpperCase().padStart(2, '0');
rawData += long;
//====================写======第21-28位 数据标识符=========================================//
if (FUNCTION_CODE_MAP.set[Key_Real_Name]) {
let dataCode = FUNCTION_CODE_MAP.set[Key_Real_Name].dataCode;
let dataIdentifier = calculateDataIdentifier(dataCode);
rawData += dataIdentifier;
} else {
throw new Error(`FUNCTION_CODE_MAP中不支持写入此数据标识(${Key_Real_Name})`);
}
// 这里密码等级grade 02 是需要反转的,不过只有一个字节,所以反转之后和原来一样,但是是要+33H下发给设备
const grade = "35"
// 这里password是需要反转的,比如:密码是123456,下发给设备其实是563412,不过这里是222222,目前先不考虑,
// 但是是需要每两位+33H下发给设备
const password = "555555"
// 这里operatorCode是需要按照每两位反转的,并且需要+33H下发,但是目前没有用到,所以不考虑
const operatorCode = "C0C1C2C3"
rawData += ( grade + password + operatorCode)
//写入的数值,这里以日期及星期为例,也需要反转,按照 "星期 日 月 年"的顺序下发给设备
// 按照每两位分隔字符串
const arr = [];
for (let i = 0; i < Value_Real_Name.length; i += 2) {
arr.push(Value_Real_Name.substring(i, i + 2));
}
// 准备工作:反转数组,并且每个字节都减去33H
const temp = arr.reverse();
const resultArray = temp.map(hex => {
// 将16进制字符串转换为十进制数
const decimalValue = parseInt(hex, 16);
// 减去33H(即51的十进制值)
const result = decimalValue + 0x33;
// 如果需要将结果转换回16进制字符串,可以使用toString(16)
return result.toString(16).toUpperCase().padStart(2, '0');
});
//console.log("反转数组resultArray",resultArray)
rawData += ( resultArray.join(''))
}
//==========================第29-30位 CS校验和===========================================//
// Step 3: 计算校验和
let checksum = calculateChecksum(rawData);
rawData += checksum; //
//==========================第31-32位 固定值 "16"========================================//
// Step 4: 结束固定部分
rawData += '16'; //
// 设备能识别的格式数据
return hexToBase64(rawData)
}
// 计算校验和 cs校验
// rawDataTemp-使用CS校验位前面的临时的全部报文,算出一个字节的cs校验码
function calculateChecksum(rawDataTemp) {
const result = rawDataTemp.substring(0, rawDataTemp.length);
let sum = 0;
for (let i = 0; i < result.length; i += 2) {
sum += parseInt(result.substring(i, i + 2), 16);
}
sum = sum % 256;
// console.log("cs校验",result,sum.toString(16).padStart(2, '0'))
return sum.toString(16).padStart(2, '0').toUpperCase();
}
//数据标识的报文
function calculateDataIdentifier(dataCode) {
let hexData = dataCode.match(/.{2}/g).map(x => (parseInt(x, 16) + 0x33).toString(16).padStart(2, '0')).reverse().join('');
// console.log("数据标识的报文验",hexData)
return hexData;
}
//10进制和16进制相互转换
function convertNumber(value, fromBase, toBase) {
// 检查输入是否为字符串或数字
if (typeof value !== 'string' && typeof value !== 'number') {
throw new Error('Input value must be a string or a number');
}
// 检查输入的进制是否为10或16
if ((fromBase !== 10 && fromBase !== 16) || (toBase !== 10 && toBase !== 16)) {
throw new Error('Base must be either 10 or 16');
}
// 将输入值转换为字符串
const valueStr = value.toString();
// 将输入值从 fromBase 转换为10进制
const decimalValue = parseInt(valueStr, fromBase);
// 检查转换是否成功
if (isNaN(decimalValue)) {
throw new Error('Invalid number format for the given base');
}
// 将10进制值转换为 toBase
const convertedValue = decimalValue.toString(toBase);
return convertedValue;
}
//将16进制字符串 => base64格式数据
function hexToBase64(hexString) {
// 将16进制字符串转换为二进制数据
let binaryData = Buffer.from(hexString, 'hex');
// 将二进制数据转换为Base64格式
let base64String = binaryData.toString('base64');
return base64String;
}
function base64ToHex(base64String) {
// 将Base64格式数据解码为二进制数据
let binaryData = Buffer.from(base64String, 'base64');
// 将二进制数据转换为16进制字符串
let hexString = binaryData.toString('hex').toUpperCase();
return hexString;
}
测试-例子
////////////////////////////////////设备上报(设备发布,回复平台的抄读指令)/////////////////////////////////////////////////////////////////
//A相电压
{"data":"68000098700001689106333434356943EC16","identifier":"Ua"}
//B相电压
{"data":"68000098700001689106333534356943ED16","identifier":"Ub"}
//C相电压
{"data":"680000987000016891063336343534B32916","identifier":"Uc"}
//A相电流
{"data":"68000098700001689107333435356943342216","identifier":"Ia"}
//B相电流
{"data":"68000098700001689107333535356943443316","identifier":"Ib"}
//C相电流
{"data":"68000098700001689107333635356943C5B516","identifier":"Ic"}
//A相有功功率
{"data":"68000098700001689107333436356943342316","identifier":"Pa"}
//B相有功功率
{"data":"68000098700001689107333536356943443416","identifier":"Pb"}
//C相有功功率
{"data":"68000098700001689107333636356943C5B616","identifier":"Pc"}
//A相无功功率
{"data":"68000098700001689107333437356943342416","identifier":"Qa"}
//B相无功功率
{"data":"68000098700001689107333537356943443516","identifier":"Qb"}
//C相无功功率
{"data":"68000098700001689107333637356973C5E716","identifier":"Qc"}
//A相视在功率
{"data":"68000098700001689107333438356943342516","identifier":"Sa"}
//B相视在功率
{"data":"68000098700001689107333538356943443616","identifier":"Sb"}
//C相视在功率
{"data":"680000987000016891073336383569B3C52816","identifier":"Sc"}
//A相功率因数
{"data":"6800009870000168910633343935A5452F16","identifier":"PFa"}
//B相功率因数
{"data":"68000098700001689106333539355545E016","identifier":"PFb"}
//C相功率因数
{"data":"680000987000016891063336393568CC7B16","identifier":"PFc"}
//正向有功总电能
{"data":"6800009870000168910833333433CCBBAA990916","identifier":"FPEnergy"}
//反向有功总电能
{"data":"680000987000016891083333353344556677B616","identifier":"OPEnergy"}
//尖时段有功总电能
{"data":"680000987000016891083334343399AABBCC0A16","identifier":"FPEnergy1"}
//峰时段有功总电能
{"data":"6800009870000168910833353433555555559516","identifier":"FPEnergy2"}
//平时段有功总电能
{"data":"680000987000016891083336343366666666DA16","identifier":"FPEnergy3"}
//谷时段有功总电能
{"data":"680000987000016891083337343377665544B916","identifier":"FPEnergy4"}
//A相正向有功电能
{"data":"6800009870000168910833334833AABB99CC1D16","identifier":"FPEnergyA"}
//B相正向有功电能
{"data":"6800009870000168910833335C33CCAABB993116","identifier":"FPEnergyB"}
//C相正向有功电能
{"data":"6800009870000168910833337033AAAAAAAA2316","identifier":"FPEnergyC"}
//A相反向有功电能
{"data":"6800009870000168910833334933AABB99CC1E16","identifier":"OPEnergyA"}
//B相反向有功电能
{"data":"6800009870000168910833335D33CCAABB993216","identifier":"OPEnergyB"}
//C相反向有功电能
{"data":"6800009870000168910833337133AAAAAAAA2416","identifier":"OPEnergyC"}
//频率
{"data":"680000987000016891063533B335CC33BF16","identifier":"GridFreq"}
{"data":"680000987000016891063533B33533CCBF16","identifier":"GridFreq"}
//温度
{"data":"680000987000016891063A33B335AACC3B16","identifier":"Temp"}
{"data":"680000987000016891063A33B335CCAA3B16","identifier":"Temp"}
//总有功功率
{"data":"6800009870000168910733333635AABBCC7316","identifier":"P"}
{"data":"6800009870000168910733333635CCBBAA7316","identifier":"P"}
//总无功功率
{"data":"6800009870000168910733333735AABBCC7416","identifier":"Q"}
{"data":"6800009870000168910733333735CCBBAA7416","identifier":"Q"}
//总视在功率
{"data":"6800009870000168910733333835AABBCC7516","identifier":"S"}
{"data":"6800009870000168910733333835CCBBAA7516","identifier":"S"}
//总功率因数
{"data":"6800009870000168910633333935CCBBCB16","identifier":"PF"}
{"data":"6800009870000168910633333935BBAAD516","identifier":"PF"}
//组合有功总电能
{"data":"6800009870000168910833333333353637BC9C16","identifier":"PEnergy"}
{"data":"6800009870000168910833333333BC3736359C16","identifier":"PEnergy"}
//软件版本号
{"data":"684501001123226891243433B337535353535C6B636464676563655B65636389696363856164796165636679757D3F16","identifier":"Version"}
//时间
{"data":"6845010011232268910735343337737A490D16","identifier":"TimeHMS"}
//日期及星期
{"data":"6845010011232268910834343337385A45570516","identifier":"TimeYMDW"}
//////////////////////////////////////////////设备接收(平台下发指令去抄读)///////////////////////////////////////////////////////////////////
{"deviceName":"119903769212","params":{"PF":true},"address":"","method":"thing.service.property.get","functionCode":"0X11"}
//645电表产品-子设备 PK
//645电表产品-子设备 DN
//租户id
// 想抄读的属性标识符
{"productKey":"CJByFt0I3NF",
"deviceName":"000098700001",
"type": 0,
"functionCode": "0x11",
"tenantId": "7zt3ng7xjk",
"params": { "Ua": true },
"identifier": "ReadData"
}
protocolToRawData('{"deviceName":"119903769212","params":{"PF":true},"address":"","method":"thing.service.property.get","functionCode":"0X11"}')
rawDataToProtocol('{"data":"6845010011232268910733333735C459B3A616","identifier":"Q"}')
rawDataToProtocol('{"data":"6845010011232268910733333735C459D3C616","identifier":"Q"}')
/////////////////////////时间-读///////////////////////////////
{
"productKey": "aITqSMsV7nN",
"deviceName": "222311000145",
"type": 0,
"functionCode": "0x11",
"tenantId": "3atjs4bym3",
"params": { "TimeHMS": true },
"identifier": "ReadData"
}
/////////////////////////时间-写///////////////////////////////
{
"productKey": "aITqSMsV7nN",
"deviceName": "222311000145",
"type": 1,
"functionCode": "0x14",
"tenantId": "3atjs4bym3",
"params": { "TimeHMS": "121212" },
"identifier": "TimeHMS"
}
/////////////////////////日期-读///////////////////////////////
{
"productKey": "aITqSMsV7nN",
"deviceName": "222311000145",
"type": 0,
"functionCode": "0x11",
"tenantId": "3atjs4bym3",
"params": { "TimeYMDW": true },
"identifier": "ReadData"
}
/////////////////////////日期-写///////////////////////////////
{
"productKey": "aITqSMsV7nN",
"deviceName": "222311000145",
"type": 1,
"functionCode": "0x14",
"tenantId": "3atjs4bym3",
"params": { "TimeYMDW": "24122503" },
"identifier": "TimeYMDW"
}
小结
如果能重新再写一次,我想这个解析脚本应该会可以写的再好一点。