rust/bt-ascs: Implement Enable operation This change implements the "Enable" operation for the ASCS client. The implementation includes: - A new `enable` method on the `AudioStreamEndpointHandle`. - Unit tests for success and failure scenarios. - A new unit test for the `Enable` decoding logic. Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: I19f47179aaffaf529c63791f338a8d4a1e6bcb5e Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2420
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 90a64c4..6b7fac7 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -542,6 +542,32 @@ self.perform_and_verify_operation(op, pending_ases).await } + + /// Performs the Enable operation on one or more ASEs. + /// + /// This method sends the Enable request to the remote server, + /// waits for control point notifications for all involved ASEs, and + /// verifies that the successfully enabled ASEs have transitioned to + /// the `Enabling` state. + /// + /// # Arguments + /// * `ases_with_metadata` - A vector of `AseIdWithMetadata` structs + /// specifying the target ASE IDs and their codec metadata. + /// + /// # Returns + /// On success, returns a `Vec<ResponseCode>` containing the response + /// codes for any ASEs where the enable operation failed. + pub async fn enable( + &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)?; + + let op = AseControlOperation::Enable { ases_with_metadata, responses: vec![] }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] @@ -1244,4 +1270,112 @@ ); assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Streaming); } + + #[test] + fn enable_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 to QosConfigured state + let sink_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, + ]; + client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SINK_ASE_HANDLE, + AudioDirection::Sink, + &sink_value, + ) + .unwrap(); + + let enable_req = AseIdWithMetadata { ase_id: AseId(1), metadata: vec![] }; + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x03, // Opcode: Enable + 0x01, // Num ASEs + 0x01, // ASE ID: 1 + 0x00, // Metadata Length: 0 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x03, // Opcode: Enable + 0x01, // Num ASEs + 0x01, 0x00, 0x00, // ASE ID: 1, Success + ], + maybe_truncated: false, + }), + ); + + #[rustfmt::skip] + service.notify( + &SINK_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SINK_ASE_HANDLE, + value: vec![ + 0x01, // ASE ID: 1 + 0x03, // ASE State: Enabling + 0x01, // CIG ID: 1 + 0x01, // CIS ID: 1 + 0x00, // Metadata Length: 0 + ], + maybe_truncated: false, + }), + ); + + let enable_fut = client.enable(vec![enable_req]); + let outcome = run_to_completion(enable_fut).expect("enable should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!(client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state, AseState::Enabling); + } + + #[test] + fn enable_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"); + + // Transition the sink ASE to Streaming state to test client-side validation. + let streaming_value = vec![ + 0x01, // ASE ID: 1 + 0x04, // ASE State: Streaming + 0x01, // CIG ID: 1 + 0x01, // CIS ID: 1 + 0x00, // Metadata Length: 0 + ]; + client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SINK_ASE_HANDLE, + AudioDirection::Sink, + &streaming_value, + ) + .unwrap(); + + let enable_req = AseIdWithMetadata { ase_id: AseId(1), metadata: vec![] }; + + let enable_fut = client.enable(vec![enable_req]); + let err = run_to_completion(enable_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(1), + opcode: AseControlPointOpcode::Enable, + actual: AseState::Streaming, + }) + )); + } }