Phase 3 PR 48 -- DL205 CDAB word order for Float32 end-to-end test. The driver has supported ModbusByteOrder.WordSwap (CDAB) since PR 24 for all multi-register types -- the underlying word-swap code path was already there. PR 48 closes the loop with an integration test that validates it end-to-end against the dl205 pymodbus profile: HR[1056..1057] stores IEEE-754 1.5f with the low word at the lower address (0x0000 at HR[1056], 0x3FC0 at HR[1057]). Reading with WordSwap returns 1.5f; reading with BigEndian returns a tiny denormal (~5.74e-41) -- a silent "value is 0" bug that typically surfaces in the field only when an operator notices a setpoint readout stuck at 0 while the PLC display shows the real value. Test asserts both: WordSwap==1.5f AND BigEndian!=1.5f, proving the flag is not a no-op. No driver code changes -- the word-swap normalization at NormalizeWordOrder() has handled Float32/Int32/UInt32 correctly since PR 24 and the unit test suite already covers it (Int32_WordSwap_decodes_CDAB_layout + Float32 equivalent). This PR exists primarily to lock in the integration-level validation so future refactors of the codec don't silently break DL205/DL260 floats. 6/6 DL205 integration tests pass with MODBUS_SIM_PROFILE=dl205.

This commit is contained in:
Joseph Doherty
2026-04-18 21:51:15 -04:00
parent 2b5222f5db
commit 463c5a4320

View File

@@ -0,0 +1,64 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
/// <summary>
/// Verifies DL205/DL260 CDAB word ordering for 32-bit floats against the
/// <c>dl205.json</c> pymodbus profile. DirectLOGIC stores IEEE-754 singles with the low
/// word at the lower register address (CDAB) rather than the high word (ABCD). Reading
/// <c>HR[1056..1057]</c> with <see cref="ModbusByteOrder.BigEndian"/> produces a tiny
/// denormal (~5.74e-41) instead of the intended 1.5f — a silent "value is 0" bug in the
/// field unless the caller opts into <see cref="ModbusByteOrder.WordSwap"/>.
/// </summary>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "DL205")]
public sealed class DL205FloatCdabQuirkTests(ModbusSimulatorFixture sim)
{
[Fact]
public async Task DL205_Float32_CDAB_decodes_1_5f_from_HR1056()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "dl205",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != dl205 — skipping (standard profile does not seed HR[1056..1057]).");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("DL205_Float_CDAB",
ModbusRegion.HoldingRegisters, Address: 1056,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.WordSwap),
// Control: same address, BigEndian — proves the default decode produces garbage.
new ModbusTagDefinition("DL205_Float_ABCD",
ModbusRegion.HoldingRegisters, Address: 1056,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-cdab");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["DL205_Float_CDAB", "DL205_Float_ABCD"],
TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe(1.5f, "DL205 Float32 with WordSwap (CDAB) must decode HR[1056..1057] as 1.5f");
// The BigEndian read of the same wire bytes should differ — not asserting the exact
// denormal value (that couples the test to IEEE-754 bit math) but the two decodes MUST
// disagree, otherwise the word-order flag is a no-op.
results[1].StatusCode.ShouldBe(0u);
results[1].Value.ShouldNotBe(1.5f);
}
}