rust/bt-bass: Implement Stream for BASS Server

Add a Stream implementation for emitting relevant BASS events. Upper
layer clients can consume these events and take action for the various
control point operations.

Handle GATT read requests on the Receive State Characteristic.

Handle GATT write requests on the Control Point Characteristic.

Refactor SourceID and other state management into a separate struct for
clearer separation of concerns.

Bug: 534436439
Test: cargo test, ./presubmit.sh

Change-Id: Id5f14161f9aa9ef600b3fa518b70c182d1701606
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3180
diff --git a/rust/bt-bap/src/types.rs b/rust/bt-bap/src/types.rs
index f3946d6..5e62c73 100644
--- a/rust/bt-bap/src/types.rs
+++ b/rust/bt-bap/src/types.rs
@@ -85,6 +85,19 @@
     }
 }
 
+/// A 16-octet (128-bit) Broadcast Code used for decrypting encrypted
+/// Broadcast Audio Streams. See BAP v1.0.1 Section 3.7.2.2 for more details.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct BroadcastCode(pub [u8; 16]);
+
+impl BroadcastCode {
+    pub const BYTE_SIZE: usize = 16;
+
+    pub fn new(raw_value: [u8; 16]) -> Self {
+        Self(raw_value)
+    }
+}
+
 /// To associate a PA, used to expose broadcast Audio Stream parameters, with a
 /// broadcast Audio Stream, the Broadcast Source shall transmit EA PDUs that
 /// include the following data. This struct represents the AD data value
diff --git a/rust/bt-bass/src/client.rs b/rust/bt-bass/src/client.rs
index d6d239f..a1188c2 100644
--- a/rust/bt-bass/src/client.rs
+++ b/rust/bt-bass/src/client.rs
@@ -13,7 +13,7 @@
 use log::warn;
 use parking_lot::Mutex;
 
-use bt_bap::types::BroadcastId;
+use bt_bap::types::{BroadcastCode, BroadcastId};
 use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
 use bt_common::generic_audio::metadata_ltv::Metadata;
 use bt_common::packet_encoding::Decodable;
@@ -371,7 +371,7 @@
     ) -> Result<(), Error> {
         let source_id = self.get_source_id(&broadcast_id)?;
 
-        let op = SetBroadcastCodeOperation::new(source_id, broadcast_code.clone());
+        let op = SetBroadcastCodeOperation::new(source_id, BroadcastCode::new(broadcast_code));
         self.write_to_bascp(op).await?;
 
         // Save the broadcast code we sent.
diff --git a/rust/bt-bass/src/lib.rs b/rust/bt-bass/src/lib.rs
index e32b283..3c3b2fa 100644
--- a/rust/bt-bass/src/lib.rs
+++ b/rust/bt-bass/src/lib.rs
@@ -3,10 +3,13 @@
 // found in the LICENSE file.
 
 pub mod client;
-pub mod server;
-pub mod types;
 pub use crate::client::error::Error as ClientError;
-pub use crate::server::Error as ServerError;
+
+pub mod server;
+pub use crate::server::error::Error as ServerError;
+pub use crate::server::ServerEvent;
+
+pub mod types;
 
 #[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
index c7bbde7..715a803 100644
--- a/rust/bt-bass/src/server.rs
+++ b/rust/bt-bass/src/server.rs
@@ -4,22 +4,28 @@
 
 //! Implements the Broadcast Audio Scan Service (BASS) server role.
 
-use bt_gatt::server::{LocalService, Server as _, ServiceDefinition, ServiceId};
+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, CharacteristicProperty, Handle, SecurityLevels, ServiceKind,
+    AttributePermissions, CharacteristicProperty, GattError, Handle, SecurityLevels, ServiceKind,
 };
 use bt_gatt::Characteristic;
+use futures::stream::Stream;
 use futures::Future;
 use pin_project::pin_project;
-use std::collections::HashMap;
+use std::collections::{BTreeMap, HashMap};
+use std::num::NonZeroU8;
+use std::task::{Poll, Waker};
 
-use crate::types::{
-    BroadcastReceiveState, SourceId, BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID,
-    BROADCAST_AUDIO_SCAN_SERVICE_UUID, BROADCAST_RECEIVE_STATE_UUID,
-};
+use crate::types::*;
 
 pub mod error;
-pub use error::Error;
+use error::Error;
 
 /// Service identifier assigned to the published BASS GATT service instance.
 const BASS_SERVICE_ID: ServiceId = ServiceId::new(1);
@@ -35,9 +41,6 @@
 /// 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.
@@ -48,19 +51,26 @@
 #[pin_project(project = LocalServiceProj)]
 enum LocalServiceState<T: bt_gatt::ServerTypes> {
     /// Service definition has not been registered in the GATT database.
-    NotPublished,
+    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 },
+    Published {
+        service: T::LocalService,
+        #[pin]
+        events: T::ServiceEventStream,
+    },
+    Terminated,
 }
 
 impl<T: bt_gatt::ServerTypes> Default for LocalServiceState<T> {
     fn default() -> Self {
-        Self::NotPublished
+        Self::NotPublished { waker: None }
     }
 }
 
@@ -68,28 +78,45 @@
     fn is_published(&self) -> bool {
         matches!(self, LocalServiceState::Published { .. })
     }
+}
 
-    fn poll_publish(
+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<'_>,
-    ) -> 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)))
-                    }
+    ) -> 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)
+                        }
+                    };
                 }
             }
-            LocalServiceProj::Published { .. } => std::task::Poll::Ready(Ok(())),
         }
     }
 }
@@ -166,37 +193,318 @@
 
         let _ = service_def.add_characteristic(Self::build_control_point());
 
-        // Broadcast Receive State characteristics (Read, Notify; Encryption Required)
+        // 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 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 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));
 
-            // 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;
+            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)))?;
             }
 
-            receive_state_characteristics
-                .insert(handle, PublishedReceiveStateCharacteristic { source_id, handle, state });
-            source_id_to_handle.insert(source_id, handle);
+            state.receive_state_characteristics.insert(
+                handle,
+                PublishedReceiveStateCharacteristic { handle, state: receive_state },
+            );
         }
 
-        let next_source_id = (num_receive_states + 1) as SourceId;
+        Ok(Server { service_def, local_service: Default::default(), state })
+    }
+}
 
-        Ok(Server {
-            service_def,
-            local_service: Default::default(),
-            control_point_handle: CONTROL_POINT_HANDLE,
-            receive_state_characteristics,
-            source_id_to_handle,
-            next_source_id,
-        })
+/// 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)
+    }
+
+    /// 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],
+        responder: impl WriteResponder,
+    ) -> Option<ServerEvent> {
+        // 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 {
+            responder.error(GattError::WriteNotPermitted);
+            return None;
+        }
+
+        match self.handle_control_point_write(peer_id, value) {
+            Ok(event) => {
+                responder.acknowledge();
+                Some(event)
+            }
+            Err(err) => {
+                responder.error(err);
+                None
+            }
+        }
+    }
+
+    /// 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<ServerEvent, GattError> {
+        let mut event = ServerEvent::decode(peer_id, val_bytes)?;
+
+        match &mut event {
+            ServerEvent::RemoteScanState { .. } => {}
+            ServerEvent::AddSource { source_id, operation, .. } => {
+                *source_id = self.handle_add_source_write(operation)?;
+            }
+            ServerEvent::ModifySource { operation, .. } => {
+                self.handle_modify_source_write(operation)?;
+            }
+            ServerEvent::SetBroadcastCode { source_id, .. } => {
+                if !self.is_valid_source_id(*source_id) {
+                    return Err(ERROR_INVALID_SOURCE_ID);
+                }
+            }
+            ServerEvent::RemoveSource { source_id, .. } => {
+                self.handle_remove_source_write(*source_id)?;
+            }
+        }
+
+        Ok(event)
+    }
+
+    fn handle_add_source_write(
+        &mut self,
+        operation: &AddSourceOperation,
+    ) -> Result<SourceId, GattError> {
+        // Find the next available empty Receive State characteristic entry.
+        let handle = {
+            let Some(chrc) =
+                self.receive_state_characteristics.values().find(|c| c.state.is_empty())
+            else {
+                return Err(GattError::InsufficientResources);
+            };
+            chrc.handle
+        };
+
+        let source_id = self.allocate_source_id(handle)?;
+        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,
+        );
+
+        chrc.state = BroadcastReceiveState::NonEmpty(initial_state);
+        Ok(source_id)
+    }
+
+    fn handle_modify_source_write(
+        &mut self,
+        operation: &ModifySourceOperation,
+    ) -> Result<(), GattError> {
+        let Some(handle) = self.source_id_to_handle.get(&operation.source_id) 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 mut 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();
+
+        state.subgroups = subgroups;
+        state.pa_sync_state = PaSyncState::from_pa_sync(operation.pa_sync);
+
+        Ok(())
+    }
+
+    fn handle_remove_source_write(&mut self, source_id: SourceId) -> Result<(), 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;
+        Ok(())
     }
 }
 
@@ -209,14 +517,7 @@
     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,
+    state: ServerState,
 }
 
 impl<T: bt_gatt::ServerTypes> Server<T> {
@@ -225,66 +526,59 @@
         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 { .. }
-        ) {
+        if !matches!(self.local_service, LocalServiceState::NotPublished { .. }) {
             return Err(Error::AlreadyPublished);
         }
 
-        let LocalServiceState::NotPublished = std::mem::replace(
+        let LocalServiceState::NotPublished { waker } = 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;
+        if let Some(w) = waker {
+            w.wake();
         }
 
-        let Some(slot) = self.receive_state_characteristics.get(&handle) else {
-            responder.error(GattError::InvalidHandle);
-            return;
-        };
+        Ok(())
+    }
+}
 
-        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..]);
+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();
+                    if let Some(event) =
+                        this.state.handle_write(peer_id, handle, &val_vec, responder)
+                    {
+                        return Poll::Ready(Some(Ok(event)));
+                    }
+                }
+                _ => {
+                    // TODO(b/534436439): Track CCC for Broadcast Receive State
+                    // characteristics and `PeerInfo` if needed.
+                }
             }
-        } else {
-            responder.error(GattError::UnlikelyError);
         }
     }
 }
@@ -293,29 +587,15 @@
 mod tests {
     use super::*;
     use bt_bap::types::BroadcastId;
-    use bt_common::core::{AddressType, AdvertisingSetId};
-    use bt_gatt::server::ReadResponder;
+    use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
     use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes};
     use bt_gatt::types::GattError;
+    use futures::task::Context;
     use futures::FutureExt;
-    use parking_lot::Mutex;
-    use std::sync::Arc;
+    use futures::StreamExt;
+    use std::task::Poll;
 
-    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));
-        }
-    }
+    use crate::types::{EncryptionStatus, PaSync, PaSyncState, ReceiveState};
 
     fn make_test_receive_state(source_id: SourceId) -> BroadcastReceiveState {
         BroadcastReceiveState::NonEmpty(ReceiveState::new(
@@ -345,12 +625,36 @@
         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() {
-        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()
@@ -367,8 +671,8 @@
         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());
+        // 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 })) =
@@ -384,57 +688,419 @@
 
     #[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");
+        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)
-        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![]);
+        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)
-        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());
+        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)
-        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);
+        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();
+
+        let add_op = AddSourceOperation::new(
+            AddressType::Public,
+            [1, 2, 3, 4, 5, 6],
+            AdvertisingSetId::try_from(1).unwrap(),
+            BroadcastId::try_from(0x123456).unwrap(),
+            PaSync::DoNotSync,
+            PeriodicAdvertisingInterval(0xFFFF),
+            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);
+
+        let Poll::Ready(Some(Ok(event))) = server.poll_next_unpin(&mut noop_cx) else {
+            panic!("Expected ServerEvent");
+        };
+        assert_eq!(
+            event,
+            ServerEvent::AddSource { peer_id: PeerId(1), source_id: 1, operation: add_op }
+        );
+
+        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_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();
+
+        let modify_op = ModifySourceOperation::new(
+            1,
+            PaSync::SyncPastAvailable,
+            PeriodicAdvertisingInterval(0x0010),
+            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 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 }
+        );
+
+        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_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();
+
+        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::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 server = ServerBuilder::new()
-            .add_receive_state_characteristic(BroadcastReceiveState::Empty)
-            .build::<FakeTypes>()
-            .expect("building server works");
+        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();
 
-        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);
+        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 server = ServerBuilder::new()
-            .add_receive_state_characteristic(BroadcastReceiveState::Empty)
-            .build::<FakeTypes>()
-            .expect("building server works");
+        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();
 
-        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);
+        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 poll_publish_failure() {
-        use futures::task::Context;
-        use std::task::Poll;
-
+    fn publish_failure() {
         let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
 
         let mut server = ServerBuilder::new()
@@ -446,11 +1112,13 @@
         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!(server.publish(fake_gatt_server).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))))
+            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
@@ -461,15 +1129,41 @@
     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");
+        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
-        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");
+        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 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));
+    }
 }
diff --git a/rust/bt-bass/src/types.rs b/rust/bt-bass/src/types.rs
index 2418d9e..64bf76c 100644
--- a/rust/bt-bass/src/types.rs
+++ b/rust/bt-bass/src/types.rs
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use bt_bap::types::BroadcastId;
+use bt_bap::types::{BroadcastCode, BroadcastId};
 use bt_common::core::ltv::LtValue;
 use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
 use bt_common::generic_audio::metadata_ltv::*;
 use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError};
 use bt_common::{decodable_enum, Uuid};
+use bt_gatt::types::GattError;
 use std::str::FromStr;
 
 pub const ADDRESS_BYTE_SIZE: usize = 6;
@@ -21,6 +22,11 @@
 pub const BROADCAST_AUDIO_SCAN_CONTROL_POINT_UUID: Uuid = Uuid::from_u16(0x2BC7);
 pub const BROADCAST_RECEIVE_STATE_UUID: Uuid = Uuid::from_u16(0x2BC8);
 
+/// ATT Application Error Codes for the Broadcast Audio Scan Service.
+/// See BASS v1.0 Table 3.2.
+pub const ERROR_OPCODE_NOT_SUPPORTED: GattError = GattError::ApplicationError80;
+pub const ERROR_INVALID_SOURCE_ID: GattError = GattError::ApplicationError81;
+
 pub type SourceId = u8;
 
 /// Index into the vector of BIG subgroups. Valid value range is [0 to len of
@@ -284,10 +290,10 @@
 /// See Broadcast Audio Scan Service spec v1.0 Section 3.1.1.5 for details.
 #[derive(Debug, PartialEq)]
 pub struct ModifySourceOperation {
-    source_id: SourceId,
-    pa_sync: PaSync,
-    pa_interval: PeriodicAdvertisingInterval,
-    subgroups: Vec<BigSubgroup>,
+    pub(crate) source_id: SourceId,
+    pub(crate) pa_sync: PaSync,
+    pub(crate) pa_interval: PeriodicAdvertisingInterval,
+    pub(crate) subgroups: Vec<BigSubgroup>,
 }
 
 impl ModifySourceOperation {
@@ -382,16 +388,16 @@
 /// See Broadcast Audio Scan Service spec v1.0 Section 3.1.1.6 for details.
 #[derive(Debug, PartialEq)]
 pub struct SetBroadcastCodeOperation {
-    source_id: SourceId,
-    broadcast_code: [u8; 16],
+    pub(crate) source_id: SourceId,
+    pub(crate) broadcast_code: BroadcastCode,
 }
 
 impl SetBroadcastCodeOperation {
-    const BROADCAST_CODE_LEN: usize = 16;
+    const BROADCAST_CODE_LEN: usize = BroadcastCode::BYTE_SIZE;
     const PACKET_SIZE: usize =
         ControlPointOpcode::BYTE_SIZE + SOURCE_ID_BYTE_SIZE + Self::BROADCAST_CODE_LEN;
 
-    pub fn new(source_id: SourceId, broadcast_code: [u8; 16]) -> Self {
+    pub fn new(source_id: SourceId, broadcast_code: BroadcastCode) -> Self {
         SetBroadcastCodeOperation { source_id, broadcast_code }
     }
 }
@@ -413,8 +419,9 @@
         let decode_fn = || {
             let _ = Self::check_opcode(buf[0])?;
             let source_id = buf[1];
-            let mut broadcast_code = [0; Self::BROADCAST_CODE_LEN];
-            broadcast_code.copy_from_slice(&buf[2..2 + Self::BROADCAST_CODE_LEN]);
+            let mut raw_code = [0; Self::BROADCAST_CODE_LEN];
+            raw_code.copy_from_slice(&buf[2..2 + Self::BROADCAST_CODE_LEN]);
+            let broadcast_code = BroadcastCode::new(raw_code);
             Ok((Self { source_id, broadcast_code }, Self::PACKET_SIZE))
         };
 
@@ -435,7 +442,7 @@
 
         buf[0] = Self::opcode() as u8;
         buf[1] = self.source_id;
-        buf[2..2 + Self::BROADCAST_CODE_LEN].copy_from_slice(&self.broadcast_code);
+        buf[2..2 + Self::BROADCAST_CODE_LEN].copy_from_slice(&self.broadcast_code.0);
         Ok(())
     }
 
@@ -446,7 +453,7 @@
 
 /// See Broadcast Audio Scan Service spec v1.0 Section 3.1.1.7 for details.
 #[derive(Debug, PartialEq)]
-pub struct RemoveSourceOperation(SourceId);
+pub struct RemoveSourceOperation(pub(crate) SourceId);
 
 impl RemoveSourceOperation {
     const PACKET_SIZE: usize = ControlPointOpcode::BYTE_SIZE + SOURCE_ID_BYTE_SIZE;
@@ -762,7 +769,6 @@
         + EncryptionStatus::MIN_PACKET_SIZE
         + NUM_SUBGROUPS_BYTE_SIZE;
 
-    #[cfg(any(test, feature = "test-utils"))]
     pub fn new(
         source_id: u8,
         source_address_type: AddressType,
@@ -920,6 +926,17 @@
     }
 }
 
+impl PaSyncState {
+    /// Returns the initial [`PaSyncState`] corresponding to a requested
+    /// [`PaSync`].
+    pub fn from_pa_sync(pa_sync: PaSync) -> Self {
+        match pa_sync {
+            PaSync::SyncPastAvailable => Self::SyncInfoRequest,
+            _ => Self::NotSynced,
+        }
+    }
+}
+
 /// Represents BIG_Encryption and Bad_Code params from BASS spec v.1.0 Table
 /// 3.9.
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -1310,10 +1327,10 @@
         // Encoding.
         let op = SetBroadcastCodeOperation::new(
             0x0A,
-            [
+            BroadcastCode::new([
                 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14,
                 0x15, 0x16,
-            ],
+            ]),
         );
         assert_eq!(op.encoded_len(), 18);
         let mut buf = vec![0; op.encoded_len()];
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index c65d05b..07527e3 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -196,7 +196,7 @@
 /// use in correlating with ServiceEvents from peers.  Stacks must translate
 /// the actual GATT handles to the handles provided in the ServiceDefinition
 /// for these events.
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
 pub struct Handle(pub u64);
 
 /// Whether a service is marked as Primary or Secondary on the server.