6888b8c55a
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>
169 lines
7.8 KiB
Markdown
169 lines
7.8 KiB
Markdown
# 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.
|