diff --git a/docs/src/advanced/dbus.md b/docs/src/advanced/dbus.md index 161ec37c..5ce8d026 100644 --- a/docs/src/advanced/dbus.md +++ b/docs/src/advanced/dbus.md @@ -55,8 +55,8 @@ nmrs: nm.list_devices() ``` nmrs: nm.monitor_network_changes(callback) - → D-Bus: Subscribe to AccessPointAdded/Removed signals - ← D-Bus: Signal whenever an AP appears or disappears + → D-Bus: Subscribe to AccessPointAdded/Removed and AP Strength changes + ← D-Bus: Signal whenever an AP appears, disappears, or changes strength → nmrs: Invoke callback ``` diff --git a/docs/src/api/network-manager.md b/docs/src/api/network-manager.md index 637dce68..544c3322 100644 --- a/docs/src/api/network-manager.md +++ b/docs/src/api/network-manager.md @@ -91,7 +91,7 @@ let config = nm.timeout_config(); | Method | Returns | Description | |--------|---------|-------------| -| `monitor_network_changes(callback)` | `Result<()>` | Watch for AP changes (runs forever) | +| `monitor_network_changes(callback)` | `Result<()>` | Watch for AP and signal strength changes (runs forever) | | `monitor_device_changes(callback)` | `Result<()>` | Watch for device state changes (runs forever) | ## Thread Safety diff --git a/docs/src/guide/monitoring.md b/docs/src/guide/monitoring.md index 3cbee9f7..1347420a 100644 --- a/docs/src/guide/monitoring.md +++ b/docs/src/guide/monitoring.md @@ -4,7 +4,8 @@ nmrs uses D-Bus signals to provide real-time notifications when network state ch ## Network Change Monitoring -Subscribe to network list changes (access points appearing or disappearing): +Subscribe to network changes (access points appearing or disappearing, or signal +strength changing): ```rust use nmrs::NetworkManager; @@ -22,7 +23,7 @@ async fn main() -> nmrs::Result<()> { } ``` -`monitor_network_changes()` subscribes to D-Bus signals for access point additions and removals on all Wi-Fi devices. The callback fires whenever the visible network list changes. +`monitor_network_changes()` subscribes to D-Bus signals for access point additions, removals, and signal strength updates on all Wi-Fi devices. The callback fires whenever the visible network list or signal data changes. ## Device State Monitoring diff --git a/nmrs/CHANGELOG.md b/nmrs/CHANGELOG.md index 1c29e985..4c9835ae 100644 --- a/nmrs/CHANGELOG.md +++ b/nmrs/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the `nmrs` crate will be documented in this file. - Support for specifying Bluetooth adapter in `BluetoothIdentity` ([#267](https://github.com/cachebag/nmrs/pull/267)) ### Fixed +- `monitor_network_changes` now fires for Wi-Fi access point signal strength changes, not only access point additions and removals ([#363](https://github.com/cachebag/nmrs/issues/363)) - Add `Send` bound to monitoring stream trait objects so `monitor_network_changes` and `monitor_device_changes` work with `tokio::spawn` ([#359](https://github.com/cachebag/nmrs/pull/359)) - Line-accurate source locations for `.ovpn` directives and blocks ([#318](https://github.com/cachebag/nmrs/pull/318)) - `key_direction` when nested under `tls_auth` and as a standalone directive ([#320](https://github.com/cachebag/nmrs/pull/320)) @@ -215,4 +216,3 @@ All notable changes to the `nmrs` crate will be documented in this file. [0.2.0-beta]: https://github.com/cachebag/nmrs/compare/v0.1.1-beta...v0.2.0-beta [0.1.1-beta]: https://github.com/cachebag/nmrs/compare/v0.1.0-beta...v0.1.1-beta [0.1.0-beta]: https://github.com/cachebag/nmrs/releases/tag/v0.1.0-beta - diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index de22b056..6715dfcc 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -632,9 +632,10 @@ impl NetworkManager { .await } /// - /// Subscribes to D-Bus signals for access point additions and removals - /// on all Wi-Fi devices. Invokes the callback whenever the network list - /// changes, enabling live UI updates without polling. + /// Subscribes to D-Bus signals for access point additions, removals, and + /// signal strength changes on all Wi-Fi devices. Invokes the callback + /// whenever the network list or signal data changes, enabling live UI + /// updates without polling. /// /// This function runs indefinitely until an error occurs. Run it in a /// background task. diff --git a/nmrs/src/monitoring/network.rs b/nmrs/src/monitoring/network.rs index 84ce776e..d7259ed5 100644 --- a/nmrs/src/monitoring/network.rs +++ b/nmrs/src/monitoring/network.rs @@ -1,25 +1,37 @@ //! Real-time network monitoring using D-Bus signals. //! //! Provides functionality to monitor access point changes (additions/removals) -//! in real-time without needing to poll. This enables live UI updates. +//! and signal strength changes in real-time without needing to poll. This +//! enables live UI updates. use futures::stream::{Stream, StreamExt}; use log::{debug, warn}; +use std::collections::HashSet; use std::pin::Pin; use tokio::select; use tokio::sync::watch; use zbus::Connection; +use zvariant::OwnedObjectPath; use crate::Result; use crate::api::models::ConnectionError; -use crate::dbus::{NMDeviceProxy, NMProxy, NMWirelessProxy}; +use crate::dbus::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; use crate::types::constants::device_type; +type NetworkChangeStream = Pin + Send>>; + +enum NetworkChange { + Added(OwnedObjectPath), + Removed(OwnedObjectPath), + SignalStrengthChanged, +} + /// Monitors access point changes on all Wi-Fi devices. /// /// Subscribes to `AccessPointAdded` and `AccessPointRemoved` signals on all -/// wireless devices. When any signal is received, invokes the callback to -/// notify the caller that the network list has changed. +/// wireless devices, plus `Strength` property changes on visible access points. +/// When any signal is received, invokes the callback to notify the caller that +/// the network list or signal data has changed. /// /// This function runs indefinitely until an error occurs or the connection /// is lost. Run it in a background task. @@ -44,7 +56,8 @@ where let devices = nm.get_devices().await?; // Use dynamic dispatch to handle different signal stream types - let mut streams: Vec + Send>>> = Vec::new(); + let mut streams: Vec = Vec::new(); + let mut monitored_access_points = HashSet::new(); // Subscribe to signals on all Wi-Fi devices for dev_path in devices { @@ -65,11 +78,45 @@ where let added_stream = wifi.receive_access_point_added().await?; let removed_stream = wifi.receive_access_point_removed().await?; - // Box both streams as trait objects - streams.push(Box::pin(added_stream.map(|_| ()))); - streams.push(Box::pin(removed_stream.map(|_| ()))); + streams.push(Box::pin(added_stream.map(|signal| { + signal.args().map_or_else( + |err| { + debug!("Failed to parse AccessPointAdded signal: {err}"); + NetworkChange::SignalStrengthChanged + }, + |args| NetworkChange::Added(args.path().clone()), + ) + }))); + streams.push(Box::pin(removed_stream.map(|signal| { + signal.args().map_or_else( + |err| { + debug!("Failed to parse AccessPointRemoved signal: {err}"); + NetworkChange::SignalStrengthChanged + }, + |args| NetworkChange::Removed(args.path().clone()), + ) + }))); + + match wifi.access_points().await { + Ok(ap_paths) => { + for ap_path in ap_paths { + if !monitored_access_points.insert(ap_path.to_string()) { + continue; + } + + match access_point_strength_stream(conn, ap_path.clone()).await { + Ok(stream) => streams.push(stream), + Err(err) => debug!( + "Failed to monitor signal strength for access point {}: {}", + ap_path, err + ), + } + } + } + Err(err) => debug!("Failed to list access points on device {dev_path}: {err}"), + } - debug!("Subscribed to AP signals on device: {dev_path}"); + debug!("Subscribed to network change signals on device: {dev_path}"); } if streams.is_empty() { @@ -93,7 +140,23 @@ where } signal = merged.next() => { match signal { - Some(_) => callback(), + Some(NetworkChange::Added(path)) => { + if monitored_access_points.insert(path.to_string()) { + match access_point_strength_stream(conn, path.clone()).await { + Ok(stream) => merged.push(stream), + Err(err) => debug!( + "Failed to monitor signal strength for access point {}: {}", + path, err + ), + } + } + callback(); + } + Some(NetworkChange::Removed(path)) => { + monitored_access_points.remove(path.as_str()); + callback(); + } + Some(NetworkChange::SignalStrengthChanged) => callback(), None => break, } } @@ -107,3 +170,20 @@ where Err(ConnectionError::Stuck("monitoring stream ended".into())) } + +async fn access_point_strength_stream( + conn: &Connection, + ap_path: OwnedObjectPath, +) -> Result { + let ap = NMAccessPointProxy::builder(conn) + .path(ap_path.clone())? + .build() + .await?; + + let stream = ap.receive_strength_changed().await.skip(1).map(move |_| { + debug!("Access point signal strength changed: {ap_path}"); + NetworkChange::SignalStrengthChanged + }); + + Ok(Box::pin(stream)) +}