| // 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 bt_gatt::pii::GetPeerAddr; | |
| use futures::stream::FusedStream; | |
| use futures::Stream; | |
| use std::sync::Arc; | |
| use thiserror::Error; | |
| use bt_bap::types::{BroadcastCode, BroadcastId}; | |
| use bt_bass::client::error::Error as BassClientError; | |
| use bt_bass::client::event::Event as BassEvent; | |
| use bt_bass::client::BroadcastAudioScanServiceClient; | |
| #[cfg(any(test, feature = "debug"))] | |
| use bt_bass::types::BroadcastReceiveState; | |
| use bt_bass::types::{BisSync, PaSync}; | |
| use bt_common::core::{AdvertisingSetId, PeriodicAdvertisingInterval}; | |
| use bt_common::packet_encoding::Error as PacketError; | |
| use bt_common::PeerId; | |
| #[cfg(any(test, feature = "debug"))] | |
| use bt_gatt::types::Handle; | |
| use std::collections::HashMap; | |
| use crate::assistant::DiscoveredBroadcastSources; | |
| #[derive(Debug, Error)] | |
| pub enum Error { | |
| #[error("Failed to take event stream at Broadcast Audio Scan Service client")] | |
| UnavailableBassEventStream, | |
| #[error("Broadcast Audio Scan Service client error: {0:?}")] | |
| BassClient(#[from] BassClientError), | |
| #[error("Incomplete information for broadcast source with peer id ({0})")] | |
| NotEnoughInfo(PeerId), | |
| #[error("Broadcast source with peer id ({0}) does not exist")] | |
| DoesNotExist(PeerId), | |
| #[error("Failed to lookup address for peer id ({0}): {1:?}")] | |
| AddressLookupError(PeerId, bt_gatt::types::Error), | |
| #[error("Packet error: {0}")] | |
| PacketError(#[from] PacketError), | |
| } | |
| /// Connected scan delegator peer. Clients can use this | |
| /// object to perform Broadcast Audio Scan Service operations on the | |
| /// scan delegator peer. | |
| /// Not thread-safe and only one operation must be done at a time. | |
| pub struct Peer<T: bt_gatt::GattTypes> { | |
| peer_id: PeerId, | |
| // Keep for peer connection. | |
| _client: T::Client, | |
| bass: BroadcastAudioScanServiceClient<T>, | |
| // TODO(b/309015071): add a field for pacs. | |
| broadcast_sources: Arc<DiscoveredBroadcastSources>, | |
| } | |
| impl<T: bt_gatt::GattTypes> Peer<T> { | |
| pub(crate) fn new( | |
| peer_id: PeerId, | |
| client: T::Client, | |
| bass: BroadcastAudioScanServiceClient<T>, | |
| broadcast_sources: Arc<DiscoveredBroadcastSources>, | |
| ) -> Self { | |
| Peer { peer_id, _client: client, bass, broadcast_sources } | |
| } | |
| pub fn peer_id(&self) -> PeerId { | |
| self.peer_id | |
| } | |
| /// Takes event stream for BASS events from this scan delegator peer. | |
| /// Clients can call this method to start subscribing to BASS events. | |
| pub fn take_event_stream( | |
| &mut self, | |
| ) -> Result<impl Stream<Item = Result<BassEvent, BassClientError>> + FusedStream, Error> { | |
| self.bass.take_event_stream().ok_or(Error::UnavailableBassEventStream) | |
| } | |
| /// Send broadcast code for a particular broadcast. | |
| pub async fn send_broadcast_code( | |
| &self, | |
| broadcast_id: BroadcastId, | |
| broadcast_code: BroadcastCode, | |
| ) -> Result<(), Error> { | |
| self.bass.set_broadcast_code(broadcast_id, broadcast_code).await.map_err(Into::into) | |
| } | |
| /// Sends a command to add a particular broadcast source. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `broadcast_source_pid` - peer id of the braodcast source that's to be | |
| /// added to this scan delegator peer | |
| /// * `address_lookup` - An implementation of [`GetPeerAddr`] that will be | |
| /// used to look up the peer's address. | |
| /// * `pa_sync` - pa sync mode the peer should attempt to be in | |
| /// * `bis_sync` - desired BIG to BIS synchronization information. If the | |
| /// set is empty, no preference value is used for all the BIGs | |
| pub async fn add_broadcast_source( | |
| &self, | |
| source_peer_id: PeerId, | |
| advertising_sid: AdvertisingSetId, | |
| address_lookup: &impl GetPeerAddr, | |
| pa_sync: PaSync, | |
| bis_sync: HashMap<u8, BisSync>, | |
| ) -> Result<(), Error> { | |
| let mut broadcast_source = self | |
| .broadcast_sources | |
| .get_by_key(source_peer_id, advertising_sid) | |
| .ok_or(Error::DoesNotExist(source_peer_id))?; | |
| let (broadcast_addr, broadcast_addr_type) = address_lookup | |
| .get_peer_address(source_peer_id) | |
| .await | |
| .map_err(|err| Error::AddressLookupError(source_peer_id, err))?; | |
| broadcast_source.with_address(broadcast_addr).with_address_type(broadcast_addr_type); | |
| if !broadcast_source.is_ready_to_add() { | |
| return Err(Error::NotEnoughInfo(source_peer_id)); | |
| } | |
| self.bass | |
| .add_broadcast_source( | |
| broadcast_source.broadcast_id.unwrap(), | |
| broadcast_source.address_type.unwrap(), | |
| broadcast_source.address.unwrap(), | |
| advertising_sid, | |
| pa_sync, | |
| broadcast_source | |
| .periodic_advertising_interval | |
| .unwrap_or(PeriodicAdvertisingInterval::unknown()), | |
| broadcast_source.endpoint_to_big_subgroups(bis_sync).map_err(Error::PacketError)?, | |
| ) | |
| .await | |
| .map_err(Into::into) | |
| } | |
| /// Sends a command to to update a particular broadcast source's PA sync. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `broadcast_id` - broadcast id of the broadcast source that's to be | |
| /// updated | |
| /// * `pa_sync` - pa sync mode the scan delegator peer should attempt to be | |
| /// in. | |
| /// * `bis_sync` - desired BIG to BIS synchronization information | |
| pub async fn update_broadcast_source_sync( | |
| &self, | |
| broadcast_id: BroadcastId, | |
| pa_sync: PaSync, | |
| bis_sync: HashMap<u8, BisSync>, | |
| ) -> Result<(), Error> { | |
| let pa_interval = self | |
| .broadcast_sources | |
| .get_by_broadcast_id(&broadcast_id) | |
| .map(|bs| bs.periodic_advertising_interval) | |
| .unwrap_or(None); | |
| self.bass | |
| .modify_broadcast_source(broadcast_id, pa_sync, pa_interval, bis_sync, None) | |
| .await | |
| .map_err(Into::into) | |
| } | |
| /// Sends a command to remove a particular broadcast source. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `broadcast_id` - broadcast id of the braodcast source that's to be | |
| /// removed from the scan delegator | |
| pub async fn remove_broadcast_source(&self, broadcast_id: BroadcastId) -> Result<(), Error> { | |
| self.bass.remove_broadcast_source(broadcast_id).await.map_err(Into::into) | |
| } | |
| /// Sends a command to inform the scan delegator peer that we have | |
| /// started scanning for broadcast sources on behalf of it. | |
| pub async fn inform_remote_scan_started(&self) -> Result<(), Error> { | |
| self.bass.remote_scan_started().await.map_err(Into::into) | |
| } | |
| /// Sends a command to inform the scan delegator peer that we have | |
| /// stopped scanning for broadcast sources on behalf of it. | |
| pub async fn inform_remote_scan_stopped(&self) -> Result<(), Error> { | |
| self.bass.remote_scan_stopped().await.map_err(Into::into) | |
| } | |
| /// Returns a list of BRS characteristics' latest values the scan delegator | |
| /// has received. | |
| #[cfg(any(test, feature = "debug"))] | |
| pub fn get_broadcast_receive_states(&self) -> Vec<(Handle, BroadcastReceiveState)> { | |
| self.bass.known_broadcast_sources() | |
| } | |
| } | |
| #[cfg(test)] | |
| pub(crate) mod tests { | |
| use super::*; | |
| use assert_matches::assert_matches; | |
| use bt_gatt::pii::StaticPeerAddr; | |
| use futures::{pin_mut, FutureExt}; | |
| use std::collections::HashMap; | |
| use std::task::Poll; | |
| use bt_common::core::{AddressType, AdvertisingSetId}; | |
| use bt_gatt::test_utils::{FakeClient, FakeGetPeerAddr, FakePeerService, FakeTypes}; | |
| use bt_gatt::types::{ | |
| AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, | |
| }; | |
| use bt_gatt::Characteristic; | |
| use crate::types::BroadcastSource; | |
| const RECEIVE_STATE_HANDLE: Handle = Handle(0x11); | |
| const AUDIO_SCAN_CONTROL_POINT_HANDLE: Handle = Handle(0x12); | |
| pub(crate) fn fake_bass_service() -> FakePeerService { | |
| let mut peer_service = FakePeerService::new(); | |
| // One broadcast receive state and one broadcast audio scan control | |
| // point characteristic handles. | |
| peer_service.add_characteristic( | |
| Characteristic { | |
| handle: RECEIVE_STATE_HANDLE, | |
| uuid: bt_bass::types::BROADCAST_RECEIVE_STATE_UUID, | |
| properties: CharacteristicProperties(vec![ | |
| CharacteristicProperty::Broadcast, | |
| CharacteristicProperty::Notify, | |
| ]), | |
| permissions: AttributePermissions::default(), | |
| descriptors: vec![], | |
| }, | |
| vec![], | |
| ); | |
| peer_service.add_characteristic( | |
| Characteristic { | |
| handle: AUDIO_SCAN_CONTROL_POINT_HANDLE, | |
| uuid: bt_bass::types::BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID, | |
| properties: CharacteristicProperties(vec![CharacteristicProperty::Broadcast]), | |
| permissions: AttributePermissions::default(), | |
| descriptors: vec![], | |
| }, | |
| vec![], | |
| ); | |
| peer_service | |
| } | |
| fn setup() -> (Peer<FakeTypes>, FakePeerService, Arc<DiscoveredBroadcastSources>) { | |
| let peer_service = fake_bass_service(); | |
| let broadcast_sources = DiscoveredBroadcastSources::new(); | |
| ( | |
| Peer { | |
| peer_id: PeerId(0x1), | |
| _client: FakeClient::new(), | |
| bass: BroadcastAudioScanServiceClient::<FakeTypes>::create_for_test( | |
| peer_service.clone(), | |
| Handle(0x1), | |
| ), | |
| broadcast_sources: broadcast_sources.clone(), | |
| }, | |
| peer_service, | |
| broadcast_sources, | |
| ) | |
| } | |
| #[test] | |
| fn take_event_stream() { | |
| let (mut peer, _peer_service, _broadcast_source) = setup(); | |
| let _event_stream = peer.take_event_stream().expect("should succeed"); | |
| // If we try to take the event stream the second time, it should fail. | |
| assert!(peer.take_event_stream().is_err()); | |
| } | |
| #[test] | |
| fn add_broadcast_source_fail() { | |
| let (peer, _peer_service, broadcast_source) = setup(); | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| // Should fail because broadcast source doesn't exist. | |
| { | |
| let fut = peer.add_broadcast_source( | |
| PeerId(1001), | |
| AdvertisingSetId::try_from(1).unwrap(), | |
| &FakeGetPeerAddr, | |
| PaSync::SyncPastUnavailable, | |
| HashMap::new(), | |
| ); | |
| pin_mut!(fut); | |
| let polled = fut.poll_unpin(&mut noop_cx); | |
| assert_matches!(polled, Poll::Ready(Err(Error::DoesNotExist(_)))); | |
| } | |
| let _ = broadcast_source.merge_broadcast_source_data( | |
| &(PeerId(1001), AdvertisingSetId::try_from(1).unwrap()), | |
| &BroadcastSource::default().with_broadcast_id(BroadcastId::try_from(1001).unwrap()), | |
| ); | |
| // Should fail because peer address couldn't be looked up. | |
| { | |
| let address_lookup = | |
| StaticPeerAddr::new_for_peer(PeerId(1002), [1, 2, 3, 4, 5, 6], AddressType::Public); | |
| let fut = peer.add_broadcast_source( | |
| PeerId(1001), | |
| AdvertisingSetId::try_from(1).unwrap(), | |
| &address_lookup, | |
| PaSync::SyncPastUnavailable, | |
| HashMap::new(), | |
| ); | |
| pin_mut!(fut); | |
| let polled = fut.poll_unpin(&mut noop_cx); | |
| assert_matches!(polled, Poll::Ready(Err(Error::AddressLookupError(_, _)))); | |
| } | |
| // Should fail because not enough information. | |
| { | |
| let address_lookup = | |
| StaticPeerAddr::new_for_peer(PeerId(1001), [1, 2, 3, 4, 5, 6], AddressType::Public); | |
| let fut = peer.add_broadcast_source( | |
| PeerId(1001), | |
| AdvertisingSetId::try_from(1).unwrap(), | |
| &address_lookup, | |
| PaSync::SyncPastUnavailable, | |
| HashMap::new(), | |
| ); | |
| pin_mut!(fut); | |
| let polled = fut.poll_unpin(&mut noop_cx); | |
| assert_matches!(polled, Poll::Ready(Err(Error::NotEnoughInfo(_)))); | |
| } | |
| } | |
| } |