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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nmrs-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories = ["gui"]
publish = false

[dependencies]
nmrs = { path = "../nmrs", version = "1.1.0" }
nmrs = { version = "2.0.0-dev", path = "../nmrs"}
gtk = { version = "0.10.3", package = "gtk4" }
glib = "0.21.5"
tokio = { version = "1.48.0", features = ["full"] }
Expand Down
2 changes: 1 addition & 1 deletion nmrs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nmrs"
version = "1.1.0"
version = "2.0.0-dev"
authors = ["Akrm Al-Hakimi <alhakimiakrmj@gmail.com>"]
edition.workspace = true
rust-version = "1.78.0"
Expand Down
21 changes: 15 additions & 6 deletions nmrs/examples/bluetooth_connect.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/// Connect to a Bluetooth device using NetworkManager.
use nmrs::models::BluetoothIdentity;
use nmrs::{NetworkManager, Result};

#[tokio::main]
async fn main() -> Result<()> {
let nm = NetworkManager::new().await?;
Expand All @@ -17,18 +16,20 @@ async fn main() -> Result<()> {
return Ok(());
}

// This will print all devices that have been explicitly paired using
// `bluetoothctl pair <MAC_ADDRESS>`
println!("\nAvailable Bluetooth devices:");
for (i, device) in devices.iter().enumerate() {
println!(" {}. {}", i + 1, device);
}

// Example: Connect to the fourth device
if let Some(device) = devices.get(3) {
// Connect to the first device in the list
if let Some(device) = devices.first() {
println!("\nConnecting to: {}", device);

let settings = BluetoothIdentity {
bdaddr: device.bdaddr.clone(),
bt_device_type: device.bt_device_type.clone(),
bt_device_type: device.bt_caps.into(),
};

let name = device
Expand All @@ -39,9 +40,17 @@ async fn main() -> Result<()> {
.unwrap_or("Bluetooth Device");

match nm.connect_bluetooth(name, &settings).await {
Ok(_) => println!("✓ Successfully connected to {}", name),
Err(e) => eprintln!("✗ Failed to connect: {}", e),
Ok(_) => println!("✓ Successfully connected to {name}"),
Err(e) => {
eprintln!("✗ Failed to connect: {}", e);
return Ok(());
}
}

/* match nm.forget_bluetooth(name).await {
Ok(_) => println!("Disconnected {name}"),
Err(e) => eprintln!("Failed to forget: {e}"),
}*/
}

Ok(())
Expand Down
19 changes: 12 additions & 7 deletions nmrs/src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,11 +888,12 @@ pub struct BluetoothIdentity {
/// ```rust
/// use nmrs::models::{BluetoothDevice, BluetoothNetworkRole, DeviceState};
///
/// let role = BluetoothNetworkRole::PanU as u32;
/// let bt_device = BluetoothDevice {
/// bdaddr: "00:1A:7D:DA:71:13".into(),
/// name: Some("Foo".into()),
/// alias: Some("Bar".into()),
/// bt_device_type: BluetoothNetworkRole::PanU,
/// bt_caps: role,
/// state: DeviceState::Activated,
/// };
/// ```
Expand All @@ -905,7 +906,7 @@ pub struct BluetoothDevice {
/// Device alias from BlueZ
pub alias: Option<String>,
/// Bluetooth device type (DUN or PANU)
pub bt_device_type: BluetoothNetworkRole,
pub bt_caps: u32,
/// Current device state
pub state: DeviceState,
}
Expand Down Expand Up @@ -991,11 +992,12 @@ impl Display for Device {
/// Formats the device information as "alias (device_type) [bdaddr]".
impl Display for BluetoothDevice {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let role = BluetoothNetworkRole::from(self.bt_caps);
write!(
f,
"{} ({}) [{}]",
self.alias.as_deref().unwrap_or("unknown"),
self.bt_device_type,
role,
self.bdaddr
)
}
Expand Down Expand Up @@ -1823,28 +1825,30 @@ mod tests {

#[test]
fn test_bluetooth_device_creation() {
let role = BluetoothNetworkRole::PanU as u32;
let device = BluetoothDevice {
bdaddr: "00:1A:7D:DA:71:13".into(),
name: Some("MyPhone".into()),
alias: Some("Phone".into()),
bt_device_type: BluetoothNetworkRole::PanU,
bt_caps: role,
state: DeviceState::Activated,
};

assert_eq!(device.bdaddr, "00:1A:7D:DA:71:13");
assert_eq!(device.name, Some("MyPhone".into()));
assert_eq!(device.alias, Some("Phone".into()));
assert!(matches!(device.bt_device_type, BluetoothNetworkRole::PanU));
assert!(matches!(device.bt_caps, _role));
assert_eq!(device.state, DeviceState::Activated);
}

#[test]
fn test_bluetooth_device_display() {
let role = BluetoothNetworkRole::PanU as u32;
let device = BluetoothDevice {
bdaddr: "00:1A:7D:DA:71:13".into(),
name: Some("MyPhone".into()),
alias: Some("Phone".into()),
bt_device_type: BluetoothNetworkRole::PanU,
bt_caps: role,
state: DeviceState::Activated,
};

Expand All @@ -1856,11 +1860,12 @@ mod tests {

#[test]
fn test_bluetooth_device_display_no_alias() {
let role = BluetoothNetworkRole::Dun as u32;
let device = BluetoothDevice {
bdaddr: "00:1A:7D:DA:71:13".into(),
name: Some("MyPhone".into()),
alias: None,
bt_device_type: BluetoothNetworkRole::Dun,
bt_caps: role,
state: DeviceState::Disconnected,
};

Expand Down
51 changes: 46 additions & 5 deletions nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use zbus::Connection;

use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity};
use crate::core::bluetooth::connect_bluetooth;
use crate::core::connection::{connect, connect_wired, forget};
use crate::core::connection::{connect, connect_wired, forget_by_name_and_type};
use crate::core::connection_settings::{get_saved_connection_path, has_saved_connection};
use crate::core::device::{
list_bluetooth_devices, list_devices, set_wifi_enabled, wait_for_wifi_ready, wifi_enabled,
Expand All @@ -16,6 +16,7 @@ use crate::monitoring::device as device_monitor;
use crate::monitoring::info::show_details;
use crate::monitoring::network as network_monitor;
use crate::monitoring::wifi::{current_connection_info, current_ssid};
use crate::types::constants::device_type;
use crate::Result;

/// High-level interface to NetworkManager over D-Bus.
Expand Down Expand Up @@ -173,6 +174,23 @@ impl NetworkManager {
/// Connects to a bluetooth device using the provided identity.
///
/// # Example
///
/// ```no_run
/// use nmrs::{NetworkManager, models::BluetoothIdentity, models::BluetoothNetworkRole};
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// let identity = BluetoothIdentity {
/// bdaddr: "C8:1F:E8:F0:51:57".into(),
/// bt_device_type: BluetoothNetworkRole::PanU,
/// };
///
/// nm.connect_bluetooth("My Phone", &identity).await?;
/// Ok(())
/// }
///
/// ```
pub async fn connect_bluetooth(&self, name: &str, identity: &BluetoothIdentity) -> Result<()> {
connect_bluetooth(&self.conn, name, identity).await
}
Expand All @@ -186,7 +204,7 @@ impl NetworkManager {
///
/// # Example
///
/// ```no_run
/// ```rust
/// use nmrs::{NetworkManager, VpnCredentials, VpnType, WireGuardPeer};
///
/// # async fn example() -> nmrs::Result<()> {
Expand Down Expand Up @@ -379,11 +397,34 @@ impl NetworkManager {
get_saved_connection_path(&self.conn, ssid).await
}

/// Forgets (deletes) a saved connection for the given SSID.
/// Forgets (deletes) a saved WiFi connection for the given SSID.
///
/// If currently connected to this network, disconnects first.
/// If currently connected to this network, disconnects first, then deletes
/// all saved connection profiles matching the SSID.
///
/// # Returns
///
/// Returns `Ok(())` if at least one connection was deleted successfully.
/// Returns `NoSavedConnection` if no matching connections were found.
pub async fn forget(&self, ssid: &str) -> Result<()> {
forget(&self.conn, ssid).await
forget_by_name_and_type(&self.conn, ssid, Some(device_type::WIFI)).await
}

/// Forgets (deletes) a saved Bluetooth connection.
///
/// If currently connected to this device, it will disconnect first before
/// deleting the connection profile. Can match by connection name or bdaddr.
///
/// # Arguments
///
/// * `name` - Connection name or bdaddr to forget
///
/// # Returns
///
/// Returns `Ok(())` if the connection was deleted successfully.
/// Returns `NoSavedConnection` if no matching connection was found.
pub async fn forget_bluetooth(&self, name: &str) -> Result<()> {
forget_by_name_and_type(&self.conn, name, Some(device_type::BLUETOOTH)).await
}

/// Monitors Wi-Fi network changes in real-time.
Expand Down
68 changes: 64 additions & 4 deletions nmrs/src/core/bluetooth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
use log::debug;
use zbus::Connection;
use zvariant::OwnedObjectPath;
// use futures_timer::Delay;

use crate::builders::bluetooth;
use crate::core::connection_settings::get_saved_connection_path;
use crate::dbus::BluezDeviceExtProxy;
use crate::core::state_wait::{wait_for_connection_activation, wait_for_device_disconnect};
use crate::dbus::{BluezDeviceExtProxy, NMDeviceProxy};
use crate::monitoring::bluetooth::Bluetooth;
use crate::monitoring::transport::ActiveTransport;
use crate::types::constants::device_state;
use crate::types::constants::device_type;
use crate::ConnectionError;
use crate::{dbus::NMProxy, models::BluetoothIdentity, Result};

Expand Down Expand Up @@ -50,6 +54,24 @@ pub(crate) async fn populate_bluez_info(
}
}

pub(crate) async fn find_bluetooth_device(
conn: &Connection,
nm: &NMProxy<'_>,
) -> Result<OwnedObjectPath> {
let devices = nm.get_devices().await?;

for dp in devices {
let dev = NMDeviceProxy::builder(conn)
.path(dp.clone())?
.build()
.await?;
if dev.device_type().await? == device_type::BLUETOOTH {
return Ok(dp);
}
}
Err(ConnectionError::NoBluetoothDevice)
}

/// Connects to a Bluetooth device using NetworkManager.
///
/// This function establishes a Bluetooth network connection. The flow:
Expand Down Expand Up @@ -105,8 +127,7 @@ pub(crate) async fn connect_bluetooth(
// Find the Bluetooth hardware adapter
// Note: Unlike WiFi, Bluetooth connections in NetworkManager don't require
// specifying a specific device. We use "/" to let NetworkManager auto-select.
let bt_device = OwnedObjectPath::try_from("/")
.map_err(|e| ConnectionError::InvalidAddress(format!("Invalid device path: {}", e)))?;
let bt_device = find_bluetooth_device(conn, &nm).await?;
debug!("Using auto-select device path for Bluetooth connection");

// Check for saved connection
Expand Down Expand Up @@ -156,14 +177,53 @@ pub(crate) async fn connect_bluetooth(
)
.await?;

crate::core::state_wait::wait_for_connection_activation(conn, &active_conn).await?;
wait_for_connection_activation(conn, &active_conn).await?;
}
}

log::info!("Successfully connected to Bluetooth device '{name}'");
Ok(())
}

/// Disconnects a Bluetooth device and waits for it to reach disconnected state.
///
/// Calls the Disconnect method on the device and waits for the `StateChanged`
/// signal to indicate the device has reached Disconnected or Unavailable state.
pub(crate) async fn disconnect_bluetooth_and_wait(
conn: &Connection,
dev_path: &OwnedObjectPath,
) -> Result<()> {
let dev = NMDeviceProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;

// Check if already disconnected
let current_state = dev.state().await?;
if current_state == device_state::DISCONNECTED || current_state == device_state::UNAVAILABLE {
debug!("Bluetooth device already disconnected");
return Ok(());
}

let raw: zbus::proxy::Proxy = zbus::proxy::Builder::new(conn)
.destination("org.freedesktop.NetworkManager")?
.path(dev_path.clone())?
.interface("org.freedesktop.NetworkManager.Device")?
.build()
.await?;

debug!("Sending disconnect request to Bluetooth device");
let _ = raw.call_method("Disconnect", &()).await;

// Wait for disconnect using signal-based monitoring
wait_for_device_disconnect(&dev).await?;

// Brief stabilization delay
// Delay::new(timeouts::stabilization_delay()).await;

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading