M3 R3.2: AddHistoricalValuesAsync supports Double + Int (Int2/Int4/UInt4)
Extended the historical-write serializer from Float-only to all five analog types EnsureTagAsync supports. Captured each type's "ON" buffer live from the native client (sandbox tag per type, written + captured + deleted): - The 4-byte value descriptor (C0 10 01 00) is CONSTANT across types — it does not encode the type. - The value is u32(0) + native-width value, width by the tag's declared type: Float->float32, Double->double64, Int2->int16, Int4->int32, UInt4->uint32. HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer now takes the HistorianDataType and encodes accordingly (unsupported types throw ProtocolEvidenceMissingException). The orchestrator resolves the type from the tag-info NativeDataTypeDescriptor via MapDataType. Harness capture-write gained --data-type. Golden-tested against all five live captures + the gated write/read-back test validated each type end-to-end through the pure-managed SDK; 281 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -77,6 +77,7 @@ internal sealed class HistorianGrpcHistoricalWriteOrchestrator
|
||||
}
|
||||
|
||||
Guid tagGuid = parsed[0].TypeId;
|
||||
HistorianDataType dataType = HistorianWcfTagClient.MapDataType(parsed[0].NativeDataTypeDescriptor);
|
||||
|
||||
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
|
||||
foreach (HistorianHistoricalValue value in values)
|
||||
@@ -84,6 +85,7 @@ internal sealed class HistorianGrpcHistoricalWriteOrchestrator
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
byte[] buffer = HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer(
|
||||
tagGuid,
|
||||
dataType,
|
||||
value.TimestampUtc,
|
||||
value.Value,
|
||||
DateTime.UtcNow,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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 a live
|
||||
/// capture of the native 2023 R2 gRPC client driving
|
||||
/// <c>HistorianAccess.AddNonStreamedValue → SendValues</c> against a sandbox tag (see
|
||||
/// <c>docs/plans/revision-write-path.md</c> §"R3.1 CAPTURED"). The native non-streamed write does
|
||||
/// NOT use the TransactionService <c>AddNonStreamValues</c> path the static decompile suggested — it
|
||||
/// <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"/>).
|
||||
///
|
||||
@@ -18,46 +18,61 @@ namespace AVEVA.Historian.Client.Wcf;
|
||||
/// 0x02 UInt16 sampleCount = 1
|
||||
/// 0x04 UInt32 10 + valueBlob.Length // total buffer length
|
||||
/// 0x08 UInt16 valueBlob.Length
|
||||
/// 0x0A valueBlob (46 bytes for one analog double):
|
||||
/// 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 analog value descriptor (constant for the Float path: C0 10 01 00)
|
||||
/// +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 (zero for a 4-byte Float)
|
||||
/// +0x2A Float32 value // the Float value sits in the high half of the 8-byte slot
|
||||
/// +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)
|
||||
/// </code>
|
||||
///
|
||||
/// Captured against a Float tag: the value occupies an 8-byte slot as <c>u32(0) + float32(value)</c>
|
||||
/// (the 4-byte IEEE-754 float in the high dword, NOT an 8-byte double). Only the Float encoding is
|
||||
/// captured; Double/Int/string tag types use a different descriptor + value width and are rejected
|
||||
/// until captured.
|
||||
/// Captured for the five analog types <c>EnsureTagAsync</c> supports (Float/Double/Int2/Int4/UInt4).
|
||||
/// 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 for the analog double value path. The other observed quality/descriptor
|
||||
// bytes in the AddS2 "OS" buffer are event-specific; here this 4-byte block sits between the
|
||||
// quality and the received-time FILETIME and was constant across captured analog writes.
|
||||
private static readonly byte[] AnalogDoubleDescriptor = [0xC0, 0x10, 0x01, 0x00];
|
||||
// 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];
|
||||
|
||||
private const int ValueBlobLength = 16 + 8 + 2 + 4 + 8 + 8; // 46
|
||||
// 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), and
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static byte[] SerializeAddStreamValuesBuffer(
|
||||
Guid tagGuid,
|
||||
HistorianDataType dataType,
|
||||
DateTime sampleTimeUtc,
|
||||
double value,
|
||||
DateTime receivedTimeUtc,
|
||||
ushort quality = OpcQualityGood)
|
||||
{
|
||||
byte[] valueBlob = SerializeValueBlob(tagGuid, sampleTimeUtc, value, receivedTimeUtc, quality);
|
||||
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);
|
||||
@@ -68,26 +83,45 @@ internal static class HistorianHistoricalWriteProtocol
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static byte[] SerializeValueBlob(
|
||||
Guid tagGuid,
|
||||
DateTime sampleTimeUtc,
|
||||
double value,
|
||||
DateTime receivedTimeUtc,
|
||||
ushort quality)
|
||||
private static byte[] EncodeNativeValue(HistorianDataType dataType, double value)
|
||||
{
|
||||
byte[] blob = new byte[ValueBlobLength];
|
||||
Span<byte> span = blob;
|
||||
|
||||
tagGuid.ToByteArray().CopyTo(span[..16]);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(span.Slice(16, 8), ToFileTime(sampleTimeUtc));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(24, 2), quality);
|
||||
AnalogDoubleDescriptor.CopyTo(span.Slice(26, 4));
|
||||
BinaryPrimitives.WriteInt64LittleEndian(span.Slice(30, 8), ToFileTime(receivedTimeUtc));
|
||||
// The value sits in an 8-byte slot as u32(0) + float32(value): the captured Float tag stored
|
||||
// a 4-byte IEEE-754 float in the high dword, not an 8-byte double.
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(38, 4), 0);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(span.Slice(42, 4), (float)value);
|
||||
return blob;
|
||||
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;
|
||||
}
|
||||
default:
|
||||
throw new ProtocolEvidenceMissingException(
|
||||
$"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " +
|
||||
"Captured types: Float, Double, Int2, Int4, UInt4.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long ToFileTime(DateTime value)
|
||||
|
||||
Reference in New Issue
Block a user