esp_radio/wifi/scan.rs
1//! Wi-Fi scanning.
2
3use core::{marker::PhantomData, mem::MaybeUninit};
4
5use esp_hal::time::Duration;
6use procmacros::BuilderLite;
7
8use crate::{
9 sys::include,
10 wifi::{
11 Ssid,
12 WifiController,
13 WifiError,
14 ap::{AccessPointInfo, convert_ap_info},
15 esp_wifi_result,
16 },
17};
18
19/// Configuration for active or passive scan.
20///
21/// # Comparison of active and passive scan
22///
23/// | | **Active** | **Passive** |
24/// |--------------------------------------|------------|-------------|
25/// | **Power consumption** | High | Low |
26/// | **Time required (typical behavior)** | Low | High |
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[cfg_attr(feature = "defmt", derive(defmt::Format))]
29#[non_exhaustive]
30pub enum ScanTypeConfig {
31 /// Active scan with min and max scan time per channel. This is the default
32 /// and recommended if you are unsure.
33 ///
34 /// # Procedure
35 /// 1. Send probe request on each channel.
36 /// 2. Wait for probe response. Wait at least `min` time, but if no response is received, wait
37 /// up to `max` time.
38 /// 3. Switch channel.
39 /// 4. Repeat from 1.
40 Active {
41 /// Minimum scan time per channel. Defaults to 10ms.
42 min: Duration,
43 /// Maximum scan time per channel. Defaults to 20ms.
44 max: Duration,
45 },
46 /// Passive scan
47 ///
48 /// # Procedure
49 /// 1. Wait for beacon for given duration.
50 /// 2. Switch channel.
51 /// 3. Repeat from 1.
52 ///
53 /// # Note
54 /// It is recommended to avoid duration longer than 1500ms, as it may cause
55 /// a station to disconnect from the Access Point.
56 Passive(Duration),
57}
58
59impl Default for ScanTypeConfig {
60 fn default() -> Self {
61 Self::Active {
62 min: Duration::from_millis(10),
63 max: Duration::from_millis(20),
64 }
65 }
66}
67
68impl ScanTypeConfig {
69 pub(crate) fn validate(&self) {
70 if matches!(self, Self::Passive(dur) if *dur > Duration::from_millis(1500)) {
71 warn!(
72 "Passive scan duration longer than 1500ms may cause a station to disconnect from the access point"
73 );
74 }
75 }
76}
77
78/// Scan configuration.
79#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, BuilderLite)]
80#[cfg_attr(feature = "defmt", derive(defmt::Format))]
81#[non_exhaustive]
82pub struct ScanConfig {
83 /// SSID to filter for.
84 /// If [`None`] is passed, all SSIDs will be returned.
85 /// If [`Some`] is passed, only the APs matching the given SSID will be
86 /// returned.
87 pub(crate) ssid: Option<Ssid>,
88 /// BSSID to filter for.
89 /// If [`None`] is passed, all BSSIDs will be returned.
90 /// If [`Some`] is passed, only the APs matching the given BSSID will be
91 /// returned.
92 pub(crate) bssid: Option<[u8; 6]>,
93 /// Channel to filter for.
94 /// If [`None`] is passed, all channels will be returned.
95 /// If [`Some`] is passed, only the APs on the given channel will be
96 /// returned.
97 pub(crate) channel: Option<u8>,
98 /// Whether to show hidden networks.
99 pub(crate) show_hidden: bool,
100 /// Scan type, active or passive.
101 pub(crate) scan_type: ScanTypeConfig,
102 /// The maximum number of networks to return when scanning.
103 /// If [`None`] is passed, all networks will be returned.
104 /// If [`Some`] is passed, the specified number of networks will be returned.
105 pub(crate) max: Option<usize>,
106}
107
108/// Wi-Fi scan results.
109#[derive(Debug)]
110#[cfg_attr(feature = "defmt", derive(defmt::Format))]
111#[non_exhaustive]
112pub struct ScanResults<'d> {
113 /// Number of APs to return
114 remaining: usize,
115 /// Ensures the result list is free'd when this struct is dropped.
116 _drop_guard: FreeApListOnDrop,
117 /// Hold a lifetime to ensure the scan list is freed before a new scan is started.
118 _marker: PhantomData<&'d mut ()>,
119}
120
121impl<'d> ScanResults<'d> {
122 /// Create new Wi-Fi scan results.
123 pub fn new(_controller: &'d mut WifiController<'_>) -> Result<Self, WifiError> {
124 // Construct Self first. This ensures we'll free the result list even if `get_ap_num`
125 // returns an error.
126 let mut this = Self {
127 remaining: 0,
128 _drop_guard: FreeApListOnDrop,
129 _marker: PhantomData,
130 };
131
132 let mut bss_total = 0;
133 unsafe { esp_wifi_result!(include::esp_wifi_scan_get_ap_num(&mut bss_total))? };
134
135 this.remaining = bss_total as usize;
136
137 Ok(this)
138 }
139}
140
141impl Iterator for ScanResults<'_> {
142 type Item = AccessPointInfo;
143
144 fn next(&mut self) -> Option<Self::Item> {
145 if self.remaining == 0 {
146 return None;
147 }
148
149 self.remaining -= 1;
150
151 let mut record: MaybeUninit<include::wifi_ap_record_t> = MaybeUninit::uninit();
152
153 // We could detect ESP_FAIL to see if we've exhausted the list, but we know the number of
154 // results. Reading the number of results also ensures we're in the correct state, so
155 // unwrapping here should never fail.
156 unwrap!(unsafe {
157 esp_wifi_result!(include::esp_wifi_scan_get_ap_record(record.as_mut_ptr()))
158 });
159
160 Some(convert_ap_info(unsafe { record.assume_init_ref() }))
161 }
162}
163
164/// AP list on-drop guard.
165#[derive(Debug)]
166#[cfg_attr(feature = "defmt", derive(defmt::Format))]
167pub(super) struct FreeApListOnDrop;
168
169impl FreeApListOnDrop {
170 /// Do not automatically free the AP list when the guard is dropped.
171 pub fn defuse(self) {
172 core::mem::forget(self);
173 }
174}
175
176impl Drop for FreeApListOnDrop {
177 fn drop(&mut self) {
178 unsafe {
179 include::esp_wifi_clear_ap_list();
180 }
181 }
182}