Skip to main content

bluez_async/
device.rs

1use bluez_generated::OrgBluezDevice1Properties;
2use dbus::Path;
3use dbus::arg::{PropMap, RefArg, Variant, cast};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt::{self, Display, Formatter};
7use std::str::FromStr;
8use uuid::Uuid;
9
10use crate::{AdapterId, BluetoothError, MacAddress};
11
12/// Opaque identifier for a Bluetooth device which the system knows about. This includes a reference
13/// to which Bluetooth adapter it was discovered on, which means that any attempt to connect to it
14/// will also happen from that adapter (in case the system has more than one).
15#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
16pub struct DeviceId {
17    #[serde(with = "crate::serde_path")]
18    pub(crate) object_path: Path<'static>,
19}
20
21impl DeviceId {
22    pub(crate) fn new(object_path: &str) -> Self {
23        Self {
24            object_path: object_path.to_owned().into(),
25        }
26    }
27
28    /// Get the ID of the Bluetooth adapter on which this device was discovered, e.g. `"hci0"`.
29    pub fn adapter(&self) -> AdapterId {
30        let index = self
31            .object_path
32            .rfind('/')
33            .expect("DeviceId object_path must contain a slash.");
34        AdapterId::new(&self.object_path[0..index])
35    }
36}
37
38impl From<DeviceId> for Path<'static> {
39    fn from(id: DeviceId) -> Self {
40        id.object_path
41    }
42}
43
44impl FromStr for DeviceId {
45    type Err = BluetoothError;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        if !s.starts_with("hci") {
49            return Err(BluetoothError::DeviceIdParseError(s.to_string()));
50        }
51
52        let path = Path::new(format!("/org/bluez/{}", s))
53            .map_err(|_| BluetoothError::DeviceIdParseError(s.to_string()))?;
54
55        Ok(DeviceId {
56            object_path: path.into_static(),
57        })
58    }
59}
60
61impl Display for DeviceId {
62    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
63        write!(
64            f,
65            "{}",
66            self.object_path
67                .to_string()
68                .strip_prefix("/org/bluez/")
69                .ok_or(fmt::Error)?
70        )
71    }
72}
73
74/// Information about a Bluetooth device which was discovered.
75/// See https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct DeviceInfo {
78    /// An opaque identifier for the device, including a reference to which adapter it was
79    /// discovered on. This can be used to connect to it.
80    pub id: DeviceId,
81    /// The MAC address of the device.
82    pub mac_address: MacAddress,
83    /// The type of MAC address the device uses.
84    pub address_type: AddressType,
85    /// The human-readable name of the device, if available.
86    /// Use the Alias property instead.
87    pub name: Option<String>,
88    /// The appearance of the device, as defined by GAP.
89    pub appearance: Option<u16>,
90    /// The GATT service UUIDs (if any) from the device's advertisement or service discovery.
91    ///
92    /// Note that service discovery only happens after a connection has been made to the device, but
93    /// BlueZ may cache the list of services after it is disconnected.
94    pub services: Vec<Uuid>,
95    /// Whether the device is currently paired with the adapter.
96    pub paired: bool,
97    /// Whether the device is currently connected to the adapter.
98    pub connected: bool,
99    /// The Received Signal Strength Indicator of the device advertisement or inquiry.
100    pub rssi: Option<i16>,
101    /// The transmission power level advertised by the device.
102    pub tx_power: Option<i16>,
103    /// Manufacturer-specific advertisement data, if any. The keys are 'manufacturer IDs'.
104    pub manufacturer_data: HashMap<u16, Vec<u8>>,
105    /// The GATT service data from the device's advertisement, if any. This is a map from the
106    /// service UUID to its data.
107    pub service_data: HashMap<Uuid, Vec<u8>>,
108    /// Whether service discovery has finished for the device.
109    pub services_resolved: bool,
110    /// The Bluetooth friendly name. This defaults to the system hostname.
111    pub alias: Option<String>,
112    /// The Bluetooth class of device, automatically configured by DMI/ACPI information
113    /// or provided as static configuration.
114    pub class: Option<u32>,
115    // Indicates if the information exchanged on pairing process has been stored
116    // and will be persisted.
117    pub bonded: bool,
118    // Proposed icon name according to the freedesktop.org icon naming specification.
119    pub icon: Option<String>,
120    // Indicates if the remote is seen as trusted.
121    pub trusted: bool,
122    // If set to true any incoming connections from the device will be immediately rejected.
123    // Any device drivers will also be removed.
124    pub blocked: bool,
125    // Set to true if the device only supports the pre-2.1 Bluetooth pairing mechanism.
126    // This property is useful during device discovery to anticipate whether legacy or
127    // simple pairing will occur if pairing is initiated.
128    pub legacy_pairing: bool,
129    // Remote Device ID information in modalias format used by the kernel and udev.
130    pub modalias: Option<String>,
131    // If set to true this device will be allowed to wake the host from system suspend.
132    pub wake_allowed: bool,
133}
134
135impl DeviceInfo {
136    pub(crate) fn from_properties(
137        id: DeviceId,
138        device_properties: OrgBluezDevice1Properties,
139    ) -> Result<DeviceInfo, BluetoothError> {
140        let mac_address = device_properties
141            .address()
142            .ok_or(BluetoothError::RequiredPropertyMissing("Address"))?
143            .parse()?;
144        let address_type = device_properties
145            .address_type()
146            .ok_or(BluetoothError::RequiredPropertyMissing("AddressType"))?
147            .parse()?;
148        let services = get_services(device_properties);
149        let manufacturer_data = get_manufacturer_data(device_properties).unwrap_or_default();
150        let service_data = get_service_data(device_properties).unwrap_or_default();
151
152        Ok(DeviceInfo {
153            id,
154            mac_address,
155            address_type,
156            name: device_properties.name().cloned(),
157            appearance: device_properties.appearance(),
158            services,
159            paired: device_properties
160                .paired()
161                .ok_or(BluetoothError::RequiredPropertyMissing("Paired"))?,
162            connected: device_properties
163                .connected()
164                .ok_or(BluetoothError::RequiredPropertyMissing("Connected"))?,
165            rssi: device_properties.rssi(),
166            tx_power: device_properties.tx_power(),
167            manufacturer_data,
168            service_data,
169            services_resolved: device_properties
170                .services_resolved()
171                .ok_or(BluetoothError::RequiredPropertyMissing("ServicesResolved"))?,
172            alias: device_properties.alias().cloned(),
173            class: device_properties.class(),
174            bonded: device_properties.bonded().unwrap_or_default(),
175            icon: device_properties.icon().cloned(),
176            trusted: device_properties
177                .trusted()
178                .ok_or(BluetoothError::RequiredPropertyMissing("Trusted"))?,
179            blocked: device_properties
180                .blocked()
181                .ok_or(BluetoothError::RequiredPropertyMissing("Blocked"))?,
182            legacy_pairing: device_properties
183                .legacy_pairing()
184                .ok_or(BluetoothError::RequiredPropertyMissing("LegacyPairing"))?,
185            modalias: device_properties.modalias().cloned(),
186            wake_allowed: device_properties.wake_allowed().unwrap_or(false),
187        })
188    }
189}
190
191/// MAC address type of a Bluetooth device.
192#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
193pub enum AddressType {
194    /// Public address.
195    Public,
196    /// Random address.
197    Random,
198}
199
200impl AddressType {
201    fn as_str(&self) -> &'static str {
202        match self {
203            Self::Public => "public",
204            Self::Random => "random",
205        }
206    }
207}
208
209impl Display for AddressType {
210    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
211        f.write_str(self.as_str())
212    }
213}
214
215impl FromStr for AddressType {
216    type Err = BluetoothError;
217
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        match s {
220            "public" => Ok(Self::Public),
221            "random" => Ok(Self::Random),
222            _ => Err(BluetoothError::AddressTypeParseError(s.to_owned())),
223        }
224    }
225}
226
227fn get_manufacturer_data(
228    device_properties: OrgBluezDevice1Properties,
229) -> Option<HashMap<u16, Vec<u8>>> {
230    Some(convert_manufacturer_data(
231        device_properties.manufacturer_data()?,
232    ))
233}
234
235pub(crate) fn convert_manufacturer_data(
236    data: &HashMap<u16, Variant<Box<dyn RefArg>>>,
237) -> HashMap<u16, Vec<u8>> {
238    data.iter()
239        .filter_map(|(&k, v)| {
240            if let Some(v) = cast::<Vec<u8>>(&v.0) {
241                Some((k, v.to_owned()))
242            } else {
243                log::warn!("Manufacturer data had wrong type: {:?}", &v.0);
244                None
245            }
246        })
247        .collect()
248}
249
250fn get_service_data(
251    device_properties: OrgBluezDevice1Properties,
252) -> Option<HashMap<Uuid, Vec<u8>>> {
253    Some(convert_service_data(device_properties.service_data()?))
254}
255
256pub(crate) fn convert_service_data(data: &PropMap) -> HashMap<Uuid, Vec<u8>> {
257    data.iter()
258        .filter_map(|(k, v)| match Uuid::parse_str(k) {
259            Ok(uuid) => {
260                if let Some(v) = cast::<Vec<u8>>(&v.0) {
261                    Some((uuid, v.to_owned()))
262                } else {
263                    log::warn!("Service data had wrong type: {:?}", &v.0);
264                    None
265                }
266            }
267            Err(err) => {
268                log::warn!("Error parsing service data UUID: {}", err);
269                None
270            }
271        })
272        .collect()
273}
274
275fn get_services(device_properties: OrgBluezDevice1Properties) -> Vec<Uuid> {
276    if let Some(uuids) = device_properties.uuids() {
277        convert_services(uuids)
278    } else {
279        vec![]
280    }
281}
282
283pub(crate) fn convert_services(uuids: &[String]) -> Vec<Uuid> {
284    uuids
285        .iter()
286        .filter_map(|uuid| {
287            Uuid::parse_str(uuid)
288                .map_err(|err| {
289                    log::warn!("Error parsing service data UUID: {}", err);
290                    err
291                })
292                .ok()
293        })
294        .collect()
295}
296
297#[cfg(test)]
298mod tests {
299    use crate::uuid_from_u32;
300
301    use super::*;
302
303    #[test]
304    fn device_adapter() {
305        let adapter_id = AdapterId::new("/org/bluez/hci0");
306        let device_id = DeviceId::new("/org/bluez/hci0/dev_11_22_33_44_55_66");
307        assert_eq!(device_id.adapter(), adapter_id);
308    }
309
310    #[test]
311    fn to_string() {
312        let device_id = DeviceId::new("/org/bluez/hci0/dev_11_22_33_44_55_66");
313        assert_eq!(device_id.to_string(), "hci0/dev_11_22_33_44_55_66");
314    }
315
316    #[test]
317    fn from_string() {
318        let device_id = DeviceId::from_str("hci0/dev_11_22_33_44_55_66");
319        assert_eq!(device_id.unwrap().to_string(), "hci0/dev_11_22_33_44_55_66");
320
321        let invalid_id = DeviceId::from_str("dev_11_22_33_44_55_66");
322        assert_eq!(
323            invalid_id.unwrap_err().to_string(),
324            "Error parsing DeviceId string: dev_11_22_33_44_55_66"
325        );
326    }
327
328    #[test]
329    fn service_data() {
330        let uuid = uuid_from_u32(0x11223344);
331        let mut service_data: PropMap = HashMap::new();
332        service_data.insert(uuid.to_string(), Variant(Box::new(vec![1u8, 2, 3])));
333        let mut device_properties: PropMap = HashMap::new();
334        device_properties.insert("ServiceData".to_string(), Variant(Box::new(service_data)));
335
336        let mut expected_service_data = HashMap::new();
337        expected_service_data.insert(uuid, vec![1u8, 2, 3]);
338
339        assert_eq!(
340            get_service_data(OrgBluezDevice1Properties(&device_properties)),
341            Some(expected_service_data)
342        );
343    }
344
345    #[test]
346    fn manufacturer_data() {
347        let manufacturer_id = 0x1122;
348        let mut manufacturer_data: HashMap<u16, Variant<Box<dyn RefArg>>> = HashMap::new();
349        manufacturer_data.insert(manufacturer_id, Variant(Box::new(vec![1u8, 2, 3])));
350        let mut device_properties: PropMap = HashMap::new();
351        device_properties.insert(
352            "ManufacturerData".to_string(),
353            Variant(Box::new(manufacturer_data)),
354        );
355
356        let mut expected_manufacturer_data = HashMap::new();
357        expected_manufacturer_data.insert(manufacturer_id, vec![1u8, 2, 3]);
358
359        assert_eq!(
360            get_manufacturer_data(OrgBluezDevice1Properties(&device_properties)),
361            Some(expected_manufacturer_data)
362        );
363    }
364
365    #[test]
366    fn device_info_minimal() {
367        let id = DeviceId::new("/org/bluez/hci0/dev_11_22_33_44_55_66");
368        let mut device_properties: PropMap = HashMap::new();
369        device_properties.insert(
370            "Address".to_string(),
371            Variant(Box::new("00:11:22:33:44:55".to_string())),
372        );
373        device_properties.insert(
374            "AddressType".to_string(),
375            Variant(Box::new("public".to_string())),
376        );
377        device_properties.insert("Paired".to_string(), Variant(Box::new(false)));
378        device_properties.insert("Connected".to_string(), Variant(Box::new(false)));
379        device_properties.insert("ServicesResolved".to_string(), Variant(Box::new(false)));
380        device_properties.insert("Bonded".to_string(), Variant(Box::new(false)));
381        device_properties.insert("Trusted".to_string(), Variant(Box::new(false)));
382        device_properties.insert("Blocked".to_string(), Variant(Box::new(false)));
383        device_properties.insert("LegacyPairing".to_string(), Variant(Box::new(false)));
384
385        let device =
386            DeviceInfo::from_properties(id.clone(), OrgBluezDevice1Properties(&device_properties))
387                .unwrap();
388        assert_eq!(
389            device,
390            DeviceInfo {
391                id,
392                mac_address: "00:11:22:33:44:55".parse().unwrap(),
393                address_type: AddressType::Public,
394                name: None,
395                appearance: None,
396                services: vec![],
397                paired: false,
398                connected: false,
399                rssi: None,
400                tx_power: None,
401                manufacturer_data: HashMap::new(),
402                service_data: HashMap::new(),
403                services_resolved: false,
404                alias: None,
405                class: None,
406                bonded: false,
407                icon: None,
408                trusted: false,
409                blocked: false,
410                legacy_pairing: false,
411                modalias: None,
412                wake_allowed: false,
413            }
414        )
415    }
416
417    #[test]
418    fn get_services_none() {
419        let device_properties: PropMap = HashMap::new();
420
421        assert_eq!(
422            get_services(OrgBluezDevice1Properties(&device_properties)),
423            Vec::<Uuid>::new(),
424        )
425    }
426
427    #[test]
428    fn get_services_some() {
429        let uuid = uuid_from_u32(0x11223344);
430        let uuids = vec![uuid.to_string()];
431        let mut device_properties: PropMap = HashMap::new();
432        device_properties.insert("UUIDs".to_string(), Variant(Box::new(uuids)));
433
434        assert_eq!(
435            get_services(OrgBluezDevice1Properties(&device_properties)),
436            vec![uuid]
437        )
438    }
439
440    #[test]
441    fn address_type_parse() {
442        for &address_type in &[AddressType::Public, AddressType::Random] {
443            assert_eq!(
444                address_type.to_string().parse::<AddressType>().unwrap(),
445                address_type
446            );
447        }
448    }
449}