extern crate alloc;
use alloc::{string::String as AllocString, vec::Vec as AllocVec};
use embassy_futures::block_on;
use esp_hal::{interrupt::software::SoftwareInterruptControl, timer::timg::TimerGroup};
use esp_radio::wifi::{
AuthenticationMethod, Config, WifiController, ap::AccessPointInfo, scan::ScanConfig,
sta::StationConfig,
};
use heapless::String;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuthMethod {
Open,
Wpa,
Wpa2,
Wpa3,
Unknown,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccessPoint {
pub ssid: String<32>,
pub rssi_dbm: i8,
pub channel: u8,
pub auth: AuthMethod,
}
pub type AccessPoints = AllocVec<AccessPoint>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Credentials {
pub ssid: String<32>,
pub password: String<64>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiState {
Stopped,
Scanning,
Connecting,
Connected,
}
pub trait WifiStation {
type Error;
fn scan_async(
&mut self,
) -> impl core::future::Future<Output = Result<AccessPoints, Self::Error>> + '_;
fn connect_async<'a>(
&'a mut self,
credentials: &'a Credentials,
) -> impl core::future::Future<Output = Result<(), Self::Error>> + 'a;
fn disconnect_async(
&mut self,
) -> impl core::future::Future<Output = Result<(), Self::Error>> + '_;
fn state(&self) -> WifiState;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EspRadioWifiError {
Init,
ResourcesUnavailable,
NotStarted,
Configure,
Scan,
Connect,
Disconnect,
}
pub struct RadioResources {
pub wifi: esp_hal::peripherals::WIFI<'static>,
pub timer_group0: esp_hal::peripherals::TIMG0<'static>,
pub software_interrupt: esp_hal::peripherals::SW_INTERRUPT<'static>,
}
pub struct EspRadioWifi {
state: WifiState,
resources: Option<RadioResources>,
controller: Option<WifiController<'static>>,
}
impl EspRadioWifi {
#[must_use]
pub const fn new(resources: RadioResources) -> Self {
Self {
state: WifiState::Stopped,
resources: Some(resources),
controller: None,
}
}
pub fn start(&mut self) -> Result<(), EspRadioWifiError> {
if self.controller.is_some() {
return Ok(());
}
let resources = self
.resources
.take()
.ok_or(EspRadioWifiError::ResourcesUnavailable)?;
let timg0 = TimerGroup::new(resources.timer_group0);
let sw_interrupt = SoftwareInterruptControl::new(resources.software_interrupt);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
let (controller, _interfaces) = esp_radio::wifi::new(resources.wifi, Default::default())
.map_err(|_| EspRadioWifiError::Init)?;
self.controller = Some(controller);
self.state = WifiState::Stopped;
Ok(())
}
pub fn scan(&mut self) -> Result<AccessPoints, EspRadioWifiError> {
block_on(self.scan_async())
}
pub async fn scan_async(&mut self) -> Result<AccessPoints, EspRadioWifiError> {
self.start()?;
let controller = self
.controller
.as_mut()
.ok_or(EspRadioWifiError::NotStarted)?;
self.state = WifiState::Scanning;
let scan_config = ScanConfig::default().with_max(16);
let aps = controller.scan_async(&scan_config).await.map_err(|_| {
self.state = WifiState::Stopped;
EspRadioWifiError::Scan
})?;
self.state = WifiState::Stopped;
Ok(convert_access_points(&aps))
}
pub fn connect(&mut self, credentials: &Credentials) -> Result<(), EspRadioWifiError> {
block_on(self.connect_async(credentials))
}
pub async fn connect_async(
&mut self,
credentials: &Credentials,
) -> Result<(), EspRadioWifiError> {
self.start()?;
let controller = self
.controller
.as_mut()
.ok_or(EspRadioWifiError::NotStarted)?;
let config = Config::Station(station_config(credentials));
controller
.set_config(&config)
.map_err(|_| EspRadioWifiError::Configure)?;
self.state = WifiState::Connecting;
controller.connect_async().await.map_err(|_| {
self.state = WifiState::Stopped;
EspRadioWifiError::Connect
})?;
self.state = WifiState::Connected;
Ok(())
}
pub fn disconnect(&mut self) -> Result<(), EspRadioWifiError> {
block_on(self.disconnect_async())
}
pub async fn disconnect_async(&mut self) -> Result<(), EspRadioWifiError> {
self.start()?;
let controller = self
.controller
.as_mut()
.ok_or(EspRadioWifiError::NotStarted)?;
if controller.is_connected() {
self.state = WifiState::Connecting;
controller
.disconnect_async()
.await
.map_err(|_| EspRadioWifiError::Disconnect)?;
}
self.state = WifiState::Stopped;
Ok(())
}
#[must_use]
pub const fn state(&self) -> WifiState {
self.state
}
}
impl WifiStation for EspRadioWifi {
type Error = EspRadioWifiError;
fn scan_async(
&mut self,
) -> impl core::future::Future<Output = Result<AccessPoints, Self::Error>> + '_ {
Self::scan_async(self)
}
fn connect_async<'a>(
&'a mut self,
credentials: &'a Credentials,
) -> impl core::future::Future<Output = Result<(), Self::Error>> + 'a {
Self::connect_async(self, credentials)
}
fn disconnect_async(
&mut self,
) -> impl core::future::Future<Output = Result<(), Self::Error>> + '_ {
Self::disconnect_async(self)
}
fn state(&self) -> WifiState {
self.state()
}
}
fn station_config(credentials: &Credentials) -> StationConfig {
let mut config = StationConfig::default()
.with_ssid(credentials.ssid.as_str())
.with_password(AllocString::from(credentials.password.as_str()));
if credentials.password.is_empty() {
config = config.with_auth_method(AuthenticationMethod::None);
}
config
}
fn convert_access_points(aps: &[AccessPointInfo]) -> AccessPoints {
let mut out = AllocVec::new();
for ap in aps {
let mut ssid = String::<32>::new();
let _ = ssid.push_str(ap.ssid.as_str());
out.push(AccessPoint {
ssid,
rssi_dbm: ap.signal_strength,
channel: ap.channel,
auth: ap.auth_method.map_or(AuthMethod::Unknown, convert_auth),
});
}
out
}
fn convert_auth(auth: AuthenticationMethod) -> AuthMethod {
match auth {
AuthenticationMethod::None => AuthMethod::Open,
AuthenticationMethod::Wpa => AuthMethod::Wpa,
AuthenticationMethod::Wpa2Personal
| AuthenticationMethod::WpaWpa2Personal
| AuthenticationMethod::Wpa2Enterprise => AuthMethod::Wpa2,
AuthenticationMethod::Wpa3Personal
| AuthenticationMethod::Wpa2Wpa3Personal
| AuthenticationMethod::Wpa3EntSuiteB192Bit
| AuthenticationMethod::Wpa3ExtPsk
| AuthenticationMethod::Wpa3ExtPskMixed
| AuthenticationMethod::Wpa3Enterprise
| AuthenticationMethod::Wpa2Wpa3Enterprise => AuthMethod::Wpa3,
_ => AuthMethod::Unknown,
}
}