Skip to main content

bt_broadcast_assistant/assistant/
event.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 core::pin::Pin;
6use futures::stream::{
7    abortable, AbortHandle, FusedStream, FuturesUnordered, SelectAll, Stream, StreamExt,
8};
9use futures::FutureExt;
10use std::collections::HashMap;
11use std::future::Future;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14use std::task::Poll;
15
16use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastId};
17use bt_common::core::{AdvertisingSetId, PeriodicAdvertisingInterval};
18use bt_common::packet_encoding::Decodable;
19use bt_common::packet_encoding::Error as PacketError;
20use bt_common::PeerId;
21use bt_gatt::central::{AdvertisingDatum, ScanResult};
22use bt_gatt::periodic_advertising::{PeriodicAdvertising, SyncConfiguration, SyncReport};
23use bt_gatt::GattTypes;
24
25use crate::assistant::{
26    DiscoveredBroadcastSources, Error, BASIC_AUDIO_ANNOUNCEMENT_SERVICE,
27    BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE,
28};
29use crate::types::BroadcastSource;
30
31#[derive(Debug)]
32pub enum Event {
33    FoundBroadcastSource {
34        peer: PeerId,
35        advertising_sid: AdvertisingSetId,
36        source: BroadcastSource,
37    },
38    CouldNotParseAdvertisingData {
39        peer: PeerId,
40        error: PacketError,
41    },
42}
43
44type PeriodicAdvertisingSyncResult<T> = Result<
45    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream,
46    bt_gatt::types::Error,
47>;
48
49type PeriodicAdvertisingFuture<T> =
50    Pin<Box<dyn Future<Output = (PeerId, u8, PeriodicAdvertisingSyncResult<T>)>>>;
51
52type PeriodicAdvertisingStream =
53    Pin<Box<dyn Stream<Item = (PeerId, u8, Result<SyncReport, bt_gatt::types::Error>)>>>;
54
55/// A stream of discovered broadcast sources.
56/// This stream polls the scan results from GATT client to discover
57/// available broadcast sources.
58pub struct EventStream<T: bt_gatt::GattTypes> {
59    scan_result_stream: Pin<Box<<T as bt_gatt::GattTypes>::ScanResultStream>>,
60    terminated: bool,
61
62    broadcast_sources: Arc<DiscoveredBroadcastSources>,
63    broadcast_source_scan_started: Arc<AtomicBool>,
64
65    periodic_advertising: Option<T::PeriodicAdvertising>,
66
67    establishing_periodic_advertising_syncs: FuturesUnordered<PeriodicAdvertisingFuture<T>>,
68    active_periodic_advertising_sync_streams: SelectAll<PeriodicAdvertisingStream>,
69    active_syncs: HashMap<(PeerId, u8), Option<AbortHandle>>,
70}
71
72impl<T: bt_gatt::GattTypes> Unpin for EventStream<T> {}
73
74impl<T: bt_gatt::GattTypes> EventStream<T>
75where
76    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
77    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
78{
79    pub(crate) fn new(
80        scan_result_stream: T::ScanResultStream,
81        periodic_advertising: Option<T::PeriodicAdvertising>,
82        broadcast_sources: Arc<DiscoveredBroadcastSources>,
83        broadcast_source_scan_started: Arc<AtomicBool>,
84    ) -> Self {
85        Self {
86            scan_result_stream: Box::pin(scan_result_stream),
87            terminated: false,
88            broadcast_sources,
89            broadcast_source_scan_started,
90            periodic_advertising,
91            establishing_periodic_advertising_syncs: FuturesUnordered::new(),
92            active_periodic_advertising_sync_streams: SelectAll::new(),
93            active_syncs: HashMap::new(),
94        }
95    }
96
97    /// Polls the futures currently establishing periodic advertising syncs.
98    ///
99    /// Returns `Poll::Ready(())` if any future resolved (successfully
100    /// establishing a sync or failing), which indicates progress was made.
101    /// Returns `Poll::Pending` if no progress was made.
102    fn poll_establishing_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<()> {
103        if self.establishing_periodic_advertising_syncs.is_terminated() {
104            return Poll::Pending;
105        }
106
107        match self.establishing_periodic_advertising_syncs.poll_next_unpin(cx) {
108            Poll::Ready(Some((peer_id, sid, Ok(stream)))) => {
109                self.handle_established_sync(peer_id, sid, stream);
110                Poll::Ready(())
111            }
112            Poll::Ready(Some((peer_id, sid, Err(_)))) => {
113                self.active_syncs.remove(&(peer_id, sid));
114                Poll::Ready(())
115            }
116            Poll::Ready(None) | Poll::Pending => Poll::Pending,
117        }
118    }
119
120    /// Polls the active periodic advertising sync report streams.
121    ///
122    /// Returns:
123    /// - `Poll::Ready(Some(event))` if progress was made and a completed
124    ///   `FoundBroadcastSource` event is ready to be returned.
125    /// - `Poll::Ready(None)` if progress was made (e.g., a report was processed
126    ///   but was incomplete, or a stream failed and was removed), but no event
127    ///   is ready yet.
128    /// - `Poll::Pending` if no progress was made.
129    fn poll_active_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Option<Event>> {
130        if self.active_periodic_advertising_sync_streams.is_terminated() {
131            return Poll::Pending;
132        }
133
134        match self.active_periodic_advertising_sync_streams.poll_next_unpin(cx) {
135            Poll::Ready(Some((peer_id, sid, Ok(report)))) => {
136                match self.handle_periodic_advertising_report(peer_id, sid, report) {
137                    Some(event) => Poll::Ready(Some(event)),
138                    None => Poll::Ready(None), // Progressed, but no event
139                }
140            }
141            Poll::Ready(Some((peer_id, sid, Err(_)))) => {
142                self.active_syncs.remove(&(peer_id, sid));
143                Poll::Ready(None) // Progressed, but no event
144            }
145            Poll::Ready(None) | Poll::Pending => Poll::Pending,
146        }
147    }
148
149    /// Returns the broadcast source if the scanned peer is a broadcast source.
150    /// Returns an error if parsing of the scan result data fails and None if
151    /// the scanned peer is not a broadcast source.
152    fn try_into_broadcast_source(
153        scan_result: &ScanResult,
154    ) -> Result<Option<BroadcastSource>, PacketError> {
155        let mut source = None;
156        for datum in &scan_result.advertised {
157            match datum {
158                AdvertisingDatum::ServiceData(uuid, data)
159                    if *uuid == BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE =>
160                {
161                    let bid = BroadcastId::decode(data.as_slice()).0?;
162                    source.get_or_insert(BroadcastSource::default()).with_broadcast_id(bid);
163                }
164                AdvertisingDatum::BroadcastName(name) => {
165                    source
166                        .get_or_insert(BroadcastSource::default())
167                        .with_broadcast_name(name.clone());
168                }
169                _ => {}
170            }
171        }
172        if let Some(src) = &mut source {
173            src.periodic_advertising_interval =
174                scan_result.periodic_advertising_interval.map(PeriodicAdvertisingInterval);
175        }
176        Ok(source)
177    }
178
179    fn handle_established_sync(
180        &mut self,
181        peer_id: PeerId,
182        sid: u8,
183        stream: <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream,
184    ) {
185        let mapped_stream = stream.map(move |report| (peer_id, sid, report));
186        let (abortable_stream, abort_handle) = abortable(mapped_stream);
187
188        self.active_periodic_advertising_sync_streams.push(Box::pin(abortable_stream));
189        self.active_syncs.insert((peer_id, sid), Some(abort_handle));
190    }
191
192    fn handle_periodic_advertising_report(
193        &mut self,
194        peer_id: PeerId,
195        sid: u8,
196        report: SyncReport,
197    ) -> Option<Event> {
198        let SyncReport::PeriodicAdvertisingReport(report) = report else {
199            return None;
200        };
201
202        let Some(base) = parse_base_from_advertising_data(&report.data) else {
203            return None;
204        };
205
206        let (broadcast_source, changed) = self.broadcast_sources.merge_broadcast_source_data(
207            &(peer_id, AdvertisingSetId(sid)),
208            &BroadcastSource::default().with_endpoint(base),
209        );
210
211        if broadcast_source.is_ready_to_add() && changed {
212            if let Some(Some(handle)) = self.active_syncs.remove(&(peer_id, sid)) {
213                handle.abort();
214            }
215            return Some(Event::FoundBroadcastSource {
216                peer: peer_id,
217                advertising_sid: AdvertisingSetId(sid),
218                source: broadcast_source,
219            });
220        }
221        None
222    }
223
224    fn handle_scan_result(&mut self, scanned: ScanResult) -> Option<Event> {
225        let found_source = match Self::try_into_broadcast_source(&scanned) {
226            Ok(Some(src)) => src,
227            Ok(None) => return None,
228            Err(e) => {
229                return Some(Event::CouldNotParseAdvertisingData { peer: scanned.id, error: e })
230            }
231        };
232
233        let Some(raw_sid) = scanned.advertising_sid else {
234            return None;
235        };
236        let sid = AdvertisingSetId(raw_sid);
237
238        let (broadcast_source, changed) =
239            self.broadcast_sources.merge_broadcast_source_data(&(scanned.id, sid), &found_source);
240
241        if broadcast_source.is_ready_to_add() && changed {
242            return Some(Event::FoundBroadcastSource {
243                peer: scanned.id,
244                advertising_sid: sid,
245                source: broadcast_source,
246            });
247        }
248
249        // See if it's appropriate to establish PA sync for this broadcast source.
250
251        // If we already have the endpoint data (BASE), we don't need to establish a
252        // sync.
253        if broadcast_source.endpoint.is_some() {
254            return None;
255        }
256
257        // If the platform doesn't support periodic advertising, we cannot sync.
258        let Some(ref pa) = self.periodic_advertising else {
259            return None;
260        };
261
262        // If we are already actively syncing (or establishing a sync) for this
263        // peer/SID, don't start another one.
264        let key = (scanned.id, sid.0);
265        if self.active_syncs.contains_key(&key) {
266            return None;
267        }
268
269        self.active_syncs.insert(key, None);
270        let fut = pa.sync_to_advertising_reports(
271            scanned.id,
272            sid.0,
273            SyncConfiguration { filter_duplicates: true },
274        );
275        let mapped_fut = fut.map(move |res| (scanned.id, sid.0, res));
276        self.establishing_periodic_advertising_syncs.push(Box::pin(mapped_fut));
277
278        None
279    }
280}
281
282fn parse_base_from_advertising_data(
283    data: &[AdvertisingDatum],
284) -> Option<BroadcastAudioSourceEndpoint> {
285    for datum in data {
286        let AdvertisingDatum::ServiceData(uuid, service_data) = datum else {
287            continue;
288        };
289        if *uuid != BASIC_AUDIO_ANNOUNCEMENT_SERVICE {
290            continue;
291        }
292        let (Ok(base), _) = BroadcastAudioSourceEndpoint::decode(service_data) else {
293            continue;
294        };
295        return Some(base);
296    }
297    None
298}
299
300impl<T: bt_gatt::GattTypes> Drop for EventStream<T> {
301    fn drop(&mut self) {
302        self.broadcast_source_scan_started.store(false, Ordering::Relaxed);
303    }
304}
305
306impl<T: bt_gatt::GattTypes> FusedStream for EventStream<T>
307where
308    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
309    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
310{
311    fn is_terminated(&self) -> bool {
312        self.terminated
313    }
314}
315
316impl<T: bt_gatt::GattTypes> Stream for EventStream<T>
317where
318    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
319    <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
320{
321    type Item = Result<Event, Error>;
322
323    fn poll_next(
324        mut self: std::pin::Pin<&mut Self>,
325        cx: &mut std::task::Context<'_>,
326    ) -> Poll<Option<Self::Item>> {
327        if self.terminated {
328            return Poll::Ready(None);
329        }
330
331        loop {
332            let mut progressed = false;
333
334            if self.poll_establishing_syncs(cx).is_ready() {
335                progressed = true;
336            }
337
338            match self.poll_active_syncs(cx) {
339                Poll::Ready(Some(event)) => return Poll::Ready(Some(Ok(event))),
340                Poll::Ready(None) => progressed = true,
341                Poll::Pending => {}
342            }
343
344            match self.scan_result_stream.poll_next_unpin(cx) {
345                Poll::Ready(Some(Ok(scanned))) => {
346                    progressed = true;
347                    if let Some(event) = self.handle_scan_result(scanned) {
348                        return Poll::Ready(Some(Ok(event)));
349                    }
350                }
351                Poll::Ready(None | Some(Err(_))) => {
352                    self.terminated = true;
353                    self.broadcast_source_scan_started.store(false, Ordering::Relaxed);
354                    return Poll::Ready(Some(Err(Error::CentralScanTerminated)));
355                }
356                Poll::Pending => {}
357            }
358
359            if !progressed {
360                break;
361            }
362        }
363
364        Poll::Pending
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    use assert_matches::assert_matches;
373
374    use bt_common::core::{AddressType, AdvertisingSetId};
375    use bt_gatt::central::{AdvertisingDatum, PeerName};
376    use bt_gatt::test_utils::{
377        FakePeriodicAdvertising, FakeTypes, ScannedResultStream, ScannedResultStreamController,
378    };
379    use bt_gatt::types::Error as BtGattError;
380    use bt_gatt::types::GattError;
381
382    fn setup_stream(
383    ) -> (EventStream<FakeTypes>, ScannedResultStreamController, FakePeriodicAdvertising) {
384        let fake_scan_result_stream = ScannedResultStream::new();
385        let controller = fake_scan_result_stream.controller();
386        let broadcast_sources = DiscoveredBroadcastSources::new();
387        let broadcast_source_scan_started = Arc::new(AtomicBool::new(false));
388        let pa = FakePeriodicAdvertising::default();
389
390        (
391            EventStream::<FakeTypes>::new(
392                fake_scan_result_stream,
393                Some(pa.clone()),
394                broadcast_sources,
395                broadcast_source_scan_started,
396            ),
397            controller,
398            pa,
399        )
400    }
401
402    #[test]
403    fn poll_found_broadcast_source_events() {
404        let (mut stream, scan_result_controller, pa) = setup_stream();
405
406        // Scanned a broadcast source and its broadcast id.
407        let broadcast_source_pid = PeerId(1005);
408
409        scan_result_controller.add_scanned_result(Ok(ScanResult {
410            id: broadcast_source_pid,
411            connectable: true,
412            name: PeerName::Unknown,
413            advertised: vec![
414                AdvertisingDatum::ServiceData(
415                    BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE,
416                    vec![0x01, 0x02, 0x03],
417                ),
418                AdvertisingDatum::BroadcastName("Test Broadcast".to_string()),
419            ],
420            advertising_sid: Some(1),
421            periodic_advertising_interval: Some(0x0100),
422        }));
423
424        // Found broadcast source event shouldn't have been sent since braodcast source
425        // information isn't complete.
426        let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
427        assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
428
429        // Poll again to let the PA sync transition from Establishing to Established.
430        assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
431
432        // Pretend somehow address, address type were filled out.
433        let _ = stream.broadcast_sources.merge_broadcast_source_data(
434            &(broadcast_source_pid, AdvertisingSetId(1)),
435            &BroadcastSource::default()
436                .with_address([1, 2, 3, 4, 5, 6])
437                .with_address_type(AddressType::Public),
438        );
439
440        // Scanned broadcast source's BASE data:
441        #[rustfmt::skip]
442        let base_data = vec![
443            0x10, 0x20, 0x30, 0x02,               // presentation delay, num of subgroups
444            0x01, 0x03, 0x00, 0x00, 0x00, 0x00,   // num of bis, codec id (big #1)
445            0x00,                                 // codec specific config len
446            0x00,                                 // metadata len,
447            0x01, 0x00,                           // bis index, codec specific config len (big #1 / bis #1)
448            0x01, 0x02, 0x00, 0x00, 0x00, 0x00,   // num of bis, codec id (big #2)
449            0x00,                                 // codec specific config len
450            0x00,                                 // metadata len,
451            0x01, 0x03, 0x02, 0x05, 0x08,         // bis index, codec specific config len, codec frame blocks LTV (big #2 / bis #2)
452        ];
453
454        let advertising_data =
455            vec![AdvertisingDatum::ServiceData(BASIC_AUDIO_ANNOUNCEMENT_SERVICE, base_data)];
456
457        // Get the fake periodic advertising sync sender that was registered when we
458        // polled the scan result
459        let periodic_advertising_sender = pa
460            .get_sender(broadcast_source_pid)
461            .expect("should have registered periodic advertising sync sender");
462
463        // Send the PA report containing the BASE data
464        periodic_advertising_sender
465            .unbounded_send(Ok(SyncReport::PeriodicAdvertisingReport(
466                bt_gatt::periodic_advertising::PeriodicAdvertisingReport {
467                    rssi: -50,
468                    data: advertising_data,
469                    event_counter: None,
470                    subevent: None,
471                    timestamp: 0,
472                },
473            )))
474            .unwrap();
475
476        // Expect the stream to send out broadcast source found event since information
477        // is complete.
478        let Poll::Ready(Some(Ok(event))) = stream.poll_next_unpin(&mut noop_cx) else {
479            panic!("should have received event");
480        };
481        assert_matches!(event, Event::FoundBroadcastSource { peer, advertising_sid, source } => {
482            assert_eq!(peer, broadcast_source_pid);
483            assert_eq!(advertising_sid, AdvertisingSetId(1));
484            assert_eq!(source.periodic_advertising_interval, Some(PeriodicAdvertisingInterval(0x0100)));
485            assert_eq!(source.address, Some([1, 2, 3, 4, 5, 6]));
486            assert_eq!(source.broadcast_name, Some("Test Broadcast".to_string()));
487        });
488
489        // Verify that the PA sync was stopped (removed from active_syncs) to conserve
490        // resources
491        assert!(stream.active_syncs.is_empty());
492
493        // Subsequent polls should be pending
494        assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
495    }
496
497    #[test]
498    fn central_scan_stream_terminates() {
499        let (mut stream, scan_result_controller, _pa) = setup_stream();
500
501        // Mimick scan error.
502        scan_result_controller.add_scanned_result(Err(BtGattError::Gatt(GattError::InvalidPdu)));
503
504        let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
505        match stream.poll_next_unpin(&mut noop_cx) {
506            Poll::Ready(Some(Err(e))) => assert_matches!(e, Error::CentralScanTerminated),
507            _ => panic!("should have received central scan terminated error"),
508        }
509
510        // Entire stream should have terminated.
511        assert_matches!(stream.poll_next_unpin(&mut noop_cx), Poll::Ready(None));
512        assert_matches!(stream.is_terminated(), true);
513    }
514}