rust/bt-ascs: Implement Receiver Stop Ready operation This change implements the client-side "Receiver Stop Ready" operation as defined in ASCS v1.0.1, Section 5.6. - Add the `receiver_stop_ready` method to `AudioStreamEndpointHandle`. - Update `AseControlOperation` to handle encoding and state transitions. - Add unit tests for success and various failure scenarios. Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: I5c5482b6271d869a70f04671a122e2f86d91430c Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2481
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 71a4192..20131be 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -328,6 +328,30 @@ Ok(()) } + fn verify_ase_directions<'a>( + &self, + pending_ases: impl IntoIterator<Item = &'a AseId>, + opcode: AseControlPointOpcode, + expected: AudioDirection, + ) -> Result<(), Error> { + for &ase_id in pending_ases { + let direction = self + .endpoints + .lookup_by_ase_id(ase_id) + .ok_or(Error::Client(ClientError::UnknownAseId(ase_id)))? + .endpoint + .direction; + if direction != expected { + return Err(Error::Client(ClientError::InvalidDirection { + ase_id, + opcode, + direction, + })); + } + } + Ok(()) + } + /// Encodes an ASE control operation and writes it to the ASE Control Point /// characteristic. async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> { @@ -537,20 +561,11 @@ ) -> Result<AseControlOperationOutcome, Error> { let pending_ases: HashSet<AseId> = ases.iter().cloned().collect(); self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &pending_ases)?; - - for ase_id in &pending_ases { - let endpoint = self - .endpoints - .lookup_by_ase_id(*ase_id) - .expect("ASE ID verified by validate_arguments"); - if endpoint.endpoint.direction != AudioDirection::Source { - return Err(Error::Client(ClientError::InvalidDirection { - ase_id: *ase_id, - opcode: AseControlPointOpcode::ReceiverStartReady, - direction: endpoint.endpoint.direction, - })); - } - } + self.verify_ase_directions( + &pending_ases, + AseControlPointOpcode::ReceiverStartReady, + AudioDirection::Source, + )?; let op = AseControlOperation::ReceiverStartReady { ases }; @@ -573,6 +588,31 @@ self.perform_and_verify_operation(op, pending_ases).await } + + /// Performs the Receiver Stop Ready operation on one or more Source ASEs. + /// + /// # Arguments + /// * `ases` - A vector of `AseId`s to stop. + /// + /// # Returns + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. + pub async fn receiver_stop_ready( + &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.verify_ase_directions( + &pending_ases, + AseControlPointOpcode::ReceiverStopReady, + AudioDirection::Source, + )?; + + let op = AseControlOperation::ReceiverStopReady { ases }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] @@ -1680,4 +1720,139 @@ }) )); } + + #[test] + fn receiver_stop_ready_success() { + 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 Disabling state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x05, // ASE State: Disabling + 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![ + 0x06, // Opcode: Receiver Stop Ready + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x06, // Opcode: Receiver Stop Ready + 0x01, // Num ASEs + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + // Expected state transitions back to QoS Configured + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 0x02, // ASE State: QoS Configured + 0x01, 0x01, 0x10, 0x27, 0x00, 0x00, 0x01, 0x64, 0x00, 0x02, 0x0A, 0x00, 0x40, 0x9C, 0x00, + ], + maybe_truncated: false, + }), + ); + + let stop_fut = client.receiver_stop_ready(vec![AseId(2)]); + let outcome = run_to_completion(stop_fut).expect("receiver stop ready should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!( + client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, + AseState::QosConfigured + ); + } + + #[test] + fn receiver_stop_ready_fail_invalid_direction() { + 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"); + + // Pre-condition SINK_ASE_HANDLE (ASE 1) to Disabling (or other state) + let sink_value = vec![ + 0x01, // ASE ID: 1 + 0x05, // ASE State: Disabling + 0x01, 0x01, 0x00, + ]; + client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SINK_ASE_HANDLE, + AudioDirection::Sink, + &sink_value, + ) + .unwrap(); + + let stop_fut = client.receiver_stop_ready(vec![AseId(1)]); + let err = run_to_completion(stop_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidDirection { + ase_id: AseId(1), + opcode: AseControlPointOpcode::ReceiverStopReady, + direction: AudioDirection::Sink, + }) + )); + } + + #[test] + fn receiver_stop_ready_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 + // ReceiverStopReady + let stop_fut = client.receiver_stop_ready(vec![AseId(2)]); + let err = run_to_completion(stop_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(2), + opcode: AseControlPointOpcode::ReceiverStopReady, + actual: AseState::Idle, + }) + )); + } + + #[test] + fn receiver_stop_ready_fail_unknown_ase_id() { + let service = setup_fake_service(); + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service); + let mut client = run_to_completion(client_fut).expect("client creation should succeed"); + + let stop_fut = client.receiver_stop_ready(vec![AseId(99)]); + let err = run_to_completion(stop_fut).expect_err("should fail for unknown ASE ID"); + + assert!(matches!(err, Error::Client(ClientError::UnknownAseId(AseId(99))))); + } }