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);
}
}