From 95b924cdbe9348c26e170c31fd19bbdbd06e87ca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 15:27:04 -0400 Subject: [PATCH] =?UTF-8?q?fix(write):=20re-gate=20UInt1=20=E2=80=94=20his?= =?UTF-8?q?torian=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]