Files
lmxopcua/docs/HistoricalDataAccess.md
Joseph Doherty 71339307fa Doc refresh (task #203) — driver docs split + drivers index + IHistoryProvider-aware HistoricalDataAccess
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>
2026-04-20 01:33:53 -04:00

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:

  1. Resolves the target NodeHandle to a (DriverInstanceId, fullReference) pair.
  2. Checks the owning driver's DriverTypeMetadata to see if the type may advertise history at all (fast reject for types that never implement IHistoryProvider).
  3. If the driver instance implements IHistoryProvider, wraps the ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync / ReadEventsAsync call in CapabilityInvoker.InvokeAsync(... DriverCapability.HistoryRead ...).
  4. Translates the HistoryReadResult into an OPC UA HistoryData + ExtensionObject.
  5. Manages the continuation point via HistoryContinuationPointManager so 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 v1 ZB.MOM.WW.LmxOpcUa.Historian.Aveva plugin into Driver.Galaxy.Host/Backend/Historian/. The IPC wire-upHistoryReadRequest / HistoryReadResponse message kinds, Proxy-side ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync / ReadEventsAsync forwarding — is in place on GalaxyProxyDriver. What remains to close on a given branch is Host-side mapping of HistoryAggregateType onto the AnalogSummaryQuery column names (done in GalaxyProxyDriver.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 surface GalaxyIpcException { Code = "not-implemented" } or backend-specific errors rather than populated HistoryReadResults. Track the remaining work against the Phase 2 Galaxy out-of-process gate in docs/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 (MapFromMxAccessQualityMapToOpcUaStatusCode):

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 the HistoryRead access bit. The OPC UA stack checks this bit before routing history requests to the Core dispatcher; nodes without it are rejected before reaching IHistoryProvider.

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 HistorianConfigurationServerName / 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.