| // 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_bap::types::BroadcastCode; | |
| use bt_common::packet_encoding::{Decodable, Encodable}; | |
| use bt_common::PeerId; | |
| use bt_gatt::server::{ | |
| LocalService, ReadResponder, Server as _, ServiceDefinition, ServiceEvent, ServiceId, | |
| WriteResponder, | |
| }; | |
| use bt_gatt::types::{ | |
| AttributePermissions, CharacteristicProperties, CharacteristicProperty, GattError, Handle, | |
| SecurityLevels, ServiceKind, | |
| }; | |
| use bt_gatt::Characteristic; | |
| use futures::stream::Stream; | |
| use futures::Future; | |
| use log::warn; | |
| use pin_project::pin_project; | |
| use std::collections::{BTreeMap, HashMap}; | |
| use std::num::NonZeroU8; | |
| use std::task::{Poll, Waker}; | |
| use crate::types::*; | |
| pub mod error; | |
| 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)] | |
| struct PublishedReceiveStateCharacteristic { | |
| /// Handle assigned to this GATT characteristic. | |
| handle: Handle, | |
| /// Current Broadcast Receive State value. | |
| state: BroadcastReceiveState, | |
| } | |
| /// A pending GATT notification to be dispatched following a state modification. | |
| #[derive(Debug, PartialEq, Eq)] | |
| struct PendingNotification { | |
| /// Handle of the characteristic to notify. | |
| handle: Handle, | |
| /// Encoded GATT characteristic value. | |
| value: Vec<u8>, | |
| } | |
| /// A parsed request to write to the Control Point characteristic. | |
| struct ControlPointWriteResult { | |
| /// The decoded event associated with this request. | |
| event: ServerEvent, | |
| /// An optional notification to be sent to GATT peers. | |
| notification: Option<PendingNotification>, | |
| } | |
| /// 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 { | |
| waker: Option<Waker>, | |
| }, | |
| /// Service registration is in progress. | |
| Preparing { | |
| #[pin] | |
| fut: T::LocalServiceFut, | |
| }, | |
| /// Service registration is complete and active in the GATT database. | |
| Published { | |
| service: T::LocalService, | |
| #[pin] | |
| events: T::ServiceEventStream, | |
| }, | |
| Terminated, | |
| } | |
| impl<T: bt_gatt::ServerTypes> Default for LocalServiceState<T> { | |
| fn default() -> Self { | |
| Self::NotPublished { waker: None } | |
| } | |
| } | |
| impl<T: bt_gatt::ServerTypes> LocalServiceState<T> { | |
| fn is_published(&self) -> bool { | |
| matches!(self, LocalServiceState::Published { .. }) | |
| } | |
| fn service(&self) -> Option<&T::LocalService> { | |
| match self { | |
| Self::Published { service, .. } => Some(service), | |
| _ => None, | |
| } | |
| } | |
| fn notify(&self, notification: &PendingNotification) { | |
| let Some(service) = self.service() else { | |
| warn!("Attempted to notify GATT peers on an unpublished service"); | |
| return; | |
| }; | |
| service.notify(¬ification.handle, ¬ification.value, &[]); | |
| } | |
| } | |
| impl<T: bt_gatt::ServerTypes> Stream for LocalServiceState<T> { | |
| type Item = Result<bt_gatt::server::ServiceEvent<T>, Error>; | |
| fn poll_next( | |
| mut self: std::pin::Pin<&mut Self>, | |
| cx: &mut std::task::Context<'_>, | |
| ) -> Poll<Option<Self::Item>> { | |
| loop { | |
| match self.as_mut().project() { | |
| LocalServiceProj::Terminated => return Poll::Ready(None), | |
| LocalServiceProj::NotPublished { waker } => { | |
| *waker = Some(cx.waker().clone()); | |
| return Poll::Pending; | |
| } | |
| LocalServiceProj::Preparing { fut } => { | |
| let service_result = futures::ready!(fut.poll(cx)); | |
| let Ok(service) = service_result else { | |
| self.as_mut().set(LocalServiceState::NotPublished { waker: None }); | |
| return Poll::Ready(Some(Err(Error::Gatt(service_result.err().unwrap())))); | |
| }; | |
| let events = service.publish(); | |
| self.as_mut().set(LocalServiceState::Published { service, events }); | |
| } | |
| LocalServiceProj::Published { service: _, events } => { | |
| return match futures::ready!(events.poll_next(cx)) { | |
| Some(Ok(event)) => Poll::Ready(Some(Ok(event))), | |
| Some(Err(e)) => { | |
| self.as_mut().set(LocalServiceState::Terminated); | |
| Poll::Ready(Some(Err(Error::Gatt(e)))) | |
| } | |
| None => { | |
| self.as_mut().set(LocalServiceState::Terminated); | |
| Poll::Ready(None) | |
| } | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// 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: CharacteristicProperties = | |
| (&[CharacteristicProperty::Write, CharacteristicProperty::WriteWithoutResponse]).into(); | |
| Characteristic { | |
| handle: CONTROL_POINT_HANDLE, | |
| uuid: BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID, | |
| properties: cp_properties, | |
| 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 = CharacteristicProperties::READ_NOTIFY; | |
| Characteristic { | |
| handle, | |
| uuid: BROADCAST_RECEIVE_STATE_UUID, | |
| properties, | |
| 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). Handle(1) is allocated to the Control Point | |
| // Characteristic. | |
| const FIRST_RECEIVE_STATE_HANDLE: Handle = Handle(2); | |
| let num_receive_states = self.receive_states.len(); | |
| let mut state = ServerState::new(num_receive_states); | |
| for (i, mut receive_state) in self.receive_states.into_iter().enumerate() { | |
| let handle = Handle(FIRST_RECEIVE_STATE_HANDLE.0 + i as u64); | |
| let _ = service_def.add_characteristic(Self::build_receive_state(handle)); | |
| if let BroadcastReceiveState::NonEmpty(ref mut r) = receive_state { | |
| r.source_id = state | |
| .allocate_source_id(handle) | |
| .map_err(|e| Error::Gatt(bt_gatt::types::Error::Gatt(e)))?; | |
| } | |
| state.receive_state_characteristics.insert( | |
| handle, | |
| PublishedReceiveStateCharacteristic { handle, state: receive_state }, | |
| ); | |
| } | |
| Ok(Server { service_def, local_service: Default::default(), state }) | |
| } | |
| } | |
| /// Events emitted by the [`Server`] when a remote GATT client writes to the | |
| /// Control Point. | |
| #[derive(Debug, PartialEq)] | |
| pub enum ServerEvent { | |
| /// A client requested a change in remote scanning state. | |
| RemoteScanState { peer_id: PeerId, is_scanning: bool }, | |
| /// A client requested adding a new Broadcast Source. | |
| AddSource { peer_id: PeerId, source_id: SourceId, operation: AddSourceOperation }, | |
| /// A client requested modifying an existing Broadcast Source. | |
| ModifySource { peer_id: PeerId, source_id: SourceId, operation: ModifySourceOperation }, | |
| /// A client provided a Broadcast Code for an encrypted Broadcast Source. | |
| SetBroadcastCode { peer_id: PeerId, source_id: SourceId, broadcast_code: BroadcastCode }, | |
| /// A client requested removing a Broadcast Source. | |
| RemoveSource { peer_id: PeerId, source_id: SourceId }, | |
| } | |
| impl ServerEvent { | |
| /// Attempts to decode a raw control point write request into a | |
| /// [`ServerEvent`]. | |
| fn decode(peer_id: PeerId, val_bytes: &[u8]) -> Result<Self, GattError> { | |
| if val_bytes.is_empty() { | |
| return Err(GattError::WriteRequestRejected); | |
| } | |
| let raw_opcode = val_bytes[0]; | |
| let Ok(opcode) = ControlPointOpcode::try_from(raw_opcode) else { | |
| return Err(ERROR_OPCODE_NOT_SUPPORTED); | |
| }; | |
| match opcode { | |
| ControlPointOpcode::RemoteScanStopped => { | |
| let _ = decode_control_point_op::<RemoteScanStoppedOperation>(val_bytes)?; | |
| Ok(ServerEvent::RemoteScanState { peer_id, is_scanning: false }) | |
| } | |
| ControlPointOpcode::RemoteScanStarted => { | |
| let _ = decode_control_point_op::<RemoteScanStartedOperation>(val_bytes)?; | |
| Ok(ServerEvent::RemoteScanState { peer_id, is_scanning: true }) | |
| } | |
| ControlPointOpcode::AddSource => { | |
| let operation = decode_control_point_op::<AddSourceOperation>(val_bytes)?; | |
| // `source_id` will be assigned internally by the Server. | |
| Ok(ServerEvent::AddSource { peer_id, source_id: 0, operation }) | |
| } | |
| ControlPointOpcode::ModifySource => { | |
| let operation = decode_control_point_op::<ModifySourceOperation>(val_bytes)?; | |
| Ok(ServerEvent::ModifySource { peer_id, source_id: operation.source_id, operation }) | |
| } | |
| ControlPointOpcode::SetBroadcastCode => { | |
| let operation = decode_control_point_op::<SetBroadcastCodeOperation>(val_bytes)?; | |
| Ok(ServerEvent::SetBroadcastCode { | |
| peer_id, | |
| source_id: operation.source_id, | |
| broadcast_code: operation.broadcast_code, | |
| }) | |
| } | |
| ControlPointOpcode::RemoveSource => { | |
| let operation = decode_control_point_op::<RemoveSourceOperation>(val_bytes)?; | |
| Ok(ServerEvent::RemoveSource { peer_id, source_id: operation.0 }) | |
| } | |
| } | |
| } | |
| } | |
| /// Attempts to decode a control point operation payload. | |
| fn decode_control_point_op<O: Decodable>(val_bytes: &[u8]) -> Result<O, GattError> { | |
| let (Ok(op), consumed) = O::decode(val_bytes) else { | |
| return Err(GattError::WriteRequestRejected); | |
| }; | |
| if consumed != val_bytes.len() { | |
| return Err(GattError::WriteRequestRejected); | |
| } | |
| Ok(op) | |
| } | |
| /// Internal state of the BASS GATT Server. | |
| #[derive(Debug)] | |
| struct ServerState { | |
| /// Broadcast Receive State characteristics identified by the assigned | |
| /// GATT handle. | |
| receive_state_characteristics: BTreeMap<Handle, PublishedReceiveStateCharacteristic>, | |
| /// Unique IDs tracking each assigned GATT Handle. | |
| source_id_to_handle: HashMap<SourceId, Handle>, | |
| /// Next available Source ID to be assigned to an empty Receive State | |
| /// characteristic. | |
| /// Must be in [1, 255]. See BASS spec v1.0 Section 3.2.1. | |
| next_source_id: NonZeroU8, | |
| } | |
| impl ServerState { | |
| fn new(capacity: usize) -> Self { | |
| Self { | |
| receive_state_characteristics: BTreeMap::new(), | |
| source_id_to_handle: HashMap::with_capacity(capacity), | |
| next_source_id: NonZeroU8::MIN, | |
| } | |
| } | |
| /// Allocates the next available, unused [`SourceId`] and maps it to | |
| /// `handle`. | |
| /// | |
| /// Returns the assigned ID on success. | |
| /// Returns `GattError::InsufficientResources` if all 255 Source IDs | |
| /// are currently in use. | |
| fn allocate_source_id(&mut self, handle: Handle) -> Result<SourceId, GattError> { | |
| // Upper bound on the maximum number of unique SourceIDs (u8 max). | |
| for _ in 0..NonZeroU8::MAX.get() { | |
| let id = self.next_source_id.get(); | |
| self.next_source_id = self.next_source_id.checked_add(1).unwrap_or(NonZeroU8::MIN); | |
| if !self.source_id_to_handle.contains_key(&id) { | |
| self.source_id_to_handle.insert(id, handle); | |
| return Ok(id); | |
| } | |
| } | |
| Err(GattError::InsufficientResources) | |
| } | |
| /// Allocates a new SourceId for the next available Receive State | |
| /// characteristic entry. | |
| /// | |
| /// Returns the assigned [`Handle`] and [`SourceId`] on success. | |
| fn allocate_receive_state_chrc(&mut self) -> Result<(Handle, SourceId), GattError> { | |
| let handle = self | |
| .receive_state_characteristics | |
| .values() | |
| .find(|c| c.state.is_empty()) | |
| .map(|c| c.handle) | |
| .ok_or(GattError::InsufficientResources)?; | |
| let source_id = self.allocate_source_id(handle)?; | |
| Ok((handle, source_id)) | |
| } | |
| /// Handles a read request for a characteristic on the BASS server. | |
| fn handle_read(&self, handle: Handle, offset: usize, responder: impl ReadResponder) { | |
| // Only the Broadcast Receive State characteristics can be read. | |
| // See BASS spec v1.0 Section 3.3.2. | |
| if handle == CONTROL_POINT_HANDLE { | |
| responder.error(GattError::ReadNotPermitted); | |
| return; | |
| } | |
| let Some(chrc) = self.receive_state_characteristics.get(&handle) else { | |
| responder.error(GattError::InvalidHandle); | |
| return; | |
| }; | |
| let len = chrc.state.encoded_len(); | |
| if offset > len { | |
| responder.error(GattError::InvalidOffset); | |
| return; | |
| } | |
| let mut buf = vec![0u8; len]; | |
| match chrc.state.encode(&mut buf) { | |
| Ok(_) => responder.respond(&buf[offset..]), | |
| Err(_) => responder.error(GattError::UnlikelyError), | |
| } | |
| } | |
| /// Handles a write request for a characteristic on the BASS server. | |
| fn handle_write( | |
| &mut self, | |
| peer_id: PeerId, | |
| handle: Handle, | |
| value: &[u8], | |
| ) -> Result<ControlPointWriteResult, GattError> { | |
| // Only the Broadcast Audio Scan Control Point characteristic can be | |
| // written to by a client. See BASS v1.0 Section 3.3. | |
| if handle != CONTROL_POINT_HANDLE { | |
| return Err(GattError::WriteNotPermitted); | |
| } | |
| self.handle_control_point_write(peer_id, value) | |
| } | |
| /// Returns true if the specified `source_id` is assigned to a non-empty | |
| /// Receive State characteristic. | |
| fn is_valid_source_id(&self, source_id: SourceId) -> bool { | |
| self.source_id_to_handle | |
| .get(&source_id) | |
| .and_then(|h| self.receive_state_characteristics.get(h)) | |
| .map_or(false, |s| !s.state.is_empty()) | |
| } | |
| fn handle_control_point_write( | |
| &mut self, | |
| peer_id: PeerId, | |
| val_bytes: &[u8], | |
| ) -> Result<ControlPointWriteResult, GattError> { | |
| let mut event = ServerEvent::decode(peer_id, val_bytes)?; | |
| let notification = match &mut event { | |
| ServerEvent::RemoteScanState { .. } => None, | |
| ServerEvent::AddSource { source_id, operation, .. } => { | |
| let (notification, assigned_source_id) = self.handle_add_source_write(operation)?; | |
| *source_id = assigned_source_id; | |
| Some(notification) | |
| } | |
| ServerEvent::ModifySource { operation, .. } => { | |
| let notification = self.handle_modify_source_write(operation)?; | |
| Some(notification) | |
| } | |
| ServerEvent::SetBroadcastCode { source_id, .. } => { | |
| if !self.is_valid_source_id(*source_id) { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| } | |
| None | |
| } | |
| ServerEvent::RemoveSource { source_id, .. } => { | |
| let notification = self.handle_remove_source_write(*source_id)?; | |
| Some(notification) | |
| } | |
| }; | |
| Ok(ControlPointWriteResult { event, notification }) | |
| } | |
| fn handle_add_source_write( | |
| &mut self, | |
| operation: &AddSourceOperation, | |
| ) -> Result<(PendingNotification, SourceId), GattError> { | |
| let (handle, source_id) = self.allocate_receive_state_chrc()?; | |
| let chrc = | |
| self.receive_state_characteristics.get_mut(&handle).expect("just checked existence"); | |
| let pa_sync_state = PaSyncState::from_pa_sync(operation.pa_sync); | |
| let subgroups = operation | |
| .subgroups | |
| .iter() | |
| .map(|s| BigSubgroup::new(Some(s.bis_sync.clone())).with_metadata(s.metadata.clone())) | |
| .collect(); | |
| let initial_state = ReceiveState::new( | |
| source_id, | |
| operation.advertiser_address_type, | |
| operation.advertiser_address, | |
| operation.advertising_sid, | |
| operation.broadcast_id, | |
| pa_sync_state, | |
| EncryptionStatus::NotEncrypted, | |
| subgroups, | |
| ); | |
| let new_state = BroadcastReceiveState::NonEmpty(initial_state); | |
| let mut value = vec![0u8; new_state.encoded_len()]; | |
| new_state.encode(&mut value).map_err(|_| GattError::UnlikelyError)?; | |
| chrc.state = new_state; | |
| Ok((PendingNotification { handle, value }, source_id)) | |
| } | |
| fn handle_modify_source_write( | |
| &mut self, | |
| operation: &ModifySourceOperation, | |
| ) -> Result<PendingNotification, GattError> { | |
| let Some(handle) = self.source_id_to_handle.get(&operation.source_id).copied() else { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| }; | |
| let Some(chrc) = self.receive_state_characteristics.get_mut(&handle) else { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| }; | |
| let BroadcastReceiveState::NonEmpty(ref state) = chrc.state else { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| }; | |
| let subgroups = operation | |
| .subgroups | |
| .iter() | |
| .map(|s| BigSubgroup::new(Some(s.bis_sync.clone())).with_metadata(s.metadata.clone())) | |
| .collect(); | |
| let mut modified_state = state.clone(); | |
| modified_state.subgroups = subgroups; | |
| modified_state.pa_sync_state = PaSyncState::from_pa_sync(operation.pa_sync); | |
| let new_state = BroadcastReceiveState::NonEmpty(modified_state); | |
| let mut value = vec![0u8; new_state.encoded_len()]; | |
| new_state.encode(&mut value).map_err(|_| GattError::UnlikelyError)?; | |
| chrc.state = new_state; | |
| Ok(PendingNotification { handle, value }) | |
| } | |
| fn handle_remove_source_write( | |
| &mut self, | |
| source_id: SourceId, | |
| ) -> Result<PendingNotification, GattError> { | |
| let Some(handle) = self.source_id_to_handle.get(&source_id).copied() else { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| }; | |
| let Some(chrc) = self.receive_state_characteristics.get_mut(&handle) else { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| }; | |
| if chrc.state.is_empty() { | |
| return Err(ERROR_INVALID_SOURCE_ID); | |
| } | |
| let _ = self.source_id_to_handle.remove(&source_id); | |
| chrc.state = BroadcastReceiveState::Empty; | |
| let mut value = vec![0u8; chrc.state.encoded_len()]; | |
| chrc.state.encode(&mut value).map_err(|_| GattError::UnlikelyError)?; | |
| Ok(PendingNotification { handle, value }) | |
| } | |
| } | |
| /// 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>, | |
| state: ServerState, | |
| } | |
| 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() | |
| } | |
| /// Publishes the service to the GATT database. | |
| pub fn publish(&mut self, server: T::Server) -> Result<(), Error> { | |
| if !matches!(self.local_service, LocalServiceState::NotPublished { .. }) { | |
| return Err(Error::AlreadyPublished); | |
| } | |
| let LocalServiceState::NotPublished { waker } = std::mem::replace( | |
| &mut self.local_service, | |
| LocalServiceState::Preparing { fut: server.prepare(self.service_def.clone()) }, | |
| ) else { | |
| unreachable!(); | |
| }; | |
| if let Some(w) = waker { | |
| w.wake(); | |
| } | |
| Ok(()) | |
| } | |
| /// Adds a new [`ReceiveState`] to the next available characteristic slot | |
| /// and sends a GATT notification to all subscribed peers. | |
| /// | |
| /// Characteristic slots are allocated up-front when constructing the server | |
| /// via [`ServerBuilder`]. This method populates an available empty slot | |
| /// with `state` when initiated locally. | |
| /// | |
| /// Returns the assigned [`SourceId`] for the characteristic on success. | |
| /// Returns [`Error::ServerFull`] if all characteristic slots are occupied. | |
| pub fn add_receive_state(&mut self, mut state: ReceiveState) -> Result<SourceId, Error> { | |
| let (_handle, source_id) = | |
| self.state.allocate_receive_state_chrc().map_err(|_| Error::ServerFull)?; | |
| state.source_id = source_id; | |
| self.set_receive_state_and_notify(source_id, BroadcastReceiveState::NonEmpty(state))?; | |
| Ok(source_id) | |
| } | |
| /// Updates the existing [`BroadcastReceiveState`] characteristic with the | |
| /// provided `state` and sends a GATT notification to all subscribed peers. | |
| /// | |
| /// Returns [`Error::InvalidSourceId`] if there is no such characteristic. | |
| pub fn update_receive_state(&mut self, state: ReceiveState) -> Result<(), Error> { | |
| self.set_receive_state_and_notify(state.source_id(), BroadcastReceiveState::NonEmpty(state)) | |
| } | |
| /// Clears the entry for the specified Broadcast Receive State | |
| /// characteristic and sends a GATT notification to all subscribed peers. | |
| /// | |
| /// Returns [`Error::InvalidSourceId`] if there is no such characteristic. | |
| pub fn clear_receive_state(&mut self, source_id: SourceId) -> Result<(), Error> { | |
| self.set_receive_state_and_notify(source_id, BroadcastReceiveState::Empty) | |
| } | |
| /// Updates the state for the specified Broadcast Receive Characteristic and | |
| /// sends a GATT notification to all subscribed peers. | |
| /// | |
| /// Returns [`Error::InvalidSourceId`] if there is no such characteristic. | |
| fn set_receive_state_and_notify( | |
| &mut self, | |
| source_id: SourceId, | |
| new_state: BroadcastReceiveState, | |
| ) -> Result<(), Error> { | |
| let handle = *self | |
| .state | |
| .source_id_to_handle | |
| .get(&source_id) | |
| .ok_or(Error::InvalidSourceId(source_id))?; | |
| let chrc = self | |
| .state | |
| .receive_state_characteristics | |
| .get_mut(&handle) | |
| .ok_or(Error::InvalidSourceId(source_id))?; | |
| let mut value = vec![0u8; new_state.encoded_len()]; | |
| new_state.encode(&mut value)?; | |
| if new_state.is_empty() { | |
| self.state.source_id_to_handle.remove(&source_id); | |
| } else { | |
| self.state.source_id_to_handle.insert(source_id, handle); | |
| } | |
| chrc.state = new_state; | |
| self.local_service.notify(&PendingNotification { handle, value }); | |
| Ok(()) | |
| } | |
| } | |
| impl<T: bt_gatt::ServerTypes> Stream for Server<T> { | |
| type Item = Result<ServerEvent, Error>; | |
| fn poll_next( | |
| mut self: std::pin::Pin<&mut Self>, | |
| cx: &mut std::task::Context<'_>, | |
| ) -> Poll<Option<Self::Item>> { | |
| loop { | |
| let mut this = self.as_mut().project(); | |
| let gatt_event = match futures::ready!(this.local_service.as_mut().poll_next(cx)) { | |
| Some(Ok(event)) => event, | |
| Some(Err(e)) => return Poll::Ready(Some(Err(e))), | |
| None => return Poll::Ready(None), | |
| }; | |
| match gatt_event { | |
| ServiceEvent::Read { handle, offset, responder, .. } => { | |
| this.state.handle_read(handle, offset as usize, responder); | |
| } | |
| ServiceEvent::Write { handle, peer_id, value, responder, .. } => { | |
| let val_vec = value.to_owned(); | |
| match this.state.handle_write(peer_id, handle, &val_vec) { | |
| Ok(result) => { | |
| if let Some(notification) = result.notification { | |
| this.local_service.notify(¬ification); | |
| } | |
| responder.acknowledge(); | |
| return Poll::Ready(Some(Ok(result.event))); | |
| } | |
| Err(err) => { | |
| responder.error(err); | |
| } | |
| } | |
| } | |
| _ => { | |
| // TODO(b/534436439): Track CCC for Broadcast Receive State | |
| // characteristics and `PeerInfo` if needed. | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use assert_matches::assert_matches; | |
| use bt_bap::types::BroadcastId; | |
| use bt_common::core::{Address, AddressType, AdvertisingSetId, PeriodicAdvertisingInterval}; | |
| use bt_common::generic_audio::metadata_ltv::Metadata; | |
| use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes}; | |
| use bt_gatt::types::GattError; | |
| use futures::task::Context; | |
| use futures::FutureExt; | |
| use futures::StreamExt; | |
| use std::task::Poll; | |
| use crate::types::{BigSubgroup, EncryptionStatus, PaSync, PaSyncState, ReceiveState}; | |
| fn make_test_inner_receive_state(source_id: SourceId) -> ReceiveState { | |
| ReceiveState::new( | |
| source_id, | |
| AddressType::Public, | |
| Address::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), | |
| AdvertisingSetId::try_from(1).unwrap(), | |
| BroadcastId::try_from(0x123456).unwrap(), | |
| PaSyncState::NotSynced, | |
| EncryptionStatus::NotEncrypted, | |
| vec![], | |
| ) | |
| } | |
| fn make_test_receive_state(source_id: SourceId) -> BroadcastReceiveState { | |
| BroadcastReceiveState::NonEmpty(make_test_inner_receive_state(source_id)) | |
| } | |
| #[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))); | |
| } | |
| fn setup_test_server( | |
| receive_states: Vec<BroadcastReceiveState>, | |
| ) -> ( | |
| Server<FakeTypes>, | |
| FakeServer, | |
| futures::channel::mpsc::UnboundedReceiver<FakeServerEvent>, | |
| Context<'static>, | |
| ) { | |
| let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref()); | |
| let (fake_gatt_server, mut event_receiver) = FakeServer::new(); | |
| let mut event_stream = event_receiver.next(); | |
| let mut builder = ServerBuilder::new(); | |
| for state in receive_states { | |
| builder = builder.add_receive_state_characteristic(state); | |
| } | |
| let mut server = builder.build::<FakeTypes>().expect("building server works"); | |
| server.publish(fake_gatt_server.clone()).expect("publish ok"); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| assert!(matches!( | |
| event_stream.poll_unpin(&mut noop_cx), | |
| Poll::Ready(Some(FakeServerEvent::Published { .. })) | |
| )); | |
| (server, fake_gatt_server, event_receiver, noop_cx) | |
| } | |
| #[test] | |
| fn publish_server() { | |
| 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()); | |
| // Polling stream completes publication preparation | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| 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 (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty, make_test_receive_state(2)]); | |
| let mut event_stream = event_receiver.next(); | |
| assert!(server.is_published()); | |
| // 1. Read empty Receive State slot (Handle 2) | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, Handle(2), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_eq!(value.unwrap(), vec![]); | |
| // 2. Read populated Receive State slot (Handle 3) | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, Handle(3), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(3)); | |
| assert!(!value.unwrap().is_empty()); | |
| // 3. Read unknown handle (Handle 99) | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, Handle(99), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(99)); | |
| assert!(matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::InvalidHandle) | |
| )); | |
| } | |
| #[test] | |
| fn control_point_remote_scan_state_changed() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // Simulate RemoteScanStarted request | |
| let raw_cmd = vec![ControlPointOpcode::RemoteScanStarted as u8]; | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_cmd); | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| assert_eq!(event, ServerEvent::RemoteScanState { peer_id: PeerId(1), is_scanning: true }); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| // Simulate RemoteScanStopped request | |
| let raw_cmd = vec![ControlPointOpcode::RemoteScanStopped as u8]; | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_cmd); | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| assert_eq!(event, ServerEvent::RemoteScanState { peer_id: PeerId(1), is_scanning: false }); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| } | |
| #[test] | |
| fn control_point_invalid_opcode_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // Simulate invalid opcode request | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, vec![0xFF]); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::ApplicationError80) | |
| )); | |
| } | |
| #[test] | |
| fn control_point_unallocated_source_id_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // Operation targeting SourceId = 1 (slot exists, but is empty) | |
| let modify_op = ModifySourceOperation::new( | |
| 1, | |
| PaSync::DoNotSync, | |
| PeriodicAdvertisingInterval(0xFFFF), | |
| vec![], | |
| ); | |
| let mut raw_bytes = vec![0u8; modify_op.encoded_len()]; | |
| modify_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::ApplicationError81) | |
| )); | |
| } | |
| #[test] | |
| fn control_point_nonexistent_source_id_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // ModifySource operation targeting SourceId = 99 (does not exist) | |
| let modify_op = ModifySourceOperation::new( | |
| 99, | |
| PaSync::DoNotSync, | |
| PeriodicAdvertisingInterval(0xFFFF), | |
| vec![], | |
| ); | |
| let mut raw_bytes = vec![0u8; modify_op.encoded_len()]; | |
| modify_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::ApplicationError81) | |
| )); | |
| } | |
| #[test] | |
| fn control_point_extra_bytes_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // RemoteScanStarted opcode (1 byte) with extra trailing byte (0xFF) | |
| let raw_cmd = vec![ControlPointOpcode::RemoteScanStarted as u8, 0xFF]; | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_cmd); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::WriteRequestRejected) | |
| )); | |
| } | |
| #[test] | |
| fn control_point_add_source_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(1), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| // 1. Remote client writes AddSource to Control Point (Handle 1) | |
| let add_op = AddSourceOperation::new( | |
| AddressType::Public, | |
| Address::new([1, 2, 3, 4, 5, 6]), | |
| AdvertisingSetId::try_from(1).unwrap(), | |
| BroadcastId::try_from(0x123456).unwrap(), | |
| PaSync::SyncPastAvailable, | |
| PeriodicAdvertisingInterval(0x0010), | |
| vec![], | |
| ); | |
| let mut raw_bytes = vec![0u8; add_op.encoded_len()]; | |
| add_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| // Server stream emits AddSource event | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| let ServerEvent::AddSource { peer_id, source_id, operation } = event else { | |
| panic!("Expected AddSource event, got {event:?}"); | |
| }; | |
| assert_eq!(peer_id, PeerId(1)); | |
| assert_eq!(source_id, 1); | |
| assert_eq!(operation, add_op); | |
| // First notification: Initial minimal state created upon Control Point | |
| // write | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected first Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| let (first_notification_state, _) = BroadcastReceiveState::decode(&value); | |
| let BroadcastReceiveState::NonEmpty(initial_state) = | |
| first_notification_state.expect("valid decode") | |
| else { | |
| panic!("Expected NonEmpty state in first notification"); | |
| }; | |
| assert_eq!(initial_state.source_id, 1); | |
| assert_eq!(initial_state.pa_sync_state, PaSyncState::SyncInfoRequest); | |
| // Write response is sent to client | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| // 2. Upper layer client processes the event, establishes PA sync, and | |
| // updates state | |
| let mut updated_state = initial_state; | |
| updated_state.pa_sync_state = PaSyncState::Synced; | |
| assert!(server.update_receive_state(updated_state).is_ok()); | |
| // Second notification: Upper layer client calls public API method | |
| // (update_receive_state) | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, peers, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected second Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_eq!(peers, vec![PeerId(1)]); | |
| let (second_notification_state, _) = BroadcastReceiveState::decode(&value); | |
| let BroadcastReceiveState::NonEmpty(final_state) = | |
| second_notification_state.expect("valid decode") | |
| else { | |
| panic!("Expected NonEmpty state in second notification"); | |
| }; | |
| assert_eq!(final_state.source_id, 1); | |
| assert_eq!(final_state.pa_sync_state, PaSyncState::Synced); | |
| } | |
| #[test] | |
| fn control_point_modify_source_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(1), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| // 1. Remote client writes ModifySource to Control Point (Handle 1) | |
| let modify_op = ModifySourceOperation::new( | |
| 1, | |
| PaSync::DoNotSync, | |
| PeriodicAdvertisingInterval(0xFFFF), | |
| vec![], | |
| ); | |
| let mut raw_bytes = vec![0u8; modify_op.encoded_len()]; | |
| modify_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| // Server stream emits ModifySource event | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| assert_eq!( | |
| event, | |
| ServerEvent::ModifySource { peer_id: PeerId(1), source_id: 1, operation: modify_op } | |
| ); | |
| // First notification: State updated upon Control Point write | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected first Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| let (first_state, _) = BroadcastReceiveState::decode(&value); | |
| let BroadcastReceiveState::NonEmpty(modified_state) = first_state.expect("valid decode") | |
| else { | |
| panic!("Expected NonEmpty state"); | |
| }; | |
| assert_eq!(modified_state.pa_sync_state, PaSyncState::NotSynced); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| // 2. Upper layer client processes the event and calls public API method | |
| // (update_receive_state) | |
| let mut updated_state = modified_state; | |
| updated_state.pa_sync_state = PaSyncState::FailedToSync; | |
| assert!(server.update_receive_state(updated_state).is_ok()); | |
| // Second notification: Upper layer client calls public API method | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, peers, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected second Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_eq!(peers, vec![PeerId(1)]); | |
| let (second_state, _) = BroadcastReceiveState::decode(&value); | |
| let BroadcastReceiveState::NonEmpty(final_state) = second_state.expect("valid decode") | |
| else { | |
| panic!("Expected NonEmpty state"); | |
| }; | |
| assert_eq!(final_state.pa_sync_state, PaSyncState::FailedToSync); | |
| } | |
| #[test] | |
| fn control_point_set_broadcast_code_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| let code = BroadcastCode::new([0xAB; 16]); | |
| let set_code_op = SetBroadcastCodeOperation::new(1, code); | |
| let mut raw_bytes = vec![0u8; set_code_op.encoded_len()]; | |
| set_code_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| assert_eq!( | |
| event, | |
| ServerEvent::SetBroadcastCode { | |
| peer_id: PeerId(1), | |
| source_id: 1, | |
| broadcast_code: code, | |
| } | |
| ); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| } | |
| #[test] | |
| fn control_point_remove_source_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(1), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let remove_op = RemoveSourceOperation::new(1); | |
| let mut raw_bytes = vec![0u8; remove_op.encoded_len()]; | |
| remove_op.encode(&mut raw_bytes).expect("encode ok"); | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, raw_bytes); | |
| let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else { | |
| panic!("Expected ServerEvent"); | |
| }; | |
| assert_eq!(event, ServerEvent::RemoveSource { peer_id: PeerId(1), source_id: 1 }); | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert!(value.is_empty()); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert!(value.is_ok()); | |
| } | |
| #[test] | |
| fn write_non_control_point_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // Writing to BroadcastReceiveState characteristic handle (Handle 2) | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(2), 0, vec![0x01]); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::WriteNotPermitted) | |
| ); | |
| } | |
| #[test] | |
| fn control_point_empty_payload_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| // Writing empty payload to Control Point | |
| fake_gatt_server.incoming_write(PeerId(1), BASS_SERVICE_ID, Handle(1), 0, vec![]); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::WriteResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected WriteResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(1)); | |
| assert_matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::WriteRequestRejected) | |
| ); | |
| } | |
| #[test] | |
| fn read_control_point_fails() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, CONTROL_POINT_HANDLE, 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, CONTROL_POINT_HANDLE); | |
| assert_matches!( | |
| value.unwrap_err(), | |
| bt_gatt::types::Error::Gatt(GattError::ReadNotPermitted) | |
| ); | |
| } | |
| #[test] | |
| fn read_receive_state_invalid_offset() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, Handle(2), 10); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_matches!(value.unwrap_err(), bt_gatt::types::Error::Gatt(GattError::InvalidOffset)); | |
| } | |
| #[test] | |
| fn publish_failure() { | |
| 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).is_ok()); | |
| assert_matches!( | |
| server.poll_next_unpin(&mut noop_cx), | |
| Poll::Ready(Some(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 (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![state]); | |
| let mut event_stream = event_receiver.next(); | |
| // Verify slot's internal state and encoded read value have source_id = | |
| // 1 | |
| fake_gatt_server.incoming_read(PeerId(1), BASS_SERVICE_ID, Handle(2), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle: _, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| let buf = value.expect("ok"); | |
| assert_eq!(buf[0], 1); | |
| } | |
| #[test] | |
| fn add_receive_state_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(10), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let state = make_test_inner_receive_state(99); | |
| let result = server.add_receive_state(state); | |
| assert_eq!(result.expect("add ok"), 1); | |
| // Expect the GATT notification for the updated characteristic. | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, peers, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_eq!(value[0], 1); | |
| assert_eq!(peers, vec![PeerId(10)]); | |
| } | |
| #[test] | |
| fn add_receive_state_when_full_fails() { | |
| let (mut server, _fake_gatt_server, _event_receiver, _noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let state = make_test_inner_receive_state(2); | |
| let result = server.add_receive_state(state); | |
| assert_matches!(result, Err(Error::ServerFull)); | |
| } | |
| #[test] | |
| fn update_receive_state_invalid_source_id_fails() { | |
| let (mut server, _fake_gatt_server, _event_receiver, _noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::Empty]); | |
| let state = make_test_inner_receive_state(99); | |
| let result = server.update_receive_state(state); | |
| assert_matches!(result, Err(Error::InvalidSourceId(99))); | |
| } | |
| #[test] | |
| fn update_receive_state_notifies_peers() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(10), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let new_inner = make_test_inner_receive_state(1); | |
| let expected_state = BroadcastReceiveState::NonEmpty(new_inner.clone()); | |
| let mut expected_bytes = vec![0u8; expected_state.encoded_len()]; | |
| expected_state.encode(&mut expected_bytes).expect("encode ok"); | |
| let result = server.update_receive_state(new_inner); | |
| assert!(result.is_ok()); | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, peers, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert_eq!(value, expected_bytes); | |
| assert_eq!(peers, vec![PeerId(10)]); | |
| } | |
| #[test] | |
| fn clear_receive_state_success() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| fake_gatt_server.incoming_client_configuration( | |
| PeerId(10), | |
| BASS_SERVICE_ID, | |
| Handle(2), | |
| bt_gatt::server::NotificationType::Notify, | |
| ); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let result = server.clear_receive_state(1); | |
| assert!(result.is_ok()); | |
| let Poll::Ready(Some(FakeServerEvent::Notified { handle, value, peers, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected Notified event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| assert!(value.is_empty()); | |
| assert_eq!(peers, vec![PeerId(10)]); | |
| } | |
| #[test] | |
| fn clear_and_reuse_receive_state_slot() { | |
| let (mut server, _fake_gatt_server, _event_receiver, _noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| // Clear existing state in slot 1 | |
| assert!(server.clear_receive_state(1).is_ok()); | |
| // Adding a new state should successfully reuse the cleared slot and | |
| // allocate the next ID | |
| let new_state = make_test_inner_receive_state(50); | |
| let result = server.add_receive_state(new_state); | |
| assert_eq!(result.expect("add ok"), 2); | |
| } | |
| #[test] | |
| fn add_receive_state_sequential_slots() { | |
| let (mut server, _fake_gatt_server, _event_receiver, _noop_cx) = setup_test_server(vec![ | |
| BroadcastReceiveState::Empty, | |
| BroadcastReceiveState::Empty, | |
| BroadcastReceiveState::Empty, | |
| ]); | |
| let state1 = make_test_inner_receive_state(10); | |
| let state2 = make_test_inner_receive_state(20); | |
| let state3 = make_test_inner_receive_state(30); | |
| assert_eq!(server.add_receive_state(state1).unwrap(), 1); | |
| assert_eq!(server.add_receive_state(state2).unwrap(), 2); | |
| assert_eq!(server.add_receive_state(state3).unwrap(), 3); | |
| // Fourth add should fail as server is full | |
| let state4 = make_test_inner_receive_state(40); | |
| assert_matches!(server.add_receive_state(state4), Err(Error::ServerFull)); | |
| } | |
| #[test] | |
| fn read_cleared_receive_state_returns_empty() { | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![make_test_receive_state(1)]); | |
| let mut event_stream = event_receiver.next(); | |
| // Clear the state | |
| assert!(server.clear_receive_state(1).is_ok()); | |
| // Perform a GATT Read on Handle(2) | |
| fake_gatt_server.incoming_read(PeerId(10), BASS_SERVICE_ID, Handle(2), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| let buf = value.expect("read ok"); | |
| assert!(buf.is_empty()); | |
| } | |
| #[test] | |
| fn update_receive_state_encoding_failure_preserves_state() { | |
| let initial_inner = make_test_inner_receive_state(1); | |
| let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) = | |
| setup_test_server(vec![BroadcastReceiveState::NonEmpty(initial_inner.clone())]); | |
| let mut event_stream = event_receiver.next(); | |
| // Create an invalid ReceiveState whose metadata total length exceeds | |
| // 255 bytes (causing BigSubgroup encode to fail) | |
| let vendor_metadata = vec![Metadata::AudioActiveState(true); 130]; | |
| let mut invalid_inner = initial_inner.clone(); | |
| invalid_inner.subgroups = vec![BigSubgroup::new(None).with_metadata(vendor_metadata)]; | |
| // Attempting to update to invalid_inner should return a Packet encoding | |
| // error | |
| let result = server.update_receive_state(invalid_inner); | |
| assert_matches!(result, Err(Error::Packet(_))); | |
| // Perform a GATT Read on Handle(2) to verify internal memory state was | |
| // NOT mutated to invalid_state | |
| fake_gatt_server.incoming_read(PeerId(10), BASS_SERVICE_ID, Handle(2), 0); | |
| let _ = server.poll_next_unpin(&mut noop_cx); | |
| let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = | |
| event_stream.poll_unpin(&mut noop_cx) | |
| else { | |
| panic!("Expected ReadResponded event"); | |
| }; | |
| assert_eq!(handle, Handle(2)); | |
| // Read value should successfully decode back to initial_inner state | |
| let buf = value.expect("read ok"); | |
| let (decoded_state, _) = BroadcastReceiveState::decode(&buf); | |
| assert_eq!( | |
| decoded_state.expect("decode ok"), | |
| BroadcastReceiveState::NonEmpty(initial_inner) | |
| ); | |
| } | |
| #[test] | |
| fn allocate_source_id_skips_in_use_ids() { | |
| let mut state = ServerState::new(5); | |
| state.source_id_to_handle.insert(1, Handle(10)); | |
| // Force the next ID to be 1 to simulate wrap around. | |
| state.next_source_id = NonZeroU8::new(1).unwrap(); | |
| let allocated_id = state.allocate_source_id(Handle(20)).unwrap(); | |
| assert_eq!(allocated_id, 2); | |
| assert_eq!(state.source_id_to_handle.get(&2), Some(&Handle(20))); | |
| } | |
| #[test] | |
| fn allocate_source_id_error_when_full() { | |
| let mut state = ServerState::new(255); | |
| for id in 1..=255 { | |
| state.source_id_to_handle.insert(id, Handle(u64::from(id))); | |
| } | |
| assert_eq!(state.allocate_source_id(Handle(300)), Err(GattError::InsufficientResources)); | |
| } | |
| } |