From 75754478accab5768eb0bb304a6d0245c63ae325 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 14:07:35 -0400 Subject: [PATCH] docs(twincat): TC2 leading-dot globals, port 801 / no AddRoute, string + declared-type rules, native arrays refused --- docs/Driver.TwinCAT.Cli.md | 2 +- docs/drivers/TwinCAT.md | 68 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/docs/Driver.TwinCAT.Cli.md b/docs/Driver.TwinCAT.Cli.md index 4633952c..a5cccf3d 100644 --- a/docs/Driver.TwinCAT.Cli.md +++ b/docs/Driver.TwinCAT.Cli.md @@ -124,7 +124,7 @@ UDTs / function-block instances don't appear. | Flag | Default | Purpose | |---|---|---| -| `--prefix` | _(none)_ | Case-sensitive instance-path prefix filter (e.g. `GVL_Fixture`) | +| `--prefix` | _(none)_ | Case-sensitive instance-path prefix filter (e.g. `GVL_Fixture`). A TwinCAT 2 global keeps its leading dot, so filter with it (`.OPC_`) | | `--max` | `500` | Max symbols to print. `0` = unbounded | ```powershell diff --git a/docs/drivers/TwinCAT.md b/docs/drivers/TwinCAT.md index 081f8752..11269c53 100644 --- a/docs/drivers/TwinCAT.md +++ b/docs/drivers/TwinCAT.md @@ -51,7 +51,13 @@ an AMS Net ID + port. 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`). +when omitted (`ads://5.23.91.23.1.1`) — **a TwinCAT 2 target must name `:801`**. + +The driver never registers a route on the PLC: it sends no UDP `AddRoute` and +writes nothing to the PLC's route table. The route from the PLC back to this +host's AMS Net ID (and the AMS router's route to the PLC) is provisioned +out-of-band — the same posture as the ignitionoee driver's +`Register Route on Connect = off` (TwinCAT-009). ### Symbol path form @@ -62,6 +68,15 @@ mirrors IEC 61131-3 structured-text identifiers: global-variable-list (`Motor1.Status.Running`), array subscripts (`Data[5]`, `Matrix[1,2]`), and bit-access (`Flags.0`). +**TwinCAT 2 globals keep their leading dot.** A TC2 global is named with ONE +leading dot (`.OPC_HEARTBEAT`, `.OPC_MACHINEDATA.PROCESSSTATUS`, +`.ARRCARRIERDATA_V[1].SWORKORDER`) and the PLC requires it — without it +`SYM_HNDBYNAME` answers 0x710 (symbol not found), observed live on a TC 2.11 CX. +The parser keeps the dot on the root segment, so bit access on a TC2 global +(`.OPC_STATUS.WORD.3`) still reads the parent word `.OPC_STATUS.WORD`. Exactly one +dot: `..x`, `.` and `.[1]` are rejected. (Before this, the dot made the parser +give up and the raw string went to ADS verbatim, losing bit-index handling.) + **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 @@ -79,6 +94,48 @@ 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. +Discovered variables are named by their full instance path (flat, no dot +splitting), so a TC2 global appears as `.OPC_HEARTBEAT` — with its dot, never +as an empty-named folder. TwinCAT 2 reports many top-level integers by their +ADS base-type name (`INT16`, `UINT16` — 354 symbols on a real TC 2.11 Elma +cleaner) and pads some member type names (`"REAL "`); both map like their IEC +twins, so those symbols are no longer silently dropped. The CLI `browse +--prefix` filter is a case-sensitive `StartsWith` on that name, so filter TC2 +globals with the dot (`--prefix .OPC_`; `OPC_` matches nothing). + +## Strings and declared-type checking + +Ported from the ignitionoee TwinCAT driver (0.19.2–0.19.4), where each was +found against a real TwinCAT 2 PLC: + +- **A `String` / `WString` tag matches a PLC `STRING(n)` / `WSTRING(n)` of any + `n`** and is read at the PLC's own width — `STRING(n)` is n+1 bytes, + `WSTRING(n)` 2(n+1). This also applies to native notifications, which + previously could not register a string at all (the Beckhoff marshaller needs + an explicit length and threw), taking the rest of that subscribe call down + with it. +- **A string ends at its first NUL** (`0x00` for STRING, an aligned `U+0000` + for WSTRING). Bytes after it are stale content from a longer earlier value + (live: work order `67504440` read back with a stale `"\0" + "1"` tail). +- **A STRING decodes as Windows-1252** (umlauts, `°` in alarm texts), or UTF-8 + when the target reports UTF-8 string data (TwinCAT 3.1 4026+ "UTF8 Encoding + for STRING"). It no longer depends on the server's culture — the Beckhoff + marshaller falls back to the host's ANSI code page, which is 1250 on a Polish + host. Writes still go through the Beckhoff marshaller. +- **String-ness is a kind.** A numeric tag over a PLC string, a string tag over + a numeric, `String` over a `WSTRING` (or vice versa), and a string over a + struct/array are refused with `BadTypeMismatch` and a WARN, instead of + publishing the PLC's bytes as the wrong type at Good. +- **Native notifications are additionally width- and float-checked.** A + notification delivers raw bytes decoded as the *declared* type, so `DInt` + over a PLC `REAL` (same width, bit pattern reinterpreted) or `DInt` over + `LINT` (truncated) is refused too. A same-width, same-kind divergence (`DInt` + vs `UDINT`, `Time` vs a 4-byte integer) is tolerated. A refused tag is + isolated: it publishes `BadTypeMismatch`, the rest of the subscription + registers, and the probe-tick retry re-checks it after a PLC program change. + Reads go through the Beckhoff symbol cache, which converts numeric values by + the PLC's own type, so only the string-kind rule applies there. + ## Capability surface `TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable` @@ -145,8 +202,13 @@ A TwinCAT tag becomes a **1-D OPC UA array node** when its `TagConfig` JSON carr **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. +response. + +⚠️ **Native notifications do not carry arrays.** The notification path registers the +declared *scalar* type, so before the declared-type check an array tag subscribed natively +published only its first element at Good. It is now refused with `BadTypeMismatch` (the PLC +symbol is an array, not a scalar). Subscribe array tags with `UseNativeNotifications=false` +(the poll engine uses the array read above) until native array notifications are added. **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