Files
lmxopcua/docs/Subscriptions.md
Joseph Doherty 21e0fdd4cd Docs audit — fill gaps so the top-level docs/ reference matches shipped code
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>
2026-04-23 09:42:42 -04:00

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's ISubscribable, wrapped by CapabilityInvoker (the rest of this doc).
  • NodeSourceKind.Virtual — subscribes via VirtualTagSource (src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs), which forwards change events emitted by VirtualTagEngine as OnDataChange. 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 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