Merge TwinCAT fixes ported from ignitionoee + live-validated on the Elma PLCs
This commit is contained in:
+91
-3
@@ -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,74 @@ 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.
|
||||
- **Sizes come from the resolved data type, not the symbol.** On a TwinCAT 2
|
||||
target the Beckhoff library reports a dotted *member* symbol's `ByteSize` as if
|
||||
the PLC's byte count were a bit count (`.OPC_MACHINEDATA.PROCESSSTATUS`
|
||||
`STRING(80)` → 11 B, an `INT` member → 1, a `UDINT` → 1) while its resolved
|
||||
`DataType.ByteSize` is correct (81 / 2 / 4, matching raw `SYM_INFOBYNAMEEX`).
|
||||
String reads therefore address the symbol's index group/offset explicitly at
|
||||
the type's width — sizing them by the symbol truncated `"ready for operation"`
|
||||
to `"ready for o"` at Good.
|
||||
- **A `VAR_IN_OUT` member is a reference, not a value.** TC2 names it
|
||||
`STRING(20) (VAR_IN_OUT)` (4 bytes) but the library resolves its type to the
|
||||
plain `STRING(20)`; the symbol's own name wins and such a tag is refused
|
||||
(`BadTypeMismatch`) instead of publishing the memory at the pointer as text.
|
||||
|
||||
### Live validation (2026-10-07, TwinCAT 2.11 Elma cleaners)
|
||||
|
||||
Read-only, against Z33856 (`10.201.45.236.1.1:801`) and Z33733
|
||||
(`5.25.53.46.1.1:801`) over the VPN, through an in-process
|
||||
`Beckhoff.TwinCAT.Ads.TcpRouter` in a scratch harness: ReadState RUN; browse
|
||||
discovered 45,083 / 42,666 variables incl. the `INT16`/`UINT16` globals
|
||||
(`.ACTUALOPERATIONMODE`, `.ADSSTATE_RUN`) and 3,663 / 3,564 leading-dot names, no
|
||||
empty names; `.ADSSTATE_RUN` = 5 with bits `.0`/`.1`/`.2` = true/false/true;
|
||||
work-order and part-number strings whose PLC buffers carry a stale tail after
|
||||
the NUL (`68261005\0` + `01`) read as `68261005` through both poll and native
|
||||
notifications; `DInt` over a `STRING(80)` → `BadTypeMismatch` in both paths with
|
||||
the rest of the subscription Good. No PLC value held a genuine non-ASCII string
|
||||
to exercise Windows-1252 live — that rule is unit-tested only.
|
||||
|
||||
## Capability surface
|
||||
|
||||
`TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable`
|
||||
@@ -145,8 +228,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
|
||||
|
||||
Reference in New Issue
Block a user