From cd1874688f2dc7c2da114962ffa9f9bff12fb7f7 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Mon, 22 Dec 2025 15:57:03 -0500 Subject: [PATCH 1/3] fix: proxy field name for `bd_addr` replaced with correct `hw_addr` - We now list bluetooth devices based on their capabilities, which requires us to proxy the Bluetooth interface of course - Refactored `forget()` to target more than just wifi devices, as with the case in bluetooth devices, we dont actually need all the fields wifi does - Changed the `BluetoothDevice` model to take `bt_caps` instead of the `bt_device_type` which is canonical with what NM expects from Bluetooth devices (since we can just grab the type whenever we want via `DeviceType` in `NMDeviceProxy`) - Other pedantic changes to tests, and docs due to changes above --- Cargo.lock | 101 +++++++++++++++++++ nmrs/Cargo.toml | 2 +- nmrs/examples/bluetooth_connect.rs | 21 ++-- nmrs/src/api/models.rs | 19 ++-- nmrs/src/api/network_manager.rs | 32 +++++- nmrs/src/core/bluetooth.rs | 68 ++++++++++++- nmrs/src/core/connection.rs | 152 +++++++++++++++++++++-------- nmrs/src/core/device.rs | 24 +++-- nmrs/src/dbus/bluetooth.rs | 2 +- nmrs/src/monitoring/bluetooth.rs | 4 +- nmrs/tests/integration_test.rs | 16 ++- 11 files changed, 363 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79bd2c0..8e5179e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.21" @@ -386,6 +395,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -868,6 +900,30 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -960,6 +1016,7 @@ name = "nmrs" version = "1.1.0" dependencies = [ "async-trait", + "env_logger", "futures", "futures-timer", "log", @@ -1110,6 +1167,21 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "portable-atomic" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1163,6 +1235,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rustc_version" version = "0.4.1" diff --git a/nmrs/Cargo.toml b/nmrs/Cargo.toml index f3e498f..71299ff 100644 --- a/nmrs/Cargo.toml +++ b/nmrs/Cargo.toml @@ -21,7 +21,7 @@ thiserror.workspace = true uuid.workspace = true futures.workspace = true futures-timer.workspace = true -async-trait = "0.1.89" +async-trait.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/nmrs/examples/bluetooth_connect.rs b/nmrs/examples/bluetooth_connect.rs index 92b56ec..c07eb54 100644 --- a/nmrs/examples/bluetooth_connect.rs +++ b/nmrs/examples/bluetooth_connect.rs @@ -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?; @@ -17,18 +16,20 @@ async fn main() -> Result<()> { return Ok(()); } + // This will print all devices that have been explicitly paired using + // `bluetoothctl pair ` 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 @@ -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(()) diff --git a/nmrs/src/api/models.rs b/nmrs/src/api/models.rs index 2c4115a..531323e 100644 --- a/nmrs/src/api/models.rs +++ b/nmrs/src/api/models.rs @@ -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, /// }; /// ``` @@ -905,7 +906,7 @@ pub struct BluetoothDevice { /// Device alias from BlueZ pub alias: Option, /// Bluetooth device type (DUN or PANU) - pub bt_device_type: BluetoothNetworkRole, + pub bt_caps: u32, /// Current device state pub state: DeviceState, } @@ -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 ) } @@ -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, }; @@ -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, }; diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 2d4f362..1e273e1 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -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, @@ -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. @@ -379,11 +380,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. diff --git a/nmrs/src/core/bluetooth.rs b/nmrs/src/core/bluetooth.rs index 6d2e71c..515d3ae 100644 --- a/nmrs/src/core/bluetooth.rs +++ b/nmrs/src/core/bluetooth.rs @@ -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}; @@ -50,6 +54,24 @@ pub(crate) async fn populate_bluez_info( } } +pub(crate) async fn find_bluetooth_device( + conn: &Connection, + nm: &NMProxy<'_>, +) -> Result { + 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: @@ -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 @@ -156,7 +177,7 @@ 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?; } } @@ -164,6 +185,45 @@ pub(crate) async fn connect_bluetooth( 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::*; diff --git a/nmrs/src/core/connection.rs b/nmrs/src/core/connection.rs index 3d06461..4e3db4a 100644 --- a/nmrs/src/core/connection.rs +++ b/nmrs/src/core/connection.rs @@ -159,66 +159,117 @@ pub(crate) async fn connect_wired(conn: &Connection) -> Result<()> { Ok(()) } -/// Forgets (deletes) all saved connections for a network. +/// Generic function to forget (delete) connections by name and optionally by device type. /// -/// If currently connected to this network, disconnects first, then deletes -/// all saved connection profiles matching the SSID. Matches are found by -/// both the connection ID and the wireless SSID bytes. +/// This handles disconnection if currently active, then deletes the connection profile(s). +/// Can be used for WiFi, Bluetooth, or any NetworkManager connection type. /// +/// # Arguments +/// +/// * `conn` - D-Bus connection +/// * `name` - Connection name/identifier to forget +/// * `device_filter` - Optional device type filter (e.g., `Some(device_type::BLUETOOTH)`) +/// +/// # Returns +/// +/// Returns `Ok(())` if at least one connection was deleted successfully. /// Returns `NoSavedConnection` if no matching connections were found. -pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { +pub(crate) async fn forget_by_name_and_type( + conn: &Connection, + name: &str, + device_filter: Option, +) -> Result<()> { use std::collections::HashMap; use zvariant::{OwnedObjectPath, Value}; - debug!("Starting forget operation for: {ssid}"); + debug!( + "Starting forget operation for: {name} (device filter: {:?})", + device_filter + ); let nm = NMProxy::new(conn).await?; + // Disconnect if currently active let devices = nm.get_devices().await?; for dev_path in &devices { let dev = NMDeviceProxy::builder(conn) .path(dev_path.clone())? .build() .await?; - if dev.device_type().await? != device_type::WIFI { - continue; + + let dev_type = dev.device_type().await?; + + // Skip if device type doesn't match our filter + if let Some(filter) = device_filter { + if dev_type != filter { + continue; + } } - let wifi = NMWirelessProxy::builder(conn) - .path(dev_path.clone())? - .build() - .await?; - if let Ok(ap_path) = wifi.active_access_point().await { - if ap_path.as_str() != "/" { - let ap = NMAccessPointProxy::builder(conn) - .path(ap_path.clone())? - .build() - .await?; - if let Ok(bytes) = ap.ssid().await { - if decode_ssid_or_empty(&bytes) == ssid { - debug!("Disconnecting from active network: {ssid}"); - if let Err(e) = disconnect_wifi_and_wait(conn, dev_path).await { - warn!("Disconnect wait failed: {e}"); - let final_state = dev.state().await?; - if final_state != device_state::DISCONNECTED - && final_state != device_state::UNAVAILABLE - { - error!( - "Device still connected (state: {final_state}), cannot safely delete" - ); - return Err(ConnectionError::Stuck(format!( - "disconnect failed, device in state {final_state}" - ))); + // Handle WiFi-specific disconnect logic + if dev_type == device_type::WIFI { + let wifi = NMWirelessProxy::builder(conn) + .path(dev_path.clone())? + .build() + .await?; + if let Ok(ap_path) = wifi.active_access_point().await { + if ap_path.as_str() != "/" { + let ap = NMAccessPointProxy::builder(conn) + .path(ap_path.clone())? + .build() + .await?; + if let Ok(bytes) = ap.ssid().await { + if decode_ssid_or_empty(&bytes) == name { + debug!("Disconnecting from active WiFi network: {name}"); + if let Err(e) = disconnect_wifi_and_wait(conn, dev_path).await { + warn!("Disconnect wait failed: {e}"); + let final_state = dev.state().await?; + if final_state != device_state::DISCONNECTED + && final_state != device_state::UNAVAILABLE + { + error!( + "Device still connected (state: {final_state}), cannot safely delete" + ); + return Err(ConnectionError::Stuck(format!( + "disconnect failed, device in state {final_state}" + ))); + } + debug!("Device confirmed disconnected, proceeding with deletion"); } - debug!("Device confirmed disconnected, proceeding with deletion"); + debug!("WiFi disconnect phase completed"); } - debug!("Disconnect phase completed"); } } } } + // Handle Bluetooth-specific disconnect logic + else if dev_type == device_type::BLUETOOTH { + // Check if this Bluetooth device is currently active + let state = dev.state().await?; + if state != device_state::DISCONNECTED && state != device_state::UNAVAILABLE { + debug!("Disconnecting from active Bluetooth device: {name}"); + if let Err(e) = + crate::core::bluetooth::disconnect_bluetooth_and_wait(conn, dev_path).await + { + warn!("Bluetooth disconnect failed: {e}"); + let final_state = dev.state().await?; + if final_state != device_state::DISCONNECTED + && final_state != device_state::UNAVAILABLE + { + error!( + "Bluetooth device still connected (state: {final_state}), cannot safely delete" + ); + return Err(ConnectionError::Stuck(format!( + "disconnect failed, device in state {final_state}" + ))); + } + } + debug!("Bluetooth disconnect phase completed"); + } + } } + // Delete connection profiles (generic, works for all types) debug!("Starting connection deletion phase..."); let settings: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn) @@ -247,15 +298,17 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { let mut should_delete = false; + // Match by connection ID (works for all connection types) if let Some(conn_sec) = settings_map.get("connection") { if let Some(Value::Str(id)) = conn_sec.get("id") { - if id.as_str() == ssid { + if id.as_str() == name { should_delete = true; debug!("Found connection by ID: {id}"); } } } + // Additional WiFi-specific matching by SSID if let Some(wifi_sec) = settings_map.get("802-11-wireless") { if let Some(Value::Array(arr)) = wifi_sec.get("ssid") { let mut raw = Vec::new(); @@ -264,9 +317,19 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { raw.push(b); } } - if decode_ssid_or_empty(&raw) == ssid { + if decode_ssid_or_empty(&raw) == name { should_delete = true; - debug!("Found connection by SSID match"); + debug!("Found WiFi connection by SSID match"); + } + } + } + + // Matching by bdaddr for Bluetooth connections + if let Some(bt_sec) = settings_map.get("bluetooth") { + if let Some(Value::Str(bdaddr)) = bt_sec.get("bdaddr") { + if bdaddr.as_str() == name { + should_delete = true; + debug!("Found Bluetooth connection by bdaddr match"); } } } @@ -295,11 +358,18 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { } if deleted_count > 0 { - info!("Successfully deleted {deleted_count} connection(s) for '{ssid}'"); + info!("Successfully deleted {deleted_count} connection(s) for '{name}'"); Ok(()) } else { - debug!("No saved connections found for '{ssid}'"); - Err(ConnectionError::NoSavedConnection) + debug!("No saved connections found for '{name}'"); + + // For Bluetooth, it's normal to have no NetworkManager connection profile if the device is only paired in BlueZ. + if device_filter == Some(device_type::BLUETOOTH) { + debug!("Bluetooth device '{name}' has no NetworkManager connection profile (device may only be paired in BlueZ)"); + Ok(()) + } else { + Err(ConnectionError::NoSavedConnection) + } } } diff --git a/nmrs/src/core/device.rs b/nmrs/src/core/device.rs index 33341f2..6d4af19 100644 --- a/nmrs/src/core/device.rs +++ b/nmrs/src/core/device.rs @@ -10,7 +10,7 @@ use zbus::Connection; use crate::api::models::{BluetoothDevice, ConnectionError, Device, DeviceIdentity, DeviceState}; use crate::core::bluetooth::populate_bluez_info; use crate::core::state_wait::wait_for_wifi_device_ready; -use crate::dbus::{NMDeviceProxy, NMProxy}; +use crate::dbus::{NMBluetoothProxy, NMDeviceProxy, NMProxy}; use crate::types::constants::device_type; use crate::Result; @@ -82,23 +82,28 @@ pub(crate) async fn list_bluetooth_devices(conn: &Connection) -> Result Result Result; + fn hw_address(&self) -> Result; /// Bluetooth capabilities of the device (either DUN or NAP). /// diff --git a/nmrs/src/monitoring/bluetooth.rs b/nmrs/src/monitoring/bluetooth.rs index 3fc250c..42f7215 100644 --- a/nmrs/src/monitoring/bluetooth.rs +++ b/nmrs/src/monitoring/bluetooth.rs @@ -76,7 +76,7 @@ pub(crate) async fn current_bluetooth_bdaddr(conn: &Connection) -> Option Option<(String, ); let bt = try_log!(bt_builder.build().await, "Failed to build Bluetooth proxy"); - if let (Ok(bdaddr), Ok(capabilities)) = (bt.bd_address().await, bt.bt_capabilities().await) + if let (Ok(bdaddr), Ok(capabilities)) = (bt.hw_address().await, bt.bt_capabilities().await) { return Some((bdaddr, capabilities)); } diff --git a/nmrs/tests/integration_test.rs b/nmrs/tests/integration_test.rs index 9a64dc7..cc15cb9 100644 --- a/nmrs/tests/integration_test.rs +++ b/nmrs/tests/integration_test.rs @@ -560,6 +560,7 @@ async fn test_device_states() { // Verify that all devices have valid states for device in &devices { // DeviceState should be one of the known states + // The struct is non-exhaustive and so we allow Other(_) match device.state { DeviceState::Unmanaged | DeviceState::Unavailable @@ -572,6 +573,9 @@ async fn test_device_states() { | DeviceState::Other(_) => { // Valid state } + _ => { + panic!("Invalid device state: {:?}", device.state); + } } } } @@ -589,6 +593,7 @@ async fn test_device_types() { // Verify that all devices have valid types for device in &devices { // DeviceType should be one of the known types + // The struct is non-exhaustive and so we allow Other(_) match device.device_type { DeviceType::Ethernet | DeviceType::Wifi @@ -598,6 +603,9 @@ async fn test_device_types() { | DeviceType::Other(_) => { // Valid type } + _ => { + panic!("Invalid device type: {:?}", device.device_type); + } } } } @@ -1076,7 +1084,7 @@ async fn test_list_bluetooth_devices() { "Bluetooth device: {} ({}) - {}", device.alias.as_deref().unwrap_or("unknown"), device.bdaddr, - device.bt_device_type + device.bt_caps ); } } @@ -1115,11 +1123,12 @@ fn test_bluetooth_identity_structure() { fn test_bluetooth_device_structure() { use nmrs::models::{BluetoothDevice, BluetoothNetworkRole}; + 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, }; @@ -1134,11 +1143,12 @@ fn test_bluetooth_device_structure() { fn test_bluetooth_device_display() { use nmrs::models::{BluetoothDevice, BluetoothNetworkRole}; + 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, }; From 8b2c6a0e68c477643e9a86cac6d187dc7918b86d Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Mon, 22 Dec 2025 15:58:56 -0500 Subject: [PATCH 2/3] chore: bump cargo version to `2.0.0-dev` --- Cargo.lock | 103 +------------------------------------------- nmrs-gui/Cargo.toml | 2 +- nmrs/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e5179e..b8c5c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" version = "0.6.21" @@ -395,29 +386,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -900,30 +868,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "jiff" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "js-sys" version = "0.3.81" @@ -1013,10 +957,9 @@ dependencies = [ [[package]] name = "nmrs" -version = "1.1.0" +version = "2.0.0-dev" dependencies = [ "async-trait", - "env_logger", "futures", "futures-timer", "log", @@ -1167,21 +1110,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "portable-atomic" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1235,35 +1163,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - [[package]] name = "rustc_version" version = "0.4.1" diff --git a/nmrs-gui/Cargo.toml b/nmrs-gui/Cargo.toml index fcafcd8..a4998c6 100644 --- a/nmrs-gui/Cargo.toml +++ b/nmrs-gui/Cargo.toml @@ -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"] } diff --git a/nmrs/Cargo.toml b/nmrs/Cargo.toml index 71299ff..f3e498f 100644 --- a/nmrs/Cargo.toml +++ b/nmrs/Cargo.toml @@ -21,7 +21,7 @@ thiserror.workspace = true uuid.workspace = true futures.workspace = true futures-timer.workspace = true -async-trait.workspace = true +async-trait = "0.1.89" [dev-dependencies] tokio.workspace = true From e970579c3a0bdb0d55b689227302f46b4a59c5c7 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Mon, 22 Dec 2025 16:21:00 -0500 Subject: [PATCH 3/3] docs: update example for `connect_bluetooth()` and bump version --- nmrs/Cargo.toml | 2 +- nmrs/src/api/network_manager.rs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/nmrs/Cargo.toml b/nmrs/Cargo.toml index f3e498f..1875a9d 100644 --- a/nmrs/Cargo.toml +++ b/nmrs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nmrs" -version = "1.1.0" +version = "2.0.0-dev" authors = ["Akrm Al-Hakimi "] edition.workspace = true rust-version = "1.78.0" diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 1e273e1..cccd1d6 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -174,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 } @@ -187,7 +204,7 @@ impl NetworkManager { /// /// # Example /// - /// ```no_run + /// ```rust /// use nmrs::{NetworkManager, VpnCredentials, VpnType, WireGuardPeer}; /// /// # async fn example() -> nmrs::Result<()> {