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 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::Undefined => "Undefined",
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct PacketA {
115 pub mac: [u8; 6],
116 pub rssi: i32,
117 pub timestamp: u32,
118 pub rate: u32,
119 pub sgi: u32,
120 pub secondary_channel: u32,
121 pub channel: u32,
122 pub bandwidth: u32,
123 pub antenna: u32,
124 pub sig_mode: u32,
125 pub mcs: u32,
126 pub smoothing: u32,
127 pub not_sounding: u32,
128 pub aggregation: u32,
129 pub stbc: u32,
130 pub fec_coding: u32,
131 pub ampdu_cnt: u32,
132 pub noise_floor: i32,
133 pub rx_state: u32,
134 pub sig_len: u32,
135 pub date_time: Option<DateTime>,
136 pub sequence_number: u16,
137 pub data_format: RxCsiFmt,
138 pub csi_data_len: u16,
139 pub csi_data: Vec<i8>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct PacketBc5 {
147 pub mac: [u8; 6],
148 pub rssi: i32,
149 pub timestamp: u32,
150 pub rate: u32,
151 pub noise_floor: i32,
152 pub sig_len: u32,
153 pub rx_state: u32,
154 pub dump_len: u32,
155 pub cur_bb_format: u32,
156 pub rx_channel_estimate_info_vld: u32,
157 pub rx_channel_estimate_len: u32,
158 pub second: u32,
159 pub channel: u32,
160 pub is_group: u32,
161 pub rxend_state: u32,
162 pub rxmatch3: u32,
163 pub rxmatch2: u32,
164 pub rxmatch1: u32,
165 pub date_time: Option<DateTime>,
166 pub sequence_number: u16,
167 pub csi_data_len: u16,
168 pub data_format: RxCsiFmt,
169 pub csi_data: Vec<i8>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct PacketBc6 {
177 pub mac: [u8; 6],
178 pub rssi: i32,
179 pub timestamp: u32,
180 pub rate: u32,
181 pub noise_floor: i32,
182 pub sig_len: u32,
183 pub rx_state: u32,
184 pub dump_len: u32,
185 pub he_sigb_len: u32,
186 pub cur_single_mpdu: u32,
187 pub cur_bb_format: u32,
188 pub rx_channel_estimate_info_vld: u32,
189 pub rx_channel_estimate_len: u32,
190 pub second: u32,
191 pub channel: u32,
192 pub is_group: u32,
193 pub rxend_state: u32,
194 pub rxmatch3: u32,
195 pub rxmatch2: u32,
196 pub rxmatch1: u32,
197 pub rxmatch0: u32,
198 pub date_time: Option<DateTime>,
199 pub sequence_number: u16,
200 pub csi_data_len: u16,
201 pub data_format: RxCsiFmt,
202 pub csi_data: Vec<i8>,
203}
204
205#[derive(Debug, Clone)]
211pub struct DecodedCsi {
212 pub mac: [u8; 6],
214 pub rssi: i32,
215 pub timestamp: u32,
216 pub rate: u32,
217 pub noise_floor: i32,
218 pub sig_len: u32,
219 pub rx_state: u32,
220 pub channel: u32,
221 pub date_time: Option<DateTime>,
222 pub sequence_number: u16,
223 pub data_format: RxCsiFmt,
224 pub csi_data_len: u16,
225 pub csi_data: Vec<i8>,
226
227 pub sgi: Option<u32>,
229 pub secondary_channel: Option<u32>,
230 pub bandwidth: Option<u32>,
231 pub antenna: Option<u32>,
232 pub sig_mode: Option<u32>,
233 pub mcs: Option<u32>,
234 pub smoothing: Option<u32>,
235 pub not_sounding: Option<u32>,
236 pub aggregation: Option<u32>,
237 pub stbc: Option<u32>,
238 pub fec_coding: Option<u32>,
239 pub ampdu_cnt: Option<u32>,
240
241 pub dump_len: Option<u32>,
243 pub cur_bb_format: Option<u32>,
244 pub rx_channel_estimate_info_vld: Option<u32>,
245 pub rx_channel_estimate_len: Option<u32>,
246 pub second: Option<u32>,
247 pub is_group: Option<u32>,
248 pub rxend_state: Option<u32>,
249 pub rxmatch3: Option<u32>,
250 pub rxmatch2: Option<u32>,
251 pub rxmatch1: Option<u32>,
252
253 pub he_sigb_len: Option<u32>,
255 pub cur_single_mpdu: Option<u32>,
256 pub rxmatch0: Option<u32>,
257}
258
259impl From<PacketA> for DecodedCsi {
260 fn from(p: PacketA) -> Self {
261 DecodedCsi {
262 mac: p.mac,
263 rssi: p.rssi,
264 timestamp: p.timestamp,
265 rate: p.rate,
266 noise_floor: p.noise_floor,
267 sig_len: p.sig_len,
268 rx_state: p.rx_state,
269 channel: p.channel,
270 date_time: p.date_time,
271 sequence_number: p.sequence_number,
272 data_format: p.data_format,
273 csi_data_len: p.csi_data_len,
274 csi_data: p.csi_data,
275 sgi: Some(p.sgi),
276 secondary_channel: Some(p.secondary_channel),
277 bandwidth: Some(p.bandwidth),
278 antenna: Some(p.antenna),
279 sig_mode: Some(p.sig_mode),
280 mcs: Some(p.mcs),
281 smoothing: Some(p.smoothing),
282 not_sounding: Some(p.not_sounding),
283 aggregation: Some(p.aggregation),
284 stbc: Some(p.stbc),
285 fec_coding: Some(p.fec_coding),
286 ampdu_cnt: Some(p.ampdu_cnt),
287 dump_len: None,
288 cur_bb_format: None,
289 rx_channel_estimate_info_vld: None,
290 rx_channel_estimate_len: None,
291 second: None,
292 is_group: None,
293 rxend_state: None,
294 rxmatch3: None,
295 rxmatch2: None,
296 rxmatch1: None,
297 he_sigb_len: None,
298 cur_single_mpdu: None,
299 rxmatch0: None,
300 }
301 }
302}
303
304impl From<PacketBc5> for DecodedCsi {
305 fn from(p: PacketBc5) -> Self {
306 DecodedCsi {
307 mac: p.mac,
308 rssi: p.rssi,
309 timestamp: p.timestamp,
310 rate: p.rate,
311 noise_floor: p.noise_floor,
312 sig_len: p.sig_len,
313 rx_state: p.rx_state,
314 channel: p.channel,
315 date_time: p.date_time,
316 sequence_number: p.sequence_number,
317 data_format: p.data_format,
318 csi_data_len: p.csi_data_len,
319 csi_data: p.csi_data,
320 sgi: None,
321 secondary_channel: None,
322 bandwidth: None,
323 antenna: None,
324 sig_mode: None,
325 mcs: None,
326 smoothing: None,
327 not_sounding: None,
328 aggregation: None,
329 stbc: None,
330 fec_coding: None,
331 ampdu_cnt: None,
332 dump_len: Some(p.dump_len),
333 cur_bb_format: Some(p.cur_bb_format),
334 rx_channel_estimate_info_vld: Some(p.rx_channel_estimate_info_vld),
335 rx_channel_estimate_len: Some(p.rx_channel_estimate_len),
336 second: Some(p.second),
337 is_group: Some(p.is_group),
338 rxend_state: Some(p.rxend_state),
339 rxmatch3: Some(p.rxmatch3),
340 rxmatch2: Some(p.rxmatch2),
341 rxmatch1: Some(p.rxmatch1),
342 he_sigb_len: None,
343 cur_single_mpdu: None,
344 rxmatch0: None,
345 }
346 }
347}
348
349impl From<PacketBc6> for DecodedCsi {
350 fn from(p: PacketBc6) -> Self {
351 DecodedCsi {
352 mac: p.mac,
353 rssi: p.rssi,
354 timestamp: p.timestamp,
355 rate: p.rate,
356 noise_floor: p.noise_floor,
357 sig_len: p.sig_len,
358 rx_state: p.rx_state,
359 channel: p.channel,
360 date_time: p.date_time,
361 sequence_number: p.sequence_number,
362 data_format: p.data_format,
363 csi_data_len: p.csi_data_len,
364 csi_data: p.csi_data,
365 sgi: None,
366 secondary_channel: None,
367 bandwidth: None,
368 antenna: None,
369 sig_mode: None,
370 mcs: None,
371 smoothing: None,
372 not_sounding: None,
373 aggregation: None,
374 stbc: None,
375 fec_coding: None,
376 ampdu_cnt: None,
377 dump_len: Some(p.dump_len),
378 cur_bb_format: Some(p.cur_bb_format),
379 rx_channel_estimate_info_vld: Some(p.rx_channel_estimate_info_vld),
380 rx_channel_estimate_len: Some(p.rx_channel_estimate_len),
381 second: Some(p.second),
382 is_group: Some(p.is_group),
383 rxend_state: Some(p.rxend_state),
384 rxmatch3: Some(p.rxmatch3),
385 rxmatch2: Some(p.rxmatch2),
386 rxmatch1: Some(p.rxmatch1),
387 he_sigb_len: Some(p.he_sigb_len),
388 cur_single_mpdu: Some(p.cur_single_mpdu),
389 rxmatch0: Some(p.rxmatch0),
390 }
391 }
392}
393
394#[derive(Debug)]
396pub struct DecodeError(postcard::Error);
397
398impl std::fmt::Display for DecodeError {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 write!(f, "failed to decode CSI frame: {}", self.0)
401 }
402}
403
404impl std::error::Error for DecodeError {}
405
406impl From<postcard::Error> for DecodeError {
407 fn from(e: postcard::Error) -> Self {
408 DecodeError(e)
409 }
410}
411
412pub fn decode(frame: &[u8], chip: ChipVariant) -> Result<DecodedCsi, DecodeError> {
419 let mut owned = frame.to_vec();
420 let decoded = match chip {
421 ChipVariant::Esp32Family => {
422 let (p, _) = postcard::take_from_bytes_cobs::<PacketA>(&mut owned)?;
423 p.into()
424 }
425 ChipVariant::Esp32c5 => {
426 let (p, _) = postcard::take_from_bytes_cobs::<PacketBc5>(&mut owned)?;
427 p.into()
428 }
429 ChipVariant::Esp32c6 => {
430 let (p, _) = postcard::take_from_bytes_cobs::<PacketBc6>(&mut owned)?;
431 p.into()
432 }
433 };
434 Ok(decoded)
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
445 fn decode_packet_a_roundtrip() {
446 let pkt = PacketA {
447 mac: [0xde, 0xad, 0xbe, 0xef, 0x00, 0x01],
448 rssi: -42,
449 timestamp: 123_456,
450 rate: 11,
451 sgi: 1,
452 secondary_channel: 0,
453 channel: 6,
454 bandwidth: 0,
455 antenna: 0,
456 sig_mode: 1,
457 mcs: 7,
458 smoothing: 0,
459 not_sounding: 1,
460 aggregation: 0,
461 stbc: 0,
462 fec_coding: 0,
463 ampdu_cnt: 0,
464 noise_floor: -96,
465 rx_state: 0,
466 sig_len: 128,
467 date_time: Some(DateTime {
468 year: 2026,
469 month: 6,
470 day: 22,
471 hour: 12,
472 minute: 30,
473 second: 15,
474 millisecond: 250,
475 }),
476 sequence_number: 4242,
477 data_format: RxCsiFmt::HtBw20,
478 csi_data_len: 4,
479 csi_data: vec![1, -2, 3, -4],
480 };
481
482 let mut buf = vec![0u8; 1024];
485 let cobs = postcard::to_slice_cobs(&pkt, &mut buf).unwrap();
486 let body = cobs.strip_suffix(&[0]).unwrap_or(cobs);
487
488 let out = decode(body, ChipVariant::Esp32Family).unwrap();
489 assert_eq!(out.mac, pkt.mac);
490 assert_eq!(out.rssi, -42);
491 assert_eq!(out.channel, 6);
492 assert_eq!(out.mcs, Some(7));
493 assert_eq!(out.noise_floor, -96);
494 assert_eq!(out.sequence_number, 4242);
495 assert_eq!(out.data_format, RxCsiFmt::HtBw20);
496 assert_eq!(out.csi_data, vec![1, -2, 3, -4]);
497 assert_eq!(out.dump_len, None);
498 let dt = out.date_time.expect("date_time present");
499 assert_eq!((dt.year, dt.month, dt.day), (2026, 6, 22));
500 }
501
502 #[test]
503 fn decode_packet_bc6_roundtrip() {
504 let pkt = PacketBc6 {
505 mac: [1, 2, 3, 4, 5, 6],
506 rssi: -55,
507 timestamp: 9,
508 rate: 1,
509 noise_floor: -90,
510 sig_len: 64,
511 rx_state: 0,
512 dump_len: 100,
513 he_sigb_len: 7,
514 cur_single_mpdu: 1,
515 cur_bb_format: 2,
516 rx_channel_estimate_info_vld: 1,
517 rx_channel_estimate_len: 64,
518 second: 3,
519 channel: 11,
520 is_group: 0,
521 rxend_state: 0,
522 rxmatch3: 0,
523 rxmatch2: 0,
524 rxmatch1: 1,
525 rxmatch0: 1,
526 date_time: None,
527 sequence_number: 7,
528 csi_data_len: 2,
529 data_format: RxCsiFmt::Undefined,
530 csi_data: vec![-1, 1],
531 };
532 let mut buf = vec![0u8; 1024];
533 let cobs = postcard::to_slice_cobs(&pkt, &mut buf).unwrap();
534 let body = cobs.strip_suffix(&[0]).unwrap_or(cobs);
535
536 let out = decode(body, ChipVariant::Esp32c6).unwrap();
537 assert_eq!(out.mac, [1, 2, 3, 4, 5, 6]);
538 assert_eq!(out.he_sigb_len, Some(7));
539 assert_eq!(out.rxmatch0, Some(1));
540 assert_eq!(out.sgi, None);
541 assert_eq!(out.csi_data, vec![-1, 1]);
542 assert!(out.date_time.is_none());
543 }
544
545 #[test]
550 fn undefined_format_preserves_numeric_cur_bb_format() {
551 let pkt = PacketBc5 {
552 mac: [1, 2, 3, 4, 5, 6],
553 rssi: -50,
554 timestamp: 1,
555 rate: 1,
556 noise_floor: -90,
557 sig_len: 64,
558 rx_state: 0,
559 dump_len: 100,
560 cur_bb_format: 4,
561 rx_channel_estimate_info_vld: 1,
562 rx_channel_estimate_len: 64,
563 second: 0,
564 channel: 36,
565 is_group: 0,
566 rxend_state: 0,
567 rxmatch3: 0,
568 rxmatch2: 0,
569 rxmatch1: 0,
570 date_time: None,
571 sequence_number: 1,
572 csi_data_len: 2,
573 data_format: RxCsiFmt::Undefined,
574 csi_data: vec![1, -1],
575 };
576 let mut buf = vec![0u8; 1024];
577 let cobs = postcard::to_slice_cobs(&pkt, &mut buf).unwrap();
578 let body = cobs.strip_suffix(&[0]).unwrap_or(cobs);
579
580 let out = decode(body, ChipVariant::Esp32c5).unwrap();
581 assert_eq!(out.data_format, RxCsiFmt::Undefined);
582 assert_eq!(out.cur_bb_format, Some(4));
583 }
584
585 #[test]
586 fn chip_string_mapping() {
587 assert_eq!(ChipVariant::from_chip_str("ESP32"), Some(ChipVariant::Esp32Family));
588 assert_eq!(ChipVariant::from_chip_str("esp32c6"), Some(ChipVariant::Esp32c6));
589 assert_eq!(ChipVariant::from_chip_str("esp32c5"), Some(ChipVariant::Esp32c5));
590 assert_eq!(ChipVariant::from_chip_str("weird"), None);
591 }
592}