fuchsia_bluetooth/types/
peer.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_bluetooth::{Appearance, DeviceClass};
6use fidl_fuchsia_bluetooth_sys as fsys;
7#[cfg(target_os = "fuchsia")]
8use fuchsia_inspect::Node;
9#[cfg(target_os = "fuchsia")]
10use fuchsia_inspect_contrib::log::WriteInspect;
11use std::fmt;
12
13use crate::assigned_numbers::find_service_uuid;
14use crate::error::Error;
15#[cfg(target_os = "fuchsia")]
16use crate::inspect::*;
17use crate::types::{Address, PeerId, Uuid};
18
19#[derive(Clone, Debug, PartialEq)]
20pub struct Peer {
21    /// Uniquely identifies this peer on the current system.
22    pub id: PeerId,
23
24    /// Bluetooth device address that identifies this peer.
25    /// Avoid using this and prefer to use name and/or appearance instead.
26    ///
27    /// NOTE: Clients should prefer to use the `id` field to keep track of
28    /// peers instead of their address.
29    pub address: Address,
30
31    /// The Bluetooth technologies that are supported by this peer.
32    pub technology: fsys::TechnologyType,
33
34    /// Whether or not a BR/EDR and/or LE connection exists to this peer.
35    pub connected: bool,
36
37    /// Whether or not this peer is bonded.
38    pub bonded: bool,
39
40    /// The name of the peer, if known.
41    pub name: Option<String>,
42
43    /// The LE appearance property. Present if this peer supports LE and the
44    /// appearance information was obtained over advertising and/or GATT.
45    pub appearance: Option<Appearance>,
46
47    /// The class of device for this device, if known.
48    pub device_class: Option<DeviceClass>,
49
50    /// The most recently obtained advertising signal strength for this peer. Present if known.
51    pub rssi: Option<i8>,
52
53    /// The most recently obtained transmission power for this peer. Present if known.
54    pub tx_power: Option<i8>,
55
56    /// The list of peer service UUIDs known to be available on the LE transport.
57    pub le_services: Vec<Uuid>,
58
59    /// The cached list of service UUIDs previously discovered on the BR/EDR transport.
60    pub bredr_services: Vec<Uuid>,
61}
62
63#[cfg(target_os = "fuchsia")]
64impl Peer {
65    /// Records the immutable properties of this peer to the Node given.
66    fn record_inspect(&self, node: &Node) {
67        node.record_string("peer_id", &self.id.to_string());
68        node.record_string("technology", &self.technology.debug());
69        node.record_string("appearance", &self.appearance.debug());
70        node.record_string("address", &self.address.to_string());
71        if let Some(rssi) = self.rssi {
72            node.record_int("rssi", rssi as i64);
73        }
74        if let Some(tx_power) = self.tx_power {
75            node.record_int("tx_power", tx_power as i64);
76        }
77        node.record_uint("connected", self.connected.to_property());
78        node.record_uint("bonded", self.bonded.to_property());
79        if !self.le_services.is_empty() {
80            node.record_string("le_services", &self.le_services.to_property());
81        }
82        if !self.bredr_services.is_empty() {
83            node.record_string("bredr_services", &self.bredr_services.to_property());
84        }
85        if let Some(name) = &self.name {
86            node.record_string("name", name);
87        }
88    }
89}
90
91/// Generate a unique ID to use with audio_core, given the `peer_id` and whether it
92/// will be an input device. Current format is:
93/// [
94///   0x42, 0x54, - Prefix reserved for Bluetooth Audio devices
95///   0xUU, 0xID, - UUID for the service being provided locally on this device:
96///      - 0x11, 0x1E Handsfree (for input devices)
97///      - 0x11, 0x1F Handsfree Audio Gateway (for output devices)
98///      - 0x11, 0x0A A2DP AudioSource
99///      - 0x11, 0x0B A2DP AudioSink (unused for now)
100///   0x00, 0x00, 0x00, 0x00 - Reserved for Future Use
101///   (PeerId in big endian, 8 bytes)
102/// ]
103///
104/// Panics if the uuid provided is not a 16-bit Bluetooth Service UUID.
105pub fn peer_audio_stream_id(peer_id: PeerId, uuid: Uuid) -> [u8; 16] {
106    let mut unique_id = [0; 16];
107    unique_id[0] = 0x42;
108    unique_id[1] = 0x54;
109    let short: u16 = uuid.try_into().expect("UUID should be 16-bit");
110    unique_id[2..4].copy_from_slice(&(short.to_be_bytes()));
111    unique_id[8..].copy_from_slice(&(peer_id.0.to_be_bytes()));
112    unique_id
113}
114
115/// Given the unique ID used with the audio_core, attempts to extract
116/// the Bluetooth PeerId and the UUID for the service being provided to
117/// the peer from the local device:
118/// * `audio_id` - See [peer_audio_stream_id] for expected format
119///
120/// Returns an error if the device is not a valid Bluetooth audio device.
121pub fn audio_stream_id_to_peer(audio_id: &[u8; 16]) -> Result<(PeerId, Uuid), Error> {
122    if audio_id[0] != 0x42 || audio_id[1] != 0x54 {
123        return Err(Error::other("Not a Bluetooth audio device"));
124    }
125    let uuid = Uuid::new16(u16::from_be_bytes(audio_id[2..4].try_into().unwrap()));
126    let peer_id = PeerId(u64::from_be_bytes(audio_id[8..16].try_into().unwrap()));
127    Ok((peer_id, uuid))
128}
129
130#[cfg(target_os = "fuchsia")]
131impl ImmutableDataInspect<Peer> for ImmutableDataInspectManager {
132    fn new(data: &Peer, manager: Node) -> ImmutableDataInspectManager {
133        data.record_inspect(&manager);
134        Self { _manager: manager }
135    }
136}
137
138#[cfg(target_os = "fuchsia")]
139impl IsInspectable for Peer {
140    type I = ImmutableDataInspectManager;
141}
142
143#[cfg(target_os = "fuchsia")]
144impl WriteInspect for Peer {
145    fn write_inspect<'a>(&self, writer: &Node, key: impl Into<std::borrow::Cow<'a, str>>) {
146        writer.record_child(key, |node| {
147            self.record_inspect(node);
148        });
149    }
150}
151
152impl fmt::Display for Peer {
153    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
154        writeln!(fmt, "Peer:")?;
155        writeln!(fmt, "\tId:\t\t{}", self.id)?;
156        writeln!(fmt, "\tAddress:\t{}", self.address)?;
157        writeln!(fmt, "\tTechnology:\t{:?}", self.technology)?;
158        if let Some(name) = &self.name {
159            writeln!(fmt, "\tName:\t\t{}", name)?;
160        }
161        if let Some(appearance) = &self.appearance {
162            writeln!(fmt, "\tAppearance:\t{:?}", appearance)?;
163        }
164        if let Some(rssi) = &self.rssi {
165            writeln!(fmt, "\tRSSI:\t\t{}", rssi)?;
166        }
167        if let Some(tx_power) = &self.tx_power {
168            writeln!(fmt, "\tTX Power:\t{}", tx_power)?;
169        }
170        writeln!(fmt, "\tConnected:\t{}", self.connected)?;
171        writeln!(fmt, "\tBonded:\t\t{}", self.bonded)?;
172        writeln!(fmt, "\tLE Services:\t{:?}", names_from_services(&self.le_services))?;
173        writeln!(fmt, "\tBR/EDR Serv.:\t{:?}", names_from_services(&self.bredr_services))?;
174        Ok(())
175    }
176}
177
178fn names_from_services(services: &Vec<Uuid>) -> Vec<String> {
179    services
180        .iter()
181        .map(|uuid| {
182            let uuid = uuid.to_string();
183            find_service_uuid(&uuid).map(|an| an.name.into()).unwrap_or(uuid)
184        })
185        .collect()
186}
187
188impl TryFrom<fsys::Peer> for Peer {
189    type Error = Error;
190    fn try_from(src: fsys::Peer) -> Result<Peer, Self::Error> {
191        Ok(Peer {
192            id: src.id.ok_or_else(|| Error::missing("sys.Peer.id"))?.into(),
193            address: src.address.ok_or_else(|| Error::missing("sys.Peer.address"))?.into(),
194            technology: src.technology.ok_or_else(|| Error::missing("sys.Peer.technology"))?,
195            connected: src.connected.unwrap_or(false),
196            bonded: src.bonded.unwrap_or(false),
197            name: src.name.clone(),
198            appearance: src.appearance,
199            device_class: src.device_class,
200            rssi: src.rssi,
201            tx_power: src.tx_power,
202            le_services: src.le_services.unwrap_or(vec![]).iter().map(Uuid::from).collect(),
203            bredr_services: src.bredr_services.unwrap_or(vec![]).iter().map(Uuid::from).collect(),
204        })
205    }
206}
207
208impl From<&Peer> for fsys::Peer {
209    fn from(src: &Peer) -> fsys::Peer {
210        fsys::Peer {
211            id: Some(src.id.into()),
212            address: Some(src.address.into()),
213            technology: Some(src.technology),
214            connected: Some(src.connected),
215            bonded: Some(src.bonded),
216            name: src.name.clone(),
217            appearance: src.appearance,
218            device_class: src.device_class,
219            rssi: src.rssi,
220            tx_power: src.tx_power,
221            services: None,
222            le_services: Some(src.le_services.iter().map(|uuid| uuid.into()).collect()),
223            bredr_services: Some(src.bredr_services.iter().map(|uuid| uuid.into()).collect()),
224            ..Default::default()
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    use fidl_fuchsia_bluetooth as fbt;
234    use proptest::collection::vec;
235    use proptest::option;
236    use proptest::prelude::*;
237
238    #[test]
239    fn try_from_sys_id_not_present() {
240        let peer = fsys::Peer::default();
241        let peer = Peer::try_from(peer);
242        assert!(peer.is_err());
243    }
244
245    #[test]
246    fn try_from_sys_address_not_present() {
247        let peer = fsys::Peer { id: Some(fbt::PeerId { value: 1 }), ..Default::default() };
248        let peer = Peer::try_from(peer);
249        assert!(peer.is_err());
250    }
251
252    #[test]
253    fn try_from_sys_technology_not_present() {
254        let peer = fsys::Peer {
255            id: Some(fbt::PeerId { value: 1 }),
256            address: Some(fbt::Address {
257                type_: fbt::AddressType::Public,
258                bytes: [1, 2, 3, 4, 5, 6],
259            }),
260            ..Default::default()
261        };
262        let peer = Peer::try_from(peer);
263        assert!(peer.is_err());
264    }
265
266    fn any_address() -> impl Strategy<Value = Address> {
267        any::<[u8; 6]>().prop_map(Address::Public)
268    }
269
270    fn any_technology() -> impl Strategy<Value = fsys::TechnologyType> {
271        prop_oneof![
272            Just(fsys::TechnologyType::LowEnergy),
273            Just(fsys::TechnologyType::Classic),
274            Just(fsys::TechnologyType::DualMode),
275        ]
276    }
277
278    fn any_appearance() -> impl Strategy<Value = Appearance> {
279        prop_oneof![Just(Appearance::Unknown), Just(Appearance::Computer),]
280    }
281
282    fn any_device_class() -> impl Strategy<Value = DeviceClass> {
283        any::<u32>().prop_map(|value| DeviceClass { value })
284    }
285
286    fn any_uuids() -> impl Strategy<Value = Vec<Uuid>> {
287        vec(any::<[u8; 16]>().prop_map(Uuid::from_bytes), 0..3)
288    }
289
290    fn any_peer() -> impl Strategy<Value = Peer> {
291        (
292            // Trait `Strategy` is only implemented for tuples of up to size 10.
293            (
294                any::<u64>(), // id
295                any_address(),
296                any_technology(),
297                any::<bool>(),                       // connected
298                any::<bool>(),                       // bonded
299                option::of("[a-zA-Z][a-zA-Z0-9_]*"), // name
300                option::of(any_appearance()),
301                option::of(any_device_class()),
302                option::of(any::<i8>()), // rssi
303                option::of(any::<i8>()), // tx power
304            ),
305            any_uuids(), // le_services
306            any_uuids(), // bredr_services
307        )
308            .prop_map(
309                |(
310                    (
311                        id,
312                        address,
313                        technology,
314                        connected,
315                        bonded,
316                        name,
317                        appearance,
318                        device_class,
319                        rssi,
320                        tx_power,
321                    ),
322                    le_services,
323                    bredr_services,
324                )| {
325                    Peer {
326                        id: PeerId(id),
327                        address,
328                        technology,
329                        connected,
330                        bonded,
331                        name,
332                        appearance,
333                        device_class,
334                        rssi,
335                        tx_power,
336                        le_services,
337                        bredr_services,
338                    }
339                },
340            )
341    }
342
343    proptest! {
344        #[test]
345        fn peer_sys_roundtrip(peer in any_peer()) {
346            use std::convert::TryInto;
347
348            let sys = fsys::Peer::from(&peer);
349            assert_eq!(peer, sys.try_into().expect("valid Peer"));
350        }
351    }
352
353    proptest! {
354        #[test]
355        fn peer_audio_stream_id_generation(id1 in prop::num::u64::ANY, id2 in prop::num::u64::ANY, uuid1 in prop::num::u16::ANY, uuid2 in prop::num::u16::ANY) {
356            let peer1 = PeerId(id1);
357            let peer2 = PeerId(id2);
358            let service1: Uuid = Uuid::new16(uuid1);
359            let service2: Uuid = Uuid::new16(uuid2);
360
361            if id1 == id2 {
362                assert_eq!(peer_audio_stream_id(peer1, service1.clone()), peer_audio_stream_id(peer2, service1.clone()));
363            } else {
364                assert_ne!(peer_audio_stream_id(peer1, service1.clone()), peer_audio_stream_id(peer2, service1.clone()));
365            }
366
367            if service1 == service2 {
368                assert_eq!(peer_audio_stream_id(peer1, service1), peer_audio_stream_id(peer1, service2));
369            } else {
370                assert_ne!(peer_audio_stream_id(peer1, service1), peer_audio_stream_id(peer1, service2));
371            }
372        }
373    }
374
375    #[test]
376    fn peer_from_audio_stream_id() {
377        use fidl_fuchsia_bluetooth_bredr::ServiceClassProfileIdentifier;
378
379        let a2dp_device = [
380            0x42, 0x54, 0x11, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
381            0x07, 0x08,
382        ];
383        let (peer_id, uuid) = audio_stream_id_to_peer(&a2dp_device).expect("should be converted");
384        assert_eq!(peer_id.0, 0x0102030405060708);
385        assert_eq!(uuid, ServiceClassProfileIdentifier::AudioSource.into());
386
387        let non_bluetooth_device = [0x0A; 16];
388        let _ = audio_stream_id_to_peer(&non_bluetooth_device)
389            .expect_err("should have failed to convert");
390    }
391}