Jesse Braham 5e1648f338
Minor documentation improvements for esp-radio (#3986)
* Use correct spelling/case for Wi-Fi in docs

* Link to functions mentioned in doc comments
2025-08-26 08:44:45 +00:00

3246 lines
101 KiB
Rust

//! Wi-Fi
#![deny(missing_docs)]
pub mod event;
mod internal;
pub(crate) mod os_adapter;
pub(crate) mod state;
use alloc::{collections::vec_deque::VecDeque, string::String};
use core::{
fmt::Debug,
marker::PhantomData,
mem,
mem::MaybeUninit,
ptr::addr_of,
task::Poll,
time::Duration,
};
use enumset::{EnumSet, EnumSetType};
use esp_hal::{asynch::AtomicWaker, sync::Locked};
use esp_wifi_sys::include::{
WIFI_PROTOCOL_11AX,
WIFI_PROTOCOL_11B,
WIFI_PROTOCOL_11G,
WIFI_PROTOCOL_11N,
WIFI_PROTOCOL_LR,
wifi_pkt_rx_ctrl_t,
wifi_scan_channel_bitmap_t,
};
#[cfg(feature = "wifi-eap")]
use esp_wifi_sys::include::{
esp_eap_client_clear_ca_cert,
esp_eap_client_clear_certificate_and_key,
esp_eap_client_clear_identity,
esp_eap_client_clear_new_password,
esp_eap_client_clear_password,
esp_eap_client_clear_username,
esp_eap_client_set_ca_cert,
esp_eap_client_set_certificate_and_key,
esp_eap_client_set_disable_time_check,
esp_eap_client_set_fast_params,
esp_eap_client_set_identity,
esp_eap_client_set_new_password,
esp_eap_client_set_pac_file,
esp_eap_client_set_password,
esp_eap_client_set_ttls_phase2_method,
esp_eap_client_set_username,
esp_eap_fast_config,
esp_wifi_sta_enterprise_enable,
};
#[cfg(all(feature = "sniffer", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
use esp_wifi_sys::include::{
esp_wifi_80211_tx,
esp_wifi_set_promiscuous,
esp_wifi_set_promiscuous_rx_cb,
wifi_promiscuous_pkt_t,
wifi_promiscuous_pkt_type_t,
};
use num_derive::FromPrimitive;
#[doc(hidden)]
pub(crate) use os_adapter::*;
use portable_atomic::{AtomicUsize, Ordering};
use procmacros::BuilderLite;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(all(feature = "smoltcp", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
use smoltcp::phy::{Device, DeviceCapabilities, RxToken, TxToken};
pub use state::*;
use crate::{
Controller,
common_adapter::*,
config::PowerSaveMode,
esp_wifi_result,
hal::ram,
wifi::private::PacketBuffer,
};
const MTU: usize = crate::CONFIG.mtu;
#[cfg(all(feature = "csi", esp32c6))]
use crate::binary::include::wifi_csi_acquire_config_t;
#[cfg(feature = "csi")]
#[instability::unstable]
pub use crate::binary::include::wifi_csi_info_t;
#[cfg(feature = "csi")]
#[instability::unstable]
use crate::binary::include::{
esp_wifi_set_csi,
esp_wifi_set_csi_config,
esp_wifi_set_csi_rx_cb,
wifi_csi_config_t,
};
use crate::binary::{
c_types,
include::{
self,
__BindgenBitfieldUnit,
esp_err_t,
esp_interface_t_ESP_IF_WIFI_AP,
esp_interface_t_ESP_IF_WIFI_STA,
esp_supplicant_deinit,
esp_supplicant_init,
esp_wifi_connect,
esp_wifi_deinit_internal,
esp_wifi_disconnect,
esp_wifi_get_mode,
esp_wifi_init_internal,
esp_wifi_internal_free_rx_buffer,
esp_wifi_internal_reg_rxcb,
esp_wifi_internal_tx,
esp_wifi_scan_start,
esp_wifi_set_config,
esp_wifi_set_country,
esp_wifi_set_mode,
esp_wifi_set_protocol,
esp_wifi_set_tx_done_cb,
esp_wifi_sta_get_rssi,
esp_wifi_start,
esp_wifi_stop,
g_wifi_default_wpa_crypto_funcs,
wifi_active_scan_time_t,
wifi_ap_config_t,
wifi_auth_mode_t,
wifi_cipher_type_t_WIFI_CIPHER_TYPE_CCMP,
wifi_config_t,
wifi_country_policy_t_WIFI_COUNTRY_POLICY_MANUAL,
wifi_country_t,
wifi_interface_t,
wifi_interface_t_WIFI_IF_AP,
wifi_interface_t_WIFI_IF_STA,
wifi_mode_t,
wifi_mode_t_WIFI_MODE_AP,
wifi_mode_t_WIFI_MODE_APSTA,
wifi_mode_t_WIFI_MODE_NULL,
wifi_mode_t_WIFI_MODE_STA,
wifi_pmf_config_t,
wifi_scan_config_t,
wifi_scan_threshold_t,
wifi_scan_time_t,
wifi_scan_type_t_WIFI_SCAN_TYPE_ACTIVE,
wifi_scan_type_t_WIFI_SCAN_TYPE_PASSIVE,
wifi_sort_method_t_WIFI_CONNECT_AP_BY_SIGNAL,
wifi_sta_config_t,
},
};
/// Supported Wi-Fi authentication methods.
#[derive(Debug, Default, PartialOrd, EnumSetType)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum AuthMethod {
/// No authentication (open network).
None,
/// Wired Equivalent Privacy (WEP) authentication.
Wep,
/// Wi-Fi Protected Access (WPA) authentication.
Wpa,
/// Wi-Fi Protected Access 2 (WPA2) Personal authentication (default).
#[default]
Wpa2Personal,
/// WPA/WPA2 Personal authentication (supports both).
WpaWpa2Personal,
/// WPA2 Enterprise authentication.
Wpa2Enterprise,
/// WPA3 Personal authentication.
Wpa3Personal,
/// WPA2/WPA3 Personal authentication (supports both).
Wpa2Wpa3Personal,
/// WLAN Authentication and Privacy Infrastructure (WAPI).
WapiPersonal,
}
/// Supported Wi-Fi protocols.
#[derive(Debug, Default, PartialOrd, EnumSetType)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum Protocol {
/// 802.11b protocol.
P802D11B,
/// 802.11b/g protocol.
P802D11BG,
/// 802.11b/g/n protocol (default).
#[default]
P802D11BGN,
/// 802.11b/g/n long-range (LR) protocol.
P802D11BGNLR,
/// 802.11 long-range (LR) protocol.
P802D11LR,
/// 802.11b/g/n/ax protocol.
P802D11BGNAX,
}
/// Secondary Wi-Fi channels.
#[derive(EnumSetType, Debug, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Default)]
pub enum SecondaryChannel {
// TODO: Need to extend that for 5GHz
/// No secondary channel (default).
#[default]
None,
/// Secondary channel is above the primary channel.
Above,
/// Secondary channel is below the primary channel.
Below,
}
/// Access point country information.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct Country([u8; 2]);
impl Country {
fn try_from_c(info: &wifi_country_t) -> Option<Self> {
// Find the null terminator or end of array
let cc_len = info
.cc
.iter()
.position(|&b| b == 0)
.unwrap_or(info.cc.len());
if cc_len < 2 {
return None;
}
// Validate that we have at least 2 valid ASCII characters
let cc_slice = &info.cc[..cc_len.min(2)];
if cc_slice.iter().all(|&b| b.is_ascii_uppercase()) {
Some(Self([cc_slice[0], cc_slice[1]]))
} else {
None
}
}
/// Returns the country code as a string slice.
pub fn country_code(&self) -> &str {
unsafe {
// SAFETY: we have verified in the constructor that the bytes are upper-case ASCII.
core::str::from_utf8_unchecked(&self.0)
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Country {
fn format(&self, fmt: defmt::Formatter<'_>) {
self.country_code().format(fmt)
}
}
/// Information about a detected Wi-Fi access point.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct AccessPointInfo {
/// The SSID of the access point.
// TODO: we can use the `alloc` feature once we have `defmt` 1.0.2
#[cfg_attr(feature = "defmt", defmt(Debug2Format))]
pub ssid: String,
/// The BSSID (MAC address) of the access point.
pub bssid: [u8; 6],
/// The channel the access point is operating on.
pub channel: u8,
/// The secondary channel configuration of the access point.
pub secondary_channel: SecondaryChannel,
/// The signal strength of the access point (RSSI).
pub signal_strength: i8,
/// The authentication method used by the access point.
pub auth_method: Option<AuthMethod>,
/// The country information of the access point (if available from beacon frames).
pub country: Option<Country>,
}
/// Configuration for a Wi-Fi access point.
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct AccessPointConfiguration {
/// The SSID of the access point.
pub ssid: String,
/// Whether the SSID is hidden or visible.
pub ssid_hidden: bool,
/// The channel the access point will operate on.
pub channel: u8,
/// The secondary channel configuration.
pub secondary_channel: Option<u8>,
/// The set of protocols supported by the access point.
pub protocols: EnumSet<Protocol>,
/// The authentication method to be used by the access point.
pub auth_method: AuthMethod,
/// The password for securing the access point (if applicable).
pub password: String,
/// The maximum number of connections allowed on the access point.
pub max_connections: u16,
}
impl AccessPointConfiguration {
fn validate(&self) -> Result<(), WifiError> {
if self.ssid.len() > 32 {
return Err(WifiError::InvalidArguments);
}
if self.password.len() > 64 {
return Err(WifiError::InvalidArguments);
}
Ok(())
}
}
impl Default for AccessPointConfiguration {
fn default() -> Self {
Self {
ssid: String::from("iot-device"),
ssid_hidden: false,
channel: 1,
secondary_channel: None,
protocols: (Protocol::P802D11B | Protocol::P802D11BG | Protocol::P802D11BGN),
auth_method: AuthMethod::None,
password: String::new(),
max_connections: 255,
}
}
}
impl core::fmt::Debug for AccessPointConfiguration {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AccessPointConfiguration")
.field("ssid", &self.ssid)
.field("ssid_hidden", &self.ssid_hidden)
.field("channel", &self.channel)
.field("secondary_channel", &self.secondary_channel)
.field("protocols", &self.protocols)
.field("auth_method", &self.auth_method)
.field("password", &"**REDACTED**")
.field("max_connections", &self.max_connections)
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for AccessPointConfiguration {
fn format(&self, fmt: defmt::Formatter<'_>) {
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Default)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct ProtocolSet(EnumSet<Protocol>);
#[cfg(feature = "defmt")]
impl defmt::Format for ProtocolSet {
fn format(&self, fmt: defmt::Formatter<'_>) {
for (i, p) in self.0.into_iter().enumerate() {
if i > 0 {
defmt::write!(fmt, " ");
}
defmt::write!(fmt, "{}", p);
}
}
}
let protocol_set = ProtocolSet(self.protocols);
defmt::write!(
fmt,
"AccessPointConfiguration {{\
ssid: {}, \
ssid_hidden: {}, \
channel: {}, \
secondary_channel: {}, \
protocols: {}, \
auth_method: {}, \
password: **REDACTED**, \
max_connections: {}, \
}}",
self.ssid.as_str(),
self.ssid_hidden,
self.channel,
self.secondary_channel,
protocol_set,
self.auth_method,
self.max_connections
);
}
}
/// Client configuration for a Wi-Fi connection.
#[derive(Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct ClientConfiguration {
/// The SSID of the Wi-Fi network.
pub ssid: String,
/// The BSSID (MAC address) of the client.
pub bssid: Option<[u8; 6]>,
// pub protocol: Protocol,
/// The authentication method for the Wi-Fi connection.
pub auth_method: AuthMethod,
/// The password for the Wi-Fi connection.
pub password: String,
/// The Wi-Fi channel to connect to.
pub channel: Option<u8>,
}
impl ClientConfiguration {
fn validate(&self) -> Result<(), WifiError> {
if self.ssid.len() > 32 {
return Err(WifiError::InvalidArguments);
}
if self.password.len() > 64 {
return Err(WifiError::InvalidArguments);
}
Ok(())
}
}
impl core::fmt::Debug for ClientConfiguration {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ClientConfiguration")
.field("ssid", &self.ssid)
.field("bssid", &self.bssid)
.field("auth_method", &self.auth_method)
.field("password", &"**REDACTED**")
.field("channel", &self.channel)
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for ClientConfiguration {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(
fmt,
"ClientConfiguration {{\
ssid: {}, \
bssid: {:?}, \
auth_method: {:?}, \
password: **REDACTED**, \
channel: {:?}, \
}}",
self.ssid.as_str(),
self.bssid,
self.auth_method,
self.channel
)
}
}
/// Configuration for EAP-FAST authentication protocol.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[cfg(feature = "wifi-eap")]
#[instability::unstable]
pub struct EapFastConfig {
/// Specifies the provisioning mode for EAP-FAST.
pub fast_provisioning: u8,
/// The maximum length of the PAC (Protected Access Credentials) list.
pub fast_max_pac_list_len: u8,
/// Indicates whether the PAC file is in binary format.
pub fast_pac_format_binary: bool,
}
/// Phase 2 authentication methods
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[cfg(feature = "wifi-eap")]
#[instability::unstable]
pub enum TtlsPhase2Method {
/// EAP (Extensible Authentication Protocol).
Eap,
/// MSCHAPv2 (Microsoft Challenge Handshake Authentication Protocol 2).
Mschapv2,
/// MSCHAP (Microsoft Challenge Handshake Authentication Protocol).
Mschap,
/// PAP (Password Authentication Protocol).
Pap,
/// CHAP (Challenge Handshake Authentication Protocol).
Chap,
}
#[cfg(feature = "wifi-eap")]
impl TtlsPhase2Method {
/// Maps the phase 2 method to a raw `u32` representation.
fn to_raw(&self) -> u32 {
match self {
TtlsPhase2Method::Eap => {
esp_wifi_sys::include::esp_eap_ttls_phase2_types_ESP_EAP_TTLS_PHASE2_EAP
}
TtlsPhase2Method::Mschapv2 => {
esp_wifi_sys::include::esp_eap_ttls_phase2_types_ESP_EAP_TTLS_PHASE2_MSCHAPV2
}
TtlsPhase2Method::Mschap => {
esp_wifi_sys::include::esp_eap_ttls_phase2_types_ESP_EAP_TTLS_PHASE2_MSCHAP
}
TtlsPhase2Method::Pap => {
esp_wifi_sys::include::esp_eap_ttls_phase2_types_ESP_EAP_TTLS_PHASE2_PAP
}
TtlsPhase2Method::Chap => {
esp_wifi_sys::include::esp_eap_ttls_phase2_types_ESP_EAP_TTLS_PHASE2_CHAP
}
}
}
}
/// Configuration for an EAP (Extensible Authentication Protocol) client.
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[cfg(feature = "wifi-eap")]
#[instability::unstable]
pub struct EapClientConfiguration {
/// The SSID of the network the client is connecting to.
pub ssid: String,
/// The BSSID (MAC Address) of the specific access point.
pub bssid: Option<[u8; 6]>,
// pub protocol: Protocol,
/// The authentication method used for EAP.
pub auth_method: AuthMethod,
/// The identity used during authentication.
pub identity: Option<String>,
/// The username used for inner authentication.
/// Some EAP methods require a username for authentication.
pub username: Option<String>,
/// The password used for inner authentication.
pub password: Option<String>,
/// A new password to be set during the authentication process.
/// Some methods support password changes during authentication.
pub new_password: Option<String>,
/// Configuration for EAP-FAST.
pub eap_fast_config: Option<EapFastConfig>,
/// A PAC (Protected Access Credential) file for EAP-FAST.
pub pac_file: Option<&'static [u8]>,
/// A boolean flag indicating whether time checking is enforced during
/// authentication.
pub time_check: bool,
/// A CA (Certificate Authority) certificate for validating the
/// authentication server's certificate.
pub ca_cert: Option<&'static [u8]>,
/// A tuple containing the client's certificate, private key, and an
/// intermediate certificate.
#[allow(clippy::type_complexity)]
pub certificate_and_key: Option<(&'static [u8], &'static [u8], Option<&'static [u8]>)>,
/// The Phase 2 authentication method used for EAP-TTLS.
pub ttls_phase2_method: Option<TtlsPhase2Method>,
/// The specific Wi-Fi channel to use for the connection.
pub channel: Option<u8>,
}
#[cfg(feature = "wifi-eap")]
impl EapClientConfiguration {
fn validate(&self) -> Result<(), WifiError> {
if self.ssid.len() > 32 {
return Err(WifiError::InvalidArguments);
}
if self.identity.as_ref().unwrap_or(&String::new()).len() > 128 {
return Err(WifiError::InvalidArguments);
}
if self.username.as_ref().unwrap_or(&String::new()).len() > 128 {
return Err(WifiError::InvalidArguments);
}
if self.password.as_ref().unwrap_or(&String::new()).len() > 64 {
return Err(WifiError::InvalidArguments);
}
if self.new_password.as_ref().unwrap_or(&String::new()).len() > 64 {
return Err(WifiError::InvalidArguments);
}
Ok(())
}
}
#[cfg(feature = "wifi-eap")]
impl Debug for EapClientConfiguration {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("EapClientConfiguration")
.field("ssid", &self.ssid)
.field("bssid", &self.bssid)
.field("auth_method", &self.auth_method)
.field("channel", &self.channel)
.field("identity", &self.identity)
.field("username", &self.username)
.field("password", &"**REDACTED**")
.field("new_password", &"**REDACTED**")
.field("eap_fast_config", &self.eap_fast_config)
.field("time_check", &self.time_check)
.field("pac_file set", &self.pac_file.is_some())
.field("ca_cert set", &self.ca_cert.is_some())
.field("certificate_and_key set", &"**REDACTED**")
.field("ttls_phase2_method", &self.ttls_phase2_method)
.finish()
}
}
#[cfg(feature = "defmt")]
#[cfg(feature = "wifi-eap")]
impl defmt::Format for EapClientConfiguration {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(
fmt,
"EapClientConfiguration {{\
ssid: {}, \
bssid: {:?}, \
auth_method: {:?}, \
channel: {:?}, \
identity: {:?}, \
username: {:?}, \
password: **REDACTED**, \
new_password: **REDACTED**, \
eap_fast_config: {:?}, \
time_check: {}, \
pac_file: {}, \
ca_cert: {}, \
certificate_and_key: **REDACTED**, \
ttls_phase2_method: {:?}, \
}}",
self.ssid.as_str(),
self.bssid,
self.auth_method,
self.channel,
&self.identity.as_ref().map_or("", |v| v.as_str()),
&self.username.as_ref().map_or("", |v| v.as_str()),
self.eap_fast_config,
self.time_check,
self.pac_file,
self.ca_cert,
self.ttls_phase2_method,
)
}
}
#[cfg(feature = "wifi-eap")]
impl Default for EapClientConfiguration {
fn default() -> Self {
EapClientConfiguration {
ssid: String::new(),
bssid: None,
auth_method: AuthMethod::Wpa2Enterprise,
identity: None,
username: None,
password: None,
channel: None,
eap_fast_config: None,
time_check: false,
new_password: None,
pac_file: None,
ca_cert: None,
certificate_and_key: None,
ttls_phase2_method: None,
}
}
}
/// Introduces Wi-Fi configuration options.
#[derive(EnumSetType, Debug, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum Capability {
/// The device operates as a client, connecting to an existing network.
Client,
/// The device operates as an access point, allowing other devices to
/// connect to it.
AccessPoint,
/// The device can operate in both client and access point modes
/// simultaneously.
Mixed,
}
/// Configuration of Wi-Fi operation mode.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum Configuration {
/// No configuration (default).
#[default]
None,
/// Client-only configuration.
Client(ClientConfiguration),
/// Access point-only configuration.
AccessPoint(AccessPointConfiguration),
/// Simultaneous client and access point configuration.
Mixed(ClientConfiguration, AccessPointConfiguration),
/// EAP client configuration for enterprise Wi-Fi.
#[cfg(feature = "wifi-eap")]
#[cfg_attr(feature = "serde", serde(skip))]
EapClient(EapClientConfiguration),
}
impl Configuration {
fn validate(&self) -> Result<(), WifiError> {
match self {
Configuration::None => Ok(()),
Configuration::Client(client_configuration) => client_configuration.validate(),
Configuration::AccessPoint(access_point_configuration) => {
access_point_configuration.validate()
}
Configuration::Mixed(client_configuration, access_point_configuration) => {
client_configuration.validate()?;
access_point_configuration.validate()
}
#[cfg(feature = "wifi-eap")]
Configuration::EapClient(eap_client_configuration) => {
eap_client_configuration.validate()
}
}
}
/// Returns a reference to the client configuration if available.
pub fn as_client_conf_ref(&self) -> Option<&ClientConfiguration> {
match self {
Self::Client(client_conf) => Some(client_conf),
Self::Mixed(client_conf, _) => Some(client_conf),
_ => None,
}
}
/// Returns a reference to the access point configuration if available.
pub fn as_ap_conf_ref(&self) -> Option<&AccessPointConfiguration> {
match self {
Self::AccessPoint(ap_conf) => Some(ap_conf),
Self::Mixed(_, ap_conf) => Some(ap_conf),
_ => None,
}
}
/// Returns a mutable reference to the client configuration, creating it if
/// necessary.
pub fn as_client_conf_mut(&mut self) -> &mut ClientConfiguration {
match self {
Self::Client(client_conf) => client_conf,
Self::Mixed(_, _) => {
let prev = mem::replace(self, Self::None);
match prev {
Self::Mixed(client_conf, _) => {
*self = Self::Client(client_conf);
self.as_client_conf_mut()
}
_ => unreachable!(),
}
}
_ => {
*self = Self::Client(Default::default());
self.as_client_conf_mut()
}
}
}
/// Returns a mutable reference to the access point configuration, creating
/// it if necessary.
pub fn as_ap_conf_mut(&mut self) -> &mut AccessPointConfiguration {
match self {
Self::AccessPoint(ap_conf) => ap_conf,
Self::Mixed(_, _) => {
let prev = mem::replace(self, Self::None);
match prev {
Self::Mixed(_, ap_conf) => {
*self = Self::AccessPoint(ap_conf);
self.as_ap_conf_mut()
}
_ => unreachable!(),
}
}
_ => {
*self = Self::AccessPoint(Default::default());
self.as_ap_conf_mut()
}
}
}
/// Retrieves mutable references to both the `ClientConfiguration`
/// and `AccessPointConfiguration`.
pub fn as_mixed_conf_mut(
&mut self,
) -> (&mut ClientConfiguration, &mut AccessPointConfiguration) {
match self {
Self::Mixed(client_conf, ap_conf) => (client_conf, ap_conf),
Self::AccessPoint(_) => {
let prev = mem::replace(self, Self::None);
match prev {
Self::AccessPoint(ap_conf) => {
*self = Self::Mixed(Default::default(), ap_conf);
self.as_mixed_conf_mut()
}
_ => unreachable!(),
}
}
Self::Client(_) => {
let prev = mem::replace(self, Self::None);
match prev {
Self::Client(client_conf) => {
*self = Self::Mixed(client_conf, Default::default());
self.as_mixed_conf_mut()
}
_ => unreachable!(),
}
}
_ => {
*self = Self::Mixed(Default::default(), Default::default());
self.as_mixed_conf_mut()
}
}
}
}
trait AuthMethodExt {
fn to_raw(&self) -> wifi_auth_mode_t;
fn from_raw(raw: wifi_auth_mode_t) -> Self;
}
impl AuthMethodExt for AuthMethod {
fn to_raw(&self) -> wifi_auth_mode_t {
match self {
AuthMethod::None => include::wifi_auth_mode_t_WIFI_AUTH_OPEN,
AuthMethod::Wep => include::wifi_auth_mode_t_WIFI_AUTH_WEP,
AuthMethod::Wpa => include::wifi_auth_mode_t_WIFI_AUTH_WPA_PSK,
AuthMethod::Wpa2Personal => include::wifi_auth_mode_t_WIFI_AUTH_WPA2_PSK,
AuthMethod::WpaWpa2Personal => include::wifi_auth_mode_t_WIFI_AUTH_WPA_WPA2_PSK,
AuthMethod::Wpa2Enterprise => include::wifi_auth_mode_t_WIFI_AUTH_WPA2_ENTERPRISE,
AuthMethod::Wpa3Personal => include::wifi_auth_mode_t_WIFI_AUTH_WPA3_PSK,
AuthMethod::Wpa2Wpa3Personal => include::wifi_auth_mode_t_WIFI_AUTH_WPA2_WPA3_PSK,
AuthMethod::WapiPersonal => include::wifi_auth_mode_t_WIFI_AUTH_WAPI_PSK,
}
}
fn from_raw(raw: wifi_auth_mode_t) -> Self {
match raw {
include::wifi_auth_mode_t_WIFI_AUTH_OPEN => AuthMethod::None,
include::wifi_auth_mode_t_WIFI_AUTH_WEP => AuthMethod::Wep,
include::wifi_auth_mode_t_WIFI_AUTH_WPA_PSK => AuthMethod::Wpa,
include::wifi_auth_mode_t_WIFI_AUTH_WPA2_PSK => AuthMethod::Wpa2Personal,
include::wifi_auth_mode_t_WIFI_AUTH_WPA_WPA2_PSK => AuthMethod::WpaWpa2Personal,
include::wifi_auth_mode_t_WIFI_AUTH_WPA2_ENTERPRISE => AuthMethod::Wpa2Enterprise,
include::wifi_auth_mode_t_WIFI_AUTH_WPA3_PSK => AuthMethod::Wpa3Personal,
include::wifi_auth_mode_t_WIFI_AUTH_WPA2_WPA3_PSK => AuthMethod::Wpa2Wpa3Personal,
include::wifi_auth_mode_t_WIFI_AUTH_WAPI_PSK => AuthMethod::WapiPersonal,
_ => unreachable!(),
}
}
}
/// Wi-Fi Mode (Sta and/or Ap)
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum WifiMode {
/// Station mode.
Sta,
/// Access Point mode.
Ap,
/// Both Station and Access Point modes.
ApSta,
}
impl WifiMode {
pub(crate) fn current() -> Result<Self, WifiError> {
let mut mode = wifi_mode_t_WIFI_MODE_NULL;
esp_wifi_result!(unsafe { esp_wifi_get_mode(&mut mode) })?;
Self::try_from(mode)
}
/// Returns true if this mode works as a client
pub fn is_sta(&self) -> bool {
match self {
Self::Sta | Self::ApSta => true,
Self::Ap => false,
}
}
/// Returns true if this mode works as an access point
pub fn is_ap(&self) -> bool {
match self {
Self::Sta => false,
Self::Ap | Self::ApSta => true,
}
}
}
impl TryFrom<&Configuration> for WifiMode {
type Error = WifiError;
/// Based on the current `Configuration`, derives a `WifiMode` based on it.
fn try_from(config: &Configuration) -> Result<Self, Self::Error> {
let mode = match config {
Configuration::None => return Err(WifiError::UnknownWifiMode),
Configuration::AccessPoint(_) => Self::Ap,
Configuration::Client(_) => Self::Sta,
Configuration::Mixed(_, _) => Self::ApSta,
#[cfg(feature = "wifi-eap")]
Configuration::EapClient(_) => Self::Sta,
};
Ok(mode)
}
}
impl TryFrom<wifi_mode_t> for WifiMode {
type Error = WifiError;
/// Converts a `wifi_mode_t` C-type into a `WifiMode`.
fn try_from(value: wifi_mode_t) -> Result<Self, Self::Error> {
#[allow(non_upper_case_globals)]
match value {
include::wifi_mode_t_WIFI_MODE_STA => Ok(Self::Sta),
include::wifi_mode_t_WIFI_MODE_AP => Ok(Self::Ap),
include::wifi_mode_t_WIFI_MODE_APSTA => Ok(Self::ApSta),
_ => Err(WifiError::UnknownWifiMode),
}
}
}
impl From<WifiMode> for wifi_mode_t {
fn from(val: WifiMode) -> Self {
#[allow(non_upper_case_globals)]
match val {
WifiMode::Sta => wifi_mode_t_WIFI_MODE_STA,
WifiMode::Ap => wifi_mode_t_WIFI_MODE_AP,
WifiMode::ApSta => wifi_mode_t_WIFI_MODE_APSTA,
}
}
}
#[cfg(feature = "csi")]
pub(crate) trait CsiCallback: FnMut(crate::binary::include::wifi_csi_info_t) {}
#[cfg(feature = "csi")]
impl<T> CsiCallback for T where T: FnMut(crate::binary::include::wifi_csi_info_t) {}
#[cfg(feature = "csi")]
unsafe extern "C" fn csi_rx_cb<C: CsiCallback>(
ctx: *mut crate::wifi::c_types::c_void,
data: *mut crate::binary::include::wifi_csi_info_t,
) {
unsafe {
let csi_callback = &mut *(ctx as *mut C);
csi_callback(*data);
}
}
/// Channel state information (CSI) configuration.
#[derive(Clone, PartialEq, Eq)]
#[cfg(all(not(esp32c6), feature = "csi"))]
pub struct CsiConfig {
/// Enable to receive legacy long training field(lltf) data.
pub lltf_en: bool,
/// Enable to receive HT long training field(htltf) data.
pub htltf_en: bool,
/// Enable to receive space time block code HT long training
/// field(stbc-htltf2) data.
pub stbc_htltf2_en: bool,
/// Enable to generate htlft data by averaging lltf and ht_ltf data when
/// receiving HT packet. Otherwise, use ht_ltf data directly.
pub ltf_merge_en: bool,
/// Enable to turn on channel filter to smooth adjacent sub-carrier. Disable
/// it to keep independence of adjacent sub-carrier.
pub channel_filter_en: bool,
/// Manually scale the CSI data by left shifting or automatically scale the
/// CSI data. If set true, please set the shift bits. false: automatically.
/// true: manually.
pub manu_scale: bool,
/// Manually left shift bits of the scale of the CSI data. The range of the
/// left shift bits is 0~15.
pub shift: u8,
/// Enable to dump 802.11 ACK frame.
pub dump_ack_en: bool,
}
/// Channel state information (CSI) configuration.
#[derive(Clone, PartialEq, Eq)]
#[cfg(all(esp32c6, feature = "csi"))]
pub struct CsiConfig {
/// Enable to acquire CSI.
pub enable: u32,
/// Enable to acquire L-LTF when receiving a 11g PPDU.
pub acquire_csi_legacy: u32,
/// Enable to acquire HT-LTF when receiving an HT20 PPDU.
pub acquire_csi_ht20: u32,
/// Enable to acquire HT-LTF when receiving an HT40 PPDU.
pub acquire_csi_ht40: u32,
/// Enable to acquire HE-LTF when receiving an HE20 SU PPDU.
pub acquire_csi_su: u32,
/// Enable to acquire HE-LTF when receiving an HE20 MU PPDU.
pub acquire_csi_mu: u32,
/// Enable to acquire HE-LTF when receiving an HE20 DCM applied PPDU.
pub acquire_csi_dcm: u32,
/// Enable to acquire HE-LTF when receiving an HE20 Beamformed applied PPDU.
pub acquire_csi_beamformed: u32,
/// When receiving an STBC applied HE PPDU, 0- acquire the complete
/// HE-LTF1, 1- acquire the complete HE-LTF2, 2- sample evenly among the
/// HE-LTF1 and HE-LTF2.
pub acquire_csi_he_stbc: u32,
/// Vvalue 0-3.
pub val_scale_cfg: u32,
/// Enable to dump 802.11 ACK frame, default disabled.
pub dump_ack_en: u32,
/// Reserved.
pub reserved: u32,
}
#[cfg(feature = "csi")]
impl Default for CsiConfig {
#[cfg(not(esp32c6))]
fn default() -> Self {
Self {
lltf_en: true,
htltf_en: true,
stbc_htltf2_en: true,
ltf_merge_en: true,
channel_filter_en: true,
manu_scale: false,
shift: 0,
dump_ack_en: false,
}
}
#[cfg(esp32c6)]
fn default() -> Self {
// https://github.com/esp-rs/esp-wifi-sys/blob/2a466d96fe8119d49852fc794aea0216b106ba7b/esp-wifi-sys/headers/esp_wifi_he_types.h#L67-L82
Self {
enable: 1,
acquire_csi_legacy: 1,
acquire_csi_ht20: 1,
acquire_csi_ht40: 1,
acquire_csi_su: 1,
acquire_csi_mu: 1,
acquire_csi_dcm: 1,
acquire_csi_beamformed: 1,
acquire_csi_he_stbc: 2,
val_scale_cfg: 2,
dump_ack_en: 1,
reserved: 19,
}
}
}
#[cfg(feature = "csi")]
impl From<CsiConfig> for wifi_csi_config_t {
fn from(config: CsiConfig) -> Self {
#[cfg(not(esp32c6))]
{
wifi_csi_config_t {
lltf_en: config.lltf_en,
htltf_en: config.htltf_en,
stbc_htltf2_en: config.stbc_htltf2_en,
ltf_merge_en: config.ltf_merge_en,
channel_filter_en: config.channel_filter_en,
manu_scale: config.manu_scale,
shift: config.shift,
dump_ack_en: config.dump_ack_en,
}
}
#[cfg(esp32c6)]
{
wifi_csi_acquire_config_t {
_bitfield_align_1: [0; 0],
_bitfield_1: wifi_csi_acquire_config_t::new_bitfield_1(
config.enable,
config.acquire_csi_legacy,
config.acquire_csi_ht20,
config.acquire_csi_ht40,
config.acquire_csi_su,
config.acquire_csi_mu,
config.acquire_csi_dcm,
config.acquire_csi_beamformed,
config.acquire_csi_he_stbc,
config.val_scale_cfg,
config.dump_ack_en,
config.reserved,
),
}
}
}
}
#[cfg(feature = "csi")]
impl CsiConfig {
/// Set CSI data configuration
pub(crate) fn apply_config(&self) -> Result<(), WifiError> {
let conf: wifi_csi_config_t = self.clone().into();
unsafe {
esp_wifi_result!(esp_wifi_set_csi_config(&conf))?;
}
Ok(())
}
/// Register the RX callback function of CSI data. Each time a CSI data is
/// received, the callback function will be called.
pub(crate) fn set_receive_cb<C: CsiCallback>(&mut self, cb: C) -> Result<(), WifiError> {
let cb = alloc::boxed::Box::new(cb);
let cb_ptr = alloc::boxed::Box::into_raw(cb) as *mut crate::wifi::c_types::c_void;
unsafe {
esp_wifi_result!(esp_wifi_set_csi_rx_cb(Some(csi_rx_cb::<C>), cb_ptr))?;
}
Ok(())
}
/// Enable or disable CSI
pub(crate) fn set_csi(&self, enable: bool) -> Result<(), WifiError> {
// https://github.com/esp-rs/esp-wifi-sys/blob/2a466d96fe8119d49852fc794aea0216b106ba7b/esp-wifi-sys/headers/esp_wifi.h#L1241
unsafe {
esp_wifi_result!(esp_wifi_set_csi(enable))?;
}
Ok(())
}
}
const RX_QUEUE_SIZE: usize = crate::CONFIG.rx_queue_size;
const TX_QUEUE_SIZE: usize = crate::CONFIG.tx_queue_size;
pub(crate) static DATA_QUEUE_RX_AP: Locked<VecDeque<PacketBuffer>> = Locked::new(VecDeque::new());
pub(crate) static DATA_QUEUE_RX_STA: Locked<VecDeque<PacketBuffer>> = Locked::new(VecDeque::new());
/// Common errors.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum WifiError {
/// Wi-Fi module is not initialized or not initialized for `Wi-Fi`
/// operations.
NotInitialized,
/// Internal Wi-Fi error.
InternalError(InternalWifiError),
/// The device disconnected from the network or failed to connect to it.
Disconnected,
/// Unknown Wi-Fi mode (not Sta/Ap/ApSta).
UnknownWifiMode,
/// Unsupported operation or mode.
Unsupported,
/// Passed arguments are invalid.
InvalidArguments,
}
/// Events generated by the Wi-Fi driver.
#[derive(Debug, FromPrimitive, EnumSetType)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
#[repr(i32)]
pub enum WifiEvent {
/// Wi-Fi is ready for operation.
WifiReady = 0,
/// Scan operation has completed.
ScanDone,
/// Station mode started.
StaStart,
/// Station mode stopped.
StaStop,
/// Station connected to a network.
StaConnected,
/// Station disconnected from a network.
StaDisconnected,
/// Station authentication mode changed.
StaAuthmodeChange,
/// Station WPS succeeds in enrollee mode.
StaWpsErSuccess,
/// Station WPS fails in enrollee mode.
StaWpsErFailed,
/// Station WPS timeout in enrollee mode.
StaWpsErTimeout,
/// Station WPS pin code in enrollee mode.
StaWpsErPin,
/// Station WPS overlap in enrollee mode.
StaWpsErPbcOverlap,
/// Soft-AP start.
ApStart,
/// Soft-AP stop.
ApStop,
/// A station connected to Soft-AP.
ApStaconnected,
/// A station disconnected from Soft-AP.
ApStadisconnected,
/// Received probe request packet in Soft-AP interface.
ApProbereqrecved,
/// Received report of FTM procedure.
FtmReport,
/// AP's RSSI crossed configured threshold.
StaBssRssiLow,
/// Status indication of Action Tx operation.
ActionTxStatus,
/// Remain-on-Channel operation complete.
RocDone,
/// Station beacon timeout.
StaBeaconTimeout,
/// Connectionless module wake interval has started.
ConnectionlessModuleWakeIntervalStart,
/// Soft-AP WPS succeeded in registrar mode.
ApWpsRgSuccess,
/// Soft-AP WPS failed in registrar mode.
ApWpsRgFailed,
/// Soft-AP WPS timed out in registrar mode.
ApWpsRgTimeout,
/// Soft-AP WPS pin code in registrar mode.
ApWpsRgPin,
/// Soft-AP WPS overlap in registrar mode.
ApWpsRgPbcOverlap,
/// iTWT setup.
ItwtSetup,
/// iTWT teardown.
ItwtTeardown,
/// iTWT probe.
ItwtProbe,
/// iTWT suspended.
ItwtSuspend,
/// TWT wakeup event.
TwtWakeup,
/// bTWT setup.
BtwtSetup,
/// bTWT teardown.
BtwtTeardown,
/// NAN (Neighbor Awareness Networking) discovery has started.
NanStarted,
/// NAN discovery has stopped.
NanStopped,
/// NAN service discovery match found.
NanSvcMatch,
/// Replied to a NAN peer with service discovery match.
NanReplied,
/// Received a follow-up message in NAN.
NanReceive,
/// Received NDP (Neighbor Discovery Protocol) request from a NAN peer.
NdpIndication,
/// NDP confirm indication.
NdpConfirm,
/// NAN datapath terminated indication.
NdpTerminated,
/// Wi-Fi home channel change, doesn't occur when scanning.
HomeChannelChange,
/// Received Neighbor Report response.
StaNeighborRep,
}
/// Error originating from the underlying drivers
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
#[repr(i32)]
pub enum InternalWifiError {
/// Out of memory
NoMem = 0x101,
/// Invalid argument
InvalidArg = 0x102,
/// Wi-Fi driver was not installed by [esp_wifi_init](crate::binary::include::esp_wifi_init)
NotInit = 0x3001,
/// Wi-Fi driver was not started by [esp_wifi_start]
NotStarted = 0x3002,
/// Wi-Fi driver was not stopped by [esp_wifi_stop]
NotStopped = 0x3003,
/// Wi-Fi interface error
Interface = 0x3004,
/// Wi-Fi mode error
Mode = 0x3005,
/// Wi-Fi internal state error
State = 0x3006,
/// Wi-Fi internal control block of station or soft-AP error
Conn = 0x3007,
/// Wi-Fi internal NVS module error
Nvs = 0x3008,
/// MAC address is invalid
InvalidMac = 0x3009,
/// SSID is invalid
InvalidSsid = 0x300A,
/// Password is invalid
InvalidPassword = 0x300B,
/// Timeout error
Timeout = 0x300C,
/// Wi-Fi is in sleep state (RF closed) and wakeup failed
WakeFail = 0x300D,
/// The caller would block
WouldBlock = 0x300E,
/// Station still in disconnect status
NotConnected = 0x300F,
/// Failed to post the event to Wi-Fi task
PostFail = 0x3012,
/// Invalid Wi-Fi state when init/deinit is called
InvalidInitState = 0x3013,
/// Returned when Wi-Fi is stopping
StopState = 0x3014,
/// The Wi-Fi connection is not associated
NotAssociated = 0x3015,
/// The Wi-Fi TX is disallowed
TxDisallowed = 0x3016,
}
/// Get the STA MAC address
pub fn sta_mac(mac: &mut [u8; 6]) {
unsafe {
read_mac(mac as *mut u8, 0);
}
}
/// Get the AP MAC address
pub fn ap_mac(mac: &mut [u8; 6]) {
unsafe {
read_mac(mac as *mut u8, 1);
}
}
pub(crate) fn wifi_init() -> Result<(), WifiError> {
unsafe {
internal::G_CONFIG.wpa_crypto_funcs = g_wifi_default_wpa_crypto_funcs;
internal::G_CONFIG.feature_caps = internal::__ESP_RADIO_G_WIFI_FEATURE_CAPS;
#[cfg(coex)]
esp_wifi_result!(coex_init())?;
esp_wifi_result!(esp_wifi_init_internal(addr_of!(internal::G_CONFIG)))?;
esp_wifi_result!(esp_wifi_set_mode(wifi_mode_t_WIFI_MODE_NULL))?;
esp_wifi_result!(esp_supplicant_init())?;
esp_wifi_result!(esp_wifi_set_tx_done_cb(Some(esp_wifi_tx_done_cb)))?;
esp_wifi_result!(esp_wifi_internal_reg_rxcb(
esp_interface_t_ESP_IF_WIFI_STA,
Some(recv_cb_sta)
))?;
// until we support APSTA we just register the same callback for AP and STA
esp_wifi_result!(esp_wifi_internal_reg_rxcb(
esp_interface_t_ESP_IF_WIFI_AP,
Some(recv_cb_ap)
))?;
#[cfg(any(esp32, esp32s3))]
{
static mut NVS_STRUCT: [u32; 12] = [0; 12];
chip_specific::__ESP_RADIO_G_MISC_NVS = addr_of!(NVS_STRUCT) as u32;
}
Ok(())
}
}
#[cfg(coex)]
pub(crate) fn coex_initialize() -> i32 {
debug!("call coex-initialize");
unsafe {
let res = crate::binary::include::esp_coex_adapter_register(
core::ptr::addr_of_mut!(internal::G_COEX_ADAPTER_FUNCS).cast(),
);
if res != 0 {
error!("Error: esp_coex_adapter_register {}", res);
return res;
}
let res = crate::binary::include::coex_pre_init();
if res != 0 {
error!("Error: coex_pre_init {}", res);
return res;
}
0
}
}
pub(crate) unsafe extern "C" fn coex_init() -> i32 {
#[cfg(coex)]
{
debug!("coex-init");
#[allow(clippy::needless_return)]
return unsafe { crate::binary::include::coex_init() };
}
#[cfg(not(coex))]
0
}
fn wifi_deinit() -> Result<(), crate::InitializationError> {
esp_wifi_result!(unsafe { esp_wifi_stop() })?;
esp_wifi_result!(unsafe { esp_wifi_deinit_internal() })?;
esp_wifi_result!(unsafe { esp_supplicant_deinit() })?;
Ok(())
}
unsafe extern "C" fn recv_cb_sta(
buffer: *mut c_types::c_void,
len: u16,
eb: *mut c_types::c_void,
) -> esp_err_t {
let packet = PacketBuffer { buffer, len, eb };
// We must handle the result outside of the lock because
// PacketBuffer::drop must not be called in a critical section.
// Dropping an PacketBuffer will call `esp_wifi_internal_free_rx_buffer`
// which will try to lock an internal mutex. If the mutex is already taken,
// the function will try to trigger a context switch, which will fail if we
// are in an interrupt-free context.
match DATA_QUEUE_RX_STA.with(|queue| {
if queue.len() < RX_QUEUE_SIZE {
queue.push_back(packet);
Ok(())
} else {
Err(packet)
}
}) {
Ok(()) => {
embassy::STA_RECEIVE_WAKER.wake();
include::ESP_OK as esp_err_t
}
_ => {
debug!("RX QUEUE FULL");
include::ESP_ERR_NO_MEM as esp_err_t
}
}
}
unsafe extern "C" fn recv_cb_ap(
buffer: *mut c_types::c_void,
len: u16,
eb: *mut c_types::c_void,
) -> esp_err_t {
let packet = PacketBuffer { buffer, len, eb };
// We must handle the result outside of the critical section because
// PacketBuffer::drop must not be called in a critical section.
// Dropping an PacketBuffer will call `esp_wifi_internal_free_rx_buffer`
// which will try to lock an internal mutex. If the mutex is already taken,
// the function will try to trigger a context switch, which will fail if we
// are in an interrupt-free context.
match DATA_QUEUE_RX_AP.with(|queue| {
if queue.len() < RX_QUEUE_SIZE {
queue.push_back(packet);
Ok(())
} else {
Err(packet)
}
}) {
Ok(()) => {
embassy::AP_RECEIVE_WAKER.wake();
include::ESP_OK as esp_err_t
}
_ => {
debug!("RX QUEUE FULL");
include::ESP_ERR_NO_MEM as esp_err_t
}
}
}
pub(crate) static WIFI_TX_INFLIGHT: AtomicUsize = AtomicUsize::new(0);
fn decrement_inflight_counter() {
unwrap!(
WIFI_TX_INFLIGHT.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
Some(x.saturating_sub(1))
})
);
}
#[ram]
unsafe extern "C" fn esp_wifi_tx_done_cb(
_ifidx: u8,
_data: *mut u8,
_data_len: *mut u16,
_tx_status: bool,
) {
trace!("esp_wifi_tx_done_cb");
decrement_inflight_counter();
embassy::TRANSMIT_WAKER.wake();
}
pub(crate) fn wifi_start() -> Result<(), WifiError> {
unsafe {
esp_wifi_result!(esp_wifi_start())?;
let mode = WifiMode::current()?;
// This is not an if-else because in AP-STA mode, both are true
if mode.is_ap() {
esp_wifi_result!(include::esp_wifi_set_inactive_time(
wifi_interface_t_WIFI_IF_AP,
crate::CONFIG.ap_beacon_timeout
))?;
}
if mode.is_sta() {
esp_wifi_result!(include::esp_wifi_set_inactive_time(
wifi_interface_t_WIFI_IF_STA,
crate::CONFIG.beacon_timeout
))?;
};
}
Ok(())
}
/// Configuration for active or passive scan. For details see the [Wi-Fi Alliance FAQ](https://www.wi-fi.org/knowledge-center/faq/what-are-passive-and-active-scanning).
///
/// # Comparison of active and passive scan
///
/// | | **Active** | **Passive** |
/// |--------------------------------------|------------|-------------|
/// | **Power consumption** | High | Low |
/// | **Time required (typical behavior)** | Low | High |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScanTypeConfig {
/// Active scan with min and max scan time per channel. This is the default
/// and recommended if you are unsure.
///
/// # Procedure
/// 1. Send probe request on each channel.
/// 2. Wait for probe response. Wait at least `min` time, but if no response is received, wait
/// up to `max` time.
/// 3. Switch channel.
/// 4. Repeat from 1.
Active {
/// Minimum scan time per channel. Defaults to 10ms.
min: Duration,
/// Maximum scan time per channel. Defaults to 20ms.
max: Duration,
},
/// Passive scan
///
/// # Procedure
/// 1. Wait for beacon for given duration.
/// 2. Switch channel.
/// 3. Repeat from 1.
///
/// # Note
/// It is recommended to avoid duration longer thean 1500ms, as it may cause
/// a station to disconnect from the AP.
Passive(Duration),
}
impl Default for ScanTypeConfig {
fn default() -> Self {
Self::Active {
min: Duration::from_millis(10),
max: Duration::from_millis(20),
}
}
}
impl ScanTypeConfig {
fn validate(&self) {
if matches!(self, Self::Passive(dur) if *dur > Duration::from_millis(1500)) {
warn!(
"Passive scan duration longer than 1500ms may cause a station to disconnect from the AP"
);
}
}
}
/// Scan configuration
#[derive(Clone, Copy, Default, PartialEq, Eq, BuilderLite)]
pub struct ScanConfig<'a> {
/// SSID to filter for.
/// If [`None`] is passed, all SSIDs will be returned.
/// If [`Some`] is passed, only the APs matching the given SSID will be
/// returned.
pub ssid: Option<&'a str>,
/// BSSID to filter for.
/// If [`None`] is passed, all BSSIDs will be returned.
/// If [`Some`] is passed, only the APs matching the given BSSID will be
/// returned.
pub bssid: Option<[u8; 6]>,
/// Channel to filter for.
/// If [`None`] is passed, all channels will be returned.
/// If [`Some`] is passed, only the APs on the given channel will be
/// returned.
pub channel: Option<u8>,
/// Whether to show hidden networks.
pub show_hidden: bool,
/// Scan type, active or passive.
pub scan_type: ScanTypeConfig,
/// The maximum number of networks to return when scanning.
/// If [`None`] is passed, all networks will be returned.
/// If [`Some`] is passed, the specified number of networks will be returned.
pub max: Option<usize>,
}
pub(crate) fn wifi_start_scan(
block: bool,
ScanConfig {
ssid,
mut bssid,
channel,
show_hidden,
scan_type,
..
}: ScanConfig<'_>,
) -> i32 {
scan_type.validate();
let (scan_time, scan_type) = match scan_type {
ScanTypeConfig::Active { min, max } => (
wifi_scan_time_t {
active: wifi_active_scan_time_t {
min: min.as_millis() as u32,
max: max.as_millis() as u32,
},
passive: 0,
},
wifi_scan_type_t_WIFI_SCAN_TYPE_ACTIVE,
),
ScanTypeConfig::Passive(dur) => (
wifi_scan_time_t {
active: wifi_active_scan_time_t { min: 0, max: 0 },
passive: dur.as_millis() as u32,
},
wifi_scan_type_t_WIFI_SCAN_TYPE_PASSIVE,
),
};
let mut ssid_buf = ssid.map(|m| {
let mut buf = alloc::vec::Vec::from_iter(m.bytes());
buf.push(b'\0');
buf
});
let ssid = ssid_buf
.as_mut()
.map(|e| e.as_mut_ptr())
.unwrap_or_else(core::ptr::null_mut);
let bssid = bssid
.as_mut()
.map(|e| e.as_mut_ptr())
.unwrap_or_else(core::ptr::null_mut);
let scan_config = wifi_scan_config_t {
ssid,
bssid,
channel: channel.unwrap_or(0),
show_hidden,
scan_type,
scan_time,
home_chan_dwell_time: 0,
channel_bitmap: wifi_scan_channel_bitmap_t {
ghz_2_channels: 0,
ghz_5_channels: 0,
},
};
unsafe { esp_wifi_scan_start(&scan_config, block) }
}
mod private {
use super::*;
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
/// Take care not to drop this while in a critical section.
///
/// Dropping an PacketBuffer will call
/// `esp_wifi_internal_free_rx_buffer` which will try to lock an
/// internal mutex. If the mutex is already taken, the function will try
/// to trigger a context switch, which will fail if we are in a critical
/// section.
pub struct PacketBuffer {
pub(crate) buffer: *mut c_types::c_void,
pub(crate) len: u16,
pub(crate) eb: *mut c_types::c_void,
}
unsafe impl Send for PacketBuffer {}
impl Drop for PacketBuffer {
fn drop(&mut self) {
trace!("Dropping PacketBuffer, freeing memory");
unsafe { esp_wifi_internal_free_rx_buffer(self.eb) };
}
}
impl PacketBuffer {
pub fn as_slice_mut(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.buffer as *mut u8, self.len as usize) }
}
}
}
/// Wi-Fi device operational modes.
#[derive(Debug, Clone, Copy)]
pub enum WifiDeviceMode {
/// Station mode.
Sta,
/// Access Point mode.
Ap,
}
impl WifiDeviceMode {
fn mac_address(&self) -> [u8; 6] {
match self {
WifiDeviceMode::Sta => {
let mut mac = [0; 6];
sta_mac(&mut mac);
mac
}
WifiDeviceMode::Ap => {
let mut mac = [0; 6];
ap_mac(&mut mac);
mac
}
}
}
fn data_queue_rx(&self) -> &'static Locked<VecDeque<PacketBuffer>> {
match self {
WifiDeviceMode::Sta => &DATA_QUEUE_RX_STA,
WifiDeviceMode::Ap => &DATA_QUEUE_RX_AP,
}
}
fn can_send(&self) -> bool {
WIFI_TX_INFLIGHT.load(Ordering::SeqCst) < TX_QUEUE_SIZE
}
fn increase_in_flight_counter(&self) {
WIFI_TX_INFLIGHT.fetch_add(1, Ordering::SeqCst);
}
fn tx_token(&self) -> Option<WifiTxToken> {
if !self.can_send() {
crate::preempt::yield_task();
}
if self.can_send() {
Some(WifiTxToken { mode: *self })
} else {
None
}
}
fn rx_token(&self) -> Option<(WifiRxToken, WifiTxToken)> {
let is_empty = self.data_queue_rx().with(|q| q.is_empty());
if is_empty || !self.can_send() {
crate::preempt::yield_task();
}
let is_empty = is_empty && self.data_queue_rx().with(|q| q.is_empty());
if !is_empty {
self.tx_token().map(|tx| (WifiRxToken { mode: *self }, tx))
} else {
None
}
}
fn interface(&self) -> wifi_interface_t {
match self {
WifiDeviceMode::Sta => wifi_interface_t_WIFI_IF_STA,
WifiDeviceMode::Ap => wifi_interface_t_WIFI_IF_AP,
}
}
fn register_transmit_waker(&self, cx: &mut core::task::Context<'_>) {
embassy::TRANSMIT_WAKER.register(cx.waker())
}
fn register_receive_waker(&self, cx: &mut core::task::Context<'_>) {
match self {
WifiDeviceMode::Sta => embassy::STA_RECEIVE_WAKER.register(cx.waker()),
WifiDeviceMode::Ap => embassy::AP_RECEIVE_WAKER.register(cx.waker()),
}
}
fn register_link_state_waker(&self, cx: &mut core::task::Context<'_>) {
match self {
WifiDeviceMode::Sta => embassy::STA_LINK_STATE_WAKER.register(cx.waker()),
WifiDeviceMode::Ap => embassy::AP_LINK_STATE_WAKER.register(cx.waker()),
}
}
fn link_state(&self) -> embassy_net_driver::LinkState {
match self {
WifiDeviceMode::Sta => {
if matches!(sta_state(), WifiState::StaConnected) {
embassy_net_driver::LinkState::Up
} else {
embassy_net_driver::LinkState::Down
}
}
WifiDeviceMode::Ap => {
if matches!(ap_state(), WifiState::ApStarted) {
embassy_net_driver::LinkState::Up
} else {
embassy_net_driver::LinkState::Down
}
}
}
}
}
/// A wifi device implementing smoltcp's Device trait.
pub struct WifiDevice<'d> {
_phantom: PhantomData<&'d ()>,
mode: WifiDeviceMode,
}
impl WifiDevice<'_> {
/// Retrieves the MAC address of the Wi-Fi device.
pub fn mac_address(&self) -> [u8; 6] {
self.mode.mac_address()
}
/// Receives data from the Wi-Fi device (only when `smoltcp` feature is
/// disabled).
#[cfg(not(feature = "smoltcp"))]
pub fn receive(&mut self) -> Option<(WifiRxToken, WifiTxToken)> {
self.mode.rx_token()
}
/// Transmits data through the Wi-Fi device (only when `smoltcp` feature is
/// disabled).
#[cfg(not(feature = "smoltcp"))]
pub fn transmit(&mut self) -> Option<WifiTxToken> {
self.mode.tx_token()
}
}
fn convert_ap_info(record: &include::wifi_ap_record_t) -> AccessPointInfo {
let str_len = record
.ssid
.iter()
.position(|&c| c == 0)
.unwrap_or(record.ssid.len());
let ssid_ref = unsafe { core::str::from_utf8_unchecked(&record.ssid[..str_len]) };
let mut ssid = String::new();
ssid.push_str(ssid_ref);
AccessPointInfo {
ssid,
bssid: record.bssid,
channel: record.primary,
secondary_channel: match record.second {
include::wifi_second_chan_t_WIFI_SECOND_CHAN_NONE => SecondaryChannel::None,
include::wifi_second_chan_t_WIFI_SECOND_CHAN_ABOVE => SecondaryChannel::Above,
include::wifi_second_chan_t_WIFI_SECOND_CHAN_BELOW => SecondaryChannel::Below,
_ => panic!(),
},
signal_strength: record.rssi,
auth_method: Some(AuthMethod::from_raw(record.authmode)),
country: Country::try_from_c(&record.country),
}
}
/// The radio metadata header of the received packet, which is the common header
/// at the beginning of all RX callback buffers in promiscuous mode.
#[cfg(not(any(esp32c6)))]
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct RxControlInfo {
/// Received Signal Strength Indicator (RSSI) of the packet, in dBm.
pub rssi: i32,
/// PHY rate encoding of the packet. Only valid for non-HT (802.11b/g)
/// packets.
pub rate: u32,
/// Protocol of the received packet: 0 for non-HT (11bg), 1 for HT (11n), 3
/// for VHT (11ac).
pub sig_mode: u32,
/// Modulation and Coding Scheme (MCS). Indicates modulation for HT (11n)
/// packets.
pub mcs: u32,
/// Channel bandwidth of the packet: 0 for 20MHz, 1 for 40MHz.
pub cwb: u32,
/// Channel estimate smoothing: 1 recommends smoothing; 0 recommends
/// per-carrier-independent estimate.
pub smoothing: u32,
/// Sounding indicator: 0 for sounding PPDU (used for channel estimation); 1
/// for non-sounding PPDU.
pub not_sounding: u32,
/// Aggregation status: 0 for MPDU packet, 1 for AMPDU packet.
pub aggregation: u32,
/// Space-Time Block Coding (STBC) status: 0 for non-STBC packet, 1 for STBC
/// packet.
pub stbc: u32,
/// Forward Error Correction (FEC) status: indicates if LDPC coding is used
/// for 11n packets.
pub fec_coding: u32,
/// Short Guard Interval (SGI): 0 for long guard interval, 1 for short guard
/// interval.
pub sgi: u32,
/// Number of subframes aggregated in an AMPDU packet.
pub ampdu_cnt: u32,
/// Primary channel on which the packet is received.
pub channel: u32,
/// Secondary channel on which the packet is received: 0 for none, 1 for
/// above, 2 for below.
pub secondary_channel: u32,
/// Timestamp of when the packet is received, in microseconds. Precise only
/// if modem sleep or light sleep is not enabled.
pub timestamp: u32,
/// Noise floor of the Radio Frequency module, in dBm.
pub noise_floor: i32,
/// Antenna number from which the packet is received: 0 for antenna 0, 1 for
/// antenna 1.
pub ant: u32,
/// Length of the packet including the Frame Check Sequence (FCS).
pub sig_len: u32,
/// State of the packet: 0 for no error, other values indicate error codes.
pub rx_state: u32,
}
/// The radio metadata header of the received packet, which is the common header
/// at the beginning of all RX callback buffers in promiscuous mode.
#[cfg(esp32c6)]
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct RxControlInfo {
/// Received Signal Strength Indicator (RSSI) of the packet, in dBm.
pub rssi: i32,
/// PHY rate encoding of the packet. Only valid for non-HT (802.11b/g)
/// packets.
pub rate: u32,
/// Length of the received packet including the Frame Check Sequence (FCS).
pub sig_len: u32,
/// Reception state of the packet: 0 for no error, others indicate error
/// codes.
pub rx_state: u32,
/// Length of the dump buffer.
pub dump_len: u32,
/// Length of HE-SIG-B field (802.11ax).
pub he_sigb_len: u32,
/// Indicates if this is a single MPDU.
pub cur_single_mpdu: u32,
/// Current baseband format.
pub cur_bb_format: u32,
/// Channel estimation validity.
pub rx_channel_estimate_info_vld: u32,
/// Length of the channel estimation.
pub rx_channel_estimate_len: u32,
/// Timing information in seconds.
pub second: u32,
/// Primary channel on which the packet is received.
pub channel: u32,
/// Noise floor of the Radio Frequency module, in dBm.
pub noise_floor: i32,
/// Indicates if this is a group-addressed frame.
pub is_group: u32,
/// End state of the packet reception.
pub rxend_state: u32,
/// Indicate whether the reception frame is from interface 3.
pub rxmatch3: u32,
/// Indicate whether the reception frame is from interface 2.
pub rxmatch2: u32,
/// Indicate whether the reception frame is from interface 1.
pub rxmatch1: u32,
/// Indicate whether the reception frame is from interface 0.
pub rxmatch0: u32,
}
impl RxControlInfo {
/// Create an instance from a raw pointer to [wifi_pkt_rx_ctrl_t].
///
/// # Safety
/// When calling this, you must ensure, that `rx_cntl` points to a valid
/// instance of [wifi_pkt_rx_ctrl_t].
pub unsafe fn from_raw(rx_cntl: *const wifi_pkt_rx_ctrl_t) -> Self {
#[cfg(not(esp32c6))]
let rx_control_info = unsafe {
RxControlInfo {
rssi: (*rx_cntl).rssi(),
rate: (*rx_cntl).rate(),
sig_mode: (*rx_cntl).sig_mode(),
mcs: (*rx_cntl).mcs(),
cwb: (*rx_cntl).cwb(),
smoothing: (*rx_cntl).smoothing(),
not_sounding: (*rx_cntl).not_sounding(),
aggregation: (*rx_cntl).aggregation(),
stbc: (*rx_cntl).stbc(),
fec_coding: (*rx_cntl).fec_coding(),
sgi: (*rx_cntl).sgi(),
ampdu_cnt: (*rx_cntl).ampdu_cnt(),
channel: (*rx_cntl).channel(),
secondary_channel: (*rx_cntl).secondary_channel(),
timestamp: (*rx_cntl).timestamp(),
noise_floor: (*rx_cntl).noise_floor(),
ant: (*rx_cntl).ant(),
sig_len: (*rx_cntl).sig_len(),
rx_state: (*rx_cntl).rx_state(),
}
};
#[cfg(esp32c6)]
let rx_control_info = unsafe {
RxControlInfo {
rssi: (*rx_cntl).rssi(),
rate: (*rx_cntl).rate(),
sig_len: (*rx_cntl).sig_len(),
rx_state: (*rx_cntl).rx_state(),
dump_len: (*rx_cntl).dump_len(),
he_sigb_len: (*rx_cntl).he_sigb_len(),
cur_single_mpdu: (*rx_cntl).cur_single_mpdu(),
cur_bb_format: (*rx_cntl).cur_bb_format(),
rx_channel_estimate_info_vld: (*rx_cntl).rx_channel_estimate_info_vld(),
rx_channel_estimate_len: (*rx_cntl).rx_channel_estimate_len(),
second: (*rx_cntl).second(),
channel: (*rx_cntl).channel(),
noise_floor: (*rx_cntl).noise_floor(),
is_group: (*rx_cntl).is_group(),
rxend_state: (*rx_cntl).rxend_state(),
rxmatch3: (*rx_cntl).rxmatch3(),
rxmatch2: (*rx_cntl).rxmatch2(),
rxmatch1: (*rx_cntl).rxmatch1(),
rxmatch0: (*rx_cntl).rxmatch0(),
}
};
rx_control_info
}
}
/// Represents a Wi-Fi packet in promiscuous mode.
#[cfg(all(feature = "sniffer", feature = "unstable"))]
#[instability::unstable]
pub struct PromiscuousPkt<'a> {
/// Control information related to packet reception.
pub rx_cntl: RxControlInfo,
/// Frame type of the received packet.
pub frame_type: wifi_promiscuous_pkt_type_t,
/// Length of the received packet.
pub len: usize,
/// Data contained in the received packet.
pub data: &'a [u8],
}
#[cfg(all(feature = "sniffer", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
impl PromiscuousPkt<'_> {
/// # Safety
/// When calling this, you have to ensure, that `buf` points to a valid
/// [wifi_promiscuous_pkt_t].
pub(crate) unsafe fn from_raw(
buf: *const wifi_promiscuous_pkt_t,
frame_type: wifi_promiscuous_pkt_type_t,
) -> Self {
let rx_cntl = unsafe { RxControlInfo::from_raw(&(*buf).rx_ctrl) };
let len = rx_cntl.sig_len as usize;
PromiscuousPkt {
rx_cntl,
frame_type,
len,
data: unsafe {
core::slice::from_raw_parts(
(buf as *const u8).add(core::mem::size_of::<wifi_pkt_rx_ctrl_t>()),
len,
)
},
}
}
}
#[cfg(all(feature = "sniffer", feature = "unstable"))]
static SNIFFER_CB: Locked<Option<fn(PromiscuousPkt<'_>)>> = Locked::new(None);
#[cfg(all(feature = "sniffer", feature = "unstable"))]
unsafe extern "C" fn promiscuous_rx_cb(buf: *mut core::ffi::c_void, frame_type: u32) {
unsafe {
if let Some(sniffer_callback) = SNIFFER_CB.with(|callback| *callback) {
let promiscuous_pkt = PromiscuousPkt::from_raw(buf as *const _, frame_type);
sniffer_callback(promiscuous_pkt);
}
}
}
#[cfg(all(feature = "sniffer", feature = "unstable"))]
#[instability::unstable]
/// A Wi-Fi sniffer.
#[non_exhaustive]
pub struct Sniffer {}
#[cfg(all(feature = "sniffer", feature = "unstable"))]
impl Sniffer {
pub(crate) fn new() -> Self {
// This shouldn't fail, since the way this is created, means that wifi will
// always be initialized.
unwrap!(esp_wifi_result!(unsafe {
esp_wifi_set_promiscuous_rx_cb(Some(promiscuous_rx_cb))
}));
Self {}
}
/// Set promiscuous mode enabled or disabled.
#[instability::unstable]
pub fn set_promiscuous_mode(&self, enabled: bool) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_set_promiscuous(enabled) })?;
Ok(())
}
/// Transmit a raw frame.
#[instability::unstable]
pub fn send_raw_frame(
&mut self,
use_sta_interface: bool,
buffer: &[u8],
use_internal_seq_num: bool,
) -> Result<(), WifiError> {
esp_wifi_result!(unsafe {
esp_wifi_80211_tx(
if use_sta_interface {
wifi_interface_t_WIFI_IF_STA
} else {
wifi_interface_t_WIFI_IF_AP
} as wifi_interface_t,
buffer.as_ptr() as *const _,
buffer.len() as i32,
use_internal_seq_num,
)
})
}
/// Set the callback for receiving a packet.
#[instability::unstable]
pub fn set_receive_cb(&mut self, cb: fn(PromiscuousPkt<'_>)) {
SNIFFER_CB.with(|callback| *callback = Some(cb));
}
}
// see https://docs.rs/smoltcp/0.7.1/smoltcp/phy/index.html
#[cfg(all(feature = "smoltcp", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
impl Device for WifiDevice<'_> {
type RxToken<'a>
= WifiRxToken
where
Self: 'a;
type TxToken<'a>
= WifiTxToken
where
Self: 'a;
fn receive(
&mut self,
_instant: smoltcp::time::Instant,
) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
self.mode.rx_token()
}
fn transmit(&mut self, _instant: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
self.mode.tx_token()
}
fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
let mut caps = DeviceCapabilities::default();
caps.max_transmission_unit = MTU;
caps.max_burst_size = if crate::CONFIG.max_burst_size == 0 {
None
} else {
Some(crate::CONFIG.max_burst_size)
};
caps
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct WifiRxToken {
mode: WifiDeviceMode,
}
impl WifiRxToken {
/// Consumes the RX token and applies the callback function to the received
/// data buffer.
pub fn consume_token<R, F>(self, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
let mut data = self.mode.data_queue_rx().with(|queue| {
unwrap!(
queue.pop_front(),
"unreachable: transmit()/receive() ensures there is a packet to process"
)
});
// We handle the received data outside of the lock because
// PacketBuffer::drop must not be called in a critical section.
// Dropping an PacketBuffer will call `esp_wifi_internal_free_rx_buffer`
// which will try to lock an internal mutex. If the mutex is already
// taken, the function will try to trigger a context switch, which will
// fail if we are in an interrupt-free context.
let buffer = data.as_slice_mut();
dump_packet_info(buffer);
f(buffer)
}
}
#[cfg(all(feature = "smoltcp", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
impl RxToken for WifiRxToken {
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&[u8]) -> R,
{
self.consume_token(|t| f(t))
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct WifiTxToken {
mode: WifiDeviceMode,
}
impl WifiTxToken {
/// Consumes the TX token and applies the callback function to the received
/// data buffer.
pub fn consume_token<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
self.mode.increase_in_flight_counter();
// (safety): creation of multiple Wi-Fi devices with the same mode is impossible
// in safe Rust, therefore only smoltcp _or_ embassy-net can be used at
// one time
static mut BUFFER: [u8; MTU] = [0u8; MTU];
let buffer = unsafe { &mut BUFFER[..len] };
let res = f(buffer);
esp_wifi_send_data(self.mode.interface(), buffer);
res
}
}
#[cfg(all(feature = "smoltcp", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
impl TxToken for WifiTxToken {
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
self.consume_token(len, f)
}
}
// FIXME data here has to be &mut because of `esp_wifi_internal_tx` signature,
// requiring a *mut ptr to the buffer Casting const to mut is instant UB, even
// though in reality `esp_wifi_internal_tx` copies the buffer into its own
// memory and does not modify
pub(crate) fn esp_wifi_send_data(interface: wifi_interface_t, data: &mut [u8]) {
trace!("sending... {} bytes", data.len());
dump_packet_info(data);
let len = data.len() as u16;
let ptr = data.as_mut_ptr().cast();
let res = unsafe { esp_wifi_internal_tx(interface, ptr, len) };
if res != 0 {
warn!("esp_wifi_internal_tx {}", res);
decrement_inflight_counter();
} else {
trace!("esp_wifi_internal_tx ok");
}
}
fn apply_ap_config(config: &AccessPointConfiguration) -> Result<(), WifiError> {
let mut cfg = wifi_config_t {
ap: wifi_ap_config_t {
ssid: [0; 32],
password: [0; 64],
ssid_len: 0,
channel: config.channel,
authmode: config.auth_method.to_raw(),
ssid_hidden: if config.ssid_hidden { 1 } else { 0 },
max_connection: config.max_connections as u8,
beacon_interval: 100,
pairwise_cipher: wifi_cipher_type_t_WIFI_CIPHER_TYPE_CCMP,
ftm_responder: false,
pmf_cfg: wifi_pmf_config_t {
capable: true,
required: false,
},
sae_pwe_h2e: 0,
csa_count: 3,
dtim_period: 2,
},
};
if config.auth_method == AuthMethod::None && !config.password.is_empty() {
return Err(WifiError::InternalError(InternalWifiError::InvalidArg));
}
unsafe {
cfg.ap.ssid[0..(config.ssid.len())].copy_from_slice(config.ssid.as_bytes());
cfg.ap.ssid_len = config.ssid.len() as u8;
cfg.ap.password[0..(config.password.len())].copy_from_slice(config.password.as_bytes());
esp_wifi_result!(esp_wifi_set_config(wifi_interface_t_WIFI_IF_AP, &mut cfg))
}
}
fn apply_sta_config(config: &ClientConfiguration) -> Result<(), WifiError> {
let mut cfg = wifi_config_t {
sta: wifi_sta_config_t {
ssid: [0; 32],
password: [0; 64],
scan_method: crate::CONFIG.scan_method,
bssid_set: config.bssid.is_some(),
bssid: config.bssid.unwrap_or_default(),
channel: config.channel.unwrap_or(0),
listen_interval: crate::CONFIG.listen_interval,
sort_method: wifi_sort_method_t_WIFI_CONNECT_AP_BY_SIGNAL,
threshold: wifi_scan_threshold_t {
rssi: -99,
authmode: config.auth_method.to_raw(),
},
pmf_cfg: wifi_pmf_config_t {
capable: true,
required: false,
},
sae_pwe_h2e: 3,
_bitfield_align_1: [0; 0],
_bitfield_1: __BindgenBitfieldUnit::new([0; 4]),
failure_retry_cnt: crate::CONFIG.failure_retry_cnt,
_bitfield_align_2: [0; 0],
_bitfield_2: __BindgenBitfieldUnit::new([0; 4]),
sae_pk_mode: 0, // ??
sae_h2e_identifier: [0; 32],
},
};
if config.auth_method == AuthMethod::None && !config.password.is_empty() {
return Err(WifiError::InternalError(InternalWifiError::InvalidArg));
}
unsafe {
cfg.sta.ssid[0..(config.ssid.len())].copy_from_slice(config.ssid.as_bytes());
cfg.sta.password[0..(config.password.len())].copy_from_slice(config.password.as_bytes());
esp_wifi_result!(esp_wifi_set_config(wifi_interface_t_WIFI_IF_STA, &mut cfg))
}
}
#[cfg(feature = "wifi-eap")]
fn apply_sta_eap_config(config: &EapClientConfiguration) -> Result<(), WifiError> {
let mut cfg = wifi_config_t {
sta: wifi_sta_config_t {
ssid: [0; 32],
password: [0; 64],
scan_method: crate::CONFIG.scan_method,
bssid_set: config.bssid.is_some(),
bssid: config.bssid.unwrap_or_default(),
channel: config.channel.unwrap_or(0),
listen_interval: crate::CONFIG.listen_interval,
sort_method: wifi_sort_method_t_WIFI_CONNECT_AP_BY_SIGNAL,
threshold: wifi_scan_threshold_t {
rssi: -99,
authmode: config.auth_method.to_raw(),
},
pmf_cfg: wifi_pmf_config_t {
capable: true,
required: false,
},
sae_pwe_h2e: 3,
_bitfield_align_1: [0; 0],
_bitfield_1: __BindgenBitfieldUnit::new([0; 4]),
failure_retry_cnt: crate::CONFIG.failure_retry_cnt,
_bitfield_align_2: [0; 0],
_bitfield_2: __BindgenBitfieldUnit::new([0; 4]),
sae_pk_mode: 0, // ??
sae_h2e_identifier: [0; 32],
},
};
unsafe {
cfg.sta.ssid[0..(config.ssid.len())].copy_from_slice(config.ssid.as_bytes());
esp_wifi_result!(esp_wifi_set_config(wifi_interface_t_WIFI_IF_STA, &mut cfg))?;
if let Some(identity) = &config.identity {
esp_wifi_result!(esp_eap_client_set_identity(
identity.as_str().as_ptr(),
identity.len() as i32
))?;
} else {
esp_eap_client_clear_identity();
}
if let Some(username) = &config.username {
esp_wifi_result!(esp_eap_client_set_username(
username.as_str().as_ptr(),
username.len() as i32
))?;
} else {
esp_eap_client_clear_username();
}
if let Some(password) = &config.password {
esp_wifi_result!(esp_eap_client_set_password(
password.as_str().as_ptr(),
password.len() as i32
))?;
} else {
esp_eap_client_clear_password();
}
if let Some(new_password) = &config.new_password {
esp_wifi_result!(esp_eap_client_set_new_password(
new_password.as_str().as_ptr(),
new_password.len() as i32
))?;
} else {
esp_eap_client_clear_new_password();
}
if let Some(pac_file) = &config.pac_file {
esp_wifi_result!(esp_eap_client_set_pac_file(
pac_file.as_ptr(),
pac_file.len() as i32
))?;
}
if let Some(phase2_method) = &config.ttls_phase2_method {
esp_wifi_result!(esp_eap_client_set_ttls_phase2_method(
phase2_method.to_raw()
))?;
}
if let Some(ca_cert) = config.ca_cert {
esp_wifi_result!(esp_eap_client_set_ca_cert(
ca_cert.as_ptr(),
ca_cert.len() as i32
))?;
} else {
esp_eap_client_clear_ca_cert();
}
if let Some((cert, key, password)) = config.certificate_and_key {
let (pwd, pwd_len) = if let Some(pwd) = password {
(pwd.as_ptr(), pwd.len() as i32)
} else {
(core::ptr::null(), 0)
};
esp_wifi_result!(esp_eap_client_set_certificate_and_key(
cert.as_ptr(),
cert.len() as i32,
key.as_ptr(),
key.len() as i32,
pwd,
pwd_len,
))?;
} else {
esp_eap_client_clear_certificate_and_key();
}
if let Some(cfg) = &config.eap_fast_config {
let params = esp_eap_fast_config {
fast_provisioning: cfg.fast_provisioning as i32,
fast_max_pac_list_len: cfg.fast_max_pac_list_len as i32,
fast_pac_format_binary: cfg.fast_pac_format_binary,
};
esp_wifi_result!(esp_eap_client_set_fast_params(params))?;
}
esp_wifi_result!(esp_eap_client_set_disable_time_check(!&config.time_check))?;
// esp_eap_client_set_suiteb_192bit_certification unsupported because we build
// without MBEDTLS
// esp_eap_client_use_default_cert_bundle unsupported because we build without
// MBEDTLS
esp_wifi_result!(esp_wifi_sta_enterprise_enable())?;
Ok(())
}
}
fn dump_packet_info(_buffer: &mut [u8]) {
#[cfg(dump_packets)]
{
info!("@WIFIFRAME {:?}", _buffer);
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! esp_wifi_result {
($value:expr) => {{
use num_traits::FromPrimitive;
let result = $value;
if result != esp_wifi_sys::include::ESP_OK as i32 {
warn!("{} returned an error: {}", stringify!($value), result);
Err(WifiError::InternalError(unwrap!(FromPrimitive::from_i32(
result
))))
} else {
Ok::<(), WifiError>(())
}
}};
}
pub(crate) mod embassy {
use embassy_net_driver::{Capabilities, Driver, HardwareAddress, RxToken, TxToken};
use esp_hal::asynch::AtomicWaker;
use super::*;
// We can get away with a single tx waker because the transmit queue is shared
// between interfaces.
pub(crate) static TRANSMIT_WAKER: AtomicWaker = AtomicWaker::new();
pub(crate) static AP_RECEIVE_WAKER: AtomicWaker = AtomicWaker::new();
pub(crate) static AP_LINK_STATE_WAKER: AtomicWaker = AtomicWaker::new();
pub(crate) static STA_RECEIVE_WAKER: AtomicWaker = AtomicWaker::new();
pub(crate) static STA_LINK_STATE_WAKER: AtomicWaker = AtomicWaker::new();
impl RxToken for WifiRxToken {
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
self.consume_token(f)
}
}
impl TxToken for WifiTxToken {
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
self.consume_token(len, f)
}
}
impl Driver for WifiDevice<'_> {
type RxToken<'a>
= WifiRxToken
where
Self: 'a;
type TxToken<'a>
= WifiTxToken
where
Self: 'a;
fn receive(
&mut self,
cx: &mut core::task::Context<'_>,
) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
self.mode.register_receive_waker(cx);
self.mode.register_transmit_waker(cx);
self.mode.rx_token()
}
fn transmit(&mut self, cx: &mut core::task::Context<'_>) -> Option<Self::TxToken<'_>> {
self.mode.register_transmit_waker(cx);
self.mode.tx_token()
}
fn link_state(
&mut self,
cx: &mut core::task::Context<'_>,
) -> embassy_net_driver::LinkState {
self.mode.register_link_state_waker(cx);
self.mode.link_state()
}
fn capabilities(&self) -> Capabilities {
let mut caps = Capabilities::default();
caps.max_transmission_unit = MTU;
caps.max_burst_size = if crate::CONFIG.max_burst_size == 0 {
None
} else {
Some(crate::CONFIG.max_burst_size)
};
caps
}
fn hardware_address(&self) -> HardwareAddress {
HardwareAddress::Ethernet(self.mac_address())
}
}
}
pub(crate) fn apply_power_saving(ps: PowerSaveMode) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_sys::include::esp_wifi_set_ps(ps.into()) })?;
Ok(())
}
struct FreeApListOnDrop;
impl FreeApListOnDrop {
pub fn defuse(self) {
core::mem::forget(self);
}
}
impl Drop for FreeApListOnDrop {
fn drop(&mut self) {
unsafe {
include::esp_wifi_clear_ap_list();
}
}
}
/// Represents the Wi-Fi controller and its associated interfaces.
#[non_exhaustive]
pub struct Interfaces<'d> {
/// Station mode Wi-Fi device.
pub sta: WifiDevice<'d>,
/// Access Point mode Wi-Fi device.
pub ap: WifiDevice<'d>,
/// ESP-NOW interface.
#[cfg(all(feature = "esp-now", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
pub esp_now: crate::esp_now::EspNow<'d>,
/// Wi-Fi sniffer interface.
#[cfg(all(feature = "sniffer", feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
pub sniffer: Sniffer,
}
/// Create a Wi-Fi controller and it's associated interfaces.
///
/// Dropping the controller will deinitialize / stop Wi-Fi.
///
/// Make sure to **not** call this function while interrupts are disabled, or IEEE 802.15.4 is
/// currently in use.
pub fn new<'d>(
_inited: &'d Controller<'d>,
_device: crate::hal::peripherals::WIFI<'d>,
) -> Result<(WifiController<'d>, Interfaces<'d>), WifiError> {
if crate::is_interrupts_disabled() {
return Err(WifiError::Unsupported);
}
let mut controller = WifiController {
_phantom: Default::default(),
};
crate::wifi::wifi_init()?;
let mut cntry_code = [0u8; 3];
cntry_code[..crate::CONFIG.country_code.len()]
.copy_from_slice(crate::CONFIG.country_code.as_bytes());
cntry_code[2] = crate::CONFIG.country_code_operating_class;
unsafe {
let country = wifi_country_t {
cc: cntry_code,
schan: 1,
nchan: 13,
max_tx_power: 20,
policy: wifi_country_policy_t_WIFI_COUNTRY_POLICY_MANUAL,
};
esp_wifi_result!(esp_wifi_set_country(&country))?;
}
// At some point the "High-speed ADC" entropy source became available.
unsafe { esp_hal::rng::TrngSource::increase_entropy_source_counter() };
controller.set_power_saving(PowerSaveMode::default())?;
Ok((
controller,
Interfaces {
sta: WifiDevice {
_phantom: Default::default(),
mode: WifiDeviceMode::Sta,
},
ap: WifiDevice {
_phantom: Default::default(),
mode: WifiDeviceMode::Ap,
},
#[cfg(all(feature = "esp-now", feature = "unstable"))]
esp_now: crate::esp_now::EspNow::new_internal(),
#[cfg(all(feature = "sniffer", feature = "unstable"))]
sniffer: Sniffer::new(),
},
))
}
/// Wi-Fi controller.
#[non_exhaustive]
pub struct WifiController<'d> {
_phantom: PhantomData<&'d ()>,
}
impl Drop for WifiController<'_> {
fn drop(&mut self) {
if let Err(e) = crate::wifi::wifi_deinit() {
warn!("Failed to cleanly deinit wifi: {:?}", e);
}
esp_hal::rng::TrngSource::decrease_entropy_source_counter(unsafe {
esp_hal::Internal::conjure()
});
}
}
impl WifiController<'_> {
/// Set CSI configuration and register the receiving callback.
#[cfg(feature = "csi")]
#[instability::unstable]
pub fn set_csi(
&mut self,
mut csi: CsiConfig,
cb: impl FnMut(crate::wifi::wifi_csi_info_t) + Send,
) -> Result<(), WifiError> {
csi.apply_config()?;
csi.set_receive_cb(cb)?;
csi.set_csi(true)?;
Ok(())
}
/// Set the Wi-Fi protocol.
///
/// This will set the wifi protocol to the desired protocol, the default for
/// this is: `WIFI_PROTOCOL_11B|WIFI_PROTOCOL_11G|WIFI_PROTOCOL_11N`
///
/// # Arguments:
///
/// * `protocols` - The desired protocols
///
/// # Example:
///
/// ```
/// wifi_controller.set_protocol(Protocol::P802D11BGNLR.into());
/// ```
pub fn set_protocol(&mut self, protocols: EnumSet<Protocol>) -> Result<(), WifiError> {
let protocol = protocols
.into_iter()
.map(|v| match v {
Protocol::P802D11B => WIFI_PROTOCOL_11B,
Protocol::P802D11BG => WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G,
Protocol::P802D11BGN => WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N,
Protocol::P802D11BGNLR => {
WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR
}
Protocol::P802D11LR => WIFI_PROTOCOL_LR,
Protocol::P802D11BGNAX => {
WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_11AX
}
})
.fold(0, |combined, protocol| combined | protocol) as u8;
let mode = self.mode()?;
if mode.is_sta() {
esp_wifi_result!(unsafe {
esp_wifi_set_protocol(wifi_interface_t_WIFI_IF_STA, protocol)
})?;
}
if mode.is_ap() {
esp_wifi_result!(unsafe {
esp_wifi_set_protocol(wifi_interface_t_WIFI_IF_AP, protocol)
})?;
}
Ok(())
}
/// Configures modem power saving.
pub fn set_power_saving(&mut self, ps: PowerSaveMode) -> Result<(), WifiError> {
apply_power_saving(ps)
}
/// A blocking wifi network scan with caller-provided scanning options.
pub fn scan_with_config_sync(
&mut self,
config: ScanConfig<'_>,
) -> Result<alloc::vec::Vec<AccessPointInfo>, WifiError> {
esp_wifi_result!(crate::wifi::wifi_start_scan(true, config))?;
self.scan_results(config.max.unwrap_or(usize::MAX))
}
fn scan_results(&mut self, max: usize) -> Result<alloc::vec::Vec<AccessPointInfo>, WifiError> {
let mut scanned = alloc::vec::Vec::<AccessPointInfo>::new();
let mut bss_total: u16 = max as u16;
// Prevents memory leak on error
let guard = FreeApListOnDrop;
unsafe { esp_wifi_result!(include::esp_wifi_scan_get_ap_num(&mut bss_total))? };
guard.defuse();
let mut record: MaybeUninit<include::wifi_ap_record_t> = MaybeUninit::uninit();
for _ in 0..usize::min(bss_total as usize, max) {
let record = unsafe { MaybeUninit::assume_init_mut(&mut record) };
unsafe { esp_wifi_result!(include::esp_wifi_scan_get_ap_record(record))? };
let ap_info = convert_ap_info(record);
scanned.push(ap_info);
}
unsafe { esp_wifi_result!(include::esp_wifi_clear_ap_list())? };
Ok(scanned)
}
/// Starts the Wi-Fi controller.
pub fn start(&mut self) -> Result<(), WifiError> {
crate::wifi::wifi_start()
}
/// Stops the Wi-Fi controller.
pub fn stop(&mut self) -> Result<(), WifiError> {
self.stop_impl()
}
/// Connect Wi-Fi station to the AP.
///
/// - If station is connected , call [Self::disconnect] to disconnect.
/// - Scanning will not be effective until connection between device and the AP is established.
/// - If device is scanning and connecting at the same time, it will abort scanning and return a
/// warning message and error
pub fn connect(&mut self) -> Result<(), WifiError> {
self.connect_impl()
}
/// Disconnect Wi-Fi station from the AP.
pub fn disconnect(&mut self) -> Result<(), WifiError> {
self.disconnect_impl()
}
/// Get the RSSI information of AP to which the device is associated with.
/// The value is obtained from the last beacon.
///
/// <div class="warning">
///
/// - This API should be called after station connected to AP.
/// - Use this API only in STA or AP-STA mode.
/// </div>
///
/// # Errors
/// This function returns [WifiError::Unsupported] if the STA side isn't
/// running. For example, when configured for AP only.
pub fn rssi(&self) -> Result<i32, WifiError> {
if self.mode()?.is_sta() {
let mut rssi: i32 = 0;
// Will return ESP_FAIL -1 if called in AP mode.
esp_wifi_result!(unsafe { esp_wifi_sta_get_rssi(&mut rssi) })?;
Ok(rssi)
} else {
Err(WifiError::Unsupported)
}
}
/// Get the supported capabilities of the controller.
pub fn capabilities(&self) -> Result<EnumSet<crate::wifi::Capability>, WifiError> {
let caps =
enumset::enum_set! { Capability::Client | Capability::AccessPoint | Capability::Mixed };
Ok(caps)
}
/// Set the configuration.
///
/// This will set the mode accordingly.
/// You need to use Wifi::connect() for connecting to an AP.
///
/// Passing [Configuration::None] will disable both, AP and STA mode.
///
/// If you don't intend to use Wi-Fi anymore at all consider tearing down
/// Wi-Fi completely.
pub fn set_configuration(&mut self, conf: &Configuration) -> Result<(), WifiError> {
conf.validate()?;
let mode = match conf {
Configuration::None => wifi_mode_t_WIFI_MODE_NULL,
Configuration::Client(_) => wifi_mode_t_WIFI_MODE_STA,
Configuration::AccessPoint(_) => wifi_mode_t_WIFI_MODE_AP,
Configuration::Mixed(_, _) => wifi_mode_t_WIFI_MODE_APSTA,
#[cfg(feature = "wifi-eap")]
Configuration::EapClient(_) => wifi_mode_t_WIFI_MODE_STA,
};
esp_wifi_result!(unsafe { esp_wifi_set_mode(mode) })?;
match conf {
Configuration::None => Ok::<(), WifiError>(()),
Configuration::Client(config) => apply_sta_config(config),
Configuration::AccessPoint(config) => apply_ap_config(config),
Configuration::Mixed(sta_config, ap_config) => {
apply_ap_config(ap_config).and_then(|()| apply_sta_config(sta_config))
}
#[cfg(feature = "wifi-eap")]
Configuration::EapClient(config) => apply_sta_eap_config(config),
}
.inspect_err(|_| {
// we/the driver might have applied a partial configuration
// so we better disable AP/STA just in case the caller ignores the error we
// return here - they will run into futher errors this way
unsafe { esp_wifi_set_mode(wifi_mode_t_WIFI_MODE_NULL) };
})?;
Ok(())
}
/// Set the Wi-Fi mode.
///
/// This will override the mode inferred by [Self::set_configuration].
pub fn set_mode(&mut self, mode: WifiMode) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_set_mode(mode.into()) })?;
Ok(())
}
fn stop_impl(&mut self) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_stop() })
}
fn connect_impl(&mut self) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_connect() })
}
fn disconnect_impl(&mut self) -> Result<(), WifiError> {
esp_wifi_result!(unsafe { esp_wifi_disconnect() })
}
/// Checks if the Wi-Fi controller has started.
///
/// This function should be called after the `start` method to verify if the
/// Wi-Fi has started successfully.
pub fn is_started(&self) -> Result<bool, WifiError> {
if matches!(
crate::wifi::sta_state(),
WifiState::StaStarted | WifiState::StaConnected | WifiState::StaDisconnected
) {
return Ok(true);
}
if matches!(crate::wifi::ap_state(), WifiState::ApStarted) {
return Ok(true);
}
Ok(false)
}
/// Checks if the Wi-Fi controller is connected to an AP.
///
/// This function should be called after the `connect` method to verify if
/// the connection was successful.
pub fn is_connected(&self) -> Result<bool, WifiError> {
match crate::wifi::sta_state() {
crate::wifi::WifiState::StaConnected => Ok(true),
crate::wifi::WifiState::StaDisconnected => Err(WifiError::Disconnected),
// FIXME: Should any other enum value trigger an error instead of returning false?
_ => Ok(false),
}
}
fn mode(&self) -> Result<WifiMode, WifiError> {
WifiMode::current()
}
/// An async Wi-Fi network scan with caller-provided scanning options.
pub async fn scan_with_config_async(
&mut self,
config: ScanConfig<'_>,
) -> Result<alloc::vec::Vec<AccessPointInfo>, WifiError> {
Self::clear_events(WifiEvent::ScanDone);
esp_wifi_result!(wifi_start_scan(false, config))?;
// Prevents memory leak if `scan_n`'s future is dropped.
let guard = FreeApListOnDrop;
WifiEventFuture::new(WifiEvent::ScanDone).await;
guard.defuse();
let result = self.scan_results(config.max.unwrap_or(usize::MAX))?;
Ok(result)
}
/// Async version of [`crate::wifi::WifiController`]'s `start` method
pub async fn start_async(&mut self) -> Result<(), WifiError> {
let mut events = enumset::enum_set! {};
let mode = self.mode()?;
if mode.is_ap() {
events |= WifiEvent::ApStart;
}
if mode.is_sta() {
events |= WifiEvent::StaStart;
}
Self::clear_events(events);
wifi_start()?;
self.wait_for_all_events(events, false).await;
Ok(())
}
/// Async version of [`crate::wifi::WifiController`]'s `stop` method
pub async fn stop_async(&mut self) -> Result<(), WifiError> {
let mut events = enumset::enum_set! {};
let mode = self.mode()?;
if mode.is_ap() {
events |= WifiEvent::ApStop;
}
if mode.is_sta() {
events |= WifiEvent::StaStop;
}
Self::clear_events(events);
crate::wifi::WifiController::stop_impl(self)?;
self.wait_for_all_events(events, false).await;
reset_ap_state();
reset_sta_state();
Ok(())
}
/// Async version of [`crate::wifi::WifiController`]'s `connect` method
pub async fn connect_async(&mut self) -> Result<(), WifiError> {
Self::clear_events(WifiEvent::StaConnected | WifiEvent::StaDisconnected);
let err = crate::wifi::WifiController::connect_impl(self).err();
if MultiWifiEventFuture::new(WifiEvent::StaConnected | WifiEvent::StaDisconnected)
.await
.contains(WifiEvent::StaDisconnected)
{
Err(err.unwrap_or(WifiError::Disconnected))
} else {
Ok(())
}
}
/// Async version of [`crate::wifi::WifiController`]'s `Disconnect`
/// method
pub async fn disconnect_async(&mut self) -> Result<(), WifiError> {
// If not connected, this will do nothing.
// It will also wait forever for a `StaDisconnected` event that will never come.
// Return early instead of hanging.
if !matches!(self.is_connected(), Ok(true)) {
return Ok(());
}
Self::clear_events(WifiEvent::StaDisconnected);
crate::wifi::WifiController::disconnect_impl(self)?;
WifiEventFuture::new(WifiEvent::StaDisconnected).await;
Ok(())
}
fn clear_events(events: impl Into<EnumSet<WifiEvent>>) {
WIFI_EVENTS.with(|evts| evts.get_mut().remove_all(events.into()));
}
/// Wait for one [`WifiEvent`].
pub async fn wait_for_event(&mut self, event: WifiEvent) {
Self::clear_events(event);
WifiEventFuture::new(event).await
}
/// Wait for one of multiple [`WifiEvent`]s. Returns the events that
/// occurred while waiting.
pub async fn wait_for_events(
&mut self,
events: EnumSet<WifiEvent>,
clear_pending: bool,
) -> EnumSet<WifiEvent> {
if clear_pending {
Self::clear_events(events);
}
MultiWifiEventFuture::new(events).await
}
/// Wait for multiple [`WifiEvent`]s.
pub async fn wait_for_all_events(
&mut self,
mut events: EnumSet<WifiEvent>,
clear_pending: bool,
) {
if clear_pending {
Self::clear_events(events);
}
while !events.is_empty() {
let fired = MultiWifiEventFuture::new(events).await;
events -= fired;
}
}
}
impl WifiEvent {
pub(crate) fn waker(&self) -> &'static AtomicWaker {
// for now use only one waker for all events
// if that ever becomes a problem we might want to pick some events to use their
// own
static WAKER: AtomicWaker = AtomicWaker::new();
&WAKER
}
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub(crate) struct WifiEventFuture {
event: WifiEvent,
}
impl WifiEventFuture {
/// Creates a new `Future` for the specified Wi-Fi event.
pub fn new(event: WifiEvent) -> Self {
Self { event }
}
}
impl core::future::Future for WifiEventFuture {
type Output = ();
fn poll(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> Poll<Self::Output> {
self.event.waker().register(cx.waker());
if WIFI_EVENTS.with(|events| events.get_mut().remove(self.event)) {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub(crate) struct MultiWifiEventFuture {
event: EnumSet<WifiEvent>,
}
impl MultiWifiEventFuture {
/// Creates a new `Future` for the specified set of Wi-Fi events.
pub fn new(event: EnumSet<WifiEvent>) -> Self {
Self { event }
}
}
impl core::future::Future for MultiWifiEventFuture {
type Output = EnumSet<WifiEvent>;
fn poll(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> Poll<Self::Output> {
let output = WIFI_EVENTS.with(|events| {
let events = events.get_mut();
let active = events.intersection(self.event);
events.remove_all(active);
active
});
if output.is_empty() {
for event in self.event.iter() {
event.waker().register(cx.waker());
}
Poll::Pending
} else {
Poll::Ready(output)
}
}
}