Skip to main content

esp_csi_rs/
central_peripheral.rs

1//! The **central / peripheral** node taxonomy, and the ESP-NOW configuration it carries.
2//!
3//! This vocabulary predates [`NodeRole`](crate::NodeRole) and was removed in "Replace ESP-NOW
4//! central/peripheral with emitter/collector roles". It is restored here, **alongside** the newer
5//! roles rather than in place of them, because the two describe different things and both are in
6//! use:
7//!
8//! * [`NodeRole`](crate::NodeRole) says what a node is *for* — put energy in the channel, or measure
9//!   it. That is the right axis for a raw-injection capture, where the transmitter is unassociated
10//!   and there is no exchange at all.
11//! * [`Node`] describes a *paired ESP-NOW exchange*, where one side drives and the other responds.
12//!   The emitter/collector split cannot express it: an ESP-NOW central both transmits control frames
13//!   and measures the replies, so it is neither a pure emitter nor a pure collector.
14//!
15//! The naming is admittedly backwards — the "central" is the receiver that aggregates CSI while its
16//! "peripherals" transmit — and that was one of the reasons for the original removal. It is kept
17//! as-is because it is the on-the-wire and on-the-CLI contract; renaming it would break every
18//! configuration that uses it without making the exchange any easier to describe.
19//!
20//! `ap` and `sta` collection are NOT duplicated here: [`crate::central`] re-exports the live
21//! [`crate::collector`] modules, so there is one copy of that code.
22
23// `WifiPhyRate` moved from `wifi` to `esp_now` in esp-radio 0.18 — it only ever described the
24// ESP-NOW peer rate, so the move is where it belongs.
25use esp_radio::esp_now::WifiPhyRate;
26use esp_radio::wifi::SecondaryChannel;
27
28use crate::node::{WifiApConfig, WifiSnifferConfig, WifiStationConfig};
29
30/// Configuration for ESP-NOW traffic generation.
31///
32/// Used by both Central and Peripheral nodes when operating in ESP-NOW mode.
33/// Construct with `EspNowConfig::default()` then chain `with_channel` /
34/// `with_phy_rate` to override defaults — both nodes must agree on the
35/// channel for ESP-NOW frames to be received.
36pub struct EspNowConfig {
37    phy_rate: WifiPhyRate,
38    pub(crate) channel: u8,
39    /// Optional pre-configured peer MAC. When `None` (default) the pair uses
40    /// automatic, magic-prefix-based pairing. When `Some`, the magic prefix is
41    /// dropped from every frame and the source-MAC filter is the discriminator
42    /// from the first frame — both nodes must each be configured with the
43    /// other's MAC.
44    peer_mac: Option<[u8; 6]>,
45    /// Optional HT40 secondary channel. When `Some`, the node runs HT40 (40 MHz)
46    /// on `channel` + this secondary; when `None`, HT20. Only meaningful when
47    /// `force_phy` is set.
48    secondary_channel: Option<SecondaryChannel>,
49    /// When set, the node forces the ESP-NOW TX PHY (`phy_rate` +
50    /// HT20/HT40 from `secondary_channel`) via a per-peer rate config — which
51    /// requires bringing the radio up in started STA mode. When clear (default),
52    /// the radio is left in its default state and ESP-NOW frames go out at the
53    /// driver's default (legacy) PHY. Set by `with_phy_rate` / `with_ht40`.
54    force_phy: bool,
55}
56
57impl Default for EspNowConfig {
58    fn default() -> Self {
59        Self {
60            phy_rate: WifiPhyRate::RateMcs0Lgi,
61            // Channel 1 is empirically less congested than 11 in most
62            // residential / office environments — APs on auto-select tend
63            // to bias toward 11 because it's the upper bound in US/EU.
64            // Override with `with_channel` if your environment differs.
65            channel: 1,
66            peer_mac: None,
67            secondary_channel: None,
68            force_phy: false,
69        }
70    }
71}
72
73impl EspNowConfig {
74    /// Recommended base config for the fast one-to-one (asymmetric simplex)
75    /// mode: forces HT20 at MCS7 Long-GI for maximum CSI packets/sec. Chain
76    /// `with_channel` / `with_ht40` to override. Used by
77    /// [`CentralOpMode::EspNowFastCollector`] / [`PeripheralOpMode::EspNowFastSource`].
78    pub fn fast_default() -> Self {
79        Self::default().with_phy_rate(WifiPhyRate::RateMcs7Lgi)
80    }
81
82    /// Override the 2.4 GHz channel (1–14). Both central and peripheral
83    /// must be configured with the same channel.
84    pub fn with_channel(mut self, channel: u8) -> Self {
85        self.channel = channel;
86        self
87    }
88
89    /// Force the ESP-NOW TX PHY rate (e.g. `RateMcs0Lgi` … `RateMcs7Lgi`, or a
90    /// legacy rate). Applied per-peer via `esp_now_set_peer_rate_config`, which
91    /// brings the radio up in started STA mode. Combine with [`with_ht40`] for
92    /// a 40 MHz bandwidth; without it the rate is sent at HT20 (for MCS rates)
93    /// or the matching legacy mode. Without calling this (or `with_ht40`) the
94    /// PHY is left at the driver default.
95    ///
96    /// [`with_ht40`]: EspNowConfig::with_ht40
97    pub fn with_phy_rate(mut self, phy_rate: WifiPhyRate) -> Self {
98        self.phy_rate = phy_rate;
99        self.force_phy = true;
100        self
101    }
102
103    /// Pre-configure the peer's MAC address for manual pairing.
104    ///
105    /// Switches off automatic magic-prefix pairing: no magic is sent, and each
106    /// node accepts frames only from the configured peer MAC (source-MAC
107    /// filtering applies from the first frame). The central must be given the
108    /// peripheral's MAC and vice-versa, and both nodes must use the same
109    /// pairing mode for frames to parse.
110    pub fn with_peer_mac(mut self, peer_mac: [u8; 6]) -> Self {
111        self.peer_mac = Some(peer_mac);
112        self
113    }
114
115    /// Configured 2.4 GHz channel.
116    pub fn channel(&self) -> u8 {
117        self.channel
118    }
119
120    /// Configured PHY rate.
121    pub fn phy_rate(&self) -> &WifiPhyRate {
122        &self.phy_rate
123    }
124
125    /// Configured peer MAC for manual pairing, or `None` for automatic
126    /// magic-prefix pairing.
127    pub fn peer_mac(&self) -> Option<[u8; 6]> {
128        self.peer_mac
129    }
130
131    /// Run the ESP-NOW TX at HT40 (40 MHz) with `secondary` as the HT40
132    /// secondary channel, using the configured [`with_phy_rate`] (default
133    /// `RateMcs0Lgi`). Implies `force_phy`. Without this the PHY is HT20 (if a
134    /// rate is forced) or the driver default. Verify on-air (CSI `bandwidth`
135    /// field) that HT40 actually engaged.
136    ///
137    /// [`with_phy_rate`]: EspNowConfig::with_phy_rate
138    pub fn with_ht40(mut self, secondary: SecondaryChannel) -> Self {
139        self.secondary_channel = Some(secondary);
140        self.force_phy = true;
141        self
142    }
143
144    /// Configured HT40 secondary channel, or `None` for HT20.
145    pub fn secondary_channel(&self) -> Option<SecondaryChannel> {
146        self.secondary_channel
147    }
148
149    /// Whether the ESP-NOW TX PHY (rate + bandwidth) is forced via a per-peer
150    /// rate config (set by [`with_phy_rate`] / [`with_ht40`]).
151    ///
152    /// [`with_phy_rate`]: EspNowConfig::with_phy_rate
153    /// [`with_ht40`]: EspNowConfig::with_ht40
154    pub fn force_phy(&self) -> bool {
155        self.force_phy
156    }
157}
158/// Central node operational modes.
159pub enum CentralOpMode {
160    /// Drive an ESP-NOW exchange with a peripheral node.
161    EspNow(EspNowConfig),
162    /// Associate as a Wi-Fi station to harvest CSI from received frames.
163    WifiStation(WifiStationConfig),
164    /// Run a self-contained softAP CSI collector: start an access point (plus a
165    /// minimal DHCP server) so a [`CentralOpMode::WifiStation`] node can
166    /// associate and generate steady uplink traffic, captured as CSI on this AP.
167    WifiAccessPoint(WifiApConfig),
168    /// Fast one-to-one ESP-NOW collector (asymmetric simplex): broadcast a
169    /// sparse discovery beacon until a [`PeripheralOpMode::EspNowFastSource`] is
170    /// heard, then stop beaconing and go RX-only, capturing CSI from the source's
171    /// continuous unicast flood. Maximizes CSI packets/sec by leaving all airtime
172    /// to the single transmitter.
173    EspNowFastCollector(EspNowConfig),
174}
175
176// Enum for Peripheral modes, each wrapping its specific config.
177/// Peripheral node operational modes.
178pub enum PeripheralOpMode {
179    /// Reply to a central's ESP-NOW control frames.
180    EspNow(EspNowConfig),
181    /// Run as a Wi-Fi promiscuous sniffer; CSI is captured from every
182    /// frame received on the locked channel.
183    WifiSniffer(WifiSnifferConfig),
184    /// Fast one-to-one ESP-NOW source (asymmetric simplex): listen for a
185    /// [`CentralOpMode::EspNowFastCollector`] beacon, learn its MAC, then unicast
186    /// a continuous forced-PHY flood for the collector to capture as CSI.
187    EspNowFastSource(EspNowConfig),
188}
189
190/// High-level node type and mode.
191pub enum Node {
192    /// Run as the peripheral side of the chosen [`PeripheralOpMode`].
193    Peripheral(PeripheralOpMode),
194    /// Run as the central side of the chosen [`CentralOpMode`].
195    Central(CentralOpMode),
196}
197/// CSI collection behaviour for the node.
198///
199/// `Listener` keeps CSI traffic flowing without processing packets; `Collector` actively processes
200/// it. A `Listener` sniffer is effectively useless — traffic arrives and nothing reads it — which is
201/// worth knowing before configuring one.
202///
203/// Restored with the rest of this taxonomy. Distinct from [`crate::CollectorMode`] despite the
204/// similar name: this says *how much* a node does with CSI, that one says *how* a collector obtains
205/// frames to measure.
206#[derive(PartialEq, Eq, Clone, Copy)]
207pub enum CollectionMode {
208    /// Enables CSI collection and processes CSI data.
209    Collector,
210    /// Enables CSI collection but does not process CSI data.
211    Listener,
212}