esp_csi_rs/wire/setup.rs
1//! A measurement setup, in IEEE 802.11bf's terms.
2//!
3//! 802.11bf negotiates a *measurement setup* before any sounding: how often to sound, at what
4//! bandwidth, which responders take part, and what form their reports take. Each sounding sequence
5//! under it is a *measurement instance*. This type carries the same parameters, split the way a
6//! sniffer forces them apart: an [`AcquisitionFilter`] (what a node accepts, which every mode has)
7//! and optional [`StimulusParams`] (how the channel is sounded, which only a node that sounds it
8//! has).
9//!
10//! A controller can speak these terms today. A node maps the parameters onto whatever its mode
11//! actually does — an ESP-NOW initiator's periodicity becomes its packet rate — and refuses the ones
12//! it cannot honour rather than ignoring them. When an 802.11bf mode exists, the same setup maps
13//! onto a negotiated one, with no change for the controller.
14//!
15//! Like the rest of [`crate::wire`], this module has no `esp-*` imports.
16
17use heapless::Vec;
18use postcard::experimental::max_size::MaxSize;
19use serde::{Deserialize, Serialize};
20
21use super::{Bandwidth, PpduFormat};
22
23/// Responders a setup can name.
24pub const MAX_RESPONDERS: usize = 8;
25
26/// A measurement setup: an id, what to accept, and — for a node that sounds the channel — how to
27/// sound it.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
29#[non_exhaustive]
30pub struct MeasurementSetup {
31 /// Setup id. Stamped on every measurement taken under it, as
32 /// [`Stimulus::Controlled::setup_id`](super::Stimulus::Controlled).
33 pub id: u8,
34 /// What a node accepts.
35 pub acquisition: AcquisitionFilter,
36 /// How the channel is sounded. `None` for a node that does not sound it — a sniffer.
37 pub stimulus: Option<StimulusParams>,
38}
39
40impl MeasurementSetup {
41 /// A setup that accepts everything and sounds nothing.
42 pub fn new(id: u8) -> Self {
43 Self {
44 id,
45 acquisition: AcquisitionFilter::default(),
46 stimulus: None,
47 }
48 }
49
50 /// Set what to accept.
51 pub fn with_acquisition(mut self, acquisition: AcquisitionFilter) -> Self {
52 self.acquisition = acquisition;
53 self
54 }
55
56 /// Set how to sound the channel.
57 pub fn with_stimulus(mut self, stimulus: StimulusParams) -> Self {
58 self.stimulus = Some(stimulus);
59 self
60 }
61}
62
63/// Which measurements a node accepts.
64#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, MaxSize)]
65#[non_exhaustive]
66pub struct AcquisitionFilter {
67 /// Primary channel. `None` keeps the mode's own.
68 pub channel: Option<u8>,
69 /// Bandwidth. `None` accepts whatever arrives.
70 pub bandwidth: Option<Bandwidth>,
71 /// PPDU formats to acquire. Empty accepts every format.
72 pub ppdu_formats: PpduSet,
73 /// Accept only this transmitter. `None` accepts any.
74 pub transmitter: Option<[u8; 6]>,
75}
76
77/// A set of [`PpduFormat`]s.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, MaxSize)]
79pub struct PpduSet(u16);
80
81impl PpduSet {
82 /// The empty set, which a filter reads as "every format".
83 pub const ANY: Self = Self(0);
84
85 const fn bit(f: PpduFormat) -> u16 {
86 1 << match f {
87 PpduFormat::Unknown => 0,
88 PpduFormat::Dsss => 1,
89 PpduFormat::NonHt => 2,
90 PpduFormat::Ht => 3,
91 PpduFormat::Vht => 4,
92 PpduFormat::VhtMu => 5,
93 PpduFormat::HeSu => 6,
94 PpduFormat::HeMu => 7,
95 PpduFormat::HeErSu => 8,
96 PpduFormat::HeTb => 9,
97 PpduFormat::Eht => 10,
98 }
99 }
100
101 /// This set plus `f`.
102 pub const fn with(self, f: PpduFormat) -> Self {
103 Self(self.0 | Self::bit(f))
104 }
105
106 /// Whether `f` is in the set. The empty set contains every format.
107 pub const fn contains(self, f: PpduFormat) -> bool {
108 self.0 == 0 || self.0 & Self::bit(f) != 0
109 }
110
111 /// Whether the set is empty (every format accepted).
112 pub const fn is_any(self) -> bool {
113 self.0 == 0
114 }
115
116 /// Whether every format in the set is an HE (802.11ax) format.
117 pub const fn only_he(self) -> bool {
118 let he = Self::bit(PpduFormat::HeSu)
119 | Self::bit(PpduFormat::HeMu)
120 | Self::bit(PpduFormat::HeErSu)
121 | Self::bit(PpduFormat::HeTb);
122 self.0 != 0 && self.0 & !he == 0
123 }
124
125 /// Whether the set excludes the legacy formats (DSSS and non-HT OFDM).
126 pub const fn excludes_legacy(self) -> bool {
127 self.0 != 0 && self.0 & (Self::bit(PpduFormat::Dsss) | Self::bit(PpduFormat::NonHt)) == 0
128 }
129}
130
131/// How the channel is sounded.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
133#[non_exhaustive]
134pub struct StimulusParams {
135 /// Interval between soundings, microseconds.
136 pub periodicity_us: u32,
137 /// Sounding bandwidth.
138 pub bandwidth: Bandwidth,
139 /// The form a measurement report takes.
140 pub report: ReportType,
141 /// Subcarrier grouping of a [`ReportType::Grouped`] report (1 = no grouping).
142 pub ng: u8,
143 /// Bits per quantised value of a [`ReportType::Grouped`] report.
144 pub nb: u8,
145 /// Trigger-based sounding (the access point triggers responders) rather than non-TB.
146 pub trigger_based: bool,
147 /// Responders taking part. Empty means any that pairs.
148 pub responders: Vec<[u8; 6], MAX_RESPONDERS>,
149}
150
151impl StimulusParams {
152 /// Sound every `periodicity_us` at `bandwidth`, raw reports, no grouping, non-TB, any
153 /// responder.
154 pub fn new(periodicity_us: u32, bandwidth: Bandwidth) -> Self {
155 Self {
156 periodicity_us,
157 bandwidth,
158 report: ReportType::Raw,
159 ng: 1,
160 nb: 8,
161 trigger_based: false,
162 responders: Vec::new(),
163 }
164 }
165
166 /// Sounding rate in Hz implied by the periodicity, saturated to `u16`.
167 pub const fn rate_hz(&self) -> u16 {
168 if self.periodicity_us == 0 {
169 return u16::MAX;
170 }
171 let hz = 1_000_000 / self.periodicity_us;
172 if hz > u16::MAX as u32 { u16::MAX } else if hz == 0 { 1 } else { hz as u16 }
173 }
174}
175
176/// The form of a measurement report.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
178#[non_exhaustive]
179pub enum ReportType {
180 /// The full channel estimate as the receiver produced it.
181 Raw,
182 /// A grouped, quantised report ([`CsiPayload::Grouped`](super::CsiPayload::Grouped)).
183 Grouped,
184}
185
186/// Why a node could not take on a measurement setup.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188#[non_exhaustive]
189pub enum SetupError {
190 /// The mode does not sound the channel, but the setup asked it to.
191 NoStimulus,
192 /// The mode sounds the channel, and the setup's stimulus asks for something it cannot do. The
193 /// field names which parameter.
194 UnsupportedStimulus(&'static str),
195 /// The acquisition filter asks for something this node cannot select. The field names which
196 /// parameter.
197 UnsupportedAcquisition(&'static str),
198}