using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// /// End-to-end round-trip against the rtu_over_tcp.json pymodbus profile (or a real /// serial→Ethernet Modbus gateway when MODBUS_RTU_SIM_ENDPOINT points at one), driving the /// full + real stack with /// . Proves the driver reads a seeded holding register /// and writes+reads-back a scratch register when the wire carries RTU framing /// ([addr][PDU][CRC-16], no MBAP header) rather than Modbus/TCP. /// /// /// pymodbus 3.13's simulator honors framer=rtu on a TCP server — confirmed on the wire /// (controller spike, 2026-07-24): a raw RTU FC03 read of HR[5] returned /// 01 03 02 00 05 78 47, a pure RTU frame with no MBAP header (data 0x0005 = 5). So the /// fixture is the plain pymodbus simulator with framer/port swapped — no stdlib fallback server. /// [Collection(ModbusRtuOverTcpCollection.Name)] [Trait("Category", "Integration")] [Trait("Device", "RtuOverTcp")] public sealed class ModbusRtuOverTcpTests(ModbusRtuOverTcpFixture sim) { /// Reads a seeded holding register (HR[5]=5) over RTU-over-TCP framing. [Fact] public async Task Reads_a_seeded_holding_register_over_rtu_framing() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); var opts = new ModbusDriverOptions { Host = sim.Host, Port = sim.Port, UnitId = 1, Transport = ModbusTransportMode.RtuOverTcp, Timeout = TimeSpan.FromSeconds(2), Probe = new ModbusProbeOptions { Enabled = false }, RawTags = ModbusRawTags.Entries([ new ModbusTagDefinition( Name: "HR5", Region: ModbusRegion.HoldingRegisters, Address: 5, DataType: ModbusDataType.UInt16, Writable: false), ]), }; await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-rtu-int"); await driver.InitializeAsync(driverConfigJson: "{}", TestContext.Current.CancellationToken); var results = await driver.ReadAsync(["HR5"], TestContext.Current.CancellationToken); results.Count.ShouldBe(1); results[0].StatusCode.ShouldBe(0u); // Good results[0].Value.ShouldBe((ushort)5); // HR[5] seeded = address-as-value } /// Writes then reads back a scratch holding register (HR[200]) over RTU-over-TCP framing. [Fact] public async Task Writes_and_reads_back_a_holding_register_over_rtu_framing() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); var opts = new ModbusDriverOptions { Host = sim.Host, Port = sim.Port, UnitId = 1, Transport = ModbusTransportMode.RtuOverTcp, Timeout = TimeSpan.FromSeconds(2), Probe = new ModbusProbeOptions { Enabled = false }, RawTags = ModbusRawTags.Entries([ new ModbusTagDefinition( Name: "HR200", Region: ModbusRegion.HoldingRegisters, Address: 200, DataType: ModbusDataType.UInt16, Writable: true), ]), }; await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-rtu-int-w"); await driver.InitializeAsync(driverConfigJson: "{}", TestContext.Current.CancellationToken); var writes = await driver.WriteAsync( [new WriteRequest("HR200", (ushort)4242)], TestContext.Current.CancellationToken); writes.Count.ShouldBe(1); writes[0].StatusCode.ShouldBe(0u); var back = await driver.ReadAsync(["HR200"], TestContext.Current.CancellationToken); back.Count.ShouldBe(1); back[0].StatusCode.ShouldBe(0u); back[0].Value.ShouldBe((ushort)4242); } }