Files
lmxopcua/docs/MxAccessBridge.md
Joseph Doherty 965e430f48 Add component-level documentation for all 14 server subsystems
Provides technical documentation covering OPC UA server, address space,
Galaxy repository, MXAccess bridge, data types, read/write, subscriptions,
alarms, historian, incremental sync, configuration, dashboard, service
hosting, and CLI tool. Updates README with component documentation table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:47:59 -04:00

6.9 KiB

MXAccess Bridge

The MXAccess bridge connects the OPC UA server to the AVEVA System Platform runtime through the ArchestrA.MxAccess COM API. It handles all COM threading requirements, translates between OPC UA read/write requests and MXAccess operations, and manages connection health.

STA Thread Requirement

MXAccess is a COM-based API that requires a Single-Threaded Apartment (STA). All COM objects -- LMXProxyServer instantiation, Register, AddItem, AdviseSupervisory, Write, and cleanup calls -- must execute on the same STA thread. Calling COM objects from the wrong thread causes marshalling failures or silent data corruption.

StaComThread provides a dedicated STA thread with the apartment state set before the thread starts:

_thread = new Thread(ThreadEntry) { Name = "MxAccess-STA", IsBackground = true };
_thread.SetApartmentState(ApartmentState.STA);

Work items are queued via RunAsync(Action) or RunAsync<T>(Func<T>), which enqueue the work to a ConcurrentQueue<Action> and post a WM_APP message to wake the pump. Each work item is wrapped in a TaskCompletionSource so callers can await the result from any thread.

Win32 Message Pump

COM callbacks (like OnDataChange) are delivered through the Windows message loop. StaComThread runs a standard Win32 message pump using P/Invoke:

  1. PeekMessage primes the message queue (required before PostThreadMessage works)
  2. GetMessage blocks until a message arrives
  3. WM_APP messages drain the work queue
  4. WM_APP + 1 drains the queue and posts WM_QUIT to exit the loop
  5. All other messages are passed through TranslateMessage/DispatchMessage for COM callback delivery

Without this message pump, MXAccess COM callbacks would never fire and the server would receive no live data.

LMXProxyServer COM Object

MxProxyAdapter wraps the real ArchestrA.MxAccess.LMXProxyServer COM object behind the IMxProxy interface. This abstraction allows unit tests to substitute a fake proxy without requiring the ArchestrA runtime.

The COM object lifecycle:

  1. Register(clientName) -- Creates a new LMXProxyServer instance, wires up OnDataChange and OnWriteComplete event handlers, and calls Register to obtain a connection handle
  2. Unregister(handle) -- Unwires event handlers, calls Unregister, and releases the COM object via Marshal.ReleaseComObject

Register/AddItem/AdviseSupervisory Pattern

Every MXAccess data operation follows a three-step pattern, all executed on the STA thread:

  1. AddItem(handle, address) -- Resolves a Galaxy tag reference (e.g., TestMachine_001.MachineID) to an integer item handle
  2. AdviseSupervisory(handle, itemHandle) -- Subscribes the item for supervisory data change callbacks
  3. The runtime begins delivering OnDataChange events for the item

For writes, after AddItem + AdviseSupervisory, Write(handle, itemHandle, value, securityClassification) sends the value to the runtime. The OnWriteComplete callback confirms or rejects the write.

Cleanup reverses the pattern: UnAdviseSupervisory then RemoveItem.

OnDataChange and OnWriteComplete Callbacks

OnDataChange

Fired by the COM runtime on the STA thread when a subscribed tag value changes. The handler in MxAccessClient.EventHandlers.cs:

  1. Maps the integer phItemHandle back to a tag address via _handleToAddress
  2. Maps the MXAccess quality code to the internal Quality enum
  3. Checks MXSTATUS_PROXY for error details and adjusts quality accordingly
  4. Converts the timestamp to UTC
  5. Constructs a Vtq (Value/Timestamp/Quality) and delivers it to:
    • The stored per-tag subscription callback
    • Any pending one-shot read completions
    • The global OnTagValueChanged event (consumed by LmxNodeManager)

OnWriteComplete

Fired when the runtime acknowledges or rejects a write. The handler resolves the pending TaskCompletionSource<bool> for the item handle. If MXSTATUS_PROXY.success == 0, the write is considered failed and the error detail is logged.

Reconnection Logic

MxAccessClient implements automatic reconnection through two mechanisms:

Monitor loop

StartMonitor launches a background task that polls at MonitorIntervalSeconds. On each cycle:

  • If the state is Disconnected or Error and AutoReconnect is enabled, it calls ReconnectAsync
  • If connected and a probe tag is configured, it checks the probe staleness threshold

Reconnect sequence

ReconnectAsync performs a full disconnect-then-connect cycle:

  1. Increment the reconnect counter
  2. DisconnectAsync -- Tears down all active subscriptions (UnAdviseSupervisory + RemoveItem for each), detaches COM event handlers, calls Unregister, and clears all handle mappings
  3. ConnectAsync -- Creates a fresh LMXProxyServer, registers, replays all stored subscriptions, and re-subscribes the probe tag

Stored subscriptions (_storedSubscriptions) persist across reconnects. When ConnectAsync succeeds, ReplayStoredSubscriptionsAsync iterates all stored entries and calls AddItem + AdviseSupervisory for each.

Probe Tag Health Monitoring

A configurable probe tag (e.g., a frequently updating Galaxy attribute) serves as a connection health indicator. After connecting, the client subscribes to the probe tag and records _lastProbeValueTime on every OnDataChange callback.

The monitor loop compares DateTime.UtcNow - _lastProbeValueTime against ProbeStaleThresholdSeconds. If the probe value has not updated within the threshold, the connection is assumed stale and a reconnect is forced. This catches scenarios where the COM connection is technically alive but the runtime has stopped delivering data.

Why Marshal.ReleaseComObject Is Needed

The .NET runtime's garbage collector releases COM references non-deterministically. For MXAccess, delayed release can leave stale COM connections open, preventing clean re-registration. MxProxyAdapter.Unregister calls Marshal.ReleaseComObject(_lmxProxy) in a finally block to immediately release the COM reference count to zero. This ensures the underlying COM server is freed before a reconnect attempt creates a new instance.

Key source files

  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/StaComThread.cs -- STA thread and Win32 message pump
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.cs -- Core client class (partial)
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Connection.cs -- Connect, disconnect, reconnect
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Subscription.cs -- Subscribe, unsubscribe, replay
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.ReadWrite.cs -- Read and write operations
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.EventHandlers.cs -- OnDataChange and OnWriteComplete handlers
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxAccessClient.Monitor.cs -- Background health monitor
  • src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/MxProxyAdapter.cs -- COM object wrapper
  • src/ZB.MOM.WW.LmxOpcUa.Host/Domain/IMxAccessClient.cs -- Client interface