diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205XInputTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205XInputTests.cs new file mode 100644 index 0000000..5073113 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205XInputTests.cs @@ -0,0 +1,71 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205; + +/// +/// Verifies the DL260 X-input discrete-input mapping against the dl205.json +/// pymodbus profile. X-inputs are FC02 discrete-input-only (Modbus doesn't allow writes +/// to discrete inputs), and the DirectLOGIC convention is X0 → DI 0 with octal offsets +/// for subsequent addresses. The sim seeds X20 octal (= DI 16) = ON so the test can +/// prove the helper routes through to the right cell. +/// +/// +/// X0 / X1 / …X17 octal all share cell 0 (DI 0-15 → cell 0 bits 0-15) which conflicts +/// with the V0 uint16 marker; we can't seed both types at cell 0 under shared-blocks +/// semantics. So the test uses X20 octal (first address beyond the cell-0 boundary) which +/// lands cleanly at cell 1 bit 0 and leaves the V0 register-zero quirk intact. +/// +[Collection(ModbusSimulatorCollection.Name)] +[Trait("Category", "Integration")] +[Trait("Device", "DL205")] +public sealed class DL205XInputTests(ModbusSimulatorFixture sim) +{ + [Fact] + public async Task DL260_X20_octal_maps_to_DiscreteInput_16_and_reads_ON() + { + 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."); + } + + // X20 octal = decimal 16 = DI 16 per the DL260 convention (X-inputs start at DI 0). + var di = DirectLogicAddress.XInputToDiscrete("X20"); + di.ShouldBe((ushort)16); + + var options = BuildOptions(sim, [ + new ModbusTagDefinition("DL260_X20", + ModbusRegion.DiscreteInputs, Address: di, + DataType: ModbusDataType.Bool, Writable: false), + // Unpopulated-X control: pymodbus returns 0 (not exception) for any bit in the + // configured DI range that wasn't explicitly seeded — per docs/v2/dl205.md + // "Reading a non-populated X input ... returns zero, not an exception". + new ModbusTagDefinition("DL260_X21_off", + ModbusRegion.DiscreteInputs, Address: DirectLogicAddress.XInputToDiscrete("X21"), + DataType: ModbusDataType.Bool, Writable: false), + ]); + await using var driver = new ModbusDriver(options, driverInstanceId: "dl205-xinput"); + await driver.InitializeAsync("{}", TestContext.Current.CancellationToken); + + var results = await driver.ReadAsync(["DL260_X20", "DL260_X21_off"], TestContext.Current.CancellationToken); + + results[0].StatusCode.ShouldBe(0u); + results[0].Value.ShouldBe(true, "dl205.json seeds cell 1 bit 0 (X20 octal = DI 16) = ON"); + + results[1].StatusCode.ShouldBe(0u, "unpopulated X inputs must read cleanly — DL260 does NOT raise an exception"); + results[1].Value.ShouldBe(false); + } + + private static ModbusDriverOptions BuildOptions(ModbusSimulatorFixture sim, IReadOnlyList tags) + => new() + { + Host = sim.Host, + Port = sim.Port, + UnitId = 1, + Timeout = TimeSpan.FromSeconds(2), + Tags = tags, + Probe = new ModbusProbeOptions { Enabled = false }, + }; +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json index 0f01e8c..f39abd2 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json @@ -36,6 +36,7 @@ [1280, 1282], [1343, 1343], [1407, 1407], + [1, 1], [128, 128], [192, 192], [250, 250], @@ -88,6 +89,9 @@ ], "bits": [ + {"_quirk": "X-input bank marker cell. X0 -> DI 0 conflicts with uint16 V0 at cell 0, so this marker covers X20 octal (= decimal 16 = DI 16 = cell 1 bit 0). X20=ON, X23 octal (DI 19 = cell 1 bit 3)=ON -> cell 1 value = 0b00001001 = 9.", + "addr": 1, "value": 9}, + {"_quirk": "Y-output bank marker cell. pymodbus's simulator maps Modbus FC01/02/05 bit-addresses to cell index = bit_addr / 16; so Modbus coil 2048 lives at cell 128 bit 0. Y0=ON (bit 0), Y1=OFF (bit 1), Y2=ON (bit 2) -> value=0b00000101=5 proves DL260 mapping Y0 -> coil 2048.", "addr": 128, "value": 5},