Skip to content

Commit a4d1eb9

Browse files
committed
feat(#216): multiple bt adapter support
1 parent e2add33 commit a4d1eb9

6 files changed

Lines changed: 100 additions & 28 deletions

File tree

nmrs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
All notable changes to the `nmrs` crate will be documented in this file.
44

55
## [Unreleased]
6+
### Added
7+
- Support for specifying Bluetooth adapter in `BluetoothIdentity` ([#267](https://github.com/cachebag/nmrs/pull/267))
8+
69
### Changed
710
- Convert BDADDR to BlueZ device path via `bluez_device_path` helper ([#266](https://github.com/cachebag/nmrs/pull/266))
811

nmrs/src/api/models.rs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,15 +1729,12 @@ pub struct BluetoothIdentity {
17291729
pub bdaddr: String,
17301730
/// Bluetooth device type (DUN or PANU)
17311731
pub bt_device_type: BluetoothNetworkRole,
1732+
/// BlueZ adapter name (e.g. `"hci0"`, `"hci1"`). Defaults to `"hci0"` when `None`.
1733+
pub adapter: Option<String>,
17321734
}
17331735

17341736
impl BluetoothIdentity {
1735-
/// Creates a new `BluetoothIdentity`.
1736-
///
1737-
/// # Arguments
1738-
///
1739-
/// * `bdaddr` - Bluetooth MAC address (e.g., "00:1A:7D:DA:71:13")
1740-
/// * `bt_device_type` - Bluetooth network role (PanU or Dun)
1737+
/// Creates a new `BluetoothIdentity` using the default adapter.
17411738
///
17421739
/// # Errors
17431740
///
@@ -1762,6 +1759,38 @@ impl BluetoothIdentity {
17621759
Ok(Self {
17631760
bdaddr,
17641761
bt_device_type,
1762+
adapter: None,
1763+
})
1764+
}
1765+
1766+
/// Creates a new `BluetoothIdentity` targeting a specific adapter.
1767+
///
1768+
/// # Errors
1769+
///
1770+
/// Returns a `ConnectionError` if the provided `bdaddr` is not a
1771+
/// valid Bluetooth MAC address format.
1772+
///
1773+
/// # Example
1774+
///
1775+
/// ```rust
1776+
/// use nmrs::models::{BluetoothIdentity, BluetoothNetworkRole};
1777+
///
1778+
/// let identity = BluetoothIdentity::with_adapter(
1779+
/// "00:1A:7D:DA:71:13".into(),
1780+
/// BluetoothNetworkRole::PanU,
1781+
/// "hci1".into(),
1782+
/// ).unwrap();
1783+
/// ```
1784+
pub fn with_adapter(
1785+
bdaddr: String,
1786+
bt_device_type: BluetoothNetworkRole,
1787+
adapter: String,
1788+
) -> Result<Self, ConnectionError> {
1789+
validate_bluetooth_address(&bdaddr)?;
1790+
Ok(Self {
1791+
bdaddr,
1792+
bt_device_type,
1793+
adapter: Some(adapter),
17651794
})
17661795
}
17671796
}
@@ -1776,8 +1805,6 @@ impl BluetoothIdentity {
17761805
///
17771806
/// # Example
17781807
///
1779-
/// # Example
1780-
///
17811808
/// ```rust
17821809
/// use nmrs::models::{BluetoothDevice, BluetoothNetworkRole, DeviceState};
17831810
///
@@ -1788,6 +1815,7 @@ impl BluetoothIdentity {
17881815
/// Some("Phone".into()),
17891816
/// role,
17901817
/// DeviceState::Activated,
1818+
/// Some("hci0".into()),
17911819
/// );
17921820
/// ```
17931821
#[non_exhaustive]
@@ -1803,19 +1831,13 @@ pub struct BluetoothDevice {
18031831
pub bt_caps: u32,
18041832
/// Current device state
18051833
pub state: DeviceState,
1834+
/// BlueZ adapter name (e.g. `"hci0"`)
1835+
pub adapter: Option<String>,
18061836
}
18071837

18081838
impl BluetoothDevice {
18091839
/// Creates a new `BluetoothDevice`.
18101840
///
1811-
/// # Arguments
1812-
///
1813-
/// * `bdaddr` - Bluetooth MAC address
1814-
/// * `name` - Friendly device name from BlueZ
1815-
/// * `alias` - Device alias from BlueZ
1816-
/// * `bt_caps` - Bluetooth device capabilities/type
1817-
/// * `state` - Current device state
1818-
///
18191841
/// # Example
18201842
///
18211843
/// ```rust
@@ -1828,6 +1850,7 @@ impl BluetoothDevice {
18281850
/// Some("Phone".into()),
18291851
/// role,
18301852
/// DeviceState::Activated,
1853+
/// Some("hci0".into()),
18311854
/// );
18321855
/// ```
18331856
#[must_use]
@@ -1837,13 +1860,15 @@ impl BluetoothDevice {
18371860
alias: Option<String>,
18381861
bt_caps: u32,
18391862
state: DeviceState,
1863+
adapter: Option<String>,
18401864
) -> Self {
18411865
Self {
18421866
bdaddr,
18431867
name,
18441868
alias,
18451869
bt_caps,
18461870
state,
1871+
adapter,
18471872
}
18481873
}
18491874
}
@@ -2972,13 +2997,15 @@ mod tests {
29722997
Some("Phone".into()),
29732998
role,
29742999
DeviceState::Activated,
3000+
Some("hci0".into()),
29753001
);
29763002

29773003
assert_eq!(device.bdaddr, "00:1A:7D:DA:71:13");
29783004
assert_eq!(device.name, Some("MyPhone".into()));
29793005
assert_eq!(device.alias, Some("Phone".into()));
29803006
assert!(matches!(device.bt_caps, _role));
29813007
assert_eq!(device.state, DeviceState::Activated);
3008+
assert_eq!(device.adapter, Some("hci0".into()));
29823009
}
29833010

29843011
#[test]
@@ -2990,6 +3017,7 @@ mod tests {
29903017
Some("Phone".into()),
29913018
role,
29923019
DeviceState::Activated,
3020+
None,
29933021
);
29943022

29953023
let display_str = format!("{}", device);
@@ -3007,6 +3035,7 @@ mod tests {
30073035
None,
30083036
role,
30093037
DeviceState::Disconnected,
3038+
None,
30103039
);
30113040

30123041
let display_str = format!("{}", device);

nmrs/src/core/bluetooth.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ use crate::{
4343
pub(crate) async fn populate_bluez_info(
4444
conn: &Connection,
4545
bdaddr: &str,
46+
adapter: Option<&str>,
4647
) -> Result<(Option<String>, Option<String>)> {
4748
validate_bluetooth_address(bdaddr)?;
4849

49-
let bluez_path = bluez_device_path(bdaddr);
50+
let bluez_path = bluez_device_path(bdaddr, adapter);
5051

5152
match BluezDeviceExtProxy::builder(conn)
5253
.path(bluez_path)?
@@ -142,8 +143,11 @@ pub(crate) async fn connect_bluetooth(
142143
// Check for saved connection
143144
let saved = get_saved_connection_path(conn, name).await?;
144145

145-
let specific_object = OwnedObjectPath::try_from(bluez_device_path(&settings.bdaddr))
146-
.map_err(|e| ConnectionError::InvalidAddress(format!("Invalid BlueZ path: {e}")))?;
146+
let specific_object = OwnedObjectPath::try_from(bluez_device_path(
147+
&settings.bdaddr,
148+
settings.adapter.as_deref(),
149+
))
150+
.map_err(|e| ConnectionError::InvalidAddress(format!("Invalid BlueZ path: {e}")))?;
147151

148152
match saved {
149153
Some(saved_path) => {
@@ -238,13 +242,21 @@ mod tests {
238242
use crate::models::BluetoothNetworkRole;
239243

240244
#[test]
241-
fn test_bluez_path_format() {
245+
fn test_bluez_path_format_default_adapter() {
242246
assert_eq!(
243-
bluez_device_path("00:1A:7D:DA:71:13"),
247+
bluez_device_path("00:1A:7D:DA:71:13", None),
244248
"/org/bluez/hci0/dev_00_1A_7D_DA_71_13"
245249
);
246250
}
247251

252+
#[test]
253+
fn test_bluez_path_format_specific_adapter() {
254+
assert_eq!(
255+
bluez_device_path("00:1A:7D:DA:71:13", Some("hci1")),
256+
"/org/bluez/hci1/dev_00_1A_7D_DA_71_13"
257+
);
258+
}
259+
248260
#[test]
249261
fn test_bluez_path_format_various_addresses() {
250262
let test_cases = [
@@ -255,7 +267,7 @@ mod tests {
255267

256268
for (bdaddr, expected) in test_cases {
257269
assert_eq!(
258-
bluez_device_path(bdaddr),
270+
bluez_device_path(bdaddr, None),
259271
expected,
260272
"Failed for bdaddr: {bdaddr}"
261273
);
@@ -268,12 +280,26 @@ mod tests {
268280
BluetoothIdentity::new("00:1A:7D:DA:71:13".into(), BluetoothNetworkRole::PanU).unwrap();
269281

270282
assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13");
283+
assert_eq!(identity.adapter, None);
271284
assert!(matches!(
272285
identity.bt_device_type,
273286
BluetoothNetworkRole::PanU
274287
));
275288
}
276289

290+
#[test]
291+
fn test_bluetooth_identity_with_adapter() {
292+
let identity = BluetoothIdentity::with_adapter(
293+
"00:1A:7D:DA:71:13".into(),
294+
BluetoothNetworkRole::PanU,
295+
"hci1".into(),
296+
)
297+
.unwrap();
298+
299+
assert_eq!(identity.bdaddr, "00:1A:7D:DA:71:13");
300+
assert_eq!(identity.adapter, Some("hci1".into()));
301+
}
302+
277303
// Note: Most of the core connection functions require a real D-Bus connection
278304
// and NetworkManager running, so they are better suited for integration tests.
279305
}

nmrs/src/core/device.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,15 @@ pub(crate) async fn list_bluetooth_devices(conn: &Connection) -> Result<Vec<Blue
177177
let raw_state = d_proxy.state().await?;
178178
let state = raw_state.into();
179179

180-
let bluez_info = populate_bluez_info(conn, &bdaddr).await?;
180+
let bluez_info = populate_bluez_info(conn, &bdaddr, None).await?;
181181

182182
devices.push(BluetoothDevice::new(
183183
bdaddr,
184184
bluez_info.0,
185185
bluez_info.1,
186186
bt_caps,
187187
state,
188+
None,
188189
));
189190
}
190191
Ok(devices)
@@ -269,6 +270,7 @@ mod tests {
269270
Some("Test".into()),
270271
panu,
271272
DeviceState::Activated,
273+
None,
272274
);
273275

274276
assert_eq!(device.bdaddr, "00:1A:7D:DA:71:13");

nmrs/src/util/utils.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,21 @@ pub(crate) async fn extract_connection_state_reason(
241241

242242
/// Constructs a BlueZ D-Bus object path from a Bluetooth device address.
243243
///
244-
/// Converts a BDADDR like `"00:1A:7D:DA:71:13"` into
245-
/// `"/org/bluez/hci0/dev_00_1A_7D_DA_71_13"`.
246-
// TODO: Instead of hardcoding hci0, determine the actual adapter name.
247-
pub(crate) fn bluez_device_path(bdaddr: &str) -> String {
248-
format!("/org/bluez/hci0/dev_{}", bdaddr.replace(':', "_"))
244+
/// Uses the given adapter name (e.g. `"hci0"`) or defaults to `"hci0"`
245+
/// when `None` is provided.
246+
///
247+
/// # Example
248+
///
249+
/// ```ignore
250+
/// bluez_device_path("00:1A:7D:DA:71:13", None)
251+
/// // => "/org/bluez/hci0/dev_00_1A_7D_DA_71_13"
252+
///
253+
/// bluez_device_path("00:1A:7D:DA:71:13", Some("hci1"))
254+
/// // => "/org/bluez/hci1/dev_00_1A_7D_DA_71_13"
255+
/// ```
256+
pub(crate) fn bluez_device_path(bdaddr: &str, adapter: Option<&str>) -> String {
257+
let adapter = adapter.unwrap_or("hci0");
258+
format!("/org/bluez/{adapter}/dev_{}", bdaddr.replace(':', "_"))
249259
}
250260

251261
/// Macro to convert Result to Option with error logging.

nmrs/tests/integration_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,7 @@ fn test_bluetooth_device_structure() {
11301130
Some("Phone".into()),
11311131
role,
11321132
DeviceState::Activated,
1133+
None,
11331134
);
11341135

11351136
assert_eq!(device.bdaddr, "00:1A:7D:DA:71:13");
@@ -1150,6 +1151,7 @@ fn test_bluetooth_device_display() {
11501151
Some("Phone".into()),
11511152
role,
11521153
DeviceState::Activated,
1154+
None,
11531155
);
11541156

11551157
let display = format!("{}", device);

0 commit comments

Comments
 (0)