node-red通过指令方式读取DL/T645-2007通信协议数据
node-red通过指令方式读取DL/T645-2007通信协议数据,并将结果数据进行处理,得到最终输出数据
·
node-red通过指令方式读取DL/T645-2007通信协议数据
参考链接:
一、DL/T645-2007通信协议介绍
DL/T645协议是针对电表通信而制定的通信协议,主要有两个版本,分别是DL/T645-97和DL/T645-07。
- DL/T645-1997是1997年版本,DL/T645-2007是2007年的修正版本,目前最新版本是2007版。
- 目前电表根据型号不同可能支持其中一个版本,开发时需要注意。
- 实际应用过程中电表虽然有多个型号,但只要采用的都是07版本,就仅需要兼容这一个版本的协议即可。
- 本文限只针对DL/T645-2007。
1.1 DL/T645通信链路
DL/T645协议设计初时采用RS-485 标准串行电气接口,为主-从结构的半双工通信方式。所以设计了起始符、结束符、效验码等标记保证数据准确性,当然也可以通过TCP方式通信。
1.2 DL/T645-2007数据格式
每条数据由:帧起始符、从站地址域、控制码、数据域长度、数据域、帧信息纵向校验码及帧结束符7个域组成。每部分由若干字节组成。
测试数据信息如下:
- 测试电表编号:1023504796
- 通讯波特率:2400
- 数据位:8位
- 停止位:1位
- 校验方式:CS检验
有点类似ModBus协议,格式如下:
其中:
- 地址域的地址刚好和电表上的
地址号相反
,如1023504796 编号对应的地址域号 A0=96 ,A1=47,A2=50,A3=23,A4=10,A5=00 - 控制码格式如下:
- 读取数据发送的数据域根据协议附件的 DI0 DI1 DI2 DI3 顺序来填写 ,
需要注意的是协议上顺序刚好是反过来的。
数据域说明:
- 读C相电压: 33 36 34 35
- 读电压数据块:33 32 34 35
- 读A相电流:33 34 35 35
- 读当前组合有功总电能:33 33 33 33
- 读当前正向有功总电能:33 33 34 33
- 读当前反向有功总电能:33 33 35 33
- 上图实例解析:
发送咨询用(当前)正向有功总电能 :fefefefe 68 964750231000 68 11 4 33333333 xx16
- fefefefe:报文解析
- 68:帧起始符
- 964750231000 :表号,解析为:1023504796
- 11 :控制码-读数据
- 04:读取寄存器的数据长度
- 33333433:对应了DI0到DI3的值 00 00 01 00对应了协议表的(当前)正向有功总电能
- xx:CS校验码,算法详见下节
- 16:结束符
1.3 CS校验码生成算法
除去数据头,对其它数据进行16进制相加,将得到的校验位和取后两位。
/**十六进制相加得到十六进制数*/
function hexAdd(hexarr) {
let sum = 0;
for (let h of hexarr) {
sum += h;
}
return sum.toString(16).toUpperCase(); // 将结果转换为大写的十六进制字符串
}
运行结果如下,得到校验和‘312’,取后两位,即校验码为:‘12
’
1.4 返回数据解析
返回的数据:fefefefe 68 964750231000 68 91 8 33333333 97a34b33 4d16
格式讲解:
- fefefefe :数据头每条数据都要带(
返回的不一定就是4个,自行截取
)- 68:针起始符
- 964750231000:表号 解析为:1023504796. 从右向左每一个字节的bcd码拼接在一起,就是电表贴的条码上的数字
- 68 :针起始符
- 91:控制码 读取成功
- 08:返回寄存器加数据的长度为8个byte
- 33333333:对应了DI0到DI3的值 00 00 01 00对应了协议表的(当前)总电能
- 97a34b33:返回的数据,数据处理详见下节
- 4d:校验码
- 16 :结束符
1.5 返回数据处理
- 数据域传输时低字节在前,高字节在后
- 传输时发送方按字节进行加33H处理,接收方按字节进行减33H处理
- 第一步一定要做数据校验,血泪的教训,有时候返回的不一定是这个电表地址的数据,所以一定要将返回数据中的地址提取出来,和实际的地址进行对比校验,地址一致时才保留对应数据。地址校验如下:
/** 从返回的数组中得到电表地址,进行校验
* response:返回的数据数组
* addr:实际的电表地址
*/
function verifiAddr(response, addr) {
let adrs = [];
let num = 0;
for (let i = 5;i<5+6; i++) {//地址从数组第5位开始,一共6位长度
let r = response[i];
if (r < 10) {
adrs.push("0" + r.toString(16));
} else {
adrs.push(r.toString(16));
}
num++;
if (num == 6) {
break;
}
}
let address = "";//电表地址字符串
for (let i = adrs.length - 1; i >= 0; i--) {
address += adrs[i];
}
address = address.replace(/^0+/, '');//去掉地址开头的0字符 - 一般开头的0是不表示的
if (addr == address) {//判断地址是否一致
return true;
}
return false;
}
如果地址一致了,再处理数据。
- 返回的数据处理如下:
从右向左每一个字节减去0x33(十进制为51),然后得到的值进行bcd转换成实际的值。最后乘以0.01得到最后的结果值
// vi -> 对应di的处理 obj代表返回的数据数组
let v0 = (D0-51).toString(16).padStart(2, '0'); //D0-字节减去0x33(十进制为51),转成16进制字符串 因为一个字节是2位,所以小于2位字节的在前面补0
let v1 = (obj[19] - 51).toString(16).padStart(2, '0');
let v2 = (obj[20] - 51).toString(16).padStart(2, '0');
let v3 = (obj[21] - 51).toString(16).padStart(2, '0');
let v = v3+v2+v1+v0; // 值为各字节位数据字符串相加 - 注意这里要反过来相加,从V3->V0
v = parseInt(v);//将字符串转成数字
v = v*0.01;//乘以倍率0.01 (kwh);
代入返回的数据’97A34B33’ 为:
// vi -> 对应di的处理 obj代表返回的数据数组
let v0 = (0x97-0x33).toString(16).padStart(2, '0');//->(151-51) .toString(16).padStart(2, '0');->100.toString(16).padStart(2, '0');->64
let v1 = (0xa3- 0x33).toString(16).padStart(2, '0');//70
let v2 = (0x4b-0x33).toString(16).padStart(2, '0');//18
let v3 = (0x33-0x33).toString(16).padStart(2, '0');//00
let v = v3+v2+v1+v0; // 00187064
v = parseInt(v);//187064
v = v*0.01;//1870.64 (kwh) - 最终输出数据
二、node-red实现
tcp节点配置:
回到目录
2.1 指令生成函数
/** 生成电表地址为addr的获取电能数据的指令 */
function genInstruction(addr) {
let arr = ([0x68].concat(genAdrrArray(addr))).concat([0x68, 0x11, 0x04, 0x33, 0x33, 0x33, 0x33]);
arr.push(genCS(arr));
arr.push(0x16);
return Buffer.from(arr);
}
/** 生成CS校验码:对数据进行16进制相加,将得到的校验位和取后两位 */
function genCS(hexarr) {
let sum = 0;
for (let h of hexarr) {
sum += h;
}
sum = sum.toString(16).toUpperCase(); // 将结果转换为大写的十六进制字符串
return parseInt('0x' + sum.slice(-2), 16);
}
/** 根据地址号生成地址域数据格式:
* 地址域的地址刚好和电表上的地址号相反,如1023504796 编号对应的地址域号 A0=96 ,A1=47,A2=50,A3=23,A4=10,A5=00
*/
function genAdrrArray(addrStr) {
let addr = [];
let i = addrStr.length;
while (i >= 2) {
addr.push(parseInt('0x' + addrStr.slice(i - 2, i)));
i = i - 2;
}
if (addr.length < 6) {
for (let i = addr.length; i < 6; i++) {
addr.push(parseInt('0x00'));
}
}
return addr;
}
2.2 数据处理函数
var obj=msg.payload;
msg.flag = false;
if(obj != undefined && obj.length == 24){
msg.flag = verifiAddr(obj,msg.addr);//校验返回的地址与实际地址是否一致
if (msg.flag == true){//地址一致
let a = (obj[18] - 51).toString(16).padStart(2, '0');
let b = (obj[19] - 51).toString(16).padStart(2, '0');
let c = (obj[20] - 51).toString(16).padStart(2, '0');
let d = (obj[21] - 51).toString(16).padStart(2, '0');
msg.data = parseInt(d+c+b+a) * 0.01;
}
}
return msg;
/** 从返回的数组中得到电表地址,进行校验 */
function verifiAddr(response, addr) {
let adrs = [];
let num = 0;
for (let i = 5;i<5+6; i++) {
let r = response[i];
if (r < 10) {
adrs.push("0" + r.toString(16));
} else {
adrs.push(r.toString(16));
}
num++;
if (num == 6) {
break;
}
}
let address = "";
for (let i = adrs.length - 1; i >= 0; i--) {
address += adrs[i];
}
address = address.replace(/^0+/, '');
if (addr == address) {
return true;
}
return false;
}
三、node-red源码
[
{
"id": "66d6c6bb536610b0",
"type": "tcp request",
"z": "9924065d76bade16",
"name": "",
"server": "192.168.84.150",
"port": "26",
"out": "char",
"ret": "buffer",
"splitc": "0x16",
"newline": "",
"trim": false,
"tls": "",
"x": 1120,
"y": 60,
"wires": [
[
"5c00f33a44a4331a",
"ea100cc4f35b0a93"
]
]
},
{
"id": "d332d6fb48bd8d63",
"type": "inject",
"z": "9924065d76bade16",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 810,
"y": 60,
"wires": [
[
"18059526b6448bc0"
]
]
},
{
"id": "18059526b6448bc0",
"type": "function",
"z": "9924065d76bade16",
"name": "X01",
"func": "msg.head_code = \"X01\";\nmsg.addr = \"1023504796\";\n//根据地址,生成指令\nmsg.payload = genInstruction(msg.addr);\nreturn msg;\n/** 生成电表地址为addr的获取电能数据的指令 */\nfunction genInstruction(addr) {\n let arr = ([0x68].concat(genAdrrArray(addr))).concat([0x68, 0x11, 0x04, 0x33, 0x33, 0x33, 0x33]);\n arr.push(genCS(arr));\n arr.push(0x16);\n return Buffer.from(arr);\n}\n/** 生成CS校验码:对数据进行16进制相加,将得到的校验位和取后两位 */\nfunction genCS(hexarr) {\n let sum = 0;\n for (let h of hexarr) {\n sum += h;\n }\n sum = sum.toString(16).toUpperCase(); // 将结果转换为大写的十六进制字符串 \n return parseInt('0x' + sum.slice(-2), 16);\n}\n/** 根据地址号生成地址域数据格式:\n * 地址域的地址刚好和电表上的地址号相反,如1023504796 编号对应的地址域号 A0=96 ,A1=47,A2=50,A3=23,A4=10,A5=00\n */\nfunction genAdrrArray(addrStr) {\n let addr = [];\n let i = addrStr.length;\n while (i >= 2) {\n addr.push(parseInt('0x' + addrStr.slice(i - 2, i)));\n i = i - 2;\n }\n if (addr.length < 6) {\n for (let i = addr.length; i < 6; i++) {\n addr.push(parseInt('0x00'));\n }\n }\n return addr;\n}",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 950,
"y": 60,
"wires": [
[
"66d6c6bb536610b0",
"1765ec4d4fafb702"
]
]
},
{
"id": "5c00f33a44a4331a",
"type": "function",
"z": "9924065d76bade16",
"name": "数据处理",
"func": "var obj=msg.payload;\nmsg.flag = false;\nif(obj != undefined && obj.length == 24){\n msg.flag = verifiAddr(obj,msg.addr);//校验返回的地址与实际地址是否一致\n if (msg.flag == true){//地址一致\n let a = (obj[18] - 51).toString(16).padStart(2, '0');\n let b = (obj[19] - 51).toString(16).padStart(2, '0');\n let c = (obj[20] - 51).toString(16).padStart(2, '0');\n let d = (obj[21] - 51).toString(16).padStart(2, '0');\n msg.data = parseInt(d+c+b+a) * 0.01;\n }\n}\nreturn msg;\n/** 从返回的数组中得到电表地址,进行校验 */\nfunction verifiAddr(response, addr) {\n let adrs = [];\n let num = 0;\n for (let i = 5;i<5+6; i++) {\n let r = response[i];\n if (r < 10) {\n adrs.push(\"0\" + r.toString(16));\n } else {\n adrs.push(r.toString(16));\n }\n num++;\n if (num == 6) {\n break;\n }\n }\n let address = \"\";\n for (let i = adrs.length - 1; i >= 0; i--) {\n address += adrs[i];\n }\n address = address.replace(/^0+/, '');\n if (addr == address) {\n return true;\n }\n return false;\n}\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1300,
"y": 60,
"wires": [
[
"808b9e6f43c26805"
]
]
},
{
"id": "1765ec4d4fafb702",
"type": "debug",
"z": "9924065d76bade16",
"name": "debug 24",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1060,
"y": 100,
"wires": []
},
{
"id": "808b9e6f43c26805",
"type": "debug",
"z": "9924065d76bade16",
"name": "debug 25",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1420,
"y": 100,
"wires": []
},
{
"id": "ea100cc4f35b0a93",
"type": "debug",
"z": "9924065d76bade16",
"name": "debug 26",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1260,
"y": 120,
"wires": []
}
]
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
已为社区贡献32条内容
所有评论(0)