645协议解析-js脚本

         最近项目中用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"
}







 小结

        如果能重新再写一次,我想这个解析脚本应该会可以写的再好一点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小白-RMS

谢谢老板

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值