Files
ruvnet--RuView/v2/crates/wifi-densepose-hardware/src/bin/aggregator.rs
T
ruv 4d0521ca08 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>
2026-05-11 10:48:00 -04:00

85 lines
2.5 KiB
Rust

//! UDP aggregator CLI for receiving ESP32 CSI frames (ADR-018).
//!
//! Listens for ADR-018 binary CSI frames on a UDP socket, parses each
//! packet, and prints a one-line summary to stdout.
//!
//! Usage:
//! cargo run -p wifi-densepose-hardware --bin aggregator -- --bind 0.0.0.0:5005
use std::net::UdpSocket;
use std::process;
use clap::Parser;
use wifi_densepose_hardware::{Esp32CsiParser, ParseError};
/// UDP aggregator for ESP32 CSI nodes (ADR-018).
#[derive(Parser)]
#[command(name = "aggregator", about = "Receive and display live CSI frames from ESP32 nodes")]
struct Cli {
/// Address:port to bind the UDP listener to.
#[arg(long, default_value = "0.0.0.0:5005")]
bind: String,
/// Print raw hex dump alongside parsed output.
#[arg(long, short)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
let socket = match UdpSocket::bind(&cli.bind) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: cannot bind to {}: {}", cli.bind, e);
process::exit(1);
}
};
eprintln!("Listening on {}...", cli.bind);
let mut buf = [0u8; 2048];
loop {
let (n, src) = match socket.recv_from(&mut buf) {
Ok(r) => r,
Err(e) => {
eprintln!("recv error: {}", e);
continue;
}
};
if cli.verbose {
eprintln!(" [{} bytes from {}]", n, src);
}
match Esp32CsiParser::parse_frame(&buf[..n]) {
Ok((frame, _consumed)) => {
let mean_amp = frame.mean_amplitude();
println!(
"[node:{} seq:{}] sc={} rssi={} amp={:.1}",
frame.metadata.node_id,
frame.metadata.sequence,
frame.subcarrier_count(),
frame.metadata.rssi_dbm,
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);
}
}
}
}
}