rust/bt-ascs: Implement Receiver Start Ready operation This change implements the client-side "Receiver Start Ready" operation as defined in ASCS v1.0.1, Section 5.4. This also includes a major refactoring of the ASE control operations in the `AudioStreamEndpointHandle` to use a common `perform_operation` helper method. This reduces code duplication and improves maintainability. - The `configure_codec`, `configure_qos`, and `enable` operations were updated to use this new helper. - The `AseControlOperation` enum was improved by renaming `is_valid_state` to `is_valid_operation` and `next_state` to `expected_next_state` to better reflect their purpose. Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: I916d9553eb529d6940635bcc14098e948f9a0cad Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2460
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 6b7fac7..b20abb4 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -38,6 +38,8 @@ InvalidStartState { ase_id: AseId, opcode: AseControlPointOpcode, actual: AseState }, #[error("Remote server rejected operation: {0:?}")] RejectedOperation(Vec<ResponseCode>), + #[error("ASE {ase_id:?} has invalid direction {direction:?} for opcode {opcode:?}")] + InvalidDirection { ase_id: AseId, opcode: AseControlPointOpcode, direction: AudioDirection }, } /// Represents a single source/sink ASE state characteristic. @@ -276,6 +278,8 @@ Ok(DiscoveredEndpoints { sink: sink_endpoints, source: source_endpoints }) } + /// Reads the current value of an ASE characteristic from the remote server + /// and decodes it into an [`AudioStreamEndpoint`]. async fn read_and_create_endpoint( gatt_client: &T::PeerService, handle: Handle, @@ -294,35 +298,28 @@ 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(()) - } - - fn validate_arguments( + /// Validates that the target ASE IDs are known to this client and that + /// the requested `opcode` is allowed in their current state according + /// to the ASCS state machine. + /// + /// Returns `Ok(())` if validation passes, or a `ClientError` (such as + /// `UnknownAseId` or `InvalidStartState`) if any target ASE fails + /// validation. + fn validate_arguments<'a>( &mut self, opcode: AseControlPointOpcode, - pending_ases: &HashSet<AseId>, + pending_ases: impl IntoIterator<Item = &'a AseId>, ) -> Result<(), Error> { - self.verify_ase_ids(pending_ases.iter().cloned())?; - - for ase_id in pending_ases { + for &ase_id in pending_ases { let endpoint = &self .endpoints - .lookup_by_ase_id(*ase_id) - .ok_or(Error::Client(ClientError::UnknownAseId(*ase_id)))? + .lookup_by_ase_id(ase_id) + .ok_or(Error::Client(ClientError::UnknownAseId(ase_id)))? .endpoint; if !opcode.allowed_in_state(&endpoint.state) { return Err(Error::Client(ClientError::InvalidStartState { - ase_id: *ase_id, + ase_id, opcode, actual: endpoint.state, })); @@ -331,19 +328,8 @@ Ok(()) } - /// Reads the current value of an ASE characteristic from the remote server, - /// decodes it as an [`AudioStreamEndpoint`], updates the local cache of - /// sink or source endpoints, and returns the updated endpoint. - /// - /// # Arguments - /// * `handle` - The GATT characteristic handle of the ASE to read. - /// * `direction` - The direction (Sink or Source) of the ASE. - /// - /// # Returns - /// * `Ok(AudioStreamEndpoint)` - The newly read and updated endpoint - /// struct. - /// * `Err(Error)` - If reading from the GATT client fails, or if the read - /// value cannot be decoded. + /// Encodes an ASE control operation and writes it to the ASE Control Point + /// characteristic. async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> { use bt_common::packet_encoding::Encodable; @@ -468,21 +454,13 @@ /// 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. + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. pub async fn configure_codec( &mut self, codec_configurations: Vec<CodecConfiguration>, @@ -497,32 +475,13 @@ /// Performs the Configure QoS operation on one or more ASEs. /// - /// This method sends the Configure QoS 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 `QosConfigured` state. - /// - /// # Arguments - /// * `qos_configurations` - A vector of `QosConfiguration` structs - /// containing the QoS parameters for each ASE. - /// - /// # Returns - /// On success, returns a `Vec<ResponseCode>` containing the response - /// codes for any ASEs where the configuration failed. - /// Performs the Configure QoS operation on one or more ASEs. - /// - /// This method sends the Configure QoS 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 `QosConfigured` state. - /// /// # Arguments /// * `requests` - A vector of `QosConfigurationRequest` enums specifying /// either custom or server-preferred configurations for each ASE. /// /// # Returns - /// On success, returns a `Vec<ResponseCode>` containing the response - /// codes for any ASEs where the configuration failed. + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. pub async fn configure_qos( &mut self, requests: Vec<QosConfigurationRequest>, @@ -545,18 +504,13 @@ /// 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. + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. pub async fn enable( &mut self, ases_with_metadata: Vec<AseIdWithMetadata>, @@ -568,6 +522,40 @@ self.perform_and_verify_operation(op, pending_ases).await } + + /// Performs the Receiver Start Ready operation on one or more Source ASEs. + /// + /// # Arguments + /// * `ases` - A vector of `AseId`s to start. + /// + /// # Returns + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. + pub async fn receiver_start_ready( + &mut self, + ases: Vec<AseId>, + ) -> 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, + })); + } + } + + let op = AseControlOperation::ReceiverStartReady { ases }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] @@ -1378,4 +1366,182 @@ }) )); } + + #[test] + fn receiver_start_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 to Enabling state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x03, // ASE State: Enabling + 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![ + 0x04, // Opcode: Receiver Start Ready + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x04, // Opcode: Receiver Start Ready + 0x01, // Num ASEs + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 0x04, // ASE State: Streaming + 0x01, // CIG ID: 1 + 0x01, // CIS ID: 1 + 0x00, // Metadata Length: 0 + ], + maybe_truncated: false, + }), + ); + + let start_fut = client.receiver_start_ready(vec![AseId(2)]); + let outcome = run_to_completion(start_fut).expect("receiver start ready should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Streaming); + } + + #[test] + fn receiver_start_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"); + + // ASE 2 (Source) is in Idle state, which is invalid for ReceiverStartReady + let start_fut = client.receiver_start_ready(vec![AseId(2)]); + let err = run_to_completion(start_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(2), + opcode: AseControlPointOpcode::ReceiverStartReady, + actual: AseState::Idle, + }) + )); + } + + #[test] + fn receiver_start_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 Enabling state + let sink_value = vec![ + 0x01, // ASE ID: 1 + 0x03, // ASE State: Enabling + 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 start_fut = client.receiver_start_ready(vec![AseId(1)]); + let err = run_to_completion(start_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidDirection { + ase_id: AseId(1), + opcode: AseControlPointOpcode::ReceiverStartReady, + direction: AudioDirection::Sink, + }) + )); + } + + #[test] + fn receiver_start_ready_rejected() { + 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 to Enabling state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x03, // ASE State: Enabling + 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![ + 0x04, // Opcode: Receiver Start Ready + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + ], + ); + + // Notify rejection from server due to Insufficient Resources (Response Code + // 0x0D) + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x04, // Opcode: Receiver Start Ready + 0x01, // Num ASEs + 0x02, 0x0D, 0x00, // ASE ID: 2, Insufficient Resources + ], + maybe_truncated: false, + }), + ); + + let start_fut = client.receiver_start_ready(vec![AseId(2)]); + let outcome = run_to_completion(start_fut).expect("receiver start ready should succeed"); + + assert_eq!(outcome.rejected().len(), 1); + assert_eq!( + outcome.rejected()[&AseId(2)], + ResponseCode::InsufficientResources { ase_id: AseId(2) } + ); + // The state should remain Enabling + assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Enabling); + } }