using System.Buffers.Binary; using AVEVA.Historian.Client.Models; namespace AVEVA.Historian.Client.Wcf; /// /// Serializer for the M3 historical (non-streamed original / backfill) value write — the /// HistoryService.AddStreamValues values buffer. Decoded byte-for-byte from live /// captures of the native 2023 R2 gRPC client driving /// HistorianAccess.AddNonStreamedValue → SendValues against sandbox tags of each analog type /// (see docs/plans/revision-write-path.md §"R3.1 CAPTURED"). The native non-streamed write /// rides HistoryService.AddStreamValues with an "ON" storage-sample buffer, the analog sibling /// of the AddS2 "OS" event buffer (). /// /// /// values buffer (single analog sample): /// 0x00 UInt16 0x4E4F // "ON" signature /// 0x02 UInt16 sampleCount = 1 /// 0x04 UInt32 10 + valueBlob.Length // total buffer length /// 0x08 UInt16 valueBlob.Length /// 0x0A valueBlob: /// +0x00 GUID tag GUID (the per-tag GUID = the value ParseTagInfoRecord reads as "typeId") /// +0x10 Int64 sample FILETIME (UTC — the VTQ value timestamp) /// +0x18 UInt16 OpcQuality = 192 (good) /// +0x1A 4 bytes value descriptor — CONSTANT C0 10 01 00 across all analog types (the server /// interprets the value bytes by the tag's declared type) /// +0x1E Int64 received/version FILETIME (UTC) /// +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) /// 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) 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 { public const ushort BufferSignature = 0x4E4F; // "ON" public const ushort OpcQualityGood = 192; // Captured constant: the 4-byte value descriptor was identical across all analog types // (Float/Double/Int2/Int4/UInt4). The value type is conveyed by the tag's registration, not here. private static readonly byte[] ValueDescriptor = [0xC0, 0x10, 0x01, 0x00]; // valueBlob fixed prefix: GUID(16) + sampleFT(8) + quality(2) + descriptor(4) + receivedFT(8) + // value high dword(4) = 42, then the native-width value bytes. private const int ValueBlobPrefixLength = 16 + 8 + 2 + 4 + 8 + 4; // 42 /// /// Builds the AddStreamValues values buffer for a single analog historical sample. /// 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 /// (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. /// public static byte[] SerializeAddStreamValuesBuffer( Guid tagGuid, HistorianDataType dataType, DateTime sampleTimeUtc, double value, DateTime receivedTimeUtc, ushort quality = OpcQualityGood) { byte[] valueBytes = EncodeNativeValue(dataType, value); byte[] valueBlob = new byte[ValueBlobPrefixLength + valueBytes.Length]; Span span = valueBlob; tagGuid.ToByteArray().CopyTo(span[..16]); BinaryPrimitives.WriteInt64LittleEndian(span.Slice(16, 8), ToFileTime(sampleTimeUtc)); BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(24, 2), quality); ValueDescriptor.CopyTo(span.Slice(26, 4)); BinaryPrimitives.WriteInt64LittleEndian(span.Slice(30, 8), ToFileTime(receivedTimeUtc)); BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(38, 4), 0); // value high dword (constant 0) valueBytes.CopyTo(span.Slice(42, valueBytes.Length)); byte[] buffer = new byte[10 + valueBlob.Length]; BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), BufferSignature); BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 1); // sampleCount BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), checked((uint)(10 + valueBlob.Length))); BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(8, 2), checked((ushort)valueBlob.Length)); valueBlob.CopyTo(buffer.AsSpan(10)); return buffer; } private static byte[] EncodeNativeValue(HistorianDataType dataType, double value) { switch (dataType) { case HistorianDataType.Float: { byte[] b = new byte[4]; BinaryPrimitives.WriteSingleLittleEndian(b, (float)value); return b; } case HistorianDataType.Double: { byte[] b = new byte[8]; BinaryPrimitives.WriteDoubleLittleEndian(b, value); return b; } case HistorianDataType.Int2: { byte[] b = new byte[2]; BinaryPrimitives.WriteInt16LittleEndian(b, checked((short)value)); return b; } case HistorianDataType.Int4: { byte[] b = new byte[4]; BinaryPrimitives.WriteInt32LittleEndian(b, checked((int)value)); return b; } case HistorianDataType.UInt4: { byte[] b = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value)); return b; } 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, Int8, UInt8."); } } private static long ToFileTime(DateTime value) { DateTime utc = value.Kind == DateTimeKind.Utc ? value : value.ToUniversalTime(); return utc.ToFileTimeUtc(); } }