Skip to main content

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}