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>
112 lines
6.9 KiB
Markdown
112 lines
6.9 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
_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
|