fix(host): 256-bin HE-LTF ingest end-to-end + latent offset bugs (#1005)

Audit of every ADR-018 consumer against live C6 HE20 frames (532B/256-bin):
- sensing-server + CLI calibrate parsers read n_subcarriers from one byte
  (256 decoded as 0) with stale seq/rssi offsets (rssi always 0 — latent,
  pre-existing, confirmed vs firmware csi_collector.c). Fixed to the real
  ADR-018 layout; n_subcarriers u8->u16; byte 18 surfaced as typed PpduType.
- sensing-server probe buffer 256B -> 2048B (532B datagram errored on Windows)
- per-node grid gate: lock densest (n_subcarriers, ppdu_type) grid, re-warm
  on upgrade, skip sparser minority frames — HT-64 never mixes into an
  HE-256 baseline window
- hardware parser: HE-aware bandwidth classification (256-FFT HE20 = 20MHz,
  was Bw160); PpduType/Adr018Flags re-exported
- verbatim live frames (532B HE-SU, 148B HT) embedded as regression fixtures
- archive python parser: bandwidth heuristic mirror fix

Live-validated: calibrate --tier he20 consumed 600x 256-bin frames into an
ADR-135 He20 baseline (242 tones) skipping 94 HT frames; sensing-server
shows node 12 active with real RSSI (-40dBm). 765 tests green across the
three crates; workspace check clean; Python proof PASS.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-06-11 09:46:12 -04:00
parent d347d44030
commit 25cf88f18e
9 changed files with 463 additions and 73 deletions
+59 -24
View File
@@ -8,22 +8,24 @@
//!
//! # Wire format parsed here (option b — local parser, no cross-crate dep)
//!
//! Authoritative layout: firmware `csi_collector.c` (ADR-018 + ADR-110).
//!
//! Offset Size Field
//! ────── ──── ─────────────────────────────────────────────────────────────
//! 0 4 Magic: 0xC511_0001 (LE u32)
//! 4 1 node_id (u8)
//! 5 1 n_antennas (u8)
//! 6 1 n_subcarriers (u8)
//! 7 1 (reserved)
//! 8 2 freq_mhz (LE u16)
//! 10 4 sequence (LE u32)
//! 14 1 rssi (i8)
//! 15 1 noise_floor (i8)
//! 16 4 (reserved / padding)
//! 6 2 n_subcarriers (LE u16 — 256 for ESP32-C6 HE-SU frames, #1005)
//! 8 4 freq_mhz (LE u32)
//! 12 4 sequence (LE u32)
//! 16 1 rssi (i8)
//! 17 1 noise_floor (i8)
//! 18 1 PPDU type (ADR-110: 0=HT/legacy, 1=HE-SU, 2=HE-MU, 3=HE-TB)
//! 19 1 flags (ADR-110: bit0 bw40, bit4 time-sync valid)
//! 20 2 × n_antennas × n_subcarriers IQ pairs: i_val (i8), q_val (i8)
//!
//! This parser mirrors `parse_esp32_frame` in
//! `wifi-densepose-sensing-server/src/csi.rs` exactly (same magic, same layout).
//! `wifi-densepose-sensing-server/src/csi.rs` (same magic, same layout).
use anyhow::{bail, Result};
use clap::Args;
@@ -261,11 +263,15 @@ pub(crate) fn parse_csi_packet(buf: &[u8], tier: &str) -> Option<CsiFrame> {
let node_id = buf[4];
let n_antennas = buf[5] as usize;
let n_subcarriers = buf[6] as usize;
let freq_mhz = u16::from_le_bytes([buf[8], buf[9]]);
let _sequence = u32::from_le_bytes([buf[10], buf[11], buf[12], buf[13]]);
let rssi = buf[14] as i8;
let noise_floor = buf[15] as i8;
// u16 since ADR-110 / #1005: ESP32-C6 HE-SU frames carry 256 bins
// (the old single-byte read decoded 256 = 0x0100 LE as 0 subcarriers).
let n_subcarriers = u16::from_le_bytes([buf[6], buf[7]]) as usize;
let freq_mhz = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let freq_mhz = u16::try_from(freq_mhz).unwrap_or(0);
let _sequence = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
let rssi = buf[16] as i8;
let noise_floor = buf[17] as i8;
let _ppdu_type = buf[18]; // ADR-110; baseline tier gating is by count
let n_pairs = n_antennas * n_subcarriers;
let iq_start = 20usize;
@@ -414,24 +420,53 @@ mod tests {
assert!(parse_csi_packet(&buf, "ht20").is_none());
}
/// Build an ADR-018 frame (correct firmware layout, ADR-110 bytes 18-19).
fn build_frame(n_subcarriers: u16, ppdu: u8) -> Vec<u8> {
let mut buf = vec![0u8; 20 + n_subcarriers as usize * 2];
buf[0..4].copy_from_slice(&0xC511_0001u32.to_le_bytes());
buf[4] = 12; // node_id
buf[5] = 1; // n_antennas
buf[6..8].copy_from_slice(&n_subcarriers.to_le_bytes());
buf[8..12].copy_from_slice(&2432u32.to_le_bytes()); // freq_mhz
buf[12..16].copy_from_slice(&11610u32.to_le_bytes()); // sequence
buf[16] = (-40i8) as u8; // rssi
buf[17] = (-87i8) as u8; // noise floor
buf[18] = ppdu;
buf[19] = 0x10; // time-sync valid
for k in 0..n_subcarriers as usize {
buf[20 + k * 2] = (10 + (k % 100) as i8) as u8;
buf[20 + k * 2 + 1] = (k % 50) as u8;
}
buf
}
#[test]
fn test_parse_csi_packet_valid() {
let mut buf = vec![0u8; 24]; // 20-byte header + 2 IQ pairs (1 antenna, 2 subcarriers)
// Magic 0xC511_0001 LE
buf[0] = 0x01; buf[1] = 0x00; buf[2] = 0x11; buf[3] = 0xC5;
buf[5] = 1; // n_antennas
buf[6] = 2; // n_subcarriers
// freq_mhz = 2437 (channel 6)
buf[8] = 0x85; buf[9] = 0x09;
// IQ pairs at offset 20: (10, 20), (5, 15)
buf[20] = 10i8 as u8; buf[21] = 20i8 as u8;
buf[22] = (-5i8) as u8; buf[23] = 15i8 as u8;
let buf = build_frame(2, 0);
let frame = parse_csi_packet(&buf, "ht20");
assert!(frame.is_some());
let f = frame.unwrap();
assert_eq!(f.num_spatial_streams(), 1);
assert_eq!(f.num_subcarriers(), 2);
assert_eq!(f.metadata.rssi_dbm, -40);
assert_eq!(f.metadata.noise_floor_dbm, -87);
}
#[test]
fn test_parse_csi_packet_he_su_256_bins() {
// ESP32-C6 HE-SU frame (issue #1005): n_subcarriers = 256 = 0x0100 LE.
// The pre-#1005 single-byte read decoded this as 0 subcarriers.
let buf = build_frame(256, 1);
assert_eq!(buf.len(), 532); // matches the live wire size
let f = parse_csi_packet(&buf, "he20").expect("256-bin HE frame must parse");
assert_eq!(f.num_subcarriers(), 256);
assert_eq!(f.metadata.rssi_dbm, -40);
// A 256-bin frame is accepted by the he20 recorder (num_subcarriers
// tier total) and rejected by ht20 (52/64) — no HT/HE mixing.
let mut he = wifi_densepose_signal::CalibrationRecorder::new(tier_config("he20"));
assert!(he.record(&f).is_ok());
let mut ht = wifi_densepose_signal::CalibrationRecorder::new(tier_config("ht20"));
assert!(ht.record(&f).is_err());
}
#[test]