Files
lmxopcua/docs/drivers/Historian.Wonderware.md
Joseph Doherty 1be06502c7
v2-ci / build (push) Failing after 43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
fix(historian): correct AlarmHistorian config-key refs in docs + install (review)
2026-06-12 12:25:13 -04:00

157 lines
8.1 KiB
Markdown

# Wonderware Historian Backend
The Wonderware Historian backend is **not a tag driver** — it has no address
space, no `IDriver` lifecycle, and exposes no PLC. It is a **server-side
historian sink**: an optional sidecar that gives OtOpcUa read access to AVEVA
System Platform (Wonderware) Historian history and a write-back path for alarm
events. It runs only when `AlarmHistorian:Enabled=true`.
The host connects to the sidecar over **TCP** (plaintext in dev, optional TLS
in prod), so the OtOpcUa host no longer needs to be on the same machine as the
sidecar — a remote host on a different VM is fully supported.
For the sidecar's place in a deployment, see
[ServiceHosting.md](../ServiceHosting.md). For the alarm-history store-and-forward
flow that drains into it, see [AlarmHistorian.md](../AlarmHistorian.md).
## Architecture
```
+-------------------------------------------+
| OtOpcUa Host (.NET 10 AnyCPU) |
| Server.History.IHistoryRouter --read--+--+
| Core.AlarmHistorian.SqliteStore | |
| AndForwardSink --write----+--+
| WonderwareHistorianClient (.NET 10) | |
+-------------------------------------------+ |
| TCP (optional TLS)
MessagePack frames | shared-secret Hello auth
v
+-------------------------------------------+
| OtOpcUaWonderwareHistorian (sidecar) |
| net48 / x64 |
| TcpFrameServer + HistorianFrameHandler |
| HistorianDataSource (reads) |
| SdkAlarmHistorianWriteBackend (writes) |
| aahClientManaged / HistorianAccess |
+-------------------------------------------+
```
The split exists because the AVEVA Historian SDK (`aahClientManaged` +
native `aahClient.dll`) is .NET Framework 4.8 / x64 — so it lives out-of-process
in the sidecar, and everything in the OtOpcUa host stays .NET 10 AnyCPU. The
host never references the SDK; it speaks the TCP contract only. Because the
transport is TCP, the host and sidecar can run on different machines.
### Transport & security
The sidecar listens on a configurable TCP port (`OTOPCUA_HISTORIAN_TCP_PORT`,
default **32569**) and bind address (`OTOPCUA_HISTORIAN_BIND`, default `0.0.0.0`).
`Install-Services.ps1` adds a Windows Firewall inbound rule for the port
automatically.
**TLS (optional, recommended for cross-machine deployments):**
Set `OTOPCUA_HISTORIAN_TLS_ENABLED=true` on the sidecar and supply the server
certificate via `OTOPCUA_HISTORIAN_TLS_CERT` (PFX file path, or
`LocalMachine\My\<thumbprint>` for a cert already in the machine store) and
`OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD` if the PFX is password-protected. On the
client/host side set `AlarmHistorian:UseTls=true`; optionally set
`ServerCertThumbprint` to pin the server certificate's SHA-1 thumbprint instead
of relying on normal CA-chain validation.
**Shared secret (required in all modes):**
Regardless of whether TLS is on, the client always sends a `Hello` frame
carrying the `SharedSecret`; the sidecar rejects connections where the secret
does not match. The Windows-SID pipe ACL from the previous named-pipe transport
is replaced by this combination of TLS + shared secret.
**TLS troubleshooting note:** If TLS fails on every connection attempt, the
most likely cause is a missing private key or an ACL on the key file — the
sidecar loads the certificate with `MachineKeySet` (required for service
accounts with no loaded user profile), and `SslStream` defers private-key
access to the first handshake, so a bad key surfaces as repeated connection
failures (→ exit 2 → NSSM restart), not a startup error.
## Project split
| Project | Target | Role |
|---------|--------|------|
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/` | net48 / x64 | The **sidecar** (`OutputType=Exe`). Hosts the TCP server, the historian reader, and the alarm-write backend bound to the AVEVA SDK |
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/` | net10.0 | `WonderwareHistorianClient` — the in-host TCP client consumed by the history router and the alarm sink |
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/` | net10.0 | `WonderwareHistorianClientOptions` (host, port, TLS, shared secret, timeouts) |
> The csproj targets **net48 / x64** (`PlatformTarget=x64`) — the AVEVA Historian
> 2020 SDK ships an x64 `aahClientManaged` build; the earlier x86 default was an
> inherited v1 artifact, not a constraint of the Historian SDK.
## What it does
The sidecar exposes two surfaces, both over the same TCP connection:
### Read path — `IHistorianDataSource`
`HistorianDataSource` (in the sidecar) reads history through the
`aahClientManaged` SDK; `WonderwareHistorianClient` (in the host) implements
`IHistorianDataSource` and maps returned samples back to OPC UA `DataValue`s for
`Server.History.IHistoryRouter`. The read surface is:
| Call | Maps to |
|------|---------|
| `ReadRawAsync` | Raw historical samples for a tag over a time range |
| `ReadProcessedAsync` / `ReadAggregateAsync` | Aggregated samples at an interval |
| `ReadAtTimeAsync` | Samples at specific timestamps |
| `ReadEventsAsync` | Historical events for a source |
| `GetHealthSnapshot` | Connection health for the host-side health surface |
### Write path — alarm-historian write-back
`WonderwareHistorianClient` also implements `IAlarmHistorianWriter`. Alarm events
are drained into the sidecar from `Core.AlarmHistorian.SqliteStoreAndForwardSink`
and persisted by `SdkAlarmHistorianWriteBackend` via
`HistorianAccess.AddStreamedValue(HistorianEvent, out HistorianAccessError)`. The
production writer is wrapped by `AahClientManagedAlarmEventWriter`, which handles
batch orchestration and per-event `HistorianAccessError` outcome classification
(connection-class errors are retryable; malformed-argument errors are not).
The alarm write path can be disabled independently of reads by setting
`OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=false` — the sidecar then rejects
`WriteAlarmEvents` frames while still serving history reads.
## Hosting and IPC
- **Process**: `OtOpcUaWonderwareHistorian`, installed/managed by
`scripts/install/` (`Install-Services.ps1 -InstallWonderwareHistorian`).
- **Spawn config**: TCP port and bind address are set via
`OTOPCUA_HISTORIAN_TCP_PORT` (default 32569) and `OTOPCUA_HISTORIAN_BIND`
(default `0.0.0.0`). TLS is controlled by `OTOPCUA_HISTORIAN_TLS_ENABLED` /
`OTOPCUA_HISTORIAN_TLS_CERT` / `OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD`. The
shared secret is passed via `OTOPCUA_HISTORIAN_SECRET`. Historian connection
settings come from `OTOPCUA_HISTORIAN_SERVER` / `_PORT` / `_INTEGRATED` /
`_USER` / `_PASS` etc. (see
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs`).
- **TCP-only mode**: with `OTOPCUA_HISTORIAN_ENABLED!=true` the sidecar boots
without loading the SDK at all — used for smoke and IPC tests.
- **Wire**: MessagePack-framed request/reply over TCP (optionally TLS). The
client proves the shared secret in a `Hello` frame before any history calls.
The client owns a single channel with one in-flight call at a time and retries
a transport failure once before propagating — broader backoff is the caller's
responsibility.
## Testing
- **Sidecar unit tests** —
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/` cover the
reader, the alarm-write backend outcome classification, and the TCP frame
handler with a faked SDK seam; `TcpRoundTripTests` exercises the plaintext +
TLS paths including the bad-secret rejection case.
- **Client unit tests** —
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/`
cover the TCP client + framing against loopback `TcpListener` fixtures.
## Further reading
- [ServiceHosting.md](../ServiceHosting.md) — where the sidecar fits in a
deployment and how it's installed
- [AlarmHistorian.md](../AlarmHistorian.md) — the alarm store-and-forward flow
that feeds the write-back path