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 }