Restructure the driver-facing docs to match the OtOpcUa v2 multi-driver
reality (Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, OPC UA Client
— 8 drivers total; Galaxy ships as three projects) and the capability-interface
architecture where every driver opts into IDriver + whichever of IReadable /
IWritable / ITagDiscovery / ISubscribable / IHostConnectivityProbe /
IPerCallHostResolver / IAlarmSource / IHistoryProvider / IRediscoverable it
supports. Doc scope follows the code: one-driver-specific docs scoped to that
driver, cross-driver concerns live once at the top level, per-driver specs
cross-link to docs/v2/driver-specs.md rather than duplicate.
What changed per file:
- docs/MxAccessBridge.md -> docs/drivers/Galaxy.md (git mv + rewrite): retitled
"Galaxy Driver", reframed as one of seven drivers. Added Project Split table
(Shared .NET Standard 2.0 / Host .NET 4.8 x86 / Proxy .NET 10) and Why
Out-of-Process section citing both the MXAccess bitness constraint and Tier C
stability isolation per docs/v2/plan.md section 4. Added IPC Transport
section covering pipe naming, MessagePack framing, DACL that denies Admins,
shared-secret handshake, heartbeat, and CallAsync<TReq,TResp> dispatch.
Moved file paths from src/ZB.MOM.WW.LmxOpcUa.Host/MxAccess/* to
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/MxAccess/* and added the
Shared + Proxy key-file tables. Added CapabilityInvoker + OTOPCUA0001
analyzer callout. Cross-linked to drivers/README.md, Galaxy-Repository.md,
HistoricalDataAccess.md.
- docs/GalaxyRepository.md -> docs/drivers/Galaxy-Repository.md (git mv +
rewrite): retitled "Galaxy Repository — Tag Discovery for the Galaxy
Driver", opened with a comparison table showing how every driver's
ITagDiscovery source is different (AB CIP @tags walker, TwinCAT
SymbolLoaderFactory, FOCAS CNC queries, OPC UA Client Session.Browse, etc).
Repositioned GalaxyRepositoryService as the Galaxy driver's
ITagDiscovery.DiscoverAsync implementation. Updated paths to
Driver.Galaxy.Host/Backend/GalaxyRepository/*. Added IRediscoverable section
covering the on-change-redeploy IPC path.
- docs/drivers/README.md (new): index with ground-truth driver table —
project path, stability tier, wire library, capability-interface list, and
one notable quirk per driver. Verified against the driver csproj files and
class declarations on focas-pr3-remaining-capabilities (the most recent
branch containing every driver). Galaxy gets its own dedicated docs; the
other seven drivers cross-link to docs/v2/driver-specs.md. Lists the full
Core.Abstractions capability surface, DriverTypeRegistry, CapabilityInvoker,
and OTOPCUA0001 analyzer.
- docs/HistoricalDataAccess.md (rewrite): reframed around IHistoryProvider as
a per-driver optional capability interface. Replaced v1 HistorianPluginLoader
/ AvevaHistorianPluginEntry plugin architecture with the v2 story —
Historian.Aveva was merged into Driver.Galaxy.Host/Backend/Historian/ and
IPC-forwarded through GalaxyProxyDriver. Documented all four IHistoryProvider
methods (ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync /
ReadEventsAsync), CapabilityInvoker wrapping with DriverCapability.HistoryRead,
and the per-driver coverage matrix (Galaxy + OPC UA Client implement; the
six protocol drivers don't and return BadHistoryOperationUnsupported). Kept
the cluster-failover + health-counter + quality-mapping detail for the
Galaxy Historian implementation. Flagged one gap: Proxy forwards all four
history message kinds but the Host-side HistoryAggregateType -> AnalogSummary
column mapping may surface GalaxyIpcException{Code="not-implemented"} on a
given branch until the Phase 2 Galaxy out-of-process gate lands.
Driver list built against ground truth (src on focas-pr3-remaining-capabilities):
Driver.Galaxy.{Shared,Host,Proxy}, Driver.Modbus, Driver.S7, Driver.AbCip,
Driver.AbLegacy, Driver.TwinCAT, Driver.FOCAS, Driver.OpcUaClient.
Capability interface lists verified against each *Driver.cs class declaration.
Aveva Historian ported to Driver.Galaxy.Host/Backend/Historian/; no separate
Historian.Aveva assembly on v2 branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Historical Data Access
OPC UA HistoryRead is a per-driver optional capability in OtOpcUa. The Core dispatches HistoryRead service calls to the owning driver through the IHistoryProvider capability interface (src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs). Drivers that don't implement the interface return BadHistoryOperationUnsupported for every history call on their nodes; that is the expected behavior for protocol drivers (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) whose wire protocols carry no time-series data.
Historian integration is no longer a separate bolt-on assembly, as it was in v1 (ZB.MOM.WW.LmxOpcUa.Historian.Aveva plugin). It is now one optional capability any driver can implement. The first implementation is the Galaxy driver's Wonderware Historian integration; OPC UA Client forwards HistoryRead to the upstream server. Every other driver leaves the capability unimplemented and the Core short-circuits history calls on nodes that belong to those drivers.
IHistoryProvider
Four methods, mapping onto the four OPC UA HistoryRead service variants:
| Method | OPC UA service | Notes |
|---|---|---|
ReadRawAsync |
HistoryReadRawModified (raw subset) | Returns HistoryReadResult { Samples, ContinuationPoint? }. The Core handles ContinuationPoint pagination. |
ReadProcessedAsync |
HistoryReadProcessed | Takes a HistoryAggregateType (Average / Minimum / Maximum / Total / Count) and a bucket interval. Drivers that can't express an aggregate throw NotSupportedException; the Core translates that into BadAggregateNotSupported. |
ReadAtTimeAsync |
HistoryReadAtTime | Default implementation throws NotSupportedException — drivers without interpolation / prior-boundary support leave the default. |
ReadEventsAsync |
HistoryReadEvents | Historical alarm/event rows, distinct from the live IAlarmSource stream. Default throws; only drivers with an event historian (Galaxy's A&E log) override. |
Supporting DTOs live alongside the interface in Core.Abstractions:
HistoryReadResult(IReadOnlyList<DataValueSnapshot> Samples, byte[]? ContinuationPoint)HistoryAggregateType— enum{ Average, Minimum, Maximum, Total, Count }HistoricalEvent(EventId, SourceName?, EventTimeUtc, ReceivedTimeUtc, Message?, Severity)HistoricalEventsResult(IReadOnlyList<HistoricalEvent> Events, byte[]? ContinuationPoint)
Dispatch through CapabilityInvoker
All four HistoryRead surfaces are wrapped by CapabilityInvoker (Core/Resilience/CapabilityInvoker.cs) with DriverCapability.HistoryRead. The Polly pipeline keyed on (DriverInstanceId, HostName, DriverCapability.HistoryRead) provides timeout, circuit-breaker, and bulkhead defaults per the driver's stability tier (see docs/v2/driver-stability.md).
The dispatch point is DriverNodeManager in ZB.MOM.WW.OtOpcUa.Server. When the OPC UA stack calls HistoryRead, the node manager:
- Resolves the target
NodeHandleto a(DriverInstanceId, fullReference)pair. - Checks the owning driver's
DriverTypeMetadatato see if the type may advertise history at all (fast reject for types that never implementIHistoryProvider). - If the driver instance implements
IHistoryProvider, wraps theReadRawAsync/ReadProcessedAsync/ReadAtTimeAsync/ReadEventsAsynccall inCapabilityInvoker.InvokeAsync(... DriverCapability.HistoryRead ...). - Translates the
HistoryReadResultinto an OPC UAHistoryData+ExtensionObject. - Manages the continuation point via
HistoryContinuationPointManagerso clients can page through large result sets.
Driver-level history code never sees the continuation-point protocol or the OPC UA stack types — those stay in the Core.
Driver coverage
| Driver | Implements IHistoryProvider? |
Source |
|---|---|---|
| Galaxy | Yes — raw, processed, at-time, events | aahClientManaged SDK (Wonderware Historian) on the Host side, forwarded through the Proxy's IPC |
| OPC UA Client | Yes — raw, processed, at-time, events (forwarded to upstream) | Opc.Ua.Client.Session.HistoryRead against the remote server |
| Modbus | No | Wire protocol has no time-series concept |
| Siemens S7 | No | S7comm has no time-series concept |
| AB CIP | No | CIP has no time-series concept |
| AB Legacy | No | PCCC has no time-series concept |
| TwinCAT | No | ADS symbol reads are point-in-time; archiving is an external concern |
| FOCAS | No | Default — FOCAS has no general-purpose historian API |
Galaxy — Wonderware Historian (aahClientManaged)
The Galaxy driver's IHistoryProvider implementation lives on the Host side (.NET 4.8 x86) in src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/Backend/Historian/. The Proxy's GalaxyProxyDriver.ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync / ReadEventsAsync each serializes a HistoryRead*Request and awaits the matching HistoryRead*Response over the named pipe (see drivers/Galaxy.md).
Host-side, HistorianDataSource uses the AVEVA Historian managed SDK (aahClientManaged.dll) to query historical data via a cursor-based API through ArchestrA.HistorianAccess:
HistoryQuery— raw historical samples (timestamp, value, OPC quality)AnalogSummaryQuery— pre-computed aggregates (Average, Minimum, Maximum, ValueCount, First, Last, StdDev)
The SDK DLLs are pulled into the Galaxy.Host project at build time; the Server and every other driver project remain SDK-free.
Gap / status note. The raw SDK wrapper (
HistorianDataSource,HistorianClusterEndpointPicker,HistorianHealthSnapshot, etc.) has been ported from the v1ZB.MOM.WW.LmxOpcUa.Historian.Avevaplugin intoDriver.Galaxy.Host/Backend/Historian/. The IPC wire-up —HistoryReadRequest/HistoryReadResponsemessage kinds, Proxy-sideReadRawAsync/ReadProcessedAsync/ReadAtTimeAsync/ReadEventsAsyncforwarding — is in place onGalaxyProxyDriver. What remains to close on a given branch is Host-side mapping ofHistoryAggregateTypeonto theAnalogSummaryQuerycolumn names (done inGalaxyProxyDriver.MapAggregateToColumn; the Host side must mirror it) and the end-to-end integration test that was held by the v1 plugin suite. Until those land on a given driver branch, history calls against Galaxy may surfaceGalaxyIpcException { Code = "not-implemented" }or backend-specific errors rather than populatedHistoryReadResults. Track the remaining work against the Phase 2 Galaxy out-of-process gate indocs/v2/plan.md.
Aggregate function mapping
GalaxyProxyDriver.MapAggregateToColumn (Proxy-side) translates the OPC UA Part 13 standard aggregate enum onto AnalogSummaryQuery column names consumed by HistorianDataSource.ReadAggregateAsync:
HistoryAggregateType |
Result Property |
|---|---|
Average |
Average |
Minimum |
Minimum |
Maximum |
Maximum |
Count |
ValueCount |
HistoryAggregateType.Total is not supported by Wonderware AnalogSummary and raises NotSupportedException, which the Core translates to BadAggregateNotSupported. Additional OPC UA aggregates (Start, End, StandardDeviationPopulation) sit on the Historian columns First, Last, StdDev and can be exposed by extending the enum + mapping together.
Read-only cluster failover
HistorianConfiguration.ServerNames accepts an ordered list of cluster nodes. HistorianClusterEndpointPicker iterates the list in configuration order, marks failed nodes with a FailureCooldownSeconds window, and re-admits them when the cooldown elapses. One picker instance is shared by the process-values connection and the event-history connection (two SDK silos), so a node failure on one silo immediately benches it for the other. FailureCooldownSeconds = 0 disables the cooldown — the SDK's own retry semantics are the sole gate.
Host-side cluster health is surfaced via HistorianHealthSnapshot { NodeCount, HealthyNodeCount, ActiveProcessNode, ActiveEventNode, Nodes } and forwarded to the Proxy so the Admin UI Historian panel can render a per-node table. HealthCheckService flips overall service health to Degraded when HealthyNodeCount < NodeCount.
Runtime health counters
HistorianDataSource maintains per-read counters — TotalQueries, TotalSuccesses, TotalFailures, ConsecutiveFailures, LastSuccessTime, LastFailureTime, LastError, ProcessConnectionOpen, EventConnectionOpen — so the dashboard can distinguish "backend loaded but never queried" from "backend loaded and queries are failing". LastError is prefixed with the read path (raw:, aggregate:, at-time:, events:) so operators can tell which silo is broken. HealthCheckService degrades at ConsecutiveFailures >= 3.
Quality mapping
The Historian SDK returns standard OPC DA quality values in QueryResult.OpcQuality (UInt16). The low byte flows through the shared QualityMapper pipeline (MapFromMxAccessQuality → MapToOpcUaStatusCode):
| OPC Quality Byte | OPC DA Family | OPC UA StatusCode |
|---|---|---|
| 0-63 | Bad | Bad (with sub-code when an exact enum match exists) |
| 64-191 | Uncertain | Uncertain (with sub-code when an exact enum match exists) |
| 192+ | Good | Good (with sub-code when an exact enum match exists) |
See Domain/QualityMapper.cs and Domain/Quality.cs in Driver.Galaxy.Host for the full table.
OPC UA Client — upstream forwarding
The OPC UA Client driver (Driver.OpcUaClient) implements IHistoryProvider by forwarding each call to the upstream server via Session.HistoryRead. Raw / processed / at-time / events map onto the stack's native HistoryRead details types. Continuation points are passed through — the Core's HistoryContinuationPointManager treats the driver as an opaque pager.
Historizing flag and AccessLevel
During variable node creation, drivers that advertise history set:
if (attr.IsHistorized)
accessLevel |= AccessLevels.HistoryRead;
variable.Historizing = attr.IsHistorized;
Historizing = true— tells OPC UA clients that the node has historical data available.AccessLevels.HistoryRead— enables theHistoryReadaccess bit. The OPC UA stack checks this bit before routing history requests to the Core dispatcher; nodes without it are rejected before reachingIHistoryProvider.
The IsHistorized flag originates in the driver's discovery output. For Galaxy it comes from the repository query detecting a HistoryExtension primitive (see drivers/Galaxy-Repository.md). For OPC UA Client it is copied from the upstream server's Historizing property.
Configuration
Driver-specific historian config lives in each driver's DriverConfig JSON blob, validated against the driver type's DriverConfigJsonSchema in DriverTypeRegistry. The Galaxy driver's historian section carries the fields exercised by HistorianConfiguration — ServerName / ServerNames, FailureCooldownSeconds, IntegratedSecurity / UserName / Password, Port (default 32568), CommandTimeoutSeconds, RequestTimeoutSeconds, MaxValuesPerRead. The OPC UA Client driver inherits its timeouts from the upstream session.
See Configuration.md for the schema shape and validation path.