docs(historian): TCP transport
This commit is contained in:
+12
-2
@@ -176,9 +176,16 @@ When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
|
||||
"AlarmHistorian": {
|
||||
"Enabled": true,
|
||||
"DatabasePath": "C:\\ProgramData\\OtOpcUa\\alarmhistorian.db",
|
||||
"PipeName": "\\\\.\\pipe\\wonderware-historian",
|
||||
"SharedSecret": "<token from historian sidecar config>",
|
||||
"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`. |
|
||||
| `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`. |
|
||||
| `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.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ A production OtOpcUa deployment runs **one binary per node**, plus the optional
|
||||
| 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 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.
|
||||
|
||||
@@ -70,7 +70,7 @@ Used by Traefik for the active-leader-only routing pattern (see [Architecture-v2
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
[ServiceHosting.md](../ServiceHosting.md). For the alarm-history store-and-forward
|
||||
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----+--+
|
||||
| WonderwareHistorianClient (.NET 10) | |
|
||||
+-------------------------------------------+ |
|
||||
| named pipe
|
||||
MessagePack frames | (shared secret + allowed-SID)
|
||||
| TCP (optional TLS)
|
||||
MessagePack frames | shared-secret Hello auth
|
||||
v
|
||||
+-------------------------------------------+
|
||||
| OtOpcUaWonderwareHistorian (sidecar) |
|
||||
| net48 / x64 |
|
||||
| PipeServer + HistorianFrameHandler |
|
||||
| TcpFrameServer + HistorianFrameHandler |
|
||||
| HistorianDataSource (reads) |
|
||||
| SdkAlarmHistorianWriteBackend (writes) |
|
||||
| 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` +
|
||||
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 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 | 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.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.Contracts/` | net10.0 | `WonderwareHistorianClientOptions` (pipe name, shared secret, timeouts) |
|
||||
| `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
|
||||
@@ -52,7 +86,7 @@ host never references the SDK; it speaks the pipe contract only.
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -87,29 +121,32 @@ The alarm write path can be disabled independently of reads by setting
|
||||
|
||||
- **Process**: `OtOpcUaWonderwareHistorian`, installed/managed by
|
||||
`scripts/install/` (`Install-Services.ps1 -InstallWonderwareHistorian`).
|
||||
- **Spawn config**: the supervisor passes the pipe name, the allowed server
|
||||
principal SID, and a per-process shared secret via environment
|
||||
(`OTOPCUA_HISTORIAN_PIPE`, `OTOPCUA_ALLOWED_SID`, `OTOPCUA_HISTORIAN_SECRET`);
|
||||
Historian connection settings come from `OTOPCUA_HISTORIAN_SERVER` /
|
||||
`_PORT` / `_INTEGRATED` / `_USER` / `_PASS` etc. (see
|
||||
- **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`).
|
||||
- **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.
|
||||
- **Wire**: MessagePack-framed request/reply; the named-pipe ACL restricts the
|
||||
pipe to the allowed SID and the client proves the shared secret in a Hello
|
||||
frame. 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.
|
||||
- **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 pipe-frame
|
||||
handler with a faked SDK seam.
|
||||
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 pipe client + framing against an in-process duplex pipe pair.
|
||||
cover the TCP client + framing against loopback `TcpListener` fixtures.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
Reference in New Issue
Block a user