use esp_hal::peripherals::WIFI;
use esp_radio::wifi::{ControllerConfig, Interfaces, WifiController, scan::ScanConfig};
use thiserror_no_std::Error;
pub use esp_radio::wifi::{AuthenticationMethod, ap::AccessPointInfo};
pub type ScanList = heapless::Vec<AccessPointInfo, 16>;
#[derive(Debug, Error)]
pub enum WifiError {
#[error("WiFi controller error")]
Controller(#[from] esp_radio::wifi::WifiError),
}
pub struct Wifi {
controller: WifiController<'static>,
#[cfg_attr(not(feature = "wifi-sta"), allow(dead_code))]
interfaces: Interfaces<'static>,
}
impl Wifi {
pub fn new(wifi: WIFI<'static>) -> Result<Self, WifiError> {
let (controller, interfaces) = esp_radio::wifi::new(wifi, controller_config())?;
Ok(Self {
controller,
interfaces,
})
}
pub async fn scan(&mut self) -> Result<ScanList, WifiError> {
self.scan_with(&ScanConfig::default()).await
}
pub async fn scan_with(&mut self, config: &ScanConfig) -> Result<ScanList, WifiError> {
Ok(to_scan_list(self.controller.scan_async(config).await?))
}
}
fn controller_config() -> ControllerConfig {
let config = ControllerConfig::default();
#[cfg(feature = "fire27")]
let config = config.with_dynamic_rx_buf_num(16);
config
}
fn to_scan_list(aps: impl IntoIterator<Item = AccessPointInfo>) -> ScanList {
let mut out = ScanList::new();
for ap in aps.into_iter().take(out.capacity()) {
let _ = out.push(ap);
}
out
}
#[cfg(feature = "wifi-sta")]
pub use sta::*;
#[cfg(feature = "wifi-sta")]
mod sta {
use super::*;
use embassy_futures::{
join::join,
select::{Either, select},
};
use embassy_sync::{
blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal,
};
use embassy_time::{Duration, Timer};
use esp_radio::wifi::{Config, Interface, PowerSaveMode, sta::StationConfig};
pub struct StaCredentials<'a> {
pub ssid: &'a str,
pub password: &'a str,
pub auth: AuthenticationMethod,
}
pub enum IpSetup {
Dhcp,
Static(embassy_net::StaticConfigV4),
}
enum Command {
Scan(ScanConfig),
Connect,
Disconnect,
}
#[allow(clippy::large_enum_variant)]
enum Response {
Scan(Result<ScanList, WifiError>),
Unit(Result<(), WifiError>),
}
static CMD: Signal<CriticalSectionRawMutex, Command> = Signal::new();
static RESP: Signal<CriticalSectionRawMutex, Response> = Signal::new();
static CTRL_LOCK: Mutex<CriticalSectionRawMutex, ()> = Mutex::new(());
pub struct WifiControl {
_private: (),
}
pub struct WifiRunner {
controller: WifiController<'static>,
net: embassy_net::Runner<'static, Interface<'static>>,
desired_up: bool,
}
impl Wifi {
pub fn into_sta<const N: usize>(
self,
creds: StaCredentials<'_>,
ip: IpSetup,
seed: u64,
resources: &'static mut embassy_net::StackResources<N>,
) -> Result<(embassy_net::Stack<'static>, WifiControl, WifiRunner), WifiError> {
let Wifi {
mut controller,
interfaces,
} = self;
let station = StationConfig::default()
.with_ssid(creds.ssid)
.with_password(creds.password.into())
.with_auth_method(creds.auth);
controller.set_config(&Config::Station(station))?;
controller.set_power_saving(PowerSaveMode::None)?;
let net_config = match ip {
IpSetup::Dhcp => embassy_net::Config::dhcpv4(Default::default()),
IpSetup::Static(cfg) => embassy_net::Config::ipv4_static(cfg),
};
let (stack, net) = embassy_net::new(interfaces.station, net_config, resources, seed);
Ok((
stack,
WifiControl { _private: () },
WifiRunner {
controller,
net,
desired_up: true,
},
))
}
}
impl WifiControl {
pub async fn scan(&self) -> Result<ScanList, WifiError> {
self.scan_with(ScanConfig::default()).await
}
pub async fn scan_with(&self, config: ScanConfig) -> Result<ScanList, WifiError> {
let _guard = CTRL_LOCK.lock().await;
CMD.signal(Command::Scan(config));
match RESP.wait().await {
Response::Scan(result) => result,
Response::Unit(_) => unreachable!("scan command yields a scan response"),
}
}
pub async fn connect(&self) -> Result<(), WifiError> {
self.unit_command(Command::Connect).await
}
pub async fn disconnect(&self) -> Result<(), WifiError> {
self.unit_command(Command::Disconnect).await
}
async fn unit_command(&self, command: Command) -> Result<(), WifiError> {
let _guard = CTRL_LOCK.lock().await;
CMD.signal(command);
match RESP.wait().await {
Response::Unit(result) => result,
Response::Scan(_) => unreachable!("unit command yields a unit response"),
}
}
}
#[embassy_executor::task]
pub async fn wifi_task(runner: WifiRunner) {
let WifiRunner {
mut controller,
mut net,
mut desired_up,
} = runner;
join(net.run(), run_actor(&mut controller, &mut desired_up)).await;
}
async fn run_actor(controller: &mut WifiController<'static>, desired_up: &mut bool) {
loop {
if *desired_up && !controller.is_connected() {
let outcome = {
let connect = controller.connect_async();
select(connect, CMD.wait()).await
};
match outcome {
Either::First(Ok(_)) => {} Either::First(Err(error)) => {
warn!("WiFi connect failed: {:?}", error);
let backoff = select(Timer::after(Duration::from_secs(2)), CMD.wait()).await;
if let Either::Second(command) = backoff {
handle(controller, desired_up, command).await;
}
}
Either::Second(command) => handle(controller, desired_up, command).await,
}
} else if *desired_up {
let outcome = {
let disconnect = controller.wait_for_disconnect_async();
select(disconnect, CMD.wait()).await
};
if let Either::Second(command) = outcome {
handle(controller, desired_up, command).await;
}
} else {
let command = CMD.wait().await;
handle(controller, desired_up, command).await;
}
}
}
async fn handle(
controller: &mut WifiController<'static>,
desired_up: &mut bool,
command: Command,
) {
match command {
Command::Scan(config) => {
let result = controller
.scan_async(&config)
.await
.map(to_scan_list)
.map_err(WifiError::from);
RESP.signal(Response::Scan(result));
}
Command::Connect => {
*desired_up = true;
RESP.signal(Response::Unit(Ok(())));
}
Command::Disconnect => {
*desired_up = false;
let result = controller
.disconnect_async()
.await
.map(|_| ())
.map_err(WifiError::from);
RESP.signal(Response::Unit(result));
}
}
}
}