rust/bt-ascs: Refactor common types and implement basic client Refactor common types that are used for both ASCS client and server. Also do a basic implementation of ASCS client. Test: cargo test Change-Id: I1e71f43ec66ea1727f99a589d38a92e8b9bf763c Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2269
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs new file mode 100644 index 0000000..8f51ce5 --- /dev/null +++ b/rust/bt-ascs/src/client.rs
@@ -0,0 +1,304 @@ +// Copyright 2026 The Fuchsia Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use futures::stream::{BoxStream, StreamExt}; +use std::collections::HashMap; + +use bt_common::Uuid; +use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic}; +use bt_gatt::types::Handle; + +use crate::types::*; + +/// 16-bit UUID value for the characteristics offered by the Audio +/// Stream Control Service. +pub const ASE_CONTROL_POINT_UUID: Uuid = Uuid::from_u16(0x2BC6); +pub const SINK_ASE_UUID: Uuid = Uuid::from_u16(0x2BC4); +pub const SOURCE_ASE_UUID: Uuid = Uuid::from_u16(0x2BC5); + +#[derive(Debug, thiserror::Error, PartialEq, Clone)] +pub enum ClientError { + #[error("Remote server is missing control point characteristic")] + MissingControlPointCharacteristic, + #[error("Remote server should only have 1 control point characteristic")] + ExtraControlPointCharacteristic, + #[error("Remote server doesn't have any audio stream endpoints")] + MissingAudioStreamEndpoints, +} + +/// Represents a single source/sink ASE state characteristic. +pub struct AudioStreamEndpointHandle { + pub endpoint: AudioStreamEndpoint, + pub notification_stream: + BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>, +} + +// See ASCS v1.0.1 4.2 for details. +pub struct AseControlPoint { + pub handle: Handle, + pub notification_stream: + BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>, +} + +/// Creates Audio Stream Control Service (ASCS) client instance +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 type SinkAndSourceEndpoints = + (HashMap<Handle, AudioStreamEndpointHandle>, HashMap<Handle, AudioStreamEndpointHandle>); + +impl<T: bt_gatt::GattTypes> AudioStreamControlServiceClient<T> { + pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error> + where + <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?; + + Ok(Self { gatt_client, control_point, sink_endpoints, source_endpoints }) + } + + async fn discover_control_point(gatt_client: &T::PeerService) -> Result<AseControlPoint, Error> + where + <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, + { + let cp_chars = ServiceCharacteristic::<T>::find(gatt_client, ASE_CONTROL_POINT_UUID) + .await + .map_err(Error::Gatt)?; + if cp_chars.is_empty() { + return Err(ClientError::MissingControlPointCharacteristic.into()); + } + if cp_chars.len() > 1 { + return Err(ClientError::ExtraControlPointCharacteristic.into()); + } + let cp_handle = *cp_chars[0].handle(); + let cp_stream = gatt_client.subscribe(&cp_handle); + Ok(AseControlPoint { handle: cp_handle, notification_stream: cp_stream.boxed() }) + } + + async fn discover_all_endpoints( + gatt_client: &T::PeerService, + ) -> Result<SinkAndSourceEndpoints, Error> + where + <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, + { + let sink_chars = ServiceCharacteristic::<T>::find(gatt_client, SINK_ASE_UUID) + .await + .map_err(Error::Gatt)?; + + let source_chars = ServiceCharacteristic::<T>::find(gatt_client, SOURCE_ASE_UUID) + .await + .map_err(Error::Gatt)?; + + if sink_chars.is_empty() && source_chars.is_empty() { + return Err(ClientError::MissingAudioStreamEndpoints.into()); + } + + let mut sink_endpoints = HashMap::new(); + for c in sink_chars { + let handle = *c.handle(); + 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 }); + } + + let mut source_endpoints = HashMap::new(); + for c in source_chars { + let handle = *c.handle(); + 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 }); + } + + Ok((sink_endpoints, source_endpoints)) + } + + async fn read_and_create_endpoint( + gatt_client: &T::PeerService, + handle: Handle, + direction: AudioDirection, + ) -> Result<AudioStreamEndpoint, Error> { + let mut buf = vec![0; 255]; + let (read_bytes, _truncated) = + 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), + )) + })?; + Ok(endpoint) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bt_gatt::test_utils::{FakePeerService, FakeTypes}; + use bt_gatt::types::{AttributePermissions, CharacteristicProperty}; + use bt_gatt::Characteristic; + + const CONTROL_POINT_HANDLE: Handle = Handle(1); + const SINK_ASE_HANDLE: Handle = Handle(2); + const SOURCE_ASE_HANDLE: Handle = Handle(3); + + fn setup_fake_service() -> FakePeerService { + let mut service = FakePeerService::new(); + // Add Control Point + service.add_characteristic( + Characteristic { + handle: CONTROL_POINT_HANDLE, + uuid: ASE_CONTROL_POINT_UUID, + properties: CharacteristicProperty::Write + | CharacteristicProperty::WriteWithoutResponse + | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![], + ); + // Add Sink ASE + service.add_characteristic( + Characteristic { + handle: SINK_ASE_HANDLE, + uuid: SINK_ASE_UUID, + properties: CharacteristicProperty::Read | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![0x01, 0x00], // ASE ID 1, State Idle + ); + // Add Source ASE + service.add_characteristic( + Characteristic { + handle: SOURCE_ASE_HANDLE, + uuid: SOURCE_ASE_UUID, + properties: CharacteristicProperty::Read | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![0x02, 0x00], // ASE ID 2, State Idle + ); + service + } + + fn run_to_completion<F: std::future::Future>(fut: F) -> F::Output { + let mut fut = std::pin::pin!(fut); + let mut cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); + match fut.as_mut().poll(&mut cx) { + std::task::Poll::Ready(res) => res, + std::task::Poll::Pending => panic!("Future did not complete synchronously"), + } + } + + #[test] + fn client_creation_success() { + let service = setup_fake_service(); + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone()); + 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); + + let sink = &client.sink_endpoints[&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; + assert_eq!(source.ase_id, AseId(2)); + assert_eq!(source.state, AseState::Idle); + assert_eq!(source.direction, AudioDirection::Source); + } + + #[test] + fn client_creation_missing_control_point() { + let mut service = FakePeerService::new(); + // Only add Sink (no Control Point) + service.add_characteristic( + Characteristic { + handle: SINK_ASE_HANDLE, + uuid: SINK_ASE_UUID, + properties: CharacteristicProperty::Read | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![0x01, 0x00], + ); + + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service); + let err = match run_to_completion(client_fut) { + Err(e) => e, + Ok(_) => panic!("expected error, got Ok"), + }; + assert!(matches!(err, Error::Client(ClientError::MissingControlPointCharacteristic))); + } + + #[test] + fn client_creation_missing_endpoints() { + let mut service = FakePeerService::new(); + // Only add Control Point (no endpoints) + service.add_characteristic( + Characteristic { + handle: CONTROL_POINT_HANDLE, + uuid: ASE_CONTROL_POINT_UUID, + properties: CharacteristicProperty::Write | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![], + ); + + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service); + let err = match run_to_completion(client_fut) { + Err(e) => e, + Ok(_) => panic!("expected error, got Ok"), + }; + assert!(matches!(err, Error::Client(ClientError::MissingAudioStreamEndpoints))); + } + + #[test] + fn client_creation_extra_control_point() { + let mut service = FakePeerService::new(); + // Add first Control Point + service.add_characteristic( + Characteristic { + handle: CONTROL_POINT_HANDLE, + uuid: ASE_CONTROL_POINT_UUID, + properties: CharacteristicProperty::Write | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![], + ); + // Add second Control Point + service.add_characteristic( + Characteristic { + handle: Handle(4), + uuid: ASE_CONTROL_POINT_UUID, + properties: CharacteristicProperty::Write | CharacteristicProperty::Notify, + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + vec![], + ); + + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service); + let err = match run_to_completion(client_fut) { + Err(e) => e, + Ok(_) => panic!("expected error, got Ok"), + }; + assert!(matches!(err, Error::Client(ClientError::ExtraControlPointCharacteristic))); + } +}
diff --git a/rust/bt-ascs/src/lib.rs b/rust/bt-ascs/src/lib.rs index f3f7380..8a6a6b4 100644 --- a/rust/bt-ascs/src/lib.rs +++ b/rust/bt-ascs/src/lib.rs
@@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +pub mod client; pub mod server; pub mod types; pub use types::Error;
diff --git a/rust/bt-ascs/src/server.rs b/rust/bt-ascs/src/server.rs index 9a329fa..3ee9823 100644 --- a/rust/bt-ascs/src/server.rs +++ b/rust/bt-ascs/src/server.rs
@@ -2,10 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -use bt_common::core::ltv::LtValue; use bt_common::core::CodecId; use bt_common::generic_audio::metadata_ltv::Metadata; -use bt_common::packet_encoding::Encodable; use bt_common::{PeerId, Uuid}; use bt_gatt::server::{LocalService, Server, ServiceDefinition, ServiceId}; use bt_gatt::server::{ReadResponder, WriteResponder}; @@ -23,6 +21,18 @@ use crate::types::*; +#[derive(Debug, thiserror::Error)] +pub enum ServerError { + #[error("Server Only Operation")] + ServerOnlyOperation, + #[error("Service is already published")] + AlreadyPublished, + #[error("Issue publishing service: {0}")] + PublishError(#[from] bt_gatt::types::Error), + #[error("Unknown Peer: {0}")] + UnknownPeer(bt_common::PeerId), +} + #[pin_project(project = LocalServiceProj)] enum LocalServiceState<T: bt_gatt::ServerTypes> { NotPublished { @@ -87,9 +97,9 @@ let service_result = futures::ready!(fut.poll(cx)); let Ok(service) = service_result else { self.as_mut().set(LocalServiceState::Terminated); - return Poll::Ready(Some(Err(Error::PublishError( + return Poll::Ready(Some(Err(Error::from(ServerError::PublishError( service_result.err().unwrap(), - )))); + ))))); }; let events = service.publish(); self.as_mut().set(LocalServiceState::Published { service, events }); @@ -99,15 +109,15 @@ let item = futures::ready!(events.poll_next(cx)); let Some(gatt_result) = item else { self.as_mut().set(LocalServiceState::Terminated); - return Poll::Ready(Some(Err(Error::PublishError( + return Poll::Ready(Some(Err(Error::from(ServerError::PublishError( "GATT server terminated".into(), - )))); + ))))); }; let Ok(event) = gatt_result else { self.as_mut().set(LocalServiceState::Terminated); - return Poll::Ready(Some(Err(Error::PublishError( + return Poll::Ready(Some(Err(Error::from(ServerError::PublishError( gatt_result.err().unwrap(), - )))); + ))))); }; return Poll::Ready(Some(Ok(event))); } @@ -144,24 +154,23 @@ if self.opcode.is_none() || self.response_codes.is_empty() { return None; } - let mut notification = Vec::with_capacity(2 + self.response_codes.len() * 3); - // Opcode - notification.push(self.opcode.unwrap().into()); - if let ResponseCode::InvalidLength { .. } | ResponseCode::UnsupportedOpcode { .. } = - self.response_codes[0] + use bt_common::packet_encoding::Encodable; + + let opcode = self.opcode.unwrap().into(); + let notification = if let ResponseCode::InvalidLength { .. } + | ResponseCode::UnsupportedOpcode { .. } = self.response_codes[0] { - // UnsupportedOpcode or InvalidLength. Number_of_ASEs shall be set to 0xFF - // See ASCS v1.0.1 Table 4.7. We only include the first response_code. - notification.push(0xFF); - notification.extend(self.response_codes[0].notify_value()); - return Some(notification); - } - // Number_of_ASEs - notification.push(self.response_codes.len() as u8); - for response in &self.response_codes { - notification.extend(response.notify_value()); - } - Some(notification) + ControlPointNotification::new_error(opcode, self.response_codes[0].to_code()) + } else { + let mut cp_notification = ControlPointNotification::new(opcode); + for response in &self.response_codes { + cp_notification.add_response(response.clone()); + } + cp_notification + }; + let mut buf = vec![0; notification.encoded_len()]; + notification.encode(&mut buf).unwrap(); + Some(buf) } } @@ -288,7 +297,7 @@ pub fn publish(&mut self, server: &T::Server) -> Result<(), Error> { if !self.local_service.is_not_published() { - return Err(Error::AlreadyPublished); + return Err(Error::from(ServerError::AlreadyPublished)); } let LocalServiceState::NotPublished { waker } = std::mem::replace( &mut self.local_service, @@ -306,8 +315,10 @@ ase_id: AseId, cis: (CigId, CisId), ) -> Result<(), Error> { - let endpoints = - self.client_endpoints.get_mut(&peer_id).ok_or(Error::UnknownPeer(peer_id))?; + let endpoints = self + .client_endpoints + .get_mut(&peer_id) + .ok_or(Error::from(ServerError::UnknownPeer(peer_id)))?; endpoints.established_cis(ase_id, cis); for operation in endpoints.autonomous_operations() { self.queue_operation_unpin(peer_id, operation); @@ -321,8 +332,10 @@ ase_id: AseId, cis: (CigId, CisId), ) -> Result<(), Error> { - let endpoints = - self.client_endpoints.get_mut(&peer_id).ok_or(Error::UnknownPeer(peer_id))?; + let endpoints = self + .client_endpoints + .get_mut(&peer_id) + .ok_or(Error::from(ServerError::UnknownPeer(peer_id)))?; endpoints.released_cis(ase_id, cis); for operation in endpoints.autonomous_operations() { self.queue_operation_unpin(peer_id, operation); @@ -383,159 +396,6 @@ } } -#[derive(Debug, Clone, Copy, PartialEq)] -enum AudioDirection { - Sink, - Source, -} - -impl From<&AudioDirection> for bt_common::Uuid { - fn from(value: &AudioDirection) -> Self { - match value { - AudioDirection::Sink => Uuid::from_u16(0x2BC4), - AudioDirection::Source => Uuid::from_u16(0x2BC5), - } - } -} - -#[derive(Debug, Clone)] -enum AseAdditionalParameters { - /// When in states with no additional parameters: Idle, Releasing - None, - CodecConfigured { - framing: Framing, - preferred_phys: Vec<Phy>, - preferred_retransmission_number: u8, - max_transport_latency: MaxTransportLatency, - presentation_delay_range: PresentationDelayRange, - codec_id: CodecId, - codec_config: Vec<u8>, - }, - QosConfigured { - configuration: QosConfiguration, - }, - /// When Enabling, Streaming, or Disabling - Streaming { - cig_id: CigId, - cis_id: CisId, - metadata: Vec<Metadata>, - qos_configured: QosConfiguration, - }, -} - -impl AseAdditionalParameters { - fn char_size(&self) -> usize { - match self { - AseAdditionalParameters::None => 0, - AseAdditionalParameters::CodecConfigured { codec_config, .. } => { - 23 + codec_config.len() - } - AseAdditionalParameters::QosConfigured { .. } => 15, - AseAdditionalParameters::Streaming { metadata, .. } => { - metadata.iter().fold(3, |total, m| total + m.encoded_len() as usize) - } - } - } - fn into_char_value(&self) -> Vec<u8> { - match self { - AseAdditionalParameters::None => Vec::new(), - AseAdditionalParameters::CodecConfigured { - framing, - preferred_phys, - preferred_retransmission_number, - max_transport_latency, - presentation_delay_range, - codec_id, - codec_config, - } => { - let mut value = Vec::with_capacity(self.char_size()); - value.resize(self.char_size() - codec_config.len(), 0); - value[0] = (*framing) as u8; - value[1] = Phy::to_bits(preferred_phys.iter()); - value[2] = *preferred_retransmission_number; - max_transport_latency.encode(&mut value[3..]).unwrap(); - presentation_delay_range.encode(&mut value[5..]).unwrap(); - codec_id.encode(&mut value[17..]).unwrap(); - value[22] = codec_config.len() as u8; - value.extend(codec_config.clone()); - value - } - AseAdditionalParameters::QosConfigured { - configuration: - QosConfiguration { - cig_id, - cis_id, - sdu_interval, - framing, - phy, - max_sdu, - retransmission_number, - max_transport_latency, - presentation_delay, - .. - }, - } => { - let mut value = Vec::with_capacity(self.char_size()); - value.resize(self.char_size(), 0); - cig_id.encode(&mut value[0..]).unwrap(); - cis_id.encode(&mut value[1..]).unwrap(); - sdu_interval.encode(&mut value[2..]).unwrap(); - framing.encode(&mut value[5..]).unwrap(); - value[6] = Phy::to_bits(phy.iter()); - max_sdu.encode(&mut value[7..]).unwrap(); - value[9] = *retransmission_number; - max_transport_latency.encode(&mut value[10..]).unwrap(); - presentation_delay.encode(&mut value[12..]).unwrap(); - value - } - AseAdditionalParameters::Streaming { cig_id, cis_id, metadata, .. } => { - let mut value = Vec::with_capacity(self.char_size()); - value.resize(self.char_size(), 0); - cig_id.encode(&mut value[0..]).unwrap(); - cis_id.encode(&mut value[1..]).unwrap(); - value[2] = metadata.iter().fold(0usize, |acc, i| acc + i.encoded_len()) as u8; - LtValue::encode_all(metadata.clone().into_iter(), &mut value[3..]).unwrap(); - value - } - } - } -} - -impl From<QosConfiguration> for AseAdditionalParameters { - fn from(value: QosConfiguration) -> Self { - Self::QosConfigured { configuration: value } - } -} - -#[derive(Debug, Clone)] -struct AudioStreamEndpoint { - handle: Handle, - direction: AudioDirection, - ase_id: AseId, - state: AseState, - additional: AseAdditionalParameters, -} - -impl AudioStreamEndpoint { - fn into_char_value(&self) -> Vec<u8> { - let mut value = Vec::with_capacity(2 + self.additional.char_size()); - value.push(self.ase_id.into()); - value.push(self.state.into()); - value.extend(self.additional.into_char_value()); - value - } - - fn get_cis(&self) -> Option<(CigId, CisId)> { - match &self.additional { - AseAdditionalParameters::QosConfigured { configuration } => { - Some((configuration.cig_id, configuration.cis_id)) - } - AseAdditionalParameters::Streaming { cig_id, cis_id, .. } => Some((*cig_id, *cis_id)), - _ => None, - } - } -} - impl From<&AudioStreamEndpoint> for Characteristic { fn from(value: &AudioStreamEndpoint) -> Self { let properties = CharacteristicProperty::Read | CharacteristicProperty::Notify; @@ -712,7 +572,7 @@ cig_id: configuration.cig_id, cis_id: configuration.cis_id, metadata: self.metadata, - qos_configured: configuration, + qos_configured: Some(configuration), }; let _ = self.sender.send(Ok(self.endpoint)); } @@ -730,7 +590,8 @@ } pub fn accept(mut self) { - let AseAdditionalParameters::Streaming { qos_configured, .. } = self.endpoint.additional + let AseAdditionalParameters::Streaming { qos_configured: Some(qos_configured), .. } = + self.endpoint.additional else { unreachable!(); };
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs index 1ca6f3f..324a2b7 100644 --- a/rust/bt-ascs/src/types.rs +++ b/rust/bt-ascs/src/types.rs
@@ -3,31 +3,32 @@ // found in the LICENSE file. use bt_common::packet_encoding::{Decodable, Encodable}; -use bt_common::{codable_as_bitmask, decodable_enum}; +use bt_common::{codable_as_bitmask, decodable_enum, Uuid}; +use bt_gatt::types::Handle; use thiserror::Error; +use crate::client::ClientError; +use crate::server::ServerError; +use bt_common::core::ltv::LtValue; use bt_common::core::CodecId; use bt_common::generic_audio::metadata_ltv::Metadata; +use bt_gatt::types::Error as BtGattError; /// Error type #[derive(Debug, Error)] pub enum Error { - #[error("Reserved for Future Use: {0}")] - ReservedFutureUse(String), - #[error("Server Only Operation")] - ServerOnlyOperation, - #[error("Service is already published")] - AlreadyPublished, - #[error("Issue publishing service: {0}")] - PublishError(bt_gatt::types::Error), - #[error("Unsupported configuration: {0}")] - Unsupported(String), - #[error("Unknown Peer: {0}")] - UnknownPeer(bt_common::PeerId), + #[error("ASCS Server error: {0}")] + Server(#[from] ServerError), + #[error("ASCS Client error: {0}")] + Client(#[from] ClientError), + #[error("GATT operation error: {0}")] + Gatt(#[from] BtGattError), + #[error("Internal error occurred: {0}")] + Internal(String), } #[non_exhaustive] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ResponseCode { Success { ase_id: AseId }, UnsupportedOpcode { opcode_byte: u8 }, @@ -43,7 +44,7 @@ } impl ResponseCode { - fn to_code(&self) -> u8 { + pub(crate) fn to_code(&self) -> u8 { match self { ResponseCode::Success { .. } => 0x00, ResponseCode::UnsupportedOpcode { .. } => 0x01, @@ -122,9 +123,81 @@ pub(crate) fn notify_value(&self) -> Vec<u8> { [self.ase_id_value(), self.to_code(), self.reason_byte()].into() } + + pub fn decode_response( + buf: &[u8], + opcode: u8, + ) -> Result<Self, bt_common::packet_encoding::Error> { + if buf.len() < 3 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let ase_id_val = buf[0]; + let code = buf[1]; + let reason = buf[2]; + let ase_id = AseId(ase_id_val); + + match code { + 0x00 => Ok(Self::Success { ase_id }), + 0x01 => { + if ase_id_val != 0 { + return Err(bt_common::packet_encoding::Error::OutOfRange); + } + Ok(Self::UnsupportedOpcode { opcode_byte: opcode }) + } + 0x02 => { + if ase_id_val != 0 { + return Err(bt_common::packet_encoding::Error::OutOfRange); + } + Ok(Self::InvalidLength { opcode_byte: opcode }) + } + 0x03 => Ok(Self::InvalidAseId { value: ase_id_val }), + 0x04 => Ok(Self::InvalidAseStateMachineTransition { ase_id }), + 0x05 => Ok(Self::InvalidAseDirection { ase_id }), + 0x06 => Ok(Self::UnsupportedAudioCapablities { ase_id }), + 0x07 => { + let reason = ResponseReason::try_from(reason) + .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?; + Ok(Self::ConfigurationParameterValue { + ase_id, + issue: ResponseIssue::Unsupported, + reason, + }) + } + 0x08 => { + let reason = ResponseReason::try_from(reason) + .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?; + Ok(Self::ConfigurationParameterValue { + ase_id, + issue: ResponseIssue::Rejected, + reason, + }) + } + 0x09 => { + let reason = ResponseReason::try_from(reason) + .map_err(|_| bt_common::packet_encoding::Error::OutOfRange)?; + Ok(Self::ConfigurationParameterValue { + ase_id, + issue: ResponseIssue::Invalid, + reason, + }) + } + 0x0A => { + Ok(Self::Metadata { ase_id, issue: ResponseIssue::Unsupported, type_value: reason }) + } + 0x0B => { + Ok(Self::Metadata { ase_id, issue: ResponseIssue::Rejected, type_value: reason }) + } + 0x0C => { + Ok(Self::Metadata { ase_id, issue: ResponseIssue::Invalid, type_value: reason }) + } + 0x0D => Ok(Self::InsufficientResources { ase_id }), + 0x0E => Ok(Self::UnspecifiedError { ase_id }), + _ => Err(bt_common::packet_encoding::Error::OutOfRange), + } + } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ResponseIssue { Unsupported, Rejected, @@ -199,6 +272,327 @@ } } +#[derive(Debug, Clone, PartialEq)] +pub enum AseAdditionalParameters { + None, + CodecConfigured { + framing: Framing, + preferred_phys: Vec<Phy>, + preferred_retransmission_number: u8, + max_transport_latency: MaxTransportLatency, + presentation_delay_range: PresentationDelayRange, + codec_id: CodecId, + codec_config: Vec<u8>, + }, + QosConfigured { + configuration: QosConfiguration, + }, + Streaming { + cig_id: CigId, + cis_id: CisId, + metadata: Vec<Metadata>, + qos_configured: Option<QosConfiguration>, + }, +} + +impl AseAdditionalParameters { + pub fn char_size(&self) -> usize { + match self { + AseAdditionalParameters::None => 0, + AseAdditionalParameters::CodecConfigured { codec_config, .. } => { + 23 + codec_config.len() + } + AseAdditionalParameters::QosConfigured { .. } => 15, + AseAdditionalParameters::Streaming { metadata, .. } => { + metadata.iter().fold(3, |total, m| total + m.encoded_len() as usize) + } + } + } + + pub fn into_char_value(&self) -> Vec<u8> { + match self { + AseAdditionalParameters::None => Vec::new(), + AseAdditionalParameters::CodecConfigured { + framing, + preferred_phys, + preferred_retransmission_number, + max_transport_latency, + presentation_delay_range, + codec_id, + codec_config, + } => { + let mut value = Vec::with_capacity(self.char_size()); + value.resize(self.char_size() - codec_config.len(), 0); + value[0] = (*framing) as u8; + value[1] = Phy::to_bits(preferred_phys.iter()); + value[2] = *preferred_retransmission_number; + max_transport_latency.encode(&mut value[3..]).unwrap(); + presentation_delay_range.encode(&mut value[5..]).unwrap(); + codec_id.encode(&mut value[17..]).unwrap(); + value[22] = codec_config.len() as u8; + value.extend(codec_config.clone()); + value + } + AseAdditionalParameters::QosConfigured { + configuration: + QosConfiguration { + cig_id, + cis_id, + sdu_interval, + framing, + phy, + max_sdu, + retransmission_number, + max_transport_latency, + presentation_delay, + .. + }, + } => { + let mut value = Vec::with_capacity(self.char_size()); + value.resize(self.char_size(), 0); + cig_id.encode(&mut value[0..]).unwrap(); + cis_id.encode(&mut value[1..]).unwrap(); + sdu_interval.encode(&mut value[2..]).unwrap(); + framing.encode(&mut value[5..]).unwrap(); + value[6] = Phy::to_bits(phy.iter()); + max_sdu.encode(&mut value[7..]).unwrap(); + value[9] = *retransmission_number; + max_transport_latency.encode(&mut value[10..]).unwrap(); + presentation_delay.encode(&mut value[12..]).unwrap(); + value + } + AseAdditionalParameters::Streaming { cig_id, cis_id, metadata, .. } => { + let mut value = Vec::with_capacity(self.char_size()); + value.resize(self.char_size(), 0); + cig_id.encode(&mut value[0..]).unwrap(); + cis_id.encode(&mut value[1..]).unwrap(); + value[2] = metadata.iter().fold(0usize, |acc, i| acc + i.encoded_len()) as u8; + LtValue::encode_all(metadata.clone().into_iter(), &mut value[3..]).unwrap(); + value + } + } + } + + pub fn decode( + ase_id: AseId, + state: &AseState, + buf: &[u8], + ) -> (Result<Self, ResponseCode>, usize) { + match state { + AseState::Idle | AseState::Releasing => (Ok(Self::None), 0), + AseState::CodecConfigured => { + if buf.len() < 23 { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let Ok(framing) = Framing::try_from(buf[0]) else { + return (Err(ResponseCode::invalid_length()), buf.len()); + }; + let preferred_phys = Phy::from_bits(buf[1]).collect(); + let preferred_retransmission_number = buf[2]; + let Ok(max_transport_latency) = MaxTransportLatency::decode(&buf[3..5]).0 else { + return (Err(ResponseCode::invalid_length()), buf.len()); + }; + let Ok(presentation_delay_range) = PresentationDelayRange::decode(&buf[5..17]).0 + else { + return (Err(ResponseCode::invalid_length()), buf.len()); + }; + let Ok(codec_id) = CodecId::decode(&buf[17..22]).0 else { + return (Err(ResponseCode::invalid_length()), buf.len()); + }; + let codec_specific_configuration_len = buf[22] as usize; + let total_len = 23 + codec_specific_configuration_len; + if buf.len() < total_len { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let codec_config = buf[23..total_len].to_vec(); + ( + Ok(Self::CodecConfigured { + framing, + preferred_phys, + preferred_retransmission_number, + max_transport_latency, + presentation_delay_range, + codec_id, + codec_config, + }), + total_len, + ) + } + AseState::QosConfigured => { + if buf.len() < 15 { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let mut temp_buf = Vec::with_capacity(16); + temp_buf.push(ase_id.into()); + temp_buf.extend_from_slice(&buf[0..15]); + let (config_res, _) = QosConfiguration::decode(&temp_buf); + match config_res { + Ok(configuration) => (Ok(Self::QosConfigured { configuration }), 15), + Err(e) => (Err(e), buf.len()), + } + } + AseState::Enabling | AseState::Streaming | AseState::Disabling => { + if buf.len() < 3 { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let cig_id = match CigId::try_from(buf[0]) { + Ok(id) => id, + Err(_) => return (Err(ResponseCode::invalid_length()), buf.len()), + }; + let cis_id = match CisId::try_from(buf[1]) { + Ok(id) => id, + Err(_) => return (Err(ResponseCode::invalid_length()), buf.len()), + }; + let metadata_length = buf[2] as usize; + let total_len = 3 + metadata_length; + if buf.len() < total_len { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + + use bt_common::core::ltv::Error as LtvError; + use bt_common::core::ltv::LtValue; + let (metadata_results, consumed) = + Metadata::decode_all(&buf[3..3 + metadata_length]); + if consumed != metadata_length { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let metadata_result: Result<Vec<Metadata>, LtvError<<Metadata as LtValue>::Type>> = + metadata_results.into_iter().collect(); + let Ok(metadata) = metadata_result else { + match metadata_result.unwrap_err() { + LtvError::MissingType | LtvError::MissingData(_) => { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + LtvError::UnrecognizedType(_, type_value) => { + return ( + Err(ResponseCode::Metadata { + ase_id, + issue: ResponseIssue::Unsupported, + type_value, + }), + total_len, + ); + } + LtvError::LengthOutOfRange(_, t, _) + | LtvError::TypeFailedToDecode(t, _) => { + return ( + Err(ResponseCode::Metadata { + ase_id, + issue: ResponseIssue::Invalid, + type_value: t.into(), + }), + total_len, + ); + } + } + }; + (Ok(Self::Streaming { cig_id, cis_id, metadata, qos_configured: None }), total_len) + } + } + } +} + +impl From<QosConfiguration> for AseAdditionalParameters { + fn from(value: QosConfiguration) -> Self { + Self::QosConfigured { configuration: value } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioDirection { + Sink, + Source, +} + +impl From<&AudioDirection> for bt_common::Uuid { + fn from(value: &AudioDirection) -> Self { + match value { + AudioDirection::Sink => Uuid::from_u16(0x2BC4), + AudioDirection::Source => Uuid::from_u16(0x2BC5), + } + } +} + +#[derive(Debug, Clone)] +pub struct AudioStreamEndpoint { + pub handle: Handle, + pub direction: AudioDirection, + pub ase_id: AseId, + pub state: AseState, + pub additional: AseAdditionalParameters, +} + +impl AudioStreamEndpoint { + /// Decodes an `AudioStreamEndpoint` from an ASE characteristic value. + /// + /// # Arguments + /// * `handle` - The GATT handle of the ASE characteristic. + /// * `direction` - The direction of the ASE (Sink or Source). + /// * `char_value_buf` - The characteristic value bytes read from the + /// server. + /// + /// # Expected `char_value_buf` Layout (ASCS v1.0 Section 4.3): + /// * **Octet 0:** `ASE_ID` (1 octet) + /// * **Octet 1:** `ASE_State` (1 octet) + /// * **Octet 2+:** `Additional_ASE_Parameters` (variable octets, see ASCS + /// v1.0 Table 4.2) + pub fn from_char_value( + handle: Handle, + direction: AudioDirection, + char_value_buf: &[u8], + ) -> Result<Self, ResponseCode> { + if char_value_buf.len() < 2 { + return Err(ResponseCode::invalid_length()); + } + let ase_id = AseId::try_from(char_value_buf[0])?; + let Ok(state) = AseState::try_from(char_value_buf[1]) else { + return Err(ResponseCode::invalid_length()); + }; + let (additional_res, _) = + AseAdditionalParameters::decode(ase_id, &state, &char_value_buf[2..]); + let additional = additional_res?; + + Ok(Self { handle, direction, ase_id, state, additional }) + } + + /// Encodes this endpoint's current state into a GATT characteristic value + /// buffer. + pub fn into_char_value(&self) -> Vec<u8> { + let mut buf = vec![0; self.encoded_len()]; + self.encode(&mut buf).unwrap(); + buf + } + + pub fn get_cis(&self) -> Option<(CigId, CisId)> { + match &self.additional { + AseAdditionalParameters::QosConfigured { configuration } => { + Some((configuration.cig_id, configuration.cis_id)) + } + AseAdditionalParameters::Streaming { cig_id, cis_id, .. } => Some((*cig_id, *cis_id)), + _ => None, + } + } +} + +impl Encodable for AudioStreamEndpoint { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + 2 + self.additional.char_size() + } + + 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.state.into(); + let add_bytes = self.additional.into_char_value(); + buf[2..2 + add_bytes.len()].copy_from_slice(&add_bytes); + Ok(()) + } +} + decodable_enum! { pub enum AseControlPointOpcode<u8, bt_common::packet_encoding::Error, OutOfRange> { ConfigCodec = 0x01, @@ -289,7 +683,9 @@ AseControlOperation::ReceiverStopReady { .. } => Ok(0x06), AseControlOperation::UpdateMetadata { .. } => Ok(0x07), AseControlOperation::Release { .. } => Ok(0x08), - AseControlOperation::Released { .. } => Err(Error::ServerOnlyOperation), + AseControlOperation::Released { .. } => { + Err(Error::Server(ServerError::ServerOnlyOperation)) + } } } } @@ -894,7 +1290,7 @@ /// preferred range of the Presentation Delay parameter to be requested by the /// ASCS Client. Prefered Minimum must be above min, and preferred_max must be /// below max. Either of these being None indicates no preference. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct PresentationDelayRange { min: PresentationDelay, max: PresentationDelay, @@ -904,6 +1300,22 @@ impl PresentationDelayRange { const BYTE_SIZE: usize = PresentationDelay::BYTE_SIZE * 4; + + pub fn min(&self) -> &PresentationDelay { + &self.min + } + + pub fn max(&self) -> &PresentationDelay { + &self.max + } + + pub fn preferred_min(&self) -> Option<&PresentationDelay> { + self.preferred_min.as_ref() + } + + pub fn preferred_max(&self) -> Option<&PresentationDelay> { + self.preferred_max.as_ref() + } /// Make a new delay range with no preference. Returns /// ResponseCode::InvalidLength if min > max or the value is out of the /// acceptable range (PresentationDelay is 24 bits) @@ -1035,6 +1447,166 @@ } } +impl Decodable for PresentationDelayRange { + type Error = ResponseCode; + + fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { + if buf.len() < Self::BYTE_SIZE { + return (Err(ResponseCode::invalid_length()), buf.len()); + } + let min = match PresentationDelay::decode(&buf[0..3]).0 { + Ok(min) => min, + Err(e) => return (Err(e), buf.len()), + }; + let max = match PresentationDelay::decode(&buf[3..6]).0 { + Ok(max) => max, + Err(e) => return (Err(e), buf.len()), + }; + let pref_min_val = u32::from_le_bytes([buf[6], buf[7], buf[8], 0]); + let preferred_min = if pref_min_val == 0 { + None + } else { + Some(PresentationDelay { microseconds: pref_min_val }) + }; + let pref_max_val = u32::from_le_bytes([buf[9], buf[10], buf[11], 0]); + let preferred_max = if pref_max_val == 0 { + None + } else { + Some(PresentationDelay { microseconds: pref_max_val }) + }; + (Ok(Self { min, max, preferred_min, preferred_max }), Self::BYTE_SIZE) + } +} + +/// Represents an ASE Control Point notification sent by an ASCS Server to a +/// Client. +/// +/// See ASCS v1.0 Section 4.2 for the frame structure and notification +/// properties. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControlPointNotification { + pub opcode: u8, + pub number_of_ases: u8, + pub responses: Vec<ResponseCode>, +} + +impl ControlPointNotification { + /// Creates a new, empty `ControlPointNotification` for standard multi-ASE + /// operations. + /// + /// The notification starts with a response count of `0`. + /// + /// # Arguments + /// * `opcode` - The opcode of the operation being responded to. + pub fn new(opcode: u8) -> Self { + Self { opcode, number_of_ases: 0, responses: Vec::new() } + } + + /// Creates a new global error `ControlPointNotification`. + /// + /// A response count of `0xFF` is used strictly when the entire control + /// point write is rejected globally before individual ASEs are + /// processed (e.g., unrecognized opcode or invalid packet length). The + /// notification contains a single error response with an + /// ASE ID of `0x00`. See ASCS v1.0 Table 4.7. + /// + /// # Arguments + /// * `opcode` - The opcode of the operation being responded to. + /// * `response_code` - Either `0x01` (Unsupported Opcode) or `0x02` + /// (Invalid Length). + pub fn new_error(opcode: u8, response_code: u8) -> Self { + let code = match response_code { + 0x01 => ResponseCode::UnsupportedOpcode { opcode_byte: opcode }, + 0x02 => ResponseCode::InvalidLength { opcode_byte: opcode }, + _ => unreachable!(), + }; + Self { opcode, number_of_ases: 0xFF, responses: vec![code] } + } + + /// Adds a single ASE response code to the notification list. + /// + /// If the notification is currently in a global error state (response count + /// `0xFF`), calling this method clears the error and transitions it + /// back to a standard multi-ASE response list. + /// + /// # Arguments + /// * `response` - The `ResponseCode` variant to append to the notification. + pub fn add_response(&mut self, response: ResponseCode) { + if self.number_of_ases == 0xFF { + self.number_of_ases = 0; + self.responses.clear(); + } + self.responses.push(response); + self.number_of_ases = self.responses.len() as u8; + } +} + +impl Encodable for ControlPointNotification { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> usize { + 2 + self.responses.len() * 3 + } + + fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(Self::Error::BufferTooSmall); + } + buf[0] = self.opcode; + buf[1] = self.number_of_ases; + let mut idx = 2; + for r in &self.responses { + let val = r.notify_value(); + buf[idx..idx + 3].copy_from_slice(&val); + idx += 3; + } + Ok(()) + } +} + +impl Decodable for ControlPointNotification { + type Error = bt_common::packet_encoding::Error; + + fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { + if buf.len() < 2 { + return (Err(Self::Error::UnexpectedDataLength), buf.len()); + } + let opcode = buf[0]; + let number_of_ases = buf[1]; + let responses_buf = &buf[2..]; + let mut responses = Vec::new(); + + if number_of_ases == 0xFF { + if responses_buf.len() < 3 { + return (Err(Self::Error::UnexpectedDataLength), buf.len()); + } + let r = match ResponseCode::decode_response(&responses_buf[0..3], opcode) { + Ok(r) => r, + Err(e) => return (Err(e), buf.len()), + }; + if r.ase_id_value() != 0 || (r.to_code() != 0x01 && r.to_code() != 0x02) { + return (Err(Self::Error::UnexpectedDataLength), buf.len()); + } + responses.push(r); + (Ok(Self { opcode, number_of_ases, responses }), 5) + } else { + let expected_len = number_of_ases as usize * 3; + if responses_buf.len() < expected_len { + return (Err(Self::Error::UnexpectedDataLength), buf.len()); + } + for i in 0..(number_of_ases as usize) { + let idx = i * 3; + let r = match ResponseCode::decode_response(&responses_buf[idx..idx + 3], opcode) { + Ok(r) => r, + Err(e) => return (Err(e), buf.len()), + }; + responses.push(r); + } + (Ok(Self { opcode, number_of_ases, responses }), 2 + expected_len) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1312,4 +1884,122 @@ assert_eq!(&encoded, expected); } + + #[test] + fn test_ase_endpoint_roundtrip() { + use bt_gatt::types::Handle; + // Test with AdditionalParameters::None + let endpoint_none = AudioStreamEndpoint { + handle: Handle(1), + direction: AudioDirection::Sink, + ase_id: AseId(1), + state: AseState::Idle, + additional: AseAdditionalParameters::None, + }; + let mut buf = vec![0; endpoint_none.encoded_len()]; + endpoint_none.encode(&mut buf).unwrap(); + assert_eq!(buf.len(), 2); + assert_eq!(buf, vec![1, 0]); + + let decoded_endpoint_none = + AudioStreamEndpoint::from_char_value(Handle(1), AudioDirection::Sink, &buf).unwrap(); + assert_eq!(decoded_endpoint_none.ase_id, endpoint_none.ase_id); + assert_eq!(decoded_endpoint_none.state, endpoint_none.state); + assert_eq!(decoded_endpoint_none.additional, endpoint_none.additional); + + // Test with AdditionalParameters::CodecConfigured + let delay_range = PresentationDelayRange::build(1000, 2000).unwrap(); + let endpoint_codec = AudioStreamEndpoint { + handle: Handle(2), + direction: AudioDirection::Source, + ase_id: AseId(2), + state: AseState::CodecConfigured, + additional: AseAdditionalParameters::CodecConfigured { + framing: Framing::Unframed, + preferred_phys: vec![Phy::Le1MPhy].into_iter().collect(), + preferred_retransmission_number: 3, + max_transport_latency: MaxTransportLatency::try_from( + std::time::Duration::from_millis(10), + ) + .unwrap(), + presentation_delay_range: delay_range, + codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3), + codec_config: vec![1, 2, 3], + }, + }; + let mut buf2 = vec![0; endpoint_codec.encoded_len()]; + endpoint_codec.encode(&mut buf2).unwrap(); + + let decoded_endpoint_codec = + AudioStreamEndpoint::from_char_value(Handle(2), AudioDirection::Source, &buf2).unwrap(); + assert_eq!(decoded_endpoint_codec.ase_id, endpoint_codec.ase_id); + assert_eq!(decoded_endpoint_codec.state, endpoint_codec.state); + assert_eq!(decoded_endpoint_codec.additional, endpoint_codec.additional); + } + + #[test] + fn control_point_notification_roundtrip() { + // 1. Test Standard Multi-ASE notification roundtrip + let mut notification = ControlPointNotification::new(0x01); // Opcode ConfigCodec + notification.add_response(ResponseCode::Success { ase_id: AseId(1) }); + notification.add_response(ResponseCode::InvalidAseId { value: 2 }); + + let mut buf = vec![0; notification.encoded_len()]; + assert!(notification.encode(&mut buf).is_ok()); + + #[rustfmt::skip] + let expected_bytes = &[ + 0x01, // Opcode + 0x02, // Number of ASEs + 0x01, 0x00, 0x00, // Response 1: ASE 1, Success, None + 0x02, 0x03, 0x00, // Response 2: ASE 2, Invalid ASE ID, None + ]; + assert_eq!(buf, expected_bytes); + + let (decoded, consumed) = ControlPointNotification::decode(&buf); + assert_eq!(consumed, buf.len()); + assert_eq!(decoded.expect("should succeed"), notification); + + // 2. Test Global Error Notification (0xFF) roundtrip + let err_notification = ControlPointNotification::new_error(0x01, 0x02); // Invalid Length error + let mut err_buf = vec![0; err_notification.encoded_len()]; + assert!(err_notification.encode(&mut err_buf).is_ok()); + + #[rustfmt::skip] + let expected_err_bytes = &[ + 0x01, // Opcode + 0xFF, // Number of ASEs: Global Error + 0x00, 0x02, 0x00, // ASE 0, Invalid Length, None + ]; + assert_eq!(err_buf, expected_err_bytes); + + let (decoded_err, consumed_err) = ControlPointNotification::decode(&err_buf); + assert_eq!(consumed_err, err_buf.len()); + assert_eq!(decoded_err.expect("should succeed"), err_notification); + + // 3. Test Validation constraint fails on malformed 0xFF notification + // Case A: number_of_ases = 0xFF, but ase_id is not 0x00 (e.g., AseId 1) + #[rustfmt::skip] + let malformed_bytes_ase = &[ + 0x01, // Opcode + 0xFF, // Global Error + 0x01, 0x02, 0x00, // ASE 1, Invalid Length, None (ase_id must be 0x00) + ]; + let (res_malformed_ase, _) = ControlPointNotification::decode(malformed_bytes_ase); + assert_eq!(res_malformed_ase, Err(bt_common::packet_encoding::Error::OutOfRange)); + + // Case B: number_of_ases = 0xFF, but response_code is not 0x01 or 0x02 (e.g. + // Success 0x00) + #[rustfmt::skip] + let malformed_bytes_code = &[ + 0x01, // Opcode + 0xFF, // Global Error + 0x00, 0x00, 0x00, // ASE 0, Success, None (response code must be 0x01 or 0x02) + ]; + let (res_malformed_code, _) = ControlPointNotification::decode(malformed_bytes_code); + assert_eq!( + res_malformed_code, + Err(bt_common::packet_encoding::Error::UnexpectedDataLength) + ); + } }