# Driver Feature Gaps vs Commercial OPC/SCADA Gateways

This document compares each non-Modbus, non-LMX driver in the OtOpcUa server against the feature surfaces of the dominant commercial gateways (Kepware KEPServerEX / PTC Kepware Edge, AVEVA OI Server / DAServer, Software Toolbox TOP Server, Matrikon, Unified Automation UaGateway, MTConnect-class Fanuc adapters, Beckhoff TF6100, etc.).

The intent is to:

- inventory what we already ship (with file:line citations into the current codebase)
- list missing or under-served features that are table-stakes for sites replacing those commercial gateways
- preserve the design choices that should NOT change just because a competitor does it differently

LMX (Galaxy / MXAccess) and Modbus are tracked elsewhere and are excluded here.

## Drivers covered

| Driver | Section | Implementation plan |
|---|---|---|
| AbCip — Allen-Bradley EtherNet/IP (ControlLogix / CompactLogix / Micro800 / GuardLogix) | [↓](#abcip-allen-bradley-ethernetip--logix) | [`plans/abcip-plan.md`](plans/abcip-plan.md) |
| AbLegacy — Allen-Bradley PLC-5 / SLC / MicroLogix (PCCC) | [↓](#ablegacy-allen-bradley-plc-5--slc--micrologix) | [`plans/ablegacy-plan.md`](plans/ablegacy-plan.md) |
| FOCAS — Fanuc CNC FOCAS / FOCAS2 | [↓](#focas-fanuc-cnc) | [`plans/focas-plan.md`](plans/focas-plan.md) |
| OpcUaClient — OPC UA aggregation client | [↓](#opcuaclient-opc-ua-aggregation-client) | [`plans/opcuaclient-plan.md`](plans/opcuaclient-plan.md) |
| S7 — Siemens S7-300 / 400 / 1200 / 1500 | [↓](#s7-siemens-s7-3004001200--1500) | [`plans/s7-plan.md`](plans/s7-plan.md) |
| TwinCAT — Beckhoff TwinCAT 2 / 3 (ADS) | [↓](#twincat-beckhoff-ads) | [`plans/twincat-plan.md`](plans/twincat-plan.md) |

## How to read this document

Every gap below is rated **[Build]** (recommended) or **[Skip]** (not recommended) inline at the start of the bullet. The same rating appears in the per-driver `### Recommendations` table with its rationale. The per-driver implementation plan in `docs/plans/` covers the **[Build]** items only.

---

## AbCip (Allen-Bradley EtherNet/IP — Logix)

### What we ship today

- Per-device `ab://gateway[:port]/cip-path` host-address with multi-hop CIP path via a comma-separated string (e.g. `1,2,2,192.168.50.20,1,0`) — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs:23`.
- Four PLC-family profiles (`ControlLogix`, `CompactLogix`, `Micro800`, `GuardLogix`) selecting libplctag plc attribute, ConnectionSize default (504/4002/488), default CIP path (`1,0` or empty), connected-vs-unconnected hint, request-packing flag, and MaxFragmentBytes — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs:13-62`.
- N devices per driver instance with per-device bulkhead/breaker keying — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs:19`.
- Pre-declared static tag map (`AbCipTagDefinition`) keyed by `Name`, with `TagPath`, `DataType`, `Writable`, `WriteIdempotent`, `Members`, `SafetyTag` — `AbCipDriverOptions.cs:95-103`.
- Logix atomic types `BOOL/SINT/INT/DINT/LINT/USINT/UINT/UDINT/ULINT/REAL/LREAL/STRING/DT` plus `Structure` marker — `src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs:16-37`.
- Optional online controller browse via libplctag `@tags` pseudo-tag, surfaced under a `Discovered/` sub-folder; controller- and program-scope (`Program:Main.X`) tags emitted; system/module/routine/task tags filtered — `AbCipDriver.cs:674-757`, `AbCipSystemTagFilter.cs`.
- UDT / Predefined-Structure handling: declaration-driven member fan-out (Variable per member) plus runtime CIP Template Object (class 0x6C) decoder + per-device `(deviceHostAddress, templateInstanceId)` template cache — `CipTemplateObjectDecoder.cs`, `AbCipTemplateCache.cs`, `AbCipDriver.cs:70-103`.
- Whole-UDT read coalescing — `AbCipUdtReadPlanner` groups members of the same parent and reads the parent once, decoding members from the buffer at computed byte offsets — `AbCipDriver.cs:323-449`, `AbCipUdtReadPlanner.cs`, `AbCipUdtMemberLayout.cs`.
- BOOL-in-DINT addressing (`Tag.N` bit-index) with read-decode + RMW write through a per-parent `SemaphoreSlim` and cached parent-DINT runtime — `AbCipDriver.cs:494-614`, `AbCipTagPath.cs`.
- Polling subscription overlay shared with other drivers (`PollGroupEngine`) — `AbCipDriver.cs:56-59,187-195`.
- Per-device connectivity probe with configurable interval/timeout/probe tag (default off until tag configured) and `OnHostStatusChanged` events — `AbCipDriverOptions.cs:131-143`, `AbCipDriver.cs:235-295`.
- ALMD alarm projection (opt-in) polling `InFaulted` + `Severity`, raising `OnAlarmEvent` on edges, with ack-write — `AbCipAlarmProjection.cs`, `AbCipDriverOptions.cs:42-58`.
- GuardLogix safety-tag flag forces `SecurityClassification.ViewOnly` — `AbCipDriverOptions.cs:89-94`, `AbCipDriver.cs:474-478`.
- libplctag-status → OPC UA StatusCode mapping (`BadCommunicationError`, `BadNotWritable`, `BadTypeMismatch`, `BadOutOfRange`, `BadNodeIdUnknown`) — `AbCipStatusMapper.cs`.
- Tier-B reinit (`ReinitializeAsync`) tearing down all `IAbCipTagRuntime` handles — `AbCipDriver.cs:163-167`.
- CLI test client: `probe`, `read`, `write`, `subscribe` against the same driver — `docs/Driver.AbCip.Cli.md`.

### Gaps vs commercial gateways

- **[Build]** **Offline tag import from L5K / L5X** — present in: both (Kepware Logix Database Settings; TOP Server Auto Tag Generation). Why it matters: lets engineers stage a project against a Studio 5000 export with no PLC online, the de-facto config workflow at Rockwell shops.
- **[Build]** **CSV tag import / export** — present in: both. Why it matters: Kepware/AVEVA users routinely round-trip tag lists through Excel; replacing them without CSV makes mass-config painful.
- **[Build]** **Tag descriptions / engineering metadata** — present in: both (descriptions imported with L5X). Why it matters: descriptions become the OPC UA `Description`/`DisplayName`, expected by HMI/Historian engineers.
- **[Build]** **Logical-blocking / logical-non-blocking protocol modes** — present in: both (TOP Server names them; Kepware exposes equivalent "Optimize for read" / structure-block reads). Why it matters: whole-UDT vs per-member read strategy is the single biggest performance lever; we have one-direction whole-UDT only via `AbCipUdtReadPlanner`, no structure-block read for non-grouped members.
- **[Build]** **Symbolic vs logical (instance-ID) addressing toggle** — present in: both. Why it matters: logical addressing skips ASCII parsing on every poll, ~3-5x faster for high-tag-count rigs; libplctag supports it but we don't expose the choice.
- **[Build]** **Configurable CIP Connection Size per device** — present in: both (Kepware 500-4000 byte slider, TOP Server "Max Packet Size"). Why it matters: we hard-code the family default (4002/504/488); no field knob to tune for switches that fragment large frames or for legacy v19 firmware that won't accept Large Forward Open.
- **[Skip]** **Inactivity timeout / connection idle disconnect** — present in: both. Why it matters: long-idle CIP sessions get reaped silently by some firewalls; commercial drivers expose a keep-alive cadence we don't.
- **[Build]** **Per-tag scan rate / scan group bucketing** — present in: both (Kepware "scan classes", AVEVA Topic update intervals). Why it matters: lets engineers separate fast 100ms machine-state tags from 5s recipe data; we have one publishing-interval-per-subscription with no per-tag override.
- **[Skip]** **"Respect tag-specified scan rate" mode** — present in: Kepware. Why it matters: lets the static tag table override client-requested rate, important when an HMI subscribes too fast and overruns the PLC.
- **[Skip]** **Initial value cache / "first updates from cache"** — present in: Kepware. Why it matters: avoids a stall while a fresh subscription waits for its first poll; common SCADA expectation.
- **[Build]** **Multi-tag write packing (write-multi)** — present in: both. Why it matters: we serialise writes one-by-one in `AbCipDriver.WriteAsync`; without CIP multi-request packing for writes a recipe-download is N round-trips instead of one.
- **[Build]** **AOI (Add-On Instruction) input/output handling** — present in: Kepware (with explicit InOut limitation note). Why it matters: AOIs are how modern Logix code is structured; the Template Object decoder probably handles the layout but we don't surface AOI-specific browse paths.
- **[Build]** **Native STRING (Logix STRING / custom STRINGxx) decoding** — present in: both (Kepware preserves descriptors; AVEVA exposes as native string). Why it matters: we map Logix `STRING` to `DriverDataType.String` but `AbCipDataType.cs` flags whole-string only; no support for user-defined `STRINGnn` variants of different DATA-array sizes.
- **[Build]** **64-bit integer surface (LINT/ULINT)** — present in: both. Why it matters: Logix v32+ exposes LINT for 64-bit counters/timestamps; we widen them into `Int32` per a TODO at `AbCipDataType.cs:53`, losing the upper bits.
- **[Skip]** **Structure / UDT as first-class OPC UA structured type** — present in: both (Kepware emits child tags; AVEVA exposes via native UDT). Why it matters: we emit `DriverDataType.String` placeholder for whole-UDT, only members are fully typed; OPC UA clients can't bind to a UDT shape.
- **[Build]** **Array element / array slice addressing** — present in: both (Kepware `Tag[3,5]`, slice `Tag[0..15]`). Why it matters: `AbCipTagPath` supports indexed elements but the driver has no array-slice read for adjacent indices; reading `Tag[0..99]` becomes 100 individual reads.
- **[Skip]** **PLC-5 / SLC-500 bridging via ControlLogix gateway** — present in: both (Kepware Logix Gateway, TOP Server NET-ENI). Why it matters: thousands of legacy AB sites front a PLC-5/SLC behind a 1756-ENBT; without the bridge those plants can't migrate to us in one step.
- **[Build]** **Hot-standby ControlLogix redundancy (paired EN2T IPs)** — present in: AVEVA (and Kepware via secondary device). Why it matters: ControlLogix HSBY pairs are standard in continuous-process plants; today our driver has one host address per device, no automatic failover to the partner chassis.
- **[Build]** **Diagnostics / system tags (`_ConnectionStatus`, `_ScanRate`, `_TagCount`, `_DeviceError`)** — present in: both. Why it matters: SCADA dashboards bind to these for live driver health; we expose `IHostConnectivityProbe` + `DriverHealth` but not as browseable OPC UA variables.
- **[Build]** **Tag-write deadband / write-on-change / write-coalesce** — present in: both. Why it matters: avoids hammering the PLC on jittery analogue setpoints; we write every request straight through.
- **[Skip]** **Unsolicited messages (PLC-pushed CIP MSG)** — present in: AVEVA (DASABCIP unsolicited topic), Kepware (separate "ControlLogix Unsolicited" driver). Why it matters: event-driven alarm/recipe-complete signals from the PLC arrive with sub-100ms latency vs our 1s alarm-poll loop.
- **[Skip]** **CIP Generic / Class 3 message passthrough** — present in: both. Why it matters: enables custom tooling (drive parameters, motion config, MSG instruction targets) for shops that have built around it.
- **[Skip]** **Configurable per-device connection count / connection pooling** — present in: both (AVEVA: max 31). Why it matters: lets operators trade PLC CPU cost against parallelism for high-throughput rigs; we run one connection per tag handle implicitly.
- **[Build]** **Online tag-database refresh trigger** — present in: AVEVA (`$Sys$UpdateTagInfo`). Why it matters: lets ops force re-browse after a Studio 5000 download without restarting the driver; we only re-browse on full driver reinit.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | Offline L5K / L5X import | Yes | De-facto Studio 5000 workflow; engineers won't switch without it |
| 2 | CSV tag import / export | Yes | Common round-trip via Excel for mass config |
| 3 | Tag descriptions / engineering metadata | Yes | Free once L5X import lands; expected as OPC UA `Description` |
| 4 | Logical-blocking / non-blocking modes | Yes | Biggest perf lever; today only whole-UDT coalescing |
| 5 | Symbolic vs logical (instance-ID) toggle | Yes | 3-5x perf on dense rigs; libplctag already supports it |
| 6 | Configurable Connection Size per device | Yes | Cheap field knob for v19 firmware / fragmenting switches |
| 7 | Inactivity timeout / keep-alive cadence | No | Rarely an issue with libplctag-managed connections |
| 8 | Per-tag scan rate / scan groups | Yes | Standard SCADA expectation; mixed-rate tag tables |
| 9 | "Respect tag-specified scan rate" mode | No | Niche; OPC UA subscription rate already covers it |
| 10 | Initial value cache / first-update from cache | No | OPC UA subscription sampling already handles first-update |
| 11 | Multi-tag write packing | Yes | Recipe-download speed; one PDU vs N |
| 12 | AOI input / output handling | Yes | Standard modern Logix code structure |
| 13 | Native STRING / STRINGnn decoding | Yes | Table-stakes; we passthrough as String only |
| 14 | 64-bit LINT / ULINT fidelity | Yes | Correctness on Logix v32+; we silently truncate (TODO in code) |
| 15 | UDT as first-class OPC UA structured type | No | Member fan-out already works; structured-type plumbing is heavy |
| 16 | Array slice addressing `Tag[0..15]` | Yes | Perf; reads of N-element arrays in one call |
| 17 | PLC-5 / SLC bridging through CLX | No | AbLegacy driver covers this protocol family |
| 18 | Hot-standby ControlLogix redundancy | Yes | Continuous-process plants standardize on HSBY pairs |
| 19 | Diagnostic system tags (`_ConnectionStatus` etc.) | Yes | HMI dashboards bind to them; cheap given DriverHealth |
| 20 | Write deadband / write-on-change | Yes | Analog setpoints flood the PLC without it |
| 21 | Unsolicited CIP MSG ingestion | No | Separate driver in commercial; design-heavy; niche |
| 22 | CIP Generic / Class 3 passthrough | No | Niche custom-tooling territory |
| 23 | Per-device connection count / pooling | No | libplctag manages connections; premature |
| 24 | Online tag-DB refresh trigger | Yes | Cheap; avoids restart after PLC download |

### Notable parity (keep)

- libplctag-class wire layer covering ControlLogix/CompactLogix/Micro800/GuardLogix on EtherNet/IP CIP — same controller coverage as the commercial drivers (minus PLC-5/SLC).
- Multi-hop CIP path syntax with bridge-through chassis (`1,2,2,IP,1,0` form) — matches Kepware/AVEVA routing semantics.
- Online controller browse with program-scope vs controller-scope distinction and system-tag filtering — same shape as Kepware Auto Tag Generation.
- CIP Template Object (class 0x6C) decoder for live UDT-shape resolution + cache — feature-parity with Kepware's structure-aware Auto Tag Generation.
- Whole-UDT read coalescing for grouped members — matches TOP Server "logical blocking" optimisation for the cases it covers.
- BOOL-in-DINT bit-index addressing with RMW serialisation per parent — same semantics commercial drivers expose for `Tag.N` bit access.
- Per-PLC-family Connection Size / connected-messaging / fragment-bytes profile — mirrors the per-controller "model" picker in Kepware.
- ALMD alarm projection with edge-detected raise/clear — reasonable parity for the alarm subset of FT Alarms & Events that those drivers do not natively translate.
- Per-device circuit-breaker / bulkhead isolation keyed on `(driver, hostName)` — better operational story than the typical commercial gateway, which trips the whole channel on one bad device.
- GuardLogix safety-tag write rejection at config time — explicit, matches Rockwell's safety-partition rules.

### Sources

- [Kepware Allen-Bradley ControlLogix Ethernet driver overview](https://support.ptc.com/help/kepware/drivers/en/kepware/drivers/CONTROLLOGIXETHERNET/Overview.html)
- [Kepware Logix Database Settings (offline / online ATG, L5K/L5X)](https://support.ptc.com/help/kepware/drivers/en/kepware/drivers/CONTROLLOGIXETHERNET/Logix_Database_Settings.html)
- [Kepware Preparing for Automatic Tag Database Generation](https://support.ptc.com/help/kepware/drivers/en/kepware/drivers/CONTROLLOGIXETHERNET/Preparing_for_Automatic_Tag_Database_Generation.html)
- [Kepware Device Properties — Scan Mode (respect tag-specified, demand poll, initial cache)](https://support.ptc.com/help/kepware/drivers/en/kepware/drivers/Device_Properties_Scan_Mode.html)
- [Kepware Allen-Bradley ControlLogix Ethernet driver manual (PDF, 2025)](https://downloads.softwaretoolbox.com/demodnld/prod_docs/topserver_help_pdf/Common/allen-bradley-controllogix-ethernet-manual.pdf)
- [Kepware Allen-Bradley ControlLogix Server (Unsolicited)](https://www.ptc.com/en/store/kepware/drivers/allen-bradley-controllogix-unsolicited)
- [Kepware System Tags](https://support.ptc.com/help/kepware/kepware_edge/en/kepware/kepware-edge/system-tags.html)
- [TOP Server ControlLogix protocol modes (symbolic / logical-blocking / logical-non-blocking)](https://blog.softwaretoolbox.com/optimizing-controllogix-protocol-modes)
- [TOP Server Rockwell ControlLogix Ethernet OPC driver details](https://softwaretoolbox.com/top-server/rockwell-ab-controllogix-ethernet)
- [TOP Server ControlLogix Ethernet performance optimization](https://softwaretoolbox.com/top-server/rockwell-ab-controllogix-performance)
- [Software Toolbox FAQ — making configuration choices for ControlLogix Ethernet](https://help.softwaretoolbox.com/faq/1658)
- [AVEVA Communication Drivers Pack — ABCIP Driver user guide (PDF)](https://cdn.logic-control.com/docs/aveva/communications-pack/OIABCIP.pdf)
- [Wonderware DASABCIP user guide (PDF)](https://cdn.logic-control.com/media/DASABCIP.pdf)
- [Wonderware OI.ABCIP server user guide (PDF, v7.0)](https://s3-us-west-2.amazonaws.com/wonderwarepacwest/downloads/oi-abcip-user-guide.pdf)
- [Industrial Software Solutions — DASABCIP unsolicited message handling](https://industrial-software.com/training-support/tech-notes/74-how-configure-wonderware-dasabcip-unsolicited-message-handling/)
- [Industrial Software Solutions — `$Sys$UpdateTagInfo` with ABCIP](https://industrial-software.com/training-support/tech-notes/119-using-sysupdatetaginfo-with-abcip-oi-servers/)
- [AVEVA — Configure the ABCIP Communication Driver](https://docs.aveva.com/bundle/sp-cdp-drivers/page/193749.html)

---

## AbLegacy (Allen-Bradley PLC-5 / SLC / MicroLogix)

### What we ship today

- Per-device family knob: `Slc500` / `MicroLogix` / `Plc5` / `LogixPccc`, each mapped to a libplctag PLC attribute, default CIP path, max-tag-bytes, and string/long-file capability flags (`PlcFamilies/AbLegacyPlcFamilyProfile.cs:14-54`).
- Single transport: PCCC encapsulated in EtherNet/IP via libplctag, with `ab://gateway[:port]/cip-path` host strings supporting CLX-bridged routing (`AbLegacyHostAddress.cs:14-52`).
- File-letter set: `N`, `F`, `B`, `L`, `ST`, `T`, `C`, `R`, `I`, `O`, `S`, `A` parsed and validated; trailing `/N` bit index and `.SUBELEMENT` (ACC/PRE/EN/DN/TT/CU/CD/LEN/POS/ER) recognised (`AbLegacyAddress.cs:97-101`, `AbLegacyDataType.cs:9-29`).
- Data types: `Bit`, `Int` (N/A), `Long` (L), `Float` (F), `String` (ST), `TimerElement`, `CounterElement`, `ControlElement` — all surfacing as `Boolean` / `Int32` / `Float32` / `String` driver types (`AbLegacyDataType.cs:34-44`).
- Bit-within-N-word write path: read-modify-write against a parent-word runtime, serialised by per-parent `SemaphoreSlim` (`AbLegacyDriver.cs:353-409`).
- Polling overlay via shared `PollGroupEngine` exposed through `ISubscribable`; per-publishing-interval grouping (`AbLegacyDriver.cs:268-276`).
- Connectivity probe loop per device (default `S:0`, configurable interval/timeout) emitting `HostStatusChangedEventArgs` transitions (`AbLegacyDriver.cs:283-336`, `AbLegacyDriverOptions.cs:36-44`).
- Capability surfaces: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IPerCallHostResolver` — flat `AbLegacy/<host>/<tag>` browse tree built from static config (`AbLegacyDriver.cs:11-12`, `238-264`).
- Static-config tag list only (`AbLegacyTagDefinition`); writes can be flagged `Writable=false` and `WriteIdempotent=true` (`AbLegacyDriverOptions.cs:28-34`).
- Status mapping for libplctag error codes to OPC UA StatusCodes (`AbLegacyStatusMapper.cs`).

### Gaps vs commercial gateways

- **[Skip]** **Serial DF1 transports (full-duplex, half-duplex master/slave, KF2/KF3, radio modem)** — present in: both. Why: libplctag PCCC is Ethernet-only; no COM-port path means PLC-5/SLC/ML serial deployments are unreachable.
- **[Build]** **DH+ via 1756-DHRIO / 1784-PKTX gateway routing** — present in: both. Why: DH+ Gateway is the canonical way to reach PLC-5 nodes through a CLX rack today; we expose a CIP path but no station-number addressing or DH+ link-id concept.
- **[Skip]** **DH-485 routing through 1761-NET-AIC / 1747-AIC** — present in: both. Why: MicroLogix 1000/1200 and SLC 5/03 multi-drop deployments need DH-485 station addressing.
- **[Skip]** **`M0` / `M1` module file access (block-transfer / RIO data)** — present in: Kepware, AVEVA. Why: Required for any PLC-5 with RIO modules or specialty cards (motion, weigh, vision); PCCC has dedicated frames.
- **[Build]** **`PD` (PID), `MG` (Message), `PLS` (programmable limit switch), `BT` (block transfer) function/structure files** — present in: both. Why: Standard SLC/PLC-5 file types for PID loops and message instructions; we cap at T/C/R structures only.
- **[Skip]** **`D` (BCD) and Long-BCD types** — present in: both. Why: Some legacy SLC/PLC-5 programs store recipe / setpoint data as packed BCD; we only ship binary `Int`/`Long`.
- **[Build]** **PLC-5 octal addressing for I/O word/bit (`I:001/17`)** — present in: both. Why: Native PLC-5 documentation and RSLogix 5 use octal; rejecting decimal-only addresses misreads real configs.
- **[Build]** **Indirect / indexed addressing (`N7:[N7:0]`, `N[N7:0]:5`)** — present in: both. Why: Common pattern for recipe / batch lookup tables; libplctag supports it but our parser only accepts literal `<letter><file>:<word>`.
- **[Build]** **Array reads / contiguous block addressing (`N7:0,10` or `N7:0[10]`)** — present in: both. Why: One PCCC request can pull up to ~120 words; absent array syntax forces N round-trips for 1-of-N tags and breaks block-read sizing optimisation.
- **[Build]** **String-file (`ST`) read/write path in production** — present in: both. Why: Type is enum-listed but `AbLegacyDataTypeExtensions.ToDriverDataType` maps to `String` only; ST is an 82-byte fixed buffer with a length word and we have no integration coverage to confirm round-trip.
- **[Build]** **Sub-element predefined symbol coverage (timer `.PRE/.ACC/.EN/.TT/.DN`, counter `.CU/.CD/.OV/.UN`, control `.LEN/.POS/.ER/.UL/.IN/.FD`)** — present in: both. Why: Parser admits any all-letters sub-element but the `TimerElement/CounterElement/ControlElement` types collapse to a single `Int32`, losing per-bit Boolean semantics that HMIs expect (`.DN` should be Bit, not Int32).
- **[Skip]** **Block read-size negotiation per family** — present in: both. Why: We carry `MaxTagBytes` as a constant but never plumb it into a request optimiser; libplctag's PCCC chunking is implicit and not tunable per-tag-group.
- **[Build]** **Auto-demote on comm failure** — present in: both. Why: Kepware/TOP Server temporarily off-scan a non-responsive device for N seconds so other devices on the channel keep flowing; we only switch a `HostState` flag and keep retrying.
- **[Skip]** **Communication serialisation across multiple devices on one channel** — present in: both. Why: DH+/DF1 networks share a single physical link; we have no channel concept, so a slow PLC-5 can starve a fast SLC on the same DH+ link.
- **[Build]** **RSLogix 500 (`.RSS`) / RSLogix 5 (`.RSP`) / `.SLC` symbol & data-table import for automatic tag generation** — present in: both (DF1, AB Ethernet drivers). Why: Manual `AbLegacyTagDefinition` entries scale poorly; commercial tools parse RSLogix exports to seed tags and descriptions.
- **[Skip]** **Online browse / data-table discovery from the controller** — present in: Kepware (Create-from-Device). Why: PCCC has a "read file directory" frame; we don't issue it, so `DiscoverAsync` only ever returns the static config.
- **[Skip]** **DF1 error checking selection (BCC vs CRC-16)** — present in: both. Why: Some serial gear (older modems) only does BCC; not applicable until serial transport ships, but flagged for parity.
- **[Build]** **Per-tag deadband / change filter on subscriptions** — present in: both. Why: Polling overlay publishes every poll; commercial drivers suppress no-op publishes by absolute-deadband or scaling.
- **[Skip]** **PLC-5 typed-write / typed-read selection vs SLC protected typed reads** — present in: both. Why: Kepware exposes "Optimization Method" and "Force Logical=Yes" knobs that materially affect performance on slower processors; we use libplctag defaults silently.
- **[Build]** **Diagnostic counters (request count, response time, retries, last-error per device, comm-failures)** — present in: both (built-in `_System` / `_DiagnosticTags`). Why: We surface a `DriverHealth` enum but no per-device tag-level diagnostics for an HMI to bind to.
- **[Build]** **Per-device timeout / retry overrides** — present in: both. Why: We have one driver-wide `Timeout` (`AbLegacyDriverOptions.cs:16`) and one probe timeout; SLC 5/01 vs SLC 5/05 vs MicroLogix 1100 need very different values on a shared driver.
- **[Skip]** **Write completion semantics — synchronous-confirmation vs queued** — present in: both. Why: Commercial drivers offer "write optimization (latest value only / write-through / disable)"; ours always writes through, which floods slow channels with redundant writes.
- **[Build]** **MicroLogix-specific item naming (e.g. `RTC:0.HR`, `HSC:0`, `DLS:0` for daylight savings)** — present in: both. Why: MicroLogix 1100/1400 have proprietary function files that don't share file letters with SLC and our `IsKnownFileLetter` whitelist rejects them.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | Serial DF1 transports | No | Declining install base; libplctag has no serial path; major scope |
| 2 | DH+ via 1756-DHRIO bridging | Yes | Real-world PLC-5 path; libplctag CIP routing already supports it |
| 3 | DH-485 routing (1761/1747-AIC) | No | Very legacy; rare in greenfield |
| 4 | M0 / M1 module file access | No | Niche RIO modules; declining |
| 5 | PD / MG / PLS / BT files | Yes | PID files are common in real SLC programs |
| 6 | D (BCD) and Long-BCD types | No | Very legacy data convention |
| 7 | PLC-5 octal addressing | Yes | Correctness for actual PLC-5 sites |
| 8 | Indirect / indexed addressing | Yes | Standard recipe / lookup pattern |
| 9 | Array contiguous block addressing | Yes | Big perf gain; one PCCC frame vs N |
| 10 | ST string read / write production verification | Yes | Type is enum-listed but untested; cheap to validate |
| 11 | Sub-element bit semantics (`.DN` as Bit, etc.) | Yes | Correctness; HMIs expect Boolean for `.DN`/`.EN`/`.TT` |
| 12 | Block read-size negotiation per family | No | libplctag handles chunking implicitly |
| 13 | Auto-demote on comm failure | Yes | Standard SCADA resilience; one slow PLC starves fast ones |
| 14 | Channel-shared comm serialisation | No | Only matters for serial / DH+ (transport not built) |
| 15 | RSLogix 500/5 (.RSS / .RSP) symbol import | Yes | Workflow parity; manual config doesn't scale |
| 16 | Online controller browse / data-table discovery | No | PCCC dir frame limited; libplctag support unclear |
| 17 | DF1 BCC vs CRC-16 selection | No | Predicated on DF1 transport (gap #1) |
| 18 | Per-tag deadband / change filter | Yes | Polling overlay floods every poll without it |
| 19 | PLC-5 typed-read selection / Force Logical | No | libplctag defaults are sound; niche tuning |
| 20 | Diagnostic counters as tags | Yes | HMI binding; cheap given existing health probe |
| 21 | Per-device timeout / retry overrides | Yes | SLC 5/01 vs 5/05 vs ML1100 differ; cheap |
| 22 | Write completion semantics options | No | Niche tuning; current write-through is safe default |
| 23 | MicroLogix function-file naming (RTC/HSC/DLS) | Yes | Correctness for ML1100/1400 deployments |

### Notable parity (keep)

- Family enum + per-family profile keeps SLC 500 / MicroLogix / PLC-5 / LogixPccc-mode behavioural differences explicit instead of probed at runtime (`PlcFamilies/AbLegacyPlcFamilyProfile.cs:14-54`).
- ControlLogix-bridged routing string (`ab://gw/1,0`) matches Kepware's "Routing Path" concept and is how real PLC-5 deployments are reached today (`AbLegacyHostAddress.cs:14-52`).
- Bit-within-N-word RMW with per-parent serialisation prevents the classic two-writer-tear bug other drivers ship (`AbLegacyDriver.cs:353-384`).
- Probe loop with explicit `HostState` transitions gives a cleaner diagnostic surface than Kepware's lump-sum auto-demote (`AbLegacyDriver.cs:283-336`).
- Status-file probe (`S:0`) is the same heartbeat Rockwell HMIs traditionally use, and it's family-agnostic (`AbLegacyDriverOptions.cs:43`).
- libplctag back-end inherits ongoing community fixes for PCCC frame edge-cases without us owning the wire decoder.

### Sources

- [Kepware Allen-Bradley Ethernet Driver Manual (PDF)](https://cdn.logic-control.com/docs/kepware/Manuals/Drivers/Allen-Bradley/Allen-Bradley%20Ethernet%20Driver.pdf)
- [Kepware Allen-Bradley DF1 Driver Manual (PDF)](https://cdn.logic-control.com/docs/kepware/Manuals/Drivers/Allen-Bradley/Allen-Bradley%20DF1%20Driver.pdf)
- [Kepware Allen-Bradley ControlLogix Ethernet Driver Manual (PDF, 2025)](https://downloads.softwaretoolbox.com/demodnld/prod_docs/topserver_help_pdf/Common/allen-bradley-controllogix-ethernet-manual.pdf)
- [Kepware Allen-Bradley ControlLogix Driver Manual (PDF, 2017)](https://ftp.softwaretoolbox.com/demodnld/prod_docs/topserver_help_pdf/v5_20/controllogix_ethernet.pdf)
- [Kepware Allen-Bradley Ethernet driver product page](https://www.kepware.com/en-us/products/kepserverex/drivers/allen-bradley-ethernet/)
- [TOP Server Rockwell DF1 Serial driver](https://softwaretoolbox.com/top-server/rockwell-ab-df1)
- [AVEVA Communication Drivers Pack 2023 R2 readme](https://www.wmkit.com/archives/aveva-communication-drivers-pack-2023-r2-readme.html)
- [AVEVA Communication Drivers Pack 2020 R2 readme](https://industrial-software.com/wp-content/uploads/Communication_Drivers/oi-communication-drivers-pack-2020-r2/Readme.html)
- [AVEVA Communication Drivers datasheet (PDF)](https://www.aveva.com/content/dam/aveva/documents/datasheets/Datasheet_AVEVA-CommunicationDrivers_11-19.pdf)
- [AVEVA OI ABCIP user guide (PDF)](https://s3-us-west-2.amazonaws.com/wonderwarepacwest/downloads/oi-abcip-user-guide.pdf)
- [Kepware Logix Database Settings (Create-from-Device / .L5K import)](https://support.ptc.com/help/kepware/drivers/en/kepware/drivers/CONTROLLOGIXETHERNET/Logix_Database_Settings.html)
- [Rockwell DF1 Protocol and Command Set reference (1770-RM516, PDF)](https://literature.rockwellautomation.com/idc/groups/literature/documents/rm/1770-rm516_-en-p.pdf)

---

## FOCAS (Fanuc CNC)

### What we ship today

- TCP-only Ethernet transport on port 8193 via the pure-managed `Focas.Wire` client; no Fwlib DLL, no P/Invoke, no out-of-process Tier-C host (`docs/drivers/FOCAS.md:8-13`, retired Host noted at `:25-27`).
- One driver instance can host N CNCs, each keyed by `focas://{ip}[:{port}]` (`FocasDriverOptions.cs:10`, `FocasDeviceOptions:92-95`).
- Per-device CNC series declaration (`Zero_i_D/F/MF/TF`, `Sixteen_i`, `Thirty_i`, `ThirtyOne_i`, `ThirtyTwo_i`, `PowerMotion_i`, `Unknown`) with init-time capability matrix validating macro / parameter / PMC ranges per series (`FocasCncSeries.cs:21-47`, `FocasCapabilityMatrix.cs:29-138`).
- User-authored tag addressing for: PMC bits/bytes (`X0.0`, `R100`, `R100.3`), CNC parameters (`PARAM:1815/0`), and macro variables (`MACRO:500`) — wired through `cnc_rdpmcrng` / `cnc_rdparam` / `cnc_rdmacro` (`docs/drivers/FOCAS.md:62-66, 90`).
- Atomic data types: Bit, Byte, Int16, Int32, Float32, Float64, String (`FocasDataType.cs:10-26`).
- Read-only by design — `WriteAsync` returns `BadNotWritable`; no `cnc_wrparam` / `pmc_wrpmcrng` / `cnc_wrmacro` paths exist (`FocasDriver.cs:222-279`, `docs/drivers/FOCAS.md:17-18, 91`).
- Optional `FixedTree` auto-populated subtree per device (`FocasFixedTreeOptions:26-51`) populated at bootstrap from `cnc_sysinfo` + `cnc_rdaxisname` + `cnc_rdspdlname`, polled at three cadences (axis 250 ms, program 1 s, timer 30 s):
  - `Identity/` — `SeriesNumber`, `Version`, `MaxAxes`, `CncType`, `MtType`, `AxisCount` (`FocasDriver.cs:299-304`).
  - `Axes/{name}/` — `AbsolutePosition`, `MachinePosition`, `RelativePosition`, `DistanceToGo`, `ServoLoad` (cap-gated) (`FocasDriver.cs:307-316`).
  - `Axes/FeedRate/Actual`, `Axes/SpindleSpeed/Actual` (single-channel rates — first axis only, `FocasDriver.cs:317-318`, `:646-651`).
  - `Spindle/{name}/Load`, `Spindle/{name}/MaxRpm` (cap-gated, multi-spindle aware) (`FocasDriver.cs:323-336`).
  - `Program/Name`, `ONumber`, `Number`, `MainNumber`, `Sequence`, `BlockCount` (`FocasDriver.cs:339-347`).
  - `OperationMode/Mode` + `ModeText` ("MDI"/"AUTO"/"EDIT"/"HANDLE"/"JOG"/"TEACH_IN_HANDLE"/"REFERENCE"/"REMOTE"/"TEST"/"TJOG") (`IFocasClient.cs:213-226`).
  - `Timers/PowerOnSeconds`, `OperatingSeconds`, `CuttingSeconds`, `CycleSeconds` (`FocasDriver.cs:355-362`).
- Per-series node suppression: optional API probes at bootstrap, `EW_FUNC` / `EW_NOOPT` / `EW_VERSION` causes the corresponding subtree to not be emitted (`docs/drivers/FOCAS.md:134-142`, `FocasDriver.cs:497-526`).
- Active-alarm projection via `IAlarmSource` (opt-in, polls `cnc_rdalmmsg2` at 2 s default), differential raise/clear with mapped alarm types `Parameter / PulseCode / Overtravel / Overheat / Servo / DataIo / MemoryCheck / MacroAlarm`, severity buckets, and ack as no-op (`FocasAlarmProjectionOptions:79-85`, `IFocasClient.cs:275-287`, `docs/drivers/FOCAS.md:154-181`).
- Connectivity probe via `cnc_rdcncstat` on configurable interval; transitions fire `OnHostStatusChanged` (`FocasProbeOptions:110-115`, `docs/drivers/FOCAS.md:94`).
- Optional proactive handle-recycle loop to release FWLIB session handles on a cadence (defends against the documented handle-leak bugs and finite ~5–10 connection pool) (`FocasHandleRecycleOptions:68-72`, `docs/drivers/FOCAS.md:184-205`).
- Subscriptions are emulated via the shared `PollGroupEngine` (FOCAS has no push) (`FocasDriver.cs:451-461`).
- `IPerCallHostResolver` so each tag's reads route to its declared device, enabling per-host bulkhead resilience (decision #144) (`FocasDriver.cs:850-857`, `FocasDriverOptions.cs:3-7`).

### Gaps vs commercial gateways / MTConnect adapters

- **[Build]** **Writes (parameters / PMC / macro)** — Kepware "Fanuc Focas HSSB and Ethernet Driver", Ignition Fanuc, Memex Merlin, Predator MDC. Why: Macro / PMC writes are the canonical mechanism for DPRNT-free supervisory feedback to ladder logic; we explicitly return `BadNotWritable`.
- **[Skip]** **HSSB (high-speed serial bus) transport** — Kepware, MTConnect Fanuc Adapter (Cincinnati), Memex. Why: HSSB is the only path on machines with no FOCAS Ethernet option licensed; we are TCP:8193 only, no `hssb` discovery, no PCI handle.
- **[Build]** **FOCAS password / unlock parameter** — Kepware ("Password" property), MTConnect adapter. Why: Some controllers gate `cnc_wrparam` and certain reads behind a connection-level password; we have no such property in `FocasDeviceOptions`.
- **[Build]** **Multi-path / multi-channel CNC support** — Kepware (Path number 1..n), MTConnect (per-path Components). Why: 30i/31i/32i can host 2-10 paths each with their own program / position / mode; our `cnc_setpath`-equivalent never runs and the fixed tree implicitly assumes path 1.
- **[Skip]** **Series 15, Series 15i, Power Mate D/H, Series 35i** — Kepware lists 15/15i, MTConnect adapter handles legacy. Why: Our `FocasCncSeries` enum stops at Power Motion i + 16i; legacy Series 15 deployments would either fail validation or be forced to `Unknown`.
- **[Build]** **`cnc_getfigure` decimal scaling** — Kepware, MTConnect, Memex. Why: Position values are exposed as raw scaled ints (Float64-typed) and we punt the divide-by-10^N onto the client; commercial gateways present pre-scaled millimeters/inches. (Acknowledged TODO in `docs/drivers/FOCAS.md:144-148`.)
- **[Build]** **G-code / modal info (`cnc_modal`)** — Kepware ModalCodes group, MTConnect (FunctionalMode, MotionMode, PlaneCode, etc.), Ignition. Why: Modal G/M-code state (G54 active, G90/91, G17/18/19, M03/04/05, S/F overrides) is one of the most-asked CNC tag groups; we have neither a fixed-tree exposure nor a `MODAL:` address scheme.
- **[Build]** **Tool number, current tool, tool life management** — Kepware (T-code, ToolLife group), MTConnect (`ToolNumber`, `ToolGroup`), Memex, Predator MDC. Why: Live `cnc_rdtlife*` / current T-code are core MES integration data; absent.
- **[Skip]** **Tool offset table read/write (`cnc_rdtofs` / `cnc_wrtofs`)** — Kepware, Ignition. Why: Tool length / wear / radius compensation tables are often supervisory-edited; we have no `TOFS:` address scheme.
- **[Build]** **Work coordinate offsets (G54..G59 + extended via `cnc_rdzofs` / `cnc_wrzofs`)** — Kepware "WorkOffsets" group, MTConnect (`PartCount` and `WorkCoordinate`). Why: Setup automation needs to read/poke work offsets; absent.
- **[Build]** **Override values (Feedrate %, Rapid %, Spindle %, Jog %)** — Kepware OverrideGroup, MTConnect (`PathFeedrateOverride`, `RotaryVelocityOverride`). Why: Operator-modulated speeds are crucial for OEE/MES; not in the dynamic snapshot.
- **[Build]** **Status / running flags surfaced as nodes (Auto, Run, Motion, Mstb, EmergencyStop, Edit, Tmmode, Alarm bool)** — MTConnect adapter exposes `Execution`, `ControllerMode`, `EmergencyStop` directly. Why: We poll `cnc_rdcncstat` only as a Boolean probe; the 9-field ODBST struct (tmmode/aut/run/motion/mstb/emergency/alarm/edit) is never projected to nodes.
- **[Build]** **Parts count / required parts (`cnc_rdparam` 6711/6712/6713)** — Kepware "PartCount", MTConnect `PartCountAct/Min/Max`. Why: Part counters are MES bread-and-butter; reachable today only by user-authored `PARAM:6711` tag, not in the fixed tree.
- **[Build]** **Diagnostic numbers (`cnc_rddiag` / `cnc_rddiagdgn`)** — Kepware Diagnostic group, MTConnect. Why: Servo/spindle diagnostics (axis position errors, current, temperature) are essential for predictive maintenance; no `DIAG:` address scheme.
- **[Build]** **PMC data ranges (D/T/C/K/F/G addresses) for Series 16i** — partially limited by our matrix (`PmcLetters(Sixteen_i)` only allows X/Y/R/D, `FocasCapabilityMatrix.cs:80`). Why: Real 16i ladders use F/G signals for handshakes; users would have to set Series=Unknown to bypass validation.
- **[Build]** **Bulk PMC range read (`pmc_rdpmcrng` multi-byte)** — Kepware coalesces consecutive PMC bytes; we issue one request per tag. Why: One TCP RTT per PMC byte at scale will saturate; commercial drivers batch into ranges of up to 1KB.
- **[Build]** **Alarm history (`cnc_rdalmhistry` / `cnc_rdalmhistry5`)** — MTConnect adapter, Memex. Why: Acked alarms persist in a CNC ring buffer; we surface only the active alarm list.
- **[Build]** **External operator messages (`cnc_rdopmsg` / `cnc_rdopmsg2` / `cnc_rdopmsg3`)** — Kepware OpMessage tag, MTConnect (`Message` data item). Why: Macro programmers display operator messages via #3006 / G65 P9099 etc.; not exposed.
- **[Skip]** **Program list / upload / download / delete (`cnc_rdprogdir` / `cnc_upstart` / `cnc_dnstart` family)** — Kepware program-management group, Predator MDC, Memex Merlin. Why: DNC drip-feed is a primary use case for MDC products; entirely absent.
- **[Build]** **Currently-executing program text (`cnc_rdactpt` / `cnc_rdexecprog`)** — Kepware "CurrentProgram", MTConnect `Block` and `Line`. Why: Live block display / current sequence content; we expose `Sequence` (number) but not the block text.
- **[Skip]** **DPRNT / external data input (`cnc_rdmacrohk` / external macro)** — Predator MDC, Forcam, Memex (DPRNT collector). Why: DPRNT is the standard 1980s-vintage CNC-to-MES messaging path; we have no DPRNT TCP listener and no macro-call subscription.
- **[Skip]** **Servo / spindle deep info (`cnc_rdsvinfo` / `cnc_rdspinfo`)** — Kepware, Memex. Why: Servo cycle counts, spindle motor speed/temp; absent (we only expose load percent).
- **[Skip]** **Per-axis acceleration / jerk / feed-per-rev** — MTConnect (`AccelerationSpec`, `Jerk`, `Feedrate`). Why: Beyond actual feed; absent.
- **[Build]** **Cycle time per part / last cycle time / cycle start timestamp** — MTConnect (`ProcessTimer`), Memex. Why: We expose accumulating timers but not "last completed cycle" deltas.
- **[Skip]** **`cnc_rdrelpos` reset / preset, `cnc_setpath`, `cnc_wrabsmac`** — operator-style write commands. Why: Read-only-by-design covers it, but commercial parity assumes selective writes.
- **[Skip]** **CNC time/date sync (`cnc_rdtimer` clock variant / `cnc_rtime`)** — Kepware, Memex. Why: Setting CNC system clock from a master time source is common in audited environments; absent.
- **[Build]** **Connection-level statistics + retry counters surfaced as variables** — Kepware exposes per-channel stats; we publish health but not as variables.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | Writes (parameters / PMC / macro) | Yes | Key MES feedback path; current read-only is too narrow |
| 2 | HSSB transport | No | PCI hardware; declining; reopens fwlib distribution problem |
| 3 | FOCAS password / unlock | Yes | Cheap once writes ship; some controllers gate reads too |
| 4 | Multi-path / multi-channel CNC | Yes | 30i/31i/32i routinely have multiple paths |
| 5 | Series 15 / Power Mate D-H / Series 35i | No | Very legacy; small install base |
| 6 | `cnc_getfigure` decimal scaling | Yes | Already TODO; clients shouldn't compute scaling |
| 7 | Modal G-code / M-code state | Yes | One of the most-asked CNC tag groups |
| 8 | Tool number / tool life management | Yes | Core MES integration data |
| 9 | Tool offset table read / write | No | Write-heavy; defer with general write decision |
| 10 | Work coordinate offsets (G54..) | Yes | Setup automation needs read / poke |
| 11 | Override values (Feed / Rapid / Spindle / Jog) | Yes | OEE / MES bread-and-butter |
| 12 | ODBST status flags as nodes | Yes | Cheap; project the 9 fields we already read |
| 13 | Parts count in fixed tree | Yes | MES table-stakes; simple `cnc_rdparam` projection |
| 14 | Diagnostic numbers (`cnc_rddiag`) | Yes | Predictive maintenance |
| 15 | PMC F / G letters for 16i | Yes | Correctness; real ladders use F/G handshakes |
| 16 | Bulk PMC range read | Yes | Big perf gain at scale |
| 17 | Alarm history (`cnc_rdalmhistry`) | Yes | Auditing; small extension to alarm projection |
| 18 | Operator messages (`cnc_rdopmsg*`) | Yes | Cheap; common macro feedback |
| 19 | Program list / upload / download / delete | No | DNC product territory; significant scope |
| 20 | Currently-executing program text | Yes | HMI displays expect block view |
| 21 | DPRNT TCP listener | No | Significant scope; modern paths supersede it |
| 22 | Servo / spindle deep info | No | Specialty; load% covers most needs |
| 23 | Per-axis acceleration / jerk / feed-per-rev | No | Niche advanced telemetry |
| 24 | Cycle time per part / last cycle delta | Yes | OEE-essential |
| 25 | Operator write commands (preset etc.) | No | Read-only design choice; revisit only with general writes |
| 26 | CNC time / date sync | No | Rare ask; commonly handled by CNC NTP |
| 27 | Connection statistics as variables | Yes | Cheap given existing health |

### Notable parity (keep)

- Pure-managed wire client (no Fwlib distribution problem) — significant operational win vs Kepware's HSSB driver DLL stack.
- Per-series capability matrix at `InitializeAsync` time prevents silent runtime `BadOutOfRange` on misconfigured macro/parameter/PMC numbers.
- Fixed-tree per-API capability probes auto-suppress nodes the CNC doesn't support — operators don't see nodes that perpetually return `BadDeviceFailure`.
- `IPerCallHostResolver` integrates each device into the shared resilience bulkhead (Phase 6.1) — comparable to Kepware's per-device "channel" isolation.
- Three-tier poll cadence (axis fast / program medium / timer slow) is closer to MTConnect adapter behaviour than Kepware's single-rate channel scan.
- Handle-recycle loop is a thoughtful defence against documented Fanuc handle-leak firmware bugs — not present in many commercial drivers.
- Alarm projection differentiates raise vs clear and maps `ALM_TYPE_*` to OPC UA severity buckets — closer to A&E semantics than the simple "alarm bit" Kepware exposes.

### Sources

- https://www.kepware.com/en-us/products/kepserverex/drivers/fanuc-focas-hssb-ethernet/ — Kepware Fanuc Focas HSSB and Ethernet Driver
- https://github.com/mtconnect/cppagent_dev/tree/main/agent/adapter/fanuc — MTConnect Fanuc adapter reference
- https://github.com/Ladder99/focas-mock — managed Focas wire client (the OSS basis we consume)
- https://www.inductiveautomation.com/exchange/2218 — Ignition Fanuc FOCAS driver module
- https://memex.ca/merlin-tempus-mes-suite/ — Memex Merlin OEE / Fanuc connectivity
- https://www.predator-software.com/cnc-data-collection.htm — Predator MDC / DNC capabilities
- https://www.forcam.com/en/products/factory-data-collection/ — Forcam Force MES Fanuc driver
- Fanuc FOCAS Developer Kit `fwlib32.h` (mirrored at `strangesast/fwlib`) — authoritative API surface
- https://www.mtconnect.org/standard-2 — MTConnect Standard Part 2 Devices Information Model

---

## OpcUaClient (OPC UA Aggregation Client)

### What we ship today

- **Endpoint config**: single `EndpointUrl` plus ordered `EndpointUrls` failover list with `PerEndpointConnectTimeout` per-attempt budget (`OpcUaClientDriverOptions.cs:22-40`); failover sweep tries each in order on init and on session drop (`OpcUaClientDriver.cs:95-118`).
- **Security policies**: `None`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss` plus `Sign` / `SignAndEncrypt` modes; explicit policy+mode matching against the server's `GetEndpoints` response, no silent fallback to a weaker cipher (`OpcUaClientDriver.cs:299-336`).
- **Identity tokens**: Anonymous, Username/Password, and X509 user-certificate (PFX with private key) — built once and reused across every failover attempt (`OpcUaClientDriver.cs:244-369`).
- **Certificate management**: per-process PKI store rooted at `%LocalAppData%\OtOpcUa\pki` with own/trusted/issuers/rejected directories; SDK auto-creates the application instance certificate at startup; `AutoAcceptCertificates` dev knob hooks the validator's `BadCertificateUntrusted` path (`OpcUaClientDriver.cs:163-217`).
- **Session lifecycle**: configurable `SessionTimeout`, `KeepAliveInterval`, `ReconnectPeriod`, `ApplicationUri`, `SessionName`, operation `Timeout` (`OpcUaClientDriverOptions.cs:82-112`).
- **Reconnect**: native `Session.KeepAlive` event drives a `SessionReconnectHandler` with a 2-minute max retry period; SDK's automatic `TransferSubscriptions` migrates monitored items onto the rebuilt channel; keep-alive is rewired onto the new session post-recovery (`OpcUaClientDriver.cs:1297-1359`).
- **Discovery**: two-pass recursive browse from `BrowseRoot` (default `ObjectsFolder`) with `MaxBrowseDepth=10` and `MaxDiscoveredNodes=10_000` caps; pass 2 batch-reads `DataType` + `ValueRank` + `UserAccessLevel` + `Historizing` per variable in one Session.ReadAsync (`OpcUaClientDriver.cs:596-810`).
- **Type mapping**: built-in OPC UA scalar types → `DriverDataType`; structs/enums/extension objects fall through to String passthrough; `ValueRank>=0` flags arrays (`OpcUaClientDriver.cs:820-836`).
- **ACL bridge**: `UserAccessLevel.CurrentWrite` → `SecurityClassification.Operate`, otherwise `ViewOnly`; gating happens server-side in DriverNodeManager (`OpcUaClientDriver.cs:844-850`).
- **Read/Write**: batched ReadAsync/WriteAsync with NodeId pre-parse + per-tag `BadNodeIdInvalid` short-circuit; cascading-quality preserves upstream `StatusCode` and `SourceTimestamp` verbatim; transport faults fan out as `BadCommunicationError` (`OpcUaClientDriver.cs:441-568`).
- **Subscriptions**: native MonitoredItem forwarding with publishing-interval floor of 50 ms, `KeepAliveCount=10`, `LifetimeCount=1000`, `QueueSize=1`, `DiscardOldest=true`, `Reporting` mode, `TimestampsToReturn.Both` (`OpcUaClientDriver.cs:854-914`).
- **Alarms (A&C)**: EventFilter SelectClauses on `BaseEventType` + `ConditionType` (EventId/EventType/SourceNode/Message/Severity/Time/ConditionId), source-node filter set, `QueueSize=1000` for burst tolerance, `Acknowledge` method invocation forwarded as `CallAsync`; severity bucketed Low/Medium/High/Critical per OPC UA Part 9 (`OpcUaClientDriver.cs:967-1143`).
- **HistoryRead pass-through**: `ReadRawAsync`, `ReadProcessedAsync` (Average/Min/Max/Total/Count standard aggregates), `ReadAtTimeAsync` with continuation point support (`OpcUaClientDriver.cs:1154-1264`).
- **Diagnostics**: per-driver `HostName` reflects the URL actually connected (not the first candidate); `HostState` transitions Running/Stopped/Unknown driven by keep-alive; `DriverHealth` carries `LastSuccessfulRead` + last error (`OpcUaClientDriver.cs:1281-1372`).
- **Capability surface**: 8/8 — `IDriver`, `ITagDiscovery`, `IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`.

### Gaps vs commercial UA aggregators

- **[Build]** **Reverse Connect (server-initiated client connect)** — present in: UaGateway, Prosys Forge, Kepware (1.5+), Matrikon. Why: lets the upstream server traverse outbound-only firewalls (typical OT-DMZ direction); a hard requirement for many regulated plant networks.
- **[Build]** **Discovery URL with `FindServers` / `FindServersOnNetwork`** — present in: Kepware, UaGateway, Matrikon. Why: we accept only an explicit endpoint URL; commercial gateways resolve a discovery URL and let the operator pick from advertised endpoints in a UI without copying the policy/mode tuple by hand.
- **[Skip]** **Multicast / LDS-ME registration** — present in: UaGateway, Prosys. Why: lets clients discover this gateway via the Local Discovery Server without static config.
- **[Skip]** **GDS push management (Part 12)** — present in: UaGateway, Prosys. Why: certificate provisioning, renewal, trust-list updates pushed from a central GDS — required for fleets >10 endpoints; we have no `ServerConfigurationType` method support and no automatic renewal hook.
- **[Build]** **Per-tag advanced subscription tuning** — present in: Kepware, UaGateway, Cogent. Why: `SamplingInterval`, `QueueSize`, `DiscardOldest`, `MonitoringMode`, `DataChangeFilter` (DeadbandType=Absolute/Percent, Trigger=Status/StatusValue/StatusValueTimestamp) are hard-coded (50 ms / 1 / true / Reporting / no deadband). No way to set deadbands per tag — a baseline aggregator feature for analog noise filtering.
- **[Build]** **Per-subscription tuning (`PublishingInterval` / `KeepAliveCount` / `LifetimeCount` / `MaxNotificationsPerPublish` / `Priority`)** — present in: all listed gateways. Why: we hard-code 10/1000/0/0 in `Subscription` and `MaxNotificationsPerPublish=0` (unlimited) is a denial-of-service surface against high-event-rate servers; high-tag-count deployments need to split subscriptions across priorities.
- **[Build]** **Selective import / namespace remap** — present in: Kepware, Matrikon, UaGateway, Cogent. Why: we mirror everything under `BrowseRoot` and re-prefix with a single "Remote" folder; commercial aggregators support per-branch include/exclude rules, namespace-URI remapping, alias paths, and re-keyed BrowseNames.
- **[Build]** **Type definition mirroring (ObjectTypes / VariableTypes / DataTypes / ReferenceTypes)** — present in: UaGateway, Prosys, Kepware. Why: we walk Object + Variable nodes only; HasTypeDefinition references and custom type nodes are dropped, so downstream UI clients lose type-aware rendering and structured DataTypes decode as String passthrough.
- **[Build]** **Method node mirroring + pass-through `Call`** — present in: UaGateway, Matrikon, Kepware. Why: `NodeClass.Method` is filtered out of the browse and `IDriver` has no `CallMethodAsync` capability; clients cannot invoke remote methods through the gateway. (`Acknowledge` is the only call we forward, hard-coded for A&C.)
- **[Build]** **Automatic re-import on remote `ServerStatus.NodeVersion` / `ModelChangeEvent`** — present in: UaGateway, Kepware, Prosys. Why: we don't subscribe to `ServerStatus.State` or `BaseModelChangeEventType`; if the upstream server adds nodes mid-flight the new tags don't appear until the driver is reinitialized.
- **[Skip]** **HistoryUpdate / HistoryRead-Modified / Annotation pass-through** — present in: UaGateway, Prosys Historian, Kepware (LocalHistorian). Why: we ship Raw/Processed/AtTime only; `IsReadModified=false` is hard-coded; no `HistoryUpdate`, no `DeleteRawModified`, no annotation forwarding. Many MES integrations need backfill writes.
- **[Build]** **`ReadEventsAsync` (HistoryRead Events)** — explicitly deferred per memory entry. Why: `IHistoryProvider.ReadEventsAsync` interface lacks an `EventFilter SelectClauses` parameter to carry the field projection.
- **[Build]** **Aggregate function set** — present in: UaGateway, Prosys, Kepware. Why: we map only Average/Minimum/Maximum/Total/Count; OPC UA Part 13 standard catalog has 30+ (TimeAverage, Interpolative, StdDev, DurationGood, NumberOfTransitions, etc.) that historian-class clients expect.
- **[Build]** **Redundant-server URI list (`ServerUriArray`) and transparent failover** — present in: Kepware, UaGateway, Matrikon. Why: our `EndpointUrls` is a one-shot connect-attempt list, not a live redundancy group; we don't read the upstream `ServerRedundancyType` or fail over mid-session on `ServiceLevel` drop.
- **[Build]** **Maximum nodes per Read/Write/Browse honored from server capabilities** — present in: all listed gateways. Why: we delegate chunking to the SDK but never query `Server.ServerCapabilities.OperationLimits.MaxNodesPerRead/Write/Browse`; on undersized servers this can produce `BadTooManyOperations` instead of automatic fragmentation.
- **[Skip]** **Connection / session pooling for multi-instance scale-out** — present in: UaGateway, Cogent. Why: each driver instance opens its own session even when N drivers point at the same upstream; commercial gateways multiplex one session per remote across multiple downstream contexts to cut session count and cert-handshake load.
- **[Build]** **Diagnostics counters (PublishRequest count, NotificationsPerSecond, MissingPublishRequests, dropped-notification rate)** — present in: UaGateway, Prosys. Why: `DriverHealth` carries `LastSuccessfulRead` + last error string only; no per-server message-rate counters or publish-queue health metrics for the Admin dashboard.
- **[Skip]** **Kerberos / OAuth2 / IssuedToken (JWT) user identity** — present in: Kepware (Kerberos), UaGateway, Prosys. Why: we support Anonymous/Username/Certificate only; no `IssuedIdentityToken` token type, no Kerberos SPNEGO, no JWT bearer flow that newer security stacks (Azure AD) expect.
- **[Skip]** **WriteAsync attribute scope beyond Value** — present in: UaGateway, Matrikon. Why: `WriteAsync` hard-codes `AttributeId = Attributes.Value`; no way to write `StatusCode`, `SourceTimestamp`, or non-Value attributes (rare but a documented OPC UA capability).
- **[Build]** **CRL / revocation list configuration** — present in: Kepware, UaGateway. Why: the cert-validator hooks `BadCertificateUntrusted` only; revoked-cert chains aren't explicitly checked or surfaced as a distinct fault, and there's no `RejectSHA1SignedCertificates` knob.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | Reverse Connect | Yes | OT-DMZ outbound-only is the standard plant-network direction |
| 2 | Discovery URL `FindServers` | Yes | Standard UX; saves manual policy / mode tuple copy |
| 3 | Multicast / LDS-ME registration | No | Server-side responsibility, not aggregator's |
| 4 | GDS push management (Part 12) | No | Significant infra; rare for our deployment scale |
| 5 | Per-tag advanced subscription tuning (deadband, queue, mode) | Yes | Deadbands are baseline analog filtering |
| 6 | Per-subscription tuning (publishing / keep-alive / lifetime) | Yes | Avoid DoS on bursty servers; operability |
| 7 | Selective import / namespace remap | Yes | Curation is a baseline aggregator feature |
| 8 | Type definition mirroring | Yes | UI clients lose structure decoding without it |
| 9 | Method node mirroring + `Call` passthrough | Yes | Clear functional gap; `IDriver` capability missing |
| 10 | Auto re-import on `ModelChangeEvent` | Yes | Correctness when remote topology changes |
| 11 | HistoryUpdate / Modified / Annotation passthrough | No | MES backfill scope; defer |
| 12 | `ReadEventsAsync` (HistoryRead Events) | Yes | Fix the `IHistoryProvider` abstraction gap |
| 13 | Full Aggregate function set (Part 13) | Yes | Cheap to forward; historian clients expect it |
| 14 | `ServerUriArray` redundant failover | Yes | HA expectation when upstream is redundant |
| 15 | Honor server `OperationLimits` | Yes | Correctness; avoids `BadTooManyOperations` |
| 16 | Connection / session pooling | No | Premature; current per-instance model is simple and adequate |
| 17 | Diagnostics counters | Yes | Operability; admin dashboard needs publish-rate visibility |
| 18 | Kerberos / OAuth2 / JWT identity | No | Significant security work; defer until AD integration drives it |
| 19 | Write attribute scope beyond Value | No | Niche; rarely used in OPC UA practice |
| 20 | CRL / revocation handling | Yes | Security baseline expectation |

### Notable parity (keep)

- Cascading-quality contract: upstream `StatusCode` and `SourceTimestamp` preserved verbatim across Read, Subscribe, History — a baseline OPC-to-OPC bridging requirement.
- Native subscription forwarding (no polling translation layer) — matches Kepware/UaGateway architecture, not Matrikon Tunneller's COM-bridge approach.
- Two-pass discovery batching attribute reads — many naive aggregators issue per-node Reads which makes 10k-node servers take minutes.
- Explicit policy+mode endpoint matching (no silent downgrade) — matches UaGateway's behavior; Kepware historically defaulted to "best available" which has been a CVE source.
- Per-endpoint connect-timeout in failover sweep — bounded init budget is a property most of the listed gateways added late.
- SDK-managed `TransferSubscriptions` on reconnect — matches the OPC Foundation reference behavior; no hand-rolled migration code.

### Sources

- OPC Foundation UA-.NETStandard SDK docs — https://github.com/OPCFoundation/UA-.NETStandard
- Kepware KEPServerEX OPC UA Client — https://www.ptc.com/en/products/kepware/kepserverex/clients/opc-ua-client
- Matrikon OPC UA Tunneller — https://www.matrikonopc.com/products/opc-tunneller/
- Unified Automation UaGateway — https://www.unified-automation.com/products/wrapper-and-gateway/ua-gateway.html
- Prosys OPC UA Forge / Historian — https://www.prosysopc.com/products/opc-ua-forge/
- Cogent DataHub OPC UA — https://www.cogentdatahub.com/products/opc-ua/
- AVEVA System Platform OI.UACLIENT — https://docs.aveva.com (Operations Integration UACLIENT)
- OPC UA Part 4 (Services), Part 5 (Information Model), Part 9 (A&C), Part 11 (HistoricalAccess), Part 12 (Discovery & GDS), Part 13 (Aggregates), Part 14 (PubSub) — https://reference.opcfoundation.org/

---

## S7 (Siemens S7-300/400/1200/1500)

### What we ship today

- Native S7comm over ISO-on-TCP via S7netplus; default port 102, configurable so an in-CI Snap7 server can bind 1102 (`S7DriverOptions.cs:32`, `S7Driver.cs:87`).
- CPU family selector — `S71200`, `S71500`, `S71200Smart`, `S7200`, `S7300`, `S7400` — enum forwarded straight to S7netplus to pick the remote TSAP slot byte (`S7DriverOptions.cs:34-38`).
- Rack/slot configuration with documented conventions (S7-300 slot 2, S7-400 slot 2/3, S7-1200/1500 slot 0) (`S7DriverOptions.cs:42-51`).
- Single-connection-per-PLC policy enforced by a `SemaphoreSlim` because the CPU's comms mailbox is scanned at most once per cycle (`S7Driver.cs:23-27,60-67`).
- Static tag table parsed at `InitializeAsync` so syntactic typos fail fast instead of bleeding through as `BadInternalError` per read (`S7Driver.cs:103-110`).
- Address parser accepts DB / M / I / Q / T / C with X/B/W/D widths and 0-7 bit offsets, case-insensitive, with structured `FormatException` messages (`S7AddressParser.cs:65-216`).
- Scalar reads/writes for Bool, Byte, Int16/UInt16, Int32/UInt32, Float32 with explicit signed/unsigned reinterpret of S7netplus' boxed unsigned return values (`S7Driver.cs:231-251,306-322`).
- PUT/GET-disabled detection — `S7.Net.PlcException` mapped to `BadDeviceFailure` and surfaced as a configuration alert rather than retried via Polly (`S7Driver.cs:200-208`, `S7DriverOptions.cs:14-25`).
- Polled `ISubscribable` overlay floored at 100 ms to avoid wire-side queueing past CPU scan; per-tag last-value diffing for change-of-value publishing (`S7Driver.cs:365-425`).
- `IHostConnectivityProbe` using `ReadStatusAsync` (CPU Run/Stop) every probe interval, gated on the same semaphore so it doesn't race a live read (`S7Driver.cs:457-489`).
- Per-tag `WriteIdempotent` flag for replay-safe write retry policy (`S7DriverOptions.cs:91-104`).
- Snap7-server-backed integration fixture covers atomic typed reads + DB write-then-read round-trip on `localhost:1102` (`docs/drivers/S7-Test-Fixture.md:1-60`).
- Test CLI — probe / read / write / subscribe — with the same address grammar and CPU/slot flags (`docs/Driver.S7.Cli.md`).

### Gaps vs commercial gateways

- **[Build]** **S7-1500 Optimized DB / Symbolic addressing (S7Plus)** — present in: Kepware "Siemens S7 Plus", Ignition, AVEVA OI.SIDIRECT (limited). Why: S7netplus speaks classic S7comm only; optimized DBs reorder fields and have no fixed byte offsets, so absolute `DB1.DBW0` reads return `BadDeviceFailure` until "Optimized block access" is unchecked in TIA Portal.
- **[Build]** **PDU size negotiation surfaced to operators** — present in: Kepware, TOP Server, AVEVA OI.SIDIRECT. Why: Modern S7 CPUs negotiate PDU sizes from 240 up to 960 bytes; we accept whatever S7netplus negotiates with no operator visibility into the cap and no per-request packing strategy that uses the negotiated size.
- **[Build]** **Multi-variable PDU packing / read coalescing** — present in: every commercial gateway. Why: `ReadAsync(IReadOnlyList<string>)` issues one S7netplus call per tag inside the semaphore (`S7Driver.cs:182-214`); commercial gateways bin-pack contiguous DB ranges into a single multi-item PDU which is 5-50× faster on dense tag groups.
- **[Build]** **TSAP / Connection Type selector (PG / OP / S7-Basic / Other)** — present in: Kepware, TOP Server, AVEVA. Why: S7netplus picks PG-style TSAPs; sites that need OP-class slots (e.g. fenced HMI connections, license-counted PG slots) cannot pick. Some S7-1500 hardening modes refuse PG access from non-allowlisted clients.
- **[Build]** **Symbol-table / TIA Portal export browse** — present in: Kepware (online symbol upload on S7-1500), Ignition (TIA tag CSV import), TOP Server (tag-import wizard from `.AWL`/`.udt`/`.xml`). Why: We ship a static tag table only (`S7DriverOptions.cs:55-57`); operators must hand-edit the JSON. No `.tia`/`.s7p` import, no online symbol read of the S7-1500 PG symbol table.
- **[Build]** **UDT / STRUCT / nested-DB handling** — present in: Kepware, Ignition, TOP Server. Why: Tag map is flat scalar-only — no UDT fan-out into member variables, no `Array of <UDT>` indexing. Real S7-1500 projects expose hundreds of UDT-typed DBs.
- **[Build]** **Array tags (ValueRank=1)** — present in: every commercial gateway. Why: `S7TagDefinition` has no array dimension; `MapDataType` always returns `IsArray: false` (`S7Driver.cs:337-345`). OPC UA arrays of S7 `Array[0..n]` are unaddressable.
- **[Build]** **STRING / WSTRING / DTL / S5TIME / TIME / DATE_AND_TIME read+write** — present in: every commercial gateway. Why: Enum entries exist but every code path throws `NotSupportedException` (`S7Driver.cs:241-245,316-320`); S7 `STRING` has a 2-byte header, `WString` is UTF-16 with a 4-byte header, `DTL` is 12 bytes, `S5TIME` is BCD-encoded — none are wired up.
- **[Build]** **64-bit types (LInt / ULInt / LReal / LWord)** — present in: Kepware S7 Plus, Ignition, TOP Server S7-1500 driver. Why: `Int64`/`UInt64`/`Float64` cases throw `NotSupportedException` (`S7Driver.cs:241-243`); S7-1500 `LReal` (8-byte double) is the standard analog representation in modern projects.
- **[Build]** **Instance-DB / FB-block parameter access** — present in: Kepware, Ignition (with TIA import). Why: We address by absolute DB number; instance DBs of multi-instance FBs need symbolic resolution (`MyFB_Instance.MyParam`) which our parser doesn't accept.
- **[Build]** **CPU diagnostic buffer / SZL reads** — present in: Kepware (CPU diagnostic tags), TOP Server (`@Diagnostic` tags), AVEVA OI.SIDIRECT. Why: We probe `ReadStatusAsync` only (`S7Driver.cs:476`); SZL IDs 0x0000-0xFFFF (CPU type, firmware version, cycle time min/max/avg, diagnostic-buffer entries, hardware module status) are not exposed as system tags.
- **[Skip]** **AS-Alarms / Alarm_S/SQ/D/DQ / S7 ProDiag** — present in: Kepware (Alarms suite), Ignition. Why: No `IAlarmSource` implementation; CPU-resident alarms (Alarm_S blocks, ProDiag supervision messages, system diagnostic messages) are invisible to OPC UA A&E clients. CPU diagnostic-buffer entries similarly not surfaced.
- **[Skip]** **CPU Run/Stop control / block download / PG functions** — present in: Kepware (limited), AVEVA OI.SIDIRECT. Why: `ReadStatusAsync` is the only PG-class call we make; remote `WriteCpuStop` / `WriteCpuStart`, block download, password authentication for PG functions are absent.
- **[Build]** **PLC password / protection-level handling** — present in: Kepware, TOP Server, AVEVA. Why: S7-300/400 protection levels 1-3 and S7-1200/1500's "Connection mechanisms" / "Full access incl. fail-safe" tiers can require a password on connect; S7netplus's `Plc` ctor takes no password and we have no place to plumb one through.
- **[Skip]** **S7-1500 "Secure Communication" (TLS / certificate-based)** — present in: Siemens-direct (OPC UA on S7-1500), Kepware S7 Plus partial. Why: S7-1500 firmware V3.0+ supports authenticated PG connections with certificates; we connect plaintext over TCP only. Sites with hardened CPUs (`Access protection = high` + cert required) won't accept the driver.
- **[Skip]** **S7-400H / redundant H-system support** — present in: Kepware (paired-IP with sticky-master), AVEVA OI.SIDIRECT. Why: We have one host/port; H-systems present two sync'd CPUs on two IPs and the driver should fail over without losing subscriptions. Driver-level redundancy is unimplemented (server-level redundancy in `docs/Redundancy.md` is a separate axis).
- **[Skip]** **Multi-CPU rack / multiple TSAPs per rack** — present in: Kepware, TOP Server. Why: One Plc instance binds one (rack, slot); S7-400 multi-CPU racks expose 2-4 CPUs that need parallel sessions to drive in parallel.
- **[Skip]** **MPI / Profibus / RFC1006-routed transports** — present in: Kepware, AVEVA OI.SIDIRECT (DASSIDirect legacy paths), TOP Server. Why: S7netplus is Ethernet-only. Brownfield S7-300 sites still routed via CP 5611/5613 MPI cards or via S7-1500-as-router for fenced subnets are out of reach.
- **[Build]** **LOGO! 8 / S7-200 / S7-200 Smart variant tuning** — present in: Kepware "Siemens TCP/IP Ethernet" (LOGO!), Sharp7 (S7-200 Smart), Ignition. Why: `CpuType.S7200`/`S7200Smart` exists in S7netplus but the V-memory area (`V` letter) is not in our parser's switch (`S7AddressParser.cs:88-97`). LOGO!'s VM range and S7-200's V/SM areas are unaddressable.
- **[Build]** **Per-tag scan group / publish rate** — present in: Kepware (scan classes), Ignition (tag groups), TOP Server (scan rate per tag). Why: Subscriptions take one publishingInterval for the whole tag list (`S7Driver.cs:365-380`); a CPU with mixed 100 ms / 1 s / 10 s tags needs three subscribe calls and three semaphore-serialized poll loops.
- **[Build]** **Deadband / on-change suppression with absolute or percent thresholds** — present in: every commercial gateway. Why: We diff exact-equal only (`S7Driver.cs:419`); no analog deadband — a noisy float tag floods the bus.
- **[Build]** **Block-read coalescing for contiguous DB regions** — present in: every commercial gateway. Why: Reading `DB1.DBW0`, `DB1.DBW2`, `DB1.DBW4` issues 3 calls; commercial drivers issue a single FC=04 ReadVarRequest covering bytes 0-5 and slice client-side.
- **[Skip]** **Connection-resource budget management / max-parallel-jobs (AmqLen)** — present in: Kepware, TOP Server. Why: S7-1200/1500 expose 8-64 connection-resources and a per-connection parallel-jobs cap (Amq); we hold one connection and serialize, but commercial drivers open 2-4 connections per CPU to multiplex. We have no operator knob.
- **[Build]** **Pre-flight / online-test of PUT/GET enablement** — present in: Kepware (config validation step), AVEVA. Why: We surface `BadDeviceFailure` only at first read (`S7Driver.cs:200-208`); commercial drivers warn during connection wizard via SZL probe before the operator commits config.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | S7-1500 Optimized DB / Symbolic addressing (S7Plus) | Yes | Hard blocker on modern S7-1500 sites |
| 2 | PDU size negotiation surfaced | Yes | Cheap operability; no behavior change |
| 3 | Multi-variable PDU packing | Yes | 5-50x perf; current per-tag-per-call is the baseline gap |
| 4 | TSAP / Connection Type selector | Yes | Hardened CPUs reject PG-class slots |
| 5 | Symbol-table / TIA Portal export browse | Yes | Workflow parity; static JSON doesn't scale |
| 6 | UDT / STRUCT / nested-DB handling | Yes | Real S7-1500 projects expose hundreds of UDTs |
| 7 | Array tags (ValueRank=1) | Yes | Table-stakes; currently unaddressable |
| 8 | STRING / WSTRING / DTL / S5TIME / TIME / DT | Yes | Standard datatypes; currently throw `NotSupported` |
| 9 | 64-bit types (LInt / ULInt / LReal / LWord) | Yes | LReal is the standard analog representation on S7-1500 |
| 10 | Instance-DB / FB parameter access | Yes | Modern symbolic structure; absolute DBs alone are limiting |
| 11 | CPU diagnostic buffer / SZL reads | Yes | Operability; firmware / cycle-time visibility |
| 12 | AS-Alarms / Alarm_S / ProDiag | No | Significant scope; alarms are a separate workstream |
| 13 | CPU Run / Stop control / block download | No | Security / safety risk; out of scope |
| 14 | PLC password / protection-level handling | Yes | Hardened CPUs require it (S7netplus support permitting) |
| 15 | S7-1500 Secure Communication / TLS | No | Significant work; defer |
| 16 | S7-400H redundant H-system support | No | Rare in our deployment scope |
| 17 | Multi-CPU rack parallel sessions | No | Rare; one session per CPU works |
| 18 | MPI / Profibus / RFC1006-routed transports | No | Declining; brownfield only |
| 19 | LOGO! 8 / S7-200 V-memory area | Yes | Small parser fix broadens coverage materially |
| 20 | Per-tag scan group / publish rate | Yes | Operability; mixed-rate is normal |
| 21 | Deadband / on-change with thresholds | Yes | Analog noise mitigation |
| 22 | Block-read coalescing for contiguous DBs | Yes | Big perf win; complements multi-variable PDU packing |
| 23 | Connection-resource budget / parallel jobs | No | Premature; one connection works for most rigs |
| 24 | Pre-flight PUT/GET enablement test | Yes | UX improvement; cheap |

### Notable parity (keep)

- Single-connection-per-PLC + semaphore serialization is the documented S7netplus / Snap7 best practice and matches what TOP Server / AVEVA do in their default profile.
- 100 ms minimum publishing interval correctly reflects CPU mailbox scan reality — commercial gateways advertise "1 ms scan" in marketing then quietly floor to ~100 ms in practice.
- Strict address-parse-at-init with structured exceptions (rather than per-read `BadInternalError`) is better operator UX than Kepware's "you'll find out at runtime" default.
- PUT/GET-disabled mapped to a sticky `BadDeviceFailure` instead of being retried by Polly — Polly retry against a CPU that will keep refusing is exactly the failure mode that floods commercial deployments.
- `WriteIdempotent` per-tag flag is finer-grained than Kepware's connection-level `Auto Demote` and matches the safe-replay reality: DB set-points are replayable, M/Q edge-triggered bits are not.
- Probe path uses `ReadStatusAsync` (single CPU-state PDU) rather than a tag read — doubles as "PLC actually up" without polluting the comms mailbox.
- Driver-instance host/port format (`host:port`) matches the Modbus driver so Admin UI can render both families uniformly.
- Snap7-server CI fixture closes the "no commercial vendor offers a meaningful S7 simulator" gap that Kepware/TOP Server users hit on day one.

### Sources

- https://www.kepserverexopc.com/products/siemens-tcpip-ethernet/ (Kepware Siemens TCP/IP Ethernet)
- https://www.kepware.com/en-us/products/kepserverex/drivers/siemens-s7-plus/ (Kepware S7 Plus — Optimized DB / Symbolic addressing)
- https://www.aveva.com/en/products/communication-drivers/ (AVEVA OI Server / DASSIDirect)
- https://www.softwaretoolbox.com/topserver-siemens-suite (TOP Server Siemens Suite)
- https://docs.inductiveautomation.com/docs/8.1/platform/connecting-to-devices/siemens (Ignition Siemens driver guide)
- https://github.com/S7NetPlus/s7netplus (S7netplus library)
- https://snap7.sourceforge.net/ (Snap7)
- https://github.com/evcc-io/sharp7 (Sharp7 fork — S7-1200/1500 PUT/GET semantics)
- https://cache.industry.siemens.com/dl/files/591/68018591/att_956083/v1/s71500_communication_function_manual_en-US_en-US.pdf (Siemens S7-1500 Communication Function Manual)
- https://support.industry.siemens.com/cs/document/26224811 (Siemens — TSAPs and connection resources)
- https://support.industry.siemens.com/cs/document/89260861 (Siemens — SZL list IDs / system status lists)
- https://docs.tia.siemens.cloud/r/en-us/v20/safety-and-security/secure-communication (S7-1500 secure communication)

---

## TwinCAT (Beckhoff ADS)

### What we ship today

- `TwinCATDriver` implements `IReadable`, `IWritable`, `ISubscribable`, `ITagDiscovery`, `IHostConnectivityProbe`, `IPerCallHostResolver` over Beckhoff's `Beckhoff.TwinCAT.Ads` v6 `AdsClient` (`src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs:11-12`, `AdsTwinCATClient.cs:22-24`).
- AMS addressing parses `ads://{netId}:{port}` with the six-octet AmsNetId and TC3 default port 851 (also documents 801/811/821 for TC2 and 10000 for system service) (`TwinCATAmsAddress.cs:20-64`).
- Native ADS notifications via `AddDeviceNotificationExAsync` with `AdsTransMode.OnChange` and per-tag cycle time; falls back to shared `PollGroupEngine` when `UseNativeNotifications=false` (`TwinCATDriver.cs:296-339`, `AdsTwinCATClient.cs:130-160`).
- IEC 61131-3 atomic data type surface — Bool, S/U Int 8/16/32/64, Real, LReal, String, WString, Time, Date, DT, TOD (`TwinCATDataType.cs:9-30`).
- Symbol path parser supports POU/GVL prefix, struct member walks, array subscripts incl. multi-dim `Matrix[1,2]`, and bit-access `.0..31` (`TwinCATSymbolPath.cs:1-104`).
- Bit-indexed BOOL **read** path: read parent word as `uint`, mask locally (`AdsTwinCATClient.cs:57-64`, `ExtractBit`).
- Optional controller-side symbol browse via `SymbolLoaderFactory` (flat mode), with system-symbol filter for `TwinCAT_*`, `Constants.*`, `Mc_*`, `__*` (`AdsTwinCATClient.cs:178-195`, `TwinCATSystemSymbolFilter.cs`).
- Per-device probe loop calls `ReadStateAsync` and emits `OnHostStatusChanged` Running/Stopped transitions (`TwinCATDriver.cs:366-402`).
- Status-code mapping `AdsErrorCode` → OPC UA via `TwinCATStatusMapper`; auto-reconnect on dropped client (`TwinCATDriver.cs:413-429`).
- Sized strings `STRING(80)` / `WSTRING(80)` are tolerated in browse — type name parens stripped to bare atom (`AdsTwinCATClient.cs:200-206`).
- Live-tested against TCBSD VM and Hyper-V XAR — 30 integration test cases (read/write/array/subscribe/browse/reconnect/probe), 110 unit tests (`docs/drivers/TwinCAT-Test-Fixture.md`).

### Gaps vs commercial gateways

- **[Build]** **ADS Sum commands (sum-read / sum-write / sum-add-notification)** — present in: Kepware, TF6100, Ignition, TwinCAT.Ads itself. Why: we issue one `ReadValueAsync` per tag in a loop (`TwinCATDriver.cs:118-156`); commercial drivers batch into `IndexGroup=0xF080..0xF084` sum requests for ~10x throughput on multi-thousand tag scans.
- **[Build]** **Handle-based access (CreateVariableHandle / ReadByHandle)** — present in: Kepware, TF6100, AdsClient itself. Why: we resolve the symbolic name on every read; cached handles cut per-request bytes and AMS overhead, especially over WAN/multi-hop.
- **[Build]** **STRUCT / UDT decomposition with offline TMC parsing** — present in: Kepware (TwinCAT TMC import), TF6100 (native), Ignition. Why: `TwinCATDataType.Structure` is declared but discovery skips non-atomic symbols (`AdsTwinCATClient.cs:224`); we can't expose nested UDT trees without hand-declaring every leaf.
- **[Build]** **Bit-indexed BOOL writes** — present in: Kepware, TF6100. Why: we throw `NotSupportedException` (`AdsTwinCATClient.cs:99-100`); commercial drivers do read-modify-write or use `ADSIGRP_SYM_VALBYNAME` with the `.N` syntax the runtime supports for some primitives.
- **[Build]** **Multi-dim / whole-array reads** — present in: Kepware, TF6100. Why: we parse `Matrix[1,2]` element-by-element but never read the array in one ADS call; sized-array marshalling is in `TwinCAT.Ads` but unused here.
- **[Build]** **Int64 fidelity** — present in: TF6100, Ignition. Why: `LInt`/`ULInt` map to `DriverDataType.Int32` (`TwinCATDataType.ToDriverDataType` line 40 with explicit "matches Int64 gap" comment) — silent precision loss above 2^31.
- **[Build]** **TIME / DATE / DT / TOD as native OPC UA types** — present in: TF6100 (DateTime/Duration), Kepware. Why: we marshal all four as raw `UDINT` (`AdsTwinCATClient.cs:278-280`) leaving timestamp interpretation to the client.
- **[Build]** **ENUM / ALIAS / REFERENCE / POINTER / INTERFACE / UNION** — present in: TF6100, Kepware (partial). Why: not in `TwinCATDataType`; symbol-mapper returns `null` and skips.
- **[Skip]** **Multi-target / multi-route AMS gateway** — present in: Kepware, Ignition (one driver instance, many devices). Why: we accept N `Devices` but each requires its own `TwinCATDeviceOptions`; no central route table, no `StaticRoutes.xml` management, no AMS-router credential handling.
- **[Skip]** **TwinCAT 3.1.4024+ Secure ADS / ADS-over-TLS** — present in: TF6100, recent TwinCAT.Ads. Why: `AdsClient.Connect` is called without secure-ADS opts; no certificate or pre-shared-key knobs in `TwinCATDriverOptions`.
- **[Skip]** **Route credential management** — present in: Kepware (route auth UI), TF6100. Why: relies entirely on the host AMS router's pre-authorized routes; we have no in-driver way to add a route or supply credentials.
- **[Skip]** **NC-axis / CNC channel / EtherCAT slave I/O surfaces** — present in: TF6100 (full NC namespace), Kepware (NC variables). Why: our system-symbol filter actively drops `Mc_*` (`TwinCATSystemSymbolFilter.cs:28`); we treat NC plumbing as noise.
- **[Skip]** **System-service ports** (`AMSPORT_R0_REALTIME=200`, `R0_TCOMSERVER=10000`, `EVENTLOG=110`) — present in: TF6100, Kepware (system data). Why: only `Devices` are PLC-runtime ports in practice; no helpers for system-service requests, run/config-mode switches, or Real-Time diagnostic counters.
- **[Build]** **Event log ingest (TwinCAT EventLogger / TC3 Eventing)** — present in: TF6100 (alarms/conditions), Ignition. Why: we don't implement `IAlarmSource`; AMS port 110 events never surface as OPC UA AC events.
- **[Skip]** **PLC RPC / method invocation (TC3 method calls via ADS)** — present in: TF6100. Why: `IWritable` is value-only; no surface for `RpcInvoke`-style method calls on FB instances.
- **[Skip]** **Per-PLC-runtime fan-out (port 851/852/853)** — partially present. Why: technically supported via separate `Devices` entries, but no helper that auto-discovers which runtimes exist on a controller via the system service.
- **[Build]** **Sub-millisecond cycle accuracy / max-delay tuning** — present in: TF6100, Kepware. Why: `NotificationSettings(OnChange, cycleMs, 0)` clamps cycle to 1 ms and sets max-delay to 0 (`AdsTwinCATClient.cs:144-145`); no per-tag override of `MaxDelay` to coalesce bursty signals.
- **[Build]** **Cycle-time / jitter / PLC-state diagnostics** — present in: TF6100, Kepware. Why: probe only checks reachability; we don't surface cycle-time, jitter, RT-state or `_AppInfo.OnlineChangeCnt` as health signals.
- **[Build]** **Online change / symbol-version invalidation** — present in: TF6100, Ignition. Why: no listener on `ADSIGRP_SYMVAL_BYHND` invalidation event; an online change silently invalidates cached handles (we have none, but adding handles needs this).
- **[Skip]** **File-system access via ADS (`ADSIGRP_FOPEN/FREAD`)** — present in: TF6100. Why: not implemented; useful for reading recipe files / log uploads without a separate transport.

### Recommendations

| # | Gap | Build? | Rationale |
|---|-----|:------:|-----------|
| 1 | ADS Sum commands | Yes | ~10x throughput for multi-thousand-tag scans; blocker at scale |
| 2 | Handle-based access (caching) | Yes | Perf; reduces per-request bytes and AMS overhead |
| 3 | STRUCT / UDT decomposition with TMC parsing | Yes | Real projects have nested UDTs we currently can't expose |
| 4 | Bit-indexed BOOL writes | Yes | Correctness; we read bits but throw on write |
| 5 | Multi-dim / whole-array reads | Yes | Perf; library supports it |
| 6 | Int64 fidelity (LInt / ULInt) | Yes | Correctness; we silently truncate |
| 7 | TIME / DATE / DT / TOD as native UA types | Yes | Correctness; raw UDINT pushes interpretation to clients |
| 8 | ENUM / ALIAS / REFERENCE / POINTER / INTERFACE / UNION | Yes | At least ENUM and ALIAS are common in real projects |
| 9 | Multi-target / multi-route AMS gateway | No | Per-device config already works |
| 10 | Secure ADS / ADS-over-TLS | No | Significant work; defer |
| 11 | Route credential management | No | Host-level AMS router responsibility |
| 12 | NC-axis / CNC channel / EtherCAT slave I/O surfaces | No | Specialty; not in target use cases |
| 13 | System-service ports | No | Niche operational tooling |
| 14 | Event log / TC3 alarms (`IAlarmSource`) | Yes | Currently no `IAlarmSource` implementation; capability gap |
| 15 | PLC RPC / method invocation | No | Niche; design-heavy |
| 16 | Per-PLC-runtime auto-discover | No | Cosmetic; manual port config works |
| 17 | Sub-millisecond max-delay tuning | Yes | Cheap; helps coalesce bursty signals |
| 18 | Cycle-time / jitter / PLC-state diagnostics | Yes | Operability; cheap given existing probe |
| 19 | Online-change / symbol-version invalidation | Yes | Required if handle caching lands (gap #2) |
| 20 | File-system access via ADS | No | Niche; out of scope |

### Notable parity (keep)

- Native `OnChange` notifications (not polling) — matches TF6100/Kepware default and is the right CPU/latency posture.
- Symbolic addressing (no manual index-group/offset arithmetic) — same DX as Kepware's TwinCAT driver.
- Live integration suite against a real runtime (TCBSD + XAR), not just mocks — better than Ignition's stock TwinCAT module which lacks bundled hardware tests.
- System-symbol filter so `Discovered/` doesn't drown the address space — Kepware ships an equivalent.
- Config-driven tag declarations as the authoritative path; `EnableControllerBrowse` is opt-in — matches "tag-import-then-curate" workflow Kepware encourages.
- AmsNetId + port modelled correctly with TC3-vs-TC2 default port awareness — matches TF6100 conventions.

### Sources

- https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_ads_intro/index.html
- https://infosys.beckhoff.com/english.php?content=../content/1033/tcadsdll2/117571083.html (Sum commands / index groups)
- https://infosys.beckhoff.com/english.php?content=../content/1033/tf6100_tc3_opcua/index.html (TF6100)
- https://github.com/Beckhoff/TF6100-OPC-UA-Sample
- https://github.com/Beckhoff/TC3-AdsClient-Csharp / `Beckhoff.TwinCAT.Ads` NuGet docs
- https://www.kepserverexlibrary.kepware.com/Beckhoff%20TwinCAT (Kepware Beckhoff TwinCAT driver manual)
- https://docs.inductiveautomation.com/docs/8.1/platform/connections/devices (Ignition device drivers)
- https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_security_management/ (Secure ADS)
- https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_adsnetref/ (NotificationSettings, AdsTransMode)
