341 lines
15 KiB
Rust
341 lines
15 KiB
Rust
//! The Nexmon-supported Broadcom chip registry and Raspberry Pi model map
|
|
//! (ADR-095 D15, ADR-096) — including the **Raspberry Pi 5**.
|
|
//!
|
|
//! nexmon_csi runs on a handful of patched Broadcom/Cypress chips. This module
|
|
//! names them ([`NexmonChip`]), maps Raspberry Pi models to their chip
|
|
//! ([`RaspberryPiModel`]), resolves the on-the-wire `chip_ver` word back to a
|
|
//! chip (best-effort — the raw value is always preserved), and builds a
|
|
//! [`rvcsi_core::AdapterProfile`] (supported channels / bandwidths / expected
|
|
//! subcarrier counts) for each — so `validate_frame` can bound CSI frames
|
|
//! against the device that produced them.
|
|
//!
|
|
//! The Raspberry Pi 5 carries the same **CYW43455 (BCM43455c0)** 802.11ac
|
|
//! wireless as the Pi 3B+ / Pi 4 / Pi 400 — the chip with the most mature
|
|
//! nexmon_csi support — so Pi 5 CSI captures use the [`NexmonChip::Bcm43455c0`]
|
|
//! profile (20/40/80 MHz, 64/128/256 subcarriers, 2.4 + 5 GHz). The chip is also
|
|
//! auto-detected at runtime from each frame's `chip_ver` (see
|
|
//! [`crate::NexmonPcapAdapter`]).
|
|
|
|
use rvcsi_core::{AdapterKind, AdapterProfile};
|
|
|
|
/// A Broadcom/Cypress WiFi chip nexmon_csi is known to run on.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[non_exhaustive]
|
|
pub enum NexmonChip {
|
|
/// BCM43455c0 / CYW43455 — 802.11ac, 2.4 + 5 GHz, 20/40/80 MHz. The
|
|
/// flagship nexmon_csi target: **Raspberry Pi 3B+, Pi 4, Pi 400 and Pi 5**,
|
|
/// plus the Pi Zero W. Modern int16 I/Q CSI export.
|
|
Bcm43455c0,
|
|
/// BCM43436b0 — 802.11n, 2.4 GHz only, 20/40 MHz. Raspberry Pi Zero 2 W.
|
|
Bcm43436b0,
|
|
/// BCM4366c0 — 802.11ac, 2.4 + 5 GHz, up to 80 MHz. ASUS RT-AC86U. Modern int16 export.
|
|
Bcm4366c0,
|
|
/// BCM4375b1 — 802.11ax-class, 2.4 + 5 GHz. Some Samsung Galaxy S10/S20.
|
|
Bcm4375b1,
|
|
/// BCM4358 — 802.11ac. Nexus 6P (and similar). Some firmwares use the legacy
|
|
/// packed-float CSI export (see [`NexmonChip::uses_int16_iq`]).
|
|
Bcm4358,
|
|
/// BCM4339 — 802.11ac. Nexus 5. Legacy packed-float CSI export.
|
|
Bcm4339,
|
|
/// A chip we don't recognise — the raw `chip_ver` word from the packet.
|
|
Unknown {
|
|
/// The `chip_ver` word as it appeared on the wire.
|
|
chip_ver: u16,
|
|
},
|
|
}
|
|
|
|
impl NexmonChip {
|
|
/// Stable lower-case slug (`"bcm43455c0"`, `"bcm4366c0"`, ...; `"unknown:0xNNNN"` for [`NexmonChip::Unknown`]).
|
|
pub fn slug(self) -> String {
|
|
match self {
|
|
NexmonChip::Bcm43455c0 => "bcm43455c0".to_string(),
|
|
NexmonChip::Bcm43436b0 => "bcm43436b0".to_string(),
|
|
NexmonChip::Bcm4366c0 => "bcm4366c0".to_string(),
|
|
NexmonChip::Bcm4375b1 => "bcm4375b1".to_string(),
|
|
NexmonChip::Bcm4358 => "bcm4358".to_string(),
|
|
NexmonChip::Bcm4339 => "bcm4339".to_string(),
|
|
NexmonChip::Unknown { chip_ver } => format!("unknown:0x{chip_ver:04x}"),
|
|
}
|
|
}
|
|
|
|
/// A friendlier display name including a typical host device.
|
|
pub fn description(self) -> &'static str {
|
|
match self {
|
|
NexmonChip::Bcm43455c0 => "BCM43455c0 / CYW43455 (Raspberry Pi 3B+/4/400/5, Pi Zero W) — 802.11ac, 2.4+5 GHz",
|
|
NexmonChip::Bcm43436b0 => "BCM43436b0 (Raspberry Pi Zero 2 W) — 802.11n, 2.4 GHz",
|
|
NexmonChip::Bcm4366c0 => "BCM4366c0 (ASUS RT-AC86U) — 802.11ac, 2.4+5 GHz",
|
|
NexmonChip::Bcm4375b1 => "BCM4375b1 (Samsung Galaxy S10/S20) — 802.11ax-class, 2.4+5 GHz",
|
|
NexmonChip::Bcm4358 => "BCM4358 (Nexus 6P) — 802.11ac",
|
|
NexmonChip::Bcm4339 => "BCM4339 (Nexus 5) — 802.11ac",
|
|
NexmonChip::Unknown { .. } => "unknown Broadcom/Cypress chip",
|
|
}
|
|
}
|
|
|
|
/// Whether this chip's nexmon_csi firmware exports CSI in the modern int16
|
|
/// LE I/Q format ([`crate::NEXMON_CSI_FMT_INT16_IQ`]). The BCM4339 and some
|
|
/// BCM4358 firmwares use the legacy *packed-float* export instead (not yet
|
|
/// implemented by the shim — see `ffi::NEXMON_CSI_FMT_INT16_IQ`).
|
|
pub fn uses_int16_iq(self) -> bool {
|
|
!matches!(self, NexmonChip::Bcm4339 | NexmonChip::Bcm4358)
|
|
}
|
|
|
|
/// Whether the chip supports the 5 GHz band (and therefore 802.11ac wide channels).
|
|
pub fn dual_band(self) -> bool {
|
|
matches!(
|
|
self,
|
|
NexmonChip::Bcm43455c0 | NexmonChip::Bcm4366c0 | NexmonChip::Bcm4375b1 | NexmonChip::Bcm4358 | NexmonChip::Bcm4339
|
|
)
|
|
}
|
|
|
|
/// Resolve a `chip_ver` word from a nexmon_csi UDP header to a chip
|
|
/// (best-effort — matches the Broadcom chip-ID convention `0x4345` = BCM4345
|
|
/// family, `0x4339`, `0x4358`, `0x4366`, `0x4375`; anything else is
|
|
/// [`NexmonChip::Unknown`]). The c0/b0 revision suffix isn't carried by this
|
|
/// word; the int16-vs-packed-float export distinction is handled separately.
|
|
pub fn from_chip_ver(chip_ver: u16) -> NexmonChip {
|
|
match chip_ver {
|
|
0x4345 => NexmonChip::Bcm43455c0,
|
|
0x4339 => NexmonChip::Bcm4339,
|
|
0x4358 => NexmonChip::Bcm4358,
|
|
0x4366 => NexmonChip::Bcm4366c0,
|
|
0x4375 => NexmonChip::Bcm4375b1,
|
|
// 43436's chip id varies by source; treat it as unknown unless we see it.
|
|
other => NexmonChip::Unknown { chip_ver: other },
|
|
}
|
|
}
|
|
|
|
/// Parse a chip name/slug (`"bcm43455c0"`, `"43455c0"`, `"cyw43455"`, ...).
|
|
pub fn from_slug(s: &str) -> Option<NexmonChip> {
|
|
let s = s.trim().to_ascii_lowercase();
|
|
match s.as_str() {
|
|
"bcm43455c0" | "43455c0" | "43455" | "bcm43455" | "cyw43455" => Some(NexmonChip::Bcm43455c0),
|
|
"bcm43436b0" | "43436b0" | "43436" | "bcm43436" => Some(NexmonChip::Bcm43436b0),
|
|
"bcm4366c0" | "4366c0" | "4366" | "bcm4366" => Some(NexmonChip::Bcm4366c0),
|
|
"bcm4375b1" | "4375b1" | "4375" | "bcm4375" => Some(NexmonChip::Bcm4375b1),
|
|
"bcm4358" | "4358" => Some(NexmonChip::Bcm4358),
|
|
"bcm4339" | "4339" => Some(NexmonChip::Bcm4339),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 5 GHz UNII channels (a representative set; nexmon picks a control channel via `makecsiparams`).
|
|
const FIVE_GHZ_CHANNELS: &[u16] = &[
|
|
36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149,
|
|
153, 157, 161, 165,
|
|
];
|
|
|
|
fn channels_for(chip: NexmonChip) -> Vec<u16> {
|
|
let mut v: Vec<u16> = (1..=13).collect();
|
|
if chip.dual_band() {
|
|
v.extend_from_slice(FIVE_GHZ_CHANNELS);
|
|
}
|
|
v
|
|
}
|
|
|
|
fn bandwidths_for(chip: NexmonChip) -> Vec<u16> {
|
|
match chip {
|
|
NexmonChip::Bcm43455c0 | NexmonChip::Bcm4366c0 | NexmonChip::Bcm4358 | NexmonChip::Bcm4339 => vec![20, 40, 80],
|
|
NexmonChip::Bcm4375b1 => vec![20, 40, 80, 160],
|
|
NexmonChip::Bcm43436b0 => vec![20, 40],
|
|
NexmonChip::Unknown { .. } => vec![20, 40, 80],
|
|
}
|
|
}
|
|
|
|
/// Subcarrier (FFT) count per supported bandwidth: 20→64, 40→128, 80→256, 160→512.
|
|
fn subcarrier_counts_for(chip: NexmonChip) -> Vec<u16> {
|
|
bandwidths_for(chip)
|
|
.iter()
|
|
.map(|bw| (bw / 20) * 64)
|
|
.collect()
|
|
}
|
|
|
|
/// Build the [`rvcsi_core::AdapterProfile`] for a Nexmon chip — the channels /
|
|
/// bandwidths / expected subcarrier counts `validate_frame` will bound CSI
|
|
/// frames against, plus the live-capability flags (Nexmon supports monitor mode
|
|
/// and injection on these chips).
|
|
pub fn nexmon_adapter_profile(chip: NexmonChip) -> AdapterProfile {
|
|
AdapterProfile {
|
|
adapter_kind: AdapterKind::Nexmon,
|
|
chip: Some(chip.slug()),
|
|
firmware_version: None,
|
|
driver_version: None,
|
|
supported_channels: channels_for(chip),
|
|
supported_bandwidths_mhz: bandwidths_for(chip),
|
|
expected_subcarrier_counts: subcarrier_counts_for(chip),
|
|
supports_live_capture: true,
|
|
supports_injection: true,
|
|
supports_monitor_mode: true,
|
|
}
|
|
}
|
|
|
|
/// Raspberry Pi models with on-board WiFi that nexmon_csi can extract CSI from.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[non_exhaustive]
|
|
pub enum RaspberryPiModel {
|
|
/// Raspberry Pi 3 Model B+ — CYW43455 / BCM43455c0.
|
|
Pi3BPlus,
|
|
/// Raspberry Pi 4 Model B — CYW43455 / BCM43455c0.
|
|
Pi4,
|
|
/// Raspberry Pi 400 — CYW43455 / BCM43455c0.
|
|
Pi400,
|
|
/// **Raspberry Pi 5** — CYW43455 / BCM43455c0 (same wireless as the Pi 4).
|
|
Pi5,
|
|
/// Raspberry Pi Zero W — CYW43438? No — the Zero W uses the BCM43438 (2.4 GHz
|
|
/// only), which nexmon_csi does **not** support; included here only so callers
|
|
/// can detect and reject it. Use a Zero 2 W instead.
|
|
PiZeroW,
|
|
/// Raspberry Pi Zero 2 W — BCM43436b0 (2.4 GHz only).
|
|
PiZero2W,
|
|
}
|
|
|
|
impl RaspberryPiModel {
|
|
/// The Broadcom/Cypress WiFi chip on this board.
|
|
pub fn nexmon_chip(self) -> NexmonChip {
|
|
match self {
|
|
RaspberryPiModel::Pi3BPlus
|
|
| RaspberryPiModel::Pi4
|
|
| RaspberryPiModel::Pi400
|
|
| RaspberryPiModel::Pi5 => NexmonChip::Bcm43455c0,
|
|
RaspberryPiModel::PiZero2W => NexmonChip::Bcm43436b0,
|
|
RaspberryPiModel::PiZeroW => NexmonChip::Unknown { chip_ver: 0x4343 }, // BCM43438 — not CSI-capable
|
|
}
|
|
}
|
|
|
|
/// Whether nexmon_csi can extract CSI from this board's WiFi.
|
|
pub fn csi_supported(self) -> bool {
|
|
!matches!(self, RaspberryPiModel::PiZeroW)
|
|
}
|
|
|
|
/// Stable slug (`"pi5"`, `"pi4"`, `"pi3b+"`, `"pi400"`, `"pizero2w"`, `"pizerow"`).
|
|
pub fn slug(self) -> &'static str {
|
|
match self {
|
|
RaspberryPiModel::Pi3BPlus => "pi3b+",
|
|
RaspberryPiModel::Pi4 => "pi4",
|
|
RaspberryPiModel::Pi400 => "pi400",
|
|
RaspberryPiModel::Pi5 => "pi5",
|
|
RaspberryPiModel::PiZeroW => "pizerow",
|
|
RaspberryPiModel::PiZero2W => "pizero2w",
|
|
}
|
|
}
|
|
|
|
/// Parse a model slug (accepts `pi5`, `pi 5`, `rpi5`, `raspberrypi5`, `pi3b+`/`pi3bplus`, ...).
|
|
pub fn from_slug(s: &str) -> Option<RaspberryPiModel> {
|
|
let s: String = s.trim().to_ascii_lowercase().chars().filter(|c| !c.is_whitespace() && *c != '_' && *c != '-').collect();
|
|
let s = s.strip_prefix("raspberrypi").or_else(|| s.strip_prefix("rpi")).unwrap_or(&s);
|
|
match s {
|
|
"pi5" | "5" => Some(RaspberryPiModel::Pi5),
|
|
"pi4" | "4" | "pi4b" => Some(RaspberryPiModel::Pi4),
|
|
"pi400" | "400" => Some(RaspberryPiModel::Pi400),
|
|
"pi3b+" | "pi3bplus" | "3b+" | "3bplus" => Some(RaspberryPiModel::Pi3BPlus),
|
|
"pizero2w" | "zero2w" | "pizero2" => Some(RaspberryPiModel::PiZero2W),
|
|
"pizerow" | "zerow" => Some(RaspberryPiModel::PiZeroW),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build the [`rvcsi_core::AdapterProfile`] for a Raspberry Pi model (its
|
|
/// [`RaspberryPiModel::nexmon_chip`]'s profile, with the `chip` string tagged
|
|
/// with the model for legibility).
|
|
pub fn raspberry_pi_profile(model: RaspberryPiModel) -> AdapterProfile {
|
|
let mut p = nexmon_adapter_profile(model.nexmon_chip());
|
|
p.chip = Some(format!("{} ({})", model.nexmon_chip().slug(), model.slug()));
|
|
p
|
|
}
|
|
|
|
/// The full registry of Nexmon-supported chips, for `rvcsi nexmon-chips` and SDK callers.
|
|
pub fn known_chips() -> &'static [NexmonChip] {
|
|
&[
|
|
NexmonChip::Bcm43455c0,
|
|
NexmonChip::Bcm43436b0,
|
|
NexmonChip::Bcm4366c0,
|
|
NexmonChip::Bcm4375b1,
|
|
NexmonChip::Bcm4358,
|
|
NexmonChip::Bcm4339,
|
|
]
|
|
}
|
|
|
|
/// The full registry of Raspberry Pi models this crate knows about.
|
|
pub fn known_pi_models() -> &'static [RaspberryPiModel] {
|
|
&[
|
|
RaspberryPiModel::Pi5,
|
|
RaspberryPiModel::Pi4,
|
|
RaspberryPiModel::Pi400,
|
|
RaspberryPiModel::Pi3BPlus,
|
|
RaspberryPiModel::PiZero2W,
|
|
RaspberryPiModel::PiZeroW,
|
|
]
|
|
}
|
|
|
|
impl crate::ffi::NexmonCsiHeader {
|
|
/// Resolve this packet's chip from its `chip_ver` word (best-effort; the raw
|
|
/// `chip_ver` field is always preserved). For a Raspberry Pi 5 (or 4/400/3B+)
|
|
/// capture this returns [`NexmonChip::Bcm43455c0`].
|
|
pub fn chip(&self) -> NexmonChip {
|
|
NexmonChip::from_chip_ver(self.chip_ver)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn pi5_uses_the_same_chip_as_pi4() {
|
|
assert_eq!(RaspberryPiModel::Pi5.nexmon_chip(), NexmonChip::Bcm43455c0);
|
|
assert_eq!(RaspberryPiModel::Pi4.nexmon_chip(), NexmonChip::Bcm43455c0);
|
|
assert!(RaspberryPiModel::Pi5.csi_supported());
|
|
let p = raspberry_pi_profile(RaspberryPiModel::Pi5);
|
|
assert_eq!(p.adapter_kind, AdapterKind::Nexmon);
|
|
assert!(p.chip.as_deref().unwrap().contains("pi5"));
|
|
assert_eq!(p.supported_bandwidths_mhz, vec![20, 40, 80]);
|
|
assert_eq!(p.expected_subcarrier_counts, vec![64, 128, 256]);
|
|
assert!(p.accepts_channel(36)); // 5 GHz
|
|
assert!(p.accepts_channel(6)); // 2.4 GHz
|
|
assert!(p.accepts_subcarrier_count(256)); // VHT80
|
|
assert!(!p.accepts_subcarrier_count(57));
|
|
assert!(p.supports_monitor_mode && p.supports_injection);
|
|
}
|
|
|
|
#[test]
|
|
fn chip_ver_resolution_best_effort() {
|
|
assert_eq!(NexmonChip::from_chip_ver(0x4345), NexmonChip::Bcm43455c0);
|
|
assert_eq!(NexmonChip::from_chip_ver(0x4339), NexmonChip::Bcm4339);
|
|
assert_eq!(NexmonChip::from_chip_ver(0x4366), NexmonChip::Bcm4366c0);
|
|
assert!(matches!(NexmonChip::from_chip_ver(0xABCD), NexmonChip::Unknown { chip_ver: 0xABCD }));
|
|
}
|
|
|
|
#[test]
|
|
fn chip_traits() {
|
|
assert!(NexmonChip::Bcm43455c0.uses_int16_iq());
|
|
assert!(!NexmonChip::Bcm4339.uses_int16_iq());
|
|
assert!(NexmonChip::Bcm43455c0.dual_band());
|
|
assert!(!NexmonChip::Bcm43436b0.dual_band());
|
|
assert_eq!(nexmon_adapter_profile(NexmonChip::Bcm43436b0).supported_bandwidths_mhz, vec![20, 40]);
|
|
assert_eq!(nexmon_adapter_profile(NexmonChip::Bcm43436b0).expected_subcarrier_counts, vec![64, 128]);
|
|
// unknown chip -> a permissive-ish 802.11ac default
|
|
let u = nexmon_adapter_profile(NexmonChip::Unknown { chip_ver: 0 });
|
|
assert_eq!(u.supported_bandwidths_mhz, vec![20, 40, 80]);
|
|
}
|
|
|
|
#[test]
|
|
fn slug_parsing() {
|
|
assert_eq!(NexmonChip::from_slug("CYW43455"), Some(NexmonChip::Bcm43455c0));
|
|
assert_eq!(NexmonChip::from_slug("bcm4366c0"), Some(NexmonChip::Bcm4366c0));
|
|
assert_eq!(NexmonChip::from_slug("nope"), None);
|
|
assert_eq!(RaspberryPiModel::from_slug("Pi 5"), Some(RaspberryPiModel::Pi5));
|
|
assert_eq!(RaspberryPiModel::from_slug("raspberry-pi-5"), Some(RaspberryPiModel::Pi5));
|
|
assert_eq!(RaspberryPiModel::from_slug("pi3bplus"), Some(RaspberryPiModel::Pi3BPlus));
|
|
assert_eq!(RaspberryPiModel::from_slug("pi42"), None);
|
|
assert_eq!(NexmonChip::Bcm43455c0.slug(), "bcm43455c0");
|
|
assert_eq!(RaspberryPiModel::Pi5.slug(), "pi5");
|
|
}
|
|
|
|
#[test]
|
|
fn registries_nonempty_and_pi5_present() {
|
|
assert!(known_chips().contains(&NexmonChip::Bcm43455c0));
|
|
assert!(known_pi_models().contains(&RaspberryPiModel::Pi5));
|
|
}
|
|
}
|