Merge b647-twincat: ADS sum-read (opt-in) + TwinCAT diagnostics on /hosts (#645 #310/#314, #612 docs)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 19:33:22 -04:00
17 changed files with 1463 additions and 49 deletions
+48 -1
View File
@@ -185,9 +185,56 @@ notifications; `DInt` over a `STRING(80)` → `BadTypeMismatch` in both paths wi
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.
## Sum-read and diagnostics (Gitea #645)
Two items ported from the stranded `auto/driver-gaps` branch (#310, #314), re-implemented against the
current raw-byte read path rather than cherry-picked (the branch predates the type guard, the string codec
and the array codec). Unit-tested against the fake client only; the live gate is read-only and pending a
route (see `docs/v3/twincat-backlog.md`).
### Sum-read (opt-in)
`useSumRead: true` (default **off**) batches a poll's reads for one device into ADS sum-read commands
(`IndexGroup 0xF080`) of at most `sumReadMaxItems` symbols (default 200, ceiling 500) and at most 32 KiB of
payload (read lengths + 4 bytes each) instead of one round-trip per tag. TC2 CE 2.11 support for the sum
command is unverified, hence opt-in.
- **Eligibility is strict.** The sum path reads raw bytes at the symbol's index group / offset and decodes them
as the DECLARED type (the notification rules), so a scalar rides the batch only when it passes
`TwinCATTypeGuard.CheckNotification` — same width, same float/integer kind. A tag the lenient single read
accepts by value conversion (`DInt` over a PLC `INT`, `Real` over `LREAL` or `DINT`) is read **individually**
inside the call, keeping the Beckhoff conversion; the batch never publishes reinterpreted bytes at Good.
Arrays use the same raw-bytes rule as on the single path. A type-mismatched item gets `BadTypeMismatch`
without joining the batch; a bit-indexed BOOL and a handle-addressed symbol (`0xF005`) are read singly.
- Per-item ADS error codes map to that tag only (`MapAdsError`); a response shorter than asked is a size error
for that item, not a failed chunk. The overall code is the command's.
- **Latch vs read-singly.** Only overall ADS **1793 / 1794** (service / index group not supported) throws
`TwinCATSumReadRefusedException`; the driver logs once, marks the device `SumReadUnsupported` and reads
singly until re-initialise. **Any other non-comms overall code** (1792 general error, 1795 offset, 1797 size, 1798 data, ...) means
*this request* was rejected — typically too big — so that chunk is read singly for this poll, nothing is
latched, and a Warning is logged once per device so the live gate can see sum-read is not working there. A
comms-level failure (router / port / timeout) is neither: that chunk goes `BadCommunicationError` and the next poll tries again.
- A one-item chunk is just a single read. Sum-**write** is not implemented (writes stay per-tag).
### `/hosts` diagnostics (`IDriverDiagnostics`, opt-in)
Behind `diagnosticsEnabled` (default **off** — not yet live-gated). The driver implements the optional
`IDriverDiagnostics` bag (the S7 `NegotiatedPduSize` precedent, #640), deliberately not synthetic raw tags.
When enabled, after each healthy probe tick **and after the host-state transition** it reads up to four
`TwinCAT_SystemInfoVarList` symbols per device — `_AppInfo.AppName`, `_AppInfo.OnlineChangeCnt`,
`_TaskInfo[1].CycleTime`, `_TaskInfo[1].LastExecTime` (100 ns ticks) — so up to **4 extra read requests per
device per probe interval** (default 5 s). The ADS state is the one `ProbeAsync` already read; no second
ReadState. One device: flat keys `TwinCAT.AdsState`, `.AppName`, `.OnlineChangeCnt`, `.CycleTimeMs`,
`.LastExecTimeMs`, `.HeadroomMs` (cycle − last exec; negative = the task overran; this is spare time, not
jitter). Several devices: one line per device, `TwinCAT.<DeviceName or host>` =
`Run; Plc30 App; online-changes 7; cycle 10.0 ms; exec 2.5 ms; headroom 7.5 ms`, so the host's 16-entry cap
covers 16 devices. Times round to 0.1 ms (the bag's digest is in the publish fingerprint). Each symbol is
best-effort: a runtime that does not export one (older TC2) keeps the others, and a symbol answering "not
found" is not re-asked until a new client is installed (reconnect).
## Capability surface
`TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable`
`TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable, ISkippedTagReporter, IDriverDiagnostics`
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs`).
| Capability | Path | Notes |
+52 -18
View File
@@ -5,27 +5,61 @@ The v2 TwinCAT driver is considered solid: 28 integration tests (14 `[TwinCATFac
three latent driver bugs shaken out (notification cycle units, `STRING(N)` mapper,
bit-indexed BOOL path). Further work is deferred.
Archived from `docs/drivers/TwinCAT-Test-Fixture.md` § Follow-up candidates.
Archived from `docs/drivers/TwinCAT-Test-Fixture.md` § Follow-up candidates. Refreshed 2026-10-08
(Gitea #612 / #645); live-validation results live in `docs/drivers/TwinCAT.md` § "Live validation".
## Deferred items
## Hardware available (as of 2026-10-08)
1. **TC2 coverage** — spin up a TC2 runtime (Windows CE IPC or legacy XAR)
and run the same suite; any delta surfaces. Blocked on hardware.
The Elma cleaner fleet (25 presses, 23 live) is the only real TwinCAT hardware in reach, and it is
**production — strictly read-only**:
2. **Notification coalescing under load** — run the subscribe test while
the PLC cycle is saturated (bump `lineSim` complexity, watch for
dropped notifications). Doable on current rig; deferred as lower
priority than v3 feature work.
| Press | Runtime | AMS | Notes |
|---|---|---|---|
| 22 presses (e.g. Z33856 `10.201.45.236.1.1`, Z33733 `5.25.53.46.1.1`) | **TC2 2.11 on WinCE 6.0** | port **801** | read-validated 2026-10-07 |
| **Z35474** (`5.118.56.93.1.1`, 10.201.23.227) | **TC3 3.1.4024** (Win NT 10) | port **851** | the **only TC3 press**; "Plc30 App" |
| TCBSD fixture `10.100.0.128` (`41.169.163.43.1.1:851`) | TC3 | 851 | not a press; unreachable from the Mac |
3. **Multi-hop AMS route** — add a test behind an IPC gateway with a
chained route entry. Blocked on hardware (gateway IPC).
Access facts: ADS is TCP 48898. The driver sends no AddRoute (routes are provisioned out of band) and has
no IP field, so a host needs a local AMS router — TwinCAT XAR, or `Beckhoff.TwinCAT.Ads.TcpRouter` hosted
in-process (the driver csproj does not reference TcpRouter; the 2026-10-07 run used a scratch harness).
The Mac reaches the presses directly over the VPN. **The PLC routes left by earlier tooling are STALE:**
they name the Mac's old VPN IP (10.181.198.189), and the Mac is now 10.181.207.140, so a source NetId of
`10.181.207.140.1.1` has no route on any press. We must not AddRoute on production, so the **live gate for
this backlog is deferred pending a user-approved route** (owner refreshes the route, or the user approves
reusing the old source NetId). Nothing below that says "live" has been run against TC3.
4. **License-rotation automation** — XAR's 7-day trial expires on
schedule. Either automate `TcActivate.exe /reactivate` via a scheduled
task on the VM (not officially supported; reportedly works for some
TC3 builds), or buy a paid runtime license (~$1k one-time per runtime
per CPU) to kill the rotation. Ops item, not code.
## Status of the original items
5. **Lab rig** — cheapest IPC (CX7000 / CX9020) on a dedicated network;
the only route that covers TC2 + real EtherCAT I/O timing + cycle
jitter under CPU load. Blocked on hardware + budget.
1. **TC2 coverage — DONE (read side).** No longer "blocked on hardware": the 2026-10-07 Elma run
(TwinCAT 2.11) verified browse, scalar/array/string reads and native notifications on TC2. What is
still unverified there: sum-read support (see item 6) and anything that writes.
2. **Notification coalescing under load** — run the subscribe test while the PLC cycle is saturated.
Needs the TCBSD fixture (loading a production press is out of the question) and the `GVL_Perf` /
`FB_PerfChurn` fixture project from the stranded `auto/driver-gaps` branch. Fixture-gated.
3. **Multi-hop AMS route** — a test behind an IPC gateway with a chained route entry. No hardware.
4. **License-rotation automation** — XAR's 7-day trial expires on schedule. Ops item, not code.
5. **Lab rig** — cheapest IPC on a dedicated network; the only route that covers real EtherCAT I/O
timing and cycle jitter under CPU load. Blocked on hardware + budget.
## Added by #612 / #645
6. **ADS sum-read (#310) — code DONE, live gate pending.** Opt-in `useSumRead` / `sumReadMaxItems`
(strict width+kind eligibility; latch only on ADS 1793/1794; see `docs/drivers/TwinCAT.md` § "Sum-read"). Unit-tested against the fake client only. Live gate
(read-only, valid on a press): enable on Z33856 / Z33733 (TC2) and Z35474 (TC3), compare a batched poll
to individual reads value-for-value, and record whether TC2 CE 2.11 answers or refuses the sum command
(either outcome is a result — a refusal must fall back cleanly). Needs the route above.
7. **Driver diagnostics (#314) — code DONE (opt-in `diagnosticsEnabled`, default off), live gate pending.** ADS state, app name, online-change
count, task cycle time, last exec time and headroom on the `/hosts` diagnostics bag. Live gate: confirm
`TwinCAT_SystemInfoVarList._TaskInfo[1]` resolves on TC2 2.11 (the driver tolerates its absence).
8. **Flat vs VirtualTree browse on TC3 (#612)** — open. Only Z35474 is TC3; read-only symbol-table
upload in both modes, compare leaf sets. Add a `SymbolsLoadMode` option only if Flat drops struct
members. Needs the route. (Flat on TC2 already proved fine: 45,083 / 42,666 variables.)
9. **Discovered struct/FB member writes (#612)** — deferred: writes, never on a press; fixture-only.
Likewise **sum-write** was deliberately NOT ported from the stranded branch.