1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! The node model: the attributes that describe a node in a CSI sensing network, named in IEEE
//! 802.11bf's terms wherever the standard has a term.
//!
//! The normative description is `docs/network-model.md`, included below so that the document and
//! the types it describes cannot drift: there is one copy, and it reaches docs.rs, GitHub and every
//! README link from the same file. No other document in the ecosystem restates it.
//!
//! The types in this module are that document's image. Three rules connect them:
//!
//! 1. The attributes are **independent** ([`Mode`], [`SessionRole`], [`SensingRole`],
//! [`Reporting`]). Collapsing any two loses a configuration somebody deploys.
//! 2. **Illegal combinations are not offered.** Each mode exposes only the attributes it admits;
//! the fixed ones are `const` accessors with no setter to call.
//! 3. The roles and the reporting are **computed from the mode**, never stored beside it. A second
//! home for a value is a second place to get it wrong.
//!
//! Every public enum here is `#[non_exhaustive]`, so a new mode — a negotiated IEEE 802.11bf
//! sensing mode, say — or a new kind of reporting is a minor release, not a breaking one. Inside
//! this crate the matches stay exhaustive, so a new variant is still a compile error everywhere it
//! has to be handled.
//!
/// A node's part in a sensing session: IEEE 802.11bf's *sensing initiator* or *sensing
/// responder*.
///
/// The initiator is the node that originates the soundings a session measures; a responder takes
/// part in soundings an initiator set up. A node that takes part in no session — a sniffer
/// observing traffic it neither originates nor answers, or an emitter sounding for whoever listens
/// — has no session role, which the accessors report as `None` (an *observer*).
///
/// The host that starts and stops a run is **not** an initiator; it is the [`Controller`], which is
/// never a node.
///
/// | Mode | Session role |
/// |---|---|
/// | ESP-NOW | initiator or responder (configurable) |
/// | ESP-NOW simplex | source: initiator; peer: responder |
/// | Wi-Fi station | initiator when it drives the measured traffic, otherwise `None` |
/// | Wi-Fi access point | initiator |
/// | Wi-Fi sniffer | `None` |
/// | Emitter | `None` |
/// What starts and stops a run: the host tier, never a node.
///
/// Whatever calls [`CsiNode::run`](crate::CsiNode::run) and
/// [`CsiNodeClient::send_stop`](crate::CsiNodeClient::send_stop) is the run's controller. In
/// practice that is the serial console of `esp-csi-cli-rs`, the HTTP control route of
/// `csi-webserver`, the on-device UI task of `esp-csi-litetui-rs`, or your own application.
///
/// Controlling a run is not initiating a session: a [`SessionRole::Initiator`] is a node that
/// originates soundings inside the WLAN. In IEEE 802.11bf terms the controller is the layer above
/// the MAC that requests measurements through its service interface. No node reports this role;
/// the type exists so the crates that drive a node can name it instead of inventing a word.
;
/// Which end of a sounding a node is: IEEE 802.11bf's *sensing transmitter* and *sensing
/// receiver*.
///
/// The sensing transmitter sends the PPDUs a measurement is taken from; the sensing receiver
/// measures them. A node in a symmetric exchange does both.
///
/// | Mode | Sensing role |
/// |---|---|
/// | ESP-NOW | transmitter-receiver |
/// | ESP-NOW simplex | source: transmitter; peer: receiver |
/// | Wi-Fi station | transmitter-receiver when it drives the traffic, otherwise receiver |
/// | Wi-Fi access point | receiver |
/// | Wi-Fi sniffer | receiver |
/// | Emitter | transmitter |
/// Where a reported measurement goes.
/// Whether, and how often, a node's measurements leave it.
///
/// Every variant except [`None`](Self::None) delivers measurements; they differ in which ones. A
/// node that reports nothing is not an idle node — it is one that must exist for the measurement to
/// happen but contributes no data: the responder an initiator needs something to sound, a node that
/// keeps traffic on the channel while another measures, or a receiver with its output switched off
/// to separate acquisition cost from delivery cost.
///
/// [`ThresholdBased`](Self::ThresholdBased) is IEEE 802.11bf's threshold-based reporting: the node
/// reports only while the channel is changing. It is useful well before 802.11bf hardware exists,
/// because gating delivery on motion **on the device** takes the load off a USB or UART link.
/// [`Decimate`](Self::Decimate) thins a steady stream to every *n*th measurement.
///
/// Whether a node reports at all is also an ESP-NOW protocol field. An initiator announces it in
/// [`ControlPacket::initiator_reports`](crate::ControlPacket), and a responder that hears an
/// initiator reporting [`None`](Self::None) promotes itself to `Report { to: Host }` so the pair
/// still produces a dataset. That promotion happens at runtime and does not rewrite the node's
/// configuration, so [`CsiNode::reporting`](crate::CsiNode::reporting) reports what was configured
/// while [`runtime_reporting`](crate::runtime_reporting) reports what is in force.
/// Parameters of [`Reporting::ThresholdBased`].
///
/// The node keeps a slow running average of the channel's amplitude profile over up to 32 sampled
/// subcarriers, and scores every measurement by how far its profile's **shape** departs from that
/// average, on a `0..=65535` scale. Both profiles are normalised by their own totals first, so the
/// receiver's per-frame gain changes score nothing; `65535` is a profile with no overlap at all. A
/// measurement scoring at least `level` is reported and opens a `hold_ms` window in which every
/// measurement is reported, so a movement is captured whole rather than as its loudest frames.
///
/// **Calibrate `level` on your own link.** Raw 8-bit estimates carry per-frame quantisation noise,
/// so a still room does not score zero. Measured on two ESP32-C5s at HE20 (245 subcarriers,
/// 100 frames/s, an empty room), single-frame scores had a median of ~2700 and a 99th percentile
/// of ~4300. Run the node with [`variation_only`](Self::variation_only) first, read the scores it
/// reports while nothing moves, and set `level` comfortably above their high percentile; the level
/// that separates motion from stillness has not been measured here and depends on the room.
/// Which end of an asymmetric ESP-NOW simplex exchange a node is.
///
/// The ends are built through [`SimplexConfig::source`] and [`SimplexConfig::peer`], which take
/// different arguments because the two ends consume different settings.
/// Configuration for the asymmetric ESP-NOW simplex exchange.
///
/// One node owns all the transmit airtime and floods; the other goes receive-only after discovery
/// and measures the flood. Leaving the channel to a single transmitter is what makes this the
/// highest-rate pairing the crate offers.
///
/// **The two ends are not interchangeable, so they are not built the same way.** The source needs a
/// PHY rate, a bandwidth and a periodicity; the peer forces no TX PHY at all — it only receives —
/// so handing it those settings would silently ignore them. Each constructor takes only what its
/// end consumes.
///
/// The session role follows the soundings, as it does everywhere else in this model: the node that
/// floods is the [`Initiator`](SessionRole::Initiator), and the node that beacons to be found and
/// then falls silent is the [`Responder`](SessionRole::Responder).
/// How a node reaches the channel.
///
/// This is the model's extension point and it is deliberately abstract: it names the link a node
/// uses to take part in the network, not a property of CSI measurement itself. Any link that can
/// excite a channel and yield a channel measurement can supply a mode.
///
/// Each variant admits only the attribute combinations that are meaningful for that link, and it
/// admits them through its config type rather than through a rule written down somewhere:
///
/// | Mode | Session role | Sensing role | Reporting |
/// |---|---|---|---|
/// | [`EspNow`](Self::EspNow) | initiator or responder | transmitter-receiver | any |
/// | [`EspNowSimplex`](Self::EspNowSimplex) | source: initiator; peer: responder | source: transmitter; peer: receiver | source: none; peer: report |
/// | [`Sniffer`](Self::Sniffer) | none (observer) | receiver | report, threshold-based or decimate — never `None` |
/// | [`Station`](Self::Station) | initiator or none | transmitter-receiver or receiver | any |
/// | [`AccessPoint`](Self::AccessPoint) | initiator | receiver | any |
/// | [`Emitter`](Self::Emitter) | none | transmitter | none |
/// What a [`RadioProfile`](crate::RadioProfile) is told about the node it is configuring.
///
/// A view rather than the mode itself, and `#[non_exhaustive]` on purpose. Profiles live
/// out-of-tree, so every change to what they are handed is a breaking change for someone; passing a
/// struct means the node can start offering a profile more of its configuration or its hardware
/// without changing the trait's signature again.
///
/// [`Mode`] stays exhaustive inside this crate, because there a new mode *should* be a compile
/// error in the dispatch that has to handle it.