100b44a365
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
151 lines
7.1 KiB
C#
151 lines
7.1 KiB
C#
using System.Buffers.Binary;
|
|
using AVEVA.Historian.Client.Models;
|
|
|
|
namespace AVEVA.Historian.Client.Wcf;
|
|
|
|
/// <remarks>
|
|
/// Serializer for the M3 historical (non-streamed original / backfill) value write — the
|
|
/// <c>HistoryService.AddStreamValues</c> <c>values</c> buffer. Decoded byte-for-byte from live
|
|
/// captures of the native 2023 R2 gRPC client driving
|
|
/// <c>HistorianAccess.AddNonStreamedValue → SendValues</c> against sandbox tags of each analog type
|
|
/// (see <c>docs/plans/revision-write-path.md</c> §"R3.1 CAPTURED"). The native non-streamed write
|
|
/// rides <c>HistoryService.AddStreamValues</c> with an "ON" storage-sample buffer, the analog sibling
|
|
/// of the AddS2 "OS" event buffer (<see cref="HistorianEventWriteProtocol"/>).
|
|
///
|
|
/// <code>
|
|
/// 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)
|
|
/// </code>
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Builds the <c>AddStreamValues</c> <c>values</c> buffer for a single analog historical sample.
|
|
/// <paramref name="tagGuid"/> is the per-tag GUID (from the gRPC tag-info read),
|
|
/// <paramref name="dataType"/> is the tag's declared analog type (selects the value width), and
|
|
/// <paramref name="receivedTimeUtc"/> is the storage/received timestamp the orchestrator stamps.
|
|
/// Throws <see cref="ProtocolEvidenceMissingException"/> 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 <see langword="double"/>;
|
|
/// full 64-bit range is a separate follow-on.
|
|
/// </summary>
|
|
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<byte> 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();
|
|
}
|
|
}
|