feat(rvcsi): Raspberry Pi 5 (BCM43455c0) + Nexmon chip registry

Adds first-class support for the Raspberry Pi 5's WiFi chip (CYW43455 /
BCM43455c0 — the same 802.11ac wireless as the Pi 4 / Pi 3B+ / Pi 400, and the
chip with the most mature nexmon_csi support), plus a registry of the other
Nexmon-supported Broadcom/Cypress chips.

rvcsi-adapter-nexmon — new `chips.rs`:
- `NexmonChip` (Bcm43455c0, Bcm43436b0, Bcm4366c0, Bcm4375b1, Bcm4358, Bcm4339,
  Unknown{chip_ver}) + `RaspberryPiModel` (Pi5/Pi4/Pi400/Pi3BPlus/PiZero2W/
  PiZeroW) — Pi5/Pi4/Pi400/Pi3B+ → Bcm43455c0; PiZero2W → Bcm43436b0.
- `nexmon_adapter_profile(chip)` / `raspberry_pi_profile(model)` build the
  per-device `AdapterProfile` (channels: 2.4 GHz 1-13 + 5 GHz UNII for dual-band;
  bandwidths 20/40/80[/160]; expected subcarrier counts 64/128/256[/512]) that
  `validate_frame` bounds CSI frames against.
- `NexmonChip::from_chip_ver` (0x4345 → Bcm43455c0, 0x4339, 0x4358, 0x4366,
  0x4375 — best-effort; the raw `chip_ver` is always preserved) and `from_slug`
  / `RaspberryPiModel::from_slug` ("pi5", "raspberry pi 4", "bcm43455c0", ...).
- `NexmonCsiHeader::chip()`; `NexmonPcapAdapter` auto-detects the chip from the
  packets' `chip_ver` and uses the matching profile, overridable via
  `.with_chip(NexmonChip)` / `.with_pi_model(RaspberryPiModel)`; `.detected_chip()`.

rvcsi-runtime: `decode_nexmon_pcap_for(.., chip_spec)` (validate against a chip /
Pi model, drop non-conforming) + `nexmon_profile_for(spec)`; `NexmonPcapSummary`
gains `chip_names` + `detected_chip`; `CaptureSummary` gains `chip`.

rvcsi-cli: `record --source nexmon-pcap --chip pi5`; new `nexmon-chips`
subcommand (lists chips + Pi models, human or `--json`); `inspect-nexmon` and
`inspect` now print the resolved chip.

rvcsi-node (napi-rs): `nexmonDecodePcap` gains an optional `chip` arg;
`nexmonChipName(chipVer)`, `nexmonProfile(spec)`, `nexmonChips()`. @ruv/rvcsi
SDK + `.d.ts` updated (AdapterProfile / NexmonChipsListing interfaces, the new
fns, `chip` on CaptureSummary, `chip_names`/`detected_chip` on NexmonPcapSummary).

168 rvcsi tests pass (adapter-nexmon 22→28, cli 9→10), 0 failures, clippy-clean.
The synthetic test captures now stamp chip_ver = 0x4345 (the BCM4345 family chip
ID), so the chip-detection happy path is exercised end to end.
ADR-096, CHANGELOG, README, CLAUDE.md updated.

https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
This commit is contained in:
Claude
2026-05-13 01:32:27 +00:00
parent b116a99481
commit d40411e6d7
17 changed files with 815 additions and 63 deletions
@@ -21,6 +21,9 @@ test('exports the expected functions and class', () => {
'nexmonDecodePcap',
'inspectNexmonPcap',
'decodeChanspec',
'nexmonChipName',
'nexmonProfile',
'nexmonChips',
'inspectCaptureFile',
'eventsFromCaptureFile',
'exportCaptureToRfMemory',
+70 -2
View File
@@ -108,12 +108,29 @@ export interface ValidationBreakdown {
recovered: number;
}
/** A source's capability descriptor (channels / bandwidths / expected subcarrier counts). */
export interface AdapterProfile {
adapter_kind: AdapterKind;
/** Chip string, e.g. `"bcm43455c0 (pi5)"`, or `null`. */
chip: string | null;
firmware_version: string | null;
driver_version: string | null;
supported_channels: number[];
supported_bandwidths_mhz: number[];
expected_subcarrier_counts: number[];
supports_live_capture: boolean;
supports_injection: boolean;
supports_monitor_mode: boolean;
}
/** Compact summary of a `.rvcsi` capture file. */
export interface CaptureSummary {
capture_version: number;
session_id: number;
source_id: string;
adapter_kind: string;
/** The header's adapter-profile `chip` string, if any (e.g. `"bcm43455c0 (pi5)"`). */
chip: string | null;
frame_count: number;
first_timestamp_ns: number;
last_timestamp_ns: number;
@@ -136,8 +153,12 @@ export interface NexmonPcapSummary {
channels: number[];
bandwidths_mhz: number[];
subcarrier_counts: number[];
/** Distinct chip-version words (e.g. 0x0142 = BCM43455c0). */
/** Distinct chip-version words (e.g. 0x4345 = the BCM4345 family). */
chip_versions: number[];
/** Distinct resolved chip slugs (`"bcm43455c0"` for a Raspberry Pi 3B+/4/400/5). */
chip_names: string[];
/** The chip the adapter settled on (all packets agreed) — `"bcm43455c0"` for a Pi 5 capture. */
detected_chip: string;
/** `[min, max]` RSSI in dBm, or `null` for an empty capture. */
rssi_dbm_range: [number, number] | null;
}
@@ -153,6 +174,35 @@ export interface DecodedChanspec {
is_5ghz: boolean;
}
/** One Nexmon-supported chip in the {@link nexmonChips} listing. */
export interface NexmonChipInfo {
/** Slug, e.g. `"bcm43455c0"`. */
slug: string;
/** Human description incl. a typical host device. */
description: string;
/** Whether the chip supports the 5 GHz band. */
dualBand: boolean;
/** Whether its firmware exports CSI in the modern int16 I/Q format. */
int16IqExport: boolean;
bandwidthsMhz: number[];
expectedSubcarrierCounts: number[];
}
/** One Raspberry Pi model in the {@link nexmonChips} listing. */
export interface RaspberryPiModelInfo {
/** Slug, e.g. `"pi5"`. */
slug: string;
/** The chip on this board (`"bcm43455c0"` for the Pi 5), or `null` if not CSI-capable. */
chip: string | null;
csiSupported: boolean;
}
/** The {@link nexmonChips} listing. */
export interface NexmonChipsListing {
chips: NexmonChipInfo[];
raspberryPiModels: RaspberryPiModelInfo[];
}
/** rvCSI runtime version string. */
export function rvcsiVersion(): string;
@@ -180,13 +230,16 @@ export function exportCaptureToRfMemory(capturePath: string, outJsonlPath: strin
/**
* Decode the *real* nexmon_csi UDP payloads inside a libpcap `.pcap` buffer
* into validated frames. `port` defaults to 5500. Throws on a non-pcap buffer.
* into validated frames. `port` defaults to 5500. `chip` (`'pi5'`,
* `'bcm43455c0'`, ...) validates against that device's profile and drops the
* non-conforming frames. Throws on a non-pcap buffer or an unknown `chip`.
*/
export function nexmonDecodePcap(
pcap: Buffer | Uint8Array,
sourceId: string,
sessionId: number,
port?: number,
chip?: string,
): CsiFrame[];
/** Summarize a nexmon_csi `.pcap` file. `port` defaults to 5500. */
@@ -195,6 +248,21 @@ export function inspectNexmonPcap(path: string, port?: number): NexmonPcapSummar
/** Decode a Broadcom d11ac chanspec word. */
export function decodeChanspec(chanspec: number): DecodedChanspec;
/**
* Resolve a `chip_ver` word from a nexmon_csi packet to a chip slug
* (`'bcm43455c0'` for a Raspberry Pi 3B+/4/400/5; `'unknown:0xNNNN'` otherwise).
*/
export function nexmonChipName(chipVer: number): string;
/**
* The {@link AdapterProfile} for a chip / Raspberry-Pi-model spec (`'pi5'`,
* `'bcm43455c0'`, `'raspberry pi 4'`, ...). Throws on an unknown spec.
*/
export function nexmonProfile(spec: string): AdapterProfile;
/** Listing of the Nexmon-supported chips + Raspberry Pi models (incl. the Pi 5 → BCM43455c0). */
export function nexmonChips(): NexmonChipsListing;
/** Streaming capture runtime: a source + the DSP stage + the event pipeline. */
export class RvCsi {
private constructor(rt: unknown);
+44 -3
View File
@@ -98,17 +98,25 @@ function exportCaptureToRfMemory(capturePath, outJsonlPath) {
* @param {string} sourceId
* @param {number} sessionId
* @param {number} [port] CSI UDP port (default 5500)
* @param {string} [chip] chip / Raspberry-Pi-model spec to validate against
* (e.g. `'pi5'`, `'bcm43455c0'`); non-conforming frames are dropped
* @returns {import('./index').CsiFrame[]}
*/
function nexmonDecodePcap(pcap, sourceId, sessionId, port) {
function nexmonDecodePcap(pcap, sourceId, sessionId, port, chip) {
return JSON.parse(
binding().nexmonDecodePcap(pcap, String(sourceId), u32(sessionId), port == null ? undefined : Number(port)),
binding().nexmonDecodePcap(
pcap,
String(sourceId),
u32(sessionId),
port == null ? undefined : Number(port),
chip == null ? undefined : String(chip),
),
);
}
/**
* Summarize a nexmon_csi `.pcap` file (link type, CSI frame count, channels,
* bandwidths, chip versions, RSSI range, time span).
* bandwidths, chip versions + resolved chip names, RSSI range, time span).
* @param {string} path
* @param {number} [port] CSI UDP port (default 5500)
* @returns {import('./index').NexmonPcapSummary}
@@ -126,6 +134,36 @@ function decodeChanspec(chanspec) {
return JSON.parse(binding().decodeChanspec(u32(chanspec)));
}
/**
* Resolve a `chip_ver` word from a nexmon_csi packet to a chip slug
* (`'bcm43455c0'` for a Raspberry Pi 3B+/4/400/5; `'unknown:0xNNNN'` otherwise).
* @param {number} chipVer
* @returns {string}
*/
function nexmonChipName(chipVer) {
return binding().nexmonChipName(u32(chipVer));
}
/**
* The AdapterProfile (channels / bandwidths / expected subcarrier counts /
* capability flags) for a chip / Raspberry-Pi-model spec (`'pi5'`,
* `'bcm43455c0'`, ...). Throws on an unknown spec.
* @param {string} spec
* @returns {import('./index').AdapterProfile}
*/
function nexmonProfile(spec) {
return JSON.parse(binding().nexmonProfile(String(spec)));
}
/**
* Listing of the Nexmon-supported chips + the Raspberry Pi models that carry
* them (incl. the Pi 5 → BCM43455c0).
* @returns {import('./index').NexmonChipsListing}
*/
function nexmonChips() {
return JSON.parse(binding().nexmonChips());
}
/** Streaming capture runtime: a source + the DSP stage + the event pipeline. */
class RvCsi {
/** @param {*} rt the underlying napi RvcsiRuntime handle */
@@ -203,6 +241,9 @@ module.exports = {
nexmonDecodePcap,
inspectNexmonPcap,
decodeChanspec,
nexmonChipName,
nexmonProfile,
nexmonChips,
inspectCaptureFile,
eventsFromCaptureFile,
exportCaptureToRfMemory,
+56 -4
View File
@@ -95,22 +95,26 @@ pub fn export_capture_to_rf_memory(capture_path: String, out_jsonl_path: String)
/// Decode the *real* nexmon_csi UDP payloads inside a libpcap `.pcap` `Buffer`
/// into a JSON array of validated `CsiFrame`s. `port` is the CSI UDP port
/// (omit / `null` ⇒ 5500). Throws if the buffer isn't a parseable classic pcap.
/// (omit / `null` ⇒ 5500); `chip` is an optional chip / Raspberry-Pi-model spec
/// (`"pi5"`, `"bcm43455c0"`, ...) — when given, frames are validated against
/// that device's profile and the non-conforming ones dropped. Throws if the
/// buffer isn't a parseable classic pcap or `chip` is unrecognised.
#[napi]
pub fn nexmon_decode_pcap(
pcap: Buffer,
source_id: String,
session_id: u32,
port: Option<u16>,
chip: Option<String>,
) -> napi::Result<String> {
let frames = runtime::decode_nexmon_pcap(pcap.as_ref(), &source_id, session_id as u64, port)
let frames = runtime::decode_nexmon_pcap_for(pcap.as_ref(), &source_id, session_id as u64, port, chip.as_deref())
.map_err(napi_err)?;
to_json(&frames)
}
/// Summarize a nexmon_csi `.pcap` file (link type, frame counts, channels,
/// bandwidths, chip versions, RSSI range, time span); returns JSON for a
/// `NexmonPcapSummary`. `port` defaults to 5500.
/// bandwidths, chip versions + resolved chip names, RSSI range, time span);
/// returns JSON for a `NexmonPcapSummary`. `port` defaults to 5500.
#[napi]
pub fn inspect_nexmon_pcap(path: String, port: Option<u16>) -> napi::Result<String> {
let summary = runtime::summarize_nexmon_pcap(&path, port).map_err(napi_err)?;
@@ -130,6 +134,54 @@ pub fn decode_chanspec(chanspec: u32) -> napi::Result<String> {
}))
}
/// Resolve a `chip_ver` word from a nexmon_csi packet to a chip slug
/// (`"bcm43455c0"` for a Raspberry Pi 3B+/4/400/5; `"unknown:0xNNNN"` otherwise).
#[napi]
pub fn nexmon_chip_name(chip_ver: u32) -> String {
rvcsi_adapter_nexmon::NexmonChip::from_chip_ver((chip_ver & 0xFFFF) as u16).slug()
}
/// The `AdapterProfile` (channels / bandwidths / expected subcarrier counts /
/// capability flags) for a chip / Raspberry-Pi-model spec (`"pi5"`,
/// `"bcm43455c0"`, `"raspberry pi 4"`, ...); returns JSON. Throws if unknown.
#[napi]
pub fn nexmon_profile(spec: String) -> napi::Result<String> {
let p = runtime::nexmon_profile_for(&spec)
.ok_or_else(|| napi::Error::from_reason(format!("unknown nexmon chip / Raspberry Pi model `{spec}`")))?;
to_json(&p)
}
/// JSON listing of the Nexmon-supported chips + the Raspberry Pi models that
/// carry them (incl. the Pi 5 → BCM43455c0): `{ chips: [...], raspberryPiModels: [...] }`.
#[napi]
pub fn nexmon_chips() -> napi::Result<String> {
use rvcsi_adapter_nexmon::{known_chips, known_pi_models, nexmon_adapter_profile, NexmonChip};
let chips: Vec<_> = known_chips()
.iter()
.map(|c| {
let p = nexmon_adapter_profile(*c);
serde_json::json!({
"slug": c.slug(), "description": c.description(),
"dualBand": c.dual_band(), "int16IqExport": c.uses_int16_iq(),
"bandwidthsMhz": p.supported_bandwidths_mhz,
"expectedSubcarrierCounts": p.expected_subcarrier_counts,
})
})
.collect();
let pis: Vec<_> = known_pi_models()
.iter()
.map(|m| {
let chip = m.nexmon_chip();
serde_json::json!({
"slug": m.slug(),
"chip": if matches!(chip, NexmonChip::Unknown { .. }) { serde_json::Value::Null } else { serde_json::Value::String(chip.slug()) },
"csiSupported": m.csi_supported(),
})
})
.collect();
to_json(&serde_json::json!({ "chips": chips, "raspberryPiModels": pis }))
}
// ---------------------------------------------------------------------------
// Streaming runtime class
// ---------------------------------------------------------------------------