| // Copyright 2023 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use core::pin::Pin; | |
| use futures::stream::{ | |
| abortable, AbortHandle, FusedStream, FuturesUnordered, SelectAll, Stream, StreamExt, | |
| }; | |
| use futures::FutureExt; | |
| use std::collections::HashMap; | |
| use std::future::Future; | |
| use std::sync::atomic::{AtomicBool, Ordering}; | |
| use std::sync::Arc; | |
| use std::task::Poll; | |
| use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastId}; | |
| use bt_common::core::{AdvertisingSetId, PeriodicAdvertisingInterval}; | |
| use bt_common::packet_encoding::Decodable; | |
| use bt_common::packet_encoding::Error as PacketError; | |
| use bt_common::PeerId; | |
| use bt_gatt::central::{AdvertisingDatum, ScanResult}; | |
| use bt_gatt::periodic_advertising::{PeriodicAdvertising, SyncConfiguration, SyncReport}; | |
| use bt_gatt::GattTypes; | |
| use crate::assistant::{ | |
| BroadcastSourceKey, DiscoveredBroadcastSources, Error, BASIC_AUDIO_ANNOUNCEMENT_SERVICE, | |
| BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, | |
| }; | |
| use crate::types::BroadcastSource; | |
| #[derive(Debug)] | |
| pub enum Event { | |
| FoundBroadcastSource { | |
| peer: PeerId, | |
| advertising_sid: AdvertisingSetId, | |
| source: BroadcastSource, | |
| }, | |
| CouldNotParseAdvertisingData { | |
| peer: PeerId, | |
| error: PacketError, | |
| }, | |
| } | |
| type PeriodicAdvertisingSyncResult<T> = Result< | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream, | |
| bt_gatt::types::Error, | |
| >; | |
| type PeriodicAdvertisingFuture<T> = | |
| Pin<Box<dyn Future<Output = (BroadcastSourceKey, PeriodicAdvertisingSyncResult<T>)>>>; | |
| type PeriodicAdvertisingStream = | |
| Pin<Box<dyn Stream<Item = (BroadcastSourceKey, Result<SyncReport, bt_gatt::types::Error>)>>>; | |
| /// A stream of discovered broadcast sources. | |
| /// This stream polls the scan results from GATT client to discover | |
| /// available broadcast sources. | |
| pub struct EventStream<T: bt_gatt::GattTypes> { | |
| scan_result_stream: Pin<Box<<T as bt_gatt::GattTypes>::ScanResultStream>>, | |
| terminated: bool, | |
| broadcast_sources: Arc<DiscoveredBroadcastSources>, | |
| broadcast_source_scan_started: Arc<AtomicBool>, | |
| periodic_advertising: Option<T::PeriodicAdvertising>, | |
| establishing_periodic_advertising_syncs: FuturesUnordered<PeriodicAdvertisingFuture<T>>, | |
| active_periodic_advertising_sync_streams: SelectAll<PeriodicAdvertisingStream>, | |
| active_syncs: HashMap<BroadcastSourceKey, Option<AbortHandle>>, | |
| } | |
| impl<T: bt_gatt::GattTypes> Unpin for EventStream<T> {} | |
| impl<T: bt_gatt::GattTypes> EventStream<T> | |
| where | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static, | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static, | |
| { | |
| pub(crate) fn new( | |
| scan_result_stream: T::ScanResultStream, | |
| periodic_advertising: Option<T::PeriodicAdvertising>, | |
| broadcast_sources: Arc<DiscoveredBroadcastSources>, | |
| broadcast_source_scan_started: Arc<AtomicBool>, | |
| ) -> Self { | |
| Self { | |
| scan_result_stream: Box::pin(scan_result_stream), | |
| terminated: false, | |
| broadcast_sources, | |
| broadcast_source_scan_started, | |
| periodic_advertising, | |
| establishing_periodic_advertising_syncs: FuturesUnordered::new(), | |
| active_periodic_advertising_sync_streams: SelectAll::new(), | |
| active_syncs: HashMap::new(), | |
| } | |
| } | |
| /// Polls the futures currently establishing periodic advertising syncs. | |
| /// | |
| /// Returns `Poll::Ready(())` if any future resolved (successfully | |
| /// establishing a sync or failing), which indicates progress was made. | |
| /// Returns `Poll::Pending` if no progress was made. | |
| fn poll_establishing_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<()> { | |
| if self.establishing_periodic_advertising_syncs.is_terminated() { | |
| return Poll::Pending; | |
| } | |
| match self.establishing_periodic_advertising_syncs.poll_next_unpin(cx) { | |
| Poll::Ready(Some((key, Ok(stream)))) => { | |
| self.handle_established_sync(key, stream); | |
| Poll::Ready(()) | |
| } | |
| Poll::Ready(Some((key, Err(_)))) => { | |
| self.active_syncs.remove(&key); | |
| Poll::Ready(()) | |
| } | |
| Poll::Ready(None) | Poll::Pending => Poll::Pending, | |
| } | |
| } | |
| /// Polls the active periodic advertising sync report streams. | |
| /// | |
| /// Returns: | |
| /// - `Poll::Ready(Some(event))` if progress was made and a completed | |
| /// `FoundBroadcastSource` event is ready to be returned. | |
| /// - `Poll::Ready(None)` if progress was made (e.g., a report was processed | |
| /// but was incomplete, or a stream failed and was removed), but no event | |
| /// is ready yet. | |
| /// - `Poll::Pending` if no progress was made. | |
| fn poll_active_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Option<Event>> { | |
| if self.active_periodic_advertising_sync_streams.is_terminated() { | |
| return Poll::Pending; | |
| } | |
| match self.active_periodic_advertising_sync_streams.poll_next_unpin(cx) { | |
| Poll::Ready(Some((key, Ok(report)))) => { | |
| match self.handle_periodic_advertising_report(key, report) { | |
| Some(event) => Poll::Ready(Some(event)), | |
| None => Poll::Ready(None), // Progressed, but no event | |
| } | |
| } | |
| Poll::Ready(Some((key, Err(_)))) => { | |
| self.active_syncs.remove(&key); | |
| Poll::Ready(None) // Progressed, but no event | |
| } | |
| Poll::Ready(None) | Poll::Pending => Poll::Pending, | |
| } | |
| } | |
| /// Returns the broadcast source if the scanned peer is a broadcast source. | |
| /// Returns an error if parsing of the scan result data fails and None if | |
| /// the scanned peer is not a broadcast source. | |
| fn try_into_broadcast_source( | |
| scan_result: &ScanResult, | |
| ) -> Result<Option<BroadcastSource>, PacketError> { | |
| let mut source = None; | |
| for datum in &scan_result.advertised { | |
| match datum { | |
| AdvertisingDatum::ServiceData(uuid, data) | |
| if *uuid == BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE => | |
| { | |
| let bid = BroadcastId::decode(data.as_slice()).0?; | |
| source.get_or_insert(BroadcastSource::default()).with_broadcast_id(bid); | |
| } | |
| AdvertisingDatum::BroadcastName(name) => { | |
| source | |
| .get_or_insert(BroadcastSource::default()) | |
| .with_broadcast_name(name.clone()); | |
| } | |
| _ => {} | |
| } | |
| } | |
| if let Some(src) = &mut source { | |
| src.periodic_advertising_interval = | |
| scan_result.periodic_advertising_interval.map(PeriodicAdvertisingInterval); | |
| } | |
| Ok(source) | |
| } | |
| fn handle_established_sync( | |
| &mut self, | |
| key: BroadcastSourceKey, | |
| stream: <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream, | |
| ) { | |
| let mapped_stream = stream.map(move |report| (key, report)); | |
| let (abortable_stream, abort_handle) = abortable(mapped_stream); | |
| self.active_periodic_advertising_sync_streams.push(Box::pin(abortable_stream)); | |
| self.active_syncs.insert(key, Some(abort_handle)); | |
| } | |
| fn handle_periodic_advertising_report( | |
| &mut self, | |
| key: BroadcastSourceKey, | |
| report: SyncReport, | |
| ) -> Option<Event> { | |
| let SyncReport::PeriodicAdvertisingReport(report) = report else { | |
| return None; | |
| }; | |
| let Some(base) = parse_base_from_advertising_data(&report.data) else { | |
| return None; | |
| }; | |
| let (broadcast_source, changed) = self | |
| .broadcast_sources | |
| .merge_broadcast_source_data(&key, &BroadcastSource::default().with_endpoint(base)); | |
| if broadcast_source.is_ready_to_add() && changed { | |
| if let Some(Some(handle)) = self.active_syncs.remove(&key) { | |
| handle.abort(); | |
| } | |
| return Some(Event::FoundBroadcastSource { | |
| peer: key.0, | |
| advertising_sid: key.1, | |
| source: broadcast_source, | |
| }); | |
| } | |
| None | |
| } | |
| fn handle_scan_result(&mut self, scanned: ScanResult) -> Option<Event> { | |
| let found_source = match Self::try_into_broadcast_source(&scanned) { | |
| Ok(Some(src)) => src, | |
| Ok(None) => return None, | |
| Err(e) => { | |
| return Some(Event::CouldNotParseAdvertisingData { peer: scanned.id, error: e }) | |
| } | |
| }; | |
| let Some(raw_sid) = scanned.advertising_sid else { | |
| return None; | |
| }; | |
| let Ok(sid) = AdvertisingSetId::try_from(raw_sid) else { | |
| return None; | |
| }; | |
| let key = (scanned.id, sid); | |
| let (broadcast_source, changed) = | |
| self.broadcast_sources.merge_broadcast_source_data(&key, &found_source); | |
| if broadcast_source.is_ready_to_add() && changed { | |
| return Some(Event::FoundBroadcastSource { | |
| peer: key.0, | |
| advertising_sid: key.1, | |
| source: broadcast_source, | |
| }); | |
| } | |
| // See if it's appropriate to establish PA sync for this broadcast source. | |
| // If we already have the endpoint data (BASE), we don't need to establish a | |
| // sync. | |
| if broadcast_source.endpoint.is_some() { | |
| return None; | |
| } | |
| // If the platform doesn't support periodic advertising, we cannot sync. | |
| let Some(ref pa) = self.periodic_advertising else { | |
| return None; | |
| }; | |
| // If we are already actively syncing (or establishing a sync) for this | |
| // peer/SID, don't start another one. | |
| if self.active_syncs.contains_key(&key) { | |
| return None; | |
| } | |
| self.active_syncs.insert(key, None); | |
| let fut = pa.sync_to_advertising_reports( | |
| scanned.id, | |
| sid.value(), | |
| SyncConfiguration { filter_duplicates: true }, | |
| ); | |
| let mapped_fut = fut.map(move |res| (key, res)); | |
| self.establishing_periodic_advertising_syncs.push(Box::pin(mapped_fut)); | |
| None | |
| } | |
| } | |
| fn parse_base_from_advertising_data( | |
| data: &[AdvertisingDatum], | |
| ) -> Option<BroadcastAudioSourceEndpoint> { | |
| for datum in data { | |
| let AdvertisingDatum::ServiceData(uuid, service_data) = datum else { | |
| continue; | |
| }; | |
| if *uuid != BASIC_AUDIO_ANNOUNCEMENT_SERVICE { | |
| continue; | |
| } | |
| let (Ok(base), _) = BroadcastAudioSourceEndpoint::decode(service_data) else { | |
| continue; | |
| }; | |
| return Some(base); | |
| } | |
| None | |
| } | |
| impl<T: bt_gatt::GattTypes> Drop for EventStream<T> { | |
| fn drop(&mut self) { | |
| self.broadcast_source_scan_started.store(false, Ordering::Relaxed); | |
| } | |
| } | |
| impl<T: bt_gatt::GattTypes> FusedStream for EventStream<T> | |
| where | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static, | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static, | |
| { | |
| fn is_terminated(&self) -> bool { | |
| self.terminated | |
| } | |
| } | |
| impl<T: bt_gatt::GattTypes> Stream for EventStream<T> | |
| where | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static, | |
| <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static, | |
| { | |
| type Item = Result<Event, Error>; | |
| fn poll_next( | |
| mut self: std::pin::Pin<&mut Self>, | |
| cx: &mut std::task::Context<'_>, | |
| ) -> Poll<Option<Self::Item>> { | |
| if self.terminated { | |
| return Poll::Ready(None); | |
| } | |
| loop { | |
| let mut progressed = false; | |
| if self.poll_establishing_syncs(cx).is_ready() { | |
| progressed = true; | |
| } | |
| match self.poll_active_syncs(cx) { | |
| Poll::Ready(Some(event)) => return Poll::Ready(Some(Ok(event))), | |
| Poll::Ready(None) => progressed = true, | |
| Poll::Pending => {} | |
| } | |
| match self.scan_result_stream.poll_next_unpin(cx) { | |
| Poll::Ready(Some(Ok(scanned))) => { | |
| progressed = true; | |
| if let Some(event) = self.handle_scan_result(scanned) { | |
| return Poll::Ready(Some(Ok(event))); | |
| } | |
| } | |
| Poll::Ready(None | Some(Err(_))) => { | |
| self.terminated = true; | |
| self.broadcast_source_scan_started.store(false, Ordering::Relaxed); | |
| return Poll::Ready(Some(Err(Error::CentralScanTerminated))); | |
| } | |
| Poll::Pending => {} | |
| } | |
| if !progressed { | |
| break; | |
| } | |
| } | |
| Poll::Pending | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use assert_matches::assert_matches; | |
| use bt_common::core::{Address, AddressType, AdvertisingSetId}; | |
| use bt_gatt::central::{AdvertisingDatum, PeerName}; | |
| use bt_gatt::test_utils::{ | |
| FakePeriodicAdvertising, FakeTypes, ScannedResultStream, ScannedResultStreamController, | |
| }; | |
| use bt_gatt::types::Error as BtGattError; | |
| use bt_gatt::types::GattError; | |
| fn setup_stream( | |
| ) -> (EventStream<FakeTypes>, ScannedResultStreamController, FakePeriodicAdvertising) { | |
| let fake_scan_result_stream = ScannedResultStream::new(); | |
| let controller = fake_scan_result_stream.controller(); | |
| let broadcast_sources = DiscoveredBroadcastSources::new(); | |
| let broadcast_source_scan_started = Arc::new(AtomicBool::new(false)); | |
| let pa = FakePeriodicAdvertising::default(); | |
| ( | |
| EventStream::<FakeTypes>::new( | |
| fake_scan_result_stream, | |
| Some(pa.clone()), | |
| broadcast_sources, | |
| broadcast_source_scan_started, | |
| ), | |
| controller, | |
| pa, | |
| ) | |
| } | |
| #[test] | |
| fn poll_found_broadcast_source_events() { | |
| let (mut stream, scan_result_controller, pa) = setup_stream(); | |
| // Scanned a broadcast source and its broadcast id. | |
| let broadcast_source_pid = PeerId(1005); | |
| scan_result_controller.add_scanned_result(Ok(ScanResult { | |
| id: broadcast_source_pid, | |
| connectable: true, | |
| name: PeerName::Unknown, | |
| advertised: vec![ | |
| AdvertisingDatum::ServiceData( | |
| BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, | |
| vec![0x01, 0x02, 0x03], | |
| ), | |
| AdvertisingDatum::BroadcastName("Test Broadcast".to_string()), | |
| ], | |
| advertising_sid: Some(1), | |
| periodic_advertising_interval: Some(0x0100), | |
| })); | |
| // Found broadcast source event shouldn't have been sent since braodcast source | |
| // information isn't complete. | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(stream.poll_next_unpin(&mut noop_cx).is_pending()); | |
| // Poll again to let the PA sync transition from Establishing to Established. | |
| assert!(stream.poll_next_unpin(&mut noop_cx).is_pending()); | |
| // Pretend somehow address, address type were filled out. | |
| let _ = stream.broadcast_sources.merge_broadcast_source_data( | |
| &(broadcast_source_pid, AdvertisingSetId::try_from(1).unwrap()), | |
| &BroadcastSource::default() | |
| .with_address(Address::new([1, 2, 3, 4, 5, 6])) | |
| .with_address_type(AddressType::Public), | |
| ); | |
| // Scanned broadcast source's BASE data: | |
| #[rustfmt::skip] | |
| let base_data = vec![ | |
| 0x10, 0x20, 0x30, 0x02, // presentation delay, num of subgroups | |
| 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, // num of bis, codec id (big #1) | |
| 0x00, // codec specific config len | |
| 0x00, // metadata len, | |
| 0x01, 0x00, // bis index, codec specific config len (big #1 / bis #1) | |
| 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, // num of bis, codec id (big #2) | |
| 0x00, // codec specific config len | |
| 0x00, // metadata len, | |
| 0x01, 0x03, 0x02, 0x05, 0x08, // bis index, codec specific config len, codec frame blocks LTV (big #2 / bis #2) | |
| ]; | |
| let advertising_data = | |
| vec![AdvertisingDatum::ServiceData(BASIC_AUDIO_ANNOUNCEMENT_SERVICE, base_data)]; | |
| // Get the fake periodic advertising sync sender that was registered when we | |
| // polled the scan result | |
| let periodic_advertising_sender = pa | |
| .get_sender(broadcast_source_pid) | |
| .expect("should have registered periodic advertising sync sender"); | |
| // Send the PA report containing the BASE data | |
| periodic_advertising_sender | |
| .unbounded_send(Ok(SyncReport::PeriodicAdvertisingReport( | |
| bt_gatt::periodic_advertising::PeriodicAdvertisingReport { | |
| rssi: -50, | |
| data: advertising_data, | |
| event_counter: None, | |
| subevent: None, | |
| timestamp: 0, | |
| }, | |
| ))) | |
| .unwrap(); | |
| // Expect the stream to send out broadcast source found event since information | |
| // is complete. | |
| let Poll::Ready(Some(Ok(event))) = stream.poll_next_unpin(&mut noop_cx) else { | |
| panic!("should have received event"); | |
| }; | |
| assert_matches!(event, Event::FoundBroadcastSource { peer, advertising_sid, source } => { | |
| assert_eq!(peer, broadcast_source_pid); | |
| assert_eq!(advertising_sid, AdvertisingSetId::try_from(1).unwrap()); | |
| assert_eq!(source.periodic_advertising_interval, Some(PeriodicAdvertisingInterval(0x0100))); | |
| assert_eq!(source.address, Some(Address::new([1, 2, 3, 4, 5, 6]))); | |
| assert_eq!(source.broadcast_name, Some("Test Broadcast".to_string())); | |
| }); | |
| // Verify that the PA sync was stopped (removed from active_syncs) to conserve | |
| // resources | |
| assert!(stream.active_syncs.is_empty()); | |
| // Subsequent polls should be pending | |
| assert!(stream.poll_next_unpin(&mut noop_cx).is_pending()); | |
| } | |
| #[test] | |
| fn central_scan_stream_terminates() { | |
| let (mut stream, scan_result_controller, _pa) = setup_stream(); | |
| // Mimick scan error. | |
| scan_result_controller.add_scanned_result(Err(BtGattError::Gatt(GattError::InvalidPdu))); | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| match stream.poll_next_unpin(&mut noop_cx) { | |
| Poll::Ready(Some(Err(e))) => assert_matches!(e, Error::CentralScanTerminated), | |
| _ => panic!("should have received central scan terminated error"), | |
| } | |
| // Entire stream should have terminated. | |
| assert_matches!(stream.poll_next_unpin(&mut noop_cx), Poll::Ready(None)); | |
| assert_matches!(stream.is_terminated(), true); | |
| } | |
| } |