10 KiB
Beckhoff TwinCAT (ADS) Driver
Getting-started guide for the Beckhoff TwinCAT driver. This is the short path —
for the full per-field spec read docs/v2/driver-specs.md §6,
for hands-on CLI testing read Driver.TwinCAT.Cli.md,
and for the test-harness map read TwinCAT-Test-Fixture.md.
What it talks to
Beckhoff PLC runtimes — TwinCAT 2 and TwinCAT 3 — over the Beckhoff ADS
protocol carried by AMS routing. The driver runs in-process in the OtOpcUa
server's .NET 10 AnyCPU host. It compiles and runs without a local AMS router,
but every wire call returns BadCommunicationError until a router is reachable
(the router translates an AMS Net ID to an IP route).
Addressing is symbol-based: tags are referenced by their TwinCAT symbolic
name (e.g. MAIN.bStart, GVL.Counter, Motor1.Status.Running) rather than by
raw memory offset. One driver instance fans out to N targets, each identified by
an AMS Net ID + port.
Project split
| Project | Target | Role |
|---|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ |
net10.0 | In-process driver — hosts the ADS client, symbol-path parser, and per-device probe loops |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/ |
net10.0 | Config records + the TwinCATDataType enum bound from DriverConfig JSON |
Minimum deployment
"Drivers": {
"twincat-cell-1": {
"Type": "TwinCAT",
"Config": {
"Devices": [ { "HostAddress": "ads://5.23.91.23.1.1:851", "DeviceName": "Cell1" } ],
"Tags": [
{ "Name": "Start", "DeviceHostAddress": "ads://5.23.91.23.1.1:851",
"SymbolPath": "MAIN.bStart", "DataType": "Bool", "Writable": true },
{ "Name": "Count", "DeviceHostAddress": "ads://5.23.91.23.1.1:851",
"SymbolPath": "GVL.Counter", "DataType": "Int32", "Writable": false }
]
}
}
}
AMS address form
HostAddress is an ads://{netId}:{port} URI parsed by
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAmsAddress.cs. The Net ID
is six dot-separated octets (NOT an IP — a Beckhoff-specific identifier the
router maps to a route); the port is the AMS service port (851 = TC3 PLC runtime
1, 852 = runtime 2, 801 / 811 / 821 = TC2 PLC runtimes). Port defaults to 851
when omitted (ads://5.23.91.23.1.1).
Symbol path form
Symbol paths are parsed by
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSymbolPath.cs, which
mirrors IEC 61131-3 structured-text identifiers: global-variable-list
(GVL.Counter), program variable (MAIN.bStart), struct member access
(Motor1.Status.Running), array subscripts (Data[5], Matrix[1,2]), and
bit-access (Flags.0).
BOOL-within-word writes — writing a bit-access path (e.g. MAIN.Flags.3)
is a driver-level parent-word read-modify-write: the driver reads the parent
word as UDInt, flips the target bit, and writes the word back, under a
per-parent lock. The lock serialises the driver's own concurrent bit-writers
against the same word, but cannot guard against the PLC program writing that
word between the driver's read and write — this is an inherent limitation of
any software-level RMW.
Tag discovery
DiscoverAsync always emits the pre-declared Tags as the authoritative config
path, under TwinCAT/{device}/. When EnableControllerBrowse is set, the
driver also walks each device's symbol table and surfaces controller-resident
globals / program locals under a Discovered/ sub-folder; any symbol-loader
error falls back to pre-declared-only so a flaky symbol download never blocks
discovery.
Capability surface
TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs).
| Capability | Path | Notes |
|---|---|---|
IReadable |
ReadAsync → ADS ReadValueAsync |
Per-device client, lazily connected and serialized per device |
IWritable |
WriteAsync → ADS WriteValueAsync |
BOOL-within-word writes (e.g. MAIN.Flags.3) use a driver-level parent-word read-modify-write (read as UDInt, flip bit, write back) under a per-parent lock. Read-only tags return BadNotWritable. |
ITagDiscovery |
DiscoverAsync |
Pre-declared tags + opt-in controller symbol browse |
ISubscribable |
native ADS notifications (default), poll fallback | UseNativeNotifications=true registers device notifications so the PLC pushes changes; false uses the shared PollGroupEngine |
IHostConnectivityProbe |
per-device probe loop | One HostConnectivityStatus per configured device; Running/Stopped transitions raise OnHostStatusChanged |
IPerCallHostResolver |
ResolveHost lookup in the tag map |
Routes each call to the device of the referenced tag; returns an empty-string sentinel when unresolved |
IRediscoverable |
symbol-version-changed callback | A PLC re-download fires OnRediscoveryNeeded so the address space is rebuilt |
Rediscovery on PLC re-download
IRediscoverable is the distinguishing capability. When the ADS client detects
DeviceSymbolVersionInvalid (1809 / 0x0711) — the documented TwinCAT
symbol-version-changed signal, raised when a PLC program is re-downloaded —
every symbol and notification handle is invalidated. The driver raises
OnRediscoveryNeeded with a TwinCAT scope hint so Core rebuilds the address
space rather than treating it as a transient connection error.
Native notifications
By default the driver registers native ADS device notifications: the PLC pushes
value changes on its own cycle, which is strictly better for latency and CPU
than polling. NotificationMaxDelayMs lets TwinCAT coalesce notifications up to
a batching delay for high-churn signals. Set UseNativeNotifications=false for
deployments where the AMS router has notification limits you can't raise — then
the driver falls through to the shared poll engine.
Single-connection-per-device
Each device's ADS client is lazily connected and serialized by a per-device
connect gate, so a concurrent read / write / probe can't race a client
create-or-dispose. Probe-initiated connects use the probe timeout; reads and
writes use the driver-wide Timeout.
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/cover the AMS / symbol-path parsers, the status mapper, and the driver lifecycle via a fake ADS client factory. - Integration fixture — see TwinCAT-Test-Fixture.md for the harness map.
- CLI — Driver.TwinCAT.Cli.md documents the standalone read/write/browse/probe CLI for manual checks.
1-D array support
A TwinCAT tag becomes a 1-D OPC UA array node when its TagConfig JSON carries
"isArray": true and "arrayLength": N (N ≥ 1). The canonical rule:
isArray: true + arrayLength >= 1 → array; isArray: false (any length) → scalar.
Read mechanism — the driver performs an ADS native array symbol read against the
declared SymbolPath. ADS returns the full array value buffer for a symbol that maps to a
TwinCAT array type (e.g. ARRAY [0..9] OF REAL); the driver reads N elements from the
response. When UseNativeNotifications is enabled, ADS also pushes array-value changes in
a single notification payload — no per-element polling.
Unit test coverage — tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ covers
the array symbol read path via the fake ADS client factory. The TwinCAT integration
fixture is down during normal dev.
Live-verify — integration-fixture-gated (requires the TwinCAT Docker fixture / AMS router).
Deferrals — array writes (ADS WriteValueAsync for an array), multi-dimensional
arrays, per-element historization. IRediscoverable still fires and rebuilds array nodes
on PLC re-download (unchanged semantics from scalar nodes).
See Uns.md §Array tags for the cross-driver coverage matrix and the UI authoring flow.
Controller-discovered struct/UDT/FB member variables
When EnableControllerBrowse is set, BrowseSymbolsAsync walks the device's symbol tree
recursively and expands Struct / UDT / Function-Block typed symbols into their atomic
member leaves as individually addressable OPC UA Variables, surfaced under the Discovered/
sub-folder.
What is discovered
- Each symbol of a struct, UDT, or FB type is recursively expanded. The walk follows the ADS
symbol tree's
SubSymbolscollection. - Full instance paths are preserved for every leaf (e.g.
MAIN.Motor.Speed,GVL.Drive1.Status.Running). - The recursion is depth-capped at 8 levels to guard against pathological or circular-like layouts.
- Unsupported leaves (types the driver cannot map to an OPC UA data type) are silently dropped rather than surfaced as error nodes.
Pre-declared Structure-typed tags
Pre-declared Tags with a DataType of Structure are still rejected — the driver cannot
read an opaque struct blob without knowing its member layout. For structs that must be
individually addressed, use EnableControllerBrowse to let the driver discover the member
layout from the controller's symbol table, rather than pre-declaring the container tag.
Member writes
Discovered struct/UDT/FB member writes are deferred in this release. Scalar atomic
members can be written when pre-declared with Writable: true; the controller-browse expansion
path does not yet wire the write channel.
Live caveat — Flat-mode vs VirtualTree-mode browse
The ADS client's BrowseSymbolsAsync may return symbols in Flat mode (a flat list of all
instance paths, with no SubSymbols populated) on some TC3 runtime configurations, in which
case the recursive struct expansion produces no sub-symbols and the walk yields only top-level
symbols. If a live TC3 browse does not populate SubSymbols on struct-typed symbols, the
alternative is VirtualTree mode (which returns a hierarchical symbol tree with SubSymbols
populated). VirtualTree-mode browse is an unverified follow-up; if Flat-mode struct expansion
produces empty Discovered/ sub-folders for struct symbols, switch to VirtualTree mode via the
driver configuration.
Further reading
docs/v2/driver-specs.md §6— full per-field spec and DriverConfig JSON shape- Driver.TwinCAT.Cli.md — standalone TwinCAT driver CLI
- TwinCAT-Test-Fixture.md — test-harness map