Skip to main content

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}