use log::debug;
use zbus::Connection;
use zvariant::OwnedObjectPath;
use crate::ConnectionError;
use crate::builders::bluetooth;
use crate::core::connection_settings::get_saved_connection_path;
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::util::utils::bluez_device_path;
use crate::util::validation::validate_bluetooth_address;
use crate::{
Result,
dbus::NMProxy,
models::{BluetoothIdentity, TimeoutConfig},
};
pub(crate) async fn populate_bluez_info(
conn: &Connection,
bdaddr: &str,
) -> Result<(Option<String>, Option<String>)> {
validate_bluetooth_address(bdaddr)?;
let bluez_path = bluez_device_path(bdaddr);
match BluezDeviceExtProxy::builder(conn)
.path(bluez_path)?
.build()
.await
{
Ok(proxy) => {
let name = proxy.name().await.ok();
let alias = proxy.alias().await.ok();
Ok((name, alias))
}
Err(_) => Ok((None, None)),
}
}
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)
}
pub(crate) async fn connect_bluetooth(
conn: &Connection,
name: &str,
settings: &BluetoothIdentity,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
debug!(
"Connecting to '{}' (Bluetooth) | bdaddr={} type={:?}",
name, settings.bdaddr, settings.bt_device_type
);
let nm = NMProxy::new(conn).await?;
if let Some(active) = Bluetooth::current(conn).await {
debug!("Currently connected to Bluetooth device: {active}");
if active == settings.bdaddr {
debug!("Already connected to {active}, skipping connect()");
return Ok(());
}
} else {
debug!("Not currently connected to any Bluetooth device");
}
let bt_device = find_bluetooth_device(conn, &nm).await?;
debug!("Using auto-select device path for Bluetooth connection");
let saved = get_saved_connection_path(conn, name).await?;
let specific_object = OwnedObjectPath::try_from(bluez_device_path(&settings.bdaddr))
.map_err(|e| ConnectionError::InvalidAddress(format!("Invalid BlueZ path: {e}")))?;
match saved {
Some(saved_path) => {
debug!(
"Activating saved Bluetooth connection: {}",
saved_path.as_str()
);
let active_conn = nm
.activate_connection(saved_path, bt_device.clone(), specific_object)
.await?;
let timeout = timeout_config.map(|c| c.connection_timeout);
crate::core::state_wait::wait_for_connection_activation(conn, &active_conn, timeout)
.await?;
}
None => {
debug!("No saved connection found, creating new Bluetooth connection");
let opts = crate::api::models::ConnectionOptions {
autoconnect: false, autoconnect_priority: None,
autoconnect_retries: None,
};
let connection_settings = bluetooth::build_bluetooth_connection(name, settings, &opts);
debug!(
"Creating Bluetooth connection with settings: {:#?}",
connection_settings
);
let (_, active_conn) = nm
.add_and_activate_connection(
connection_settings,
bt_device.clone(),
specific_object,
)
.await?;
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
}
}
log::info!("Successfully connected to Bluetooth device '{name}'");
Ok(())
}
pub(crate) async fn disconnect_bluetooth_and_wait(
conn: &Connection,
dev_path: &OwnedObjectPath,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
let dev = NMDeviceProxy::builder(conn)
.path(dev_path.clone())?
.build()
.await?;
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;
let timeout = timeout_config.map(|c| c.disconnect_timeout);
wait_for_device_disconnect(&dev, timeout).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::BluetoothNetworkRole;
#[test]
fn test_bluez_path_format() {
assert_eq!(
bluez_device_path("00:1A:7D:DA:71:13"),
"/org/bluez/hci0/dev_00_1A_7D_DA_71_13"
);
}
#[test]
fn test_bluez_path_format_various_addresses() {
let test_cases = [
("AA:BB:CC:DD:EE:FF", "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF"),
("00:00:00:00:00:00", "/org/bluez/hci0/dev_00_00_00_00_00_00"),
("C8:1F:E8:F0:51:57", "/org/bluez/hci0/dev_C8_1F_E8_F0_51_57"),
];
for (bdaddr, expected) in test_cases {
assert_eq!(
bluez_device_path(bdaddr),
expected,
"Failed for bdaddr: {bdaddr}"
);
}
}
#[test]
fn test_bluetooth_identity_structure() {
let identity =
BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap();
assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13");
assert!(matches!(
identity.bt_device_type,
BluetoothNetworkRole::PanU
));
}
}