| // 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. |
| |
| //! Implements the Broadcast Audio Scan Service (BASS) server role. |
| |
| use bt_gatt::server::{LocalService, Server as _, ServiceDefinition, ServiceId}; |
| use bt_gatt::types::{ |
| AttributePermissions, CharacteristicProperty, Handle, SecurityLevels, ServiceKind, |
| }; |
| use bt_gatt::Characteristic; |
| use futures::Future; |
| use pin_project::pin_project; |
| use std::collections::HashMap; |
| |
| use crate::types::{ |
| BroadcastReceiveState, SourceId, BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID, |
| BROADCAST_AUDIO_SCAN_SERVICE_UUID, BROADCAST_RECEIVE_STATE_UUID, |
| }; |
| |
| pub mod error; |
| pub use error::Error; |
| |
| /// Service identifier assigned to the published BASS GATT service instance. |
| const BASS_SERVICE_ID: ServiceId = ServiceId::new(1); |
| |
| /// Handle assigned to the Broadcast Audio Scan Control Point characteristic. |
| const CONTROL_POINT_HANDLE: Handle = Handle(1); |
| |
| /// Maximum number of Broadcast Receive State characteristics that can be hosted |
| /// by the server. See BASS v1.0 Section 3.2.1. |
| const MAX_RECEIVE_STATES: usize = 255; |
| |
| /// Internal representation of a Broadcast Receive State characteristic slot. |
| /// See BASS v1.0 Section 3.2. |
| #[derive(Debug)] |
| pub(crate) struct PublishedReceiveStateCharacteristic { |
| /// 1-indexed source identifier. |
| /// See BASS v1.0 Section 3.2.1. |
| source_id: SourceId, |
| /// Handle assigned to this GATT characteristic. |
| handle: Handle, |
| /// Current Broadcast Receive State value. |
| state: BroadcastReceiveState, |
| } |
| |
| /// Internal publication state lifecycle of the BASS GATT service. |
| #[pin_project(project = LocalServiceProj)] |
| enum LocalServiceState<T: bt_gatt::ServerTypes> { |
| /// Service definition has not been registered in the GATT database. |
| NotPublished, |
| /// Service registration is in progress. |
| Preparing { |
| #[pin] |
| fut: T::LocalServiceFut, |
| }, |
| /// Service registration is complete and active in the GATT database. |
| Published { service: T::LocalService }, |
| } |
| |
| impl<T: bt_gatt::ServerTypes> Default for LocalServiceState<T> { |
| fn default() -> Self { |
| Self::NotPublished |
| } |
| } |
| |
| impl<T: bt_gatt::ServerTypes> LocalServiceState<T> { |
| fn is_published(&self) -> bool { |
| matches!(self, LocalServiceState::Published { .. }) |
| } |
| |
| fn poll_publish( |
| mut self: std::pin::Pin<&mut Self>, |
| cx: &mut std::task::Context<'_>, |
| ) -> std::task::Poll<Result<(), Error>> { |
| match self.as_mut().project() { |
| LocalServiceProj::NotPublished => std::task::Poll::Pending, |
| LocalServiceProj::Preparing { fut } => { |
| let service_result = futures::ready!(fut.poll(cx)); |
| match service_result { |
| Ok(service) => { |
| let _ = service.publish(); |
| self.set(LocalServiceState::Published { service }); |
| std::task::Poll::Ready(Ok(())) |
| } |
| Err(e) => { |
| self.set(LocalServiceState::NotPublished); |
| std::task::Poll::Ready(Err(Error::Gatt(e))) |
| } |
| } |
| } |
| LocalServiceProj::Published { .. } => std::task::Poll::Ready(Ok(())), |
| } |
| } |
| } |
| |
| /// Builder for constructing a BASS GATT [`Server`]. |
| #[derive(Default)] |
| pub struct ServerBuilder { |
| /// Staged Broadcast Receive State characteristics. |
| /// There must be at least one such staged characteristic. |
| receive_states: Vec<BroadcastReceiveState>, |
| } |
| |
| impl ServerBuilder { |
| pub fn new() -> Self { |
| Self::default() |
| } |
| |
| /// Adds a Broadcast Receive State characteristic slot to the builder. |
| pub fn add_receive_state_characteristic(mut self, state: BroadcastReceiveState) -> Self { |
| self.receive_states.push(state); |
| self |
| } |
| |
| /// Constructs the Broadcast Audio Scan Control Point characteristic. |
| /// Defined in BASS v1.0 Section 3.1. |
| fn build_control_point() -> Characteristic { |
| let cp_properties = |
| CharacteristicProperty::Write | CharacteristicProperty::WriteWithoutResponse; |
| Characteristic { |
| handle: CONTROL_POINT_HANDLE, |
| uuid: BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID, |
| properties: cp_properties.clone(), |
| permissions: AttributePermissions::with_levels( |
| &cp_properties, |
| &SecurityLevels::encryption_required(), |
| ), |
| descriptors: Vec::new(), |
| } |
| } |
| |
| /// Constructs the Broadcast Receive State characteristic. |
| /// Defined in BASS v1.0 Section 3.2. |
| fn build_receive_state(handle: Handle) -> Characteristic { |
| let properties = CharacteristicProperty::Read | CharacteristicProperty::Notify; |
| Characteristic { |
| handle, |
| uuid: BROADCAST_RECEIVE_STATE_UUID, |
| properties: properties.clone(), |
| permissions: AttributePermissions::with_levels( |
| &properties, |
| &SecurityLevels::encryption_required(), |
| ), |
| descriptors: Vec::new(), |
| } |
| } |
| |
| /// Builds a [`Server`] instance after verifying required service |
| /// characteristics. |
| pub fn build<T: bt_gatt::ServerTypes>(self) -> Result<Server<T>, Error> { |
| // Per BASS v1.0 Section 3.2, there must be [1,255] Broadcast Receive State |
| // characteristics. |
| if self.receive_states.is_empty() { |
| return Err(Error::MissingReceiveState); |
| } |
| if self.receive_states.len() > MAX_RECEIVE_STATES { |
| return Err(Error::ExceedsMaxReceiveStates); |
| } |
| |
| let mut service_def = ServiceDefinition::new( |
| BASS_SERVICE_ID, |
| BROADCAST_AUDIO_SCAN_SERVICE_UUID, |
| ServiceKind::Primary, |
| ); |
| |
| let _ = service_def.add_characteristic(Self::build_control_point()); |
| |
| // Broadcast Receive State characteristics (Read, Notify; Encryption Required) |
| const FIRST_RECEIVE_STATE_HANDLE: Handle = Handle(2); |
| let num_receive_states = self.receive_states.len(); |
| let mut receive_state_characteristics = HashMap::with_capacity(num_receive_states); |
| let mut source_id_to_handle = HashMap::with_capacity(num_receive_states); |
| for (i, mut state) in self.receive_states.into_iter().enumerate() { |
| let source_id = (i + 1) as u8; |
| let handle = Handle(FIRST_RECEIVE_STATE_HANDLE.0 + i as u64); |
| let _ = service_def.add_characteristic(Self::build_receive_state(handle)); |
| |
| // Override Source ID as this is assigned by the server. See BASS v1.0 Section |
| // 3.2.1. |
| if let BroadcastReceiveState::NonEmpty(ref mut receive_state) = state { |
| receive_state.source_id = source_id; |
| } |
| |
| receive_state_characteristics |
| .insert(handle, PublishedReceiveStateCharacteristic { source_id, handle, state }); |
| source_id_to_handle.insert(source_id, handle); |
| } |
| |
| let next_source_id = (num_receive_states + 1) as SourceId; |
| |
| Ok(Server { |
| service_def, |
| local_service: Default::default(), |
| control_point_handle: CONTROL_POINT_HANDLE, |
| receive_state_characteristics, |
| source_id_to_handle, |
| next_source_id, |
| }) |
| } |
| } |
| |
| /// The BASS GATT Server implementation. |
| /// |
| /// Manages the Control Point characteristic and one or more Broadcast Receive |
| /// State characteristics. |
| #[pin_project] |
| pub struct Server<T: bt_gatt::ServerTypes> { |
| service_def: ServiceDefinition, |
| #[pin] |
| local_service: LocalServiceState<T>, |
| control_point_handle: Handle, |
| /// Broadcast Receive State characteristics mapped by GATT handle. |
| receive_state_characteristics: HashMap<Handle, PublishedReceiveStateCharacteristic>, |
| /// Map from Source ID to characteristic handle. |
| source_id_to_handle: HashMap<SourceId, Handle>, |
| /// Next available Source ID to be assigned to an empty Receive State |
| /// characteristic. |
| next_source_id: SourceId, |
| } |
| |
| impl<T: bt_gatt::ServerTypes> Server<T> { |
| /// Returns true if the server has successfully published the GATT service. |
| pub fn is_published(&self) -> bool { |
| self.local_service.is_published() |
| } |
| |
| /// Helper for polling publication to transition from Preparing to Published |
| /// state. |
| // TODO(b/534436439): Remove once the Stream implementation is defined. |
| #[cfg(test)] |
| pub(crate) fn poll_publish( |
| self: std::pin::Pin<&mut Self>, |
| cx: &mut std::task::Context<'_>, |
| ) -> std::task::Poll<Result<(), Error>> { |
| self.project().local_service.poll_publish(cx) |
| } |
| |
| /// Publishes the service to the GATT database. |
| pub fn publish(&mut self, server: T::Server) -> Result<(), Error> { |
| if matches!( |
| self.local_service, |
| LocalServiceState::Preparing { .. } | LocalServiceState::Published { .. } |
| ) { |
| return Err(Error::AlreadyPublished); |
| } |
| |
| let LocalServiceState::NotPublished = std::mem::replace( |
| &mut self.local_service, |
| LocalServiceState::Preparing { fut: server.prepare(self.service_def.clone()) }, |
| ) else { |
| unreachable!(); |
| }; |
| Ok(()) |
| } |
| |
| /// Handles a read request for a characteristic on the BASS server. |
| // TODO(b/534436439): Remove once the Stream implementation is defined. |
| #[cfg(test)] |
| fn handle_read( |
| &self, |
| handle: Handle, |
| offset: usize, |
| responder: impl bt_gatt::server::ReadResponder, |
| ) { |
| use bt_common::packet_encoding::Encodable; |
| use bt_gatt::types::GattError; |
| |
| if handle == self.control_point_handle { |
| responder.error(GattError::ReadNotPermitted); |
| return; |
| } |
| |
| let Some(slot) = self.receive_state_characteristics.get(&handle) else { |
| responder.error(GattError::InvalidHandle); |
| return; |
| }; |
| |
| let mut buf = vec![0u8; slot.state.encoded_len()]; |
| if slot.state.encode(&mut buf).is_ok() { |
| if offset > buf.len() { |
| responder.error(GattError::InvalidOffset); |
| } else { |
| responder.respond(&buf[offset..]); |
| } |
| } else { |
| responder.error(GattError::UnlikelyError); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use bt_bap::types::BroadcastId; |
| use bt_common::core::AddressType; |
| use bt_gatt::server::ReadResponder; |
| use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes}; |
| use bt_gatt::types::GattError; |
| use futures::FutureExt; |
| use parking_lot::Mutex; |
| use std::sync::Arc; |
| |
| use crate::types::{EncryptionStatus, PaSyncState, ReceiveState}; |
| |
| struct TestReadResponder { |
| result: Arc<Mutex<Option<Result<Vec<u8>, GattError>>>>, |
| } |
| |
| impl ReadResponder for TestReadResponder { |
| fn respond(self, value: &[u8]) { |
| *self.result.lock() = Some(Ok(value.to_vec())); |
| } |
| |
| fn error(self, error: GattError) { |
| *self.result.lock() = Some(Err(error)); |
| } |
| } |
| |
| fn make_test_receive_state(source_id: SourceId) -> BroadcastReceiveState { |
| BroadcastReceiveState::NonEmpty(ReceiveState::new( |
| source_id, |
| AddressType::Public, |
| [0x01, 0x02, 0x03, 0x04, 0x05, 0x06], |
| 1, |
| BroadcastId::try_from(0x123456).unwrap(), |
| PaSyncState::NotSynced, |
| EncryptionStatus::NotEncrypted, |
| vec![], |
| )) |
| } |
| |
| #[test] |
| fn empty_builder_fails() { |
| let builder = ServerBuilder::new(); |
| assert!(matches!(builder.build::<FakeTypes>(), Err(Error::MissingReceiveState))); |
| } |
| |
| #[test] |
| fn too_many_receive_states_fails() { |
| let mut builder = ServerBuilder::new(); |
| for _ in 0..=MAX_RECEIVE_STATES { |
| builder = builder.add_receive_state_characteristic(BroadcastReceiveState::Empty); |
| } |
| assert!(matches!(builder.build::<FakeTypes>(), Err(Error::ExceedsMaxReceiveStates))); |
| } |
| |
| #[test] |
| fn publish_server() { |
| use futures::task::Context; |
| use futures::StreamExt; |
| use std::task::Poll; |
| |
| let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref()); |
| |
| let mut server = ServerBuilder::new() |
| .add_receive_state_characteristic(BroadcastReceiveState::Empty) |
| .add_receive_state_characteristic(make_test_receive_state(2)) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| assert!(!server.is_published()); |
| |
| let (fake_gatt_server, mut event_receiver) = FakeServer::new(); |
| let mut event_stream = event_receiver.next(); |
| |
| assert!(server.publish(fake_gatt_server.clone()).is_ok()); |
| assert!(server.publish(fake_gatt_server).is_err()); |
| |
| // Poll publish to complete preparation and transition to Published state |
| assert!(std::pin::Pin::new(&mut server).poll_publish(&mut noop_cx).is_ready()); |
| assert!(server.is_published()); |
| |
| let Poll::Ready(Some(FakeServerEvent::Published { id, definition })) = |
| event_stream.poll_unpin(&mut noop_cx) |
| else { |
| panic!("Expected published event"); |
| }; |
| |
| assert_eq!(id, BASS_SERVICE_ID); |
| assert_eq!(definition.uuid(), BROADCAST_AUDIO_SCAN_SERVICE_UUID); |
| assert_eq!(definition.characteristics().count(), 3); |
| } |
| |
| #[test] |
| fn read_receive_state() { |
| let server = ServerBuilder::new() |
| .add_receive_state_characteristic(BroadcastReceiveState::Empty) |
| .add_receive_state_characteristic(make_test_receive_state(2)) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| // 1. Read empty Receive State slot (Handle 2) |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(Handle(2), 0, TestReadResponder { result: res.clone() }); |
| assert_eq!(res.lock().take().unwrap().expect("ok"), vec![]); |
| |
| // 2. Read populated Receive State slot (Handle 3) |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(Handle(3), 0, TestReadResponder { result: res.clone() }); |
| assert!(!res.lock().take().unwrap().expect("ok").is_empty()); |
| |
| // 3. Read unknown handle (Handle 99) |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(Handle(99), 0, TestReadResponder { result: res.clone() }); |
| assert_eq!(res.lock().take().unwrap().expect_err("err"), GattError::InvalidHandle); |
| } |
| |
| #[test] |
| fn read_control_point_fails() { |
| let server = ServerBuilder::new() |
| .add_receive_state_characteristic(BroadcastReceiveState::Empty) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(CONTROL_POINT_HANDLE, 0, TestReadResponder { result: res.clone() }); |
| assert_eq!(res.lock().take().unwrap().expect_err("err"), GattError::ReadNotPermitted); |
| } |
| |
| #[test] |
| fn read_receive_state_invalid_offset() { |
| let server = ServerBuilder::new() |
| .add_receive_state_characteristic(BroadcastReceiveState::Empty) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(Handle(2), 10, TestReadResponder { result: res.clone() }); |
| assert_eq!(res.lock().take().unwrap().expect_err("err"), GattError::InvalidOffset); |
| } |
| |
| #[test] |
| fn poll_publish_failure() { |
| use futures::task::Context; |
| use std::task::Poll; |
| |
| let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref()); |
| |
| let mut server = ServerBuilder::new() |
| .add_receive_state_characteristic(BroadcastReceiveState::Empty) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| let (fake_gatt_server, _event_receiver) = FakeServer::new(); |
| fake_gatt_server |
| .set_next_prepare_result(Err(bt_gatt::types::Error::Gatt(GattError::UnlikelyError))); |
| |
| assert!(server.publish(fake_gatt_server.clone()).is_ok()); |
| |
| assert!(matches!( |
| std::pin::Pin::new(&mut server).poll_publish(&mut noop_cx), |
| Poll::Ready(Err(Error::Gatt(bt_gatt::types::Error::Gatt(GattError::UnlikelyError)))) |
| )); |
| |
| // Verifying server reset state to NotPublished so it is not published |
| assert!(!server.is_published()); |
| } |
| |
| #[test] |
| fn builder_normalizes_source_id() { |
| // Create state with an arbitrary source_id = 99 |
| let state = make_test_receive_state(99); |
| let server = ServerBuilder::new() |
| .add_receive_state_characteristic(state) |
| .build::<FakeTypes>() |
| .expect("building server works"); |
| |
| // Verify slot's internal state and encoded read value have source_id = 1 |
| let res = Arc::new(Mutex::new(None)); |
| server.handle_read(Handle(2), 0, TestReadResponder { result: res.clone() }); |
| let buf = res.lock().take().unwrap().expect("ok"); |
| assert_eq!(buf[0], 1); |
| } |
| } |