From f2bc36349e63cd601ae9c90f57246b76aaa50885 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 25 Apr 2026 13:32:23 -0400 Subject: [PATCH] =?UTF-8?q?Auto:=20ablegacy-2=20=E2=80=94=20MicroLogix=20f?= =?UTF-8?q?unction-file=20letters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #245 --- .../AbLegacyAddress.cs | 16 +++- .../AbLegacyDataType.cs | 67 ++++++++++++++++ .../PlcFamilies/AbLegacyPlcFamilyProfile.cs | 15 ++-- .../AbLegacyAddressTests.cs | 79 +++++++++++++++++++ 4 files changed, 171 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 f255362..54c67f7 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs @@ -107,7 +107,11 @@ public sealed record AbLegacyAddress( } // Reject unknown file letters — these cover SLC/ML/PLC-5 canonical families. - if (!IsKnownFileLetter(letter)) return null; + // Function-file letters (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI) are MicroLogix-only. + if (!IsKnownFileLetter(letter)) + { + if (!IsFunctionFileLetter(letter) || profile?.SupportsFunctionFiles != true) return null; + } var octalForIo = profile?.OctalIoAddressing == true && (letter == "I" || letter == "O"); @@ -151,4 +155,14 @@ public sealed record AbLegacyAddress( "N" or "F" or "B" or "L" or "ST" or "T" or "C" or "R" or "I" or "O" or "S" or "A" => true, _ => false, }; + + /// + /// MicroLogix 1100/1400 function-file prefixes. Each maps to a single fixed instance with a + /// known sub-element catalogue (see ). + /// + internal static bool IsFunctionFileLetter(string letter) => letter switch + { + "RTC" or "HSC" or "DLS" or "MMI" or "PTO" or "PWM" or "STI" or "EII" or "IOS" or "BHI" => true, + _ => false, + }; } diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs index 8e5bfad..48e253d 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs @@ -26,6 +26,72 @@ public enum AbLegacyDataType CounterElement, /// Control sub-element — caller addresses .LEN, .POS, .EN, .DN, .ER. ControlElement, + /// + /// MicroLogix 1100/1400 function-file sub-element (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI). + /// Sub-element catalogue lives in . + /// + MicroLogixFunctionFile, +} + +/// +/// MicroLogix function-file sub-element catalogue. Covers the most-commonly-addressed members +/// per file — not exhaustive (Rockwell defines 30+ on RTC alone). Unknown sub-elements fall +/// back to at the +/// boundary so the driver never refuses a tag the customer happens to know about. +/// +public static class AbLegacyFunctionFile +{ + /// + /// Driver-surface type for .. + /// Returns if the sub-element is unrecognised — keeps + /// the driver permissive without forcing every quirk into the catalogue. + /// + public static DriverDataType SubElementType(string fileLetter, string? subElement) + { + if (subElement is null) return DriverDataType.Int32; + var key = (fileLetter.ToUpperInvariant(), subElement.ToUpperInvariant()); + return key switch + { + // Real-time clock — all stored as Int16 (year is 4-digit Int16). + ("RTC", "HR") or ("RTC", "MIN") or ("RTC", "SEC") or + ("RTC", "MON") or ("RTC", "DAY") or ("RTC", "YR") or ("RTC", "DOW") => DriverDataType.Int32, + ("RTC", "DS") or ("RTC", "BL") or ("RTC", "EN") => DriverDataType.Boolean, + + // High-speed counter — accumulator/preset are Int32, status flags are bits. + ("HSC", "ACC") or ("HSC", "PRE") or ("HSC", "OVF") or ("HSC", "UNF") => DriverDataType.Int32, + ("HSC", "EN") or ("HSC", "UF") or ("HSC", "IF") or + ("HSC", "IN") or ("HSC", "IH") or ("HSC", "IL") or + ("HSC", "DN") or ("HSC", "CD") or ("HSC", "CU") => DriverDataType.Boolean, + + // Daylight saving + memory module info. + ("DLS", "STR") or ("DLS", "STD") => DriverDataType.Int32, + ("DLS", "EN") => DriverDataType.Boolean, + ("MMI", "FT") or ("MMI", "LBN") => DriverDataType.Int32, + ("MMI", "MP") or ("MMI", "MCP") => DriverDataType.Boolean, + + // Pulse-train / PWM output blocks. + ("PTO", "ACC") or ("PTO", "OF") or ("PTO", "IDA") or ("PTO", "ODA") => DriverDataType.Int32, + ("PTO", "EN") or ("PTO", "DN") or ("PTO", "EH") or ("PTO", "ED") or + ("PTO", "RP") or ("PTO", "OUT") => DriverDataType.Boolean, + ("PWM", "ACC") or ("PWM", "OF") or ("PWM", "PE") or ("PWM", "PD") => DriverDataType.Int32, + ("PWM", "EN") or ("PWM", "DN") or ("PWM", "EH") or ("PWM", "ED") or + ("PWM", "RP") or ("PWM", "OUT") => DriverDataType.Boolean, + + // Selectable timed interrupt + event input interrupt. + ("STI", "SPM") or ("STI", "ER") or ("STI", "PFN") => DriverDataType.Int32, + ("STI", "EN") or ("STI", "TIE") or ("STI", "DN") or + ("STI", "PS") or ("STI", "ED") => DriverDataType.Boolean, + ("EII", "PFN") or ("EII", "ER") => DriverDataType.Int32, + ("EII", "EN") or ("EII", "TIE") or ("EII", "PE") or + ("EII", "ES") or ("EII", "ED") => DriverDataType.Boolean, + + // I/O status + base hardware info — mostly status flags + a few counters. + ("IOS", "ID") or ("IOS", "TYP") => DriverDataType.Int32, + ("BHI", "OS") or ("BHI", "FRN") or ("BHI", "BSN") or ("BHI", "CC") => DriverDataType.Int32, + + _ => DriverDataType.Int32, + }; + } } /// Map a PCCC data type to the driver-surface . @@ -40,6 +106,7 @@ public static class AbLegacyDataTypeExtensions AbLegacyDataType.String => DriverDataType.String, AbLegacyDataType.TimerElement or AbLegacyDataType.CounterElement or AbLegacyDataType.ControlElement => DriverDataType.Int32, + AbLegacyDataType.MicroLogixFunctionFile => DriverDataType.Int32, _ => DriverDataType.Int32, }; } 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 cb81574..7cdea61 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/PlcFamilies/AbLegacyPlcFamilyProfile.cs @@ -10,7 +10,8 @@ public sealed record AbLegacyPlcFamilyProfile( int MaxTagBytes, bool SupportsStringFile, bool SupportsLongFile, - bool OctalIoAddressing) + bool OctalIoAddressing, + bool SupportsFunctionFiles) { public static AbLegacyPlcFamilyProfile ForFamily(AbLegacyPlcFamily family) => family switch { @@ -27,7 +28,8 @@ public sealed record AbLegacyPlcFamilyProfile( MaxTagBytes: 240, // SLC 5/05 PCCC max packet data 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 + OctalIoAddressing: false, // SLC500 I:/O: indices are decimal in RSLogix 500 + SupportsFunctionFiles: false); // SLC500 has no function files public static readonly AbLegacyPlcFamilyProfile MicroLogix = new( LibplctagPlcAttribute: "micrologix", @@ -35,7 +37,8 @@ public sealed record AbLegacyPlcFamilyProfile( MaxTagBytes: 232, SupportsStringFile: true, SupportsLongFile: false, // ML 1100/1200/1400 don't ship L files - OctalIoAddressing: false); // MicroLogix follows SLC-style decimal I/O addressing + 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 public static readonly AbLegacyPlcFamilyProfile Plc5 = new( LibplctagPlcAttribute: "plc5", @@ -43,7 +46,8 @@ public sealed record AbLegacyPlcFamilyProfile( MaxTagBytes: 240, // DF1 full-duplex packet limit at 264 bytes, PCCC-over-EIP caps lower SupportsStringFile: true, SupportsLongFile: false, // PLC-5 predates L files - OctalIoAddressing: true); // RSLogix 5 displays I:/O: word + bit indices as octal + OctalIoAddressing: true, // RSLogix 5 displays I:/O: word + bit indices as octal + SupportsFunctionFiles: false); /// /// Logix ControlLogix / CompactLogix accessed through the legacy PCCC compatibility layer. @@ -56,7 +60,8 @@ public sealed record AbLegacyPlcFamilyProfile( MaxTagBytes: 240, SupportsStringFile: true, SupportsLongFile: true, - OctalIoAddressing: false); // Logix natively uses decimal arrays even via the PCCC bridge + OctalIoAddressing: false, // Logix natively uses decimal arrays even via the PCCC bridge + SupportsFunctionFiles: 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 8039a0f..a3fcc5b 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyAddressTests.cs @@ -139,4 +139,83 @@ public sealed class AbLegacyAddressTests AbLegacyPlcFamilyProfile.MicroLogix.OctalIoAddressing.ShouldBeFalse(); AbLegacyPlcFamilyProfile.LogixPccc.OctalIoAddressing.ShouldBeFalse(); } + + // ---- MicroLogix function-file letters (Issue #245) ---- + // + // MicroLogix 1100/1400 expose RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI function files. Other + // PCCC families (SLC500 / PLC-5 / LogixPccc) reject those file letters. + + [Theory] + [InlineData("RTC:0.HR", "RTC", "HR")] + [InlineData("RTC:0.MIN", "RTC", "MIN")] + [InlineData("RTC:0.YR", "RTC", "YR")] + [InlineData("HSC:0.ACC", "HSC", "ACC")] + [InlineData("HSC:0.PRE", "HSC", "PRE")] + [InlineData("HSC:0.EN", "HSC", "EN")] + [InlineData("DLS:0.STR", "DLS", "STR")] + [InlineData("PTO:0.OF", "PTO", "OF")] + [InlineData("PWM:0.EN", "PWM", "EN")] + [InlineData("STI:0.SPM", "STI", "SPM")] + [InlineData("EII:0.PFN", "EII", "PFN")] + [InlineData("MMI:0.FT", "MMI", "FT")] + [InlineData("BHI:0.OS", "BHI", "OS")] + [InlineData("IOS:0.ID", "IOS", "ID")] + public void TryParse_MicroLogix_accepts_function_files(string input, string expectedLetter, string expectedSub) + { + var a = AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.MicroLogix); + a.ShouldNotBeNull(); + a.FileLetter.ShouldBe(expectedLetter); + a.SubElement.ShouldBe(expectedSub); + } + + [Theory] + [InlineData("RTC:0.HR")] + [InlineData("HSC:0.ACC")] + [InlineData("PTO:0.OF")] + [InlineData("BHI:0.OS")] + public void TryParse_Slc500_rejects_function_files(string input) + { + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Slc500).ShouldBeNull(); + } + + [Theory] + [InlineData("RTC:0.HR")] + [InlineData("HSC:0.ACC")] + public void TryParse_Plc5_and_LogixPccc_reject_function_files(string input) + { + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.Plc5).ShouldBeNull(); + AbLegacyAddress.TryParse(input, AbLegacyPlcFamily.LogixPccc).ShouldBeNull(); + } + + [Fact] + public void TryParse_Default_overload_rejects_function_files() + { + // Without a family the parser cannot allow MicroLogix-only letters — back-compat with + // the family-less overload from before #244. + AbLegacyAddress.TryParse("RTC:0.HR").ShouldBeNull(); + AbLegacyAddress.TryParse("HSC:0.ACC").ShouldBeNull(); + } + + [Fact] + public void MicroLogixProfile_advertises_function_file_support() + { + AbLegacyPlcFamilyProfile.MicroLogix.SupportsFunctionFiles.ShouldBeTrue(); + AbLegacyPlcFamilyProfile.Slc500.SupportsFunctionFiles.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.Plc5.SupportsFunctionFiles.ShouldBeFalse(); + AbLegacyPlcFamilyProfile.LogixPccc.SupportsFunctionFiles.ShouldBeFalse(); + } + + [Theory] + [InlineData("RTC", "HR", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData("RTC", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData("HSC", "ACC", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData("HSC", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData("DLS", "STR", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Int32)] + [InlineData("DLS", "EN", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + [InlineData("PWM", "OUT", ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.Boolean)] + public void FunctionFile_subelement_catalogue_maps_to_expected_driver_type( + string letter, string sub, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType expected) + { + AbLegacyFunctionFile.SubElementType(letter, sub).ShouldBe(expected); + } }