From 286ab3ba414995af8cf4d8ee1944593a1eaf3947 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 25 Apr 2026 19:08:51 -0400 Subject: [PATCH] =?UTF-8?q?Auto:=20ablegacy-5=20=E2=80=94=20PD/MG/PLS/BT?= =?UTF-8?q?=20structure=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PD (PID), MG (Message), PLS (Programmable Limit Switch) and BT (Block Transfer) file types to the PCCC parser. New AbLegacyDataType enum members (PidElement / MessageElement / PlsElement / BlockTransferElement) plus per-type sub-element catalogue: - PD: SP/PV/CV/KP/KI/KD/MAXS/MINS/DB/OUT as Float32; EN/DN/MO/PE/ AUTO/MAN/SP_VAL/SP_LL/SP_HL as Boolean (word-0 status bits). - MG: RBE/MS/SIZE/LEN as Int32; EN/EW/ER/DN/ST/CO/NR/TO as Boolean. - PLS: LEN as Int32 (bit table varies per PLC). - BT: RLEN/DLEN as Int32; EN/ST/DN/ER/CO/EW/TO/NR as Boolean. Per-family flags on AbLegacyPlcFamilyProfile gate availability: - PD/MG: SLC500 + PLC-5 (operator + status bits both present). - PLS/BT: PLC-5 only (chassis-IO block transfer is PLC-5-specific). - MicroLogix + LogixPccc: rejected — no legacy file-letter form. Status-bit indices match Rockwell DTAM / 1747-RM001 / 1785-6.5.12: PD word 0 bits 0-8, MG/BT word 0 bits 8-15. PLC-set status bits (PE/DN/SP_*; ST/DN/ER/CO/EW/NR/TO) are surfaced as ViewOnly via IsPlcSetStatusBit, matching the Timer/Counter/Control pattern from ablegacy-3. LibplctagLegacyTagRuntime decodes PD non-bit members as Float32 and MG/BT/PLS non-bit members as Int32; status bits route through GetBit with the bit-position encoded by the driver via StatusBitIndex. Tests: parser positive cases per family + negative cases per family, catalogue + bit-index + read-only-bit assertions. Closes #248 --- .../AbLegacyAddress.cs | 36 +++- .../AbLegacyDataType.cs | 114 ++++++++++++ .../LibplctagLegacyTagRuntime.cs | 23 +++ .../PlcFamilies/AbLegacyPlcFamilyProfile.cs | 32 +++- .../AbLegacyAddressTests.cs | 166 ++++++++++++++++++ 5 files changed, 365 insertions(+), 6 deletions(-) diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs index 1bb89b6..5d4d712 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs @@ -156,9 +156,19 @@ public sealed record AbLegacyAddress( // Reject unknown file letters — these cover SLC/ML/PLC-5 canonical families. // Function-file letters (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI) are MicroLogix-only. + // Structure-file letters (PD/MG/PLS/BT) are gated per family — PD/MG are common on + // SLC500 + PLC-5; PLS/BT are PLC-5 only. MicroLogix and LogixPccc reject them. if (!IsKnownFileLetter(letter)) { - if (!IsFunctionFileLetter(letter) || profile?.SupportsFunctionFiles != true) return null; + if (IsFunctionFileLetter(letter)) + { + if (profile?.SupportsFunctionFiles != true) return null; + } + else if (IsStructureFileLetter(letter)) + { + if (!StructureFileSupported(letter, profile)) return null; + } + else return null; } var octalForIo = profile?.OctalIoAddressing == true && (letter == "I" || letter == "O"); @@ -273,4 +283,28 @@ public sealed record AbLegacyAddress( "RTC" or "HSC" or "DLS" or "MMI" or "PTO" or "PWM" or "STI" or "EII" or "IOS" or "BHI" => true, _ => false, }; + + /// + /// Structure-file prefixes added in #248: PD (PID), MG (Message), PLS (Programmable Limit + /// Switch), BT (Block Transfer). Per-family availability is gated by the matching + /// Supports*File flag on . + /// + internal static bool IsStructureFileLetter(string letter) => letter switch + { + "PD" or "MG" or "PLS" or "BT" => true, + _ => false, + }; + + private static bool StructureFileSupported(string letter, AbLegacyPlcFamilyProfile? profile) + { + if (profile is null) return false; + return letter switch + { + "PD" => profile.SupportsPidFile, + "MG" => profile.SupportsMessageFile, + "PLS" => profile.SupportsPlsFile, + "BT" => profile.SupportsBlockTransferFile, + _ => false, + }; + } } diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs index fc55970..562d77f 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs @@ -31,6 +31,30 @@ public enum AbLegacyDataType /// Sub-element catalogue lives in . /// MicroLogixFunctionFile, + /// + /// PD-file (PID) sub-element — caller addresses .SP, .PV, .CV, + /// .KP, .KI, .KD, .MAXS, .MINS, .DB, .OUT + /// (Float) and .EN, .DN, .MO, .PE, .AUTO, .MAN + /// (Boolean status bits in word 0). + /// + PidElement, + /// + /// MG-file (Message) sub-element — caller addresses .RBE, .MS, .SIZE, + /// .LEN (Int32) and .EN, .EW, .ER, .DN, .ST, + /// .CO, .NR, .TO (Boolean status bits). + /// + MessageElement, + /// + /// PLS-file (Programmable Limit Switch) sub-element — caller addresses .LEN + /// (Int32). Bit semantics vary by PLC; unknown sub-elements fall back to Int32. + /// + PlsElement, + /// + /// BT-file (Block Transfer) sub-element — caller addresses .RLEN, .DLEN + /// (Int32) and .EN, .ST, .DN, .ER, .CO, .EW, + /// .TO, .NR (Boolean status bits in word 0). + /// + BlockTransferElement, } /// @@ -107,6 +131,12 @@ public static class AbLegacyDataTypeExtensions AbLegacyDataType.TimerElement or AbLegacyDataType.CounterElement or AbLegacyDataType.ControlElement => DriverDataType.Int32, AbLegacyDataType.MicroLogixFunctionFile => DriverDataType.Int32, + // PD/MG/PLS/BT default to Int32 at the parent-element level. The sub-element-aware + // EffectiveDriverDataType refines specific members (Float for PID gains, Boolean for + // status bits). + AbLegacyDataType.PidElement or AbLegacyDataType.MessageElement + or AbLegacyDataType.PlsElement or AbLegacyDataType.BlockTransferElement + => DriverDataType.Int32, _ => DriverDataType.Int32, }; @@ -141,6 +171,39 @@ public static class AbLegacyDataTypeExtensions "LEN" or "POS" => DriverDataType.Int32, _ => t.ToDriverDataType(), }, + // PD-file (PID): SP/PV/CV/KP/KI/KD/MAXS/MINS/DB/OUT are 32-bit floats; EN/DN/MO/PE/ + // AUTO/MAN/SP_VAL/SP_LL/SP_HL are status bits in word 0. + AbLegacyDataType.PidElement => key switch + { + "SP" or "PV" or "CV" or "KP" or "KI" or "KD" + or "MAXS" or "MINS" or "DB" or "OUT" => DriverDataType.Float32, + "EN" or "DN" or "MO" or "PE" + or "AUTO" or "MAN" or "SP_VAL" or "SP_LL" or "SP_HL" => DriverDataType.Boolean, + _ => t.ToDriverDataType(), + }, + // MG-file (Message): RBE/MS/SIZE/LEN are control words; EN/EW/ER/DN/ST/CO/NR/TO are + // status bits. + AbLegacyDataType.MessageElement => key switch + { + "RBE" or "MS" or "SIZE" or "LEN" => DriverDataType.Int32, + "EN" or "EW" or "ER" or "DN" or "ST" or "CO" or "NR" or "TO" => DriverDataType.Boolean, + _ => t.ToDriverDataType(), + }, + // PLS-file (Programmable Limit Switch): LEN is a length word; bit semantics vary by + // PLC so unknown sub-elements stay Int32. + AbLegacyDataType.PlsElement => key switch + { + "LEN" => DriverDataType.Int32, + _ => t.ToDriverDataType(), + }, + // BT-file (Block Transfer, PLC-5): RLEN/DLEN are length words; EN/ST/DN/ER/CO/EW/ + // TO/NR are status bits in word 0. + AbLegacyDataType.BlockTransferElement => key switch + { + "RLEN" or "DLEN" => DriverDataType.Int32, + "EN" or "ST" or "DN" or "ER" or "CO" or "EW" or "TO" or "NR" => DriverDataType.Boolean, + _ => t.ToDriverDataType(), + }, _ => t.ToDriverDataType(), }; } @@ -187,6 +250,50 @@ public static class AbLegacyDataTypeExtensions "EN" => 15, _ => null, }, + // PD element word 0 (SLC 5/02+ PID, 1747-RM001 / PLC-5 PID-RM): bit 0=EN, 1=PE, + // 2=DN, 3=MO (manual mode), 4=AUTO, 5=MAN, 6=SP_VAL, 7=SP_LL, 8=SP_HL. Bits 4–8 are + // the SP-validity / SP-limit flags exposed in RSLogix 5 / 500. + AbLegacyDataType.PidElement => key switch + { + "EN" => 0, + "PE" => 1, + "DN" => 2, + "MO" => 3, + "AUTO" => 4, + "MAN" => 5, + "SP_VAL" => 6, + "SP_LL" => 7, + "SP_HL" => 8, + _ => null, + }, + // MG element word 0 (PLC-5 MSG / SLC 5/05 MSG, 1785-6.5.12 / 1747-RM001): + // bit 15=EN, 14=ST, 13=DN, 12=ER, 11=CO, 10=EW, 9=NR, 8=TO. + AbLegacyDataType.MessageElement => key switch + { + "TO" => 8, + "NR" => 9, + "EW" => 10, + "CO" => 11, + "ER" => 12, + "DN" => 13, + "ST" => 14, + "EN" => 15, + _ => null, + }, + // BT element word 0 (PLC-5 chassis BTR/BTW, 1785-6.5.12): + // bit 15=EN, 14=ST, 13=DN, 12=ER, 11=CO, 10=EW, 9=NR, 8=TO. Same layout as MG. + AbLegacyDataType.BlockTransferElement => key switch + { + "TO" => 8, + "NR" => 9, + "EW" => 10, + "CO" => 11, + "ER" => 12, + "DN" => 13, + "ST" => 14, + "EN" => 15, + _ => null, + }, _ => null, }; } @@ -205,6 +312,13 @@ public static class AbLegacyDataTypeExtensions AbLegacyDataType.TimerElement => key is "DN" or "TT", AbLegacyDataType.CounterElement => key is "DN" or "OV" or "UN", AbLegacyDataType.ControlElement => key is "DN" or "EM" or "ER" or "FD" or "UL" or "IN", + // PID: PE (PID-error), DN (process-done), SP_VAL/SP_LL/SP_HL are PLC-set status. + // EN/MO/AUTO/MAN are operator-controllable via the .EN bit / mode select. + AbLegacyDataType.PidElement => key is "PE" or "DN" or "SP_VAL" or "SP_LL" or "SP_HL", + // MG/BT: ST (started), DN (done), ER (error), CO (continuous), EW (enabled-waiting), + // NR (no-response), TO (timeout) are PLC-set. EN is operator-driven via the rung. + AbLegacyDataType.MessageElement => key is "ST" or "DN" or "ER" or "CO" or "EW" or "NR" or "TO", + AbLegacyDataType.BlockTransferElement => key is "ST" or "DN" or "ER" or "CO" or "EW" or "NR" or "TO", _ => false, }; } diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs index 556d3d7..e978886 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs @@ -48,6 +48,17 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime or AbLegacyDataType.ControlElement => bitIndex is int statusBit ? _tag.GetBit(statusBit) : _tag.GetInt32(0), + // PD-file (PID): non-bit members (SP/PV/CV/KP/KI/KD/MAXS/MINS/DB/OUT) are 32-bit floats. + // Status bits (EN/DN/MO/PE/AUTO/MAN/SP_VAL/SP_LL/SP_HL) live in the parent control word + // and read through GetBit — the driver encodes the position via StatusBitIndex. + AbLegacyDataType.PidElement => bitIndex is int pidBit + ? _tag.GetBit(pidBit) + : _tag.GetFloat32(0), + // MG/BT/PLS: non-bit members (RBE/MS/SIZE/LEN, RLEN/DLEN) are word-sized integers. + AbLegacyDataType.MessageElement or AbLegacyDataType.BlockTransferElement + or AbLegacyDataType.PlsElement => bitIndex is int statusBit2 + ? _tag.GetBit(statusBit2) + : _tag.GetInt32(0), _ => null, }; @@ -83,6 +94,18 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime case AbLegacyDataType.ControlElement: _tag.SetInt32(0, Convert.ToInt32(value)); break; + // PD-file non-bit writes route to the Float backing store. Status-bit writes within + // the parent word are blocked at the driver layer (PLC-set bits are read-only and + // operator-controllable bits go through the bit-RMW path with the parent word typed + // as Int). + case AbLegacyDataType.PidElement: + _tag.SetFloat32(0, Convert.ToSingle(value)); + break; + case AbLegacyDataType.MessageElement: + case AbLegacyDataType.BlockTransferElement: + case AbLegacyDataType.PlsElement: + _tag.SetInt32(0, Convert.ToInt32(value)); + break; default: throw new NotSupportedException($"AbLegacyDataType {type} not writable."); } diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs index 7cdea61..a657782 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs @@ -11,7 +11,11 @@ public sealed record AbLegacyPlcFamilyProfile( bool SupportsStringFile, bool SupportsLongFile, bool OctalIoAddressing, - bool SupportsFunctionFiles) + bool SupportsFunctionFiles, + bool SupportsPidFile, + bool SupportsMessageFile, + bool SupportsPlsFile, + bool SupportsBlockTransferFile) { public static AbLegacyPlcFamilyProfile ForFamily(AbLegacyPlcFamily family) => family switch { @@ -29,7 +33,11 @@ public sealed record AbLegacyPlcFamilyProfile( SupportsStringFile: true, // ST file available SLC 5/04+ SupportsLongFile: true, // L file available SLC 5/05+ OctalIoAddressing: false, // SLC500 I:/O: indices are decimal in RSLogix 500 - SupportsFunctionFiles: false); // SLC500 has no function files + SupportsFunctionFiles: false, // SLC500 has no function files + SupportsPidFile: true, // SLC 5/02+ supports PD via PID instruction + SupportsMessageFile: true, // SLC 5/02+ supports MG via MSG instruction + SupportsPlsFile: false, // SLC500 has no native PLS file (uses SQO/SQC instead) + SupportsBlockTransferFile: false); // SLC500 has no BT file (BT is PLC-5 ChassisIO only) public static readonly AbLegacyPlcFamilyProfile MicroLogix = new( LibplctagPlcAttribute: "micrologix", @@ -38,7 +46,11 @@ public sealed record AbLegacyPlcFamilyProfile( SupportsStringFile: true, SupportsLongFile: false, // ML 1100/1200/1400 don't ship L files OctalIoAddressing: false, // MicroLogix follows SLC-style decimal I/O addressing - SupportsFunctionFiles: true); // ML 1100/1400 expose RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI + SupportsFunctionFiles: true, // ML 1100/1400 expose RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI + SupportsPidFile: false, // MicroLogix 1100/1400 use PID-instruction-only addressing — no PD file type + SupportsMessageFile: false, // No MG file — MSG instruction control words live in standard files + SupportsPlsFile: false, + SupportsBlockTransferFile: false); public static readonly AbLegacyPlcFamilyProfile Plc5 = new( LibplctagPlcAttribute: "plc5", @@ -47,7 +59,11 @@ public sealed record AbLegacyPlcFamilyProfile( SupportsStringFile: true, SupportsLongFile: false, // PLC-5 predates L files OctalIoAddressing: true, // RSLogix 5 displays I:/O: word + bit indices as octal - SupportsFunctionFiles: false); + SupportsFunctionFiles: false, + SupportsPidFile: true, // PLC-5 PID instruction needs PD file + SupportsMessageFile: true, // PLC-5 MSG instruction needs MG file + SupportsPlsFile: true, // PLC-5 has PLS (programmable limit switch) file + SupportsBlockTransferFile: true); // PLC-5 chassis I/O block transfer (BTR/BTW) needs BT file /// /// Logix ControlLogix / CompactLogix accessed through the legacy PCCC compatibility layer. @@ -61,7 +77,13 @@ public sealed record AbLegacyPlcFamilyProfile( SupportsStringFile: true, SupportsLongFile: true, OctalIoAddressing: false, // Logix natively uses decimal arrays even via the PCCC bridge - SupportsFunctionFiles: false); + SupportsFunctionFiles: false, + // Logix native UDTs (PID_ENHANCED / MESSAGE) replace the legacy PD/MG file types — the + // PCCC bridge does not expose them as letter-prefixed files. + SupportsPidFile: false, + SupportsMessageFile: false, + SupportsPlsFile: false, + SupportsBlockTransferFile: false); } /// Which PCCC PLC family the device is. diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs index c4f20ea..8ec8fa5 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs @@ -333,4 +333,170 @@ public sealed class AbLegacyAddressTests { AbLegacyFunctionFile.SubElementType(letter, sub).ShouldBe(expected); } + + // ---- Structure files PD/MG/PLS/BT (Issue #248) ---- + // + // PD (PID), MG (Message), PLS (Programmable Limit Switch), BT (Block Transfer) — accepted on + // SLC500 + PLC-5 for PD/MG, PLC-5 only for PLS/BT. MicroLogix and LogixPccc reject all four. + + [Theory] + [InlineData("PD10:0.SP")] + [InlineData("PD10:0.PV")] + [InlineData("PD10:0.KP")] + [InlineData("PD10:0.EN")] + [InlineData("MG11:0.LEN")] + [InlineData("MG11:0.DN")] + public void TryParse_Slc500_accepts_pd_and_mg(string input) + { + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldNotBeNull(); + } + + [Theory] + [InlineData("PLS12:0.LEN")] + [InlineData("BT13:0.RLEN")] + [InlineData("BT13:0.EN")] + public void TryParse_Slc500_rejects_pls_and_bt(string input) + { + // PLS/BT are PLC-5 only; SLC500 must reject. + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldBeNull(); + } + + [Theory] + [InlineData("PD10:0.KP", "PD", "KP")] + [InlineData("MG11:0.EN", "MG", "EN")] + [InlineData("PLS12:0.LEN", "PLS", "LEN")] + [InlineData("BT13:0.RLEN", "BT", "RLEN")] + [InlineData("BT13:0.DN", "BT", "DN")] + public void TryParse_Plc5_accepts_all_structure_files(string input, string letter, string sub) + { + var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5); + a.ShouldNotBeNull(); + a.FileLetter.ShouldBe(letter); + a.SubElement.ShouldBe(sub); + } + + [Theory] + [InlineData("PD10:0.SP")] + [InlineData("MG11:0.LEN")] + [InlineData("PLS12:0.LEN")] + [InlineData("BT13:0.RLEN")] + public void TryParse_MicroLogix_rejects_all_structure_files(string input) + { + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.MicroLogix).ShouldBeNull(); + } + + [Theory] + [InlineData("PD10:0.SP")] + [InlineData("MG11:0.LEN")] + [InlineData("PLS12:0.LEN")] + [InlineData("BT13:0.RLEN")] + public void TryParse_LogixPccc_rejects_all_structure_files(string input) + { + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.LogixPccc).ShouldBeNull(); + } + + [Fact] + public void TryParse_Default_overload_rejects_structure_files() + { + // Without a family the parser cannot allow structure-file letters. + AbLegacyAddress.TryParse("PD10:0.SP").ShouldBeNull(); + AbLegacyAddress.TryParse("MG11:0.LEN").ShouldBeNull(); + AbLegacyAddress.TryParse("PLS12:0.LEN").ShouldBeNull(); + AbLegacyAddress.TryParse("BT13:0.RLEN").ShouldBeNull(); + } + + [Fact] + public void Profiles_advertise_structure_file_support_per_family() + { + AbLegacyPlcFamilyProfile.Slc500.SupportsPidFile.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Slc500.SupportsMessageFile.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Slc500.SupportsPlsFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.Slc500.SupportsBlockTransferFile.ShouldBeFalse(); + + AbLegacyPlcFamilyProfile.Plc5.SupportsPidFile.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Plc5.SupportsMessageFile.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Plc5.SupportsPlsFile.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Plc5.SupportsBlockTransferFile.ShouldBeTrue(); + + AbLegacyPlcFamilyProfile.MicroLogix.SupportsPidFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.MicroLogix.SupportsMessageFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.MicroLogix.SupportsPlsFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.MicroLogix.SupportsBlockTransferFile.ShouldBeFalse(); + + AbLegacyPlcFamilyProfile.LogixPccc.SupportsPidFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.LogixPccc.SupportsMessageFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.LogixPccc.SupportsPlsFile.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.LogixPccc.SupportsBlockTransferFile.ShouldBeFalse(); + } + + [Theory] + // PID Float members. + [InlineData(AbLegacyDataType.PidElement, "SP", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + [InlineData(AbLegacyDataType.PidElement, "PV", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + [InlineData(AbLegacyDataType.PidElement, "KP", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + [InlineData(AbLegacyDataType.PidElement, "KI", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + [InlineData(AbLegacyDataType.PidElement, "KD", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + [InlineData(AbLegacyDataType.PidElement, "OUT", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Float32)] + // PID status bits. + [InlineData(AbLegacyDataType.PidElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.PidElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.PidElement, "MO", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.PidElement, "PE", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + // MG Int32 control words. + [InlineData(AbLegacyDataType.MessageElement, "RBE", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData(AbLegacyDataType.MessageElement, "LEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + // MG status bits. + [InlineData(AbLegacyDataType.MessageElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.MessageElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.MessageElement, "TO", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + // PLS LEN. + [InlineData(AbLegacyDataType.PlsElement, "LEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + // BT control words + status bits. + [InlineData(AbLegacyDataType.BlockTransferElement, "RLEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData(AbLegacyDataType.BlockTransferElement, "DLEN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData(AbLegacyDataType.BlockTransferElement, "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData(AbLegacyDataType.BlockTransferElement, "DN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + public void Structure_subelements_resolve_to_expected_driver_type( + AbLegacyDataType type, string sub, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType expected) + { + AbLegacyDataTypeExtensions.EffectiveDriverDataType(type, sub).ShouldBe(expected); + } + + [Theory] + // PD bits in word 0. + [InlineData(AbLegacyDataType.PidElement, "EN", 0)] + [InlineData(AbLegacyDataType.PidElement, "PE", 1)] + [InlineData(AbLegacyDataType.PidElement, "DN", 2)] + [InlineData(AbLegacyDataType.PidElement, "MO", 3)] + // MG/BT share the same 8..15 layout. + [InlineData(AbLegacyDataType.MessageElement, "TO", 8)] + [InlineData(AbLegacyDataType.MessageElement, "EN", 15)] + [InlineData(AbLegacyDataType.BlockTransferElement, "TO", 8)] + [InlineData(AbLegacyDataType.BlockTransferElement, "EN", 15)] + public void Structure_status_bit_indices_match_rockwell( + AbLegacyDataType type, string sub, int expectedBit) + { + AbLegacyDataTypeExtensions.StatusBitIndex(type, sub).ShouldBe(expectedBit); + } + + [Theory] + // PD: PE + DN + SP_VAL/SP_LL/SP_HL are PLC-set (read-only); EN + MO + AUTO + MAN are + // operator-controllable. + [InlineData(AbLegacyDataType.PidElement, "PE", true)] + [InlineData(AbLegacyDataType.PidElement, "DN", true)] + [InlineData(AbLegacyDataType.PidElement, "SP_VAL", true)] + [InlineData(AbLegacyDataType.PidElement, "EN", false)] + [InlineData(AbLegacyDataType.PidElement, "MO", false)] + // MG/BT: ST/DN/ER/CO/EW/NR/TO are PLC-set; EN is operator-driven. + [InlineData(AbLegacyDataType.MessageElement, "DN", true)] + [InlineData(AbLegacyDataType.MessageElement, "ER", true)] + [InlineData(AbLegacyDataType.MessageElement, "TO", true)] + [InlineData(AbLegacyDataType.MessageElement, "EN", false)] + [InlineData(AbLegacyDataType.BlockTransferElement, "DN", true)] + [InlineData(AbLegacyDataType.BlockTransferElement, "EN", false)] + public void Structure_plc_set_status_bits_are_marked_read_only( + AbLegacyDataType type, string sub, bool expected) + { + AbLegacyDataTypeExtensions.IsPlcSetStatusBit(type, sub).ShouldBe(expected); + } }