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>
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.