Files
histsdk/README.md
T
dohertj2 6888b8c55a Wire SDK for remote-TCP end to end; live-verify RemoteTcpIntegrated
Executes docs/plans/tcp-connection-validation.md. Full read-only SDK
surface now works against a remote AVEVA Historian over Net.TCP with
Windows transport authentication. 124/124 tests pass; the +10 new live
integration tests in RemoteTcpIntegrationTests.cs are gated by
HISTORIAN_REMOTE_TCP_HOST + HISTORIAN_REMOTE_TCP_TAG.

Two SDK bugs found while executing the plan:

1. Historian2020ProtocolDialect.ReadRawAsync / ReadAggregateAsync /
   ReadAtTimeAsync / ReadEventsAsync had explicit
   `if (_options.Transport != HistorianTransport.LocalPipe) return Missing<T>`
   guards. These were a guardrail from before the orchestrators handled
   TCP; the orchestrators have always used CreateBindingPair(options)
   which dispatches on transport correctly. Gates removed.

2. HistorianWcfStatusClient and HistorianWcfEventOrchestrator hardcoded
   HistorianWcfBindingFactory.CreatePipeEndpointAddress for the auxiliary
   services (Stat, Trx, Retr). Worked for LocalPipe; for TCP it produced
   an EndpointAddress with scheme net.pipe attached to a TCP binding
   (channel factory rejected the URI). Worse, when only the endpoint was
   transport-aware, the binding still requested a Windows-transport-
   security upgrade that the Stat endpoint over TCP doesn't support
   (auxiliaries don't repeat the auth — the Hist session is already
   authenticated). Added two helpers:
   - HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, name)
     -> net.pipe for LocalPipe, net.tcp for remote
   - HistorianWcfBindingFactory.CreateAuxiliaryBinding(options)
     -> NamedPipe for LocalPipe, plain MdasNetTcpBinding for remote
   Both call sites updated.

Live verification against the remote (probed previously in prior
sessions; reachability re-confirmed today):
- ProbeAsync over RemoteTcpIntegrated and RemoteTcpCertificate
- ReadRawAsync (8 samples returned for SysTimeSec)
- ReadAggregateAsync (TimeWeightedAverage, 1-min cycle, 10-min window)
- ReadAtTimeAsync (3 timestamps)
- BrowseTagNamesAsync (finds the test tag)
- GetTagMetadataAsync (full metadata populated)
- ReadEventsAsync (chain runs without throwing)
- GetConnectionStatusAsync (ConnectedToServer=true)
- GetSystemParameterAsync (HistorianVersion="20,0,000,000")

The default 'NT SERVICE\aahClientAccessPoint' SPN turned out to work
for the remote too — discovery workstream A (SPN-finding) was not
needed in practice.

README and the TCP plan doc updated to reflect the executed status.

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

169 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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
```csharp
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
```powershell
dotnet build .\Histsdk.slnx --no-restore
dotnet test .\Histsdk.slnx --no-build --logger "console;verbosity=minimal"
```
Run a single test class:
```powershell
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:
```powershell
$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`](AGENTS.md) | Standing project constraints and safety rules. |
| [`CLAUDE.md`](CLAUDE.md) | Build commands, code architecture, the active protocol blocker (now resolved) and SDK surface. |
| [`docs/reverse-engineering/handoff.md`](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`](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.