mirror of
https://github.com/ruvnet/RuView
synced 2026-08-08 20:11:43 +00:00
feat(hardware): add Qualcomm CSI simulator and vendor roadmap (#1359)
This commit is contained in:
@@ -18,6 +18,7 @@ mod field_localize;
|
||||
mod model_format;
|
||||
mod multistatic_bridge;
|
||||
mod mediatek_csi;
|
||||
mod qualcomm_csi;
|
||||
mod realtek_radar;
|
||||
pub mod pose;
|
||||
mod rvf_container;
|
||||
@@ -1038,6 +1039,10 @@ struct AppStateInner {
|
||||
latest_mediatek_csi: Option<mediatek_csi::MediatekCsiSnapshot>,
|
||||
/// Instant of the last validated MediaTek CSI UDP frame.
|
||||
last_mediatek_frame: Option<std::time::Instant>,
|
||||
/// Latest validated Qualcomm CSI summary; raw matrices are not retained here.
|
||||
latest_qualcomm_csi: Option<qualcomm_csi::QualcommCsiSnapshot>,
|
||||
/// Instant of the last validated Qualcomm CSI UDP frame.
|
||||
last_qualcomm_frame: Option<std::time::Instant>,
|
||||
tx: broadcast::Sender<String>,
|
||||
// ADR-099 D2/D3/D4: real-time CSI introspection tap. Per-frame state +
|
||||
// a parallel broadcast topic (`/ws/introspection`) running alongside
|
||||
@@ -1223,6 +1228,13 @@ impl AppStateInner {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.source.starts_with("qualcomm") {
|
||||
if let Some(last) = self.last_qualcomm_frame {
|
||||
if last.elapsed() > ESP32_OFFLINE_TIMEOUT {
|
||||
return format!("{}:offline", self.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.source.clone()
|
||||
}
|
||||
}
|
||||
@@ -3391,6 +3403,14 @@ async fn latest_mediatek_csi(State(state): State<SharedState>) -> Json<serde_jso
|
||||
}
|
||||
}
|
||||
|
||||
async fn latest_qualcomm_csi(State(state): State<SharedState>) -> Json<serde_json::Value> {
|
||||
let s = state.read().await;
|
||||
match &s.latest_qualcomm_csi {
|
||||
Some(snapshot) => Json(serde_json::to_value(snapshot).unwrap_or_default()),
|
||||
None => Json(serde_json::json!({"status": "no Qualcomm CSI data yet"})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate WiFi-derived pose keypoints from sensing data.
|
||||
///
|
||||
/// Keypoint positions are modulated by real signal features rather than a pure
|
||||
@@ -5485,7 +5505,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
let addr = format!("0.0.0.0:{udp_port}");
|
||||
let socket = match UdpSocket::bind(&addr).await {
|
||||
Ok(s) => {
|
||||
info!("UDP listening on {addr} for ESP32 CSI, MediaTek CSI, and RTL8720F radar frames");
|
||||
info!("UDP listening on {addr} for ESP32, MediaTek, Qualcomm CSI, and RTL8720F radar frames");
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -5498,6 +5518,26 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
||||
loop {
|
||||
match socket.recv_from(&mut buf).await {
|
||||
Ok((len, src)) => {
|
||||
if len >= 4
|
||||
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
|
||||
== wifi_densepose_hardware::qualcomm_csi::QUALCOMM_CSI_MAGIC
|
||||
{
|
||||
match wifi_densepose_hardware::qualcomm_csi::CsiFrame::from_bytes(&buf[..len]) {
|
||||
Ok((frame, consumed)) if consumed == len => {
|
||||
let snapshot = qualcomm_csi::QualcommCsiSnapshot::from_frame(&frame);
|
||||
debug!("Qualcomm CSI from {src}: profile={} seq={} dimensions={}x{}x{}", snapshot.chipset, snapshot.sequence, snapshot.tx_count, snapshot.rx_count, snapshot.subcarrier_count);
|
||||
let json = serde_json::to_string(&snapshot).ok();
|
||||
let mut s = state.write().await;
|
||||
s.source = snapshot.source.to_string();
|
||||
s.last_qualcomm_frame = Some(std::time::Instant::now());
|
||||
s.latest_qualcomm_csi = Some(snapshot);
|
||||
if let Some(json) = json { let _ = s.tx.send(json); }
|
||||
}
|
||||
Ok((_, consumed)) => warn!("Qualcomm CSI datagram from {src} has trailing bytes: consumed={consumed} received={len}"),
|
||||
Err(error) => warn!("Rejected Qualcomm CSI datagram from {src}: {error}"),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if len >= 4
|
||||
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
|
||||
== wifi_densepose_hardware::mediatek_csi::MEDIATEK_CSI_MAGIC
|
||||
@@ -7638,6 +7678,8 @@ async fn main() {
|
||||
last_realtek_frame: None,
|
||||
latest_mediatek_csi: None,
|
||||
last_mediatek_frame: None,
|
||||
latest_qualcomm_csi: None,
|
||||
last_qualcomm_frame: None,
|
||||
tx,
|
||||
intro: wifi_densepose_sensing_server::introspection::IntrospectionState::new(),
|
||||
intro_tx,
|
||||
@@ -7856,6 +7898,7 @@ async fn main() {
|
||||
.route("/api/v1/sensing/latest", get(latest))
|
||||
.route("/api/v1/radar/latest", get(latest_realtek_radar))
|
||||
.route("/api/v1/csi/mediatek/latest", get(latest_mediatek_csi))
|
||||
.route("/api/v1/csi/qualcomm/latest", get(latest_qualcomm_csi))
|
||||
// Per-node health endpoint
|
||||
.route("/api/v1/nodes", get(nodes_endpoint))
|
||||
// ADR-110 iter 29 — per-node mesh sync state for HTTP clients.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Bounded summaries for ADR-269 Qualcomm MIMO CSI frames.
|
||||
|
||||
use serde::Serialize;
|
||||
use wifi_densepose_hardware::qualcomm_csi::{CsiFlags, CsiFrame, CsiPayload, ReportKind};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub(crate) struct QualcommCsiSnapshot {
|
||||
pub event_type: &'static str,
|
||||
pub source: &'static str,
|
||||
pub report_kind: &'static str,
|
||||
pub sequence: u32,
|
||||
pub timestamp_us: u64,
|
||||
pub device_id: String,
|
||||
pub chipset: &'static str,
|
||||
pub center_freq_khz: u32,
|
||||
pub bandwidth_mhz: u16,
|
||||
pub tx_count: u8,
|
||||
pub rx_count: u8,
|
||||
pub subcarrier_count: u16,
|
||||
pub element_count: usize,
|
||||
pub ppdu_type: String,
|
||||
pub rssi_dbm: Vec<i8>,
|
||||
pub noise_floor_dbm: i8,
|
||||
pub calibrated: bool,
|
||||
pub synthetic: bool,
|
||||
pub saturated: bool,
|
||||
pub time_synchronized: bool,
|
||||
pub dropped_predecessor: bool,
|
||||
pub calibration_id: u32,
|
||||
pub subcarrier_spacing_hz: f32,
|
||||
pub mean_amplitude: Option<f32>,
|
||||
pub peak_amplitude: Option<f32>,
|
||||
}
|
||||
|
||||
impl QualcommCsiSnapshot {
|
||||
pub(crate) fn from_frame(frame: &CsiFrame) -> Self {
|
||||
let synthetic = frame.flags.contains(CsiFlags::SYNTHETIC);
|
||||
let (mean_amplitude, peak_amplitude) = amplitude_summary(frame);
|
||||
Self {
|
||||
event_type: "qualcomm_csi",
|
||||
source: if synthetic {
|
||||
"qualcomm:simulated"
|
||||
} else {
|
||||
"qualcomm"
|
||||
},
|
||||
report_kind: match frame.report_kind {
|
||||
ReportKind::Csi => "csi",
|
||||
ReportKind::Capabilities => "capabilities",
|
||||
},
|
||||
sequence: frame.sequence,
|
||||
timestamp_us: frame.timestamp_us,
|
||||
device_id: format!("{:016x}", frame.device_id),
|
||||
chipset: frame.chipset.name(),
|
||||
center_freq_khz: frame.center_freq_khz,
|
||||
bandwidth_mhz: frame.bandwidth_mhz,
|
||||
tx_count: frame.tx_count,
|
||||
rx_count: frame.rx_count,
|
||||
subcarrier_count: frame.subcarrier_count,
|
||||
element_count: frame.payload.len(),
|
||||
ppdu_type: format!("{:?}", frame.ppdu_type).to_ascii_lowercase(),
|
||||
rssi_dbm: frame.payload.rssi_dbm().to_vec(),
|
||||
noise_floor_dbm: frame.noise_floor_dbm,
|
||||
calibrated: frame.flags.contains(CsiFlags::CALIBRATED),
|
||||
synthetic,
|
||||
saturated: frame.flags.contains(CsiFlags::SATURATED),
|
||||
time_synchronized: frame.flags.contains(CsiFlags::TIME_SYNCHRONIZED),
|
||||
dropped_predecessor: frame.flags.contains(CsiFlags::DROPPED_PREDECESSOR),
|
||||
calibration_id: frame.calibration_id,
|
||||
subcarrier_spacing_hz: frame.subcarrier_spacing_hz,
|
||||
mean_amplitude,
|
||||
peak_amplitude,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn amplitude_summary(frame: &CsiFrame) -> (Option<f32>, Option<f32>) {
|
||||
let amplitudes: Vec<f32> = match &frame.payload {
|
||||
CsiPayload::ComplexI16 { values, .. } => values
|
||||
.iter()
|
||||
.map(|[i, q]| (*i as f32).hypot(*q as f32) * frame.scale)
|
||||
.collect(),
|
||||
CsiPayload::ComplexF32 { values, .. } => values
|
||||
.iter()
|
||||
.map(|[i, q]| i.hypot(*q) * frame.scale)
|
||||
.collect(),
|
||||
CsiPayload::Bytes(_) => return (None, None),
|
||||
};
|
||||
if amplitudes.is_empty() {
|
||||
return (None, None);
|
||||
}
|
||||
let mean = amplitudes.iter().sum::<f32>() / amplitudes.len() as f32;
|
||||
let peak = amplitudes.into_iter().max_by(f32::total_cmp);
|
||||
(Some(mean), peak)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wifi_densepose_hardware::qualcomm_csi::simulator::{QualcommCsiSimulator, SimulatorConfig};
|
||||
|
||||
#[test]
|
||||
fn simulator_summary_preserves_dimensions_and_provenance() {
|
||||
let mut sim = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||
let snapshot = QualcommCsiSnapshot::from_frame(&sim.next_frame());
|
||||
assert_eq!(snapshot.source, "qualcomm:simulated");
|
||||
assert_eq!(
|
||||
(
|
||||
snapshot.tx_count,
|
||||
snapshot.rx_count,
|
||||
snapshot.subcarrier_count
|
||||
),
|
||||
(2, 3, 114)
|
||||
);
|
||||
assert_eq!(snapshot.element_count, 684);
|
||||
assert!(snapshot.mean_amplitude.unwrap() > 0.0);
|
||||
assert!(snapshot.peak_amplitude.unwrap() >= snapshot.mean_amplitude.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_summary_does_not_invent_signal_statistics() {
|
||||
let sim = QualcommCsiSimulator::new(SimulatorConfig::default()).unwrap();
|
||||
let snapshot = QualcommCsiSnapshot::from_frame(&sim.capabilities_frame());
|
||||
assert_eq!(snapshot.report_kind, "capabilities");
|
||||
assert_eq!(snapshot.mean_amplitude, None);
|
||||
assert!(snapshot.rssi_dbm.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user