1use embassy_executor::Spawner;
22use embassy_futures::select::{Either, select};
23use embassy_net::{
24 Config as NetConfig, Ipv4Address, Ipv4Cidr, Runner, StackResources, StaticConfigV4,
25};
26use embassy_time::{Duration, Instant, Timer};
27use esp_hal::peripherals::WIFI;
28use esp_radio::wifi::{
29 AuthenticationMethod, Config as WifiConfig, ControllerConfig, Interface, WifiController,
30 ap::AccessPointConfig,
31 event::{EventInfo, MessageResult},
32 sta::StationConfig,
33};
34use heapless::String;
35use static_cell::StaticCell;
36
37use crate::config::{Config, ConnectionType};
38use crate::hardware::efuse;
39
40mod dhcp;
41pub mod http;
42mod ws;
43
44pub const AP_IP: Ipv4Address = Ipv4Address::new(192, 168, 4, 1);
47
48pub const AP_PREFIX: u8 = 24;
50
51const STA_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
54
55const STA_DHCP_TIMEOUT: Duration = Duration::from_secs(15);
58
59const STA_RECONNECT_DELAY: Duration = Duration::from_secs(5);
62
63const SOCKETS: usize = 8;
66
67static STACK_STA: StaticCell<StackResources<SOCKETS>> = StaticCell::new();
70
71static STACK_AP: StaticCell<StackResources<SOCKETS>> = StaticCell::new();
75
76fn ap_ssid(config: &Config) -> String<32> {
78 if config.ap_config.ssid.is_empty() {
79 efuse::default_ap_ssid()
80 } else {
81 config.ap_config.ssid.clone()
82 }
83}
84
85fn build_ap(ssid: &str, password: &str, wpa2: bool) -> AccessPointConfig {
90 let ap = AccessPointConfig::default().with_ssid(ssid);
91 if wpa2 {
92 ap.with_auth_method(AuthenticationMethod::Wpa2Personal)
93 .with_password(password.into())
94 } else {
95 ap
96 }
97}
98
99fn build_sta(config: &Config) -> StationConfig {
104 let password = config.station_config.password.as_str();
105 let sta = StationConfig::default().with_ssid(config.station_config.ssid.as_str());
106 if password.len() >= 8 {
107 sta.with_auth_method(AuthenticationMethod::Wpa2Personal)
108 .with_password(password.into())
109 } else {
110 if !password.is_empty() {
111 log::warn!("WiFi station password below the WPA2 minimum of 8 chars — joining openly");
112 }
113 sta.with_auth_method(AuthenticationMethod::None)
114 }
115}
116
117fn stack_seed() -> u64 {
120 let [b0, b1, b2, b3, b4, b5] = efuse::mac_bytes();
121 u64::from_le_bytes([b0, b1, b2, b3, b4, b5, 0, 0])
122}
123
124async fn try_station(controller: &mut WifiController<'_>) -> bool {
129 match select(
130 controller.connect_async(),
131 Timer::after(STA_CONNECT_TIMEOUT),
132 )
133 .await
134 {
135 Either::First(Ok(info)) => {
136 log::info!(
137 "WiFi station '{}' connected (channel {})",
138 info.ssid.as_str(),
139 info.channel
140 );
141 true
142 }
143 Either::First(Err(err)) => {
144 log::warn!("WiFi station connect failed: {err:?}");
145 false
146 }
147 Either::Second(()) => {
148 log::warn!("WiFi station connect timed out");
149 false
150 }
151 }
152}
153
154async fn start_station(device: Interface<'static>, spawner: Spawner) -> bool {
162 #[allow(clippy::default_trait_access)]
163 let net_config = NetConfig::dhcpv4(Default::default());
164 let (stack, runner) = embassy_net::new(
165 device,
166 net_config,
167 STACK_STA.init(StackResources::new()),
168 stack_seed(),
169 );
170 spawn_runner(runner, spawner);
171 log::info!("WiFi station: waiting for a DHCP lease");
172 let deadline = Instant::now().saturating_add(STA_DHCP_TIMEOUT);
173 let warn_at = Instant::now().saturating_add(Duration::from_secs(10));
174 let mut lease_warned = false;
175 loop {
176 if let Some(cfg) = stack.config_v4() {
177 log::info!("WiFi station up: ip {}", cfg.address.address());
178 break;
179 }
180 if Instant::now() >= deadline {
181 log::warn!(
182 "WiFi station: no DHCP lease after {} s — falling back to AP",
183 STA_DHCP_TIMEOUT.as_secs()
184 );
185 return false;
186 }
187 if !lease_warned && Instant::now() >= warn_at {
188 log::warn!("WiFi station: no DHCP lease yet — still waiting");
189 lease_warned = true;
190 }
191 Timer::after(Duration::from_millis(250)).await;
192 }
193
194 http::spawn_all(spawner, stack);
195 log::info!("http/ws server up on port {}", http::HTTP_PORT);
196 true
197}
198
199async fn start_ap(device: Interface<'static>, ssid: &str, spawner: Spawner) {
202 #[allow(clippy::default_trait_access)]
203 let net_config = NetConfig::ipv4_static(StaticConfigV4 {
204 address: Ipv4Cidr::new(AP_IP, AP_PREFIX),
205 gateway: None,
206 dns_servers: Default::default(),
207 });
208 let (stack, runner) = embassy_net::new(
209 device,
210 net_config,
211 STACK_AP.init(StackResources::new()),
212 stack_seed(),
213 );
214 spawn_runner(runner, spawner);
215 stack.wait_link_up().await;
216 log::info!("WiFi AP up: {AP_IP} (ssid '{ssid}')");
217
218 match dhcp::task(stack) {
219 Ok(token) => spawner.spawn(token),
220 Err(err) => log::error!("DHCP server spawn failed: {err:?}"),
221 }
222
223 http::spawn_all(spawner, stack);
224 log::info!("http/ws server up on port {}", http::HTTP_PORT);
225}
226
227fn spawn_runner(runner: Runner<'static, Interface<'static>>, spawner: Spawner) {
229 match runner_task(runner) {
230 Ok(token) => spawner.spawn(token),
231 Err(err) => log::error!("network runner spawn failed: {err:?}"),
232 }
233}
234
235#[embassy_executor::task]
243pub async fn task(wifi: WIFI<'static>, config: Config, spawner: Spawner) {
244 let ssid = ap_ssid(&config);
245 let password = config.ap_config.password.as_str();
246 let wpa2 = password.len() >= 8;
247 if !password.is_empty() && !wpa2 {
248 log::warn!("WiFi AP password below the WPA2 minimum of 8 chars — open network");
249 }
250
251 let want_sta =
254 config.connection == ConnectionType::WifiSta && !config.station_config.ssid.is_empty();
255 if config.connection == ConnectionType::WifiSta && config.station_config.ssid.is_empty() {
256 log::warn!("WiFi station mode selected but no SSID configured — starting AP");
257 }
258
259 let initial_config = if want_sta {
260 log::info!(
261 "WiFi: connecting to station '{}'",
262 config.station_config.ssid
263 );
264 WifiConfig::Station(build_sta(&config))
265 } else {
266 log::info!(
267 "WiFi: starting AP '{ssid}' ({})",
268 if wpa2 { "WPA2" } else { "open" }
269 );
270 WifiConfig::AccessPoint(build_ap(ssid.as_str(), password, wpa2))
271 };
272
273 let init = esp_radio::wifi::new(
274 wifi,
275 ControllerConfig::default().with_initial_config(initial_config),
276 );
277 if let Err(err) = &init {
278 log::error!("WiFi init failed: {err:?} — network stack disabled");
279 }
280 let Ok((mut controller, interfaces)) = init else {
281 loop {
282 core::future::pending::<()>().await;
283 }
284 };
285
286 let station_up = want_sta
289 && try_station(&mut controller).await
290 && start_station(interfaces.station, spawner).await;
291 if !station_up {
292 if want_sta {
293 log::warn!("WiFi station unavailable — falling back to AP '{ssid}'");
294 let fallback = controller.set_config(&WifiConfig::AccessPoint(build_ap(
295 ssid.as_str(),
296 password,
297 wpa2,
298 )));
299 if let Err(err) = fallback {
300 log::error!("WiFi AP fallback failed: {err:?} — network stack disabled");
301 loop {
302 core::future::pending::<()>().await;
303 }
304 }
305 }
306 start_ap(interfaces.access_point, ssid.as_str(), spawner).await;
307 }
308
309 let mut subscriber = match controller.subscribe() {
314 Ok(subscriber) => Some(subscriber),
315 Err(err) => {
316 log::warn!("WiFi event subscription failed: {err:?}");
317 None
318 }
319 };
320 loop {
321 let link_lost = match subscriber.as_mut() {
322 Some(events) => match events.next_event().await {
323 MessageResult::Message(info) => {
324 log::info!("WiFi event: {info:?}");
325 station_up && matches!(info, EventInfo::StationDisconnected { .. })
326 }
327 MessageResult::Lagged(missed) => {
328 log::warn!("WiFi event queue lagged, {missed} dropped");
329 false
330 }
331 },
332 None => core::future::pending::<bool>().await,
333 };
334 if link_lost {
335 log::warn!("WiFi station link lost — re-associating");
336 drop(subscriber); while !try_station(&mut controller).await {
338 Timer::after(STA_RECONNECT_DELAY).await;
339 }
340 subscriber = match controller.subscribe() {
341 Ok(subscriber) => Some(subscriber),
342 Err(err) => {
343 log::error!("WiFi event subscription failed after reconnect: {err:?}");
344 None
345 }
346 };
347 }
348 }
349}
350
351#[embassy_executor::task]
353async fn runner_task(mut runner: Runner<'static, Interface<'static>>) {
354 runner.run().await;
355}