Skip to main content

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}