Recover stashed driver-gaps work from pre-v2-mxgw-merge working tree
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>
This commit is contained in:
899
docs/plans/twincat-plan.md
Normal file
899
docs/plans/twincat-plan.md
Normal file
@@ -0,0 +1,899 @@
|
||||
# TwinCAT Driver — Implementation Plan
|
||||
|
||||
> Source of gap analysis: [featuregaps.md → TwinCAT](../featuregaps.md#twincat-beckhoff-ads)
|
||||
>
|
||||
> Covers Build = Yes items only.
|
||||
|
||||
## Summary
|
||||
|
||||
The TwinCAT driver (`src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/`) ships a solid baseline:
|
||||
six capability interfaces over `Beckhoff.TwinCAT.Ads` v6 `AdsClient`, native
|
||||
`AdsTransMode.OnChange` notifications, AMS address parsing, symbol-path parser
|
||||
with multi-dim subscripts, controller-side browse with system-symbol filtering,
|
||||
and a 30-case live integration suite against TCBSD + Hyper-V XAR. Twelve gaps
|
||||
remain rated Build=Yes in `docs/featuregaps.md` and they cluster cleanly into
|
||||
five themes:
|
||||
|
||||
1. **Data-type correctness** — `LInt`/`ULInt` silently truncated to Int32
|
||||
(explicit `// matches Int64 gap` comment in `TwinCATDataType.cs:40`),
|
||||
`TIME`/`DATE`/`DT`/`TOD` marshalled as raw `UDINT` rather than native UA
|
||||
types, `ENUM`/`ALIAS` skipped at browse, bit-indexed BOOL writes throw,
|
||||
multi-dim and whole-array reads not batched.
|
||||
2. **Performance** — every read is a `ReadValueAsync` call with re-resolved
|
||||
symbolic name; no Sum commands, no handle caching. Multi-thousand-tag
|
||||
scans pay symbol resolution + per-tag AMS round-trip cost on every cycle.
|
||||
3. **Operability** — `NotificationSettings(OnChange, cycleMs, 0)` clamps
|
||||
max-delay to zero with no per-tag override; probe loop only checks
|
||||
reachability — no cycle-time / jitter / `_AppInfo` / RT-state telemetry.
|
||||
4. **UDT decomposition** — `Structure` is declared in the enum but discovery
|
||||
skips non-atomic symbols (`AdsTwinCATClient.cs:224`); to expose nested UDT
|
||||
trees we need TMC-file parsing or runtime data-type table introspection.
|
||||
5. **Alarms** — no `IAlarmSource` implementation; TC3 EventLogger / AMS port
|
||||
110 events never surface as OPC UA AC events.
|
||||
|
||||
The plan ships as five phases / 12 PRs. Phases 1-3 are all narrow scope and can
|
||||
land in parallel where dependencies allow. Phase 4 (UDT/TMC) is the largest
|
||||
single piece of work and is called out as such. Phase 5 (alarms) requires
|
||||
investigation up front (Beckhoff TC3 EventLogger NuGet availability — see
|
||||
Open questions).
|
||||
|
||||
Hyper-V conflict gating: live integration runs against the TCBSD VM
|
||||
(`docs/drivers/TwinCAT-Test-Fixture.md`, AmsNetId `41.169.163.43.1.1` at
|
||||
`10.100.0.128`) since the local Hyper-V XAR can't co-exist with Docker
|
||||
Desktop. All wire-level tests gate on `[TwinCATFact]` / `[TwinCATTheory]`
|
||||
and skip cleanly when `TWINCAT_TARGET_NETID` is unset.
|
||||
|
||||
## Phased delivery
|
||||
|
||||
| Phase | Theme | PRs | Sequencing |
|
||||
|---|---|---|---|
|
||||
| 1 | Data-type correctness | 1.1 — 1.5 | Independent; ship in any order |
|
||||
| 2 | Performance — Sum + handles | 2.1 — 2.3 | 2.3 depends on 2.2 |
|
||||
| 3 | Operability — max-delay + diagnostics | 3.1 — 3.2 | Independent |
|
||||
| 4 | UDT decomposition with TMC parsing | 4.1 | Stand-alone; significant scope |
|
||||
| 5 | TC3 EventLogger alarms | 5.1 | Stand-alone; spike first |
|
||||
|
||||
Total: 12 PRs covering the 12 Build=Yes gaps.
|
||||
|
||||
Recommended landing order: **Phase 1 (correctness) → Phase 3 (operability) →
|
||||
Phase 2 (perf) → Phase 5 (alarms) → Phase 4 (UDT)**. Correctness first because
|
||||
it's cheap and removes fixtures' `Skip("Int64 gap")`-style workarounds.
|
||||
Operability before perf because the diagnostics surface created in 3.2 makes it
|
||||
much easier to validate Sum-command throughput claims in 2.1.
|
||||
|
||||
## Per-PR detail
|
||||
|
||||
### Phase 1 — Data-type correctness
|
||||
|
||||
#### PR 1.1 — Int64 fidelity for `LINT` / `ULINT`
|
||||
|
||||
**Scope**: Map `LInt`/`ULInt` to `DriverDataType.Int64` (currently truncates to
|
||||
Int32 per `TwinCATDataType.cs:40` comment "matches Int64 gap"). `MapToClrType`
|
||||
already returns `typeof(long)`/`typeof(ulong)`; the truncation is purely in the
|
||||
`ToDriverDataType` extension.
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDataType.cs` — change line 40 to
|
||||
`=> DriverDataType.Int64;` (drop the gap comment).
|
||||
- Verify `DriverDataType.Int64` exists in `Core.Abstractions` — if not, add it
|
||||
(likely scope creep into `ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs`).
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: none — the wire-level `AdsClient.ReadValueAsync`
|
||||
already returns `long`/`ulong` boxed in `result.Value` when called with
|
||||
`typeof(long)` per `MapToClrType`.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: extend `TwinCATCapabilityTests` — assert `LInt.ToDriverDataType() ==
|
||||
Int64`, `ULInt.ToDriverDataType() == Int64`.
|
||||
- Integration: extend `GVL_Primitives` to include an `LINT` (`nLargeCounter`)
|
||||
seeded with `0x1_0000_0000L` (above Int32 range). Add a `[TwinCATTheory]`
|
||||
case asserting the value round-trips without truncation. May need a new
|
||||
`GVL_Primitives.lLong : LINT` symbol if not already present (the existing
|
||||
16-primitive theory in `TwinCAT3SmokeTests.cs` covers `LInt`/`ULInt` —
|
||||
inspect what value it seeds and tighten the assertion).
|
||||
|
||||
**Effort**: S (half day).
|
||||
**Deps**: none.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` "Data types" table — drop the "marshal as
|
||||
`UDINT` on the wire" caveat for `LInt` / `ULInt` (this PR keeps Int64 fidelity);
|
||||
`docs/drivers/TwinCAT-Test-Fixture.md` "Bugs caught by live runs" gains a 4th
|
||||
entry pinning the truncation regression.
|
||||
- Fixture (TCBSD PLC project): `PLC/GVLs/GVL_Primitives.TcGVL` adds
|
||||
`vLargeCounter : LINT := 16#1_0000_0000` (above Int32 range) + matching
|
||||
`vLargeCounterU : ULINT`; `tests/.../TwinCatProject/README.md` "GVL_Primitives
|
||||
numeric seeds" enumerates the new symbols.
|
||||
- Integration tests: `TwinCAT3SmokeTests.cs` — extend the 16-case
|
||||
`[TwinCATTheory]` to 17/18 cases covering the new LINT/ULINT seeds; assert
|
||||
the value round-trips without truncation.
|
||||
- E2E: no change to `scripts/e2e/test-twincat.ps1` — the bridge script targets
|
||||
a single DINT counter, untouched by Int64 work.
|
||||
|
||||
#### PR 1.2 — TIME / DATE / DT / TOD as native UA types
|
||||
|
||||
**Scope**: Stop marshalling `TIME` / `DATE` / `DT` / `TOD` as raw `UDINT`
|
||||
(`AdsTwinCATClient.cs:278-280`). Map according to IEC 61131-3 semantics:
|
||||
|
||||
- `TIME` (ms duration) → `DriverDataType.Duration` (UA `Double` seconds, or
|
||||
add `Duration` to `DriverDataType` if missing).
|
||||
- `DATE` (days since 1970-01-01) → `DriverDataType.DateTime` (midnight UTC).
|
||||
- `DT` (seconds since 1970-01-01) → `DriverDataType.DateTime`.
|
||||
- `TOD` (ms since midnight) → `DriverDataType.DateTime` (today's date +
|
||||
offset) or a dedicated `TimeOfDay` type if the abstraction supports it.
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDataType.cs` — update
|
||||
`ToDriverDataType` mapping for the four IEC time types.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — `MapToClrType`
|
||||
returns the raw UDINT today; keep that for the wire read but post-process
|
||||
inside `ReadValueAsync` / `ConvertForWrite` to convert UDINT ↔ `DateTime` /
|
||||
`TimeSpan`. Symmetrical change in `OnAdsNotificationEx` so subscriptions see
|
||||
the same shape.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: still `AdsClient.ReadValueAsync(symbol,
|
||||
typeof(uint), ct)`. Beckhoff exposes `PlcOpenDate` / `PlcOpenTimeOfDay` etc.
|
||||
in `TwinCAT.Ads.TypeSystem` — using those types directly would simplify
|
||||
conversion but tightens our coupling. Investigate during PR.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: round-trip helpers UDINT-since-epoch ↔ `DateTime` for each variant.
|
||||
- Integration: add `GVL_Primitives.dCurrentTime : DT` seeded with a known
|
||||
literal (e.g. `DT#2026-01-15-12:00:00`); assert the driver returns a
|
||||
`DateTime` matching that instant within 1 s.
|
||||
|
||||
**Effort**: M (1-2 days).
|
||||
**Deps**: none. May expose missing `Duration` in `DriverDataType` enum.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` "Data types" section — replace the
|
||||
"marshal as `UDINT` on the wire — CLI takes a numeric raw value" paragraph
|
||||
with native syntax (e.g. `read -t DateTime` returns ISO-8601, `write -t Time
|
||||
-v 00:00:01.500` for IEC TIME duration). New examples for each of the four
|
||||
IEC time types under `read` / `write`.
|
||||
- Fixture (TCBSD PLC project): `PLC/GVLs/GVL_Primitives.TcGVL` adds
|
||||
`dCurrentTime : DT := DT#2026-01-15-12:00:00`, `tCycleDuration : TIME :=
|
||||
T#1500ms`, `dToday : DATE := DATE#2026-04-25`, `tShiftStart : TOD :=
|
||||
TOD#06:30:00`. Existing primitives theory in
|
||||
`tests/.../TwinCatProject/README.md` § "Type coverage" gets the seed values
|
||||
documented.
|
||||
- Integration tests: `TwinCAT3SmokeTests.cs` — new
|
||||
`Driver_round_trips_TIME_DATE_DT_TOD_as_native_UA_types` `[TwinCATFact]`
|
||||
reading each variable and asserting the CLR shape (`TimeSpan` / `DateTime`).
|
||||
Update the existing 16-case primitive `[TwinCATTheory]` to assert native
|
||||
types instead of raw `UDINT` for these four entries.
|
||||
- E2E: `scripts/e2e/test-twincat.ps1` unchanged for now (single DINT bridge);
|
||||
follow-up could add a DT-typed bridge node but it's not on the critical path.
|
||||
|
||||
#### PR 1.3 — Bit-indexed BOOL writes (read-modify-write)
|
||||
|
||||
**Scope**: Replace the `NotSupportedException` at `AdsTwinCATClient.cs:99-100`
|
||||
with a read-modify-write sequence: read parent word as `uint`, set/clear bit,
|
||||
write the word back. Must serialize against concurrent writes to the same
|
||||
parent word — a single `SemaphoreSlim` keyed on parent symbol path is
|
||||
sufficient (concurrency on bit writes within the same parent is rare and the
|
||||
PLC cycle is the natural lower bound on contention anyway).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — replace `throw`
|
||||
branch in `WriteValueAsync` with RMW logic mirroring `ReadValueAsync`'s
|
||||
bit-index path. Add `ConcurrentDictionary<string, SemaphoreSlim>
|
||||
_bitWriteLocks` keyed on parent symbol.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: `AdsClient.ReadValueAsync(parent, typeof(uint))`
|
||||
+ `AdsClient.WriteValueAsync(parent, modifiedWord)`. Both already used.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: extend `TwinCATReadWriteTests` with a `FakeTwinCATClient` test
|
||||
covering set + clear of bits 0, 7, 15, 31 of a `uint` parent.
|
||||
- Integration: add a new `[TwinCATFact]` —
|
||||
`Driver_round_trips_bit_indexed_BOOL_write_and_read` against
|
||||
`GVL_Primitives.vWord.4` (the `0xBEEF` word's bit-4); flip to true, read
|
||||
back as true, flip to false, read back as false.
|
||||
|
||||
**Effort**: S-M (1 day).
|
||||
**Deps**: none. Closes task #181 referenced in the existing `NotSupported`
|
||||
exception message.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` `write` section — add an example
|
||||
`otopcua-twincat-cli write -n ... -s "GVL_Primitives.vWord.4" -t Bool -v
|
||||
true` and a note explaining the RMW semantics + concurrency caveat (parent
|
||||
word is locked per write — concurrent bit writes on the same word
|
||||
serialize). `docs/drivers/TwinCAT-Test-Fixture.md` "Bugs caught by live
|
||||
runs" updates entry #3 to note that writes now also work (read previously
|
||||
shipped; write was the gap).
|
||||
- Fixture (TCBSD PLC project): no schema change required —
|
||||
`GVL_Primitives.vWord` already exists with seed `0xBEEF`. Tests use bits 4
|
||||
(clear) and 7 (set) to round-trip.
|
||||
- Integration tests: `TwinCAT3SmokeTests.cs` — new
|
||||
`Driver_round_trips_bit_indexed_BOOL_write_and_read` `[TwinCATFact]`. Unit
|
||||
tests in `TwinCATReadWriteTests` extended via `FakeTwinCATClient` for bits
|
||||
0/7/15/31 of a `uint` parent.
|
||||
- E2E: no change.
|
||||
|
||||
#### PR 1.4 — Multi-dim and whole-array reads
|
||||
|
||||
**Scope**: Expand `ReadValueAsync` / `WriteValueAsync` to handle whole-array
|
||||
reads via Beckhoff's array marshalling, instead of element-by-element. The
|
||||
symbol-path parser already produces `TwinCATSymbolSegment.Subscripts` with N
|
||||
dims; today the driver only reads single elements (one path per request).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — when a tag
|
||||
declares `IsArray=true` (extend `TwinCATTagDefinition`), use
|
||||
`AdsClient.ReadValueAsync(symbol, typeof(int[]))` / `typeof(double[,])` etc.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — surface
|
||||
`IsArray` + `ArrayDim` through `DriverAttributeInfo` in `DiscoverAsync`.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATTagDefinition.cs` (if exists,
|
||||
in `TwinCATDriverOptions.cs`) — add `bool IsArray`, `int[]? ArrayDimensions`.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: `AdsClient.ReadValueAsync(symbol, Type, ct)`
|
||||
accepts CLR array types. For dynamically-sized reads use
|
||||
`AdsClient.ReadAnyAsync<T[]>(...)` or pass `Array.CreateInstance(elemType,
|
||||
dims)`. SymbolLoader yields a `Symbol.Category == DataTypeCategory.Array` we
|
||||
can inspect to autoderive dimensions during discovery.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: parse `Matrix[1,2]` and verify ranking / dimension flow into the
|
||||
request shape via `FakeTwinCATClient`.
|
||||
- Integration: extend `GVL_Arrays` with a 5x5 `aReal2D : ARRAY [1..5, 1..5]
|
||||
OF REAL`; new `[TwinCATFact]` reads the whole array in one call and
|
||||
verifies element count + values.
|
||||
|
||||
**Effort**: M (2-3 days).
|
||||
**Deps**: none. Sets up the array-shape plumbing the rest of the driver
|
||||
needs anyway.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` `read` section — add whole-array example
|
||||
(`read -s "GVL_Arrays.aReal2D"` returns the full matrix as JSON) plus a
|
||||
dedicated "Arrays" sub-section calling out 1-D / N-D / array-of-struct
|
||||
semantics. `docs/drivers/TwinCAT-Test-Fixture.md` "What it actually covers"
|
||||
list adds the whole-array bullet.
|
||||
- Fixture (TCBSD PLC project): `PLC/GVLs/GVL_Arrays.TcGVL` already declares
|
||||
`ARRAY[1..4,1..4] OF REAL` per `TwinCatProject/README.md` § "Array
|
||||
coverage". This PR adds a 5x5 `aReal2D : ARRAY [1..5, 1..5] OF REAL`
|
||||
initialised with a deterministic pattern (e.g. `(i-1)*5 + (j-1)`) so the
|
||||
whole-array test can assert each element. README "Array coverage" gets the
|
||||
new symbol.
|
||||
- Integration tests: `TwinCAT3SmokeTests.cs` — new
|
||||
`Driver_reads_whole_2D_array_in_one_call` `[TwinCATFact]`. Unit tests
|
||||
extend `TwinCATSymbolPathTests` for multi-dim subscript shape.
|
||||
- E2E: no change to `scripts/e2e/test-twincat.ps1` (scalar bridge); a future
|
||||
array-bridge scenario is captured in the consolidated section below.
|
||||
|
||||
#### PR 1.5 — ENUM and ALIAS at discovery
|
||||
|
||||
**Scope**: `MapSymbolTypeName` returns `null` for any non-atomic type
|
||||
(`AdsTwinCATClient.cs:224`), so ENUM and ALIAS symbols are silently dropped
|
||||
during browse. ENUM is essentially a sized-integer with named members; ALIAS
|
||||
is a renamed atomic. Both are extremely common in real projects (motor states,
|
||||
recipe-step IDs, bit-flag groups).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` —
|
||||
`MapSymbolTypeName` keyed only on the type name today; switch to inspecting
|
||||
`symbol.DataType` + `symbol.Category` from `TwinCAT.TypeSystem`. For
|
||||
`DataTypeCategory.Enum` walk `EnumType.EnumValues` and pick the underlying
|
||||
base type. For `DataTypeCategory.Alias` resolve `AliasType.BaseType`
|
||||
recursively until atomic.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/BrowseSymbolsAsync` —
|
||||
surface enum members so the OPC UA layer can later emit them as
|
||||
EnumStrings.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: `TwinCAT.Ads.TypeSystem.SymbolLoaderFactory`
|
||||
already returns full `IDataType` objects with `Category`, `EnumType`,
|
||||
`AliasType`, etc. No new APIs.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: extend `TwinCATSymbolBrowserTests` — fake an enum symbol via
|
||||
`FakeTwinCATClient`; assert it browses with the underlying base type.
|
||||
- Integration: add `E_LineState : (Idle, Running, Faulted)` + a GVL instance
|
||||
variable; new `[TwinCATFact]` browses + reads it as `Int16` (or whatever
|
||||
the underlying type is).
|
||||
|
||||
**Effort**: M (1-2 days).
|
||||
**Deps**: none. POINTER / REFERENCE / INTERFACE / UNION are explicitly
|
||||
out-of-scope for this PR — they need real-world demand and a much larger
|
||||
type-system rework. ENUM and ALIAS are the 80% case.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` `browse` section — note that ENUM and
|
||||
ALIAS symbols now appear in the output (previously dropped); add a Data
|
||||
types row for "Enum (surfaced as underlying integer with EnumStrings)"
|
||||
and "Alias (resolved to base atomic)". `docs/drivers/TwinCAT-Test-
|
||||
Fixture.md` "What it actually covers" extends with the enum/alias bullet.
|
||||
- Fixture (TCBSD PLC project): `PLC/DUTs/E_AxisState.TcDUT` and
|
||||
`E_Severity.TcDUT` already exist; `PLC/DUTs/T_Temperature.TcDUT` and
|
||||
`T_MeterPerSec.TcDUT` already exist. `PLC/GVLs/GVL_Enums.TcGVL` already
|
||||
exposes them at the root per `TwinCatProject/README.md` § "Enum + alias
|
||||
coverage" — no fixture change needed for this PR. README's "Integration-
|
||||
test contract" gets a new entry for `GVL_Enums.currentSeverity` /
|
||||
`currentTemperature` so the new browse assertion has a stable target.
|
||||
- Integration tests: `TwinCAT3SmokeTests.cs` — new
|
||||
`Driver_browses_enums_and_aliases_with_resolved_base_types` `[TwinCATFact]`
|
||||
asserting the four `GVL_Enums` symbols surface with the correct underlying
|
||||
CLR type (`Int32` for E_AxisState, `Int16` for E_Severity, `Double` for
|
||||
the LREAL aliases).
|
||||
- E2E: no change.
|
||||
|
||||
### Phase 2 — Performance (Sum commands + handle caching)
|
||||
|
||||
#### PR 2.1 — ADS Sum-read / Sum-write
|
||||
|
||||
**Scope**: Today `ReadAsync` loops over `fullReferences` issuing one
|
||||
`ReadValueAsync` per tag (`TwinCATDriver.cs:118-156`). Beckhoff's ADS Sum
|
||||
commands (`IndexGroup=0xF080..0xF084`) batch N reads/writes into a single AMS
|
||||
request. `Beckhoff.TwinCAT.Ads` v6 exposes this via
|
||||
`AdsClient.ReadWriteAsync` with `SumCommand` request envelopes —
|
||||
specifically `SumSymbolRead` / `SumSymbolWrite` from
|
||||
`TwinCAT.Ads.SumCommand`. ~10x throughput on multi-thousand-tag scans
|
||||
according to Beckhoff InfoSys.
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — new
|
||||
`ReadValuesAsync(IReadOnlyList<(string symbol, Type clrType)>, ct)` returning
|
||||
a parallel array of `(value, status)`.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::ReadAsync` — bucket
|
||||
`fullReferences` by `DeviceHostAddress`, call the new client method per
|
||||
bucket. `bitIndex` handling stays per-tag (RMW post-step).
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ITwinCATClient.cs` — add the
|
||||
bulk-read / bulk-write surface.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**:
|
||||
- `AdsClient.ReadWriteAsync(IndexGroup=0xF080, IndexOffset=count, ...)` for
|
||||
raw sum-read by handle.
|
||||
- Higher-level: `TwinCAT.Ads.SumCommand.SumSymbolRead(client, symbols)` /
|
||||
`SumSymbolWrite(client, symbols, values)` in v6. Verify the exact namespace
|
||||
during PR — Beckhoff sometimes re-shuffles between minor versions.
|
||||
- For symbolic (no handle) batching: `SumSymbolReadByName`.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: `FakeTwinCATClient.ReadValuesAsync` fakes the bulk surface; test
|
||||
ordering preservation, partial-failure mapping, empty-input handling.
|
||||
- Integration: `[TwinCATFact]` reads 100 declared tags in one call, asserts
|
||||
value parity with 100 single-call equivalents and measures wall-clock
|
||||
difference (assert under 50% of the loop baseline).
|
||||
|
||||
**Effort**: M-L (3 days).
|
||||
**Deps**: none (handle caching in 2.2 amplifies the win but isn't required).
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/v3/twincat-backlog.md` perf note moves out (Sum-commands no
|
||||
longer deferred) — add a closed-out bullet pointing at this PR. New
|
||||
performance section in `docs/drivers/TwinCAT-Test-Fixture.md` documenting
|
||||
the throughput baseline + Sum-command delta. `docs/Driver.TwinCAT.Cli.md`
|
||||
doesn't expose Sum directly to the user — the CLI still drives one symbol
|
||||
per call — so no CLI doc change.
|
||||
- Fixture (TCBSD PLC project, primary fixture-extension surface): add a new
|
||||
`PLC/GVLs/GVL_Perf.TcGVL` declaring `aTags : ARRAY[1..1000] OF DINT` plus
|
||||
a `MAIN` rung (or new `FB_PerfChurn` POU) that increments each element on
|
||||
a rotating subset. `TwinCatProject/README.md` § "Required project state"
|
||||
gains a "Performance scenarios" subsection documenting the 1000-tag GVL.
|
||||
- Integration tests: new perf test
|
||||
`Driver_sum_read_1000_tags_beats_loop_baseline_by_5x` (`[TwinCATFact]`,
|
||||
perf-tier — guarded behind a separate `TWINCAT_PERF=1` env flag so CI
|
||||
noise from VM jitter doesn't flap the suite). Unit tests cover ordering,
|
||||
partial-failure mapping, empty-input via `FakeTwinCATClient.ReadValuesAsync`.
|
||||
- E2E: `scripts/e2e/test-twincat.ps1` unchanged for the canonical bridge;
|
||||
perf scripts live alongside as a separate `scripts/perf/twincat-sum.ps1`
|
||||
if/when introduced (deferred — integration test is sufficient).
|
||||
|
||||
#### PR 2.2 — Handle-based access with caching
|
||||
|
||||
**Scope**: Cache `AdsClient.CreateVariableHandleAsync` results so per-read
|
||||
overhead drops from "resolve symbolic name + read by name" to "read by handle"
|
||||
— smaller AMS payloads, no name resolution on each call. Cache lifetime is
|
||||
process-scoped; eviction is via the PR 2.3 invalidation listener. Until 2.3
|
||||
ships the cache must be cleared on `AdsClient` reconnect (the existing
|
||||
auto-reconnect path in `EnsureConnectedAsync`).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — add
|
||||
`ConcurrentDictionary<string, uint> _handleCache`. Wrap reads/writes through
|
||||
`EnsureHandleAsync(symbolPath)` that hits the cache or calls
|
||||
`CreateVariableHandleAsync`. On `AdsErrorCode.DeviceSymbolVersionInvalid`
|
||||
(0x710 / 1808) evict the entry and retry once.
|
||||
- Dispose path: `DeleteVariableHandleAsync` for every cached handle on
|
||||
`AdsClient.Dispose` to be a good citizen with the runtime.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**:
|
||||
- `AdsClient.CreateVariableHandleAsync(string symbol, ct)` → returns
|
||||
`ResultHandle` with `.Handle` (uint).
|
||||
- `AdsClient.ReadAnyAsync<T>(IndexGroup=0xF005, IndexOffset=handle, ct)`
|
||||
reads by handle.
|
||||
- `AdsClient.WriteAnyAsync(IndexGroup=0xF005, IndexOffset=handle, value, ct)`.
|
||||
- `AdsClient.DeleteVariableHandleAsync(uint handle, ct)`.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: `FakeTwinCATClient` records handle-create / read-by-handle calls;
|
||||
test asserts second read of same symbol uses cached handle (zero new
|
||||
creates).
|
||||
- Integration: subscribe + read 50 tags, capture AMS round-trips via probe
|
||||
counter, assert the second pass uses ~50% of the bytes (handle = 4 bytes
|
||||
vs symbol path = N bytes).
|
||||
|
||||
**Effort**: M (2 days).
|
||||
**Deps**: combines with PR 2.1 for sum-read-by-handle (highest perf path).
|
||||
Without 2.3, handles can go stale after an online change — call out the
|
||||
caveat in driver options and add a manual `FlushOptionalCachesAsync` invocation
|
||||
that wipes the handle cache.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/drivers/TwinCAT-Test-Fixture.md` perf section gets a paragraph
|
||||
noting that handles drop AMS payload size for repeated reads (4 bytes vs.
|
||||
N-byte symbol path); call out the staleness caveat (online-change
|
||||
invalidation lands in 2.3). `docs/Driver.TwinCAT.Cli.md` adds a brief note
|
||||
in the `subscribe` / `read` sections that handles are cached transparently
|
||||
— no user-visible flag.
|
||||
- Fixture (TCBSD PLC project): no change required — handle caching is
|
||||
observable via byte-counter on the wire, not via PLC-side state. The
|
||||
perf-scenario `GVL_Perf.aTags` from PR 2.1 doubles as the exercise target.
|
||||
- Integration tests: new
|
||||
`Driver_handle_cache_avoids_repeat_symbol_resolution` `[TwinCATFact]`
|
||||
reads the same 50 symbols twice; asserts second pass uses cached handles
|
||||
(probed via diagnostics counters from PR 3.2 if shipped, otherwise via a
|
||||
test-only hook on `AdsTwinCATClient`). Unit tests on
|
||||
`FakeTwinCATClient.HandleCacheTests` assert second read of same symbol
|
||||
triggers zero new handle creates.
|
||||
- E2E: no change.
|
||||
|
||||
#### PR 2.3 — Symbol-version invalidation listener
|
||||
|
||||
**Scope**: TwinCAT publishes a "symbol table version changed" notification on
|
||||
ADS Index Group `ADSIGRP_SYMVAL_BYHND` (or rather, version bumps land via
|
||||
`SystemServiceLoadFile` style notifications + `SymbolVersion` reads). When the
|
||||
PLC takes an online change, all cached handles are silently invalidated; the
|
||||
next read returns `DeviceSymbolVersionInvalid` if you're lucky and a wrong
|
||||
value if you're not. We register a notification on the symbol-version index
|
||||
and wipe the handle cache on bump.
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` — on connect,
|
||||
call `AddDeviceNotificationAsync(ADSIGRP_SYM_VERSION, 0, length=1, ...)`
|
||||
with `AdsTransMode.OnChange`. On callback, clear `_handleCache` + log.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**:
|
||||
- `AdsClient.AddDeviceNotificationAsync(uint indexGroup, uint indexOffset,
|
||||
int length, NotificationSettings, object userData, ct)` — the raw,
|
||||
index-group-based variant (not the symbol-name `Ex` variant we use today).
|
||||
- Index group: `AdsReservedIndexGroup.SymbolVersion` (0xF008). One byte
|
||||
payload that's the current symbol-version counter. Confirm during PR — open
|
||||
question (c) below.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: extend `TwinCATNativeNotificationTests` — `FakeTwinCATClient` exposes
|
||||
a `FireSymbolVersionChange()` method; test asserts handle cache is cleared
|
||||
and subsequent reads recreate handles.
|
||||
- Integration: `[TwinCATFact]` triggers an online change on the TCBSD project
|
||||
(rebuild a GVL with one new variable + login activate) — needs a project
|
||||
helper that automates the online-change. May ship behind a manual gate
|
||||
(`[TwinCATFact(Reason="requires-manual-online-change")]`) initially.
|
||||
|
||||
**Effort**: M (2 days).
|
||||
**Deps**: PR 2.2 (no point invalidating an empty cache). Confirm
|
||||
`SymbolVersion` index-group constant in `Beckhoff.TwinCAT.Ads` v6 — open
|
||||
question (c) below.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/drivers/TwinCAT-Test-Fixture.md` section on "What it does NOT
|
||||
cover" — drop the implicit "online-change handling" gap. New paragraph in
|
||||
the perf section noting handle cache is now self-invalidating.
|
||||
`docs/Driver.TwinCAT.Cli.md` no change (transparent to CLI user).
|
||||
- Fixture (TCBSD PLC project): no schema change. Operator workflow gains an
|
||||
online-change drill — `TwinCatProject/README.md` adds a § "Online-change
|
||||
test scenario" describing the steps (open project, add a dummy variable
|
||||
to `GVL_Perf`, "Login + Activate" → triggers the symbol-version bump).
|
||||
This is the manual gate for the integration assertion.
|
||||
- Integration tests: new `Driver_invalidates_handle_cache_on_symbol_version_bump`
|
||||
`[TwinCATFact]` — initially gated `[TwinCATFact(Reason="requires-manual-online-change")]`
|
||||
until automation lands. Unit tests cover the callback path via
|
||||
`FakeTwinCATClient.FireSymbolVersionChange()`.
|
||||
- E2E: no change.
|
||||
|
||||
### Phase 3 — Operability
|
||||
|
||||
#### PR 3.1 — Per-tag MaxDelay tuning
|
||||
|
||||
**Scope**: Today `NotificationSettings` is hard-coded as `(OnChange, cycleMs,
|
||||
0)` (`AdsTwinCATClient.cs:144-145`). MaxDelay=0 means "fire as soon as the
|
||||
change is detected, no coalescing"; for bursty high-frequency signals this
|
||||
floods the OPC UA subscription queue. Surface MaxDelay as a per-tag option
|
||||
(default 0 to preserve current behavior).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverOptions.cs` — add
|
||||
`int? MaxDelayMs` to `TwinCATTagDefinition`.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::SubscribeAsync` —
|
||||
pass through to client.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs::AddNotificationAsync`
|
||||
— accept `int maxDelayMs`, plumb into `NotificationSettings(...,
|
||||
cycleMs, maxDelayMs)`.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: `NotificationSettings(AdsTransMode mode, int
|
||||
cycleTime, int maxDelay)` — both args in milliseconds per Beckhoff InfoSys
|
||||
`tcadsnetref/7313319051`.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: extend `TwinCATNativeNotificationTests` — assert the plumbed
|
||||
`maxDelayMs` lands on `NotificationSettings`.
|
||||
- Integration: subscribe to `GVL_Fixture.nCounter` with `MaxDelayMs=500`;
|
||||
assert delivery rate is ≤ 2 Hz even when PLC cycle is 10 ms.
|
||||
|
||||
**Effort**: S (half day).
|
||||
**Deps**: none.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: `docs/Driver.TwinCAT.Cli.md` `subscribe` flag table — add `--max-delay-ms`
|
||||
with default `0` and a note that nonzero coalesces high-frequency PLC
|
||||
signals. Update the description of `-i` / `--interval-ms` to disambiguate
|
||||
cycle vs. max-delay (both pass through to `NotificationSettings`).
|
||||
`docs/drivers/TwinCAT-Test-Fixture.md` "Notification coalescing under
|
||||
jitter" caveat — noting per-tag MaxDelay is now configurable.
|
||||
- Fixture (TCBSD PLC project): no change required — `GVL_Fixture.nCounter`
|
||||
already increments on every 10 ms cycle (see `MAIN.TcPOU`), so the test
|
||||
can drive a 100 Hz change rate and verify ≤ 2 Hz delivery with
|
||||
`MaxDelayMs=500`. README "Required project state" gets a one-line note
|
||||
that the counter doubles as the coalescing-test driver.
|
||||
- Integration tests: new `Driver_coalesces_notifications_at_max_delay`
|
||||
`[TwinCATFact]` subscribes to `GVL_Fixture.nCounter` with `MaxDelayMs=500`
|
||||
and asserts delivered-event count ≤ 3 over a 1 s window.
|
||||
- E2E: `scripts/e2e/test-twincat.ps1` `Test-SubscribeSeesChange` is a
|
||||
one-shot subscribe; no change. A future high-rate variant could test
|
||||
coalescing end-to-end through the OPC UA bridge but it's not on the
|
||||
critical path.
|
||||
|
||||
#### PR 3.2 — Cycle-time / jitter / PLC-state diagnostics
|
||||
|
||||
**Scope**: Probe loop today only checks reachability via `ReadStateAsync`
|
||||
(`TwinCATDriver.cs::ProbeLoopAsync`). Surface cycle-time, jitter, and online-
|
||||
change counter as health signals via the standard `_AppInfo` /
|
||||
`TwinCAT_SystemInfoVarList._AppInfo` GVL (the same one we filter out of
|
||||
discovery). Specifically:
|
||||
|
||||
- `_AppInfo.OnlineChangeCnt` (UDINT) — incremented on every online change.
|
||||
- `_AppInfo.AppName` (STRING) — TC project name, useful for
|
||||
cross-instance identification.
|
||||
- `_TaskInfo[1].CycleTime` (UDINT, 100 ns units) — the configured PLC cycle.
|
||||
- `_TaskInfo[1].LastExecTime` (UDINT, 100 ns units) — most recent measured
|
||||
cycle execution; jitter is the delta against `CycleTime`.
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::ProbeLoopAsync` —
|
||||
augment success path to also read these four symbols. Surface via a new
|
||||
`TwinCATDeviceDiagnostics` record on `DeviceState`. Emit through
|
||||
`IDriverDiagnostics` (the cross-driver diagnostics surface introduced for
|
||||
Modbus prohibition events — task #154).
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs` — leave
|
||||
the filter as-is for the user-visible browse; the probe path reads system
|
||||
symbols directly without going through discovery.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: still `AdsClient.ReadValueAsync(symbol, type,
|
||||
ct)`. The symbols are read by name, not by index group, so no new API.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: `FakeTwinCATClient` exposes `SetSystemSymbolValue(string name, object
|
||||
value)` so tests can drive the diagnostics surface deterministically.
|
||||
- Integration: `[TwinCATFact]` connects to TCBSD, asserts the diagnostics
|
||||
block populates `CycleTimeMs > 0` and `OnlineChangeCnt >= 0` within one
|
||||
probe interval.
|
||||
|
||||
**Effort**: M (1-2 days).
|
||||
**Deps**: confirm `IDriverDiagnostics` shape from existing Modbus diagnostics
|
||||
RPC (task #154 in MEMORY); it should be reusable.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: new section "Diagnostics" in `docs/drivers/TwinCAT-Test-Fixture.md`
|
||||
documenting the four exposed signals (cycle time, jitter, online-change
|
||||
counter, app name) and where they surface in the cross-driver
|
||||
diagnostics RPC. `docs/Driver.TwinCAT.Cli.md` `probe` section gains a
|
||||
"Health probe" sub-section noting the same symbols can be read directly
|
||||
via `probe -s "TwinCAT_SystemInfoVarList._AppInfo.OnlineChangeCnt"`
|
||||
(the existing example) plus the new `_TaskInfo[1].CycleTime` /
|
||||
`LastExecTime`. Add `docs/v3/twincat-backlog.md` cross-link confirming
|
||||
cycle-time/jitter no longer deferred.
|
||||
- Fixture (TCBSD PLC project): no change required — `_AppInfo` and
|
||||
`_TaskInfo[1]` are TwinCAT system GVLs, present on every runtime. The
|
||||
`TwinCATSystemSymbolFilter` already drops them from user browse;
|
||||
`TwinCatProject/README.md` adds a one-line "These symbols are read by
|
||||
the probe loop, not project-defined" callout.
|
||||
- Integration tests: new `Probe_loop_surfaces_cycle_time_and_online_change_count`
|
||||
`[TwinCATFact]` asserts the diagnostics record populates within one
|
||||
probe interval against TCBSD. Unit tests via `FakeTwinCATClient.SetSystemSymbolValue`
|
||||
drive the diagnostics surface deterministically.
|
||||
- E2E: no change. Future enhancement could expose driver diagnostics via a
|
||||
CLI subcommand (`otopcua-twincat-cli diagnostics -n ...`) — captured in
|
||||
the consolidated section below as a follow-up.
|
||||
|
||||
### Phase 4 — UDT decomposition with TMC parsing
|
||||
|
||||
#### PR 4.1 — Nested UDT browse via TMC parsing
|
||||
|
||||
**Scope**: Largest single piece of work in the plan. `TwinCATDataType.Structure`
|
||||
exists but `BrowseSymbolsAsync` skips non-atomic symbols
|
||||
(`AdsTwinCATClient.cs:224`); to expose nested UDT trees we either:
|
||||
|
||||
1. **Online**: walk the `IDataType` tree returned by `SymbolLoaderFactory` —
|
||||
each `IStructType` exposes `SubItems` recursively. This is what
|
||||
`Beckhoff.TwinCAT.Ads` v6's TypeSystem already gives us at runtime; we just
|
||||
never recursed.
|
||||
2. **Offline (TMC file)**: parse the TwinCAT Module Class XML file the project
|
||||
compiles to (`*.tmc`), build a type catalogue, drive discovery from it
|
||||
without requiring a live runtime.
|
||||
|
||||
We ship the **online** path first (PR 4.1) because it covers 100% of the case
|
||||
where the runtime is reachable, and `SymbolLoaderFactory` already does the
|
||||
heavy lifting. TMC offline parsing is deferred to a hypothetical PR 4.2 if a
|
||||
disconnected-discovery use case emerges (unlikely; live integration tests
|
||||
demonstrate runtime is always available in our deployments).
|
||||
|
||||
**Files**:
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs` —
|
||||
`BrowseSymbolsAsync` recurses into `IStructType.SubItems`, yielding one
|
||||
`TwinCATDiscoveredSymbol` per leaf with the dotted instance path
|
||||
(`MyStruct.Inner.Field`). For arrays-of-structs, expand element-by-element
|
||||
up to a configurable bound (default 1024) — beyond that, expose only the
|
||||
array root with `IsArray=true`.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::DiscoverAsync` —
|
||||
fold the recursed structure into the existing `Discovered/` folder tree
|
||||
using `IAddressSpaceBuilder.Folder` for each struct member.
|
||||
- New: `TwinCATTypeWalker.cs` — pure helper that takes an `IDataType` and
|
||||
yields `(instancePath, atomicType, readOnly)` tuples. Unit-testable without
|
||||
touching `AdsClient`.
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**:
|
||||
- `TwinCAT.TypeSystem.IStructType` — `SubItems` (collection of
|
||||
`IMember`); each member has `BaseType`, `Name`, `Offset`.
|
||||
- `TwinCAT.TypeSystem.IArrayType` — `Dimensions`, `BaseType`.
|
||||
- `TwinCAT.TypeSystem.IEnumType` — handled in PR 1.5 (atomic surface).
|
||||
- `TwinCAT.TypeSystem.IAliasType.BaseType` — recurse until atomic.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: new `TwinCATTypeWalkerTests` — feed synthetic `IDataType` trees,
|
||||
assert the flattened paths and types.
|
||||
- Integration: extend `GVL_Plant` (already has `Line1.Stations[1].Axes[1].Motor`
|
||||
per `TwinCAT3SmokeTests.cs`) — the existing `Driver_reads_deeply_nested_UDT_path`
|
||||
test reads a known-leaf path; add a new test that browses into the same
|
||||
GVL and asserts the entire tree shape matches expectation. Should yield
|
||||
~50+ leaves.
|
||||
|
||||
**Effort**: L (4-5 days). Most of the cost is in the addressspace-builder
|
||||
folder/variable plumbing, not the type walking itself.
|
||||
**Deps**: PR 1.5 (ENUM/ALIAS) — without it, struct members of enum type
|
||||
silently drop. PR 1.4 (whole-array reads) is helpful but not blocking.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: this is the **largest doc-write of the plan**.
|
||||
`docs/Driver.TwinCAT.Cli.md` gains a new top-level "UDT decomposition"
|
||||
section explaining the dotted-instance browse syntax (`MyStruct.Inner.
|
||||
Field`), array-of-struct expansion bound, and how members surface via
|
||||
`browse`. The existing `read` example "Nested UDT member" gets expanded
|
||||
with a multi-level case targeting the plant hierarchy. `docs/drivers/
|
||||
TwinCAT-Test-Fixture.md` "What it actually covers" gets a UDT bullet
|
||||
per-member rather than per-leaf. Update `docs/v3/twincat-backlog.md` —
|
||||
remove the implicit UDT-decomposition gap.
|
||||
- Fixture (TCBSD PLC project, primary fixture-extension surface): the
|
||||
existing `GVL_Plant.Line1.Stations[1..3].Axes[1..4]...` 5-level
|
||||
hierarchy already provides ~50+ leaves per `TwinCatProject/README.md`
|
||||
§ "5-level plant hierarchy" + § "Live value churn". This PR may add a
|
||||
few **edge cases** to stress the type walker:
|
||||
- `PLC/DUTs/ST_NestedFlags.TcDUT` — struct containing a BIT-packed
|
||||
member (e.g. `Flags : DWORD` with named bit-mask aliases).
|
||||
- `PLC/DUTs/ST_RecursiveCap.TcDUT` — struct with a self-pointer (must
|
||||
be capped by the type walker, not infinite-recurse). Demonstrates
|
||||
POINTER skip behavior.
|
||||
- Add an `ARRAY [1..2000] OF ST_AlarmRecord` to exercise the
|
||||
`MaxArrayExpansion` (default 1024) cutoff.
|
||||
README § "Complex hierarchy" gets the new edge-case DUTs documented.
|
||||
- Integration tests: new `TwinCATTypeWalkerTests` (unit) feeding synthetic
|
||||
`IDataType` trees. Live: `Driver_browses_full_plant_hierarchy_yields_50_plus_leaves`,
|
||||
`Driver_caps_array_of_struct_expansion_at_configured_bound`,
|
||||
`Driver_handles_self_referential_struct_without_recursion` against the
|
||||
new edge-case DUTs.
|
||||
- E2E: `scripts/e2e/test-twincat.ps1` could gain a UDT-bridge scenario
|
||||
(`-BridgeNodeId` pointing at `GVL_Plant.Line1.Stations[1].Axes[1].Motor.
|
||||
Temperature`) but this requires the OPC UA server's address-space to
|
||||
reflect the decomposed tree — keep as a follow-up after server-side
|
||||
rendering ships in v3.
|
||||
|
||||
### Phase 5 — TC3 EventLogger alarms
|
||||
|
||||
#### PR 5.1 — `IAlarmSource` via TC3 EventLogger
|
||||
|
||||
**Scope**: TwinCAT 3.1 build 4022+ ships TcEventLogger as a system service
|
||||
exposing alarms/events on AMS port 110 (`AMSPORT_EVENTLOG`). Implement
|
||||
`IAlarmSource` over that interface so PLC alarms surface as OPC UA AC events.
|
||||
|
||||
**Open question (b) below** drives the implementation: does Beckhoff publish
|
||||
a managed wrapper, or do we hit AMS port 110 directly?
|
||||
|
||||
If a managed wrapper exists:
|
||||
- `Beckhoff.TwinCAT.Ads.TcEventLogger` (or similar) — subscribe via
|
||||
`EventLogger.AlarmRaised` event.
|
||||
|
||||
If not (likely — InfoSys docs lean on `TcCOM` C++ APIs):
|
||||
- Open a second `AdsClient` connection to port 110 via
|
||||
`_secondaryClient.Connect(netId, 110)`.
|
||||
- Use `AddDeviceNotificationAsync` on the alarm-list index group
|
||||
(`ADSIGRP_TCEVENTLOG_ALARMS`, exact constant TBD during spike).
|
||||
- Decode the binary event payload into `AlarmEvent` records (severity,
|
||||
source, message, time-of-occurrence, ack state).
|
||||
|
||||
**Files**:
|
||||
- New: `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAlarmSource.cs`
|
||||
— implements `IAlarmSource` (currently used by Galaxy / Wonderware).
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs` — declare
|
||||
`IAlarmSource` interface, delegate to the helper.
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverOptions.cs` — new
|
||||
`bool EnableAlarms` (default `false` until production-validated).
|
||||
|
||||
**Beckhoff.TwinCAT.Ads API**: TBD pending spike. Falls back to raw
|
||||
`AdsClient.AddDeviceNotificationAsync` on port 110 if no managed wrapper.
|
||||
|
||||
**Test plan**:
|
||||
- Unit: fake event-logger feeds synthetic alarms; assert `IAlarmSource`
|
||||
surface raises events with correct shape.
|
||||
- Integration: TCBSD project gains an `Alarm.Raise(...)` call site on a GVL
|
||||
bool transition; new `[TwinCATFact]` subscribes via the driver, toggles the
|
||||
trigger, asserts the alarm appears in the source within 5 s.
|
||||
|
||||
**Effort**: L (4-5 days), most of which is the spike. If no managed wrapper
|
||||
exists, add another L (3-4 days) to implement the binary protocol decoder.
|
||||
**Deps**: spike answer to open question (b) — surface that as an explicit
|
||||
investigation PR before committing to the build.
|
||||
|
||||
**Docs / fixture / e2e**:
|
||||
- Docs: **new file** `docs/drivers/TwinCAT.md` (the existing
|
||||
`TwinCAT-Test-Fixture.md` is fixture-only) covering the alarm
|
||||
configuration surface — `EnableAlarms` option, AMS port 110 routing,
|
||||
severity / source / message decode, OPC UA AC mapping. Spike output
|
||||
goes to `docs/v3/twincat-eventlogger-spike.md` per open question (b).
|
||||
`docs/Driver.TwinCAT.Cli.md` gains a new `alarms` subcommand (subscribe
|
||||
+ print stream) mirroring the OPC UA Client CLI's `alarms` verb.
|
||||
`docs/drivers/TwinCAT-Test-Fixture.md` "Alarms / history" caveat
|
||||
removed; capability matrix gets `IAlarmSource = yes`.
|
||||
- Fixture (TCBSD PLC project, primary fixture-extension surface): add
|
||||
`PLC/POUs/FB_AlarmHarness.TcPOU` that calls `FB_TcLogEvent` (or
|
||||
equivalent TC3 EventLogger PLC API) on a 5 s tick, raising / clearing
|
||||
a known event class. New `PLC/GVLs/GVL_Alarms.TcGVL` exposes the
|
||||
trigger booleans the test toggles. `TwinCatProject/README.md` § new
|
||||
"Alarm scenarios" subsection documents the event class IDs + severity
|
||||
+ cleared-on transitions. The existing `ST_Alarm` DUT remains for
|
||||
PLC-level data; the EventLogger is the AC source.
|
||||
- Integration tests: new `TwinCATAlarmIntegrationTests.cs` —
|
||||
`Driver_raises_alarm_event_when_PLC_logs_event` `[TwinCATFact]`
|
||||
toggles the trigger via `WriteAsync`, asserts the alarm appears in
|
||||
`IAlarmSource.AlarmRaised` within 5 s. Includes a clear-event variant.
|
||||
Unit tests via fake event-logger feed synthetic alarms.
|
||||
- E2E: `scripts/e2e/test-twincat.ps1` gains a `Test-AlarmRoundTrip`
|
||||
step (toggle PLC trigger → assert event surfaces via OPC UA AC client)
|
||||
once the server-side wiring is in. Likely defers to a follow-up PR
|
||||
after the server-tier alarm rendering catches up.
|
||||
|
||||
## Documentation, fixture, and e2e impact
|
||||
|
||||
Consolidated view across all 12 PRs. The **TCBSD fixture PLC project**
|
||||
(`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/`) is
|
||||
the **primary fixture-extension surface** — it's a real TwinCAT XAE project
|
||||
committed object-by-object as `.TcGVL` / `.TcDUT` / `.TcPOU` files. Most PRs
|
||||
extend it by adding GVL variables, DUTs (structs / enums / aliases), or POUs
|
||||
(function blocks driving live data churn). The TCBSD VM at AmsNetId
|
||||
`41.169.163.43.1.1` on `10.100.0.128` is the deployment target (per memory
|
||||
entry `project_tcbsd_fixture.md`); the project bypasses the local Hyper-V/RTIME
|
||||
conflict (per `project_twincat_hyperv_conflict.md`) by running on ESXi.
|
||||
|
||||
### User docs touched
|
||||
|
||||
| PR | `docs/Driver.TwinCAT.Cli.md` | `docs/drivers/TwinCAT-Test-Fixture.md` | `docs/v3/twincat-backlog.md` | Other |
|
||||
|---|---|---|---|---|
|
||||
| 1.1 LINT/ULINT | Data-types caveat removed | Bugs-caught entry #4 | — | — |
|
||||
| 1.2 TIME/DATE/DT/TOD | Native-type syntax + 4 examples | — | — | — |
|
||||
| 1.3 Bit-write | `write` example + RMW note | Bugs-caught entry #3 update | — | — |
|
||||
| 1.4 Arrays | New "Arrays" sub-section + read example | Coverage list bullet | — | — |
|
||||
| 1.5 ENUM/ALIAS | `browse` data-types rows | Coverage list bullet | — | — |
|
||||
| 2.1 Sum cmds | — | New "Performance" section | Closed-out perf bullet | — |
|
||||
| 2.2 Handles | Cache note in `read` / `subscribe` | Perf-section paragraph | — | — |
|
||||
| 2.3 Sym-version | — | Online-change-handling caveat dropped | — | — |
|
||||
| 3.1 MaxDelay | `--max-delay-ms` flag | Coalescing caveat updated | — | — |
|
||||
| 3.2 Diagnostics | `probe` health-symbols sub-section | New "Diagnostics" section | Cycle-time bullet closed | — |
|
||||
| 4.1 UDT | New top-level "UDT decomposition" section | Coverage list per-member | UDT-decomp gap removed | — |
|
||||
| 5.1 Alarms | New `alarms` subcommand | "Alarms" caveat removed | — | **New** `docs/drivers/TwinCAT.md`; **new** `docs/v3/twincat-eventlogger-spike.md` |
|
||||
|
||||
### TCBSD fixture PLC project changes
|
||||
|
||||
| PR | GVL changes | DUT changes | POU changes | README section |
|
||||
|---|---|---|---|---|
|
||||
| 1.1 LINT/ULINT | `GVL_Primitives.vLargeCounter`, `vLargeCounterU` | — | — | "GVL_Primitives numeric seeds" |
|
||||
| 1.2 TIME/DATE/DT/TOD | `GVL_Primitives.dCurrentTime`, `tCycleDuration`, `dToday`, `tShiftStart` | — | — | "Type coverage" seed values |
|
||||
| 1.3 Bit-write | _(reuse `GVL_Primitives.vWord`)_ | — | — | — |
|
||||
| 1.4 Arrays | `GVL_Arrays.aReal2D : ARRAY[1..5,1..5] OF REAL` | — | — | "Array coverage" |
|
||||
| 1.5 ENUM/ALIAS | _(reuse `GVL_Enums`; new `currentSeverity`/`currentTemperature` instance vars)_ | — | — | "Integration-test contract" entry |
|
||||
| 2.1 Sum cmds | **`GVL_Perf.aTags : ARRAY[1..1000] OF DINT`** | — | New `FB_PerfChurn` driving rotating writes | New "Performance scenarios" subsection |
|
||||
| 2.2 Handles | _(reuse `GVL_Perf.aTags`)_ | — | — | — |
|
||||
| 2.3 Sym-version | _(no schema change; manual online-change drill)_ | — | — | New "Online-change test scenario" |
|
||||
| 3.1 MaxDelay | _(reuse `GVL_Fixture.nCounter` 100 Hz driver)_ | — | — | One-line note in "Required project state" |
|
||||
| 3.2 Diagnostics | _(reads system GVLs `_AppInfo`, `_TaskInfo[1]`)_ | — | — | Probe-symbols callout |
|
||||
| 4.1 UDT | _(reuse `GVL_Plant`; possibly grow `aLargeAlarms : ARRAY[1..2000] OF ST_AlarmRecord`)_ | New `ST_NestedFlags`, `ST_RecursiveCap`, `ST_AlarmRecord` | — | "Complex hierarchy" edge-cases |
|
||||
| 5.1 Alarms | New `GVL_Alarms` (trigger booleans) | — | New `FB_AlarmHarness` calling `FB_TcLogEvent` | New "Alarm scenarios" |
|
||||
|
||||
### Integration test additions
|
||||
|
||||
All new tests gate on `[TwinCATFact]` / `[TwinCATTheory]` against
|
||||
`TWINCAT_TARGET_NETID`. Most ship in `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.
|
||||
IntegrationTests/TwinCAT3SmokeTests.cs`; PR 5.1 introduces a new
|
||||
`TwinCATAlarmIntegrationTests.cs`. The existing 30-case suite grows to
|
||||
roughly **45 cases** end-of-plan, plus a perf-tier guarded behind
|
||||
`TWINCAT_PERF=1`.
|
||||
|
||||
### E2E scripts
|
||||
|
||||
`scripts/e2e/test-twincat.ps1` is the single TwinCAT e2e bridge today; it's
|
||||
gated behind `TWINCAT_TRUST_WIRE=1` (see task #221 — CI fixture). The plan
|
||||
intentionally **does not change** the canonical bridge for most PRs because
|
||||
the bridge exercises one DINT counter through the OPC UA server, and that
|
||||
path stays correct. PRs 1.2 (DT bridge), 1.4 (array bridge), 4.1 (UDT
|
||||
bridge), 5.1 (alarm round-trip) each list speculative e2e extensions but
|
||||
they're explicitly marked as follow-ups gated on server-side rendering
|
||||
catching up.
|
||||
|
||||
## Skip-rated items (for context)
|
||||
|
||||
These are intentionally not built. Listed for future-reader completeness so
|
||||
nobody re-invests effort that was already triaged:
|
||||
|
||||
| # | Gap | Why skip |
|
||||
|---|---|---|
|
||||
| 9 | Multi-target / multi-route AMS gateway | Per-device config in `TwinCATDriverOptions.Devices` already supports N targets |
|
||||
| 10 | Secure ADS / ADS-over-TLS | Significant work — TC3.1 build 4024+ feature, host-router-level config; defer |
|
||||
| 11 | Route credential management | Host-level AMS router responsibility (`StaticRoutes.xml`); not driver scope |
|
||||
| 12 | NC-axis / CNC channel / EtherCAT slave I/O | Specialty; system-symbol filter actively drops `Mc_*` (`TwinCATSystemSymbolFilter.cs:28`) |
|
||||
| 13 | System-service ports (200/10000) | Niche operational tooling; user-runtime ports cover real use cases |
|
||||
| 15 | PLC RPC / method invocation | Niche; design-heavy; no demand signal yet |
|
||||
| 16 | Per-PLC-runtime auto-discover | Cosmetic; manual port config in options works |
|
||||
| 20 | File-system access via ADS (FOPEN/FREAD) | Niche; out of scope |
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **(a) TMC parsing — separate library or embedded?**
|
||||
Phase 4 ships the **online type-walker** path which uses
|
||||
`Beckhoff.TwinCAT.Ads.TypeSystem.SymbolLoaderFactory` and needs a live
|
||||
runtime. If a future use case needs offline discovery (e.g. address-space
|
||||
pre-bake at build time without a reachable PLC), do we:
|
||||
- vendor a TMC-XML parser into this driver, or
|
||||
- build a separate `ZB.MOM.WW.OtOpcUa.Tooling.TwinCAT` CLI that emits a
|
||||
pre-baked tag manifest?
|
||||
The latter cleanly separates build-time tooling from runtime driver code
|
||||
and matches how Galaxy.Host is split. Decision deferred until demand
|
||||
appears; recommend the CLI route when it does.
|
||||
|
||||
2. **(b) Beckhoff TC3 EventLogger NuGet — published, or AMS port 110 raw?**
|
||||
Need to spike against the current `Beckhoff.TwinCAT.Ads` v6 NuGet API
|
||||
surface. Beckhoff InfoSys lists a `Tc3_EventLogger` PLC library and a
|
||||
TcCOM C++ API but the .NET surface is thinner. PR 5.1 starts with a
|
||||
one-day spike documented as `docs/v3/twincat-eventlogger-spike.md` before
|
||||
committing to the implementation path.
|
||||
|
||||
3. **(c) Symbol-version invalidation event details**
|
||||
PR 2.3 needs the exact index-group constant and notification semantics for
|
||||
the symbol-version counter. `AdsReservedIndexGroup.SymbolVersion` (0xF008)
|
||||
is the working hypothesis but the field on the v6 enum needs verification
|
||||
— the older `TwinCAT.Ads.AdsReservedIndexGroup` enum had different naming.
|
||||
Beckhoff InfoSys `tcadscommon/tcadscommon_indexgroups` is the reference;
|
||||
confirm during the PR 2.3 spike. Fallback: poll the version counter at
|
||||
probe-loop cadence and treat any change as an invalidation.
|
||||
|
||||
## References
|
||||
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDataType.cs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSymbolPath.cs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAmsAddress.cs`
|
||||
- `docs/featuregaps.md` — TwinCAT (Beckhoff ADS) section
|
||||
- `docs/v3/twincat-backlog.md` — deferred items (TC2, multi-hop, lab IPC)
|
||||
- `docs/drivers/TwinCAT-Test-Fixture.md` — TCBSD + XAR fixture details
|
||||
- Beckhoff InfoSys: <https://infosys.beckhoff.com/english.php?content=../content/1033/tcadsdll2/117571083.html> (Sum commands)
|
||||
- Beckhoff InfoSys: <https://infosys.beckhoff.com/english.php?content=../content/1033/tcadsnetref/7313319051.html> (NotificationSettings)
|
||||
- Beckhoff GitHub: <https://github.com/Beckhoff/TC3-AdsClient-Csharp>
|
||||
Reference in New Issue
Block a user