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)
|
* [17] Noise floor (i8)
|
||||||
* [18..19] Reserved
|
* [18..19] Reserved
|
||||||
* [20..] I/Q data (raw bytes from ESP-IDF callback)
|
* [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)
|
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 iq_len = (uint16_t)info->len;
|
||||||
uint16_t n_subcarriers = iq_len / (2 * n_antennas);
|
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) {
|
if (frame_size > buf_len) {
|
||||||
ESP_LOGW(TAG, "Buffer too small: need %u, have %u", (unsigned)frame_size, (unsigned)buf_len);
|
ESP_LOGW(TAG, "Buffer too small: need %u, have %u", (unsigned)frame_size, (unsigned)buf_len);
|
||||||
return 0;
|
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 */
|
/* I/Q data */
|
||||||
memcpy(&buf[CSI_HEADER_SIZE], info->buf, iq_len);
|
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;
|
return frame_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -903,6 +903,10 @@ struct Esp32Frame {
|
||||||
noise_floor: i8,
|
noise_floor: i8,
|
||||||
amplitudes: Vec<f64>,
|
amplitudes: Vec<f64>,
|
||||||
phases: 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
|
/// Sensing update broadcast to WebSocket clients
|
||||||
|
|
@ -1696,6 +1700,7 @@ fn parse_csi_lean(buf: &[u8]) -> Option<Esp32Frame> {
|
||||||
noise_floor: noise,
|
noise_floor: noise,
|
||||||
amplitudes,
|
amplitudes,
|
||||||
phases,
|
phases,
|
||||||
|
sensor_timestamp_us: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1767,6 +1772,15 @@ fn parse_esp32_frame(buf: &[u8]) -> Option<Esp32Frame> {
|
||||||
noise_floor,
|
noise_floor,
|
||||||
amplitudes,
|
amplitudes,
|
||||||
phases,
|
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,
|
noise_floor: -90,
|
||||||
amplitudes: multi_ap_frame.amplitudes.clone(),
|
amplitudes: multi_ap_frame.amplitudes.clone(),
|
||||||
phases: multi_ap_frame.phases.clone(),
|
phases: multi_ap_frame.phases.clone(),
|
||||||
|
sensor_timestamp_us: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Step 4b: Update frame history and extract features ───────
|
// ── 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,
|
noise_floor: -90,
|
||||||
amplitudes: vec![signal_pct],
|
amplitudes: vec![signal_pct],
|
||||||
phases: vec![0.0],
|
phases: vec![0.0],
|
||||||
|
sensor_timestamp_us: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut s = state.write().await;
|
let mut s = state.write().await;
|
||||||
|
|
@ -2846,6 +2862,7 @@ fn generate_simulated_frame(tick: u64) -> Esp32Frame {
|
||||||
noise_floor: -90,
|
noise_floor: -90,
|
||||||
amplitudes,
|
amplitudes,
|
||||||
phases,
|
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_noise_floor = frame.noise_floor;
|
||||||
ns.latest_n_antennas = frame.n_antennas;
|
ns.latest_n_antennas = frame.n_antennas;
|
||||||
// ADR-106 follow-up: server-side receive timestamp
|
// ADR-106: prefer sensor's `rx_ctrl.timestamp`
|
||||||
// in µs since UNIX epoch. Not as precise as
|
// (monotonic µs since FW boot) when the new-FW
|
||||||
// sensor-side `info->rx_ctrl.timestamp` would be,
|
// trailing 4 bytes are present. Falls back to
|
||||||
// but good enough for cross-node alignment within
|
// server SystemTime (UNIX µs) if old FW or peek
|
||||||
// ~1 ms (Mac monotonic + LAN jitter). Sensor-side
|
// failed. Two distinct reference frames; the
|
||||||
// timestamp deferred to a future FW change that
|
// serialized value is whichever was set.
|
||||||
// extends the 0xC511_0001 header — see ADR-106
|
ns.latest_timestamp_us = match frame.sensor_timestamp_us {
|
||||||
// Open Items.
|
Some(ts) => ts as u64, // sensor monotonic µs
|
||||||
ns.latest_timestamp_us = std::time::SystemTime::now()
|
None => std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_micros() as u64)
|
.map(|d| d.as_micros() as u64)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
|
||||||
let sample_rate_hz = 1000.0 / 500.0_f64;
|
let sample_rate_hz = 1000.0 / 500.0_f64;
|
||||||
let (features, mut classification, breathing_rate_hz, sub_variances, raw_motion) =
|
let (features, mut classification, breathing_rate_hz, sub_variances, raw_motion) =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue