1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ChipVariant {
22 Esp32Family,
24 Esp32c5,
26 Esp32c6,
28}
29
30impl ChipVariant {
31 pub fn from_chip_str(chip: &str) -> Option<Self> {
36 match chip.trim().to_ascii_lowercase().as_str() {
37 "esp32" | "esp32c3" | "esp32s3" | "esp32s2" => Some(Self::Esp32Family),
38 "esp32c5" => Some(Self::Esp32c5),
39 "esp32c6" => Some(Self::Esp32c6),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct DateTime {
50 pub year: u64,
51 pub month: u64,
52 pub day: u64,
53 pub hour: u64,
54 pub minute: u64,
55 pub second: u64,
56 pub millisecond: u64,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum RxCsiFmt {
66 Bw20,
67 HtBw20,
68 HtBw20Stbc,
69 SecbBw20,
70 SecbHtBw20,
71 SecbHtBw20Stbc,
72 SecbHtBw40,
73 SecbHtBw40Stbc,
74 SecaBw20,
75 SecaHtBw20,
76 SecaHtBw20Stbc,
77 SecaHtBw40,
78 SecaHtBw40Stbc,
79 VhtBw20,
81 He20Su,
83 He20Mu,
85 Undefined,
86}
87
88impl RxCsiFmt {
89 pub fn as_str(self) -> &'static str {
91 match self {
92 Self::Bw20 => "Bw20",
93 Self::HtBw20 => "HtBw20",
94 Self::HtBw20Stbc => "HtBw20Stbc",
95 Self::SecbBw20 => "SecbBw20",
96 Self::SecbHtBw20 => "SecbHtBw20",
97 Self::SecbHtBw20Stbc => "SecbHtBw20Stbc",
98 Self::SecbHtBw40 => "SecbHtBw40",
99 Self::SecbHtBw40Stbc => "SecbHtBw40Stbc",
100 Self::SecaBw20 => "SecaBw20",
101 Self::SecaHtBw20 => "SecaHtBw20",
102 Self::SecaHtBw20Stbc => "SecaHtBw20Stbc",
103 Self::SecaHtBw40 => "SecaHtBw40",
104 Self::SecaHtBw40Stbc => "SecaHtBw40Stbc",
105 Self::VhtBw20 => "VhtBw20",
106 Self::He20Su => "He20Su",
107 Self::He20Mu => "He20Mu",
108 Self::Undefined => "Undefined",
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct PacketA {
117 pub mac: [u8; 6],
118 pub rssi: i32,
119 pub timestamp: u32,
120 pub rate: u32,
121 pub sgi: u32,
122 pub secondary_channel: u32,
123 pub channel: u32,
124 pub bandwidth: u32,
125 pub antenna: u32,
126 pub sig_mode: u32,
127 pub mcs: u32,
128 pub smoothing: u32,
129 pub not_sounding: u32,
130 pub aggregation: u32,
131 pub stbc: u32,
132 pub fec_coding: u32,
133 pub ampdu_cnt: u32,
134 pub noise_floor: i32,
135 pub rx_state: u32,
136 pub sig_len: u32,
137 pub date_time: Option<DateTime>,
138 pub sequence_number: u16,
139 pub data_format: RxCsiFmt,
140 pub csi_data_len: u16,
141 pub csi_data: Vec<i8>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct PacketBc5 {
149 pub mac: [u8; 6],
150 pub rssi: i32,
151 pub timestamp: u32,
152 pub rate: u32,
153 pub noise_floor: i32,
154 pub sig_len: u32,
155 pub rx_state: u32,
156 pub dump_len: u32,
157 pub cur_bb_format: u32,
158 pub rx_channel_estimate_info_vld: u32,
159 pub rx_channel_estimate_len: u32,
160 pub second: u32,
161 pub channel: u32,
162 pub is_group: u32,
163 pub rxend_state: u32,
164 pub rxmatch3: u32,
165 pub rxmatch2: u32,
166 pub rxmatch1: u32,
167 pub date_time: Option<DateTime>,
168 pub sequence_number: u16,
169 pub csi_data_len: u16,
170 pub data_format: RxCsiFmt,
171 pub csi_data: Vec<i8>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct PacketBc6 {
179 pub mac: [u8; 6],
180 pub rssi: i32,
181 pub timestamp: u32,
182 pub rate: u32,
183 pub noise_floor: i32,
184 pub sig_len: u32,
185 pub rx_state: u32,
186 pub dump_len: u32,
187 pub he_sigb_len: u32,
188 pub cur_single_mpdu: u32,
189 pub cur_bb_format: u32,
190 pub rx_channel_estimate_info_vld: u32,
191 pub rx_channel_estimate_len: u32,
192 pub second: u32,
193 pub channel: u32,
194 pub is_group: u32,
195 pub rxend_state: u32,
196 pub rxmatch3: u32,
197 pub rxmatch2: u32,
198 pub rxmatch1: u32,
199 pub rxmatch0: u32,
200 pub date_time: Option<DateTime>,
201 pub sequence_number: u16,
202 pub csi_data_len: u16,
203 pub data_format: RxCsiFmt,
204 pub csi_data: Vec<i8>,
205}
206
207#[derive(Debug, Clone)]
213pub struct DecodedCsi {
214 pub mac: [u8; 6],
216 pub rssi: i32,
217 pub timestamp: u32,
218 pub rate: u32,
219 pub noise_floor: i32,
220 pub sig_len: u32,
221 pub rx_state: u32,
222 pub channel: u32,
223 pub date_time: Option<DateTime>,
224 pub sequence_number: u16,
225 pub data_format: RxCsiFmt,
226 pub csi_data_len: u16,
227 pub csi_data: Vec<i8>,
228
229 pub sgi: Option<u32>,
231 pub secondary_channel: Option<u32>,
232 pub bandwidth: Option<u32>,
233 pub antenna: Option<u32>,
234 pub sig_mode: Option<u32>,
235 pub mcs: Option<u32>,
236 pub smoothing: Option<u32>,
237 pub not_sounding: Option<u32>,
238 pub aggregation: Option<u32>,
239 pub stbc: Option<u32>,
240 pub fec_coding: Option<u32>,
241 pub ampdu_cnt: Option<u32>,
242
243 pub dump_len: Option<u32>,
245 pub cur_bb_format: Option<u32>,
246 pub rx_channel_estimate_info_vld: Option<u32>,
247 pub rx_channel_estimate_len: Option<u32>,
248 pub second: Option<u32>,
249 pub is_group: Option<u32>,
250 pub rxend_state: Option<u32>,
251 pub rxmatch3: Option<u32>,
252 pub rxmatch2: Option<u32>,
253 pub rxmatch1: Option<u32>,
254
255 pub he_sigb_len: Option<u32>,
257 pub cur_single_mpdu: Option<u32>,
258 pub rxmatch0: Option<u32>,
259}
260
261impl From<PacketA> for DecodedCsi {
262 fn from(p: PacketA) -> Self {
263 DecodedCsi {
264 mac: p.mac,
265 rssi: p.rssi,
266 timestamp: p.timestamp,
267 rate: p.rate,
268 noise_floor: p.noise_floor,
269 sig_len: p.sig_len,
270 rx_state: p.rx_state,
271 channel: p.channel,
272 date_time: p.date_time,
273 sequence_number: p.sequence_number,
274 data_format: p.data_format,
275 csi_data_len: p.csi_data_len,
276 csi_data: p.csi_data,
277 sgi: Some(p.sgi),
278 secondary_channel: Some(p.secondary_channel),
279 bandwidth: Some(p.bandwidth),
280 antenna: Some(p.antenna),
281 sig_mode: Some(p.sig_mode),
282 mcs: Some(p.mcs),
283 smoothing: Some(p.smoothing),
284 not_sounding: Some(p.not_sounding),
285 aggregation: Some(p.aggregation),
286 stbc: Some(p.stbc),
287 fec_coding: Some(p.fec_coding),
288 ampdu_cnt: Some(p.ampdu_cnt),
289 dump_len: None,
290 cur_bb_format: None,
291 rx_channel_estimate_info_vld: None,
292 rx_channel_estimate_len: None,
293 second: None,
294 is_group: None,
295 rxend_state: None,
296 rxmatch3: None,
297 rxmatch2: None,
298 rxmatch1: None,
299 he_sigb_len: None,
300 cur_single_mpdu: None,
301 rxmatch0: None,
302 }
303 }
304}
305
306impl From<PacketBc5> for DecodedCsi {
307 fn from(p: PacketBc5) -> Self {
308 DecodedCsi {
309 mac: p.mac,
310 rssi: p.rssi,
311 timestamp: p.timestamp,
312 rate: p.rate,
313 noise_floor: p.noise_floor,
314 sig_len: p.sig_len,
315 rx_state: p.rx_state,
316 channel: p.channel,
317 date_time: p.date_time,
318 sequence_number: p.sequence_number,
319 data_format: p.data_format,
320 csi_data_len: p.csi_data_len,
321 csi_data: p.csi_data,
322 sgi: None,
323 secondary_channel: None,
324 bandwidth: None,
325 antenna: None,
326 sig_mode: None,
327 mcs: None,
328 smoothing: None,
329 not_sounding: None,
330 aggregation: None,
331 stbc: None,
332 fec_coding: None,
333 ampdu_cnt: None,
334 dump_len: Some(p.dump_len),
335 cur_bb_format: Some(p.cur_bb_format),
336 rx_channel_estimate_info_vld: Some(p.rx_channel_estimate_info_vld),
337 rx_channel_estimate_len: Some(p.rx_channel_estimate_len),
338 second: Some(p.second),
339 is_group: Some(p.is_group),
340 rxend_state: Some(p.rxend_state),
341 rxmatch3: Some(p.rxmatch3),
342 rxmatch2: Some(p.rxmatch2),
343 rxmatch1: Some(p.rxmatch1),
344 he_sigb_len: None,
345 cur_single_mpdu: None,
346 rxmatch0: None,
347 }
348 }
349}
350
351impl From<PacketBc6> for DecodedCsi {
352 fn from(p: PacketBc6) -> Self {
353 DecodedCsi {
354 mac: p.mac,
355 rssi: p.rssi,
356 timestamp: p.timestamp,
357 rate: p.rate,
358 noise_floor: p.noise_floor,
359 sig_len: p.sig_len,
360 rx_state: p.rx_state,
361 channel: p.channel,
362 date_time: p.date_time,
363 sequence_number: p.sequence_number,
364 data_format: p.data_format,
365 csi_data_len: p.csi_data_len,
366 csi_data: p.csi_data,
367 sgi: None,
368 secondary_channel: None,
369 bandwidth: None,
370 antenna: None,
371 sig_mode: None,
372 mcs: None,
373 smoothing: None,
374 not_sounding: None,
375 aggregation: None,
376 stbc: None,
377 fec_coding: None,
378 ampdu_cnt: None,
379 dump_len: Some(p.dump_len),
380 cur_bb_format: Some(p.cur_bb_format),
381 rx_channel_estimate_info_vld: Some(p.rx_channel_estimate_info_vld),
382 rx_channel_estimate_len: Some(p.rx_channel_estimate_len),
383 second: Some(p.second),
384 is_group: Some(p.is_group),
385 rxend_state: Some(p.rxend_state),
386 rxmatch3: Some(p.rxmatch3),
387 rxmatch2: Some(p.rxmatch2),
388 rxmatch1: Some(p.rxmatch1),
389 he_sigb_len: Some(p.he_sigb_len),
390 cur_single_mpdu: Some(p.cur_single_mpdu),
391 rxmatch0: Some(p.rxmatch0),
392 }
393 }
394}
395
396#[derive(Debug)]
398pub struct DecodeError(postcard::Error);
399
400impl std::fmt::Display for DecodeError {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 write!(f, "failed to decode CSI frame: {}", self.0)
403 }
404}
405
406impl std::error::Error for DecodeError {}
407
408impl From<postcard::Error> for DecodeError {
409 fn from(e: postcard::Error) -> Self {
410 DecodeError(e)
411 }
412}
413
414pub fn decode(frame: &[u8], chip: ChipVariant) -> Result<DecodedCsi, DecodeError> {
421 let mut owned = frame.to_vec();
422 let decoded = match chip {
423 ChipVariant::Esp32Family => {
424 let (p, _) = postcard::take_from_bytes_cobs::<PacketA>(&mut owned)?;
425 p.into()
426 }
427 ChipVariant::Esp32c5 => {
428 let (p, _) = postcard::take_from_bytes_cobs::<PacketBc5>(&mut owned)?;
429 p.into()
430 }
431 ChipVariant::Esp32c6 => {
432 let (p, _) = postcard::take_from_bytes_cobs::<PacketBc6>(&mut owned)?;
433 p.into()
434 }
435 };
436 Ok(decoded)
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
447 fn decode_packet_a_roundtrip() {
448 let pkt = PacketA {
449 mac: [0xde, 0xad, 0xbe, 0xef, 0x00, 0x01],
450 rssi: -42,
451 timestamp: 123_456,
452 rate: 11,
453 sgi: 1,
454 secondary_channel: 0,
455 channel: 6,
456 bandwidth: 0,
457 antenna: 0,
458 sig_mode: 1,
459 mcs: 7,
460 smoothing: 0,
461 not_sounding: 1,
462 aggregation: 0,
463 stbc: 0,
464 fec_coding: 0,
465 ampdu_cnt: 0,
466 noise_floor: -96,
467 rx_state: 0,
468 sig_len: 128,
469 date_time: Some(DateTime {
470 year: 2026,
471 month: 6,
472 day: 22,
473 hour: 12,
474 minute: 30,
475 second: 15,
476 millisecond: 250,
477 }),
478 sequence_number: 4242,
479 data_format: RxCsiFmt::HtBw20,
480 csi_data_len: 4,
481 csi_data: vec![1, -2, 3, -4],
482 };
483
484 let mut buf = vec![0u8; 1024];
487 let cobs = postcard::to_slice_cobs(&pkt, &mut buf).unwrap();
488 let body = cobs.strip_suffix(&[0]).unwrap_or(cobs);
489
490 let out = decode(body, ChipVariant::Esp32Family).unwrap();
491 assert_eq!(out.mac, pkt.mac);
492 assert_eq!(out.rssi, -42);
493 assert_eq!(out.channel, 6);
494 assert_eq!(out.mcs, Some(7));
495 assert_eq!(out.noise_floor, -96);
496 assert_eq!(out.sequence_number, 4242);
497 assert_eq!(out.data_format, RxCsiFmt::HtBw20);
498 assert_eq!(out.csi_data, vec![1, -2, 3, -4]);
499 assert_eq!(out.dump_len, None);
500 let dt = out.date_time.expect("date_time present");
501 assert_eq!((dt.year, dt.month, dt.day), (2026, 6, 22));
502 }
503
504 #[test]
505 fn decode_packet_bc6_roundtrip() {
506 let pkt = PacketBc6 {
507 mac: [1, 2, 3, 4, 5, 6],
508 rssi: -55,
509 timestamp: 9,
510 rate: 1,
511 noise_floor: -90,
512 sig_len: 64,
513 rx_state: 0,
514 dump_len: 100,
515 he_sigb_len: 7,
516 cur_single_mpdu: 1,
517 cur_bb_format: 2,
518 rx_channel_estimate_info_vld: 1,
519 rx_channel_estimate_len: 64,
520 second: 3,
521 channel: 11,
522 is_group: 0,
523 rxend_state: 0,
524 rxmatch3: 0,
525 rxmatch2: 0,
526 rxmatch1: 1,
527 rxmatch0: 1,
528 date_time: None,
529 sequence_number: 7,
530 csi_data_len: 2,
531 data_format: RxCsiFmt::Undefined,
532 csi_data: vec![-1, 1],
533 };
534 let mut buf = vec![0u8; 1024];
535 let cobs = postcard::to_slice_cobs(&pkt, &mut buf).unwrap();
536 let body = cobs.strip_suffix(&[0]).unwrap_or(cobs);
537
538 let out = decode(body, ChipVariant::Esp32c6).unwrap();
539 assert_eq!(out.mac, [1, 2, 3, 4, 5, 6]);
540 assert_eq!(out.he_sigb_len, Some(7));
541 assert_eq!(out.rxmatch0, Some(1));
542 assert_eq!(out.sgi, None);
543 assert_eq!(out.csi_data, vec![-1, 1]);
544 assert!(out.date_time.is_none());
545 }
546
547 #[test]
548 fn chip_string_mapping() {
549 assert_eq!(ChipVariant::from_chip_str("ESP32"), Some(ChipVariant::Esp32Family));
550 assert_eq!(ChipVariant::from_chip_str("esp32c6"), Some(ChipVariant::Esp32c6));
551 assert_eq!(ChipVariant::from_chip_str("esp32c5"), Some(ChipVariant::Esp32c5));
552 assert_eq!(ChipVariant::from_chip_str("weird"), None);
553 }
554}