Phase 3 PR 35 — IHistoryProvider gains ReadAtTime + ReadEvents; Proxy implements both #34

Merged
dohertj2 merged 1 commits from phase-3-pr35-history-readtime-readevents into v2 2026-04-18 16:12:44 -04:00
Owner

Rounds out the OPC UA Part 11 HistoryRead capability surface. HistoryReadAtTime + HistoryReadEvents are the last two modes not covered by the existing ReadRawAsync / ReadProcessedAsync. Consumes the PR-10 / PR-11 IPC contracts the Host already ships.

Interface additions

Added to Core.Abstractions.IHistoryProvider with default interface implementations that throw NotSupportedException — existing impls (Galaxy / OPC UA Client) keep compiling, only drivers whose backend carries the capability override:

  • ReadAtTimeAsync(string fullReference, IReadOnlyList<DateTime> timestampsUtc, …) — interpolated per-timestamp sample.
  • ReadEventsAsync(string? sourceName, DateTime start, DateTime end, int maxEvents, …) — historian event rows.

New domain types in Core.Abstractions:

  • HistoricalEvent — one historian row (EventId, SourceName, EventTimeUtc vs ReceivedTimeUtc, Message, Severity).
  • HistoricalEventsResult — event list + continuation-point.

Proxy implementation

GalaxyProxyDriver.ReadAtTimeAsync converts DateTime[] → Unix-ms, calls the existing MessageKind.HistoryReadAtTimeRequest. Trusts the Host's one-sample-per-timestamp contract (Host pads with bad-quality on missing timestamps — re-aligning on the Proxy would duplicate the Host's interpolation policy).

GalaxyProxyDriver.ReadEventsAsync hits HistoryReadEventsRequest; ToHistoricalEvent maps GalaxyHistoricalEvent (MessagePack, Unix-ms) → HistoricalEvent with DateTimeKind.Utc tagged on both timestamps so downstream serializers don't apply local-time offsets.

Tests

HistoricalEventMappingTests (3 new unit cases):

  • Every field maps correctly wire → domain.
  • Null SourceName / DisplayText preserve — system events without a source come out with null, callers can distinguish them from alarm events.
  • Both timestamps come out as DateTimeKind.Utc — regression guard against a future refactor using conversions that default to Unspecified.

Driver.Galaxy.Proxy.Tests Unit: 17 / 0 (14 prior + 3 new). Full solution builds clean.

Scope exclusions (next PR)

  • DriverNodeManager HistoryRead service-handler wiring — where the OPC UA Server routes HistoryReadAtTime / HistoryReadEvents service requests onto the capability methods. Mechanical compared to this PR; split for reviewability.
  • Full-loop integration test (OPC UA client → server → IPC → Host → HistorianDataSource → back).

docs/v2/lmx-followups.md #1 updated to reflect the split — capability surface complete, service-handler wiring + integration test remain.

Rounds out the OPC UA Part 11 HistoryRead capability surface. `HistoryReadAtTime` + `HistoryReadEvents` are the last two modes not covered by the existing `ReadRawAsync` / `ReadProcessedAsync`. Consumes the PR-10 / PR-11 IPC contracts the Host already ships. ## Interface additions Added to `Core.Abstractions.IHistoryProvider` with **default interface implementations** that throw `NotSupportedException` — existing impls (Galaxy / OPC UA Client) keep compiling, only drivers whose backend carries the capability override: - `ReadAtTimeAsync(string fullReference, IReadOnlyList<DateTime> timestampsUtc, …)` — interpolated per-timestamp sample. - `ReadEventsAsync(string? sourceName, DateTime start, DateTime end, int maxEvents, …)` — historian event rows. New domain types in `Core.Abstractions`: - `HistoricalEvent` — one historian row (EventId, SourceName, EventTimeUtc vs ReceivedTimeUtc, Message, Severity). - `HistoricalEventsResult` — event list + continuation-point. ## Proxy implementation `GalaxyProxyDriver.ReadAtTimeAsync` converts `DateTime[]` → Unix-ms, calls the existing `MessageKind.HistoryReadAtTimeRequest`. Trusts the Host's one-sample-per-timestamp contract (Host pads with bad-quality on missing timestamps — re-aligning on the Proxy would duplicate the Host's interpolation policy). `GalaxyProxyDriver.ReadEventsAsync` hits `HistoryReadEventsRequest`; `ToHistoricalEvent` maps `GalaxyHistoricalEvent` (MessagePack, Unix-ms) → `HistoricalEvent` with `DateTimeKind.Utc` tagged on both timestamps so downstream serializers don't apply local-time offsets. ## Tests `HistoricalEventMappingTests` (3 new unit cases): - Every field maps correctly wire → domain. - Null `SourceName` / `DisplayText` preserve — system events without a source come out with null, callers can distinguish them from alarm events. - Both timestamps come out as `DateTimeKind.Utc` — regression guard against a future refactor using conversions that default to `Unspecified`. Driver.Galaxy.Proxy.Tests Unit: **17 / 0** (14 prior + 3 new). Full solution builds clean. ## Scope exclusions (next PR) - `DriverNodeManager` HistoryRead service-handler wiring — where the OPC UA Server routes HistoryReadAtTime / HistoryReadEvents service requests onto the capability methods. Mechanical compared to this PR; split for reviewability. - Full-loop integration test (OPC UA client → server → IPC → Host → `HistorianDataSource` → back). `docs/v2/lmx-followups.md` #1 updated to reflect the split — capability surface complete, service-handler wiring + integration test remain.
dohertj2 added 1 commit 2026-04-18 16:12:42 -04:00
Interface additions use C# default interface implementations that throw NotSupportedException — existing IHistoryProvider implementations keep compiling, only drivers whose backend carries the relevant capability override. This matches the 'capabilities are optional per driver' design already used by IHistoryProvider.ReadProcessedAsync's docs (Modbus / OPC UA Client drivers never had an event historian and the default-throw path lets callers see BadHistoryOperationUnsupported naturally). New HistoricalEvent record models one historian row (EventId, SourceName, EventTimeUtc + ReceivedTimeUtc — process vs historian-persist timestamps, Message, Severity mapped to OPC UA's 1-1000 range); HistoricalEventsResult pairs the event list with a continuation-point token for future batching. Both live in Core.Abstractions so downstream (Proxy, Host, Server) reference a single domain shape — no Shared-contract leak into the driver-facing interface.
GalaxyProxyDriver.ReadAtTimeAsync maps the domain DateTime[] to Unix-ms longs, calls CallAsync on the existing MessageKind.HistoryReadAtTimeRequest, and trusts the Host's one-sample-per-requested-timestamp contract (the Host pads with bad-quality snapshots for timestamps it can't interpolate; re-aligning on the Proxy side would duplicate the Host's interpolation policy logic). ReadEventsAsync does the same for HistoryReadEventsRequest; ToHistoricalEvent translates GalaxyHistoricalEvent (MessagePack-annotated, Unix-ms) to the domain record, explicitly tagging DateTimeKind.Utc on both timestamp fields so downstream serializers (JSON, OPC UA types) don't apply an unexpected local-time offset.
Tests — HistoricalEventMappingTests (3 new Proxy.Tests unit cases): every field maps correctly from wire to domain; null SourceName and null DisplayText preserve through the mapping (system events without a source come out with null so callers can distinguish them from alarm events); both timestamps come out as DateTimeKind.Utc (regression guard against a future refactor using DateTime.FromFileTimeUtc or similar that defaults to Unspecified). Driver.Galaxy.Proxy.Tests Unit suite: 17 pass / 0 fail (14 prior + 3 new). Full solution build clean, 0 errors.
Scope exclusions — DriverNodeManager HistoryRead service-handler wiring (on the OPC UA Server side, where HistoryReadAtTime and HistoryReadEvents service requests land) and the full-loop integration test (OPC UA client → server → IPC → Host → HistorianDataSource → back) are deferred to a focused follow-up PR. The capability surface is the load-bearing change; wiring the service handlers is mechanical in comparison and worth its own PR for reviewability. docs/v2/lmx-followups.md #1 updated with the split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit 5cc120d836 into v2 2026-04-18 16:12:44 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#34