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:
@@ -189,9 +189,10 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
|
||||
HistorianClient client = new(BuildOptions(host));
|
||||
|
||||
// A backfill sample at a fixed historical second, with a distinctive value.
|
||||
// A backfill sample at a fixed historical second, with a distinctive whole-number value so
|
||||
// it round-trips for any analog tag type (Float/Double/Int2/Int4/UInt4).
|
||||
DateTime stamp = new DateTime(DateTime.UtcNow.Year, 1, 2, 3, 4, 5, DateTimeKind.Utc);
|
||||
const double expected = 222.5;
|
||||
const double expected = 7777;
|
||||
bool wrote = await client.AddHistoricalValuesAsync(
|
||||
sandboxTag!,
|
||||
[new HistorianHistoricalValue(stamp, expected)],
|
||||
|
||||
@@ -1,41 +1,60 @@
|
||||
using System.Buffers.Binary;
|
||||
using AVEVA.Historian.Client.Models;
|
||||
using AVEVA.Historian.Client.Wcf;
|
||||
|
||||
namespace AVEVA.Historian.Client.Tests;
|
||||
|
||||
public sealed class WcfHistoricalWriteProtocolTests
|
||||
{
|
||||
// The exact 56-byte HistoryService.AddStreamValues "values" buffer captured live from the native
|
||||
// 2023 R2 client writing a historical Float sample (124.5) to a sandbox tag — see
|
||||
// docs/plans/revision-write-path.md §"R3.1 CAPTURED".
|
||||
private const string CapturedBufferHex =
|
||||
"4f4e0100380000002e00" + // "ON" + count(1) + totalLen(56) + payloadLen(46)
|
||||
"d51d3107e9f8664793b155d3d1aef544" + // tag GUID 07311dd5-f8e9-4766-93b1-55d3d1aef544
|
||||
"70df38b1d101dd01" + // sample FILETIME
|
||||
"c000" + // OpcQuality = 192
|
||||
"c0100100" + // analog double descriptor
|
||||
"e64bcb74e201dd01" + // received/version FILETIME
|
||||
"000000000000f942"; // double 124.5
|
||||
|
||||
[Fact]
|
||||
public void SerializeAddStreamValuesBuffer_MatchesCapturedNativeBuffer()
|
||||
// Exact HistoryService.AddStreamValues "values" buffers captured live from the native 2023 R2
|
||||
// client writing one historical sample to a sandbox tag of each analog type — see
|
||||
// docs/plans/revision-write-path.md §"R3.1 CAPTURED". The value descriptor (C0 10 01 00) is
|
||||
// constant across types; only the value width differs.
|
||||
[Theory]
|
||||
[InlineData(HistorianDataType.Float, 124.5,
|
||||
"4f4e0100380000002e00d51d3107e9f8664793b155d3d1aef54470df38b1d101dd01c000c0100100e64bcb74e201dd01000000000000f942")]
|
||||
[InlineData(HistorianDataType.Double, 124.5,
|
||||
"4f4e01003c0000003200" + "7f970bb0b5a7344bb7bf6899ff06d027" + "c07d5f23d701dd01" + "c000" + "c0100100" + "08eff1e6e701dd01" + "000000000000000000205f40")]
|
||||
[InlineData(HistorianDataType.Int2, 1234d,
|
||||
"4f4e0100360000002c00" + "35bb0ca5865a7f4498f06bf4e4d9ab23" + "f012f356d701dd01" + "c000" + "c0100100" + "9546841ae801dd01" + "00000000d204")]
|
||||
[InlineData(HistorianDataType.Int4, 12345d,
|
||||
"4f4e0100380000002e00" + "ca9735f7f841b244b56f9c14ccfeac32" + "b09bc72fd701dd01" + "c000" + "c0100100" + "104b59f3e701dd01" + "0000000039300000")]
|
||||
[InlineData(HistorianDataType.UInt4, 305419896d,
|
||||
"4f4e0100380000002e00" + "e7ae22d8e4cc65439ebd8bcb09402974" + "602d6663d701dd01" + "c000" + "c0100100" + "498af726e801dd01" + "0000000078563412")]
|
||||
public void SerializeAddStreamValuesBuffer_MatchesCapturedNativeBuffer(HistorianDataType dataType, double value, string capturedHex)
|
||||
{
|
||||
var tagGuid = new Guid("07311dd5-f8e9-4766-93b1-55d3d1aef544");
|
||||
DateTime sampleTime = DateTime.FromFileTimeUtc(0x01dd01d1b138df70);
|
||||
DateTime receivedTime = DateTime.FromFileTimeUtc(0x01dd01e274cb4be6);
|
||||
byte[] captured = Convert.FromHexString(capturedHex);
|
||||
|
||||
// The GUID + both FILETIMEs sit at fixed offsets (the value width varies after them).
|
||||
var tagGuid = new Guid(captured.AsSpan(10, 16).ToArray());
|
||||
long sampleFt = BinaryPrimitives.ReadInt64LittleEndian(captured.AsSpan(26, 8));
|
||||
long receivedFt = BinaryPrimitives.ReadInt64LittleEndian(captured.AsSpan(40, 8));
|
||||
|
||||
byte[] actual = HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer(
|
||||
tagGuid, sampleTime, value: 124.5, receivedTime, quality: 192);
|
||||
tagGuid,
|
||||
dataType,
|
||||
DateTime.FromFileTimeUtc(sampleFt),
|
||||
value,
|
||||
DateTime.FromFileTimeUtc(receivedFt),
|
||||
quality: 192);
|
||||
|
||||
Assert.Equal(Convert.FromHexString(CapturedBufferHex), actual);
|
||||
Assert.Equal(captured, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeAddStreamValuesBuffer_UnsupportedType_Throws()
|
||||
{
|
||||
Assert.Throws<ProtocolEvidenceMissingException>(() =>
|
||||
HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer(
|
||||
Guid.NewGuid(), HistorianDataType.SingleByteString, DateTime.UtcNow, 1.0, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeAddStreamValuesBuffer_HeaderDeclaresLengths()
|
||||
{
|
||||
byte[] buffer = HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer(
|
||||
Guid.NewGuid(), DateTime.UtcNow, value: 1.0, DateTime.UtcNow);
|
||||
Guid.NewGuid(), HistorianDataType.Float, DateTime.UtcNow, value: 1.0, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(56, buffer.Length);
|
||||
Assert.Equal(0x4E4F, BitConverter.ToUInt16(buffer, 0)); // "ON"
|
||||
Assert.Equal(1, BitConverter.ToUInt16(buffer, 2)); // sampleCount
|
||||
Assert.Equal((uint)buffer.Length, BitConverter.ToUInt32(buffer, 4));
|
||||
|
||||
Reference in New Issue
Block a user