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}