namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// AB CIP / EtherNet-IP driver configuration, bound from the driver's DriverConfig /// JSON at DriverHost.RegisterAsync. One instance supports N devices (PLCs) behind /// the same driver; per-device routing is keyed on /// via IPerCallHostResolver. /// /// /// Per v2 plan decisions #11 (libplctag), #41 (AbCip vs AbLegacy split), #143–144 (per-call /// host resolver + resilience keys), #144 (bulkhead keyed on (DriverInstanceId, HostName)). /// public sealed class AbCipDriverOptions { /// /// PLCs this driver instance talks to. Each device contributes its own /// string as the hostName key used by resilience pipelines and the Admin UI. /// public IReadOnlyList Devices { get; init; } = []; /// Pre-declared tag map across all devices — AB discovery lands in PR 5. public IReadOnlyList Tags { get; init; } = []; /// /// L5K (Studio 5000 controller export) imports merged into at /// InitializeAsync. Each entry points at one L5K file + the device whose tags it /// describes; the parser extracts TAG + DATATYPE blocks and produces /// records (alias tags + ExternalAccess=None tags /// skipped — see ). Pre-declared entries /// win on Name conflicts so operators can override import results without /// editing the L5K source. /// public IReadOnlyList L5kImports { get; init; } = []; /// /// L5X (Studio 5000 XML controller export) imports merged into at /// InitializeAsync. Same shape and merge semantics as — /// the entries differ only in source format. Pre-declared entries win /// on Name conflicts; entries already produced by also win /// so an L5X re-export of the same controller doesn't double-emit. See /// for the format-specific mechanics. /// public IReadOnlyList L5xImports { get; init; } = []; /// /// Kepware-format CSV imports merged into at InitializeAsync. /// Same merge semantics as / — /// pre-declared entries win on Name conflicts, and tags /// produced by earlier import collections (L5K → L5X → CSV in call order) also win /// so an Excel-edited copy of the same controller does not double-emit. See /// for the column layout + parse rules. /// public IReadOnlyList CsvImports { get; init; } = []; /// Per-device probe settings. Falls back to defaults when omitted. public AbCipProbeOptions Probe { get; init; } = new(); /// /// Default libplctag call timeout applied to reads/writes/discovery when the caller does /// not pass a more specific value. Matches the Modbus driver's 2-second default. /// public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2); /// /// When true, DiscoverAsync walks each device's Logix symbol table via /// the @tags pseudo-tag + surfaces controller-resident globals under a /// Discovered/ sub-folder. Pre-declared tags always emit regardless. Default /// false to keep the strict-config path for deployments where only declared tags /// should appear in the address space. /// public bool EnableControllerBrowse { get; init; } /// /// Task #177 — when true, declared ALMD tags are surfaced as alarm conditions /// via ; the driver polls each subscribed /// alarm's InFaulted + Severity members + fires OnAlarmEvent on /// state transitions. Default false — operators explicitly opt in because /// projection semantics don't exactly mirror Rockwell FT Alarm & Events; shops /// running FT Live should keep this off + take alarms through the native route. /// public bool EnableAlarmProjection { get; init; } /// /// Poll interval for the ALMD projection loop. Shorter intervals catch faster edges /// at the cost of PLC round-trips; edges shorter than this interval are invisible to /// the projection (a 0→1→0 transition within one tick collapses to no event). Default /// 1 second — matches typical SCADA alarm-refresh conventions. /// public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1); /// /// PR abcip-3.1 — optional sink for non-fatal driver warnings (legacy-firmware /// ConnectionSize mis-match, etc.). Production hosting wires this to Serilog; /// unit tests pin a list-collecting lambda to assert which warnings fired. null /// swallows warnings — convenient for back-compat deployments that don't care. /// public Action? OnWarning { get; init; } } /// /// One PLC endpoint. must parse via /// ; misconfigured devices fail driver /// initialization rather than silently connecting to nothing. /// /// Canonical ab://gateway[:port]/cip-path string. /// Which per-family profile to apply. Determines ConnectionSize, /// request-packing support, unconnected-only hint, and other quirks. /// Optional display label for Admin UI. Falls back to . /// PR abcip-3.1 — optional override for the family-default /// . Threads through to /// libplctag's connection_size attribute on the underlying tag handle so operators can /// dial the CIP Forward Open buffer down for legacy firmware (v19-and-earlier ControlLogix /// caps at 504) or up for high-throughput shops on FW20+. Validated against the Kepware /// supported range [500..4002] at InitializeAsync; out-of-range values fault the /// driver. null uses the family default — back-compat with deployments that haven't /// touched the knob. /// PR abcip-3.2 — controls whether the driver addresses tags by /// ASCII symbolic path (the default), by CIP logical-segment instance ID, or asks the driver /// to pick. Logical addressing skips per-poll ASCII parsing on every read and unlocks /// symbol-table-cached scans for 500+-tag projects, but requires a one-time symbol-table /// walk at first read + is unsupported on Micro800 / SLC500 / PLC5 (their CIP firmware does /// not honour Symbol Object instance IDs). When the user picks /// against an unsupported family the driver logs a warning + falls back to symbolic so /// misconfiguration does not fault the driver. currently /// resolves to symbolic — a future PR will plumb a real auto-detection heuristic; the docs /// in docs/drivers/AbCip-Performance.md §"Addressing mode" call this out. /// PR abcip-3.3 — picks how a multi-member UDT batch is read on this /// device. issues one read per parent UDT and decodes /// each subscribed member from the buffer in-memory (the historical behaviour that ships in /// task #194 — best when a large fraction of a UDT's members are subscribed). /// bundles per-member reads into one CIP /// Multi-Service Packet — best for sparse UDT subscriptions where reading the whole UDT /// buffer just to extract one or two fields wastes wire bandwidth. /// (the default) lets the planner pick per-batch using /// : if the subscribed-member fraction is below /// the threshold MultiPacket wins, otherwise WholeUdt wins. Family compatibility — Micro800 / /// SLC500 / PLC5 lack Multi-Service-Packet support per /// ; user-forced /// against those families logs a warning + falls /// back to at device-init time. The libplctag .NET /// wrapper (1.5.x) does not expose a public knob for explicit Multi-Service-Packet bundling, /// so today's MultiPacket runtime issues one libplctag read per member; the planner's grouping /// is still load-bearing because it gives the runtime the right plan to execute when an /// upstream wrapper release exposes wire-level bundling. /// PR abcip-3.3 — sparsity-threshold knob the planner /// uses when is . The /// planner divides subscribedMembers / totalMembers for each parent UDT in a batch; /// a fraction strictly less than the threshold picks /// , else . /// Default 0.25 — picked because reading 1/4 of a UDT's members is the rough break-even /// where the wire-cost of one whole-UDT read still beats N member reads on ControlLogix's /// 4002-byte connection size; see docs/drivers/AbCip-Performance.md §"Read strategy". /// Clamped to [0..1] at planner time; values outside the range silently saturate. public sealed record AbCipDeviceOptions( string HostAddress, AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix, string? DeviceName = null, int? ConnectionSize = null, AddressingMode AddressingMode = AddressingMode.Auto, ReadStrategy ReadStrategy = ReadStrategy.Auto, double MultiPacketSparsityThreshold = 0.25); /// /// PR abcip-3.3 — per-device strategy for reading multi-member UDT batches. /// mirrors the task #194 behaviour: one libplctag read on the parent tag, each subscribed member /// decoded from the buffer at its computed offset. bundles per-member /// reads into one CIP Multi-Service Packet so sparse UDT subscriptions don't pay for the whole /// UDT buffer. lets the planner pick per-batch using /// . /// /// /// Strategy resolution lives at two layers: /// /// Device init — user-forced against a family whose /// profile sets /// = false (Micro800, SLC500, PLC5) falls back to with a /// warning. stays as-is (the planner re-evaluates per batch). /// Per-batch (Auto only) — for each parent UDT in the request set, the planner /// computes subscribedMembers / totalMembers and routes the group through /// when the fraction is below the threshold, else /// . /// /// libplctag .NET wrapper (1.5.x) does not expose explicit Multi-Service-Packet bundling, /// so today's runtime issues one libplctag read per member when the planner picks MultiPacket — /// the same wrapper limitation called out in PR abcip-3.1 (ConnectionSize) and PR abcip-3.2 /// (instance-ID addressing). The planner's grouping is still observable from tests + future-proofs /// the driver for when an upstream wrapper release exposes wire-level bundling. /// public enum ReadStrategy { /// Driver picks per-batch based on /// . Default. Auto = 0, /// One read per parent UDT; members decoded from the buffer in-memory. Best when a /// large fraction of the UDT's members are subscribed (dense reads). WholeUdt = 1, /// Bundle per-member reads into one CIP Multi-Service Packet. Best when only a few /// members of a large UDT are subscribed (sparse reads). Unsupported on Micro800 / SLC500 / /// PLC5; the driver warns + falls back to at device init. MultiPacket = 2, } /// /// PR abcip-3.2 — how the AB CIP driver addresses tags on a given device. /// is the historical default + matches every previous driver build: each read carries the tag /// name as ASCII bytes + the controller parses the path on every request. /// uses CIP logical-segment instance IDs (Symbol Object class 0x6B) — the controller looks the /// tag up in its own symbol table once + the driver caches the resolved instance ID for /// subsequent reads, eliminating the per-poll ASCII parse step. lets the /// driver pick (today: always Symbolic; a future PR fingerprints the controller and switches /// to Logical when supported). /// /// /// Logical addressing requires a one-time symbol-table walk at the first read on the device /// (the driver issues an @tags read via and stores /// the name → instance-id map on the per-device DeviceState). It is unsupported on /// Micro800 / SLC500 / PLC5 — see . /// The libplctag .NET wrapper (1.5.x) does not expose a public knob for instance-ID /// addressing, so the driver translates Logical → libplctag attribute via reflection on /// NativeTagWrapper.SetAttributeString — same best-effort fallback pattern as /// PR abcip-3.1's ConnectionSize plumbing. /// public enum AddressingMode { /// Driver picks. Currently resolves to ; future PR may /// auto-detect based on family + firmware + symbol-table size. Auto = 0, /// ASCII symbolic-path addressing — the libplctag default. Per-poll ASCII parse on /// the controller; works on every CIP family. Symbolic = 1, /// CIP logical-segment / instance-ID addressing. Requires a one-time /// symbol-table walk at first read; subsequent reads skip ASCII parsing on the /// controller. Unsupported on Micro800 / SLC500 / PLC5. Logical = 2, } /// /// One AB-backed OPC UA variable. Mirrors the ModbusTagDefinition shape. /// /// Tag name; becomes the OPC UA browse name and full reference. /// Which device () this tag lives on. /// Logix symbolic path (controller or program scope). /// Logix atomic type, or for UDT-typed tags. /// When true and the tag's ExternalAccess permits writes, IWritable routes writes here. /// Per plan decisions #44–#45, #143 — safe to replay on write timeout. Default false. /// For -typed tags, the declared UDT /// member layout. When supplied, discovery fans out the UDT into a folder + one Variable per /// member (member TagPath = {tag.TagPath}.{member.Name}). When null on a Structure /// tag, the driver treats it as a black-box and relies on downstream configuration to address /// members individually via dotted syntax. Ignored for atomic types. /// GuardLogix safety-partition tag hint. When true, the driver /// forces SecurityClassification.ViewOnly on discovery regardless of /// — safety tags can only be written from the safety task of a /// GuardLogix controller; non-safety writes violate the safety-partition isolation and are /// rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the /// write attempt failing at runtime. /// Capacity of the DATA character array on a Logix STRING / STRINGnn /// UDT — 82 for the stock STRING, 20/40/80/etc for user-defined STRING_20, /// STRING_40, STRING_80 variants. Threads through libplctag's /// str_max_capacity attribute so the wrapper allocates the correct backing buffer /// and GetString / SetString truncate at the right boundary. null /// keeps libplctag's default 82-byte STRING behaviour for back-compat. Ignored for /// non- types. /// Tag description carried from the L5K/L5X export (or set explicitly /// in pre-declared config). Surfaces as the OPC UA Description attribute on the /// produced Variable node so SCADA / engineering clients see the comment from the source /// project. null leaves Description unset, matching pre-2.3 behaviour. /// PR abcip-4.1 — optional per-tag publish rate (in milliseconds) that /// overrides the subscription's default publishingInterval for this tag. Mirrors /// Kepware's "scan classes" + Siemens / Mitsubishi per-tag scan groups; the driver buckets /// tags by resolved interval at time + runs one /// loop per distinct interval so a fast HMI /// tag is not delayed behind a slow batch tag's 10 s tick. null = use the subscription /// default (legacy behaviour). The 100 ms floor enforced by the engine still applies — a /// ScanRateMs < 100 is clamped up. UDT member tags inherit the parent tag's /// ScanRateMs at member-fan-out time. See /// docs/drivers/AbCip-Operability.md §"Per-tag scan rate". /// PR abcip-4.2 — optional numeric write deadband. When set and both /// the previous successfully-written value and the new write are numeric, the driver suppresses /// the next write if |new - last| < WriteDeadband. Suppressed writes still return /// Good so the OPC UA write semantics observed by clients are unchanged — the driver /// simply skips the wire round-trip. Mirrors Kepware's "Deadband (write)" knob and is the /// write-side companion to the read-side deadband already shipped at the OPC UA monitored-item /// layer. NaN / Infinity values bypass suppression (let the wire decide). See /// docs/drivers/AbCip-Operability.md §"Write deadband / write-on-change". /// PR abcip-4.2 — optional write-on-change gate. When true and /// the new write equals the previous successfully-written value, the driver suppresses the /// write (returns Good without hitting the wire). Combines with /// for numeric tags — the deadband test takes priority for numerics, equality is the fallback /// for non-numeric types (BOOL setpoints, STRING constants, etc.). Default false — /// legacy behaviour where every write goes to the wire. public sealed record AbCipTagDefinition( string Name, string DeviceHostAddress, string TagPath, AbCipDataType DataType, bool Writable = true, bool WriteIdempotent = false, IReadOnlyList? Members = null, bool SafetyTag = false, int? StringLength = null, string? Description = null, int? ScanRateMs = null, double? WriteDeadband = null, bool WriteOnChange = false); /// /// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. Speed, /// Status), DataType is the atomic Logix type, Writable/WriteIdempotent mirror /// . Declaration-driven — the real CIP Template Object reader /// (class 0x6C) that would auto-discover member layouts lands as a follow-up PR. /// /// /// carries the per-member comment from L5K/L5X UDT definitions so /// the OPC UA Variable nodes produced for individual members surface their descriptions too, /// not just the top-level tag. /// PR abcip-2.6 — tags AOI parameters as Input / Output / /// InOut / Local. Plain UDT members default to . Discovery /// groups Input / Output / InOut members under sub-folders so an AOI-typed tag fans out as /// Tag/Inputs/..., Tag/Outputs/..., Tag/InOut/... while Local stays at the /// UDT root — matching how AOIs visually present in Studio 5000. /// public sealed record AbCipStructureMember( string Name, AbCipDataType DataType, bool Writable = true, bool WriteIdempotent = false, int? StringLength = null, string? Description = null, AoiQualifier AoiQualifier = AoiQualifier.Local); /// /// PR abcip-2.6 — directional qualifier for AOI parameters. Surfaces the Studio 5000 /// Usage attribute (Input / Output / InOut) so discovery can group /// AOI members into sub-folders and downstream consumers can reason about parameter direction. /// Plain UDT members (non-AOI types) default to , which keeps them at the /// UDT root + indicates they are internal storage rather than a directional parameter. /// public enum AoiQualifier { /// UDT member or AOI local tag — non-directional, browsed at the parent's root. Local, /// AOI input parameter — written by the caller, read by the AOI body. Input, /// AOI output parameter — written by the AOI body, read by the caller. Output, /// AOI bidirectional parameter — passed by reference, both sides may read/write. InOut, } /// /// One L5K-import entry. Either or must be /// set (FilePath wins when both supplied — useful for tests that pre-load fixtures into /// options without touching disk). /// /// Target device HostAddress tags from this file are bound to. /// On-disk path to a *.L5K export. Loaded eagerly at InitializeAsync. /// Pre-loaded L5K body — used by tests + Admin UI uploads. /// Optional prefix prepended to imported tag names to avoid collisions /// when ingesting multiple files into one driver instance. public sealed record AbCipL5kImportOptions( string DeviceHostAddress, string? FilePath = null, string? InlineText = null, string NamePrefix = ""); /// /// One L5X-import entry. Mirrors field-for-field — the /// two are kept as distinct types so configuration JSON makes the source format explicit /// (an L5X file under an L5kImports entry would parse-fail confusingly otherwise). /// /// Target device HostAddress tags from this file are bound to. /// On-disk path to a *.L5X XML export. Loaded eagerly at InitializeAsync. /// Pre-loaded L5X body — used by tests + Admin UI uploads. /// Optional prefix prepended to imported tag names to avoid collisions /// when ingesting multiple files into one driver instance. public sealed record AbCipL5xImportOptions( string DeviceHostAddress, string? FilePath = null, string? InlineText = null, string NamePrefix = ""); /// /// One Kepware-format CSV import entry. Field shape mirrors /// so configuration JSON stays consistent across the three import sources. /// /// Target device HostAddress tags from this file are bound to. /// On-disk path to a Kepware-format *.csv. Loaded eagerly at InitializeAsync. /// Pre-loaded CSV body — used by tests + Admin UI uploads. /// Optional prefix prepended to imported tag names to avoid collisions. public sealed record AbCipCsvImportOptions( string DeviceHostAddress, string? FilePath = null, string? InlineText = null, string NamePrefix = ""); /// Which AB PLC family the device is — selects the profile applied to connection params. public enum AbCipPlcFamily { ControlLogix, CompactLogix, Micro800, GuardLogix, } /// /// Background connectivity-probe settings. Enabled by default; the probe reads a cheap tag /// on the PLC at the configured interval to drive /// state transitions + Admin UI health status. /// public sealed class AbCipProbeOptions { public bool Enabled { get; init; } = true; public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5); public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2); /// /// Tag path used for the probe. If null, the driver attempts to read a default /// system tag (PR 8 wires this up — the choice is family-dependent, e.g. /// @raw_cpu_type on ControlLogix or a user-configured probe tag on Micro800). /// public string? ProbeTagPath { get; init; } }