using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Modbus; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests; /// /// #144 family-native parser branch — DL205 + MELSEC. Family flag drives the parser to /// try the family's native syntax (V2000, D100, X20 hex/octal) before falling back to /// Modicon / mnemonic. /// [Trait("Category", "Unit")] public sealed class ModbusFamilyParserTests { // ----- DL205 native: V-memory (octal), Y/C/X/SP coils + discrete ----- [Theory] [InlineData("V0", 0)] [InlineData("V2000", 1024)] // octal 2000 = decimal 1024 [InlineData("V40400", 16640)] // octal 40400 = decimal 16640 (system bank in user mapping per the helper) public void DL205_VMemory_To_HoldingRegisters(string addr, int expectedOffset) { var p = ModbusAddressParser.Parse(addr, ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.HoldingRegisters); p.Offset.ShouldBe((ushort)expectedOffset); p.DataType.ShouldBe(ModbusDataType.Int16); } [Fact] public void DL205_Y_Output_Maps_To_Coils_Bank() { var p = ModbusAddressParser.Parse("Y0", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.Coils); p.Offset.ShouldBe((ushort)2048); // YOutputBaseCoil p.DataType.ShouldBe(ModbusDataType.Bool); } [Fact] public void DL205_C_Relay_Maps_To_Coils_Bank_NotModiconCoil() { // Cross-family ambiguity check: under Generic, "C100" is mnemonic Coils[99]. // Under DL205 family, "C100" is a control relay → CRelayBaseCoil + octal(100) = 3072 + 64. var p = ModbusAddressParser.Parse("C100", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.Coils); p.Offset.ShouldBe((ushort)(3072 + 64)); } [Fact] public void DL205_X_Input_Maps_To_DiscreteInputs() { var p = ModbusAddressParser.Parse("X17", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.DiscreteInputs); p.Offset.ShouldBe((ushort)15); // octal 17 = decimal 15 } [Fact] public void DL205_SP_Special_Relay_Maps_To_DiscreteInputs() { var p = ModbusAddressParser.Parse("SP10", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.DiscreteInputs); p.Offset.ShouldBe((ushort)(1024 + 8)); // SpecialBaseDiscrete + octal(10) } [Fact] public void DL205_Falls_Back_To_Modicon_When_Native_Misses() { // 40001 isn't a DL205 native form — falls through to the Modicon parser, returns // HoldingRegisters[0]. Important for users mixing legacy Modicon entries with native. var p = ModbusAddressParser.Parse("40001", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.HoldingRegisters); p.Offset.ShouldBe((ushort)0); } // ----- MELSEC native: D / X / Y / M with sub-family-aware X/Y parsing ----- [Fact] public void MELSEC_D_Register_Maps_To_HoldingRegisters() { var p = ModbusAddressParser.Parse("D100", ModbusFamily.MELSEC); p.Region.ShouldBe(ModbusRegion.HoldingRegisters); p.Offset.ShouldBe((ushort)100); // base 0 + decimal 100 } [Fact] public void MELSEC_M_Relay_Maps_To_Coils_Decimal() { var p = ModbusAddressParser.Parse("M50", ModbusFamily.MELSEC); p.Region.ShouldBe(ModbusRegion.Coils); p.Offset.ShouldBe((ushort)50); } [Fact] public void MELSEC_Q_Family_Treats_X20_As_Hex() { var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.Q_L_iQR); p.Region.ShouldBe(ModbusRegion.DiscreteInputs); p.Offset.ShouldBe((ushort)0x20); // hex 20 = decimal 32 } [Fact] public void MELSEC_F_Family_Treats_X20_As_Octal() { var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.F_iQF); p.Region.ShouldBe(ModbusRegion.DiscreteInputs); p.Offset.ShouldBe((ushort)16); // octal 20 = decimal 16 } // ----- Cross-family safety / Generic regression ----- [Fact] public void Generic_Family_Does_Not_Try_DL205_Branch() { // "V2000" under Generic isn't a known mnemonic OR a Modicon address → parse fails. // (Only DL205 / MELSEC families know V-memory.) ModbusAddressParser.TryParse("V2000", ModbusFamily.Generic, MelsecFamily.Q_L_iQR, out _, out var error) .ShouldBeFalse(); error.ShouldNotBeNull(); } [Fact] public void C100_Under_Generic_Means_Modicon_Coil_99() { // Regression guard against the cross-family ambiguity: Generic must keep mnemonic "C" // mapping (Coil at offset = decimal-100 - 1). var p = ModbusAddressParser.Parse("C100", ModbusFamily.Generic); p.Region.ShouldBe(ModbusRegion.Coils); p.Offset.ShouldBe((ushort)99); } [Fact] public void Suffix_Grammar_Composes_With_Native_Address() { // V2000:F:CDAB:5 should parse end-to-end: DL205 V2000 → HR[1024], Float32, word-swap, array of 5. var p = ModbusAddressParser.Parse("V2000:F:CDAB:5", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.HoldingRegisters); p.Offset.ShouldBe((ushort)1024); p.DataType.ShouldBe(ModbusDataType.Float32); p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap); p.ArrayCount.ShouldBe(5); } [Fact] public void DL205_Bit_Suffix_On_VMemory() { var p = ModbusAddressParser.Parse("V2000.7", ModbusFamily.DL205); p.Region.ShouldBe(ModbusRegion.HoldingRegisters); p.Offset.ShouldBe((ushort)1024); p.Bit.ShouldBe((byte)7); p.DataType.ShouldBe(ModbusDataType.BitInRegister); } }