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