rust/bt-bass: Initial implementation of BASS server Add initial scaffolding for the BASS GATT Server. Introduce a BASS ServerBuilder to create a valid server with characteristics. Define characteristics for the Service, Control Point, and Broadcast Receive State. Test: cargo test -p bt-bass, ./presubmit.sh Bug: b/534436439 Change-Id: Ie5e4bd965aba680b294e02a42304096907a83e98 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3160
diff --git a/rust/bt-bass/Cargo.toml b/rust/bt-bass/Cargo.toml index 51fe888..533e06b 100644 --- a/rust/bt-bass/Cargo.toml +++ b/rust/bt-bass/Cargo.toml
@@ -14,6 +14,7 @@ bt-gatt = { workspace = true, features = ["test-utils"] } futures.workspace = true log.workspace = true +pin-project.workspace = true parking_lot.workspace = true thiserror.workspace = true
diff --git a/rust/bt-bass/src/lib.rs b/rust/bt-bass/src/lib.rs index 1f8980b..e32b283 100644 --- a/rust/bt-bass/src/lib.rs +++ b/rust/bt-bass/src/lib.rs
@@ -3,8 +3,10 @@ // found in the LICENSE file. pub mod client; +pub mod server; pub mod types; -pub use crate::client::error::Error; +pub use crate::client::error::Error as ClientError; +pub use crate::server::Error as ServerError; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils;
diff --git a/rust/bt-bass/src/server.rs b/rust/bt-bass/src/server.rs new file mode 100644 index 0000000..4521795 --- /dev/null +++ b/rust/bt-bass/src/server.rs
@@ -0,0 +1,475 @@ +// 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); + } +}
diff --git a/rust/bt-bass/src/server/error.rs b/rust/bt-bass/src/server/error.rs new file mode 100644 index 0000000..b5727f3 --- /dev/null +++ b/rust/bt-bass/src/server/error.rs
@@ -0,0 +1,34 @@ +// 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 thiserror::Error; + +use crate::types::SourceId; +use bt_common::packet_encoding::Error as PacketError; +use bt_gatt::types::Error as BTGattError; + +/// Errors encountered when operating a BASS GATT server. +#[derive(Debug, Error)] +pub enum Error { + #[error("Service is already published")] + AlreadyPublished, + + #[error("Service should support at least one Broadcast Receive State characteristic")] + MissingReceiveState, + + #[error("Exceeded maximum number of Broadcast Receive State characteristics (255)")] + ExceedsMaxReceiveStates, + + #[error("An unsupported opcode ({0:#x}) used in Control Point operation")] + OpCodeNotSupported(u8), + + #[error("Invalid source id ({0}) used in Control Point operation")] + InvalidSourceId(SourceId), + + #[error("Packet encoding/decoding error: {0}")] + Packet(#[from] PacketError), + + #[error("GATT operation error: {0}")] + Gatt(#[from] BTGattError), +}
diff --git a/rust/bt-bass/src/types.rs b/rust/bt-bass/src/types.rs index 30a4388..a3d1222 100644 --- a/rust/bt-bass/src/types.rs +++ b/rust/bt-bass/src/types.rs
@@ -15,8 +15,9 @@ const PA_SYNC_BYTE_SIZE: usize = 1; const SOURCE_ID_BYTE_SIZE: usize = 1; -/// 16-bit UUID value for the characteristics offered by the Broadcast Audio -/// Scan Service. +/// 16-bit UUID values for the Broadcast Audio Scan Service and its +/// characteristics. +pub const BROADCAST_AUDIO_SCAN_SERVICE_UUID: Uuid = Uuid::from_u16(0x184F); pub const BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID: Uuid = Uuid::from_u16(0x2BC7); pub const BROADCAST_RECEIVE_STATE_UUID: Uuid = Uuid::from_u16(0x2BC8);
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs index e8e7de9..a5bee21 100644 --- a/rust/bt-gatt/src/test_utils.rs +++ b/rust/bt-gatt/src/test_utils.rs
@@ -453,6 +453,7 @@ sender: UnboundedSender<FakeServerEvent>, notification_peers: HashSet<PeerId>, indication_peers: HashSet<PeerId>, + next_prepare_result: Option<Result<()>>, } #[derive(Clone, Debug)] @@ -465,8 +466,12 @@ &self, service: server::ServiceDefinition, ) -> <FakeTypes as ServerTypes>::LocalServiceFut { + let mut lock = self.inner.lock(); + if let Some(Err(e)) = lock.next_prepare_result.take() { + return futures::future::ready(Err(e)); + } let id = service.id(); - self.inner.lock().services.insert(id, service); + lock.services.insert(id, service); futures::future::ready(Ok(FakeLocalService::new(id, self.inner.clone()))) } } @@ -482,12 +487,17 @@ sender, notification_peers: HashSet::new(), indication_peers: HashSet::new(), + next_prepare_result: None, })), }, receiver, ) } + pub fn set_next_prepare_result(&self, res: Result<()>) { + self.inner.lock().next_prepare_result = Some(res); + } + pub fn service(&self, id: server::ServiceId) -> Option<ServiceDefinition> { self.inner.lock().services.get(&id).cloned() }