1use core::sync::atomic::{AtomicI8, AtomicU8, Ordering};
28
29use embassy_executor::{SpawnError, Spawner};
30use embassy_futures::select::{Either, select};
31use embassy_net::dns::DnsQueryType;
32use embassy_net::udp::{PacketMetadata, UdpSocket};
33use embassy_net::{Runner, Stack, StackResources};
34use embassy_time::{Duration, Instant, Timer, with_timeout};
35use esp_hal::peripherals::WIFI;
36use esp_hal::time::Duration as HalDuration;
37pub use esp_radio::wifi::ap::AccessPointInfo;
38pub use esp_radio::wifi::scan::ScanConfig;
39use esp_radio::wifi::scan::ScanTypeConfig;
40use esp_radio::wifi::sta::StationConfig;
41pub use esp_radio::wifi::{AuthenticationMethod, WifiController};
42use esp_radio::wifi::{AuthenticationMethodConfig, Config, ControllerConfig, Interface, WifiError};
43use oxstack_net::sntp;
44pub use oxstack_net::sntp::ServerTime;
45pub use oxstack_net::time::DateTime;
46use static_cell::StaticCell;
47
48pub use embassy_net;
49
50pub const DEFAULT_NTP_SERVER: &str = "pool.ntp.org";
52pub const SOCKETS: usize = 4;
54
55#[derive(Clone, Copy, Debug)]
57pub struct Credentials<'a> {
58 pub ssid: &'a str,
60 pub password: &'a str,
62}
63
64#[derive(Debug)]
66#[cfg_attr(feature = "defmt", derive(defmt::Format))]
67pub enum Error {
68 InvalidCredentials,
70 Wifi(WifiError),
72 Spawn,
74}
75
76impl From<SpawnError> for Error {
77 fn from(_: SpawnError) -> Self {
78 Error::Spawn
79 }
80}
81
82pub mod link {
84 use super::*;
85
86 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
88 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
89 pub enum State {
90 Idle = 0,
92 Connecting = 1,
94 Connected = 2,
97 Retrying = 3,
99 }
100
101 pub(super) static STATE: AtomicU8 = AtomicU8::new(State::Idle as u8);
102 pub(super) static RSSI: AtomicI8 = AtomicI8::new(i8::MIN);
103
104 pub fn state() -> State {
106 match STATE.load(Ordering::Relaxed) {
107 1 => State::Connecting,
108 2 => State::Connected,
109 3 => State::Retrying,
110 _ => State::Idle,
111 }
112 }
113
114 pub fn rssi() -> Option<i8> {
116 let r = RSSI.load(Ordering::Relaxed);
117 (state() == State::Connected && r != i8::MIN).then_some(r)
118 }
119
120 pub(super) fn set(s: State) {
121 STATE.store(s as u8, Ordering::Relaxed);
122 }
123}
124
125pub fn start_station(
128 spawner: Spawner,
129 wifi: WIFI<'static>,
130 creds: Credentials<'_>,
131) -> Result<Stack<'static>, Error> {
132 let ssid = creds
133 .ssid
134 .try_into()
135 .map_err(|_| Error::InvalidCredentials)?;
136 let auth = if creds.password.is_empty() {
137 AuthenticationMethodConfig::Open
138 } else {
139 AuthenticationMethodConfig::Wpa2Personal(
141 creds
142 .password
143 .try_into()
144 .map_err(|_| Error::InvalidCredentials)?,
145 )
146 };
147 let station = Config::Station(
148 StationConfig::default()
149 .with_ssid(ssid)
150 .with_authentication(auth),
151 );
152
153 let interface = Interface::station();
154 let controller = WifiController::new(
155 wifi,
156 ControllerConfig::default().with_initial_config(station),
157 )
158 .map_err(Error::Wifi)?;
159
160 static RESOURCES: StaticCell<StackResources<{ SOCKETS + 2 }>> = StaticCell::new();
161 let rng = esp_hal::rng::Rng::new();
162 let seed = u64::from(rng.random()) << 32 | u64::from(rng.random());
164 let (stack, runner) = embassy_net::new(
165 interface,
166 embassy_net::Config::dhcpv4(Default::default()),
167 RESOURCES.init(StackResources::new()),
168 seed,
169 );
170
171 spawner.spawn(link_task(controller)?);
172 spawner.spawn(net_task(runner)?);
173 Ok(stack)
174}
175
176#[embassy_executor::task]
177async fn link_task(mut controller: WifiController<'static>) {
178 loop {
179 link::set(link::State::Connecting);
180 if controller.connect_async().await.is_ok() {
181 link::set(link::State::Connected);
182 loop {
184 let rssi = controller
185 .rssi()
186 .ok()
187 .map_or(i8::MIN, |r| r.clamp(-127, 0) as i8);
188 link::RSSI.store(rssi, Ordering::Relaxed);
189 let dropped = select(
190 controller.wait_for_disconnect_async(),
191 Timer::after(Duration::from_secs(2)),
192 )
193 .await;
194 if let Either::First(_) = dropped {
195 break;
196 }
197 }
198 }
199 link::set(link::State::Retrying);
200 Timer::after(Duration::from_secs(5)).await;
201 }
202}
203
204#[embassy_executor::task]
205async fn net_task(mut runner: Runner<'static, Interface>) {
206 runner.run().await
207}
208
209#[derive(Debug)]
211#[cfg_attr(feature = "defmt", derive(defmt::Format))]
212pub enum TimeError {
213 Dns,
215 Socket,
217 Timeout,
219 Sntp(sntp::Error),
221}
222
223pub async fn query_time(stack: Stack<'static>, server: &str) -> Result<ServerTime, TimeError> {
225 let addrs = stack
226 .dns_query(server, DnsQueryType::A)
227 .await
228 .map_err(|_| TimeError::Dns)?;
229 let addr = *addrs.first().ok_or(TimeError::Dns)?;
230
231 let (mut rx_meta, mut tx_meta) = ([PacketMetadata::EMPTY; 2], [PacketMetadata::EMPTY; 2]);
232 let (mut rx_buf, mut tx_buf) = ([0u8; 128], [0u8; 128]);
233 let mut socket = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf);
234 socket.bind(0).map_err(|_| TimeError::Socket)?;
235 socket
236 .send_to(&sntp::request(), (addr, sntp::PORT))
237 .await
238 .map_err(|_| TimeError::Socket)?;
239
240 let mut reply = [0u8; sntp::PACKET_LEN];
241 loop {
242 let (n, meta) = with_timeout(Duration::from_secs(3), socket.recv_from(&mut reply))
243 .await
244 .map_err(|_| TimeError::Timeout)?
245 .map_err(|_| TimeError::Socket)?;
246 if meta.endpoint.addr == addr {
248 return sntp::parse_reply(&reply[..n]).map_err(TimeError::Sntp);
249 }
250 }
251}
252
253#[derive(Clone, Copy, Debug)]
255#[cfg_attr(feature = "defmt", derive(defmt::Format))]
256pub struct WallClock {
257 unix_us_at_boot: i64,
259}
260
261impl WallClock {
262 pub fn from_server_time(t: ServerTime) -> Self {
264 let unix_us = t.unix_seconds as i64 * 1_000_000 + i64::from(t.micros);
265 Self {
266 unix_us_at_boot: unix_us - Instant::now().as_micros() as i64,
267 }
268 }
269
270 pub fn unix_micros(&self) -> i64 {
272 self.unix_us_at_boot + Instant::now().as_micros() as i64
273 }
274
275 pub fn unix_seconds(&self) -> i64 {
277 self.unix_micros().div_euclid(1_000_000)
278 }
279
280 pub fn subsec_micros(&self) -> u32 {
282 self.unix_micros().rem_euclid(1_000_000) as u32
283 }
284
285 pub fn next_second(&self) -> Instant {
289 let to_go = 1_000_000 - u64::from(self.subsec_micros());
290 Instant::now() + Duration::from_micros(to_go)
291 }
292
293 pub fn now(&self, offset_minutes: i32) -> DateTime {
295 DateTime::from_unix(self.unix_seconds(), offset_minutes)
296 }
297}
298
299pub async fn sync_clock(stack: Stack<'static>, server: &str) -> Result<WallClock, TimeError> {
301 Ok(WallClock::from_server_time(
302 query_time(stack, server).await?,
303 ))
304}
305
306pub fn scan_config() -> ScanConfig {
311 ScanConfig::default()
312 .with_max(32)
313 .with_show_hidden(true)
314 .with_scan_type(ScanTypeConfig::Active {
315 min: HalDuration::from_millis(30),
316 max: HalDuration::from_millis(120),
317 })
318}
319
320pub struct Scanner<'d> {
322 controller: WifiController<'d>,
323}
324
325pub fn scanner(wifi: WIFI<'_>) -> Result<Scanner<'_>, Error> {
327 let controller = WifiController::new(wifi, ControllerConfig::default()).map_err(Error::Wifi)?;
328 Ok(Scanner { controller })
329}
330
331impl<'d> Scanner<'d> {
332 pub async fn scan(&mut self) -> Result<alloc::vec::Vec<AccessPointInfo>, Error> {
334 self.scan_with(&scan_config()).await
335 }
336
337 pub async fn scan_with(
339 &mut self,
340 config: &ScanConfig,
341 ) -> Result<alloc::vec::Vec<AccessPointInfo>, Error> {
342 let mut aps = self
343 .controller
344 .scan_async(config)
345 .await
346 .map_err(Error::Wifi)?;
347 aps.sort_unstable_by_key(|ap| core::cmp::Reverse(ap.signal_strength));
348 Ok(aps)
349 }
350
351 pub fn controller(&mut self) -> &mut WifiController<'d> {
353 &mut self.controller
354 }
355}