Strict width+kind eligibility for sum-batched scalars (others read singly); latch only on ADS
1793/1794, split 1792/1797/1798 without latching; 32 KiB per-command budget and 500-item ceiling;
short sum response handled per item. Diagnostics: opt-in diagnosticsEnabled (default off), reuse the
probe's ReadState, sample after the host-state transition, JitterMs renamed HeadroomMs, compact
per-device line so many devices fit the 16-entry cap, unavailable-symbol latch cleared on reconnect.
AdminUI: min/max on sum-read max, diagnostics checkbox. Pure planner extracted and unit-tested.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Sum-read (#310): batches a poll's reads per device into sum commands capped at sumReadMaxItems,
per-item ADS errors mapped per tag, falls back to individual reads when the target refuses the
command. Diagnostics (#314): ADS state, app name, online-change count, task cycle/exec/jitter via
IDriverDiagnostics. Re-implemented against the current raw-byte read path (stranded auto/driver-gaps
commits predate it). Sum-write not ported. Unit tests only; live gate pending a route.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
An array tag subscribed natively used to register only its declared scalar
(publishing element 0 at Good) and was then refused wholesale with
BadTypeMismatch. The driver now hands the authored ArrayLength to the client,
which registers the whole array as raw bytes and decodes every notification.
- TwinCATTypeGuard.CheckArray plans the layout from the RESOLVED array type:
element checked like a scalar notification (width / float kind / string
kind); STRING(n)/WSTRING(n) elements stride n+1 / 2(n+1); multi-dim arrays
flattened in PLC memory order; an authored ArrayLength shorter than the PLC
array reads the leading sub-range, a longer one is refused; struct / pointer
/ nested-array elements, non-array symbols and VAR_IN_OUT refs are refused.
- CheckNotificationSize caps a native array at 128 KiB (largest registration
proven on a live TC 2.11 target); above it the tag is refused BadOutOfRange.
- TwinCATArrayCodec decodes the bytes into the element-typed CLR array.
- The poll read uses the same plan + codec instead of the Beckhoff typed array
read, which failed live for most real arrays (exact CLR element type, rank-N
for multi-dim, member arrays sliced by the under-reported TC2 symbol size).
- TwinCATNotificationRefusedException carries the refusal's status; the type
mismatch derives from it and the driver isolates either per tag.
Port of the ignitionoee TwinCAT string + type-reconciliation fixes onto the
Beckhoff-library client:
- STRING/WSTRING subscriptions never worked: AddDeviceNotificationExAsync was
called with typeof(string) and no length, and the Beckhoff any-type
marshaller throws ArgumentNullException for a string without one, so one
string tag tore down its whole subscribe call. A string now registers the
PLC's own byte width as raw bytes (0.19.2: STRING(n) of any n).
- Strings are read as raw bytes and decoded by TwinCATStringCodec. The
Beckhoff marshaller already cut a read at the first NUL (0.19.3 / TwinCAT-007
were not defects on the read path); the codec keeps that rule for both STRING
and WSTRING and extends it to the new notification path. A STRING now decodes
as Windows-1252 (TwinCAT-010) - or UTF-8 when the target reports UTF-8
strings - instead of the host culture's ANSI code page the Beckhoff
marshaller falls back to (1250 on a Polish host, 1251 Cyrillic, ...).
- TwinCATTypeGuard (TwinCAT-002/-008): a read refuses a numeric tag over a PLC
string (and vice versa / STRING vs WSTRING / string over a container) with
BadTypeMismatch instead of handing back a string object at Good; a
notification - raw bytes decoded as the DECLARED type - additionally refuses
width and float/integer mismatches (DINT over REAL reinterpreted the bits).
- A refused notification is isolated: that tag publishes BadTypeMismatch with
one WARN, keeps a dead intent for the probe-tick retry, and the rest of the
subscription registers.
Native ADS notifications (the default subscribe mode) were stored as opaque handles
with no record of the symbol/type/interval/handler needed to replay. On a client swap
(EnsureConnectedAsync building a fresh client after a drop) the notifications were
silently orphaned — no Bad status, no error, pushes just stopped until redeploy.
Compounding: the IsConnected fast-path keys on AMS-port state, not wire liveness, and
a probe failure only transitioned state without recycling the dead client.
Fix:
- Store REPLAYABLE INTENT: NativeRegistration (symbol/type/bit/interval/onChange +
swappable live handle) hung off DeviceState.NativeRegistrations, populated by
SubscribeAsync via RegisterNotificationAsync (under ConnectGate).
- Split EnsureConnectedAsync into a gate wrapper + EnsureConnectedUnderGateAsync core;
when the core installs a NEW client it replays every stored intent onto it and swaps
the live handle (disposing the dead one). Register + replay both run under ConnectGate
so they can't race.
- Probe loop: on a wire-probe failure (false or throw) RecycleClientAsync disposes+nulls
the client so the next tick rebuilds + replays — closes the fast-path-keys-on-port-state
compounding bug.
No TwinCAT docker fixture exists (integration needs a real TC3 XAR), so the fake-client
unit tests are the authoritative coverage:
- 4 new guards in TwinCATReconnectReplayTests (replay-onto-fresh-client + push reaches
OnDataChange + old handle disposed; replay-all-tags; unsubscribe-after-reconnect stops
replaying; probe-failure recycles+rebuilds).
- Full TwinCAT unit suite 174/174 green; full solution builds 0 errors.
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
- Driver.TwinCAT-004: corrected the IEC time-type inline comments;
documented that the driver currently surfaces them as raw UInt32
counters.
- Driver.TwinCAT-006: ResolveHost returns a documented UnresolvedHost
sentinel when no devices are configured instead of returning the
logical DriverInstanceId (which never matches GetHostStatuses).
- Driver.TwinCAT-014: wired Probe.Timeout into the probe-loop call and
added a NotificationMaxDelayMs config knob threaded through
AddNotificationAsync.
- Driver.TwinCAT-015: Dispose() runs a genuinely synchronous teardown
with bounded waits (no sync-over-async deadlock pattern).
- Driver.TwinCAT-016: pinned the Structure-tag rejection and the
probe-loop vs read disposal race with regression tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Driver.TwinCAT-001 — InitializeAsync/ReinitializeAsync ignored driverConfigJson.
Extracted the DTO-to-options parse into a shared TwinCATDriverFactoryExtensions.ParseOptions;
InitializeAsync now re-parses driverConfigJson into a mutable _options field, so a config
generation pushed via ReinitializeAsync (added/removed devices, tags, probe settings) is
actually applied at runtime.
Driver.TwinCAT-002 — LInt/ULInt narrowed to Int32. ToDriverDataType now maps LInt to Int64,
ULInt to UInt64, UDInt to UInt32, UInt/USInt to UInt16, Int/SInt to Int16, and the IEC
TIME/DATE/DT/TOD types to UInt32 (their raw UDINT counter). Removed the stale "Int64 gap"
comment — no truncation or sign flips at the OPC UA encode layer.
Driver.TwinCAT-007 — EnsureConnectedAsync was not thread-safe. Connect/reconnect is now
serialized per device by a SemaphoreSlim (DeviceState.ConnectGate) with a double-checked
connect, mirroring the S7 driver. Concurrent read/write/probe callers can no longer leak a
client or race a create-vs-dispose.
Driver.TwinCAT-008 — native ADS notification callbacks ran driver logic on the AMS router
thread. AdsTwinCATClient now enqueues AdsNotificationEx callbacks onto a bounded Channel
drained by a dedicated managed task; the router-thread callback only does a non-blocking
TryWrite, so a slow consumer cannot stall ADS notification delivery process-wide.
Driver.TwinCAT-013 — TwinCATDriver did not implement IRediscoverable. The driver now
implements IRediscoverable; AdsTwinCATClient detects ADS 0x0702 (symbol-version-changed) on
read/write paths and raises OnSymbolVersionChanged, which the driver forwards as
OnRediscoveryNeeded so Core rebuilds the address space after a PLC program re-download.
Adds TwinCATHighFindingsRegressionTests covering all five fixes; updates the data-type
mapping assertion in TwinCATDriverTests. TwinCAT driver builds clean; 119 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.
- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
integration, install).
Build green (0 errors); unit tests pass. Docs left for a separate pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>