| // 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(_)))); |
| } |
| } |