| // Copyright 2026 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_common::PeerId; | |
| use bt_gatt::client::FromCharacteristic; | |
| use bt_gatt::client::PeerService; | |
| use bt_gatt::client::PeerServiceHandle; | |
| use bt_gatt::Client; | |
| use bt_gatt::GattTypes; | |
| use crate::types::{Error, SupportedCapabilities}; | |
| /// Represents a connection to a single peer and manages its audio streams. | |
| pub struct BapUnicastClient<T: GattTypes> { | |
| peer_id: PeerId, | |
| #[allow(unused)] | |
| gatt_client: T::Client, | |
| // TODO: Add ASCS and PACS clients here. | |
| } | |
| impl<T: GattTypes> BapUnicastClient<T> { | |
| pub fn new(peer_id: PeerId, gatt_client: T::Client) -> Self { | |
| Self { peer_id, gatt_client } | |
| } | |
| pub fn peer_id(&self) -> PeerId { | |
| self.peer_id | |
| } | |
| /// Discovers PACS and ASCS and returns supported configurations. | |
| pub async fn discover(&mut self) -> Result<SupportedCapabilities, Error> | |
| where | |
| <T as GattTypes>::NotificationStream: Send, | |
| { | |
| // 1. Discover PACS and read capabilities. | |
| let pacs_handles = self.gatt_client.find_service(bt_pacs::PACS_UUID).await?; | |
| if pacs_handles.len() != 1 { | |
| return Err(Error::Configuration(format!( | |
| "{} PACS service(s) - not unique", | |
| pacs_handles.len() | |
| ))); | |
| } | |
| let handle = &pacs_handles[0]; | |
| let mut pac_records = vec![]; | |
| let service = handle.connect().await?; | |
| let chrs = service.discover_characteristics(None).await?; | |
| for chr in chrs { | |
| if chr.uuid == bt_pacs::SourcePac::UUID { | |
| let source_pac = bt_pacs::SourcePac::try_read::<T>(chr, &service).await?; | |
| pac_records.extend(source_pac.capabilities); | |
| } else if chr.uuid == bt_pacs::SinkPac::UUID { | |
| let sink_pac = bt_pacs::SinkPac::try_read::<T>(chr, &service).await?; | |
| pac_records.extend(sink_pac.capabilities); | |
| } | |
| } | |
| // 2. Discover ASCS and count endpoints. | |
| let ascs_handles = self.gatt_client.find_service(bt_ascs::server::ASCS_UUID).await?; | |
| if ascs_handles.len() != 1 { | |
| return Err(Error::Configuration(format!( | |
| "{} ASCS service(s) - not unique", | |
| ascs_handles.len() | |
| ))); | |
| } | |
| let handle = &ascs_handles[0]; | |
| let mut sink_ase_count = 0; | |
| let mut source_ase_count = 0; | |
| let service = handle.connect().await?; | |
| let ascs_client = | |
| bt_ascs::client::AudioStreamControlServiceClient::<T>::create(service).await?; | |
| sink_ase_count += ascs_client.endpoints.sink_ases().len(); | |
| source_ase_count += ascs_client.endpoints.source_ases().len(); | |
| Ok(crate::mapping::map_capabilities(&pac_records, sink_ase_count, source_ase_count)) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use crate::test_utils::{setup_ascs_service, setup_pacs_service}; | |
| use crate::Configuration; | |
| use bt_common::PeerId; | |
| use bt_gatt::test_utils::{FakeClient, FakeTypes}; | |
| #[test] | |
| fn test_discover_success() { | |
| let mut client = FakeClient::new(); | |
| let _pacs_service = setup_pacs_service(&mut client); | |
| let _ascs_service = setup_ascs_service(&mut client, 1, 1); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let mapping = futures::executor::block_on(unicast_client.discover()) | |
| .expect("discovery should succeed"); | |
| assert!(mapping.contains_key(&Configuration::MonoAudioSink)); | |
| assert!(mapping.contains_key(&Configuration::Conversational)); | |
| } | |
| #[test] | |
| fn test_discover_no_services() { | |
| let client = FakeClient::new(); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let result = futures::executor::block_on(unicast_client.discover()); | |
| assert!(matches!(result, Err(Error::Configuration(_)))); | |
| } | |
| #[test] | |
| fn test_discover_no_pacs_service() { | |
| let mut client = FakeClient::new(); | |
| let _ascs_service = setup_ascs_service(&mut client, 1, 1); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let result = futures::executor::block_on(unicast_client.discover()); | |
| assert!(matches!(result, Err(Error::Configuration(_)))); | |
| } | |
| #[test] | |
| fn test_discover_multiple_pacs_services() { | |
| let mut client = FakeClient::new(); | |
| let _pacs_service1 = setup_pacs_service(&mut client); | |
| let _pacs_service2 = setup_pacs_service(&mut client); | |
| let _ascs_service = setup_ascs_service(&mut client, 1, 1); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let result = futures::executor::block_on(unicast_client.discover()); | |
| assert!(matches!(result, Err(Error::Configuration(_)))); | |
| } | |
| #[test] | |
| fn test_discover_no_ascs_service() { | |
| let mut client = FakeClient::new(); | |
| let _pacs_service = setup_pacs_service(&mut client); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let result = futures::executor::block_on(unicast_client.discover()); | |
| assert!(matches!(result, Err(Error::Configuration(_)))); | |
| } | |
| #[test] | |
| fn test_discover_multiple_ascs_services() { | |
| let mut client = FakeClient::new(); | |
| let _pacs_service = setup_pacs_service(&mut client); | |
| let _ascs_service1 = setup_ascs_service(&mut client, 1, 1); | |
| let _ascs_service2 = setup_ascs_service(&mut client, 1, 1); | |
| let mut unicast_client = BapUnicastClient::<FakeTypes>::new(PeerId(1), client); | |
| let result = futures::executor::block_on(unicast_client.discover()); | |
| assert!(matches!(result, Err(Error::Configuration(_)))); | |
| } | |
| } |