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

84 KiB
Raw Blame History

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) plans/abcip-plan.md
AbLegacy — Allen-Bradley PLC-5 / SLC / MicroLogix (PCCC) plans/ablegacy-plan.md
FOCAS — Fanuc CNC FOCAS / FOCAS2 plans/focas-plan.md
OpcUaClient — OPC UA aggregation client plans/opcuaclient-plan.md
S7 — Siemens S7-300 / 400 / 1200 / 1500 plans/s7-plan.md
TwinCAT — Beckhoff TwinCAT 2 / 3 (ADS) 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, SafetyTagAbCipDriverOptions.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.ViewOnlyAbCipDriverOptions.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


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


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


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.CurrentWriteSecurityClassification.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


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


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