bluez_async/
device.rs

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