1#![no_std]
32#![no_main]
33
34use embassy_executor::Spawner;
35use embassy_futures::join::join;
36use embassy_sync::blocking_mutex::Mutex;
37use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
38use embassy_time::{Duration, Timer};
39use esp_csi_rs::csi::CsiPacket;
40use esp_csi_rs::logging::logging::{LogMode, init_logger};
41use esp_csi_rs::wire::{CsiPayload, PpduFormat};
42use esp_csi_rs::{
43 CsiConfig, CsiNode, CsiNodeClient, MeasurementSetup, NodeHardware, Reporting, Threshold,
44 WifiSnifferConfig, log_ln, set_csi_callback,
45};
46use esp_hal::clock::CpuClock;
47use esp_hal::timer::timg::TimerGroup;
48use esp_radio::wifi::WifiController;
49use {esp_backtrace as _, esp_println as _};
50
51extern crate alloc;
52
53const CHANNEL: u8 = 7;
55
56const REPORTING: Reporting = Reporting::HOST;
60#[allow(dead_code)]
61const EXAMPLE_THRESHOLD: Threshold = Threshold::new(6000, 500);
62
63const HE20: bool = false;
65
66const MAX_SOURCES: usize = 4;
68
69static WIFI_CONTROLLER: static_cell::StaticCell<WifiController<'static>> =
70 static_cell::StaticCell::new();
71
72esp_bootloader_esp_idf::esp_app_desc!();
73
74#[derive(Clone, Copy)]
75struct SourceTally {
76 mac: [u8; 6],
77 count: u32,
78 rssi: i32,
79 ppdu: PpduFormat,
80 mapped: usize,
82}
83
84fn ppdu_name(p: PpduFormat) -> &'static str {
85 match p {
86 PpduFormat::Dsss => "DSSS",
87 PpduFormat::NonHt => "non-HT",
88 PpduFormat::Ht => "HT",
89 PpduFormat::Vht | PpduFormat::VhtMu => "VHT",
90 PpduFormat::HeSu => "HE SU",
91 PpduFormat::HeMu | PpduFormat::HeErSu | PpduFormat::HeTb => "HE",
92 _ => "?",
93 }
94}
95
96static SOURCES: Mutex<
98 CriticalSectionRawMutex,
99 core::cell::RefCell<heapless::Vec<SourceTally, MAX_SOURCES>>,
100> = Mutex::new(core::cell::RefCell::new(heapless::Vec::new()));
101
102fn on_csi(packet: &CsiPacket) {
103 SUBCARRIERS.store(
105 packet.subcarriers() as u32,
106 core::sync::atomic::Ordering::Relaxed,
107 );
108 let ppdu = packet.meta().ppdu;
109 let mapped = match &packet.frame.payload {
110 CsiPayload::EspRaw { layout, .. } => layout.indices().count(),
111 _ => 0,
112 };
113 SOURCES.lock(|cell| {
114 let mut list = cell.borrow_mut();
115 if let Some(entry) = list.iter_mut().find(|e| e.mac == packet.mac()) {
116 entry.count += 1;
117 entry.rssi = packet.rssi() as i32;
118 entry.ppdu = ppdu;
119 entry.mapped = mapped;
120 return;
121 }
122 let _ = list.push(SourceTally {
123 mac: packet.mac(),
124 count: 1,
125 rssi: packet.rssi() as i32,
126 ppdu,
127 mapped,
128 });
129 });
130}
131
132async fn report_task() {
133 let mut previous: heapless::Vec<([u8; 6], u32), MAX_SOURCES> = heapless::Vec::new();
134 loop {
135 Timer::after_secs(1).await;
136 let snapshot = SOURCES.lock(|cell| cell.borrow().clone());
137 if snapshot.is_empty() {
138 log_ln!("No CSI yet — is anything transmitting on channel {}?", CHANNEL);
139 continue;
140 }
141 for entry in snapshot.iter() {
142 let last = previous
143 .iter()
144 .find(|(mac, _)| *mac == entry.mac)
145 .map(|(_, c)| *c)
146 .unwrap_or(0);
147 log_ln!(
148 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} {} CSI/s total {} RSSI {} subcarriers {} {} layout maps {}",
149 entry.mac[0],
150 entry.mac[1],
151 entry.mac[2],
152 entry.mac[3],
153 entry.mac[4],
154 entry.mac[5],
155 entry.count.wrapping_sub(last),
156 entry.count,
157 entry.rssi,
158 SUBCARRIERS.load(core::sync::atomic::Ordering::Relaxed),
159 ppdu_name(entry.ppdu),
160 entry.mapped,
161 );
162 }
163 previous.clear();
164 for entry in snapshot.iter() {
165 let _ = previous.push((entry.mac, entry.count));
166 }
167 }
168}
169
170static SUBCARRIERS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
173
174fn csi_config() -> CsiConfig {
176 #[cfg(any(feature = "esp32c5", feature = "esp32c6"))]
177 if HE20 {
178 return CsiConfig::he20();
179 }
180 CsiConfig::default()
181}
182
183#[esp_rtos::main]
184async fn main(spawner: Spawner) -> ! {
185 let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
186 let peripherals = esp_hal::init(config);
187 init_logger(spawner, LogMode::Text);
188
189 esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 61440);
190
191 let timg0 = TimerGroup::new(peripherals.TIMG0);
192 esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0);
193
194 let config_radio = esp_radio::wifi::ControllerConfig::default();
195 let wifi_controller = esp_radio::wifi::WifiController::new(peripherals.WIFI, config_radio)
196 .expect("Failed to initialize Wi-Fi controller");
197 let controller = WIFI_CONTROLLER.init(wifi_controller);
198
199 log_ln!("Starting sniffer (observer, receiver) on channel {}", CHANNEL);
200
201 let mut node_handle = CsiNodeClient::new();
202 let hardware = NodeHardware::new(controller);
203 let mut sniffer = WifiSnifferConfig::default().with_channel(CHANNEL);
204 match REPORTING {
205 Reporting::Report { to } => sniffer = sniffer.with_report_to(to),
206 Reporting::ThresholdBased(t) => sniffer = sniffer.with_threshold(t),
207 Reporting::Decimate(n) => sniffer = sniffer.with_decimation(n),
208 _ => {}
209 }
210 let setup = MeasurementSetup::new().acquisition(csi_config());
211 let mut node = CsiNode::sniffer(sniffer, setup, hardware);
212 node.set_protocol(if HE20 {
213 esp_radio::wifi::Protocol::AX
214 } else {
215 esp_radio::wifi::Protocol::N
216 });
217
218 set_csi_callback(on_csi);
219 let _ = &mut node_handle;
220 join(node.run(), report_task()).await;
221
222 loop {
223 log_ln!("Sniffer stopped");
224 Timer::after(Duration::from_secs(5)).await;
225 }
226}