650e27075f
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
95 lines
4.1 KiB
C#
95 lines
4.1 KiB
C#
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);
|
|
}
|
|
}
|