feat(write): un-gate UInt1/Int8/UInt8 tag-create + historical-value encoders
GetAnalogDataTypeCode gains UInt1=0x08/Int8=0x19/UInt8=0x39 (read-side MapDataType already maps these); EncodeNativeValue gains 1-byte UInt1 + 8-byte LE Int8/UInt8 (mirrors the captured Double value layout). Value API stays double; 2^53 exact-magnitude ceiling documented. Golden tests + live round-trip follow. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -28,10 +28,12 @@ namespace AVEVA.Historian.Client.Wcf;
|
|||||||
/// +0x26 UInt32 0 // value high dword (constant zero)
|
/// +0x26 UInt32 0 // value high dword (constant zero)
|
||||||
/// +0x2A value bytes, native width by tag type:
|
/// +0x2A value bytes, native width by tag type:
|
||||||
/// Float → Float32(4) · Double → Float64(8) · Int2 → Int16(2)
|
/// Float → Float32(4) · Double → Float64(8) · Int2 → Int16(2)
|
||||||
/// Int4 → Int32(4) · UInt4 → UInt32(4)
|
/// Int4 → Int32(4) · UInt4 → UInt32(4) · UInt1 → UInt8(1)
|
||||||
|
/// Int8 → Int64(8) · UInt8 → UInt64(8)
|
||||||
/// </code>
|
/// </code>
|
||||||
///
|
///
|
||||||
/// Captured for the five analog types <c>EnsureTagAsync</c> supports (Float/Double/Int2/Int4/UInt4).
|
/// 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.
|
||||||
/// Other tag types have no captured value encoding and are rejected.
|
/// Other tag types have no captured value encoding and are rejected.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static class HistorianHistoricalWriteProtocol
|
internal static class HistorianHistoricalWriteProtocol
|
||||||
@@ -53,6 +55,8 @@ internal static class HistorianHistoricalWriteProtocol
|
|||||||
/// <paramref name="dataType"/> is the tag's declared analog type (selects the value width), and
|
/// <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.
|
/// <paramref name="receivedTimeUtc"/> is the storage/received timestamp the orchestrator stamps.
|
||||||
/// Throws <see cref="ProtocolEvidenceMissingException"/> for tag types without a captured encoding.
|
/// Throws <see cref="ProtocolEvidenceMissingException"/> for tag types without a captured encoding.
|
||||||
|
/// 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>
|
/// </summary>
|
||||||
public static byte[] SerializeAddStreamValuesBuffer(
|
public static byte[] SerializeAddStreamValuesBuffer(
|
||||||
Guid tagGuid,
|
Guid tagGuid,
|
||||||
@@ -117,10 +121,26 @@ internal static class HistorianHistoricalWriteProtocol
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value));
|
BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value));
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
case HistorianDataType.UInt1:
|
||||||
|
{
|
||||||
|
return [checked((byte)value)];
|
||||||
|
}
|
||||||
|
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:
|
default:
|
||||||
throw new ProtocolEvidenceMissingException(
|
throw new ProtocolEvidenceMissingException(
|
||||||
$"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " +
|
$"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " +
|
||||||
"Captured types: Float, Double, Int2, Int4, UInt4.");
|
"Captured types: Float, Double, Int2, Int4, UInt4, UInt1, Int8, UInt8.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ internal static class HistorianTagWriteProtocol
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Native CDataType wire codes per data type — captured 2026-05-04 by probing
|
/// Native CDataType wire codes per data type — captured by probing every type via
|
||||||
/// every type via instrument-wcf-writemessage. Matches the codes already documented
|
/// instrument-wcf-writemessage. Matches the codes already documented in
|
||||||
/// in <see cref="HistorianWcfTagClient"/> MapDataType for the read path.
|
/// <see cref="HistorianWcfTagClient"/> MapDataType for the read path; UInt1/Int8/UInt8
|
||||||
|
/// reuse the same read-side codes (0x08/0x19/0x39).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static byte GetAnalogDataTypeCode(Models.HistorianDataType dataType) => dataType switch
|
public static byte GetAnalogDataTypeCode(Models.HistorianDataType dataType) => dataType switch
|
||||||
{
|
{
|
||||||
@@ -70,8 +71,11 @@ internal static class HistorianTagWriteProtocol
|
|||||||
Models.HistorianDataType.UInt4 => 0x11,
|
Models.HistorianDataType.UInt4 => 0x11,
|
||||||
Models.HistorianDataType.Int2 => 0x29,
|
Models.HistorianDataType.Int2 => 0x29,
|
||||||
Models.HistorianDataType.Int4 => 0x31,
|
Models.HistorianDataType.Int4 => 0x31,
|
||||||
|
Models.HistorianDataType.UInt1 => 0x08,
|
||||||
|
Models.HistorianDataType.Int8 => 0x19,
|
||||||
|
Models.HistorianDataType.UInt8 => 0x39,
|
||||||
_ => throw new ProtocolEvidenceMissingException(
|
_ => throw new ProtocolEvidenceMissingException(
|
||||||
$"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4."),
|
$"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4, UInt1, Int8, UInt8."),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly byte[] AnalogPadding16 = new byte[16];
|
private static readonly byte[] AnalogPadding16 = new byte[16];
|
||||||
@@ -118,7 +122,8 @@ internal static class HistorianTagWriteProtocol
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializes a CTagMetadata payload for an analog tag. Live-verified for Float,
|
/// Serializes a CTagMetadata payload for an analog tag. Live-verified for Float,
|
||||||
/// Double, Int2, Int4, UInt4 — see <see cref="GetAnalogDataTypeCode"/> for the
|
/// Double, Int2, Int4, UInt4; UInt1/Int8/UInt8 supported via the read-side type
|
||||||
|
/// codes (pending live round-trip) — see <see cref="GetAnalogDataTypeCode"/> for the
|
||||||
/// type-code mapping. Output matches the byte-for-byte capture for the same inputs.
|
/// 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
|
/// 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.
|
/// `1A 03` scaling marker; otherwise emits `1F` + 4 doubles in order.
|
||||||
|
|||||||
Reference in New Issue
Block a user