using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; [Trait("Category", "Unit")] public sealed class DirectLogicAddressTests { [Theory] [InlineData("V0", (ushort)0x0000)] [InlineData("V1", (ushort)0x0001)] [InlineData("V7", (ushort)0x0007)] [InlineData("V10", (ushort)0x0008)] [InlineData("V2000", (ushort)0x0400)] // canonical DL205/DL260 user-memory start [InlineData("V7777", (ushort)0x0FFF)] [InlineData("V10000", (ushort)0x1000)] [InlineData("V17777", (ushort)0x1FFF)] public void UserVMemoryToPdu_converts_octal_V_prefix(string v, ushort expected) => DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected); [Theory] [InlineData("0", (ushort)0)] [InlineData("2000", (ushort)0x0400)] [InlineData("v2000", (ushort)0x0400)] // lowercase v [InlineData(" V2000 ", (ushort)0x0400)] // surrounding whitespace public void UserVMemoryToPdu_accepts_bare_or_prefixed_or_padded(string v, ushort expected) => DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected); [Theory] [InlineData("V8")] // 8 is not a valid octal digit [InlineData("V19")] [InlineData("V2009")] public void UserVMemoryToPdu_rejects_non_octal_digits(string v) { Should.Throw(() => DirectLogicAddress.UserVMemoryToPdu(v)) .Message.ShouldContain("octal"); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("V")] public void UserVMemoryToPdu_rejects_empty_input(string? v) => Should.Throw(() => DirectLogicAddress.UserVMemoryToPdu(v!)); [Fact] public void UserVMemoryToPdu_overflow_rejected() { // 200000 octal = 0x10000 — one past ushort range. Should.Throw(() => DirectLogicAddress.UserVMemoryToPdu("V200000")); } [Fact] public void SystemVMemoryBasePdu_is_0x2100_for_V40400() { // V40400 on DL260 / H2-ECOM100 absolute mode → PDU 0x2100 (decimal 8448), NOT 0x4100 // which a naive octal-to-decimal of 40400 octal would give (= 16640). DirectLogicAddress.SystemVMemoryBasePdu.ShouldBe((ushort)0x2100); DirectLogicAddress.SystemVMemoryToPdu(0).ShouldBe((ushort)0x2100); } [Fact] public void SystemVMemoryToPdu_offsets_within_bank() { DirectLogicAddress.SystemVMemoryToPdu(1).ShouldBe((ushort)0x2101); DirectLogicAddress.SystemVMemoryToPdu(0x100).ShouldBe((ushort)0x2200); } [Fact] public void SystemVMemoryToPdu_rejects_overflow() { // ushort wrap: 0xFFFF - 0x2100 = 0xDEFF; anything above should throw. Should.NotThrow(() => DirectLogicAddress.SystemVMemoryToPdu(0xDEFF)); Should.Throw(() => DirectLogicAddress.SystemVMemoryToPdu(0xDF00)); } // --- Bit memory: Y-output, C-relay, X-input, SP-special --- [Theory] [InlineData("Y0", (ushort)2048)] [InlineData("Y1", (ushort)2049)] [InlineData("Y7", (ushort)2055)] [InlineData("Y10", (ushort)2056)] // octal 10 = decimal 8 [InlineData("Y17", (ushort)2063)] // octal 17 = decimal 15 [InlineData("Y777", (ushort)2559)] // top of DL260 Y range per doc table public void YOutputToCoil_adds_octal_offset_to_2048(string y, ushort expected) => DirectLogicAddress.YOutputToCoil(y).ShouldBe(expected); [Theory] [InlineData("C0", (ushort)3072)] [InlineData("C1", (ushort)3073)] [InlineData("C10", (ushort)3080)] [InlineData("C1777", (ushort)4095)] // top of DL260 C range public void CRelayToCoil_adds_octal_offset_to_3072(string c, ushort expected) => DirectLogicAddress.CRelayToCoil(c).ShouldBe(expected); [Theory] [InlineData("X0", (ushort)0)] [InlineData("X17", (ushort)15)] [InlineData("X777", (ushort)511)] // top of DL260 X range public void XInputToDiscrete_adds_octal_offset_to_0(string x, ushort expected) => DirectLogicAddress.XInputToDiscrete(x).ShouldBe(expected); [Theory] [InlineData("SP0", (ushort)1024)] [InlineData("SP7", (ushort)1031)] [InlineData("sp0", (ushort)1024)] // lowercase prefix [InlineData("SP777", (ushort)1535)] public void SpecialToDiscrete_adds_octal_offset_to_1024(string sp, ushort expected) => DirectLogicAddress.SpecialToDiscrete(sp).ShouldBe(expected); [Theory] [InlineData("Y8")] [InlineData("C9")] [InlineData("X18")] public void Bit_address_rejects_non_octal_digits(string bad) => Should.Throw(() => { if (bad[0] == 'Y') DirectLogicAddress.YOutputToCoil(bad); else if (bad[0] == 'C') DirectLogicAddress.CRelayToCoil(bad); else DirectLogicAddress.XInputToDiscrete(bad); }); [Theory] [InlineData("Y")] [InlineData("C")] [InlineData("")] public void Bit_address_rejects_empty(string bad) => Should.Throw(() => DirectLogicAddress.YOutputToCoil(bad)); [Fact] public void YOutputToCoil_accepts_lowercase_prefix() => DirectLogicAddress.YOutputToCoil("y0").ShouldBe((ushort)2048); [Fact] public void CRelayToCoil_accepts_bare_octal_without_C_prefix() => DirectLogicAddress.CRelayToCoil("0").ShouldBe((ushort)3072); }