rpi_host/manager.rs
1use std::time::Duration;
2
3use crate::error::{WifiError, WifiResult};
4use crate::nmcli::NmCli;
5use crate::types::{
6 ConnectionStatus, ConnectivityResult, HotspotConfig, NetworkInfo, WifiMode,
7};
8use log::{debug, info, warn};
9
10/// Default targets for internet connectivity checks
11const DEFAULT_CONNECTIVITY_TARGETS: &[&str] = &[
12 "8.8.8.8", // Google DNS
13 "1.1.1.1", // Cloudflare DNS
14 "208.67.222.222", // OpenDNS
15];
16
17/// High-level WiFi manager for Raspberry Pi
18///
19/// Provides an easy-to-use API for switching between client and hotspot modes,
20/// scanning networks, and checking internet connectivity.
21///
22/// # Example
23///
24/// ```no_run
25/// use rpi_host::WifiManager;
26///
27/// let wifi = WifiManager::new()?;
28///
29/// // Connect to a network
30/// wifi.connect("MyNetwork", Some("password123"))?;
31///
32/// // Check internet connectivity
33/// if wifi.has_internet()? {
34/// println!("Connected to the internet!");
35/// }
36///
37/// // Start a hotspot
38/// wifi.start_hotspot("MyHotspot", Some("hotspotpass"))?;
39/// # Ok::<(), rpi_host::WifiError>(())
40/// ```
41pub struct WifiManager {
42 nmcli: NmCli,
43}
44
45impl WifiManager {
46 /// Create a new WifiManager using the default interface (wlan0)
47 ///
48 /// # Errors
49 ///
50 /// Returns an error if the interface doesn't exist or WiFi is not available.
51 pub fn new() -> WifiResult<Self> {
52 Self::with_interface("wlan0")
53 }
54
55 /// Create a new WifiManager for a specific interface
56 ///
57 /// # Arguments
58 ///
59 /// * `interface` - The name of the wireless interface (e.g., "wlan0", "wlan1")
60 ///
61 /// # Errors
62 ///
63 /// Returns an error if the interface doesn't exist.
64 pub fn with_interface(interface: impl Into<String>) -> WifiResult<Self> {
65 let interface = interface.into();
66 let nmcli = NmCli::new(&interface);
67
68 // Verify the interface exists
69 if !nmcli.interface_exists()? {
70 return Err(WifiError::InterfaceNotFound(interface));
71 }
72
73 info!("WifiManager initialized for interface: {}", interface);
74 Ok(Self { nmcli })
75 }
76
77 /// Get the wireless interface name
78 pub fn interface(&self) -> &str {
79 self.nmcli.interface()
80 }
81
82 /// Get the current WiFi mode (Client, Hotspot, or Disconnected)
83 pub fn get_mode(&self) -> WifiResult<WifiMode> {
84 let active_conn = self.nmcli.get_active_connection()?;
85
86 match active_conn {
87 None => Ok(WifiMode::Disconnected),
88 Some(_) => {
89 if self.nmcli.is_hotspot_active()? {
90 Ok(WifiMode::Hotspot)
91 } else {
92 Ok(WifiMode::Client)
93 }
94 }
95 }
96 }
97
98 /// Get detailed status of the current connection
99 pub fn status(&self) -> WifiResult<ConnectionStatus> {
100 let mode = self.get_mode()?;
101 let connection_name = self.nmcli.get_active_connection()?;
102 let ip_address = self.nmcli.get_ip_address()?;
103 let has_internet = if mode == WifiMode::Client {
104 self.has_internet().unwrap_or(false)
105 } else {
106 false
107 };
108
109 Ok(ConnectionStatus {
110 mode,
111 connection_name,
112 ip_address,
113 has_internet,
114 interface: self.interface().to_string(),
115 })
116 }
117
118 /// Scan for available WiFi networks
119 ///
120 /// Returns a list of networks sorted by signal strength (strongest first).
121 pub fn scan(&self) -> WifiResult<Vec<NetworkInfo>> {
122 info!("Scanning for WiFi networks on {}", self.interface());
123 self.nmcli.scan_networks()
124 }
125
126 /// Connect to a WiFi network as a client
127 ///
128 /// # Arguments
129 ///
130 /// * `ssid` - The network name to connect to
131 /// * `password` - Optional password for the network
132 ///
133 /// # Example
134 ///
135 /// ```no_run
136 /// # use rpi_host::WifiManager;
137 /// # let wifi = WifiManager::new()?;
138 /// // Connect to an open network
139 /// wifi.connect("OpenNetwork", None)?;
140 ///
141 /// // Connect to a secured network
142 /// wifi.connect("SecuredNetwork", Some("mypassword"))?;
143 /// # Ok::<(), rpi_host::WifiError>(())
144 /// ```
145 pub fn connect(&self, ssid: impl AsRef<str>, password: Option<&str>) -> WifiResult<()> {
146 let ssid = ssid.as_ref();
147 info!("Connecting to network: {}", ssid);
148
149 // Ensure WiFi is enabled
150 self.ensure_wifi_enabled()?;
151
152 // If currently in hotspot mode, stop it first
153 if self.get_mode()? == WifiMode::Hotspot {
154 debug!("Stopping active hotspot before connecting");
155 self.stop_hotspot()?;
156 }
157
158 // Connect to the network
159 self.nmcli.connect(ssid, password)?;
160
161 // Wait a moment for connection to establish
162 std::thread::sleep(Duration::from_secs(2));
163
164 // Verify connection
165 let active = self.nmcli.get_active_connection()?;
166 if active.as_deref() == Some(ssid) {
167 info!("Successfully connected to {}", ssid);
168 Ok(())
169 } else {
170 Err(WifiError::ConnectionFailed {
171 ssid: ssid.to_string(),
172 reason: "Connection did not establish".to_string(),
173 })
174 }
175 }
176
177 /// Connect to a WiFi network and wait for internet connectivity
178 ///
179 /// Similar to `connect()`, but also waits for and verifies internet connectivity.
180 ///
181 /// # Arguments
182 ///
183 /// * `ssid` - The network name to connect to
184 /// * `password` - Optional password for the network
185 /// * `timeout` - Maximum time to wait for internet connectivity
186 pub fn connect_with_internet(
187 &self,
188 ssid: impl AsRef<str>,
189 password: Option<&str>,
190 timeout: Duration,
191 ) -> WifiResult<()> {
192 self.connect(ssid.as_ref(), password)?;
193
194 // Wait for internet connectivity
195 let start = std::time::Instant::now();
196 while start.elapsed() < timeout {
197 if self.has_internet()? {
198 return Ok(());
199 }
200 std::thread::sleep(Duration::from_millis(500));
201 }
202
203 Err(WifiError::NoInternetConnectivity)
204 }
205
206 /// Disconnect from the current network
207 pub fn disconnect(&self) -> WifiResult<()> {
208 info!("Disconnecting from current network");
209 self.nmcli.disconnect()
210 }
211
212 /// Start a WiFi hotspot with default settings
213 ///
214 /// # Arguments
215 ///
216 /// * `ssid` - The name for the hotspot
217 /// * `password` - Optional password (must be at least 8 characters if provided)
218 ///
219 /// # Example
220 ///
221 /// ```no_run
222 /// # use rpi_host::WifiManager;
223 /// # let wifi = WifiManager::new()?;
224 /// // Create an open hotspot
225 /// wifi.start_hotspot("MyPiHotspot", None)?;
226 ///
227 /// // Create a secured hotspot
228 /// wifi.start_hotspot("MyPiHotspot", Some("password123"))?;
229 /// # Ok::<(), rpi_host::WifiError>(())
230 /// ```
231 pub fn start_hotspot(&self, ssid: impl Into<String>, password: Option<&str>) -> WifiResult<()> {
232 let config = HotspotConfig::new(ssid);
233 let config = if let Some(pwd) = password {
234 config.with_password(pwd)
235 } else {
236 config
237 };
238 self.start_hotspot_with_config(config)
239 }
240
241 /// Start a WiFi hotspot with custom configuration
242 ///
243 /// # Example
244 ///
245 /// ```no_run
246 /// # use rpi_host::{WifiManager, HotspotConfig, HotspotBand};
247 /// # let wifi = WifiManager::new()?;
248 /// let config = HotspotConfig::new("MyHotspot")
249 /// .with_password("securepass")
250 /// .with_band(HotspotBand::A) // Use 5GHz
251 /// .with_channel(36);
252 ///
253 /// wifi.start_hotspot_with_config(config)?;
254 /// # Ok::<(), rpi_host::WifiError>(())
255 /// ```
256 pub fn start_hotspot_with_config(&self, config: HotspotConfig) -> WifiResult<()> {
257 info!("Starting hotspot: {}", config.ssid);
258
259 // Ensure WiFi is enabled
260 self.ensure_wifi_enabled()?;
261
262 // Disconnect from any current connection
263 let current_mode = self.get_mode()?;
264 if current_mode != WifiMode::Disconnected {
265 debug!("Disconnecting from current connection before starting hotspot");
266 self.disconnect()?;
267 std::thread::sleep(Duration::from_secs(1));
268 }
269
270 // Create the hotspot
271 self.nmcli.create_hotspot(
272 &config.ssid,
273 config.password.as_deref(),
274 &config.band.to_string(),
275 config.channel,
276 )?;
277
278 // Wait for hotspot to start
279 std::thread::sleep(Duration::from_secs(2));
280
281 // Verify hotspot is active
282 if self.nmcli.is_hotspot_active()? {
283 info!("Hotspot {} started successfully", config.ssid);
284 Ok(())
285 } else {
286 Err(WifiError::HotspotCreationFailed(
287 "Hotspot did not start".to_string(),
288 ))
289 }
290 }
291
292 /// Stop the current hotspot
293 pub fn stop_hotspot(&self) -> WifiResult<()> {
294 info!("Stopping hotspot");
295 self.nmcli.stop_hotspot()
296 }
297
298 /// Check if internet is currently available
299 ///
300 /// Performs a quick connectivity check to verify internet access.
301 pub fn has_internet(&self) -> WifiResult<bool> {
302 // First try NetworkManager's built-in check
303 if let Ok(connected) = self.nmcli.nm_connectivity_check() {
304 return Ok(connected);
305 }
306
307 // Fall back to ping check
308 self.check_connectivity().map(|r| r.is_connected)
309 }
310
311 /// Check internet connectivity with detailed results
312 ///
313 /// # Example
314 ///
315 /// ```no_run
316 /// # use rpi_host::WifiManager;
317 /// # let wifi = WifiManager::new()?;
318 /// let result = wifi.check_connectivity()?;
319 /// if result.is_connected {
320 /// println!("Connected! Latency: {:?}ms", result.latency_ms);
321 /// }
322 /// # Ok::<(), rpi_host::WifiError>(())
323 /// ```
324 pub fn check_connectivity(&self) -> WifiResult<ConnectivityResult> {
325 self.check_connectivity_to(DEFAULT_CONNECTIVITY_TARGETS[0], Duration::from_secs(5))
326 }
327
328 /// Check connectivity to a specific target
329 ///
330 /// # Arguments
331 ///
332 /// * `target` - IP address or hostname to ping
333 /// * `timeout` - Maximum time to wait for response
334 pub fn check_connectivity_to(
335 &self,
336 target: &str,
337 timeout: Duration,
338 ) -> WifiResult<ConnectivityResult> {
339 debug!("Checking connectivity to {}", target);
340
341 let latency = self.nmcli.check_connectivity(target, timeout.as_secs())?;
342
343 Ok(ConnectivityResult {
344 is_connected: latency.is_some(),
345 latency_ms: latency,
346 target: target.to_string(),
347 })
348 }
349
350 /// Check connectivity to multiple targets and return true if any succeed
351 ///
352 /// This is more reliable than checking a single target.
353 pub fn check_connectivity_multi(&self, targets: &[&str]) -> WifiResult<bool> {
354 for target in targets {
355 if let Ok(Some(_)) = self.nmcli.check_connectivity(target, 3) {
356 return Ok(true);
357 }
358 }
359 Ok(false)
360 }
361
362 /// Wait for internet connectivity with timeout
363 ///
364 /// Useful after connecting to a network to wait for DHCP and routing to complete.
365 pub fn wait_for_internet(&self, timeout: Duration) -> WifiResult<()> {
366 let start = std::time::Instant::now();
367
368 while start.elapsed() < timeout {
369 if self.has_internet()? {
370 return Ok(());
371 }
372 std::thread::sleep(Duration::from_millis(500));
373 }
374
375 Err(WifiError::Timeout(
376 "Waiting for internet connectivity".to_string(),
377 ))
378 }
379
380 /// Enable WiFi if it's currently disabled
381 pub fn enable_wifi(&self) -> WifiResult<()> {
382 self.nmcli.enable_wifi()
383 }
384
385 /// Disable WiFi
386 pub fn disable_wifi(&self) -> WifiResult<()> {
387 self.nmcli.disable_wifi()
388 }
389
390 /// Check if WiFi is enabled
391 pub fn is_wifi_enabled(&self) -> WifiResult<bool> {
392 self.nmcli.is_wifi_enabled()
393 }
394
395 /// Delete a saved network connection profile
396 pub fn forget_network(&self, ssid: &str) -> WifiResult<()> {
397 info!("Forgetting network: {}", ssid);
398 self.nmcli.delete_connection(ssid)
399 }
400
401 /// Ensure WiFi is enabled, enabling it if necessary
402 fn ensure_wifi_enabled(&self) -> WifiResult<()> {
403 if !self.is_wifi_enabled()? {
404 warn!("WiFi is disabled, enabling it");
405 self.enable_wifi()?;
406 std::thread::sleep(Duration::from_secs(1));
407 }
408 Ok(())
409 }
410}
411
412impl std::fmt::Debug for WifiManager {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("WifiManager")
415 .field("interface", &self.interface())
416 .finish()
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 // Note: These tests require actual hardware and root permissions to run
425 // They are marked as ignored by default
426
427 #[test]
428 #[ignore]
429 fn test_wifi_manager_creation() {
430 let wifi = WifiManager::new();
431 assert!(wifi.is_ok());
432 }
433
434 #[test]
435 #[ignore]
436 fn test_scan_networks() {
437 let wifi = WifiManager::new().unwrap();
438 let networks = wifi.scan();
439 assert!(networks.is_ok());
440 }
441
442 #[test]
443 #[ignore]
444 fn test_get_mode() {
445 let wifi = WifiManager::new().unwrap();
446 let mode = wifi.get_mode();
447 assert!(mode.is_ok());
448 }
449}