Skip to main content

bt_broadcast_assistant/
assistant.rs

1// Copyright 2023 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 parking_lot::Mutex;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use thiserror::Error;
10
11use bt_bap::types::BroadcastId;
12use bt_bass::client::error::Error as BassClientError;
13use bt_bass::client::BroadcastAudioScanServiceClient;
14use bt_common::{core::AdvertisingSetId, PeerId, Uuid};
15use bt_gatt::central::*;
16use bt_gatt::client::PeerServiceHandle;
17use bt_gatt::types::Error as GattError;
18use bt_gatt::Client;
19
20pub mod event;
21use event::*;
22pub mod peer;
23pub use peer::Peer;
24
25use crate::types::*;
26
27pub const BROADCAST_AUDIO_SCAN_SERVICE: Uuid = Uuid::from_u16(0x184F);
28pub const BASIC_AUDIO_ANNOUNCEMENT_SERVICE: Uuid = Uuid::from_u16(0x1851);
29pub const BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE: Uuid = Uuid::from_u16(0x1852);
30
31pub type BroadcastSourceKey = (PeerId, AdvertisingSetId);
32
33#[derive(Debug, Error)]
34pub enum Error {
35    #[error("GATT operation error: {0:?}")]
36    Gatt(#[from] GattError),
37
38    #[error("Broadcast Audio Scan Service client error at peer ({0}): {1:?}")]
39    BassClient(PeerId, BassClientError),
40
41    #[error("Not connected to Broadcast Audio Scan Service at peer ({0})")]
42    NotConnectedToBass(PeerId),
43
44    #[error("Central scanning terminated unexpectedly")]
45    CentralScanTerminated,
46
47    #[error("Failed to connect to service ({1}) at peer ({0})")]
48    ConnectionFailure(PeerId, Uuid),
49
50    #[error("Broadcast Assistant was already started previously. It cannot be started twice")]
51    AlreadyStarted,
52
53    #[error("Failed due to error: {0}")]
54    Generic(String),
55}
56
57/// Contains information about the currently-known broadcast
58/// sources and the peers they were found on
59#[derive(Debug)]
60pub(crate) struct DiscoveredBroadcastSources(Mutex<HashMap<BroadcastSourceKey, BroadcastSource>>);
61
62impl DiscoveredBroadcastSources {
63    /// Creates a shareable instance of `DiscoveredBroadcastSources`.
64    pub fn new() -> Arc<Self> {
65        Arc::new(Self(Mutex::new(HashMap::new())))
66    }
67
68    /// Merges the broadcast source data with existing broadcast source data.
69    /// Returns the copy of the broadcast source data after the merge and
70    /// indicates whether it has changed from before or not.
71    pub(crate) fn merge_broadcast_source_data(
72        &self,
73        key: &BroadcastSourceKey,
74        data: &BroadcastSource,
75    ) -> (BroadcastSource, bool) {
76        let mut lock = self.0.lock();
77        let source = lock.entry(*key).or_default();
78        let before = source.clone();
79
80        source.merge(data);
81
82        let after = source.clone();
83        let changed = before != after;
84
85        (after, changed)
86    }
87
88    /// Get a BroadcastSource from a peer id and advertising SID.
89    pub(crate) fn get_by_key(
90        &self,
91        peer_id: PeerId,
92        advertising_sid: AdvertisingSetId,
93    ) -> Option<BroadcastSource> {
94        let lock = self.0.lock();
95        lock.get(&(peer_id, advertising_sid)).cloned()
96    }
97
98    /// Get a BroadcastSource from associated broadcast id.
99    fn get_by_broadcast_id(&self, broadcast_id: &BroadcastId) -> Option<BroadcastSource> {
100        let lock = self.0.lock();
101        let info = lock.values().find(|v| v.broadcast_id == Some(*broadcast_id));
102        info.cloned()
103    }
104}
105
106pub struct BroadcastAssistant<T: bt_gatt::GattTypes> {
107    central: T::Central,
108    broadcast_sources: Arc<DiscoveredBroadcastSources>,
109    broadcast_source_scan_started: Arc<AtomicBool>,
110}
111
112impl<T: bt_gatt::GattTypes + 'static> BroadcastAssistant<T> {
113    // Creates a broadcast assistant and sets it up to be ready
114    // for broadcast source scanning. Clients must use the `start`
115    // method to poll the event stream for scan results.
116    pub fn new(central: T::Central) -> Self {
117        Self {
118            central,
119            broadcast_sources: DiscoveredBroadcastSources::new(),
120            broadcast_source_scan_started: Arc::new(AtomicBool::new(false)),
121        }
122    }
123
124    /// List of scan filters for advertisement data Broadcast Assistant should
125    /// look for, which are:
126    /// - Service data with Broadcast Audio Announcement Service UUID from
127    ///   Broadcast Sources (see BAP spec v1.0.1 Section 3.7.2.1 for details)
128    // TODO(b/308481381): define filter for finding broadcast sink.
129    fn scan_filters() -> Vec<ScanFilter> {
130        vec![Filter::HasServiceData(BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE).into()]
131    }
132
133    /// Start broadcast assistant. Returns EventStream that the upper layer can
134    /// poll. Upper layer can call methods on BroadcastAssistant based on the
135    /// events it sees.
136    pub fn start(&mut self) -> Result<EventStream<T>, Error> {
137        if self.is_started() {
138            return Err(Error::AlreadyStarted);
139        }
140        let scan_result_stream = self.central.scan(&Self::scan_filters());
141        self.broadcast_source_scan_started.store(true, Ordering::Relaxed);
142
143        let periodic_advertising = self
144            .central
145            .periodic_advertising()
146            .inspect_err(|e| {
147                // TODO(b/433285146): This should eventually fail the start operation.
148                // For now, log a warning and fallback to EA-only.
149                log::warn!(
150                    "Periodic Advertising not supported by platform: {:?}. Falling back to EA-only scanning.",
151                    e
152                );
153            })
154            .ok();
155
156        Ok(EventStream::<T>::new(
157            scan_result_stream,
158            periodic_advertising,
159            self.broadcast_sources.clone(),
160            self.broadcast_source_scan_started.clone(),
161        ))
162    }
163
164    /// Returns whether or not Broadcast Assistant has started.
165    fn is_started(&self) -> bool {
166        self.broadcast_source_scan_started.load(Ordering::Relaxed)
167    }
168
169    pub fn scan_for_scan_delegators(&mut self) -> Result<T::ScanResultStream, Error> {
170        if self.is_started() {
171            return Err(Error::Generic(format!(
172                "Cannot scan for scan delegators while scanning for broadcast sources"
173            )));
174        }
175        // Scan for service data with Broadcast Audio Scan Service UUID to look
176        // for Broadcast Sink collocated with the Scan Delegator (see BAP spec v1.0.1
177        // Section 3.9.2 for details).
178        Ok(self.central.scan(&vec![Filter::HasServiceData(BROADCAST_AUDIO_SCAN_SERVICE).into()]))
179    }
180
181    pub async fn connect_to_scan_delegator(&self, peer_id: PeerId) -> Result<Peer<T>, Error>
182    where
183        <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
184    {
185        let client = self.central.connect(peer_id).await?;
186        let service_handles = client.find_service(BROADCAST_AUDIO_SCAN_SERVICE).await?;
187
188        for handle in service_handles {
189            if handle.uuid() != BROADCAST_AUDIO_SCAN_SERVICE || !handle.is_primary() {
190                continue;
191            }
192            let service = handle.connect().await?;
193            let bass = BroadcastAudioScanServiceClient::<T>::create(service)
194                .await
195                .map_err(|e| Error::BassClient(peer_id, e))?;
196
197            let connected_peer =
198                Peer::<T>::new(peer_id, client, bass, self.broadcast_sources.clone());
199            return Ok(connected_peer);
200        }
201        Err(Error::ConnectionFailure(peer_id, BROADCAST_AUDIO_SCAN_SERVICE))
202    }
203
204    // Manually adds broadcast source information for debugging purposes.
205    #[cfg(any(test, feature = "debug"))]
206    pub fn force_discover_broadcast_source(
207        &self,
208        peer_id: PeerId,
209        address: [u8; 6],
210        address_type: bt_common::core::AddressType,
211        advertising_sid: bt_common::core::AdvertisingSetId,
212    ) -> Result<BroadcastSource, Error> {
213        let broadcast_source = BroadcastSource {
214            address: Some(address),
215            address_type: Some(address_type),
216            broadcast_id: None,
217            periodic_advertising_interval: None,
218            endpoint: None,
219            broadcast_name: None,
220        };
221
222        Ok(self
223            .broadcast_sources
224            .merge_broadcast_source_data(&(peer_id, advertising_sid), &broadcast_source)
225            .0)
226    }
227
228    // Manually adds broadcast source information for debugging purposes.
229    #[cfg(any(test, feature = "debug"))]
230    pub fn force_discover_broadcast_source_metadata(
231        &self,
232        peer_id: PeerId,
233        advertising_sid: bt_common::core::AdvertisingSetId,
234        big_metadata: Vec<Vec<bt_common::generic_audio::metadata_ltv::Metadata>>,
235    ) -> Result<BroadcastSource, Error> {
236        use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastIsochronousGroup};
237        use bt_common::core::CodecId;
238
239        let mut big = Vec::new();
240        for metadata in big_metadata {
241            let group = BroadcastIsochronousGroup {
242                codec_id: CodecId::Assigned(bt_common::core::CodingFormat::ALawLog), // mock.
243                codec_specific_configs: vec![],
244                metadata,
245                bis: vec![],
246            };
247            big.push(group);
248        }
249
250        let endpoint = BroadcastAudioSourceEndpoint { presentation_delay_ms: 0, big };
251
252        let broadcast_source = BroadcastSource {
253            address: None,
254            address_type: None,
255            broadcast_id: None,
256            periodic_advertising_interval: None,
257            endpoint: Some(endpoint),
258            broadcast_name: None,
259        };
260
261        Ok(self
262            .broadcast_sources
263            .merge_broadcast_source_data(&(peer_id, advertising_sid), &broadcast_source)
264            .0)
265    }
266
267    // Gets the broadcast sources currently known by the broadcast
268    // assistant.
269    pub fn known_broadcast_sources(
270        &self,
271    ) -> std::collections::HashMap<BroadcastSourceKey, BroadcastSource> {
272        self.broadcast_sources.0.lock().clone()
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    use futures::{pin_mut, FutureExt};
281    use std::task::Poll;
282
283    use bt_bap::types::*;
284    use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
285    use bt_common::generic_audio::metadata_ltv::Metadata;
286    use bt_gatt::test_utils::{FakeCentral, FakeClient, FakeTypes};
287
288    use crate::assistant::peer::tests::fake_bass_service;
289
290    #[test]
291    fn merge_broadcast_source() {
292        let discovered = DiscoveredBroadcastSources::new();
293        let bid1 = BroadcastId::try_from(1001).unwrap();
294        let key1 = (PeerId(1001), AdvertisingSetId(1));
295
296        // 1. Merge initial source data for SID 1.
297        let (bs, changed) = discovered.merge_broadcast_source_data(
298            &key1,
299            &BroadcastSource::default()
300                .with_address([1, 2, 3, 4, 5, 6])
301                .with_address_type(AddressType::Public)
302                .with_broadcast_id(bid1),
303        );
304        assert!(changed);
305        assert_eq!(
306            bs,
307            BroadcastSource {
308                address: Some([1, 2, 3, 4, 5, 6]),
309                address_type: Some(AddressType::Public),
310                broadcast_id: Some(bid1),
311                periodic_advertising_interval: None,
312                endpoint: None,
313                broadcast_name: None,
314            }
315        );
316
317        // 2. Merge endpoint and PA interval for SID 1.
318        let (bs, changed) = discovered.merge_broadcast_source_data(
319            &key1,
320            &BroadcastSource::default()
321                .with_address_type(AddressType::Random)
322                .with_endpoint(BroadcastAudioSourceEndpoint {
323                    presentation_delay_ms: 32,
324                    big: vec![],
325                })
326                .with_periodic_advertising_interval(PeriodicAdvertisingInterval(0x0100)),
327        );
328        assert!(changed);
329        assert_eq!(
330            bs,
331            BroadcastSource {
332                address: Some([1, 2, 3, 4, 5, 6]),
333                address_type: Some(AddressType::Random),
334                broadcast_id: Some(bid1),
335                periodic_advertising_interval: Some(PeriodicAdvertisingInterval(0x0100)),
336                endpoint: Some(BroadcastAudioSourceEndpoint {
337                    presentation_delay_ms: 32,
338                    big: vec![]
339                }),
340                broadcast_name: None,
341            }
342        );
343
344        // 3. Merge duplicate endpoint for SID 1 (no change).
345        let (_, changed) = discovered.merge_broadcast_source_data(
346            &key1,
347            &BroadcastSource::default().with_address_type(AddressType::Random).with_endpoint(
348                BroadcastAudioSourceEndpoint { presentation_delay_ms: 32, big: vec![] },
349            ),
350        );
351        assert!(!changed);
352
353        // 4. Merge a new broadcast source with a different SID (SID 2) for the same
354        //    peer.
355        let bid2 = BroadcastId::try_from(1002).unwrap();
356        let key2 = (PeerId(1001), AdvertisingSetId(2));
357        let (bs, changed) = discovered.merge_broadcast_source_data(
358            &key2,
359            &BroadcastSource::default()
360                .with_address([1, 2, 3, 4, 5, 6])
361                .with_address_type(AddressType::Public)
362                .with_broadcast_id(bid2),
363        );
364        assert!(changed);
365        assert_eq!(
366            bs,
367            BroadcastSource {
368                address: Some([1, 2, 3, 4, 5, 6]),
369                address_type: Some(AddressType::Public),
370                broadcast_id: Some(bid2),
371                periodic_advertising_interval: None,
372                endpoint: None,
373                broadcast_name: None,
374            }
375        );
376
377        // Verify both entries coexist in the registry
378        assert!(discovered.get_by_key(key1.0, key1.1).is_some());
379        assert!(discovered.get_by_key(key2.0, key2.1).is_some());
380
381        // Verify get_by_broadcast_id works for both and maps to correct keys
382        let lock = discovered.0.lock();
383        let entry1 = lock.iter().find(|(_, v)| v.broadcast_id == Some(bid1)).unwrap();
384        assert_eq!(entry1.0 .1, AdvertisingSetId(1));
385        let entry2 = lock.iter().find(|(_, v)| v.broadcast_id == Some(bid2)).unwrap();
386        assert_eq!(entry2.0 .1, AdvertisingSetId(2));
387    }
388
389    #[test]
390    fn start_stream() {
391        let mut assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
392        let stream = assistant.start().expect("can start stream");
393
394        // Stream can only be started once.
395        assert!(assistant.is_started());
396        assert!(assistant.start().is_err());
397
398        // After the stream is dropped, it can be started again.
399        drop(stream);
400        assert!(!assistant.is_started());
401        assert!(assistant.start().is_ok());
402    }
403
404    #[test]
405    fn connect_to_scan_delegator() {
406        // Set up fake GATT related objects.
407        let mut central = FakeCentral::new();
408        let mut client = FakeClient::new();
409        central.add_client(PeerId(1004), client.clone());
410        let service = fake_bass_service();
411        client.add_service(BROADCAST_AUDIO_SCAN_SERVICE, true, service.clone());
412
413        let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
414        let assistant = BroadcastAssistant::<FakeTypes>::new(central);
415        let conn_fut = assistant.connect_to_scan_delegator(PeerId(1004));
416        pin_mut!(conn_fut);
417        let polled = conn_fut.poll_unpin(&mut noop_cx);
418        let Poll::Ready(res) = polled else {
419            panic!("should be ready");
420        };
421        let _ = res.expect("should be ok");
422    }
423
424    #[test]
425    fn force_discover_broadcast_source_test() {
426        let assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
427        let peer_id = PeerId(1);
428        let address = [1, 2, 3, 4, 5, 6];
429        let address_type = AddressType::Public;
430        let sid = AdvertisingSetId(1);
431
432        let source =
433            assistant.force_discover_broadcast_source(peer_id, address, address_type, sid).unwrap();
434
435        assert_eq!(source.address, Some(address));
436        assert_eq!(source.address_type, Some(address_type));
437
438        // Verify it is registered in the assistant under the correct key
439        let known = assistant.known_broadcast_sources();
440        assert_eq!(known.get(&(peer_id, sid)).unwrap().address, Some(address));
441    }
442
443    #[test]
444    fn force_discover_broadcast_source_metadata_test() {
445        let assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
446        let peer_id = PeerId(1);
447        let metadata = vec![vec![Metadata::BroadcastAudioImmediateRenderingFlag]];
448        let sid = AdvertisingSetId(1);
449
450        let source = assistant
451            .force_discover_broadcast_source_metadata(peer_id, sid, metadata.clone())
452            .unwrap();
453
454        let endpoint = source.endpoint.unwrap();
455        assert_eq!(endpoint.big.len(), 1);
456        assert_eq!(endpoint.big[0].metadata, metadata[0]);
457
458        // Verify it is registered in the assistant under the correct key
459        let known = assistant.known_broadcast_sources();
460        assert!(known.contains_key(&(peer_id, sid)));
461    }
462}