feat(adr-106): real sensor µs timestamp (rx_ctrl.timestamp) — flashed via OTA
Closes ADR-106 open item #1: server now receives the real WiFi RX timestamp from the sensor's hardware controller instead of stamping on receipt with SystemTime. FW (csi_collector.c csi_serialize_frame): Append uint32_t = info->rx_ctrl.timestamp (µs since FW boot, monotonic per ESP-IDF docs) as 4 trailing bytes after I/Q data. Header layout unchanged → old server parsers still work (they ignore tail bytes per existing `if buf.len() >= expected` check). Server (parse_esp32_frame): Opportunistically read trailing 4 bytes as u32 LE into Esp32Frame.sensor_timestamp_us. Old FW → None, new FW → Some(µs). udp_receiver_task uses sensor timestamp when present, falls back to server SystemTime if not. Result published as NodeInfo.timestamp_us. Flashed both sensors via OTA (no USB dance): 192.168.0.101: ota_0 → ota_1 ✓ 192.168.0.100: ota_1 → ota_0 ✓ Live verify: WS timestamps now sub-1e12 (sensor monotonic, ~39s after FW boot), Δ between successive frames = 43.3 ms ≈ 23 fps sampling jitter, sub-ms precision. Cross-node skew = sensor boot time delta (here ~292 ms). For sync the host can subtract per-node boot offset learned from the first packet pair.
This commit is contained in:
parent
274984d3a9
commit
b787f40a86
|
|
@ -214,6 +214,10 @@ static esp_timer_handle_t s_hop_timer = NULL;
|
|||
* [17] Noise floor (i8)
|
||||
* [18..19] Reserved
|
||||
* [20..] I/Q data (raw bytes from ESP-IDF callback)
|
||||
* [20+iq_len .. 20+iq_len+3] ADR-106: sensor timestamp_us (u32 LE)
|
||||
* from info->rx_ctrl.timestamp. Trailing
|
||||
* 4 bytes — server parses opportunistically;
|
||||
* old server tolerant of extra bytes.
|
||||
*/
|
||||
size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf_len)
|
||||
{
|
||||
|
|
@ -225,7 +229,7 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
|
|||
uint16_t iq_len = (uint16_t)info->len;
|
||||
uint16_t n_subcarriers = iq_len / (2 * n_antennas);
|
||||
|
||||
size_t frame_size = CSI_HEADER_SIZE + iq_len;
|
||||
size_t frame_size = CSI_HEADER_SIZE + iq_len + 4 /* ADR-106 trailing timestamp_us */;
|
||||
if (frame_size > buf_len) {
|
||||
ESP_LOGW(TAG, "Buffer too small: need %u, have %u", (unsigned)frame_size, (unsigned)buf_len);
|
||||
return 0;
|
||||
|
|
@ -278,6 +282,13 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
|
|||
/* I/Q data */
|
||||
memcpy(&buf[CSI_HEADER_SIZE], info->buf, iq_len);
|
||||
|
||||
/* ADR-106: trailing sensor µs timestamp from rx_ctrl.timestamp.
|
||||
* This is monotonic µs since FW boot (per ESP-IDF docs) and lets
|
||||
* the host align frames across nodes within ~µs once the boot
|
||||
* offsets are learned. Old server ignores trailing bytes. */
|
||||
uint32_t ts_us = info->rx_ctrl.timestamp;
|
||||
memcpy(&buf[CSI_HEADER_SIZE + iq_len], &ts_us, 4);
|
||||
|
||||
return frame_size;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -903,6 +903,10 @@ struct Esp32Frame {
|
|||
noise_floor: i8,
|
||||
amplitudes: Vec<f64>,
|
||||
phases: Vec<f64>,
|
||||
/// ADR-106 trailing field — sensor µs timestamp from
|
||||
/// `info->rx_ctrl.timestamp`. Monotonic µs since FW boot.
|
||||
/// `None` for old FW that doesn't carry it.
|
||||
sensor_timestamp_us: Option<u32>,
|
||||
}
|
||||
|
||||
/// Sensing update broadcast to WebSocket clients
|
||||
|
|
@ -1696,6 +1700,7 @@ fn parse_csi_lean(buf: &[u8]) -> Option<Esp32Frame> {
|
|||
noise_floor: noise,
|
||||
amplitudes,
|
||||
phases,
|
||||
sensor_timestamp_us: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1767,6 +1772,15 @@ fn parse_esp32_frame(buf: &[u8]) -> Option<Esp32Frame> {
|
|||
noise_floor,
|
||||
amplitudes,
|
||||
phases,
|
||||
// ADR-106: trailing 4-byte sensor µs timestamp from new FW.
|
||||
// Old FW: buf has exactly `iq_start + iq_len` bytes ⇒ Option::None.
|
||||
// New FW: 4 more bytes after I/Q ⇒ parse as u32 LE.
|
||||
sensor_timestamp_us: if buf.len() >= expected_len + 4 {
|
||||
Some(u32::from_le_bytes([
|
||||
buf[expected_len], buf[expected_len + 1],
|
||||
buf[expected_len + 2], buf[expected_len + 3],
|
||||
]))
|
||||
} else { None },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2511,6 +2525,7 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
|
|||
noise_floor: -90,
|
||||
amplitudes: multi_ap_frame.amplitudes.clone(),
|
||||
phases: multi_ap_frame.phases.clone(),
|
||||
sensor_timestamp_us: None,
|
||||
};
|
||||
|
||||
// ── Step 4b: Update frame history and extract features ───────
|
||||
|
|
@ -2689,6 +2704,7 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
|
|||
noise_floor: -90,
|
||||
amplitudes: vec![signal_pct],
|
||||
phases: vec![0.0],
|
||||
sensor_timestamp_us: None,
|
||||
};
|
||||
|
||||
let mut s = state.write().await;
|
||||
|
|
@ -2846,6 +2862,7 @@ fn generate_simulated_frame(tick: u64) -> Esp32Frame {
|
|||
noise_floor: -90,
|
||||
amplitudes,
|
||||
phases,
|
||||
sensor_timestamp_us: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5247,18 +5264,19 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
|||
}
|
||||
ns.latest_noise_floor = frame.noise_floor;
|
||||
ns.latest_n_antennas = frame.n_antennas;
|
||||
// ADR-106 follow-up: server-side receive timestamp
|
||||
// in µs since UNIX epoch. Not as precise as
|
||||
// sensor-side `info->rx_ctrl.timestamp` would be,
|
||||
// but good enough for cross-node alignment within
|
||||
// ~1 ms (Mac monotonic + LAN jitter). Sensor-side
|
||||
// timestamp deferred to a future FW change that
|
||||
// extends the 0xC511_0001 header — see ADR-106
|
||||
// Open Items.
|
||||
ns.latest_timestamp_us = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_micros() as u64)
|
||||
.unwrap_or(0);
|
||||
// ADR-106: prefer sensor's `rx_ctrl.timestamp`
|
||||
// (monotonic µs since FW boot) when the new-FW
|
||||
// trailing 4 bytes are present. Falls back to
|
||||
// server SystemTime (UNIX µs) if old FW or peek
|
||||
// failed. Two distinct reference frames; the
|
||||
// serialized value is whichever was set.
|
||||
ns.latest_timestamp_us = match frame.sensor_timestamp_us {
|
||||
Some(ts) => ts as u64, // sensor monotonic µs
|
||||
None => std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_micros() as u64)
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
let sample_rate_hz = 1000.0 / 500.0_f64;
|
||||
let (features, mut classification, breathing_rate_hz, sub_variances, raw_motion) =
|
||||
|
|
|
|||
Loading…
Reference in New Issue