Phase 2 PR 7 — wire IHistoryProvider.ReadProcessedAsync end-to-end #6

Merged
dohertj2 merged 1 commits from phase-2-pr7-history-processed into v2 2026-04-18 06:59:03 -04:00
Owner

Summary

Wire IHistoryProvider.ReadProcessedAsync end-to-end. PR 5 ported HistorianDataSource.ReadAggregateAsync into Galaxy.Host but left it internal — GalaxyProxyDriver.ReadProcessedAsync still threw NotSupportedException, so OPC UA HistoryReadProcessed calls against v2 were rejected at the driver boundary. This PR closes that gap.

Changes

  • Shared.Contracts — new HistoryReadProcessedRequest / HistoryReadProcessedResponse + MessageKind 0x62/0x63. Carries IntervalMs + AggregateColumn string so Galaxy.Host stays OPC-UA-free.
  • HostGalaxyFrameHandler routes the new kind; all three IGalaxyBackend implementations get HistoryReadProcessedAsync; MxAccessGalaxyBackend delegates to HistorianDataSource.ReadAggregateAsync, maps HistorianAggregateSample to GalaxyDataValue (null bucket → BadNoData 0x800E0000u, otherwise Good 0u).
  • ProxyReadProcessedAsync flipped from the NotSupported throw to a real IPC call; OPC UA HistoryAggregateType mapped to Wonderware AnalogSummary column name (Average / Minimum / Maximum / ValueCount). Total throws NotSupportedException with a message steering callers to Average × Count — there's no matching SDK column.

Guards

  • MxAccessGalaxyBackend.HistoryReadProcessedAsync rejects historian=null ("Historian disabled") and IntervalMs <= 0 ("HistoryReadProcessed requires IntervalMs > 0") up front.
  • SDK exceptions surface as Success=false with the message chained into the Proxy's InvalidOperationException.

Tests

  • HistoryReadProcessedTests (4) — disabled path, zero-interval rejection, value + column + interval flow through, null bucket → BadNoData.
  • AggregateColumnMappingTests (5) — theory over supported enum values, Total → NotSupported.
  • InternalsVisibleTo("...Proxy.Tests") added so MapAggregateToColumn can be tested without reflection.

Test plan

  • dotnet build ZB.MOM.WW.OtOpcUa.slnx — 0 errors, 201 pre-existing warnings
  • dotnet test tests/.../Host.Tests/ --filter "Category=Unit" — 28/28 pass
  • dotnet test tests/.../Proxy.Tests/ --filter "Category=Unit" — 14/14 pass
  • Reviewer: on a machine with Wonderware Historian + live data, fire a HistoryReadProcessed from the Client CLI against a historized attribute and compare per-bucket Average values against aahHistorian.exe for parity.

Deferred

  • ReadAtTime + ReadEvents + Health IPC surfaces — ported in PR 5 but need their own contract messages.
  • Alarm subsystem wire-up (OnAlarmEvent raising from MxAccessGalaxyBackend) — overlaps the ReadEventsAsync IPC work since both come from HistorianAccess.CreateEventQuery.
  • Proxy-side richer quality-byte mapping — today we map Good/Uncertain/Bad categories; the raw byte survives the IPC hop so the Proxy could promote specific OPC DA codes (BadNotConnected, UncertainSubNormal, etc.) before handing DataValueSnapshot to ISubscribable consumers.

Merge order

Branches off phase-2-pr5-historian. Fast-forwards once PR 5 merges; otherwise rebase after PR 5 lands.


🤖 Generated with Claude Code

## Summary Wire `IHistoryProvider.ReadProcessedAsync` end-to-end. PR 5 ported `HistorianDataSource.ReadAggregateAsync` into Galaxy.Host but left it internal — `GalaxyProxyDriver.ReadProcessedAsync` still threw `NotSupportedException`, so OPC UA `HistoryReadProcessed` calls against v2 were rejected at the driver boundary. This PR closes that gap. ## Changes - **Shared.Contracts** — new `HistoryReadProcessedRequest` / `HistoryReadProcessedResponse` + `MessageKind` 0x62/0x63. Carries `IntervalMs` + `AggregateColumn` string so Galaxy.Host stays OPC-UA-free. - **Host** — `GalaxyFrameHandler` routes the new kind; all three `IGalaxyBackend` implementations get `HistoryReadProcessedAsync`; `MxAccessGalaxyBackend` delegates to `HistorianDataSource.ReadAggregateAsync`, maps `HistorianAggregateSample` to `GalaxyDataValue` (null bucket → `BadNoData 0x800E0000u`, otherwise `Good 0u`). - **Proxy** — `ReadProcessedAsync` flipped from the `NotSupported` throw to a real IPC call; OPC UA `HistoryAggregateType` mapped to Wonderware AnalogSummary column name (`Average` / `Minimum` / `Maximum` / `ValueCount`). `Total` throws `NotSupportedException` with a message steering callers to `Average × Count` — there's no matching SDK column. ## Guards - `MxAccessGalaxyBackend.HistoryReadProcessedAsync` rejects `historian=null` ("Historian disabled") and `IntervalMs <= 0` ("HistoryReadProcessed requires IntervalMs > 0") up front. - SDK exceptions surface as `Success=false` with the message chained into the Proxy's `InvalidOperationException`. ## Tests - `HistoryReadProcessedTests` (4) — disabled path, zero-interval rejection, value + column + interval flow through, null bucket → BadNoData. - `AggregateColumnMappingTests` (5) — theory over supported enum values, `Total` → NotSupported. - `InternalsVisibleTo("...Proxy.Tests")` added so `MapAggregateToColumn` can be tested without reflection. ## Test plan - [x] `dotnet build ZB.MOM.WW.OtOpcUa.slnx` — 0 errors, 201 pre-existing warnings - [x] `dotnet test tests/.../Host.Tests/ --filter "Category=Unit"` — 28/28 pass - [x] `dotnet test tests/.../Proxy.Tests/ --filter "Category=Unit"` — 14/14 pass - [ ] Reviewer: on a machine with Wonderware Historian + live data, fire a `HistoryReadProcessed` from the Client CLI against a historized attribute and compare per-bucket Average values against `aahHistorian.exe` for parity. ## Deferred - `ReadAtTime` + `ReadEvents` + `Health` IPC surfaces — ported in PR 5 but need their own contract messages. - Alarm subsystem wire-up (`OnAlarmEvent` raising from `MxAccessGalaxyBackend`) — overlaps the `ReadEventsAsync` IPC work since both come from `HistorianAccess.CreateEventQuery`. - Proxy-side richer quality-byte mapping — today we map Good/Uncertain/Bad categories; the raw byte survives the IPC hop so the Proxy could promote specific OPC DA codes (BadNotConnected, UncertainSubNormal, etc.) before handing `DataValueSnapshot` to `ISubscribable` consumers. ## Merge order Branches off `phase-2-pr5-historian`. Fast-forwards once PR 5 merges; otherwise rebase after PR 5 lands. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 changed target branch from phase-2-pr5-historian to v2 2026-04-18 06:58:43 -04:00
dohertj2 added 1 commit 2026-04-18 06:58:43 -04:00
Phase 2 PR 7 — wire IHistoryProvider.ReadProcessedAsync end-to-end. PR 5 ported HistorianDataSource.ReadAggregateAsync into Galaxy.Host but left it internal — GalaxyProxyDriver.ReadProcessedAsync still threw NotSupportedException, so OPC UA clients issuing HistoryReadProcessed requests against the v2 topology got rejected at the driver boundary. This PR closes that gap by adding two new Shared.Contracts messages (HistoryReadProcessedRequest/Response, MessageKind 0x62/0x63), routing them through GalaxyFrameHandler, implementing HistoryReadProcessedAsync on all three IGalaxyBackend implementations (Stub/DbBacked return the canonical "pending" Success=false, MxAccessGalaxyBackend delegates to _historian.ReadAggregateAsync), mapping HistorianAggregateSample → GalaxyDataValue at the IPC boundary (null bucket Value → BadNoData 0x800E0000u, otherwise Good 0u), and flipping GalaxyProxyDriver.ReadProcessedAsync from the NotSupported throw to a real IPC call with OPC UA HistoryAggregateType enum mapped to Wonderware AnalogSummary column name on the Proxy side (Average → "Average", Minimum → "Minimum", Maximum → "Maximum", Count → "ValueCount", Total → NotSupported since there's no direct SDK column for sum). Decision #13 IPC data-shape stays intact — HistoryReadProcessedResponse carries GalaxyDataValue[] with the same MessagePack value + OPC UA StatusCode + timestamps shape as the other history responses, so the Proxy's existing ToSnapshot helper handles the conversion without a new code path. MxAccessGalaxyBackend.HistoryReadProcessedAsync guards: null historian → "Historian disabled" (symmetric with HistoryReadAsync); IntervalMs <= 0 → "HistoryReadProcessed requires IntervalMs > 0" (prevents division-by-zero inside the SDK's Resolution parameter); exception during SDK call → Success=false Values=[] with the message so the Proxy surfaces it as InvalidOperationException with a clean error chain. Tests — HistoryReadProcessedTests (new, 4 cases): disabled-error when historian null, rejects zero interval, maps Good sample with Value=12.34 and the Proxy-supplied AggregateColumn + IntervalMs flow unchanged through to the fake IHistorianDataSource, maps null Value bucket to 0x800E0000u BadNoData with null ValueBytes. AggregateColumnMappingTests (new, 5 cases in Proxy.Tests): theory covers all 4 supported HistoryAggregateType enum values → correct column string, and asserts Total throws NotSupportedException with a message that steers callers to Average/Minimum/Maximum/Count (the SDK's AnalogSummaryQueryResult doesn't expose a sum column — the closest is Average × ValueCount which is the responsibility of a caller-side aggregation rather than an extra IPC round-trip). InternalsVisibleTo added to Galaxy.Proxy csproj so Proxy.Tests can reach the internal MapAggregateToColumn static. Builds — Galaxy.Host (net48 x86) + Galaxy.Proxy (net10) both 0 errors, full solution 201 warnings (pre-existing) / 0 errors. Test counts — Host.Tests Unit suite: 28 pass (4 new processed + 9 PR5 historian + 15 pre-existing); Proxy.Tests Unit suite: 14 pass (5 new column-mapping + 9 pre-existing). Deferred to a later PR — ReadAtTime + ReadEvents + Health IPC surfaces (HistorianDataSource has them ported in PR 5 but they need additional contract messages and would push this PR past a comfortable review size); the alarm subsystem wire-up (OnAlarmEvent raising from MxAccessGalaxyBackend) which overlaps the ReadEventsAsync IPC work since both pull from HistorianAccess.CreateEventQuery on the SDK side; the Proxy-side quality-byte refinement where HistorianDataSource's per-sample raw quality byte gets decoded through the existing QualityMapper instead of the category-only mapping in ToWire(HistorianSample) — doesn't change correctness today since Good/Uncertain/Bad categories are all the Admin UI and OPC UA clients surface, but richer OPC DA status codes (BadNotConnected, UncertainSubNormal, etc.) are available on the wire and the Proxy could promote them before handing DataValueSnapshot to ISubscribable consumers. This PR branches off phase-2-pr5-historian because it directly extends the Historian IPC surface added there; if PR 5 merges first PR 7 fast-forwards, otherwise it needs a rebase after PR 5 lands. 3717405aa6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit e250356e2a into v2 2026-04-18 06:59:03 -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#6