#![allow(clippy::future_not_send, reason = "single-threaded")]
use core::convert::Infallible;
use core::{cell::RefCell, future::Future};
use cortex_m::peripheral::SCB;
use defmt::{info, warn};
use embassy_executor::Spawner;
use embassy_net::Ipv4Address;
use embassy_rp::{
Peri,
dma::ChannelInstance,
peripherals::{PIN_23, PIN_24, PIN_25, PIN_29},
};
use embassy_sync::blocking_mutex::{Mutex, raw::CriticalSectionRawMutex};
use embassy_time::{Duration, Instant, Timer, with_timeout};
use heapless::Vec;
use portable_atomic::{AtomicBool, Ordering};
use static_cell::StaticCell;
use crate::flash_block::FlashBlockRp;
use crate::{Error, Result};
use device_envoy_core::button::Button;
use device_envoy_core::wifi_auto::{WifiCredentials as InnerWifiCredentials, WifiStartMode};
mod dhcp;
mod dns;
pub mod fields;
mod portal;
mod stack;
use dns::dns_server_task;
use stack::WifiStatic as InnerWifiStatic;
pub use stack::WifiPio;
pub(crate) use stack::{Wifi, WifiEvent};
pub use device_envoy_core::wifi_auto::WifiAuto;
pub use device_envoy_core::wifi_auto::WifiAutoEvent;
pub use device_envoy_core::wifi_auto::WifiAutoField;
pub use device_envoy_core::wifi_auto::WifiStack;
const MAX_CONNECT_ATTEMPTS: u8 = 4;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(40);
const RETRY_BASE_DELAY: Duration = Duration::from_secs(3);
const RETRY_JITTER_MAX: Duration = Duration::from_millis(500);
const MAX_WIFI_AUTO_FIELDS: usize = 8;
pub(crate) struct WifiAutoStatic {
wifi: InnerWifiStatic,
wifi_auto_cell: StaticCell<WifiAutoInner>,
force_captive_portal: AtomicBool,
defaults: Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>>,
fields_storage: StaticCell<
Vec<&'static (dyn WifiAutoField<Error = crate::Error> + Sync), MAX_WIFI_AUTO_FIELDS>,
>,
}
pub struct WifiAutoRp {
wifi_auto: &'static WifiAutoInner,
}
struct WifiAutoInner {
wifi: &'static Wifi,
spawner: Spawner,
force_captive_portal: &'static AtomicBool,
defaults: &'static Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>>,
fields: &'static [&'static (dyn WifiAutoField<Error = crate::Error> + Sync)],
}
impl WifiAutoStatic {
#[must_use]
pub const fn new() -> Self {
WifiAutoStatic {
wifi: Wifi::new_static(),
wifi_auto_cell: StaticCell::new(),
force_captive_portal: AtomicBool::new(false),
defaults: Mutex::new(RefCell::new(None)),
fields_storage: StaticCell::new(),
}
}
fn force_captive_portal_flag(&'static self) -> &'static AtomicBool {
&self.force_captive_portal
}
fn defaults(
&'static self,
) -> &'static Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>> {
&self.defaults
}
}
impl WifiAutoRp {
#[allow(clippy::too_many_arguments)]
pub fn new<const N: usize, PIO: WifiPio, DMA: ChannelInstance, FlashBlockType>(
pin_23: Peri<'static, PIN_23>,
pin_24: Peri<'static, PIN_24>,
pin_25: Peri<'static, PIN_25>,
pin_29: Peri<'static, PIN_29>,
pio: Peri<'static, PIO>,
dma: Peri<'static, DMA>,
wifi_credentials_flash_block: FlashBlockType,
captive_portal_ssid: &'static str,
custom_fields: [&'static (dyn WifiAutoField<Error = crate::Error> + Sync); N],
spawner: Spawner,
) -> Result<Self>
where
FlashBlockType: crate::flash_block::FlashBlock<Error = Error> + Into<FlashBlockRp>,
crate::pio_irqs::DmaAllIrqs: embassy_rp::interrupt::typelevel::Binding<
DMA::Interrupt,
embassy_rp::dma::InterruptHandler<DMA>,
>,
{
let mut wifi_credentials_flash_block = wifi_credentials_flash_block.into();
static WIFI_AUTO_STATIC: WifiAutoStatic = WifiAutoInner::new_static();
let wifi_auto_static = &WIFI_AUTO_STATIC;
let stored_credentials = Wifi::peek_credentials(&mut wifi_credentials_flash_block);
let stored_start_mode = Wifi::peek_start_mode(&mut wifi_credentials_flash_block);
if matches!(stored_start_mode, WifiStartMode::CaptivePortal) {
if let Some(credentials) = stored_credentials.clone() {
wifi_auto_static.defaults.lock(|cell| {
*cell.borrow_mut() = Some(credentials);
});
}
}
let extras_ready = custom_fields
.iter()
.all(|field| field.is_satisfied().unwrap_or(false));
if !extras_ready {
if let Some(credentials) = stored_credentials.clone() {
wifi_auto_static.defaults.lock(|cell| {
*cell.borrow_mut() = Some(credentials);
});
}
Wifi::prepare_start_mode(
&mut wifi_credentials_flash_block,
WifiStartMode::CaptivePortal,
)
.map_err(|_| Error::StorageCorrupted)?;
}
let wifi = Wifi::new_with_captive_portal_ssid(
&wifi_auto_static.wifi,
pin_23,
pin_24,
pin_25,
pin_29,
pio,
dma,
wifi_credentials_flash_block,
captive_portal_ssid,
spawner,
);
let fields_ref: &'static [&'static (dyn WifiAutoField<Error = crate::Error> + Sync)] =
if N > 0 {
assert!(
N <= MAX_WIFI_AUTO_FIELDS,
"WifiAutoRp supports at most {} custom fields",
MAX_WIFI_AUTO_FIELDS
);
let mut storage: Vec<
&'static (dyn WifiAutoField<Error = crate::Error> + Sync),
MAX_WIFI_AUTO_FIELDS,
> = Vec::new();
for field in custom_fields {
storage.push(field).unwrap_or_else(|_| unreachable!());
}
let stored_vec = wifi_auto_static.fields_storage.init(storage);
stored_vec.as_slice()
} else {
&[]
};
let instance = wifi_auto_static.wifi_auto_cell.init(WifiAutoInner {
wifi,
spawner,
force_captive_portal: wifi_auto_static.force_captive_portal_flag(),
defaults: wifi_auto_static.defaults(),
fields: fields_ref,
});
Ok(Self {
wifi_auto: instance,
})
}
pub async fn connect<OnEvent, OnEventFuture>(
self,
button: &mut impl Button,
on_event: OnEvent,
) -> Result<WifiStack>
where
OnEvent: FnMut(WifiAutoEvent) -> OnEventFuture,
OnEventFuture: Future<Output = Result<()>>,
{
<Self as WifiAuto>::connect(self, button, on_event).await
}
}
impl device_envoy_core::wifi_auto::WifiAuto for WifiAutoRp {
type Error = Error;
async fn connect<OnEvent, OnEventFuture>(
self,
button: &mut impl Button,
on_event: OnEvent,
) -> Result<WifiStack>
where
OnEvent: FnMut(WifiAutoEvent) -> OnEventFuture,
OnEventFuture: Future<Output = Result<()>>,
{
let button_reset_stabilize_cycles: u32 = 300_000;
cortex_m::asm::delay(button_reset_stabilize_cycles);
if button.is_pressed() {
if self.wifi_auto.wifi.current_start_mode() != WifiStartMode::CaptivePortal {
info!("WifiAutoRp: force-captive-portal requested via button");
self.wifi_auto
.wifi
.set_start_mode(WifiStartMode::CaptivePortal)
.map_err(|_| Error::StorageCorrupted)?;
info!("WifiAutoRp: rebooting now to apply CaptivePortal startup mode");
SCB::sys_reset();
} else {
info!("WifiAutoRp: force request ignored (already in CaptivePortal mode)");
self.wifi_auto.force_captive_portal();
}
}
self.wifi_auto.connect(on_event).await
}
}
impl WifiAutoInner {
#[must_use]
const fn new_static() -> WifiAutoStatic {
WifiAutoStatic::new()
}
fn force_captive_portal(&self) {
self.force_captive_portal.store(true, Ordering::Relaxed);
}
fn extra_fields_ready(&self) -> Result<bool> {
for field in self.fields {
let satisfied = field.is_satisfied().map_err(|_| Error::StorageCorrupted)?;
if !satisfied {
info!("WifiAutoRp: custom field not satisfied, forcing captive portal");
return Ok(false);
}
}
info!(
"WifiAutoRp: all {} custom fields satisfied",
self.fields.len()
);
Ok(true)
}
device_envoy_core::__impl_wifi_auto_connect! {
fn connect(&self as wifi_auto_inner, on_event) -> Result<WifiStack> {
wifi_auto_inner.ensure_connected_with(&mut on_event).await?;
Ok(wifi_auto_inner.wifi.wait_for_stack().await)
}
}
async fn ensure_connected_with<Fut, F>(&self, on_event: &mut F) -> Result<()>
where
F: FnMut(WifiAutoEvent) -> Fut,
Fut: Future<Output = Result<()>>,
{
loop {
let force_captive_portal = self.force_captive_portal.swap(false, Ordering::AcqRel);
let start_mode = self.wifi.current_start_mode();
let persisted_wifi_credentials = self.wifi.load_persisted_credentials();
let has_credentials = persisted_wifi_credentials.is_some();
let extras_ready = self.extra_fields_ready()?;
let enter_captive_portal = device_envoy_core::wifi_auto::should_enter_captive_portal(
start_mode,
force_captive_portal,
has_credentials,
extras_ready,
);
info!(
"WifiAutoRp: force={} has_credentials={} extras_ready={} enter_captive_portal={}",
force_captive_portal, has_credentials, extras_ready, enter_captive_portal
);
struct RpWifiAutoBackend<'a> {
wifi_auto_inner: &'a WifiAutoInner,
force_captive_portal: bool,
}
impl device_envoy_core::wifi_auto::WifiAutoBackend for RpWifiAutoBackend<'_> {
type Error = Error;
fn force_captive_portal(&self) -> bool {
self.force_captive_portal
}
fn try_count(&self) -> u8 {
MAX_CONNECT_ATTEMPTS
}
fn load_start_mode(&self) -> Result<WifiStartMode> {
Ok(self.wifi_auto_inner.wifi.current_start_mode())
}
fn custom_fields_satisfied(&self) -> Result<bool> {
self.wifi_auto_inner.extra_fields_ready()
}
fn load_persisted_credentials(&self) -> Result<Option<InnerWifiCredentials>> {
Ok(self.wifi_auto_inner.wifi.load_persisted_credentials())
}
fn persist_credentials(
&self,
wifi_credentials: &InnerWifiCredentials,
) -> Result<()> {
self.wifi_auto_inner
.wifi
.persist_credentials(wifi_credentials)
.map_err(|_| Error::StorageCorrupted)
}
fn set_start_mode(&self, wifi_start_mode: WifiStartMode) -> Result<()> {
self.wifi_auto_inner
.wifi
.set_start_mode(wifi_start_mode)
.map_err(|_| Error::StorageCorrupted)
}
fn on_connect_attempt(
&mut self,
try_index: u8,
) -> impl Future<Output = Result<bool>> + '_ {
async move {
let attempt = try_index + 1;
info!(
"WifiAutoRp: connection attempt {}/{}",
attempt, MAX_CONNECT_ATTEMPTS
);
if self
.wifi_auto_inner
.wait_for_client_ready_with_timeout(CONNECT_TIMEOUT)
.await
{
return Ok(true);
}
warn!("WifiAutoRp: connection attempt {} timed out", attempt);
let retry_delay = retry_delay_with_jitter(try_index);
info!(
"WifiAutoRp: retrying after {} ms (attempt {})",
retry_delay.as_millis(),
attempt
);
Timer::after(retry_delay).await;
Ok(false)
}
}
fn run_captive_portal(
&mut self,
) -> impl Future<Output = Result<InnerWifiCredentials>> + '_ {
async move {
match self.wifi_auto_inner.run_captive_portal().await {
Ok(infallible) => match infallible {},
Err(error) => Err(error),
}
}
}
fn on_resolved_credentials(
&mut self,
_wifi_credentials: &InnerWifiCredentials,
) -> impl Future<Output = Result<()>> + '_ {
async { Ok(()) }
}
}
let mut wifi_auto_backend = RpWifiAutoBackend {
wifi_auto_inner: self,
force_captive_portal,
};
let connected = device_envoy_core::wifi_auto::connect_with_backend(
&mut wifi_auto_backend,
on_event,
)
.await?;
if connected {
return Ok(());
}
info!(
"WifiAutoRp: failed to connect after {} attempts, returning to captive portal",
MAX_CONNECT_ATTEMPTS
);
if let Some(credentials) = self.wifi.load_persisted_credentials() {
self.defaults.lock(|cell| {
*cell.borrow_mut() = Some(credentials);
});
}
info!("WifiAutoRp: writing CaptivePortal mode to flash");
self.wifi
.set_start_mode(WifiStartMode::CaptivePortal)
.map_err(|_| Error::StorageCorrupted)?;
info!("WifiAutoRp: flash write complete, waiting 1 second before reset");
Timer::after_secs(1).await;
info!("WifiAutoRp: resetting device now");
SCB::sys_reset();
}
}
async fn wait_for_client_ready_with_timeout(&self, timeout: Duration) -> bool {
with_timeout(timeout, async {
loop {
match self.wifi.wait_for_wifi_event().await {
WifiEvent::ClientReady => break,
WifiEvent::CaptivePortalReady => {
info!(
"WifiAutoRp: received captive-portal-ready event while waiting for client mode"
);
}
}
}
})
.await
.is_ok()
}
#[allow(unreachable_code)]
async fn run_captive_portal(&self) -> Result<Infallible> {
self.wifi.wait_for_wifi_event().await;
let stack = self.wifi.wait_for_stack().await;
let captive_portal_ip = Ipv4Address::new(192, 168, 4, 1);
match dns_server_task(stack, captive_portal_ip) {
Ok(dns_server_token) => self.spawner.spawn(dns_server_token),
Err(err) => {
info!("WifiAutoRp: DNS server task spawn failed: {:?}", err);
}
}
let defaults_owned = self
.defaults
.lock(|cell| cell.borrow_mut().take())
.or_else(|| self.wifi.load_persisted_credentials());
let submission =
portal::collect_credentials(stack, self.spawner, defaults_owned.as_ref(), self.fields)
.await?;
self.wifi.persist_credentials(&submission).map_err(|err| {
warn!("{}", err);
Error::StorageCorrupted
})?;
Timer::after_millis(750).await;
SCB::sys_reset();
loop {
cortex_m::asm::nop();
}
}
}
fn retry_delay_with_jitter(attempt_index: u8) -> Duration {
let base_ms = RETRY_BASE_DELAY.as_millis();
assert!(base_ms > 0, "RETRY_BASE_DELAY must be positive");
let jitter_max_ms = RETRY_JITTER_MAX.as_millis();
let multiplier = 1u64
.checked_shl(u32::from(attempt_index))
.expect("attempt_index must fit in shift");
let delay_ms = base_ms
.checked_mul(multiplier)
.expect("retry delay must fit in millis");
let jitter_ms = if jitter_max_ms == 0 {
0
} else {
Instant::now().as_millis() % (jitter_max_ms + 1)
};
let total_ms = delay_ms
.checked_add(jitter_ms)
.expect("retry delay with jitter must fit in millis");
Duration::from_millis(total_ms)
}