(十)多UnitId模拟:一个网关下面挂多个从站怎么测
GitHub 项目地址:https://github.com/lidecong133/YModbus
单个从站模拟能解决很多问题,但还不够。
现场经常遇到这种结构:一台 TCP 网关下面挂多台 RTU 仪表,或者一条 RS485 总线上挂了多个从站。主站连的 IP、端口或串口都一样,变化的是 UnitId / SlaveId。
这时候要测试主站轮询逻辑,就需要多站号模拟。
YModbus 里对应的是:
ModbusTcpSlaveNetworkModbusRtuSlaveNetworkModbusAsciiSlaveNetwork
UnitId和SlaveId先别混
TCP 里常叫 Unit ID。
RTU / ASCII 里常叫 Slave ID。
名字不同,本质都是目标站号。
TCP 网关场景下,host + port只是连接到网关,UnitId 才是网关后面的设备。
RTU 总线场景下,串口参数只是打开总线,SlaveId 才是总线上的目标设备。
这个概念没分清,多站号调试一定会乱。
TCP多UnitId示例
下面这个例子在本机127.0.0.1:1502上模拟两个 UnitId。
usingSystem.Net;usingYModbus.Slave;ModbusSlaveDataStoreunit1Store=new(pointCount:100);unit1Store.SetHoldingRegister(0,1234);unit1Store.SetHoldingRegister(1,5678);ModbusSlaveDataStoreunit2Store=new(pointCount:100);unit2Store.SetHoldingRegister(0,2222);unit2Store.SetHoldingRegister(1,3333);awaitusingModbusTcpSlaveNetworknetwork=new(newModbusTcpSlaveNetworkOptions{ListenAddress=IPAddress.Loopback,Port=1502});network.AddSlave(newModbusSlaveDefinition{UnitId=1,PointCount=100},unit1Store);network.AddSlave(newModbusSlaveDefinition{UnitId=2,PointCount=100},unit2Store);awaitnetwork.StartAsync();Console.WriteLine("TCP slave network is running on 127.0.0.1:1502.");Console.ReadLine();awaitnetwork.StopAsync();主站读 1 号站,返回1234、5678。
主站读 2 号站,返回2222、3333。
同一个端口,不同 UnitId,数据区不同。这就是 TCP 网关测试的核心。
主站侧用MultiUnitClient更顺
这种场景主站侧建议用ModbusMultiUnitClient。
usingYModbus.Clients;awaitusingModbusMultiUnitClientclient=awaitModbusClientFactory.CreateTcpMultiUnitAsync(host:"127.0.0.1",port:1502);ushort[]unit1=awaitclient.ReadHoldingRegistersAsync(1,0,2);ushort[]unit2=awaitclient.ReadHoldingRegistersAsync(2,0,2);第一个参数就是 UnitId。
这样代码和现场结构能对上:同一个网关,多个站号。
运行中修改某个站号的数据
测试主站轮询时,经常要模拟某个设备数据变化。
可以通过TryGetDataStore拿到对应站号的数据区:
if(network.TryGetDataStore(1,outIModbusSlaveDataStore?store)&&storeisnotnull){store.SetHoldingRegister(0,9999);}主站下一轮读 UnitId 1,就会读到新值。
这可以模拟温度变化、报警置位、计数器增加。比拿真实设备反复造条件方便很多。
RTU多SlaveId
RTU 多站号网络使用同一个串口通道。
usingSystem.IO.Ports;usingYModbus.Serial;usingYModbus.Slave;usingSerialPortport=new("COM3",9600,Parity.None,8,StopBits.One);port.Open();awaitusingModbusRtuSlaveNetworknetwork=new(newSerialPortChannel(port,leaveOpen:true));network.AddSlave(newModbusSlaveDefinition{SlaveId=1,PointCount=100});network.AddSlave(newModbusSlaveDefinition{SlaveId=2,PointCount=100});awaitnetwork.StartAsync();Console.ReadLine();awaitnetwork.StopAsync();RTU 请求的第一个字节就是 SlaveId。网络会按这个字节把请求路由到不同数据区。
ASCII 多站号写法类似,只是换成ModbusAsciiSlaveNetwork。
awaitusingModbusAsciiSlaveNetworknetwork=new(newSerialPortChannel(port,leaveOpen:true));network.AddSlave(newModbusSlaveDefinition{SlaveId=1,PointCount=100});network.AddSlave(newModbusSlaveDefinition{SlaveId=2,PointCount=100});awaitnetwork.StartAsync();多站号最该看报文
多站号出错时,最先看的不是寄存器值,而是请求里的站号。
TCP 请求里,UnitId 会出现在 MBAP 后面。RTU 请求里,第一个字节就是 SlaveId。
如果你以为主站在读 2 号站,但从站报文里看到的是 1 号站,那问题就很明确。
可以给 network 挂 Traffic 事件:
network.Traffic+=(_,traffic)=>{Console.WriteLine($"{traffic.Direction}{traffic.Message}{Convert.ToHexString(traffic.Frame)}");};主站和从站两边都看报文,能少很多猜测。
适合做自动化回归
多 UnitId 模拟还有一个很实用的用途:给主站程序做回归测试。
你可以固定一套模拟环境:
UnitId 1: 温度设备 UnitId 2: 压力设备 UnitId 3: IO 模块每个设备放一组固定数据。主站跑一轮轮询,检查解析结果。
以后改主站代码,再跑同一套模拟环境,就能知道有没有把多站号逻辑改坏。
到这里
单个从站解决“模拟一个设备”。
多 UnitId / 多 SlaveId 网络解决“模拟一组设备”。
如果你的项目里有 TCP 网关、RS485 多仪表轮询、多设备采集,建议尽早用这套方式把测试环境搭起来。
