Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_ByteOrderTests.cs
Joseph Doherty b54724a812 Phase 3 PR 57 -- S7 byte-order + fingerprint integration tests against s7_1500 pymodbus profile. Three facts in new S7_ByteOrderTests class: (1) S7_Float32_ABCD_decodes_1_5f_from_HR100 reads HR[100..101] with ModbusByteOrder.BigEndian AND with WordSwap on the same wire bytes; asserts BigEndian==1.5f AND WordSwap!=1.5f -- proving both that Siemens S7 stores Float32 in ABCD word order (opposite of DL260 CDAB) and that the ByteOrder flag is not a no-op on the same wire buffer. (2) S7_Int32_ABCD_decodes_0x12345678_from_HR300 reads HR[300]=0x1234 + HR[301]=0x5678 with BigEndian and asserts the reassembled Int32 = 0x12345678; documents the contrast with DL260 CDAB Int32 encoding. (3) S7_DB1_fingerprint_marker_at_HR0_reads_0xABCD reads HR[0]=0xABCD -- real MB_SERVER deployments reserve DB1.DBW0 as a fingerprint so clients can verify they're pointing at the right DB, protecting against typos in the MB_SERVER.MB_HOLD_REG.DB_number parameter. No driver code changes -- the ByteOrder.BigEndian path has existed since PR 24; this PR exists to lock in the S7-specific semantics at the integration level so future refactors of NormalizeWordOrder can't silently break S7. All 3 tests gate on MODBUS_SIM_PROFILE=s7_1500 so they skip cleanly against dl205 or standard profiles. Verified end-to-end: 4/4 S7 integration tests pass (1 smoke from PR 56 + 3 new). No regression in driver unit tests. Per the per-quirk-PR plan: the S7 quirks NOT testable via pymodbus sim (MB_SERVER STATUS 0x8383 optimized-DB behavior, port-per-connection semantics, CP 343-1 Lean license rejection, STOP-mode non-determinism) remain in docs/v2/s7.md as design guidance for driver users rather than automated tests -- they're TIA-Portal-side or CP-hardware-side behaviors that pymodbus cannot reproduce without custom Python actions.
2026-04-18 22:58:44 -04:00

133 lines
5.9 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
/// <summary>
/// Verifies the Siemens S7 big-endian (<c>ABCD</c>) word-order default for Float32 and
/// Int32 against the <c>s7_1500.json</c> pymodbus profile. S7's native CPU types are
/// big-endian end-to-end, so <c>MB_SERVER</c> places the high word at the lower register
/// address — <b>opposite</b> of DL260's CDAB. The driver's S7-family tag config must
/// therefore default to <see cref="ModbusByteOrder.BigEndian"/>; selecting
/// <see cref="ModbusByteOrder.WordSwap"/> against an S7 would decode garbage.
/// </summary>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "S7")]
public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
{
[Fact]
public async Task S7_Float32_ABCD_decodes_1_5f_from_HR100()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "s7_1500",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != s7_1500 — skipping (s7_1500 profile is the only one seeding HR[100..101] ABCD).");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Float_ABCD",
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
// Control: same address with WordSwap should decode garbage — proves the
// two code paths diverge on S7 wire bytes.
new ModbusTagDefinition("S7_Float_CDAB_control",
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.WordSwap),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-float-abcd");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(
["S7_Float_ABCD", "S7_Float_CDAB_control"],
TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe(1.5f, "S7 MB_SERVER stores Float32 in ABCD word order; BigEndian decode returns 1.5f");
results[1].StatusCode.ShouldBe(0u);
results[1].Value.ShouldNotBe(1.5f, "applying CDAB swap to S7 ABCD bytes must produce a different value — confirms the flag is not a no-op and S7 profile default must be BigEndian");
}
[Fact]
public async Task S7_Int32_ABCD_decodes_0x12345678_from_HR300()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "s7_1500",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != s7_1500 — skipping.");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Int32_ABCD",
ModbusRegion.HoldingRegisters, Address: 300,
DataType: ModbusDataType.Int32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-int-abcd");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["S7_Int32_ABCD"], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe(0x12345678,
"S7 Int32 stored as HR[300]=0x1234, HR[301]=0x5678 with ABCD order decodes to 0x12345678 — DL260 would store the reverse order");
}
[Fact]
public async Task S7_DB1_fingerprint_marker_at_HR0_reads_0xABCD()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "s7_1500",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != s7_1500 — skipping.");
}
// Real-world MB_SERVER deployments typically reserve DB1.DBW0 as a fingerprint so
// clients can verify they're pointing at the right DB (protects against typos in
// the MB_SERVER.MB_HOLD_REG.DB_number parameter). 0xABCD is the convention.
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Fingerprint",
ModbusRegion.HoldingRegisters, Address: 0,
DataType: ModbusDataType.UInt16, Writable: false),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-fingerprint");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["S7_Fingerprint"], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe((ushort)0xABCD);
}
}