/ firmware / tests / wifi.rs
wifi.rs
  1  //! `describe("WiFi")`
  2  //!
  3  //! WiFi station-mode tests: scan for access points and join the
  4  //! configured home network via DHCP. Merged from wifi_scan.rs and
  5  //! wifi_dhcp.rs.
  6  //!
  7  //! NOTE: Running all 5 tests in one `cargo +esp tt --test wifi` invocation
  8  //! will crash after the 2nd test. The WiFi radio coprocessor holds state in
  9  //! RTC memory that survives the soft-reset probe-rs issues between tests,
 10  //! so `esp_radio::wifi::new()` fails on the 3rd boot. Every test passes
 11  //! individually — run them with a name filter:
 12  //!     cargo +esp tt --test wifi -- user_joins
 13  
 14  #![no_std]
 15  #![no_main]
 16  
 17  extern crate alloc;
 18  
 19  #[path = "common/mod.rs"]
 20  mod common;
 21  
 22  use defmt::info;
 23  use embassy_time::Duration;
 24  use esp_radio::wifi::{
 25      Config as WifiConfig,
 26      scan::ScanConfig,
 27      sta::StationConfig,
 28  };
 29  
 30  use common::{Device, tasks};
 31  
 32  const WIFI_SSID: &str = env!("WIFI_SSID");
 33  const WIFI_PSK: &str = env!("WIFI_PSK");
 34  const SCAN_MAX_RESULTS: usize = 8;
 35  
 36  esp_bootloader_esp_idf::esp_app_desc!();
 37  
 38  #[cfg(test)]
 39  #[embedded_test::setup]
 40  fn setup() {
 41      rtt_target::rtt_init_defmt!();
 42  }
 43  
 44  #[cfg(test)]
 45  #[embedded_test::tests(default_timeout = 45, executor = esp_rtos::embassy::Executor::new())]
 46  mod tests {
 47      use super::*;
 48  
 49      #[init]
 50      fn init() -> Device {
 51          info!("=== WiFi — describe block ===");
 52          common::setup::boot_device()
 53      }
 54  
 55      /// `it("user scans and sees nearby access points")`
 56      #[test]
 57      #[timeout(20)]
 58      async fn user_scans_and_sees_nearby_access_points(
 59          mut device: Device,
 60      ) -> Result<(), &'static str> {
 61          let mut wifi_controller = device
 62              .wifi_controller
 63              .take()
 64              .ok_or("device WiFi controller already consumed")?;
 65  
 66          let station_config = WifiConfig::Station(
 67              StationConfig::default()
 68                  .with_ssid(WIFI_SSID)
 69                  .with_password(WIFI_PSK.into()),
 70          );
 71  
 72          wifi_controller
 73              .set_config(&station_config)
 74              .map_err(|_| "device: failed to apply station config")?;
 75  
 76          info!("user asks the device to scan for nearby access points");
 77          let visible = wifi_controller
 78              .scan_async(&ScanConfig::default().with_max(SCAN_MAX_RESULTS))
 79              .await
 80              .map_err(|_| "device: WiFi scan failed")?;
 81  
 82          info!("scan complete count={=usize}", visible.len());
 83          for (i, ap) in visible.iter().enumerate() {
 84              info!(
 85                  "AP {=usize}: ssid={=str} channel={=u8} rssi={=i8}",
 86                  i + 1,
 87                  ap.ssid.as_str(),
 88                  ap.channel,
 89                  ap.signal_strength,
 90              );
 91          }
 92  
 93          defmt::assert!(!visible.is_empty(), "WiFi scan returned zero access points");
 94  
 95          embassy_time::Timer::after(Duration::from_millis(250)).await;
 96          device.wifi_controller = Some(wifi_controller);
 97          Ok(())
 98      }
 99  
100      /// `it("user joins home WiFi and receives a DHCP lease")`
101      #[test]
102      #[timeout(30)]
103      async fn user_joins_home_wifi_and_receives_dhcp_lease(
104          mut device: Device,
105      ) -> Result<(), &'static str> {
106          let embassy_spawner =
107              unsafe { embassy_executor::Spawner::for_current_executor() }.await;
108  
109          tasks::wifi::connect_to_home_access_point(&mut device, embassy_spawner).await?;
110  
111          let stack = device
112              .embassy_network_stack
113              .ok_or("device: embassy-net stack missing after DHCP")?;
114          let ipv4 = stack
115              .config_v4()
116              .ok_or("device: DHCP completed but no IPv4 config")?;
117  
118          info!(
119              "DHCP address acquired ipv4={=[u8]:?}",
120              ipv4.address.address().octets()
121          );
122          Ok(())
123      }
124  
125      /// `it("user observes default credentials match env vars")`
126      #[test]
127      async fn user_observes_default_credentials_match_env_vars(
128          _device: Device,
129      ) -> Result<(), &'static str> {
130          use firmware::networking::wifi::credentials;
131  
132          let defaults = credentials::default_credentials();
133  
134          info!(
135              "default ssid={=str} password_len={=usize}",
136              defaults.ssid.as_str(),
137              defaults.password.len()
138          );
139  
140          defmt::assert_eq!(defaults.ssid.as_str(), WIFI_SSID);
141          defmt::assert_eq!(defaults.password.as_str(), WIFI_PSK);
142          defmt::assert!(!defaults.ssid.is_empty(), "default SSID is empty");
143          Ok(())
144      }
145  
146      /// `it("user writes and reads WiFi credentials from flash")`
147      #[test]
148      async fn user_writes_and_reads_wifi_credentials_from_flash(
149          mut device: Device,
150      ) -> Result<(), &'static str> {
151          use firmware::networking::wifi::credentials;
152  
153          let flash_peripheral = device.flash.take().ok_or("FLASH peripheral consumed")?;
154          let mut flash = esp_storage::FlashStorage::new(flash_peripheral).multicore_auto_park();
155  
156          // Save whatever is currently in flash so we can restore it
157          let original = credentials::read_from_flash(&mut flash);
158  
159          // Write test credentials
160          let wrote = credentials::write_to_flash(&mut flash, "test-ssid", "test-pass");
161          defmt::assert!(wrote, "write_to_flash failed");
162  
163          // Read back and verify
164          let readback = credentials::read_from_flash(&mut flash)
165              .ok_or("read_from_flash returned None after write")?;
166  
167          defmt::assert_eq!(readback.ssid.as_str(), "test-ssid");
168          defmt::assert_eq!(readback.password.as_str(), "test-pass");
169  
170          info!("credential flash roundtrip verified");
171  
172          // Restore original credentials
173          if let Some(orig) = original {
174              credentials::write_to_flash(&mut flash, orig.ssid.as_str(), orig.password.as_str());
175          }
176  
177          Ok(())
178      }
179  
180      /// `it("user observes read_from_flash returns None when no credentials stored")`
181      #[test]
182      async fn user_observes_read_returns_none_without_credentials(
183          mut device: Device,
184      ) -> Result<(), &'static str> {
185          use embedded_storage::nor_flash::NorFlash;
186          use firmware::networking::wifi::credentials;
187  
188          let flash_peripheral = device.flash.take().ok_or("FLASH peripheral consumed")?;
189          let mut flash = esp_storage::FlashStorage::new(flash_peripheral).multicore_auto_park();
190  
191          // Save original
192          let original = credentials::read_from_flash(&mut flash);
193  
194          // Erase the credential sector
195          NorFlash::erase(&mut flash, 0x1000, 0x2000)
196              .map_err(|_| "flash erase failed")?;
197  
198          // Read should return None
199          let result = credentials::read_from_flash(&mut flash);
200          defmt::assert!(result.is_none(), "expected None after erasing credentials");
201  
202          info!("read_from_flash returns None when flash is erased");
203  
204          // Restore original credentials
205          if let Some(orig) = original {
206              credentials::write_to_flash(&mut flash, orig.ssid.as_str(), orig.password.as_str());
207          }
208  
209          Ok(())
210      }
211  }