HCAL → modern-.NET reimplementation — capability matrix
Feasibility map for a clean managed-.NET client that replaces the AVEVA Historian
SDK (aahClientManaged / HCAL). Grounded in: the real ArchestrA.HistorianAccess
public surface (aahClientManaged.xml), the recovered 2023 R2 gRPC contract, the
existing histsdk reimplementation, and the event/storage analysis in
histevents.md.
Legend
Status (histsdk today) — ✅ implemented + live-verified · 🟗 partial · ⬜ not yet
Feasibility tier
| Tier |
Meaning |
Effort |
| DONE |
already working in histsdk |
0 |
| TRIVIAL |
gRPC op known, payload already decoded or empty |
XS (hrs) |
| CAPTURE |
one instrument-and-capture of a native payload, then serialize + golden-byte test |
S (days) |
| BOUNDED |
gRPC op exists; decode one proprietary bytes payload |
S–M |
| HARD |
whole subsystem to reimplement |
L (weeks) |
| GATED |
blocked server-side — client effort doesn't unblock it |
n/a |
Effort = incremental work on top of histsdk's existing infrastructure (auth chain,
transport, frame/byte primitives, test harness). All non-DONE items assume the
gRPC transport as the foundation (clean protobuf envelope; only the inner byte
blob needs RE).
1. Connection & session
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Probe / version |
TestConnection, GetV |
*Service.GetInterfaceVersion |
✅ |
DONE |
|
| Open connection (Process) |
OpenConnection |
History.OpenConnection (+ ExchangeKey auth) |
✅ |
DONE |
full auth chain works |
| Open connection (Event) |
OpenConnection (Event type) |
History.OpenConnection event mode |
🟗 |
TRIVIAL |
read path already opens it; flag = ConnectionType.Event |
| Close connection |
CloseConnection |
History.CloseConnection |
✅ |
DONE |
|
| Connection status |
GetConnectionStatus |
Status.GetHistorianConsoleStatus |
✅ |
DONE |
|
| Open/close storage connection |
OpenStorageConnection, CloseStorageConnection |
Storage.OpenStorageConnection2 |
⬜ |
BOUNDED |
needed for any data-write path; storage-engine session |
2. Reads — process data
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Raw / full history |
CreateHistoryQuery → Start/MoveNext/End |
Retrieval.StartQuery→GetNextQueryResultBuffer→EndQuery |
✅ |
DONE |
row buffer parsed |
| Aggregate (interp/avg/min/max/…) |
CreateHistoryQuery (RetrievalMode) |
same |
✅ |
DONE |
all 15 RetrievalModes mapped |
| At-time / value-at |
(interp window) |
same |
✅ |
DONE |
|
| Analog summary |
CreateAnalogSummaryQuery |
Retrieval.StartQuery (summary mode) |
🟗 |
BOUNDED |
mode variant of existing query |
| State summary |
CreateStateSummaryQuery |
Retrieval.StartQuery (state mode) |
⬜ |
BOUNDED |
extra row layout to decode |
| Block read |
ReadBlocks |
Storage.LoadBlocks |
⬜ |
BOUNDED |
low-level; rarely needed |
3. Reads — events
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Event query |
CreateEventQuery → Start/MoveNext/End |
Retrieval.StartEventQuery→GetNextEventQueryResultBuffer→EndEventQuery |
✅ |
DONE |
rows + typed property bag parsed; CM_EVENT registration done |
| Event filters |
EventQuery.AddEventFilter / AddEventFilterCondition |
filter bytes in StartEventQuery request |
⬜ |
BOUNDED |
encode filter predicate into request buffer |
4. Browse & metadata
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Tag name browse |
CreateTagQuery → GetTagNames |
Retrieval.StartTagQuery/QueryTag (or LikeTagnames) |
✅ |
DONE |
wildcard works |
| Tag metadata |
GetTagInfoByName, TagQuery.GetTagInfo |
Retrieval.GetTagInfosFromName |
✅ |
DONE |
|
| Extended properties (read) |
GetTagExtendedPropertiesByName |
Retrieval.GetTagExtendedPropertiesFromName |
⬜ |
BOUNDED |
TEP buffer decode |
| Localized properties (read) |
GetTagLocalizedPropertiesByName |
Retrieval.GetTagLocalizedPropertiesFromName |
⬜ |
BOUNDED |
|
| SQL passthrough |
ExecuteSqlCommand |
Retrieval.ExecuteSqlCommand |
⬜ |
TRIVIAL |
thin string-in / status-out |
5. Tag configuration (writes)
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Create analog tag |
AddTag |
History.EnsureTags (EnsT2) |
✅ |
DONE |
Float/Double/Int2/Int4/UInt2/UInt4 + scaling |
| Create string/discrete tag |
AddTag |
History.EnsureTags |
⬜ |
GATED/BOUNDED |
native AddTag rejects these types server-side; needs different metadata path |
| Delete tag(s) |
DeleteTags |
History.DeleteTags |
✅ |
DONE |
|
| Rename tag(s) |
RenameTags |
(History op) |
⬜ |
BOUNDED |
AllowRenameTags param already probed |
| Add/Delete extended properties |
AddTagExtendedProperties, DeleteTagExtendedPropertiesByName |
History.AddTagExtendedProperties / DeleteTagExtendedProperties |
⬜ |
BOUNDED |
gRPC op + TEP serialize |
| Add/Delete localized properties |
AddTagLocalizedProperties, DeleteTagLocalizedPropertiesByName |
History.AddTagLocalizedProperties / DeleteTagLocalizedProperties |
⬜ |
BOUNDED |
|
6. Data writes — values
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Stream process values |
AddStreamedValue(HistorianDataValue) |
Storage.AddStreamValues |
⬜ |
GATED |
runtime cache only ingests from IOServer/AppServer pipelines (129 Tag not found in cache). Not a client bug |
| Stream events |
AddStreamedValue(HistorianEvent) |
Storage.AddStreamValues (event VTQ) |
⬜ |
CAPTURE |
full path mapped; need CCommonArchestraEventValue::PackToVtq blob bytes. See histevents.md |
| Non-streamed / historical insert |
AddNonStreamedValue, SendNonStreamedValues |
Transaction.AddNonStreamValues(Begin/End) |
⬜ |
BOUNDED |
explicit original-data insert via Transaction svc; verify ingest permission on target |
| Versioned streamed value |
AddVersionedStreamedValue |
Storage.AddStreamValues2 |
⬜ |
CAPTURE |
revision flag on the VTQ |
7. Revisions / edits (modify stored data)
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Insert/update/delete revision values |
AddRevisionValue(s), AddRevisionValuesBegin/End |
(storage-engine / transaction path) |
⬜ |
HARD |
prior RE: revision-write needs the non-WCF storage-engine pipe (STransactPipeClient2), not the WCF/gRPC surface |
| Event update/delete (revise) |
HistorianEvent.Update/.Delete |
UpdateEventStatus (+ revised VTQ) |
⬜ |
CAPTURE |
RevisionVersion + Update/Delete flags in the event VTQ |
8. Status & system info
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| System parameter |
GetSystemParameter |
Status.GetSystemParameter |
✅ |
DONE |
|
| Runtime parameter |
GetRuntimeParameter |
Status.GetRuntimeParameter |
⬜ |
TRIVIAL |
same shape as GetSystemParameter |
| Historian info |
GetHistorianInfo |
Status.GetHistorianInfo |
🟗 |
BOUNDED |
GETHI buffer; partially decoded (incl. EventStorageMode @ offset 514) |
| Server timezone |
GetSystemTimeZoneInfo |
Status.GetSystemTimeZoneName |
⬜ |
TRIVIAL |
|
| Historization status |
GetHistorizationStatus |
Status op |
⬜ |
BOUNDED |
|
| Store-and-forward status |
GetStoreForwardStatus |
(push events / pull GETHI) |
🟗 |
HARD |
currently synthesized; real read needs duplex push or a decoded pull endpoint — see store-forward plan |
9. Store-and-forward (offline buffering)
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| SF buffering + replay |
(implicit on write conns) |
Storage/Transaction *Snapshot + Forward*Snapshot |
⬜ |
HARD |
full subsystem: local cache format, snapshot framing, recovery log, forward-on-reconnect. Pragmatic alt: a simpler local queue, not bit-faithful SF |
| Event SF |
(event conn) |
Forward**Event**SnapshotBegin/…/End |
⬜ |
HARD |
dedicated event-snapshot SF stream |
| SF parameters |
Get/Set SFP |
Storage.GetSFParameter/SetSFParameter |
⬜ |
BOUNDED |
|
10. Redundancy / multi-historian
| Capability |
HCAL API |
2023 R2 gRPC op |
histsdk |
Tier |
Notes |
| Tiered/redundant access, failover |
MultiHistorianAccess.* (OpenConnectionToAll, AddSecondaries, partner watchdog, ReSyncTags) |
N×single-historian sessions + client logic |
⬜ |
HARD |
mostly client-side orchestration over §1–§6; build last |
| Replication config |
(server aahReplication) |
— |
⬜ |
GATED |
server-side concern |
Roll-up & recommended cut line
Phase 0 — already DONE (✅): probe · open/close · raw+aggregate+at-time reads ·
event reads · tag browse · tag metadata · system parameter · connection status ·
create/delete analog tag. This is a usable modern client today.
Phase 1 — TRIVIAL/BOUNDED, high value (S–M each): ExecuteSqlCommand ·
runtime parameter · server timezone · extended/localized property read · event
filters · summary/state-summary queries · rename tags · ext/localized property
writes · GetHistorianInfo. Each is "gRPC op exists, decode one buffer, golden-byte
test." Knocks out most of the remaining read/config surface.
Phase 2 — CAPTURE (one native capture each, S): event sending (the headline
gap — fully mapped, one PackToVtq capture away) · versioned/non-streamed value
writes. Now feasible locally since the Historian is installed.
Defer / simplify (HARD): store-and-forward (do a pragmatic local queue instead of
bit-faithful SF) · revision/edit writes (separate storage-engine pipe) · multi-
historian redundancy (client orchestration, build last).
Won't unblock from the client (GATED): streaming process-sample writes
(AddS2) — server cache only ingests from IOServer/AppServer pipelines; confirm your
ingestion model rather than chasing this. Non-analog tag creation likely needs a
distinct server path.
Cross-cutting realities (apply to every non-DONE row)
- Inner payloads stay proprietary even under gRPC — the
bytes fields carry
native VTQ / CTagMetadata / event-value formats. These are version-sensitive;
pin to the server version probed at connect and fail closed on mismatch.
- Validation needs a live Historian — now available locally, which is what makes
the CAPTURE-tier items practical.
- Support tradeoff — you take on maintenance across Historian versions in exchange
for shedding the stock SDK's bugs (mixed-mode marshaling, WCF quirks, global state)
for the surface you cover.
Bottom line
A modern-.NET HCAL replacement is feasible and ~60–70% done for a typical
read+browse+config+event-read workload. The remaining high-value surface is mostly
BOUNDED/CAPTURE (incremental, well-understood), with only store-and-forward,
revision-edit, and redundancy being genuine HARD subsystems — and one true wall
(GATED process-sample writes) that no client can remove.