Rewrite seven core-architecture docs to match the shipped multi-driver platform. The v1 single-driver LmxNodeManager framing is replaced with the Core + capability-interface model — Galaxy is now one driver of seven, and each doc points at the current class names + source paths. What changed per file: - OpcUaServer.md — OtOpcUaServer as StandardServer host; per-driver DriverNodeManager + CapabilityInvoker wiring; Config-DB-driven configuration (sp_PublishGeneration, DraftRevisionToken, Admin UI); Phase 6.2 AuthorizationGate integration. - AddressSpace.md — GenericDriverNodeManager.BuildAddressSpaceAsync walks ITagDiscovery.DiscoverAsync and streams DriverAttributeInfo through IAddressSpaceBuilder; CapturingBuilder registers alarm-condition sinks; per-driver NodeId schemes replace the fixed ns=1;s=ZB root. - ReadWriteOperations.md — OnReadValue / OnWriteValue dispatch to IReadable.ReadAsync / IWritable.WriteAsync through CapabilityInvoker, honoring WriteIdempotentAttribute (#143); two-layer authorization (WriteAuthzPolicy + Phase 6.2 AuthorizationGate). - Subscriptions.md — ISubscribable.SubscribeAsync/UnsubscribeAsync is the capability surface; STA-thread story is now Galaxy-specific (StaPump inside Driver.Galaxy.Host), other drivers are free-threaded. - AlarmTracking.md — IAlarmSource is optional; AlarmSurfaceInvoker wraps Subscribe/Ack/Unsubscribe with fan-out by IPerCallHostResolver and the no-retry AlarmAcknowledge pipeline (#143); CapturingBuilder registers sinks at build time. - DataTypeMapping.md — DriverDataType + SecurityClassification are the driver-agnostic enums; per-driver mappers (GalaxyProxyDriver inline, AbCipDataType, ModbusDriver, etc.); SecurityClassification is metadata only, ACL enforcement is at the server layer. - IncrementalSync.md — IRediscoverable covers backend-change signals; sp_ComputeGenerationDiff + DiffViewer drive generation-level change detection; IDriver.ReinitializeAsync is the in-process recovery path.
4.9 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.
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