use core::sync::atomic::{AtomicI8, AtomicU8, Ordering};
use embassy_executor::{SpawnError, Spawner};
use embassy_futures::select::{Either, select};
use embassy_net::dns::DnsQueryType;
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_net::{Runner, Stack, StackResources};
use embassy_time::{Duration, Instant, Timer, with_timeout};
use esp_hal::peripherals::WIFI;
use esp_hal::time::Duration as HalDuration;
pub use esp_radio::wifi::ap::AccessPointInfo;
pub use esp_radio::wifi::scan::ScanConfig;
use esp_radio::wifi::scan::ScanTypeConfig;
use esp_radio::wifi::sta::StationConfig;
pub use esp_radio::wifi::{AuthenticationMethod, WifiController};
use esp_radio::wifi::{AuthenticationMethodConfig, Config, ControllerConfig, Interface, WifiError};
use oxstack_net::sntp;
pub use oxstack_net::sntp::ServerTime;
pub use oxstack_net::time::DateTime;
use static_cell::StaticCell;
pub use embassy_net;
pub const DEFAULT_NTP_SERVER: &str = "pool.ntp.org";
pub const SOCKETS: usize = 4;
#[derive(Clone, Copy, Debug)]
pub struct Credentials<'a> {
pub ssid: &'a str,
pub password: &'a str,
}
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error {
InvalidCredentials,
Wifi(WifiError),
Spawn,
}
impl From<SpawnError> for Error {
fn from(_: SpawnError) -> Self {
Error::Spawn
}
}
pub mod link {
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum State {
Idle = 0,
Connecting = 1,
Connected = 2,
Retrying = 3,
}
pub(super) static STATE: AtomicU8 = AtomicU8::new(State::Idle as u8);
pub(super) static RSSI: AtomicI8 = AtomicI8::new(i8::MIN);
pub fn state() -> State {
match STATE.load(Ordering::Relaxed) {
1 => State::Connecting,
2 => State::Connected,
3 => State::Retrying,
_ => State::Idle,
}
}
pub fn rssi() -> Option<i8> {
let r = RSSI.load(Ordering::Relaxed);
(state() == State::Connected && r != i8::MIN).then_some(r)
}
pub(super) fn set(s: State) {
STATE.store(s as u8, Ordering::Relaxed);
}
}
pub fn start_station(
spawner: Spawner,
wifi: WIFI<'static>,
creds: Credentials<'_>,
) -> Result<Stack<'static>, Error> {
let ssid = creds
.ssid
.try_into()
.map_err(|_| Error::InvalidCredentials)?;
let auth = if creds.password.is_empty() {
AuthenticationMethodConfig::Open
} else {
AuthenticationMethodConfig::Wpa2Personal(
creds
.password
.try_into()
.map_err(|_| Error::InvalidCredentials)?,
)
};
let station = Config::Station(
StationConfig::default()
.with_ssid(ssid)
.with_authentication(auth),
);
let interface = Interface::station();
let controller = WifiController::new(
wifi,
ControllerConfig::default().with_initial_config(station),
)
.map_err(Error::Wifi)?;
static RESOURCES: StaticCell<StackResources<{ SOCKETS + 2 }>> = StaticCell::new();
let rng = esp_hal::rng::Rng::new();
let seed = u64::from(rng.random()) << 32 | u64::from(rng.random());
let (stack, runner) = embassy_net::new(
interface,
embassy_net::Config::dhcpv4(Default::default()),
RESOURCES.init(StackResources::new()),
seed,
);
spawner.spawn(link_task(controller)?);
spawner.spawn(net_task(runner)?);
Ok(stack)
}
#[embassy_executor::task]
async fn link_task(mut controller: WifiController<'static>) {
loop {
link::set(link::State::Connecting);
if controller.connect_async().await.is_ok() {
link::set(link::State::Connected);
loop {
let rssi = controller
.rssi()
.ok()
.map_or(i8::MIN, |r| r.clamp(-127, 0) as i8);
link::RSSI.store(rssi, Ordering::Relaxed);
let dropped = select(
controller.wait_for_disconnect_async(),
Timer::after(Duration::from_secs(2)),
)
.await;
if let Either::First(_) = dropped {
break;
}
}
}
link::set(link::State::Retrying);
Timer::after(Duration::from_secs(5)).await;
}
}
#[embassy_executor::task]
async fn net_task(mut runner: Runner<'static, Interface>) {
runner.run().await
}
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TimeError {
Dns,
Socket,
Timeout,
Sntp(sntp::Error),
}
pub async fn query_time(stack: Stack<'static>, server: &str) -> Result<ServerTime, TimeError> {
let addrs = stack
.dns_query(server, DnsQueryType::A)
.await
.map_err(|_| TimeError::Dns)?;
let addr = *addrs.first().ok_or(TimeError::Dns)?;
let (mut rx_meta, mut tx_meta) = ([PacketMetadata::EMPTY; 2], [PacketMetadata::EMPTY; 2]);
let (mut rx_buf, mut tx_buf) = ([0u8; 128], [0u8; 128]);
let mut socket = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf);
socket.bind(0).map_err(|_| TimeError::Socket)?;
socket
.send_to(&sntp::request(), (addr, sntp::PORT))
.await
.map_err(|_| TimeError::Socket)?;
let mut reply = [0u8; sntp::PACKET_LEN];
loop {
let (n, meta) = with_timeout(Duration::from_secs(3), socket.recv_from(&mut reply))
.await
.map_err(|_| TimeError::Timeout)?
.map_err(|_| TimeError::Socket)?;
if meta.endpoint.addr == addr {
return sntp::parse_reply(&reply[..n]).map_err(TimeError::Sntp);
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct WallClock {
unix_us_at_boot: i64,
}
impl WallClock {
pub fn from_server_time(t: ServerTime) -> Self {
let unix_us = t.unix_seconds as i64 * 1_000_000 + i64::from(t.micros);
Self {
unix_us_at_boot: unix_us - Instant::now().as_micros() as i64,
}
}
pub fn unix_micros(&self) -> i64 {
self.unix_us_at_boot + Instant::now().as_micros() as i64
}
pub fn unix_seconds(&self) -> i64 {
self.unix_micros().div_euclid(1_000_000)
}
pub fn subsec_micros(&self) -> u32 {
self.unix_micros().rem_euclid(1_000_000) as u32
}
pub fn next_second(&self) -> Instant {
let to_go = 1_000_000 - u64::from(self.subsec_micros());
Instant::now() + Duration::from_micros(to_go)
}
pub fn now(&self, offset_minutes: i32) -> DateTime {
DateTime::from_unix(self.unix_seconds(), offset_minutes)
}
}
pub async fn sync_clock(stack: Stack<'static>, server: &str) -> Result<WallClock, TimeError> {
Ok(WallClock::from_server_time(
query_time(stack, server).await?,
))
}
pub fn scan_config() -> ScanConfig {
ScanConfig::default()
.with_max(32)
.with_show_hidden(true)
.with_scan_type(ScanTypeConfig::Active {
min: HalDuration::from_millis(30),
max: HalDuration::from_millis(120),
})
}
pub struct Scanner<'d> {
controller: WifiController<'d>,
}
pub fn scanner(wifi: WIFI<'_>) -> Result<Scanner<'_>, Error> {
let controller = WifiController::new(wifi, ControllerConfig::default()).map_err(Error::Wifi)?;
Ok(Scanner { controller })
}
impl<'d> Scanner<'d> {
pub async fn scan(&mut self) -> Result<alloc::vec::Vec<AccessPointInfo>, Error> {
self.scan_with(&scan_config()).await
}
pub async fn scan_with(
&mut self,
config: &ScanConfig,
) -> Result<alloc::vec::Vec<AccessPointInfo>, Error> {
let mut aps = self
.controller
.scan_async(config)
.await
.map_err(Error::Wifi)?;
aps.sort_unstable_by_key(|ap| core::cmp::Reverse(ap.signal_strength));
Ok(aps)
}
pub fn controller(&mut self) -> &mut WifiController<'d> {
&mut self.controller
}
}