Merge pull request '[ablegacy] AbLegacy — MicroLogix function-file letters' (#322) from auto/ablegacy/2 into auto/driver-gaps

This commit was merged in pull request #322.
This commit is contained in:
2026-04-25 13:34:46 -04:00
4 changed files with 171 additions and 6 deletions

View File

@@ -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,
};
/// <summary>
/// MicroLogix 1100/1400 function-file prefixes. Each maps to a single fixed instance with a
/// known sub-element catalogue (see <see cref="AbLegacyDataType"/>).
/// </summary>
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,
};
}

View File

@@ -26,6 +26,72 @@ public enum AbLegacyDataType
CounterElement,
/// <summary>Control sub-element — caller addresses <c>.LEN</c>, <c>.POS</c>, <c>.EN</c>, <c>.DN</c>, <c>.ER</c>.</summary>
ControlElement,
/// <summary>
/// MicroLogix 1100/1400 function-file sub-element (RTC/HSC/DLS/MMI/PTO/PWM/STI/EII/IOS/BHI).
/// Sub-element catalogue lives in <see cref="AbLegacyFunctionFile.SubElementType"/>.
/// </summary>
MicroLogixFunctionFile,
}
/// <summary>
/// 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 <see cref="DriverDataType.Int32"/> at the <see cref="AbLegacyDataTypeExtensions"/>
/// boundary so the driver never refuses a tag the customer happens to know about.
/// </summary>
public static class AbLegacyFunctionFile
{
/// <summary>
/// Driver-surface type for <paramref name="fileLetter"/>.<paramref name="subElement"/>.
/// Returns <see cref="DriverDataType.Int32"/> if the sub-element is unrecognised — keeps
/// the driver permissive without forcing every quirk into the catalogue.
/// </summary>
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,
};
}
}
/// <summary>Map a PCCC data type to the driver-surface <see cref="DriverDataType"/>.</summary>
@@ -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,
};
}

View File

@@ -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);
/// <summary>
/// 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);
}
/// <summary>Which PCCC PLC family the device is.</summary>

View File

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