Auto: opcuaclient-12 — IHistoryProvider.ReadEventsAsync EventFilter spec + impl

Adds a filter-aware overload of IHistoryProvider.ReadEventsAsync that carries
EventFilter SelectClauses + WhereClause, and implements it on the OPC UA
Client driver via Session.HistoryReadAsync + ReadEventDetails.

The change is additive (default-impl returns NotSupportedException) so the
existing Galaxy.Proxy.GalaxyProxyDriver implementation keeps compiling
against the fixed-field overload — no cross-driver refactor required.

* Core.Abstractions: new EventHistoryRequest / SimpleAttributeSpec /
  ContentFilterSpec records mirror the OPC UA wire shape transport-neutrally.
  HistoricalEventBatch / HistoricalEventRow carry an open-ended Fields bag
  keyed by SimpleAttributeSpec.FieldName so server-side dispatch can re-align
  with the client's wire-side SelectClause order.
* OpcUaClient driver: new ReadEventsAsync(fullReference, EventHistoryRequest, ct)
  builds an EventFilter, calls Session.HistoryReadAsync, and unwraps
  HistoryEvent.Events into HistoricalEventBatch rows. Default SelectClause
  set matches BuildHistoryEvent on the server side. ContentFilter bytes are
  decoded through the live session's MessageContext (passthrough — the
  driver does not evaluate filters).
* Unit tests: 7 new tests cover SelectClause translation, default-clause
  fallback, malformed where-clause swallowing, uninitialized-driver guard,
  null-request guard, and IHistoryProvider default fallback.
* Integration scaffold: build-only [Fact] gated on opc-plc --alm; flips to
  green when the fixture image is upgraded.
* Docs: HistoryRead Events section in docs/drivers/OpcUaClient.md plus a
  cross-link from Client.CLI.md historyread page.
* E2E: -HistoryEvents switch on scripts/e2e/test-opcuaclient.ps1 confirms
  the gateway round-trips HistoryReadEvents without
  BadHistoryOperationUnsupported (gated; defaults to skip).

Closes #284
This commit is contained in:
Joseph Doherty
2026-04-26 09:29:40 -04:00
parent 2ee61c0999
commit c36903d6a0
7 changed files with 657 additions and 10 deletions

View File

@@ -126,3 +126,70 @@ expose a "ReverseConnect.Endpoint" config knob).
- Public-internet OPC UA: reverse-connect is a network-policy workaround,
not a security primitive. Always pair with `Sign` or `SignAndEncrypt`
+ a vetted user-token policy.
## HistoryRead Events
The driver passes through OPC UA `HistoryReadEvents` to the upstream server.
HistoryRead Raw / Processed / AtTime ship in the same code path
(`ExecuteHistoryReadAsync`); event history takes a slightly different shape
because the client sends an `EventFilter` (SelectClauses + WhereClause) rather
than a plain numeric / time-based detail block.
### Wire path
`IHistoryProvider.ReadEventsAsync(fullReference, EventHistoryRequest, ct)`
translates to:
```
new ReadEventDetails {
StartTime,
EndTime,
NumValuesPerNode,
Filter = EventFilter { SelectClauses, WhereClause }
}
```
…and is sent through `Session.HistoryReadAsync` to the upstream server. The
returned `HistoryEvent.Events` collection (one `HistoryEventFieldList` per
historical event) is unwrapped into `HistoricalEventBatch.Events`, where each
`HistoricalEventRow.Fields` dictionary is keyed by the
`SimpleAttributeSpec.FieldName` the caller supplied. The server-side history
dispatcher uses those keys to align fields with the wire-side SelectClause
order — drivers don't have to honour the entire OPC UA `EventFilter` shape
verbatim.
### SelectClauses
When `EventHistoryRequest.SelectClauses` is `null` the driver falls back to a
default set that matches `BuildHistoryEvent` on the server side:
| Field | Browse path | Notes |
| --- | --- | --- |
| `EventId` | `EventId` | BaseEventType — stable unique id. |
| `SourceName` | `SourceName` | Source-object name. |
| `Time` | `Time` | Process-side event timestamp. Used for `OccurrenceTime`. |
| `Message` | `Message` | LocalizedText payload. |
| `Severity` | `Severity` | OPC UA 1-1000 scale. |
| `ReceiveTime` | `ReceiveTime` | Server-side ingest timestamp. |
Custom SelectClauses are supported — pass any
`IReadOnlyList<SimpleAttributeSpec>`. Each entry's `TypeDefinitionId`
defaults to `BaseEventType` when `null`; pass an explicit NodeId text (e.g.
`"i=2782"` for `ConditionType`) to reach typed-condition fields.
### WhereClause
`ContentFilterSpec.EncodedOperands` carries the binary-encoded
`ContentFilter` from the wire. The driver decodes it into the SDK
`ContentFilter` and attaches it to the outgoing `EventFilter` verbatim — the
OPC UA Client driver is a passthrough for filter semantics, it does not
evaluate them. A malformed filter is dropped silently; the SelectClause
projection still goes out.
### Continuation points
Returned in `HistoricalEventBatch.ContinuationPoint`. The server-side
HistoryRead facade is responsible for round-tripping these so a paged event
read against a chatty upstream completes incrementally. The driver itself
doesn't track them — every `ReadEventsAsync` call issues a fresh
`HistoryReadAsync`.