docs(#655): history is server-side only; rewrite OPC-009 and stale driver-history references

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 23:36:25 -04:00
parent f7f7d948cd
commit 67059e479f
21 changed files with 72 additions and 97 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ multiple gateways is not.
### 4. Alarms / history
PCCC has no alarm object + no history object. Driver doesn't implement
`IAlarmSource` or `IHistoryProvider` — no test coverage is the correct shape.
`IAlarmSource` (history is server-side for every driver) — no test coverage is the correct shape.
### 5. File-type coverage
+1 -1
View File
@@ -34,7 +34,7 @@ For the driver spec (capability surface, config shape, addressing), see [docs/v2
+---------------------------------------+
```
History reads moved server-side in PR 7.2 (`IHistoryRouter`). Galaxy no longer implements `IHistoryProvider` of its own.
History reads are served server-side by `IHistorianDataSource` (HistorianGateway); Galaxy has no history capability of its own.
`IAlarmSource` was retired with PR 7.2 and **restored in PR B.2** of the
alarms-over-gateway epic ([docs/plans/alarms-over-gateway.md](../plans/alarms-over-gateway.md)).
+2 -2
View File
@@ -87,8 +87,8 @@ boundary splits, etc.).
### 5. Alarms / history
Not a Modbus concept. Driver doesn't implement `IAlarmSource` or
`IHistoryProvider`; no test coverage is the correct shape.
Not a Modbus concept. Driver doesn't implement `IAlarmSource` (no driver has a history
capability; history is server-side); no test coverage is the correct shape.
## When to trust the Modbus fixture, when to reach for a rig
+3 -3
View File
@@ -23,9 +23,9 @@ coverage map, see [Modbus-Test-Fixture.md](Modbus-Test-Fixture.md).
## Capability Surface
`ModbusDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable`
(`Driver.Modbus/ModbusDriver.cs`). There is **no `IAlarmSource`** and no
`IHistoryProvider` — the Modbus protocol expresses neither, so those capabilities
are out of scope by design.
(`Driver.Modbus/ModbusDriver.cs`). There is **no `IAlarmSource`** — the Modbus protocol has no
alarm object, so that capability is out of scope by design. (No driver carries a
history capability; history is server-side.)
| Capability | Implementation entry point | Notes |
|------------|---------------------------|-------|
+1 -1
View File
@@ -55,7 +55,7 @@ public sealed class MqttDriver
| `IHostConnectivityProbe` | `GetHostStatuses` | 1-second poll of `MqttConnection.State`; `HostName` is `"{host}:{port}"` |
| `IRediscoverable` | `OnRediscoveryNeeded` | Nothing raises it in Plain mode. **Sparkplug raises it** when a birth for an *authored* scope carries a metric-name set different from the last one seen for that scope — both conditions are anti-storm, see [Rediscovery](#rediscovery-sparkplug) |
**Not implemented:** `IPerCallHostResolver`, `IAlarmSource`, `IHistoryProvider`.
**Not implemented:** `IPerCallHostResolver`, `IAlarmSource`.
Materialized nodes are `IsAlarm: false`, `IsHistorized: false`. A node is
`SecurityClassification.Operate` when its tag declares a write destination and
`ViewOnly` when it does not — the browse-time answer comes from the same encoder the write path uses,
+5 -14
View File
@@ -58,7 +58,7 @@ Wire-level surfaces verified: `IDriver` + `IReadable` + `ISubscribable` +
against the in-process OtOpcUa server (`ServerEventRig`, authenticating with the rig's API-key credential):
read (+ missing node), write through the node-write gateway (+ read-only refusal, + failed-write revert),
subscribe/unsubscribe, host-status Running -> not-Running when the server stops, and discovery. These need
no Docker. The HistoryRead wire leg is deferred (needs an `IHistorianDataSource` stub on the server).
no Docker. There is no HistoryRead leg: the driver has no history capability (#655).
### Unit
@@ -82,11 +82,10 @@ mirrors the full capability matrix):
driver config
- `OpcUaClientAlarmTests` — A&E severity bucket (1–1000 → Low / Medium /
High / Critical), subscribe / unsubscribe / ack contract
- `OpcUaClientHistoryTests` — historical data read + interpolation contract
Capability surfaces whose contract is verified: `IDriver`, `ITagDiscovery`,
`IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`,
`IAlarmSource`, `IHistoryProvider`.
`IAlarmSource`.
## What it does NOT cover
@@ -113,15 +112,7 @@ The lossy window is not characterized.
session — the shape that breaks publish-queue-size tuning in the wild — is
not exercised.
### 4. Real historian mappings
`IHistoryProvider.ReadRawAsync` + `ReadProcessedAsync` +
`ReadAtTimeAsync` + `ReadEventsAsync` are contract-mocked. Against a real
historian (AVEVA Historian, Prosys historian, Kepware LocalHistorian) each
has specific interpolation + bad-quality-handling quirks the contract test
doesn't see.
### 5. Real A&E events
### 4. Real A&E events
Alarm subscription is mocked via filtered monitored items; the actual
`EventFilter` select-clause behavior against a server that exposes typed
@@ -187,8 +178,8 @@ Beyond that:
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml`
— `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--ut --aa --alm --pn=50000`
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
session-factory seam tests mock through; implements `IAlarmSource` +
`IHistoryProvider` (unique among drivers); does NOT implement `IRediscoverable`
session-factory seam tests mock through; implements `IAlarmSource`;
does NOT implement `IRediscoverable`
- `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs` —
the v2 dual-endpoint integration harness a future loopback client test could
piggyback on (v1 `OpcUaServerIntegrationTests.cs` retired with the v1 server project)
+11 -29
View File
@@ -79,7 +79,7 @@ initialise (the AdminUI form drops them on the next save). Pinned by
## Capability surface
`OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IAlarmSource, IHistoryProvider`
`OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IAlarmSource`
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs:31`).
| Capability | Path | Notes |
@@ -90,7 +90,6 @@ initialise (the AdminUI form drops them on the next save). Pinned by
| `ISubscribable` | native OPC UA subscriptions / monitored items | The remote server pushes data changes |
| `IHostConnectivityProbe` | session keep-alive | Host key is the endpoint URL actually connected to after the failover sweep |
| `IAlarmSource` | `SubscribeAlarmsAsync` (EventFilter) + `AcknowledgeAsync` | Subscribes to upstream alarm/condition events and forwards acks |
| `IHistoryProvider` | `ReadRawAsync` / `ReadProcessedAsync` / `ReadAtTimeAsync` → `Session.HistoryReadAsync` | **Unique to this driver** — passthrough history read against the upstream server |
> **Native alarms (#619).** An alarm raw tag is `TagConfig` `{ "nodeId": "<upstream SourceNode>", "alarm": {...} }`.
> The host subscribes with RawPaths and routes on (driver, RawPath), so the driver matches each
@@ -112,35 +111,18 @@ initialise (the AdminUI form drops them on the next save). Pinned by
> This driver does **not** implement `IRediscoverable` — there is no
> push-driven rediscovery signal from a remote OPC UA server in this driver.
> `IHistoryProvider` is implemented by no other driver; history reads for every
> other source route server-side through `IHistoryRouter`.
### History passthrough
### History: not supported (Gitea #655)
`IHistoryProvider` forwards `HistoryRead` to the upstream server's own historian.
Raw, processed (the full `HistoryAggregateType` set — Average / Minimum / Maximum / Total / Count plus
Interpolative, TimeAverage, Start, End, Delta, Range, Min/Max ActualTime, Start/EndBound, DeltaBounds,
DurationGood/Bad, PercentGood/Bad, WorstQuality, NumberOfTransitions, AnnotationCount and the
population StandardDeviation/Variance — each mapped to its OPC UA Part 13 standard aggregate NodeId; #285),
and at-time reads are supported; each
returned `DataValue` keeps its upstream `StatusCode` and timestamps verbatim.
**The per-node `HistoryReadResult.StatusCode` is honoured (#649).** The driver never reads a Bad node status as
empty data: `BadAggregateNotSupported` on a Processed read throws `HistoryAggregateNotSupportedException`, and any
other Bad status (`BadUserAccessDenied`, `BadNodeIdUnknown`, `BadHistoryOperationUnsupported`, ...) throws
`HistoryReadStatusException` carrying the code (the constructor rejects non-Bad codes). `Good`, `GoodNoData`,
`GoodMoreData` (continuation point) and `Uncertain` are unchanged, and a node-level `BadNoData` is still an empty
result. The status is matched on its code bits, so an info/overflow sub-status does not dodge the match.
⚠️ **This is currently latent.** The OtOpcUa server does not dispatch HistoryRead to a driver's `IHistoryProvider`
(it serves only the server-wide `IHistorianDataSource`, see `docs/Historian.md`), so no OPC UA client of OtOpcUa
sees this driver's history results or its refusals today. The server's HistoryRead arms (`ServeNodeAsync`,
`ServeRawPagedAsync`) do map a `HistoryReadStatusException` thrown by an `IHistorianDataSource` to that node's
status verbatim, and the driver/wire tests exercise that path; if driver history is ever routed to the server,
refusals will reach clients correctly instead of as empty Good data. A routed deterministic refusal would also
need excluding from the HistoryRead pipeline's retry/breaker handling.
Event-history (`ReadEventsAsync`) is left at the interface default — the
interface doesn't yet carry the EventFilter surface needed to forward it.
This driver does **not** proxy the upstream server's HistoryRead. The server serves
HistoryRead only through the server-wide `IHistorianDataSource` (see `docs/Historian.md`);
drivers carry no history capability, and the former `IHistoryProvider` passthrough
(Raw / Processed / AtTime / Events) was never dispatched to and has been deleted.
`isHistorized` on an OpcUaClient tag means OtOpcUa records the value into the historian
from now on (numeric only), not that upstream history is forwarded. A tag without
`isHistorized` has no HistoryRead access bit and answers `BadHistoryOperationUnsupported`.
If upstream-history proxying is wanted later, it needs a new intent key, a dispatch path
through `DriverHostActor`, and a continuation-point rework — see #655.
### Certificate trust
+4 -5
View File
@@ -9,10 +9,9 @@ OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `C
- `IHostConnectivityProbe` — per-host reachability events
- `IPerCallHostResolver` — multi-host drivers that route each call to a target endpoint at dispatch time
- `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
- `ISkippedTagReporter` — authored tags the driver could not build into its poll table (see [Skipped tags](#skipped-tags) below)
- `IHistorianDataSource` — server-side historian read backend registration (the HistorianGateway backend), distinct from the driver-side `IHistoryProvider` HistoryRead path
- `IHistorianDataSource` — server-side historian read backend registration (the HistorianGateway backend) (drivers carry no history capability; Gitea #655)
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.
@@ -71,7 +70,7 @@ Three properties worth knowing:
| [AB Legacy](AbLegacy.md) | `Driver.AbLegacy` | A | libplctag PCCC | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver | SLC 500 / MicroLogix. File-based addressing (`N7:0`, `F8:0`) — no symbol table, tag list is user-authored in the config DB |
| [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 |
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHostConnectivityProbe | Gateway/aggregation driver — does not proxy upstream history (no driver has a history capability). 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 |
| [MQTT](Mqtt.md) | `Driver.Mqtt` (+ `.Browser`, `.Contracts`) | A | [MQTTnet 5](https://github.com/dotnet/MQTTnet) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | **Push, not poll, and read-only** — the broker delivers PUBLISH frames the driver forwards straight to `ISubscribable.OnDataChange`; `IReadable` serves the last retained/received value from cache, and there is **no `IWritable`** (nothing publishes back). Subscriptions are indexed **by topic filter, not by topic**, so wildcard tags survive a reconnect. A rejected CONNACK throws `MqttConnectRejectedException`: unrecoverable codes → `Faulted` + supervisor stop, transient → retry. **Two modes:** `Plain` binds a tag to a concrete topic + JSON path; `SparkplugB` decodes vendored Eclipse-Tahu protobuf under one `spBv1.0/{groupId}/#` subscription and binds by the `group/edgeNode/device?/metric` tuple (birth/alias/seq-gap state machine, death→STALE, scoped rebirth NCMD). `IRediscoverable` fires on a DBIRTH but is **inert platform-side** — see [#507](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/507) |
| [MTConnect](MTConnect.md) | `Driver.MTConnect` (+ `.Contracts`) | — | Hand-rolled `System.Xml.Linq` HTTP/XML client against a vendor-neutral MTConnect Agent (`/probe`, `/current`, `/sample`) — no TrakHound dependency (dropped; see the driver doc) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | Read-only by design (no `IWritable`). Production data plane is entirely `ISubscribable`/`OnDataChange` — `IReadable` is CLI/test-only. Browse is free via the Wave-0 universal discovery browser, no bespoke browser project. `IRediscoverable`/`IHostConnectivityProbe` have no server-side consumer yet — a pre-existing fleet-wide gap, not MTConnect-specific |
| [Sql](Sql.md) | `Driver.Sql` (+ `.Browser`, `.Contracts`) | A | `Microsoft.Data.SqlClient` behind an `ISqlDialect` seam (`SqlServerDialect` shipped; Postgres/ODBC deferred) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe | Read-only DB-poll driver with a bespoke schema browser. **Credentials are never persisted** — the deploy gate rejects a stored `connectionString` (#498) and catalog identifiers are validated against the live catalog's own spelling (#496). `SupportsOnlineDiscovery => false` by design (the bespoke browser wins). `SqlTagModel.Query` is deferred to P3 and rejected end-to-end by parser, validator, planner and editor. Merged `4ad54037` |
@@ -93,7 +92,7 @@ Three properties worth knowing:
- [AbLegacy.md](AbLegacy.md) — AB Legacy PCCC driver (SLC 500 / MicroLogix / PLC-5): file-based addressing, user-authored tag list
- [S7.md](S7.md) — Siemens S7 driver (S7-300/400/1200/1500 + S7-200): getting started, config, data-block addressing, serialized single-connection model
- [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
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, reconnect behaviour
- [Mqtt.md](Mqtt.md) — MQTT broker-subscribe driver: broker/TLS/auth config, topic + JSON-path tag binding, retained-value seeding, `#`-observation browser, CONNACK-rejection semantics, **Sparkplug B** (birth/alias/seq/rebirth state machine, birth-driven browse picker, death→STALE), and the **Known gaps** table
- **MTConnect** has a short getting-started doc because the hand-rolled-vs-TrakHound divergence, the
@@ -122,7 +121,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 HistorianGateway-backed `IHistorianDataSource` instead. Other drivers do not implement the interface and return `BadHistoryOperationUnsupported`.
- [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md) — v1 archive of the retired per-driver history design (aggregate mapping, continuation points). History is now served only by the server-side `IHistorianDataSource` (HistorianGateway); no driver implements a history capability, and nodes without `isHistorized` 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, TwinCAT, and MQTT 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) — historical design record of the A / B / C tier model. Only `DriverTier` + the `CapabilityPolicy` defaults per tier × capability (`DriverResilienceOptions.GetTierDefaults`) are live; its `MemoryTracking` / recycle / out-of-process isolation machinery was deleted (#522) and every driver runs Tier A.
+1 -1
View File
@@ -155,7 +155,7 @@ TC3; TC2 compatibility is not exercised.
### 5. Alarms / history
Driver doesn't implement `IAlarmSource` or `IHistoryProvider` — not in scope
Driver doesn't implement `IAlarmSource` (history is server-side for every driver) — not in scope
for this driver family. TwinCAT 3's TcEventLogger could theoretically back
an `IAlarmSource`, but shipping that is a separate feature.