参考链接:

一、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协议,格式如下:

1

其中:

  1. 地址域的地址刚好和电表上的地址号相反,如1023504796 编号对应的地址域号 A0=96 ,A1=47,A2=50,A3=23,A4=10,A5=00
  2. 控制码格式如下:
    2
  3. 读取数据发送的数据域根据协议附件的 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
  1. 上图实例解析:
    发送咨询用(当前)正向有功总电能 :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
2

回到目录

1.4 返回数据解析

3

返回的数据: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处理
  1. 第一步一定要做数据校验,血泪的教训,有时候返回的不一定是这个电表地址的数据,所以一定要将返回数据中的地址提取出来,和实际的地址进行对比校验,地址一致时才保留对应数据。地址校验如下:
/** 从返回的数组中得到电表地址,进行校验
* 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;
}

如果地址一致了,再处理数据。

  1. 返回的数据处理如下:

从右向左每一个字节减去0x33(十进制为51),然后得到的值进行bcd转换成实际的值。最后乘以0.01得到最后的结果值
5

// 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实现

2
tcp节点配置:
1
回到目录

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": []
    }
]

回到目录

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐