1pub const FRAME_TYPE_DATA: u8 = 0x08;
3pub const FRAME_TYPE_RTS: u8 = 0xb4;
5
6const IEEE80211_RADIOTAP_MCS_HAVE_BW: u8 = 0x01;
7const IEEE80211_RADIOTAP_MCS_HAVE_MCS: u8 = 0x02;
8const IEEE80211_RADIOTAP_MCS_HAVE_GI: u8 = 0x04;
9const IEEE80211_RADIOTAP_MCS_HAVE_FEC: u8 = 0x10;
10const IEEE80211_RADIOTAP_MCS_HAVE_STBC: u8 = 0x20;
11const IEEE80211_RADIOTAP_MCS_SGI: u8 = 0x04;
12const IEEE80211_RADIOTAP_MCS_FEC_LDPC: u8 = 0x10;
13const IEEE80211_RADIOTAP_MCS_STBC_SHIFT: u8 = 5;
14
15const IEEE80211_RADIOTAP_VHT_FLAG_STBC: u8 = 0x01;
16const IEEE80211_RADIOTAP_VHT_FLAG_SGI: u8 = 0x04;
17const IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0: u8 = 0x01;
18
19const RADIOTAP_PRESENT_RATE: u32 = 1 << 2;
20const RADIOTAP_PRESENT_CHANNEL: u32 = 1 << 3;
21const RADIOTAP_PRESENT_TX_FLAGS: u32 = 1 << 15;
22const RADIOTAP_PRESENT_MCS: u32 = 1 << 19;
23const RADIOTAP_PRESENT_VHT: u32 = 1 << 21;
24const RADIOTAP_TX_FLAGS_NO_ACK: u16 = 0x0008;
25
26pub const RADIOTAP_LEGACY_LEN: usize = 13;
28pub const RADIOTAP_HT_LEN: usize = 13;
30pub const RADIOTAP_VHT_LEN: usize = 22;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ChannelBandwidth {
36 Mhz20,
38 Mhz40,
40 Mhz80,
42 Mhz160,
44}
45
46impl ChannelBandwidth {
47 pub const fn realtek_desc_bits(self) -> u8 {
49 match self {
50 Self::Mhz20 => 0,
51 Self::Mhz40 => 1,
52 Self::Mhz80 | Self::Mhz160 => 2,
53 }
54 }
55
56 const fn ht_mcs_bits(self) -> u8 {
57 match self {
58 Self::Mhz20 => 0,
59 Self::Mhz40 | Self::Mhz80 | Self::Mhz160 => 1,
60 }
61 }
62
63 const fn vht_bits(self) -> u8 {
64 match self {
65 Self::Mhz20 => 0x00,
66 Self::Mhz40 => 0x01,
67 Self::Mhz80 => 0x04,
68 Self::Mhz160 => 0x0b,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TxModeKind {
76 Legacy,
78 Ht,
80 Vht,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct TxMode {
87 pub kind: TxModeKind,
89 pub legacy_rate_500kbps: u8,
91 pub ht_mcs: u8,
93 pub vht_mcs: u8,
95 pub vht_nss: u8,
97 pub bandwidth: ChannelBandwidth,
99 pub short_gi: bool,
101 pub ldpc: bool,
103 pub stbc: bool,
105}
106
107impl TxMode {
108 pub const fn legacy(rate_500kbps: u8) -> Self {
110 Self {
111 kind: TxModeKind::Legacy,
112 legacy_rate_500kbps: rate_500kbps,
113 ht_mcs: 0,
114 vht_mcs: 0,
115 vht_nss: 1,
116 bandwidth: ChannelBandwidth::Mhz20,
117 short_gi: false,
118 ldpc: false,
119 stbc: false,
120 }
121 }
122
123 pub const fn legacy_6m() -> Self {
125 Self::legacy(12)
126 }
127
128 pub const fn legacy_1m() -> Self {
130 Self::legacy(2)
131 }
132
133 pub const fn ht(mcs: u8) -> Self {
135 Self {
136 kind: TxModeKind::Ht,
137 legacy_rate_500kbps: 12,
138 ht_mcs: mcs,
139 vht_mcs: 0,
140 vht_nss: 1,
141 bandwidth: ChannelBandwidth::Mhz20,
142 short_gi: false,
143 ldpc: false,
144 stbc: false,
145 }
146 }
147
148 pub const fn vht(nss: u8, mcs: u8) -> Self {
150 Self {
151 kind: TxModeKind::Vht,
152 legacy_rate_500kbps: 12,
153 ht_mcs: 0,
154 vht_mcs: mcs,
155 vht_nss: nss,
156 bandwidth: ChannelBandwidth::Mhz20,
157 short_gi: false,
158 ldpc: false,
159 stbc: false,
160 }
161 }
162}
163
164impl Default for TxMode {
165 fn default() -> Self {
166 Self::legacy_6m()
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct TxRadioParams {
173 pub mcs_index: u8,
175 pub nss: u8,
177 pub bandwidth: ChannelBandwidth,
179 pub short_gi: bool,
181 pub stbc: u8,
183 pub ldpc: bool,
185 pub vht: bool,
187 pub frame_type: u8,
189}
190
191impl TxRadioParams {
192 pub const fn openipc_uplink_default() -> Self {
194 Self {
195 mcs_index: 0,
196 nss: 1,
197 bandwidth: ChannelBandwidth::Mhz20,
198 short_gi: false,
199 stbc: 1,
200 ldpc: true,
201 vht: false,
202 frame_type: FRAME_TYPE_RTS,
203 }
204 }
205}
206
207impl Default for TxRadioParams {
208 fn default() -> Self {
209 Self::openipc_uplink_default()
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct RadiotapTxInfo {
216 pub vht: bool,
218 pub mcs_index: u8,
220 pub nss: u8,
222 pub bandwidth: ChannelBandwidth,
224 pub short_gi: bool,
226 pub stbc: u8,
228 pub ldpc: bool,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct RadiotapTxMetadata {
235 pub mode: Option<TxMode>,
237 pub frequency_mhz: Option<u16>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum RadiotapError {
244 TooShort,
246 InvalidLength,
248 UnsupportedHeader,
250}
251
252impl std::fmt::Display for RadiotapError {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 Self::TooShort => write!(f, "radiotap packet is too short"),
256 Self::InvalidLength => write!(f, "radiotap length is invalid"),
257 Self::UnsupportedHeader => write!(f, "unsupported radiotap TX header"),
258 }
259 }
260}
261
262impl std::error::Error for RadiotapError {}
263
264pub fn build_radiotap_header(params: TxRadioParams) -> Vec<u8> {
266 if params.vht {
267 build_vht_radiotap_header(params)
268 } else {
269 build_ht_radiotap_header(params)
270 }
271}
272
273pub fn build_stream_radiotap(mode: TxMode) -> Vec<u8> {
275 match mode.kind {
276 TxModeKind::Legacy => build_legacy_radiotap_header(mode),
277 TxModeKind::Ht => build_ht_radiotap_header(TxRadioParams {
278 mcs_index: mode.ht_mcs,
279 bandwidth: mode.bandwidth,
280 short_gi: mode.short_gi,
281 stbc: u8::from(mode.stbc),
282 ldpc: mode.ldpc,
283 vht: false,
284 ..TxRadioParams::default()
285 }),
286 TxModeKind::Vht => build_vht_radiotap_header(TxRadioParams {
287 mcs_index: mode.vht_mcs,
288 nss: mode.vht_nss,
289 bandwidth: mode.bandwidth,
290 short_gi: mode.short_gi,
291 stbc: u8::from(mode.stbc),
292 ldpc: mode.ldpc,
293 vht: true,
294 ..TxRadioParams::default()
295 }),
296 }
297}
298
299pub fn build_stream_radiotap_on_channel(mode: TxMode, channel: u8) -> Option<Vec<u8>> {
304 let frequency = crate::wifi::channel_to_frequency(channel)?;
305 let (present, mut fields) = match mode.kind {
306 TxModeKind::Legacy => {
307 let mut fields = vec![mode.legacy_rate_500kbps, 0];
308 fields.extend_from_slice(&frequency.to_le_bytes());
309 fields.extend_from_slice(&0u16.to_le_bytes());
310 fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
311 (
312 RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS,
313 fields,
314 )
315 }
316 TxModeKind::Ht => {
317 let params = TxRadioParams {
318 mcs_index: mode.ht_mcs,
319 bandwidth: mode.bandwidth,
320 short_gi: mode.short_gi,
321 stbc: u8::from(mode.stbc),
322 ldpc: mode.ldpc,
323 vht: false,
324 ..TxRadioParams::default()
325 };
326 let base = build_ht_radiotap_header(params);
327 let mut fields = Vec::with_capacity(9);
328 fields.extend_from_slice(&frequency.to_le_bytes());
329 fields.extend_from_slice(&0u16.to_le_bytes());
330 fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
331 fields.extend_from_slice(&base[10..13]);
332 (
333 RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS | RADIOTAP_PRESENT_MCS,
334 fields,
335 )
336 }
337 TxModeKind::Vht => {
338 let params = TxRadioParams {
339 mcs_index: mode.vht_mcs,
340 nss: mode.vht_nss,
341 bandwidth: mode.bandwidth,
342 short_gi: mode.short_gi,
343 stbc: u8::from(mode.stbc),
344 ldpc: mode.ldpc,
345 vht: true,
346 ..TxRadioParams::default()
347 };
348 let base = build_vht_radiotap_header(params);
349 let mut fields = Vec::with_capacity(18);
350 fields.extend_from_slice(&frequency.to_le_bytes());
351 fields.extend_from_slice(&0u16.to_le_bytes());
352 fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
353 fields.extend_from_slice(&base[10..22]);
354 (
355 RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS | RADIOTAP_PRESENT_VHT,
356 fields,
357 )
358 }
359 };
360 let length = 8usize.checked_add(fields.len())?;
361 let length = u16::try_from(length).ok()?;
362 let mut header = Vec::with_capacity(length as usize);
363 header.extend_from_slice(&[0, 0]);
364 header.extend_from_slice(&length.to_le_bytes());
365 header.extend_from_slice(&present.to_le_bytes());
366 header.append(&mut fields);
367 Some(header)
368}
369
370pub fn build_legacy_radiotap_header(mode: TxMode) -> Vec<u8> {
372 vec![
373 0x00,
374 0x00,
375 RADIOTAP_LEGACY_LEN as u8,
376 0x00,
377 (RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) as u8,
378 ((RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) >> 8) as u8,
379 0x00,
380 0x00,
381 mode.legacy_rate_500kbps,
382 0x00,
383 (RADIOTAP_TX_FLAGS_NO_ACK & 0xff) as u8,
384 (RADIOTAP_TX_FLAGS_NO_ACK >> 8) as u8,
385 0x00,
386 ]
387}
388
389pub fn build_ht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
391 let known = IEEE80211_RADIOTAP_MCS_HAVE_MCS
392 | IEEE80211_RADIOTAP_MCS_HAVE_BW
393 | IEEE80211_RADIOTAP_MCS_HAVE_GI
394 | IEEE80211_RADIOTAP_MCS_HAVE_STBC
395 | IEEE80211_RADIOTAP_MCS_HAVE_FEC;
396 let mut flags = params.bandwidth.ht_mcs_bits();
397 if params.short_gi {
398 flags |= IEEE80211_RADIOTAP_MCS_SGI;
399 }
400 if params.ldpc {
401 flags |= IEEE80211_RADIOTAP_MCS_FEC_LDPC;
402 }
403 flags |= (params.stbc & 0x03) << IEEE80211_RADIOTAP_MCS_STBC_SHIFT;
404
405 vec![
406 0x00,
407 0x00,
408 RADIOTAP_HT_LEN as u8,
409 0x00,
410 0x00,
411 0x80,
412 0x08,
413 0x00,
414 0x08,
415 0x00,
416 known,
417 flags,
418 params.mcs_index.min(31),
419 ]
420}
421
422pub fn build_vht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
424 let mut flags = 0u8;
425 if params.stbc != 0 {
426 flags |= IEEE80211_RADIOTAP_VHT_FLAG_STBC;
427 }
428 if params.short_gi {
429 flags |= IEEE80211_RADIOTAP_VHT_FLAG_SGI;
430 }
431 let nss = params.nss.clamp(1, 4);
432 let mcs = params.mcs_index.min(9);
433 let mcs_nss0 = (mcs << 4) | nss;
434 let coding = if params.ldpc {
435 IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0
436 } else {
437 0
438 };
439
440 vec![
441 0x00,
442 0x00,
443 RADIOTAP_VHT_LEN as u8,
444 0x00,
445 0x00,
446 0x80,
447 0x20,
448 0x00,
449 0x08,
450 0x00,
451 0x45,
452 0x00,
453 flags,
454 params.bandwidth.vht_bits(),
455 mcs_nss0,
456 0x00,
457 0x00,
458 0x00,
459 coding,
460 0x00,
461 0x00,
462 0x00,
463 ]
464}
465
466pub fn radiotap_len(packet: &[u8]) -> Result<usize, RadiotapError> {
468 if packet.len() < 4 {
469 return Err(RadiotapError::TooShort);
470 }
471 let len = u16::from_le_bytes([packet[2], packet[3]]) as usize;
472 if len == 0 || len >= packet.len() {
473 return Err(RadiotapError::InvalidLength);
474 }
475 Ok(len)
476}
477
478pub fn parse_tx_mode_str(spec: &str) -> TxMode {
480 parse_tx_mode_str_impl(spec, false).unwrap_or_default()
481}
482
483pub fn try_parse_tx_mode_str(spec: &str) -> Option<TxMode> {
489 parse_tx_mode_str_impl(spec, true)
490}
491
492fn parse_tx_mode_str_impl(spec: &str, reject_unknown_modifier: bool) -> Option<TxMode> {
493 let trimmed = spec
494 .chars()
495 .filter(|ch| !ch.is_whitespace())
496 .flat_map(char::to_uppercase)
497 .collect::<String>();
498 if trimmed.is_empty() {
499 return None;
500 }
501
502 let mut tokens = trimmed.split('/');
503 let rate_token = tokens.next()?;
504 let mut mode = parse_tx_rate_token(rate_token)?;
505
506 for token in tokens {
507 match token {
508 "SGI" => mode.short_gi = true,
509 "LDPC" => mode.ldpc = true,
510 "STBC" => mode.stbc = true,
511 "20" => mode.bandwidth = ChannelBandwidth::Mhz20,
512 "40" => mode.bandwidth = ChannelBandwidth::Mhz40,
513 "80" => mode.bandwidth = ChannelBandwidth::Mhz80,
514 "160" => mode.bandwidth = ChannelBandwidth::Mhz160,
515 _ if reject_unknown_modifier => return None,
516 _ => {}
517 }
518 }
519 Some(mode)
520}
521
522pub fn parse_radiotap_tx_mode(packet: &[u8]) -> Result<Option<TxMode>, RadiotapError> {
524 Ok(parse_radiotap_tx_metadata(packet)?.mode)
525}
526
527pub fn parse_radiotap_tx_channel(packet: &[u8]) -> Result<Option<u8>, RadiotapError> {
529 Ok(parse_radiotap_tx_metadata(packet)?
530 .frequency_mhz
531 .and_then(crate::wifi::frequency_to_channel))
532}
533
534pub fn parse_radiotap_tx_metadata(packet: &[u8]) -> Result<RadiotapTxMetadata, RadiotapError> {
536 let len = radiotap_len(packet)?;
537 if len < 8 || packet.len() < len {
538 return Err(RadiotapError::InvalidLength);
539 }
540 let present = u32::from_le_bytes(packet[4..8].try_into().expect("present word is in range"));
541 if present & (1 << 31) != 0 {
542 return Err(RadiotapError::UnsupportedHeader);
543 }
544 let supported = RADIOTAP_PRESENT_RATE
545 | RADIOTAP_PRESENT_CHANNEL
546 | RADIOTAP_PRESENT_TX_FLAGS
547 | RADIOTAP_PRESENT_MCS
548 | RADIOTAP_PRESENT_VHT;
549 if present & !supported != 0 {
550 return Err(RadiotapError::UnsupportedHeader);
551 }
552
553 let mut offset = 8usize;
554 let mut mode = None;
555 let mut frequency_mhz = None;
556
557 if present & RADIOTAP_PRESENT_RATE != 0 {
558 require_field(packet, len, offset, 1)?;
559 mode = Some(TxMode::legacy(packet[offset]));
560 offset += 1;
561 }
562
563 if present & RADIOTAP_PRESENT_CHANNEL != 0 {
564 offset = align(offset, 2);
565 require_field(packet, len, offset, 4)?;
566 frequency_mhz = Some(u16::from_le_bytes([packet[offset], packet[offset + 1]]));
567 offset += 4;
568 }
569
570 if present & RADIOTAP_PRESENT_TX_FLAGS != 0 {
571 offset = align(offset, 2);
572 require_field(packet, len, offset, 2)?;
573 offset += 2;
574 }
575
576 if present & RADIOTAP_PRESENT_MCS != 0 {
577 require_field(packet, len, offset, 3)?;
578 let known = packet[offset];
579 let flags = packet[offset + 1];
580 let mcs = packet[offset + 2];
581 let mut ht = TxMode::ht(if known & IEEE80211_RADIOTAP_MCS_HAVE_MCS != 0 {
582 mcs.min(31)
583 } else {
584 0
585 });
586 ht.bandwidth = if known & IEEE80211_RADIOTAP_MCS_HAVE_BW != 0 && flags & 0x03 == 1 {
587 ChannelBandwidth::Mhz40
588 } else {
589 ChannelBandwidth::Mhz20
590 };
591 ht.short_gi =
592 known & IEEE80211_RADIOTAP_MCS_HAVE_GI != 0 && flags & IEEE80211_RADIOTAP_MCS_SGI != 0;
593 ht.ldpc = known & IEEE80211_RADIOTAP_MCS_HAVE_FEC != 0
594 && flags & IEEE80211_RADIOTAP_MCS_FEC_LDPC != 0;
595 ht.stbc = known & IEEE80211_RADIOTAP_MCS_HAVE_STBC != 0
596 && ((flags >> IEEE80211_RADIOTAP_MCS_STBC_SHIFT) & 0x03) != 0;
597 mode = Some(ht);
598 offset += 3;
599 }
600
601 if present & RADIOTAP_PRESENT_VHT != 0 {
602 offset = align(offset, 2);
603 require_field(packet, len, offset, 12)?;
604 let known = u16::from_le_bytes([packet[offset], packet[offset + 1]]);
605 let flags = packet[offset + 2];
606 let bandwidth = match packet[offset + 3] & 0x1f {
607 1..=3 => ChannelBandwidth::Mhz40,
608 4..=10 => ChannelBandwidth::Mhz80,
609 11..=31 => ChannelBandwidth::Mhz160,
610 _ => ChannelBandwidth::Mhz20,
611 };
612 let mcs_nss = packet[offset + 4];
613 let mut vht = TxMode::vht((mcs_nss & 0x0f).clamp(1, 4), ((mcs_nss >> 4) & 0x0f).min(9));
614 if known & (1 << 6) != 0 {
615 vht.bandwidth = bandwidth;
616 }
617 vht.short_gi = known & (1 << 2) != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_SGI != 0;
618 vht.stbc = known & 1 != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_STBC != 0;
619 vht.ldpc = packet[offset + 8] & IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0 != 0;
620 mode = Some(vht);
621 }
622
623 Ok(RadiotapTxMetadata {
624 mode,
625 frequency_mhz,
626 })
627}
628
629pub fn parse_radiotap_tx_info(packet: &[u8]) -> Result<RadiotapTxInfo, RadiotapError> {
631 match parse_radiotap_tx_mode(packet)? {
632 Some(mode) => Ok(RadiotapTxInfo {
633 vht: mode.kind == TxModeKind::Vht,
634 mcs_index: match mode.kind {
635 TxModeKind::Legacy | TxModeKind::Ht => mode.ht_mcs,
636 TxModeKind::Vht => mode.vht_mcs,
637 },
638 nss: mode.vht_nss,
639 bandwidth: mode.bandwidth,
640 short_gi: mode.short_gi,
641 stbc: u8::from(mode.stbc),
642 ldpc: mode.ldpc,
643 }),
644 None => Err(RadiotapError::UnsupportedHeader),
645 }
646}
647
648fn parse_tx_rate_token(token: &str) -> Option<TxMode> {
649 match token {
650 "1M" => Some(TxMode::legacy(2)),
651 "2M" => Some(TxMode::legacy(4)),
652 "5.5M" | "5_5M" | "5M" => Some(TxMode::legacy(11)),
653 "6M" => Some(TxMode::legacy(12)),
654 "9M" => Some(TxMode::legacy(18)),
655 "11M" => Some(TxMode::legacy(22)),
656 "12M" => Some(TxMode::legacy(24)),
657 "18M" => Some(TxMode::legacy(36)),
658 "24M" => Some(TxMode::legacy(48)),
659 "36M" => Some(TxMode::legacy(72)),
660 "48M" => Some(TxMode::legacy(96)),
661 "54M" => Some(TxMode::legacy(108)),
662 _ => {
663 if let Some(raw) = token.strip_prefix("MCS") {
664 return raw
665 .parse::<u8>()
666 .ok()
667 .filter(|mcs| *mcs <= 31)
668 .map(TxMode::ht);
669 }
670 if let Some(raw) = token.strip_prefix("VHT") {
671 let (nss_raw, mcs_raw) = raw.split_once("SS_MCS")?;
672 let nss = nss_raw.parse::<u8>().ok()?;
673 let mcs = mcs_raw.parse::<u8>().ok()?;
674 if (1..=4).contains(&nss) && mcs <= 9 {
675 return Some(TxMode::vht(nss, mcs));
676 }
677 }
678 None
679 }
680 }
681}
682
683const fn align(offset: usize, alignment: usize) -> usize {
684 (offset + alignment - 1) & !(alignment - 1)
685}
686
687fn require_field(
688 packet: &[u8],
689 radiotap_len: usize,
690 offset: usize,
691 len: usize,
692) -> Result<(), RadiotapError> {
693 if offset + len <= radiotap_len && offset + len <= packet.len() {
694 Ok(())
695 } else {
696 Err(RadiotapError::InvalidLength)
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn ht_header_roundtrips_tx_info() {
706 let params = TxRadioParams {
707 mcs_index: 3,
708 bandwidth: ChannelBandwidth::Mhz40,
709 short_gi: true,
710 stbc: 1,
711 ldpc: true,
712 ..TxRadioParams::default()
713 };
714 let mut packet = build_radiotap_header(params);
715 packet.extend_from_slice(&[0u8; 24]);
716 let parsed = parse_radiotap_tx_info(&packet).unwrap();
717 assert_eq!(parsed.mcs_index, 3);
718 assert_eq!(parsed.bandwidth, ChannelBandwidth::Mhz40);
719 assert!(parsed.short_gi);
720 assert!(parsed.ldpc);
721 assert_eq!(parsed.stbc, 1);
722 }
723
724 #[test]
725 fn parses_devourer_tx_mode_strings() {
726 let mode = parse_tx_mode_str("vht2ss_mcs5/80/sgi/ldpc/stbc");
727 assert_eq!(mode.kind, TxModeKind::Vht);
728 assert_eq!(mode.vht_nss, 2);
729 assert_eq!(mode.vht_mcs, 5);
730 assert_eq!(mode.bandwidth, ChannelBandwidth::Mhz80);
731 assert!(mode.short_gi);
732 assert!(mode.ldpc);
733 assert!(mode.stbc);
734 }
735
736 #[test]
737 fn strict_tx_mode_parser_rejects_typos() {
738 assert!(try_parse_tx_mode_str("MCS4/SGI").is_some());
739 assert!(try_parse_tx_mode_str("MCS4/FAST").is_none());
740 assert!(try_parse_tx_mode_str("MCS99").is_none());
741 assert!(try_parse_tx_mode_str("").is_none());
742 assert_eq!(parse_tx_mode_str("not-a-rate"), TxMode::default());
743 }
744
745 #[test]
746 fn parses_devourer_cck_rate_names() {
747 for (spec, rate) in [
748 ("1M", 2),
749 ("2M", 4),
750 ("5.5M", 11),
751 ("5_5M", 11),
752 ("11M", 22),
753 ] {
754 let mode = parse_tx_mode_str(spec);
755 assert_eq!(mode.kind, TxModeKind::Legacy);
756 assert_eq!(mode.legacy_rate_500kbps, rate);
757 }
758 }
759
760 #[test]
761 fn legacy_stream_radiotap_carries_rate_and_tx_flags() {
762 let mode = TxMode::legacy(12);
763 let mut packet = build_stream_radiotap(mode);
764 packet.extend_from_slice(&[0u8; 24]);
765 let parsed = parse_radiotap_tx_mode(&packet).unwrap().unwrap();
766 assert_eq!(parsed.kind, TxModeKind::Legacy);
767 assert_eq!(parsed.legacy_rate_500kbps, 12);
768 }
769
770 #[test]
771 fn channel_headers_roundtrip_for_every_rate_family() {
772 for mode in [TxMode::legacy_6m(), TxMode::ht(4), TxMode::vht(1, 7)] {
773 let mut packet = build_stream_radiotap_on_channel(mode, 36).unwrap();
774 packet.extend_from_slice(&[0u8; 24]);
775 let metadata = parse_radiotap_tx_metadata(&packet).unwrap();
776 assert_eq!(metadata.mode, Some(mode));
777 assert_eq!(metadata.frequency_mhz, Some(5180));
778 assert_eq!(parse_radiotap_tx_channel(&packet).unwrap(), Some(36));
779 }
780 }
781
782 #[test]
783 fn ignores_out_of_band_channel_frequency_without_losing_rate() {
784 let mut packet = build_stream_radiotap_on_channel(TxMode::ht(2), 36).unwrap();
785 packet[8..10].copy_from_slice(&4_900u16.to_le_bytes());
786 packet.extend_from_slice(&[0u8; 24]);
787 assert_eq!(parse_radiotap_tx_channel(&packet).unwrap(), None);
788 assert_eq!(
789 parse_radiotap_tx_mode(&packet).unwrap(),
790 Some(TxMode::ht(2))
791 );
792 }
793}