rust/bt-ascs: Implement Configure Codec operation Implement config codec operation for ASCS client. - Create `AudioStreamEndpointHandle` struct so clients of the library can perform a ASE control operation on a set of audio stream endpoints - To reduce work on clients, ensure the `configure_codec` method returns the result from the server after the control operation is requested Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: I961354cd3d0ba69f2fe099a88039009301c743dc Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2320
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 8f51ce5..691f15a 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -3,8 +3,9 @@ // found in the LICENSE file. use futures::stream::{BoxStream, StreamExt}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use bt_common::packet_encoding::Decodable; use bt_common::Uuid; use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic}; use bt_gatt::types::Handle; @@ -25,10 +26,20 @@ ExtraControlPointCharacteristic, #[error("Remote server doesn't have any audio stream endpoints")] MissingAudioStreamEndpoints, + #[error("Notification stream closed before receiving all responses")] + NotificationStreamClosed, + #[error( + "ASE {ase_id:?} transitioned to unexpected state: expected {expected:?}, got {actual:?}" + )] + UnexpectedStateTransition { ase_id: AseId, expected: AseState, actual: AseState }, + #[error("Unknown ASE ID: {0:?}")] + UnknownAseId(AseId), + #[error("ASE {ase_id:?} is in invalid starting state {actual:?} for opcode {opcode:?}")] + InvalidStartState { ase_id: AseId, opcode: AseControlPointOpcode, actual: AseState }, } /// Represents a single source/sink ASE state characteristic. -pub struct AudioStreamEndpointHandle { +pub struct ClientEndpoint { pub endpoint: AudioStreamEndpoint, pub notification_stream: BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>, @@ -45,12 +56,63 @@ pub struct AudioStreamControlServiceClient<T: bt_gatt::GattTypes> { pub gatt_client: T::PeerService, pub control_point: AseControlPoint, - pub sink_endpoints: HashMap<Handle, AudioStreamEndpointHandle>, - pub source_endpoints: HashMap<Handle, AudioStreamEndpointHandle>, + pub endpoints: DiscoveredEndpoints, } -pub type SinkAndSourceEndpoints = - (HashMap<Handle, AudioStreamEndpointHandle>, HashMap<Handle, AudioStreamEndpointHandle>); +pub struct DiscoveredEndpoints { + pub sink: HashMap<Handle, ClientEndpoint>, + pub source: HashMap<Handle, ClientEndpoint>, +} + +impl DiscoveredEndpoints { + /// Returns the ASE IDs of all discovered sink Audio Stream Endpoints. + pub fn sink_ases(&self) -> Vec<AseId> { + self.sink.values().map(|e| e.endpoint.ase_id).collect() + } + + /// Returns the ASE IDs of all discovered source Audio Stream Endpoints. + pub fn source_ases(&self) -> Vec<AseId> { + self.source.values().map(|e| e.endpoint.ase_id).collect() + } + + pub(crate) fn lookup_by_ase_id(&self, ase_id: AseId) -> Option<&ClientEndpoint> { + if let Some(e) = self.sink.values().find(|e| e.endpoint.ase_id == ase_id) { + return Some(e); + } + if let Some(e) = self.source.values().find(|e| e.endpoint.ase_id == ase_id) { + return Some(e); + } + None + } +} + +/// The outcome of an ASE control point operation, partitioning successfully +/// transitioned endpoints and failed response codes. +#[derive(Debug, Clone, PartialEq)] +pub struct AseControlOperationOutcome { + accepted: HashMap<AseId, AudioStreamEndpoint>, + rejected: HashMap<AseId, ResponseCode>, +} + +impl AseControlOperationOutcome { + pub(crate) fn new( + accepted: HashMap<AseId, AudioStreamEndpoint>, + rejected: HashMap<AseId, ResponseCode>, + ) -> Self { + Self { accepted, rejected } + } + + /// Returns the map of successfully transitioned [`AudioStreamEndpoint`]s, + /// keyed by their [`AseId`]. + pub fn accepted(&self) -> &HashMap<AseId, AudioStreamEndpoint> { + &self.accepted + } + + /// Returns the map of rejected [`ResponseCode`]s, keyed by their [`AseId`]. + pub fn rejected(&self) -> &HashMap<AseId, ResponseCode> { + &self.rejected + } +} impl<T: bt_gatt::GattTypes> AudioStreamControlServiceClient<T> { pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error> @@ -58,9 +120,9 @@ <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, { let control_point = Self::discover_control_point(&gatt_client).await?; - let (sink_endpoints, source_endpoints) = Self::discover_all_endpoints(&gatt_client).await?; + let endpoints = Self::discover_all_endpoints(&gatt_client).await?; - Ok(Self { gatt_client, control_point, sink_endpoints, source_endpoints }) + Ok(Self { gatt_client, control_point, endpoints }) } async fn discover_control_point(gatt_client: &T::PeerService) -> Result<AseControlPoint, Error> @@ -83,7 +145,7 @@ async fn discover_all_endpoints( gatt_client: &T::PeerService, - ) -> Result<SinkAndSourceEndpoints, Error> + ) -> Result<DiscoveredEndpoints, Error> where <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, { @@ -105,8 +167,7 @@ let endpoint = Self::read_and_create_endpoint(gatt_client, handle, AudioDirection::Sink).await?; let notification_stream = gatt_client.subscribe(&handle).boxed(); - sink_endpoints - .insert(handle, AudioStreamEndpointHandle { endpoint, notification_stream }); + sink_endpoints.insert(handle, ClientEndpoint { endpoint, notification_stream }); } let mut source_endpoints = HashMap::new(); @@ -115,11 +176,10 @@ let endpoint = Self::read_and_create_endpoint(gatt_client, handle, AudioDirection::Source).await?; let notification_stream = gatt_client.subscribe(&handle).boxed(); - source_endpoints - .insert(handle, AudioStreamEndpointHandle { endpoint, notification_stream }); + source_endpoints.insert(handle, ClientEndpoint { endpoint, notification_stream }); } - Ok((sink_endpoints, source_endpoints)) + Ok(DiscoveredEndpoints { sink: sink_endpoints, source: source_endpoints }) } async fn read_and_create_endpoint( @@ -139,11 +199,191 @@ })?; 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(()) + } + + async fn read_and_update_endpoint( + &mut self, + handle: &Handle, + direction: AudioDirection, + ) -> Result<AudioStreamEndpoint, Error> { + let mut buf = vec![0; 255]; + let (read_bytes, _truncated) = self + .gatt_client + .read_characteristic(handle, 0, &mut buf[..]) + .await + .map_err(Error::Gatt)?; + let endpoint = + AudioStreamEndpoint::from_char_value(*handle, direction, &buf[0..read_bytes]).map_err( + |e| { + bt_gatt::types::Error::other(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to decode AudioStreamEndpoint: {:?}", e), + )) + }, + )?; + + let endpoints = match direction { + AudioDirection::Sink => &mut self.endpoints.sink, + AudioDirection::Source => &mut self.endpoints.source, + }; + if let Some(endpoint_handle) = endpoints.get_mut(handle) { + endpoint_handle.endpoint = endpoint.clone(); + } + Ok(endpoint) + } + + async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> { + use bt_common::packet_encoding::Encodable; + + let mut buf = vec![0; operation.encoded_len()]; + operation + .encode(&mut buf[..]) + .map_err(|e| Error::Internal(format!("Failed to encode operation: {:?}", e)))?; + + self.gatt_client + .write_characteristic( + &self.control_point.handle, + bt_gatt::types::WriteMode::WithoutResponse, + 0, + &buf[..], + ) + .await + .map_err(Error::Gatt) + } + + async fn collect_operation_responses( + &mut self, + opcode: u8, + mut remaining_ases: HashSet<AseId>, + ) -> Result<HashMap<AseId, ResponseCode>, Error> { + let mut responses = HashMap::new(); + + while !remaining_ases.is_empty() { + let notification = self + .control_point + .notification_stream + .next() + .await + .ok_or(Error::Client(ClientError::NotificationStreamClosed))?; + let notification = notification.map_err(Error::from)?; + + let (cp_notification, _) = ControlPointNotification::decode(¬ification.value[..]); + let cp_notification = cp_notification.map_err(|e| { + bt_gatt::types::Error::other(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to decode ControlPointNotification: {:?}", e), + )) + })?; + if cp_notification.opcode != opcode { + continue; + } + + if cp_notification.number_of_ases == 0xFF { + return Err(Error::Internal(format!( + "Server rejected operation: {:?}", + cp_notification.responses + ))); + } + + for r in cp_notification.responses { + if remaining_ases.remove(&r.ase_id()) { + responses.insert(r.ase_id(), r); + } + } + } + + Ok(responses) + } + + 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 mut successful_endpoints = HashMap::new(); + let mut failed_responses = HashMap::new(); + let opcode = AseControlPointOpcode::try_from(&operation).map_err(|e| { + Error::Internal(format!("Failed to resolve opcode from operation: {:?}", e)) + })?; + + for r in responses.into_values() { + if !r.is_success() { + failed_responses.insert(r.ase_id(), r); + continue; + } + let client_endpoint = self + .endpoints + .lookup_by_ase_id(r.ase_id()) + .ok_or(Error::Client(ClientError::UnknownAseId(r.ase_id())))?; + let handle = client_endpoint.endpoint.handle; + let direction = client_endpoint.endpoint.direction; + + let endpoint = self.read_and_update_endpoint(&handle, direction).await?; + + if !opcode.allows_transition_to(endpoint.direction, &endpoint.state) { + let expected = opcode.operation_target_states(endpoint.direction)[0]; + return Err(Error::Client(ClientError::UnexpectedStateTransition { + ase_id: endpoint.ase_id, + expected, + actual: endpoint.state, + })); + } + successful_endpoints.insert(endpoint.ase_id, endpoint); + } + + Ok(AseControlOperationOutcome::new(successful_endpoints, failed_responses)) + } + + /// 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. + pub async fn configure_codec( + &mut self, + codec_configurations: Vec<CodecConfiguration>, + ) -> Result<AseControlOperationOutcome, Error> { + let pending_ases: HashSet<AseId> = codec_configurations.iter().map(|c| c.ase_id).collect(); + self.verify_ase_ids(pending_ases.iter().cloned())?; + + let op = AseControlOperation::ConfigCodec { codec_configurations, responses: vec![] }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] mod tests { use super::*; + use bt_common::core::CodecId; use bt_gatt::test_utils::{FakePeerService, FakeTypes}; use bt_gatt::types::{AttributePermissions, CharacteristicProperty}; use bt_gatt::Characteristic; @@ -208,18 +448,21 @@ let client = run_to_completion(client_fut).expect("client creation should succeed"); assert_eq!(client.control_point.handle, CONTROL_POINT_HANDLE); - assert_eq!(client.sink_endpoints.len(), 1); - assert_eq!(client.source_endpoints.len(), 1); + assert_eq!(client.endpoints.sink.len(), 1); + assert_eq!(client.endpoints.source.len(), 1); - let sink = &client.sink_endpoints[&SINK_ASE_HANDLE].endpoint; + let sink = &client.endpoints.sink[&SINK_ASE_HANDLE].endpoint; assert_eq!(sink.ase_id, AseId(1)); assert_eq!(sink.state, AseState::Idle); assert_eq!(sink.direction, AudioDirection::Sink); - let source = &client.source_endpoints[&SOURCE_ASE_HANDLE].endpoint; + let source = &client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint; assert_eq!(source.ase_id, AseId(2)); assert_eq!(source.state, AseState::Idle); assert_eq!(source.direction, AudioDirection::Source); + + assert_eq!(client.endpoints.sink_ases(), vec![AseId(1)]); + assert_eq!(client.endpoints.source_ases(), vec![AseId(2)]); } #[test] @@ -301,4 +544,155 @@ }; assert!(matches!(err, Error::Client(ClientError::ExtraControlPointCharacteristic))); } + + #[test] + fn configure_codec() { + 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"); + + let config1 = 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![], + }; + let config2 = 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![], + }; + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x01, // Opcode: Config Codec + 0x02, // Num ASEs + 0x01, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 1 Config + 0x02, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 2 Config + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x01, // Opcode + 0x02, // Num ASEs + 0x01, 0x00, 0x00, // ASE 1 Response: Success + 0x02, 0x03, 0x02, // ASE 2 Response: Failed (Invalid ASE ID) + ], + maybe_truncated: false, + }), + ); + + #[rustfmt::skip] + service.add_characteristic( + Characteristic { + handle: SINK_ASE_HANDLE, + uuid: SINK_ASE_UUID, + properties: CharacteristicProperty::Read | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![ + 0x01, // ASE ID: 1 + 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, + ], + ); + + let configure_fut = client.configure_codec(vec![config1, config2]); + let outcome = run_to_completion(configure_fut).expect("configure codec should succeed"); + + assert_eq!( + client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state, + AseState::CodecConfigured + ); + assert_eq!(outcome.accepted().len(), 1); + let endpoint = &outcome.accepted()[&AseId(1)]; + let qos = endpoint.qos_parameters().unwrap(); + assert_eq!(qos.framing, Framing::Unframed); + assert_eq!(qos.preferred_phys, vec![Phy::Le1MPhy]); + assert_eq!(qos.preferred_retransmission_number, 2); + + assert_eq!(outcome.rejected().len(), 1); + assert_eq!(outcome.rejected()[&AseId(2)], ResponseCode::InvalidAseId { value: 2 }); + } + + #[test] + fn configure_codec_fail_unknown_ase_id() { + 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"); + + let config = CodecConfiguration { + ase_id: AseId(99), // Non-existent ASE ID + target_latency: TargetLatency::TargetLowLatency, + target_phy: TargetPhy::Le1MPhy, + codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3), + codec_specific_configuration: vec![], + }; + + let configure_fut = client.configure_codec(vec![config]); + let err = run_to_completion(configure_fut).expect_err("should fail due to unknown ASE ID"); + assert!(matches!(err, Error::Client(ClientError::UnknownAseId(AseId(99))))); + } + + #[test] + fn configure_codec_global_error() { + 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"); + + let config = 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![], + }; + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x01, // Opcode: Config Codec + 0x01, // Num ASEs + 0x01, 0x01, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // ASE 1 Config + ], + ); + + // Let's mimic server side throwing a global invalid length error. + // (in reality this wouldn't happen because our configure codec operation was + // encoded correctly). + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x01, // Opcode + 0xFF, // Num ASEs: Global Error + 0x00, 0x02, 0x00, // ASE 0, Invalid Length, None + ], + maybe_truncated: false, + }), + ); + + let configure_fut = client.configure_codec(vec![config]); + let err = run_to_completion(configure_fut).expect_err("should fail with global error"); + + assert!(matches!( + err, + Error::Internal(ref s) if s.contains("Server rejected operation") && s.contains("InvalidLength") + )); + } }
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs index 324a2b7..6175baf 100644 --- a/rust/bt-ascs/src/types.rs +++ b/rust/bt-ascs/src/types.rs
@@ -124,6 +124,14 @@ [self.ase_id_value(), self.to_code(), self.reason_byte()].into() } + pub fn ase_id(&self) -> AseId { + AseId(self.ase_id_value()) + } + + pub fn is_success(&self) -> bool { + matches!(self, Self::Success { .. }) + } + pub fn decode_response( buf: &[u8], opcode: u8, @@ -272,6 +280,19 @@ } } +impl Encodable for AseId { + type Error = bt_common::packet_encoding::Error; + fn encoded_len(&self) -> usize { + 1 + } + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.is_empty() { + return Err(Self::Error::BufferTooSmall); + } + buf[0] = self.0; + Ok(()) + } +} #[derive(Debug, Clone, PartialEq)] pub enum AseAdditionalParameters { None, @@ -513,7 +534,7 @@ } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct AudioStreamEndpoint { pub handle: Handle, pub direction: AudioDirection, @@ -572,6 +593,36 @@ _ => None, } } + + pub fn qos_parameters(&self) -> Option<QosParameters> { + let AseAdditionalParameters::CodecConfigured { + framing, + preferred_phys, + preferred_retransmission_number, + max_transport_latency, + presentation_delay_range, + .. + } = &self.additional + else { + return None; + }; + Some(QosParameters { + framing: *framing, + preferred_phys: preferred_phys.clone(), + preferred_retransmission_number: *preferred_retransmission_number, + max_transport_latency: *max_transport_latency, + presentation_delay_range: presentation_delay_range.clone(), + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct QosParameters { + pub framing: Framing, + pub preferred_phys: Vec<Phy>, + pub preferred_retransmission_number: u8, + pub max_transport_latency: MaxTransportLatency, + pub presentation_delay_range: PresentationDelayRange, } impl Encodable for AudioStreamEndpoint { @@ -628,6 +679,37 @@ }; allowed_states.contains(state) } + + /// Returns the list of expected valid states that targeted ASEs should + /// transition into following a successful invocation of this operation, + /// depending on the audio stream endpoint direction. + pub(crate) fn operation_target_states(&self, direction: AudioDirection) -> &[AseState] { + match self { + Self::ConfigCodec { .. } => &[AseState::CodecConfigured], + Self::ConfigQos { .. } => &[AseState::QosConfigured], + Self::Enable { .. } => &[AseState::Enabling, AseState::Streaming], + Self::ReceiverStartReady { .. } => &[AseState::Streaming], + Self::Disable { .. } => match direction { + AudioDirection::Sink => &[AseState::QosConfigured], + AudioDirection::Source => &[AseState::Disabling], + }, + Self::ReceiverStopReady { .. } => &[AseState::QosConfigured], + Self::UpdateMetadata { .. } => &[AseState::Enabling, AseState::Streaming], + Self::Release { .. } => { + &[AseState::Releasing, AseState::Idle, AseState::CodecConfigured] + } + } + } + + /// Returns true if the given `next_state` is a valid transition state + /// for this operation and audio direction according to the specification. + pub(crate) fn allows_transition_to( + &self, + direction: AudioDirection, + next_state: &AseState, + ) -> bool { + self.operation_target_states(direction).contains(next_state) + } } /// ASE Control Operations. These can be initiated by a server or client. @@ -799,6 +881,108 @@ } } +impl Encodable for AseControlOperation { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + match self { + AseControlOperation::ConfigCodec { codec_configurations, .. } => { + 2 + codec_configurations.iter().map(|c| c.encoded_len()).sum::<usize>() + } + AseControlOperation::ConfigQos { qos_configurations, .. } => { + 2 + qos_configurations.len() * QosConfiguration::BYTE_SIZE + } + AseControlOperation::Enable { ases_with_metadata, .. } => { + 2 + ases_with_metadata.iter().map(|a| a.encoded_len()).sum::<usize>() + } + AseControlOperation::ReceiverStartReady { ases } => 2 + ases.len() * AseId::BYTE_SIZE, + AseControlOperation::Disable { ases } => 2 + ases.len() * AseId::BYTE_SIZE, + AseControlOperation::ReceiverStopReady { ases } => 2 + ases.len() * AseId::BYTE_SIZE, + AseControlOperation::UpdateMetadata { ases_with_metadata, .. } => { + 2 + ases_with_metadata.iter().map(|a| a.encoded_len()).sum::<usize>() + } + AseControlOperation::Release { ases } => 2 + ases.len() * AseId::BYTE_SIZE, + AseControlOperation::Released { .. } => 0, + } + } + + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(Self::Error::BufferTooSmall); + } + let opcode: u8 = self.try_into().map_err(|_| Self::Error::OutOfRange)?; + buf[0] = opcode; + match self { + AseControlOperation::ConfigCodec { codec_configurations, .. } => { + buf[1] = codec_configurations.len() as u8; + let mut idx = 2; + for c in codec_configurations { + c.encode(&mut buf[idx..])?; + idx += c.encoded_len(); + } + } + AseControlOperation::ConfigQos { qos_configurations, .. } => { + buf[1] = qos_configurations.len() as u8; + let mut idx = 2; + for q in qos_configurations { + q.encode(&mut buf[idx..])?; + idx += QosConfiguration::BYTE_SIZE; + } + } + AseControlOperation::Enable { ases_with_metadata, .. } => { + buf[1] = ases_with_metadata.len() as u8; + let mut idx = 2; + for a in ases_with_metadata { + a.encode(&mut buf[idx..])?; + idx += a.encoded_len(); + } + } + AseControlOperation::ReceiverStartReady { ases } => { + buf[1] = ases.len() as u8; + let mut idx = 2; + for a in ases { + a.encode(&mut buf[idx..])?; + idx += AseId::BYTE_SIZE; + } + } + AseControlOperation::Disable { ases } => { + buf[1] = ases.len() as u8; + let mut idx = 2; + for a in ases { + a.encode(&mut buf[idx..])?; + idx += AseId::BYTE_SIZE; + } + } + AseControlOperation::ReceiverStopReady { ases } => { + buf[1] = ases.len() as u8; + let mut idx = 2; + for a in ases { + a.encode(&mut buf[idx..])?; + idx += AseId::BYTE_SIZE; + } + } + AseControlOperation::UpdateMetadata { ases_with_metadata, .. } => { + buf[1] = ases_with_metadata.len() as u8; + let mut idx = 2; + for a in ases_with_metadata { + a.encode(&mut buf[idx..])?; + idx += a.encoded_len(); + } + } + AseControlOperation::Release { ases } => { + buf[1] = ases.len() as u8; + let mut idx = 2; + for a in ases { + a.encode(&mut buf[idx..])?; + idx += AseId::BYTE_SIZE; + } + } + AseControlOperation::Released { .. } => return Err(Self::Error::OutOfRange), + } + Ok(()) + } +} + decodable_enum! { pub enum TargetLatency<u8, bt_common::packet_encoding::Error, OutOfRange> { TargetLowLatency = 0x01, @@ -908,6 +1092,28 @@ } } +impl Encodable for CodecConfiguration { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + Self::MIN_BYTE_SIZE + self.codec_specific_configuration.len() + } + + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(Self::Error::BufferTooSmall); + } + buf[0] = self.ase_id.into(); + buf[1] = self.target_latency.into(); + buf[2] = self.target_phy.into(); + self.codec_id.encode(&mut buf[3..8])?; + buf[8] = self.codec_specific_configuration.len() as u8; + buf[9..9 + self.codec_specific_configuration.len()] + .copy_from_slice(&self.codec_specific_configuration); + Ok(()) + } +} + /// Represents Config QoS parameters for a single ASE. See ASCS v1.0.1 Section /// 5.2. #[derive(Debug, Clone, PartialEq)] @@ -1015,6 +1221,31 @@ } } +impl Encodable for QosConfiguration { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + Self::BYTE_SIZE + } + + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.len() < Self::BYTE_SIZE { + return Err(Self::Error::BufferTooSmall); + } + buf[0] = self.ase_id.into(); + self.cig_id.encode(&mut buf[1..2])?; + self.cis_id.encode(&mut buf[2..3])?; + self.sdu_interval.encode(&mut buf[3..6])?; + self.framing.encode(&mut buf[6..7])?; + buf[7] = Phy::to_bits(self.phy.iter()); + self.max_sdu.encode(&mut buf[8..10])?; + buf[10] = self.retransmission_number; + self.max_transport_latency.encode(&mut buf[11..13])?; + self.presentation_delay.encode(&mut buf[13..16])?; + Ok(()) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct CigId(u8); @@ -1447,6 +1678,28 @@ } } +impl Encodable for AseIdWithMetadata { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + let metadata_len: usize = self.metadata.iter().map(|m| m.encoded_len()).sum(); + 2 + metadata_len + } + + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(Self::Error::BufferTooSmall); + } + buf[0] = self.ase_id.into(); + let metadata_len: usize = self.metadata.iter().map(|m| m.encoded_len()).sum(); + buf[1] = metadata_len as u8; + + use bt_common::core::ltv::LtValue; + LtValue::encode_all(self.metadata.clone().into_iter(), &mut buf[2..])?; + Ok(()) + } +} + impl Decodable for PresentationDelayRange { type Error = ResponseCode; @@ -1617,6 +1870,38 @@ use bt_common::generic_audio::{codec_configuration, AudioLocation}; #[test] + fn opcode_state_rules() { + // 1. allowed_in_state tests + assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Idle)); + assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::CodecConfigured)); + assert!(!AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Streaming)); + + assert!(AseControlPointOpcode::Disable.allowed_in_state(&AseState::Streaming)); + assert!(!AseControlPointOpcode::Disable.allowed_in_state(&AseState::Idle)); + + // 2. operation_target_states & allows_transition_to tests + // Sink direction + assert!(AseControlPointOpcode::Disable + .allows_transition_to(AudioDirection::Sink, &AseState::QosConfigured)); + assert!(!AseControlPointOpcode::Disable + .allows_transition_to(AudioDirection::Sink, &AseState::Disabling)); + + // Source direction + assert!(AseControlPointOpcode::Disable + .allows_transition_to(AudioDirection::Source, &AseState::Disabling)); + assert!(!AseControlPointOpcode::Disable + .allows_transition_to(AudioDirection::Source, &AseState::QosConfigured)); + + // UpdateMetadata (same state allowed) + assert!(AseControlPointOpcode::UpdateMetadata + .allows_transition_to(AudioDirection::Source, &AseState::Enabling)); + assert!(AseControlPointOpcode::UpdateMetadata + .allows_transition_to(AudioDirection::Source, &AseState::Streaming)); + assert!(!AseControlPointOpcode::UpdateMetadata + .allows_transition_to(AudioDirection::Source, &AseState::Idle)); + } + + #[test] fn codec_configuration_roundtrip() { let codec_specific_configuration = vec![ codec_configuration::CodecConfiguration::SamplingFrequency(