Files
mxaccess/docs/ASB-Variant-Wire-Format.md
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 06:21:00 -04:00

11 KiB

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.