Skip to main content

atmos/kernel/
wifi.rs

1// wifi.rs - CYW43455 WiFi driver for RPi3B+ via SDIO (Broadcom FMAC protocol)
2// Firmware must be placed at SylFS /boot/brcmfmac43455-sdio.bin
3#![allow(dead_code)]
4
5extern crate alloc;
6use crate::kernel::sdio;
7use alloc::vec::Vec;
8use spin::Mutex;
9
10// ---- SDPCM transport layer constants ----
11const SDPCM_HDR_LEN: usize = 12; // SDPCM header size
12const SDPCM_CHAN_CONTROL: u8 = 0; // ioctl channel
13const SDPCM_CHAN_EVENT: u8 = 1; // async event channel
14const SDPCM_CHAN_DATA: u8 = 2; // Ethernet data channel
15
16// ---- CDC ioctl header constants ----
17const CDC_HDR_LEN: usize = 16;
18const CDC_FLAG_SET: u32 = 0x0000; // SET ioctl
19const CDC_FLAG_GET: u32 = 0x0002; // GET ioctl (bit 1)
20const CDC_ID_SHIFT: u32 = 16; // transaction id in bits [31:16]
21
22// ---- BRCMF ioctl command numbers ----
23const BRCMF_C_GET_REVINFO: u32 = 202;
24const BRCMF_C_SET_INFRA: u32 = 20;
25const BRCMF_C_SET_AUTH: u32 = 22;
26const BRCMF_C_SET_WSEC: u32 = 134;
27const BRCMF_C_SET_SSID: u32 = 26;
28const BRCMF_C_DISASSOC: u32 = 52;
29const BRCMF_C_SET_PM: u32 = 86;
30const BRCMF_C_SET_VAR: u32 = 263; // set iovar
31const BRCMF_C_GET_VAR: u32 = 262; // get iovar
32const BRCMF_C_SET_SCAN_CHANNEL_TIME: u32 = 185;
33const BRCMF_C_SET_SCAN_UNASSOC_TIME: u32 = 187;
34
35// ---- BRCMF event types (received on SDPCM_CHAN_EVENT) ----
36const BRCMF_E_SET_SSID: u32 = 0;
37const BRCMF_E_AUTH: u32 = 3;
38const BRCMF_E_DEAUTH: u32 = 5;
39const BRCMF_E_ASSOC: u32 = 7;
40const BRCMF_E_DISASSOC: u32 = 11;
41const BRCMF_E_LINK: u32 = 16;
42const BRCMF_E_PSK_SUP: u32 = 46;
43const BRCMF_E_STATUS_SUCCESS: u32 = 0;
44
45// ---- WiFi security ----
46const WSEC_AES: u32 = 4; // WPA2-AES
47const WSEC_PASSPHRASE: u16 = 0x0200; // flag: key is ASCII passphrase (firmware converts to PSK)
48
49// ---- Backplane / clock registers ----
50const SBSDIO_FUNC1_CHIPCLKCSR: u32 = 0x1000E;
51const SBSDIO_HT_AVAIL: u8 = 0x80; // HT clock available
52const SBSDIO_HT_AVAIL_REQ: u8 = 0x10; // request HT clock
53const SBSDIO_ALP_AVAIL_REQ: u8 = 0x08;
54const SBSDIO_FUNC1_SBADDRLOW: u32 = 0x1000A;
55const SBSDIO_FUNC1_SBADDRMID: u32 = 0x1000B;
56const SBSDIO_FUNC1_SBADDRHIGH: u32 = 0x1000C;
57
58// ---- CYW43455 chip addresses ----
59const BRCMF_BACKPLANE_ARM: u32 = 0x18003000;
60
61// ---- WiFi state machine ----
62#[derive(Copy, Clone, PartialEq, Eq)]
63pub enum WifiState {
64    Uninitialized,
65    FirmwareLoaded,
66    Idle,
67    Scanning,
68    Connecting,
69    Connected,
70    Error,
71}
72
73pub struct WifiNetwork {
74    pub ssid: [u8; 32],
75    pub ssid_len: usize,
76    pub rssi: i8,
77    pub channel: u8,
78    pub security: WifiSecurity,
79    pub bssid: [u8; 6],
80}
81
82#[derive(Copy, Clone, PartialEq, Eq)]
83pub enum WifiSecurity {
84    Open,
85    Wpa2Personal,
86    Unknown,
87}
88
89// ---- Global state ----
90static WIFI_STATE: Mutex<WifiState> = Mutex::new(WifiState::Uninitialized);
91static mut WIFI_IP: [u8; 4] = [0; 4];
92static mut WIFI_SSID: [u8; 32] = [0; 32];
93static mut WIFI_SSID_LEN: usize = 0;
94static mut TX_SEQ: u8 = 0;
95static mut TX_ID: u16 = 0;
96
97// ---- Utilities ----
98
99fn delay_ms(ms: u32) {
100    for _ in 0..(ms * 50_000) {
101        unsafe { core::arch::asm!("nop") };
102    }
103}
104
105// ============================================================
106// Backplane access (via SDIO function 1)
107// ============================================================
108
109unsafe fn set_backplane_window(addr: u32) -> bool {
110    let a0 = ((addr >> 8) & 0xFF) as u8;
111    let a1 = ((addr >> 16) & 0xFF) as u8;
112    let a2 = ((addr >> 24) & 0xFF) as u8;
113    sdio::cmd52(sdio::SDIO_FUNC_BACKPLANE, true, SBSDIO_FUNC1_SBADDRLOW, a0).is_some()
114        && sdio::cmd52(sdio::SDIO_FUNC_BACKPLANE, true, SBSDIO_FUNC1_SBADDRMID, a1).is_some()
115        && sdio::cmd52(sdio::SDIO_FUNC_BACKPLANE, true, SBSDIO_FUNC1_SBADDRHIGH, a2).is_some()
116}
117
118unsafe fn backplane_read32(addr: u32) -> Option<u32> {
119    let window = addr & !0x7FFF;
120    if !set_backplane_window(window) {
121        return None;
122    }
123    let offset = (addr & 0x7FFF) | 0x08000; // 32-bit access flag
124    let mut buf = [0u8; 4];
125    if !sdio::cmd53(sdio::SDIO_FUNC_BACKPLANE, false, offset, &mut buf) {
126        return None;
127    }
128    Some(u32::from_le_bytes(buf))
129}
130
131unsafe fn backplane_write32(addr: u32, val: u32) -> bool {
132    let window = addr & !0x7FFF;
133    if !set_backplane_window(window) {
134        return false;
135    }
136    let offset = (addr & 0x7FFF) | 0x08000;
137    sdio::cmd53(
138        sdio::SDIO_FUNC_BACKPLANE,
139        true,
140        offset,
141        &mut val.to_le_bytes().clone(),
142    )
143}
144
145// ============================================================
146// Firmware upload and start
147// ============================================================
148
149unsafe fn upload_firmware(firmware: &[u8]) -> bool {
150    crate::info!("[NET] WiFi: Uploading firmware ({} bytes)...", firmware.len());
151    const CHUNK: usize = 64;
152    let mut offset = 0usize;
153    while offset < firmware.len() {
154        let chunk_len = (firmware.len() - offset).min(CHUNK);
155        let addr = offset as u32;
156        if !set_backplane_window(addr & !0x7FFF) {
157            return false;
158        }
159        let reg = addr & 0x7FFF;
160        let mut buf = [0u8; CHUNK];
161        buf[..chunk_len].copy_from_slice(&firmware[offset..offset + chunk_len]);
162        if !sdio::cmd53(sdio::SDIO_FUNC_BACKPLANE, true, reg, &mut buf[..chunk_len]) {
163            crate::info!("[NET] WiFi: firmware write failed at offset 0x{:X}", offset);
164            return false;
165        }
166        offset += chunk_len;
167        if offset.is_multiple_of(64 * 1024) {
168            crate::info!(
169                "WiFi: firmware {}KB / {}KB",
170                offset / 1024,
171                firmware.len() / 1024
172            );
173        }
174    }
175    crate::info!("[NET] WiFi: Firmware uploaded.");
176    true
177}
178
179unsafe fn start_firmware() -> bool {
180    // Release ARM core reset
181    if !backplane_write32(BRCMF_BACKPLANE_ARM + 0x800, 0) {
182        crate::info!("[NET] WiFi: ARM reset write failed");
183        return false;
184    }
185    delay_ms(50);
186    crate::info!("[NET] WiFi: ARM core reset released.");
187    true
188}
189
190// ============================================================
191// Wait for HT clock (firmware ready signal)
192// ============================================================
193
194unsafe fn wait_ht_clock() -> bool {
195    // Request HT (High Throughput) clock from chip
196    sdio::cmd52(
197        sdio::SDIO_FUNC_BACKPLANE,
198        true,
199        SBSDIO_FUNC1_CHIPCLKCSR,
200        SBSDIO_HT_AVAIL_REQ | SBSDIO_ALP_AVAIL_REQ,
201    );
202    // Wait up to 1000ms
203    for _ in 0..1000 {
204        let clk =
205            sdio::cmd52(sdio::SDIO_FUNC_BACKPLANE, false, SBSDIO_FUNC1_CHIPCLKCSR, 0).unwrap_or(0);
206        if (clk & SBSDIO_HT_AVAIL) != 0 {
207            crate::info!("[NET] WiFi: HT clock available (CSR=0x{:02X})", clk);
208            return true;
209        }
210        delay_ms(1);
211    }
212    crate::info!("[NET] WiFi: HT clock timeout");
213    false
214}
215
216// ============================================================
217// SDPCM frame transport (over SDIO function 2 WLAN FIFO)
218// ============================================================
219
220// Build and send an SDPCM frame on the specified channel.
221// `payload` = bytes after the SDPCM header (CDC header + data for control channel).
222unsafe fn sdpcm_send(channel: u8, payload: &[u8]) -> bool {
223    let total = SDPCM_HDR_LEN + payload.len();
224    if total > 512 {
225        crate::info!("[NET] WiFi: sdpcm_send frame too large: {}", total);
226        return false;
227    }
228    let mut frame = [0u8; 512];
229    // SDPCM header (12 bytes)
230    let len = total as u16;
231    frame[0] = (len & 0xFF) as u8;
232    frame[1] = (len >> 8) as u8;
233    frame[2] = !(len as u8);
234    frame[3] = !((len >> 8) as u8);
235    frame[4] = TX_SEQ;
236    TX_SEQ = TX_SEQ.wrapping_add(1);
237    frame[5] = channel & 0x0F;
238    frame[6] = 0; // next_len
239    frame[7] = SDPCM_HDR_LEN as u8; // data offset
240    frame[8] = 0; // flow control mask
241    frame[9] = 0; // credit
242    frame[10] = 0;
243    frame[11] = 0;
244    frame[SDPCM_HDR_LEN..total].copy_from_slice(payload);
245    // Send via CMD53 to WLAN FIFO (func 2, addr 0, no-increment)
246    sdio::cmd53(sdio::SDIO_FUNC_WLAN, true, 0, &mut frame[..total])
247}
248
249// Try to receive one SDPCM frame from the WLAN FIFO.
250// Returns (channel, data_offset, frame_bytes) on success.
251unsafe fn sdpcm_recv() -> Option<(u8, usize, Vec<u8>)> {
252    // Check if F2 interrupt is pending (CCCR_INT_PENDING reg 0x05, bit 2 = F2)
253    let int_pending = sdio::cmd52(sdio::SDIO_FUNC_BUS, false, 0x05, 0)?;
254    if (int_pending & 0x04) == 0 {
255        return None;
256    }
257
258    // Read first 4 bytes to discover frame length
259    let mut hdr4 = [0u8; 4];
260    if !sdio::cmd53(sdio::SDIO_FUNC_WLAN, false, 0, &mut hdr4) {
261        return None;
262    }
263    let frame_len = u16::from_le_bytes([hdr4[0], hdr4[1]]) as usize;
264    let frame_chk = u16::from_le_bytes([hdr4[2], hdr4[3]]) as usize;
265    // Validate header checksum
266    if frame_len == 0 || (frame_len ^ frame_chk) != 0xFFFF || frame_len < SDPCM_HDR_LEN {
267        return None;
268    }
269
270    let mut frame = alloc::vec![0u8; frame_len];
271    frame[..4].copy_from_slice(&hdr4);
272
273    // Read the rest of the frame (up to 508 bytes per CMD53 call)
274    let remaining = frame_len - 4;
275    if remaining > 0 {
276        let mut offset = 4usize;
277        while offset < frame_len {
278            let chunk = (frame_len - offset).min(508);
279            if !sdio::cmd53(
280                sdio::SDIO_FUNC_WLAN,
281                false,
282                0,
283                &mut frame[offset..offset + chunk],
284            ) {
285                return None;
286            }
287            offset += chunk;
288        }
289    }
290
291    let channel = frame[5] & 0x0F;
292    let data_off = frame[7] as usize;
293    if data_off > frame_len {
294        return None;
295    }
296    Some((channel, data_off, frame))
297}
298
299// ============================================================
300// CDC ioctl layer
301// ============================================================
302
303// Send a CDC ioctl and wait for a matching response.
304// `get`: true = GET, false = SET
305// `payload_buf`: for SET it contains data to send; for GET it will receive the response.
306// Returns number of response bytes written to `payload_buf` on success.
307unsafe fn ioctl(cmd: u32, get: bool, payload_buf: &mut [u8]) -> Option<usize> {
308    TX_ID = TX_ID.wrapping_add(1);
309    let id = TX_ID as u32;
310    let flags = if get { CDC_FLAG_GET } else { CDC_FLAG_SET } | (id << CDC_ID_SHIFT);
311    let plen = payload_buf.len();
312
313    // Build CDC frame: 16-byte header + payload
314    let mut cdc = alloc::vec![0u8; CDC_HDR_LEN + plen];
315    cdc[0..4].copy_from_slice(&cmd.to_le_bytes());
316    cdc[4..8].copy_from_slice(&(plen as u32).to_le_bytes());
317    cdc[8..12].copy_from_slice(&flags.to_le_bytes());
318    cdc[12..16].copy_from_slice(&0u32.to_le_bytes()); // status = 0 (request)
319    if plen > 0 && !get {
320        cdc[CDC_HDR_LEN..].copy_from_slice(payload_buf);
321    }
322
323    if !sdpcm_send(SDPCM_CHAN_CONTROL, &cdc) {
324        crate::info!("[NET] WiFi: ioctl cmd={} send failed", cmd);
325        return None;
326    }
327
328    // Poll for matching response (up to 2000ms)
329    for _ in 0..2000 {
330        if let Some((chan, data_off, frame)) = sdpcm_recv() {
331            if chan == SDPCM_CHAN_CONTROL {
332                if data_off + CDC_HDR_LEN > frame.len() {
333                    continue;
334                }
335                let cdc_resp = &frame[data_off..];
336                let resp_flags =
337                    u32::from_le_bytes([cdc_resp[8], cdc_resp[9], cdc_resp[10], cdc_resp[11]]);
338                let resp_id = resp_flags >> CDC_ID_SHIFT;
339                if resp_id != (id & 0xFFFF) {
340                    continue;
341                } // id mismatch, keep polling
342                let status =
343                    u32::from_le_bytes([cdc_resp[12], cdc_resp[13], cdc_resp[14], cdc_resp[15]]);
344                if status != 0 {
345                    crate::info!("[NET] WiFi: ioctl cmd={} status={}", cmd, status);
346                    return None;
347                }
348                if get {
349                    let resp_plen =
350                        u32::from_le_bytes([cdc_resp[4], cdc_resp[5], cdc_resp[6], cdc_resp[7]])
351                            as usize;
352                    let copy_len = resp_plen.min(plen);
353                    let payload_start = data_off + CDC_HDR_LEN;
354                    if payload_start + copy_len <= frame.len() {
355                        payload_buf[..copy_len]
356                            .copy_from_slice(&frame[payload_start..payload_start + copy_len]);
357                        return Some(copy_len);
358                    }
359                }
360                return Some(0);
361            }
362            // Discard non-control frames silently (events, data)
363        }
364        delay_ms(1);
365    }
366    crate::info!("[NET] WiFi: ioctl cmd={} timeout", cmd);
367    None
368}
369
370unsafe fn ioctl_set_u32(cmd: u32, val: u32) -> bool {
371    ioctl(cmd, false, &mut val.to_le_bytes().clone()).is_some()
372}
373
374// ---- Iovar helpers ----
375
376// Build iovar payload: null-terminated name + value bytes
377fn build_iovar(name: &str, data: &[u8]) -> Vec<u8> {
378    let nb = name.as_bytes();
379    let mut v = alloc::vec![0u8; nb.len() + 1 + data.len()];
380    v[..nb.len()].copy_from_slice(nb);
381    // byte[nb.len()] = 0 (null terminator, already zeroed)
382    v[nb.len() + 1..].copy_from_slice(data);
383    v
384}
385
386unsafe fn iovar_set(name: &str, data: &[u8]) -> bool {
387    let mut buf = build_iovar(name, data);
388    ioctl(BRCMF_C_SET_VAR, false, &mut buf).is_some()
389}
390
391unsafe fn iovar_set_u32(name: &str, val: u32) -> bool {
392    iovar_set(name, &val.to_le_bytes())
393}
394
395// bsscfg-scoped iovar: name\0 + bsscfg_idx(u32) + value
396unsafe fn iovar_set_bsscfg(name: &str, bsscfg: u32, data: &[u8]) -> bool {
397    let nb = name.as_bytes();
398    let mut buf = alloc::vec![0u8; nb.len() + 1 + 4 + data.len()];
399    buf[..nb.len()].copy_from_slice(nb);
400    buf[nb.len() + 1..nb.len() + 5].copy_from_slice(&bsscfg.to_le_bytes());
401    buf[nb.len() + 5..].copy_from_slice(data);
402    ioctl(BRCMF_C_SET_VAR, false, &mut buf).is_some()
403}
404
405// ============================================================
406// Event reception
407// ============================================================
408
409// Poll for a specific event type. Returns event status on success.
410// brcmf_event_msg (big-endian) is at: data_off + 4 (BDC) + 14 (Ethernet header)
411unsafe fn wait_event(want_event: u32, timeout_ms: u32) -> Option<u32> {
412    for _ in 0..timeout_ms {
413        if let Some((chan, data_off, frame)) = sdpcm_recv() {
414            if chan == SDPCM_CHAN_EVENT {
415                // BDC header: 4 bytes at data_off
416                // Ethernet header: 14 bytes
417                // brcmf_event_msg: starts at data_off + 18
418                let msg_off = data_off + 4 + 14; // skip BDC + Ethernet
419                if msg_off + 20 > frame.len() {
420                    continue;
421                }
422                // event_type at offset 4, status at offset 8 — big-endian
423                let evt = u32::from_be_bytes([
424                    frame[msg_off + 4],
425                    frame[msg_off + 5],
426                    frame[msg_off + 6],
427                    frame[msg_off + 7],
428                ]);
429                let status = u32::from_be_bytes([
430                    frame[msg_off + 8],
431                    frame[msg_off + 9],
432                    frame[msg_off + 10],
433                    frame[msg_off + 11],
434                ]);
435                crate::info!("[NET] WiFi: event={} status={}", evt, status);
436                if evt == want_event {
437                    return Some(status);
438                }
439            }
440            // Also handle control channel responses during event wait (discard)
441        }
442        delay_ms(1);
443    }
444    None
445}
446
447// Enable reception of specific events via "event_msgs" iovar (16-byte bitmask)
448unsafe fn enable_events() -> bool {
449    let mut mask = [0u8; 16];
450    // Enable: E_SET_SSID(0), E_AUTH(3), E_DEAUTH(5), E_ASSOC(7), E_DISASSOC(11), E_LINK(16), E_PSK_SUP(46)
451    let events = [0u32, 3, 5, 7, 11, 16, 46];
452    for e in &events {
453        let byte = (*e / 8) as usize;
454        let bit = *e % 8;
455        if byte < 16 {
456            mask[byte] |= 1 << bit;
457        }
458    }
459    iovar_set("event_msgs", &mask)
460}
461
462// ============================================================
463// HLP (High-Level Protocol) initialization
464// ============================================================
465
466unsafe fn hlp_init() -> bool {
467    // 1. Wait for HT clock (signals firmware is ready)
468    if !wait_ht_clock() {
469        return false;
470    }
471
472    // 2. Verify chip via GET_REVINFO (48-byte struct)
473    let mut revinfo = [0u8; 48];
474    if ioctl(BRCMF_C_GET_REVINFO, true, &mut revinfo).is_some() {
475        let chip_id = u32::from_le_bytes([revinfo[0], revinfo[1], revinfo[2], revinfo[3]]);
476        crate::info!("[NET] WiFi: chip_id=0x{:04X}", chip_id);
477    } else {
478        crate::info!("[NET] WiFi: GET_REVINFO failed (firmware may still be booting)");
479        // Non-fatal: continue
480    }
481
482    // 3. Set country code (US, rev 0)
483    // wl_country_t: country_abbrev[4] + rev(i32) + ccode[4]
484    let mut country = [0u8; 12];
485    country[0] = b'U';
486    country[1] = b'S'; // country_abbrev
487                       // rev = 0 (auto), ccode = "US"
488    country[8] = b'U';
489    country[9] = b'S';
490    if !iovar_set("country", &country) {
491        crate::info!("[NET] WiFi: country set failed (non-fatal)");
492    }
493
494    // 4. Disable power management for faster response
495    if !ioctl_set_u32(BRCMF_C_SET_PM, 0) {
496        crate::info!("[NET] WiFi: SET_PM failed (non-fatal)");
497    }
498
499    // 5. Enable event reception
500    if !enable_events() {
501        crate::info!("[NET] WiFi: enable_events failed (non-fatal)");
502    }
503
504    crate::info!("[NET] WiFi: HLP protocol initialized.");
505    true
506}
507
508// ============================================================
509// Public API
510// ============================================================
511
512/// Initialize WiFi: load firmware, start chip, run HLP init.
513pub fn init() -> bool {
514    if !sdio::is_initialized() {
515        return false;
516    }
517    crate::info!("[NET] WiFi: Initializing CYW43455...");
518
519    // Load firmware from SylFS
520    let fw_bytes = match crate::kernel::fs::read_file("/boot/brcmfmac43455-sdio.bin") {
521        Some(d) => d,
522        None => {
523            crate::info!("[NET] WiFi: firmware not found at /boot/brcmfmac43455-sdio.bin");
524            crate::info!("[NET] WiFi: copy brcmfmac43455-sdio.bin to SD card /boot/ folder.");
525            return false;
526        }
527    };
528    crate::info!("[NET] WiFi: firmware {} bytes loaded.", fw_bytes.len());
529
530    unsafe {
531        // Request ALP clock before firmware upload
532        sdio::cmd52(
533            sdio::SDIO_FUNC_BACKPLANE,
534            true,
535            SBSDIO_FUNC1_CHIPCLKCSR,
536            SBSDIO_ALP_AVAIL_REQ,
537        );
538        delay_ms(20);
539
540        if !upload_firmware(&fw_bytes) {
541            return false;
542        }
543        if !start_firmware() {
544            return false;
545        }
546    }
547
548    delay_ms(500); // wait for firmware to boot
549
550    *WIFI_STATE.lock() = WifiState::FirmwareLoaded;
551
552    unsafe {
553        if !hlp_init() {
554            crate::info!("[NET] WiFi: HLP init failed");
555            *WIFI_STATE.lock() = WifiState::Error;
556            return false;
557        }
558    }
559
560    *WIFI_STATE.lock() = WifiState::Idle;
561    crate::info!("[NET] WiFi: CYW43455 ready.");
562    true
563}
564
565/// Connect to WPA2-PSK network.
566/// Returns true when associated; IP must then be acquired via DHCP by the caller.
567pub fn connect(ssid: &str, password: &str) -> bool {
568    {
569        let state = *WIFI_STATE.lock();
570        if state != WifiState::Idle {
571            return false;
572        }
573    }
574    if ssid.is_empty() || ssid.len() > 32 {
575        return false;
576    }
577
578    crate::info!("[NET] WiFi: Connecting to '{}'...", ssid);
579    *WIFI_STATE.lock() = WifiState::Connecting;
580
581    unsafe {
582        WIFI_SSID_LEN = ssid.len();
583        WIFI_SSID[..ssid.len()].copy_from_slice(ssid.as_bytes());
584    }
585
586    unsafe {
587        // 1. Set WPA2-AES security
588        if !ioctl_set_u32(BRCMF_C_SET_WSEC, WSEC_AES) {
589            crate::info!("[NET] WiFi: SET_WSEC failed");
590            return fail_connect();
591        }
592
593        // 2. Enable supplicant (firmware handles 4-way handshake)
594        if !iovar_set_bsscfg("sup_wpa", 0, &1u32.to_le_bytes()) {
595            crate::info!("[NET] WiFi: sup_wpa failed (non-fatal)");
596        }
597        // WPA2-EAPOL version = -1 (auto)
598        if !iovar_set_bsscfg("sup_wpa2_eapver", 0, &0xFFFF_FFFFu32.to_le_bytes()) {
599            crate::info!("[NET] WiFi: sup_wpa2_eapver failed (non-fatal)");
600        }
601        // Supplicant timeout 2500ms
602        if !iovar_set_bsscfg("sup_wpa_tmo", 0, &2500u32.to_le_bytes()) {
603            crate::info!("[NET] WiFi: sup_wpa_tmo failed (non-fatal)");
604        }
605
606        // 3. Provide passphrase (wsec_pmk_t: key_len[2] + flags[2] + key[64])
607        {
608            let pw = password.as_bytes();
609            let pw_len = pw.len().min(64);
610            let mut pmk = [0u8; 68]; // 2 + 2 + 64
611            pmk[0] = pw_len as u8;
612            pmk[1] = (pw_len >> 8) as u8;
613            pmk[2] = (WSEC_PASSPHRASE & 0xFF) as u8;
614            pmk[3] = (WSEC_PASSPHRASE >> 8) as u8;
615            pmk[4..4 + pw_len].copy_from_slice(&pw[..pw_len]);
616            if !iovar_set("wsec_pmk", &pmk) {
617                crate::info!("[NET] WiFi: wsec_pmk failed");
618                return fail_connect();
619            }
620        }
621
622        // 4. Infrastructure mode (BSS)
623        if !ioctl_set_u32(BRCMF_C_SET_INFRA, 1) {
624            crate::info!("[NET] WiFi: SET_INFRA failed");
625            return fail_connect();
626        }
627
628        // 5. Open auth (firmware does WPA2 internally)
629        if !ioctl_set_u32(BRCMF_C_SET_AUTH, 0) {
630            crate::info!("[NET] WiFi: SET_AUTH failed");
631            return fail_connect();
632        }
633
634        // 6. Join: SET_SSID — wlc_ssid_t: ssid_len(u32) + ssid[32]
635        {
636            let mut ssid_buf = [0u8; 36];
637            let slen = ssid.len().min(32);
638            ssid_buf[0] = slen as u8;
639            ssid_buf[4..4 + slen].copy_from_slice(&ssid.as_bytes()[..slen]);
640            if ioctl(BRCMF_C_SET_SSID, false, &mut ssid_buf).is_none() {
641                crate::info!("[NET] WiFi: SET_SSID failed");
642                return fail_connect();
643            }
644        }
645
646        // 7. Wait for BRCMF_E_SET_SSID event (up to 15s)
647        crate::info!("[NET] WiFi: Waiting for association (up to 15s)...");
648        match wait_event(BRCMF_E_SET_SSID, 15_000) {
649            Some(BRCMF_E_STATUS_SUCCESS) => {
650                crate::info!("[NET] WiFi: Associated with '{}'!", ssid);
651                *WIFI_STATE.lock() = WifiState::Connected;
652                true
653            }
654            Some(status) => {
655                crate::info!("[NET] WiFi: Association failed (status={})", status);
656                fail_connect()
657            }
658            None => {
659                crate::info!("[NET] WiFi: Association timeout");
660                fail_connect()
661            }
662        }
663    }
664}
665
666fn fail_connect() -> bool {
667    *WIFI_STATE.lock() = WifiState::Error;
668    false
669}
670
671/// Disconnect from current AP.
672pub fn disconnect() -> bool {
673    if *WIFI_STATE.lock() != WifiState::Connected {
674        return false;
675    }
676    unsafe {
677        ioctl_set_u32(BRCMF_C_DISASSOC, 0);
678    }
679    *WIFI_STATE.lock() = WifiState::Idle;
680    true
681}
682
683/// Store IP address (called by DHCP module after lease obtained).
684pub fn set_ip(ip: [u8; 4]) {
685    unsafe {
686        WIFI_IP = ip;
687    }
688}
689
690// ---- Accessors ----
691pub fn state() -> WifiState {
692    *WIFI_STATE.lock()
693}
694pub fn is_connected() -> bool {
695    *WIFI_STATE.lock() == WifiState::Connected
696}
697pub fn get_ip() -> [u8; 4] {
698    unsafe { WIFI_IP }
699}
700pub fn get_ssid() -> &'static str {
701    unsafe { core::str::from_utf8(&WIFI_SSID[..WIFI_SSID_LEN]).unwrap_or("") }
702}