device_envoy_core/wifi_auto.rs
1//! A device abstraction for common Wi-Fi auto-provisioning types and portal helpers.
2//!
3//! See [`WifiAuto`] for the primary trait and [`WifiAutoEvent`] for connection flow events.
4
5use crate::button::Button;
6use core::future::Future;
7
8mod fields;
9mod portal;
10
11pub use portal::FormData;
12#[doc(hidden)] // Platform plumbing helper type used by RP/ESP captive portal code.
13pub use portal::HtmlBuffer;
14pub use portal::WifiAutoField;
15#[doc(hidden)] // Platform plumbing helper used by RP/ESP captive portal code.
16pub use portal::generate_config_page;
17#[doc(hidden)] // Platform plumbing helper used by RP/ESP captive portal code.
18pub use portal::parse_post;
19
20/// Canonical network stack type returned by [`WifiAuto::connect`].
21pub type WifiStack = &'static embassy_net::Stack<'static>;
22
23// This helper macro must be `pub` because downstream crates expand it in impl blocks.
24#[doc(hidden)]
25#[macro_export]
26macro_rules! __impl_wifi_auto_connect {
27 (
28 $(#[$meta:meta])*
29 fn $name:ident (&self as $self_ident:ident, $on_event:ident) -> $return_ty:ty $body:block
30 ) => {
31 $(#[$meta])*
32 pub async fn $name<OnEvent, OnEventFuture>(
33 &self,
34 mut $on_event: OnEvent,
35 ) -> $return_ty
36 where
37 OnEvent: FnMut($crate::wifi_auto::WifiAutoEvent) -> OnEventFuture,
38 OnEventFuture: core::future::Future<Output = crate::Result<()>>,
39 {
40 let $self_ident = self;
41 $body
42 }
43 };
44 (
45 $(#[$meta:meta])*
46 fn $name:ident (self as $self_ident:ident, $on_event:ident) -> $return_ty:ty $body:block
47 ) => {
48 $(#[$meta])*
49 pub async fn $name<OnEvent, OnEventFuture>(
50 self,
51 mut $on_event: OnEvent,
52 ) -> $return_ty
53 where
54 OnEvent: FnMut($crate::wifi_auto::WifiAutoEvent) -> OnEventFuture,
55 OnEventFuture: core::future::Future<Output = crate::Result<()>>,
56 {
57 let $self_ident = self;
58 $body
59 }
60 };
61}
62
63/// Events emitted while connecting.
64///
65/// See [`WifiAuto::connect`] for usage examples.
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum WifiAutoEvent {
68 /// Captive portal is ready and waiting for user configuration.
69 CaptivePortalReady,
70 /// Attempting to connect to WiFi network.
71 Connecting {
72 /// Current attempt number (0-based).
73 try_index: u8,
74 /// Total number of attempts that will be made.
75 try_count: u8,
76 },
77 /// Connection failed after all attempts, device will reset.
78 ConnectionFailed,
79}
80
81/// Shared Wi-Fi auto-provisioning error variants used across platform ports.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum WifiAutoError {
84 /// Captive-portal data or rendering format was invalid.
85 FormatError,
86 /// Stored Wi-Fi auto state is invalid for expected runtime flow.
87 StorageCorrupted,
88 /// A required custom field is missing from the Wi-Fi auto setup.
89 MissingCustomWifiAutoField,
90}
91
92/// Preferred Wi-Fi startup mode.
93#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
94#[doc(hidden)] // Startup-policy plumbing used by platform wifi_auto state machines.
95pub enum WifiStartMode {
96 /// Start directly in Wi-Fi client mode using saved credentials.
97 Client,
98 /// Start in captive-portal mode for reconfiguration.
99 CaptivePortal,
100}
101
102impl Default for WifiStartMode {
103 fn default() -> Self {
104 Self::Client
105 }
106}
107
108/// Return whether startup should enter captive-portal mode.
109#[must_use]
110#[doc(hidden)] // Backend-plumbing helper used by platform crates.
111pub const fn should_enter_captive_portal(
112 wifi_start_mode: WifiStartMode,
113 force_captive_portal: bool,
114 has_persisted_credentials: bool,
115 custom_fields_satisfied: bool,
116) -> bool {
117 force_captive_portal
118 || !custom_fields_satisfied
119 || !has_persisted_credentials
120 || matches!(wifi_start_mode, WifiStartMode::CaptivePortal)
121}
122
123/// Wi-Fi credentials collected from the captive portal.
124#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
125#[doc(hidden)] // Shared plumbing type used across platform wifi_auto implementations.
126pub struct WifiCredentials {
127 /// Network name (SSID).
128 pub ssid: heapless::String<32>,
129 /// Network password.
130 pub password: heapless::String<64>,
131}
132
133impl WifiCredentials {
134 /// Create credentials from string slices.
135 #[must_use]
136 pub fn new(ssid: &str, password: &str) -> Self {
137 assert!(!ssid.is_empty(), "ssid must not be empty");
138 let mut ssid_string = heapless::String::<32>::new();
139 ssid_string
140 .push_str(ssid)
141 .expect("ssid exceeds 32 characters");
142 let mut password_string = heapless::String::<64>::new();
143 password_string
144 .push_str(password)
145 .expect("password exceeds 64 characters");
146 Self {
147 ssid: ssid_string,
148 password: password_string,
149 }
150 }
151}
152
153/// Persisted Wi-Fi auto state shared across platform ports.
154#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
155#[doc(hidden)] // Shared persistence type used by platform wifi_auto storage backends.
156pub struct WifiAutoPersistedState {
157 /// Persisted credentials, if available.
158 pub wifi_credentials: Option<WifiCredentials>,
159 /// Preferred startup mode for next boot.
160 pub wifi_start_mode: WifiStartMode,
161}
162
163impl Default for WifiAutoPersistedState {
164 fn default() -> Self {
165 Self {
166 wifi_credentials: None,
167 wifi_start_mode: WifiStartMode::Client,
168 }
169 }
170}
171
172/// Platform-agnostic Wi-Fi auto-connect contract.
173///
174/// Platform crates implement this trait for their concrete runtime handles.
175/// Constructors remain inherent on platform types.
176///
177/// `WifiAuto::connect` handles Wi-Fi setup end-to-end. It usually tries saved
178/// credentials first, and if credentials are missing or invalid it can run a
179/// captive portal so the user can submit new credentials.
180///
181/// While `connect` runs, `on_event` receives progress updates:
182///
183/// - [`WifiAutoEvent::CaptivePortalReady`]: captive portal is ready for user input.
184/// - [`WifiAutoEvent::Connecting`]: a Wi-Fi connection attempt is in progress.
185/// - [`WifiAutoEvent::ConnectionFailed`]: all connection attempts failed.
186///
187/// # Example
188///
189/// ```rust,no_run
190/// use core::future::Future;
191/// use core::convert::Infallible;
192/// use device_envoy_core::{
193/// button::Button,
194/// wifi_auto::{WifiAuto, WifiAutoEvent, WifiStack},
195/// };
196///
197/// async fn connect_with_status(
198/// wifi_auto: impl WifiAuto<Error = Infallible>,
199/// ) -> Result<WifiStack, Infallible> {
200/// wifi_auto
201/// .connect(&mut ButtonMock, |wifi_auto_event| async move {
202/// match wifi_auto_event {
203/// WifiAutoEvent::CaptivePortalReady => {
204/// // Captive portal is ready for Wi-Fi credential entry.
205/// }
206/// WifiAutoEvent::Connecting { .. } => {
207/// // A Wi-Fi connection attempt is in progress.
208/// }
209/// WifiAutoEvent::ConnectionFailed => {
210/// // All connection attempts failed.
211/// }
212/// }
213/// Ok(())
214/// })
215/// .await
216/// }
217///
218/// # struct ButtonMock;
219/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
220/// # fn is_pressed_raw(&self) -> bool { false }
221/// # async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
222/// # }
223/// # impl Button for ButtonMock {}
224/// # struct DemoWifiAuto;
225/// # impl WifiAuto for DemoWifiAuto {
226/// # type Error = Infallible;
227/// # async fn connect<
228/// # OnEvent: FnMut(WifiAutoEvent) -> OnEventFuture,
229/// # OnEventFuture: Future<Output = Result<(), Self::Error>>,
230/// # >(
231/// # self,
232/// # button: &mut impl Button,
233/// # mut on_event: OnEvent,
234/// # ) -> Result<WifiStack, Self::Error>
235/// # {
236/// # on_event(WifiAutoEvent::Connecting {
237/// # try_index: 0,
238/// # try_count: 1,
239/// # })
240/// # .await?;
241/// # let _todo_result: Result<WifiStack, Self::Error> = todo!();
242/// # _todo_result
243/// # }
244/// # }
245/// # fn main() {
246/// # let wifi_auto = DemoWifiAuto;
247/// # let _future = connect_with_status(wifi_auto);
248/// # }
249/// ```
250#[allow(async_fn_in_trait)]
251pub trait WifiAuto {
252 /// Platform-specific error type.
253 type Error;
254
255 /// Connect to Wi-Fi, emitting progress events to `on_event`.
256 ///
257 /// See the [WifiAuto trait documentation](Self) for usage examples.
258 async fn connect<OnEvent, OnEventFuture>(
259 self,
260 button: &mut impl Button,
261 on_event: OnEvent,
262 ) -> Result<WifiStack, Self::Error>
263 where
264 OnEvent: FnMut(WifiAutoEvent) -> OnEventFuture,
265 OnEventFuture: Future<Output = Result<(), Self::Error>>;
266}
267
268/// Backend contract for platform-specific Wi-Fi auto-connect operations.
269#[doc(hidden)] // Backend-plumbing trait used by platform crates.
270pub trait WifiAutoBackend {
271 /// Platform-specific error type.
272 type Error;
273
274 /// Whether boot should force captive portal regardless of persisted state.
275 fn force_captive_portal(&self) -> bool;
276
277 /// Number of connection attempts before emitting `ConnectionFailed`.
278 fn try_count(&self) -> u8;
279
280 /// Load persisted startup mode.
281 fn load_start_mode(&self) -> Result<WifiStartMode, Self::Error>;
282
283 /// Check whether all custom fields are currently satisfied.
284 fn custom_fields_satisfied(&self) -> Result<bool, Self::Error>;
285
286 /// Load persisted credentials, if present.
287 fn load_persisted_credentials(&self) -> Result<Option<WifiCredentials>, Self::Error>;
288
289 /// Persist submitted credentials.
290 fn persist_credentials(&self, wifi_credentials: &WifiCredentials) -> Result<(), Self::Error>;
291
292 /// Persist startup mode.
293 fn set_start_mode(&self, wifi_start_mode: WifiStartMode) -> Result<(), Self::Error>;
294
295 /// Run captive portal and return submitted credentials.
296 fn run_captive_portal(
297 &mut self,
298 ) -> impl Future<Output = Result<WifiCredentials, Self::Error>> + '_;
299
300 /// Configure platform networking once credentials are resolved.
301 fn on_resolved_credentials(
302 &mut self,
303 wifi_credentials: &WifiCredentials,
304 ) -> impl Future<Output = Result<(), Self::Error>> + '_;
305
306 /// Run one platform-specific connect attempt.
307 fn on_connect_attempt(
308 &mut self,
309 try_index: u8,
310 ) -> impl Future<Output = Result<bool, Self::Error>> + '_;
311}
312
313/// Run the shared Wi-Fi auto-connect flow through a platform backend.
314#[doc(hidden)] // Backend-plumbing helper used by platform crates.
315pub async fn connect_with_backend<Backend, OnEvent, OnEventFuture>(
316 backend: &mut Backend,
317 on_event: &mut OnEvent,
318) -> Result<bool, Backend::Error>
319where
320 Backend: WifiAutoBackend,
321 OnEvent: FnMut(WifiAutoEvent) -> OnEventFuture,
322 OnEventFuture: Future<Output = Result<(), Backend::Error>>,
323{
324 let wifi_start_mode = backend.load_start_mode()?;
325 let custom_fields_satisfied = backend.custom_fields_satisfied()?;
326 let mut wifi_credentials = backend.load_persisted_credentials()?;
327 let has_persisted_credentials = wifi_credentials.is_some();
328
329 if should_enter_captive_portal(
330 wifi_start_mode,
331 backend.force_captive_portal(),
332 has_persisted_credentials,
333 custom_fields_satisfied,
334 ) {
335 on_event(WifiAutoEvent::CaptivePortalReady).await?;
336 let portal_wifi_credentials = backend.run_captive_portal().await?;
337 backend.persist_credentials(&portal_wifi_credentials)?;
338 backend.set_start_mode(WifiStartMode::Client)?;
339 wifi_credentials = Some(portal_wifi_credentials);
340 }
341
342 let wifi_credentials =
343 wifi_credentials.expect("wifi credentials should exist after captive portal fallback");
344 backend.on_resolved_credentials(&wifi_credentials).await?;
345
346 for try_index in 0..backend.try_count() {
347 on_event(WifiAutoEvent::Connecting {
348 try_index,
349 try_count: backend.try_count(),
350 })
351 .await?;
352 if backend.on_connect_attempt(try_index).await? {
353 return Ok(true);
354 }
355 }
356
357 on_event(WifiAutoEvent::ConnectionFailed).await?;
358 Ok(false)
359}