4. Modbus TCP服务器模式实现

任务目标

将工业IoT网关配置为Modbus TCP服务器,使其能够响应远程主站(如SCADA、PLC或PC调试工具)的读写请求。网关内部维护一组寄存器数据,主站可通过Modbus TCP协议读取输入寄存器与保持寄存器,并可写入保持寄存器。

实现方案

使用Python的pymodbus库在网关上启动Modbus TCP服务器。服务器绑定网关的以太网IP地址,监听标准端口502。内部使用ModbusSequentialDataBlock初始化寄存器数据区,通过ModbusSlaveContext构建数据模型,最后启动ModbusTcpServer。网关的串口或无线接口采集的数据可写入该寄存器区,从而对主站呈现为标准的Modbus从站设备。

环境依赖

网关需安装Python 3.6+及pymodbus库:

pip install pymodbus==2.5.3

完整代码

from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext

# 寄存器地址范围定义(Modbus协议中地址从0开始)
INPUT_REG_SIZE = 10      # 输入寄存器数量(只读,地址0~9)
HOLDING_REG_SIZE = 20    # 保持寄存器数量(可读写,地址0~19)

# 初始化数据块
# 输入寄存器:模拟网关采集的4路模拟量(电压、电流、温度、湿度)
input_block = ModbusSequentialDataBlock(0, [0]*INPUT_REG_SIZE)
# 保持寄存器:模拟网关可配置的参数(如报警阈值、设备启停等)
holding_block = ModbusSequentialDataBlock(0, [0]*HOLDING_REG_SIZE)

# 构建从站上下文(从站地址0x00表示响应所有单元ID)
slave_context = ModbusSlaveContext(
    di=None,              # 离散输入(本案例不使用)
    co=None,              # 线圈(本案例不使用)
    ir=input_block,       # 输入寄存器
    hr=holding_block      # 保持寄存器
)
server_context = ModbusServerContext(slaves=slave_context, single=True)

# 模拟网关采集数据更新输入寄存器(实际工程中由串口/IO线程调用)
def update_input_registers():
    # 示例:将采集值写入输入寄存器地址0~3
    values = [240, 502, 256, 680]  # 电压0.1V、电流0.01A、温度0.1℃、湿度0.1%RH
    slave_context.setValues(4, 0, values)  # 4表示输入寄存器功能码区域

# 启动前先写入一次初始采集值
update_input_registers()

if __name__ == "__main__":
    # 绑定网关所有网卡,端口502(需root权限,或用sudo运行)
    print("Modbus TCP Server starting on port 502...")
    StartTcpServer(context=server_context, address=("0.0.0.0", 502))

运行方式

  1. 将上述代码保存为modbus_server.py,上传至网关。
  2. 以root权限运行:sudo python3 modbus_server.py
  3. 确认网关IP地址(如192.168.1.100),确保主站与网关网络互通。

验证方法

在PC上使用Modbus Poll或Python的pymodbus客户端工具进行验证:

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# 读输入寄存器(功能码04),地址0,读4个
rr = client.read_input_registers(0, 4, unit=1)
print("输入寄存器:", rr.registers)  # 预期输出: [240, 502, 256, 680]

# 写保持寄存器(功能码06),地址0,写入值1234
client.write_register(0, 1234, unit=1)

# 读保持寄存器(功能码03),验证写入
hr = client.read_holding_registers(0, 1, unit=1)
print("保持寄存器:", hr.registers)  # 预期输出: [1234]

client.close()

通过条件:

  • 读输入寄存器返回[240, 502, 256, 680],与代码中写入值一致。
  • 写保持寄存器后回读值变为1234,说明写入生效。
  • 若主站使用其他单元ID(如unit=2),由于single=True,服务器同样响应。

常见问题

  • 端口占用:502端口需root权限,若提示权限不足,使用sudo运行,或改用大于1024的端口(如1502)测试。
  • 数据更新:实际工程中,update_input_registers()应由采集线程周期性调用,将实时数据写入寄存器区。
  • 寄存器地址映射:Modbus协议中地址从0开始,主站访问地址0对应数据块起始位置。若主站习惯使用1-based地址,需在网关侧做偏移映射。