Files
lmxopcua/docs/Subscriptions.md
Joseph Doherty 985b7aba26 Doc refresh (task #202) — core architecture docs for multi-driver OtOpcUa
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.
2026-04-20 01:33:28 -04:00

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 standalone Driver.Galaxy.Host Windows 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. Their OnDataChange fires 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:

  1. ISubscribable.OnDataChange fires on a thread-pool thread with a DataChangeEventArgs(subscriptionHandle, fullReference, DataValueSnapshot).
  2. Core looks up the variable by fullReference in the driver's DriverNodeManager variable map.
  3. Under the OPC UA framework Lock, the variable's Value / StatusCode / Timestamp are updated and ClearChangeMasks(SystemContext, false) is called.
  4. 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 contract
  • src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs — pipeline wrapping
  • src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Sta/StaPump.cs — Galaxy STA thread + message pump
  • Per-driver subscribe implementations in each Driver.* project