PMC-D726X 通讯协议解码器

介绍

PMC-D726X是深圳中电电力技术股份有限公司生产的一款电表,它的通信协议是基于MODBUS协议基础上进行二次开发的通信。
本文的目的,在于说明如何针对这类基于协议框架的基础上,开发针对的JS解码器。

资料

PMC-D726X装置 Modbus通信协议 (V1.0)

上面的协议文档引用自www.shwanqiao.com ,如果该文档失效,请自行去百度该设备的协议文档。

工具

考虑到很多开发者手头上并没有MODBUS物理设备,可以先使用MODBUS设备模拟器进行新手上路。

等你能够对MODBUS设备模拟器进行操作之后,你可以将MODBUS模拟器替换为你手头上的MODBUS物理设备,就可以直接通信了。

推荐工具:
ModBus设备模拟器:ModbusSlave
ModBus客户端工具:ModbusPoll

你可以在下载和安装这两个工具之后,使用ModbusSlave模拟一个TCP的MODBUS设备,然后使用ModbusPoll工具使用TCP方式去连接这ModbusSlave。

这时候,你会看到ModbusPoll会循环读取ModbusSlave的表格数据,你在ModbusPoll上修改表格数据,它也会被配置到ModbusSlave中去。

概念

关于MODBUS

很多开发者一开始没有接触过MODBUS,对MODBUS缺失概念,然后会摸不着头脑。

在MODBUS中,是一种类似内存地址的通信协议,它的数据放在连续的内存地址当中,只不过它的内存地址,在MODBUS中叫做寄存器。

我们的计算机每一个内存地址存放的是8位的数据,而MODBUS寄存器存放的是一个16位的数据。

设备厂商,通常会把MODBUS设备的数据放到一个个的寄存器中,你想对MODBUS设备进行操作的时候,就是读取寄存器数据和写入寄存器数据。

你对MODBUS设备的操作,就是读取和写入寄存器。

那么跟着对应的就是,每一个MODBUS设备的生产厂商,他们都会提供一个地址表,告诉你它在哪个寄存器上是如何存储内存数据的。

你对MODBUS设备的操作,就是根据设备厂商提供给你的那一份表,按整数、浮点数、定点数、布尔值,读取和写入数据到寄存器。

技术方案

灵狐已经提供了ModBus通信协议的框架代码,你可以在Fox-Edge的 组件仓库>动态解码 页面中搜索 MODBUS ,就可以看到该代码。

可以将Modbus解码器中的 foxLibsmodbusCoreLibs 两个库函数复制出来,作为ModBus的协议框架的代码。

然后,按照Fox-Edge动态解码器的规范,自定义 问答操作 的方法,在编码函数和解码函数中,调用 foxLibsmodbusCoreLibs 的函数。

即可实现自己的解码器PMC-D726X。

步骤

1. 复制代码

在Fox-Edge的 组件仓库>动态解码 页面中搜索 MODBUS ,就可以看到该代码。

可以将Modbus解码器中的 foxLibsmodbusCoreLibs 两个库函数复制出来,作为ModBus的协议框架的代码。

待会将这两段代码,复制到自己的 PMC-D726X 解码器中。

2. 新建代码

在Fox-Edge的 组件仓库>动态解码 页面中新建一个 PMC-D726X 解码器。

在这个解码器中,新建 foxLibsmodbusCoreLibs 两个库函数,它们的内容直接从ModBus解码器中复制过来。

然后,为 PMC-D726X 新建一个 cetLibs 库函数,新建一个 读取实时测量数据 问答操作。

3. 数据准备

先根据 PMC-D726X装置 Modbus通信协议 (V1.0) 协议文档的指导, 通过MODBUS串口测试工具,发送读取数据报文给电表设备,获得一段发送报文和测试报文,作为后面开发调试用的素材。

发送报文:读取77号设备,从寄存器地址0开始的10个寄存器
4D 03 00 00 00 0A CB C1

返回报文:从77号设备,返回了从寄存器地址0开始的10个寄存器数据
4D 03 14 00 00 5B 9A 00 00 00 00 00 00 00 00 00 00 1E 89 00 00 5B 9A 8E 83

4. 编码函数

在IDEA开发工具中,新建一个PMC-D726X.JS文件,作为开发调试环境。

然后,将刚才在Fox-Edge中复制的 foxLibsmodbusCoreLibs 两个库函数,复制代码到IDEA中的PMC-D726X.JS文件中。

然后,编写编码函数,内容如下:

/**
 * 编码器的入口函数
 * 全局参数:
 *     fox_edge_param:json string格式的设备参数的合并对象
 * 返回值:
 *   提供给通道的发送数据。根据不同的通道服务,它可能是HEX结构的文本,也可能是JSON结构的对象,请自行根据选定的通道服务进行确认
 */
function encode() {
    var param = JSON.parse(fox_edge_param);
    return encodeReadHoldingRegisterRequest(param);
}

/**
 * 数据编码
 *
 * @param param
 * @returns {string}
 */
function encodeReadHoldingRegisterRequest(param) {
    // 检查输入参数
    testEmpty(param.devAddr, "输入参数不能为空: devAddr");
    testEmpty(param.regAddr, "输入参数不能为空: regAddr");
    testEmpty(param.regCnt, "输入参数不能为空: regCnt");
    testEmpty(param.modbusMode, "输入参数不能为空: modbusMode");

    // 使用灵狐的modbus库进行编码
    var pdu = encodeReadRegistersRequest(0x03, param);
    return arrayToHex(pdu);
}

三部分代码在写入后,在IDEA中进行测试和执行

var fox_edge_param = "{\n" + "     \"devAddr\": 77,\n" + "     \"modbusMode\": \"RTU\",\n" + "     \"regAddr\": 0,\n" + "     \"regCnt\": 10\n" + "}";

var result = encode()

那么最终你会看到,编码出下列报文,证明验证成功。

4D 03 00 00 00 0A CB C1

5. 解码函数

编写解码函数,内容如下:

/**
 * 解码器的入口函数
 * 全局参数:
 *     fox_edge_data:json string格式 或者 hex string格式的接收数据
 *     fox_edge_param:json string格式的设备参数的合并对象
 * 返回值:
 *   提供给通道的发送数据。根据不同的通道服务,它可能是HEX结构的文本,也可能是JSON结构的对象,请自行根据选定的通道服务进行确认
 */
function decode() {
    var param = JSON.parse(fox_edge_param);
    var result = decodeReadHoldingRegisterRespond(fox_edge_data, param);
    return JSON.stringify(result);
}

/**
 * 解码数据
 *
 * @param hexRecv
 * @param param
 * @returns {{}}
 */
function decodeReadHoldingRegisterRespond(hexRecv, param) {
    // 检查输入参数
    testEmpty(param.devAddr, "输入参数不能为空: devAddr");
    testEmpty(param.regAddr, "输入参数不能为空: regAddr");
    testEmpty(param.regCnt, "输入参数不能为空: regCnt");
    testEmpty(param.modbusMode, "输入参数不能为空: modbusMode");

    // 使用灵狐的modbus库进行解码
    var pdu = hexToArray(hexRecv);
    var registers = decodeReadRegistersRespond(pdu, param);

    // 使用中电的库进行解码
    var result = decodeCetReadHoldingRegisters(param, registers)
    return result
}

/**
 * 用户定义的字典
 *
 * @returns {*[]}
 */
function getObjectDict() {
    var list = [];
    list.push({addr: 0, name: "A相电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 2, name: "B相电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 4, name: "C相电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 6, name: "平均相电压", format: "UINT32", unit: "x100,V"})

    list.push({addr: 8, name: "AB线电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 10, name: "BC线电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 12, name: "CA线电压", format: "UINT32", unit: "x100,V"})
    list.push({addr: 14, name: "平均线电压", format: "UINT32", unit: "x100,V"})

    return list
}

function findObjectInfo(objDict, regAddr) {
    for (var index = 0; index < objDict.length; index++) {
        var obj = objDict[index];
        if (obj.addr == regAddr) {
            return obj
        }
    }

    return null
}

function decodeObjectInfo(objInfo, reg0, reg1, reg2, reg3) {
    if (objInfo.format == "UINT32") {
        var value = (reg0 << 8) + (reg1 << 0)
        if (objInfo.unit.startsWith("x100,")) {
            value = value / 100.0;
        }
        return value
    }

    return null
}

function decodeCetReadHoldingRegisters(param, registers) {
    var result = {};

    var objectDict = getObjectDict();

    for (var index = 0; index < registers.length; index++) {
        var regAddr = param.regAddr + index;

        // 查找定义信息
        var objInfo = findObjectInfo(objectDict, regAddr)
        if (objInfo == null) {
            continue
        }

        var value = decodeObjectInfo(objInfo, registers[index], registers[index + 1], registers[index + 2], registers[index + 3])
        if (value == null) {
            continue
        }

        result[objInfo.name] = value;
    }

    return result;
}

三部分代码在写入后,在IDEA中进行测试和执行

var fox_edge_param = "{\n" + "     \"devAddr\": 77,\n" + "     \"modbusMode\": \"RTU\",\n" + "     \"regAddr\": 0,\n" + "     \"regCnt\": 10\n" + "}";

var fox_edge_data = "4D 03 14 00 00 5B 9A 00 00 00 00 00 00 00 00 00 00 1E 89 00 00 5B 9A 8E 83"

var result = decode()

那么最终你会看到,编码出下列报文,证明验证成功。

{
	"A相电压": 234.5,
	"B相电压": 0,
	"C相电压": 0,
	"平均相电压": 78.17,
	"AB线电压": 234.5
}

6. 代码说明

编写解码函数,相关的一些说明

1. 关于约定

function encode() 函数是Fox-Edge约定的编码函数的入口函数,Fox-Edge会在发送数据前调用该函数,进行数据的编码。

其中,fox_edge_param 是Fox-Edge传递给解码器的设备参数和操作参数,你可以从这里获得自己需要的设备和操作信息。

该信息的是 JSON 格式,其具体参数格式,由你定义。在开发完成之后,记得写一份文档告知其他人如何配置你需要的参数。

function decode() 函数是Fox-Edge约定的解码函数的入口函数,Fox-Edge会在接收数据前调用该函数,进行数据的解码。

2. 调用关系

解码器实际上是一个JavaScript的工程,所有的库和操作定义的函数,都会被加载到 device服务 的JavaScript引擎之中进行执行。

各函数的调用关系,JavaScript引擎会自动分析。

3. 注意事项

用户的命名自己的库函数时,要避免引用的modbus库中的函数名称重复的冲突。

7. 保存代码

复制IDEA开发的代码到Fox-Edge的 PMC-D726X 解码器中,其中可复用的部分函数,放到 cetLibs 库函数中。

那么,一个解码器基本上就在本地开发完成了。

8. 测试验证

1. 组件安装

在Fox-Edge上的 组件仓库>服务模块 选择安装TCP或者Serial通道服务。

在Fox-Edge上的 组件仓库>动态界面 选择安装PMC-D726X解码器

2. 通道配置

在Fox-Edge上的 通道管理>通道列表 添加一个TCP或者Serial的通道

如果是TCP参数,它的格式范例如下:

{
     "host": "192.168.1.8",
     "port": 502
}

其中,host/port是你的PMC-D726X设备的服务地址,目前是ModbusSlave这个设备模拟器的服务地址,由于ModbusSlave安装在你的计算机上,那么它就是你笔记本的IP。

如果是Serial参数,它的格式范例如下:

{
     "baudRate": 9600,
     "databits": 8,
     "parity": "N",
     "serialName": "ttyUSB2",
     "stopbits": 1
}

如果你安装了 本地组件/设备模板 ,那么你可以直接生成默认的通道配置参数

3. 设备配置

在Fox-Edge上的 通道管理>设备列表 添加一个PMC-D726X设备,指明该设备使用的是前面创建的通道

并在参数配置中,填写PMC-D726X的地址和协议模式

如果是TCP参数,它的格式范例如下:

{
     "devAddr": 77,
     "modbusMode": "TCP"
}

同样,如果你是串口,那么你的工作模式应该填为RTU

{
     "devAddr": 77,
     "modbusMode": "RTU"
}

如果你安装了 本地组件/设备模板 ,那么你可以直接生成默认的设备配置参数

4. 连接测试

在Fox-Edge上的 任务管理>通道操作任务 添加一个连通性测试任务,用于测试跟MODBUS设备是否连接上了。

你可以在ModbusSlave或者ModbusPoll的通信日志页面,取一条有效的通信报文,作为验证报文。

例如你是TCP通信,你在ModbusSlave的通信日志页面,复制一条通信数据内容如下:

"4D 03 00 00 00 0A CB C1"

这时候,你可以把该内容填写到模板参数中

这时候,你选择该任务后,点击页面中的发送按钮,你可以看到ModbusSlave给你返回了一段报文:

{
     "type": "serialport",
     "uuid": "25364b96-06a5-49d0-bbe2-534e0abc6dd4",
     "name": "测试通道-MODBUS",
     "mode": "exchange",
     "send": "4D 03 00 00 00 0A CB C1",
     "recv": "4D 03 14 00 00 5B 9A 00 00 00 00 00 00 00 00 00 00 1E 89 00 00 5B 9A 8E 83",
     "timeout": 2000,
     "msg": "",
     "code": 200
}

recv有数据,这时候说明你的通道连接,跟ModbusSlave是正常的,它能够应答你的Fox-Edge上的HEX报文请求。

5. 设备操作

在Fox-Edge上的 任务管理>设备操作任务 PMC-D726X设备操作任务,比如 读取实时测量数据 的操作任务。

参数说明:

{
     "devAddr": 77,---------------------------设备地址
     "modbusMode": "RTU",--------------------MODBUS的工作模式:TCP/RTU
     "regAddr": 0,--------------------------告知解码器,从0号寄存器开始,连续读取10个寄存器
     "regCnt": 10
}

范例:

{
     "devAddr": 77,
     "modbusMode": "RTU",
     "regAddr": 0,
     "regCnt": 10
}

这时候,你选择该任务后,点击页面中的发送按钮,你可以看到ModbusSlave给你返回了一段报文:

[
     {
          "uuid": "6c6d24bf-fad6-49c8-b0d7-495c0667c21d",
          "operateMode": "exchange",
          "operateName": "读取实时测量数据",
          "manufacturer": "深圳中电电力技术股份有限公司",
          "deviceType": "PMC-D726X",
          "deviceName": "中电电表(PMC-D726X)",
          "param": {
               "devAddr": 77,
               "modbusMode": "RTU",
               "regAddr": 0,
               "regCnt": 10
          },
          "timeout": 2000,
          "record": true,
          "data": {
               "commStatus": {
                    "commFailedCount": 0,
                    "commFailedTime": 0,
                    "commSuccessTime": 1753968065552
               },
               "value": {
                    "status": {-----------------------------这是解析出来的数据
                         "AB线电压": 234.5,
                         "A相电压": 234.5,
                         "B相电压": 0,
                         "C相电压": 0,
                         "平均相电压": 78.17
                    }
               }
          },
          "msg": "",
          "code": 200
     }
]

上面的返回,说明你的PMC-D726X设备已经正常访问了

9. 代码发布

考虑到用户的解码器,会在很多工程项目中使用,你可以将开发的解码器代码,发布到Fox-Edge的中央仓库中, 那么你和其他Fox-Edge的用户的各个项目就都可以使用了。