Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/advanced/dbus.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion docs/src/api/network-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/src/guide/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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

7 changes: 4 additions & 3 deletions nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
100 changes: 90 additions & 10 deletions nmrs/src/monitoring/network.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn Stream<Item = NetworkChange> + 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.
Expand All @@ -44,7 +56,8 @@ where
let devices = nm.get_devices().await?;

// Use dynamic dispatch to handle different signal stream types
let mut streams: Vec<Pin<Box<dyn Stream<Item = _> + Send>>> = Vec::new();
let mut streams: Vec<NetworkChangeStream> = Vec::new();
let mut monitored_access_points = HashSet::new();

// Subscribe to signals on all Wi-Fi devices
for dev_path in devices {
Expand All @@ -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() {
Expand All @@ -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,
}
}
Expand All @@ -107,3 +170,20 @@ where

Err(ConnectionError::Stuck("monitoring stream ended".into()))
}

async fn access_point_strength_stream(
conn: &Connection,
ap_path: OwnedObjectPath,
) -> Result<NetworkChangeStream> {
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))
}