Ship tag extended-property reads over the 2020 WCF aa/Retr/GetTepByNm op: HistorianClient.GetTagExtendedPropertiesAsync(tag) -> name/value pairs. String-handle op reached with the Open2 storage-session GUID formatted uppercase (same format that unlocked GETRP/GETHI/ExeC). Routed via the name-based native path (GetTagExtendedPropertiesByName, server-fetch flag), not the index-based TagQuery path. Evidence-backed findings from the capture: - GetTepByNm (and GetTgByNm) succeed with the uppercase handle -- further validates the resolved string-handle wall. - QTB (StartTagQuery) does NOT punch through: captured uppercase, it still fails server-side (CMdServer::StartActiveTagnamesQuery over the aahMetadataServer pipe) -- a metadata-server blocker, not handle format. - R1.6 (localized properties) has NO distinct op (only error-message/UI-text localization in the managed client); collapses into R1.5. Closed, not throwing. Wire format (golden-pinned, synthetic bytes -- no dev tag names committed): - request tagNames = uint count + per-name(uint charCount + UTF-16) - response = uint tagCount + per-tag(marker + compact-ASCII name + uint propCount + per-prop(marker + compact-ASCII name + 0x43 VT_BSTR value) + trailer); sequence-paged. Adds: HistorianTagExtendedProperty model, HistorianTagExtendedPropertyProtocol (codec), HistorianWcfTagExtendedPropertyClient (orchestration), dialect + public API; golden WcfTagExtendedPropertyProtocolTests (4) + gated live test (HISTORIAN_TEP_TAG). Tooling: Capture-TagExtendedProperties.ps1, decode-tag-properties-capture.py, harness tag-extended-properties scenario. Docs: wcf-tag-extended-properties.md; roadmap R1.5 DONE / R1.6 collapsed; wall doc + memory updated with the QTB-server-side nuance. 228 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
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#.
The supported surface (per CLAUDE.md):
| Operation | Status |
|---|---|
ProbeAsync |
live-verified |
ReadRawAsync |
live-verified |
ReadAggregateAsync |
live-verified across all 16 retrieval modes |
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 |
EnsureTagAsync |
live-verified for analog Float/Double/Int2/Int4/UInt4; ApplyScaling=true persists distinct MinRaw/MaxRaw |
DeleteTagAsync |
live-verified |
Out of scope: writing samples (AddS2 is architecturally blocked — the server's
runtime cache only ingests from configured IOServer / Application Server
pipelines), store-forward write, configuration changes, discrete/string tag
creation (native AddTag rejects them).
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 toHistorian2020ProtocolDialect; delegates probe / tag metadata / browse / status helpers to the WCF layer.Wcf/— managed WCF/MDAS layer. CustomMdasMessageEncoderwraps SOAP 1.2 + WS-Addressing 1.0 in AVEVA'sapplication/x-mdasframing. Three binding flavors viaHistorianWcfBindingFactory: plain MDAS over Net.NamedPipe (local), MDAS + Windows transport (remote/Hist-Integrated), MDAS + certificate (remote/HistCert). Service contracts inWcf/Contracts/mirror the server-side WCF surface (versioned per native interface —IHistoryServiceContract,IRetrievalServiceContract2..4,IStatusServiceContract2, etc.).Protocol/— binary frame layer.Historian2020ProtocolDialectis the version-anchored bridge between the public façade and the WCF + frame layers. Methods without protocol evidence throwProtocolEvidenceMissingExceptionrather 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 undertools/may reference native binaries. - Never commit credentials, hostnames, user names, customer tag names, or raw packet
captures.
*.ndjson,current/,aveva-install-*/, andartifacts/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
165 unit + live integration tests pass (dotnet test --logger "console;verbosity=minimal").
Full SDK surface — reads, browse, metadata, status, plus the two write ops
(EnsureTagAsync / DeleteTagAsync) — 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; deeper coverage over the cert transport plus the
explicit-credentials path await additional verification.