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}