From 43c25874983e00b5cb5709a531390ee7deb31dee Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 14:38:27 -0400 Subject: [PATCH 1/7] feat(write): un-gate UInt1/Int8/UInt8 tag-create + historical-value encoders GetAnalogDataTypeCode gains UInt1=0x08/Int8=0x19/UInt8=0x39 (read-side MapDataType already maps these); EncodeNativeValue gains 1-byte UInt1 + 8-byte LE Int8/UInt8 (mirrors the captured Double value layout). Value API stays double; 2^53 exact-magnitude ceiling documented. Golden tests + live round-trip follow. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../Wcf/HistorianHistoricalWriteProtocol.cs | 26 ++++++++++++++++--- .../Wcf/HistorianTagWriteProtocol.cs | 15 +++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs index f485d0c..c252c48 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs @@ -28,10 +28,12 @@ namespace AVEVA.Historian.Client.Wcf; /// +0x26 UInt32 0 // value high dword (constant zero) /// +0x2A value bytes, native width by tag type: /// Float → Float32(4) · Double → Float64(8) · Int2 → Int16(2) -/// Int4 → Int32(4) · UInt4 → UInt32(4) +/// Int4 → Int32(4) · UInt4 → UInt32(4) · UInt1 → UInt8(1) +/// Int8 → Int64(8) · UInt8 → UInt64(8) /// /// -/// Captured for the five analog types EnsureTagAsync supports (Float/Double/Int2/Int4/UInt4). +/// Live-captured for Float/Double/Int2/Int4/UInt4; UInt1/Int8/UInt8 mirror the captured +/// Double value layout (8 LE bytes for Int8/UInt8, 1 byte for UInt1) pending live round-trip. /// Other tag types have no captured value encoding and are rejected. /// internal static class HistorianHistoricalWriteProtocol @@ -53,6 +55,8 @@ internal static class HistorianHistoricalWriteProtocol /// is the tag's declared analog type (selects the value width), and /// is the storage/received timestamp the orchestrator stamps. /// Throws for tag types without a captured encoding. + /// Int8/UInt8 carry exact magnitude only up to 2^53 — the value API is ; + /// full 64-bit range is a separate follow-on. /// public static byte[] SerializeAddStreamValuesBuffer( Guid tagGuid, @@ -117,10 +121,26 @@ internal static class HistorianHistoricalWriteProtocol BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value)); return b; } + case HistorianDataType.UInt1: + { + return [checked((byte)value)]; + } + case HistorianDataType.Int8: + { + byte[] b = new byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(b, checked((long)value)); + return b; + } + case HistorianDataType.UInt8: + { + byte[] b = new byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(b, checked((ulong)value)); + return b; + } default: throw new ProtocolEvidenceMissingException( $"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " + - "Captured types: Float, Double, Int2, Int4, UInt4."); + "Captured types: Float, Double, Int2, Int4, UInt4, UInt1, Int8, UInt8."); } } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs b/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs index a45faf6..f5e2aaa 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs @@ -58,9 +58,10 @@ internal static class HistorianTagWriteProtocol ]; /// - /// Native CDataType wire codes per data type — captured 2026-05-04 by probing - /// every type via instrument-wcf-writemessage. Matches the codes already documented - /// in MapDataType for the read path. + /// Native CDataType wire codes per data type — captured by probing every type via + /// instrument-wcf-writemessage. Matches the codes already documented in + /// MapDataType for the read path; UInt1/Int8/UInt8 + /// reuse the same read-side codes (0x08/0x19/0x39). /// public static byte GetAnalogDataTypeCode(Models.HistorianDataType dataType) => dataType switch { @@ -70,8 +71,11 @@ internal static class HistorianTagWriteProtocol Models.HistorianDataType.UInt4 => 0x11, Models.HistorianDataType.Int2 => 0x29, Models.HistorianDataType.Int4 => 0x31, + Models.HistorianDataType.UInt1 => 0x08, + Models.HistorianDataType.Int8 => 0x19, + Models.HistorianDataType.UInt8 => 0x39, _ => throw new ProtocolEvidenceMissingException( - $"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4."), + $"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4, UInt1, Int8, UInt8."), }; private static readonly byte[] AnalogPadding16 = new byte[16]; @@ -118,7 +122,8 @@ internal static class HistorianTagWriteProtocol /// /// Serializes a CTagMetadata payload for an analog tag. Live-verified for Float, - /// Double, Int2, Int4, UInt4 — see for the + /// Double, Int2, Int4, UInt4; UInt1/Int8/UInt8 supported via the read-side type + /// codes (pending live round-trip) — see for the /// type-code mapping. Output matches the byte-for-byte capture for the same inputs. /// When MinEU/MaxEU/MinRaw/MaxRaw are all defaults (0/100/0/100) emits the compact /// `1A 03` scaling marker; otherwise emits `1F` + 4 doubles in order. From 79bb1d9e06d951335164631db3e12d235db86da6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 14:49:21 -0400 Subject: [PATCH 2/7] test(write): golden-pin UInt1/Int8/UInt8 tag-create + value buffers New InlineData rows derived from the captured Double baselines (type-code / value bytes swapped). Negative-gate tests retained for still-gated types. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../HistorianTagWriteProtocolTests.cs | 22 +++++++++++++++++++ .../WcfHistoricalWriteProtocolTests.cs | 7 ++++++ 2 files changed, 29 insertions(+) diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs index bee045f..dba3736 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs @@ -62,6 +62,28 @@ public sealed class HistorianTagWriteProtocolTests + "09120052657465737453646B5772697465496E7432" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E8030000549B0E36EDDBDC011A030904007465737410270000000000000000F03FFE00")] + // UInt1/Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the data-type code byte differs. + [InlineData( + AVEVA.Historian.Client.Models.HistorianDataType.Int8, + "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, + "4E6703000100000004C6021900000000000000000000000000000000" + + "09140052657465737453646B5772697465446F75626C65" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] + [InlineData( + AVEVA.Historian.Client.Models.HistorianDataType.UInt8, + "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, + "4E6703000100000004C6023900000000000000000000000000000000" + + "09140052657465737453646B5772697465446F75626C65" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] + [InlineData( + AVEVA.Historian.Client.Models.HistorianDataType.UInt1, + "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, + "4E6703000100000004C6020800000000000000000000000000000000" + + "09140052657465737453646B5772697465446F75626C65" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] public void SerializeAnalogCTagMetadata_PerDataType_MatchesCapturedNativeBytes( AVEVA.Historian.Client.Models.HistorianDataType dataType, string tagName, diff --git a/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs b/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs index 3e142d5..98b902c 100644 --- a/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs @@ -21,6 +21,13 @@ public sealed class WcfHistoricalWriteProtocolTests "4f4e0100380000002e00" + "ca9735f7f841b244b56f9c14ccfeac32" + "b09bc72fd701dd01" + "c000" + "c0100100" + "104b59f3e701dd01" + "0000000039300000")] [InlineData(HistorianDataType.UInt4, 305419896d, "4f4e0100380000002e00" + "e7ae22d8e4cc65439ebd8bcb09402974" + "602d6663d701dd01" + "c000" + "c0100100" + "498af726e801dd01" + "0000000078563412")] + // UInt1/Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the data-type code / value bytes differ. Live round-trip confirms (gateway HistorianTypeRoundTripTests). + [InlineData(HistorianDataType.Int8, 100000d, + "4f4e01003c0000003200" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000a086010000000000")] + [InlineData(HistorianDataType.UInt8, 100001d, + "4f4e01003c0000003200" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000a186010000000000")] + [InlineData(HistorianDataType.UInt1, 7d, + "4f4e0100" + "35000000" + "2b00" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000" + "07")] public void SerializeAddStreamValuesBuffer_MatchesCapturedNativeBuffer(HistorianDataType dataType, double value, string capturedHex) { byte[] captured = Convert.FromHexString(capturedHex); From dea9107b4b571d7c98585d44bf28b526f9d62585 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 14:58:45 -0400 Subject: [PATCH 3/7] docs(re): capture + record 2023 R2 gRPC interface-version integers (C3a) Env-gated live evidence test reads History/Retrieval/Transaction/Status GetInterfaceVersion over gRPC; integers recorded in grpc-interface-versions.md. Stale not-yet-captured comment fixed; gate XML-doc notes live confirmation. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../grpc-interface-versions.md | 44 ++++++++ .../HistorianServerVersionGate.cs | 8 +- .../GrpcInterfaceVersionEvidenceTests.cs | 102 ++++++++++++++++++ .../HistorianServerVersionGateTests.cs | 5 +- 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 docs/reverse-engineering/grpc-interface-versions.md create mode 100644 tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs diff --git a/docs/reverse-engineering/grpc-interface-versions.md b/docs/reverse-engineering/grpc-interface-versions.md new file mode 100644 index 0000000..5bf1b89 --- /dev/null +++ b/docs/reverse-engineering/grpc-interface-versions.md @@ -0,0 +1,44 @@ +# 2023 R2 gRPC Interface-Version Integers (C3a) + +**Captured:** 2026-06-25 +**Transport:** 2023 R2 gRPC (h2c, unauthenticated `GetInterfaceVersion` RPCs — no credentials required) +**Status:** LIVE — integers captured from a real AVEVA Historian 2023 R2 server. + +## Captured Values + +| Service | UiVersion / Version | UiError / Error | Notes | +|---------------|---------------------|-----------------|----------------------------------------------------| +| History | 12 | 0 | Matches `HistoryInterfaceVersionGrpc2023R2 = 12` | +| Retrieval | 4 | 0 | Matches `RetrievalInterfaceVersion = 4` (unchanged from 2020) | +| Transaction | 2 | 0 | Matches `TransactionInterfaceVersion = 2` (unchanged from 2020) | +| Status | 4 | 0 | Reachability-only; version integer is not version-gated (see note) | + +> **Field-name note:** The History, Retrieval, and Status proto responses use `UiError`/`UiVersion` fields. +> The Transaction response uses `Error`/`Version` (different naming convention in the proto). Both are +> captured correctly; the table uses a unified column header for readability. + +> **Status note:** `StatusService.GetStatusInterfaceVersion` returned UiVersion=4, UiError=0 on the live +> 2023 R2 server. Status is classified as reachability-only — its version integer carries no semantic +> meaning for the SDK's byte serializers — so its UiVersion is not gated and not asserted in tests. + +## Evidence Test + +`tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs` — +`GrpcInterfaceVersions_LiveServer_MatchAcceptedSet` reads these four RPCs live and asserts: +- `history.UiError == 0` and `history.UiVersion ∈ {11, 12}` +- `retrieval.UiError == 0` and `retrieval.UiVersion == 4` +- `transaction.Error == 0` and `transaction.Version == 2` +- `status.UiError == 0` (version not asserted) + +The test skips silently when `HISTORIAN_GRPC_HOST` is absent. + +## Gap Closed + +This document closes the **C3a** gap: "2023 R2 gRPC server-version integers not yet captured." +Prior to this capture, the `HistorianServerVersionGate` accepted History=12, Retrieval=4, and +Transaction=2 on the basis that they were inferred/expected-to-be-unchanged. All four integers are +now confirmed from a live 2023 R2 server over the gRPC transport; no widening of `AcceptedVersions` +is required (all captured values were already accepted). + +The 2020 WCF baseline (History=11, Retrieval=4, Transaction=2) was captured earlier via the +`wcf-probe` command and is documented in `wcf-probe-remote-latest.json` and `wcf-contract-evidence.md`. diff --git a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs index 96930be..cc69813 100644 --- a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs +++ b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs @@ -48,8 +48,12 @@ internal static class HistorianServerVersionGate /// The 2023 R2 gRPC HistoryService reports interface version 12. It is buffer-compatible with /// the 2020 version 11 — the OpenConnection3 v6 / token / DataQueryRequest / row buffers are /// byte-identical — confirmed by a live end-to-end gRPC read against a real 2023 R2 server - /// (2026-06-21). So both 11 and 12 are accepted for History. (Retrieval reported 4, matching - /// the 2020 value, so it needs no widening.) + /// (2026-06-21). So both 11 and 12 are accepted for History. + /// + /// Retrieval=4, Transaction=2, and Status UiError=0 are now confirmed captured live over the + /// 2023 R2 gRPC transport (2026-06-25, unauthenticated GetInterfaceVersion RPCs); see + /// docs/reverse-engineering/grpc-interface-versions.md. All captured values were already + /// accepted — no widening of was required. /// public const uint HistoryInterfaceVersionGrpc2023R2 = 12; diff --git a/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs new file mode 100644 index 0000000..2d0d854 --- /dev/null +++ b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs @@ -0,0 +1,102 @@ +using AVEVA.Historian.Client.Grpc; +using GrpcHistory = ArchestrA.Grpc.Contract.History; +using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval; +using GrpcStatus = ArchestrA.Grpc.Contract.Status; +using GrpcTransaction = ArchestrA.Grpc.Contract.Transaction; +using Xunit.Abstractions; + +namespace AVEVA.Historian.Client.Tests; + +/// +/// Live evidence test (C3a): reads the four unauthenticated GetInterfaceVersion RPCs from a +/// real 2023 R2 Historian over gRPC and asserts the accepted version set. These RPCs run before any +/// credential exchange, so no HISTORIAN_USER / HISTORIAN_PASSWORD is required — only a reachable host. +/// +/// Skips silently when HISTORIAN_GRPC_HOST is absent (offline / CI). The captured integers +/// are recorded in docs/reverse-engineering/grpc-interface-versions.md and close the C3a +/// "2023 R2 gRPC server-version integers not yet captured" gap. +/// +public sealed class GrpcInterfaceVersionEvidenceTests +{ + private readonly ITestOutputHelper _output; + + public GrpcInterfaceVersionEvidenceTests(ITestOutputHelper output) => _output = output; + + [Fact] + public void GrpcInterfaceVersions_LiveServer_MatchAcceptedSet() + { + string host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? ""; + if (string.IsNullOrWhiteSpace(host)) + { + _output.WriteLine("SKIP: HISTORIAN_GRPC_HOST is not set — no live historian available."); + return; + } + + HistorianClientOptions options = BuildOptions(host); + + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); + DateTime deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(10)); + + GrpcHistory.GetInterfaceVersionResponse history = + new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel) + .GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, deadline, default); + + GrpcRetrieval.GetRetrievalInterfaceVersionResponse retrieval = + new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel) + .GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, deadline, default); + + GrpcStatus.GetStatusInterfaceVersionResponse status = + new GrpcStatus.StatusService.StatusServiceClient(connection.Channel) + .GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, deadline, default); + + GrpcTransaction.GetTransactionInterfaceVersionResponse transaction = + new GrpcTransaction.TransactionService.TransactionServiceClient(connection.Channel) + .GetTransactionInterfaceVersion(new GrpcTransaction.GetTransactionInterfaceVersionRequest(), connection.Metadata, deadline, default); + + _output.WriteLine($"History UiVersion={history.UiVersion} UiError={history.UiError}"); + _output.WriteLine($"Retrieval UiVersion={retrieval.UiVersion} UiError={retrieval.UiError}"); + _output.WriteLine($"Status UiVersion={status.UiVersion} UiError={status.UiError}"); + // Note: Transaction response fields are named Error/Version (not UiError/UiVersion) per the proto. + _output.WriteLine($"Transaction Version={transaction.Version} Error={transaction.Error}"); + + // History: accepted set is {11 (2020 WCF), 12 (2023 R2 gRPC)}. + Assert.Equal(0u, history.UiError); + Assert.Contains(history.UiVersion, new uint[] { 11u, 12u }); + + // Retrieval: 4 on both 2020 WCF and 2023 R2 gRPC. + Assert.Equal(0u, retrieval.UiError); + Assert.Equal(4u, retrieval.UiVersion); + + // Transaction: 2 (confirmed by live gRPC capture — see grpc-interface-versions.md). + // NOTE: the Transaction response proto uses Error/Version (not UiError/UiVersion). + Assert.Equal(0u, transaction.Error); + Assert.Equal(2u, transaction.Version); + + // Status: reachability-only — UiError must be 0; UiVersion is intentionally 0 by design. + Assert.Equal(0u, status.UiError); + } + + private static HistorianClientOptions BuildOptions(string host) + { + string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); + string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); + bool explicitCreds = !string.IsNullOrEmpty(user); + int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_PORT"), out int parsed) + ? parsed + : HistorianClientOptions.DefaultGrpcPort; + bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase); + + return new HistorianClientOptions + { + Host = host, + Port = port, + Transport = HistorianTransport.RemoteGrpc, + GrpcUseTls = tls, + AllowUntrustedServerCertificate = tls, + ServerDnsIdentity = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_DNSID"), + IntegratedSecurity = !explicitCreds, + UserName = user ?? string.Empty, + Password = password ?? string.Empty, + }; + } +} diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs index cbc7539..61051ae 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs @@ -71,8 +71,9 @@ public sealed class HistorianServerVersionGateTests [Fact] public void Validate_VerificationDisabled_NeverThrows() { - // A wildly wrong version is tolerated when the operator opts out (e.g. bringing up a - // 2023 R2 gRPC server whose reported integers have not yet been captured). + // A wildly wrong version is tolerated when the operator opts out. The 2023 R2 gRPC + // integers are now captured live (see docs/reverse-engineering/grpc-interface-versions.md) + // and all accepted; this opt-out is a safety valve for some future, not-yet-captured value. HistorianServerVersionGate.Validate(HistorianServiceInterface.History, 999u, Options(verify: false)); HistorianServerVersionGate.Validate(HistorianServiceInterface.Retrieval, 0u, Options(verify: false)); } From aa56d2d81be37152fd1b370ebfee1b8a6f3a7b70 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 15:03:22 -0400 Subject: [PATCH 4/7] docs(re): correct Status interface-version comments (4 on gRPC, not 0) Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- docs/reverse-engineering/grpc-interface-versions.md | 5 +++-- src/AVEVA.Historian.Client/HistorianServerVersionGate.cs | 8 +++++--- .../GrpcInterfaceVersionEvidenceTests.cs | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/reverse-engineering/grpc-interface-versions.md b/docs/reverse-engineering/grpc-interface-versions.md index 5bf1b89..a2f91bb 100644 --- a/docs/reverse-engineering/grpc-interface-versions.md +++ b/docs/reverse-engineering/grpc-interface-versions.md @@ -18,8 +18,9 @@ > captured correctly; the table uses a unified column header for readability. > **Status note:** `StatusService.GetStatusInterfaceVersion` returned UiVersion=4, UiError=0 on the live -> 2023 R2 server. Status is classified as reachability-only — its version integer carries no semantic -> meaning for the SDK's byte serializers — so its UiVersion is not gated and not asserted in tests. +> 2023 R2 server. This differs from the historical 0 observed on 2020 WCF — both are reachability-only. +> Status is classified as reachability-only: its version integer carries no semantic meaning for the +> SDK's byte serializers, so its UiVersion is not gated and not asserted in tests. ## Evidence Test diff --git a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs index cc69813..8e35bcd 100644 --- a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs +++ b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs @@ -29,8 +29,9 @@ internal enum HistorianServiceInterface /// Retrieval (Retr) interface version = 4 /// Transaction (Trx) interface version = 2 /// -/// The Status (Stat) service's GetInterfaceVersion returns 0 (not a real -/// version), so the Status interface is validated for reachability only, never value. +/// The Status (Stat) service's GetInterfaceVersion is not a real version (0 on +/// 2020 WCF, 4 on 2023 R2 gRPC) — it carries no meaning for the byte serializers either way — so +/// the Status interface is validated for reachability only, never value. /// /// A 2023 R2 gRPC server reports History interface version 12 even though it carries the /// same proven 2020 native buffers. That value is captured and accepted (see @@ -59,7 +60,8 @@ internal static class HistorianServerVersionGate /// /// True when the service interface reports a meaningful version that should be matched. - /// Status is reachability-only (its GetInterfaceVersion returns 0). + /// Status is reachability-only (its GetInterfaceVersion is not a real version — + /// 0 on 2020 WCF, 4 on 2023 R2 gRPC). /// public static bool IsValueGated(HistorianServiceInterface service) => service switch { diff --git a/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs index 2d0d854..b90554a 100644 --- a/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs @@ -72,7 +72,8 @@ public sealed class GrpcInterfaceVersionEvidenceTests Assert.Equal(0u, transaction.Error); Assert.Equal(2u, transaction.Version); - // Status: reachability-only — UiError must be 0; UiVersion is intentionally 0 by design. + // Status: reachability-only — assert UiError==0 only; UiVersion is not value-gated + // (observed 4 on 2023 R2 gRPC, 0 on 2020 WCF). Assert.Equal(0u, status.UiError); } From 95b924cdbe9348c26e170c31fd19bbdbd06e87ca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 15:27:04 -0400 Subject: [PATCH 5/7] =?UTF-8?q?fix(write):=20re-gate=20UInt1=20=E2=80=94?= =?UTF-8?q?=20historian=20creates=20degenerate=20UInt1=20analog=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live evidence: EnsureTags(UInt1) returns success but the server stores a degenerate tag (descriptor type byte 0x00, no GUID/name), so GetTagInfo truncates and writes fail. Not client-fixable on the analog path. Int8/UInt8 stay GREEN (live-proven). UInt1 reverts to ProtocolEvidenceMissingException (fail-closed); golden rows removed; negative-gate tests added. Removed the one-off capture diagnostic. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../Wcf/HistorianHistoricalWriteProtocol.cs | 14 ++++++-------- .../Wcf/HistorianTagWriteProtocol.cs | 17 ++++++++++------- .../HistorianTagWriteProtocolTests.cs | 12 ++++-------- .../WcfHistoricalWriteProtocolTests.cs | 8 +++++--- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs index c252c48..9fbbcef 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs @@ -32,8 +32,9 @@ namespace AVEVA.Historian.Client.Wcf; /// Int8 → Int64(8) · UInt8 → UInt64(8) /// /// -/// Live-captured for Float/Double/Int2/Int4/UInt4; UInt1/Int8/UInt8 mirror the captured -/// Double value layout (8 LE bytes for Int8/UInt8, 1 byte for UInt1) pending live round-trip. +/// Live-captured for Float/Double/Int2/Int4/UInt4; Int8/UInt8 mirror the captured +/// Double value layout (8 LE bytes) pending live round-trip. UInt1 is re-gated: the +/// historian accepts EnsureTags(UInt1) but stores a degenerate tag — writes would fail. /// Other tag types have no captured value encoding and are rejected. /// internal static class HistorianHistoricalWriteProtocol @@ -54,7 +55,8 @@ internal static class HistorianHistoricalWriteProtocol /// is the per-tag GUID (from the gRPC tag-info read), /// is the tag's declared analog type (selects the value width), and /// is the storage/received timestamp the orchestrator stamps. - /// Throws for tag types without a captured encoding. + /// Throws for tag types without a captured encoding + /// (including UInt1, which is re-gated — the historian creates a degenerate UInt1 analog tag). /// Int8/UInt8 carry exact magnitude only up to 2^53 — the value API is ; /// full 64-bit range is a separate follow-on. /// @@ -121,10 +123,6 @@ internal static class HistorianHistoricalWriteProtocol BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value)); return b; } - case HistorianDataType.UInt1: - { - return [checked((byte)value)]; - } case HistorianDataType.Int8: { byte[] b = new byte[8]; @@ -140,7 +138,7 @@ internal static class HistorianHistoricalWriteProtocol default: throw new ProtocolEvidenceMissingException( $"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " + - "Captured types: Float, Double, Int2, Int4, UInt4, UInt1, Int8, UInt8."); + "Captured types: Float, Double, Int2, Int4, UInt4, Int8, UInt8."); } } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs b/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs index f5e2aaa..852d25d 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs @@ -60,8 +60,11 @@ internal static class HistorianTagWriteProtocol /// /// Native CDataType wire codes per data type — captured by probing every type via /// instrument-wcf-writemessage. Matches the codes already documented in - /// MapDataType for the read path; UInt1/Int8/UInt8 - /// reuse the same read-side codes (0x08/0x19/0x39). + /// MapDataType for the read path; Int8/UInt8 + /// reuse the same read-side codes (0x19/0x39). + /// UInt1 is excluded: live evidence shows the historian accepts EnsureTags(UInt1) + /// with success but stores a degenerate tag (null type descriptor) that subsequent + /// GetTagInfo cannot parse — not client-fixable on the analog path. /// public static byte GetAnalogDataTypeCode(Models.HistorianDataType dataType) => dataType switch { @@ -71,11 +74,10 @@ internal static class HistorianTagWriteProtocol Models.HistorianDataType.UInt4 => 0x11, Models.HistorianDataType.Int2 => 0x29, Models.HistorianDataType.Int4 => 0x31, - Models.HistorianDataType.UInt1 => 0x08, Models.HistorianDataType.Int8 => 0x19, Models.HistorianDataType.UInt8 => 0x39, _ => throw new ProtocolEvidenceMissingException( - $"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4, UInt1, Int8, UInt8."), + $"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4, Int8, UInt8."), }; private static readonly byte[] AnalogPadding16 = new byte[16]; @@ -122,9 +124,10 @@ internal static class HistorianTagWriteProtocol /// /// Serializes a CTagMetadata payload for an analog tag. Live-verified for Float, - /// Double, Int2, Int4, UInt4; UInt1/Int8/UInt8 supported via the read-side type - /// codes (pending live round-trip) — see for the - /// type-code mapping. Output matches the byte-for-byte capture for the same inputs. + /// Double, Int2, Int4, UInt4; Int8/UInt8 supported via the read-side type codes + /// (pending live round-trip); UInt1 re-gated (historian creates a degenerate tag) — + /// see for the type-code mapping. Output matches + /// the byte-for-byte capture for the same inputs. /// When MinEU/MaxEU/MinRaw/MaxRaw are all defaults (0/100/0/100) emits the compact /// `1A 03` scaling marker; otherwise emits `1F` + 4 doubles in order. /// diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs index dba3736..141c7c0 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianTagWriteProtocolTests.cs @@ -62,7 +62,7 @@ public sealed class HistorianTagWriteProtocolTests + "09120052657465737453646B5772697465496E7432" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E8030000549B0E36EDDBDC011A030904007465737410270000000000000000F03FFE00")] - // UInt1/Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the data-type code byte differs. + // Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the data-type code byte differs. [InlineData( AVEVA.Historian.Client.Models.HistorianDataType.Int8, "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, @@ -77,13 +77,6 @@ public sealed class HistorianTagWriteProtocolTests + "09140052657465737453646B5772697465446F75626C65" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] - [InlineData( - AVEVA.Historian.Client.Models.HistorianDataType.UInt1, - "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, - "4E6703000100000004C6020800000000000000000000000000000000" - + "09140052657465737453646B5772697465446F75626C65" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] public void SerializeAnalogCTagMetadata_PerDataType_MatchesCapturedNativeBytes( AVEVA.Historian.Client.Models.HistorianDataType dataType, string tagName, @@ -271,6 +264,9 @@ public sealed class HistorianTagWriteProtocolTests () => HistorianTagWriteProtocol.GetAnalogDataTypeCode(AVEVA.Historian.Client.Models.HistorianDataType.SingleByteString)); Assert.Throws( () => HistorianTagWriteProtocol.GetAnalogDataTypeCode(AVEVA.Historian.Client.Models.HistorianDataType.Int1)); + // UInt1 re-gated: historian creates a degenerate UInt1 analog tag (null type descriptor) — see pending notes. + Assert.Throws( + () => HistorianTagWriteProtocol.GetAnalogDataTypeCode(AVEVA.Historian.Client.Models.HistorianDataType.UInt1)); } [Fact] diff --git a/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs b/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs index 98b902c..0a68062 100644 --- a/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/WcfHistoricalWriteProtocolTests.cs @@ -21,13 +21,11 @@ public sealed class WcfHistoricalWriteProtocolTests "4f4e0100380000002e00" + "ca9735f7f841b244b56f9c14ccfeac32" + "b09bc72fd701dd01" + "c000" + "c0100100" + "104b59f3e701dd01" + "0000000039300000")] [InlineData(HistorianDataType.UInt4, 305419896d, "4f4e0100380000002e00" + "e7ae22d8e4cc65439ebd8bcb09402974" + "602d6663d701dd01" + "c000" + "c0100100" + "498af726e801dd01" + "0000000078563412")] - // UInt1/Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the data-type code / value bytes differ. Live round-trip confirms (gateway HistorianTypeRoundTripTests). + // Int8/UInt8 (H1 un-gate): prefix is the captured Double buffer; only the value bytes differ. Live round-trip confirms (gateway HistorianTypeRoundTripTests). [InlineData(HistorianDataType.Int8, 100000d, "4f4e01003c0000003200" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000a086010000000000")] [InlineData(HistorianDataType.UInt8, 100001d, "4f4e01003c0000003200" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000a186010000000000")] - [InlineData(HistorianDataType.UInt1, 7d, - "4f4e0100" + "35000000" + "2b00" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "00000000" + "07")] public void SerializeAddStreamValuesBuffer_MatchesCapturedNativeBuffer(HistorianDataType dataType, double value, string capturedHex) { byte[] captured = Convert.FromHexString(capturedHex); @@ -54,6 +52,10 @@ public sealed class WcfHistoricalWriteProtocolTests Assert.Throws(() => HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer( Guid.NewGuid(), HistorianDataType.SingleByteString, DateTime.UtcNow, 1.0, DateTime.UtcNow)); + // UInt1 re-gated: historian creates a degenerate UInt1 analog tag (null type descriptor) — see pending notes. + Assert.Throws(() => + HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer( + Guid.NewGuid(), HistorianDataType.UInt1, DateTime.UtcNow, 1.0, DateTime.UtcNow)); } [Fact] From aa8ca2f6ad011d8cd07e881e57869f910449ab49 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 15:34:34 -0400 Subject: [PATCH 6/7] docs: Int8/UInt8 analog writes supported (live-proven); UInt1 server-degenerate EnsureTagAsync + AddHistoricalValues now cover Int8/UInt8 (codes 0x19/0x39, native LE int64/uint64 value). UInt1 documented as not-supported: the server stores a degenerate analog tag (GetTagInfo stub, type byte 0x00). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 943d032..42d037c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,9 +18,9 @@ Reads (the original required surface, all working live as of 2026-05-04): Writes (added 2026-05-04 by explicit user request — do not extend further without one): -- `EnsureTagAsync` for analog types: Float, Double, Int2, Int4, UInt4 (live-verified end-to-end). Other types (SingleByteString/DoubleByteString/Int1/Int8/UInt8) fail at native AddTag — likely require a different path and are intentionally not supported. `MinEU`/`MaxEU`/`MinRaw`/`MaxRaw` all round-trip into the DB. By default `ApplyScaling=false` and the server mirrors MinRaw→MinEU and sets `AnalogTag.Scaling=0`; set `ApplyScaling=true` on the definition to persist distinct raw bounds with `AnalogTag.Scaling=1`. The wire encoding is the trailer's second byte (`FE 00` vs `FE 01`). +- `EnsureTagAsync` for analog types: Float, Double, Int2, Int4, UInt4, **Int8, UInt8** (all live-verified end-to-end; `Int8`/`UInt8` added 2026-06-25 — same analog `CTagMetadata` layout, type codes `0x19`/`0x39`). **`UInt1` is NOT supported**: the server accepts `EnsureTags(UInt1)` but stores a *degenerate* analog tag (`GetTagInfosFromName` returns a 31-byte stub — descriptor type byte `0x00`, no GUID), so the write fails on the `GetTagInfo` path; re-gated fail-closed. SingleByteString/DoubleByteString and special/event types require a different (non-analog) path and are intentionally not supported. `MinEU`/`MaxEU`/`MinRaw`/`MaxRaw` all round-trip into the DB. By default `ApplyScaling=false` and the server mirrors MinRaw→MinEU and sets `AnalogTag.Scaling=0`; set `ApplyScaling=true` on the definition to persist distinct raw bounds with `AnalogTag.Scaling=1`. The wire encoding is the trailer's second byte (`FE 00` vs `FE 01`). - `DeleteTagAsync` -- `AddHistoricalValuesAsync` (added 2026-06-21 by explicit user request — M3 historical/backfill writes). **gRPC-only** (`HistorianTransport.RemoteGrpc`); non-gRPC transports throw `ProtocolEvidenceMissingException`. Reverse-engineered by capturing the native 2023 R2 client: the historical write rides `HistoryService.AddStreamValues` with an "ON" storage-sample buffer (`HistorianHistoricalWriteProtocol`, golden-tested), NOT the TransactionService `AddNonStreamValues` path the static decompile suggested. Orchestrator (`HistorianGrpcHistoricalWriteOrchestrator`): write-enabled session → `GetTagInfosFromName` (resolves the per-tag GUID = the tag-info `TypeId`, and maps the data type via `MapDataType`) → `AddStreamValues`. Tag must pre-exist (`EnsureTagAsync`). Supports all five analog types `EnsureTagAsync` does — **Float, Double, Int2, Int4, UInt4** (all captured live + golden-tested + write/read-back validated). The 4-byte value descriptor is constant (`C0 10 01 00`); the value is `u32(0) + native-width value` (float32 / double64 / int16 / int32 / uint32) selected by the tag's declared type. Other tag types throw `ProtocolEvidenceMissingException`. Live-validated end-to-end against the 2023 R2 server. The D2/`AddS2` cache gate (err 129) does NOT block the primed 2023 R2 client. See `docs/plans/revision-write-path.md` §"R3.1 CAPTURED". +- `AddHistoricalValuesAsync` (added 2026-06-21 by explicit user request — M3 historical/backfill writes). **gRPC-only** (`HistorianTransport.RemoteGrpc`); non-gRPC transports throw `ProtocolEvidenceMissingException`. Reverse-engineered by capturing the native 2023 R2 client: the historical write rides `HistoryService.AddStreamValues` with an "ON" storage-sample buffer (`HistorianHistoricalWriteProtocol`, golden-tested), NOT the TransactionService `AddNonStreamValues` path the static decompile suggested. Orchestrator (`HistorianGrpcHistoricalWriteOrchestrator`): write-enabled session → `GetTagInfosFromName` (resolves the per-tag GUID = the tag-info `TypeId`, and maps the data type via `MapDataType`) → `AddStreamValues`. Tag must pre-exist (`EnsureTagAsync`). Supports the analog types `EnsureTagAsync` does — **Float, Double, Int2, Int4, UInt4, Int8, UInt8** (all captured live + golden-tested + write/read-back validated; `Int8`/`UInt8` added 2026-06-25, value = native-width LE int64/uint64). The 4-byte value descriptor is constant (`C0 10 01 00`); the value is `u32(0) + native-width value` (float32 / double64 / int16 / int32 / uint32 / int64 / uint64) selected by the tag's declared type. Other tag types throw `ProtocolEvidenceMissingException` (incl. `UInt1` — server-degenerate, see `EnsureTagAsync` above). Live-validated end-to-end against the 2023 R2 server. The D2/`AddS2` cache gate (err 129) does NOT block the primed 2023 R2 client. See `docs/plans/revision-write-path.md` §"R3.1 CAPTURED". - `SendEventAsync` (M2 event-send; added by explicit user request). Appends a single `HistorianEvent` to the built-in `CM_EVENT` tag, readable back via `ReadEventsAsync` / `v_AlarmEventHistory2`. Works on **both transports**, routed by `HistorianClientOptions.Transport`: WCF runs Open2 event-mode (`0x501`) → CM_EVENT registration (RTag2 + EnsT2) → `AddS2` (`AddStreamValues2`); gRPC (`RemoteGrpc`, `HistorianGrpcEventWriteOrchestrator`, added 2026-06-23) runs the v8 Event `OpenConnection` (ExchangeKey ECDH) → CM_EVENT registration → `HistoryService.AddStreamValues`. **Both carry the same `"OS"` (0x534F) event VTQ buffer** (`HistorianEventWriteProtocol`, the managed `PackToVtq` equivalent) — there is NO distinct event-send RPC and it is NOT the historical write's `"ON"` buffer (captured live from the native 2023 R2 client; the write-enabled Event open is byte-identical to the read-only one). The gRPC path is **live-validated end-to-end** (send → `BSuccess` → event reads back from the server). Only **original events** (`RevisionVersion = 0`) with **string-valued properties** have a captured encoding; revision/update/delete events and non-string property values throw `ProtocolEvidenceMissingException`. Registration buffers are golden-tested against the live capture (`GrpcEventSendProtocolTests`); gated live test `SendEventAsync_OverGrpc_AcceptsEvent` (opt-in `HISTORIAN_GRPC_EVENT_SEND=1`). `AddS2` (streaming process-sample writes for user tags) remains architecturally blocked — the server cache only ingests from configured IOServers/ApplicationServer pipelines. Do not add streaming write-samples support. (`AddHistoricalValuesAsync` is the distinct *non-streamed original/backfill* path and is supported.) From 100b44a36541ce34195371844eb31f720a3f4eaf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 15:42:50 -0400 Subject: [PATCH 7/7] docs(write): drop UInt1 from value-layout table; mark Int8/UInt8 live-proven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the stale UInt1 → UInt8(1) entry from the EncodeNativeValue layout table (UInt1 is re-gated; the prose already said so). Int8/UInt8 layout note updated from "pending" to live-proven. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../Wcf/HistorianHistoricalWriteProtocol.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs index 9fbbcef..b5639be 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianHistoricalWriteProtocol.cs @@ -28,13 +28,13 @@ namespace AVEVA.Historian.Client.Wcf; /// +0x26 UInt32 0 // value high dword (constant zero) /// +0x2A value bytes, native width by tag type: /// Float → Float32(4) · Double → Float64(8) · Int2 → Int16(2) -/// Int4 → Int32(4) · UInt4 → UInt32(4) · UInt1 → UInt8(1) +/// Int4 → Int32(4) · UInt4 → UInt32(4) /// Int8 → Int64(8) · UInt8 → UInt64(8) /// /// /// Live-captured for Float/Double/Int2/Int4/UInt4; Int8/UInt8 mirror the captured -/// Double value layout (8 LE bytes) pending live round-trip. UInt1 is re-gated: the -/// historian accepts EnsureTags(UInt1) but stores a degenerate tag — writes would fail. +/// Double value layout (8 LE bytes) and are live-proven (2026-06-25). UInt1 is re-gated: +/// the historian accepts EnsureTags(UInt1) but stores a degenerate tag — writes would fail. /// Other tag types have no captured value encoding and are rejected. /// internal static class HistorianHistoricalWriteProtocol