docs(historian-gateway): document gateway backend, config keys, EnsureTags hook, known gates; retire Wonderware from docs
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
HistorianGateway is now the sole historian backend (read + alarm SendEvent + continuous WriteLiveValues). Document the final state and retire the Wonderware sidecar from the docs/config/labels: - CLAUDE.md: rewrite the Historian section — ServerHistorian / ContinuousHistorization / AlarmHistorian config keys, the IHistorianProvisioning EnsureTags hook, the GatewayAlarmHistorianWriter SendEvent path + ReadEvents dependency on gateway RuntimeDb:EventReadsEnabled=true, gateway-side prerequisites (RuntimeDb flags + historian:read/write/tags:write scopes), migration note, and two KNOWN-LIMITATION callouts (live-validation gate + empty historized-ref-set recorder follow-on). - appsettings.json: fix the stale ServerHistorian block (Host/Port/SharedSecret/ ServerCertThumbprint -> Endpoint/ApiKey/UseTls/AllowUntrustedServerCertificate/ CaCertificatePath/CallTimeout, keep MaxTieClusterOverfetch); add a disabled ContinuousHistorization block; prune the orphaned Wonderware keys from AlarmHistorian (keep the SQLite knobs). ApiKey env-supplied via ServerHistorian__ApiKey (commented; valid strict JSON via _comment keys). - README.md + docs (Historian.md, AlarmHistorian.md, Configuration.md, ServiceHosting.md, DriverLifecycle.md, drivers/README.md, Uns.md, VirtualTags.md, AlarmTracking.md, Client.UI.md, README.md, TestConnectProbes.md): retire the Wonderware historian backend from current-backend descriptions; fix the stale ServerHistorian/AlarmHistorian config tables (now gateway shape); convert drivers/Historian.Wonderware.md to a retired stub pointing at the gateway. - Source/UI labels (descriptive text only, no behavior change): OtOpcUaServerHostedService.cs, HistoryPaging.cs, OtOpcUaSdkServer.cs, HistorianAdapterActor.cs, VirtualTagModal.razor, ScriptedAlarmModal.razor, AlarmsHistorian.razor now name the HistorianGateway backend. Build clean (0 errors); AdminUI.Tests green (514 passed). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -112,7 +112,7 @@ lmxopcua-fix sync modbus # rsync this repo's tests/.../Docker/
|
||||
|
||||
Override any endpoint via the env var to point at a real PLC. The local OtOpcUa server runs on this VM at `opc.tcp://localhost:4840` — **that's not on the docker host**.
|
||||
|
||||
**Local docker-dev rig — login is DISABLED, so do live `/run` verification yourself (don't wait for the user to sign in).** The local `docker-dev/docker-compose.yml` stack (AdminUI at `http://localhost:9200` via Traefik; OPC UA `opc.tcp://localhost:4840` central-1 / `:4841` central-2) runs the AdminUI with `Security__Auth__DisableLogin: "true"` — **no sign-in form; it's auto-authenticated as a full-access admin.** So AdminUI / Razor `/run` verification (deploy a config, drive a page, confirm behavior — e.g. via the Chrome browser-automation tools against `http://localhost:9200`) does **not** require the user to log in. Run it yourself; do not defer it as "user-driven sign-in required." (Caveat: OPC UA *data-plane* auth is still real LDAP against the shared GLAuth on `10.100.0.35:3893` — that only gates Client.CLI read/write **role** operations, e.g. binding a `multi-role` / `opc-writeop` user, and is independent of the AdminUI login. Things genuinely outside the local rig — real PLCs, or the AVEVA Historian + Wonderware sidecar on `10.100.0.48` — still need the user.)
|
||||
**Local docker-dev rig — login is DISABLED, so do live `/run` verification yourself (don't wait for the user to sign in).** The local `docker-dev/docker-compose.yml` stack (AdminUI at `http://localhost:9200` via Traefik; OPC UA `opc.tcp://localhost:4840` central-1 / `:4841` central-2) runs the AdminUI with `Security__Auth__DisableLogin: "true"` — **no sign-in form; it's auto-authenticated as a full-access admin.** So AdminUI / Razor `/run` verification (deploy a config, drive a page, confirm behavior — e.g. via the Chrome browser-automation tools against `http://localhost:9200`) does **not** require the user to log in. Run it yourself; do not defer it as "user-driven sign-in required." (Caveat: OPC UA *data-plane* auth is still real LDAP against the shared GLAuth on `10.100.0.35:3893` — that only gates Client.CLI read/write **role** operations, e.g. binding a `multi-role` / `opc-writeop` user, and is independent of the AdminUI login. Things genuinely outside the local rig — real PLCs, or the AVEVA Historian reached via the `ZB.MOM.WW.HistorianGateway` sidecar — still need the user.)
|
||||
|
||||
See `docs/v2/dev-environment.md` for the full inventory and rationale.
|
||||
|
||||
@@ -165,7 +165,7 @@ Address pickers in AdminUI support live browse for OpcUaClient and Galaxy driver
|
||||
|
||||
The AdminUI's global **UNS** page (`/uns`) is the single surface for managing the unified namespace fleet-wide (Area → Line → Equipment → Tag/VirtualTag), replacing the old per-cluster UNS/Equipment/Tags tabs. See `docs/Uns.md`.
|
||||
|
||||
The `/uns` **TagModal** uses **driver-typed tag-config editors**: it dispatches by the bound driver's `DriverType` to a per-driver editor (Modbus/S7/AbCip/AbLegacy/TwinCAT/Focas) via `TagConfigEditorMap`, with client-side validation via `TagConfigValidator`; unmapped drivers (OpcUaClient/Galaxy/Historian.Wonderware) fall back to the generic raw-`TagConfig`-JSON textarea. Each editor is a thin razor shell over a pure `<Driver>TagConfigModel` (`FromJson`/`ToJson`/`Validate`, preserves unknown keys). To add a driver's editor, copy the Modbus template under `Components/Shared/Uns/TagEditors/` + `Uns/TagEditors/`, reusing the driver's enums + camelCase JSON property names, and register it in `TagConfigEditorMap` + `TagConfigValidator`. See `docs/plans/2026-06-09-driver-typed-tag-editors-design.md`.
|
||||
The `/uns` **TagModal** uses **driver-typed tag-config editors**: it dispatches by the bound driver's `DriverType` to a per-driver editor (Modbus/S7/AbCip/AbLegacy/TwinCAT/Focas) via `TagConfigEditorMap`, with client-side validation via `TagConfigValidator`; unmapped drivers (OpcUaClient/Galaxy) fall back to the generic raw-`TagConfig`-JSON textarea. Each editor is a thin razor shell over a pure `<Driver>TagConfigModel` (`FromJson`/`ToJson`/`Validate`, preserves unknown keys). To add a driver's editor, copy the Modbus template under `Components/Shared/Uns/TagEditors/` + `Uns/TagEditors/`, reusing the driver's enums + camelCase JSON property names, and register it in `TagConfigEditorMap` + `TagConfigValidator`. See `docs/plans/2026-06-09-driver-typed-tag-editors-design.md`.
|
||||
|
||||
## Scripting / Script Editor
|
||||
|
||||
@@ -186,4 +186,131 @@ Inbound operator acknowledge/shelve for scripted alarms is fully implemented. Tw
|
||||
|
||||
## Historian / HistoryRead
|
||||
|
||||
Server-side OPC UA HistoryRead for historized equipment tags is implemented driver-agnostically in Phase C. A tag is historized by adding `"isHistorized": true` to its `TagConfig` JSON blob (authored in the raw-JSON textarea on the `/uns` TagModal); an optional `"historianTagname"` field overrides the default historian tagname, which is the tag's driver `FullName`. The server dispatches all history reads to the registered `IHistorianDataSource` (Wonderware historian TCP client) via the `ServerHistorian` appsettings section (`Enabled` defaults to `false`; when disabled, a `NullHistorianDataSource` is used and historized nodes return `GoodNoData` rather than an error). Supported variants: Raw, Processed (Average/Minimum/Maximum/Total/Count aggregates), and AtTime over historized variable nodes; Events over alarm-owning equipment-folder event-notifier nodes. Reads are ungated (served from any redundancy node); authorization uses the standard `AccessLevels.HistoryRead` bit set at materialization. See `docs/Historian.md` for the full guide.
|
||||
**Backend: HistorianGateway (sole historian backend).** As of the gateway-integration cutover, the
|
||||
historian read, alarm-write, and continuous-historization paths are all served by the
|
||||
**`ZB.MOM.WW.HistorianGateway`** sidecar, consumed as the Gitea-feed
|
||||
**`ZB.MOM.WW.HistorianGateway.Client`** gRPC package (`historian_gateway.v1`) behind a thin
|
||||
`IHistorianGatewayClient` seam in `ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway`. **The bespoke
|
||||
Wonderware TCP/ArchestrA sidecar projects and the vestigial `Historian.Wonderware` driver type were
|
||||
retired** — there is no Wonderware backend in the tree anymore (see `docs/drivers/Historian.Wonderware.md`,
|
||||
now a retired stub).
|
||||
|
||||
A tag is historized by adding `"isHistorized": true` to its `TagConfig` JSON blob (authored in the
|
||||
raw-JSON textarea on the `/uns` TagModal); an optional `"historianTagname"` field overrides the default
|
||||
historian tagname, which is the tag's driver `FullName`.
|
||||
|
||||
### Read path (`ServerHistorian` section)
|
||||
|
||||
The server dispatches all OPC UA HistoryRead to the registered `IHistorianDataSource` — the
|
||||
`GatewayHistorianDataSource` read client when enabled, else the `NullHistorianDataSource` default
|
||||
(historized nodes return `GoodNoData`, never an error). Supported variants: Raw, Processed
|
||||
(Average/Minimum/Maximum/Total/Count aggregates), and AtTime over historized variable nodes; Events over
|
||||
alarm-owning equipment-folder event-notifier nodes. Reads are ungated (served from any redundancy node);
|
||||
authorization uses the standard `AccessLevels.HistoryRead` bit set at materialization.
|
||||
|
||||
`ServerHistorian` appsettings keys (`ServerHistorianOptions`; `Enabled` defaults to `false`):
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `Enabled` | `false` | `true` registers the gateway read client; `false` keeps `NullHistorianDataSource` |
|
||||
| `Endpoint` | `""` | Absolute gateway URI, e.g. `https://host:5222`. Scheme selects transport (`https://` = TLS, `http://` = h2c) |
|
||||
| `ApiKey` | `""` | Peppered-HMAC key `histgw_<id>_<secret>` sent as `Authorization: Bearer`. **Supply via env `ServerHistorian__ApiKey` — never commit** |
|
||||
| `UseTls` | `true` | Connect over TLS; must match the `Endpoint` scheme |
|
||||
| `AllowUntrustedServerCertificate` | `false` | Accept a self-signed / untrusted server cert (dev / on-prem only) |
|
||||
| `CaCertificatePath` | `null` | PEM CA file pinning the gateway TLS chain; null/empty uses the OS trust store |
|
||||
| `CallTimeout` | `00:00:30` | Per-call deadline for each unary gateway read |
|
||||
| `MaxTieClusterOverfetch` | `65536` | Bounded over-fetch the HistoryRead-Raw paging uses to page within an oversized same-timestamp tie cluster (retained from the prior backend) |
|
||||
|
||||
### Alarm-history path (`AlarmHistorian` section)
|
||||
|
||||
Alarm events are written through `GatewayAlarmHistorianWriter` (the gateway **`SendEvent`** path) behind
|
||||
the durable **`SqliteStoreAndForwardSink`** — `AlarmHistorian:Enabled=true` swaps the `NullAlarmHistorianSink`
|
||||
default for the SQLite store-and-forward queue, whose drain worker forwards batches to the gateway and uses
|
||||
per-event outcomes to decide retry vs. dead-letter (never throws). The `AlarmHistorian` section carries
|
||||
only the `Enabled` gate + the SQLite knobs (`DatabasePath`, `DrainIntervalSeconds`, `Capacity`,
|
||||
`DeadLetterRetentionDays`, `BatchSize`, `MaxAttempts`) — the downstream gateway connection
|
||||
(endpoint/key/TLS) is sourced from the `ServerHistorian` section. **Alarm-history `ReadEvents` requires the
|
||||
target gateway deployed with `RuntimeDb:EventReadsEnabled=true`** (the C2 SQL event-read workaround).
|
||||
|
||||
### Continuous historization (`ContinuousHistorization` section)
|
||||
|
||||
When `ContinuousHistorization:Enabled=true` **and** `ServerHistorian` is configured, the Host builds a
|
||||
durable, crash-safe **FasterLog** outbox (`FasterLogHistorizationOutbox`) + a gateway-backed
|
||||
`IHistorianValueWriter`, and `WithOtOpcUaRuntimeActors` spawns the `ContinuousHistorizationRecorder`. The
|
||||
recorder taps the per-node dependency-mux value fan-out, appends each numeric value to the outbox (the
|
||||
crash boundary), and drains the outbox to the gateway's SQL live-value write path (**`WriteLiveValues`**)
|
||||
with exponential backoff. The gateway connection is sourced from `ServerHistorian`; this section carries
|
||||
only the recorder + outbox knobs:
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `Enabled` | `false` | `true` (with `ServerHistorian` configured) wires + spawns the recorder |
|
||||
| `OutboxPath` | `""` (required when enabled) | **Directory** holding the FasterLog segment + commit files. In production set an **absolute** path on durable storage |
|
||||
| `CommitMode` | `PerEntry` | `PerEntry` = fsync before each append returns (no loss window); `Periodic` = batched commits every `CommitIntervalMs` |
|
||||
| `CommitIntervalMs` | `100` | Periodic-mode commit cadence; required `> 0` only under `Periodic` |
|
||||
| `DrainBatchSize` | `64` | Entries peeked + written per drain pass |
|
||||
| `DrainIntervalSeconds` | `2` | Steady drain cadence (and post-success reschedule) |
|
||||
| `Capacity` | `0` | Max un-acked outbox entries before drop-oldest; `0` = unbounded |
|
||||
| `MinBackoffSeconds` | `1` | Initial retry backoff after a failed drain pass |
|
||||
| `MaxBackoffSeconds` | `30` | Cap on the exponential retry backoff |
|
||||
|
||||
### Tag auto-provisioning (`IHistorianProvisioning` EnsureTags hook)
|
||||
|
||||
`AddressSpaceApplier.Apply()` fires a **non-blocking, fire-and-forget** `IHistorianProvisioning.EnsureTagsAsync`
|
||||
hook for added historized value tags — the gateway-backed `GatewayTagProvisioner` calls the gateway's
|
||||
`EnsureTags` so a brand-new historized tag exists in the historian before the recorder's `WriteLiveValues`
|
||||
land. The hook is wrapped so a faulted/synchronously-throwing provisioner can **never** block or fail a
|
||||
deploy. Non-numeric (`String`/`DateTime`/`Reference`) data types are skipped (not provisioned); the
|
||||
recorder likewise drops + meters non-numeric values. Continuous historization is **numeric-analog only** in
|
||||
v1 (`UInt16→UInt4` is a documented fallback).
|
||||
|
||||
### Gateway-side prerequisites
|
||||
|
||||
The target HistorianGateway OtOpcUa points at **must** run with:
|
||||
|
||||
- `RuntimeDb:Enabled=true` — enables the `WriteLiveValues` SQL live path (continuous historization).
|
||||
- `RuntimeDb:EventReadsEnabled=true` — enables `ReadEvents` from `Runtime.dbo.Events` (alarm-history reads).
|
||||
- An API key carrying scopes **`historian:read`**, **`historian:write`**, **`historian:tags:write`**.
|
||||
|
||||
### Migration note (deployments upgrading from the Wonderware backend)
|
||||
|
||||
The `ServerHistorian` section changed shape. Rename the old Wonderware keys and supply the key via env:
|
||||
|
||||
| Old (Wonderware) key | New (gateway) key |
|
||||
|---|---|
|
||||
| `ServerHistorian:Host` + `:Port` | `ServerHistorian:Endpoint` (`https://host:5222`) |
|
||||
| `ServerHistorian:SharedSecret` | `ServerHistorian:ApiKey` (**env `ServerHistorian__ApiKey`**) |
|
||||
| `ServerHistorian:ServerCertThumbprint` | `ServerHistorian:CaCertificatePath` (+ `UseTls` / `AllowUntrustedServerCertificate`) |
|
||||
|
||||
The `AlarmHistorian` section's old Wonderware connection keys (`Host`/`Port`/`UseTls`/`ServerCertThumbprint`/`SharedSecret`)
|
||||
were pruned — remove them; the SQLite knobs are retained and the downstream connection now comes from
|
||||
`ServerHistorian`. See `docs/Historian.md` for the full guide.
|
||||
|
||||
### KNOWN LIMITATION 1 — live-validation gate (do before merging/trusting the cutover)
|
||||
|
||||
The cutover is code-complete but **must be live-validated against a real gateway** (VPN to
|
||||
`wonder-sql-vd03`, gateway running the prerequisites above) before it is merged or trusted. Run the
|
||||
env-gated suite:
|
||||
|
||||
```bash
|
||||
export HISTGW_GATEWAY_ENDPOINT=https://wonder-sql-vd03:5222 # absolute gateway URI; absent ⇒ all live tests skip
|
||||
export HISTGW_GATEWAY_APIKEY=histgw_<id>_<secret> # must carry historian:read + historian:write (+ tags:write) scopes
|
||||
export HISTGW_TEST_TAG=<existing-tag> # read round-trip
|
||||
export HISTGW_WRITE_SANDBOX_TAG=<writable-float-tag> # e.g. HistGW.LiveTest.Sandbox — write round-trip (EnsureTags + write)
|
||||
export HISTGW_ALARM_SOURCE=<source-name> # alarm SendEvent → ReadEvents round-trip
|
||||
dotnet test --filter "Category=LiveIntegration"
|
||||
```
|
||||
|
||||
The live suite **skips cleanly** when these env vars are absent (safe to run offline on macOS). It is the
|
||||
gate the operator runs on the VPN before trusting the cutover.
|
||||
|
||||
### KNOWN LIMITATION 2 — continuous-historization value-capture is not yet live
|
||||
|
||||
The `ContinuousHistorizationRecorder` is fully wired (actor + FasterLog outbox + gateway value-writer +
|
||||
meters) but is currently spawned with an **EMPTY historized-ref set** (`Array.Empty<string>()` in
|
||||
`WithOtOpcUaRuntimeActors`): the deployed address space — and thus the set of historized tag refs — is built
|
||||
later at deploy time, not at actor-spawn time, so there is no clean ref set to resolve at wiring time. With
|
||||
an empty set the recorder **registers interest in nothing and historizes nothing**. **Reads and alarm-writes
|
||||
work today**; the recorder's value-capture is the remaining gap, blocked on a `SetHistorizedRefs`-style feed
|
||||
driven off the deployed composition (a tracked follow-on). Until that feed lands, continuous historization
|
||||
records no values.
|
||||
|
||||
@@ -31,7 +31,7 @@ Galaxy is the only driver with an external runtime: it speaks gRPC to a separate
|
||||
- .NET 10 SDK (server, drivers, clients all target .NET 10)
|
||||
- SQL Server reachable for the central config DB
|
||||
- For Galaxy specifically: a running `mxaccessgw` deployment — see [docs/v2/Galaxy.ParityRig.md](docs/v2/Galaxy.ParityRig.md)
|
||||
- For Wonderware Historian read-back: optional `OtOpcUaWonderwareHistorian` sidecar — see [docs/ServiceHosting.md](docs/ServiceHosting.md)
|
||||
- For historian read-back / alarm history / continuous historization: a running [`ZB.MOM.WW.HistorianGateway`](docs/Historian.md) deployment (the sole historian backend; consumed as the `ZB.MOM.WW.HistorianGateway.Client` gRPC package). It must run `RuntimeDb:Enabled=true` + `RuntimeDb:EventReadsEnabled=true`, and the API key must carry `historian:read` + `historian:write` + `historian:tags:write` scopes.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -48,7 +48,7 @@ The server starts on `opc.tcp://localhost:4840` with the `None` security profile
|
||||
|
||||
## Install as Windows Services
|
||||
|
||||
Production deployment is driven by `scripts/install/Install-Services.ps1`, which registers the `OtOpcUa` server service (and optionally the `OtOpcUaWonderwareHistorian` sidecar) under a chosen service account. Galaxy support requires a separately installed `mxaccessgw` — neither this repo nor the install script provisions it.
|
||||
Production deployment is driven by `scripts/install/Install-Services.ps1`, which registers the `OtOpcUa` server service under a chosen service account. Historian support requires a separately deployed `ZB.MOM.WW.HistorianGateway` and Galaxy support a separately installed `mxaccessgw` — neither this repo nor the install script provisions them.
|
||||
|
||||
```powershell
|
||||
.\scripts\install\Install-Services.ps1 `
|
||||
@@ -56,7 +56,7 @@ Production deployment is driven by `scripts/install/Install-Services.ps1`, which
|
||||
-ServiceAccount 'DOMAIN\svc-otopcua'
|
||||
```
|
||||
|
||||
Add `-InstallWonderwareHistorian` for the historian sidecar. See the script header and [docs/ServiceHosting.md](docs/ServiceHosting.md) for full options.
|
||||
The historian backend is the external `ZB.MOM.WW.HistorianGateway` (not installed by this script). See the script header and [docs/ServiceHosting.md](docs/ServiceHosting.md) for full options.
|
||||
|
||||
## Client CLI
|
||||
|
||||
@@ -80,7 +80,7 @@ See [docs/Client.CLI.md](docs/Client.CLI.md) and [docs/Client.UI.md](docs/Client
|
||||
| Address space layout | [docs/AddressSpace.md](docs/AddressSpace.md) |
|
||||
| Read / Write dispatch (driver vs virtual vs scripted-alarm) | [docs/ReadWriteOperations.md](docs/ReadWriteOperations.md) |
|
||||
| Incremental sync (driver-backend rediscovery + config publishes) | [docs/IncrementalSync.md](docs/IncrementalSync.md) |
|
||||
| Service hosting (Server + Admin + optional historian sidecar) | [docs/ServiceHosting.md](docs/ServiceHosting.md) |
|
||||
| Service hosting (Server + Admin; external HistorianGateway backend) | [docs/ServiceHosting.md](docs/ServiceHosting.md) |
|
||||
| Security (transport, LDAP, certificates) | [docs/security.md](docs/security.md) |
|
||||
| Redundancy | [docs/Redundancy.md](docs/Redundancy.md) |
|
||||
| Status dashboard | [docs/StatusDashboard.md](docs/StatusDashboard.md) |
|
||||
|
||||
+27
-28
@@ -16,7 +16,7 @@ and [ServiceHosting.md](ServiceHosting.md).
|
||||
## Why store-and-forward
|
||||
|
||||
Scripted alarms (and any future non-Galaxy `IAlarmSource`, e.g. AB CIP ALMD)
|
||||
must reach AVEVA Historian, but the historian sidecar can be slow, busy, or
|
||||
must reach AVEVA Historian, but the historian gateway can be slow, busy, or
|
||||
disconnected. The sink decouples the alarm engine from historian reachability:
|
||||
every qualifying transition is committed to a **local SQLite queue first**, and
|
||||
a background drain worker forwards rows to the historian on a backoff-aware
|
||||
@@ -52,8 +52,8 @@ unless noted.
|
||||
`TimestampUtc`.
|
||||
- **`IAlarmHistorianWriter`** — what the drain worker delegates writes to.
|
||||
`WriteBatchAsync(batch, ct)` returns one `HistorianWriteOutcome` per event,
|
||||
in order. Production binds this to `WonderwareHistorianClient` (the AVEVA
|
||||
Historian sidecar IPC client).
|
||||
in order. Production binds this to `GatewayAlarmHistorianWriter` (the
|
||||
HistorianGateway `SendEvent` path).
|
||||
- **`HistorianWriteOutcome`** — per-event drain result: `Ack` (persisted,
|
||||
remove from queue), `RetryPlease` (transient failure — leave queued, retry
|
||||
after backoff), `PermanentFail` (malformed/unrecoverable — move to
|
||||
@@ -160,9 +160,9 @@ node whose `RedundancyRole` is `Primary` historizes, giving exactly-once
|
||||
writes across a redundant pair. `AlarmTransitionEvent` carries `AlarmTypeName`
|
||||
(the Part 9 subtype string) and `Comment` (the operator comment from the
|
||||
originating ack/shelve command) that populate the corresponding fields of
|
||||
`AlarmHistorianEvent`. `WonderwareHistorianClient` is the `IAlarmHistorianWriter`
|
||||
the drain worker delegates to. See [ServiceHosting.md](ServiceHosting.md) for
|
||||
the sidecar setup.
|
||||
`AlarmHistorianEvent`. `GatewayAlarmHistorianWriter` is the `IAlarmHistorianWriter`
|
||||
the drain worker delegates to (the gateway `SendEvent` path). See
|
||||
[ServiceHosting.md](ServiceHosting.md) for the (external) HistorianGateway setup.
|
||||
|
||||
**Scope:** scripted alarms only. Galaxy-native alarms historize via System
|
||||
Platform's `HistorizeToAveva` toggle (not this actor); AB CIP ALMD is not on
|
||||
@@ -174,40 +174,40 @@ The real sink is opt-in via the `AlarmHistorian` section of `appsettings.json`.
|
||||
When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
|
||||
`NullAlarmHistorianSink` and the feature is dormant. When `Enabled` is `true`,
|
||||
`AddAlarmHistorian` constructs `SqliteStoreAndForwardSink` and registers
|
||||
`WonderwareHistorianClient` as the `IAlarmHistorianWriter`.
|
||||
`GatewayAlarmHistorianWriter` as the `IAlarmHistorianWriter`. This section carries
|
||||
**only** the `Enabled` gate + the SQLite store-and-forward knobs — the downstream
|
||||
gateway connection (endpoint / key / TLS) is sourced from the `ServerHistorian`
|
||||
section (see [Historian.md](Historian.md)).
|
||||
|
||||
```json
|
||||
{
|
||||
"AlarmHistorian": {
|
||||
"Enabled": true,
|
||||
"DatabasePath": "C:\\ProgramData\\OtOpcUa\\alarmhistorian.db",
|
||||
"SharedSecret": "<token from historian sidecar config>",
|
||||
"BatchSize": 100
|
||||
},
|
||||
"Historian": {
|
||||
"Wonderware": {
|
||||
"Host": "localhost",
|
||||
"Port": 32569,
|
||||
"UseTls": false,
|
||||
"ServerCertThumbprint": ""
|
||||
}
|
||||
"BatchSize": 100,
|
||||
"DrainIntervalSeconds": 5,
|
||||
"Capacity": 1000000,
|
||||
"DeadLetterRetentionDays": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `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`. |
|
||||
| `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. |
|
||||
| `Enabled` | bool | `false` | Enable the SQLite store-and-forward sink (drains to the HistorianGateway `SendEvent` path). `false` → `NullAlarmHistorianSink`. |
|
||||
| `DatabasePath` | string | `alarm-historian.db` | Path to the SQLite queue file. Created on first use (WAL mode). Set an **absolute** path in production. |
|
||||
| `BatchSize` | int | `100` | Max rows per drain cycle handed to `IAlarmHistorianWriter.WriteBatchAsync`. |
|
||||
| `DrainIntervalSeconds` | int | `5` | Seconds between drain-worker ticks. |
|
||||
| `Capacity` | long | `1000000` | Max queued rows before the sink evicts the oldest (data-loss signal via `EvictedCount`). |
|
||||
| `DeadLetterRetentionDays` | int | `30` | Days to retain dead-lettered rows before purge. |
|
||||
| `MaxAttempts` | int | `10` | Maximum delivery attempts before a poison (perpetually-retrying) row is dead-lettered automatically. Must be > 0. |
|
||||
| `AlarmHistorian:Host` | string | `localhost` | DNS name or IP of the machine running the historian sidecar. |
|
||||
| `AlarmHistorian:Port` | int | `32569` | TCP port the sidecar listens on (`OTOPCUA_HISTORIAN_TCP_PORT`). |
|
||||
| `AlarmHistorian:UseTls` | bool | `false` | Wrap the TCP stream in TLS before the Hello handshake. |
|
||||
| `AlarmHistorian: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.
|
||||
> The downstream gateway connection lives in `ServerHistorian` (`Endpoint` + env `ServerHistorian__ApiKey`,
|
||||
> `UseTls`, `CaCertificatePath`); alarm-history `ReadEvents` additionally requires the gateway running
|
||||
> `RuntimeDb:EventReadsEnabled=true`. The old Wonderware connection keys (`SharedSecret` /
|
||||
> `AlarmHistorian:Host`/`Port`/`UseTls`/`ServerCertThumbprint`) were pruned.
|
||||
|
||||
> Dev and docker-dev deployments leave `Enabled` unset (defaults to `false`) so alarm transitions historize to nowhere unless a HistorianGateway is configured.
|
||||
|
||||
---
|
||||
|
||||
@@ -217,8 +217,7 @@ When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
|
||||
Part 9 surface; which alarms route to this sink.
|
||||
- [DriverLifecycle.md](DriverLifecycle.md) — `IHistorianDataSource` (the
|
||||
historian *read* surface; this page covers the *write* path) and the
|
||||
`WonderwareHistorianClient`.
|
||||
`GatewayHistorianDataSource`.
|
||||
- [ScriptedAlarms.md](ScriptedAlarms.md) — the scripted-alarm engine that emits
|
||||
most events into this sink.
|
||||
- [ServiceHosting.md](ServiceHosting.md) — the optional Wonderware historian
|
||||
sidecar.
|
||||
- [ServiceHosting.md](ServiceHosting.md) — the external HistorianGateway backend.
|
||||
|
||||
@@ -203,14 +203,14 @@ Under warm/hot redundancy, both cluster nodes run `ScriptedAlarmHostActor` and t
|
||||
## Historian write-back (non-Galaxy alarms)
|
||||
|
||||
Scripted alarms (and any future non-Galaxy `IAlarmSource` like
|
||||
AB CIP ALMD) route to AVEVA Historian via the Wonderware sidecar:
|
||||
AB CIP ALMD) route to AVEVA Historian via the HistorianGateway:
|
||||
|
||||
- `IAlarmHistorianSink` is the DI-registered intake contract. The
|
||||
default binding is `NullAlarmHistorianSink` (registered in
|
||||
`ServiceCollectionExtensions.AddOtOpcUaRuntime`). Production
|
||||
deployments override it with `SqliteStoreAndForwardSink` wrapping
|
||||
`WonderwareHistorianClient` (the AVEVA Historian sidecar IPC client)
|
||||
— see [ServiceHosting.md](ServiceHosting.md) for the sidecar setup.
|
||||
`GatewayAlarmHistorianWriter` (the HistorianGateway `SendEvent` path)
|
||||
— see [ServiceHosting.md](ServiceHosting.md) for the HistorianGateway setup.
|
||||
- `SqliteStoreAndForwardSink` queues each transition to a local
|
||||
SQLite database and drains in the background via an
|
||||
`IAlarmHistorianWriter`. **The durability guarantee is bounded**: the
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ The alarm subscription source node is saved and restored on reconnection with au
|
||||
|
||||

|
||||
|
||||
Read historical data from the Wonderware Historian.
|
||||
Read historical data from the historian (served server-side by the HistorianGateway backend).
|
||||
|
||||
### Time Range
|
||||
|
||||
|
||||
+19
-22
@@ -119,11 +119,21 @@ The Galaxy/MxAccess connection settings are **not an `appsettings` section.** Th
|
||||
|
||||
> The `OTOPCUA_GALAXY_*` environment variables that v1's in-process `Galaxy.Host` consumed **no longer live in this repo** — they moved into the separately-installed mxaccessgw gateway's own config (see the v1 archive pointer in `docs/README.md` and the Galaxy overview at [`docs/drivers/Galaxy.md`](drivers/Galaxy.md)). The only Galaxy connection secret this repo touches is the gateway API key via `ApiKeySecretRef` above.
|
||||
|
||||
### Historian config (TCP sidecar)
|
||||
### Historian config (HistorianGateway)
|
||||
|
||||
The Wonderware Historian sidecar (`OtOpcUaWonderwareHistorian`) is an independent Windows service that the OtOpcUa host connects to over TCP. It is **not** spawned as a child process by the host — the two services are started independently (e.g. by NSSM / `sc.exe`). The sidecar entry point (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs`) reads its configuration from environment variables; the OtOpcUa host side reads the `AlarmHistorian` appsettings section. See the `OTOPCUA_HISTORIAN_*` rows in the environment-variable table below.
|
||||
The historian backend is the external **`ZB.MOM.WW.HistorianGateway`** sidecar, consumed as the
|
||||
`ZB.MOM.WW.HistorianGateway.Client` gRPC package (the retired Wonderware TCP sidecar is documented at
|
||||
[`docs/drivers/Historian.Wonderware.md`](drivers/Historian.Wonderware.md)). The OtOpcUa host reads three
|
||||
appsettings sections — `ServerHistorian` (read path + gateway connection), `ContinuousHistorization`
|
||||
(FasterLog outbox + recorder draining to `WriteLiveValues`), and `AlarmHistorian` (SQLite store-and-forward
|
||||
alarm sink draining to `SendEvent`). The gateway connection (endpoint / key / TLS) lives **only** in
|
||||
`ServerHistorian`; the other two sections source it from there.
|
||||
|
||||
The in-process **client-side** options POCO is `WonderwareHistorianClientOptions` (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Contracts/WonderwareHistorianClientOptions.cs`), bound from the `AlarmHistorian` section: `Host`, `Port`, `UseTls`, `ServerCertThumbprint`, `SharedSecret`, `ConnectTimeout` (default 10s), `CallTimeout` (default 30s), `ProbeTimeoutSeconds` (`15`).
|
||||
The gateway API key is supplied via the environment variable **`ServerHistorian__ApiKey`** — never committed
|
||||
to config. The target gateway must run `RuntimeDb:Enabled=true` + `RuntimeDb:EventReadsEnabled=true`, and the
|
||||
key must carry the scopes `historian:read`, `historian:write`, `historian:tags:write`. See
|
||||
[`docs/Historian.md`](Historian.md) for the full key reference, the migration note (old Wonderware keys →
|
||||
gateway keys), and the deployment prerequisites.
|
||||
|
||||
---
|
||||
|
||||
@@ -139,29 +149,16 @@ All names are read in this repo's source via `Environment.GetEnvironmentVariable
|
||||
| `OTOPCUA_CONFIG_CONNECTION` | `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/DesignTimeDbContextFactory.cs` (design-time / `dotnet ef` only) | Read at **design time** by `DesignTimeDbContextFactory.cs` for `dotnet ef` migrations. At **runtime** the server resolves the connection string from `ConnectionStrings:ConfigDb` (env form: `ConnectionStrings__ConfigDb`) via `configuration.GetConnectionString("ConfigDb")` in `ServiceCollectionExtensions.cs` — `OTOPCUA_CONFIG_CONNECTION` appears there only as a hint in an error message, not via `GetEnvironmentVariable`. No credential is embedded in source. |
|
||||
| `ASPNETCORE_ENVIRONMENT` | ASP.NET host builder (framework) | Selects `appsettings.{Environment}.json` (e.g. `Development`). |
|
||||
|
||||
### Historian sidecar (`OTOPCUA_HISTORIAN_*`)
|
||||
### Historian (`ServerHistorian__ApiKey`)
|
||||
|
||||
All read in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs`.
|
||||
The retired Wonderware sidecar's `OTOPCUA_HISTORIAN_*` environment variables are **gone** — no source reads
|
||||
them anymore. The historian backend is now the external HistorianGateway, configured through the
|
||||
`ServerHistorian` / `ContinuousHistorization` / `AlarmHistorian` appsettings sections (above). The single
|
||||
historian secret this repo reads from the environment is the gateway API key:
|
||||
|
||||
| Variable | Effect / default |
|
||||
|---|---|
|
||||
| `OTOPCUA_HISTORIAN_TCP_PORT` | TCP port the sidecar listens on. Default `32569`. Corresponds to `AlarmHistorian:Port` on the host side. |
|
||||
| `OTOPCUA_HISTORIAN_BIND` | TCP bind address for the sidecar. Default `0.0.0.0`. |
|
||||
| `OTOPCUA_HISTORIAN_TLS_ENABLED` | `true` enables TLS on the sidecar's TCP listener. Default `false`. Corresponds to `AlarmHistorian:UseTls` on the host side. |
|
||||
| `OTOPCUA_HISTORIAN_TLS_CERT` | PFX file path **or** `LocalMachine\My\<thumbprint>` to load the sidecar TLS server certificate from the machine store. |
|
||||
| `OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD` | Password for a PFX-file certificate. Omit when using a machine-store cert. Never commit a value. |
|
||||
| `OTOPCUA_HISTORIAN_SECRET` | Per-process shared secret verified in the TCP Hello frame. Required (throws if unset). Corresponds to `AlarmHistorian:SharedSecret` on the host side. |
|
||||
| `OTOPCUA_HISTORIAN_ENABLED` | `true` opens the real Wonderware SDK connection; anything else → pipe-only mode (smoke/IPC tests). Default: not-true → pipe-only. |
|
||||
| `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED` | `false` disables the alarm-event writer (sidecar rejects `WriteAlarmEvents`). Default `true` (when `ENABLED=true`). |
|
||||
| `OTOPCUA_HISTORIAN_INTEGRATED` | `false` → SQL auth (use `USER`/`PASS`); any other value → integrated security. Default: integrated. |
|
||||
| `OTOPCUA_HISTORIAN_SERVER` | Historian server hostname. Default `localhost`. |
|
||||
| `OTOPCUA_HISTORIAN_SERVERS` | Comma-separated multi-node server list (overrides single `SERVER` when set). |
|
||||
| `OTOPCUA_HISTORIAN_PORT` | Historian port. Default `32568`. |
|
||||
| `OTOPCUA_HISTORIAN_USER` | SQL username (when not integrated). |
|
||||
| `OTOPCUA_HISTORIAN_PASS` | SQL password (when not integrated). Never commit a value. |
|
||||
| `OTOPCUA_HISTORIAN_TIMEOUT_SEC` | Command timeout (seconds). Default `30`. |
|
||||
| `OTOPCUA_HISTORIAN_MAX_VALUES` | Max values returned per read. Default `10000`. |
|
||||
| `OTOPCUA_HISTORIAN_COOLDOWN_SEC` | Failure cooldown (seconds). Default `60`. |
|
||||
| `ServerHistorian__ApiKey` | The HistorianGateway peppered-HMAC key (`histgw_<id>_<secret>`) sent as `Authorization: Bearer`. Supply via environment — **never commit**. Required when `ServerHistorian:Enabled=true`. |
|
||||
|
||||
### Driver integration-test / fixture sim endpoints
|
||||
|
||||
|
||||
+15
-16
@@ -89,8 +89,9 @@ Members:
|
||||
Implementations: every driver ships a `*DriverProbe` in its driver project
|
||||
(e.g.
|
||||
[`Driver.Modbus/ModbusDriverProbe.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverProbe.cs)
|
||||
does a bare socket open/close), plus the Wonderware historian's
|
||||
`WonderwareHistorianDriverProbe`.
|
||||
does a bare socket open/close). The historian backend is the external
|
||||
HistorianGateway (consumed as a gRPC client package, not an `IDriver`), so it
|
||||
has no driver probe.
|
||||
|
||||
Flow: the AdminUI's `AdminProbeService`
|
||||
([`AdminUI/Clients/AdminProbeService.cs`](../src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs))
|
||||
@@ -203,8 +204,8 @@ lifecycle. This is distinct from the driver capability `IHistoryProvider`:
|
||||
- `IHistoryProvider` is a *driver capability* — the server dispatches to it via
|
||||
the driver instance.
|
||||
- `IHistorianDataSource` is a *server registration* — the server resolves it by
|
||||
namespace and calls it directly, so one historian (e.g. Wonderware) can serve
|
||||
many drivers' nodes, and drivers can restart without dropping history
|
||||
namespace and calls it directly, so one historian (the HistorianGateway) can
|
||||
serve many drivers' nodes, and drivers can restart without dropping history
|
||||
availability.
|
||||
|
||||
The interface is `: IDisposable` and declares the full read surface as
|
||||
@@ -230,20 +231,18 @@ All values use the shared `DataValueSnapshot` / `HistoricalEvent` shapes;
|
||||
backend-specific quality/type encodings are translated to OPC UA `StatusCode`
|
||||
uints inside the data source.
|
||||
|
||||
Implementations:
|
||||
Implementation:
|
||||
|
||||
- `WonderwareHistorianClient`
|
||||
([`Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs))
|
||||
— the .NET 10 client that talks to the Wonderware historian sidecar over
|
||||
TCP (optional TLS). It implements both `IHistorianDataSource` (read paths) and
|
||||
`IAlarmHistorianWriter` (the alarm-event drain target; see
|
||||
- `GatewayHistorianDataSource`
|
||||
([`Driver.Historian.Gateway/GatewayHistorianDataSource.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs))
|
||||
— the read backend that talks gRPC to the external `ZB.MOM.WW.HistorianGateway`
|
||||
(via the `ZB.MOM.WW.HistorianGateway.Client` package, behind the
|
||||
`IHistorianGatewayClient` seam). The alarm-event drain target is the separate
|
||||
`GatewayAlarmHistorianWriter` (the gateway `SendEvent` path; see
|
||||
[AlarmHistorian.md](AlarmHistorian.md)).
|
||||
- `HistorianDataSource`
|
||||
([`Driver.Historian.Wonderware/Backend/HistorianDataSource.cs`](../src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Backend/HistorianDataSource.cs))
|
||||
— the in-process backend implementation behind the sidecar.
|
||||
|
||||
The optional Wonderware historian sidecar setup is described in
|
||||
[ServiceHosting.md](ServiceHosting.md).
|
||||
The HistorianGateway is the sole historian backend; its config keys and
|
||||
deployment prerequisites are in [Historian.md](Historian.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -288,7 +287,7 @@ Folders:
|
||||
- [ReadWriteOperations.md](ReadWriteOperations.md) — the driver *capability*
|
||||
interfaces (read/write/subscribe) and resilience pipeline.
|
||||
- [ServiceHosting.md](ServiceHosting.md) — role gating, the Akka cluster, and
|
||||
the optional Wonderware historian sidecar.
|
||||
the external HistorianGateway backend.
|
||||
- [AlarmHistorian.md](AlarmHistorian.md) — the store-and-forward SQLite alarm
|
||||
sink that drains to `IAlarmHistorianWriter`.
|
||||
- [Redundancy.md](Redundancy.md) — driver stability tiers in the redundancy
|
||||
|
||||
+60
-50
@@ -3,9 +3,12 @@
|
||||
Phase C wires server-side OPC UA **HistoryRead** for authored equipment tags flagged
|
||||
historized. The feature is driver-agnostic: any equipment tag (Galaxy, Modbus, OpcUaClient,
|
||||
or any other driver) can be marked historized; the server dispatches all history reads to the
|
||||
registered `IHistorianDataSource` — today, the Wonderware sidecar client
|
||||
(`WonderwareHistorianClient`). No EF migration is required; the historian flag rides in the
|
||||
existing schemaless `TagConfig` JSON blob alongside the Phase B `alarm` object.
|
||||
registered `IHistorianDataSource` — the **HistorianGateway** read client
|
||||
(`GatewayHistorianDataSource`, talking gRPC to the external `ZB.MOM.WW.HistorianGateway` via the
|
||||
`ZB.MOM.WW.HistorianGateway.Client` package). No EF migration is required; the historian flag rides in
|
||||
the existing schemaless `TagConfig` JSON blob alongside the Phase B `alarm` object. (The bespoke
|
||||
Wonderware TCP sidecar backend this replaced was retired — see
|
||||
[drivers/Historian.Wonderware.md](drivers/Historian.Wonderware.md).)
|
||||
|
||||
Design reference: [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](plans/2026-06-14-galaxy-phase-c-historian-design.md).
|
||||
|
||||
@@ -60,11 +63,12 @@ and all HistoryRead calls on historized nodes return `GoodNoData` (empty, not an
|
||||
{
|
||||
"ServerHistorian": {
|
||||
"Enabled": false,
|
||||
"Host": "localhost",
|
||||
"Port": 32569,
|
||||
"UseTls": false,
|
||||
"ServerCertThumbprint": "",
|
||||
"SharedSecret": "",
|
||||
"Endpoint": "",
|
||||
"ApiKey": "",
|
||||
"UseTls": true,
|
||||
"AllowUntrustedServerCertificate": false,
|
||||
"CaCertificatePath": null,
|
||||
"CallTimeout": "00:00:30",
|
||||
"MaxTieClusterOverfetch": 65536
|
||||
}
|
||||
}
|
||||
@@ -72,20 +76,31 @@ and all HistoryRead calls on historized nodes return `GoodNoData` (empty, not an
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `Enabled` | bool | `false` | Enable the live `WonderwareHistorianClient`. `false` → `NullHistorianDataSource` (empty reads). |
|
||||
| `Host` | string | `localhost` | DNS name or IP of the machine running the historian sidecar. |
|
||||
| `Port` | int | `32569` | TCP port the sidecar listens on (`OTOPCUA_HISTORIAN_TCP_PORT`). |
|
||||
| `UseTls` | bool | `false` | Wrap the TCP connection in TLS. |
|
||||
| `ServerCertThumbprint` | string | — | Optional SHA-1 thumbprint to pin the sidecar's TLS certificate. Leave empty for CA-chain validation. |
|
||||
| `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. |
|
||||
| `Enabled` | bool | `false` | Enable the live `GatewayHistorianDataSource`. `false` → `NullHistorianDataSource` (empty reads). |
|
||||
| `Endpoint` | string | `""` | Absolute gateway URI, e.g. `https://host:5222`. Scheme selects transport (`https://` = TLS, `http://` = h2c plaintext). Required when `Enabled`. |
|
||||
| `ApiKey` | string | `""` | The gateway peppered-HMAC key (`histgw_<id>_<secret>`) sent as `Authorization: Bearer`. Required when `Enabled`. **Supply via env `ServerHistorian__ApiKey`.** |
|
||||
| `UseTls` | bool | `true` | Connect over TLS; must match the `Endpoint` scheme. |
|
||||
| `AllowUntrustedServerCertificate` | bool | `false` | Accept a self-signed / untrusted server certificate (dev / on-prem only). |
|
||||
| `CaCertificatePath` | string\|null | `null` | PEM CA file pinning the gateway's TLS chain. Null/empty uses the OS trust store. |
|
||||
| `CallTimeout` | TimeSpan | `00:00:30` | Per-call deadline applied to each unary gateway read. |
|
||||
| `MaxTieClusterOverfetch` | int | `65536` | Maximum samples the server will fetch in one shot to page through a tie cluster (multiple samples sharing one `SourceTimestamp`). A cluster larger than this ceiling fails `BadHistoryOperationUnsupported`. Raise to handle abnormally large tie clusters; the default covers all normal-data cases. |
|
||||
|
||||
> **Do not commit `SharedSecret` to `appsettings.json`.** Set it via an environment variable,
|
||||
> a secrets store, or a deployment-time overlay. The checked-in default is always empty.
|
||||
> **Do not commit `ApiKey` to `appsettings.json`.** Set it via the environment variable
|
||||
> `ServerHistorian__ApiKey`, a secrets store, or a deployment-time overlay. The checked-in default is
|
||||
> always empty.
|
||||
|
||||
> **Gateway-side prerequisites.** The target gateway must run `RuntimeDb:Enabled=true` (continuous
|
||||
> `WriteLiveValues`) + `RuntimeDb:EventReadsEnabled=true` (alarm-history `ReadEvents`), and the API key
|
||||
> must carry the scopes `historian:read`, `historian:write`, `historian:tags:write`.
|
||||
|
||||
> **Migration from the Wonderware backend.** Rename the old keys: `Host`/`Port` → `Endpoint`
|
||||
> (`https://host:5222`); `SharedSecret` → `ApiKey` (env `ServerHistorian__ApiKey`);
|
||||
> `ServerCertThumbprint` → `CaCertificatePath` (+ `UseTls` / `AllowUntrustedServerCertificate`).
|
||||
|
||||
The `ServerHistorian` section is independent of the `AlarmHistorian` section (the alarm
|
||||
write path). They share the same Wonderware sidecar process but hold separate client
|
||||
instances and separate `SharedSecret` values.
|
||||
write path) and the `ContinuousHistorization` section (driver-value capture). All three target the
|
||||
**same** gateway — but only `ServerHistorian` carries the connection (endpoint/key/TLS); the other two
|
||||
source it from there.
|
||||
|
||||
---
|
||||
|
||||
@@ -109,7 +124,8 @@ OPC UA client can discover historized capability from the node's attributes.
|
||||
**Equipment-folder event-notifier nodes** serve Event history. Every equipment folder that
|
||||
owns at least one alarm condition is already an event notifier; the server registers a
|
||||
`sourceName` (the equipment id) for each such folder and maps event history reads to the
|
||||
Wonderware historian using that source. Event-field projection supports the standard
|
||||
HistorianGateway using that source. (Alarm-history `ReadEvents` requires the gateway running
|
||||
`RuntimeDb:EventReadsEnabled=true`.) Event-field projection supports the standard
|
||||
`BaseEventType` select clauses — `EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`,
|
||||
and `Severity`; an unsupported select operand returns a null field (spec-conformant).
|
||||
|
||||
@@ -123,7 +139,7 @@ upstream `HistoryEvent` onto `HistoricalEvent` — the same six-field projection
|
||||
node-manager itself projects when serving event history. This is a **driver-level capability**:
|
||||
the OpcUaClient driver acts as a passthrough to whatever historian the upstream server exposes,
|
||||
and is independent of the single server-side `IHistorianDataSource` backend
|
||||
(`WonderwareHistorianClient` / `NullHistorianDataSource`) that the OtOpcUa node-manager
|
||||
(`GatewayHistorianDataSource` / `NullHistorianDataSource`) that the OtOpcUa node-manager
|
||||
dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.).
|
||||
|
||||
### Graceful degradation
|
||||
@@ -138,7 +154,7 @@ dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.).
|
||||
|
||||
A historized node with no historian configured never returns an error status — it returns
|
||||
empty. This means a deployment can author and publish historized tags before the historian
|
||||
sidecar is provisioned, without producing error spikes in connected clients.
|
||||
gateway is provisioned, without producing error spikes in connected clients.
|
||||
|
||||
### Continuation-point paging (Raw)
|
||||
|
||||
@@ -187,22 +203,14 @@ are disposed when the session closes). Resuming an unknown / evicted / released
|
||||
`BadContinuationPointInvalid`. `releaseContinuationPoints` drops the stored cursors without reading
|
||||
data.
|
||||
|
||||
### Total aggregate derivation
|
||||
### Total aggregate
|
||||
|
||||
The OPC UA `Total` aggregate is **supported** over the Wonderware backend. Because the
|
||||
Wonderware `AnalogSummary` query exposes no `Total` column, the value is derived client-side
|
||||
using the time-integral identity:
|
||||
|
||||
> **Total = time-weighted Average × interval-seconds**
|
||||
|
||||
The wire request is issued with the `Average` column; each returned bucket's value is
|
||||
multiplied by `interval.TotalSeconds` before the result is returned to the OPC UA client.
|
||||
Bucket status codes and timestamps are preserved unchanged. Null (unavailable) Average
|
||||
buckets produce a null Total (`BadNoData` downstream) — the scaling is not applied.
|
||||
|
||||
This derivation is exact for piecewise-constant (step) signals. For continuously varying
|
||||
signals it is an approximation identical to the one Wonderware would apply internally, so
|
||||
the result is consistent with what AVEVA Historian reports for the same window.
|
||||
The OPC UA `Total` aggregate is **supported** over the HistorianGateway backend. The gateway exposes a
|
||||
native **`Integral`** retrieval mode, so `Total` maps straight to it (`HistoryAggregateType.Total →
|
||||
RetrievalMode.Integral`) — no client-side scaling. (This replaces the retired Wonderware path, which had no
|
||||
`Total` column and derived it client-side as time-weighted `Average × interval-seconds`.) `Count` is
|
||||
likewise a native gateway mode. Bucket status codes and timestamps are preserved unchanged; empty / null
|
||||
buckets surface as `BadNoData`.
|
||||
|
||||
### Known limitations
|
||||
|
||||
@@ -213,12 +221,12 @@ the result is consistent with what AVEVA Historian reports for the same window.
|
||||
read and there is no "full page ⇒ maybe more" signal to page on. Returning the full result with
|
||||
no continuation point is spec-conformant.
|
||||
- **No modified-value history** (`HistoryReadModified`). Requests for modified values return
|
||||
`BadHistoryOperationUnsupported`. This is **infra-gated, not a server-code gap**: the AVEVA
|
||||
Wonderware historian backend (`IHistorianDataSource`, the TCP sidecar client) exposes only a
|
||||
current-value read path — there is no modified/edited-history surface to source the data from. The
|
||||
server-side override is in place (it cleanly rejects modified reads per node) and `IsReadModified`
|
||||
is honoured; serving real modified-value history is unblocked only once the historian client/sidecar
|
||||
grows a modified-read RPC. Until then, rejecting is the correct, spec-conformant behaviour.
|
||||
`BadHistoryOperationUnsupported`. This is **infra-gated, not a server-code gap**: the HistorianGateway
|
||||
backend (`GatewayHistorianDataSource`) exposes only a current-value read path — there is no
|
||||
modified/edited-history surface to source the data from. The server-side override is in place (it cleanly
|
||||
rejects modified reads per node) and `IsReadModified` is honoured; serving real modified-value history is
|
||||
unblocked only once the gateway grows a modified-read RPC. Until then, rejecting is the correct,
|
||||
spec-conformant behaviour.
|
||||
|
||||
### Redundancy and authorization
|
||||
|
||||
@@ -309,14 +317,16 @@ above), but is not exposed by this bundled CLI.
|
||||
|
||||
## Live /run gate
|
||||
|
||||
The live read gate requires the Wonderware historian sidecar running on the WW Historian VM
|
||||
(`10.100.0.48`) and AVEVA Historian healthy. Set `ServerHistorian:Enabled=true` with the
|
||||
correct `Host`, `Port`, and `SharedSecret` in `appsettings.json` (or via environment
|
||||
variables), then deploy and publish at least one historized Galaxy tag. The gate is
|
||||
operator-driven — it is not part of the local docker-dev rig.
|
||||
The live read gate requires a reachable `ZB.MOM.WW.HistorianGateway` (VPN to `wonder-sql-vd03`) with the
|
||||
AVEVA Historian behind it healthy. Set `ServerHistorian:Enabled=true` with the correct `Endpoint`
|
||||
(`https://host:5222`) and supply `ServerHistorian__ApiKey` via the environment, then deploy and publish at
|
||||
least one historized Galaxy tag. The gate is operator-driven — it is not part of the local docker-dev rig.
|
||||
The gateway-backed driver also ships an env-gated live suite (`Category=LiveIntegration`); see the
|
||||
`HISTGW_GATEWAY_ENDPOINT` / `HISTGW_GATEWAY_APIKEY` / `HISTGW_TEST_TAG` / `HISTGW_WRITE_SANDBOX_TAG` /
|
||||
`HISTGW_ALARM_SOURCE` env vars (it skips cleanly when they are absent).
|
||||
|
||||
See [AlarmHistorian.md](AlarmHistorian.md) for the historian sidecar setup and
|
||||
[ServiceHosting.md](ServiceHosting.md) for the sidecar service configuration.
|
||||
See [AlarmHistorian.md](AlarmHistorian.md) for the alarm write path and
|
||||
[ServiceHosting.md](ServiceHosting.md) for the (external) HistorianGateway deployment.
|
||||
|
||||
---
|
||||
|
||||
@@ -373,7 +383,7 @@ phases and are recorded here so future audits don't re-flag them.
|
||||
## See also
|
||||
|
||||
- [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](plans/2026-06-14-galaxy-phase-c-historian-design.md) — full design and implementation notes
|
||||
- [AlarmHistorian.md](AlarmHistorian.md) — alarm write path; shares the same Wonderware sidecar
|
||||
- [AlarmHistorian.md](AlarmHistorian.md) — alarm write path; drains to the same HistorianGateway (`SendEvent`)
|
||||
- [AlarmTracking.md](AlarmTracking.md) — OPC UA Part 9 alarm surface (event history source)
|
||||
- [Client.CLI.md](Client.CLI.md) — full `historyread` flag reference
|
||||
- [ScriptedAlarms.md](ScriptedAlarms.md) §"Native driver alarms" — the Phase B `alarm` object in `TagConfig` (parallel carrier)
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics
|
||||
| [security.md](security.md) | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
|
||||
| [Redundancy.md](Redundancy.md) | `RedundancyCoordinator`, `ServiceLevelCalculator`, apply-lease, Prometheus metrics |
|
||||
| [Reservations.md](Reservations.md) | Fleet-wide ZTag / SAPID external-ID reservations — publish-time claim, release flow |
|
||||
| [ServiceHosting.md](ServiceHosting.md) | Single fused `OtOpcUa.Host` binary install/uninstall with `OTOPCUA_ROLES` gating, plus the optional `OtOpcUaWonderwareHistorian` sidecar |
|
||||
| [ServiceHosting.md](ServiceHosting.md) | Single fused `OtOpcUa.Host` binary install/uninstall with `OTOPCUA_ROLES` gating; the historian backend is the external HistorianGateway |
|
||||
| [StatusDashboard.md](StatusDashboard.md) | Pointer — superseded by [v2/admin-ui.md](v2/admin-ui.md) |
|
||||
|
||||
### Client tooling
|
||||
|
||||
+13
-6
@@ -2,14 +2,15 @@
|
||||
|
||||
## Overview
|
||||
|
||||
A production OtOpcUa deployment runs **one binary per node**, plus the optional Wonderware historian sidecar:
|
||||
A production OtOpcUa deployment runs **one binary per node**. The historian backend is the external
|
||||
`ZB.MOM.WW.HistorianGateway`, deployed separately (not installed by this repo's scripts):
|
||||
|
||||
| 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 TCP (optional TLS). Required only when `AlarmHistorian:Enabled=true`. May run on the same machine or a remote host. |
|
||||
| **ZB.MOM.WW.HistorianGateway** *(external — separate deployment)* | not in this repo | .NET 10 | — | The sole historian backend. OtOpcUa talks gRPC to it (via the `ZB.MOM.WW.HistorianGateway.Client` package) for HistoryRead, alarm `SendEvent`, and continuous `WriteLiveValues`. Must run `RuntimeDb:Enabled=true` + `RuntimeDb:EventReadsEnabled=true`; the API key must carry `historian:read` + `historian:write` + `historian:tags:write`. |
|
||||
|
||||
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. (The bespoke Wonderware historian sidecar this deployment used to ship was retired — see [drivers/Historian.Wonderware.md](drivers/Historian.Wonderware.md).)
|
||||
|
||||
> **v2 change.** v1's separate `OtOpcUa.Server` + `OtOpcUa.Admin` Windows services merged into a single role-gated `OtOpcUa.Host` binary. Two installers became one (with a `-Roles` parameter). The whole DI graph is composed in `OtOpcUa.Host/Program.cs`; per-role wiring is conditional on the env var.
|
||||
|
||||
@@ -72,14 +73,20 @@ Both admin and driver nodes expose:
|
||||
|
||||
Used by Traefik for the active-leader-only routing pattern (see [Architecture-v2.md](v2/Architecture-v2.md)).
|
||||
|
||||
## OtOpcUa Wonderware Historian (optional)
|
||||
## Historian backend (HistorianGateway — external)
|
||||
|
||||
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 `AlarmHistorian:Host` + `AlarmHistorian:Port` (and optionally `AlarmHistorian:UseTls`) on the OtOpcUa host side. See [Historian.Wonderware.md](drivers/Historian.Wonderware.md) for the full transport and security reference.
|
||||
The historian backend is the external `ZB.MOM.WW.HistorianGateway`, deployed and operated separately (not
|
||||
installed by `Install-Services.ps1`). OtOpcUa connects to it over gRPC via the
|
||||
`ZB.MOM.WW.HistorianGateway.Client` package — configure the `ServerHistorian:Endpoint` (`https://host:5222`)
|
||||
and supply `ServerHistorian__ApiKey` via the environment on the OtOpcUa host side. The gateway must run with
|
||||
`RuntimeDb:Enabled=true` + `RuntimeDb:EventReadsEnabled=true` and an API key carrying `historian:read` +
|
||||
`historian:write` + `historian:tags:write`. See [Historian.md](Historian.md) for the full config-key and
|
||||
deployment-prerequisite reference. (The retired Wonderware TCP sidecar: [Historian.Wonderware.md](drivers/Historian.Wonderware.md).)
|
||||
|
||||
## Install / Uninstall
|
||||
|
||||
- `scripts/install/Install-Services.ps1 -Roles admin,driver` — installs `OtOpcUaHost`.
|
||||
- `scripts/install/Uninstall-Services.ps1` — stops + removes the host service (and the historian sidecar if installed).
|
||||
- `scripts/install/Uninstall-Services.ps1` — stops + removes the host service. (The historian backend is the external HistorianGateway — not installed/removed by these scripts.)
|
||||
|
||||
## Logging
|
||||
|
||||
|
||||
+9
-8
@@ -120,14 +120,15 @@ drivers:
|
||||
| TwinCAT | Symbol path, data type, etc. |
|
||||
| FOCAS | PMC address, data type, etc. |
|
||||
| **OpcUaClient** | `FullName` (the remote OPC UA node id string) |
|
||||
| **Historian.Wonderware** | `FullName` (the Wonderware tagname to read) |
|
||||
|
||||
**OpcUaClient** and **Historian.Wonderware** were previously raw-JSON
|
||||
fallback only; they now have first-class typed editors that expose a single
|
||||
`FullName` field (PascalCase JSON key, consistent with the Galaxy editor
|
||||
convention). Both are registered in `TagConfigEditorMap` and
|
||||
`TagConfigValidator`; unknown keys in the stored JSON blob are preserved on
|
||||
round-trip.
|
||||
**OpcUaClient** was previously raw-JSON fallback only; it now has a first-class
|
||||
typed editor that exposes a single `FullName` field (PascalCase JSON key,
|
||||
consistent with the Galaxy editor convention). It is registered in
|
||||
`TagConfigEditorMap` and `TagConfigValidator`; unknown keys in the stored JSON
|
||||
blob are preserved on round-trip.
|
||||
|
||||
> The historian backend is the external HistorianGateway (no OtOpcUa-side tag
|
||||
> driver / tag-config editor). See [Historian.md](Historian.md).
|
||||
|
||||
Drivers not yet listed above (e.g. Galaxy — which uses the Galaxy address
|
||||
picker described below) still use the generic raw-`TagConfig`-JSON textarea.
|
||||
@@ -226,7 +227,7 @@ Combined with historization (values are arrays — history of the whole array sn
|
||||
|
||||
- **Array writes** (inbound client→device write of an array value) — tagged for a follow-up phase.
|
||||
- **Multi-dimensional arrays** (`ValueRank > 1`) — not supported; all arrays are 1-D.
|
||||
- **Array historization** — a historized array tag materialises with the correct `Historizing` flag, but the Wonderware sidecar historian treats the value as an opaque blob; per-element history is out of scope.
|
||||
- **Array historization** — a historized array tag materialises with the correct `Historizing` flag, but the historian backend treats the value as an opaque blob; per-element history is out of scope. (Continuous historization is numeric-analog only — array / non-numeric values are not recorded.)
|
||||
|
||||
See the individual driver docs under `docs/drivers/` for per-driver implementation details.
|
||||
|
||||
|
||||
+2
-2
@@ -96,7 +96,7 @@ What the engine pulls driver-tag values from. Reads are **synchronous** because
|
||||
|
||||
Fire-and-forget sink for evaluation results when `VirtualTagDefinition.Historize = true`. Implementations must queue internally and drain on their own cadence — a slow historian must not block script evaluation. `NullHistoryWriter.Instance` is the no-op default. Scripted-alarm emissions flow through `Core.AlarmHistorian` via `Phase7EngineComposer.RouteToHistorianAsync` (a separate concern; see [AlarmTracking.md](AlarmTracking.md)).
|
||||
|
||||
**Equipment-namespace path (H5).** The `Historize` flag is threaded end-to-end on the equipment path: `VirtualTag.Historize` → composer + artifact-decode (byte-parity) → `EquipmentVirtualTagPlan.Historize` → `VirtualTagHostActor`, which calls `IHistoryWriter.Record(nodeId, snapshot)` for every historized result (in addition to publishing the live value). The writer is injectable via DI — `DriverHostActor` resolves `IHistoryWriter` (`TryAddSingleton`, `NullHistoryWriter` default) and threads it into `VirtualTagHostActor`. **The durable AVEVA data-value sink is infra-gated**: the Wonderware historian sidecar exposes only HistoryRead + alarm-event writes (no live-data `WriteDataValues` RPC), so the production default stays `NullHistoryWriter` until that backend exists. A deployment can bind a custom `IHistoryWriter` via DI today.
|
||||
**Equipment-namespace path (H5).** The `Historize` flag is threaded end-to-end on the equipment path: `VirtualTag.Historize` → composer + artifact-decode (byte-parity) → `EquipmentVirtualTagPlan.Historize` → `VirtualTagHostActor`, which calls `IHistoryWriter.Record(nodeId, snapshot)` for every historized result (in addition to publishing the live value). The writer is injectable via DI — `DriverHostActor` resolves `IHistoryWriter` (`TryAddSingleton`, `NullHistoryWriter` default) and threads it into `VirtualTagHostActor`. **This `IHistoryWriter` seam still ships no durable binding** (`NullHistoryWriter` default). Durable continuous historization of driver/virtual values is now handled by the separate `ContinuousHistorizationRecorder` (it taps the dependency-mux value fan-out → a crash-safe FasterLog outbox → the HistorianGateway's `WriteLiveValues` path; see [Historian.md](Historian.md)), not through this seam. A deployment can still bind a custom `IHistoryWriter` via DI.
|
||||
|
||||
## Dispatch integration
|
||||
|
||||
@@ -114,7 +114,7 @@ Per [ADR-002](v2/implementation/adr-002-driver-vs-virtual-dispatch.md) Option B,
|
||||
`ITagUpstreamSource` and `IHistoryWriter` are the two ports the engine requires from its host. Both live in `Core.VirtualTags`. In the v2 actor system:
|
||||
|
||||
- **Upstream-tag feed.** `DependencyMuxActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs`) routes `DriverInstanceActor.AttributeValuePublished` events to the `VirtualTagActor` instances that declared interest in those tag refs. Each `VirtualTagActor` holds the in-memory per-tag dependency map; the `IVirtualTagEvaluator` (`RoslynVirtualTagEvaluator`) receives the dependency snapshot synchronously on the actor message thread. Reads of never-pushed dependency refs return `null` values in the dependency snapshot.
|
||||
- **`IHistoryWriter`** — the equipment-namespace path threads `Historize` end-to-end and `VirtualTagHostActor` invokes the injected writer on historized results (H5); the writer is resolved through `DriverHostActor` DI with a `NullHistoryWriter` default. The standalone `VirtualTagEngine` likewise receives `NullHistoryWriter` by default. No *durable* writer ships because the historian sidecar has no live-data write RPC (infra-gated) — see the `IHistoryWriter` section above.
|
||||
- **`IHistoryWriter`** — the equipment-namespace path threads `Historize` end-to-end and `VirtualTagHostActor` invokes the injected writer on historized results (H5); the writer is resolved through `DriverHostActor` DI with a `NullHistoryWriter` default. The standalone `VirtualTagEngine` likewise receives `NullHistoryWriter` by default. No *durable* writer ships on this seam — durable continuous historization now flows through the separate `ContinuousHistorizationRecorder` → HistorianGateway `WriteLiveValues` path (see the `IHistoryWriter` section above and [Historian.md](Historian.md)).
|
||||
|
||||
## Composition
|
||||
|
||||
|
||||
@@ -1,156 +1,46 @@
|
||||
# Wonderware Historian Backend
|
||||
# Wonderware Historian Backend — RETIRED
|
||||
|
||||
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`.
|
||||
> **This backend has been retired.** The bespoke Wonderware TCP/ArchestrA historian sidecar
|
||||
> (`OtOpcUaWonderwareHistorian`) and its `Driver.Historian.Wonderware*` projects — plus the vestigial
|
||||
> `Historian.Wonderware` driver type — were removed. **HistorianGateway is now the sole historian
|
||||
> backend** for OtOpcUa (read, alarm-write, and continuous historization).
|
||||
|
||||
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.
|
||||
## What replaced it
|
||||
|
||||
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).
|
||||
OtOpcUa now consumes the **`ZB.MOM.WW.HistorianGateway`** sidecar through the Gitea-feed
|
||||
**`ZB.MOM.WW.HistorianGateway.Client`** gRPC package (`historian_gateway.v1`), behind the
|
||||
`IHistorianGatewayClient` seam in `ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway`:
|
||||
|
||||
## Architecture
|
||||
- **HistoryRead** → `GatewayHistorianDataSource` over the `ServerHistorian` appsettings section.
|
||||
- **Alarm history** → `GatewayAlarmHistorianWriter` (the gateway `SendEvent` path) behind the durable
|
||||
`SqliteStoreAndForwardSink`; alarm-history `ReadEvents` needs the gateway running
|
||||
`RuntimeDb:EventReadsEnabled=true`.
|
||||
- **Continuous historization** → a crash-safe FasterLog outbox + `ContinuousHistorizationRecorder`
|
||||
draining to the gateway's `WriteLiveValues` (`ContinuousHistorization` section); needs the gateway
|
||||
running `RuntimeDb:Enabled=true`.
|
||||
- **Tag provisioning** → `AddressSpaceApplier` fires a non-blocking `IHistorianProvisioning` `EnsureTags`
|
||||
hook for added historized tags.
|
||||
|
||||
```
|
||||
+-------------------------------------------+
|
||||
| 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 gateway API key must carry the scopes `historian:read`, `historian:write`, `historian:tags:write`.
|
||||
|
||||
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.
|
||||
## Where to read now
|
||||
|
||||
### Transport & security
|
||||
- **[../Historian.md](../Historian.md)** — the full historian guide (read path, alarm path, continuous
|
||||
historization, config keys, migration note).
|
||||
- **[README.md](README.md)** — driver / back-end overview.
|
||||
- **[../ServiceHosting.md](../ServiceHosting.md)** — deployment (the historian backend is the external
|
||||
HistorianGateway, not an installed sidecar).
|
||||
|
||||
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.
|
||||
## Migration
|
||||
|
||||
**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.
|
||||
Deployments that carried the old `ServerHistorian` Wonderware keys must rename them:
|
||||
|
||||
**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.
|
||||
| Old (Wonderware) key | New (gateway) key |
|
||||
|---|---|
|
||||
| `ServerHistorian:Host` + `:Port` | `ServerHistorian:Endpoint` (`https://host:5222`) |
|
||||
| `ServerHistorian:SharedSecret` | `ServerHistorian:ApiKey` (supply via env `ServerHistorian__ApiKey`) |
|
||||
| `ServerHistorian:ServerCertThumbprint` | `ServerHistorian:CaCertificatePath` (+ `UseTls` / `AllowUntrustedServerCertificate`) |
|
||||
|
||||
**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
|
||||
The `AlarmHistorian` section's old Wonderware connection keys (`Host`/`Port`/`UseTls`/`ServerCertThumbprint`/`SharedSecret`)
|
||||
were pruned — remove them; the SQLite store-and-forward knobs are retained and the downstream connection is
|
||||
now sourced from `ServerHistorian`.
|
||||
|
||||
@@ -11,7 +11,7 @@ OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `C
|
||||
- `IAlarmSource` — driver-emitted OPC UA A&C events
|
||||
- `IHistoryProvider` — driver-side raw / processed / at-time / events HistoryRead (see [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md))
|
||||
- `IRediscoverable` — driver-initiated address-space rebuild notifications
|
||||
- `IHistorianDataSource` — server-side historian sink registration (the Wonderware Historian backend), distinct from the driver-side `IHistoryProvider` HistoryRead path
|
||||
- `IHistorianDataSource` — server-side historian read backend registration (the HistorianGateway backend), distinct from the driver-side `IHistoryProvider` HistoryRead path
|
||||
|
||||
Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it.
|
||||
|
||||
@@ -29,7 +29,7 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core
|
||||
| [TwinCAT](TwinCAT.md) | `Driver.TwinCAT` | B | Beckhoff `TwinCAT.Ads` (`TcAdsClient`) | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable | The only native-notification driver outside Galaxy — ADS delivers `ValueChangedCallback` events the driver forwards straight to `ISubscribable.OnDataChange` without polling. Symbol tree uploaded via `SymbolLoaderFactory` |
|
||||
| [FOCAS](FOCAS.md) | `Driver.FOCAS` | A | Pure-managed `FocasWireClient` — FOCAS/2 Ethernet binary protocol on TCP:8193, inlined into the driver assembly | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource | `IWritable` is implemented but read-only by design — `WriteAsync` returns `BadNotWritable` for every point. CNC-shaped data model (axes, spindle, PMC, macros, alarms) not a flat tag map. Previously Tier-C (Host + P/Invoke + shim DLL); retired in the 2026-04-24 migration when the managed wire client landed |
|
||||
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IHostConnectivityProbe | Gateway/aggregation driver — the only driver implementing driver-side `IHistoryProvider` (forwards HistoryRead to the upstream server). Opens a single `Session` against a remote OPC UA server and re-exposes its address space. Owns its own `ApplicationConfiguration` (distinct from `Client.Shared`) because it's always-on with keep-alive + `TransferSubscriptions` across SDK reconnect, not an interactive CLI |
|
||||
| [Historian.Wonderware](Historian.Wonderware.md) | `Driver.Historian.Wonderware` (+ `.Client`, `.Client.Contracts`) | — | `aahClientManaged` write SDK + AVEVA Historian SQL, over a pipe IPC backend | IHistorianDataSource (server-side historian sink) | Not a tag driver — a historian backend that registers `IHistorianDataSource` (`HistorianDataSource : IHistorianDataSource`) to satisfy HistoryRead and to sink tag/alarm history. No `IDriver`/`ITagDiscovery` surface |
|
||||
| [Historian.Gateway](../Historian.md) | `Driver.Historian.Gateway` | — | `ZB.MOM.WW.HistorianGateway.Client` gRPC (`historian_gateway.v1`) | IHistorianDataSource (server-side read backend) + alarm `SendEvent` writer + `WriteLiveValues` recorder + `IHistorianProvisioning` | Not a tag driver — the sole historian backend. Registers `GatewayHistorianDataSource : IHistorianDataSource` for HistoryRead and serves alarm-write + continuous historization through the gateway. No `IDriver`/`ITagDiscovery` surface. (The retired Wonderware sidecar backend it replaced is documented at [Historian.Wonderware.md](Historian.Wonderware.md).) |
|
||||
|
||||
## Per-driver documentation
|
||||
|
||||
@@ -48,8 +48,8 @@ Driver type metadata is registered at startup in `DriverTypeRegistry` (`src/Core
|
||||
- [TwinCAT.md](TwinCAT.md) — Beckhoff TwinCAT (ADS) driver: getting started, native-notification subscription, symbol-tree upload
|
||||
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, driver-side HistoryRead forwarding, reconnect behaviour
|
||||
|
||||
- **Historian.Wonderware** (server-side historian sink, not a tag driver) has its own overview page:
|
||||
- [Historian.Wonderware.md](Historian.Wonderware.md) — AVEVA Historian backend: sink registration, HistoryRead dispatch, alarm store-and-forward, deployment prerequisites
|
||||
- **Historian.Gateway** (server-side historian backend, not a tag driver) is documented in the main guide:
|
||||
- [../Historian.md](../Historian.md) — HistorianGateway backend: read-path registration, HistoryRead dispatch, alarm store-and-forward (`SendEvent`), continuous historization (`WriteLiveValues`), `EnsureTags` provisioning, config keys, deployment prerequisites. (The retired Wonderware sidecar backend it replaced: [Historian.Wonderware.md](Historian.Wonderware.md).)
|
||||
|
||||
- The full per-field spec (capability surface, config schema, addressing, data-type maps, connection settings, quirks for every driver) lives in [docs/v2/driver-specs.md](../v2/driver-specs.md). The overview pages above are the short path; that file is the authoritative per-driver reference.
|
||||
|
||||
@@ -68,7 +68,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni
|
||||
|
||||
## Related cross-driver docs
|
||||
|
||||
- [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md) — `IHistoryProvider` dispatch, aggregate mapping, continuation points. The OPC UA Client driver is the only driver that implements driver-side `IHistoryProvider` (it forwards HistoryRead to the upstream server); the Aveva Historian path is served server-side by the Wonderware `IHistorianDataSource` sink instead. Other drivers do not implement the interface and return `BadHistoryOperationUnsupported`.
|
||||
- [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md) — `IHistoryProvider` dispatch, aggregate mapping, continuation points. The OPC UA Client driver is the only driver that implements driver-side `IHistoryProvider` (it forwards HistoryRead to the upstream server); the AVEVA Historian path is served server-side by the HistorianGateway-backed `IHistorianDataSource` instead. Other drivers do not implement the interface and return `BadHistoryOperationUnsupported`.
|
||||
- [AlarmTracking.md](../AlarmTracking.md) — `IAlarmSource` event model and filtering. Implemented by Galaxy (native MxAccess alarms, working end-to-end), OPC UA Client, AB CIP, and FOCAS; AB Legacy, Modbus, S7, and TwinCAT have no alarm source.
|
||||
- [Subscriptions.md](../v1/Subscriptions.md) — how the Server multiplexes subscriptions onto `ISubscribable.OnDataChange`.
|
||||
- [docs/v2/driver-stability.md](../v2/driver-stability.md) — tier system (A / B / C), shared `CapabilityPolicy` defaults per tier × capability, `MemoryTracking` hybrid formula, and process-level recycle rules.
|
||||
|
||||
@@ -50,9 +50,10 @@ with a human-readable explanation rather than a false-green TCP-open tick.
|
||||
| **FOCAS** | `cnc_allclibhndl3` via a direct `DllImport("fwlib32")` in the probe. See [degrade semantics](#focas-degrade) below. | `"FOCAS handle OK"` | Deferred — no CNC + FWLIB |
|
||||
| **Galaxy** | gRPC unary call to `GalaxyRepository.TestConnection` on the configured mxaccessgw endpoint. See [auth-rejection rule](#galaxy-auth-rejection) below. | `"gateway gRPC OK"` | `http://10.100.0.48:5120` (mxaccessgw) |
|
||||
|
||||
**Historian.Wonderware** already performed a real handshake (`Hello` → `HelloAck`)
|
||||
before Phase 5 and was not changed by this work. See
|
||||
[`Historian.Wonderware.md`](Historian.Wonderware.md) for details.
|
||||
**Historian.Wonderware** had a TCP `Hello`→`HelloAck` handshake probe before Phase 5, but the
|
||||
Wonderware historian backend (and its driver-type / probe) has since been **retired** — the historian
|
||||
backend is now the external HistorianGateway (a gRPC client package, not a probed `IDriver`). See
|
||||
[`Historian.Wonderware.md`](Historian.Wonderware.md) (retired stub) and [`../Historian.md`](../Historian.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<section class="panel notice rise" style="animation-delay:.02s">
|
||||
Snapshot from the local node's <span class="mono">HistorianAdapterActor</span>. Default sink
|
||||
is a no-op (<span class="mono">NullAlarmHistorianSink</span>); production wires
|
||||
<span class="mono">SqliteStoreAndForwardSink</span> with the Wonderware historian sidecar
|
||||
behind it. Polling every @PollSeconds s.
|
||||
<span class="mono">SqliteStoreAndForwardSink</span> draining to the HistorianGateway
|
||||
(<span class="mono">SendEvent</span>) behind it. Polling every @PollSeconds s.
|
||||
</section>
|
||||
|
||||
@if (_status is null)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<label class="form-label">HistorizeToAveva</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.HistorizeToAveva" class="form-check-input" />
|
||||
<label class="form-check-label">Route to Wonderware sidecar</label>
|
||||
<label class="form-check-label">Route to HistorianGateway</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
<label class="form-label">Historize</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.Historize" class="form-check-input" />
|
||||
<label class="form-check-label">Send to Wonderware historian</label>
|
||||
<label class="form-check-label">Send to HistorianGateway</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
/// <param name="historianDataSource">The server-side HistoryRead backend resolved from DI — the
|
||||
/// <c>NullHistorianDataSource</c> default seeded by <c>AddOtOpcUaRuntime</c> (which runs on this driver
|
||||
/// node, the same source the address-space sink + node-write gateway come from), or the configured
|
||||
/// Wonderware read client when <c>AddServerHistorian</c> enabled it. Wired onto the node manager in
|
||||
/// HistorianGateway read client when <c>AddServerHistorian</c> enabled it. Wired onto the node manager in
|
||||
/// <see cref="StartAsync"/>.</param>
|
||||
/// <param name="configuration">App configuration; the <c>ServerHistorian</c> section is bound here to
|
||||
/// read <see cref="ServerHistorianOptions.MaxTieClusterOverfetch"/> for the node manager. Bound directly
|
||||
@@ -211,7 +211,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
logger: _loggerFactory.CreateLogger<ActorNodeWriteGateway>()));
|
||||
|
||||
// Wire the server-side read backend resolved from DI — the NullHistorianDataSource default (when
|
||||
// the ServerHistorian section is disabled) or the configured Wonderware read client (when enabled).
|
||||
// the ServerHistorian section is disabled) or the configured HistorianGateway read client (when enabled).
|
||||
// The node manager's HistoryRead overrides block-bridge to whatever source is set here.
|
||||
_server.SetHistorianDataSource(_historianDataSource);
|
||||
|
||||
|
||||
@@ -11,24 +11,37 @@
|
||||
"DisableLogin": false
|
||||
}
|
||||
},
|
||||
"ServerHistorian": {
|
||||
"_comment": "Server-side HistoryRead backend (the ZB.MOM.WW.HistorianGateway gRPC client). Disabled => NullHistorianDataSource (historized nodes return GoodNoData). The gateway must run RuntimeDb:EventReadsEnabled=true for alarm-history ReadEvents, and the API key must carry historian:read + historian:write + historian:tags:write scopes.",
|
||||
"Enabled": false,
|
||||
"Endpoint": "",
|
||||
"ApiKey": "",
|
||||
"_ApiKeyComment": "NEVER commit a real key. Supply via the environment variable ServerHistorian__ApiKey.",
|
||||
"UseTls": true,
|
||||
"AllowUntrustedServerCertificate": false,
|
||||
"CaCertificatePath": null,
|
||||
"CallTimeout": "00:00:30",
|
||||
"MaxTieClusterOverfetch": 65536
|
||||
},
|
||||
"ContinuousHistorization": {
|
||||
"_comment": "Continuous historization of driver (non-Galaxy) tag values: a crash-safe FasterLog outbox + recorder draining to the ServerHistorian gateway's WriteLiveValues. Disabled => no recorder is spawned. Requires ServerHistorian to be configured; the gateway connection (endpoint/key/TLS) is sourced from the ServerHistorian section, not here.",
|
||||
"Enabled": false,
|
||||
"OutboxPath": "",
|
||||
"_OutboxPathComment": "Directory holding the FasterLog segment + commit files. Required when Enabled=true. In production set an ABSOLUTE path on durable storage.",
|
||||
"CommitMode": "PerEntry",
|
||||
"CommitIntervalMs": 100,
|
||||
"DrainBatchSize": 64,
|
||||
"DrainIntervalSeconds": 2,
|
||||
"Capacity": 0,
|
||||
"MinBackoffSeconds": 1,
|
||||
"MaxBackoffSeconds": 30
|
||||
},
|
||||
"AlarmHistorian": {
|
||||
"_comment": "Durable SQLite store-and-forward alarm sink. Drains alarm events to the ServerHistorian gateway's SendEvent path; the downstream connection (endpoint/key/TLS) is sourced from the ServerHistorian section.",
|
||||
"Enabled": false,
|
||||
"DatabasePath": "alarm-historian.db",
|
||||
"Host": "localhost",
|
||||
"Port": 32569,
|
||||
"UseTls": false,
|
||||
"ServerCertThumbprint": null,
|
||||
"SharedSecret": "",
|
||||
"DrainIntervalSeconds": 5,
|
||||
"Capacity": 1000000,
|
||||
"DeadLetterRetentionDays": 30
|
||||
},
|
||||
"ServerHistorian": {
|
||||
"Enabled": false,
|
||||
"Host": "localhost",
|
||||
"Port": 32569,
|
||||
"UseTls": false,
|
||||
"ServerCertThumbprint": null,
|
||||
"SharedSecret": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ internal sealed record HistoryContinuationState(
|
||||
|
||||
/// <summary>
|
||||
/// Pure server-side continuation-point paging decisions for the count-capped variable-history arms
|
||||
/// (Raw / Processed). The backend (Wonderware sidecar) does NOT page — it returns up to
|
||||
/// (Raw / Processed). The backend (the HistorianGateway read client) does NOT page — it returns up to
|
||||
/// <c>NumValuesPerNode</c> samples with a null continuation point — so paging is synthesised here,
|
||||
/// time-based:
|
||||
/// <list type="bullet">
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
/// Wire the server-side HistoryRead backend (the <see cref="IHistorianDataSource"/> the node
|
||||
/// manager's HistoryRead overrides block-bridge to) onto the created
|
||||
/// <see cref="OtOpcUaNodeManager"/>. The host calls this after start with the DI-resolved source —
|
||||
/// the <c>NullHistorianDataSource</c> default (GoodNoData-empty reads) or the configured Wonderware
|
||||
/// the <c>NullHistorianDataSource</c> default (GoodNoData-empty reads) or the configured HistorianGateway
|
||||
/// read client. Passing <c>null</c> restores the Null default (the property setter null-coalesces),
|
||||
/// i.e. "no historian". No-op (returns <c>false</c>) when the node manager has not been created yet,
|
||||
/// so the caller can detect a too-early call (mirrors <see cref="SetNodeWriteGateway"/>).
|
||||
|
||||
@@ -16,9 +16,9 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||
/// Galaxy native alarm bridge, AB CIP ALMD reader) tells <see cref="AlarmHistorianEvent"/>s to this
|
||||
/// actor; the actor enqueues them on the sink fire-and-forget. Production deployments register
|
||||
/// <see cref="SqliteStoreAndForwardSink"/> against <c>IAlarmHistorianSink</c>; the sink owns the
|
||||
/// durable queue + drain-to-Wonderware-TCP-channel loop. The actor here owns nothing operational beyond
|
||||
/// durable queue + drain-to-HistorianGateway-SendEvent loop. The actor here owns nothing operational beyond
|
||||
/// the message contract — its job is to keep the engine actors on Akka's mailbox without blocking
|
||||
/// them on disk I/O or TCP channel handshakes.
|
||||
/// them on disk I/O or gateway round-trips.
|
||||
///
|
||||
/// Query queue depth + drain health via <see cref="GetStatus"/>.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user