rust/bt-ascs: Implement Disable operation This change implements the client-side "Disable" operation as defined in ASCS v1.0.1, Section 5.5. - The `disable` method was added to the `AudioStreamEndpointHandle`. - The implementation follows the established pattern of using the `perform_operation` helper method. - Unit tests have been added to verify the new functionality. Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: Ibbb39c2a8caa39cdd3805fd90983e1965eb46708 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2480
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index b20abb4..71a4192 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -556,6 +556,23 @@ self.perform_and_verify_operation(op, pending_ases).await } + + /// Performs the Disable operation on one or more ASEs. + /// + /// # Arguments + /// * `ases` - A vector of `AseId`s to disable. + /// + /// # Returns + /// 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)?; + + let op = AseControlOperation::Disable { ases }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] @@ -1544,4 +1561,123 @@ // The state should remain Enabling assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Enabling); } + + #[test] + fn disable_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 SINK_ASE_HANDLE (ASE 1) to Streaming state + let sink_value = vec![ + 0x01, // ASE ID: 1 + 0x04, // ASE State: Streaming + 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(); + + // 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![ + 0x05, // Opcode: Disable + 0x02, // Num ASEs + 0x01, // ASE ID: 1 + 0x02, // ASE ID: 2 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x05, // Opcode: Disable + 0x02, // Num ASEs + 0x01, 0x00, 0x00, // ASE ID: 1, Success + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + // Expected state updates: + // Sink ASE transitions back to QoS Configured (no Disabling state) + #[rustfmt::skip] + service.notify( + &SINK_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SINK_ASE_HANDLE, + value: vec![ + 0x01, // ASE ID: 1 + 0x02, // ASE State: QoS Configured + 0x01, 0x01, 0x10, 0x27, 0x00, 0x00, 0x01, 0x64, 0x00, 0x02, 0x0A, 0x00, 0x40, 0x9C, 0x00, + ], + maybe_truncated: false, + }), + ); + + // Source ASE transitions to Disabling + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 0x05, // ASE State: Disabling + 0x01, 0x01, 0x00, + ], + maybe_truncated: false, + }), + ); + + let disable_fut = client.disable(vec![AseId(1), AseId(2)]); + let outcome = run_to_completion(disable_fut).expect("disable should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!(client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state, AseState::QosConfigured); + assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Disabling); + } + + #[test] + fn disable_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"); + + // ASE 1 is in Idle state, which is an invalid starting state for Disable + let disable_fut = client.disable(vec![AseId(1)]); + let err = run_to_completion(disable_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(1), + opcode: AseControlPointOpcode::Disable, + actual: AseState::Idle, + }) + )); + } }