# ASB Variant Wire Format This note documents the ASB `Variant` and related value/status payloads that `MxAsbClient` currently understands. It is based on `src/MxAsbClient/AsbContracts.cs`, `MxAsbDataClient.DecodeVariant`, `MxAsbDataClient.FormatVariant`, `src/MxAsbClient.Tests/Program.cs`, and the remaining-work notes. ## ASBIData Framing ASB custom data-contract values are serialized as an `ASBIData` XML element containing base64-encoded binary. The binary uses the .NET `BinaryWriter` / `BinaryReader` primitive layout used by the client on Windows: | Field kind | Binary layout | | --- | --- | | `ushort` | 2-byte little-endian unsigned integer | | `int` / `uint` | 4-byte little-endian integer | | `long` / `ulong` | 8-byte little-endian integer | | `float` / `double` | 4-byte / 8-byte IEEE payload from `BitConverter` | | `bool` fields in custom structs | one `BinaryWriter.Write(bool)` byte | | UTF-16 strings in custom structs | 4-byte byte length, then UTF-16LE bytes | | custom arrays | 4-byte element count, then each element's custom binary body | The `Variant` body itself is: | Offset | Size | Field | Notes | | --- | ---: | --- | --- | | 0 | 2 | `Type` | `AsbDataType` numeric ID. | | 2 | 4 | `Length` | Logical payload length. Factory-created variants set this to `Payload.Length`. | | 6 | 4 | `PayloadLength` | Actual byte count emitted before `Payload`. | | 10 | `PayloadLength` | `Payload` | Type-specific bytes. Empty or missing payload is treated as empty. | `DecodeVariant` trusts the actual payload bytes for parsing. Unsupported type IDs with non-empty payloads are returned as raw `byte[]`; unsupported empty variants return `null`, except known empty string/array types, which return empty values. ## Declared Type IDs These IDs are declared by `AsbDataType`. Only the layouts listed as decode-supported below should be considered implemented behavior. | ID | Name | Current handling | | ---: | --- | --- | | 0 | `TypeByte` | Declared only; raw bytes if received. | | 1 | `TypeChar` | Declared only; raw bytes if received. | | 2 | `TypeInt16` | Declared only; raw bytes if received. | | 3 | `TypeUInt16` | Declared only; raw bytes if received. | | 4 | `TypeInt32` | Decode/write supported and live-proven. | | 5 | `TypeUInt32` | Declared only; raw bytes if received. | | 6 | `TypeInt64` | Declared only; raw bytes if received. | | 7 | `TypeUInt64` | Declared only; raw bytes if received. | | 8 | `TypeFloat` | Decode/write supported and live-proven. | | 9 | `TypeDouble` | Decode/write supported and live-proven. | | 10 | `TypeString` | Decode/write supported and live-proven. | | 11 | `TypeDateTime` | Decode/write supported and live-proven. | | 12 | `TypeDuration` | Decode/write supported and live-proven. | | 13 | `TypeGuid` | Declared only; raw bytes if received. | | 14 | `TypeByteString` | Declared only; raw bytes if received. | | 15 | `TypeLocaleId` | Declared only; raw bytes if received. | | 16 | `TypeLocalizedText` | Declared only; raw bytes if received. | | 17 | `TypeBool` | Decode/write supported and live-proven. | | 18 | `TypeSByte` | Declared only; raw bytes if received. | | 19 | `TypeErrorStatus` | Declared only; raw bytes if received. | | 20 | `TypeEnum` | Declared only; raw bytes if received. | | 21 | `TypeDataType` | Declared only; raw bytes if received. | | 22 | `TypeSecurityClassification` | Declared only; raw bytes if received. | | 23 | `TypeDataQuality` | Declared only; raw bytes if received. | | 40 | `TypeByteArray` | Declared only; raw bytes if received. | | 41 | `TypeCharArray` | Declared only; raw bytes if received. | | 42 | `TypeInt16Array` | Declared only; raw bytes if received. | | 43 | `TypeUInt16Array` | Declared only; raw bytes if received. | | 44 | `TypeInt32Array` | Decode/write supported and live-proven for deployed array tags. | | 45 | `TypeUInt32Array` | Declared only; raw bytes if received. | | 46 | `TypeInt64Array` | Declared only; raw bytes if received. | | 47 | `TypeUInt64Array` | Declared only; raw bytes if received. | | 48 | `TypeFloatArray` | Decode/write supported and live-proven for deployed array tags. | | 49 | `TypeDoubleArray` | Decode/write supported and live-proven for deployed array tags. | | 50 | `TypeStringArray` | Decode/write supported and live-proven for deployed array tags. | | 51 | `TypeDateTimeArray` | Decode/write supported and live-proven for deployed array tags. | | 52 | `TypeDurationArray` | Decode/write supported by tests; live coverage depends on deployed tags. | | 53 | `TypeGuidArray` | Declared only; raw bytes if received. | | 54 | `TypeByteStringArray` | Declared only; raw bytes if received. | | 55 | `TypeLocaleIdArray` | Declared only; raw bytes if received. | | 56 | `TypeLocalizedTextArray` | Declared only; raw bytes if received. | | 57 | `TypeBoolArray` | Decode/write supported and live-proven for deployed array tags. | | 58 | `TypeSByteArray` | Declared only; raw bytes if received. | | 60 | `TypeEnumArray` | Declared only; raw bytes if received. | | 61 | `TypeDataTypeArray` | Declared only; raw bytes if received. | | 62 | `TypeSecurityClassificationArray` | Declared only; raw bytes if received. | | 63 | `TypeDataQualityArray` | Declared only; raw bytes if received. | | 65535 | `TypeUnknown` | Used for empty/user-data placeholders. | ## Supported Scalar Payloads | Type | ID | Payload layout | Empty-payload decode | | --- | ---: | --- | --- | | `TypeBool` | 17 | one byte; non-zero is `true`, zero is `false` | `null` | | `TypeInt32` | 4 | 4-byte little-endian signed integer | `null` | | `TypeFloat` | 8 | 4-byte IEEE single from `BitConverter` | `null` | | `TypeDouble` | 9 | 8-byte IEEE double from `BitConverter` | `null` | | `TypeString` | 10 | UTF-16LE bytes, no per-string length inside the variant payload | empty string | | `TypeDateTime` | 11 | 8-byte signed Windows FILETIME from `DateTime.ToFileTimeUtc()` | `null` | | `TypeDuration` | 12 | 8-byte signed .NET tick count from `TimeSpan.Ticks` | `null` | `FormatVariant` renders scalar values as invariant strings. `DateTime` values format with the round-trip `O` pattern after `DateTime.FromFileTimeUtc()`; `Duration` values format with the constant `c` `TimeSpan` pattern. ## Supported Array Payloads Variant arrays do not carry an element count in a common header. The element count is inferred from payload byte length for fixed-width arrays, or by walking length-prefixed records for string arrays. | Type | ID | Payload layout | Empty-payload decode | | --- | ---: | --- | --- | | `TypeInt32Array` | 44 | packed 4-byte little-endian signed integers | empty `int[]` | | `TypeBoolArray` | 57 | one byte per element; non-zero is `true` | empty `bool[]` | | `TypeFloatArray` | 48 | packed 4-byte IEEE singles | empty `float[]` | | `TypeDoubleArray` | 49 | packed 8-byte IEEE doubles | empty `double[]` | | `TypeStringArray` | 50 | repeated 4-byte byte length, then UTF-16LE bytes; zero length is empty string | empty `string[]` | | `TypeDateTimeArray` | 51 | packed 8-byte Windows FILETIME values | empty `DateTime[]` | | `TypeDurationArray` | 52 | packed 8-byte .NET tick counts | empty `TimeSpan[]` | For malformed string-array payloads, decoding stops when the next declared string byte length is negative or extends beyond the remaining payload. The partial values decoded before that point are preserved. ## Timestamp and Duration Handling There are two different timestamp encodings: | Location | Encoding | | --- | --- | | `Variant` `TypeDateTime` / `TypeDateTimeArray` payloads | Windows FILETIME UTC values (`DateTime.ToFileTimeUtc()` / `DateTime.FromFileTimeUtc()`). | | `RuntimeValue.Timestamp` in read/publish wrappers | 8-byte `DateTime.ToBinary()` value followed by a one-byte `TimestampSpecified` flag. Publish mapping normalizes the timestamp to UTC for `AsbPublishedValue.TimestampUtc`. | `TypeDuration` and `TypeDurationArray` payloads are 8-byte `TimeSpan.Ticks` values. No alternate duration units are inferred. ## String Encoding Scalar `TypeString` payloads are raw UTF-16LE bytes without an inner length. The variant `Length` and `PayloadLength` fields provide the byte count. `TypeStringArray` payloads are a sequence of string records. Each record starts with a 4-byte little-endian byte length followed by that many UTF-16LE bytes. `null` and empty strings are both emitted as a zero-length string by `AsbVariantFactory.FromStringArray`, and decode as `string.Empty`. The same 4-byte byte-length plus UTF-16LE convention is also used by custom struct string fields such as `ItemIdentity.Name`, but that is outside the variant payload. ## Runtime Value, Quality, and Status Quality is not part of a `Variant` payload. Reads and publishes wrap the variant inside `RuntimeValue`, whose binary layout is: | Order | Field | Binary layout | | ---: | --- | --- | | 1 | `Timestamp` | 8-byte `DateTime.ToBinary()` value | | 2 | `TimestampSpecified` | one boolean byte | | 3 | `Value` | nested `Variant` body | | 4 | `Status` | nested `AsbStatus` body | `AsbStatus` is: | Order | Field | Binary layout | | ---: | --- | --- | | 1 | `Count` | signed byte | | 2 | payload byte length | 4-byte integer | | 3 | `Payload` | status element bytes | `AsbPublishMapper.DecodeStatus` treats `Count` as the number of payload bytes to inspect when it is positive, capped at the actual payload length. If `Count` is zero or negative, it inspects the full payload. Status element bytes are parsed as repeated records: | Record part | Meaning | | --- | --- | | marker byte bit 0-6 | `AsbStatusElementType` ID | | marker byte bit 7 set | element value is implicitly zero and no value bytes follow | | marker byte bit 7 clear | next two bytes are a little-endian `ushort` value | Known status element type IDs are: | ID | Name | | ---: | --- | | 1 | `OpcDaStatus` | | 2 | `OpcUaStatus` | | 3 | `OpcUaVendorStatus` | | 4 | `ScadaStatus` | | 5 | `MxStatusCategory` | | 6 | `MxStatusDetail` | | 7 | `MxQuality` | | 125 | `Reserved1Status` | | 126 | `Reserved2Status` | | 127 | `Reserved3Status` | `MxQuality` is summarized by masking with `0x00C0`: `0x00C0` is good, `0x0040` is uncertain, and `0x0000` is bad. Unknown quality values and unknown status element types are preserved rather than guessed. ## Proven Versus Intentionally Open Live/read/write coverage is complete for Boolean, Int32, Float, Double, String, DateTime, Duration, and the deployed array tags called out in the remaining-work notes. The non-live test suite also round-trips the supported factories through `DecodeVariant`, including the duration array path. The client intentionally does not infer layouts for declared-but-unsupported ASB types such as GUID, byte strings, localized text, enum/data-type/security/data quality variants, and their array forms. Those types stay as raw bytes until a real deployed tag, capture, or contract evidence proves the payload shape. Buffered publish batches are also intentionally not described here. The current ASB buffered flag is exposed, but observed publish responses still carry a single current `RuntimeValue` per item rather than a proven multi-sample batch payload.