rust/bt-ascs: Implement Release operation This change implements the client-side "Release" operation as defined in ASCS v1.0.1, Section 5.8. This allows a client to release all resources associated with one or more ASEs. - Added the `release` method to the `AudioStreamControlServiceClient`. - Implemented the encoding logic for the `Release` control operation. - Added unit tests to verify the success path and failure due to invalid ASE state. Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: I0cef17323e1d0b2c9cfd16a0d978bb086b81144e Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2483
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index fbd50b1..ce1e1e8 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -306,7 +306,7 @@ /// `UnknownAseId` or `InvalidStartState`) if any target ASE fails /// validation. fn validate_arguments<'a>( - &mut self, + &self, opcode: AseControlPointOpcode, pending_ases: impl IntoIterator<Item = &'a AseId>, ) -> Result<(), Error> { @@ -375,9 +375,10 @@ async fn collect_operation_responses( &mut self, - opcode: u8, - mut remaining_ases: HashSet<AseId>, + operation: &AseControlOperation, ) -> Result<HashMap<AseId, ResponseCode>, Error> { + let opcode = u8::try_from(operation)?; + let mut remaining_ases: HashSet<AseId> = operation.ase_ids().copied().collect(); let mut responses = HashMap::new(); while !remaining_ases.is_empty() { @@ -414,12 +415,10 @@ 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 responses = self.collect_operation_responses(&operation).await?; let mut successful_endpoints = HashMap::new(); let opcode = AseControlPointOpcode::try_from(&operation).map_err(|e| { @@ -489,12 +488,14 @@ &mut self, codec_configurations: Vec<CodecConfiguration>, ) -> Result<AseControlOperationOutcome, Error> { - let pending_ases: HashSet<AseId> = codec_configurations.iter().map(|c| c.ase_id).collect(); - self.validate_arguments(AseControlPointOpcode::ConfigCodec, &pending_ases)?; + self.validate_arguments( + AseControlPointOpcode::ConfigCodec, + codec_configurations.iter().map(|c| &c.ase_id), + )?; let op = AseControlOperation::ConfigCodec { codec_configurations, responses: vec![] }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Configure QoS operation on one or more ASEs. @@ -515,15 +516,17 @@ .map(|req| req.try_into_config(self)) .collect::<Result<Vec<_>, Error>>()?; - let pending_ases: HashSet<AseId> = qos_configurations.iter().map(|q| q.ase_id).collect(); - self.validate_arguments(AseControlPointOpcode::ConfigQos, &pending_ases)?; + self.validate_arguments( + AseControlPointOpcode::ConfigQos, + qos_configurations.iter().map(|q| &q.ase_id), + )?; // TODO(b/431814103): Consider ATT MTU limits when configuring multiple ASEs. // If the encoded request exceeds the current MTU, we may need to break it into // multiple Control Point writes. let op = AseControlOperation::ConfigQos { qos_configurations, responses: vec![] }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Enable operation on one or more ASEs. @@ -539,12 +542,14 @@ &mut self, ases_with_metadata: Vec<AseIdWithMetadata>, ) -> Result<AseControlOperationOutcome, Error> { - let pending_ases: HashSet<AseId> = ases_with_metadata.iter().map(|a| a.ase_id).collect(); - self.validate_arguments(AseControlPointOpcode::Enable, &pending_ases)?; + self.validate_arguments( + AseControlPointOpcode::Enable, + ases_with_metadata.iter().map(|a| &a.ase_id), + )?; let op = AseControlOperation::Enable { ases_with_metadata, responses: vec![] }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Receiver Start Ready operation on one or more Source ASEs. @@ -559,17 +564,16 @@ &mut self, ases: Vec<AseId>, ) -> Result<AseControlOperationOutcome, Error> { - let pending_ases: HashSet<AseId> = ases.iter().cloned().collect(); - self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &pending_ases)?; + self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &ases)?; self.verify_ase_directions( - &pending_ases, + &ases, AseControlPointOpcode::ReceiverStartReady, AudioDirection::Source, )?; let op = AseControlOperation::ReceiverStartReady { ases }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Disable operation on one or more ASEs. @@ -581,12 +585,11 @@ /// On success, returns an [`AseControlOperationOutcome`] containing the /// results of the operation. pub async fn disable(&mut self, ases: Vec<AseId>) -> Result<AseControlOperationOutcome, Error> { - let pending_ases: HashSet<AseId> = ases.iter().cloned().collect(); - self.validate_arguments(AseControlPointOpcode::Disable, &pending_ases)?; + self.validate_arguments(AseControlPointOpcode::Disable, &ases)?; let op = AseControlOperation::Disable { ases }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Receiver Stop Ready operation on one or more Source ASEs. @@ -601,17 +604,16 @@ &mut self, ases: Vec<AseId>, ) -> Result<AseControlOperationOutcome, Error> { - let pending_ases: HashSet<AseId> = ases.iter().cloned().collect(); - self.validate_arguments(AseControlPointOpcode::ReceiverStopReady, &pending_ases)?; + self.validate_arguments(AseControlPointOpcode::ReceiverStopReady, &ases)?; self.verify_ase_directions( - &pending_ases, + &ases, AseControlPointOpcode::ReceiverStopReady, AudioDirection::Source, )?; let op = AseControlOperation::ReceiverStopReady { ases }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await } /// Performs the Update Metadata operation on one or more ASEs. @@ -632,10 +634,25 @@ ases_with_metadata.iter().map(|a| &a.ase_id), )?; - let pending_ases: HashSet<AseId> = ases_with_metadata.iter().map(|a| a.ase_id).collect(); let op = AseControlOperation::UpdateMetadata { ases_with_metadata, responses: vec![] }; - self.perform_and_verify_operation(op, pending_ases).await + self.perform_and_verify_operation(op).await + } + + /// Performs the Release operation on one or more ASEs. + /// + /// # Arguments + /// * `ases` - A vector of `AseId`s to release. + /// + /// # Returns + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. + pub async fn release(&mut self, ases: Vec<AseId>) -> Result<AseControlOperationOutcome, Error> { + self.validate_arguments(AseControlPointOpcode::Release, &ases)?; + + let op = AseControlOperation::Release { ases }; + + self.perform_and_verify_operation(op).await } } @@ -1976,4 +1993,159 @@ }) )); } + + #[test] + fn release_success_to_idle() { + 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"); + + // Pre-condition SOURCE_ASE_HANDLE (ASE 2) to Streaming state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x04, // ASE State: Streaming + 0x01, 0x01, 0x00, + ]; + client.endpoints.source.get_mut(&SOURCE_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SOURCE_ASE_HANDLE, + AudioDirection::Source, + &source_value, + ) + .unwrap(); + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x08, // Opcode: Release + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x08, // Opcode: Release + 0x01, // Num ASEs + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + // Expected state transitions back to Idle + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 0x00, // ASE State: Idle + ], + maybe_truncated: false, + }), + ); + + let release_fut = client.release(vec![AseId(2)]); + let outcome = run_to_completion(release_fut).expect("release should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Idle); + } + + #[test] + fn release_success_to_codec_configured() { + 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"); + + // Pre-condition SOURCE_ASE_HANDLE (ASE 2) to Streaming state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x04, // ASE State: Streaming + 0x01, 0x01, 0x00, + ]; + client.endpoints.source.get_mut(&SOURCE_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SOURCE_ASE_HANDLE, + AudioDirection::Source, + &source_value, + ) + .unwrap(); + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x08, // Opcode: Release + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x08, // Opcode: Release + 0x01, // Num ASEs + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + // Expected state transitions back to Codec Configured + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 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, + ], + maybe_truncated: false, + }), + ); + + let release_fut = client.release(vec![AseId(2)]); + let outcome = run_to_completion(release_fut).expect("release should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!( + client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, + AseState::CodecConfigured + ); + } + + #[test] + fn release_fail_invalid_start_state() { + 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"); + + // Source ASE is in Idle state, which is invalid starting state for Release + let release_fut = client.release(vec![AseId(2)]); + let err = run_to_completion(release_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(2), + opcode: AseControlPointOpcode::Release, + actual: AseState::Idle, + }) + )); + } }
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs index fc330d2..27b81d3 100644 --- a/rust/bt-ascs/src/types.rs +++ b/rust/bt-ascs/src/types.rs
@@ -739,6 +739,27 @@ impl AseControlOperation { const MIN_BYTE_SIZE: usize = 3; + pub(crate) fn ase_ids(&self) -> impl Iterator<Item = &AseId> { + match self { + Self::ConfigCodec { codec_configurations, .. } => { + Box::new(codec_configurations.iter().map(|c| &c.ase_id)) + as Box<dyn Iterator<Item = &AseId>> + } + Self::ConfigQos { qos_configurations, .. } => { + Box::new(qos_configurations.iter().map(|q| &q.ase_id)) + } + Self::Enable { ases_with_metadata, .. } + | Self::UpdateMetadata { ases_with_metadata, .. } => { + Box::new(ases_with_metadata.iter().map(|a| &a.ase_id)) + } + Self::ReceiverStartReady { ases } + | Self::Disable { ases } + | Self::ReceiverStopReady { ases } + | Self::Release { ases } => Box::new(ases.iter()), + Self::Released { ase_id } => Box::new(std::iter::once(ase_id)), + } + } + fn contains_invalid_length(&self) -> bool { match self { Self::ConfigCodec { responses, .. } @@ -1897,12 +1918,47 @@ mod tests { use super::*; - use bt_common::packet_encoding::Encodable; + use std::collections::HashSet; - use bt_common::core::ltv::LtValue; use bt_common::generic_audio::{codec_configuration, AudioLocation}; #[test] + fn ase_control_operation_ase_ids() { + let op = AseControlOperation::ConfigCodec { + codec_configurations: vec![ + 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![], + }, + 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![], + }, + ], + responses: vec![], + }; + assert_eq!( + op.ase_ids().copied().collect::<HashSet<_>>(), + HashSet::from([AseId(1), AseId(2)]) + ); + + let op = AseControlOperation::ReceiverStartReady { ases: vec![AseId(3), AseId(4)] }; + assert_eq!( + op.ase_ids().copied().collect::<HashSet<_>>(), + HashSet::from([AseId(3), AseId(4)]) + ); + + let op = AseControlOperation::Released { ase_id: AseId(5) }; + assert_eq!(op.ase_ids().copied().collect::<HashSet<_>>(), HashSet::from([AseId(5)])); + } + + #[test] fn opcode_state_rules() { // 1. allowed_in_state tests assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Idle));