Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs
T
Joseph Doherty 286ab3ba41 Auto: ablegacy-5 — PD/MG/PLS/BT structure files
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
2026-04-25 19:08:51 -04:00

326 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
/// <summary>
/// PCCC data types that map onto SLC / MicroLogix / PLC-5 files. Narrower than Logix — no
/// symbolic UDTs; every type is file-typed and fixed-width.
/// </summary>
public enum AbLegacyDataType
{
/// <summary>B-file single bit (<c>B3:0/0</c>) or bit-within-N-file (<c>N7:0/3</c>).</summary>
Bit,
/// <summary>N-file integer (signed 16-bit).</summary>
Int,
/// <summary>L-file long integer — SLC 5/05+ only (signed 32-bit).</summary>
Long,
/// <summary>F-file float (32-bit IEEE-754).</summary>
Float,
/// <summary>A-file analog integer — some older hardware (signed 16-bit, semantically like N).</summary>
AnalogInt,
/// <summary>ST-file string (82-byte fixed-length + length word header).</summary>
String,
/// <summary>Timer sub-element — caller addresses <c>.ACC</c>, <c>.PRE</c>, <c>.EN</c>, <c>.DN</c>, <c>.TT</c>.</summary>
TimerElement,
/// <summary>Counter sub-element — caller addresses <c>.ACC</c>, <c>.PRE</c>, <c>.CU</c>, <c>.CD</c>, <c>.DN</c>.</summary>
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>
/// PD-file (PID) sub-element — caller addresses <c>.SP</c>, <c>.PV</c>, <c>.CV</c>,
/// <c>.KP</c>, <c>.KI</c>, <c>.KD</c>, <c>.MAXS</c>, <c>.MINS</c>, <c>.DB</c>, <c>.OUT</c>
/// (Float) and <c>.EN</c>, <c>.DN</c>, <c>.MO</c>, <c>.PE</c>, <c>.AUTO</c>, <c>.MAN</c>
/// (Boolean status bits in word 0).
/// </summary>
PidElement,
/// <summary>
/// MG-file (Message) sub-element — caller addresses <c>.RBE</c>, <c>.MS</c>, <c>.SIZE</c>,
/// <c>.LEN</c> (Int32) and <c>.EN</c>, <c>.EW</c>, <c>.ER</c>, <c>.DN</c>, <c>.ST</c>,
/// <c>.CO</c>, <c>.NR</c>, <c>.TO</c> (Boolean status bits).
/// </summary>
MessageElement,
/// <summary>
/// PLS-file (Programmable Limit Switch) sub-element — caller addresses <c>.LEN</c>
/// (Int32). Bit semantics vary by PLC; unknown sub-elements fall back to Int32.
/// </summary>
PlsElement,
/// <summary>
/// BT-file (Block Transfer) sub-element — caller addresses <c>.RLEN</c>, <c>.DLEN</c>
/// (Int32) and <c>.EN</c>, <c>.ST</c>, <c>.DN</c>, <c>.ER</c>, <c>.CO</c>, <c>.EW</c>,
/// <c>.TO</c>, <c>.NR</c> (Boolean status bits in word 0).
/// </summary>
BlockTransferElement,
}
/// <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>
public static class AbLegacyDataTypeExtensions
{
public static DriverDataType ToDriverDataType(this AbLegacyDataType t) => t switch
{
AbLegacyDataType.Bit => DriverDataType.Boolean,
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => DriverDataType.Int32,
AbLegacyDataType.Long => DriverDataType.Int32, // matches Modbus/AbCip 64→32 gap
AbLegacyDataType.Float => DriverDataType.Float32,
AbLegacyDataType.String => DriverDataType.String,
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,
};
/// <summary>
/// Sub-element-aware driver type. Timer/Counter/Control elements expose Boolean status
/// bits (<c>.DN</c>, <c>.EN</c>, <c>.TT</c>, <c>.CU</c>, <c>.CD</c>, <c>.OV</c>,
/// <c>.UN</c>, <c>.ER</c>, etc.) and Int32 word members (<c>.PRE</c>, <c>.ACC</c>,
/// <c>.LEN</c>, <c>.POS</c>). Unknown sub-elements fall back to
/// <see cref="ToDriverDataType"/> so the driver remains permissive.
/// </summary>
public static DriverDataType EffectiveDriverDataType(AbLegacyDataType t, string? subElement)
{
if (subElement is null) return t.ToDriverDataType();
var key = subElement.ToUpperInvariant();
return t switch
{
AbLegacyDataType.TimerElement => key switch
{
"EN" or "TT" or "DN" => DriverDataType.Boolean,
"PRE" or "ACC" => DriverDataType.Int32,
_ => t.ToDriverDataType(),
},
AbLegacyDataType.CounterElement => key switch
{
"CU" or "CD" or "DN" or "OV" or "UN" => DriverDataType.Boolean,
"PRE" or "ACC" => DriverDataType.Int32,
_ => t.ToDriverDataType(),
},
AbLegacyDataType.ControlElement => key switch
{
"EN" or "EU" or "DN" or "EM" or "ER" or "UL" or "IN" or "FD" => DriverDataType.Boolean,
"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(),
};
}
/// <summary>
/// Bit position within the parent control word for Timer/Counter/Control status bits.
/// Returns <c>null</c> if the sub-element is not a known bit member of the given element
/// type. Bit numbering follows Rockwell DTAM / PCCC documentation.
/// </summary>
public static int? StatusBitIndex(AbLegacyDataType t, string? subElement)
{
if (subElement is null) return null;
var key = subElement.ToUpperInvariant();
return t switch
{
// T4 element word 0: bit 13=DN, 14=TT, 15=EN.
AbLegacyDataType.TimerElement => key switch
{
"DN" => 13,
"TT" => 14,
"EN" => 15,
_ => null,
},
// C5 element word 0: bit 10=UN, 11=OV, 12=DN, 13=CD, 14=CU.
AbLegacyDataType.CounterElement => key switch
{
"UN" => 10,
"OV" => 11,
"DN" => 12,
"CD" => 13,
"CU" => 14,
_ => null,
},
// R6 element word 0: bit 8=FD, 9=IN, 10=UL, 11=ER, 12=EM, 13=DN, 14=EU, 15=EN.
AbLegacyDataType.ControlElement => key switch
{
"FD" => 8,
"IN" => 9,
"UL" => 10,
"ER" => 11,
"EM" => 12,
"DN" => 13,
"EU" => 14,
"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 48 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,
};
}
/// <summary>
/// PLC-set status bits — read-only from the OPC UA side. Operator-controllable bits
/// (e.g. <c>.EN</c> on a timer/counter, <c>.CU</c>/<c>.CD</c> rung-driven inputs) are
/// omitted so they keep default writable behaviour.
/// </summary>
public static bool IsPlcSetStatusBit(AbLegacyDataType t, string? subElement)
{
if (subElement is null) return false;
var key = subElement.ToUpperInvariant();
return t switch
{
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,
};
}
}