rust/bt-ascs: Refactor common types and implement basic client

Refactor common types that are used for both ASCS client and server.
Also do a basic implementation of ASCS client.

Test: cargo test
Change-Id: I1e71f43ec66ea1727f99a589d38a92e8b9bf763c
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2269
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
new file mode 100644
index 0000000..8f51ce5
--- /dev/null
+++ b/rust/bt-ascs/src/client.rs
@@ -0,0 +1,304 @@
+// Copyright 2026 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use futures::stream::{BoxStream, StreamExt};
+use std::collections::HashMap;
+
+use bt_common::Uuid;
+use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic};
+use bt_gatt::types::Handle;
+
+use crate::types::*;
+
+/// 16-bit UUID value for the characteristics offered by the Audio
+/// Stream Control Service.
+pub const ASE_CONTROL_POINT_UUID: Uuid = Uuid::from_u16(0x2BC6);
+pub const SINK_ASE_UUID: Uuid = Uuid::from_u16(0x2BC4);
+pub const SOURCE_ASE_UUID: Uuid = Uuid::from_u16(0x2BC5);
+
+#[derive(Debug, thiserror::Error, PartialEq, Clone)]
+pub enum ClientError {
+    #[error("Remote server is missing control point characteristic")]
+    MissingControlPointCharacteristic,
+    #[error("Remote server should only have 1 control point characteristic")]
+    ExtraControlPointCharacteristic,
+    #[error("Remote server doesn't have any audio stream endpoints")]
+    MissingAudioStreamEndpoints,
+}
+
+/// Represents a single source/sink ASE state characteristic.
+pub struct AudioStreamEndpointHandle {
+    pub endpoint: AudioStreamEndpoint,
+    pub notification_stream:
+        BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>,
+}
+
+// See ASCS v1.0.1 4.2 for details.
+pub struct AseControlPoint {
+    pub handle: Handle,
+    pub notification_stream:
+        BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>,
+}
+
+/// Creates Audio Stream Control Service (ASCS) client instance
+pub struct AudioStreamControlServiceClient<T: bt_gatt::GattTypes> {
+    pub gatt_client: T::PeerService,
+    pub control_point: AseControlPoint,
+    pub sink_endpoints: HashMap<Handle, AudioStreamEndpointHandle>,
+    pub source_endpoints: HashMap<Handle, AudioStreamEndpointHandle>,
+}
+
+pub type SinkAndSourceEndpoints =
+    (HashMap<Handle, AudioStreamEndpointHandle>, HashMap<Handle, AudioStreamEndpointHandle>);
+
+impl<T: bt_gatt::GattTypes> AudioStreamControlServiceClient<T> {
+    pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error>
+    where
+        <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
+    {
+        let control_point = Self::discover_control_point(&gatt_client).await?;
+        let (sink_endpoints, source_endpoints) = Self::discover_all_endpoints(&gatt_client).await?;
+
+        Ok(Self { gatt_client, control_point, sink_endpoints, source_endpoints })
+    }
+
+    async fn discover_control_point(gatt_client: &T::PeerService) -> Result<AseControlPoint, Error>
+    where
+        <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
+    {
+        let cp_chars = ServiceCharacteristic::<T>::find(gatt_client, ASE_CONTROL_POINT_UUID)
+            .await
+            .map_err(Error::Gatt)?;
+        if cp_chars.is_empty() {
+            return Err(ClientError::MissingControlPointCharacteristic.into());
+        }
+        if cp_chars.len() > 1 {
+            return Err(ClientError::ExtraControlPointCharacteristic.into());
+        }
+        let cp_handle = *cp_chars[0].handle();
+        let cp_stream = gatt_client.subscribe(&cp_handle);
+        Ok(AseControlPoint { handle: cp_handle, notification_stream: cp_stream.boxed() })
+    }
+
+    async fn discover_all_endpoints(
+        gatt_client: &T::PeerService,
+    ) -> Result<SinkAndSourceEndpoints, Error>
+    where
+        <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
+    {
+        let sink_chars = ServiceCharacteristic::<T>::find(gatt_client, SINK_ASE_UUID)
+            .await
+            .map_err(Error::Gatt)?;
+
+        let source_chars = ServiceCharacteristic::<T>::find(gatt_client, SOURCE_ASE_UUID)
+            .await
+            .map_err(Error::Gatt)?;
+
+        if sink_chars.is_empty() && source_chars.is_empty() {
+            return Err(ClientError::MissingAudioStreamEndpoints.into());
+        }
+
+        let mut sink_endpoints = HashMap::new();
+        for c in sink_chars {
+            let handle = *c.handle();
+            let endpoint =
+                Self::read_and_create_endpoint(gatt_client, handle, AudioDirection::Sink).await?;
+            let notification_stream = gatt_client.subscribe(&handle).boxed();
+            sink_endpoints
+                .insert(handle, AudioStreamEndpointHandle { endpoint, notification_stream });
+        }
+
+        let mut source_endpoints = HashMap::new();
+        for c in source_chars {
+            let handle = *c.handle();
+            let endpoint =
+                Self::read_and_create_endpoint(gatt_client, handle, AudioDirection::Source).await?;
+            let notification_stream = gatt_client.subscribe(&handle).boxed();
+            source_endpoints
+                .insert(handle, AudioStreamEndpointHandle { endpoint, notification_stream });
+        }
+
+        Ok((sink_endpoints, source_endpoints))
+    }
+
+    async fn read_and_create_endpoint(
+        gatt_client: &T::PeerService,
+        handle: Handle,
+        direction: AudioDirection,
+    ) -> Result<AudioStreamEndpoint, Error> {
+        let mut buf = vec![0; 255];
+        let (read_bytes, _truncated) =
+            gatt_client.read_characteristic(&handle, 0, &mut buf[..]).await.map_err(Error::Gatt)?;
+        let endpoint = AudioStreamEndpoint::from_char_value(handle, direction, &buf[0..read_bytes])
+            .map_err(|e| {
+                bt_gatt::types::Error::other(std::io::Error::new(
+                    std::io::ErrorKind::InvalidData,
+                    format!("Failed to decode AudioStreamEndpoint: {:?}", e),
+                ))
+            })?;
+        Ok(endpoint)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use bt_gatt::test_utils::{FakePeerService, FakeTypes};
+    use bt_gatt::types::{AttributePermissions, CharacteristicProperty};
+    use bt_gatt::Characteristic;
+
+    const CONTROL_POINT_HANDLE: Handle = Handle(1);
+    const SINK_ASE_HANDLE: Handle = Handle(2);
+    const SOURCE_ASE_HANDLE: Handle = Handle(3);
+
+    fn setup_fake_service() -> FakePeerService {
+        let mut service = FakePeerService::new();
+        // Add Control Point
+        service.add_characteristic(
+            Characteristic {
+                handle: CONTROL_POINT_HANDLE,
+                uuid: ASE_CONTROL_POINT_UUID,
+                properties: CharacteristicProperty::Write
+                    | CharacteristicProperty::WriteWithoutResponse
+                    | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![],
+        );
+        // Add Sink ASE
+        service.add_characteristic(
+            Characteristic {
+                handle: SINK_ASE_HANDLE,
+                uuid: SINK_ASE_UUID,
+                properties: CharacteristicProperty::Read | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![0x01, 0x00], // ASE ID 1, State Idle
+        );
+        // Add Source ASE
+        service.add_characteristic(
+            Characteristic {
+                handle: SOURCE_ASE_HANDLE,
+                uuid: SOURCE_ASE_UUID,
+                properties: CharacteristicProperty::Read | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![0x02, 0x00], // ASE ID 2, State Idle
+        );
+        service
+    }
+
+    fn run_to_completion<F: std::future::Future>(fut: F) -> F::Output {
+        let mut fut = std::pin::pin!(fut);
+        let mut cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
+        match fut.as_mut().poll(&mut cx) {
+            std::task::Poll::Ready(res) => res,
+            std::task::Poll::Pending => panic!("Future did not complete synchronously"),
+        }
+    }
+
+    #[test]
+    fn client_creation_success() {
+        let service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        assert_eq!(client.control_point.handle, CONTROL_POINT_HANDLE);
+        assert_eq!(client.sink_endpoints.len(), 1);
+        assert_eq!(client.source_endpoints.len(), 1);
+
+        let sink = &client.sink_endpoints[&SINK_ASE_HANDLE].endpoint;
+        assert_eq!(sink.ase_id, AseId(1));
+        assert_eq!(sink.state, AseState::Idle);
+        assert_eq!(sink.direction, AudioDirection::Sink);
+
+        let source = &client.source_endpoints[&SOURCE_ASE_HANDLE].endpoint;
+        assert_eq!(source.ase_id, AseId(2));
+        assert_eq!(source.state, AseState::Idle);
+        assert_eq!(source.direction, AudioDirection::Source);
+    }
+
+    #[test]
+    fn client_creation_missing_control_point() {
+        let mut service = FakePeerService::new();
+        // Only add Sink (no Control Point)
+        service.add_characteristic(
+            Characteristic {
+                handle: SINK_ASE_HANDLE,
+                uuid: SINK_ASE_UUID,
+                properties: CharacteristicProperty::Read | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![0x01, 0x00],
+        );
+
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service);
+        let err = match run_to_completion(client_fut) {
+            Err(e) => e,
+            Ok(_) => panic!("expected error, got Ok"),
+        };
+        assert!(matches!(err, Error::Client(ClientError::MissingControlPointCharacteristic)));
+    }
+
+    #[test]
+    fn client_creation_missing_endpoints() {
+        let mut service = FakePeerService::new();
+        // Only add Control Point (no endpoints)
+        service.add_characteristic(
+            Characteristic {
+                handle: CONTROL_POINT_HANDLE,
+                uuid: ASE_CONTROL_POINT_UUID,
+                properties: CharacteristicProperty::Write | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![],
+        );
+
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service);
+        let err = match run_to_completion(client_fut) {
+            Err(e) => e,
+            Ok(_) => panic!("expected error, got Ok"),
+        };
+        assert!(matches!(err, Error::Client(ClientError::MissingAudioStreamEndpoints)));
+    }
+
+    #[test]
+    fn client_creation_extra_control_point() {
+        let mut service = FakePeerService::new();
+        // Add first Control Point
+        service.add_characteristic(
+            Characteristic {
+                handle: CONTROL_POINT_HANDLE,
+                uuid: ASE_CONTROL_POINT_UUID,
+                properties: CharacteristicProperty::Write | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![],
+        );
+        // Add second Control Point
+        service.add_characteristic(
+            Characteristic {
+                handle: Handle(4),
+                uuid: ASE_CONTROL_POINT_UUID,
+                properties: CharacteristicProperty::Write | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![],
+        );
+
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service);
+        let err = match run_to_completion(client_fut) {
+            Err(e) => e,
+            Ok(_) => panic!("expected error, got Ok"),
+        };
+        assert!(matches!(err, Error::Client(ClientError::ExtraControlPointCharacteristic)));
+    }
+}
diff --git a/rust/bt-ascs/src/lib.rs b/rust/bt-ascs/src/lib.rs
index f3f7380..8a6a6b4 100644
--- a/rust/bt-ascs/src/lib.rs
+++ b/rust/bt-ascs/src/lib.rs
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+pub mod client;
 pub mod server;
 pub mod types;
 pub use types::Error;
diff --git a/rust/bt-ascs/src/server.rs b/rust/bt-ascs/src/server.rs
index 9a329fa..3ee9823 100644
--- a/rust/bt-ascs/src/server.rs
+++ b/rust/bt-ascs/src/server.rs
@@ -2,10 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use bt_common::core::ltv::LtValue;
 use bt_common::core::CodecId;
 use bt_common::generic_audio::metadata_ltv::Metadata;
-use bt_common::packet_encoding::Encodable;
 use bt_common::{PeerId, Uuid};
 use bt_gatt::server::{LocalService, Server, ServiceDefinition, ServiceId};
 use bt_gatt::server::{ReadResponder, WriteResponder};
@@ -23,6 +21,18 @@
 
 use crate::types::*;
 
+#[derive(Debug, thiserror::Error)]
+pub enum ServerError {
+    #[error("Server Only Operation")]
+    ServerOnlyOperation,
+    #[error("Service is already published")]
+    AlreadyPublished,
+    #[error("Issue publishing service: {0}")]
+    PublishError(#[from] bt_gatt::types::Error),
+    #[error("Unknown Peer: {0}")]
+    UnknownPeer(bt_common::PeerId),
+}
+
 #[pin_project(project = LocalServiceProj)]
 enum LocalServiceState<T: bt_gatt::ServerTypes> {
     NotPublished {
@@ -87,9 +97,9 @@
                     let service_result = futures::ready!(fut.poll(cx));
                     let Ok(service) = service_result else {
                         self.as_mut().set(LocalServiceState::Terminated);
-                        return Poll::Ready(Some(Err(Error::PublishError(
+                        return Poll::Ready(Some(Err(Error::from(ServerError::PublishError(
                             service_result.err().unwrap(),
-                        ))));
+                        )))));
                     };
                     let events = service.publish();
                     self.as_mut().set(LocalServiceState::Published { service, events });
@@ -99,15 +109,15 @@
                     let item = futures::ready!(events.poll_next(cx));
                     let Some(gatt_result) = item else {
                         self.as_mut().set(LocalServiceState::Terminated);
-                        return Poll::Ready(Some(Err(Error::PublishError(
+                        return Poll::Ready(Some(Err(Error::from(ServerError::PublishError(
                             "GATT server terminated".into(),
-                        ))));
+                        )))));
                     };
                     let Ok(event) = gatt_result else {
                         self.as_mut().set(LocalServiceState::Terminated);
-                        return Poll::Ready(Some(Err(Error::PublishError(
+                        return Poll::Ready(Some(Err(Error::from(ServerError::PublishError(
                             gatt_result.err().unwrap(),
-                        ))));
+                        )))));
                     };
                     return Poll::Ready(Some(Ok(event)));
                 }
@@ -144,24 +154,23 @@
         if self.opcode.is_none() || self.response_codes.is_empty() {
             return None;
         }
-        let mut notification = Vec::with_capacity(2 + self.response_codes.len() * 3);
-        // Opcode
-        notification.push(self.opcode.unwrap().into());
-        if let ResponseCode::InvalidLength { .. } | ResponseCode::UnsupportedOpcode { .. } =
-            self.response_codes[0]
+        use bt_common::packet_encoding::Encodable;
+
+        let opcode = self.opcode.unwrap().into();
+        let notification = if let ResponseCode::InvalidLength { .. }
+        | ResponseCode::UnsupportedOpcode { .. } = self.response_codes[0]
         {
-            // UnsupportedOpcode or InvalidLength. Number_of_ASEs shall be set to 0xFF
-            // See ASCS v1.0.1 Table 4.7.  We only include the first response_code.
-            notification.push(0xFF);
-            notification.extend(self.response_codes[0].notify_value());
-            return Some(notification);
-        }
-        // Number_of_ASEs
-        notification.push(self.response_codes.len() as u8);
-        for response in &self.response_codes {
-            notification.extend(response.notify_value());
-        }
-        Some(notification)
+            ControlPointNotification::new_error(opcode, self.response_codes[0].to_code())
+        } else {
+            let mut cp_notification = ControlPointNotification::new(opcode);
+            for response in &self.response_codes {
+                cp_notification.add_response(response.clone());
+            }
+            cp_notification
+        };
+        let mut buf = vec![0; notification.encoded_len()];
+        notification.encode(&mut buf).unwrap();
+        Some(buf)
     }
 }
 
@@ -288,7 +297,7 @@
 
     pub fn publish(&mut self, server: &T::Server) -> Result<(), Error> {
         if !self.local_service.is_not_published() {
-            return Err(Error::AlreadyPublished);
+            return Err(Error::from(ServerError::AlreadyPublished));
         }
         let LocalServiceState::NotPublished { waker } = std::mem::replace(
             &mut self.local_service,
@@ -306,8 +315,10 @@
         ase_id: AseId,
         cis: (CigId, CisId),
     ) -> Result<(), Error> {
-        let endpoints =
-            self.client_endpoints.get_mut(&peer_id).ok_or(Error::UnknownPeer(peer_id))?;
+        let endpoints = self
+            .client_endpoints
+            .get_mut(&peer_id)
+            .ok_or(Error::from(ServerError::UnknownPeer(peer_id)))?;
         endpoints.established_cis(ase_id, cis);
         for operation in endpoints.autonomous_operations() {
             self.queue_operation_unpin(peer_id, operation);
@@ -321,8 +332,10 @@
         ase_id: AseId,
         cis: (CigId, CisId),
     ) -> Result<(), Error> {
-        let endpoints =
-            self.client_endpoints.get_mut(&peer_id).ok_or(Error::UnknownPeer(peer_id))?;
+        let endpoints = self
+            .client_endpoints
+            .get_mut(&peer_id)
+            .ok_or(Error::from(ServerError::UnknownPeer(peer_id)))?;
         endpoints.released_cis(ase_id, cis);
         for operation in endpoints.autonomous_operations() {
             self.queue_operation_unpin(peer_id, operation);
@@ -383,159 +396,6 @@
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq)]
-enum AudioDirection {
-    Sink,
-    Source,
-}
-
-impl From<&AudioDirection> for bt_common::Uuid {
-    fn from(value: &AudioDirection) -> Self {
-        match value {
-            AudioDirection::Sink => Uuid::from_u16(0x2BC4),
-            AudioDirection::Source => Uuid::from_u16(0x2BC5),
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-enum AseAdditionalParameters {
-    /// When in states with no additional parameters: Idle, Releasing
-    None,
-    CodecConfigured {
-        framing: Framing,
-        preferred_phys: Vec<Phy>,
-        preferred_retransmission_number: u8,
-        max_transport_latency: MaxTransportLatency,
-        presentation_delay_range: PresentationDelayRange,
-        codec_id: CodecId,
-        codec_config: Vec<u8>,
-    },
-    QosConfigured {
-        configuration: QosConfiguration,
-    },
-    /// When Enabling, Streaming, or Disabling
-    Streaming {
-        cig_id: CigId,
-        cis_id: CisId,
-        metadata: Vec<Metadata>,
-        qos_configured: QosConfiguration,
-    },
-}
-
-impl AseAdditionalParameters {
-    fn char_size(&self) -> usize {
-        match self {
-            AseAdditionalParameters::None => 0,
-            AseAdditionalParameters::CodecConfigured { codec_config, .. } => {
-                23 + codec_config.len()
-            }
-            AseAdditionalParameters::QosConfigured { .. } => 15,
-            AseAdditionalParameters::Streaming { metadata, .. } => {
-                metadata.iter().fold(3, |total, m| total + m.encoded_len() as usize)
-            }
-        }
-    }
-    fn into_char_value(&self) -> Vec<u8> {
-        match self {
-            AseAdditionalParameters::None => Vec::new(),
-            AseAdditionalParameters::CodecConfigured {
-                framing,
-                preferred_phys,
-                preferred_retransmission_number,
-                max_transport_latency,
-                presentation_delay_range,
-                codec_id,
-                codec_config,
-            } => {
-                let mut value = Vec::with_capacity(self.char_size());
-                value.resize(self.char_size() - codec_config.len(), 0);
-                value[0] = (*framing) as u8;
-                value[1] = Phy::to_bits(preferred_phys.iter());
-                value[2] = *preferred_retransmission_number;
-                max_transport_latency.encode(&mut value[3..]).unwrap();
-                presentation_delay_range.encode(&mut value[5..]).unwrap();
-                codec_id.encode(&mut value[17..]).unwrap();
-                value[22] = codec_config.len() as u8;
-                value.extend(codec_config.clone());
-                value
-            }
-            AseAdditionalParameters::QosConfigured {
-                configuration:
-                    QosConfiguration {
-                        cig_id,
-                        cis_id,
-                        sdu_interval,
-                        framing,
-                        phy,
-                        max_sdu,
-                        retransmission_number,
-                        max_transport_latency,
-                        presentation_delay,
-                        ..
-                    },
-            } => {
-                let mut value = Vec::with_capacity(self.char_size());
-                value.resize(self.char_size(), 0);
-                cig_id.encode(&mut value[0..]).unwrap();
-                cis_id.encode(&mut value[1..]).unwrap();
-                sdu_interval.encode(&mut value[2..]).unwrap();
-                framing.encode(&mut value[5..]).unwrap();
-                value[6] = Phy::to_bits(phy.iter());
-                max_sdu.encode(&mut value[7..]).unwrap();
-                value[9] = *retransmission_number;
-                max_transport_latency.encode(&mut value[10..]).unwrap();
-                presentation_delay.encode(&mut value[12..]).unwrap();
-                value
-            }
-            AseAdditionalParameters::Streaming { cig_id, cis_id, metadata, .. } => {
-                let mut value = Vec::with_capacity(self.char_size());
-                value.resize(self.char_size(), 0);
-                cig_id.encode(&mut value[0..]).unwrap();
-                cis_id.encode(&mut value[1..]).unwrap();
-                value[2] = metadata.iter().fold(0usize, |acc, i| acc + i.encoded_len()) as u8;
-                LtValue::encode_all(metadata.clone().into_iter(), &mut value[3..]).unwrap();
-                value
-            }
-        }
-    }
-}
-
-impl From<QosConfiguration> for AseAdditionalParameters {
-    fn from(value: QosConfiguration) -> Self {
-        Self::QosConfigured { configuration: value }
-    }
-}
-
-#[derive(Debug, Clone)]
-struct AudioStreamEndpoint {
-    handle: Handle,
-    direction: AudioDirection,
-    ase_id: AseId,
-    state: AseState,
-    additional: AseAdditionalParameters,
-}
-
-impl AudioStreamEndpoint {
-    fn into_char_value(&self) -> Vec<u8> {
-        let mut value = Vec::with_capacity(2 + self.additional.char_size());
-        value.push(self.ase_id.into());
-        value.push(self.state.into());
-        value.extend(self.additional.into_char_value());
-        value
-    }
-
-    fn get_cis(&self) -> Option<(CigId, CisId)> {
-        match &self.additional {
-            AseAdditionalParameters::QosConfigured { configuration } => {
-                Some((configuration.cig_id, configuration.cis_id))
-            }
-            AseAdditionalParameters::Streaming { cig_id, cis_id, .. } => Some((*cig_id, *cis_id)),
-            _ => None,
-        }
-    }
-}
-
 impl From<&AudioStreamEndpoint> for Characteristic {
     fn from(value: &AudioStreamEndpoint) -> Self {
         let properties = CharacteristicProperty::Read | CharacteristicProperty::Notify;
@@ -712,7 +572,7 @@
             cig_id: configuration.cig_id,
             cis_id: configuration.cis_id,
             metadata: self.metadata,
-            qos_configured: configuration,
+            qos_configured: Some(configuration),
         };
         let _ = self.sender.send(Ok(self.endpoint));
     }
@@ -730,7 +590,8 @@
     }
 
     pub fn accept(mut self) {
-        let AseAdditionalParameters::Streaming { qos_configured, .. } = self.endpoint.additional
+        let AseAdditionalParameters::Streaming { qos_configured: Some(qos_configured), .. } =
+            self.endpoint.additional
         else {
             unreachable!();
         };
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs
index 1ca6f3f..324a2b7 100644
--- a/rust/bt-ascs/src/types.rs
+++ b/rust/bt-ascs/src/types.rs
@@ -3,31 +3,32 @@
 // found in the LICENSE file.
 
 use bt_common::packet_encoding::{Decodable, Encodable};
-use bt_common::{codable_as_bitmask, decodable_enum};
+use bt_common::{codable_as_bitmask, decodable_enum, Uuid};
+use bt_gatt::types::Handle;
 use thiserror::Error;
 
+use crate::client::ClientError;
+use crate::server::ServerError;
+use bt_common::core::ltv::LtValue;
 use bt_common::core::CodecId;
 use bt_common::generic_audio::metadata_ltv::Metadata;
+use bt_gatt::types::Error as BtGattError;
 
 /// Error type
 #[derive(Debug, Error)]
 pub enum Error {
-    #[error("Reserved for Future Use: {0}")]
-    ReservedFutureUse(String),
-    #[error("Server Only Operation")]
-    ServerOnlyOperation,
-    #[error("Service is already published")]
-    AlreadyPublished,
-    #[error("Issue publishing service: {0}")]
-    PublishError(bt_gatt::types::Error),
-    #[error("Unsupported configuration: {0}")]
-    Unsupported(String),
-    #[error("Unknown Peer: {0}")]
-    UnknownPeer(bt_common::PeerId),
+    #[error("ASCS Server error: {0}")]
+    Server(#[from] ServerError),
+    #[error("ASCS Client error: {0}")]
+    Client(#[from] ClientError),
+    #[error("GATT operation error: {0}")]
+    Gatt(#[from] BtGattError),
+    #[error("Internal error occurred: {0}")]
+    Internal(String),
 }
 
 #[non_exhaustive]
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum ResponseCode {
     Success { ase_id: AseId },
     UnsupportedOpcode { opcode_byte: u8 },
@@ -43,7 +44,7 @@
 }
 
 impl ResponseCode {
-    fn to_code(&self) -> u8 {
+    pub(crate) fn to_code(&self) -> u8 {
         match self {
             ResponseCode::Success { .. } => 0x00,
             ResponseCode::UnsupportedOpcode { .. } => 0x01,
@@ -122,9 +123,81 @@
     pub(crate) fn notify_value(&self) -> Vec<u8> {
         [self.ase_id_value(), self.to_code(), self.reason_byte()].into()
     }
+
+    pub fn decode_response(
+        buf: &[u8],
+        opcode: u8,
+    ) -> Result<Self, bt_common::packet_encoding::Error> {
+        if buf.len() < 3 {
+            return Err(bt_common::packet_encoding::Error::UnexpectedDataLength);
+        }
+        let ase_id_val = buf[0];
+        let code = buf[1];
+        let reason = buf[2];
+        let ase_id = AseId(ase_id_val);
+
+        match code {
+            0x00 => Ok(Self::Success { ase_id }),
+            0x01 => {
+                if ase_id_val != 0 {
+                    return Err(bt_common::packet_encoding::Error::OutOfRange);
+                }
+                Ok(Self::UnsupportedOpcode { opcode_byte: opcode })
+            }
+            0x02 => {
+                if ase_id_val != 0 {
+                    return Err(bt_common::packet_encoding::Error::OutOfRange);
+                }
+                Ok(Self::InvalidLength { opcode_byte: opcode })
+            }
+            0x03 => Ok(Self::InvalidAseId { value: ase_id_val }),
+            0x04 => Ok(Self::InvalidAseStateMachineTransition { ase_id }),
+            0x05 => Ok(Self::InvalidAseDirection { ase_id }),
+            0x06 => Ok(Self::UnsupportedAudioCapablities { ase_id }),
+            0x07 => {
+                let reason = ResponseReason::try_from(reason)
+                    .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?;
+                Ok(Self::ConfigurationParameterValue {
+                    ase_id,
+                    issue: ResponseIssue::Unsupported,
+                    reason,
+                })
+            }
+            0x08 => {
+                let reason = ResponseReason::try_from(reason)
+                    .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?;
+                Ok(Self::ConfigurationParameterValue {
+                    ase_id,
+                    issue: ResponseIssue::Rejected,
+                    reason,
+                })
+            }
+            0x09 => {
+                let reason = ResponseReason::try_from(reason)
+                    .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?;
+                Ok(Self::ConfigurationParameterValue {
+                    ase_id,
+                    issue: ResponseIssue::Invalid,
+                    reason,
+                })
+            }
+            0x0A => {
+                Ok(Self::Metadata { ase_id, issue: ResponseIssue::Unsupported, type_value: reason })
+            }
+            0x0B => {
+                Ok(Self::Metadata { ase_id, issue: ResponseIssue::Rejected, type_value: reason })
+            }
+            0x0C => {
+                Ok(Self::Metadata { ase_id, issue: ResponseIssue::Invalid, type_value: reason })
+            }
+            0x0D => Ok(Self::InsufficientResources { ase_id }),
+            0x0E => Ok(Self::UnspecifiedError { ase_id }),
+            _ => Err(bt_common::packet_encoding::Error::OutOfRange),
+        }
+    }
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum ResponseIssue {
     Unsupported,
     Rejected,
@@ -199,6 +272,327 @@
     }
 }
 
+#[derive(Debug, Clone, PartialEq)]
+pub enum AseAdditionalParameters {
+    None,
+    CodecConfigured {
+        framing: Framing,
+        preferred_phys: Vec<Phy>,
+        preferred_retransmission_number: u8,
+        max_transport_latency: MaxTransportLatency,
+        presentation_delay_range: PresentationDelayRange,
+        codec_id: CodecId,
+        codec_config: Vec<u8>,
+    },
+    QosConfigured {
+        configuration: QosConfiguration,
+    },
+    Streaming {
+        cig_id: CigId,
+        cis_id: CisId,
+        metadata: Vec<Metadata>,
+        qos_configured: Option<QosConfiguration>,
+    },
+}
+
+impl AseAdditionalParameters {
+    pub fn char_size(&self) -> usize {
+        match self {
+            AseAdditionalParameters::None => 0,
+            AseAdditionalParameters::CodecConfigured { codec_config, .. } => {
+                23 + codec_config.len()
+            }
+            AseAdditionalParameters::QosConfigured { .. } => 15,
+            AseAdditionalParameters::Streaming { metadata, .. } => {
+                metadata.iter().fold(3, |total, m| total + m.encoded_len() as usize)
+            }
+        }
+    }
+
+    pub fn into_char_value(&self) -> Vec<u8> {
+        match self {
+            AseAdditionalParameters::None => Vec::new(),
+            AseAdditionalParameters::CodecConfigured {
+                framing,
+                preferred_phys,
+                preferred_retransmission_number,
+                max_transport_latency,
+                presentation_delay_range,
+                codec_id,
+                codec_config,
+            } => {
+                let mut value = Vec::with_capacity(self.char_size());
+                value.resize(self.char_size() - codec_config.len(), 0);
+                value[0] = (*framing) as u8;
+                value[1] = Phy::to_bits(preferred_phys.iter());
+                value[2] = *preferred_retransmission_number;
+                max_transport_latency.encode(&mut value[3..]).unwrap();
+                presentation_delay_range.encode(&mut value[5..]).unwrap();
+                codec_id.encode(&mut value[17..]).unwrap();
+                value[22] = codec_config.len() as u8;
+                value.extend(codec_config.clone());
+                value
+            }
+            AseAdditionalParameters::QosConfigured {
+                configuration:
+                    QosConfiguration {
+                        cig_id,
+                        cis_id,
+                        sdu_interval,
+                        framing,
+                        phy,
+                        max_sdu,
+                        retransmission_number,
+                        max_transport_latency,
+                        presentation_delay,
+                        ..
+                    },
+            } => {
+                let mut value = Vec::with_capacity(self.char_size());
+                value.resize(self.char_size(), 0);
+                cig_id.encode(&mut value[0..]).unwrap();
+                cis_id.encode(&mut value[1..]).unwrap();
+                sdu_interval.encode(&mut value[2..]).unwrap();
+                framing.encode(&mut value[5..]).unwrap();
+                value[6] = Phy::to_bits(phy.iter());
+                max_sdu.encode(&mut value[7..]).unwrap();
+                value[9] = *retransmission_number;
+                max_transport_latency.encode(&mut value[10..]).unwrap();
+                presentation_delay.encode(&mut value[12..]).unwrap();
+                value
+            }
+            AseAdditionalParameters::Streaming { cig_id, cis_id, metadata, .. } => {
+                let mut value = Vec::with_capacity(self.char_size());
+                value.resize(self.char_size(), 0);
+                cig_id.encode(&mut value[0..]).unwrap();
+                cis_id.encode(&mut value[1..]).unwrap();
+                value[2] = metadata.iter().fold(0usize, |acc, i| acc + i.encoded_len()) as u8;
+                LtValue::encode_all(metadata.clone().into_iter(), &mut value[3..]).unwrap();
+                value
+            }
+        }
+    }
+
+    pub fn decode(
+        ase_id: AseId,
+        state: &AseState,
+        buf: &[u8],
+    ) -> (Result<Self, ResponseCode>, usize) {
+        match state {
+            AseState::Idle | AseState::Releasing => (Ok(Self::None), 0),
+            AseState::CodecConfigured => {
+                if buf.len() < 23 {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+                let Ok(framing) = Framing::try_from(buf[0]) else {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                };
+                let preferred_phys = Phy::from_bits(buf[1]).collect();
+                let preferred_retransmission_number = buf[2];
+                let Ok(max_transport_latency) = MaxTransportLatency::decode(&buf[3..5]).0 else {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                };
+                let Ok(presentation_delay_range) = PresentationDelayRange::decode(&buf[5..17]).0
+                else {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                };
+                let Ok(codec_id) = CodecId::decode(&buf[17..22]).0 else {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                };
+                let codec_specific_configuration_len = buf[22] as usize;
+                let total_len = 23 + codec_specific_configuration_len;
+                if buf.len() < total_len {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+                let codec_config = buf[23..total_len].to_vec();
+                (
+                    Ok(Self::CodecConfigured {
+                        framing,
+                        preferred_phys,
+                        preferred_retransmission_number,
+                        max_transport_latency,
+                        presentation_delay_range,
+                        codec_id,
+                        codec_config,
+                    }),
+                    total_len,
+                )
+            }
+            AseState::QosConfigured => {
+                if buf.len() < 15 {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+                let mut temp_buf = Vec::with_capacity(16);
+                temp_buf.push(ase_id.into());
+                temp_buf.extend_from_slice(&buf[0..15]);
+                let (config_res, _) = QosConfiguration::decode(&temp_buf);
+                match config_res {
+                    Ok(configuration) => (Ok(Self::QosConfigured { configuration }), 15),
+                    Err(e) => (Err(e), buf.len()),
+                }
+            }
+            AseState::Enabling | AseState::Streaming | AseState::Disabling => {
+                if buf.len() < 3 {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+                let cig_id = match CigId::try_from(buf[0]) {
+                    Ok(id) => id,
+                    Err(_) => return (Err(ResponseCode::invalid_length()), buf.len()),
+                };
+                let cis_id = match CisId::try_from(buf[1]) {
+                    Ok(id) => id,
+                    Err(_) => return (Err(ResponseCode::invalid_length()), buf.len()),
+                };
+                let metadata_length = buf[2] as usize;
+                let total_len = 3 + metadata_length;
+                if buf.len() < total_len {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+
+                use bt_common::core::ltv::Error as LtvError;
+                use bt_common::core::ltv::LtValue;
+                let (metadata_results, consumed) =
+                    Metadata::decode_all(&buf[3..3 + metadata_length]);
+                if consumed != metadata_length {
+                    return (Err(ResponseCode::invalid_length()), buf.len());
+                }
+                let metadata_result: Result<Vec<Metadata>, LtvError<<Metadata as LtValue>::Type>> =
+                    metadata_results.into_iter().collect();
+                let Ok(metadata) = metadata_result else {
+                    match metadata_result.unwrap_err() {
+                        LtvError::MissingType | LtvError::MissingData(_) => {
+                            return (Err(ResponseCode::invalid_length()), buf.len());
+                        }
+                        LtvError::UnrecognizedType(_, type_value) => {
+                            return (
+                                Err(ResponseCode::Metadata {
+                                    ase_id,
+                                    issue: ResponseIssue::Unsupported,
+                                    type_value,
+                                }),
+                                total_len,
+                            );
+                        }
+                        LtvError::LengthOutOfRange(_, t, _)
+                        | LtvError::TypeFailedToDecode(t, _) => {
+                            return (
+                                Err(ResponseCode::Metadata {
+                                    ase_id,
+                                    issue: ResponseIssue::Invalid,
+                                    type_value: t.into(),
+                                }),
+                                total_len,
+                            );
+                        }
+                    }
+                };
+                (Ok(Self::Streaming { cig_id, cis_id, metadata, qos_configured: None }), total_len)
+            }
+        }
+    }
+}
+
+impl From<QosConfiguration> for AseAdditionalParameters {
+    fn from(value: QosConfiguration) -> Self {
+        Self::QosConfigured { configuration: value }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AudioDirection {
+    Sink,
+    Source,
+}
+
+impl From<&AudioDirection> for bt_common::Uuid {
+    fn from(value: &AudioDirection) -> Self {
+        match value {
+            AudioDirection::Sink => Uuid::from_u16(0x2BC4),
+            AudioDirection::Source => Uuid::from_u16(0x2BC5),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct AudioStreamEndpoint {
+    pub handle: Handle,
+    pub direction: AudioDirection,
+    pub ase_id: AseId,
+    pub state: AseState,
+    pub additional: AseAdditionalParameters,
+}
+
+impl AudioStreamEndpoint {
+    /// Decodes an `AudioStreamEndpoint` from an ASE characteristic value.
+    ///
+    /// # Arguments
+    /// * `handle` - The GATT handle of the ASE characteristic.
+    /// * `direction` - The direction of the ASE (Sink or Source).
+    /// * `char_value_buf` - The characteristic value bytes read from the
+    ///   server.
+    ///
+    /// # Expected `char_value_buf` Layout (ASCS v1.0 Section 4.3):
+    /// * **Octet 0:** `ASE_ID` (1 octet)
+    /// * **Octet 1:** `ASE_State` (1 octet)
+    /// * **Octet 2+:** `Additional_ASE_Parameters` (variable octets, see ASCS
+    ///   v1.0 Table 4.2)
+    pub fn from_char_value(
+        handle: Handle,
+        direction: AudioDirection,
+        char_value_buf: &[u8],
+    ) -> Result<Self, ResponseCode> {
+        if char_value_buf.len() < 2 {
+            return Err(ResponseCode::invalid_length());
+        }
+        let ase_id = AseId::try_from(char_value_buf[0])?;
+        let Ok(state) = AseState::try_from(char_value_buf[1]) else {
+            return Err(ResponseCode::invalid_length());
+        };
+        let (additional_res, _) =
+            AseAdditionalParameters::decode(ase_id, &state, &char_value_buf[2..]);
+        let additional = additional_res?;
+
+        Ok(Self { handle, direction, ase_id, state, additional })
+    }
+
+    /// Encodes this endpoint's current state into a GATT characteristic value
+    /// buffer.
+    pub fn into_char_value(&self) -> Vec<u8> {
+        let mut buf = vec![0; self.encoded_len()];
+        self.encode(&mut buf).unwrap();
+        buf
+    }
+
+    pub fn get_cis(&self) -> Option<(CigId, CisId)> {
+        match &self.additional {
+            AseAdditionalParameters::QosConfigured { configuration } => {
+                Some((configuration.cig_id, configuration.cis_id))
+            }
+            AseAdditionalParameters::Streaming { cig_id, cis_id, .. } => Some((*cig_id, *cis_id)),
+            _ => None,
+        }
+    }
+}
+
+impl Encodable for AudioStreamEndpoint {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        2 + self.additional.char_size()
+    }
+
+    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        if buf.len() < self.encoded_len() {
+            return Err(Self::Error::BufferTooSmall);
+        }
+        buf[0] = self.ase_id.into();
+        buf[1] = self.state.into();
+        let add_bytes = self.additional.into_char_value();
+        buf[2..2 + add_bytes.len()].copy_from_slice(&add_bytes);
+        Ok(())
+    }
+}
+
 decodable_enum! {
     pub enum AseControlPointOpcode<u8, bt_common::packet_encoding::Error, OutOfRange> {
         ConfigCodec = 0x01,
@@ -289,7 +683,9 @@
             AseControlOperation::ReceiverStopReady { .. } => Ok(0x06),
             AseControlOperation::UpdateMetadata { .. } => Ok(0x07),
             AseControlOperation::Release { .. } => Ok(0x08),
-            AseControlOperation::Released { .. } => Err(Error::ServerOnlyOperation),
+            AseControlOperation::Released { .. } => {
+                Err(Error::Server(ServerError::ServerOnlyOperation))
+            }
         }
     }
 }
@@ -894,7 +1290,7 @@
 /// preferred range of the Presentation Delay parameter to be requested by the
 /// ASCS Client. Prefered Minimum must be above min, and preferred_max must be
 /// below max. Either of these being None indicates no preference.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct PresentationDelayRange {
     min: PresentationDelay,
     max: PresentationDelay,
@@ -904,6 +1300,22 @@
 
 impl PresentationDelayRange {
     const BYTE_SIZE: usize = PresentationDelay::BYTE_SIZE * 4;
+
+    pub fn min(&self) -> &PresentationDelay {
+        &self.min
+    }
+
+    pub fn max(&self) -> &PresentationDelay {
+        &self.max
+    }
+
+    pub fn preferred_min(&self) -> Option<&PresentationDelay> {
+        self.preferred_min.as_ref()
+    }
+
+    pub fn preferred_max(&self) -> Option<&PresentationDelay> {
+        self.preferred_max.as_ref()
+    }
     /// Make a new delay range with no preference. Returns
     /// ResponseCode::InvalidLength if min > max or the value is out of the
     /// acceptable range (PresentationDelay is 24 bits)
@@ -1035,6 +1447,166 @@
     }
 }
 
+impl Decodable for PresentationDelayRange {
+    type Error = ResponseCode;
+
+    fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) {
+        if buf.len() < Self::BYTE_SIZE {
+            return (Err(ResponseCode::invalid_length()), buf.len());
+        }
+        let min = match PresentationDelay::decode(&buf[0..3]).0 {
+            Ok(min) => min,
+            Err(e) => return (Err(e), buf.len()),
+        };
+        let max = match PresentationDelay::decode(&buf[3..6]).0 {
+            Ok(max) => max,
+            Err(e) => return (Err(e), buf.len()),
+        };
+        let pref_min_val = u32::from_le_bytes([buf[6], buf[7], buf[8], 0]);
+        let preferred_min = if pref_min_val == 0 {
+            None
+        } else {
+            Some(PresentationDelay { microseconds: pref_min_val })
+        };
+        let pref_max_val = u32::from_le_bytes([buf[9], buf[10], buf[11], 0]);
+        let preferred_max = if pref_max_val == 0 {
+            None
+        } else {
+            Some(PresentationDelay { microseconds: pref_max_val })
+        };
+        (Ok(Self { min, max, preferred_min, preferred_max }), Self::BYTE_SIZE)
+    }
+}
+
+/// Represents an ASE Control Point notification sent by an ASCS Server to a
+/// Client.
+///
+/// See ASCS v1.0 Section 4.2 for the frame structure and notification
+/// properties.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ControlPointNotification {
+    pub opcode: u8,
+    pub number_of_ases: u8,
+    pub responses: Vec<ResponseCode>,
+}
+
+impl ControlPointNotification {
+    /// Creates a new, empty `ControlPointNotification` for standard multi-ASE
+    /// operations.
+    ///
+    /// The notification starts with a response count of `0`.
+    ///
+    /// # Arguments
+    /// * `opcode` - The opcode of the operation being responded to.
+    pub fn new(opcode: u8) -> Self {
+        Self { opcode, number_of_ases: 0, responses: Vec::new() }
+    }
+
+    /// Creates a new global error `ControlPointNotification`.
+    ///
+    /// A response count of `0xFF` is used strictly when the entire control
+    /// point write is rejected globally before individual ASEs are
+    /// processed (e.g., unrecognized opcode or invalid packet length). The
+    /// notification contains a single error response with an
+    /// ASE ID of `0x00`. See ASCS v1.0 Table 4.7.
+    ///
+    /// # Arguments
+    /// * `opcode` - The opcode of the operation being responded to.
+    /// * `response_code` - Either `0x01` (Unsupported Opcode) or `0x02`
+    ///   (Invalid Length).
+    pub fn new_error(opcode: u8, response_code: u8) -> Self {
+        let code = match response_code {
+            0x01 => ResponseCode::UnsupportedOpcode { opcode_byte: opcode },
+            0x02 => ResponseCode::InvalidLength { opcode_byte: opcode },
+            _ => unreachable!(),
+        };
+        Self { opcode, number_of_ases: 0xFF, responses: vec![code] }
+    }
+
+    /// Adds a single ASE response code to the notification list.
+    ///
+    /// If the notification is currently in a global error state (response count
+    /// `0xFF`), calling this method clears the error and transitions it
+    /// back to a standard multi-ASE response list.
+    ///
+    /// # Arguments
+    /// * `response` - The `ResponseCode` variant to append to the notification.
+    pub fn add_response(&mut self, response: ResponseCode) {
+        if self.number_of_ases == 0xFF {
+            self.number_of_ases = 0;
+            self.responses.clear();
+        }
+        self.responses.push(response);
+        self.number_of_ases = self.responses.len() as u8;
+    }
+}
+
+impl Encodable for ControlPointNotification {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        2 + self.responses.len() * 3
+    }
+
+    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        if buf.len() < self.encoded_len() {
+            return Err(Self::Error::BufferTooSmall);
+        }
+        buf[0] = self.opcode;
+        buf[1] = self.number_of_ases;
+        let mut idx = 2;
+        for r in &self.responses {
+            let val = r.notify_value();
+            buf[idx..idx + 3].copy_from_slice(&val);
+            idx += 3;
+        }
+        Ok(())
+    }
+}
+
+impl Decodable for ControlPointNotification {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) {
+        if buf.len() < 2 {
+            return (Err(Self::Error::UnexpectedDataLength), buf.len());
+        }
+        let opcode = buf[0];
+        let number_of_ases = buf[1];
+        let responses_buf = &buf[2..];
+        let mut responses = Vec::new();
+
+        if number_of_ases == 0xFF {
+            if responses_buf.len() < 3 {
+                return (Err(Self::Error::UnexpectedDataLength), buf.len());
+            }
+            let r = match ResponseCode::decode_response(&responses_buf[0..3], opcode) {
+                Ok(r) => r,
+                Err(e) => return (Err(e), buf.len()),
+            };
+            if r.ase_id_value() != 0 || (r.to_code() != 0x01 && r.to_code() != 0x02) {
+                return (Err(Self::Error::UnexpectedDataLength), buf.len());
+            }
+            responses.push(r);
+            (Ok(Self { opcode, number_of_ases, responses }), 5)
+        } else {
+            let expected_len = number_of_ases as usize * 3;
+            if responses_buf.len() < expected_len {
+                return (Err(Self::Error::UnexpectedDataLength), buf.len());
+            }
+            for i in 0..(number_of_ases as usize) {
+                let idx = i * 3;
+                let r = match ResponseCode::decode_response(&responses_buf[idx..idx + 3], opcode) {
+                    Ok(r) => r,
+                    Err(e) => return (Err(e), buf.len()),
+                };
+                responses.push(r);
+            }
+            (Ok(Self { opcode, number_of_ases, responses }), 2 + expected_len)
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -1312,4 +1884,122 @@
 
         assert_eq!(&encoded, expected);
     }
+
+    #[test]
+    fn test_ase_endpoint_roundtrip() {
+        use bt_gatt::types::Handle;
+        // Test with AdditionalParameters::None
+        let endpoint_none = AudioStreamEndpoint {
+            handle: Handle(1),
+            direction: AudioDirection::Sink,
+            ase_id: AseId(1),
+            state: AseState::Idle,
+            additional: AseAdditionalParameters::None,
+        };
+        let mut buf = vec![0; endpoint_none.encoded_len()];
+        endpoint_none.encode(&mut buf).unwrap();
+        assert_eq!(buf.len(), 2);
+        assert_eq!(buf, vec![1, 0]);
+
+        let decoded_endpoint_none =
+            AudioStreamEndpoint::from_char_value(Handle(1), AudioDirection::Sink, &buf).unwrap();
+        assert_eq!(decoded_endpoint_none.ase_id, endpoint_none.ase_id);
+        assert_eq!(decoded_endpoint_none.state, endpoint_none.state);
+        assert_eq!(decoded_endpoint_none.additional, endpoint_none.additional);
+
+        // Test with AdditionalParameters::CodecConfigured
+        let delay_range = PresentationDelayRange::build(1000, 2000).unwrap();
+        let endpoint_codec = AudioStreamEndpoint {
+            handle: Handle(2),
+            direction: AudioDirection::Source,
+            ase_id: AseId(2),
+            state: AseState::CodecConfigured,
+            additional: AseAdditionalParameters::CodecConfigured {
+                framing: Framing::Unframed,
+                preferred_phys: vec![Phy::Le1MPhy].into_iter().collect(),
+                preferred_retransmission_number: 3,
+                max_transport_latency: MaxTransportLatency::try_from(
+                    std::time::Duration::from_millis(10),
+                )
+                .unwrap(),
+                presentation_delay_range: delay_range,
+                codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+                codec_config: vec![1, 2, 3],
+            },
+        };
+        let mut buf2 = vec![0; endpoint_codec.encoded_len()];
+        endpoint_codec.encode(&mut buf2).unwrap();
+
+        let decoded_endpoint_codec =
+            AudioStreamEndpoint::from_char_value(Handle(2), AudioDirection::Source, &buf2).unwrap();
+        assert_eq!(decoded_endpoint_codec.ase_id, endpoint_codec.ase_id);
+        assert_eq!(decoded_endpoint_codec.state, endpoint_codec.state);
+        assert_eq!(decoded_endpoint_codec.additional, endpoint_codec.additional);
+    }
+
+    #[test]
+    fn control_point_notification_roundtrip() {
+        // 1. Test Standard Multi-ASE notification roundtrip
+        let mut notification = ControlPointNotification::new(0x01); // Opcode ConfigCodec
+        notification.add_response(ResponseCode::Success { ase_id: AseId(1) });
+        notification.add_response(ResponseCode::InvalidAseId { value: 2 });
+
+        let mut buf = vec![0; notification.encoded_len()];
+        assert!(notification.encode(&mut buf).is_ok());
+
+        #[rustfmt::skip]
+        let expected_bytes = &[
+            0x01, // Opcode
+            0x02, // Number of ASEs
+            0x01, 0x00, 0x00, // Response 1: ASE 1, Success, None
+            0x02, 0x03, 0x00, // Response 2: ASE 2, Invalid ASE ID, None
+        ];
+        assert_eq!(buf, expected_bytes);
+
+        let (decoded, consumed) = ControlPointNotification::decode(&buf);
+        assert_eq!(consumed, buf.len());
+        assert_eq!(decoded.expect("should succeed"), notification);
+
+        // 2. Test Global Error Notification (0xFF) roundtrip
+        let err_notification = ControlPointNotification::new_error(0x01, 0x02); // Invalid Length error
+        let mut err_buf = vec![0; err_notification.encoded_len()];
+        assert!(err_notification.encode(&mut err_buf).is_ok());
+
+        #[rustfmt::skip]
+        let expected_err_bytes = &[
+            0x01, // Opcode
+            0xFF, // Number of ASEs: Global Error
+            0x00, 0x02, 0x00, // ASE 0, Invalid Length, None
+        ];
+        assert_eq!(err_buf, expected_err_bytes);
+
+        let (decoded_err, consumed_err) = ControlPointNotification::decode(&err_buf);
+        assert_eq!(consumed_err, err_buf.len());
+        assert_eq!(decoded_err.expect("should succeed"), err_notification);
+
+        // 3. Test Validation constraint fails on malformed 0xFF notification
+        // Case A: number_of_ases = 0xFF, but ase_id is not 0x00 (e.g., AseId 1)
+        #[rustfmt::skip]
+        let malformed_bytes_ase = &[
+            0x01, // Opcode
+            0xFF, // Global Error
+            0x01, 0x02, 0x00, // ASE 1, Invalid Length, None (ase_id must be 0x00)
+        ];
+        let (res_malformed_ase, _) = ControlPointNotification::decode(malformed_bytes_ase);
+        assert_eq!(res_malformed_ase, Err(bt_common::packet_encoding::Error::OutOfRange));
+
+        // Case B: number_of_ases = 0xFF, but response_code is not 0x01 or 0x02 (e.g.
+        // Success 0x00)
+        #[rustfmt::skip]
+        let malformed_bytes_code = &[
+            0x01, // Opcode
+            0xFF, // Global Error
+            0x00, 0x00, 0x00, // ASE 0, Success, None (response code must be 0x01 or 0x02)
+        ];
+        let (res_malformed_code, _) = ControlPointNotification::decode(malformed_bytes_code);
+        assert_eq!(
+            res_malformed_code,
+            Err(bt_common::packet_encoding::Error::UnexpectedDataLength)
+        );
+    }
 }