From 085f01123c6d7ceeca57c1823ecd50e9e62eafe2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 20 Jun 2026 15:53:59 -0400 Subject: [PATCH] docs: scope R1.8/R1.9 summary queries (decode targets located) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Established that analog/state-summary queries are reachable on 2020 WCF — they ride the proven uint-handle StartQuery2 path, and the request serializer already carries QueryType/SummaryType/ColumnSelectorFlags. Located every decode target in aahClientManaged.dll: - CAnalogSummaryValue.UnpackFromValueBuffer (0x06000394) — row decoder - CAnalogSummaryValue/Struct fields — Min/Max/First/Last/ValueCount/TimeGood/ Integral/IntegralOfSquares (+ per-field DateTimes, LinearIntegral) - CStateSummaryStruct — MinContained/MaxContained/TotalContained/PartialStart/ PartialEnd/StateEntryCount - QueryColumnSelector.Select{Analog,State,NonSummary}Columns — column flags - INSQL_QUERYTYPE / HISTORIAN_SUMMARYTYPE — the query/summary enum values UnpackFromValueBuffer is reader-call-based (no literal offsets), so a correct parser needs a captured real buffer. Per project discipline no guessed summary code was added to src/. New plan doc lays out the recover-params -> live-capture -> decode -> implement+test path. Roadmap R1.8/R1.9 marked scoped/ready. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/hcal-roadmap.md | 4 +- docs/plans/r1.8-r1.9-summary-queries.md | 66 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 docs/plans/r1.8-r1.9-summary-queries.md diff --git a/docs/plans/hcal-roadmap.md b/docs/plans/hcal-roadmap.md index 3a5c1eb..d777c22 100644 --- a/docs/plans/hcal-roadmap.md +++ b/docs/plans/hcal-roadmap.md @@ -105,8 +105,8 @@ read/browse/status surface is Windows-free and the gRPC stack is the default pat | ~~R1.5~~ | Extended-property **read** | `Retrieval.GetTagExtendedPropertiesFromName` | ⛔ **string-handle wall** (GetTepByNm takes `string handle`). TEP result buffer. | string-handle RE | | ~~R1.6~~ | Localized-property **read** | `Retrieval.GetTagLocalizedPropertiesFromName` | ⛔ **string-handle wall** (same family). | string-handle RE | | R1.7 | Event **filters** | filter bytes in `Retrieval.StartEventQuery` | filter predicate encoding (name/op/value) — **`uint`-handle**, reachable | R0.5 | -| R1.8 | Analog-summary query | `Retrieval.StartQuery` (summary mode) | summary row layout — **`uint`-handle**, reachable | — | -| R1.9 | State-summary query | `Retrieval.StartQuery` (state mode) | state-summary row layout — **`uint`-handle**, reachable | — | +| R1.8 | Analog-summary query | `Retrieval.StartQuery` (summary mode) | summary row layout — **`uint`-handle, reachable. Scoped + decode targets located** (`CAnalogSummaryValue.UnpackFromValueBuffer`, fields Min/Max/First/Last/ValueCount/Integral/…). Plan: [`r1.8-r1.9-summary-queries.md`](r1.8-r1.9-summary-queries.md) | — | +| R1.9 | State-summary query | `Retrieval.StartQuery` (state mode) | state-summary row layout — **`uint`-handle, reachable. Scoped** (`CStateSummaryStruct`: MinContained/MaxContained/TotalContained/PartialStart/PartialEnd/StateEntryCount). Plan: [`r1.8-r1.9-summary-queries.md`](r1.8-r1.9-summary-queries.md) | — | ### 1c. Bounded config writes (S–M each) | ID | Capability | gRPC op | Payload | Notes | diff --git a/docs/plans/r1.8-r1.9-summary-queries.md b/docs/plans/r1.8-r1.9-summary-queries.md new file mode 100644 index 0000000..0c9b704 --- /dev/null +++ b/docs/plans/r1.8-r1.9-summary-queries.md @@ -0,0 +1,66 @@ +# R1.8 / R1.9 — Analog-summary & State-summary queries (implementation plan) + +**Status (2026-06-20): scoped + de-risked. Reachable on 2020 WCF. Decode targets located in +the native dll. Ready to implement; not yet started in `src/` (no guessed code shipped).** + +Unlike the M1 *read* items gated by the [string-handle wall](../reverse-engineering/wcf-string-handle-wall.md), +summary queries ride the **proven `uint`-handle `StartQuery2`** path — the same call the working +raw/aggregate reads use. So they are genuinely reachable here; the only work is (a) the right +request parameters and (b) decoding the summary row buffer. + +## What's already in place + +`HistorianDataQueryRequest` + `SerializeFullHistoryRequest` +(`Wcf/HistorianDataQueryProtocol.cs`) already serialize every field a summary query needs: +`QueryType` (INSQL_QUERYTYPE), `SummaryType` (HISTORIAN_SUMMARYTYPE), `AggregationType`, +`ColumnSelectorFlags`, `Resolution`. Normal reads send `SummaryType=0` and +`ColumnSelectorFlags=0x0000_8182_0007_82FF`. A summary query is the **same request with summary +values in those three fields**, then a different row parser on the result buffer. + +## Decode targets recovered from `current/aahClientManaged.dll` + +Found via `methods … Summary` + `dnlib-method`: + +| Native artifact | Token | Use | +|---|---|---| +| `CAnalogSummaryValue.UnpackFromValueBuffer` | `0x06000394` | **the analog-summary row decoder** — a chain of buffer-reader calls (not literal offsets), so decode empirically against a captured buffer | +| `CAnalogSummaryValue.PackToVtq` | `0x06000395` | inverse (for a future write path) | +| `CAnalogSummaryValue` setters | `0x0600038A‑92` | wire field set: **StartDateTime, Min, Max, First, Last, ValueCount, TimeGood, Integral, IntegralOfSquares** | +| `CAnalogSummaryStruct` setters | `0x06000369‑77` | fuller field set: adds **MinDateTime, MaxDateTime, FirstDateTime, LastDateTime, FirstNullDateTime, LastNullFlag, LinearIntegral** | +| `CStateSummaryStruct` setters | `0x0600039B‑A0` | **state-summary fields: MinContained, MaxContained, TotalContained, PartialStart, PartialEnd, StateEntryCount** | +| `QueryColumnSelector.SelectAnalogSummaryColumns` | `0x0600004B` | builds `ColumnSelectorFlags` for analog summary via `CColumnNameMap.GetColumnFlag(name)` per column | +| `QueryColumnSelector.SelectStateSummaryColumns` | `0x0600004C` | same, state summary | +| `QueryColumnSelector.SelectNonSummaryColumns` | `0x0600004D` | the default (matches the `0x…82FF` flags reads already send) | +| `CTypeMetadata.IsAnalogSummary` / `IsStateSummary` | `0x060001A4/A5` | server-side type gating | +| `INSQL_QUERYTYPE` / `HISTORIAN_SUMMARYTYPE` | enums `0200013F` / `02000191` | the `QueryType` / `SummaryType` values to send | + +## Open questions (nail these next, in order) + +1. **Request params.** Recover the exact `QueryType` + `SummaryType` (+ whether `ColumnSelectorFlags` + must change) for analog vs state summary. Source: decompile `INSQL_QUERYTYPE`/`HISTORIAN_SUMMARYTYPE` + enum members and `Select{Analog,State}SummaryColumns` (the `GetColumnFlag` column-name `ldstr` + operands → the flag set). The standard QueryType map (Cyclic=0 … EndBound=14) is already verified; + summary is expected to be a `SummaryType`≠0 with an existing base `QueryType`, **not** a new mode + ordinal — confirm. +2. **Row layout.** Capture a real `GetNextQueryResultBuffer2` buffer for an analog summary of a + data-bearing tag (`SysTimeSec`) over a multi-hour window with an interval, then decode against the + `CAnalogSummaryValue` field set. Likely each row = StartDateTime FILETIME + the 8 typed fields. + +## Implementation steps (per the project's two-tests discipline) + +1. Add request params to `HistorianDataQueryRequest` builders (a `BuildAnalogSummaryRequest` / + `BuildStateSummaryRequest` alongside `BuildAggregateQueryRequest`). +2. **Live-probe** `SysTimeSec` via a gated diagnostic; sanitize the response into + `fixtures/protocol/analog-summary/` using the CW-1 pipeline. +3. Write `TryParseGetNextQueryResultBufferAnalogSummaryRows` (+ state variant) against the fixture. +4. Public API: `ReadAnalogSummaryAsync` / `ReadStateSummaryAsync` returning new models + `HistorianAnalogSummary` (Min/Max/First/Last/Avg=Integral÷TimeGood/ValueCount/…) and + `HistorianStateSummary` (per-state contained/partial/entry-count). Reuse `RunQuery` plumbing. +5. Golden-byte test on the parser + gated live test on `localhost` (assert non-empty, fields sane). + +## Why stop here this session + +`UnpackFromValueBuffer` is reader-call-based (no literal offset table), so a correct parser needs a +**captured real buffer** to decode against — that's the next concrete action, not a guess. Per +project rule ("never guess wire bytes; leave throwing until evidence supports it") no summary code +was added to `src/` yet. Everything above is the evidence needed to implement directly.