fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
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>
223 lines
11 KiB
Markdown
223 lines
11 KiB
Markdown
# 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.
|