rust/bt-ascs: Implement Configure Codec operation

Implement config codec operation for ASCS client.
 - Create `AudioStreamEndpointHandle` struct so clients of the library
   can perform a ASE control operation on a set of audio stream endpoints
 - To reduce work on clients, ensure the `configure_codec` method
   returns the result from the server after the control operation is
   requested

Test: cargo test -p bt-ascs
Bug: b/431814103

Change-Id: I961354cd3d0ba69f2fe099a88039009301c743dc
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2320
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index 8f51ce5..691f15a 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -3,8 +3,9 @@
 // found in the LICENSE file.
 
 use futures::stream::{BoxStream, StreamExt};
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 
+use bt_common::packet_encoding::Decodable;
 use bt_common::Uuid;
 use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic};
 use bt_gatt::types::Handle;
@@ -25,10 +26,20 @@
     ExtraControlPointCharacteristic,
     #[error("Remote server doesn't have any audio stream endpoints")]
     MissingAudioStreamEndpoints,
+    #[error("Notification stream closed before receiving all responses")]
+    NotificationStreamClosed,
+    #[error(
+        "ASE {ase_id:?} transitioned to unexpected state: expected {expected:?}, got {actual:?}"
+    )]
+    UnexpectedStateTransition { ase_id: AseId, expected: AseState, actual: AseState },
+    #[error("Unknown ASE ID: {0:?}")]
+    UnknownAseId(AseId),
+    #[error("ASE {ase_id:?} is in invalid starting state {actual:?} for opcode {opcode:?}")]
+    InvalidStartState { ase_id: AseId, opcode: AseControlPointOpcode, actual: AseState },
 }
 
 /// Represents a single source/sink ASE state characteristic.
-pub struct AudioStreamEndpointHandle {
+pub struct ClientEndpoint {
     pub endpoint: AudioStreamEndpoint,
     pub notification_stream:
         BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>,
@@ -45,12 +56,63 @@
 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 endpoints: DiscoveredEndpoints,
 }
 
-pub type SinkAndSourceEndpoints =
-    (HashMap<Handle, AudioStreamEndpointHandle>, HashMap<Handle, AudioStreamEndpointHandle>);
+pub struct DiscoveredEndpoints {
+    pub sink: HashMap<Handle, ClientEndpoint>,
+    pub source: HashMap<Handle, ClientEndpoint>,
+}
+
+impl DiscoveredEndpoints {
+    /// Returns the ASE IDs of all discovered sink Audio Stream Endpoints.
+    pub fn sink_ases(&self) -> Vec<AseId> {
+        self.sink.values().map(|e| e.endpoint.ase_id).collect()
+    }
+
+    /// Returns the ASE IDs of all discovered source Audio Stream Endpoints.
+    pub fn source_ases(&self) -> Vec<AseId> {
+        self.source.values().map(|e| e.endpoint.ase_id).collect()
+    }
+
+    pub(crate) fn lookup_by_ase_id(&self, ase_id: AseId) -> Option<&ClientEndpoint> {
+        if let Some(e) = self.sink.values().find(|e| e.endpoint.ase_id == ase_id) {
+            return Some(e);
+        }
+        if let Some(e) = self.source.values().find(|e| e.endpoint.ase_id == ase_id) {
+            return Some(e);
+        }
+        None
+    }
+}
+
+/// The outcome of an ASE control point operation, partitioning successfully
+/// transitioned endpoints and failed response codes.
+#[derive(Debug, Clone, PartialEq)]
+pub struct AseControlOperationOutcome {
+    accepted: HashMap<AseId, AudioStreamEndpoint>,
+    rejected: HashMap<AseId, ResponseCode>,
+}
+
+impl AseControlOperationOutcome {
+    pub(crate) fn new(
+        accepted: HashMap<AseId, AudioStreamEndpoint>,
+        rejected: HashMap<AseId, ResponseCode>,
+    ) -> Self {
+        Self { accepted, rejected }
+    }
+
+    /// Returns the map of successfully transitioned [`AudioStreamEndpoint`]s,
+    /// keyed by their [`AseId`].
+    pub fn accepted(&self) -> &HashMap<AseId, AudioStreamEndpoint> {
+        &self.accepted
+    }
+
+    /// Returns the map of rejected [`ResponseCode`]s, keyed by their [`AseId`].
+    pub fn rejected(&self) -> &HashMap<AseId, ResponseCode> {
+        &self.rejected
+    }
+}
 
 impl<T: bt_gatt::GattTypes> AudioStreamControlServiceClient<T> {
     pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error>
@@ -58,9 +120,9 @@
         <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?;
+        let endpoints = Self::discover_all_endpoints(&gatt_client).await?;
 
-        Ok(Self { gatt_client, control_point, sink_endpoints, source_endpoints })
+        Ok(Self { gatt_client, control_point, endpoints })
     }
 
     async fn discover_control_point(gatt_client: &T::PeerService) -> Result<AseControlPoint, Error>
@@ -83,7 +145,7 @@
 
     async fn discover_all_endpoints(
         gatt_client: &T::PeerService,
-    ) -> Result<SinkAndSourceEndpoints, Error>
+    ) -> Result<DiscoveredEndpoints, Error>
     where
         <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
     {
@@ -105,8 +167,7 @@
             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 });
+            sink_endpoints.insert(handle, ClientEndpoint { endpoint, notification_stream });
         }
 
         let mut source_endpoints = HashMap::new();
@@ -115,11 +176,10 @@
             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 });
+            source_endpoints.insert(handle, ClientEndpoint { endpoint, notification_stream });
         }
 
-        Ok((sink_endpoints, source_endpoints))
+        Ok(DiscoveredEndpoints { sink: sink_endpoints, source: source_endpoints })
     }
 
     async fn read_and_create_endpoint(
@@ -139,11 +199,191 @@
             })?;
         Ok(endpoint)
     }
+
+    fn verify_ase_ids<I>(&self, ase_ids: I) -> Result<(), Error>
+    where
+        I: IntoIterator<Item = AseId>,
+    {
+        for ase_id in ase_ids {
+            if self.endpoints.lookup_by_ase_id(ase_id).is_none() {
+                return Err(Error::Client(ClientError::UnknownAseId(ase_id)));
+            }
+        }
+        Ok(())
+    }
+
+    async fn read_and_update_endpoint(
+        &mut self,
+        handle: &Handle,
+        direction: AudioDirection,
+    ) -> Result<AudioStreamEndpoint, Error> {
+        let mut buf = vec![0; 255];
+        let (read_bytes, _truncated) = self
+            .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),
+                    ))
+                },
+            )?;
+
+        let endpoints = match direction {
+            AudioDirection::Sink => &mut self.endpoints.sink,
+            AudioDirection::Source => &mut self.endpoints.source,
+        };
+        if let Some(endpoint_handle) = endpoints.get_mut(handle) {
+            endpoint_handle.endpoint = endpoint.clone();
+        }
+        Ok(endpoint)
+    }
+
+    async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> {
+        use bt_common::packet_encoding::Encodable;
+
+        let mut buf = vec![0; operation.encoded_len()];
+        operation
+            .encode(&mut buf[..])
+            .map_err(|e| Error::Internal(format!("Failed to encode operation: {:?}", e)))?;
+
+        self.gatt_client
+            .write_characteristic(
+                &self.control_point.handle,
+                bt_gatt::types::WriteMode::WithoutResponse,
+                0,
+                &buf[..],
+            )
+            .await
+            .map_err(Error::Gatt)
+    }
+
+    async fn collect_operation_responses(
+        &mut self,
+        opcode: u8,
+        mut remaining_ases: HashSet<AseId>,
+    ) -> Result<HashMap<AseId, ResponseCode>, Error> {
+        let mut responses = HashMap::new();
+
+        while !remaining_ases.is_empty() {
+            let notification = self
+                .control_point
+                .notification_stream
+                .next()
+                .await
+                .ok_or(Error::Client(ClientError::NotificationStreamClosed))?;
+            let notification = notification.map_err(Error::from)?;
+
+            let (cp_notification, _) = ControlPointNotification::decode(&notification.value[..]);
+            let cp_notification = cp_notification.map_err(|e| {
+                bt_gatt::types::Error::other(std::io::Error::new(
+                    std::io::ErrorKind::InvalidData,
+                    format!("Failed to decode ControlPointNotification: {:?}", e),
+                ))
+            })?;
+            if cp_notification.opcode != opcode {
+                continue;
+            }
+
+            if cp_notification.number_of_ases == 0xFF {
+                return Err(Error::Internal(format!(
+                    "Server rejected operation: {:?}",
+                    cp_notification.responses
+                )));
+            }
+
+            for r in cp_notification.responses {
+                if remaining_ases.remove(&r.ase_id()) {
+                    responses.insert(r.ase_id(), r);
+                }
+            }
+        }
+
+        Ok(responses)
+    }
+
+    async fn perform_and_verify_operation(
+        &mut self,
+        operation: AseControlOperation,
+        pending_ases: HashSet<AseId>,
+    ) -> Result<AseControlOperationOutcome, Error> {
+        self.write_operation(&operation).await?;
+
+        let opcode_u8 = u8::try_from(&operation)?;
+        let responses = self.collect_operation_responses(opcode_u8, pending_ases).await?;
+
+        let mut successful_endpoints = HashMap::new();
+        let mut failed_responses = HashMap::new();
+        let opcode = AseControlPointOpcode::try_from(&operation).map_err(|e| {
+            Error::Internal(format!("Failed to resolve opcode from operation: {:?}", e))
+        })?;
+
+        for r in responses.into_values() {
+            if !r.is_success() {
+                failed_responses.insert(r.ase_id(), r);
+                continue;
+            }
+            let client_endpoint = self
+                .endpoints
+                .lookup_by_ase_id(r.ase_id())
+                .ok_or(Error::Client(ClientError::UnknownAseId(r.ase_id())))?;
+            let handle = client_endpoint.endpoint.handle;
+            let direction = client_endpoint.endpoint.direction;
+
+            let endpoint = self.read_and_update_endpoint(&handle, direction).await?;
+
+            if !opcode.allows_transition_to(endpoint.direction, &endpoint.state) {
+                let expected = opcode.operation_target_states(endpoint.direction)[0];
+                return Err(Error::Client(ClientError::UnexpectedStateTransition {
+                    ase_id: endpoint.ase_id,
+                    expected,
+                    actual: endpoint.state,
+                }));
+            }
+            successful_endpoints.insert(endpoint.ase_id, endpoint);
+        }
+
+        Ok(AseControlOperationOutcome::new(successful_endpoints, failed_responses))
+    }
+
+    /// Performs the Configure Codec operation on one or more ASEs.
+    ///
+    /// This method sends the Configure Codec request to the remote server,
+    /// waits for control point notifications for all involved ASEs, and
+    /// verifies that the successfully configured ASEs have transitioned to
+    /// the `CodecConfigured` state.
+    ///
+    /// # Arguments
+    /// * `codec_configurations` - A vector of `CodecConfiguration` structs
+    ///   containing the parameters for each ASE to be configured.
+    ///
+    /// # Returns
+    /// On success, returns a tuple containing:
+    /// * A `HashMap` mapping each successfully configured `AseId` to its
+    ///   supported `QosParameters` published by the server.
+    /// * A `Vec` containing the `ResponseCode` for any ASEs where the
+    ///   configuration failed.
+    pub async fn configure_codec(
+        &mut self,
+        codec_configurations: Vec<CodecConfiguration>,
+    ) -> Result<AseControlOperationOutcome, Error> {
+        let pending_ases: HashSet<AseId> = codec_configurations.iter().map(|c| c.ase_id).collect();
+        self.verify_ase_ids(pending_ases.iter().cloned())?;
+
+        let op = AseControlOperation::ConfigCodec { codec_configurations, responses: vec![] };
+
+        self.perform_and_verify_operation(op, pending_ases).await
+    }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use bt_common::core::CodecId;
     use bt_gatt::test_utils::{FakePeerService, FakeTypes};
     use bt_gatt::types::{AttributePermissions, CharacteristicProperty};
     use bt_gatt::Characteristic;
@@ -208,18 +448,21 @@
         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);
+        assert_eq!(client.endpoints.sink.len(), 1);
+        assert_eq!(client.endpoints.source.len(), 1);
 
-        let sink = &client.sink_endpoints[&SINK_ASE_HANDLE].endpoint;
+        let sink = &client.endpoints.sink[&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;
+        let source = &client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint;
         assert_eq!(source.ase_id, AseId(2));
         assert_eq!(source.state, AseState::Idle);
         assert_eq!(source.direction, AudioDirection::Source);
+
+        assert_eq!(client.endpoints.sink_ases(), vec![AseId(1)]);
+        assert_eq!(client.endpoints.source_ases(), vec![AseId(2)]);
     }
 
     #[test]
@@ -301,4 +544,155 @@
         };
         assert!(matches!(err, Error::Client(ClientError::ExtraControlPointCharacteristic)));
     }
+
+    #[test]
+    fn configure_codec() {
+        let mut service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        let config1 = CodecConfiguration {
+            ase_id: AseId(1),
+            target_latency: TargetLatency::TargetLowLatency,
+            target_phy: TargetPhy::Le1MPhy,
+            codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+            codec_specific_configuration: vec![],
+        };
+        let config2 = CodecConfiguration {
+            ase_id: AseId(2),
+            target_latency: TargetLatency::TargetLowLatency,
+            target_phy: TargetPhy::Le1MPhy,
+            codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+            codec_specific_configuration: vec![],
+        };
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x01, // Opcode: Config Codec
+                0x02, // Num ASEs
+                0x01, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 1 Config
+                0x02, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 2 Config
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x01, // Opcode
+                    0x02, // Num ASEs
+                    0x01, 0x00, 0x00, // ASE 1 Response: Success
+                    0x02, 0x03, 0x02, // ASE 2 Response: Failed (Invalid ASE ID)
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        #[rustfmt::skip]
+        service.add_characteristic(
+            Characteristic {
+                handle: SINK_ASE_HANDLE,
+                uuid: SINK_ASE_UUID,
+                properties: CharacteristicProperty::Read | CharacteristicProperty::Notify,
+                permissions: AttributePermissions::default(),
+                descriptors: vec![],
+            },
+            vec![
+                0x01, // ASE ID: 1
+                0x01, // ASE State: Codec Configured
+                0x00, 0x01, 0x02, 0x0A, 0x00, 0x10, 0x27, 0x00, 0x40, 0x9C, 0x00,
+                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
+            ],
+        );
+
+        let configure_fut = client.configure_codec(vec![config1, config2]);
+        let outcome = run_to_completion(configure_fut).expect("configure codec should succeed");
+
+        assert_eq!(
+            client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state,
+            AseState::CodecConfigured
+        );
+        assert_eq!(outcome.accepted().len(), 1);
+        let endpoint = &outcome.accepted()[&AseId(1)];
+        let qos = endpoint.qos_parameters().unwrap();
+        assert_eq!(qos.framing, Framing::Unframed);
+        assert_eq!(qos.preferred_phys, vec![Phy::Le1MPhy]);
+        assert_eq!(qos.preferred_retransmission_number, 2);
+
+        assert_eq!(outcome.rejected().len(), 1);
+        assert_eq!(outcome.rejected()[&AseId(2)], ResponseCode::InvalidAseId { value: 2 });
+    }
+
+    #[test]
+    fn configure_codec_fail_unknown_ase_id() {
+        let service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        let config = CodecConfiguration {
+            ase_id: AseId(99), // Non-existent ASE ID
+            target_latency: TargetLatency::TargetLowLatency,
+            target_phy: TargetPhy::Le1MPhy,
+            codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+            codec_specific_configuration: vec![],
+        };
+
+        let configure_fut = client.configure_codec(vec![config]);
+        let err = run_to_completion(configure_fut).expect_err("should fail due to unknown ASE ID");
+        assert!(matches!(err, Error::Client(ClientError::UnknownAseId(AseId(99)))));
+    }
+
+    #[test]
+    fn configure_codec_global_error() {
+        let mut service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        let config = CodecConfiguration {
+            ase_id: AseId(1),
+            target_latency: TargetLatency::TargetLowLatency,
+            target_phy: TargetPhy::Le1MPhy,
+            codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+            codec_specific_configuration: vec![],
+        };
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x01, // Opcode: Config Codec
+                0x01, // Num ASEs
+                0x01, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 1 Config
+            ],
+        );
+
+        // Let's mimic server side throwing a global invalid length error.
+        // (in reality this wouldn't happen because our configure codec operation was
+        // encoded correctly).
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x01, // Opcode
+                    0xFF, // Num ASEs: Global Error
+                    0x00, 0x02, 0x00, // ASE 0, Invalid Length, None
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let configure_fut = client.configure_codec(vec![config]);
+        let err = run_to_completion(configure_fut).expect_err("should fail with global error");
+
+        assert!(matches!(
+            err,
+            Error::Internal(ref s) if s.contains("Server rejected operation") && s.contains("InvalidLength")
+        ));
+    }
 }
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs
index 324a2b7..6175baf 100644
--- a/rust/bt-ascs/src/types.rs
+++ b/rust/bt-ascs/src/types.rs
@@ -124,6 +124,14 @@
         [self.ase_id_value(), self.to_code(), self.reason_byte()].into()
     }
 
+    pub fn ase_id(&self) -> AseId {
+        AseId(self.ase_id_value())
+    }
+
+    pub fn is_success(&self) -> bool {
+        matches!(self, Self::Success { .. })
+    }
+
     pub fn decode_response(
         buf: &[u8],
         opcode: u8,
@@ -272,6 +280,19 @@
     }
 }
 
+impl Encodable for AseId {
+    type Error = bt_common::packet_encoding::Error;
+    fn encoded_len(&self) -> usize {
+        1
+    }
+    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        if buf.is_empty() {
+            return Err(Self::Error::BufferTooSmall);
+        }
+        buf[0] = self.0;
+        Ok(())
+    }
+}
 #[derive(Debug, Clone, PartialEq)]
 pub enum AseAdditionalParameters {
     None,
@@ -513,7 +534,7 @@
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct AudioStreamEndpoint {
     pub handle: Handle,
     pub direction: AudioDirection,
@@ -572,6 +593,36 @@
             _ => None,
         }
     }
+
+    pub fn qos_parameters(&self) -> Option<QosParameters> {
+        let AseAdditionalParameters::CodecConfigured {
+            framing,
+            preferred_phys,
+            preferred_retransmission_number,
+            max_transport_latency,
+            presentation_delay_range,
+            ..
+        } = &self.additional
+        else {
+            return None;
+        };
+        Some(QosParameters {
+            framing: *framing,
+            preferred_phys: preferred_phys.clone(),
+            preferred_retransmission_number: *preferred_retransmission_number,
+            max_transport_latency: *max_transport_latency,
+            presentation_delay_range: presentation_delay_range.clone(),
+        })
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct QosParameters {
+    pub framing: Framing,
+    pub preferred_phys: Vec<Phy>,
+    pub preferred_retransmission_number: u8,
+    pub max_transport_latency: MaxTransportLatency,
+    pub presentation_delay_range: PresentationDelayRange,
 }
 
 impl Encodable for AudioStreamEndpoint {
@@ -628,6 +679,37 @@
         };
         allowed_states.contains(state)
     }
+
+    /// Returns the list of expected valid states that targeted ASEs should
+    /// transition into following a successful invocation of this operation,
+    /// depending on the audio stream endpoint direction.
+    pub(crate) fn operation_target_states(&self, direction: AudioDirection) -> &[AseState] {
+        match self {
+            Self::ConfigCodec { .. } => &[AseState::CodecConfigured],
+            Self::ConfigQos { .. } => &[AseState::QosConfigured],
+            Self::Enable { .. } => &[AseState::Enabling, AseState::Streaming],
+            Self::ReceiverStartReady { .. } => &[AseState::Streaming],
+            Self::Disable { .. } => match direction {
+                AudioDirection::Sink => &[AseState::QosConfigured],
+                AudioDirection::Source => &[AseState::Disabling],
+            },
+            Self::ReceiverStopReady { .. } => &[AseState::QosConfigured],
+            Self::UpdateMetadata { .. } => &[AseState::Enabling, AseState::Streaming],
+            Self::Release { .. } => {
+                &[AseState::Releasing, AseState::Idle, AseState::CodecConfigured]
+            }
+        }
+    }
+
+    /// Returns true if the given `next_state` is a valid transition state
+    /// for this operation and audio direction according to the specification.
+    pub(crate) fn allows_transition_to(
+        &self,
+        direction: AudioDirection,
+        next_state: &AseState,
+    ) -> bool {
+        self.operation_target_states(direction).contains(next_state)
+    }
 }
 
 /// ASE Control Operations.  These can be initiated by a server or client.
@@ -799,6 +881,108 @@
     }
 }
 
+impl Encodable for AseControlOperation {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        match self {
+            AseControlOperation::ConfigCodec { codec_configurations, .. } => {
+                2 + codec_configurations.iter().map(|c| c.encoded_len()).sum::<usize>()
+            }
+            AseControlOperation::ConfigQos { qos_configurations, .. } => {
+                2 + qos_configurations.len() * QosConfiguration::BYTE_SIZE
+            }
+            AseControlOperation::Enable { ases_with_metadata, .. } => {
+                2 + ases_with_metadata.iter().map(|a| a.encoded_len()).sum::<usize>()
+            }
+            AseControlOperation::ReceiverStartReady { ases } => 2 + ases.len() * AseId::BYTE_SIZE,
+            AseControlOperation::Disable { ases } => 2 + ases.len() * AseId::BYTE_SIZE,
+            AseControlOperation::ReceiverStopReady { ases } => 2 + ases.len() * AseId::BYTE_SIZE,
+            AseControlOperation::UpdateMetadata { ases_with_metadata, .. } => {
+                2 + ases_with_metadata.iter().map(|a| a.encoded_len()).sum::<usize>()
+            }
+            AseControlOperation::Release { ases } => 2 + ases.len() * AseId::BYTE_SIZE,
+            AseControlOperation::Released { .. } => 0,
+        }
+    }
+
+    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        if buf.len() < self.encoded_len() {
+            return Err(Self::Error::BufferTooSmall);
+        }
+        let opcode: u8 = self.try_into().map_err(|_| Self::Error::OutOfRange)?;
+        buf[0] = opcode;
+        match self {
+            AseControlOperation::ConfigCodec { codec_configurations, .. } => {
+                buf[1] = codec_configurations.len() as u8;
+                let mut idx = 2;
+                for c in codec_configurations {
+                    c.encode(&mut buf[idx..])?;
+                    idx += c.encoded_len();
+                }
+            }
+            AseControlOperation::ConfigQos { qos_configurations, .. } => {
+                buf[1] = qos_configurations.len() as u8;
+                let mut idx = 2;
+                for q in qos_configurations {
+                    q.encode(&mut buf[idx..])?;
+                    idx += QosConfiguration::BYTE_SIZE;
+                }
+            }
+            AseControlOperation::Enable { ases_with_metadata, .. } => {
+                buf[1] = ases_with_metadata.len() as u8;
+                let mut idx = 2;
+                for a in ases_with_metadata {
+                    a.encode(&mut buf[idx..])?;
+                    idx += a.encoded_len();
+                }
+            }
+            AseControlOperation::ReceiverStartReady { ases } => {
+                buf[1] = ases.len() as u8;
+                let mut idx = 2;
+                for a in ases {
+                    a.encode(&mut buf[idx..])?;
+                    idx += AseId::BYTE_SIZE;
+                }
+            }
+            AseControlOperation::Disable { ases } => {
+                buf[1] = ases.len() as u8;
+                let mut idx = 2;
+                for a in ases {
+                    a.encode(&mut buf[idx..])?;
+                    idx += AseId::BYTE_SIZE;
+                }
+            }
+            AseControlOperation::ReceiverStopReady { ases } => {
+                buf[1] = ases.len() as u8;
+                let mut idx = 2;
+                for a in ases {
+                    a.encode(&mut buf[idx..])?;
+                    idx += AseId::BYTE_SIZE;
+                }
+            }
+            AseControlOperation::UpdateMetadata { ases_with_metadata, .. } => {
+                buf[1] = ases_with_metadata.len() as u8;
+                let mut idx = 2;
+                for a in ases_with_metadata {
+                    a.encode(&mut buf[idx..])?;
+                    idx += a.encoded_len();
+                }
+            }
+            AseControlOperation::Release { ases } => {
+                buf[1] = ases.len() as u8;
+                let mut idx = 2;
+                for a in ases {
+                    a.encode(&mut buf[idx..])?;
+                    idx += AseId::BYTE_SIZE;
+                }
+            }
+            AseControlOperation::Released { .. } => return Err(Self::Error::OutOfRange),
+        }
+        Ok(())
+    }
+}
+
 decodable_enum! {
 pub enum TargetLatency<u8, bt_common::packet_encoding::Error, OutOfRange> {
     TargetLowLatency = 0x01,
@@ -908,6 +1092,28 @@
     }
 }
 
+impl Encodable for CodecConfiguration {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        Self::MIN_BYTE_SIZE + self.codec_specific_configuration.len()
+    }
+
+    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.target_latency.into();
+        buf[2] = self.target_phy.into();
+        self.codec_id.encode(&mut buf[3..8])?;
+        buf[8] = self.codec_specific_configuration.len() as u8;
+        buf[9..9 + self.codec_specific_configuration.len()]
+            .copy_from_slice(&self.codec_specific_configuration);
+        Ok(())
+    }
+}
+
 /// Represents Config QoS parameters for a single ASE. See ASCS v1.0.1 Section
 /// 5.2.
 #[derive(Debug, Clone, PartialEq)]
@@ -1015,6 +1221,31 @@
     }
 }
 
+impl Encodable for QosConfiguration {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        Self::BYTE_SIZE
+    }
+
+    fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        if buf.len() < Self::BYTE_SIZE {
+            return Err(Self::Error::BufferTooSmall);
+        }
+        buf[0] = self.ase_id.into();
+        self.cig_id.encode(&mut buf[1..2])?;
+        self.cis_id.encode(&mut buf[2..3])?;
+        self.sdu_interval.encode(&mut buf[3..6])?;
+        self.framing.encode(&mut buf[6..7])?;
+        buf[7] = Phy::to_bits(self.phy.iter());
+        self.max_sdu.encode(&mut buf[8..10])?;
+        buf[10] = self.retransmission_number;
+        self.max_transport_latency.encode(&mut buf[11..13])?;
+        self.presentation_delay.encode(&mut buf[13..16])?;
+        Ok(())
+    }
+}
+
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub struct CigId(u8);
 
@@ -1447,6 +1678,28 @@
     }
 }
 
+impl Encodable for AseIdWithMetadata {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn encoded_len(&self) -> usize {
+        let metadata_len: usize = self.metadata.iter().map(|m| m.encoded_len()).sum();
+        2 + metadata_len
+    }
+
+    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();
+        let metadata_len: usize = self.metadata.iter().map(|m| m.encoded_len()).sum();
+        buf[1] = metadata_len as u8;
+
+        use bt_common::core::ltv::LtValue;
+        LtValue::encode_all(self.metadata.clone().into_iter(), &mut buf[2..])?;
+        Ok(())
+    }
+}
+
 impl Decodable for PresentationDelayRange {
     type Error = ResponseCode;
 
@@ -1617,6 +1870,38 @@
     use bt_common::generic_audio::{codec_configuration, AudioLocation};
 
     #[test]
+    fn opcode_state_rules() {
+        // 1. allowed_in_state tests
+        assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Idle));
+        assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::CodecConfigured));
+        assert!(!AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Streaming));
+
+        assert!(AseControlPointOpcode::Disable.allowed_in_state(&AseState::Streaming));
+        assert!(!AseControlPointOpcode::Disable.allowed_in_state(&AseState::Idle));
+
+        // 2. operation_target_states & allows_transition_to tests
+        // Sink direction
+        assert!(AseControlPointOpcode::Disable
+            .allows_transition_to(AudioDirection::Sink, &AseState::QosConfigured));
+        assert!(!AseControlPointOpcode::Disable
+            .allows_transition_to(AudioDirection::Sink, &AseState::Disabling));
+
+        // Source direction
+        assert!(AseControlPointOpcode::Disable
+            .allows_transition_to(AudioDirection::Source, &AseState::Disabling));
+        assert!(!AseControlPointOpcode::Disable
+            .allows_transition_to(AudioDirection::Source, &AseState::QosConfigured));
+
+        // UpdateMetadata (same state allowed)
+        assert!(AseControlPointOpcode::UpdateMetadata
+            .allows_transition_to(AudioDirection::Source, &AseState::Enabling));
+        assert!(AseControlPointOpcode::UpdateMetadata
+            .allows_transition_to(AudioDirection::Source, &AseState::Streaming));
+        assert!(!AseControlPointOpcode::UpdateMetadata
+            .allows_transition_to(AudioDirection::Source, &AseState::Idle));
+    }
+
+    #[test]
     fn codec_configuration_roundtrip() {
         let codec_specific_configuration = vec![
             codec_configuration::CodecConfiguration::SamplingFrequency(