docs(historian): TCP transport

This commit is contained in:
Joseph Doherty
2026-06-12 12:02:50 -04:00
parent ce25581596
commit 6d5fc35747
3 changed files with 73 additions and 26 deletions
+12 -2
View File
@@ -176,9 +176,16 @@ When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
"AlarmHistorian": { "AlarmHistorian": {
"Enabled": true, "Enabled": true,
"DatabasePath": "C:\\ProgramData\\OtOpcUa\\alarmhistorian.db", "DatabasePath": "C:\\ProgramData\\OtOpcUa\\alarmhistorian.db",
"PipeName": "\\\\.\\pipe\\wonderware-historian",
"SharedSecret": "<token from historian sidecar config>", "SharedSecret": "<token from historian sidecar config>",
"BatchSize": 100 "BatchSize": 100
},
"Historian": {
"Wonderware": {
"Host": "localhost",
"Port": 32569,
"UseTls": false,
"ServerCertThumbprint": ""
}
} }
} }
``` ```
@@ -187,9 +194,12 @@ When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
|---|---|---|---| |---|---|---|---|
| `Enabled` | bool | `false` | Enable the real SQLite + Wonderware sink. `false``NullAlarmHistorianSink`. | | `Enabled` | bool | `false` | Enable the real SQLite + Wonderware sink. `false``NullAlarmHistorianSink`. |
| `DatabasePath` | string | — | Absolute path to the SQLite queue file. Created on first use (WAL mode). Required when `Enabled`. | | `DatabasePath` | string | — | Absolute path to the SQLite queue file. Created on first use (WAL mode). Required when `Enabled`. |
| `PipeName` | string | — | Named-pipe path for the Wonderware Historian sidecar IPC channel. Required when `Enabled`. |
| `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. | | `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. |
| `BatchSize` | int | `100` | Max rows per drain cycle handed to `IAlarmHistorianWriter.WriteBatchAsync`. | | `BatchSize` | int | `100` | Max rows per drain cycle handed to `IAlarmHistorianWriter.WriteBatchAsync`. |
| `Historian:Wonderware:Host` | string | `localhost` | DNS name or IP of the machine running the historian sidecar. |
| `Historian:Wonderware:Port` | int | `32569` | TCP port the sidecar listens on (`OTOPCUA_HISTORIAN_TCP_PORT`). |
| `Historian:Wonderware:UseTls` | bool | `false` | Wrap the TCP stream in TLS before the Hello handshake. |
| `Historian:Wonderware:ServerCertThumbprint` | string | — | Optional SHA-1 thumbprint to pin the sidecar's TLS server certificate. Leave empty to use normal CA-chain validation. |
> Dev and docker-dev deployments leave `Enabled` unset (defaults to `false`) so alarm transitions historize to nowhere unless a historian sidecar is present. > Dev and docker-dev deployments leave `Enabled` unset (defaults to `false`) so alarm transitions historize to nowhere unless a historian sidecar is present.
+2 -2
View File
@@ -7,7 +7,7 @@ A production OtOpcUa deployment runs **one binary per node**, plus the optional
| Process | Project | Runtime | Platform | Responsibility | | Process | Project | Runtime | Platform | Responsibility |
|---|---|---|---|---| |---|---|---|---|---|
| **OtOpcUa Host** | `src/Server/ZB.MOM.WW.OtOpcUa.Host` | .NET 10 | AnyCPU | Single fused binary. `OTOPCUA_ROLES` env decides what to mount: `admin` (Blazor + auth + control-plane singletons), `driver` (OPC UA endpoint + per-driver actors), or both. | | **OtOpcUa Host** | `src/Server/ZB.MOM.WW.OtOpcUa.Host` | .NET 10 | AnyCPU | Single fused binary. `OTOPCUA_ROLES` env decides what to mount: `admin` (Blazor + auth + control-plane singletons), `driver` (OPC UA endpoint + per-driver actors), or both. |
| **OtOpcUa Wonderware Historian** *(optional)* | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x64 (64-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over a named pipe. Required only when `Historian:Wonderware:Enabled=true`. | | **OtOpcUa Wonderware Historian** *(optional)* | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware` | .NET Framework 4.8 | x64 (64-bit) | Out-of-process sidecar exposing the Wonderware Historian SDK over TCP (optional TLS). Required only when `Historian:Wonderware:Enabled=true`. May run on the same machine or a remote host. |
Galaxy access still uses the separately-installed **mxaccessgw** sidecar (see `docs/v2/Galaxy.ParityRig.md`); the gateway owns the MXAccess COM bitness constraint (its worker is x86 net48). Nothing in the OtOpcUa repo carries that constraint anymore. Galaxy access still uses the separately-installed **mxaccessgw** sidecar (see `docs/v2/Galaxy.ParityRig.md`); the gateway owns the MXAccess COM bitness constraint (its worker is x86 net48). Nothing in the OtOpcUa repo carries that constraint anymore.
@@ -70,7 +70,7 @@ Used by Traefik for the active-leader-only routing pattern (see [Architecture-v2
## OtOpcUa Wonderware Historian (optional) ## OtOpcUa Wonderware Historian (optional)
Unchanged from v1. IPC contract types live in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/`; sidecar pipe handler in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/`. Install via `scripts/install/Install-Services.ps1 -InstallWonderwareHistorian`. IPC contract types live in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/`; sidecar TCP server in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/`. The sidecar listens on TCP port 32569 by default; `Install-Services.ps1 -InstallWonderwareHistorian` adds the Windows Firewall inbound rule. The host and sidecar may run on different machines — configure `Historian:Wonderware:Host` + `Port` (and optionally `UseTls`) on the OtOpcUa host side. See [Historian.Wonderware.md](drivers/Historian.Wonderware.md) for the full transport and security reference.
## Install / Uninstall ## Install / Uninstall
+59 -22
View File
@@ -6,6 +6,10 @@ historian sink**: an optional sidecar that gives OtOpcUa read access to AVEVA
System Platform (Wonderware) Historian history and a write-back path for alarm System Platform (Wonderware) Historian history and a write-back path for alarm
events. It runs only when `Historian:Wonderware:Enabled=true`. events. It runs only when `Historian:Wonderware: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 For the sidecar's place in a deployment, see
[ServiceHosting.md](../ServiceHosting.md). For the alarm-history store-and-forward [ServiceHosting.md](../ServiceHosting.md). For the alarm-history store-and-forward
flow that drains into it, see [AlarmHistorian.md](../AlarmHistorian.md). flow that drains into it, see [AlarmHistorian.md](../AlarmHistorian.md).
@@ -20,13 +24,13 @@ flow that drains into it, see [AlarmHistorian.md](../AlarmHistorian.md).
| AndForwardSink --write----+--+ | AndForwardSink --write----+--+
| WonderwareHistorianClient (.NET 10) | | | WonderwareHistorianClient (.NET 10) | |
+-------------------------------------------+ | +-------------------------------------------+ |
| named pipe | TCP (optional TLS)
MessagePack frames | (shared secret + allowed-SID) MessagePack frames | shared-secret Hello auth
v v
+-------------------------------------------+ +-------------------------------------------+
| OtOpcUaWonderwareHistorian (sidecar) | | OtOpcUaWonderwareHistorian (sidecar) |
| net48 / x64 | | net48 / x64 |
| PipeServer + HistorianFrameHandler | | TcpFrameServer + HistorianFrameHandler |
| HistorianDataSource (reads) | | HistorianDataSource (reads) |
| SdkAlarmHistorianWriteBackend (writes) | | SdkAlarmHistorianWriteBackend (writes) |
| aahClientManaged / HistorianAccess | | aahClientManaged / HistorianAccess |
@@ -36,15 +40,45 @@ flow that drains into it, see [AlarmHistorian.md](../AlarmHistorian.md).
The split exists because the AVEVA Historian SDK (`aahClientManaged` + The split exists because the AVEVA Historian SDK (`aahClientManaged` +
native `aahClient.dll`) is .NET Framework 4.8 / x64 — so it lives out-of-process 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 in the sidecar, and everything in the OtOpcUa host stays .NET 10 AnyCPU. The
host never references the SDK; it speaks the pipe contract only. 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 `Historian:Wonderware: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 split
| Project | Target | Role | | Project | Target | Role |
|---------|--------|------| |---------|--------|------|
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/` | net48 / x64 | The **sidecar** (`OutputType=Exe`). Hosts the named-pipe server, the historian reader, and the alarm-write backend bound to the AVEVA SDK | | `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 pipe client consumed by the history router and the alarm sink | | `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` (pipe name, shared secret, timeouts) | | `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 > The csproj targets **net48 / x64** (`PlatformTarget=x64`) — the AVEVA Historian
> 2020 SDK ships an x64 `aahClientManaged` build; the earlier x86 default was an > 2020 SDK ships an x64 `aahClientManaged` build; the earlier x86 default was an
@@ -52,7 +86,7 @@ host never references the SDK; it speaks the pipe contract only.
## What it does ## What it does
The sidecar exposes two surfaces, both over the same named pipe: The sidecar exposes two surfaces, both over the same TCP connection:
### Read path — `IHistorianDataSource` ### Read path — `IHistorianDataSource`
@@ -87,29 +121,32 @@ The alarm write path can be disabled independently of reads by setting
- **Process**: `OtOpcUaWonderwareHistorian`, installed/managed by - **Process**: `OtOpcUaWonderwareHistorian`, installed/managed by
`scripts/install/` (`Install-Services.ps1 -InstallWonderwareHistorian`). `scripts/install/` (`Install-Services.ps1 -InstallWonderwareHistorian`).
- **Spawn config**: the supervisor passes the pipe name, the allowed server - **Spawn config**: TCP port and bind address are set via
principal SID, and a per-process shared secret via environment `OTOPCUA_HISTORIAN_TCP_PORT` (default 32569) and `OTOPCUA_HISTORIAN_BIND`
(`OTOPCUA_HISTORIAN_PIPE`, `OTOPCUA_ALLOWED_SID`, `OTOPCUA_HISTORIAN_SECRET`); (default `0.0.0.0`). TLS is controlled by `OTOPCUA_HISTORIAN_TLS_ENABLED` /
Historian connection settings come from `OTOPCUA_HISTORIAN_SERVER` / `OTOPCUA_HISTORIAN_TLS_CERT` / `OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD`. The
`_PORT` / `_INTEGRATED` / `_USER` / `_PASS` etc. (see 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`). `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs`).
- **Pipe-only mode**: with `OTOPCUA_HISTORIAN_ENABLED!=true` the sidecar boots - **TCP-only mode**: with `OTOPCUA_HISTORIAN_ENABLED!=true` the sidecar boots
without loading the SDK at all — used for smoke and IPC tests. without loading the SDK at all — used for smoke and IPC tests.
- **Wire**: MessagePack-framed request/reply; the named-pipe ACL restricts the - **Wire**: MessagePack-framed request/reply over TCP (optionally TLS). The
pipe to the allowed SID and the client proves the shared secret in a Hello client proves the shared secret in a `Hello` frame before any history calls.
frame. The client owns a single channel with one in-flight call at a time and The client owns a single channel with one in-flight call at a time and retries
retries a transport failure once before propagating — broader backoff is the a transport failure once before propagating — broader backoff is the caller's
caller's responsibility. responsibility.
## Testing ## Testing
- **Sidecar unit tests** — - **Sidecar unit tests** —
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/` cover the `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/` cover the
reader, the alarm-write backend outcome classification, and the pipe-frame reader, the alarm-write backend outcome classification, and the TCP frame
handler with a faked SDK seam. handler with a faked SDK seam; `TcpRoundTripTests` exercises the plaintext +
TLS paths including the bad-secret rejection case.
- **Client unit tests** — - **Client unit tests** —
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/` `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/`
cover the pipe client + framing against an in-process duplex pipe pair. cover the TCP client + framing against loopback `TcpListener` fixtures.
## Further reading ## Further reading