灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-kernel/src/arch/aarch64/wifi.rs
//! Raspberry Pi 5 Wi-Fi: the CYW43455 radio on the BCM2712's SDIO2.
//!
//! The radio sits on the second SDHCI controller (`sdio2`, `0x10_0110_0000`,
//! cfg block at +0x400), 4-bit SDIO on GPIO 30-35, powered by WL_ON = GPIO 28
//! (all from the bcm2712 device trees). Linux leaves `sdio2` to its own
//! driver and the firmware doesn't bring it up, so this does everything by
//! hand, printing every step (the HDMI console is the only debug channel on
//! a Pi 5 without a serial adapter):
//!
//! stage 1 (`bus_up`): pin mux (the mux table differs between the BCM2712
//! C0/C1 stepping on rev 1.0 boards and D0 on rev 1.1+), power-cycle WL_ON,
//! reset the host, CMD0/CMD5/CMD3/CMD7, function 1 + ALP clock, ChipCommon
//! chip id (a CYW43455 answers 0x4345), then 4-bit bus at 25 MHz.
//!
//! stage 2 (`radio_up`): the chip's cores from its EROM, put the ARM CR4 in
//! reset, write the brcmfmac43455 firmware into its TCM (`firmware/`), the
//! NVRAM behind it, release the CPU on the image's reset vector, enable
//! function 2 (frames), then speak SDPCM + BCDC over it: ioctls / iovars on
//! the control channel (CLM blob, country, event mask, `up`), events on the
//! event channel (escan results, join progress), Ethernet frames on the data
//! channel -- what `brcmfmac` and the Pico W's cyw43 driver do, trimmed to a
//! station that scans, joins WPA2 with the firmware's own supplicant, and
//! passes Ethernet frames to `netstack` (through `netdev`).
//!
//! Untested on hardware beyond stage 1 at the time of writing (the ALP clock
//! came up; the first backplane read is where the previous attempt stopped),
//! so every stage reports what it saw.

use crate::arch::mmio::{read32, write32};
use crate::arch::timer::{delay_us, now_ms};
use crate::console_write as out;
use alloc::string::String;
use alloc::vec::Vec;

const SDIO2: usize = 0x10_0110_0000;
const SDIO2_CFG: usize = 0x10_0110_0400;
const GIO: usize = 0x10_7d50_8500; // brcmstb GPIO, bank 0 = GPIO 0..31
const PINCTRL: usize = 0x10_7d50_4100;

/// Base clock feeding the SDHCI (the device tree's fixed `clk_emmc2`).
const BASE_CLOCK_HZ: u32 = 200_000_000;

// SDHCI registers.
const BLOCK: usize = 0x04; // block size (lo 16) + block count (hi 16)
const ARG: usize = 0x08;
const XFER_CMD: usize = 0x0C; // transfer mode (lo 16) + command (hi 16)
const RESP0: usize = 0x10;
const BUF_DATA: usize = 0x20;
const PRESENT: usize = 0x24;
const HOST_CTRL: usize = 0x28; // + power 0x29, block gap 0x2A, wakeup 0x2B
const CLOCK_CTRL: usize = 0x2C; // clock (lo 16) + timeout 0x2E + reset 0x2F
const INT_STATUS: usize = 0x30;
const INT_ENABLE: usize = 0x34;
const INT_SIGNAL: usize = 0x38;
const CAPS: usize = 0x40;
const VERSION: usize = 0xFC; // slot int status (lo 16) + host version (hi 16)

// Command flags (low byte of the command register).
const RESP_NONE: u32 = 0x00;
const RESP_48: u32 = 0x02; // R4: no CRC / index check
const RESP_48_CHK: u32 = 0x1A; // R5/R6: CRC + index check
const RESP_48_BUSY: u32 = 0x1B; // R1b

// Function-1 (backplane) registers of Broadcom SDIO chips.
const SB_ADDR_LOW: u32 = 0x1000A;
const SB_ADDR_MID: u32 = 0x1000B;
const SB_ADDR_HIGH: u32 = 0x1000C;
const CHIP_CLK_CSR: u32 = 0x1000E;
const SDIO_PULLUP: u32 = 0x1000F;
const WATERMARK: u32 = 0x10008;
const DEVICE_CTL: u32 = 0x10009;
const MESBUSYCTRL: u32 = 0x1001D;
const SLEEPCSR: u32 = 0x1001F;
const FORCE_HT: u8 = 0x02;
const ALP_AVAIL_REQ: u8 = 0x08;
const HT_AVAIL_REQ: u8 = 0x10;
const FORCE_HW_CLKREQ_OFF: u8 = 0x20;
const ALP_AVAIL: u8 = 0x40;
const HT_AVAIL: u8 = 0x80;
const CHIPCOMMON: u32 = 0x1800_0000;
/// Function-1 address flag for a 32-bit backplane access (SBSDIO_SB_ACCESS_2_4B_FLAG).
const SB_ACCESS_4B: u32 = 0x8000;
/// Function-2 frames: the ChipCommon window offset with the 32-bit flag, fixed address.
const F2_ADDR: u32 = 0x8000;
const F1_BLOCK: usize = 64;
const F2_BLOCK: usize = 512;

// AI (AXI) backplane wrapper registers.
const AI_IOCTL: u32 = 0x408;
const AI_RESET_CTL: u32 = 0x800;
const IOCTL_CLK: u32 = 0x1;
const IOCTL_FGC: u32 = 0x2;
const RESET: u32 = 0x1;
const CR4_CPUHALT: u32 = 0x20;
const D11_PHYCLOCKEN: u32 = 0x4;
const D11_PHYRESET: u32 = 0x8;

// Core ids (BCMA).
const CORE_CHIPCOMMON: u32 = 0x800;
const CORE_80211: u32 = 0x812;
const CORE_SDIO_DEV: u32 = 0x829;
const CORE_ARM_CR4: u32 = 0x83E;

// SDIO device core registers (struct sdpcmd_regs).
const SD_INTSTATUS: u32 = 0x20;
const SD_HOSTINTMASK: u32 = 0x24;
const SD_TOSBMAILBOX: u32 = 0x40;
const SD_TOSBMAILBOXDATA: u32 = 0x48;
const SD_TOHOSTMAILBOXDATA: u32 = 0x4C;
const I_HMB_FRAME_IND: u32 = 1 << 6;
const I_HMB_HOST_INT: u32 = 1 << 7;
const HOSTINTMASK: u32 = 0xF0 | (1 << 13);
const SMB_INT_ACK: u32 = 1 << 1;

// SDPCM channels.
const CHAN_CONTROL: u8 = 0;
const CHAN_EVENT: u8 = 1;
const CHAN_DATA: u8 = 2;

// ioctls.
const WLC_UP: u32 = 2;
const WLC_SET_INFRA: u32 = 20;
const WLC_SET_AUTH: u32 = 22;
const WLC_SET_SSID: u32 = 26;
const WLC_DISASSOC: u32 = 52;
const WLC_SET_PM: u32 = 86;
const WLC_SET_GMODE: u32 = 110;
const WLC_SET_WSEC: u32 = 134;
const WLC_SET_BAND: u32 = 142;
const WLC_GET_VAR: u32 = 262;
const WLC_SET_VAR: u32 = 263;
const WLC_SET_WSEC_PMK: u32 = 268;

// Events.
const EV_SET_SSID: u32 = 0;
const EV_AUTH: u32 = 3;
const EV_DEAUTH: u32 = 5;
const EV_DEAUTH_IND: u32 = 6;
const EV_DISASSOC: u32 = 11;
const EV_DISASSOC_IND: u32 = 12;
const EV_LINK: u32 = 16;
const EV_PSK_SUP: u32 = 46;
const EV_ESCAN_RESULT: u32 = 69;

const FW: &[u8] = include_bytes!("../../../firmware/brcmfmac43455-sdio.bin");
const CLM: &[u8] = include_bytes!("../../../firmware/brcmfmac43455-sdio.clm_blob");
const NVRAM_TXT: &str = include_str!("../../../firmware/brcmfmac43455-sdio.txt");
const RAMBASE: u32 = 0x198000;
const RAMSIZE_DEFAULT: u32 = 0xC8000;

/// One network a scan saw.
#[derive(Clone)]
pub struct Network {
    pub ssid: String,
    pub rssi: i32,
    /// 0 open, 1 WEP, 2 WPA, 3 WPA2.
    pub secure: u8,
    pub channel: u16,
}

struct Cores {
    d11_wrap: u32,
    cr4: u32,
    cr4_wrap: u32,
    sdiod: u32,
}

struct Radio {
    cores: Cores,
    tx_seq: u8,
    tx_max: u8,
    ioctl_id: u16,
    mac: [u8; 6],
    joined: bool,
    link: bool,
    ssid: String,
    scan_done: Option<u32>,
    scan_hits: Vec<Network>,
    join_status: Option<u32>,
    psk_status: Option<u32>,
    /// The kernel's WPA2 supplicant while joining a protected network (and
    /// after, for group-key rekeys).
    sup: Option<super::wpa::Handshake>,
    sup_error: Option<&'static str>,
    /// Handshake message 1 that arrived before the association IEs were read.
    pending_eapol: Option<Vec<u8>>,
    /// The association request's RSN IE has been read back (or given up on).
    ies_known: bool,
    /// Data frames that arrived while waiting for something else.
    rx_queue: Vec<Vec<u8>>,
    frames_in: u32,
    frames_out: u32,
    /// One-line summaries of the last data frame out / in (the connection test).
    last_tx: String,
    last_rx: String,
    rx_data: u32,
    /// What ended the last association ("link down", "deauth reason 15 ...") and when (ms).
    link_note: String,
    link_lost_ms: u64,
    /// The link went away by itself after a join (not `leave`).
    dropped: bool,
}

static mut RADIO: Option<Radio> = None;
static mut BUS_TRIED: bool = false;
static mut LAST_ERROR: &str = "";

fn radio() -> Option<&'static mut Radio> {
    unsafe { (*(&raw mut RADIO)).as_mut() }
}

fn sd_rd(off: usize) -> u32 {
    unsafe { read32(SDIO2 + off) }
}
fn sd_wr(off: usize, v: u32) {
    unsafe { write32(SDIO2 + off, v) }
}

fn hex(label: &[u8], v: u64, nibbles: usize) {
    out(label);
    out(b"0x");
    let mut b = [0u8; 16];
    let n = nibbles.min(16);
    for (i, c) in b.iter_mut().enumerate().take(n) {
        let d = ((v >> ((n - 1 - i) * 4)) & 0xF) as u8;
        *c = if d < 10 { b'0' + d } else { b'a' + d - 10 };
    }
    out(&b[..n]);
    out(b"\n");
}

fn dec(label: &[u8], v: u64) {
    out(label);
    let mut b = [0u8; 20];
    let mut n = 0;
    let mut x = v;
    if x == 0 {
        b[0] = b'0';
        n = 1;
    }
    while x > 0 {
        b[n] = b'0' + (x % 10) as u8;
        x /= 10;
        n += 1;
    }
    b[..n].reverse();
    out(&b[..n]);
}

fn fail(msg: &'static str) -> Option<()> {
    unsafe { LAST_ERROR = msg };
    out(b"  wifi: ");
    out(msg.as_bytes());
    out(b"\n");
    None
}

/// Why the last bring-up step failed ("" when nothing failed yet).
pub fn last_error() -> &'static str {
    unsafe { LAST_ERROR }
}

// ── SDHCI host ───────────────────────────────────────────────────────────────

/// Set pin `pin`'s 4-bit mux field and 2-bit pull field (0 none, 1 down, 2 up).
/// (mux word, mux nibble, pad word, pad pair) per pin from pinctrl-bcm2712.c.
fn pin_set(d0: bool, pin: u32, fsel: u32, pull: u32) {
    let (mr, mb, pr, pb) = match (d0, pin) {
        (false, 28) => (3, 4, 9, 5),
        (false, 30) => (3, 6, 9, 7),
        (false, 31) => (3, 7, 9, 8),
        (false, 32) => (4, 0, 9, 9),
        (false, 33) => (4, 1, 9, 10),
        (false, 34) => (4, 2, 9, 11),
        (false, 35) => (4, 3, 9, 12),
        (true, 28) => (2, 4, 5, 10),
        (true, 30) => (2, 6, 5, 12),
        (true, 31) => (2, 7, 5, 13),
        (true, 32) => (3, 0, 5, 14),
        (true, 33) => (3, 1, 6, 0),
        (true, 34) => (3, 2, 6, 1),
        (true, 35) => (3, 3, 6, 2),
        _ => return,
    };
    unsafe {
        let m = PINCTRL + mr * 4;
        let v = read32(m) & !(0xF << (mb * 4));
        write32(m, v | (fsel << (mb * 4)));
        let p = PINCTRL + pr * 4;
        let v = read32(p) & !(0x3 << (pb * 2));
        write32(p, v | (pull << (pb * 2)));
    }
}

/// Wait until (reg & mask) == want, up to `ms` milliseconds.
fn wait(off: usize, mask: u32, want: u32, ms: u32) -> bool {
    for _ in 0..ms * 10 {
        if sd_rd(off) & mask == want {
            return true;
        }
        delay_us(100);
    }
    false
}

fn reset_cmd_line() {
    sd_wr(CLOCK_CTRL, sd_rd(CLOCK_CTRL) | (0x2 << 24));
    wait(CLOCK_CTRL, 0x2 << 24, 0, 100);
}

/// Reset the DAT line after a data error so the next transfer can go out.
fn reset_dat() {
    sd_wr(CLOCK_CTRL, sd_rd(CLOCK_CTRL) | (0x4 << 24));
    wait(CLOCK_CTRL, 0x4 << 24, 0, 100);
}

/// Issue one command; returns RESP0, or None (after printing the error bits).
fn cmd(idx: u32, arg: u32, flags: u32) -> Option<u32> {
    if !wait(PRESENT, 0x1, 0, 100) {
        out(b"    command line busy\n");
        return None;
    }
    sd_wr(INT_STATUS, 0xFFFF_FFFF);
    sd_wr(ARG, arg);
    sd_wr(XFER_CMD, ((idx << 8) | flags) << 16);
    for _ in 0..2000 {
        let st = sd_rd(INT_STATUS);
        if st & (1 << 15) != 0 {
            sd_wr(INT_STATUS, st);
            reset_cmd_line();
            out(b"    CMD");
            out(&[b'0' + (idx / 10) as u8, b'0' + (idx % 10) as u8]);
            hex(b" error, int status=", st as u64, 8);
            return None;
        }
        if st & 0x1 != 0 {
            sd_wr(INT_STATUS, 0x1);
            if flags == RESP_48_BUSY {
                wait(INT_STATUS, 0x2, 0x2, 100); // transfer complete = busy end
                sd_wr(INT_STATUS, 0x2);
            }
            return Some(sd_rd(RESP0));
        }
        delay_us(50);
    }
    out(b"    CMD");
    out(&[b'0' + (idx / 10) as u8, b'0' + (idx % 10) as u8]);
    out(b" timeout (no response)\n");
    None
}

/// CMD52 (IO_RW_DIRECT): one byte at `addr` of function `func`.
fn cmd52(write: bool, func: u32, addr: u32, data: u8) -> Option<u8> {
    let arg = ((write as u32) << 31) | ((func & 7) << 28) | ((addr & 0x1_FFFF) << 9) | data as u32;
    let r = cmd(52, arg, RESP_48_CHK)?;
    if r & 0xCB00 != 0 {
        hex(b"    CMD52 R5 flags=", ((r >> 8) & 0xFF) as u64, 2);
    }
    Some(r as u8)
}

/// CMD53 (IO_RW_EXTENDED) by PIO through the buffer port: byte mode when the
/// transfer fits one block, else block mode (`buf.len()` a multiple of
/// `block`). `incr`: incrementing address (RAM) or a FIFO (function 2 frames).
fn cmd53(write: bool, func: u32, addr: u32, incr: bool, buf: &mut [u8], block: usize) -> Option<()> {
    let n = buf.len();
    if n == 0 || (n > block && n % block != 0) || (n <= block && n > 512) {
        out(b"    CMD53: bad length\n");
        return None;
    }
    if !wait(PRESENT, 0x3, 0, 100) {
        out(b"    CMD53: command/data line busy\n");
        return None;
    }
    let (blocks, bsize) = if n > block { (n / block, block) } else { (1, n) };
    let count = if n > block { blocks as u32 } else { (n as u32) & 0x1FF };
    let arg = ((write as u32) << 31)
        | ((func & 7) << 28)
        | (((n > block) as u32) << 27)
        | ((incr as u32) << 26)
        | ((addr & 0x1_FFFF) << 9)
        | count;
    sd_wr(INT_STATUS, 0xFFFF_FFFF);
    sd_wr(BLOCK, ((blocks as u32) << 16) | bsize as u32);
    sd_wr(ARG, arg);
    // Transfer mode: data direction (bit 4 = read), multi-block + block count
    // enable for block mode. Command: 53, data present, R5 with checks.
    let mut mode = if write { 0 } else { 1 << 4 };
    if blocks > 1 {
        mode |= (1 << 5) | (1 << 1);
    }
    sd_wr(XFER_CMD, (((53 << 8) | RESP_48_CHK | (1 << 5)) << 16) | mode);
    let mut done = 0usize;
    let words = bsize.div_ceil(4);
    for _ in 0..40_000 {
        let st = sd_rd(INT_STATUS);
        if st & (1 << 15) != 0 {
            sd_wr(INT_STATUS, st);
            reset_cmd_line();
            reset_dat();
            hex(b"    CMD53 error, int status=", st as u64, 8);
            return None;
        }
        if write && st & (1 << 4) != 0 && done < n {
            sd_wr(INT_STATUS, 1 << 4);
            for _ in 0..words {
                let mut w = [0u8; 4];
                for b in w.iter_mut() {
                    if done < n {
                        *b = buf[done];
                        done += 1;
                    }
                }
                sd_wr(BUF_DATA, u32::from_le_bytes(w));
            }
        }
        if !write && st & (1 << 5) != 0 && done < n {
            sd_wr(INT_STATUS, 1 << 5);
            for _ in 0..words {
                let w = sd_rd(BUF_DATA).to_le_bytes();
                for b in w {
                    if done < n {
                        buf[done] = b;
                        done += 1;
                    }
                }
            }
        }
        if st & (1 << 1) != 0 && done >= n {
            sd_wr(INT_STATUS, 0x3);
            return Some(());
        }
        delay_us(20);
    }
    reset_dat();
    hex(b"    CMD53 timeout, int status=", sd_rd(INT_STATUS) as u64, 8);
    None
}

fn cmd53_read(func: u32, addr: u32, incr: bool, buf: &mut [u8], block: usize) -> Option<()> {
    cmd53(false, func, addr, incr, buf, block)
}

fn cmd53_write(func: u32, addr: u32, incr: bool, data: &[u8], block: usize) -> Option<()> {
    let mut tmp: Vec<u8> = data.to_vec();
    cmd53(true, func, addr, incr, &mut tmp, block)
}

// ── backplane (function 1) ───────────────────────────────────────────────────

static mut WINDOW: u32 = 0xFFFF_FFFF;

/// Point the function-1 backplane window at `addr`'s 32 KiB page.
fn set_window(addr: u32) -> Option<()> {
    let win = addr & !0x7FFF;
    if unsafe { WINDOW } == win {
        return Some(());
    }
    cmd52(true, 1, SB_ADDR_LOW, ((win >> 8) & 0x80) as u8)?;
    cmd52(true, 1, SB_ADDR_MID, ((win >> 16) & 0xFF) as u8)?;
    cmd52(true, 1, SB_ADDR_HIGH, ((win >> 24) & 0xFF) as u8)?;
    unsafe { WINDOW = win };
    Some(())
}

fn bp_read32(addr: u32) -> Option<u32> {
    set_window(addr)?;
    let mut b4 = [0u8; 4];
    if cmd53_read(1, (addr & 0x7FFF) | SB_ACCESS_4B, true, &mut b4, F1_BLOCK).is_some() {
        return Some(u32::from_le_bytes(b4));
    }
    // Four byte reads as a fallback (what an older bring-up used).
    let mut v = 0u32;
    for i in 0..4 {
        let b = cmd52(false, 1, (addr & 0x7FFF) + i, 0)?;
        v |= (b as u32) << (8 * i);
    }
    Some(v)
}

fn bp_write32(addr: u32, v: u32) -> Option<()> {
    set_window(addr)?;
    cmd53_write(1, (addr & 0x7FFF) | SB_ACCESS_4B, true, &v.to_le_bytes(), F1_BLOCK)
}

/// Write `data` into the chip's RAM at backplane address `addr`, 64 bytes a
/// command, never crossing a window.
fn ram_write(addr: u32, data: &[u8]) -> Option<()> {
    let mut off = 0usize;
    let mut next_dot = 0usize;
    while off < data.len() {
        let a = addr + off as u32;
        set_window(a)?;
        let room = 0x8000 - (a & 0x7FFF) as usize;
        let n = (data.len() - off).min(F1_BLOCK).min(room);
        cmd53_write(1, (a & 0x7FFF) | SB_ACCESS_4B, true, &data[off..off + n], F1_BLOCK)?;
        off += n;
        if off >= next_dot {
            out(b".");
            next_dot += 65_536;
        }
    }
    Some(())
}

fn ram_read(addr: u32, buf: &mut [u8]) -> Option<()> {
    let mut off = 0usize;
    while off < buf.len() {
        let a = addr + off as u32;
        set_window(a)?;
        let room = 0x8000 - (a & 0x7FFF) as usize;
        let n = (buf.len() - off).min(F1_BLOCK).min(room);
        cmd53_read(1, (a & 0x7FFF) | SB_ACCESS_4B, true, &mut buf[off..off + n], F1_BLOCK)?;
        off += n;
    }
    Some(())
}

fn ai_coredisable(wrap: u32, prereset: u32, reset: u32) -> Option<()> {
    if bp_read32(wrap + AI_RESET_CTL)? & RESET == 0 {
        bp_write32(wrap + AI_IOCTL, prereset | IOCTL_FGC | IOCTL_CLK)?;
        bp_read32(wrap + AI_IOCTL)?;
        bp_write32(wrap + AI_RESET_CTL, RESET)?;
        delay_us(20);
        for _ in 0..30 {
            if bp_read32(wrap + AI_RESET_CTL)? & RESET != 0 {
                break;
            }
            delay_us(10);
        }
    }
    bp_write32(wrap + AI_IOCTL, reset | IOCTL_FGC | IOCTL_CLK)?;
    bp_read32(wrap + AI_IOCTL)?;
    Some(())
}

fn ai_resetcore(wrap: u32, prereset: u32, reset: u32, postreset: u32) -> Option<()> {
    ai_coredisable(wrap, prereset, reset)?;
    for _ in 0..50 {
        if bp_read32(wrap + AI_RESET_CTL)? & RESET == 0 {
            break;
        }
        bp_write32(wrap + AI_RESET_CTL, 0)?;
        delay_us(60);
    }
    bp_write32(wrap + AI_IOCTL, postreset | IOCTL_CLK)?;
    bp_read32(wrap + AI_IOCTL)?;
    Some(())
}

/// The cores this driver needs, from the chip's enumeration ROM.
fn erom_scan() -> Option<Cores> {
    let mut erom = bp_read32(CHIPCOMMON + 0xFC)?;
    hex(b"  EROM at ", erom as u64, 8);
    let mut found = [(0u32, 0u32, 0u32); 4]; // (id, base, wrap) for cc, d11, cr4, sdiod
    let want = [CORE_CHIPCOMMON, CORE_80211, CORE_ARM_CR4, CORE_SDIO_DEV];
    let next = |erom: &mut u32| -> Option<u32> {
        let v = bp_read32(*erom)?;
        *erom += 4;
        Some(v)
    };
    for _ in 0..512 {
        let v = next(&mut erom)?;
        if v & 0xF == 0xF {
            break;
        }
        if v & 0x7 != 0x1 {
            continue; // not a component descriptor
        }
        let id = (v >> 8) & 0xFFF;
        let v2 = next(&mut erom)?;
        let nmw = (v2 >> 22) & 0x1F;
        let nsw = (v2 >> 27) & 0x1F;
        if nmw + nsw == 0 && id != 0x827 && id != 0x840 {
            continue;
        }
        // Address descriptors: the first slave port is the register base, the
        // first wrapper port the wrapper base.
        let (mut base, mut wrap) = (0u32, 0u32);
        loop {
            let d = next(&mut erom)?;
            let t = d & 0x7;
            if t == 0x3 {
                continue; // master port
            }
            if t != 0x5 {
                erom -= 4; // not an address descriptor: belongs to the next component
                break;
            }
            let stype = (d >> 6) & 0x3;
            if d & 0x8 != 0 {
                next(&mut erom)?; // high half of a 64-bit address
            }
            let sz = (d >> 4) & 0x3;
            if sz == 3 {
                let s = next(&mut erom)?;
                if s & 0x8 != 0 {
                    next(&mut erom)?;
                }
                continue;
            }
            if sz > 1 {
                continue; // only 4K / 8K register regions
            }
            if base == 0 && stype == 0 {
                base = d & 0xFFFF_F000;
            }
            if wrap == 0 && (stype == 2 || stype == 3) {
                wrap = d & 0xFFFF_F000;
            }
        }
        for (k, w) in want.iter().enumerate() {
            if id == *w && found[k].1 == 0 {
                found[k] = (id, base, wrap);
            }
        }
    }
    for (k, (id, base, wrap)) in found.iter().enumerate() {
        let name: &[u8] = [&b"chipcommon"[..], b"80211", b"arm-cr4", b"sdiod"][k];
        out(b"  core ");
        out(name);
        if *id == 0 {
            out(b": not found\n");
        } else {
            hex(b" base=", *base as u64, 8);
            hex(b"      wrap=", *wrap as u64, 8);
        }
    }
    if found[1].1 == 0 || found[2].1 == 0 || found[3].1 == 0 {
        fail("EROM scan did not find the 80211 / CR4 / SDIO cores");
        return None;
    }
    Some(Cores { d11_wrap: found[1].2, cr4: found[2].1, cr4_wrap: found[2].2, sdiod: found[3].1 })
}

/// The CR4's tightly coupled memory size from its bank registers.
fn tcm_ramsize(c: &Cores) -> u32 {
    let Some(cap) = bp_read32(c.cr4 + 0x04) else { return RAMSIZE_DEFAULT };
    let banks = (cap & 0xF) + ((cap >> 4) & 0xF);
    let mut size = 0u32;
    for idx in 0..banks {
        if bp_write32(c.cr4 + 0x40, idx).is_none() {
            return RAMSIZE_DEFAULT;
        }
        let Some(info) = bp_read32(c.cr4 + 0x44) else { return RAMSIZE_DEFAULT };
        size += ((info & 0x3F) + 1) * 8192;
    }
    if size == 0 || size > 0x20_0000 {
        RAMSIZE_DEFAULT
    } else {
        size
    }
}

// ── stage 1: the bus ─────────────────────────────────────────────────────────

/// Bring the SDIO bus and the radio chip up (idempotent); the chip id.
fn bus_up() -> Option<u32> {
    if crate::arch::mmio::board() != crate::arch::mmio::RpiBoard::Pi5 {
        out(b"wifi: this driver is for the Raspberry Pi 5 (CYW43455 on BCM2712 SDIO2)\n");
        unsafe { LAST_ERROR = "no Wi-Fi radio on this board (Pi 5 only)" };
        return None;
    }
    out(b"Wi-Fi (Pi 5: CYW43455 on SDIO2)\n");

    // 1. Pin mux for the stepping this board carries.
    let rev = crate::arch::mailbox::board_revision().unwrap_or(0);
    hex(b"  board revision=", rev as u64, 6);
    let d0 = rev & 0xF >= 1;
    out(if d0 { b"  BCM2712 D0 pin table (rev 1.1+)\n" } else { b"  BCM2712 C0/C1 pin table (rev 1.0)\n" });
    let sd2: [u32; 6] = if d0 { [1, 1, 1, 1, 1, 1] } else { [4, 4, 4, 3, 4, 3] };
    for (i, pin) in (30..=35u32).enumerate() {
        pin_set(d0, pin, sd2[i], if pin == 30 { 0 } else { 2 });
    }
    pin_set(d0, 28, 0, 0); // WL_ON as a plain GPIO

    // 2. Power-cycle the radio: WL_ON low, then high (+150 ms startup per DT).
    unsafe {
        write32(GIO + 0x08, read32(GIO + 0x08) & !(1 << 28)); // IODIR: output
        write32(GIO + 0x04, read32(GIO + 0x04) & !(1 << 28));
        delay_us(20_000);
        write32(GIO + 0x04, read32(GIO + 0x04) | (1 << 28));
    }
    delay_us(150_000);
    out(b"  WL_ON (GPIO 28) high: radio powered\n");

    // 3. Host controller.
    hex(b"  SDHCI version=", (sd_rd(VERSION) >> 16) as u64, 4);
    hex(b"  capabilities=", sd_rd(CAPS) as u64, 8);
    unsafe {
        // Non-removable: force card-present; pins in SD (not eMMC) mode.
        let c = read32(SDIO2_CFG) & !(1 << 30);
        write32(SDIO2_CFG, c | (1 << 31));
        let p = read32(SDIO2_CFG + 0x44) & !0x3;
        write32(SDIO2_CFG + 0x44, p | 0x2);
    }
    sd_wr(CLOCK_CTRL, 1 << 24); // software reset (all)
    if !wait(CLOCK_CTRL, 1 << 24, 0, 100) {
        fail("SDHCI reset timeout");
        return None;
    }
    unsafe { WINDOW = 0xFFFF_FFFF };
    // 3.3 V bus power (power control at 0x29), 1-bit bus for identification.
    sd_wr(HOST_CTRL, 0x0F << 8);
    delay_us(10_000);
    set_clock(400_000)?;
    sd_wr(INT_ENABLE, 0xFFFF_FFFF);
    sd_wr(INT_SIGNAL, 0);
    delay_us(10_000);
    hex(b"  present state=", sd_rd(PRESENT) as u64, 8);

    // 4. SDIO card init.
    cmd(0, 0, RESP_NONE);
    delay_us(2_000);
    let Some(ocr) = cmd(5, 0, RESP_48) else {
        fail("no answer to CMD5: radio not reachable on SDIO2");
        return None;
    };
    hex(b"  CMD5 OCR=", ocr as u64, 8);
    let mut ready = ocr;
    for _ in 0..100 {
        match cmd(5, ocr & 0x00FF_FF00, RESP_48) {
            Some(r) if r & (1 << 31) != 0 => {
                ready = r;
                break;
            },
            Some(r) => ready = r,
            None => return None,
        }
        delay_us(10_000);
    }
    if ready & (1 << 31) == 0 {
        fail("card never reported ready");
        return None;
    }
    out(b"  SDIO card ready, functions=");
    out(&[b'0' + ((ready >> 28) & 7) as u8, b'\n']);
    let rca = cmd(3, 0, RESP_48_CHK)? >> 16;
    hex(b"  RCA=", rca as u64, 4);
    cmd(7, rca << 16, RESP_48_BUSY)?;
    out(b"  card selected\n");

    // 4-bit bus at 25 MHz (brcmfmac runs 50 MHz; 25 keeps margin).
    let bic = cmd52(false, 0, 0x07, 0)?;
    cmd52(true, 0, 0x07, (bic & !0x3) | 0x02)?;
    sd_wr(HOST_CTRL, (sd_rd(HOST_CTRL) & !0x4) | 0x2);
    set_clock(25_000_000)?;
    out(b"  4-bit bus at 25 MHz\n");
    // Block sizes: 64 for the backplane function, 512 for frames.
    cmd52(true, 0, 0x110, F1_BLOCK as u8)?;
    cmd52(true, 0, 0x111, 0)?;
    cmd52(true, 0, 0x210, 0)?;
    cmd52(true, 0, 0x211, (F2_BLOCK >> 8) as u8)?;

    // 5. Function 1 (backplane), ALP clock, ChipCommon chip ID.
    let cccr = cmd52(false, 0, 0x00, 0)?;
    hex(b"  CCCR/SDIO revision=", cccr as u64, 2);
    cmd52(true, 0, 0x02, 0x02)?; // IOEx: enable F1
    let mut f1 = false;
    for _ in 0..100 {
        if cmd52(false, 0, 0x03, 0)? & 0x02 != 0 {
            f1 = true;
            break;
        }
        delay_us(10_000);
    }
    out(if f1 { b"  function 1 ready\n" } else { b"  function 1 did not come ready\n" });
    cmd52(true, 1, SDIO_PULLUP, 0)?;
    cmd52(true, 1, CHIP_CLK_CSR, FORCE_HW_CLKREQ_OFF | ALP_AVAIL_REQ)?;
    let mut alp = false;
    for _ in 0..100 {
        if cmd52(false, 1, CHIP_CLK_CSR, 0)? & ALP_AVAIL != 0 {
            alp = true;
            break;
        }
        delay_us(1_000);
    }
    out(if alp { b"  ALP clock available\n" } else { b"  ALP clock not available (continuing)\n" });
    cmd52(true, 1, CHIP_CLK_CSR, 0)?; // leave the chip to manage its clocks
    // Keep SDIO on across the chip's sleep states.
    let kso = cmd52(false, 1, SLEEPCSR, 0)?;
    if kso & 0x1 == 0 {
        cmd52(true, 1, SLEEPCSR, kso | 0x1)?;
    }
    let Some(id) = bp_read32(CHIPCOMMON) else {
        fail("backplane read of the chip id failed");
        return None;
    };
    hex(b"  ChipCommon chipid=", id as u64, 8);
    let chip = id & 0xFFFF;
    if chip == 0x4345 {
        out(b"  -> CYW43455 Wi-Fi radio found (stage 1 OK)\n");
    } else {
        hex(b"  -> unexpected chip id ", chip as u64, 4);
    }
    Some(id)
}

fn set_clock(hz: u32) -> Option<()> {
    // Divided-clock mode, f = base / (2N); stop, reprogram, restart.
    let n = if hz >= BASE_CLOCK_HZ { 0 } else { BASE_CLOCK_HZ.div_ceil(2 * hz) };
    let clk = ((n & 0xFF) << 8) | (((n >> 8) & 0x3) << 6) | 0x1;
    let timeout = (sd_rd(CLOCK_CTRL) & 0x000F_0000) | (0x0E << 16);
    sd_wr(CLOCK_CTRL, timeout | clk);
    if !wait(CLOCK_CTRL, 0x2, 0x2, 100) {
        fail("internal clock never stabilised");
        return None;
    }
    sd_wr(CLOCK_CTRL, sd_rd(CLOCK_CTRL) | 0x4); // SD clock on
    delay_us(2_000);
    Some(())
}

// ── stage 2: firmware ────────────────────────────────────────────────────────

/// The NVRAM image the firmware reads: `key=value` entries NUL-separated,
/// a terminating NUL, 4-byte padding and the length token brcmfmac appends.
fn build_nvram() -> Vec<u8> {
    let mut v: Vec<u8> = Vec::with_capacity(NVRAM_TXT.len() + 16);
    for line in NVRAM_TXT.lines() {
        let l = line.trim();
        if l.is_empty() || l.starts_with('#') || !l.contains('=') {
            continue;
        }
        v.extend_from_slice(l.as_bytes());
        v.push(0);
    }
    v.push(0);
    while v.len() % 4 != 0 {
        v.push(0);
    }
    let token = (v.len() / 4) as u32;
    let token = (!token << 16) | (token & 0xFFFF);
    v.extend_from_slice(&token.to_le_bytes());
    v
}

fn request_clock(req: u8, avail: u8, ms: u32) -> bool {
    if cmd52(true, 1, CHIP_CLK_CSR, req).is_none() {
        return false;
    }
    for _ in 0..ms {
        if let Some(v) = cmd52(false, 1, CHIP_CLK_CSR, 0) {
            if v & avail != 0 {
                return true;
            }
        }
        delay_us(1_000);
    }
    false
}

fn load_firmware(c: &Cores) -> Option<()> {
    if FW.len() < 64 {
        return fail("firmware image missing from this build");
    }
    out(if request_clock(HT_AVAIL_REQ, HT_AVAIL, 2_000) { b"  HT clock available\n" } else { b"  HT clock not reported (using ALP)\n" });
    // ARM CR4 into reset (keeping only its halt bit), the 802.11 core reset
    // with its PHY clock on: what brcmfmac calls the chip's passive state.
    let ioctl = bp_read32(c.cr4_wrap + AI_IOCTL)? & CR4_CPUHALT;
    ai_resetcore(c.cr4_wrap, ioctl, CR4_CPUHALT, CR4_CPUHALT)?;
    ai_resetcore(c.d11_wrap, D11_PHYRESET | D11_PHYCLOCKEN, D11_PHYCLOCKEN, D11_PHYCLOCKEN)?;
    out(b"  CR4 halted, 80211 core reset\n");
    let ramsize = tcm_ramsize(c);
    dec(b"  TCM: ", ramsize as u64);
    out(b" bytes at 0x198000\n");
    dec(b"  writing firmware (", FW.len() as u64);
    out(b" bytes) ");
    let t0 = now_ms();
    ram_write(RAMBASE, FW)?;
    dec(b" done in ", now_ms() - t0);
    out(b" ms\n");
    let mut check = [0u8; 64];
    ram_read(RAMBASE, &mut check)?;
    if check != FW[..64] {
        return fail("firmware read-back mismatch (RAM window writes are off)");
    }
    out(b"  firmware verified (first 64 bytes)\n");
    let nvram = build_nvram();
    if nvram.len() as u32 + FW.len() as u32 > ramsize {
        return fail("firmware + NVRAM larger than the chip's RAM");
    }
    let vaddr = RAMBASE + ramsize - nvram.len() as u32;
    dec(b"  NVRAM: ", nvram.len() as u64);
    hex(b" bytes at ", vaddr as u64, 8);
    ram_write(vaddr, &nvram)?;
    out(b"\n");
    // Activate: clear the SDIO core's interrupts, the reset vector at 0, run.
    let rstvec = u32::from_le_bytes([FW[0], FW[1], FW[2], FW[3]]);
    bp_write32(c.sdiod + SD_INTSTATUS, 0xFFFF_FFFF)?;
    ram_write(0, &rstvec.to_le_bytes())?;
    ai_resetcore(c.cr4_wrap, CR4_CPUHALT, 0, 0)?;
    hex(b"  CR4 released on reset vector ", rstvec as u64, 8);
    Some(())
}

fn start_f2(c: &Cores) -> Option<()> {
    out(if request_clock(HT_AVAIL_REQ, HT_AVAIL, 3_000) { b"  HT clock up (firmware running)\n" } else { b"  HT clock did not come up after the firmware start\n" });
    let saveclk = cmd52(false, 1, CHIP_CLK_CSR, 0)?;
    cmd52(true, 1, CHIP_CLK_CSR, saveclk | FORCE_HT)?;
    bp_write32(c.sdiod + SD_HOSTINTMASK, HOSTINTMASK)?;
    bp_write32(c.sdiod + SD_TOSBMAILBOXDATA, 4 << 16)?; // SDPCM protocol version 4
    let ioe = cmd52(false, 0, 0x02, 0)?;
    cmd52(true, 0, 0x02, ioe | 0x04)?;
    let mut ready = false;
    for _ in 0..350 {
        if cmd52(false, 0, 0x03, 0)? & 0x04 != 0 {
            ready = true;
            break;
        }
        delay_us(10_000);
    }
    if !ready {
        cmd52(true, 0, 0x02, ioe)?;
        return fail("function 2 never came ready: the firmware did not start");
    }
    out(b"  function 2 ready: firmware is up\n");
    cmd52(true, 1, WATERMARK, 0x60)?;
    let devctl = cmd52(false, 1, DEVICE_CTL, 0)?;
    cmd52(true, 1, DEVICE_CTL, devctl | 0x10)?;
    cmd52(true, 1, MESBUSYCTRL, 0x60 | 0x80)?;
    cmd52(true, 1, CHIP_CLK_CSR, saveclk)?;
    Some(())
}

// ── SDPCM frames ─────────────────────────────────────────────────────────────

fn has_credit(r: &Radio) -> bool {
    r.tx_seq != r.tx_max && (r.tx_max.wrapping_sub(r.tx_seq)) & 0x80 == 0
}

fn send_frame(chan: u8, payload: &[u8]) -> Option<()> {
    let t0 = now_ms();
    while !has_credit(radio()?) {
        poll_rx();
        if now_ms() - t0 > 2_000 {
            return fail("no transmit credit from the firmware");
        }
    }
    let r = radio()?;
    let len = 12 + payload.len();
    let mut total = len.div_ceil(4) * 4;
    if total > F2_BLOCK {
        total = total.div_ceil(F2_BLOCK) * F2_BLOCK;
    }
    let mut f: Vec<u8> = Vec::with_capacity(total);
    f.extend_from_slice(&(len as u16).to_le_bytes());
    f.extend_from_slice(&(!(len as u16)).to_le_bytes());
    f.extend_from_slice(&[r.tx_seq, chan, 0, 12, 0, 0, 0, 0]);
    f.extend_from_slice(payload);
    f.resize(total, 0);
    r.tx_seq = r.tx_seq.wrapping_add(1);
    r.frames_out += 1;
    cmd53_write(2, F2_ADDR, false, &f, F2_BLOCK)
}

/// One frame off function 2: (channel, bytes after the SDPCM headers).
fn read_frame() -> Option<(u8, Vec<u8>)> {
    let mut hdr = [0u8; 32];
    cmd53_read(2, F2_ADDR, false, &mut hdr, F2_BLOCK)?;
    let len = u16::from_le_bytes([hdr[0], hdr[1]]) as usize;
    let chk = u16::from_le_bytes([hdr[2], hdr[3]]);
    if len == 0 {
        return None;
    }
    if (len as u16) ^ chk != 0xFFFF {
        hex(b"  wifi: bad frame header ", u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]) as u64, 8);
        return None;
    }
    let mut frame: Vec<u8> = hdr.to_vec();
    if len > 32 {
        let mut rest = (len - 32).div_ceil(4) * 4;
        if rest > F2_BLOCK {
            rest = rest.div_ceil(F2_BLOCK) * F2_BLOCK;
        }
        let mut more = alloc::vec![0u8; rest];
        cmd53_read(2, F2_ADDR, false, &mut more, F2_BLOCK)?;
        frame.extend_from_slice(&more);
    }
    let r = radio()?;
    r.tx_max = frame[9];
    r.frames_in += 1;
    let chan = frame[5] & 0xF;
    let doff = frame[7] as usize;
    if len < 12 || doff < 12 || doff > len {
        return None; // a credit-only / glom header: nothing to deliver
    }
    frame.truncate(len);
    Some((chan, frame[doff..].to_vec()))
}

/// Drain pending frames: control frames are returned to the ioctl waiter,
/// events handled, data queued for `receive`.
fn poll_rx() -> Option<Vec<u8>> {
    let sdiod = radio()?.cores.sdiod;
    let st = bp_read32(sdiod + SD_INTSTATUS)?;
    if st & (I_HMB_FRAME_IND | I_HMB_HOST_INT) == 0 {
        return None;
    }
    bp_write32(sdiod + SD_INTSTATUS, st & (I_HMB_FRAME_IND | I_HMB_HOST_INT))?;
    if st & I_HMB_HOST_INT != 0 {
        // Mailbox from the firmware (fw-ready / version); acknowledge it.
        let _ = bp_read32(sdiod + SD_TOHOSTMAILBOXDATA);
        let _ = bp_write32(sdiod + SD_TOSBMAILBOX, SMB_INT_ACK);
    }
    let mut control: Option<Vec<u8>> = None;
    for _ in 0..32 {
        let Some((chan, payload)) = read_frame() else { break };
        match chan {
            CHAN_CONTROL => control = Some(payload),
            CHAN_EVENT => handle_event(&payload),
            CHAN_DATA => {
                if payload.len() > 4 {
                    let doff = 4 + (payload[3] as usize) * 4;
                    if payload.len() > doff {
                        let eth = &payload[doff..];
                        if eth.len() >= 14 && eth[12] == 0x88 && eth[13] == 0x8E {
                            supplicant_rx(eth);
                        } else {
                            let r = radio()?;
                            if r.rx_queue.len() < 64 {
                                r.rx_queue.push(eth.to_vec());
                            }
                        }
                    }
                }
            },
            _ => {},
        }
    }
    control
}

fn be32(b: &[u8], i: usize) -> u32 {
    u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]])
}
fn le32(b: &[u8], i: usize) -> u32 {
    u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]])
}
fn le16(b: &[u8], i: usize) -> u16 {
    u16::from_le_bytes([b[i], b[i + 1]])
}

/// An EAPOL frame from the access point: run it through the kernel's WPA2
/// supplicant and carry out what it asks for (replies, keys to install).
/// After associating: the RSN IE the firmware put in the association request
/// goes to the supplicant (message 2 must repeat it byte for byte), then a
/// message 1 that arrived meanwhile is handled.
fn assoc_ies_learned() {
    let ies = get_iovar("assoc_req_ies", 512).unwrap_or_default();
    let mut i = 0usize;
    let mut found = false;
    while i + 2 <= ies.len() {
        let (id, l) = (ies[i], ies[i + 1] as usize);
        if i + 2 + l > ies.len() {
            break;
        }
        if id == 0x30 {
            if let Some(sup) = radio().and_then(|r| r.sup.as_mut()) {
                sup.set_rsn_ie(&ies[i..i + 2 + l]);
                out(b"  wifi: association RSN IE taken from the firmware (");
                dec(b"", l as u64 + 2);
                out(b" bytes)\n");
                found = true;
            }
            break;
        }
        i += 2 + l;
    }
    if !found {
        out(b"  wifi: no RSN IE in the association request; using the default\n");
    }
    let Some(r) = radio() else { return };
    r.ies_known = true;
    if let Some(f) = r.pending_eapol.take() {
        supplicant_rx(&f);
    }
}

fn supplicant_rx(frame: &[u8]) {
    let Some(r) = radio() else { return };
    if !r.ies_known {
        // Message 1 can beat the SET_SSID event; answer it once the IEs are known.
        r.pending_eapol = Some(frame.to_vec());
        return;
    }
    let Some(sup) = r.sup.as_mut() else { return };
    let actions = match sup.rx(frame) {
        Ok(a) => a,
        Err(e) => {
            out(b"wifi: handshake: ");
            out(e.as_bytes());
            out(b"\n");
            r.sup_error = Some(e);
            r.psk_status = Some(0xFFFF);
            return;
        },
    };
    let ap = sup.ap();
    for a in actions {
        match a {
            super::wpa::Action::Send(f) => {
                if send_eth(&f).is_none() {
                    out(b"wifi: could not send a handshake message\n");
                }
            },
            super::wpa::Action::Pairwise(tk) => {
                // Let message 4 leave unencrypted before the key is in.
                delay_us(20_000);
                if let Err(e) = install_key(0, &tk, 0, &ap, None) {
                    dec(b"wifi: pairwise key refused, code -", e.unsigned_abs() as u64);
                    out(b"\n");
                }
            },
            super::wpa::Action::Group { index, key, tx, rsc } => {
                if let Err(e) = install_key(index as u32, &key, if tx { 2 } else { 0 }, &[0; 6], Some(rsc)) {
                    dec(b"wifi: group key refused, code -", e.unsigned_abs() as u64);
                    out(b"\n");
                }
            },
            super::wpa::Action::Done => {
                out(b"wifi: WPA2 handshake complete\n");
                let Some(r) = radio() else { return };
                r.psk_status = Some(6);
            },
        }
    }
}

/// `wsec_key`: brcmfmac's `brcmf_wsec_key_le` (164 bytes). CCMP; `ea` is the
/// access point for the pairwise key, zero for a group key; a group key's
/// receive sequence counter comes from the handshake message.
fn install_key(index: u32, key: &[u8], flags: u32, ea: &[u8; 6], rsc: Option<[u8; 8]>) -> Result<(), i32> {
    let mut b = [0u8; 164];
    b[0..4].copy_from_slice(&index.to_le_bytes());
    b[4..8].copy_from_slice(&(key.len() as u32).to_le_bytes());
    let n = key.len().min(32);
    b[8..8 + n].copy_from_slice(&key[..n]);
    b[112..116].copy_from_slice(&4u32.to_le_bytes()); // CRYPTO_ALGO_AES_CCM
    b[116..120].copy_from_slice(&flags.to_le_bytes());
    if let Some(rsc) = rsc {
        b[132..136].copy_from_slice(&1u32.to_le_bytes()); // iv_initialized
        b[140..144].copy_from_slice(&rsc[2..6]); // rxiv.hi
        b[144..146].copy_from_slice(&rsc[0..2]); // rxiv.lo
    }
    b[156..162].copy_from_slice(ea);
    set_iovar("wsec_key", &b)
}

fn handle_event(payload: &[u8]) {
    // BDC header, Ethernet header, Broadcom event header, event message.
    if payload.len() < 4 {
        return;
    }
    let p = &payload[4 + (payload[3] as usize) * 4..];
    if p.len() < 14 + 10 + 48 || u16::from_be_bytes([p[12], p[13]]) != 0x886C {
        return;
    }
    let m = &p[24..];
    let flags = u16::from_be_bytes([m[2], m[3]]);
    let ev = be32(m, 4);
    let status = be32(m, 8);
    let datalen = be32(m, 20) as usize;
    let data = &p[72..];
    let data = &data[..datalen.min(data.len())];
    let Some(r) = radio() else { return };
    match ev {
        EV_ESCAN_RESULT => {
            if status == 8 {
                parse_escan(r, data);
            } else {
                r.scan_done = Some(status);
            }
        },
        EV_SET_SSID => r.join_status = Some(status),
        EV_LINK => {
            r.link = flags & 1 != 0;
            if !r.link && r.joined {
                out(b"wifi: link down\n");
                r.joined = false;
                r.dropped = true;
                r.link_lost_ms = now_ms();
                r.link_note = alloc::format!("link down (status {status}, reason {})", be32(m, 12));
            }
        },
        EV_PSK_SUP => r.psk_status = Some(status),
        EV_AUTH => {},
        EV_DEAUTH | EV_DEAUTH_IND | EV_DISASSOC | EV_DISASSOC_IND => {
            let reason = be32(m, 12);
            if r.joined {
                out(b"wifi: disconnected by the access point\n");
                r.dropped = true;
                r.link_lost_ms = now_ms();
                r.link_note = alloc::format!(
                    "{} by the access point, reason {} ({})",
                    if ev == EV_DEAUTH || ev == EV_DEAUTH_IND { "deauthenticated" } else { "disassociated" },
                    reason,
                    reason_text(reason)
                );
            }
            r.joined = false;
            r.link = false;
        },
        _ => {},
    }
}

/// IEEE 802.11 reason codes an access point sends when it drops a station.
fn reason_text(code: u32) -> &'static str {
    match code {
        1 => "unspecified",
        2 => "previous authentication no longer valid",
        3 => "station is leaving",
        4 => "inactivity",
        5 => "access point is full",
        6 | 7 => "frame from a station that is not authenticated / associated",
        8 => "station left the BSS",
        13 => "invalid information element",
        14 => "message integrity check failed",
        15 => "4-way handshake timed out",
        16 => "group key handshake timed out",
        17 => "information element mismatch in the handshake",
        18 => "invalid group cipher",
        19 => "invalid pairwise cipher",
        20 => "invalid AKM",
        22 => "invalid RSN capabilities",
        23 => "802.1X authentication failed",
        24 => "cipher suite rejected by the security policy",
        34 => "poor channel conditions",
        _ => "no description",
    }
}

fn parse_escan(r: &mut Radio, data: &[u8]) {
    if data.len() < 12 {
        return;
    }
    let count = le16(data, 10) as usize;
    let mut off = 12usize;
    for _ in 0..count {
        if off + 128 > data.len() {
            break;
        }
        let b = &data[off..];
        let blen = le32(b, 4) as usize;
        if blen < 128 || off + blen > data.len() {
            break;
        }
        let ssid_len = (b[18] as usize).min(32);
        let ssid = String::from_utf8_lossy(&b[19..19 + ssid_len]).into_owned();
        let cap = le16(b, 16);
        let chanspec = le16(b, 72);
        let rssi = le16(b, 78) as i16 as i32;
        let ie_off = le16(b, 116) as usize;
        let ie_len = le32(b, 120) as usize;
        let mut secure = if cap & 0x10 != 0 { 1u8 } else { 0u8 };
        if ie_off + ie_len <= blen && ie_len > 0 {
            let ies = &b[ie_off..ie_off + ie_len];
            let mut i = 0usize;
            while i + 2 <= ies.len() {
                let (id, l) = (ies[i], ies[i + 1] as usize);
                if i + 2 + l > ies.len() {
                    break;
                }
                if id == 48 {
                    // RSN: version, group cipher, pairwise suites, AKM suites.
                    // PSK (2 / 6) joins with a passphrase; SAE-only (8) is WPA3.
                    let rsn = &ies[i + 2..i + 2 + l];
                    let (mut psk, mut sae) = (false, false);
                    if rsn.len() >= 8 {
                        let pc = u16::from_le_bytes([rsn[6], rsn[7]]) as usize;
                        let ak = 8 + pc * 4;
                        if rsn.len() >= ak + 2 {
                            let ac = u16::from_le_bytes([rsn[ak], rsn[ak + 1]]) as usize;
                            for k in 0..ac {
                                let o = ak + 2 + k * 4;
                                if rsn.len() >= o + 4 && rsn[o..o + 3] == [0x00, 0x0F, 0xAC] {
                                    match rsn[o + 3] {
                                        2 | 6 => psk = true,
                                        8 | 9 => sae = true,
                                        _ => {},
                                    }
                                }
                            }
                        }
                    }
                    secure = if sae && !psk { 4 } else { 3 };
                } else if id == 221 && l >= 4 && ies[i + 2..i + 6] == [0x00, 0x50, 0xF2, 0x01] && secure < 3 {
                    secure = 2;
                }
                i += 2 + l;
            }
        }
        let channel = chanspec & 0xFF;
        if !ssid.is_empty() {
            match r.scan_hits.iter_mut().find(|n| n.ssid == ssid) {
                Some(n) => {
                    if rssi > n.rssi {
                        n.rssi = rssi;
                        n.channel = channel;
                        n.secure = secure;
                    }
                },
                None => r.scan_hits.push(Network { ssid, rssi, secure, channel }),
            }
        }
        off += blen;
    }
}

// ── BCDC control channel ─────────────────────────────────────────────────────

/// An ioctl: returns the response bytes, or the firmware's error code.
fn ioctl(set: bool, cmd: u32, data: &[u8]) -> Result<Vec<u8>, i32> {
    let r = radio().ok_or(-1)?;
    r.ioctl_id = r.ioctl_id.wrapping_add(1);
    if r.ioctl_id == 0 {
        r.ioctl_id = 1;
    }
    let id = r.ioctl_id;
    let flags = ((id as u32) << 16) | if set { 0x2 } else { 0 };
    let mut p: Vec<u8> = Vec::with_capacity(16 + data.len());
    p.extend_from_slice(&cmd.to_le_bytes());
    p.extend_from_slice(&(data.len() as u32).to_le_bytes());
    p.extend_from_slice(&flags.to_le_bytes());
    p.extend_from_slice(&0u32.to_le_bytes());
    p.extend_from_slice(data);
    if send_frame(CHAN_CONTROL, &p).is_none() {
        return Err(-2);
    }
    let t0 = now_ms();
    loop {
        if let Some(resp) = poll_rx() {
            if resp.len() >= 16 {
                let rflags = le32(&resp, 8);
                let rid = (rflags >> 16) as u16;
                let status = le32(&resp, 12) as i32;
                if rid == id {
                    if rflags & 1 != 0 {
                        return Err(status);
                    }
                    return Ok(resp[16..].to_vec());
                }
            }
        }
        if now_ms() - t0 > 3_000 {
            return Err(-3);
        }
        delay_us(200);
    }
}

fn ioctl_set_u32(cmd: u32, v: u32) -> Result<(), i32> {
    ioctl(true, cmd, &v.to_le_bytes()).map(|_| ())
}

fn set_iovar(name: &str, data: &[u8]) -> Result<(), i32> {
    let mut p: Vec<u8> = Vec::with_capacity(name.len() + 1 + data.len());
    p.extend_from_slice(name.as_bytes());
    p.push(0);
    p.extend_from_slice(data);
    ioctl(true, WLC_SET_VAR, &p).map(|_| ())
}

fn set_iovar_u32(name: &str, v: u32) -> Result<(), i32> {
    set_iovar(name, &v.to_le_bytes())
}

fn set_iovar_u32x2(name: &str, a: u32, b: u32) -> Result<(), i32> {
    let mut d = [0u8; 8];
    d[..4].copy_from_slice(&a.to_le_bytes());
    d[4..].copy_from_slice(&b.to_le_bytes());
    set_iovar(name, &d)
}

fn get_iovar(name: &str, len: usize) -> Result<Vec<u8>, i32> {
    let mut p: Vec<u8> = Vec::with_capacity(name.len() + 1 + len);
    p.extend_from_slice(name.as_bytes());
    p.push(0);
    p.resize(p.len().max(len), 0);
    ioctl(false, WLC_GET_VAR, &p)
}

fn step(name: &[u8], r: Result<(), i32>) -> Option<()> {
    match r {
        Ok(()) => Some(()),
        Err(e) => {
            out(b"  wifi: ");
            out(name);
            out(b" failed, code ");
            dec(b"", (e as i64).unsigned_abs());
            out(b"\n");
            None
        },
    }
}

/// `step` for the join path: the failing iovar / ioctl and the firmware's
/// code end up in the message the picker shows.
fn stepc(name: &str, r: Result<(), i32>) -> Result<(), String> {
    match r {
        Ok(()) => Ok(()),
        Err(e) => {
            out(b"  wifi: ");
            out(name.as_bytes());
            out(b" failed, code ");
            dec(b"", (e as i64).unsigned_abs());
            out(b"\n");
            Err(alloc::format!("{name} refused by the firmware (code {e})"))
        },
    }
}

/// Signal strength of the joined network in dBm (WLC_GET_RSSI).
pub fn rssi() -> Option<i32> {
    if !is_joined() {
        return None;
    }
    let v = ioctl(false, 127, &[0u8; 4]).ok()?;
    if v.len() < 4 {
        return None;
    }
    Some(le32(&v, 0) as i32)
}

fn load_clm() -> Option<()> {
    const CHUNK: usize = 1024;
    let mut off = 0usize;
    while off < CLM.len() {
        let n = (CLM.len() - off).min(CHUNK);
        let mut flag: u16 = 1 << 12; // handler version
        if off == 0 {
            flag |= 0x2; // begin
        }
        if off + n >= CLM.len() {
            flag |= 0x4; // end
        }
        let mut d: Vec<u8> = Vec::with_capacity(12 + n);
        d.extend_from_slice(&flag.to_le_bytes());
        d.extend_from_slice(&2u16.to_le_bytes()); // CLM download type
        d.extend_from_slice(&(n as u32).to_le_bytes());
        d.extend_from_slice(&0u32.to_le_bytes());
        d.extend_from_slice(&CLM[off..off + n]);
        step(b"clmload", set_iovar("clmload", &d))?;
        off += n;
    }
    match get_iovar("clmload_status", 4) {
        Ok(v) if v.len() >= 4 && le32(&v, 0) == 0 => {
            out(b"  CLM blob loaded\n");
        },
        Ok(v) => {
            hex(b"  wifi: clmload_status=", if v.len() >= 4 { le32(&v, 0) as u64 } else { 0 }, 8);
        },
        Err(e) => {
            dec(b"  wifi: clmload_status unreadable (", (e as i64).unsigned_abs());
            out(b")\n");
        },
    }
    Some(())
}

fn radio_up() -> Option<()> {
    if radio().is_some() {
        return Some(());
    }
    let cores = erom_scan()?;
    load_firmware(&cores)?;
    start_f2(&cores)?;
    unsafe {
        *(&raw mut RADIO) = Some(Radio {
            cores,
            tx_seq: 0,
            tx_max: 1,
            ioctl_id: 0,
            mac: [0; 6],
            joined: false,
            link: false,
            ssid: String::new(),
            scan_done: None,
            scan_hits: Vec::new(),
            join_status: None,
            psk_status: None,
            sup: None,
            sup_error: None,
            pending_eapol: None,
            ies_known: true,
            rx_queue: Vec::new(),
            frames_in: 0,
            frames_out: 0,
            last_tx: String::new(),
            last_rx: String::new(),
            rx_data: 0,
        link_note: String::new(),
        link_lost_ms: 0,
        dropped: false,
        });
    }
    // Give the firmware a moment and take its first mailbox / credit frame.
    let t0 = now_ms();
    while now_ms() - t0 < 300 {
        poll_rx();
        delay_us(10_000);
    }
    // The firmware's version line: proof the control channel works.
    match get_iovar("ver", 128) {
        Ok(v) => {
            let end = v.iter().position(|&b| b == 0).unwrap_or(v.len());
            out(b"  firmware: ");
            out(String::from_utf8_lossy(&v[..end]).trim().as_bytes());
            out(b"\n");
        },
        Err(e) => {
            dec(b"  wifi: the firmware did not answer the first ioctl (", (e as i64).unsigned_abs());
            out(b")\n");
            unsafe { *(&raw mut RADIO) = None };
            unsafe { LAST_ERROR = "firmware control channel did not answer" };
            return None;
        },
    }
    load_clm()?;
    if let Ok(m) = get_iovar("cur_etheraddr", 6) {
        if m.len() >= 6 {
            let r = radio()?;
            r.mac.copy_from_slice(&m[..6]);
            out(b"  MAC ");
            for (i, b) in r.mac.iter().enumerate() {
                let h = |x: u8| if x < 10 { b'0' + x } else { b'a' + x - 10 };
                out(&[h(b >> 4), h(b & 0xF)]);
                if i < 5 {
                    out(b":");
                }
            }
            out(b"\n");
        }
    }
    // Country, events, station mode, up.
    let mut country = [0u8; 12];
    country[..2].copy_from_slice(b"XX");
    country[4..8].copy_from_slice(&(-1i32).to_le_bytes());
    country[8..10].copy_from_slice(b"XX");
    let _ = set_iovar("country", &country);
    let mut mask = [0u8; 16];
    for ev in [EV_SET_SSID, EV_AUTH, EV_DEAUTH, EV_DEAUTH_IND, EV_DISASSOC, EV_DISASSOC_IND, EV_LINK, EV_PSK_SUP, EV_ESCAN_RESULT] {
        mask[(ev / 8) as usize] |= 1 << (ev % 8);
    }
    let _ = set_iovar("event_msgs", &mask);
    let _ = set_iovar_u32("bus:txglom", 0);
    let _ = set_iovar_u32("apsta", 1);
    let _ = set_iovar_u32("ampdu_ba_wsize", 8);
    // 802.11 b/g/n on 2.4 GHz and a/n/ac on 5 GHz: HT and VHT on, the band
    // on auto, g-mode auto (these take effect before `up`).
    let _ = set_iovar_u32("nmode", 1);
    let _ = set_iovar_u32("vhtmode", 1);
    let _ = ioctl_set_u32(WLC_SET_GMODE, 1);
    let _ = ioctl_set_u32(WLC_SET_BAND, 0);
    step(b"up", ioctl(true, WLC_UP, &[]).map(|_| ()))?;
    let _ = ioctl_set_u32(WLC_SET_PM, 0);
    let flag = |name: &str| get_iovar(name, 4).ok().filter(|v| v.len() >= 4).map(|v| le32(&v, 0)).unwrap_or(0);
    out(b"  modes: 802.11b/g");
    if flag("nmode") != 0 {
        out(b"/n");
    }
    if flag("vhtmode") != 0 {
        out(b" + a/n/ac on 5 GHz");
    }
    out(b"\n  radio is up\n");
    Some(())
}

/// `wifi country <CC>`: the regulatory domain (channels 12/13 and the 5 GHz
/// set depend on it; "XX" is the world-safe default).
pub fn set_country(cc: &str) -> Result<(), String> {
    if !ensure_init() {
        return Err(String::from(last_error()));
    }
    let code = cc.trim().to_ascii_uppercase();
    if code.len() != 2 {
        return Err(String::from("a country code is two letters (JP, US, CN, GB ...)"));
    }
    let mut country = [0u8; 12];
    country[..2].copy_from_slice(code.as_bytes());
    country[4..8].copy_from_slice(&(-1i32).to_le_bytes());
    country[8..10].copy_from_slice(code.as_bytes());
    stepc("country", set_iovar("country", &country))
}

// ── public API ───────────────────────────────────────────────────────────────

/// Bus + firmware, once. False (with `last_error`) when the radio is unusable.
pub fn ensure_init() -> bool {
    if radio().is_some() {
        return true;
    }
    if unsafe { BUS_TRIED } && !last_error().is_empty() {
        return false;
    }
    unsafe { BUS_TRIED = true };
    if bus_up().is_none() {
        if last_error().is_empty() {
            unsafe { LAST_ERROR = "the radio did not answer on SDIO2" };
        }
        return false;
    }
    if radio_up().is_none() {
        if last_error().is_empty() {
            unsafe { LAST_ERROR = "firmware start failed" };
        }
        return false;
    }
    true
}

pub fn is_joined() -> bool {
    radio().is_some_and(|r| r.joined)
}

/// The link was lost on its own after a join (cleared by the next `join` / `leave`).
pub fn dropped() -> bool {
    radio().is_some_and(|r| r.dropped)
}

/// Why the last association ended and how long ago ("" while joined / never joined).
pub fn link_note() -> String {
    match radio() {
        Some(r) if !r.link_note.is_empty() && !r.joined => {
            let ago = now_ms().saturating_sub(r.link_lost_ms) / 1000;
            alloc::format!("{} -- {}:{:02} ago", r.link_note, ago / 60, ago % 60)
        },
        _ => String::new(),
    }
}

/// Handle whatever the firmware queued (link events, data) without taking a
/// data frame: for an idle desktop, so a lost link is noticed when it happens.
pub fn poll_events() {
    if radio().is_some() {
        let _ = poll_rx();
    }
}

pub fn mac() -> [u8; 6] {
    radio().map(|r| r.mac).unwrap_or([0; 6])
}

pub fn ssid() -> String {
    radio().map(|r| r.ssid.clone()).unwrap_or_default()
}

/// Scan for networks (a few seconds); strongest first.
pub fn scan() -> Result<Vec<Network>, &'static str> {
    if !ensure_init() {
        return Err(last_error());
    }
    let r = radio().ok_or("radio not up")?;
    r.scan_hits.clear();
    r.scan_done = None;
    // escan params: version 1, action start, sync id, then the scan params.
    let mut p: Vec<u8> = Vec::with_capacity(80);
    p.extend_from_slice(&1u32.to_le_bytes());
    p.extend_from_slice(&1u16.to_le_bytes());
    p.extend_from_slice(&0x1234u16.to_le_bytes());
    p.extend_from_slice(&0u32.to_le_bytes()); // ssid len
    p.extend_from_slice(&[0u8; 32]); // ssid
    p.extend_from_slice(&[0xFF; 6]); // bssid: any
    p.push(2); // bss type: any
    p.push(0); // active scan
    p.extend_from_slice(&(-1i32).to_le_bytes()); // nprobes
    p.extend_from_slice(&(-1i32).to_le_bytes()); // active time
    p.extend_from_slice(&(-1i32).to_le_bytes()); // passive time
    p.extend_from_slice(&(-1i32).to_le_bytes()); // home time
    p.extend_from_slice(&0u32.to_le_bytes()); // channel count
    p.extend_from_slice(&0u16.to_le_bytes()); // channel list
    if let Err(e) = set_iovar("escan", &p) {
        dec(b"  wifi: escan refused (", (e as i64).unsigned_abs());
        out(b")\n");
        return Err("the firmware refused to scan");
    }
    let t0 = now_ms();
    loop {
        poll_rx();
        if let Some(st) = radio().and_then(|r| r.scan_done) {
            if st != 0 {
                dec(b"  wifi: scan ended with status ", st as u64);
                out(b"\n");
            }
            break;
        }
        if now_ms() - t0 > 8_000 {
            out(b"  wifi: scan timed out\n");
            break;
        }
        delay_us(5_000);
    }
    let r = radio().ok_or("radio not up")?;
    let mut hits = r.scan_hits.clone();
    hits.sort_by(|a, b| b.rssi.cmp(&a.rssi));
    Ok(hits)
}

/// Join `ssid` (WPA2-PSK with a passphrase, open without); blocks until the
/// firmware reports the association or gives up.
pub fn join(ssid: &str, psk: &str) -> Result<(), String> {
    if !ensure_init() {
        return Err(String::from(last_error()));
    }
    if ssid.is_empty() || ssid.len() > 32 {
        return Err(String::from("network name must be 1-32 bytes"));
    }
    if !psk.is_empty() && (psk.len() < 8 || psk.len() > 63) {
        return Err(String::from("a WPA2 passphrase is 8-63 characters"));
    }
    let r = radio().ok_or_else(|| String::from("radio not up"))?;
    if r.joined {
        let _ = ioctl(true, WLC_DISASSOC, &[]);
        r.joined = false;
    }
    r.join_status = None;
    r.psk_status = None;
    r.link = false;
    r.dropped = false;
    r.link_note.clear();
    let wpa = !psk.is_empty();
    // What the last scan saw for this network: 4 = WPA3-only (SAE).
    let sae = r.scan_hits.iter().any(|n| n.ssid == ssid && n.secure == 4);
    if wpa && sae {
        out(b"  wifi: WPA3-only network, joining with SAE\n");
        stepc("wsec", ioctl_set_u32(WLC_SET_WSEC, 4))?;
        if set_iovar_u32("sup_wpa", 1).is_err() {
            let _ = set_iovar_u32x2("bsscfg:sup_wpa", 0, 1);
        }
        let mut pw = [0u8; 130];
        pw[..2].copy_from_slice(&(psk.len() as u16).to_le_bytes());
        pw[2..2 + psk.len()].copy_from_slice(psk.as_bytes());
        stepc("sae_password", set_iovar("sae_password", &pw)).map_err(|e| alloc::format!("{e}; this firmware may lack WPA3 -- enable WPA2 on the router"))?;
        stepc("infra", ioctl_set_u32(WLC_SET_INFRA, 1))?;
        stepc("auth", ioctl_set_u32(WLC_SET_AUTH, 3))?;
        stepc("wpa_auth", set_iovar_u32("wpa_auth", 0x40000))?;
    } else if wpa {
        stepc("wsec", ioctl_set_u32(WLC_SET_WSEC, 4))?;
        // This firmware has no internal supplicant (`sup_wpa` answers
        // BCME_UNSUPPORTED, -23), so the kernel does the 4-way handshake
        // itself (super::wpa) over the EAPOL frames the firmware passes up,
        // and installs the keys with `wsec_key` -- the way Linux works too.
        let pmk = super::wpa::pmk_from_passphrase(psk.as_bytes(), ssid.as_bytes());
        let mut snonce = [0u8; 32];
        if !crate::crypto::random_bytes(&mut snonce) {
            let t = crate::arch::timer::now_us();
            for (i, b) in snonce.iter_mut().enumerate() {
                *b = (t.wrapping_mul(0x9E37_79B9_7F4A_7C15).rotate_left(i as u32 * 5) >> (i % 8 * 8)) as u8;
            }
        }
        r.sup = Some(super::wpa::Handshake::new(pmk, r.mac, snonce));
        r.sup_error = None;
        // The firmware composes the association IEs itself (its RSN IE matches
        // what the access point offers -- group cipher included); the exact IE
        // it sent is read back after associating, for handshake message 2.
        r.pending_eapol = None;
        r.ies_known = false;
    } else {
        stepc("wsec", ioctl_set_u32(WLC_SET_WSEC, 0))?;
    }
    if !(wpa && sae) {
        stepc("infra", ioctl_set_u32(WLC_SET_INFRA, 1))?;
        stepc("auth", ioctl_set_u32(WLC_SET_AUTH, 0))?;
        stepc("wpa_auth", set_iovar_u32("wpa_auth", if wpa { 0x80 } else { 0 }))?;
    }
    let mut s = [0u8; 36];
    s[..4].copy_from_slice(&(ssid.len() as u32).to_le_bytes());
    s[4..4 + ssid.len()].copy_from_slice(ssid.as_bytes());
    stepc("set_ssid", ioctl(true, WLC_SET_SSID, &s).map(|_| ()))?;
    let t0 = now_ms();
    let mut assoc = false;
    loop {
        poll_rx();
        let r = radio().ok_or_else(|| String::from("radio not up"))?;
        if let Some(st) = r.join_status {
            if st != 0 {
                dec(b"  wifi: join failed, status ", st as u64);
                out(b"\n");
                return Err(match st {
                    3 => String::from("network not found"),
                    1 => String::from("the access point refused the association (status 1): check the security type; a 5 GHz network needs `wifi country <CC>`"),
                    _ => alloc::format!("the access point refused the join (status {st})"),
                });
            }
            if !assoc && wpa {
                assoc_ies_learned();
            }
            assoc = true;
        }
        if assoc && (!wpa || r.psk_status.is_some() || now_ms() - t0 > 12_000) {
            break;
        }
        if now_ms() - t0 > 20_000 {
            return Err(String::from("no answer from the access point (timeout)"));
        }
        delay_us(5_000);
    }
    let r = radio().ok_or_else(|| String::from("radio not up"))?;
    if wpa {
        match (r.psk_status, r.sup_error) {
            (Some(6), _) => {},
            (_, Some(e)) => {
                let _ = ioctl(true, WLC_DISASSOC, &[]);
                return Err(alloc::format!("WPA2 handshake: {e}"));
            },
            (Some(st), None) => {
                dec(b"  wifi: WPA2 handshake status ", st as u64);
                out(b"\n");
                return Err(alloc::format!("wrong passphrase (the WPA2 handshake failed, status {st})"));
            },
            (None, None) => {
                let _ = ioctl(true, WLC_DISASSOC, &[]);
                return Err(String::from("the WPA2 handshake did not complete (wrong passphrase?)"));
            },
        }
    }
    // No firmware power save: a polling host with no interrupt line would
    // otherwise see every frame the access point buffers arrive late. No ARP
    // or TCP offload either: the host's stack does all of it and must see
    // every frame as it crossed the air.
    let _ = ioctl_set_u32(WLC_SET_PM, 0);
    let _ = set_iovar_u32("arpoe", 0);
    let _ = set_iovar_u32("arp_ol", 0);
    let _ = set_iovar_u32("toe_ol", 0);
    r.joined = true;
    r.ssid = String::from(ssid);
    out(b"wifi: joined ");
    out(ssid.as_bytes());
    out(b"\n");
    Ok(())
}

pub fn leave() {
    if let Some(r) = radio() {
        if r.joined {
            let _ = ioctl(true, WLC_DISASSOC, &[]);
        }
        r.joined = false;
        r.link = false;
        r.dropped = false;
        r.link_note.clear();
        r.sup = None;
        r.ssid.clear();
    }
}

/// One Ethernet frame to the firmware's data channel (BDC header first).
fn send_eth(frame: &[u8]) -> Option<()> {
    let mut p: Vec<u8> = Vec::with_capacity(4 + frame.len());
    p.extend_from_slice(&[0x20, 0, 0, 0]); // BDC: protocol version 2, priority 0, if 0
    p.extend_from_slice(frame);
    send_frame(CHAN_DATA, &p)
}

/// One Ethernet frame to the air (after `join`).
pub fn transmit(frame: &[u8]) -> bool {
    if !is_joined() || frame.len() < 14 || frame.len() > 1600 {
        return false;
    }
    let ok = send_eth(frame).is_some();
    if let Some(r) = radio() {
        r.last_tx = frame_summary(frame, ok);
    }
    ok
}

/// "UDP 192.168.0.38:33012 > 1.1.1.1:53 (78 B)" and the like.
fn frame_summary(f: &[u8], ok: bool) -> String {
    if f.len() < 14 {
        return String::from("short frame");
    }
    let et = u16::from_be_bytes([f[12], f[13]]);
    let ip = |o: usize| alloc::format!("{}.{}.{}.{}", f[o], f[o + 1], f[o + 2], f[o + 3]);
    let s = if et == 0x0806 && f.len() >= 42 {
        alloc::format!("ARP {} {} > {}", if f[21] == 1 { "who-has" } else { "reply" }, ip(28), ip(38))
    } else if et == 0x0800 && f.len() >= 34 {
        let ihl = ((f[14] & 0x0F) as usize) * 4;
        let t = 14 + ihl;
        let ports = if f.len() >= t + 4 { alloc::format!(":{} > {}:{}", u16::from_be_bytes([f[t], f[t + 1]]), ip(30), u16::from_be_bytes([f[t + 2], f[t + 3]])) } else { alloc::format!(" > {}", ip(30)) };
        let proto = match f[23] {
            1 => "ICMP",
            6 => "TCP",
            17 => "UDP",
            _ => "IP",
        };
        alloc::format!("{proto} {}{ports}", ip(26))
    } else if et == 0x888E {
        String::from("EAPOL")
    } else {
        alloc::format!("ethertype {et:04x}")
    };
    alloc::format!("{s} ({} B){}", f.len(), if ok { "" } else { " NOT SENT" })
}

/// What the last frames looked like, for the connection test.
pub fn last_frames() -> String {
    match radio() {
        Some(r) => alloc::format!("wifi: {} frames in ({} data), {} out; last out: {}; last in: {}", r.frames_in, r.rx_data, r.frames_out, if r.last_tx.is_empty() { "-" } else { &r.last_tx }, if r.last_rx.is_empty() { "-" } else { &r.last_rx }),
        None => String::from("wifi: radio not up"),
    }
}

/// One received Ethernet frame, if any arrived.
pub fn receive(outbuf: &mut [u8]) -> Option<usize> {
    if radio()?.rx_queue.is_empty() {
        poll_rx();
    }
    let r = radio()?;
    if r.rx_queue.is_empty() {
        return None;
    }
    let f = r.rx_queue.remove(0);
    let n = f.len().min(outbuf.len());
    outbuf[..n].copy_from_slice(&f[..n]);
    r.rx_data += 1;
    r.last_rx = frame_summary(&f, true);
    Some(n)
}

/// A line for the console / tray.
pub fn status() -> String {
    match radio() {
        None => {
            if last_error().is_empty() {
                String::from("radio not started (wifi scan starts it)")
            } else {
                alloc::format!("radio unavailable: {}", last_error())
            }
        },
        Some(r) if r.joined => alloc::format!("joined {} ({} frames in, {} out)", r.ssid, r.frames_in, r.frames_out),
        Some(r) if !r.link_note.is_empty() => alloc::format!("not joined: {}", link_note()),
        Some(_) => String::from("radio up, not joined"),
    }
}

/// Console `wifi`: bring everything up and report.
pub fn probe() -> Option<u32> {
    if ensure_init() {
        out(b"wifi: ");
        out(status().as_bytes());
        out(b"\n");
        Some(0x4345)
    } else {
        out(b"wifi: ");
        out(last_error().as_bytes());
        out(b"\n");
        None
    }
}

← Back to index