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 #[builder_lite(skip_setter)]
88 pub(crate) ssid: Option<Ssid>,
89 /// BSSID to filter for.
90 /// If [`None`] is passed, all BSSIDs will be returned.
91 /// If [`Some`] is passed, only the APs matching the given BSSID will be
92 /// returned.
93 pub(crate) bssid: Option<[u8; 6]>,
94 /// Channel to filter for.
95 /// If [`None`] is passed, all channels will be returned.
96 /// If [`Some`] is passed, only the APs on the given channel will be
97 /// returned.
98 pub(crate) channel: Option<u8>,
99 /// Whether to show hidden networks.
100 pub(crate) show_hidden: bool,
101 /// Scan type, active or passive.
102 pub(crate) scan_type: ScanTypeConfig,
103 /// The maximum number of networks to return when scanning.
104 /// If [`None`] is passed, all networks will be returned.
105 /// If [`Some`] is passed, the specified number of networks will be returned.
106 pub(crate) max: Option<usize>,
107}
108
109impl ScanConfig {
110 /// Set the SSID of the access point.
111 pub fn with_ssid(mut self, ssid: impl Into<Ssid>) -> Self {
112 self.ssid = Some(ssid.into());
113 self
114 }
115
116 /// Clears the SSID.
117 pub fn with_ssid_none(mut self) -> Self {
118 self.ssid = None;
119 self
120 }
121}
122
123/// Wi-Fi scan results.
124#[derive(Debug)]
125#[cfg_attr(feature = "defmt", derive(defmt::Format))]
126#[non_exhaustive]
127pub struct ScanResults<'d> {
128 /// Number of APs to return
129 remaining: usize,
130 /// Ensures the result list is free'd when this struct is dropped.
131 _drop_guard: FreeApListOnDrop,
132 /// Hold a lifetime to ensure the scan list is freed before a new scan is started.
133 _marker: PhantomData<&'d mut ()>,
134}
135
136impl<'d> ScanResults<'d> {
137 /// Create new Wi-Fi scan results.
138 pub fn new(_controller: &'d mut WifiController<'_>) -> Result<Self, WifiError> {
139 // Construct Self first. This ensures we'll free the result list even if `get_ap_num`
140 // returns an error.
141 let mut this = Self {
142 remaining: 0,
143 _drop_guard: FreeApListOnDrop,
144 _marker: PhantomData,
145 };
146
147 let mut bss_total = 0;
148 unsafe { esp_wifi_result!(include::esp_wifi_scan_get_ap_num(&mut bss_total))? };
149
150 this.remaining = bss_total as usize;
151
152 Ok(this)
153 }
154}
155
156impl Iterator for ScanResults<'_> {
157 type Item = AccessPointInfo;
158
159 fn next(&mut self) -> Option<Self::Item> {
160 if self.remaining == 0 {
161 return None;
162 }
163
164 self.remaining -= 1;
165
166 let mut record: MaybeUninit<include::wifi_ap_record_t> = MaybeUninit::uninit();
167
168 // We could detect ESP_FAIL to see if we've exhausted the list, but we know the number of
169 // results. Reading the number of results also ensures we're in the correct state, so
170 // unwrapping here should never fail.
171 unwrap!(unsafe {
172 esp_wifi_result!(include::esp_wifi_scan_get_ap_record(record.as_mut_ptr()))
173 });
174
175 Some(convert_ap_info(unsafe { record.assume_init_ref() }))
176 }
177}
178
179/// AP list on-drop guard.
180#[derive(Debug)]
181#[cfg_attr(feature = "defmt", derive(defmt::Format))]
182pub(super) struct FreeApListOnDrop;
183
184impl FreeApListOnDrop {
185 /// Do not automatically free the AP list when the guard is dropped.
186 pub fn defuse(self) {
187 core::mem::forget(self);
188 }
189}
190
191impl Drop for FreeApListOnDrop {
192 fn drop(&mut self) {
193 unsafe {
194 include::esp_wifi_clear_ap_list();
195 }
196 }
197}