fix(hardware): aggregator tolerates sibling RuView UDP packet magics (#517)
The ESP32 firmware multiplexes several wire packet types onto the same
UDP port as ADR-018 raw CSI frames (magic 0xC5110001):
0xC5110002 ADR-039 edge vitals (32 B)
0xC5110003 ADR-069 feature vector
0xC5110004 ADR-063 fused vitals
0xC5110005 ADR-039 compressed CSI
0xC5110006 ADR-081 feature state
0xC5110007 ADR-095/#513 temporal classification
Esp32CsiParser only knew 0xC5110001, so the standalone `aggregator`
binary printed "parse error: Invalid magic: expected 0xc5110001, got
0xc5110002" for every vitals packet. No CSI data was lost — just noise.
Add the sibling-magic constants + ruview_sibling_packet_name(), classify
recognized siblings before the CSI-frame length gate, and return a new
ParseError::NonCsiPacket { magic, kind } instead of InvalidMagic. The
`aggregator` CLI now skips them quietly (logs "[skipped ADR-039 edge
vitals packet — not a CSI frame]" only with --verbose); the library-level
CsiAggregator already dropped them silently. New regression tests cover
all seven magics.
Closes #517
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
3f55c95b34
commit
4d0521ca08
|
|
@ -10,7 +10,7 @@ use std::net::UdpSocket;
|
|||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
use wifi_densepose_hardware::Esp32CsiParser;
|
||||
use wifi_densepose_hardware::{Esp32CsiParser, ParseError};
|
||||
|
||||
/// UDP aggregator for ESP32 CSI nodes (ADR-018).
|
||||
#[derive(Parser)]
|
||||
|
|
@ -65,6 +65,15 @@ fn main() {
|
|||
mean_amp,
|
||||
);
|
||||
}
|
||||
// The firmware sends several packet types on this UDP port
|
||||
// (ADR-039 vitals, ADR-081 feature state, ADR-095 temporal, …)
|
||||
// alongside ADR-018 CSI frames. Those are expected, not errors —
|
||||
// this CSI-only aggregator just skips them. (RuView#517)
|
||||
Err(ParseError::NonCsiPacket { kind, .. }) => {
|
||||
if cli.verbose {
|
||||
eprintln!(" [skipped {} packet — not a CSI frame]", kind);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if cli.verbose {
|
||||
eprintln!(" parse error: {}", e);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,18 @@ pub enum ParseError {
|
|||
got: u32,
|
||||
},
|
||||
|
||||
/// A recognized RuView wire packet was received that is *not* an
|
||||
/// ADR-018 raw CSI frame (e.g. ADR-039 vitals, ADR-081 feature state,
|
||||
/// ADR-095 temporal classification). The firmware multiplexes several
|
||||
/// packet types onto the same UDP port, so a CSI parser will see these
|
||||
/// interleaved with CSI frames — that is expected, not a corruption.
|
||||
/// Consumers should route the packet to the matching decoder or skip it.
|
||||
#[error("Non-CSI RuView packet on CSI socket: {kind} (magic {magic:#010x})")]
|
||||
NonCsiPacket {
|
||||
magic: u32,
|
||||
kind: &'static str,
|
||||
},
|
||||
|
||||
/// The frame indicates more subcarriers than physically possible.
|
||||
#[error("Invalid subcarrier count: {count} (max {max})")]
|
||||
InvalidSubcarrierCount {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,43 @@ use crate::csi_frame::{AntennaConfig, Bandwidth, CsiFrame, CsiMetadata, Subcarri
|
|||
use crate::error::ParseError;
|
||||
|
||||
/// ESP32 CSI binary frame magic number (ADR-018).
|
||||
const ESP32_CSI_MAGIC: u32 = 0xC5110001;
|
||||
pub const ESP32_CSI_MAGIC: u32 = 0xC5110001;
|
||||
|
||||
// ── Sibling RuView wire packets ──────────────────────────────────────────────
|
||||
// The ESP32 firmware multiplexes several packet types onto the same UDP port
|
||||
// as ADR-018 raw CSI frames. A CSI-only consumer will therefore see these
|
||||
// interleaved with CSI frames. They are *not* corruption — they just need a
|
||||
// different decoder (or can be skipped). See firmware `rv_feature_state.h`.
|
||||
|
||||
/// ADR-039 edge vitals packet (32 bytes: HR/BR/presence).
|
||||
pub const RUVIEW_VITALS_MAGIC: u32 = 0xC5110002;
|
||||
/// ADR-069 feature-vector packet.
|
||||
pub const RUVIEW_FEATURE_MAGIC: u32 = 0xC5110003;
|
||||
/// ADR-063 fused-vitals packet (multi-sensor fusion).
|
||||
pub const RUVIEW_FUSED_VITALS_MAGIC: u32 = 0xC5110004;
|
||||
/// ADR-039 compressed-CSI packet.
|
||||
pub const RUVIEW_COMPRESSED_CSI_MAGIC: u32 = 0xC5110005;
|
||||
/// ADR-081 compact feature-state packet (the default upstream payload).
|
||||
pub const RUVIEW_FEATURE_STATE_MAGIC: u32 = 0xC5110006;
|
||||
/// ADR-095 / #513 on-device temporal-classification packet.
|
||||
pub const RUVIEW_TEMPORAL_MAGIC: u32 = 0xC5110007;
|
||||
|
||||
/// If `magic` is a recognized RuView wire packet other than the ADR-018 raw
|
||||
/// CSI frame, return a human-readable name for it; otherwise `None`.
|
||||
///
|
||||
/// Used by CSI consumers to distinguish "a sibling packet I should route or
|
||||
/// skip" from "genuine garbage on the wire".
|
||||
pub fn ruview_sibling_packet_name(magic: u32) -> Option<&'static str> {
|
||||
match magic {
|
||||
RUVIEW_VITALS_MAGIC => Some("ADR-039 edge vitals"),
|
||||
RUVIEW_FEATURE_MAGIC => Some("ADR-069 feature vector"),
|
||||
RUVIEW_FUSED_VITALS_MAGIC => Some("ADR-063 fused vitals"),
|
||||
RUVIEW_COMPRESSED_CSI_MAGIC => Some("ADR-039 compressed CSI"),
|
||||
RUVIEW_FEATURE_STATE_MAGIC => Some("ADR-081 feature state"),
|
||||
RUVIEW_TEMPORAL_MAGIC => Some("ADR-095 temporal classification"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ADR-018 header size in bytes (before I/Q data).
|
||||
const HEADER_SIZE: usize = 20;
|
||||
|
|
@ -55,6 +91,18 @@ impl Esp32CsiParser {
|
|||
/// The buffer must contain at least the header (20 bytes) plus the I/Q data.
|
||||
/// Returns the parsed frame and the number of bytes consumed.
|
||||
pub fn parse_frame(data: &[u8]) -> Result<(CsiFrame, usize), ParseError> {
|
||||
// A recognized sibling packet (ADR-039 vitals, ADR-081 feature state, …)
|
||||
// multiplexed onto the CSI UDP port should be reported as such — not as
|
||||
// "insufficient data" or "invalid magic" — so callers can route or skip
|
||||
// it. These packets are all >= 4 bytes; classify before the CSI-frame
|
||||
// length gate. (RuView#517)
|
||||
if data.len() >= 4 {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if let Some(kind) = ruview_sibling_packet_name(magic) {
|
||||
return Err(ParseError::NonCsiPacket { magic, kind });
|
||||
}
|
||||
}
|
||||
|
||||
if data.len() < HEADER_SIZE {
|
||||
return Err(ParseError::InsufficientData {
|
||||
needed: HEADER_SIZE,
|
||||
|
|
@ -310,12 +358,50 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_invalid_magic() {
|
||||
let mut data = build_test_frame(1, 1, &[(10, 20)]);
|
||||
// Corrupt magic
|
||||
data[0] = 0xFF;
|
||||
// Corrupt magic to a value that isn't any known RuView packet.
|
||||
data[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
|
||||
let result = Esp32CsiParser::parse_frame(&data);
|
||||
assert!(matches!(result, Err(ParseError::InvalidMagic { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sibling_vitals_packet_is_not_invalid_magic() {
|
||||
// RuView#517: a 32-byte ADR-039 vitals packet (magic 0xC5110002)
|
||||
// arrives on the same UDP port as CSI frames. It must be reported as
|
||||
// a recognized sibling packet, not a corrupt CSI frame.
|
||||
let mut data = vec![0u8; 32];
|
||||
data[0..4].copy_from_slice(&RUVIEW_VITALS_MAGIC.to_le_bytes());
|
||||
match Esp32CsiParser::parse_frame(&data) {
|
||||
Err(ParseError::NonCsiPacket { magic, kind }) => {
|
||||
assert_eq!(magic, RUVIEW_VITALS_MAGIC);
|
||||
assert_eq!(kind, "ADR-039 edge vitals");
|
||||
}
|
||||
other => panic!("expected NonCsiPacket, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_sibling_magics_classified() {
|
||||
for m in [
|
||||
RUVIEW_VITALS_MAGIC,
|
||||
RUVIEW_FEATURE_MAGIC,
|
||||
RUVIEW_FUSED_VITALS_MAGIC,
|
||||
RUVIEW_COMPRESSED_CSI_MAGIC,
|
||||
RUVIEW_FEATURE_STATE_MAGIC,
|
||||
RUVIEW_TEMPORAL_MAGIC,
|
||||
] {
|
||||
assert!(ruview_sibling_packet_name(m).is_some(), "{m:#010x} unclassified");
|
||||
let mut data = vec![0u8; 24];
|
||||
data[0..4].copy_from_slice(&m.to_le_bytes());
|
||||
assert!(
|
||||
matches!(Esp32CsiParser::parse_frame(&data), Err(ParseError::NonCsiPacket { .. })),
|
||||
"{m:#010x} should parse as NonCsiPacket"
|
||||
);
|
||||
}
|
||||
// The CSI magic itself is not a "sibling".
|
||||
assert!(ruview_sibling_packet_name(ESP32_CSI_MAGIC).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_amplitude_phase_from_known_iq() {
|
||||
let pairs = vec![(100i8, 0i8), (0, 50), (30, 40)];
|
||||
|
|
|
|||
|
|
@ -49,7 +49,11 @@ pub mod radio_ops;
|
|||
|
||||
pub use csi_frame::{CsiFrame, CsiMetadata, SubcarrierData, Bandwidth, AntennaConfig};
|
||||
pub use error::ParseError;
|
||||
pub use esp32_parser::Esp32CsiParser;
|
||||
pub use esp32_parser::{
|
||||
Esp32CsiParser, ruview_sibling_packet_name, ESP32_CSI_MAGIC, RUVIEW_VITALS_MAGIC,
|
||||
RUVIEW_FEATURE_MAGIC, RUVIEW_FUSED_VITALS_MAGIC, RUVIEW_COMPRESSED_CSI_MAGIC,
|
||||
RUVIEW_FEATURE_STATE_MAGIC, RUVIEW_TEMPORAL_MAGIC,
|
||||
};
|
||||
pub use bridge::CsiData;
|
||||
pub use radio_ops::{
|
||||
RadioOps, RadioMode, CaptureProfile, RadioHealth, RadioError, MockRadio,
|
||||
|
|
|
|||
Loading…
Reference in New Issue