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);
+ }
}