Audit of docs/ against src/ surfaced shipped features without current-reference coverage (FOCAS CLI, Core.Scripting+VirtualTags, Core.ScriptedAlarms, Core.AlarmHistorian), an out-of-date driver count + capability matrix, ADR-002's virtual-tag dispatch not reflected in data-path docs, broken cross-references, and OpcUaServerReqs declaring OPC-020..022 that were never scoped. This commit closes all of those so operators + integrators can stay inside docs/ without falling back to v2/implementation/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.8 KiB
Subscriptions
Driver-side data-change subscriptions live behind ISubscribable (src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs). The interface is deliberately mechanism-agnostic: it covers native subscriptions (Galaxy MXAccess advisory, OPC UA monitored items on an upstream server, TwinCAT ADS notifications) and driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). Core sees the same event shape regardless — drivers fire OnDataChange and Core dispatches to the matching OPC UA monitored items.
Driver vs virtual dispatch
Per ADR-002, DriverNodeManager routes subscriptions across both driver tags and virtual (scripted) tags through the same ISubscribable contract. The per-variable NodeSourceKind (registered from DriverAttributeInfo at discovery) selects the backend:
NodeSourceKind.Driver— subscribes via the driver'sISubscribable, wrapped byCapabilityInvoker(the rest of this doc).NodeSourceKind.Virtual— subscribes viaVirtualTagSource(src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs), which forwards change events emitted byVirtualTagEngineasOnDataChange. The ref-counting, initial-value, and transfer-restoration behaviour below applies identically.
Because both kinds expose ISubscribable, Core's dispatch, ref-count map, and monitored-item fan-out are unchanged across the source branch.
ISubscribable surface
Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences,
TimeSpan publishingInterval,
CancellationToken cancellationToken);
Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken);
event EventHandler<DataChangeEventArgs>? OnDataChange;
A single SubscribeAsync call may batch many attributes and returns an opaque handle the caller passes back to UnsubscribeAsync. The driver may emit an immediate OnDataChange for each subscribed reference (the OPC UA initial-data convention) and then a push per change.
Every subscribe / unsubscribe call goes through CapabilityInvoker.ExecuteAsync(DriverCapability.Subscribe, host, …) so the per-host pipeline applies.
Reference counting at Core
Multiple OPC UA clients can monitor the same variable simultaneously. Rather than open duplicate driver subscriptions, Core maintains a ref-count per (driver, fullReference) pair: the first OPC UA monitored-item for a reference triggers ISubscribable.SubscribeAsync with that single reference; each additional monitored-item just increments the count; decrement-to-zero triggers UnsubscribeAsync. Transferred subscriptions (client reconnect → resume session) replay against the same ref-count map so active driver subscriptions are preserved across session migration.
Threading
The STA thread story is now driver-specific, not a server-wide concern:
- Galaxy runs its MXAccess COM objects on a dedicated STA thread with a Win32 message pump (
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs) inside the standaloneDriver.Galaxy.HostWindows service. The Proxy driver (Driver.Galaxy.Proxy) connects to the Host via named pipe and re-exposes the data on a free-threaded surface to Core. Core never touches COM. - Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS are free-threaded — they run their polling loops on ordinary
Tasks. TheirOnDataChangefires on thread-pool threads. - OPC UA Client delegates to the OPC Foundation stack's subscription loop.
The common contract: drivers are responsible for marshalling from whatever native thread the backend uses onto thread-pool threads before raising OnDataChange. Core's dispatch path acquires the OPC UA framework Lock and calls ClearChangeMasks on the corresponding BaseDataVariableState to notify subscribed clients.
Dispatch
Core's subscription dispatch path:
ISubscribable.OnDataChangefires on a thread-pool thread with aDataChangeEventArgs(subscriptionHandle, fullReference, DataValueSnapshot).- Core looks up the variable by
fullReferencein the driver'sDriverNodeManagervariable map. - Under the OPC UA framework
Lock, the variable'sValue/StatusCode/Timestampare updated andClearChangeMasks(SystemContext, false)is called. - The OPC Foundation stack then enqueues data-change notifications for every monitored-item attached to that variable, honoring each subscription's sampling + filter configuration.
Batch coalescing — coalescing multiple pushes for the same reference between publish cycles — is done driver-side when the backend natively supports it (Galaxy keeps the v1 coalescing dictionary); otherwise the SDK's own data-change filter suppresses no-change notifications.
Initial values
A freshly-built variable carries StatusCode = BadWaitingForInitialData until the driver delivers the first value. Drivers whose backends supply an initial read (Galaxy AdviseSupervisory, TwinCAT AddDeviceNotification) fire OnDataChange immediately after SubscribeAsync returns. Polled drivers fire the first push when their first poll cycle completes.
Transferred subscription restoration
When an OPC UA session is resumed (client reconnect with TransferSubscriptions), Core walks the transferred monitored-items and ensures every referenced (driver, fullReference) has a live driver subscription. References already active (in-process migration) skip re-subscribing; references that lost their driver-side handle during the session gap are re-subscribed via SubscribeAsync.
Key source files
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs— capability contractsrc/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs— pipeline wrappingsrc/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs— Galaxy STA thread + message pump- Per-driver subscribe implementations in each
Driver.*project