Extract historian into a runtime-loaded plugin so hosts without the Wonderware SDK can run with Historian.Enabled=false
The aahClientManaged SDK is now isolated in ZB.MOM.WW.LmxOpcUa.Historian.Aveva and loaded via HistorianPluginLoader from a Historian/ subfolder only when enabled, removing the SDK from Host's compile-time and deploy-time surface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,41 @@
|
||||
# Historical Data Access
|
||||
|
||||
`LmxNodeManager` exposes OPC UA historical data access (HDA) by querying the Wonderware Historian via the `aahClientManaged` SDK. The `HistorianDataSource` class translates OPC UA history requests into SDK queries using the `ArchestrA.HistorianAccess` API, and the node manager overrides wire the results back into the OPC UA response.
|
||||
`LmxNodeManager` exposes OPC UA historical data access (HDA) through an abstract `IHistorianDataSource` interface (`Historian/IHistorianDataSource.cs`). The Wonderware Historian implementation lives in a separate assembly, `ZB.MOM.WW.LmxOpcUa.Historian.Aveva`, which is loaded at runtime only when `Historian.Enabled=true`. This keeps the `aahClientManaged` SDK out of the core Host so deployments that do not need history do not need the SDK installed.
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
The historian surface is split across two assemblies:
|
||||
|
||||
- **`ZB.MOM.WW.LmxOpcUa.Host`** (core) owns only OPC UA / BCL types:
|
||||
- `IHistorianDataSource` -- the interface `LmxNodeManager` depends on
|
||||
- `HistorianEventDto` -- SDK-free representation of a historian event record
|
||||
- `HistorianAggregateMap` -- maps OPC UA aggregate NodeIds to AnalogSummary column names
|
||||
- `HistorianPluginLoader` -- loads the plugin via `Assembly.LoadFrom` at startup
|
||||
- `HistoryContinuationPointManager` -- paginates HistoryRead results
|
||||
- **`ZB.MOM.WW.LmxOpcUa.Historian.Aveva`** (plugin) owns everything SDK-bound:
|
||||
- `HistorianDataSource` -- implements `IHistorianDataSource`, wraps `aahClientManaged`
|
||||
- `IHistorianConnectionFactory` / `SdkHistorianConnectionFactory` -- opens and polls `ArchestrA.HistorianAccess` connections
|
||||
- `AvevaHistorianPluginEntry.Create(HistorianConfiguration)` -- the static factory invoked by the loader
|
||||
|
||||
The plugin assembly and its SDK dependencies (`aahClientManaged.dll`, `aahClient.dll`, `aahClientCommon.dll`, `Historian.CBE.dll`, `Historian.DPAPI.dll`, `ArchestrA.CloudHistorian.Contract.dll`) deploy to a `Historian/` subfolder next to `ZB.MOM.WW.LmxOpcUa.Host.exe`. See [Service Hosting](ServiceHosting.md#required-runtime-assemblies) for the full layout and deployment matrix.
|
||||
|
||||
## Plugin Loading
|
||||
|
||||
When the service starts with `Historian.Enabled=true`, `OpcUaService` calls `HistorianPluginLoader.TryLoad(config)`. The loader:
|
||||
|
||||
1. Probes `AppDomain.CurrentDomain.BaseDirectory\Historian\ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dll`.
|
||||
2. Installs a one-shot `AppDomain.AssemblyResolve` handler that redirects any `aahClientManaged`/`aahClientCommon`/`Historian.*` lookups to the same subfolder, so the CLR can resolve SDK dependencies when the plugin first JITs.
|
||||
3. Calls the plugin's `AvevaHistorianPluginEntry.Create(HistorianConfiguration)` via reflection and returns the resulting `IHistorianDataSource`.
|
||||
4. On any failure (plugin missing, entry type not found, SDK assembly unresolvable, bad image), logs a warning with the expected plugin path and returns `null`. The server starts normally and `LmxNodeManager` returns `BadHistoryOperationUnsupported` for every history call.
|
||||
|
||||
## Wonderware Historian SDK
|
||||
|
||||
The server uses the AVEVA Historian managed SDK (`aahClientManaged.dll`) to query historical data. The SDK provides a cursor-based query API through `ArchestrA.HistorianAccess`, replacing direct SQL queries against the Historian Runtime database. Two query types are used:
|
||||
The plugin uses the AVEVA Historian managed SDK (`aahClientManaged.dll`) to query historical data. The SDK provides a cursor-based query API through `ArchestrA.HistorianAccess`, replacing direct SQL queries against the Historian Runtime database. Two query types are used:
|
||||
|
||||
- **`HistoryQuery`** -- Raw historical samples with timestamp, value (numeric or string), and OPC quality.
|
||||
- **`AnalogSummaryQuery`** -- Pre-computed aggregates with properties for Average, Minimum, Maximum, ValueCount, First, Last, StdDev, and more.
|
||||
|
||||
The SDK DLLs are located in `lib/` and originate from `C:\Program Files (x86)\Wonderware\Historian\`.
|
||||
The SDK DLLs are located in `lib/` and originate from `C:\Program Files (x86)\Wonderware\Historian\`. Only the plugin project (`src/ZB.MOM.WW.LmxOpcUa.Historian.Aveva/`) references them at build time; the core Host project does not.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -29,7 +55,7 @@ public class HistorianConfiguration
|
||||
}
|
||||
```
|
||||
|
||||
When `Enabled` is `false`, the `HistorianDataSource` is not instantiated and the node manager returns `BadHistoryOperationUnsupported` for history read requests.
|
||||
When `Enabled` is `false`, `HistorianPluginLoader.TryLoad` is not called, no plugin is loaded, and the node manager returns `BadHistoryOperationUnsupported` for history read requests. When `Enabled` is `true` but the plugin cannot be loaded (missing `Historian/` subfolder, SDK assembly resolve failure, etc.), the server still starts and returns the same `BadHistoryOperationUnsupported` status with a warning in the log.
|
||||
|
||||
### Connection Properties
|
||||
|
||||
@@ -45,7 +71,7 @@ When `Enabled` is `false`, the `HistorianDataSource` is not instantiated and the
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
`HistorianDataSource` maintains a persistent connection to the Historian server via `ArchestrA.HistorianAccess`:
|
||||
`HistorianDataSource` (in the plugin assembly) maintains a persistent connection to the Historian server via `ArchestrA.HistorianAccess`:
|
||||
|
||||
1. **Lazy connect** -- The connection is established on the first query via `EnsureConnected()`.
|
||||
2. **Connection reuse** -- Subsequent queries reuse the same connection.
|
||||
@@ -56,7 +82,7 @@ The connection is opened with `ReadOnly = true` and `ConnectionType = Process`.
|
||||
|
||||
## Raw Reads
|
||||
|
||||
`HistorianDataSource.ReadRawAsync` uses a `HistoryQuery` to retrieve individual samples within a time range:
|
||||
`IHistorianDataSource.ReadRawAsync` (plugin implementation) uses a `HistoryQuery` to retrieve individual samples within a time range:
|
||||
|
||||
1. Create a `HistoryQuery` via `_connection.CreateHistoryQuery()`
|
||||
2. Configure `HistoryQueryArgs` with `TagNames`, `StartDateTime`, `EndDateTime`, and `RetrievalMode = Full`
|
||||
@@ -70,7 +96,7 @@ Each result row is converted to an OPC UA `DataValue`:
|
||||
|
||||
## Aggregate Reads
|
||||
|
||||
`HistorianDataSource.ReadAggregateAsync` uses an `AnalogSummaryQuery` to retrieve pre-computed aggregates:
|
||||
`IHistorianDataSource.ReadAggregateAsync` (plugin implementation) uses an `AnalogSummaryQuery` to retrieve pre-computed aggregates:
|
||||
|
||||
1. Create an `AnalogSummaryQuery` via `_connection.CreateAnalogSummaryQuery()`
|
||||
2. Configure `AnalogSummaryQueryArgs` with `TagNames`, `StartDateTime`, `EndDateTime`, and `Resolution` (milliseconds)
|
||||
@@ -93,7 +119,7 @@ See `Domain/QualityMapper.cs` and `Domain/Quality.cs` for the full mapping table
|
||||
|
||||
## Aggregate Function Mapping
|
||||
|
||||
`MapAggregateToColumn` translates OPC UA aggregate NodeIds to `AnalogSummaryQueryResult` property names:
|
||||
`HistorianAggregateMap.MapAggregateToColumn` (in the core Host assembly, so the node manager can validate aggregate support without requiring the plugin to be loaded) translates OPC UA aggregate NodeIds to `AnalogSummaryQueryResult` property names:
|
||||
|
||||
| OPC UA Aggregate | Result Property |
|
||||
|---|---|
|
||||
|
||||
Reference in New Issue
Block a user