Joseph Doherty 7a3cd9b76e Resolve write-path silent fails + expand EnsureTagAsync, RetrievalMode coverage
DelT and EnsT2 had two distinct silent-fail blockers; both now resolved live
end-to-end. Read path's RetrievalMode mapping was missing 11 of 15 enum values
(plus a latent Cyclic→4 bug). Investigation tooling kept as env-gated helpers.

DelT silent fail: Open2 was using NativeIntegratedReadOnlyConnectionMode (0x402);
server returned err 132 OperationNotEnabled silently. Added
NativeIntegratedWriteEnabledConnectionMode (0x401) per
HistorianAccessUtil.SetConnectionMode bit map (Process=1 | IntegratedSecurity=0x400).
Write orchestrator now opens with write-enabled mode.

EnsT2 silent fail: byte-by-byte comparison via inspector revealed two bugs in
SerializeAnalogCTagMetadata. The original "146-byte byte-for-byte match" was
misaligned — it omitted the leading 0x4E marker byte and treated WCF's `01 01 01`
EndElement closing markers as if they were part of the InBuff payload. Real native
InBuff is 144 bytes with 0x4E lead and 2-byte `FE 00` trailer. Golden test bytes
corrected.

EnsureTagAsync expansion: probed every analog data type via
instrument-wcf-writemessage; byte 11 of CTagMetadata is the data-type
discriminator (Float=0x01, Double=0x21, UInt2=0x09, UInt4=0x11, Int2=0x29,
Int4=0x31). String/Int1/Int8/UInt8 fail at native AddTag — out of scope for
this op. Range encoding decoded: defaults emit compact `1A 03`; non-default
emit `1F 00` + 4 doubles in order MinEU/MaxEU/MinRaw/MaxRaw. MinRaw/MaxRaw
sent on the wire but server mirrors them to MinEU/MaxEU when ApplyScaling=false
(verified against native — server quirk, not SDK bug).

RetrievalMode mapping: probed all 15 enum values; QueryType is just the native
enum ordinal. Replaced the broken switch with `(uint)mode`. Existing SDK
mapped Cyclic→4 (BestFit's value); Cyclic is actually 0.

CLAUDE.md updated: stale "Active Protocol Blocker" rewritten as resolved-status
block; SDK surface now reflects the read-blocker resolution and the new write
ops; "Remaining gaps" punch list refreshed.

Tools added (both env-gated, no runtime overhead unless flipped on):
- HistorianWcfMessageCaptureBehavior — captures all WCF body bytes when
  AVEVA_HISTORIAN_SDK_WIRE_CAPTURE is set; used for byte-level diff vs native.
- HistorianWcfHistAddressingBehavior — explicitly sets wsa:To header on the
  Hist channel for parity with native bytes (kept though not load-bearing).
- WriteDiag in TagWriteOrchestrator — env-gated EnsT2/DelT response logging
  (AVEVA_HISTORIAN_DELT_DIAG).

NativeTraceHarness CLI: added --write-min-eu/--write-max-eu/--write-min-raw/
--write-max-raw for capturing non-default-range EnsT2 payloads.

Tests: 130 → 161 passing (+31). Includes 16-mode RetrievalMode mapping table,
4 per-data-type EnsT2 golden tests, NonDefaultRanges golden test, 6 live
round-trip integration tests covering Float/Double/Int2/Int4/UInt4/FloatRanges,
3 live tests for previously-unmapped RetrievalMode values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:52:13 -04:00

histsdk

Pure managed .NET 10 client for AVEVA Historian's binary WCF protocol. The production SDK has no dependency on aahClientManaged.dll, aahClient.dll, or any other AVEVA native runtime — the wire protocol is reverse-engineered and re-implemented in C#.

Read-only by design. The required surface (per CLAUDE.md):

Operation Status
ProbeAsync live-verified
ReadRawAsync live-verified
ReadAggregateAsync live-verified (TimeWeightedAverage; other modes need fixtures)
ReadAtTimeAsync live-verified
ReadEventsAsync live-verified (typed event + 31-property property bag)
BrowseTagNamesAsync live-verified
GetTagMetadataAsync live-verified for 17 distinct native data-type codes
GetConnectionStatusAsync synthesized from authenticated probe (matches native semantic)
GetStoreForwardStatusAsync synthesized defaults (no SF sidecar to probe)
GetSystemParameterAsync live-verified via Stat/GetSystemParameter

Out of scope: write-back, store-forward write, configuration changes.

Quick start

using AVEVA.Historian.Client;
using AVEVA.Historian.Client.Models;

await using HistorianClient client = new(new HistorianClientOptions
{
    Host = "localhost",
    IntegratedSecurity = true,
    Transport = HistorianTransport.LocalPipe,
});

DateTime endUtc   = DateTime.UtcNow;
DateTime startUtc = endUtc - TimeSpan.FromMinutes(10);

await foreach (HistorianSample sample in client.ReadRawAsync(
    "SysTimeSec", startUtc, endUtc, maxValues: 100))
{
    Console.WriteLine($"{sample.TimestampUtc:o}  {sample.NumericValue}  Q={sample.Quality}");
}

For a remote Historian over Net.TCP set Transport = HistorianTransport.RemoteTcpIntegrated and Host to the server hostname. Both RemoteTcpIntegrated (Windows transport auth) and RemoteTcpCertificate (server-cert TLS) are now live-verified for ProbeAsync; RemoteTcpIntegrated is additionally live-verified for the full read / browse / metadata / event / status surface.

Build & test

dotnet build  .\Histsdk.slnx --no-restore
dotnet test   .\Histsdk.slnx --no-build --logger "console;verbosity=minimal"

Run a single test class:

dotnet test .\Histsdk.slnx --no-build --filter "FullyQualifiedName~WcfDataQueryProtocolTests"

Live integration tests (tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs) are gated and skip cleanly without these env vars:

$env:HISTORIAN_HOST       = 'localhost'
$env:HISTORIAN_TEST_TAG   = 'SysTimeSec'      # any tag the server has data for
# Optional for non-integrated auth:
$env:HISTORIAN_USER, $env:HISTORIAN_PASSWORD
$env:HISTORIAN_TAG_FILTER = 'Sys*'             # or any LIKE-pattern

Repository layout

src/AVEVA.Historian.Client/    Production SDK — pure managed .NET 10. No native AVEVA dependency.
tests/                          Unit tests (golden-byte / round-trip) + gated live integration tests.
tools/                          Reverse-engineering tooling (NOT shipped):
  AVEVA.Historian.ReverseEngineering/  .NET 10 CLI: WCF probes, dnlib IL inspection,
                                       IL-rewrite instrumentation (instrument-wcf-{write,read}message,
                                       instrument-openconnection*, instrument-startdataquery, etc.).
  AVEVA.Historian.NativeTraceHarness/  .NET Framework harness that loads aahClientManaged.dll for
                                       byte-for-byte parity testing against the native wrapper.
  AVEVA.Historian.NetFxWcfProbe/       .NET Framework WCF probe for ruling out .NET 10-only differences.
  AVEVA.Historian.ReverseInstrumentation/  Helper assembly injected into IL-rewritten wrapper copies.
  AVEVA.Historian.WcfCaptureServer/    Fake server for endpoint experiments.
scripts/                        PowerShell + Frida runners + Python decoders for capture analysis.
fixtures/protocol/              Sanitized golden-byte fixtures.
docs/reverse-engineering/       Sanitized handoff evidence; commit-safe summaries only.

Native AVEVA binaries (current/, aveva-install-x64/, aveva-install-x86/) are gitignored. Each developer fetches their own copy from the AVEVA installer; we neither modify nor redistribute them.

Documentation

Read in order before non-trivial work:

Doc Purpose
AGENTS.md Standing project constraints and safety rules.
CLAUDE.md Build commands, code architecture, the active protocol blocker (now resolved) and SDK surface.
docs/reverse-engineering/handoff.md Decision record + decoded protocol evidence (binding shapes, descriptor layouts, Open2 v6 response, event-row wire format, property-bag value encoding).
instructions.md Original plan + decision record.

Architecture

Three intentionally decoupled subsystems under src/AVEVA.Historian.Client/:

  • HistorianClient + HistorianClientOptions — public façade. Validates inputs; delegates reads to Historian2020ProtocolDialect; delegates probe / tag metadata / browse / status helpers to the WCF layer.
  • Wcf/ — managed WCF/MDAS layer. Custom MdasMessageEncoder wraps SOAP 1.2 + WS-Addressing 1.0 in AVEVA's application/x-mdas framing. Three binding flavors via HistorianWcfBindingFactory: plain MDAS over Net.NamedPipe (local), MDAS + Windows transport (remote /Hist-Integrated), MDAS + certificate (remote /HistCert). Service contracts in Wcf/Contracts/ mirror the server-side WCF surface (versioned per native interface — IHistoryServiceContract, IRetrievalServiceContract2..4, IStatusServiceContract2, etc.).
  • Protocol/ — binary frame layer. Historian2020ProtocolDialect is the version-anchored bridge between the public façade and the WCF + frame layers. Methods without protocol evidence throw ProtocolEvidenceMissingException rather than guessing wire bytes.
  • Models/ — public DTOs and enums. HistorianSample, HistorianAggregateSample, HistorianEvent, HistorianTagMetadata, HistorianDataType, RetrievalMode, HistorianTransport, etc.

Read flow end-to-end (live-verified against localhost):

Hist.GetV → Hist.ValCl × N → Hist.Open2 → Retr.GetV → Retr.IsOriginalAllowed →
Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2 → typed HistorianSample rows

Event flow end-to-end (live-verified):

Hist.GetV → Hist.ValCl × N → Hist.Open2 → Hist.UpdC3 → 6× Stat.GetSystemParameter →
Hist.RTag2 → Stat.GetSystemParameter(AllowRenameTags) → Trx.GetV → Stat.GetV →
Retr.GetV → Hist.EnsT2(CmEventTagId) → Retr.StartEventQuery →
loop Retr.GetNextEventQueryResultBuffer → typed HistorianEvent rows with
property dictionary → Retr.EndEventQuery → Hist.Close2

Safety

  • Production code under src/ must remain pure managed .NET 10 with no native AVEVA reference. Reverse-engineering harnesses under tools/ may reference native binaries.
  • Never commit credentials, hostnames, user names, customer tag names, or raw packet captures. *.ndjson, current/, aveva-install-*/, and artifacts/ are gitignored precisely because they accumulate identity-bearing runtime data.
  • Methods without protocol evidence throw ProtocolEvidenceMissingException. Do not stub fake behavior — leave them throwing until evidence supports an implementation.

Status

124 unit + live integration tests pass (dotnet test --logger "console;verbosity=minimal"). Full read-only SDK surface verified end-to-end against both a local Historian (LocalPipe) and a remote Historian (RemoteTcpIntegrated over Net.TCP with Windows transport auth). RemoteTcpCertificate ProbeAsync is live-verified; the other ops over the certificate transport plus the explicit-credentials path await live verification.

S
Description
No description provided
Readme 1.1 MiB
Languages
C# 85.2%
PowerShell 7.4%
JavaScript 6.1%
Python 1.3%