Skip to main content

esp_csi_rs/
measurement.rs

1//! [`MeasurementSetup`]: how often a node sounds the channel, at what width, and how it acquires
2//! the estimate.
3//!
4//! IEEE 802.11bf negotiates a *measurement setup* before any sounding. This is the node-level form
5//! of the same idea: the parameters a [`CsiNode`](crate::CsiNode) is built with, beside its
6//! [`Mode`](crate::Mode).
7//!
8//! **Two types share the name.** This one, at the crate root, is the node's own configuration and
9//! holds a [`CsiConfig`] — a chip-specific type that never leaves the device. The serialized form a
10//! controller sends over a link is [`wire::MeasurementSetup`](crate::wire::MeasurementSetup),
11//! always addressed through the `wire` module; it has no `esp-*` types in it, and its encoding is a
12//! wire contract. [`From<&MeasurementSetup>`](#impl-From%3C%26MeasurementSetup%3E-for-MeasurementSetup)
13//! produces the wire form from this one, and
14//! [`CsiNode::apply_measurement_setup`](crate::CsiNode::apply_measurement_setup) takes the wire
15//! form back onto a node.
16
17use crate::CsiConfig;
18use crate::wire::Bandwidth;
19
20/// The node-level measurement setup: periodicity, bandwidth and acquisition.
21///
22/// Build with [`MeasurementSetup::new`] (or `Default`) and chain the setters; every parameter is
23/// optional and an unset one leaves the mode's own behaviour in place.
24///
25/// ```rust,ignore
26/// let setup = MeasurementSetup::new()
27///     .periodicity_hz(100)
28///     .acquisition(CsiConfig::default());
29/// let node = CsiNode::esp_now(EspNowConfig::default(), setup, hardware);
30/// ```
31///
32/// Not to be confused with the serialized [`wire::MeasurementSetup`](crate::wire::MeasurementSetup);
33/// see the [module documentation](self).
34#[derive(Debug, Clone, Default)]
35pub struct MeasurementSetup {
36    id: u8,
37    periodicity_hz: Option<u16>,
38    bandwidth: Option<Bandwidth>,
39    acquisition: Option<CsiConfig>,
40}
41
42impl MeasurementSetup {
43    /// A setup that changes nothing: no periodicity, the mode's own bandwidth, the default
44    /// acquisition, id 0.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// How often the node sounds the channel, in soundings per second.
50    ///
51    /// Read by the modes that sound it: the ESP-NOW initiator's control-packet rate and the
52    /// responder's reply pacing, the simplex source's flood, the station's gateway ping and the
53    /// access point's downlink flood. The sniffer and the simplex peer sound nothing and ignore it;
54    /// the emitter takes its period from [`EmitterConfig`](crate::EmitterConfig) instead.
55    ///
56    /// Unset, a mode that sounds the channel uses its own default rate (as fast as the link allows
57    /// for the ESP-NOW and simplex modes; no ping traffic for the station and access point).
58    pub fn periodicity_hz(mut self, hz: u16) -> Self {
59        self.periodicity_hz = Some(hz);
60        self
61    }
62
63    /// The channel width the setup is for.
64    ///
65    /// The modes that fix their width in their own config (ESP-NOW and simplex through
66    /// `with_ht40`, the access point through its secondary channel, the emitter through its PHY)
67    /// keep that width; a different value here is reported with a warning when the node starts
68    /// rather than silently overriding the config. A station's width is whatever it associates at,
69    /// and a sniffer's follows each PPDU. The value is carried into the
70    /// [wire form](crate::wire::MeasurementSetup) as the acquisition and stimulus bandwidth.
71    pub fn bandwidth(mut self, bandwidth: Bandwidth) -> Self {
72        self.bandwidth = Some(bandwidth);
73        self
74    }
75
76    /// How the radio acquires the channel estimate. Unset, the chip's default [`CsiConfig`] is used.
77    /// An emitter and a simplex source measure nothing and ignore it.
78    pub fn acquisition(mut self, acquisition: CsiConfig) -> Self {
79        self.acquisition = Some(acquisition);
80        self
81    }
82
83    /// The setup id stamped on every controlled sounding taken under it, as
84    /// [`Stimulus::Controlled::setup_id`](crate::wire::Stimulus::Controlled). Defaults to 0.
85    pub fn id(mut self, id: u8) -> Self {
86        self.id = id;
87        self
88    }
89
90    /// The configured periodicity, in Hz, if one was set.
91    pub fn get_periodicity_hz(&self) -> Option<u16> {
92        self.periodicity_hz
93    }
94
95    /// The configured bandwidth, if one was set.
96    pub fn get_bandwidth(&self) -> Option<Bandwidth> {
97        self.bandwidth
98    }
99
100    /// The configured acquisition, if one was set.
101    pub fn get_acquisition(&self) -> Option<&CsiConfig> {
102        self.acquisition.as_ref()
103    }
104
105    /// The setup id.
106    pub fn get_id(&self) -> u8 {
107        self.id
108    }
109
110    pub(crate) fn set_periodicity_hz(&mut self, hz: Option<u16>) {
111        self.periodicity_hz = hz;
112    }
113
114    pub(crate) fn set_acquisition(&mut self, acquisition: Option<CsiConfig>) {
115        self.acquisition = acquisition;
116    }
117
118    pub(crate) fn set_id(&mut self, id: u8) {
119        self.id = id;
120    }
121}
122
123/// The serialized form of a node-level setup.
124///
125/// `id` and `bandwidth` carry over directly. A periodicity becomes a
126/// [`StimulusParams`](crate::wire::StimulusParams) with raw reports, no grouping and non-TB
127/// sounding — what the radio produces — at the setup's bandwidth (20 MHz if none was set). The
128/// acquisition is chip-specific and has no wire counterpart, so it does not travel; the receiving
129/// node keeps its own.
130impl From<&MeasurementSetup> for crate::wire::MeasurementSetup {
131    fn from(setup: &MeasurementSetup) -> Self {
132        let mut acquisition = crate::wire::AcquisitionFilter::default();
133        acquisition.bandwidth = setup.bandwidth;
134        let mut wire = crate::wire::MeasurementSetup::new(setup.id).with_acquisition(acquisition);
135        if let Some(hz) = setup.periodicity_hz {
136            wire = wire.with_stimulus(crate::wire::StimulusParams::new(
137                1_000_000 / hz.max(1) as u32,
138                setup.bandwidth.unwrap_or(Bandwidth::Mhz20),
139            ));
140        }
141        wire
142    }
143}