2. Modbus TCP 电表数据采集

任务目标

通过以太网使用 Modbus TCP 协议读取智能电表的电压、电流、有功功率等关键电气参数。本案例以 Python 的 pymodbus 库实现最小可复现的客户端,连接支持 Modbus TCP 的电表模拟器或真实设备,完成数据读取与解析。

环境与依赖

  • Python 3.8+
  • 安装依赖:pip install pymodbus==3.6.9
  • 测试目标:可使用 pymodbus 自带的模拟服务器,或任何支持 Modbus TCP 的电表设备(默认端口 502)

实现代码

以下代码实现 Modbus TCP 客户端,读取保持寄存器中的电压(地址 0x0000,单位 0.1V)、电流(地址 0x0002,单位 0.001A)、有功功率(地址 0x0004,单位 0.1W),并解析为实际工程值:

from pymodbus.client import ModbusTcpClient

# 电表寄存器映射(保持寄存器)
REG_VOLTAGE = 0x0000   # 电压,单位 0.1V
REG_CURRENT = 0x0002   # 电流,单位 0.001A
REG_POWER   = 0x0004   # 有功功率,单位 0.1W
REG_COUNT   = 6        # 一次读取6个寄存器(覆盖3个参数)

def read_meter(host: str, port: int = 502, unit_id: int = 1) -> dict:
    """读取电表电压/电流/功率,返回工程值字典"""
    client = ModbusTcpClient(host, port=port, timeout=3)
    if not client.connect():
        raise ConnectionError(f"无法连接 {host}:{port}")

    try:
        # 读取保持寄存器(功能码 0x03)
        resp = client.read_holding_registers(REG_VOLTAGE, REG_COUNT, slave=unit_id)
        if resp.isError():
            raise RuntimeError(f"Modbus 读取错误: {resp}")

        regs = resp.registers
        voltage = regs[0] * 0.1      # 0.1V → V
        current = regs[2] * 0.001    # 0.001A → A
        power   = regs[4] * 0.1      # 0.1W → W

        return {
            "voltage_v": round(voltage, 2),
            "current_a": round(current, 3),
            "power_w":   round(power, 2),
        }
    finally:
        client.close()

if __name__ == "__main__":
    # 替换为实际电表 IP;本地测试可用 pymodbus 模拟服务器
    data = read_meter("192.168.1.100")
    print(f"电压: {data['voltage_v']} V")
    print(f"电流: {data['current_a']} A")
    print(f"功率: {data['power_w']} W")

运行与验证

  1. 启动模拟服务器(无真实设备时):
    python -m pymodbus.server --host 0.0.0.0 --port 5020
    将代码中端口改为 5020,IP 改为 127.0.0.1。
  2. 运行客户端:
    python modbus_meter.py
  3. 判定通过条件:
    程序无异常退出,并打印三行参数。模拟服务器默认寄存器值为 0,因此预期输出为 电压: 0.0 V、电流: 0.0 A、功率: 0.0 W。若连接真实电表,数值应与电表显示一致(允许±1%误差)。

常见问题

  • 连接超时:确认设备 IP/端口正确,且防火墙放行 TCP 502(或自定义端口)。
  • 寄存器地址不匹配:不同厂商电表寄存器映射可能不同,需查阅设备手册调整 REG_* 常量与缩放系数。
  • 字节序问题:部分电表使用大端字节序,若读数异常可尝试 client.read_holding_registers(..., byteorder=1) 或交换寄存器高低字节。