# 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\` 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