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_TX_FLAGS: u32 = 1 << 15;
21const RADIOTAP_PRESENT_MCS: u32 = 1 << 19;
22const RADIOTAP_PRESENT_VHT: u32 = 1 << 21;
23const RADIOTAP_TX_FLAGS_NO_ACK: u16 = 0x0008;
24
25pub const RADIOTAP_LEGACY_LEN: usize = 13;
27pub const RADIOTAP_HT_LEN: usize = 13;
29pub const RADIOTAP_VHT_LEN: usize = 22;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ChannelBandwidth {
35 Mhz20,
37 Mhz40,
39 Mhz80,
41 Mhz160,
43}
44
45impl ChannelBandwidth {
46 pub const fn realtek_desc_bits(self) -> u8 {
48 match self {
49 Self::Mhz20 => 0,
50 Self::Mhz40 => 1,
51 Self::Mhz80 | Self::Mhz160 => 2,
52 }
53 }
54
55 const fn ht_mcs_bits(self) -> u8 {
56 match self {
57 Self::Mhz20 => 0,
58 Self::Mhz40 | Self::Mhz80 | Self::Mhz160 => 1,
59 }
60 }
61
62 const fn vht_bits(self) -> u8 {
63 match self {
64 Self::Mhz20 => 0x00,
65 Self::Mhz40 => 0x01,
66 Self::Mhz80 => 0x04,
67 Self::Mhz160 => 0x0b,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum TxModeKind {
75 Legacy,
77 Ht,
79 Vht,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct TxMode {
86 pub kind: TxModeKind,
88 pub legacy_rate_500kbps: u8,
90 pub ht_mcs: u8,
92 pub vht_mcs: u8,
94 pub vht_nss: u8,
96 pub bandwidth: ChannelBandwidth,
98 pub short_gi: bool,
100 pub ldpc: bool,
102 pub stbc: bool,
104}
105
106impl TxMode {
107 pub const fn legacy(rate_500kbps: u8) -> Self {
109 Self {
110 kind: TxModeKind::Legacy,
111 legacy_rate_500kbps: rate_500kbps,
112 ht_mcs: 0,
113 vht_mcs: 0,
114 vht_nss: 1,
115 bandwidth: ChannelBandwidth::Mhz20,
116 short_gi: false,
117 ldpc: false,
118 stbc: false,
119 }
120 }
121
122 pub const fn legacy_6m() -> Self {
124 Self::legacy(12)
125 }
126
127 pub const fn legacy_1m() -> Self {
129 Self::legacy(2)
130 }
131
132 pub const fn ht(mcs: u8) -> Self {
134 Self {
135 kind: TxModeKind::Ht,
136 legacy_rate_500kbps: 12,
137 ht_mcs: mcs,
138 vht_mcs: 0,
139 vht_nss: 1,
140 bandwidth: ChannelBandwidth::Mhz20,
141 short_gi: false,
142 ldpc: false,
143 stbc: false,
144 }
145 }
146
147 pub const fn vht(nss: u8, mcs: u8) -> Self {
149 Self {
150 kind: TxModeKind::Vht,
151 legacy_rate_500kbps: 12,
152 ht_mcs: 0,
153 vht_mcs: mcs,
154 vht_nss: nss,
155 bandwidth: ChannelBandwidth::Mhz20,
156 short_gi: false,
157 ldpc: false,
158 stbc: false,
159 }
160 }
161}
162
163impl Default for TxMode {
164 fn default() -> Self {
165 Self::legacy_6m()
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct TxRadioParams {
172 pub mcs_index: u8,
174 pub nss: u8,
176 pub bandwidth: ChannelBandwidth,
178 pub short_gi: bool,
180 pub stbc: u8,
182 pub ldpc: bool,
184 pub vht: bool,
186 pub frame_type: u8,
188}
189
190impl TxRadioParams {
191 pub const fn openipc_uplink_default() -> Self {
193 Self {
194 mcs_index: 0,
195 nss: 1,
196 bandwidth: ChannelBandwidth::Mhz20,
197 short_gi: false,
198 stbc: 1,
199 ldpc: true,
200 vht: false,
201 frame_type: FRAME_TYPE_RTS,
202 }
203 }
204}
205
206impl Default for TxRadioParams {
207 fn default() -> Self {
208 Self::openipc_uplink_default()
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct RadiotapTxInfo {
215 pub vht: bool,
217 pub mcs_index: u8,
219 pub nss: u8,
221 pub bandwidth: ChannelBandwidth,
223 pub short_gi: bool,
225 pub stbc: u8,
227 pub ldpc: bool,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum RadiotapError {
234 TooShort,
236 InvalidLength,
238 UnsupportedHeader,
240}
241
242impl std::fmt::Display for RadiotapError {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Self::TooShort => write!(f, "radiotap packet is too short"),
246 Self::InvalidLength => write!(f, "radiotap length is invalid"),
247 Self::UnsupportedHeader => write!(f, "unsupported radiotap TX header"),
248 }
249 }
250}
251
252impl std::error::Error for RadiotapError {}
253
254pub fn build_radiotap_header(params: TxRadioParams) -> Vec<u8> {
256 if params.vht {
257 build_vht_radiotap_header(params)
258 } else {
259 build_ht_radiotap_header(params)
260 }
261}
262
263pub fn build_stream_radiotap(mode: TxMode) -> Vec<u8> {
265 match mode.kind {
266 TxModeKind::Legacy => build_legacy_radiotap_header(mode),
267 TxModeKind::Ht => build_ht_radiotap_header(TxRadioParams {
268 mcs_index: mode.ht_mcs,
269 bandwidth: mode.bandwidth,
270 short_gi: mode.short_gi,
271 stbc: u8::from(mode.stbc),
272 ldpc: mode.ldpc,
273 vht: false,
274 ..TxRadioParams::default()
275 }),
276 TxModeKind::Vht => build_vht_radiotap_header(TxRadioParams {
277 mcs_index: mode.vht_mcs,
278 nss: mode.vht_nss,
279 bandwidth: mode.bandwidth,
280 short_gi: mode.short_gi,
281 stbc: u8::from(mode.stbc),
282 ldpc: mode.ldpc,
283 vht: true,
284 ..TxRadioParams::default()
285 }),
286 }
287}
288
289pub fn build_legacy_radiotap_header(mode: TxMode) -> Vec<u8> {
291 vec![
292 0x00,
293 0x00,
294 RADIOTAP_LEGACY_LEN as u8,
295 0x00,
296 (RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) as u8,
297 ((RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) >> 8) as u8,
298 0x00,
299 0x00,
300 mode.legacy_rate_500kbps,
301 0x00,
302 (RADIOTAP_TX_FLAGS_NO_ACK & 0xff) as u8,
303 (RADIOTAP_TX_FLAGS_NO_ACK >> 8) as u8,
304 0x00,
305 ]
306}
307
308pub fn build_ht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
310 let known = IEEE80211_RADIOTAP_MCS_HAVE_MCS
311 | IEEE80211_RADIOTAP_MCS_HAVE_BW
312 | IEEE80211_RADIOTAP_MCS_HAVE_GI
313 | IEEE80211_RADIOTAP_MCS_HAVE_STBC
314 | IEEE80211_RADIOTAP_MCS_HAVE_FEC;
315 let mut flags = params.bandwidth.ht_mcs_bits();
316 if params.short_gi {
317 flags |= IEEE80211_RADIOTAP_MCS_SGI;
318 }
319 if params.ldpc {
320 flags |= IEEE80211_RADIOTAP_MCS_FEC_LDPC;
321 }
322 flags |= (params.stbc & 0x03) << IEEE80211_RADIOTAP_MCS_STBC_SHIFT;
323
324 vec![
325 0x00,
326 0x00,
327 RADIOTAP_HT_LEN as u8,
328 0x00,
329 0x00,
330 0x80,
331 0x08,
332 0x00,
333 0x08,
334 0x00,
335 known,
336 flags,
337 params.mcs_index.min(31),
338 ]
339}
340
341pub fn build_vht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
343 let mut flags = 0u8;
344 if params.stbc != 0 {
345 flags |= IEEE80211_RADIOTAP_VHT_FLAG_STBC;
346 }
347 if params.short_gi {
348 flags |= IEEE80211_RADIOTAP_VHT_FLAG_SGI;
349 }
350 let nss = params.nss.clamp(1, 4);
351 let mcs = params.mcs_index.min(9);
352 let mcs_nss0 = (mcs << 4) | nss;
353 let coding = if params.ldpc {
354 IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0
355 } else {
356 0
357 };
358
359 vec![
360 0x00,
361 0x00,
362 RADIOTAP_VHT_LEN as u8,
363 0x00,
364 0x00,
365 0x80,
366 0x20,
367 0x00,
368 0x08,
369 0x00,
370 0x45,
371 0x00,
372 flags,
373 params.bandwidth.vht_bits(),
374 mcs_nss0,
375 0x00,
376 0x00,
377 0x00,
378 coding,
379 0x00,
380 0x00,
381 0x00,
382 ]
383}
384
385pub fn radiotap_len(packet: &[u8]) -> Result<usize, RadiotapError> {
387 if packet.len() < 4 {
388 return Err(RadiotapError::TooShort);
389 }
390 let len = u16::from_le_bytes([packet[2], packet[3]]) as usize;
391 if len == 0 || len >= packet.len() {
392 return Err(RadiotapError::InvalidLength);
393 }
394 Ok(len)
395}
396
397pub fn parse_tx_mode_str(spec: &str) -> TxMode {
399 let trimmed = spec
400 .chars()
401 .filter(|ch| !ch.is_whitespace())
402 .flat_map(char::to_uppercase)
403 .collect::<String>();
404 if trimmed.is_empty() {
405 return TxMode::default();
406 }
407
408 let mut tokens = trimmed.split('/');
409 let Some(rate_token) = tokens.next() else {
410 return TxMode::default();
411 };
412 let Some(mut mode) = parse_tx_rate_token(rate_token) else {
413 return TxMode::default();
414 };
415
416 for token in tokens {
417 match token {
418 "SGI" => mode.short_gi = true,
419 "LDPC" => mode.ldpc = true,
420 "STBC" => mode.stbc = true,
421 "20" => mode.bandwidth = ChannelBandwidth::Mhz20,
422 "40" => mode.bandwidth = ChannelBandwidth::Mhz40,
423 "80" => mode.bandwidth = ChannelBandwidth::Mhz80,
424 "160" => mode.bandwidth = ChannelBandwidth::Mhz160,
425 _ => {}
426 }
427 }
428 mode
429}
430
431pub fn parse_radiotap_tx_mode(packet: &[u8]) -> Result<Option<TxMode>, RadiotapError> {
433 let len = radiotap_len(packet)?;
434 if len < 8 || packet.len() < len {
435 return Err(RadiotapError::InvalidLength);
436 }
437 let present = u32::from_le_bytes(packet[4..8].try_into().expect("present word is in range"));
438 if present & (1 << 31) != 0 {
439 return Err(RadiotapError::UnsupportedHeader);
440 }
441
442 let mut offset = 8usize;
443 let mut mode = None;
444
445 if present & RADIOTAP_PRESENT_RATE != 0 {
446 require_field(packet, len, offset, 1)?;
447 mode = Some(TxMode::legacy(packet[offset]));
448 offset += 1;
449 }
450
451 if present & RADIOTAP_PRESENT_TX_FLAGS != 0 {
452 offset = align(offset, 2);
453 require_field(packet, len, offset, 2)?;
454 offset += 2;
455 }
456
457 if present & RADIOTAP_PRESENT_MCS != 0 {
458 require_field(packet, len, offset, 3)?;
459 let known = packet[offset];
460 let flags = packet[offset + 1];
461 let mcs = packet[offset + 2];
462 let mut ht = TxMode::ht(if known & IEEE80211_RADIOTAP_MCS_HAVE_MCS != 0 {
463 mcs.min(31)
464 } else {
465 0
466 });
467 ht.bandwidth = if known & IEEE80211_RADIOTAP_MCS_HAVE_BW != 0 && flags & 0x03 == 1 {
468 ChannelBandwidth::Mhz40
469 } else {
470 ChannelBandwidth::Mhz20
471 };
472 ht.short_gi =
473 known & IEEE80211_RADIOTAP_MCS_HAVE_GI != 0 && flags & IEEE80211_RADIOTAP_MCS_SGI != 0;
474 ht.ldpc = known & IEEE80211_RADIOTAP_MCS_HAVE_FEC != 0
475 && flags & IEEE80211_RADIOTAP_MCS_FEC_LDPC != 0;
476 ht.stbc = known & IEEE80211_RADIOTAP_MCS_HAVE_STBC != 0
477 && ((flags >> IEEE80211_RADIOTAP_MCS_STBC_SHIFT) & 0x03) != 0;
478 mode = Some(ht);
479 offset += 3;
480 }
481
482 if present & RADIOTAP_PRESENT_VHT != 0 {
483 offset = align(offset, 2);
484 require_field(packet, len, offset, 12)?;
485 let known = u16::from_le_bytes([packet[offset], packet[offset + 1]]);
486 let flags = packet[offset + 2];
487 let bandwidth = match packet[offset + 3] & 0x1f {
488 1..=3 => ChannelBandwidth::Mhz40,
489 4..=10 => ChannelBandwidth::Mhz80,
490 11..=31 => ChannelBandwidth::Mhz160,
491 _ => ChannelBandwidth::Mhz20,
492 };
493 let mcs_nss = packet[offset + 4];
494 let mut vht = TxMode::vht((mcs_nss & 0x0f).clamp(1, 4), ((mcs_nss >> 4) & 0x0f).min(9));
495 if known & (1 << 6) != 0 {
496 vht.bandwidth = bandwidth;
497 }
498 vht.short_gi = known & (1 << 2) != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_SGI != 0;
499 vht.stbc = known & 1 != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_STBC != 0;
500 vht.ldpc = packet[offset + 8] & IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0 != 0;
501 mode = Some(vht);
502 }
503
504 Ok(mode)
505}
506
507pub fn parse_radiotap_tx_info(packet: &[u8]) -> Result<RadiotapTxInfo, RadiotapError> {
509 match parse_radiotap_tx_mode(packet)? {
510 Some(mode) => Ok(RadiotapTxInfo {
511 vht: mode.kind == TxModeKind::Vht,
512 mcs_index: match mode.kind {
513 TxModeKind::Legacy | TxModeKind::Ht => mode.ht_mcs,
514 TxModeKind::Vht => mode.vht_mcs,
515 },
516 nss: mode.vht_nss,
517 bandwidth: mode.bandwidth,
518 short_gi: mode.short_gi,
519 stbc: u8::from(mode.stbc),
520 ldpc: mode.ldpc,
521 }),
522 None => Err(RadiotapError::UnsupportedHeader),
523 }
524}
525
526fn parse_tx_rate_token(token: &str) -> Option<TxMode> {
527 match token {
528 "1M" => Some(TxMode::legacy(2)),
529 "2M" => Some(TxMode::legacy(4)),
530 "5.5M" | "5M" => Some(TxMode::legacy(11)),
531 "6M" => Some(TxMode::legacy(12)),
532 "9M" => Some(TxMode::legacy(18)),
533 "11M" => Some(TxMode::legacy(22)),
534 "12M" => Some(TxMode::legacy(24)),
535 "18M" => Some(TxMode::legacy(36)),
536 "24M" => Some(TxMode::legacy(48)),
537 "36M" => Some(TxMode::legacy(72)),
538 "48M" => Some(TxMode::legacy(96)),
539 "54M" => Some(TxMode::legacy(108)),
540 _ => {
541 if let Some(raw) = token.strip_prefix("MCS") {
542 return raw
543 .parse::<u8>()
544 .ok()
545 .filter(|mcs| *mcs <= 31)
546 .map(TxMode::ht);
547 }
548 if let Some(raw) = token.strip_prefix("VHT") {
549 let (nss_raw, mcs_raw) = raw.split_once("SS_MCS")?;
550 let nss = nss_raw.parse::<u8>().ok()?;
551 let mcs = mcs_raw.parse::<u8>().ok()?;
552 if (1..=4).contains(&nss) && mcs <= 9 {
553 return Some(TxMode::vht(nss, mcs));
554 }
555 }
556 None
557 }
558 }
559}
560
561const fn align(offset: usize, alignment: usize) -> usize {
562 (offset + alignment - 1) & !(alignment - 1)
563}
564
565fn require_field(
566 packet: &[u8],
567 radiotap_len: usize,
568 offset: usize,
569 len: usize,
570) -> Result<(), RadiotapError> {
571 if offset + len <= radiotap_len && offset + len <= packet.len() {
572 Ok(())
573 } else {
574 Err(RadiotapError::InvalidLength)
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn ht_header_roundtrips_tx_info() {
584 let params = TxRadioParams {
585 mcs_index: 3,
586 bandwidth: ChannelBandwidth::Mhz40,
587 short_gi: true,
588 stbc: 1,
589 ldpc: true,
590 ..TxRadioParams::default()
591 };
592 let mut packet = build_radiotap_header(params);
593 packet.extend_from_slice(&[0u8; 24]);
594 let parsed = parse_radiotap_tx_info(&packet).unwrap();
595 assert_eq!(parsed.mcs_index, 3);
596 assert_eq!(parsed.bandwidth, ChannelBandwidth::Mhz40);
597 assert!(parsed.short_gi);
598 assert!(parsed.ldpc);
599 assert_eq!(parsed.stbc, 1);
600 }
601
602 #[test]
603 fn parses_devourer_tx_mode_strings() {
604 let mode = parse_tx_mode_str("vht2ss_mcs5/80/sgi/ldpc/stbc");
605 assert_eq!(mode.kind, TxModeKind::Vht);
606 assert_eq!(mode.vht_nss, 2);
607 assert_eq!(mode.vht_mcs, 5);
608 assert_eq!(mode.bandwidth, ChannelBandwidth::Mhz80);
609 assert!(mode.short_gi);
610 assert!(mode.ldpc);
611 assert!(mode.stbc);
612 }
613
614 #[test]
615 fn legacy_stream_radiotap_carries_rate_and_tx_flags() {
616 let mode = TxMode::legacy(12);
617 let mut packet = build_stream_radiotap(mode);
618 packet.extend_from_slice(&[0u8; 24]);
619 let parsed = parse_radiotap_tx_mode(&packet).unwrap().unwrap();
620 assert_eq!(parsed.kind, TxModeKind::Legacy);
621 assert_eq!(parsed.legacy_rate_500kbps, 12);
622 }
623}