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>
84 KiB
84 KiB
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-pathhost-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,0or 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 byName, withTagPath,DataType,Writable,WriteIdempotent,Members,SafetyTag—AbCipDriverOptions.cs:95-103. - Logix atomic types
BOOL/SINT/INT/DINT/LINT/USINT/UINT/UDINT/ULINT/REAL/LREAL/STRING/DTplusStructuremarker —src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataType.cs:16-37. - Optional online controller browse via libplctag
@tagspseudo-tag, surfaced under aDiscovered/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 —
AbCipUdtReadPlannergroups 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.Nbit-index) with read-decode + RMW write through a per-parentSemaphoreSlimand 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
OnHostStatusChangedevents —AbCipDriverOptions.cs:131-143,AbCipDriver.cs:235-295. - ALMD alarm projection (opt-in) polling
InFaulted+Severity, raisingOnAlarmEventon 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 allIAbCipTagRuntimehandles —AbCipDriver.cs:163-167. - CLI test client:
probe,read,write,subscribeagainst 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
STRINGtoDriverDataType.StringbutAbCipDataType.csflags whole-string only; no support for user-definedSTRINGnnvariants 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
Int32per a TODO atAbCipDataType.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.Stringplaceholder 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], sliceTag[0..15]). Why it matters:AbCipTagPathsupports indexed elements but the driver has no array-slice read for adjacent indices; readingTag[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 exposeIHostConnectivityProbe+DriverHealthbut 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,0form) — 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.Nbit 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
- Kepware Logix Database Settings (offline / online ATG, L5K/L5X)
- Kepware Preparing for Automatic Tag Database Generation
- Kepware Device Properties — Scan Mode (respect tag-specified, demand poll, initial cache)
- Kepware Allen-Bradley ControlLogix Ethernet driver manual (PDF, 2025)
- Kepware Allen-Bradley ControlLogix Server (Unsolicited)
- Kepware System Tags
- TOP Server ControlLogix protocol modes (symbolic / logical-blocking / logical-non-blocking)
- TOP Server Rockwell ControlLogix Ethernet OPC driver details
- TOP Server ControlLogix Ethernet performance optimization
- Software Toolbox FAQ — making configuration choices for ControlLogix Ethernet
- AVEVA Communication Drivers Pack — ABCIP Driver user guide (PDF)
- Wonderware DASABCIP user guide (PDF)
- Wonderware OI.ABCIP server user guide (PDF, v7.0)
- Industrial Software Solutions — DASABCIP unsolicited message handling
- Industrial Software Solutions —
$Sys$UpdateTagInfowith ABCIP - AVEVA — Configure the ABCIP Communication Driver
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-pathhost strings supporting CLX-bridged routing (AbLegacyHostAddress.cs:14-52). - File-letter set:
N,F,B,L,ST,T,C,R,I,O,S,Aparsed and validated; trailing/Nbit 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 asBoolean/Int32/Float32/Stringdriver 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
PollGroupEngineexposed throughISubscribable; per-publishing-interval grouping (AbLegacyDriver.cs:268-276). - Connectivity probe loop per device (default
S:0, configurable interval/timeout) emittingHostStatusChangedEventArgstransitions (AbLegacyDriver.cs:283-336,AbLegacyDriverOptions.cs:36-44). - Capability surfaces:
IDriver,IReadable,IWritable,ITagDiscovery,ISubscribable,IHostConnectivityProbe,IPerCallHostResolver— flatAbLegacy/<host>/<tag>browse tree built from static config (AbLegacyDriver.cs:11-12,238-264). - Static-config tag list only (
AbLegacyTagDefinition); writes can be flaggedWritable=falseandWriteIdempotent=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/M1module 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 binaryInt/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,10orN7: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 butAbLegacyDataTypeExtensions.ToDriverDataTypemaps toStringonly; 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 theTimerElement/CounterElement/ControlElementtypes collapse to a singleInt32, losing per-bit Boolean semantics that HMIs expect (.DNshould be Bit, not Int32). - [Skip] Block read-size negotiation per family — present in: both. Why: We carry
MaxTagBytesas 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
HostStateflag 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) /.SLCsymbol & data-table import for automatic tag generation — present in: both (DF1, AB Ethernet drivers). Why: ManualAbLegacyTagDefinitionentries 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
DiscoverAsynconly 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 aDriverHealthenum 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:0for daylight savings) — present in: both. Why: MicroLogix 1100/1400 have proprietary function files that don't share file letters with SLC and ourIsKnownFileLetterwhitelist 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
HostStatetransitions 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)
- Kepware Allen-Bradley DF1 Driver Manual (PDF)
- Kepware Allen-Bradley ControlLogix Ethernet Driver Manual (PDF, 2025)
- Kepware Allen-Bradley ControlLogix Driver Manual (PDF, 2017)
- Kepware Allen-Bradley Ethernet driver product page
- TOP Server Rockwell DF1 Serial driver
- AVEVA Communication Drivers Pack 2023 R2 readme
- AVEVA Communication Drivers Pack 2020 R2 readme
- AVEVA Communication Drivers datasheet (PDF)
- AVEVA OI ABCIP user guide (PDF)
- Kepware Logix Database Settings (Create-from-Device / .L5K import)
- Rockwell DF1 Protocol and Command Set reference (1770-RM516, PDF)
FOCAS (Fanuc CNC)
What we ship today
- TCP-only Ethernet transport on port 8193 via the pure-managed
Focas.Wireclient; 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 throughcnc_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 —
WriteAsyncreturnsBadNotWritable; nocnc_wrparam/pmc_wrpmcrng/cnc_wrmacropaths exist (FocasDriver.cs:222-279,docs/drivers/FOCAS.md:17-18, 91). - Optional
FixedTreeauto-populated subtree per device (FocasFixedTreeOptions:26-51) populated at bootstrap fromcnc_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_VERSIONcauses the corresponding subtree to not be emitted (docs/drivers/FOCAS.md:134-142,FocasDriver.cs:497-526). - Active-alarm projection via
IAlarmSource(opt-in, pollscnc_rdalmmsg2at 2 s default), differential raise/clear with mapped alarm typesParameter / 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_rdcncstaton configurable interval; transitions fireOnHostStatusChanged(FocasProbeOptions:110-115,docs/drivers/FOCAS.md:94). - Optional proactive handle-recycle loop to release FWLIB session handles on a cadence (defends against the documented handle-leak bugs and finite ~5–10 connection pool) (
FocasHandleRecycleOptions:68-72,docs/drivers/FOCAS.md:184-205). - Subscriptions are emulated via the shared
PollGroupEngine(FOCAS has no push) (FocasDriver.cs:451-461). IPerCallHostResolverso 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
hssbdiscovery, no PCI handle. - [Build] FOCAS password / unlock parameter — Kepware ("Password" property), MTConnect adapter. Why: Some controllers gate
cnc_wrparamand certain reads behind a connection-level password; we have no such property inFocasDeviceOptions. - [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
FocasCncSeriesenum stops at Power Motion i + 16i; legacy Series 15 deployments would either fail validation or be forced toUnknown. - [Build]
cnc_getfiguredecimal 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 indocs/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 aMODAL:address scheme. - [Build] Tool number, current tool, tool life management — Kepware (T-code, ToolLife group), MTConnect (
ToolNumber,ToolGroup), Memex, Predator MDC. Why: Livecnc_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 noTOFS:address scheme. - [Build] Work coordinate offsets (G54..G59 + extended via
cnc_rdzofs/cnc_wrzofs) — Kepware "WorkOffsets" group, MTConnect (PartCountandWorkCoordinate). 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,EmergencyStopdirectly. Why: We pollcnc_rdcncstatonly 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_rdparam6711/6712/6713) — Kepware "PartCount", MTConnectPartCountAct/Min/Max. Why: Part counters are MES bread-and-butter; reachable today only by user-authoredPARAM:6711tag, 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; noDIAG: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_rdpmcrngmulti-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 (Messagedata 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_dnstartfamily) — 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", MTConnectBlockandLine. Why: Live block display / current sequence content; we exposeSequence(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_rdrelposreset / 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_rdtimerclock 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
InitializeAsynctime prevents silent runtimeBadOutOfRangeon 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. IPerCallHostResolverintegrates 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 atstrangesast/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
EndpointUrlplus orderedEndpointUrlsfailover list withPerEndpointConnectTimeoutper-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_RsaPssplusSign/SignAndEncryptmodes; explicit policy+mode matching against the server'sGetEndpointsresponse, 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\pkiwith own/trusted/issuers/rejected directories; SDK auto-creates the application instance certificate at startup;AutoAcceptCertificatesdev knob hooks the validator'sBadCertificateUntrustedpath (OpcUaClientDriver.cs:163-217). - Session lifecycle: configurable
SessionTimeout,KeepAliveInterval,ReconnectPeriod,ApplicationUri,SessionName, operationTimeout(OpcUaClientDriverOptions.cs:82-112). - Reconnect: native
Session.KeepAliveevent drives aSessionReconnectHandlerwith a 2-minute max retry period; SDK's automaticTransferSubscriptionsmigrates 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(defaultObjectsFolder) withMaxBrowseDepth=10andMaxDiscoveredNodes=10_000caps; pass 2 batch-readsDataType+ValueRank+UserAccessLevel+Historizingper 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>=0flags arrays (OpcUaClientDriver.cs:820-836). - ACL bridge:
UserAccessLevel.CurrentWrite→SecurityClassification.Operate, otherwiseViewOnly; gating happens server-side in DriverNodeManager (OpcUaClientDriver.cs:844-850). - Read/Write: batched ReadAsync/WriteAsync with NodeId pre-parse + per-tag
BadNodeIdInvalidshort-circuit; cascading-quality preserves upstreamStatusCodeandSourceTimestampverbatim; transport faults fan out asBadCommunicationError(OpcUaClientDriver.cs:441-568). - Subscriptions: native MonitoredItem forwarding with publishing-interval floor of 50 ms,
KeepAliveCount=10,LifetimeCount=1000,QueueSize=1,DiscardOldest=true,Reportingmode,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=1000for burst tolerance,Acknowledgemethod invocation forwarded asCallAsync; 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),ReadAtTimeAsyncwith continuation point support (OpcUaClientDriver.cs:1154-1264). - Diagnostics: per-driver
HostNamereflects the URL actually connected (not the first candidate);HostStatetransitions Running/Stopped/Unknown driven by keep-alive;DriverHealthcarriesLastSuccessfulRead+ 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
ServerConfigurationTypemethod 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 inSubscriptionandMaxNotificationsPerPublish=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
BrowseRootand 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.Methodis filtered out of the browse andIDriverhas noCallMethodAsynccapability; clients cannot invoke remote methods through the gateway. (Acknowledgeis 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 toServerStatus.StateorBaseModelChangeEventType; 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=falseis hard-coded; noHistoryUpdate, noDeleteRawModified, no annotation forwarding. Many MES integrations need backfill writes. - [Build]
ReadEventsAsync(HistoryRead Events) — explicitly deferred per memory entry. Why:IHistoryProvider.ReadEventsAsyncinterface lacks anEventFilter SelectClausesparameter 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: ourEndpointUrlsis a one-shot connect-attempt list, not a live redundancy group; we don't read the upstreamServerRedundancyTypeor fail over mid-session onServiceLeveldrop. - [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 produceBadTooManyOperationsinstead 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:
DriverHealthcarriesLastSuccessfulRead+ 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
IssuedIdentityTokentoken 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:
WriteAsynchard-codesAttributeId = Attributes.Value; no way to writeStatusCode,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
BadCertificateUntrustedonly; revoked-cert chains aren't explicitly checked or surfaced as a distinct fault, and there's noRejectSHA1SignedCertificatesknob.
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
StatusCodeandSourceTimestamppreserved 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
TransferSubscriptionson 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
SemaphoreSlimbecause the CPU's comms mailbox is scanned at most once per cycle (S7Driver.cs:23-27,60-67). - Static tag table parsed at
InitializeAsyncso syntactic typos fail fast instead of bleeding through asBadInternalErrorper 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
FormatExceptionmessages (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.PlcExceptionmapped toBadDeviceFailureand surfaced as a configuration alert rather than retried via Polly (S7Driver.cs:200-208,S7DriverOptions.cs:14-25). - Polled
ISubscribableoverlay 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). IHostConnectivityProbeusingReadStatusAsync(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
WriteIdempotentflag 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.DBW0reads returnBadDeviceFailureuntil "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/.s7pimport, 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:
S7TagDefinitionhas no array dimension;MapDataTypealways returnsIsArray: false(S7Driver.cs:337-345). OPC UA arrays of S7Array[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); S7STRINGhas a 2-byte header,WStringis UTF-16 with a 4-byte header,DTLis 12 bytes,S5TIMEis 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/Float64cases throwNotSupportedException(S7Driver.cs:241-243); S7-1500LReal(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 (
@Diagnostictags), AVEVA OI.SIDIRECT. Why: We probeReadStatusAsynconly (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
IAlarmSourceimplementation; 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:
ReadStatusAsyncis the only PG-class call we make; remoteWriteCpuStop/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
Plcctor 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.mdis 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/S7200Smartexists in S7netplus but the V-memory area (Vletter) 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.DBW4issues 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
BadDeviceFailureonly 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
BadDeviceFailureinstead of being retried by Polly — Polly retry against a CPU that will keep refusing is exactly the failure mode that floods commercial deployments. WriteIdempotentper-tag flag is finer-grained than Kepware's connection-levelAuto Demoteand 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
TwinCATDriverimplementsIReadable,IWritable,ISubscribable,ITagDiscovery,IHostConnectivityProbe,IPerCallHostResolverover Beckhoff'sBeckhoff.TwinCAT.Adsv6AdsClient(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
AddDeviceNotificationExAsyncwithAdsTransMode.OnChangeand per-tag cycle time; falls back to sharedPollGroupEnginewhenUseNativeNotifications=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 forTwinCAT_*,Constants.*,Mc_*,__*(AdsTwinCATClient.cs:178-195,TwinCATSystemSymbolFilter.cs). - Per-device probe loop calls
ReadStateAsyncand emitsOnHostStatusChangedRunning/Stopped transitions (TwinCATDriver.cs:366-402). - Status-code mapping
AdsErrorCode→ OPC UA viaTwinCATStatusMapper; 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
ReadValueAsyncper tag in a loop (TwinCATDriver.cs:118-156); commercial drivers batch intoIndexGroup=0xF080..0xF084sum 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.Structureis 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 useADSIGRP_SYM_VALBYNAMEwith the.Nsyntax 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 inTwinCAT.Adsbut unused here. - [Build] Int64 fidelity — present in: TF6100, Ignition. Why:
LInt/ULIntmap toDriverDataType.Int32(TwinCATDataType.ToDriverDataTypeline 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 returnsnulland skips. - [Skip] Multi-target / multi-route AMS gateway — present in: Kepware, Ignition (one driver instance, many devices). Why: we accept N
Devicesbut each requires its ownTwinCATDeviceOptions; no central route table, noStaticRoutes.xmlmanagement, no AMS-router credential handling. - [Skip] TwinCAT 3.1.4024+ Secure ADS / ADS-over-TLS — present in: TF6100, recent TwinCAT.Ads. Why:
AdsClient.Connectis called without secure-ADS opts; no certificate or pre-shared-key knobs inTwinCATDriverOptions. - [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: onlyDevicesare 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:
IWritableis value-only; no surface forRpcInvoke-style method calls on FB instances. - [Skip] Per-PLC-runtime fan-out (port 851/852/853) — partially present. Why: technically supported via separate
Devicesentries, 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 ofMaxDelayto 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.OnlineChangeCntas health signals. - [Build] Online change / symbol-version invalidation — present in: TF6100, Ignition. Why: no listener on
ADSIGRP_SYMVAL_BYHNDinvalidation 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
OnChangenotifications (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;
EnableControllerBrowseis 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.AdsNuGet 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)