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>
48 KiB
TwinCAT Driver — Implementation Plan
Source of gap analysis: featuregaps.md → TwinCAT
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:
- Data-type correctness —
LInt/ULIntsilently truncated to Int32 (explicit// matches Int64 gapcomment inTwinCATDataType.cs:40),TIME/DATE/DT/TODmarshalled as rawUDINTrather than native UA types,ENUM/ALIASskipped at browse, bit-indexed BOOL writes throw, multi-dim and whole-array reads not batched. - Performance — every read is a
ReadValueAsynccall 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. - 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. - UDT decomposition —
Structureis 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. - Alarms — no
IAlarmSourceimplementation; 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.Int64exists inCore.Abstractions— if not, add it (likely scope creep intoZB.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— assertLInt.ToDriverDataType() == Int64,ULInt.ToDriverDataType() == Int64. - Integration: extend
GVL_Primitivesto include anLINT(nLargeCounter) seeded with0x1_0000_0000L(above Int32 range). Add a[TwinCATTheory]case asserting the value round-trips without truncation. May need a newGVL_Primitives.lLong : LINTsymbol if not already present (the existing 16-primitive theory inTwinCAT3SmokeTests.cscoversLInt/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 asUDINTon the wire" caveat forLInt/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.TcGVLaddsvLargeCounter : LINT := 16#1_0000_0000(above Int32 range) + matchingvLargeCounterU : 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(UADoubleseconds, or addDurationtoDriverDataTypeif 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 dedicatedTimeOfDaytype if the abstraction supports it.
Files:
src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDataType.cs— updateToDriverDataTypemapping for the four IEC time types.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs—MapToClrTypereturns the raw UDINT today; keep that for the wire read but post-process insideReadValueAsync/ConvertForWriteto convert UDINT ↔DateTime/TimeSpan. Symmetrical change inOnAdsNotificationExso 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 ↔
DateTimefor each variant. - Integration: add
GVL_Primitives.dCurrentTime : DTseeded with a known literal (e.g.DT#2026-01-15-12:00:00); assert the driver returns aDateTimematching 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 asUDINTon the wire — CLI takes a numeric raw value" paragraph with native syntax (e.g.read -t DateTimereturns ISO-8601,write -t Time -v 00:00:01.500for IEC TIME duration). New examples for each of the four IEC time types underread/write. - Fixture (TCBSD PLC project):
PLC/GVLs/GVL_Primitives.TcGVLaddsdCurrentTime : 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 intests/.../TwinCatProject/README.md§ "Type coverage" gets the seed values documented. - Integration tests:
TwinCAT3SmokeTests.cs— newDriver_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 rawUDINTfor these four entries. - E2E:
scripts/e2e/test-twincat.ps1unchanged 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— replacethrowbranch inWriteValueAsyncwith RMW logic mirroringReadValueAsync's bit-index path. AddConcurrentDictionary<string, SemaphoreSlim> _bitWriteLockskeyed on parent symbol.
Beckhoff.TwinCAT.Ads API: AdsClient.ReadValueAsync(parent, typeof(uint))
AdsClient.WriteValueAsync(parent, modifiedWord). Both already used.
Test plan:
- Unit: extend
TwinCATReadWriteTestswith aFakeTwinCATClienttest covering set + clear of bits 0, 7, 15, 31 of auintparent. - Integration: add a new
[TwinCATFact]—Driver_round_trips_bit_indexed_BOOL_write_and_readagainstGVL_Primitives.vWord.4(the0xBEEFword'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.mdwritesection — add an exampleotopcua-twincat-cli write -n ... -s "GVL_Primitives.vWord.4" -t Bool -v trueand 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.vWordalready exists with seed0xBEEF. Tests use bits 4 (clear) and 7 (set) to round-trip. - Integration tests:
TwinCAT3SmokeTests.cs— newDriver_round_trips_bit_indexed_BOOL_write_and_read[TwinCATFact]. Unit tests inTwinCATReadWriteTestsextended viaFakeTwinCATClientfor bits 0/7/15/31 of auintparent. - 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 declaresIsArray=true(extendTwinCATTagDefinition), useAdsClient.ReadValueAsync(symbol, typeof(int[]))/typeof(double[,])etc.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs— surfaceIsArray+ArrayDimthroughDriverAttributeInfoinDiscoverAsync.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATTagDefinition.cs(if exists, inTwinCATDriverOptions.cs) — addbool 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 viaFakeTwinCATClient. - Integration: extend
GVL_Arrayswith a 5x5aReal2D : 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.mdreadsection — 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.TcGVLalready declaresARRAY[1..4,1..4] OF REALperTwinCatProject/README.md§ "Array coverage". This PR adds a 5x5aReal2D : ARRAY [1..5, 1..5] OF REALinitialised 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— newDriver_reads_whole_2D_array_in_one_call[TwinCATFact]. Unit tests extendTwinCATSymbolPathTestsfor 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—MapSymbolTypeNamekeyed only on the type name today; switch to inspectingsymbol.DataType+symbol.CategoryfromTwinCAT.TypeSystem. ForDataTypeCategory.EnumwalkEnumType.EnumValuesand pick the underlying base type. ForDataTypeCategory.AliasresolveAliasType.BaseTyperecursively 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 viaFakeTwinCATClient; assert it browses with the underlying base type. - Integration: add
E_LineState : (Idle, Running, Faulted)+ a GVL instance variable; new[TwinCATFact]browses + reads it asInt16(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.mdbrowsesection — 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.TcDUTandE_Severity.TcDUTalready exist;PLC/DUTs/T_Temperature.TcDUTandT_MeterPerSec.TcDUTalready exist.PLC/GVLs/GVL_Enums.TcGVLalready exposes them at the root perTwinCatProject/README.md§ "Enum + alias coverage" — no fixture change needed for this PR. README's "Integration- test contract" gets a new entry forGVL_Enums.currentSeverity/currentTemperatureso the new browse assertion has a stable target. - Integration tests:
TwinCAT3SmokeTests.cs— newDriver_browses_enums_and_aliases_with_resolved_base_types[TwinCATFact]asserting the fourGVL_Enumssymbols surface with the correct underlying CLR type (Int32for E_AxisState,Int16for E_Severity,Doublefor 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— newReadValuesAsync(IReadOnlyList<(string symbol, Type clrType)>, ct)returning a parallel array of(value, status).src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::ReadAsync— bucketfullReferencesbyDeviceHostAddress, call the new client method per bucket.bitIndexhandling 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.ReadValuesAsyncfakes 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.mdperf note moves out (Sum-commands no longer deferred) — add a closed-out bullet pointing at this PR. New performance section indocs/drivers/TwinCAT-Test-Fixture.mddocumenting the throughput baseline + Sum-command delta.docs/Driver.TwinCAT.Cli.mddoesn'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.TcGVLdeclaringaTags : ARRAY[1..1000] OF DINTplus aMAINrung (or newFB_PerfChurnPOU) 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 separateTWINCAT_PERF=1env flag so CI noise from VM jitter doesn't flap the suite). Unit tests cover ordering, partial-failure mapping, empty-input viaFakeTwinCATClient.ReadValuesAsync. - E2E:
scripts/e2e/test-twincat.ps1unchanged for the canonical bridge; perf scripts live alongside as a separatescripts/perf/twincat-sum.ps1if/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— addConcurrentDictionary<string, uint> _handleCache. Wrap reads/writes throughEnsureHandleAsync(symbolPath)that hits the cache or callsCreateVariableHandleAsync. OnAdsErrorCode.DeviceSymbolVersionInvalid(0x710 / 1808) evict the entry and retry once.- Dispose path:
DeleteVariableHandleAsyncfor every cached handle onAdsClient.Disposeto be a good citizen with the runtime.
Beckhoff.TwinCAT.Ads API:
AdsClient.CreateVariableHandleAsync(string symbol, ct)→ returnsResultHandlewith.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:
FakeTwinCATClientrecords 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.mdperf 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.mdadds a brief note in thesubscribe/readsections 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.aTagsfrom 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 onAdsTwinCATClient). Unit tests onFakeTwinCATClient.HandleCacheTestsassert 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, callAddDeviceNotificationAsync(ADSIGRP_SYM_VERSION, 0, length=1, ...)withAdsTransMode.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-nameExvariant 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—FakeTwinCATClientexposes aFireSymbolVersionChange()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.mdsection 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.mdno change (transparent to CLI user). - Fixture (TCBSD PLC project): no schema change. Operator workflow gains an
online-change drill —
TwinCatProject/README.mdadds a § "Online-change test scenario" describing the steps (open project, add a dummy variable toGVL_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 viaFakeTwinCATClient.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— addint? MaxDelayMstoTwinCATTagDefinition.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::SubscribeAsync— pass through to client.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs::AddNotificationAsync— acceptint maxDelayMs, plumb intoNotificationSettings(..., 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 plumbedmaxDelayMslands onNotificationSettings. - Integration: subscribe to
GVL_Fixture.nCounterwithMaxDelayMs=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.mdsubscribeflag table — add--max-delay-mswith default0and a note that nonzero coalesces high-frequency PLC signals. Update the description of-i/--interval-msto disambiguate cycle vs. max-delay (both pass through toNotificationSettings).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.nCounteralready increments on every 10 ms cycle (seeMAIN.TcPOU), so the test can drive a 100 Hz change rate and verify ≤ 2 Hz delivery withMaxDelayMs=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 toGVL_Fixture.nCounterwithMaxDelayMs=500and asserts delivered-event count ≤ 3 over a 1 s window. - E2E:
scripts/e2e/test-twincat.ps1Test-SubscribeSeesChangeis 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 againstCycleTime.
Files:
src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::ProbeLoopAsync— augment success path to also read these four symbols. Surface via a newTwinCATDeviceDiagnosticsrecord onDeviceState. Emit throughIDriverDiagnostics(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:
FakeTwinCATClientexposesSetSystemSymbolValue(string name, object value)so tests can drive the diagnostics surface deterministically. - Integration:
[TwinCATFact]connects to TCBSD, asserts the diagnostics block populatesCycleTimeMs > 0andOnlineChangeCnt >= 0within 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.mddocumenting 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.mdprobesection gains a "Health probe" sub-section noting the same symbols can be read directly viaprobe -s "TwinCAT_SystemInfoVarList._AppInfo.OnlineChangeCnt"(the existing example) plus the new_TaskInfo[1].CycleTime/LastExecTime. Adddocs/v3/twincat-backlog.mdcross-link confirming cycle-time/jitter no longer deferred. - Fixture (TCBSD PLC project): no change required —
_AppInfoand_TaskInfo[1]are TwinCAT system GVLs, present on every runtime. TheTwinCATSystemSymbolFilteralready drops them from user browse;TwinCatProject/README.mdadds 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 viaFakeTwinCATClient.SetSystemSymbolValuedrive 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:
- Online: walk the
IDataTypetree returned bySymbolLoaderFactory— eachIStructTypeexposesSubItemsrecursively. This is whatBeckhoff.TwinCAT.Adsv6's TypeSystem already gives us at runtime; we just never recursed. - 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—BrowseSymbolsAsyncrecurses intoIStructType.SubItems, yielding oneTwinCATDiscoveredSymbolper 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 withIsArray=true.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs::DiscoverAsync— fold the recursed structure into the existingDiscovered/folder tree usingIAddressSpaceBuilder.Folderfor each struct member.- New:
TwinCATTypeWalker.cs— pure helper that takes anIDataTypeand yields(instancePath, atomicType, readOnly)tuples. Unit-testable without touchingAdsClient.
Beckhoff.TwinCAT.Ads API:
TwinCAT.TypeSystem.IStructType—SubItems(collection ofIMember); each member hasBaseType,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 syntheticIDataTypetrees, assert the flattened paths and types. - Integration: extend
GVL_Plant(already hasLine1.Stations[1].Axes[1].MotorperTwinCAT3SmokeTests.cs) — the existingDriver_reads_deeply_nested_UDT_pathtest 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.mdgains 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 viabrowse. The existingreadexample "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. Updatedocs/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 perTwinCatProject/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 : DWORDwith 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_AlarmRecordto exercise theMaxArrayExpansion(default 1024) cutoff. README § "Complex hierarchy" gets the new edge-case DUTs documented.
- Integration tests: new
TwinCATTypeWalkerTests(unit) feeding syntheticIDataTypetrees. 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_recursionagainst the new edge-case DUTs. - E2E:
scripts/e2e/test-twincat.ps1could gain a UDT-bridge scenario (-BridgeNodeIdpointing atGVL_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 viaEventLogger.AlarmRaisedevent.
If not (likely — InfoSys docs lean on TcCOM C++ APIs):
- Open a second
AdsClientconnection to port 110 via_secondaryClient.Connect(netId, 110). - Use
AddDeviceNotificationAsyncon the alarm-list index group (ADSIGRP_TCEVENTLOG_ALARMS, exact constant TBD during spike). - Decode the binary event payload into
AlarmEventrecords (severity, source, message, time-of-occurrence, ack state).
Files:
- New:
src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAlarmSource.cs— implementsIAlarmSource(currently used by Galaxy / Wonderware). src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs— declareIAlarmSourceinterface, delegate to the helper.src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverOptions.cs— newbool EnableAlarms(defaultfalseuntil 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
IAlarmSourcesurface 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 existingTwinCAT-Test-Fixture.mdis fixture-only) covering the alarm configuration surface —EnableAlarmsoption, AMS port 110 routing, severity / source / message decode, OPC UA AC mapping. Spike output goes todocs/v3/twincat-eventlogger-spike.mdper open question (b).docs/Driver.TwinCAT.Cli.mdgains a newalarmssubcommand (subscribe- print stream) mirroring the OPC UA Client CLI's
alarmsverb.docs/drivers/TwinCAT-Test-Fixture.md"Alarms / history" caveat removed; capability matrix getsIAlarmSource = yes.
- print stream) mirroring the OPC UA Client CLI's
- Fixture (TCBSD PLC project, primary fixture-extension surface): add
PLC/POUs/FB_AlarmHarness.TcPOUthat callsFB_TcLogEvent(or equivalent TC3 EventLogger PLC API) on a 5 s tick, raising / clearing a known event class. NewPLC/GVLs/GVL_Alarms.TcGVLexposes 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_AlarmDUT remains for PLC-level data; the EventLogger is the AC source.
- cleared-on transitions. The existing
- Integration tests: new
TwinCATAlarmIntegrationTests.cs—Driver_raises_alarm_event_when_PLC_logs_event[TwinCATFact]toggles the trigger viaWriteAsync, asserts the alarm appears inIAlarmSource.AlarmRaisedwithin 5 s. Includes a clear-event variant. Unit tests via fake event-logger feed synthetic alarms. - E2E:
scripts/e2e/test-twincat.ps1gains aTest-AlarmRoundTripstep (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
-
(a) TMC parsing — separate library or embedded? Phase 4 ships the online type-walker path which uses
Beckhoff.TwinCAT.Ads.TypeSystem.SymbolLoaderFactoryand 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.TwinCATCLI 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.
-
(b) Beckhoff TC3 EventLogger NuGet — published, or AMS port 110 raw? Need to spike against the current
Beckhoff.TwinCAT.Adsv6 NuGet API surface. Beckhoff InfoSys lists aTc3_EventLoggerPLC library and a TcCOM C++ API but the .NET surface is thinner. PR 5.1 starts with a one-day spike documented asdocs/v3/twincat-eventlogger-spike.mdbefore committing to the implementation path. -
(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 olderTwinCAT.Ads.AdsReservedIndexGroupenum had different naming. Beckhoff InfoSystcadscommon/tcadscommon_indexgroupsis 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.cssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDataType.cssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSymbolPath.cssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAmsAddress.csdocs/featuregaps.md— TwinCAT (Beckhoff ADS) sectiondocs/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