Skip to main content

esp_csi_rs/
lib.rs

1//! # A crate for CSI collection on ESP devices
2//! ## Overview
3//! This crate builds on the low level Espressif abstractions to enable the collection of Channel State Information (CSI) on ESP devices with ease.
4//! Currently this crate supports only the ESP `no-std` development framework.
5//!
6//! ### Choosing a device
7//! In terms of hardware, you need to make sure that the device you choose supports WiFi and CSI collection.
8//! Currently supported devices include:
9//! - ESP32
10//! - ESP32-C2
11//! - ESP32-C3
12//! - ESP32-C6
13//! - ESP32-S3
14//!
15//! In terms of project and software toolchain setup, you will need to specify the hardware you will be using. To minimize headache, it is recommended that you generate a project using `esp-generate` as explained next.
16//!
17//! ### Creating a project
18//! To use this crate you would need to create and setup a project for your ESP device then import the crate. This crate is compatible with the `no-std` ESP development framework. You should also select the corresponding device by activating it in the crate features.
19//!
20//! To create a projects it is highly recommended to refer the to instructions in [The Rust on ESP Book](https://docs.esp-rs.org/book/) before proceeding. The book explains the full esp-rs ecosystem, how to get started, and how to generate projects for both `std` and `no-std`.
21//!
22//! Espressif has developed a project generation tool, `esp-generate`, to ease this process and is recommended for new projects. As an example, you can create a `no-std` project for the ESP32-C3 device as follows:
23//!
24//! ```bash
25//! cargo install esp-generate
26//! esp-generate --chip=esp32c3 [project-name]
27//! ```
28//!
29//! ## Feature Flags
30#![doc = document_features::document_features!()]
31//! ## Using the Crate
32//!
33//! Each ESP device is represented as a node in a collection network. For each node, we need to configure its role in the network, the mode of operation, and the CSI collection behavior. The node role determines how the node participates in the network and interacts with other nodes, while the collection mode determines how the node handles CSI data.
34//!
35//! ### Node Roles
36//! 1) **Central Node**: This type of node is one that generates traffic, also can connect to one or more peripheral nodes.
37//! 2) **Peripheral Node**: This type of node does not generate traffic, also can optionally connect to one central node at most.
38//!
39//! ### Node Operation Modes
40//! The operation mode determines how the node operates in terms of Wi-Fi features and interactions with other nodes. The supported operation modes are:
41//! 1) **ESP-NOW**
42//! 2) **Wi-Fi Station** (Central only)
43//! 3) **Wi-Fi Sniffer** (Peripheral only)
44//!
45//! ### Collection Modes
46//! 1) **Collector**: A collector node collects and provides CSI data output from one or more devices.
47//! 2) **Listener**: A listener is a passive node. It only enables CSI collection and does not provide any CSI output.
48//!
49//! A collector node typically is the one that actively processes CSI data. A listener on the other hand typically keeps CSI traffic flowing but does not process CSI data.
50//!
51//! ## Collection Network Architechtures
52//! As ahown earlier, `esp-csi-rs` allows you to configure a device to one several operational modes including ESP-NOW, WiFi station, or WiFi sniffer. As such, `esp-csi-rs` supports several network setups allowing for flexibility in collecting CSI data. Some possible setups including the following:
53//!
54//! 1. ***Single Node:***  This is the simplest setup where only one ESP device (CSI Node) is needed. The node is configured to "sniff" packets in surrounding networks and collect CSI data. The WiFi Sniffer Peripheral Collector is the only configuration that supports this topology.
55//! 2. ***Point-to-Point:*** This set up uses two CSI Nodes, a central and a peripheral. One of them can be a collector and the other a listener. Alternatively, both can be collectors as well. Some configuration examples include
56//!     - **WiFi Station Central Collector <-> Access Point/Commercial Router**: In this configuration the CSI node can connect to any WiFi Access Point like an ESP AP or a commercial router. The node in turn sends traffic to the Access Point to acquire CSI data.
57//!     - **ESP-NOW Central Listener/Collector <-> ESP-NOW Peripheral Listener/Collector**: In this configuration a CSI central node connects to one other ESP-NOW peripheral node. Both ESP-NOW peripheral and central nodes can operate either as listeners or collectors.
58//! 3. ***Star:*** In this architechture a central node connects to several peripheral nodes. The central node triggers traffic and aggregates CSI sent back from peripheral nodes. Alternatively, CSI can be collected by the individual peripherals. Only the ESP-NOW operation mode supports this architechture. The ESP-NOW peripheral and central nodes can also operate either as listeners or collectors.
59//!
60//! ## Output Formats & Logging Modes
61//! `esp-csi-rs` is able to print CSI data in several formats. The output format can be configured when initializing the logger. The supported formats include:
62//! - **LogMode::ArrayList**: This prints CSI data as an array, where the array represents the CSI values for a received packet. This format is more compact and easier to read for large volumes of CSI data.
63//!
64//! Example output:
65//! ```
66//! [3916,-93,11,157,1,1815804,256,0,260,2,0,1,1,128,0,1,1,0,1,0,0,0,256,128,[...]]
67//! ```
68//! The array fields map to the [`CSIDataPacket`] struct fields in the following order:
69//!
70//! | Index | Field | Description |
71//! |-------|-------|-------------|
72//! | 0 | `sequence_number` | Sequence number of the packet that triggered the CSI capture |
73//! | 1 | `rssi` | Received Signal Strength Indicator (dBm) |
74//! | 2 | `rate` | PHY rate encoding (valid for non-HT / 802.11b/g packets) |
75//! | 3 | `noise_floor` | Noise floor of the RF module (dBm) |
76//! | 4 | `channel` | Primary channel on which the packet was received |
77//! | 5 | `timestamp` | Local timestamp when the packet was received (microseconds) |
78//! | 6 | `sig_len` | Length of the packet including Frame Check Sequence (FCS) |
79//! | 7 | `rx_state` | Reception state: `0` = no error, non-zero = error code |
80//! | 8 | `secondary_channel` | Secondary channel: `0` = none, `1` = above, `2` = below *(non-ESP32-C6 only)* |
81//! | 9 | `sgi` | Short Guard Interval: `0` = Long GI, `1` = Short GI *(non-ESP32-C6 only)* |
82//! | 10 | `antenna` | Antenna number: `0` = antenna 0, `1` = antenna 1 *(non-ESP32-C6 only)* |
83//! | 11 | `ampdu_cnt` | Number of subframes aggregated in AMPDU *(non-ESP32-C6 only)* |
84//! | 12 | `sig_mode` | Protocol: `0` = non-HT (11b/g), `1` = HT (11n), `3` = VHT (11ac) *(non-ESP32-C6 only)* |
85//! | 13 | `mcs` | Modulation Coding Scheme; for HT packets ranges from 0 (MCS0) to 76 (MCS76) *(non-ESP32-C6 only)* |
86//! | 14 | `bandwidth` | Channel bandwidth: `0` = 20 MHz, `1` = 40 MHz *(non-ESP32-C6 only)* |
87//! | 15 | `smoothing` | Channel estimate smoothing: `0` = unsmoothed, `1` = smoothing recommended *(non-ESP32-C6 only)* |
88//! | 16 | `not_sounding` | Sounding PPDU flag: `0` = sounding PPDU, `1` = not a sounding PPDU *(non-ESP32-C6 only)* |
89//! | 17 | `aggregation` | Aggregation type: `0` = MPDU, `1` = AMPDU *(non-ESP32-C6 only)* |
90//! | 18 | `stbc` | Space-Time Block Code: `0` = non-STBC, `1` = STBC *(non-ESP32-C6 only)* |
91//! | 19 | `fec_coding` | Forward Error Correction / LDPC flag; set for 11n LDPC packets *(non-ESP32-C6 only)* |
92//! | 20 | `sig_len` | Packet length including FCS (repeated) |
93//! | 21 | `csi_data_len` | Length of the raw CSI data (number of `i8` samples) |
94//! | 22 | `[csi_data]` | Inner array of raw CSI `i8` samples |
95//!
96//! - **LogMode::Text**: This output prints CSI data in a more verbose, human-readable format. This includes additional metadata and explanations alongside the raw CSI values, making it easier to understand the context of each packet's CSI data.
97//!
98//! Example output:
99//! ```rust
100//! mac: 56:6C:EB:6F:BC:3D
101//! sequence number: 426
102//! rssi: -82
103//! rate: 11
104//! noise floor: 165
105//! channel: 1
106//! timestamp: 2424915
107//! sig len: 332
108//! rx state: 0
109//! dump len: 336
110//! he sigb len: 2
111//! cur single mpdu: 0
112//! cur bb format: 1
113//! rx channel estimate info vld: 1
114//! rx channel estimate len: 128
115//! time seconds: 0
116//! channel: 1
117//! is group: 1
118//! rxend state: 0
119//! rxmatch3: 1
120//! rxmatch2: 0
121//! rxmatch1: 0
122//! rxmatch0: 0
123//! sig_len: 332
124//! data length: 128
125//! csi raw data: [0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 6, 0, -24, 10, -23, 9, -23, 8, -23, 7, -22, 6, -22, 5, -22, 6, -23, 5, -22, 6, -22, 6, -22, 7, -20, 7, -19, 9, -19, 10, -19, 12, -19, 12, -18, 14, -19, 14, -19, 16, -20, 17, -21, 18, -20, 18, -19, 18, -16, 18, -14, 19, -13, 18, 0, 0, -19, 22, -20, 22, -20, 22, -20, 21, -21, 19, -22, 18, -20, 16, -18, 16, -17, 15, -16, 15, -14, 15, -13, 13, -12, 13, -9, 13, -7, 14, -6, 14, -5, 13, -3, 12, 0, 13, 2, 12, 3, 12, 5, 12, 7, 13, 8, 13, 10, 13, 12, 14, 9, 1, -5, -4, 0, 0, 0, 0, 0, 0]
126//! ```
127//! - **LogMode::Serialized**: This mode serializes the `CSIDataPacket` structure and prints it in a serialized COBS format. This is a compact binary format that can be parsed by and serde compatible crate like [postcard](https://crates.io/crates/postcard). It is not human-readable but is efficient for logging large amounts of CSI data on the host without overwhelming the console output.
128//!
129//!
130//!
131//! ### Example for creating WiFi Station Central Collector
132//! There are more examples in the repository. The example below demonstrates how to collect CSI data with an ESP configured in WIFI Station mode.
133//!
134//! #### Step 1: Initialize Logger
135//! ```rust
136//! init_logger(spawner, LogMode::ArrayList);
137//! ```
138//! #### Step 2: Create a Hardware Instance for the CSI Node
139//! ```rust
140//! let csi_hardware = CSINodeHardware::new(&mut interfaces, controller);
141//! ```
142//! #### Step 3: Create a Station Configuration
143//! ```rust
144//! let client_config = ClientConfig::default()
145//!     .with_ssid("SSID".to_string())
146//!     .with_password("PASS".to_string())
147//!     .with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal);
148//!
149//! let station_config = WifiStationConfig {
150//!    client_config,  // Pass the config we created above
151//! };
152//! ```
153//! #### Step 3: Create a CSI Collection Node Instance with the Desired Configuration
154//! ```rust
155//! let mut node = CSINode::new(
156//!     esp_csi_rs::Node::Central(esp_csi_rs::CentralOpMode::WifiStation(station_config)),
157//!     CollectionMode::Collector,
158//!     Some(CsiConfig::default()),
159//!     Some(100),
160//!     csi_hardware,
161//! );
162//! ```
163//! #### Step 4: Create a CSI Node Client to Control the Node and Receive CSI Data
164//! ```rust
165//! let mut node_handle = CSINodeClient::new();
166//! ```
167//! #### Step 5: Create Handler for Printing for Certain Duration
168//! ```rust
169//! node.run_duration(1000, &mut node_handle).await;
170//! ```
171//!
172
173#![no_std]
174
175use portable_atomic::AtomicI64;
176
177use zerocopy::little_endian::{U32, U64};
178use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
179
180use embassy_futures::join::{join, join3};
181use embassy_futures::select::{select3, Either3};
182use embassy_sync::pubsub::{PubSubBehavior, Subscriber};
183
184use embassy_time::{with_timeout, Duration, Instant};
185use esp_radio::esp_now::WifiPhyRate;
186use esp_radio::wifi::{ClientConfig, CsiConfig, Interfaces, Protocol, WifiController};
187
188use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
189
190use embassy_sync::pubsub::PubSubChannel;
191use embassy_sync::signal::Signal;
192
193use heapless::Vec;
194extern crate alloc;
195use alloc::collections::BTreeMap;
196use serde::{Deserialize, Serialize};
197
198pub mod central;
199pub mod config;
200pub mod csi;
201pub mod logging;
202pub mod peripheral;
203pub mod time;
204
205use crate::central::esp_now::run_esp_now_central;
206use crate::central::sta::{run_sta_connect, sta_init};
207use crate::config::CsiConfig as CsiConfiguration;
208use crate::csi::{CSIDataPacket, RxCSIFmt};
209use crate::peripheral::esp_now::run_esp_now_peripheral;
210
211const PROC_CSI_CH_CAPACITY: usize = 20;
212const PROC_CSI_CH_SUBS: usize = 2;
213
214// PubSub Channels
215static CSI_PACKET: PubSubChannel<
216    CriticalSectionRawMutex,
217    CSIDataPacket,
218    PROC_CSI_CH_CAPACITY,
219    PROC_CSI_CH_SUBS,
220    2,
221> = PubSubChannel::new();
222
223static IS_COLLECTOR: AtomicBool = AtomicBool::new(false);
224static COLLECTION_MODE_CHANGED: Signal<CriticalSectionRawMutex, ()> = Signal::new();
225static CENTRAL_MAGIC_NUMBER: u32 = 0xA8912BF0;
226static PERIPHERAL_MAGIC_NUMBER: u32 = !CENTRAL_MAGIC_NUMBER;
227
228use portable_atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
229/// Global statistics counters (enabled with the `statistics` feature).
230#[cfg(feature = "statistics")]
231struct GlobalStats {
232    /// Total transmitted packets.
233    tx_count: AtomicU64,
234    /// Total received packets.
235    rx_count: AtomicU64,
236    /// Estimated number of dropped RX packets.
237    rx_drop_count: AtomicU32,
238    /// Capture start time (ticks).
239    capture_start_time: AtomicU64,
240    /// Current TX packet rate (Hz).
241    tx_rate_hz: AtomicU32,
242    /// Current RX packet rate (Hz).
243    rx_rate_hz: AtomicU32,
244    /// One-way latency (microseconds).
245    one_way_latency: AtomicI64,
246    /// Two-way latency (microseconds).
247    two_way_latency: AtomicI64,
248}
249
250#[cfg(feature = "statistics")]
251static STATS: GlobalStats = GlobalStats {
252    tx_count: AtomicU64::new(0),
253    rx_count: AtomicU64::new(0),
254    rx_drop_count: AtomicU32::new(0),
255    capture_start_time: AtomicU64::new(0),
256    tx_rate_hz: AtomicU32::new(0),
257    rx_rate_hz: AtomicU32::new(0),
258    one_way_latency: AtomicI64::new(0),
259    two_way_latency: AtomicI64::new(0),
260};
261// static GLOBAL_PACKET_RX_DROP_COUNT: AtomicU32 = AtomicU32::new(0);
262// static GLOBAL_PACKET_TX_COUNT: AtomicU64 = AtomicU64::new(0);
263// static GLOBAL_PACKET_RX_COUNT: AtomicU64 = AtomicU64::new(0);
264// static GLOBAL_CAPTURE_START_TIME: AtomicU64 = AtomicU64::new(0);
265// static TX_RATE_HZ: AtomicU32 = AtomicU32::new(0);
266// static RX_RATE_HZ: AtomicU32 = AtomicU32::new(0);
267// static TWO_WAY_LATENCY: AtomicI64 = AtomicI64::new(0);
268// static ONE_WAY_LATENCY: AtomicI64 = AtomicI64::new(0);
269
270// Signals
271static STOP_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
272
273/// Internal fucntion to change collection mode at runtime (e.g. Central can signal Peripheral to start/stop collecting CSI).
274fn set_runtime_collection_mode(is_collector: bool) {
275    IS_COLLECTOR.store(is_collector, Ordering::Relaxed);
276    COLLECTION_MODE_CHANGED.signal(());
277}
278
279async fn csi_data_collection(client: &mut CSINodeClient, duration: u64) {
280    with_timeout(Duration::from_secs(duration), async {
281        loop {
282            client.print_csi_w_metadata().await;
283        }
284    })
285    .await
286    .unwrap_err();
287    client.send_stop().await;
288}
289
290/// Configuration for ESP-NOW traffic generation.
291///
292/// Used by both Central and Peripheral nodes when operating in ESP-NOW mode.
293pub struct EspNowConfig {
294    phy_rate: WifiPhyRate,
295    channel: u8,
296}
297
298impl Default for EspNowConfig {
299    fn default() -> Self {
300        Self {
301            phy_rate: WifiPhyRate::RateMcs0Lgi,
302            channel: 11,
303        }
304    }
305}
306
307/// Configuration for Wi-Fi Promiscuous Sniffer mode.
308#[derive(Debug, Clone)]
309pub struct WifiSnifferConfig {
310    mac_filter: Option<[u8; 6]>,
311}
312
313impl Default for WifiSnifferConfig {
314    fn default() -> Self {
315        Self { mac_filter: None }
316    }
317}
318
319/// Configuration for Wi-Fi Station mode.
320#[derive(Debug, Clone)]
321pub struct WifiStationConfig {
322    pub client_config: ClientConfig,
323}
324
325// Enum for Central modes, each wrapping its specific config.
326
327/// Central node operational modes.
328pub enum CentralOpMode {
329    EspNow(EspNowConfig),
330    WifiStation(WifiStationConfig),
331}
332
333// Enum for Peripheral modes, each wrapping its specific config.
334/// Peripheral node operational modes.
335pub enum PeripheralOpMode {
336    EspNow(EspNowConfig),
337    WifiSniffer(WifiSnifferConfig),
338}
339
340/// High-level node type and mode.
341pub enum Node {
342    Peripheral(PeripheralOpMode), // Mode is implicit (only EspNow), directly holds config.
343    Central(CentralOpMode),       // Uses the sub-enum for mode selection.
344}
345
346/// CSI collection behavior for the node.
347///
348/// Use `Listener` to keep CSI traffic flowing without processing packets,
349/// or `Collector` to actively process CSI data. Note: `Listener` combined with
350/// a sniffer node makes the sniffer effectively useless because no CSI data is
351/// processed.
352#[derive(PartialEq, Eq, Clone, Copy)]
353pub enum CollectionMode {
354    /// Enables CSI collection and processes CSI data.
355    Collector,
356    /// Enables CSI collection but does not process CSI data.
357    Listener,
358}
359
360/// Hardware handles required to operate a CSI node.
361pub struct CSINodeHardware<'a> {
362    interfaces: &'a mut Interfaces<'static>,
363    controller: &'a mut WifiController<'static>,
364}
365
366impl<'a> CSINodeHardware<'a> {
367    /// Create a hardware bundle from the Wi-Fi `Interfaces` and `WifiController`.
368    pub fn new(
369        interfaces: &'a mut Interfaces<'static>,
370        controller: &'a mut WifiController<'static>,
371    ) -> Self {
372        Self {
373            interfaces,
374            controller,
375        }
376    }
377}
378
379type CSIRxSubscriber = Subscriber<
380    'static,
381    CriticalSectionRawMutex,
382    CSIDataPacket,
383    PROC_CSI_CH_CAPACITY,
384    PROC_CSI_CH_SUBS,
385    2,
386>;
387
388/// Client helper to receive CSI packets via a pub/sub channel.
389pub struct CSINodeClient {
390    csi_subscriber: CSIRxSubscriber,
391}
392
393impl CSINodeClient {
394    /// Create a new CSI subscriber.
395    pub fn new() -> Self {
396        Self {
397            csi_subscriber: CSI_PACKET.subscriber().unwrap(),
398        }
399    }
400
401    /// Wait for the next CSI packet.
402    pub async fn get_csi_data(&mut self) -> CSIDataPacket {
403        self.csi_subscriber.next_message_pure().await
404    }
405
406    /// Receive and print CSI data with metadata (uses crate logging).
407    pub async fn print_csi_w_metadata(&mut self) {
408        let packet = self.get_csi_data().await;
409        packet.print_csi_w_metadata();
410    }
411
412    /// Signal the running node to stop.
413    pub async fn send_stop(&self) {
414        STOP_SIGNAL.signal(());
415    }
416}
417
418/// Control packet sent from Central to Peripheral.
419#[derive(Serialize, Deserialize, Debug, PartialEq)]
420pub struct ControlPacket {
421    magic_number: u32,
422    pub is_collector: bool,
423    pub central_send_uptime: u64,
424    pub latency_offset: i64,
425}
426
427impl ControlPacket {
428    /// Create a new control packet with the provided collector flag and latency offset.
429    pub fn new(is_collector: bool, latency_offset: i64) -> Self {
430        Self {
431            magic_number: CENTRAL_MAGIC_NUMBER.into(),
432            is_collector,
433            central_send_uptime: Instant::now().as_micros(),
434            latency_offset,
435        }
436    }
437}
438
439/// Peripheral reply packet for latency/telemetry exchange.
440#[derive(Serialize, Deserialize, Debug, PartialEq)]
441pub struct PeripheralPacket {
442    magic_number: u32,        // Magic number to identify packet type
443    recv_uptime: u64,         // When Peripheral received the Control Packet
444    send_uptime: u64, // When Peripheral sent the Peripheral Packet (after receiving Control Packet)
445    central_send_uptime: u64, // When Central sent the Control Packet
446}
447
448impl PeripheralPacket {
449    /// Create a new peripheral packet using timestamps captured locally.
450    pub fn new(recv_uptime: u64, central_send_uptime: u64) -> Self {
451        Self {
452            magic_number: PERIPHERAL_MAGIC_NUMBER,
453            recv_uptime,
454            send_uptime: Instant::now().as_micros(),
455            central_send_uptime,
456        }
457    }
458}
459
460fn reset_globals() {
461    #[cfg(feature = "statistics")]
462    {
463        STATS.tx_count.store(0, Ordering::Relaxed);
464        STATS.rx_drop_count.store(0, Ordering::Relaxed);
465        STATS.tx_count.store(0, Ordering::Relaxed);
466        STATS.tx_rate_hz.store(0, Ordering::Relaxed);
467        STATS.rx_rate_hz.store(0, Ordering::Relaxed);
468        STATS.one_way_latency.store(0, Ordering::Relaxed);
469        STATS.two_way_latency.store(0, Ordering::Relaxed);
470    }
471    #[cfg(feature = "statistics")]
472    reset_global_log_drops();
473}
474
475/// Primary orchestration object for CSI collection.
476///
477/// Construct a node with `CSINode::new` or `CSINode::new_central_node`, configure
478/// optional protocol/rate/traffic frequency, then call `run()`.
479pub struct CSINode<'a> {
480    kind: Node,
481    collection_mode: CollectionMode,
482    /// CSI Configuration
483    csi_config: Option<CsiConfiguration>,
484    /// Traffic Generation Frequency
485    traffic_freq_hz: Option<u16>,
486    hardware: CSINodeHardware<'a>,
487    protocol: Option<Protocol>,
488    rate: Option<WifiPhyRate>,
489}
490
491impl<'a> CSINode<'a> {
492    /// Create a new node with explicit `Node` kind.
493    pub fn new(
494        kind: Node,
495        collection_mode: CollectionMode,
496        csi_config: Option<CsiConfiguration>,
497        traffic_freq_hz: Option<u16>,
498        hardware: CSINodeHardware<'a>,
499    ) -> Self {
500        Self {
501            kind,
502            collection_mode,
503            csi_config,
504            traffic_freq_hz,
505            hardware,
506            protocol: None,
507            rate: Some(WifiPhyRate::RateMcs0Lgi),
508        }
509    }
510
511    /// Convenience constructor for a central node.
512    pub fn new_central_node(
513        op_mode: CentralOpMode,
514        collection_mode: CollectionMode,
515        csi_config: Option<CsiConfiguration>,
516        traffic_freq_hz: Option<u16>,
517        hardware: CSINodeHardware<'a>,
518    ) -> Self {
519        Self {
520            kind: Node::Central(op_mode),
521            collection_mode,
522            csi_config,
523            traffic_freq_hz,
524            hardware,
525            protocol: None,
526            rate: Some(WifiPhyRate::RateMcs0Lgi),
527        }
528    }
529
530    /// Get the node type and operation mode.
531    pub fn get_node_type(&self) -> &Node {
532        &self.kind
533    }
534
535    /// Get the current collection mode.
536    pub fn get_collection_mode(&self) -> CollectionMode {
537        self.collection_mode
538    }
539
540    /// If central, return the active central op mode.
541    pub fn get_central_op_mode(&self) -> Option<&CentralOpMode> {
542        match &self.kind {
543            Node::Central(mode) => Some(mode),
544            Node::Peripheral(_) => None,
545        }
546    }
547
548    /// If peripheral, return the active peripheral op mode.
549    pub fn get_peripheral_op_mode(&self) -> Option<&PeripheralOpMode> {
550        match &self.kind {
551            Node::Peripheral(mode) => Some(mode),
552            Node::Central(_) => None,
553        }
554    }
555
556    /// Update CSI configuration.
557    pub fn set_csi_config(&mut self, config: CsiConfiguration) {
558        self.csi_config = Some(config);
559    }
560
561    /// Update Wi-Fi Station configuration (only applies to central station mode).
562    pub fn set_station_config(&mut self, config: WifiStationConfig) {
563        if let Node::Central(CentralOpMode::WifiStation(_)) = &mut self.kind {
564            self.kind = Node::Central(CentralOpMode::WifiStation(config));
565        }
566    }
567
568    /// Set traffic generation frequency in Hz (ESP-NOW modes).
569    pub fn set_traffic_frequency(&mut self, freq_hz: u16) {
570        self.traffic_freq_hz = Some(freq_hz);
571    }
572
573    /// Set collection mode for the node.
574    pub fn set_collection_mode(&mut self, mode: CollectionMode) {
575        self.collection_mode = mode;
576    }
577
578    /// Replace the node kind/mode.
579    pub fn set_op_mode(&mut self, mode: Node) {
580        self.kind = mode;
581    }
582
583    /// Set Wi-Fi protocol (overrides default).
584    pub fn set_protocol(&mut self, protocol: Protocol) {
585        self.protocol = Some(protocol);
586    }
587
588    /// Set Wi-Fi PHY data rate for ESP-NOW traffic.
589    pub fn set_rate(&mut self, rate: WifiPhyRate) {
590        self.rate = Some(rate);
591    }
592
593    /// Run the node until duration in seconds with internal collection.
594    ///
595    /// This initializes Wi-Fi, configures CSI, and starts mode-specific tasks.
596    pub async fn run_duration(&mut self, duration: u64, mut client: &mut CSINodeClient) {
597        let interfaces = &mut self.hardware.interfaces;
598        let controller = &mut self.hardware.controller;
599
600        // Tasks Necessary for Central Station & Sniffer
601        let sta_interface = if let Node::Central(CentralOpMode::WifiStation(config)) = &self.kind {
602            Some(sta_init(&mut interfaces.sta, config, controller))
603        } else {
604            None
605        };
606
607        // Set Wi-Fi mode to Station for all node types
608        controller.set_mode(esp_radio::wifi::WifiMode::Sta).unwrap();
609
610        // Build CSI Configuration
611        let config = match self.csi_config {
612            Some(ref config) => {
613                log_ln!("CSI Configuration Set: {:?}", config);
614                build_csi_config(config)
615            }
616            None => {
617                let default_config = CsiConfiguration::default();
618                log_ln!(
619                    "No CSI Configuration Provided. Going with defaults: {:?}",
620                    default_config
621                );
622                build_csi_config(&default_config)
623            }
624        };
625
626        // Apply Protocol if specified
627        if let Some(protocol) = self.protocol.take() {
628            let old_protocol = reconstruct_protocol(&protocol);
629            controller.set_protocol(protocol.into()).unwrap();
630            self.protocol = Some(old_protocol);
631        }
632
633        // Start the controller
634        controller.start_async().await.unwrap();
635        log_ln!("Wi-Fi Controller Started");
636        let is_collector = self.collection_mode == CollectionMode::Collector;
637        IS_COLLECTOR.store(is_collector, Ordering::Relaxed);
638
639        // Set Peripheral/Central to Collect CSI
640        set_csi(controller, config);
641        let sniffer: &esp_radio::wifi::Sniffer<'_> = &interfaces.sniffer;
642
643        // Initialize Nodes based on type
644        match &self.kind {
645            Node::Peripheral(op_mode) => match op_mode {
646                PeripheralOpMode::EspNow(esp_now_config) => {
647                    // Initialize as Peripheral node with EspNowConfig
648                    if let Some(rate) = self.rate.take() {
649                        let old_rate = reconstruct_wifi_rate(&rate);
650                        let _ = interfaces.esp_now.set_rate(rate);
651                        self.rate = Some(old_rate);
652                    }
653
654                    let main_task = run_esp_now_peripheral(
655                        &mut interfaces.esp_now,
656                        esp_now_config,
657                        self.traffic_freq_hz,
658                    );
659                    join3(
660                        main_task,
661                        run_process_csi_packet(),
662                        csi_data_collection(client, duration),
663                    )
664                    .await;
665                }
666                PeripheralOpMode::WifiSniffer(sniffer_config) => {
667                    let sniffer = &interfaces.sniffer;
668                    sniffer.set_promiscuous_mode(true).unwrap();
669                    join(
670                        run_process_csi_packet(),
671                        csi_data_collection(client, duration),
672                    )
673                    .await;
674                    run_process_csi_packet().await;
675                    sniffer.set_promiscuous_mode(false).unwrap();
676                }
677            },
678            Node::Central(op_mode) => match op_mode {
679                CentralOpMode::EspNow(esp_now_config) => {
680                    // Initialize as Central node with EspNowConfig
681                    if let Some(rate) = self.rate.take() {
682                        let old_rate = reconstruct_wifi_rate(&rate);
683                        let _ = interfaces.esp_now.set_rate(rate);
684                        self.rate = Some(old_rate);
685                    }
686
687                    let main_task = run_esp_now_central(
688                        &mut interfaces.esp_now,
689                        interfaces.sta.mac_address(),
690                        esp_now_config,
691                        self.traffic_freq_hz,
692                        is_collector,
693                    );
694                    join3(
695                        main_task,
696                        run_process_csi_packet(),
697                        csi_data_collection(client, duration),
698                    )
699                    .await;
700                }
701                CentralOpMode::WifiStation(sta_config) => {
702                    // Initialize as Wifi Station Collector with WifiStationConfig
703                    // 1. Connect to Wi-Fi network, etc.
704                    // 2. Run DHCP, NTP sync if enabled in config, etc.
705                    // 3. Spawn STA Connection Handling Task
706                    // 4. Spawn STA Network Operation Task
707                    let (sta_stack, sta_runner) = sta_interface.unwrap();
708
709                    let main_task =
710                        run_sta_connect(controller, self.traffic_freq_hz, sta_stack, sta_runner);
711                    join3(
712                        main_task,
713                        run_process_csi_packet(),
714                        csi_data_collection(client, duration),
715                    )
716                    .await;
717                }
718            },
719        }
720
721        STOP_SIGNAL.reset();
722        let _ = controller.stop_async().await;
723        reset_globals();
724    }
725
726    /// Run the node until stopped.
727    ///
728    /// This initializes Wi-Fi, configures CSI, and starts mode-specific tasks.
729    pub async fn run(&mut self) {
730        let interfaces = &mut self.hardware.interfaces;
731        let controller = &mut self.hardware.controller;
732
733        // Tasks Necessary for Central Station & Sniffer
734        let sta_interface = if let Node::Central(CentralOpMode::WifiStation(config)) = &self.kind {
735            Some(sta_init(&mut interfaces.sta, config, controller))
736        } else {
737            None
738        };
739
740        // Set Wi-Fi mode to Station for all node types
741        controller.set_mode(esp_radio::wifi::WifiMode::Sta).unwrap();
742
743        // Build CSI Configuration
744        let config = match self.csi_config {
745            Some(ref config) => {
746                log_ln!("CSI Configuration Set: {:?}", config);
747                build_csi_config(config)
748            }
749            None => {
750                let default_config = CsiConfiguration::default();
751                log_ln!(
752                    "No CSI Configuration Provided. Going with defaults: {:?}",
753                    default_config
754                );
755                build_csi_config(&default_config)
756            }
757        };
758
759        // Apply Protocol if specified
760        if let Some(protocol) = self.protocol.take() {
761            let old_protocol = reconstruct_protocol(&protocol);
762            controller.set_protocol(protocol.into()).unwrap();
763            self.protocol = Some(old_protocol);
764        }
765
766        // Start the controller
767        controller.start_async().await.unwrap();
768        log_ln!("Wi-Fi Controller Started");
769        let is_collector = self.collection_mode == CollectionMode::Collector;
770        IS_COLLECTOR.store(is_collector, Ordering::Relaxed);
771
772        // Set Peripheral/Central to Collect CSI
773        set_csi(controller, config);
774        let sniffer: &esp_radio::wifi::Sniffer<'_> = &interfaces.sniffer;
775
776        // Initialize Nodes based on type
777        match &self.kind {
778            Node::Peripheral(op_mode) => match op_mode {
779                PeripheralOpMode::EspNow(esp_now_config) => {
780                    // Initialize as Peripheral node with EspNowConfig
781                    if let Some(rate) = self.rate.take() {
782                        let old_rate = reconstruct_wifi_rate(&rate);
783                        let _ = interfaces.esp_now.set_rate(rate);
784                        self.rate = Some(old_rate);
785                    }
786
787                    let main_task = run_esp_now_peripheral(
788                        &mut interfaces.esp_now,
789                        esp_now_config,
790                        self.traffic_freq_hz,
791                    );
792                    join(main_task, run_process_csi_packet()).await;
793                }
794                PeripheralOpMode::WifiSniffer(sniffer_config) => {
795                    let sniffer = &interfaces.sniffer;
796                    sniffer.set_promiscuous_mode(true).unwrap();
797                    run_process_csi_packet().await;
798                    sniffer.set_promiscuous_mode(false).unwrap();
799                }
800            },
801            Node::Central(op_mode) => match op_mode {
802                CentralOpMode::EspNow(esp_now_config) => {
803                    // Initialize as Central node with EspNowConfig
804                    if let Some(rate) = self.rate.take() {
805                        let old_rate = reconstruct_wifi_rate(&rate);
806                        let _ = interfaces.esp_now.set_rate(rate);
807                        self.rate = Some(old_rate);
808                    }
809
810                    let main_task = run_esp_now_central(
811                        &mut interfaces.esp_now,
812                        interfaces.sta.mac_address(),
813                        esp_now_config,
814                        self.traffic_freq_hz,
815                        is_collector,
816                    );
817                    join(main_task, run_process_csi_packet()).await;
818                }
819                CentralOpMode::WifiStation(sta_config) => {
820                    // Initialize as Wifi Station Collector with WifiStationConfig
821                    // 1. Connect to Wi-Fi network, etc.
822                    // 2. Run DHCP, NTP sync if enabled in config, etc.
823                    // 3. Spawn STA Connection Handling Task
824                    // 4. Spawn STA Network Operation Task
825                    let (sta_stack, sta_runner) = sta_interface.unwrap();
826
827                    let main_task =
828                        run_sta_connect(controller, self.traffic_freq_hz, sta_stack, sta_runner);
829                    join(main_task, run_process_csi_packet()).await;
830                }
831            },
832        }
833
834        STOP_SIGNAL.reset();
835        let _ = controller.stop_async().await;
836        reset_globals();
837    }
838}
839
840#[cfg(feature = "esp32c6")]
841fn build_csi_config(csi_config: &CsiConfiguration) -> CsiConfig {
842    CsiConfig {
843        enable: csi_config.enable,
844        acquire_csi_legacy: csi_config.acquire_csi_legacy,
845        acquire_csi_ht20: csi_config.acquire_csi_ht20,
846        acquire_csi_ht40: csi_config.acquire_csi_ht40,
847        acquire_csi_su: csi_config.acquire_csi_su,
848        acquire_csi_mu: csi_config.acquire_csi_mu,
849        acquire_csi_dcm: csi_config.acquire_csi_dcm,
850        acquire_csi_beamformed: csi_config.acquire_csi_beamformed,
851        acquire_csi_he_stbc: csi_config.acquire_csi_he_stbc,
852        val_scale_cfg: csi_config.val_scale_cfg,
853        dump_ack_en: csi_config.dump_ack_en,
854        reserved: csi_config.reserved,
855    }
856}
857
858#[cfg(not(feature = "esp32c6"))]
859fn build_csi_config(csi_config: &CsiConfiguration) -> CsiConfig {
860    CsiConfig {
861        lltf_en: csi_config.lltf_en,
862        htltf_en: csi_config.htltf_en,
863        stbc_htltf2_en: csi_config.stbc_htltf2_en,
864        ltf_merge_en: csi_config.ltf_merge_en,
865        channel_filter_en: csi_config.channel_filter_en,
866        manu_scale: csi_config.manu_scale,
867        shift: csi_config.shift,
868        dump_ack_en: csi_config.dump_ack_en,
869    }
870}
871
872/// Total received CSI packets (statistics feature).
873#[cfg(feature = "statistics")]
874pub fn get_total_rx_packets() -> u64 {
875    STATS.rx_count.load(Ordering::Relaxed)
876}
877
878/// Total transmitted packets (statistics feature).
879#[cfg(feature = "statistics")]
880pub fn get_total_tx_packets() -> u64 {
881    STATS.tx_count.load(Ordering::Relaxed)
882}
883
884/// Current RX packet rate in Hz (statistics feature).
885#[cfg(feature = "statistics")]
886pub fn get_rx_rate_hz() -> u32 {
887    STATS.rx_rate_hz.load(Ordering::Relaxed)
888}
889
890/// Current TX packet rate in Hz (statistics feature).
891#[cfg(feature = "statistics")]
892pub fn get_tx_rate_hz() -> u32 {
893    STATS.tx_rate_hz.load(Ordering::Relaxed)
894}
895
896/// Packets per second received since capture start (statistics feature).
897#[cfg(feature = "statistics")]
898pub fn get_pps_rx() -> u64 {
899    let start_time = Instant::from_ticks(STATS.capture_start_time.load(Ordering::Relaxed));
900    let elapsed_secs = start_time.elapsed().as_secs() as u64;
901    let total_packets = STATS.rx_count.load(Ordering::Relaxed);
902    if elapsed_secs == 0 {
903        return total_packets;
904    }
905    total_packets / elapsed_secs
906}
907
908/// Packets per second transmitted since capture start (statistics feature).
909#[cfg(feature = "statistics")]
910pub fn get_pps_tx() -> u64 {
911    let start_time = Instant::from_ticks(STATS.capture_start_time.load(Ordering::Relaxed));
912    let elapsed_secs = start_time.elapsed().as_secs() as u64;
913    let total_packets = STATS.tx_count.load(Ordering::Relaxed);
914    if elapsed_secs == 0 {
915        return total_packets;
916    }
917    total_packets / elapsed_secs
918}
919
920/// Dropped RX packets estimate (statistics feature).
921#[cfg(feature = "statistics")]
922pub fn get_dropped_packets_rx() -> u32 {
923    STATS.rx_drop_count.load(Ordering::Relaxed)
924}
925
926/// One-way latency (statistics feature).
927#[cfg(feature = "statistics")]
928pub fn get_one_way_latency() -> i64 {
929    STATS.one_way_latency.load(Ordering::Relaxed)
930}
931
932/// Two-way latency (statistics feature).
933#[cfg(feature = "statistics")]
934pub fn get_two_way_latency() -> i64 {
935    STATS.two_way_latency.load(Ordering::Relaxed)
936}
937
938/// Sets CSI Configuration.
939fn set_csi(controller: &mut WifiController, config: CsiConfig) {
940    // Set CSI Configuration with callback
941    controller
942        .set_csi(config, |info: esp_radio::wifi::wifi_csi_info_t| {
943            capture_csi_info(info);
944        })
945        .unwrap();
946}
947
948// Function to capture CSI info from callback and publish to channel
949fn capture_csi_info(info: esp_radio::wifi::wifi_csi_info_t) {
950    if IS_COLLECTOR.load(Ordering::Relaxed) == false {
951        return;
952    }
953
954    let rssi = if info.rx_ctrl.rssi() > 127 {
955        info.rx_ctrl.rssi() - 256
956    } else {
957        info.rx_ctrl.rssi()
958    };
959
960    let mut csi_data = Vec::<i8, 612>::new();
961    // let csi_buf = info.buf;
962    let csi_buf_len = info.len;
963    let csi_slice =
964        unsafe { core::slice::from_raw_parts(info.buf as *const i8, csi_buf_len as usize) };
965    match csi_data.extend_from_slice(csi_slice) {
966        Ok(_) => {}
967        Err(_) => {
968            #[cfg(feature = "statistics")]
969            STATS.rx_drop_count.fetch_add(1, Ordering::Relaxed);
970            return;
971        }
972    }
973
974    #[cfg(not(feature = "esp32c6"))]
975    let csi_packet = CSIDataPacket {
976        sequence_number: info.rx_seq,
977        data_format: RxCSIFmt::Undefined,
978        date_time: None,
979        mac: [
980            info.mac[0],
981            info.mac[1],
982            info.mac[2],
983            info.mac[3],
984            info.mac[4],
985            info.mac[5],
986        ],
987        rssi,
988        bandwidth: info.rx_ctrl.cwb(),
989        antenna: info.rx_ctrl.ant(),
990        rate: info.rx_ctrl.rate(),
991        sig_mode: info.rx_ctrl.sig_mode(),
992        mcs: info.rx_ctrl.mcs(),
993        smoothing: info.rx_ctrl.smoothing(),
994        not_sounding: info.rx_ctrl.not_sounding(),
995        aggregation: info.rx_ctrl.aggregation(),
996        stbc: info.rx_ctrl.stbc(),
997        fec_coding: info.rx_ctrl.fec_coding(),
998        sgi: info.rx_ctrl.sgi(),
999        noise_floor: info.rx_ctrl.noise_floor(),
1000        ampdu_cnt: info.rx_ctrl.ampdu_cnt(),
1001        channel: info.rx_ctrl.channel(),
1002        secondary_channel: info.rx_ctrl.secondary_channel(),
1003        timestamp: info.rx_ctrl.timestamp(),
1004        rx_state: info.rx_ctrl.rx_state(),
1005        sig_len: info.rx_ctrl.sig_len(),
1006        csi_data_len: csi_buf_len,
1007        csi_data: csi_data,
1008    };
1009
1010    #[cfg(feature = "esp32c6")]
1011    let csi_packet = CSIDataPacket {
1012        mac: [
1013            info.mac[0],
1014            info.mac[1],
1015            info.mac[2],
1016            info.mac[3],
1017            info.mac[4],
1018            info.mac[5],
1019        ],
1020        rssi,
1021        timestamp: info.rx_ctrl.timestamp(),
1022        rate: info.rx_ctrl.rate(),
1023        noise_floor: info.rx_ctrl.noise_floor(),
1024        sig_len: info.rx_ctrl.sig_len(),
1025        rx_state: info.rx_ctrl.rx_state(),
1026        dump_len: info.rx_ctrl.dump_len(),
1027        he_sigb_len: info.rx_ctrl.he_sigb_len(),
1028        cur_single_mpdu: info.rx_ctrl.cur_single_mpdu(),
1029        cur_bb_format: info.rx_ctrl.cur_bb_format(),
1030        rx_channel_estimate_info_vld: info.rx_ctrl.rx_channel_estimate_info_vld(),
1031        rx_channel_estimate_len: info.rx_ctrl.rx_channel_estimate_len(),
1032        second: info.rx_ctrl.second(),
1033        channel: info.rx_ctrl.channel(),
1034        is_group: info.rx_ctrl.is_group(),
1035        rxend_state: info.rx_ctrl.rxend_state(),
1036        rxmatch3: info.rx_ctrl.rxmatch3(),
1037        rxmatch2: info.rx_ctrl.rxmatch2(),
1038        rxmatch1: info.rx_ctrl.rxmatch1(),
1039        rxmatch0: info.rx_ctrl.rxmatch0(),
1040        date_time: None,
1041        sequence_number: info.rx_seq,
1042        data_format: RxCSIFmt::Undefined,
1043        csi_data_len: info.len as u16,
1044        csi_data: csi_data,
1045    };
1046
1047    CSI_PACKET.publish_immediate(csi_packet);
1048    #[cfg(feature = "statistics")]
1049    STATS.rx_count.fetch_add(1, Ordering::Relaxed);
1050}
1051
1052/// Internal task that processes CSI packets from the pub/sub channel.
1053pub async fn run_process_csi_packet() {
1054    // Initialize CSI process start time
1055    #[cfg(feature = "statistics")]
1056    STATS
1057        .capture_start_time
1058        .store(Instant::now().as_ticks(), Ordering::Relaxed);
1059    // Subscribe to CSI packet capture updates
1060    let mut csi_packet_sub = CSI_PACKET.subscriber().unwrap();
1061    // Map to track sequence numbers per MAC address
1062    let mut peer_tracker: BTreeMap<[u8; 6], u16> = BTreeMap::new();
1063    let mut is_collector = IS_COLLECTOR.load(Ordering::Relaxed);
1064
1065    loop {
1066        match select3(
1067            STOP_SIGNAL.wait(),
1068            COLLECTION_MODE_CHANGED.wait(),
1069            csi_packet_sub.next_message_pure(),
1070        )
1071        .await
1072        {
1073            Either3::First(_) => {
1074                STOP_SIGNAL.signal(());
1075                break;
1076            }
1077            Either3::Second(_) => {
1078                COLLECTION_MODE_CHANGED.reset();
1079                is_collector = IS_COLLECTOR.load(Ordering::Relaxed);
1080                reset_globals();
1081                #[cfg(feature = "statistics")]
1082                STATS
1083                    .capture_start_time
1084                    .store(Instant::now().as_ticks(), Ordering::Relaxed);
1085            }
1086            Either3::Third(csi_packet) => {
1087                #[cfg(feature = "statistics")]
1088                {
1089                    if is_collector {
1090                        let current_seq = csi_packet.sequence_number;
1091
1092                        // Check if we have seen this MAC before
1093                        if let Some(&last_seq) = peer_tracker.get(&csi_packet.mac) {
1094                            // Station Mode / Hardware Sequence Number Fix:
1095                            // WiFi hardware sequence numbers (802.11) are 12-bit (0-4095).
1096                            // We use '& 0x0FFF' to handle the wraparound from 4095 -> 0 correctly.
1097                            let diff = (current_seq.wrapping_sub(last_seq)) & 0x0FFF;
1098
1099                            if diff > 1 {
1100                                let lost = (diff - 1) as u32;
1101
1102                                // Sanity check for huge gaps (e.g. router reset)
1103                                if lost < 500 {
1104                                    STATS.rx_drop_count.fetch_add(lost, Ordering::Relaxed);
1105                                }
1106                            }
1107                        }
1108
1109                        // Update tracker with new sequence
1110                        peer_tracker.insert(csi_packet.mac, current_seq);
1111                        // --- DROP DETECTION LOGIC END ---
1112                    }
1113                }
1114            }
1115        }
1116    }
1117}
1118
1119#[cfg(feature = "statistics")]
1120use crate::logging::logging::{get_log_packet_drops, reset_global_log_drops};
1121
1122fn reconstruct_wifi_rate(rate: &WifiPhyRate) -> WifiPhyRate {
1123    match rate {
1124        WifiPhyRate::Rate1mL => WifiPhyRate::Rate1mL,
1125        WifiPhyRate::Rate2m => WifiPhyRate::Rate2m,
1126        WifiPhyRate::Rate5mL => WifiPhyRate::Rate5mL,
1127        WifiPhyRate::Rate11mL => WifiPhyRate::Rate11mL,
1128        WifiPhyRate::Rate2mS => WifiPhyRate::Rate2mS,
1129        WifiPhyRate::Rate5mS => WifiPhyRate::Rate5mS,
1130        WifiPhyRate::Rate11mS => WifiPhyRate::Rate11mS,
1131        WifiPhyRate::Rate48m => WifiPhyRate::Rate48m,
1132        WifiPhyRate::Rate24m => WifiPhyRate::Rate24m,
1133        WifiPhyRate::Rate12m => WifiPhyRate::Rate12m,
1134        WifiPhyRate::Rate6m => WifiPhyRate::Rate6m,
1135        WifiPhyRate::Rate54m => WifiPhyRate::Rate54m,
1136        WifiPhyRate::Rate36m => WifiPhyRate::Rate36m,
1137        WifiPhyRate::Rate18m => WifiPhyRate::Rate18m,
1138        WifiPhyRate::Rate9m => WifiPhyRate::Rate9m,
1139        WifiPhyRate::RateMcs0Lgi => WifiPhyRate::RateMcs0Lgi,
1140        WifiPhyRate::RateMcs1Lgi => WifiPhyRate::RateMcs1Lgi,
1141        WifiPhyRate::RateMcs2Lgi => WifiPhyRate::RateMcs2Lgi,
1142        WifiPhyRate::RateMcs3Lgi => WifiPhyRate::RateMcs3Lgi,
1143        WifiPhyRate::RateMcs4Lgi => WifiPhyRate::RateMcs4Lgi,
1144        WifiPhyRate::RateMcs5Lgi => WifiPhyRate::RateMcs5Lgi,
1145        WifiPhyRate::RateMcs6Lgi => WifiPhyRate::RateMcs6Lgi,
1146        WifiPhyRate::RateMcs7Lgi => WifiPhyRate::RateMcs7Lgi,
1147        WifiPhyRate::RateMcs0Sgi => WifiPhyRate::RateMcs0Sgi,
1148        WifiPhyRate::RateMcs1Sgi => WifiPhyRate::RateMcs1Sgi,
1149        WifiPhyRate::RateMcs2Sgi => WifiPhyRate::RateMcs2Sgi,
1150        WifiPhyRate::RateMcs3Sgi => WifiPhyRate::RateMcs3Sgi,
1151        WifiPhyRate::RateMcs4Sgi => WifiPhyRate::RateMcs4Sgi,
1152        WifiPhyRate::RateMcs5Sgi => WifiPhyRate::RateMcs5Sgi,
1153        WifiPhyRate::RateMcs6Sgi => WifiPhyRate::RateMcs6Sgi,
1154        WifiPhyRate::RateMcs7Sgi => WifiPhyRate::RateMcs7Sgi,
1155        WifiPhyRate::RateLora250k => WifiPhyRate::RateLora250k,
1156        WifiPhyRate::RateLora500k => WifiPhyRate::RateLora500k,
1157        WifiPhyRate::RateMax => WifiPhyRate::RateMax,
1158    }
1159}
1160
1161fn reconstruct_protocol(protocol: &Protocol) -> Protocol {
1162    match protocol {
1163        Protocol::P802D11B => Protocol::P802D11B,
1164        Protocol::P802D11BG => Protocol::P802D11BG,
1165        Protocol::P802D11BGN => Protocol::P802D11BGN,
1166        Protocol::P802D11BGNLR => Protocol::P802D11BGNLR,
1167        Protocol::P802D11LR => Protocol::P802D11LR,
1168        Protocol::P802D11BGNAX => Protocol::P802D11BGNAX,
1169        _ => Protocol::P802D11BGNLR,
1170    }
1171}