test(modbus-rtu): add rtu_over_tcp pymodbus fixture + RTU-over-TCP integration tests
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
+94
@@ -0,0 +1,94 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end round-trip against the <c>rtu_over_tcp.json</c> pymodbus profile (or a real
|
||||
/// serial→Ethernet Modbus gateway when <c>MODBUS_RTU_SIM_ENDPOINT</c> points at one), driving the
|
||||
/// full <see cref="ModbusDriver"/> + real <see cref="ModbusRtuOverTcpTransport"/> stack with
|
||||
/// <see cref="ModbusTransportMode.RtuOverTcp"/>. Proves the driver reads a seeded holding register
|
||||
/// and writes+reads-back a scratch register when the wire carries RTU framing
|
||||
/// (<c>[addr][PDU][CRC-16]</c>, no MBAP header) rather than Modbus/TCP.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// pymodbus 3.13's simulator honors <c>framer=rtu</c> on a TCP server — confirmed on the wire
|
||||
/// (controller spike, 2026-07-24): a raw RTU FC03 read of HR[5] returned
|
||||
/// <c>01 03 02 00 05 78 47</c>, 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.
|
||||
/// </remarks>
|
||||
[Collection(ModbusRtuOverTcpCollection.Name)]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Device", "RtuOverTcp")]
|
||||
public sealed class ModbusRtuOverTcpTests(ModbusRtuOverTcpFixture sim)
|
||||
{
|
||||
/// <summary>Reads a seeded holding register (HR[5]=5) over RTU-over-TCP framing.</summary>
|
||||
[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
|
||||
}
|
||||
|
||||
/// <summary>Writes then reads back a scratch holding register (HR[200]) over RTU-over-TCP framing.</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user