# ZB.MOM.WW.SPHistorianClient Pure-managed .NET 10 client for **AVEVA System Platform Historian** (Wonderware), for the ZB.MOM.WW SCADA family. This is a **library, not a service** — it is linked directly into the consuming application and runs in-process alongside it. The wire protocol is reverse-engineered and re-implemented in C#. There is **no native AVEVA runtime dependency** — `aahClientManaged.dll` / `aahClient.dll` are not referenced or loaded. The library runs on any OS for offline/unit testing; live WCF transports require Windows. **Status: ported and rebranded into this repo; builds and 191 tests pass on macOS. Version 0.1.0. The `RemoteGrpc` (2023 R2) read path is live-verified (2026-06-19). NOT yet packed/published to any NuGet feed beyond the local `artifacts/` nupkg. NOT yet adopted by any consumer.** --- ## Supported operation surface All operations are exposed via the public façade `HistorianClient`. | Operation | Status | |---|---| | `ProbeAsync` | live-verified | | `ReadRawAsync` | live-verified | | `ReadAggregateAsync` | live-verified across the `RetrievalMode` enum (15 modes) | | `ReadAtTimeAsync` | live-verified | | `ReadBlocksAsync` | block history read | | `ReadEventsAsync` | live-verified (typed event + property bag) | | `BrowseTagNamesAsync` | live-verified | | `GetTagMetadataAsync` | live-verified across many native data-type codes | | `GetConnectionStatusAsync` | synthesized from authenticated probe | | `GetStoreForwardStatusAsync` | synthesized defaults | | `GetSystemParameterAsync` | live-verified | | `EnsureTagAsync` | live-verified for analog `Float`; `Double`/`Int2`/`Int4`/`UInt4` supported (optional `ApplyScaling` persists distinct MinRaw/MaxRaw) | | `DeleteTagAsync` | live-verified (known issue: server-side cascade may not always complete; use SMC as fallback to clean up sandbox tags) | ### Out of scope - **Writing sample values** (`AddS2`) is architecturally blocked — the server runtime cache only ingests from configured IOServer / Application Server pipelines, not from a standalone AddTag client flow. - Store-forward write, historian configuration changes, discrete/string tag creation (native AddTag rejects them). --- ## Transport matrix Configured via `HistorianClientOptions.Transport` (`HistorianTransport` enum). | Transport | Protocol | Platform | Verification | |---|---|---|---| | `LocalPipe` | WCF/MDAS over Net.NamedPipe (local) | Windows-only | live-verified (read / browse / metadata / event / status) | | `RemoteTcpIntegrated` | WCF/MDAS over Net.TCP + Windows transport auth | Windows-only | live-verified (full read / browse / metadata / event / status surface) | | `RemoteTcpCertificate` | WCF/MDAS over Net.TCP + server-cert TLS | Windows-only | `ProbeAsync` live-verified; deeper coverage pending | | `RemoteGrpc` | gRPC (2023 R2), Grpc.Net.Client/.Web | cross-platform | **live-verified 2026-06-19** against a 2023 R2 server — TLS + `StorageService.ValidateClientCredential` NTLM handshake + raw read returning correct values | --- ## DI registration ```csharp services.AddZbSpHistorianClient(new HistorianClientOptions { Host = "localhost", IntegratedSecurity = true, Transport = HistorianTransport.RemoteTcpIntegrated, }); ``` `AddZbSpHistorianClient` registers the options instance (singleton) + `HistorianClient` (transient). Because `HistorianClientOptions` uses `required`/`init`-only properties, the consumer passes a fully-built instance. In a real app, bind it from configuration: ```csharp services.AddZbSpHistorianClient( config.GetSection("Historian").Get()!); ``` The package depends only on `Microsoft.Extensions.DependencyInjection.Abstractions` — no ASP.NET Core or framework reference required. --- ## Architecture Three decoupled subsystems under `src/ZB.MOM.WW.SPHistorianClient/`: | Subsystem | Path | Responsibility | |---|---|---| | Public façade | `HistorianClient.cs`, `HistorianClientOptions.cs` | Entry point; delegates to the transport layer | | WCF/MDAS layer | `Wcf/` | Managed WCF transport; custom `MdasMessageEncoder`, binding factory, versioned service contracts | | Binary frame layer | `Protocol/` | `Historian2020ProtocolDialect`; methods without protocol evidence throw `ProtocolEvidenceMissingException` | | Public models | `Models/` | Public DTOs and enums (`HistorianSample`, `HistorianTagMetadata`, `RetrievalMode`, …) | | gRPC transport | `Grpc/` | 2023 R2 gRPC transport; recovered `.proto` compiled by Grpc.Tools at build; wire contracts keep AVEVA's `ArchestrA.Grpc.Contract.*` namespaces | --- ## Build, test, and pack commands ```bash # From ZB.MOM.WW.SPHistorianClient/ # Build dotnet build ZB.MOM.WW.SPHistorianClient.slnx dotnet build ZB.MOM.WW.SPHistorianClient.slnx -c Release # Test (offline unit/golden-byte tests run on any OS; # Windows-only WCF tests no-op off Windows; # live integration tests skip when env vars are unset — see below) dotnet test ZB.MOM.WW.SPHistorianClient.slnx # Run a single test class dotnet test ZB.MOM.WW.SPHistorianClient.slnx --filter "FullyQualifiedName~WcfDataQueryProtocolTests" # Pack (one .nupkg lands in artifacts/) dotnet pack ZB.MOM.WW.SPHistorianClient.slnx -c Release -o ./artifacts ``` ### Test posture All tests run offline by default; live integration tests are gated by environment variables and skip cleanly when unset. | Test type | Count | |---|---| | Offline unit / golden-byte tests | the bulk of the 191 total | | WCF live integration (gated) | skipped off Windows or when `HISTORIAN_HOST` is unset | | gRPC live integration (gated) | skipped when `HISTORIAN_GRPC_HOST` is unset | | **Total** | **191** | `GeneratePackageOnBuild` is off — pack explicitly with the command above. ### Live integration test environment variables **WCF transports:** | Variable | Required | Notes | |---|---|---| | `HISTORIAN_HOST` | yes (gates WCF tests) | Historian server hostname or IP | | `HISTORIAN_PORT` | optional | Override the WCF TCP port (default 32568) | | `HISTORIAN_TEST_TAG` | yes | A historized tag that exists on the server (use a system tag such as `SysTimeSec` for safe testing) | | `HISTORIAN_USER` | optional | Omit to use Windows integrated security | | `HISTORIAN_PASSWORD` | optional | Only used when `HISTORIAN_USER` is set | | `HISTORIAN_TAG_FILTER` | optional | Browse filter pattern passed to `BrowseTagNamesAsync` | **gRPC transport:** | Variable | Required | Notes | |---|---|---| | `HISTORIAN_GRPC_HOST` | yes (gates gRPC tests) | 2023 R2 gRPC endpoint host | | `HISTORIAN_GRPC_PORT` | optional | Default 32565 | | `HISTORIAN_GRPC_TLS` | optional | Set `true` to enable TLS | | `HISTORIAN_GRPC_DNSID` | optional | Override DNS identity for certificate validation | --- ## Status and provenance **Version 0.1.0.** Ported from a reverse-engineering migration bundle and rebranded into this repo. Builds and all 191 tests pass on macOS. NOT yet packed/published to the Gitea NuGet feed. NOT yet adopted by any consumer (OtOpcUa, MxAccessGateway, ScadaBridge). Production code is pure-managed .NET 10 with no native AVEVA reference. Reverse-engineering tooling and proprietary decompilations from the source bundle were intentionally excluded from this repo. **Safety rules for this library (hard — never violate):** - Never commit real server hostnames, IP addresses, or credentials. - Never commit customer tag names or live capture data (`.gitignore` blocks `*.ndjson` and similar raw-capture extensions). - Use only generic placeholders (`localhost`, ``) and built-in AVEVA system tags (e.g., `SysTimeSec`) in all documentation and test defaults.