device_envoy/wifi_auto.rs
1//! A device abstraction that connects a Pico with WiFi to the Internet and, when needed,
2//! creates a temporary WiFi network to enter credentials.
3//!
4//! See [`WifiAuto`] for the main struct and usage examples.
5
6#![allow(clippy::future_not_send, reason = "single-threaded")]
7
8use core::{cell::RefCell, convert::Infallible, future::Future};
9use cortex_m::peripheral::SCB;
10use defmt::{info, warn};
11use embassy_executor::Spawner;
12use embassy_net::{Ipv4Address, Stack};
13use embassy_rp::{
14 Peri,
15 dma::Channel,
16 gpio::Pin,
17 peripherals::{PIN_23, PIN_24, PIN_25, PIN_29},
18};
19use embassy_sync::{
20 blocking_mutex::{Mutex, raw::CriticalSectionRawMutex},
21 signal::Signal,
22};
23use embassy_time::{Duration, Instant, Timer, with_timeout};
24use heapless::Vec;
25use portable_atomic::{AtomicBool, Ordering};
26use static_cell::StaticCell;
27
28use crate::button::{Button, PressedTo};
29use crate::flash_array::FlashBlock;
30use crate::{Error, Result};
31
32mod credentials;
33mod dhcp;
34mod dns;
35pub mod fields;
36mod portal;
37mod stack;
38
39use credentials::WifiCredentials as InnerWifiCredentials;
40use dns::dns_server_task;
41use stack::{WifiStartMode, WifiStatic as InnerWifiStatic};
42
43pub use stack::WifiPio;
44pub(crate) use stack::{Wifi, WifiEvent};
45
46pub use portal::WifiAutoField;
47
48/// Events emitted while connecting. See [`WifiAuto::connect`](crate::wifi_auto::WifiAuto::connect)
49/// for usage examples.
50#[derive(Clone, Copy, Debug, defmt::Format)]
51pub enum WifiAutoEvent {
52 /// Captive portal is ready and waiting for user configuration.
53 CaptivePortalReady,
54 /// Attempting to connect to WiFi network.
55 Connecting {
56 /// Current attempt number (0-based).
57 try_index: u8,
58 /// Total number of attempts that will be made.
59 try_count: u8,
60 },
61 /// Connection failed after all attempts, device will reset.
62 ConnectionFailed,
63}
64
65const MAX_CONNECT_ATTEMPTS: u8 = 4;
66const CONNECT_TIMEOUT: Duration = Duration::from_secs(40);
67const RETRY_BASE_DELAY: Duration = Duration::from_secs(3);
68const RETRY_JITTER_MAX: Duration = Duration::from_millis(500);
69
70pub(crate) type WifiAutoEvents = Signal<CriticalSectionRawMutex, WifiAutoEvent>;
71
72const MAX_WIFI_AUTO_FIELDS: usize = 8;
73
74/// Static for [`WifiAuto`]. See [`WifiAuto`] for usage example.
75pub(crate) struct WifiAutoStatic {
76 events: WifiAutoEvents,
77 wifi: InnerWifiStatic,
78 wifi_auto_cell: StaticCell<WifiAutoInner>,
79 force_captive_portal: AtomicBool,
80 defaults: Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>>,
81 button: Mutex<CriticalSectionRawMutex, RefCell<Option<Button<'static>>>>,
82 fields_storage: StaticCell<Vec<&'static dyn WifiAutoField, MAX_WIFI_AUTO_FIELDS>>,
83}
84/// A device abstraction that connects a Pico with WiFi to the Internet and, when needed,
85/// creates a temporary WiFi network to enter credentials.
86///
87/// `WifiAuto` handles WiFi connections end-to-end. It normally connects using
88/// a saved WiFi network name (SSID) and password. If those values are missing
89/// or invalid, it temporarily creates its own WiFi network (a “captive
90/// portal”) and hosts a web form where the user can enter the local WiFi
91/// ssid and password.
92///
93/// `WifiAuto` works on the Pico 1 W and Pico 2 W, which include the CYW43 WiFi chip.
94///
95/// The typical usage pattern is:
96///
97/// 0. Ensure your hardware includes a button wired to a GPIO. The button can be used during boot to force captive-portal mode.
98/// 1. Construct a [`FlashArray`](crate::flash_array::FlashArray) to store WiFi credentials.
99/// 2. Use [`WifiAuto::new`] to construct a `WifiAuto`.
100/// 3. Use [`WifiAuto::connect`] to connect to WiFi while optionally showing status.
101///
102/// The [`WifiAuto::connect`] method returns a network stack and the button, and it consumes
103/// the `WifiAuto`. See its documentation for examples and details.
104///
105/// Let’s look at an example. Following the example, we’ll explain the details.
106/// (For additional examples, see the [wifi_auto::fields module example](crate::wifi_auto::fields)
107/// and the [`WifiAuto::connect`] docs.)
108///
109/// ## Example: Connect with logging
110///
111/// This example connects to WiFi and logs progress.
112///
113/// ```rust,no_run
114/// # #![no_std]
115/// # #![no_main]
116/// # use panic_probe as _;
117/// use device_envoy::{
118/// Result,
119/// button::PressedTo,
120/// flash_array::FlashArray,
121/// wifi_auto::{WifiAuto, WifiAutoEvent},
122/// };
123/// use embassy_time::Duration;
124///
125/// async fn connect_wifi(
126/// spawner: embassy_executor::Spawner,
127/// p: embassy_rp::Peripherals,
128/// ) -> Result<()> {
129/// // Set up flash storage for WiFi credentials
130/// let [wifi_flash] = FlashArray::<1>::new(p.FLASH)?;
131///
132/// // Construct WifiAuto
133/// let wifi_auto = WifiAuto::new(
134/// p.PIN_23, // CYW43 power
135/// p.PIN_24, // CYW43 clock
136/// p.PIN_25, // CYW43 chip select
137/// p.PIN_29, // CYW43 data
138/// p.PIO0, // WiFi PIO
139/// p.DMA_CH0, // WiFi DMA
140/// wifi_flash,
141/// p.PIN_13, // Button for reconfiguration
142/// PressedTo::Ground,
143/// "PicoAccess", // Captive-portal SSID
144/// [], // Any extra fields
145/// spawner,
146/// )?;
147///
148/// // Connect (logging status as we go)
149/// let (stack, _button) = wifi_auto
150/// .connect(|event| async move {
151/// match event {
152/// WifiAutoEvent::CaptivePortalReady =>
153/// defmt::info!("Captive portal ready"),
154/// WifiAutoEvent::Connecting { .. } =>
155/// defmt::info!("Connecting to WiFi"),
156/// WifiAutoEvent::ConnectionFailed =>
157/// defmt::info!("WiFi connection failed"),
158/// }
159/// Ok(())
160/// })
161/// .await?;
162///
163/// defmt::info!("WiFi connected");
164///
165/// loop {
166/// if let Ok(addresses) = stack.dns_query("google.com", embassy_net::dns::DnsQueryType::A).await {
167/// defmt::info!("google.com: {:?}", addresses);
168/// } else {
169/// defmt::info!("google.com: lookup failed");
170/// }
171/// embassy_time::Timer::after(Duration::from_secs(15)).await;
172/// }
173/// }
174/// ```
175///
176/// ## What happens during connection
177///
178/// While `connect` is running:
179///
180/// - The WiFi chip may reset as it switches between normal WiFi operation and
181/// hosting its own temporary WiFi network.
182/// - Your code should tolerate these resets.
183/// Initializing LEDs or displays before WiFi is fine; just be aware they may be
184/// momentarily disrupted during mode changes.
185///
186/// ## WiFi limitations
187///
188/// - Only standard SSID/password 2.4 Ghz WiFi networks are supported.
189///
190/// ## Performance and code size
191///
192/// You may choose any PIO instance and any DMA channel for WiFi.
193/// With **Thin LTO enabled**, this flexibility should have no impact on
194/// code size.
195///
196/// Recommended release profile:
197///
198/// ```toml
199/// [profile.release]
200/// # debug = 2 # uncomment for better backtraces, at the cost of code size
201/// lto = "thin"
202/// codegen-units = 1
203/// panic = "abort"
204/// ```
205///
206/// (Your application could also enable linker garbage collection (`--gc-sections`)
207/// for embedded targets. We enable it in our `rustflags`, but in recent builds
208/// it had no measurable effect on size. See the
209/// [rustc linker argument docs](https://doc.rust-lang.org/rustc/codegen-options/index.html#link-arg)
210/// and the
211/// [Cargo rustflags docs](https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags).)
212///
213/// ## Hardware model
214///
215/// On the Pico W, the CYW43 WiFi chip is wired to fixed GPIOs. You must
216/// also provide a PIO instance and a DMA channel for the WiFi driver.
217///
218/// These are supplied explicitly to [`WifiAuto::new`]. The chosen PIO/DMA
219/// pair cannot be shared with other uses; the compiler enforces this.
220pub struct WifiAuto {
221 wifi_auto: &'static WifiAutoInner,
222}
223
224struct WifiAutoInner {
225 events: &'static WifiAutoEvents,
226 wifi: &'static Wifi,
227 spawner: Spawner,
228 force_captive_portal: &'static AtomicBool,
229 defaults: &'static Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>>,
230 button: &'static Mutex<CriticalSectionRawMutex, RefCell<Option<Button<'static>>>>,
231 fields: &'static [&'static dyn WifiAutoField],
232}
233
234impl WifiAutoStatic {
235 #[must_use]
236 pub const fn new() -> Self {
237 WifiAutoStatic {
238 events: Signal::new(),
239 wifi: Wifi::new_static(),
240 wifi_auto_cell: StaticCell::new(),
241 force_captive_portal: AtomicBool::new(false),
242 defaults: Mutex::new(RefCell::new(None)),
243 button: Mutex::new(RefCell::new(None)),
244 fields_storage: StaticCell::new(),
245 }
246 }
247
248 fn force_captive_portal_flag(&'static self) -> &'static AtomicBool {
249 &self.force_captive_portal
250 }
251
252 fn defaults(
253 &'static self,
254 ) -> &'static Mutex<CriticalSectionRawMutex, RefCell<Option<InnerWifiCredentials>>> {
255 &self.defaults
256 }
257
258 fn button(
259 &'static self,
260 ) -> &'static Mutex<CriticalSectionRawMutex, RefCell<Option<Button<'static>>>> {
261 &self.button
262 }
263}
264
265impl WifiAuto {
266 /// Initialize WiFi auto-provisioning with custom configuration fields.
267 ///
268 /// # Parameters
269 ///
270 /// - `pin_23`, `pin_24`, `pin_25`, `pin_29`: the internal GPIO pins for the CYW43 WiFi chip.
271 /// - `pio`: PIO resource used for WiFi.
272 /// - `dma`: DMA resource for WiFi.
273 /// - `wifi_credentials_flash_block`: [`FlashBlock`] reserved
274 /// for WiFi credentials.
275 /// - `button_pin`: Button pin used to force setup mode on boot.
276 /// - `button_pressed_to`: Wiring for the button (ground or VCC).
277 /// - `captive_portal_ssid`: SSID shown when the device starts setup mode.
278 /// - `custom_fields`: Extra fields collected in the setup page. See the
279 /// [wifi_auto::fields module example](crate::wifi_auto::fields) for usage.
280 /// - `spawner`: Embassy task spawner for background work.
281 ///
282 /// See the [WifiAuto struct example](Self) for a complete example.
283 #[allow(clippy::too_many_arguments)]
284 pub fn new<const N: usize, PIO: WifiPio, DMA: Channel>(
285 pin_23: Peri<'static, PIN_23>,
286 pin_24: Peri<'static, PIN_24>,
287 pin_25: Peri<'static, PIN_25>,
288 pin_29: Peri<'static, PIN_29>,
289 pio: Peri<'static, PIO>,
290 dma: Peri<'static, DMA>,
291 mut wifi_credentials_flash_block: FlashBlock,
292 button_pin: Peri<'static, impl Pin>,
293 button_pressed_to: PressedTo,
294 captive_portal_ssid: &'static str,
295 custom_fields: [&'static dyn WifiAutoField; N],
296 spawner: Spawner,
297 ) -> Result<Self> {
298 static WIFI_AUTO_STATIC: WifiAutoStatic = WifiAutoInner::new_static();
299 let wifi_auto_static = &WIFI_AUTO_STATIC;
300
301 let stored_credentials = Wifi::peek_credentials(&mut wifi_credentials_flash_block);
302 let stored_start_mode = Wifi::peek_start_mode(&mut wifi_credentials_flash_block);
303 if matches!(stored_start_mode, WifiStartMode::CaptivePortal) {
304 if let Some(creds) = stored_credentials.clone() {
305 wifi_auto_static.defaults.lock(|cell| {
306 *cell.borrow_mut() = Some(creds);
307 });
308 }
309 }
310
311 // Allow the pull-up to stabilize after reset before sampling the button.
312 let button = Button::new(button_pin, button_pressed_to);
313 let button_reset_stabilize_cycles: u32 = 300_000;
314 cortex_m::asm::delay(button_reset_stabilize_cycles);
315 let force_captive_portal = button.is_pressed();
316
317 // Check if custom fields are satisfied
318 let extras_ready = custom_fields
319 .iter()
320 .all(|field| field.is_satisfied().unwrap_or(false));
321
322 if force_captive_portal || !extras_ready {
323 if let Some(creds) = stored_credentials.clone() {
324 wifi_auto_static.defaults.lock(|cell| {
325 *cell.borrow_mut() = Some(creds);
326 });
327 }
328 Wifi::prepare_start_mode(
329 &mut wifi_credentials_flash_block,
330 WifiStartMode::CaptivePortal,
331 )
332 .map_err(|_| Error::StorageCorrupted)?;
333 }
334
335 let wifi = Wifi::new_with_captive_portal_ssid(
336 &wifi_auto_static.wifi,
337 pin_23,
338 pin_24,
339 pin_25,
340 pin_29,
341 pio,
342 dma,
343 wifi_credentials_flash_block,
344 captive_portal_ssid,
345 spawner,
346 );
347
348 wifi_auto_static.button.lock(|cell| {
349 *cell.borrow_mut() = Some(button);
350 });
351
352 // Store fields array and convert to slice
353 let fields_ref: &'static [&'static dyn WifiAutoField] = if N > 0 {
354 assert!(
355 N <= MAX_WIFI_AUTO_FIELDS,
356 "WifiAuto supports at most {} custom fields",
357 MAX_WIFI_AUTO_FIELDS
358 );
359 let mut storage: Vec<&'static dyn WifiAutoField, MAX_WIFI_AUTO_FIELDS> = Vec::new();
360 for field in custom_fields {
361 storage.push(field).unwrap_or_else(|_| unreachable!());
362 }
363 let stored_vec = wifi_auto_static.fields_storage.init(storage);
364 stored_vec.as_slice()
365 } else {
366 &[]
367 };
368
369 let instance = wifi_auto_static.wifi_auto_cell.init(WifiAutoInner {
370 events: &wifi_auto_static.events,
371 wifi,
372 spawner,
373 force_captive_portal: wifi_auto_static.force_captive_portal_flag(),
374 defaults: wifi_auto_static.defaults(),
375 button: wifi_auto_static.button(),
376 fields: fields_ref,
377 });
378
379 if force_captive_portal {
380 instance.force_captive_portal();
381 }
382
383 Ok(Self {
384 wifi_auto: instance,
385 })
386 }
387
388 /// Connects to WiFi (if possible), reports status, and returns the
389 /// network stack and button, consuming the `WifiAuto`.
390 ///
391 /// See the [WifiAuto struct example](Self) for a usage example.
392 ///
393 /// This method does not return until WiFi is connected. It may briefly
394 /// restart the Pico while switching between normal WiFi operation
395 /// and hosting its temporary setup network.
396 ///
397 /// This `connect` method reports progress by calling a user-provided async
398 /// handler whenever the WiFi state changes.
399 /// The handler receives a [`WifiAutoEvent`].
400 /// The handler is called sequentially for each event and may `await`.
401 ///
402 /// The three events are:
403 /// - `CaptivePortalReady`: The device is hosting a captive portal and waiting for user input.
404 /// - `Connecting`: The device is attempting to connect to the WiFi network.
405 /// - `ConnectionFailed`: All connection attempts failed. The device
406 /// will reset and re-enter setup mode (for example, if the password
407 /// is incorrect).
408 ///
409 /// The first example uses a handler that does nothing.
410 /// The second example shows how to use an LED panel to display status messages.
411 /// The example on the [`WifiAuto`] struct shows simple logging.
412 ///
413 /// # Example 1: No-op event handler
414 /// ```rust,no_run
415 /// # // Based on examples/wifiauto2.rs.
416 /// # #![no_std]
417 /// # #![no_main]
418 /// # use panic_probe as _;
419 /// # use device_envoy::{
420 /// # Result,
421 /// # button::PressedTo,
422 /// # flash_array::FlashArray,
423 /// # wifi_auto::WifiAuto,
424 /// # };
425 /// # use embassy_executor::Spawner;
426 /// # use embassy_rp::Peripherals;
427 /// # async fn example(spawner: Spawner, p: Peripherals) -> Result<()> {
428 /// # let [wifi_flash] = FlashArray::<1>::new(p.FLASH)?;
429 /// # let wifi_auto = WifiAuto::new(
430 /// # p.PIN_23,
431 /// # p.PIN_24,
432 /// # p.PIN_25,
433 /// # p.PIN_29,
434 /// # p.PIO0,
435 /// # p.DMA_CH0,
436 /// # wifi_flash,
437 /// # p.PIN_13,
438 /// # PressedTo::Ground,
439 /// # "PicoAccess",
440 /// # [],
441 /// # spawner,
442 /// # )?;
443 /// let (_stack, _button) = wifi_auto
444 /// .connect(|_event| async move { Ok(()) })
445 /// .await?;
446 /// # Ok(())
447 /// # }
448 /// ```
449 ///
450 /// # Example 2: Using a display to show status
451 /// ```rust,no_run
452 /// # // Based on demos/f_wifi_auto/f1_dns.rs.
453 /// # #![no_std]
454 /// # #![no_main]
455 /// # use panic_probe as _;
456 /// # use device_envoy::{
457 /// # Result,
458 /// # button::PressedTo,
459 /// # flash_array::FlashArray,
460 /// # led_strip::colors,
461 /// # wifi_auto::{WifiAuto, WifiAutoEvent},
462 /// # };
463 /// # use smart_leds::RGB8;
464 /// # use embassy_executor::Spawner;
465 /// # use embassy_rp::Peripherals;
466 /// # struct Led8x12;
467 /// # impl Led8x12 {
468 /// # async fn write_text(&self, _text: &str, _colors: &[RGB8]) -> Result<()> { Ok(()) }
469 /// # }
470 /// # async fn show_animated_dots(_led8x12: &Led8x12) -> Result<()> { Ok(()) }
471 /// # const COLORS: &[RGB8] = &[colors::WHITE];
472 /// # async fn example(spawner: Spawner, p: Peripherals) -> Result<()> {
473 /// # let [wifi_flash] = FlashArray::<1>::new(p.FLASH)?;
474 /// # let wifi_auto = WifiAuto::new(
475 /// # p.PIN_23,
476 /// # p.PIN_24,
477 /// # p.PIN_25,
478 /// # p.PIN_29,
479 /// # p.PIO0,
480 /// # p.DMA_CH0,
481 /// # wifi_flash,
482 /// # p.PIN_13,
483 /// # PressedTo::Ground,
484 /// # "PicoAccess",
485 /// # [],
486 /// # spawner,
487 /// # )?;
488 /// # let led8x12 = Led8x12;
489 /// // Keep a reference so the handler can reuse the display across events.
490 /// let led8x12_ref = &led8x12;
491 /// let (stack, button) = wifi_auto
492 /// .connect(|event| async move {
493 /// match event {
494 /// WifiAutoEvent::CaptivePortalReady => {
495 /// led8x12_ref.write_text("JO\nIN", COLORS).await?;
496 /// }
497 /// WifiAutoEvent::Connecting { .. } => {
498 /// show_animated_dots(led8x12_ref).await?;
499 /// }
500 /// WifiAutoEvent::ConnectionFailed => {
501 /// led8x12_ref.write_text("FA\nIL", COLORS).await?;
502 /// }
503 /// }
504 /// Ok(())
505 /// })
506 /// .await?;
507 /// # let _stack = stack;
508 /// # let _button = button;
509 /// # Ok(())
510 /// # }
511 /// ```
512 pub async fn connect<Fut, F>(
513 self,
514 on_event: F,
515 ) -> Result<(&'static Stack<'static>, Button<'static>)>
516 where
517 F: FnMut(WifiAutoEvent) -> Fut,
518 Fut: Future<Output = Result<()>>,
519 {
520 self.wifi_auto.connect(on_event).await
521 }
522}
523
524impl WifiAutoInner {
525 #[must_use]
526 const fn new_static() -> WifiAutoStatic {
527 WifiAutoStatic::new()
528 }
529
530 fn force_captive_portal(&self) {
531 self.force_captive_portal.store(true, Ordering::Relaxed);
532 }
533
534 fn take_button(&self) -> Option<Button<'static>> {
535 self.button.lock(|cell| cell.borrow_mut().take())
536 }
537
538 fn extra_fields_ready(&self) -> Result<bool> {
539 for field in self.fields {
540 let satisfied = field.is_satisfied().map_err(|_| Error::StorageCorrupted)?;
541 if !satisfied {
542 info!("WifiAuto: custom field not satisfied, forcing captive portal");
543 return Ok(false);
544 }
545 }
546 info!(
547 "WifiAuto: all {} custom fields satisfied",
548 self.fields.len()
549 );
550 Ok(true)
551 }
552
553 async fn connect<Fut, F>(
554 &self,
555 mut on_event: F,
556 ) -> Result<(&'static Stack<'static>, Button<'static>)>
557 where
558 F: FnMut(WifiAutoEvent) -> Fut,
559 Fut: Future<Output = Result<()>>,
560 {
561 self.ensure_connected_with(&mut on_event).await?;
562 let stack = self.wifi.wait_for_stack().await;
563 let button = self.take_button().ok_or(Error::StorageCorrupted)?;
564 Ok((stack, button))
565 }
566
567 async fn signal_event_with<Fut, F>(&self, on_event: &mut F, event: WifiAutoEvent) -> Result<()>
568 where
569 F: FnMut(WifiAutoEvent) -> Fut,
570 Fut: Future<Output = Result<()>>,
571 {
572 self.events.signal(event);
573 on_event(event).await?;
574 Ok(())
575 }
576
577 async fn ensure_connected_with<Fut, F>(&self, on_event: &mut F) -> Result<()>
578 where
579 F: FnMut(WifiAutoEvent) -> Fut,
580 Fut: Future<Output = Result<()>>,
581 {
582 loop {
583 let force_captive_portal = self.force_captive_portal.swap(false, Ordering::AcqRel);
584 let start_mode = self.wifi.current_start_mode();
585 let has_creds = self.wifi.has_persisted_credentials();
586 let extras_ready = self.extra_fields_ready()?;
587 info!(
588 "WifiAuto: force={} has_creds={} extras_ready={}",
589 force_captive_portal, has_creds, extras_ready
590 );
591 if force_captive_portal
592 || matches!(start_mode, WifiStartMode::CaptivePortal)
593 || !has_creds
594 || !extras_ready
595 {
596 if has_creds {
597 if let Some(creds) = self.wifi.load_persisted_credentials() {
598 self.defaults.lock(|cell| {
599 *cell.borrow_mut() = Some(creds);
600 });
601 }
602 }
603 self.signal_event_with(on_event, WifiAutoEvent::CaptivePortalReady)
604 .await?;
605 self.run_captive_portal().await?;
606 unreachable!("Device should reset after captive portal submission");
607 }
608
609 for attempt in 1..=MAX_CONNECT_ATTEMPTS {
610 info!(
611 "WifiAuto: connection attempt {}/{}",
612 attempt, MAX_CONNECT_ATTEMPTS
613 );
614 self.signal_event_with(
615 on_event,
616 WifiAutoEvent::Connecting {
617 try_index: attempt - 1,
618 try_count: MAX_CONNECT_ATTEMPTS,
619 },
620 )
621 .await?;
622 if self
623 .wait_for_client_ready_with_timeout(CONNECT_TIMEOUT)
624 .await
625 {
626 return Ok(());
627 }
628 warn!("WifiAuto: connection attempt {} timed out", attempt);
629 let retry_delay = retry_delay_with_jitter(attempt - 1);
630 info!(
631 "WifiAuto: retrying after {} ms (attempt {})",
632 retry_delay.as_millis(),
633 attempt
634 );
635 Timer::after(retry_delay).await;
636 }
637
638 info!(
639 "WifiAuto: failed to connect after {} attempts, returning to captive portal",
640 MAX_CONNECT_ATTEMPTS
641 );
642 info!("WifiAuto: signaling ConnectionFailed event");
643 self.signal_event_with(on_event, WifiAutoEvent::ConnectionFailed)
644 .await?;
645 if let Some(creds) = self.wifi.load_persisted_credentials() {
646 self.defaults.lock(|cell| {
647 *cell.borrow_mut() = Some(creds);
648 });
649 }
650 info!("WifiAuto: writing CaptivePortal mode to flash");
651 self.wifi
652 .set_start_mode(WifiStartMode::CaptivePortal)
653 .map_err(|_| Error::StorageCorrupted)?;
654 info!("WifiAuto: flash write complete, waiting 1 second before reset");
655 Timer::after_secs(1).await;
656 info!("WifiAuto: resetting device now");
657 SCB::sys_reset();
658 }
659 }
660
661 async fn wait_for_client_ready_with_timeout(&self, timeout: Duration) -> bool {
662 with_timeout(timeout, async {
663 loop {
664 match self.wifi.wait_for_wifi_event().await {
665 WifiEvent::ClientReady => break,
666 WifiEvent::CaptivePortalReady => {
667 info!(
668 "WifiAuto: received captive-portal-ready event while waiting for client mode"
669 );
670 }
671 }
672 }
673 })
674 .await
675 .is_ok()
676 }
677
678 #[allow(unreachable_code)]
679 async fn run_captive_portal(&self) -> Result<Infallible> {
680 self.wifi.wait_for_wifi_event().await;
681 let stack = self.wifi.wait_for_stack().await;
682
683 let captive_portal_ip = Ipv4Address::new(192, 168, 4, 1);
684 if let Err(err) = self
685 .spawner
686 .spawn(dns_server_task(stack, captive_portal_ip))
687 {
688 info!("WifiAuto: DNS server task spawn failed: {:?}", err);
689 }
690
691 let defaults_owned = self
692 .defaults
693 .lock(|cell| cell.borrow_mut().take())
694 .or_else(|| self.wifi.load_persisted_credentials());
695 let submission =
696 portal::collect_credentials(stack, self.spawner, defaults_owned.as_ref(), self.fields)
697 .await?;
698 self.wifi.persist_credentials(&submission).map_err(|err| {
699 warn!("{}", err);
700 Error::StorageCorrupted
701 })?;
702
703 Timer::after_millis(750).await;
704 SCB::sys_reset();
705 loop {
706 cortex_m::asm::nop();
707 }
708 }
709}
710
711fn retry_delay_with_jitter(attempt_index: u8) -> Duration {
712 let base_ms = RETRY_BASE_DELAY.as_millis();
713 assert!(base_ms > 0, "RETRY_BASE_DELAY must be positive");
714 let jitter_max_ms = RETRY_JITTER_MAX.as_millis();
715 let multiplier = 1u64
716 .checked_shl(u32::from(attempt_index))
717 .expect("attempt_index must fit in shift");
718 let delay_ms = base_ms
719 .checked_mul(multiplier)
720 .expect("retry delay must fit in millis");
721 let jitter_ms = if jitter_max_ms == 0 {
722 0
723 } else {
724 Instant::now().as_millis() % (jitter_max_ms + 1)
725 };
726 let total_ms = delay_ms
727 .checked_add(jitter_ms)
728 .expect("retry delay with jitter must fit in millis");
729 Duration::from_millis(total_ms)
730}