Files
lmxopcua/docs/featuregaps.md
Joseph Doherty 2d07d716dc Recover stashed driver-gaps work from pre-v2-mxgw-merge working tree
Captures uncommitted work that lived in the working tree on
v2-mxgw-integration but was orthogonal to the migration. Stashed
during the v2-mxgw merge to master (2026-04-30) and replanted here on
a feature branch off master so it's git-visible rather than living in
the stash list.

Two distinct buckets:

1. Tracked fixture/config refinements (10 files, ~36 lines):
   - scripts/e2e/test-opcuaclient.ps1
   - src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json
   - 5 docker-compose.yml under tests/.../IntegrationTests/Docker/
     (AbCip, Modbus, OpcUaClient, S7)
   - 4 fixture .cs files (AbServerFixture, ModbusSimulatorFixture,
     OpcPlcFixture, Snap7ServerFixture)

2. Untracked driver-gaps queue artifacts (~8000 lines):
   - docs/plans/{abcip,ablegacy,focas,opcuaclient,s7,twincat}-plan.md
     — per-driver gap plans
   - docs/featuregaps.md — cross-cutting analysis
   - docs/v2/focas-deployment.md, docs/v2/implementation/focas-simulator-plan.md
   - followup.md — auto/driver-gaps queue follow-ups
   - scripts/queue/ — PR-queue automation tooling (12 files including
     pr-manifest.yaml at 1473 lines)

This commit is a snapshot for recoverability — review and split into
focused PRs (or discard) before merging anywhere downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:28:01 -04:00

624 lines
84 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~510 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)