diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_ByteOrderTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_ByteOrderTests.cs new file mode 100644 index 0000000..e103100 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_ByteOrderTests.cs @@ -0,0 +1,132 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7; + +/// +/// Verifies the Siemens S7 big-endian (ABCD) word-order default for Float32 and +/// Int32 against the s7_1500.json pymodbus profile. S7's native CPU types are +/// big-endian end-to-end, so MB_SERVER places the high word at the lower register +/// address — opposite of DL260's CDAB. The driver's S7-family tag config must +/// therefore default to ; selecting +/// against an S7 would decode garbage. +/// +[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); + } +}