Skip to content

radio_esp32::scan

Scanning for access points. Step 5.2 of doc/plan-radio.md, issue #67.

The three structs, and why their offsets are asserted

Section titled “The three structs, and why their offsets are asserted”

wifi_scan_config_t goes into the blob and wifi_ap_record_t comes back out of it, so both are read by code nobody here can inspect. A field at the wrong offset does not fail to compile and does not fault — it reads the neighbouring field, and the symptom is an SSID that is one byte short or an RSSI that is really a channel number.

The layouts are transcribed from esp_wifi_types.h at the pinned v4.4 and pinned by const _: () = assert!(...) on the target, where the compiler knows the real offsets. The three that actually move are called out below: the C enums are 4 bytes and force alignment, which is what puts second at 40 rather than 39 and authmode at 48 rather than 45.

The driver allocates the result list — out of our heap, through the OSI table — and holds it until [ap_records] is called, which copies it out and frees it. Calling [ap_count] and then never calling [ap_records] leaks it until the next scan. That is esp-idf’s contract, not an artefact here, and [ap_records] is written so the leak needs an early return to happen.

struct ActiveScanTime {
pub min: u32,
pub max: u32,
}

wifi_scan_time_t’s active half.

Implements: Clone, Copy

struct ApRecord {
pub bssid: [u8; 6],
pub ssid: [u8; 33],
pub primary: u8,
pub second: u32,
pub rssi: i8,
pub authmode: u32,
pub pairwise_cipher: u32,
pub group_cipher: u32,
pub ant: u32,
pub flags: u32,
pub country: Country,
}

wifi_ap_record_t. One access point, as the driver saw it.

Implements: Clone, Copy

References: Country

struct Country {
pub cc: [u8; 3],
pub schan: u8,
pub nchan: u8,
pub max_tx_power: i8,
pub policy: u32,
}

wifi_country_t. Sits at the tail of every [ApRecord].

Implements: Clone, Copy

struct ScanConfig {
pub ssid: *mut u8,
pub bssid: *mut u8,
pub channel: u8,
pub show_hidden: bool,
pub scan_type: u32,
pub scan_time: ScanTime,
}

wifi_scan_config_t.

Implements: Default

References: ScanTime

struct ScanTime {
pub active: ActiveScanTime,
pub passive: u32,
}

wifi_scan_time_t.

Implements: Clone, Copy

References: ActiveScanTime

unsafe fn ap_count() -> Result<u16, i32>

How many access points the last scan found.

Calls into the blob.

unsafe fn ap_records(out: &mut [ApRecord]) -> Result<u16, i32>

Copy the last scan’s results into out, and release the driver’s copy.

Returns how many records were written, which is min(found, out.len()). The driver frees its list here whether or not out was big enough, so a short buffer loses the overflow rather than leaving it for a second call — that is esp-idf’s behaviour and the reason [ap_count] exists.

Calls into the blob, which writes out.

References: ApRecord

fn auth_name(mode: u32) -> &'static str

A short name for an auth mode, for logging.

unsafe fn start(config: &ScanConfig, block: bool) -> i32

esp_wifi_scan_start.

With block set this does not return until the scan finishes, which for an all-channel active scan is well over a second. The blocking form is what makes a scan readable without an event loop; the non-blocking form needs [crate::wifi::event::SCAN_DONE] through [crate::events::set_handler].

Blocking here blocks the calling task, not the Wi-Fi task. The driver waits on its own event group, which reaches _event_group_wait_bits in the OSI table and blocks whoever called in — so an application task calling this yields the CPU rather than spinning.

Calls into the blob. [crate::wifi::start] must have succeeded.

References: ScanConfig

unsafe fn stop() -> i32

esp_wifi_scan_stop.

Calls into the blob.

const AUTH_OPEN: u32;

wifi_auth_mode_t values worth naming.

const AUTH_WAPI_PSK: u32;
const AUTH_WEP: u32;
const AUTH_WPA2_ENTERPRISE: u32;
const AUTH_WPA2_PSK: u32;
const AUTH_WPA2_WPA3_PSK: u32;
const AUTH_WPA3_PSK: u32;
const AUTH_WPA_PSK: u32;
const AUTH_WPA_WPA2_PSK: u32;