extern crate alloc;
use alloc::{string::String as AllocString, vec::Vec as AllocVec};
use crate::{bsp::RadioRuntimeResources, runtime};
use embassy_futures::block_on;
use esp_radio::wifi::{
AuthenticationMethod, Config, WifiController, ap::AccessPointInfo, scan::ScanConfig,
sta::StationConfig,
};
use heapless::String;
pub type StationInterface = esp_radio::wifi::Interface<'static>;
pub type NetworkInterfaces = esp_radio::wifi::Interfaces<'static>;
#[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>,
}
impl Credentials {
pub fn new(ssid: &str, password: &str) -> Result<Self, CredentialsError> {
let mut stored_ssid = String::new();
let mut stored_password = String::new();
stored_ssid
.push_str(ssid)
.map_err(|_| CredentialsError::SsidTooLong)?;
stored_password
.push_str(password)
.map_err(|_| CredentialsError::PasswordTooLong)?;
Ok(Self {
ssid: stored_ssid,
password: stored_password,
})
}
pub fn open(ssid: &str) -> Result<Self, CredentialsError> {
Self::new(ssid, "")
}
#[must_use]
pub fn is_open(&self) -> bool {
self.password.is_empty()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CredentialsError {
SsidTooLong,
PasswordTooLong,
}
#[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;
fn is_connected(&self) -> bool;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EspRadioWifiError {
Init,
ResourcesUnavailable,
NotStarted,
Configure,
Scan,
Connect,
Disconnect,
InterfacesTaken,
}
pub struct RadioResources {
pub wifi: esp_hal::peripherals::WIFI<'static>,
}
pub struct EspRadioWifi {
state: WifiState,
resources: Option<RadioResources>,
runtime: Option<RadioRuntimeResources>,
runtime_started: bool,
controller: Option<WifiController<'static>>,
interfaces: Option<NetworkInterfaces>,
}
impl EspRadioWifi {
#[must_use]
pub const fn new(resources: RadioResources, runtime: RadioRuntimeResources) -> Self {
Self {
state: WifiState::Stopped,
resources: Some(resources),
runtime: Some(runtime),
runtime_started: false,
controller: None,
interfaces: None,
}
}
#[must_use]
pub const fn new_started(resources: RadioResources) -> Self {
Self {
state: WifiState::Stopped,
resources: Some(resources),
runtime: None,
runtime_started: true,
controller: None,
interfaces: None,
}
}
pub fn start(&mut self) -> Result<(), EspRadioWifiError> {
if self.controller.is_some() {
return Ok(());
}
let resources = self
.resources
.take()
.ok_or(EspRadioWifiError::ResourcesUnavailable)?;
if !self.runtime_started {
let runtime = self
.runtime
.take()
.ok_or(EspRadioWifiError::ResourcesUnavailable)?;
runtime::start_radio_runtime(runtime);
self.runtime_started = true;
}
let (controller, interfaces) = esp_radio::wifi::new(resources.wifi, Default::default())
.map_err(|_| EspRadioWifiError::Init)?;
self.controller = Some(controller);
self.interfaces = Some(interfaces);
self.state = WifiState::Stopped;
Ok(())
}
pub fn take_interfaces(&mut self) -> Result<NetworkInterfaces, EspRadioWifiError> {
self.start()?;
self.interfaces
.take()
.ok_or(EspRadioWifiError::InterfacesTaken)
}
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 async fn ensure_connected_async(
&mut self,
credentials: &Credentials,
) -> Result<(), EspRadioWifiError> {
if self.is_connected() {
return Ok(());
}
self.connect_async(credentials).await
}
pub fn ensure_connected(&mut self, credentials: &Credentials) -> Result<(), EspRadioWifiError> {
block_on(self.ensure_connected_async(credentials))
}
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
}
#[must_use]
pub fn is_connected(&self) -> bool {
self.controller
.as_ref()
.is_some_and(WifiController::is_connected)
}
}
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 is_connected(&self) -> bool {
self.is_connected()
}
}
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.is_open() {
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,
}
}