Skip to main content

esp_radio/wifi/
ap.rs

1//! Wi-Fi access point.
2
3use procmacros::BuilderLite;
4
5#[cfg(feature = "unstable")]
6use super::CountryInfo;
7use super::{AuthenticationMethod, DisconnectReason, Protocols, SecondaryChannel, Ssid};
8use crate::{WifiError, sys::include::wifi_ap_record_t, wifi::AuthenticationMethodConfig};
9
10/// Information about a detected Wi-Fi access point.
11#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "defmt", derive(defmt::Format))]
13#[non_exhaustive]
14pub struct AccessPointInfo {
15    /// The SSID of the access point.
16    pub ssid: Ssid,
17    /// The BSSID (MAC address) of the access point.
18    pub bssid: [u8; 6],
19    /// The channel the access point is operating on.
20    pub channel: u8,
21    /// The secondary channel configuration of the access point.
22    pub secondary_channel: SecondaryChannel,
23    /// The signal strength of the access point (RSSI).
24    pub signal_strength: i8,
25    /// The authentication method used by the access point.
26    pub auth_method: Option<AuthenticationMethod>,
27    #[cfg(feature = "unstable")]
28    #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
29    /// The country information of the access point (if available from beacon frames).
30    pub country: Option<CountryInfo>,
31}
32
33/// Configuration for a Wi-Fi access point.
34#[derive(Clone, PartialEq, Eq, BuilderLite, Hash, Debug)]
35#[cfg_attr(feature = "defmt", derive(defmt::Format))]
36pub struct AccessPointConfig {
37    /// The SSID of the access point.
38    pub(crate) ssid: Ssid,
39    /// Whether the SSID is hidden or visible.
40    pub(crate) ssid_hidden: bool,
41    /// The channel the access point will operate on.
42    pub(crate) channel: u8,
43    /// The secondary channel configuration.
44    pub(crate) secondary_channel: Option<SecondaryChannel>,
45    /// The set of protocols supported by the access point.
46    pub(crate) protocols: Protocols,
47    /// The authentication method to be used by the access point.
48    pub(crate) authentication: AuthenticationMethodConfig,
49    /// The maximum number of connections allowed on the access point.
50    /// When set, this number can be clipped to a true upper limit because
51    /// ESPNow and access point connections share a common pool of hardware
52    /// encryption keys.
53    #[builder_lite(unstable)]
54    pub(crate) max_connections: u16,
55    /// Dtim period of the access point (Range: 1 ~ 10).
56    #[builder_lite(unstable)]
57    pub(crate) dtim_period: u8,
58    /// Time to force deauth the station if the Soft-AccessPoint doesn't receive any data.
59    #[builder_lite(unstable)]
60    pub(crate) beacon_timeout: u16,
61}
62
63impl AccessPointConfig {
64    pub(crate) fn validate(&self) -> Result<(), WifiError> {
65        // Soft-AP doesn't support WEP (nor WAPI/OWE, which
66        // `AuthenticationMethodConfig` doesn't include).
67        if matches!(self.authentication, AuthenticationMethodConfig::Wep(_)) {
68            warn!("WEP is not supported in access point mode.");
69            return Err(WifiError::Unsupported);
70        }
71
72        if let Some(password) = self.authentication.password()
73            && password.is_empty()
74        {
75            warn!("Access point password is empty.");
76            return Err(WifiError::InvalidPassword);
77        }
78
79        if !(1..=10).contains(&self.dtim_period) {
80            return Err(WifiError::InvalidArguments);
81        }
82
83        Ok(())
84    }
85}
86
87impl Default for AccessPointConfig {
88    fn default() -> Self {
89        Self {
90            ssid: "iot-device".try_into().expect("SSID length is valid"),
91            ssid_hidden: false,
92            channel: 1,
93            secondary_channel: None,
94            protocols: Protocols::default(),
95            authentication: AuthenticationMethodConfig::Open,
96            max_connections: 255,
97            dtim_period: 2,
98            beacon_timeout: 300,
99        }
100    }
101}
102
103/// Information about a station connected to the access point.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105#[cfg_attr(feature = "defmt", derive(defmt::Format))]
106#[non_exhaustive]
107pub struct ConnectedInfo {
108    /// The MAC address.
109    pub mac: [u8; 6],
110    /// The Association ID (AID) of the connected station.
111    pub aid: u16,
112    /// If this is a mesh child.
113    pub is_mesh_child: bool,
114}
115
116/// Information about a station disconnected from the access point.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118#[cfg_attr(feature = "defmt", derive(defmt::Format))]
119#[non_exhaustive]
120pub struct DisconnectedInfo {
121    /// The MAC address.
122    pub mac: [u8; 6],
123    /// The Association ID (AID) of the connected station.
124    pub aid: u16,
125    /// If this is a mesh child.
126    pub is_mesh_child: bool,
127    /// The disconnect reason.
128    pub reason: DisconnectReason,
129}
130
131/// Either the [ConnectedInfo] or [DisconnectedInfo].
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133#[cfg_attr(feature = "defmt", derive(defmt::Format))]
134pub enum EventInfo {
135    /// Information about a station connected to the access point.
136    Connected(ConnectedInfo),
137    /// Information about a station disconnected from the access point.
138    Disconnected(DisconnectedInfo),
139}
140
141#[allow(non_upper_case_globals)]
142pub(crate) fn convert_ap_info(record: &wifi_ap_record_t) -> AccessPointInfo {
143    // `record.ssid` is 33 bytes to always fit the NUL terminator of a
144    // maximum-length SSID - clamp to 32 in case the driver ever hands us one
145    // without it.
146    let str_len = record.ssid.iter().position(|&c| c == 0).unwrap_or(32);
147    let ssid = Ssid::try_from(&record.ssid[..str_len]).expect("SSID length is valid");
148
149    AccessPointInfo {
150        ssid,
151        bssid: record.bssid,
152        channel: record.primary,
153        secondary_channel: SecondaryChannel::from_raw(record.second),
154        signal_strength: record.rssi,
155        auth_method: Some(AuthenticationMethod::from_raw(record.authmode)),
156        #[cfg(feature = "unstable")]
157        country: CountryInfo::try_from_c(&record.country),
158    }
159}