Iter 26 — closes the ABI gap between the Python and Rust SyncPacket
decoders. Before this, Python could decode the wire but had no helpers
to apply offsets or recover per-frame mesh time; any Python-side tooling
(host scripts, replay analysers, notebooks) would have to re-implement
the math from scratch and could drift from Rust silently.
New methods on the Python SyncPacket dataclass:
local_minus_epoch_us() -> int
Signed local-vs-mesh offset. Matches Rust byte-for-byte.
apply_to_local(local_at_frame_us: int) -> int
offset = epoch_us - local_us
return local_at_frame_us + offset
Identity at local_at_frame_us == self.local_us returns epoch_us.
mesh_aligned_us_for_sequence(frame_seq: int, fps_hz: float) -> int
Sequence-based interpolation matching Rust's identical method.
Includes u32 wraparound handling via masked-subtract — verified
against Rust's iter 17 `mesh_aligned_for_sequence_handles_seq_wraparound`.
3 new Python tests (10 total in TestSyncPacketParser, all green in 0.24s):
test_apply_to_local_recovers_epoch_at_sync_point
Identity at the sync point. Also verifies local_minus_epoch_us()
matches §A0.10's measured 1,163,565 µs bench number.
test_apply_to_local_preserves_inter_frame_delta
Frame arriving 5 s after the sync on the follower's local clock
produces mesh time exactly 5 s after sync.epoch_us.
test_mesh_aligned_us_for_sequence_matches_rust
Cross-language parity with Rust's
`end_to_end_sync_decode_then_frame_mesh_recovery` (iter 20):
100 frames after sync.sequence at 20 fps = sync.epoch_us + 5 s.
Cross-checks via apply_to_local — both paths must agree.
Test count after iter 26:
Python TestSyncPacketParser: 10/10 (was 7/7)
Rust sync_packet::tests: 15/15
Combined: 25 unit tests defending the SyncPacket contract across
the two host language stacks.
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 21 — ultra-opt for protocol correctness across the two production
decoders. Pin the same 32-byte canonical hex in both Python and Rust
tests; if either decoder drifts from the wire, ONE of the tests starts
failing — and it's clear which side moved.
Canonical packet: COM9 sync-pkt #1 from §A0.12 live capture, expressed
as exact little-endian bytes:
10a111c5 09 01 06 00 magic + node + ver + flags + rsvd
f26db70100000000 local_us = 28_798_450
c5aca50100000000 epoch_us = 27_634_885
1400000000000000 sequence = 20 + reserved
Python test:
archive/v1/tests/unit/test_esp32_binary_parser.py::TestSyncPacketParser
::test_canonical_wire_bytes_match_rust_decoder
— decodes the pinned hex, asserts every field including the §A0.10
1,163,565 µs offset.
Rust test:
v2/crates/wifi-densepose-hardware/src/sync_packet.rs::tests
::canonical_wire_bytes_match_python_decoder
— decodes the same bytes, asserts the same fields, then re-encodes
via to_bytes() and asserts the round-trip produces the EXACT same
32 bytes. So this also catches drift in the Rust encoder.
Test counts after this iter:
Rust sync_packet: 15/15 green (was 14)
Python SyncPacketParser: 7/7 green (was 6)
Branch contract: if a future PR changes the firmware wire format, BOTH
tests must be updated atomically with the new canonical hex. CI will
gate this naturally.
Co-Authored-By: claude-flow <ruv@ruv.net>
Python ESP32BinaryParser was using struct format '<IBBHIIBB2x' — the
'2x' skipped bytes 18-19 as reserved. After the Rust-side decoder was
extended to surface PPDU type + flags, the Python pipeline (which
archive/v1 still uses for testing + the proof verifier) needs the same
update so its consumers see the HE metadata too.
csi_extractor.py:
- HEADER_FMT now '<IBBHIIBBBB' (captures bytes 18-19)
- New metadata fields: ppdu_type ('ht_legacy'|'he_su'|'he_mu'|'he_tb'|'unknown'),
ppdu_type_raw, he_capable, bw40, stbc, ldpc, ieee802154_sync_valid,
adr018_flags_raw
- Class constants PPDU_HT_LEGACY..PPDU_UNKNOWN mirror the firmware
test_esp32_binary_parser.py:
- build_binary_frame() takes optional ppdu_byte + flags_byte (default 0)
- New TestAdr110ByteEncoding class with 5 tests:
- Pre-ADR-110 zeros decode as 'ht_legacy' + all-flags-false
- HE-SU / HE-MU / HE-TB decode correctly
- 0xFF decodes as 'unknown'
- All-flags-set round-trip (0x1D)
11/11 parser tests pass (6 existing + 5 new). Backwards compat verified.
Pairs with the Rust-side decoder in commit 3959fabf3. Both pipelines now
read the same wire format produced by the C6 firmware's
CONFIG_CSI_FRAME_HE_TAGGING path.
Ref: ruvnet/RuView#762, draft PR #764
Co-Authored-By: claude-flow <ruv@ruv.net>
The Rust port at v2/ has been the primary codebase since the rename
in #427. The Python implementation at v1/ is no longer the active
target; the only load-bearing path is the deterministic proof bundle
at v1/data/proof/ (per ADR-011 / ADR-028 witness verification).
Move the whole Python tree into archive/v1/ and document the policy
in archive/README.md: no new features, bug fixes only when they affect
a still-load-bearing path (currently just the proof), CI continues to
verify the proof on every push and PR.
Path references updated in 26 files via path-pattern sed (only
matches v1/<known-child> patterns, never bare v1 or API URLs like
/api/v1/). Two double-prefix typos (archive/archive/v1/) caught and
hand-fixed in verify-pipeline.yml and ADR-011.
Validated:
- Python proof verify.py imports cleanly at archive/v1/data/proof/
(numpy/scipy still required; CI installs requirements-lock.txt
from archive/v1/ now)
- cargo test --workspace --no-default-features → 1,539 passed,
0 failed, 8 ignored (unaffected by Python tree relocation)
- ESP32-S3 on COM7 untouched (no firmware paths changed)
After-merge: contributors should re-run any local `python v1/...`
commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already
updated).