blob: 4521795c6c567590171069b4dfdf81c7b0823d91 [file] [edit]
// 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);
}
}