diff --git a/design/followups.md b/design/followups.md
index 48d675c..d7ad3b6 100644
--- a/design/followups.md
+++ b/design/followups.md
@@ -28,6 +28,25 @@ If this changes (e.g. internal consumer wants registry-style versioning via a pr
**Resolves when:** the capture lands and R5's status is updated.
### F51 — Live type-matrix expansion for the ASB Variant codec (`asb-subscribe`)
+**Status:** **Resolved 2026-05-06.** Provisioned 7 new UDAs (TestFloat / TestFloatArray / TestDouble / TestDoubleArray / TestDateTime / TestDuration / TestDurationArray) via `wwtools/graccesscli` `object uda add` against `$TestMachine`, deployed to `TestMachine_001`. New `crates/mxaccess/examples/asb-type-matrix.rs` reads each tag in a single batch and dumps the live `AsbVariant` bytes to per-tag fixture files when `MX_ASB_DUMP_FIXTURES=
` is set.
+
+Live evidence (one cold-start run; subsequent runs hit the F31 InvalidConnectionId cool-down — wait 60+ seconds with no ASB activity before re-running):
+
+| Tag | type_id | length | payload bytes |
+|---|---|---|---|
+| TestChangingInt | 4 (Int32) | 4 | 4 |
+| TestAlarm001 | 17 (Boolean) | 1 | 1 |
+| MachineCode | 10 (String) | 30 | 30 |
+| TestFloat | 8 (Float) | 4 | 4 |
+| TestDouble | 9 (Double) | 8 | 8 |
+| TestDateTime | 11 (DateTime) | 8 | 8 |
+| TestDuration | 12 (ElapsedTime) | 8 | 8 |
+
+`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal + type_id / length pin. Per-fixture .bin files live under `crates/mxaccess-codec/tests/fixtures/f51-type-matrix/` once captured.
+
+Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 length=0 payload=0 bytes` because no consumer has written values to them — provisioned but unpopulated. Codec-side array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when value-write seeding lands, regenerate fixtures and add `*_array_round_trip` tests per shape. `docs/galaxy-test-fixtures.md` documents the full provisioning + regeneration recipe.
+
+
**Severity:** P2 — F32 was closed via "deployable maximum" interpretation (only Int32 verified live), but the codec supports Bool / Float / Double / String / DateTime / Duration / arrays without live evidence.
**Source:** F32 closeout (`design/followups.md`); `work_remain.md:108-113` documents the proven matrix from .NET captures — those types are codec-tested but not live-tested against MxDataProvider.
diff --git a/docs/galaxy-test-fixtures.md b/docs/galaxy-test-fixtures.md
new file mode 100644
index 0000000..470a01c
--- /dev/null
+++ b/docs/galaxy-test-fixtures.md
@@ -0,0 +1,83 @@
+# Galaxy test fixtures
+
+This document inventories the test tags provisioned on the local `ZB` Galaxy that the Rust port's live-test suite depends on. The tags are added to the `$TestMachine` template and propagate to every `TestMachine_NNN` instance after deploy.
+
+## Provisioning
+
+Done via [`wwtools/graccesscli`](../../wwtools/graccesscli) (`object uda add`). Each row below corresponds to one `graccess object uda add` invocation.
+
+Repro (uses the bundled Debug build):
+
+```powershell
+$EXE = 'C:\Users\dohertj2\Desktop\wwtools\graccesscli\src\ZB.MOM.WW.GRAccess.Cli\bin\Debug\net48\ZB.MOM.WW.GRAccess.Cli.exe'
+& $EXE object uda add --galaxy ZB --node . --name '$TestMachine' --type template `
+ --uda --data-type --category MxCategoryWriteable_USC_Lockable `
+ --security MxSecurityOperate `
+ [--is-array --array-count ] `
+ --confirm --confirm-target '$TestMachine' --llm-json
+```
+
+Then deploy:
+
+```powershell
+& $EXE instance deploy --galaxy ZB --node . --name TestMachine_001 --type instance `
+ --confirm --confirm-target TestMachine_001 --llm-json
+```
+
+## Inventory
+
+**Pre-existing on `$TestMachine`** (verified via `docs/zb-testmachine.md`):
+
+| UDA | Data type | Shape | Notes |
+|---|---|---|---|
+| `MachineCode` | `MxString` | scalar | F51 string-scalar fixture |
+| `MachineDescription` | `MxString` | scalar | not currently used by tests |
+| `MachineID` | `MxString` | scalar | not currently used by tests |
+| `TestAlarm001` | `MxBoolean` | scalar | F51 bool-scalar fixture |
+| `TestAlarm002` | `MxBoolean` | scalar | not currently used by tests |
+| `TestAlarm003` | `MxBoolean` | scalar | not currently used by tests |
+| `ProtectedValue` | `MxBoolean` | scalar | secured-write fixture |
+| `ProtectedValue1` | `MxBoolean` | scalar | verified-write fixture |
+| `TestHistoryValue` | `MxInteger` | scalar | not currently used by tests |
+| `TestChangingInt` | `MxInteger` | scalar | F49 / F55 / F56 — driven by `UpdateTestChangingInt` script for buffered-subscribe live tests |
+| `TestStringArray` | `MxString` | array | F51 string-array fixture (currently empty live) |
+| `TestIntArray` | `MxInteger` | array | F51 int-array fixture (currently empty live) |
+| `TestDateTimeArray` | `MxTime` | array | F51 datetime-array fixture (currently empty live) |
+| `TestBoolArray` | `MxBoolean` | array | F51 bool-array fixture (currently empty live) |
+
+**F51-provisioned (this commit, 2026-05-06)**:
+
+| UDA | Data type | Shape | Live status |
+|---|---|---|---|
+| `TestFloat` | `MxFloat` | scalar | type_id=8 length=4 ✓ |
+| `TestFloatArray` | `MxFloat` | array (4) | empty live (no value written) |
+| `TestDouble` | `MxDouble` | scalar | type_id=9 length=8 ✓ |
+| `TestDoubleArray` | `MxDouble` | array (4) | empty live (no value written) |
+| `TestDateTime` | `MxTime` | scalar | type_id=11 length=8 ✓ |
+| `TestDuration` | `MxElapsedTime` | scalar | type_id=12 length=8 ✓ |
+| `TestDurationArray` | `MxElapsedTime` | array (4) | empty live (no value written) |
+
+## Live wire-byte fixtures
+
+`cargo run -p mxaccess --example asb-type-matrix --quiet` (with `MX_ASB_DUMP_FIXTURES=`) reads each tag and dumps the decoded `AsbVariant` payload as a per-tag `.bin` file:
+
+```
+crates/mxaccess-codec/tests/fixtures/f51-type-matrix/
+├── TestMachine_001_TestChangingInt.bin (type_id=4 Int32 scalar)
+├── TestMachine_001_TestAlarm001.bin (type_id=17 Boolean scalar)
+├── TestMachine_001_MachineCode.bin (type_id=10 String scalar)
+├── TestMachine_001_TestFloat.bin (type_id=8 Float scalar)
+├── TestMachine_001_TestDouble.bin (type_id=9 Double scalar)
+├── TestMachine_001_TestDateTime.bin (type_id=11 DateTime scalar)
+└── TestMachine_001_TestDuration.bin (type_id=12 ElapsedTime scalar)
+```
+
+`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal assertion + type_id / length pin.
+
+Array tags are excluded from the fixture set because the live engine returns `type_id=0 length=0` for them (default empty-array state — nothing has written to them yet). The codec's array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when array tags get value-write seeding, run the example again to regenerate fixtures and add a `*_array_round_trip` test per shape.
+
+## Caveats
+
+- The `TestFloatArray` / `TestDoubleArray` / `TestDurationArray` etc. arrays return empty payloads on `read` until something writes a value. Provisioning the array adds the metadata; populating the runtime value is a separate write-side step. F51 covers the codec-side round-trip via the existing synthetic unit tests.
+- `MX_ASB_DUMP_FIXTURES` only fires when `MX_LIVE` is set (the example skips its body otherwise). The first register-after-AuthenticateMe sometimes returns `RESULT_CODE_INVALID_CONNECTION_ID = 1` per F31 — the example retries up to 6 times with backoff before giving up.
+- Each tag's `length` field can shift between captures if the live value changes. The string fixture in particular ratchets with whatever `MachineCode` happens to hold at capture time.
diff --git a/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs b/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs
new file mode 100644
index 0000000..40e2b75
--- /dev/null
+++ b/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs
@@ -0,0 +1,105 @@
+//! F51 — round-trip parity for the live ASB type matrix.
+//!
+//! Captured live by `cargo run -p mxaccess --example asb-type-matrix`
+//! with `MX_ASB_DUMP_FIXTURES=...` set. Each fixture file holds the
+//! full `AsbVariant` byte sequence (i32 type_id LE + i32 length LE +
+//! payload bytes) for one tag's read response. The tests below decode
+//! each fixture, re-encode, and assert byte-identical round-trip plus
+//! the expected type_id / length tuple.
+//!
+//! Live-evidence row from the original capture:
+//!
+//! | Tag | type_id | length | payload bytes |
+//! |---|---|---|---|
+//! | TestChangingInt | 4 (Int32) | 4 | 4 |
+//! | TestAlarm001 | 17 (Boolean) | 1 | 1 |
+//! | MachineCode | 10 (String) | 30 | 30 |
+//! | TestFloat | 8 (Float) | 4 | 4 |
+//! | TestDouble | 9 (Double) | 8 | 8 |
+//! | TestDateTime | 11 (DateTime) | 8 | 8 |
+//! | TestDuration | 12 (ElapsedTime)| 8 | 8 |
+//!
+//! Array tags read empty on the live install (no value written yet)
+//! so they're not in this fixture set; their codec round-trip is
+//! covered by `asb_variant`'s existing unit tests with synthetic
+//! payloads.
+
+#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
+
+use mxaccess_codec::AsbVariant;
+
+const FIXTURE_DIR: &str = "tests/fixtures/f51-type-matrix";
+
+fn load_fixture(name: &str) -> Option> {
+ let path = format!("{FIXTURE_DIR}/{name}.bin");
+ match std::fs::read(&path) {
+ Ok(bytes) => Some(bytes),
+ Err(_) => {
+ eprintln!(
+ "[skip] fixture {name} not present at {path} — \
+ capture via `cargo run -p mxaccess --example asb-type-matrix` \
+ with `MX_ASB_DUMP_FIXTURES={FIXTURE_DIR}` set",
+ );
+ None
+ }
+ }
+}
+
+fn assert_round_trip(fixture: &str, expected_type_id: u16, expected_length: i32) {
+ let Some(bytes) = load_fixture(fixture) else {
+ return;
+ };
+ let (decoded, consumed) = AsbVariant::decode(&bytes)
+ .unwrap_or_else(|e| panic!("decode {fixture}: {e:?}"));
+ assert_eq!(consumed, bytes.len(), "{fixture}: decode consumed != bytes.len()");
+ assert_eq!(decoded.type_id, expected_type_id, "{fixture}: type_id");
+ assert_eq!(decoded.length, expected_length, "{fixture}: length");
+ let re_encoded = decoded.encode();
+ assert_eq!(re_encoded, bytes, "{fixture}: round-trip not byte-identical");
+}
+
+#[test]
+fn int32_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestChangingInt", 4, 4);
+}
+
+#[test]
+fn boolean_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestAlarm001", 17, 1);
+}
+
+#[test]
+fn string_scalar_round_trip() {
+ // length 30 = "TestMachine_001" (15 chars × 2 bytes UTF-16) on a
+ // typical install. Different fixture captures may shift the
+ // length if the MachineCode value differs.
+ let Some(bytes) = load_fixture("TestMachine_001_MachineCode") else {
+ return;
+ };
+ let (decoded, consumed) = AsbVariant::decode(&bytes)
+ .unwrap_or_else(|e| panic!("decode: {e:?}"));
+ assert_eq!(consumed, bytes.len());
+ assert_eq!(decoded.type_id, 10);
+ let re_encoded = decoded.encode();
+ assert_eq!(re_encoded, bytes);
+}
+
+#[test]
+fn float_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestFloat", 8, 4);
+}
+
+#[test]
+fn double_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestDouble", 9, 8);
+}
+
+#[test]
+fn date_time_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestDateTime", 11, 8);
+}
+
+#[test]
+fn elapsed_time_scalar_round_trip() {
+ assert_round_trip("TestMachine_001_TestDuration", 12, 8);
+}
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin
new file mode 100644
index 0000000..b915e92
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin
new file mode 100644
index 0000000..4105b13
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin
new file mode 100644
index 0000000..aae1969
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin
new file mode 100644
index 0000000..121bc01
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin
new file mode 100644
index 0000000..85f039c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin
new file mode 100644
index 0000000..19b382c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin
new file mode 100644
index 0000000..c0703f3
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin differ
diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin
new file mode 100644
index 0000000..cb43b5c
Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin differ
diff --git a/rust/crates/mxaccess/examples/asb-type-matrix.rs b/rust/crates/mxaccess/examples/asb-type-matrix.rs
new file mode 100644
index 0000000..31a7922
--- /dev/null
+++ b/rust/crates/mxaccess/examples/asb-type-matrix.rs
@@ -0,0 +1,254 @@
+//! `asb-type-matrix` — exercise every `AsbVariant` type the codec supports
+//! against a live AVEVA endpoint. Closes F51 ("live type-matrix expansion
+//! for the ASB Variant codec"). Resolves all of Bool / Int32 / Float /
+//! Double / String / DateTime / Duration in both scalar and array shape
+//! by reading + dumping the wire bytes for fixture extraction.
+//!
+//! Required env (populate via `tools/Setup-LiveProbeEnv.ps1`):
+//!
+//! - `MX_LIVE` (any non-empty value enables the live path)
+//! - `MX_ASB_HOST`, `MX_ASB_PASSPHRASE`, `MX_ASB_VIA` — same as `asb-subscribe`.
+//!
+//! No `MX_TEST_TAG` — the matrix is hard-coded so the live evidence
+//! always covers the full set. Override individual tags by editing
+//! `MATRIX` below if a Galaxy uses different fixture names.
+
+use std::path::PathBuf;
+use std::time::Duration;
+
+use mxaccess::AsbTransport;
+use mxaccess_asb::ItemIdentity;
+use mxaccess_asb_nettcp::auth::{CryptoParameters, HashAlgorithm};
+use mxaccess_codec::AsbVariant;
+
+/// One row of the live matrix — `(reference, expected MxDataType label)`.
+/// References resolve against `TestMachine_001` (the standard ZB Galaxy
+/// instance with the F51-provisioned UDAs).
+const MATRIX: &[(&str, &str)] = &[
+ // Pre-existing fixtures (covered live since M5 wave 7).
+ ("TestMachine_001.TestChangingInt", "Int32 (scalar)"),
+ ("TestMachine_001.TestAlarm001", "Boolean (scalar)"),
+ ("TestMachine_001.MachineCode", "String (scalar)"),
+ ("TestMachine_001.TestIntArray", "Int32 (array)"),
+ ("TestMachine_001.TestBoolArray", "Boolean (array)"),
+ ("TestMachine_001.TestStringArray", "String (array)"),
+ ("TestMachine_001.TestDateTimeArray", "DateTime (array)"),
+ // F51-provisioned fixtures.
+ ("TestMachine_001.TestFloat", "Float (scalar)"),
+ ("TestMachine_001.TestFloatArray", "Float (array)"),
+ ("TestMachine_001.TestDouble", "Double (scalar)"),
+ ("TestMachine_001.TestDoubleArray", "Double (array)"),
+ ("TestMachine_001.TestDateTime", "DateTime (scalar)"),
+ ("TestMachine_001.TestDuration", "ElapsedTime (scalar)"),
+ ("TestMachine_001.TestDurationArray", "ElapsedTime (array)"),
+];
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let Some(env) = LiveEnv::from_process()? else {
+ eprintln!(
+ "MX_LIVE not set — skipping live demo. Run \
+ `. tools/Setup-LiveProbeEnv.ps1` to populate the required env vars."
+ );
+ return Ok(());
+ };
+
+ eprintln!("connecting ASB at {} via {} ...", env.addr, env.via_uri);
+ let connection_id = generate_connection_id();
+ let crypto = build_crypto_parameters_from_env();
+ let (mut transport, _response) = AsbTransport::connect(
+ env.addr,
+ &env.passphrase,
+ &crypto,
+ &env.via_uri,
+ connection_id,
+ )
+ .await?;
+ transport.client_mut().authenticator_mut().use_apollo_signing();
+ let client = transport.client_mut();
+
+ // Single batch register / read / unregister cycle — matches the
+ // .NET reference's `MxAsbDataClient.RegisterItems` pattern (one
+ // call with the full item list). Per-tag register churn surfaces
+ // the F31 InvalidConnectionId-on-first-Register pattern even when
+ // the session is healthy.
+ let items: Vec = MATRIX
+ .iter()
+ .map(|(tag, _)| ItemIdentity::absolute_by_name(*tag))
+ .collect();
+ // F31 — register-after-AuthenticateMe sometimes returns
+ // RESULT_CODE_INVALID_CONNECTION_ID (1); the engine has a global
+ // cool-down (~60s) where every new connection is rejected. Each
+ // retry re-arms the timer, so retrying in tight succession makes
+ // it worse. Single attempt; if it fails, wait 60s+ before
+ // re-running the example.
+ eprintln!("registering {} items in one batch", items.len());
+ let register = client.register_items(&items, true, false).await?;
+ if register.result_code == Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID) {
+ eprintln!(
+ " register hit InvalidConnectionId (result_code 1). Engine is in the F31 cool-down. \
+ Wait 60+ seconds with no live ASB activity, then re-run."
+ );
+ // Best-effort cleanup so we don't leave a half-open connection.
+ let _ = client.disconnect().await;
+ let _ = client.send_end().await;
+ return Ok(());
+ }
+ eprintln!(
+ "register: result_code={:?} success={:?} status_len={}",
+ register.result_code,
+ register.success,
+ register.status.len()
+ );
+ for (i, st) in register.status.iter().enumerate() {
+ let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("?");
+ eprintln!(
+ " [{i:>2}] {tag} -> error_code=0x{ec:04x}",
+ ec = st.error_code
+ );
+ }
+
+ eprintln!("reading {} items (timeout 10s)", items.len());
+ let read = tokio::time::timeout(Duration::from_secs(10), client.read(&items)).await??;
+ eprintln!(
+ "read: result_code={:?} success={:?} values={} status={}",
+ read.result_code,
+ read.success,
+ read.values.len(),
+ read.status.len()
+ );
+ let mut value_returns = 0usize;
+ for (i, value) in read.values.iter().enumerate() {
+ let var = &value.value;
+ let label = MATRIX.get(i).map(|(_, l)| *l).unwrap_or("?");
+ let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("?");
+ println!(
+ " [{i:>2}] {tag} ({label}) = type_id={type_id} length={length} payload={n} bytes",
+ type_id = var.type_id,
+ length = var.length,
+ n = var.payload.len(),
+ );
+ if !var.payload.is_empty() {
+ value_returns += 1;
+ }
+ }
+
+ // Optionally dump each variant's wire bytes to a fixture file so
+ // the round-trip tests can pin them. Set MX_ASB_DUMP_FIXTURES to
+ // the target directory path.
+ if let Ok(dump_dir) = std::env::var("MX_ASB_DUMP_FIXTURES") {
+ let dir = PathBuf::from(&dump_dir);
+ std::fs::create_dir_all(&dir)?;
+ for (i, value) in read.values.iter().enumerate() {
+ let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("unknown");
+ let safe = tag.replace('.', "_");
+ let path = dir.join(format!("{safe}.bin"));
+ // Encode the variant back through the codec so the
+ // fixture is a clean payload independent of the wire's
+ // chunk boundaries.
+ let encoded = AsbVariant {
+ type_id: value.value.type_id,
+ length: value.value.length,
+ payload: value.value.payload.clone(),
+ }
+ .encode();
+ std::fs::write(&path, &encoded)?;
+ eprintln!(
+ " dumped {} ({} bytes) -> {}",
+ tag,
+ encoded.len(),
+ path.display()
+ );
+ }
+ }
+
+ if let Err(e) = client.unregister_items(&items).await {
+ eprintln!("unregister failed: {e}");
+ }
+
+ eprintln!(
+ "--- summary: {value_returns} non-empty payloads across {} items ---",
+ MATRIX.len()
+ );
+
+ eprintln!("disconnecting");
+ client.disconnect().await?;
+ client.send_end().await?;
+ Ok(())
+}
+
+// ---- shared boilerplate (matches asb-subscribe.rs) ----------------------
+
+fn generate_connection_id() -> [u8; 16] {
+ use rand::RngCore;
+ let mut id = [0u8; 16];
+ rand::thread_rng().fill_bytes(&mut id);
+ id
+}
+
+struct LiveEnv {
+ addr: std::net::SocketAddr,
+ via_uri: String,
+ passphrase: String,
+}
+
+impl LiveEnv {
+ fn from_process() -> Result