| // Copyright 2024 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 bt_common::packet_encoding::{Decodable, Encodable}; |
| use bt_common::{codable_as_bitmask, decodable_enum}; |
| use thiserror::Error; |
| |
| use bt_common::core::CodecId; |
| use bt_common::generic_audio::metadata_ltv::Metadata; |
| |
| /// 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), |
| } |
| |
| #[non_exhaustive] |
| #[derive(Debug, Clone, PartialEq)] |
| pub enum ResponseCode { |
| Success { ase_id: AseId }, |
| UnsupportedOpcode { opcode_byte: u8 }, |
| InvalidLength { opcode_byte: u8 }, |
| InvalidAseId { value: u8 }, |
| InvalidAseStateMachineTransition { ase_id: AseId }, |
| InvalidAseDirection { ase_id: AseId }, |
| UnsupportedAudioCapablities { ase_id: AseId }, |
| ConfigurationParameterValue { ase_id: AseId, issue: ResponseIssue, reason: ResponseReason }, |
| Metadata { ase_id: AseId, issue: ResponseIssue, type_value: u8 }, |
| InsufficientResources { ase_id: AseId }, |
| UnspecifiedError { ase_id: AseId }, |
| } |
| |
| impl ResponseCode { |
| fn to_code(&self) -> u8 { |
| match self { |
| ResponseCode::Success { .. } => 0x00, |
| ResponseCode::UnsupportedOpcode { .. } => 0x01, |
| ResponseCode::InvalidLength { .. } => 0x02, |
| ResponseCode::InvalidAseId { .. } => 0x03, |
| ResponseCode::InvalidAseStateMachineTransition { .. } => 0x04, |
| ResponseCode::InvalidAseDirection { .. } => 0x05, |
| ResponseCode::UnsupportedAudioCapablities { .. } => 0x06, |
| ResponseCode::ConfigurationParameterValue { |
| issue: ResponseIssue::Unsupported, .. |
| } => 0x07, |
| ResponseCode::ConfigurationParameterValue { |
| issue: ResponseIssue::Rejected, .. |
| } => 0x08, |
| ResponseCode::ConfigurationParameterValue { issue: ResponseIssue::Invalid, .. } => 0x09, |
| ResponseCode::Metadata { issue: ResponseIssue::Unsupported, .. } => 0x0A, |
| ResponseCode::Metadata { issue: ResponseIssue::Rejected, .. } => 0x0B, |
| ResponseCode::Metadata { issue: ResponseIssue::Invalid, .. } => 0x0C, |
| ResponseCode::InsufficientResources { .. } => 0x0D, |
| ResponseCode::UnspecifiedError { .. } => 0x0E, |
| } |
| } |
| |
| pub fn invalid_length() -> Self { |
| Self::invalid_length_opcode(0) |
| } |
| |
| pub fn invalid_length_opcode(opcode_byte: u8) -> Self { |
| Self::InvalidLength { opcode_byte } |
| } |
| |
| pub fn unsupported_opcode(opcode_byte: u8) -> Self { |
| Self::UnsupportedOpcode { opcode_byte } |
| } |
| |
| pub(crate) fn is_invalid_length(&self) -> bool { |
| matches!(self, Self::InvalidLength { .. }) |
| } |
| |
| fn reason_byte(&self) -> u8 { |
| match self { |
| ResponseCode::ConfigurationParameterValue { reason, .. } => (*reason).into(), |
| ResponseCode::Metadata { type_value, .. } => *type_value, |
| _ => 0x00, |
| } |
| } |
| |
| /// Get the ASE_ID value for this ResponseCode. This is included in the |
| /// control point notification in response to an operation. For |
| /// UnsupportedOpcode and InvaildLength, the ASE_ID is defined by the |
| /// spec to be 0x00. |
| pub(crate) fn ase_id_value(&self) -> u8 { |
| match self { |
| ResponseCode::UnsupportedOpcode { .. } | ResponseCode::InvalidLength { .. } => 0x00, |
| ResponseCode::InvalidAseId { value } => *value, |
| ResponseCode::Success { ase_id } |
| | ResponseCode::InvalidAseStateMachineTransition { ase_id } |
| | ResponseCode::InvalidAseDirection { ase_id } |
| | ResponseCode::UnsupportedAudioCapablities { ase_id } |
| | ResponseCode::ConfigurationParameterValue { ase_id, .. } |
| | ResponseCode::Metadata { ase_id, .. } |
| | ResponseCode::InsufficientResources { ase_id } |
| | ResponseCode::UnspecifiedError { ase_id } => (*ase_id).into(), |
| } |
| } |
| |
| pub(crate) fn error_notify_value(&self) -> Vec<u8> { |
| match self { |
| Self::UnsupportedOpcode { opcode_byte } | Self::InvalidLength { opcode_byte } => { |
| [*opcode_byte, 0xFFu8].into_iter().chain(self.notify_value()).collect() |
| } |
| _ => vec![], |
| } |
| } |
| |
| pub(crate) fn notify_value(&self) -> Vec<u8> { |
| [self.ase_id_value(), self.to_code(), self.reason_byte()].into() |
| } |
| } |
| |
| #[derive(Debug, Clone, PartialEq)] |
| pub enum ResponseIssue { |
| Unsupported, |
| Rejected, |
| Invalid, |
| } |
| |
| decodable_enum! { |
| #[non_exhaustive] |
| pub enum ResponseReason<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| CodecId = 0x01, |
| CodecSpecificConfiguration = 0x02, |
| SduInterval = 0x03, |
| Framing = 0x04, |
| Phy = 0x05, |
| MaximumSduSize = 0x06, |
| RetransmissionNumber = 0x07, |
| MaxTransportLatency = 0x08, |
| PresentationDelay = 0x09, |
| InvalidAseCisMapping = 0x0A, |
| } |
| } |
| |
| decodable_enum! { |
| |
| #[derive(Default)] |
| pub enum AseState<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| #[default] |
| Idle = 0x00, |
| CodecConfigured = 0x01, |
| QosConfigured = 0x02, |
| Enabling = 0x03, |
| Streaming = 0x04, |
| Disabling = 0x05, |
| Releasing = 0x06, |
| } |
| } |
| |
| /// Audio Stream Endpoint Identifier |
| /// Exposed by the server in ASE characteristics |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct AseId(pub u8); |
| |
| impl AseId { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| impl TryFrom<u8> for AseId { |
| type Error = ResponseCode; |
| |
| fn try_from(value: u8) -> Result<Self, Self::Error> { |
| if value == 0 { |
| return Err(ResponseCode::InvalidAseId { value }); |
| } |
| Ok(Self(value)) |
| } |
| } |
| |
| impl From<AseId> for u8 { |
| fn from(value: AseId) -> Self { |
| value.0 |
| } |
| } |
| |
| impl Decodable for AseId { |
| type Error = ResponseCode; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < 1 { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| (buf[0].try_into(), 1) |
| } |
| } |
| |
| decodable_enum! { |
| pub enum AseControlPointOpcode<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| ConfigCodec = 0x01, |
| ConfigQos = 0x02, |
| Enable = 0x03, |
| ReceiverStartReady = 0x04, |
| Disable = 0x05, |
| ReceiverStopReady = 0x06, |
| UpdateMetadata = 0x07, |
| Release = 0x08, |
| } |
| } |
| |
| impl AseControlPointOpcode { |
| pub(crate) fn allowed_in_state(&self, state: &AseState) -> bool { |
| let allowed_states: &[AseState] = match self { |
| Self::ConfigCodec { .. } => { |
| &[AseState::Idle, AseState::CodecConfigured, AseState::QosConfigured] |
| } |
| Self::ConfigQos { .. } => &[AseState::CodecConfigured, AseState::QosConfigured], |
| Self::Enable { .. } => &[AseState::QosConfigured], |
| Self::ReceiverStartReady { .. } => &[AseState::Enabling], |
| Self::ReceiverStopReady { .. } => &[AseState::Disabling], |
| Self::Disable { .. } => &[AseState::Streaming, AseState::Enabling], |
| Self::UpdateMetadata { .. } => &[AseState::Enabling, AseState::Streaming], |
| Self::Release { .. } => &[ |
| AseState::Enabling, |
| AseState::Streaming, |
| AseState::CodecConfigured, |
| AseState::QosConfigured, |
| AseState::Disabling, |
| ], |
| }; |
| allowed_states.contains(state) |
| } |
| } |
| |
| /// ASE Control Operations. These can be initiated by a server or client. |
| /// Defined in Table 4.6 of ASCS v1.0 |
| /// Unrecognized Operations result in Error Operations, which translate directly |
| /// into an Error response. |
| /// Some variants already contain responses as decoding errors are detected, |
| /// i.e. invalid parameters or metadata, which will be delivered after the |
| /// operation is complete with the results from the rest of the operation. |
| #[derive(Debug, PartialEq, Clone)] |
| pub enum AseControlOperation { |
| ConfigCodec { codec_configurations: Vec<CodecConfiguration>, responses: Vec<ResponseCode> }, |
| ConfigQos { qos_configurations: Vec<QosConfiguration>, responses: Vec<ResponseCode> }, |
| Enable { ases_with_metadata: Vec<AseIdWithMetadata>, responses: Vec<ResponseCode> }, |
| ReceiverStartReady { ases: Vec<AseId> }, |
| // The only possible error here is InvalidLength |
| Disable { ases: Vec<AseId> }, |
| // The only possible error here is InvalidLength |
| ReceiverStopReady { ases: Vec<AseId> }, |
| UpdateMetadata { ases_with_metadata: Vec<AseIdWithMetadata>, responses: Vec<ResponseCode> }, |
| // The only possible error here is InvalidLength |
| Release { ases: Vec<AseId> }, |
| // This is only initiated by the server |
| Released { ase_id: AseId }, |
| } |
| |
| impl AseControlOperation { |
| const MIN_BYTE_SIZE: usize = 3; |
| |
| fn contains_invalid_length(&self) -> bool { |
| match self { |
| Self::ConfigCodec { responses, .. } |
| | Self::ConfigQos { responses, .. } |
| | Self::Enable { responses, .. } |
| | Self::UpdateMetadata { responses, .. } => { |
| responses.into_iter().find(|x| x.is_invalid_length()).is_some() |
| } |
| _ => false, |
| } |
| } |
| } |
| |
| impl TryFrom<&AseControlOperation> for u8 { |
| type Error = Error; |
| |
| fn try_from(value: &AseControlOperation) -> Result<Self, Self::Error> { |
| match value { |
| AseControlOperation::ConfigCodec { .. } => Ok(0x01), |
| AseControlOperation::ConfigQos { .. } => Ok(0x02), |
| AseControlOperation::Enable { .. } => Ok(0x03), |
| AseControlOperation::ReceiverStartReady { .. } => Ok(0x04), |
| AseControlOperation::Disable { .. } => Ok(0x05), |
| AseControlOperation::ReceiverStopReady { .. } => Ok(0x06), |
| AseControlOperation::UpdateMetadata { .. } => Ok(0x07), |
| AseControlOperation::Release { .. } => Ok(0x08), |
| AseControlOperation::Released { .. } => Err(Error::ServerOnlyOperation), |
| } |
| } |
| } |
| |
| fn partition_results<T, E>(collection: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) { |
| let mut oks = Vec::with_capacity(collection.len()); |
| let mut errs = Vec::with_capacity(collection.len()); |
| for item in collection { |
| match item { |
| Ok(x) => oks.push(x), |
| Err(e) => errs.push(e), |
| } |
| } |
| (oks, errs) |
| } |
| |
| impl TryFrom<&AseControlOperation> for AseControlPointOpcode { |
| type Error = bt_common::packet_encoding::Error; |
| fn try_from(value: &AseControlOperation) -> Result<Self, Self::Error> { |
| u8::try_from(value).map_err(|_| Self::Error::OutOfRange)?.try_into() |
| } |
| } |
| |
| impl TryFrom<Vec<u8>> for AseControlOperation { |
| type Error = ResponseCode; |
| |
| fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { |
| if value.len() < Self::MIN_BYTE_SIZE { |
| return Err(ResponseCode::invalid_length()); |
| } |
| let operation: AseControlPointOpcode = match value[0].try_into() { |
| Ok(opcode) => opcode, |
| Err(_e) => { |
| return Err(ResponseCode::unsupported_opcode(value[0])); |
| } |
| }; |
| let number_of_ases = value[1] as usize; |
| if number_of_ases < 1 { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| } |
| let (op, consumed) = match operation { |
| AseControlPointOpcode::ConfigCodec => { |
| let (results, consumed) = |
| CodecConfiguration::decode_multiple(&value[2..], Some(number_of_ases)); |
| let (codec_configurations, responses) = partition_results(results); |
| (Self::ConfigCodec { codec_configurations, responses }, consumed) |
| } |
| AseControlPointOpcode::ConfigQos => { |
| let (results, consumed) = |
| QosConfiguration::decode_multiple(&value[2..], Some(number_of_ases)); |
| let (qos_configurations, responses) = partition_results(results); |
| (Self::ConfigQos { qos_configurations, responses }, consumed) |
| } |
| AseControlPointOpcode::Enable => { |
| let (results, consumed) = |
| AseIdWithMetadata::decode_multiple(&value[2..], Some(number_of_ases)); |
| let (ases_with_metadata, responses) = partition_results(results); |
| (Self::Enable { ases_with_metadata, responses }, consumed) |
| } |
| AseControlPointOpcode::ReceiverStartReady => { |
| // Only InvalidLength is possible |
| let (results, consumed) = AseId::decode_multiple(&value[2..], Some(number_of_ases)); |
| let Ok(ases) = results.into_iter().collect::<Result<Vec<_>, ResponseCode>>() else { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| }; |
| (Self::ReceiverStartReady { ases }, consumed) |
| } |
| AseControlPointOpcode::Disable => { |
| // Only InvalidLength is possible |
| let (results, consumed) = AseId::decode_multiple(&value[2..], Some(number_of_ases)); |
| let Ok(ases) = results.into_iter().collect::<Result<Vec<_>, ResponseCode>>() else { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| }; |
| (Self::Disable { ases }, consumed) |
| } |
| AseControlPointOpcode::ReceiverStopReady => { |
| // Only InvalidLength is possible |
| let (results, consumed) = AseId::decode_multiple(&value[2..], Some(number_of_ases)); |
| let Ok(ases) = results.into_iter().collect::<Result<Vec<_>, ResponseCode>>() else { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| }; |
| (Self::ReceiverStopReady { ases }, consumed) |
| } |
| AseControlPointOpcode::UpdateMetadata => { |
| let (results, consumed) = |
| AseIdWithMetadata::decode_multiple(&value[2..], Some(number_of_ases)); |
| let (ases_with_metadata, responses) = partition_results(results); |
| (Self::UpdateMetadata { ases_with_metadata, responses }, consumed) |
| } |
| AseControlPointOpcode::Release => { |
| // Only InvalidLength is possible |
| let (results, consumed) = AseId::decode_multiple(&value[2..], Some(number_of_ases)); |
| let Ok(ases) = results.into_iter().collect::<Result<Vec<_>, ResponseCode>>() else { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| }; |
| (Self::Release { ases }, consumed) |
| } |
| }; |
| // A client-initiated ASE Control operation shall also be defined as an invalid |
| // length operation if the total length of all parameters written by the |
| // client is not equal to the total length of all fixed parameters plus |
| // the length of any variable length parameters for that operation as |
| // defined in Section 5.1 through Section 5.8. |
| if (consumed + 2) != value.len() { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| } |
| if op.contains_invalid_length() { |
| return Err(ResponseCode::invalid_length_opcode(value[0])); |
| } |
| Ok(op) |
| } |
| } |
| |
| decodable_enum! { |
| pub enum TargetLatency<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| TargetLowLatency = 0x01, |
| TargetBalanced = 0x02, |
| TargetHighReliability = 0x03, |
| } |
| } |
| |
| impl TargetLatency { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| decodable_enum! { |
| pub enum TargetPhy<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Le1MPhy = 0x01, |
| Le2MPhy = 0x02, |
| LeCodedPhy = 0x03, |
| } |
| } |
| |
| impl TargetPhy { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| decodable_enum! { |
| pub enum Phy<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Le1MPhy = 0b0001, |
| Le2MPhy = 0b0010, |
| LeCodedPhy = 0b0100, |
| } |
| } |
| |
| impl From<TargetPhy> for Phy { |
| fn from(value: TargetPhy) -> Self { |
| match value { |
| TargetPhy::Le1MPhy => Self::Le1MPhy, |
| TargetPhy::Le2MPhy => Self::Le2MPhy, |
| TargetPhy::LeCodedPhy => Self::LeCodedPhy, |
| } |
| } |
| } |
| |
| codable_as_bitmask!(Phy, u8); |
| |
| impl Phy { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| /// Represents Config Codec parameters for a single ASE. See ASCS v1.0.1 Section |
| /// 5.2. |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct CodecConfiguration { |
| pub ase_id: AseId, |
| pub target_latency: TargetLatency, |
| pub target_phy: TargetPhy, |
| pub codec_id: CodecId, |
| pub codec_specific_configuration: Vec<u8>, |
| } |
| |
| impl CodecConfiguration { |
| const MIN_BYTE_SIZE: usize = |
| AseId::BYTE_SIZE + TargetLatency::BYTE_SIZE + TargetPhy::BYTE_SIZE + CodecId::BYTE_SIZE + 1; |
| } |
| |
| impl Decodable for CodecConfiguration { |
| type Error = ResponseCode; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::MIN_BYTE_SIZE { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| let codec_specific_configuration_len = buf[Self::MIN_BYTE_SIZE - 1] as usize; |
| let total_len = codec_specific_configuration_len + Self::MIN_BYTE_SIZE; |
| if buf.len() < total_len { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| let try_decode_fn = |buf: &[u8]| { |
| let ase_id = AseId::try_from(buf[0])?; |
| let Ok(target_latency) = TargetLatency::try_from(buf[1]) else { |
| // TODO: unclear what to do if the target latency is out of range. |
| return Err(ResponseCode::ConfigurationParameterValue { |
| ase_id, |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::MaxTransportLatency, |
| }); |
| }; |
| let Ok(target_phy) = TargetPhy::try_from(buf[2]) else { |
| return Err(ResponseCode::ConfigurationParameterValue { |
| ase_id, |
| issue: ResponseIssue::Unsupported, |
| reason: ResponseReason::Phy, |
| }); |
| }; |
| let Ok(codec_id) = CodecId::decode(&buf[3..]).0 else { |
| return Err(ResponseCode::ConfigurationParameterValue { |
| ase_id, |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::CodecId, |
| }); |
| }; |
| let codec_specific_configuration = Vec::from( |
| &buf[Self::MIN_BYTE_SIZE..Self::MIN_BYTE_SIZE + codec_specific_configuration_len], |
| ); |
| Ok(Self { ase_id, target_latency, target_phy, codec_id, codec_specific_configuration }) |
| }; |
| (try_decode_fn(buf), total_len) |
| } |
| } |
| |
| /// Represents Config QoS parameters for a single ASE. See ASCS v1.0.1 Section |
| /// 5.2. |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct QosConfiguration { |
| pub ase_id: AseId, |
| pub cig_id: CigId, |
| pub cis_id: CisId, |
| pub sdu_interval: SduInterval, |
| pub framing: Framing, |
| pub phy: Vec<Phy>, |
| pub max_sdu: MaxSdu, |
| pub retransmission_number: u8, |
| pub max_transport_latency: MaxTransportLatency, |
| pub presentation_delay: PresentationDelay, |
| } |
| |
| impl QosConfiguration { |
| const BYTE_SIZE: usize = AseId::BYTE_SIZE |
| + CigId::BYTE_SIZE |
| + CisId::BYTE_SIZE |
| + SduInterval::BYTE_SIZE |
| + Framing::BYTE_SIZE |
| + Phy::BYTE_SIZE |
| + MaxSdu::BYTE_SIZE |
| + 1 // retransmission_number |
| + MaxTransportLatency::BYTE_SIZE |
| + PresentationDelay::BYTE_SIZE; |
| } |
| |
| impl Decodable for QosConfiguration { |
| type Error = ResponseCode; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < QosConfiguration::BYTE_SIZE { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| let try_decode_fn = |buf: &[u8]| { |
| let ase_id = AseId::try_from(buf[0])?; |
| let cig_id = |
| CigId::try_from(buf[1]).map_err(|_e| ResponseCode::UnspecifiedError { ase_id })?; |
| let cis_id = |
| CisId::try_from(buf[2]).map_err(|_e| ResponseCode::UnspecifiedError { ase_id })?; |
| let sdu_interval; |
| match SduInterval::decode(&buf[3..]) { |
| (Ok(interval), _) => { |
| sdu_interval = interval; |
| } |
| (Err(bt_common::packet_encoding::Error::BufferTooSmall), _) => { |
| return Err(ResponseCode::invalid_length()); |
| } |
| (Err(bt_common::packet_encoding::Error::OutOfRange), _) => { |
| return Err(ResponseCode::ConfigurationParameterValue { |
| ase_id, |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::SduInterval, |
| }); |
| } |
| _ => unreachable!(), |
| }; |
| let Ok(framing) = Framing::try_from(buf[6]) else { |
| return Err(ResponseCode::ConfigurationParameterValue { |
| ase_id, |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::Framing, |
| }); |
| }; |
| let phy = Phy::from_bits(buf[7]).collect(); |
| let max_sdu = [buf[8], buf[9]].try_into().map_err(|_e| { |
| ResponseCode::ConfigurationParameterValue { |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::MaximumSduSize, |
| ase_id, |
| } |
| })?; |
| let retransmission_number = buf[10]; |
| let max_transport_latency = |
| MaxTransportLatency::decode(&buf[11..]).0.map_err(|e| match e { |
| bt_common::packet_encoding::Error::BufferTooSmall => { |
| ResponseCode::invalid_length() |
| } |
| bt_common::packet_encoding::Error::OutOfRange => { |
| ResponseCode::ConfigurationParameterValue { |
| issue: ResponseIssue::Invalid, |
| reason: ResponseReason::MaxTransportLatency, |
| ase_id, |
| } |
| } |
| _ => unreachable!(), |
| })?; |
| let presentation_delay = PresentationDelay::decode(&buf[13..]).0?; |
| Ok(Self { |
| ase_id, |
| cig_id, |
| cis_id, |
| sdu_interval, |
| framing, |
| phy, |
| max_sdu, |
| retransmission_number, |
| max_transport_latency, |
| presentation_delay, |
| }) |
| }; |
| (try_decode_fn(buf), QosConfiguration::BYTE_SIZE) |
| } |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub struct CigId(u8); |
| |
| impl CigId { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| impl TryFrom<u8> for CigId { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: u8) -> Result<Self, Self::Error> { |
| if value > 0xEF { |
| Err(Self::Error::OutOfRange) |
| } else { |
| Ok(CigId(value)) |
| } |
| } |
| } |
| |
| impl From<CigId> for u8 { |
| fn from(value: CigId) -> Self { |
| value.0 |
| } |
| } |
| |
| impl Encodable for CigId { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| buf[0] = self.0; |
| Ok(()) |
| } |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub struct CisId(u8); |
| |
| impl CisId { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| impl TryFrom<u8> for CisId { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: u8) -> Result<Self, Self::Error> { |
| if value > 0xEF { |
| Err(Self::Error::OutOfRange) |
| } else { |
| Ok(CisId(value)) |
| } |
| } |
| } |
| |
| impl From<CisId> for u8 { |
| fn from(value: CisId) -> Self { |
| value.0 |
| } |
| } |
| |
| impl Encodable for CisId { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| buf[0] = self.0; |
| Ok(()) |
| } |
| } |
| |
| /// SDU Inteval parameter |
| /// This value is 24 bits long and little-endian on the wire. |
| /// It is stored native-endian here. |
| /// Valid range is [0x0000FF, 0x0FFFFF]. |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] |
| pub struct SduInterval(u32); |
| |
| impl SduInterval { |
| const BYTE_SIZE: usize = 3; |
| } |
| |
| impl Decodable for SduInterval { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::BYTE_SIZE { |
| return (Err(Self::Error::BufferTooSmall), buf.len()); |
| } |
| let val = u32::from_le_bytes([buf[0], buf[1], buf[2], 0]); |
| if val < 0xFF || val > 0x0FFFFF { |
| return (Err(Self::Error::OutOfRange), Self::BYTE_SIZE); |
| } |
| (Ok(SduInterval(val)), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for SduInterval { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| [buf[0], buf[1], buf[2], _] = self.0.to_le_bytes(); |
| Ok(()) |
| } |
| } |
| |
| decodable_enum! { |
| pub enum Framing<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Unframed = 0x00, |
| Framed = 0x01, |
| } |
| } |
| |
| impl Framing { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| impl Encodable for Framing { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| buf[0] = (*self).into(); |
| Ok(()) |
| } |
| } |
| |
| /// Max SDU parameter value. |
| /// Valid range is 0x0000-0x0FFF |
| /// Transmitted in little-endian. Stored here in native-endian. |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct MaxSdu(u16); |
| |
| impl TryFrom<[u8; 2]> for MaxSdu { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: [u8; 2]) -> Result<Self, Self::Error> { |
| let value = u16::from_le_bytes([value[0], value[1]]); |
| if value > 0xFFF { |
| return Err(Self::Error::OutOfRange); |
| } |
| Ok(MaxSdu(value)) |
| } |
| } |
| |
| impl Encodable for MaxSdu { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| [buf[0], buf[1]] = self.0.to_le_bytes(); |
| Ok(()) |
| } |
| } |
| |
| impl MaxSdu { |
| const BYTE_SIZE: usize = 2; |
| } |
| |
| /// Max Transport Latency |
| /// Valid range is [0x0005, 0x0FA0]. |
| /// Transmitted in little-endian, Stored in native-endian. |
| #[derive(Debug, Clone, Copy, PartialEq)] |
| pub struct MaxTransportLatency(u16); |
| |
| impl Decodable for MaxTransportLatency { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::BYTE_SIZE { |
| return (Err(Self::Error::BufferTooSmall), buf.len()); |
| } |
| let val = u16::from_le_bytes([buf[0], buf[1]]); |
| if val < 0x0005 || val > 0x0FA0 { |
| return (Err(Self::Error::OutOfRange), Self::BYTE_SIZE); |
| } |
| (Ok(MaxTransportLatency(val)), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for MaxTransportLatency { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < 2 { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| [buf[0], buf[1]] = self.0.to_le_bytes(); |
| Ok(()) |
| } |
| } |
| |
| impl TryFrom<std::time::Duration> for MaxTransportLatency { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> { |
| let Ok(milliseconds) = u16::try_from(value.as_millis()) else { |
| return Err(Self::Error::OutOfRange); |
| }; |
| if !(0x0005..=0x0FA0).contains(&milliseconds) { |
| return Err(Self::Error::OutOfRange); |
| } |
| Ok(Self(milliseconds)) |
| } |
| } |
| |
| impl MaxTransportLatency { |
| const BYTE_SIZE: usize = 2; |
| } |
| |
| /// Presentation delay parameter value being requested by the client for an ASE. |
| /// This value is 24 bits long (0x00FFFFFF max) |
| /// Transmitted in little-endian, Stored in native-endian. |
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct PresentationDelay { |
| pub microseconds: u32, |
| } |
| |
| impl PresentationDelay { |
| const BYTE_SIZE: usize = 3; |
| } |
| |
| impl Decodable for PresentationDelay { |
| type Error = ResponseCode; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::BYTE_SIZE { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| let microseconds = u32::from_le_bytes([buf[0], buf[1], buf[2], 0]); |
| (Ok(PresentationDelay { microseconds }), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for PresentationDelay { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| let encoded_le = self.microseconds.to_le_bytes(); |
| [buf[0], buf[1], buf[2]] = [encoded_le[0], encoded_le[1], encoded_le[2]]; |
| Ok(()) |
| } |
| } |
| |
| /// Presentation Delay Range. Used to indicate the supported range and |
| /// 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)] |
| pub struct PresentationDelayRange { |
| min: PresentationDelay, |
| max: PresentationDelay, |
| preferred_min: Option<PresentationDelay>, |
| preferred_max: Option<PresentationDelay>, |
| } |
| |
| impl PresentationDelayRange { |
| const BYTE_SIZE: usize = PresentationDelay::BYTE_SIZE * 4; |
| /// 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) |
| pub fn build(min_us: u32, max_us: u32) -> Result<Self, ResponseCode> { |
| if min_us > max_us || min_us > 0x00FFFFFF || max_us > 0x00FFFFFF { |
| return Err(ResponseCode::invalid_length()); |
| } |
| Ok(Self { |
| min: PresentationDelay { microseconds: min_us }, |
| max: PresentationDelay { microseconds: max_us }, |
| preferred_min: None, |
| preferred_max: None, |
| }) |
| } |
| /// Set the preferred range. If the range does not fit into the supported |
| /// range, returns Err(ResponseCode::InvalidLength). |
| pub fn with_preferred( |
| &mut self, |
| min_us: Option<u32>, |
| max_us: Option<u32>, |
| ) -> Result<&mut Self, ResponseCode> { |
| self.preferred_min = match min_us { |
| Some(min_us) |
| if min_us < self.min.microseconds || max_us.is_some_and(|max| max < min_us) => |
| { |
| return Err(ResponseCode::invalid_length()); |
| } |
| Some(min_us) => Some(PresentationDelay { microseconds: min_us }), |
| None => None, |
| }; |
| self.preferred_max = match max_us { |
| Some(max_us) |
| if max_us > self.max.microseconds || min_us.is_some_and(|min| min > max_us) => |
| { |
| return Err(ResponseCode::invalid_length()); |
| } |
| Some(max_us) => Some(PresentationDelay { microseconds: max_us }), |
| None => None, |
| }; |
| Ok(self) |
| } |
| } |
| |
| impl Encodable for PresentationDelayRange { |
| type Error = bt_common::packet_encoding::Error; |
| fn encoded_len(&self) -> core::primitive::usize { |
| Self::BYTE_SIZE |
| } |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(Self::Error::BufferTooSmall); |
| } |
| buf[0..Self::BYTE_SIZE].fill(0); |
| self.min.encode(&mut buf[0..3])?; |
| self.max.encode(&mut buf[3..6])?; |
| if let Some(pref_min) = &self.preferred_min { |
| pref_min.encode(&mut buf[6..9])?; |
| } |
| if let Some(pref_max) = &self.preferred_max { |
| pref_max.encode(&mut buf[9..12])?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct AseIdWithMetadata { |
| pub ase_id: AseId, |
| pub metadata: Vec<Metadata>, |
| } |
| |
| impl AseIdWithMetadata { |
| const MIN_BYTE_SIZE: usize = 2; |
| } |
| |
| impl Decodable for AseIdWithMetadata { |
| type Error = ResponseCode; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::MIN_BYTE_SIZE { |
| return (Err(ResponseCode::invalid_length()), buf.len()); |
| } |
| let ase_id = match AseId::try_from(buf[0]) { |
| Ok(ase_id) => ase_id, |
| Err(e) => return (Err(e), buf.len()), |
| }; |
| let metadata_length = buf[1] as usize; |
| let total_length = Self::MIN_BYTE_SIZE + metadata_length; |
| if buf.len() < total_length { |
| 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[2..2 + 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 => return (Err(ResponseCode::invalid_length()), buf.len()), |
| 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_length, |
| ); |
| } |
| LtvError::LengthOutOfRange(_, t, _) | LtvError::TypeFailedToDecode(t, _) => { |
| return ( |
| Err(ResponseCode::Metadata { |
| ase_id, |
| issue: ResponseIssue::Invalid, |
| type_value: t.into(), |
| }), |
| total_length, |
| ); |
| } |
| } |
| }; |
| (Ok(Self { ase_id, metadata }), total_length) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use bt_common::packet_encoding::Encodable; |
| |
| use bt_common::core::ltv::LtValue; |
| use bt_common::generic_audio::{codec_configuration, AudioLocation}; |
| |
| #[test] |
| fn codec_configuration_roundtrip() { |
| let codec_specific_configuration = vec![ |
| codec_configuration::CodecConfiguration::SamplingFrequency( |
| codec_configuration::SamplingFrequency::F48000Hz, |
| ), |
| codec_configuration::CodecConfiguration::FrameDuration( |
| codec_configuration::FrameDuration::TenMs, |
| ), |
| codec_configuration::CodecConfiguration::AudioChannelAllocation( |
| [AudioLocation::FrontLeft].into_iter().collect(), |
| ), |
| codec_configuration::CodecConfiguration::CodecFramesPerSdu(1), |
| ]; |
| let codec_config_len = |
| codec_specific_configuration.iter().fold(0, |a, x| a + x.encoded_len()); |
| let mut vec = Vec::with_capacity(codec_config_len); |
| vec.resize(codec_config_len, 0); |
| LtValue::encode_all(codec_specific_configuration.clone().into_iter(), &mut vec[..]) |
| .unwrap(); |
| |
| let _codec_config = CodecConfiguration { |
| ase_id: AseId(5), |
| target_latency: TargetLatency::TargetLowLatency, |
| target_phy: TargetPhy::Le1MPhy, |
| codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3), |
| codec_specific_configuration: vec, |
| }; |
| } |
| |
| #[test] |
| fn codec_configuration_decode() { |
| let encoded = &[ |
| 0x04, 0x02, 0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x01, 0x08, 0x02, 0x02, |
| 0x01, 0x05, 0x03, 0x01, 0x00, 0x00, 0x00, 0x03, 0x04, 0x78, 0x00, |
| ]; |
| |
| let (codec_config, size) = CodecConfiguration::decode(&encoded[..]); |
| let codec_config = codec_config.unwrap(); |
| assert_eq!(size, encoded.len()); |
| assert_eq!(codec_config.ase_id, AseId(4)); |
| assert_eq!(codec_config.target_latency, TargetLatency::TargetBalanced); |
| assert_eq!(codec_config.target_phy, TargetPhy::Le2MPhy); |
| assert_eq!(codec_config.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Lc3)); |
| assert_eq!(codec_config.codec_specific_configuration.len(), 0x10); |
| } |
| |
| #[test] |
| fn ase_control_operation_decode_config_codec() { |
| let encoded = &[ |
| 0x01, 0x01, 0x04, 0x02, 0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x01, 0x08, |
| 0x02, 0x02, 0x01, 0x05, 0x03, 0x01, 0x00, 0x00, 0x00, 0x03, 0x04, 0x78, 0x00, |
| ]; |
| |
| let codec_config = CodecConfiguration::decode(&encoded[2..]).0.unwrap(); |
| let encoded_vec: Vec<u8> = encoded.into_iter().copied().collect(); |
| let operation = AseControlOperation::try_from(encoded_vec); |
| |
| assert_eq!( |
| operation, |
| Ok(AseControlOperation::ConfigCodec { |
| codec_configurations: vec![codec_config], |
| responses: Vec::new() |
| }) |
| ); |
| } |
| |
| #[test] |
| fn ase_control_operation_decode_config_codec_some_failures() { |
| #[rustfmt::skip] |
| let encoded = &[ |
| 0x01, 0x02, // Config Codec, Two ASE_IDs |
| // First ASE_ID config |
| 0x04, 0x02, 0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x01, 0x08, |
| 0x02, 0x02, 0x01, 0x05, 0x03, 0x01, 0x00, 0x00, 0x00, 0x03, 0x04, 0x78, 0x00, |
| // Second ASE_ID config, fails based on invalid parameter value |
| 0x05, 0x05, 0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, |
| ]; |
| |
| let codec_config = CodecConfiguration::decode(&encoded[2..]).0.unwrap(); |
| let encoded_vec: Vec<u8> = encoded.into_iter().copied().collect(); |
| let operation = AseControlOperation::try_from(encoded_vec); |
| |
| match operation { |
| Ok(AseControlOperation::ConfigCodec { codec_configurations, responses }) => { |
| assert_eq!(codec_configurations, vec![codec_config]); |
| assert_eq!(responses.len(), 1); |
| assert!(matches!(responses[0], ResponseCode::ConfigurationParameterValue { .. })); |
| } |
| x => panic!("Expected ConfigCodec, got {x:?}"), |
| }; |
| } |
| |
| #[test] |
| fn ase_control_operation_decode_invalid_length() { |
| #[rustfmt::skip] |
| let encoded = &[ |
| 0x01, 0x02, // Config Codec, Two ASE_IDs |
| // First ASE_ID config |
| 0x04, 0x02, 0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x01, 0x08, |
| 0x02, 0x02, 0x01, 0x05, 0x03, 0x01, 0x00, 0x00, 0x00, 0x03, 0x04, 0x78, 0x00, |
| // Second ASE_ID config, fails based on invalid length |
| 0x05, 0x05, 0x02, 0x06, 0x00, 0x00, 0x00, |
| ]; |
| |
| let encoded_vec: Vec<u8> = encoded.into_iter().copied().collect(); |
| let operation = AseControlOperation::try_from(encoded_vec); |
| |
| assert_eq!(operation, Err(ResponseCode::invalid_length_opcode(0x01))); |
| } |
| |
| #[test] |
| fn qos_configuration_decode() { |
| let encoded = &[ |
| 0x03, // ASE_ID, |
| 0x00, // CIG_ID, |
| 0x00, // CIS_ID, |
| 0x10, 0x27, 0x00, 0x01, 0x02, 0x28, 0x00, 0x02, 0xf4, 0x01, 0x18, 0x00, 0x80, |
| ]; |
| let (qos_config, consumed) = QosConfiguration::decode(&encoded[..]); |
| |
| let qos_config = qos_config.unwrap(); |
| |
| assert_eq!(consumed, encoded.len()); |
| assert_eq!(qos_config.ase_id, AseId(3)); |
| assert_eq!(qos_config.cig_id, CigId(0)); |
| assert_eq!(qos_config.cis_id, CisId(0)); |
| assert_eq!(qos_config.sdu_interval, SduInterval(10000)); |
| assert_eq!(qos_config.framing, Framing::Framed); |
| assert_eq!(qos_config.phy, vec![Phy::Le2MPhy]); |
| assert_eq!(qos_config.max_sdu, MaxSdu(40)); |
| assert_eq!(qos_config.retransmission_number, 2); |
| assert_eq!(qos_config.max_transport_latency, MaxTransportLatency(500)); |
| assert_eq!(qos_config.presentation_delay, PresentationDelay { microseconds: 8388632 }); |
| } |
| |
| #[test] |
| fn aseid_with_metadata_decode() { |
| let encoded = &[ |
| 0x03, // ASE_ID |
| 0x08, // metadata length |
| 0x04, 0x04, 0x65, 0x6E, 0x67, // Language code |
| 0x02, 0x03, 0x61, // Program Info |
| ]; |
| |
| let (ase_with_metadata, consumed) = AseIdWithMetadata::decode(&encoded[..]); |
| let ase_with_metadata = ase_with_metadata.unwrap(); |
| assert_eq!(consumed, encoded.len()); |
| assert_eq!(ase_with_metadata.ase_id, AseId(3)); |
| assert_eq!(ase_with_metadata.metadata.len(), 2); |
| } |
| |
| #[test] |
| fn ase_control_operation_decode_qos() { |
| #[rustfmt::skip] |
| let encoded_qos = &[ |
| 0x02, 0x01, // Qos OpCode, 1 number_of_ases |
| 0x03, // ASE_ID, |
| 0x00, // CIG_ID, |
| 0x00, // CIS_ID, |
| 0x10, 0x27, 0x00, // SduInterval |
| 0x01, 0x02, 0x28, 0x00, 0x02, 0xf4, |
| 0x01, 0x18, 0x00, 0x80, |
| ]; |
| assert_eq!(QosConfiguration::BYTE_SIZE, encoded_qos.len() - 2); |
| let (qos_config, consumed) = QosConfiguration::decode(&encoded_qos[2..]); |
| assert!(qos_config.is_ok()); |
| assert_eq!(consumed, encoded_qos.len() - 2); |
| let encoded_qos: Vec<u8> = encoded_qos.into_iter().copied().collect(); |
| let operation = AseControlOperation::try_from(encoded_qos); |
| assert!(operation.is_ok()); |
| |
| #[rustfmt::skip] |
| let encoded_qos_one_fails = &[ |
| 0x02, 0x03, // Qos OpCode, 3 number_of_ases |
| 0x03, // ASE_ID, |
| 0x00, // CIG_ID, |
| 0x00, // CIS_ID, |
| 0x10, 0x27, 0x00, // SduInterval |
| 0x01, 0x02, 0x28, 0x00, 0x02, 0xf4, |
| 0x01, 0x18, 0x00, 0x80, |
| 0x00, // ASE_ID, (AseId 0 is not allowed) |
| 0x00, // CIG_ID, |
| 0x00, // CIS_ID, |
| 0x10, 0x27, 0x00, // SduInterval |
| 0x01, 0x02, 0x28, 0x00, 0x02, 0xf4, |
| 0x01, 0x18, 0x00, 0x80, |
| 0x04, // ASE_ID |
| 0x00, // CIG_ID, |
| 0x00, // CIS_ID, |
| 0x10, 0x17, 0x00, // SduInterval |
| 0x01, 0x02, 0x28, 0x00, 0x02, 0xf4, |
| 0x01, 0x18, 0x00, 0x80, |
| ]; |
| |
| let encoded_qos_one_fails: Vec<u8> = encoded_qos_one_fails.into_iter().copied().collect(); |
| |
| match AseControlOperation::try_from(encoded_qos_one_fails) { |
| Ok(AseControlOperation::ConfigQos { qos_configurations, responses }) => { |
| assert_eq!(qos_configurations.len(), 2); |
| assert_eq!(responses.len(), 1); |
| assert_eq!(responses[0], ResponseCode::InvalidAseId { value: 0x00 }); |
| } |
| x => panic!("Expected ConfigQos to succeed, got {x:?}"), |
| }; |
| } |
| |
| #[test] |
| fn presentation_delay_range() { |
| // Min must be < than max, and within the right range |
| assert!(PresentationDelayRange::build(10, 1).is_err()); |
| assert!(PresentationDelayRange::build(1000, 0xC0FFEE00).is_err()); |
| assert!(PresentationDelayRange::build(0xC0FFEE00, 0xC0FFEE01).is_err()); |
| |
| let mut delay_range = PresentationDelayRange::build(1000, 2000).unwrap(); |
| |
| assert_eq!(delay_range.min.microseconds, 1000); |
| assert_eq!(delay_range.max.microseconds, 2000); |
| |
| assert!(delay_range.with_preferred(None, None).is_ok()); |
| assert_eq!(delay_range.preferred_min, None); |
| assert!(delay_range.with_preferred(Some(900), None).is_err()); |
| assert_eq!(delay_range.preferred_min, None); |
| assert!(delay_range.with_preferred(Some(1500), Some(1001)).is_err()); |
| assert_eq!(delay_range.preferred_min, None); |
| assert!(delay_range.with_preferred(Some(1500), None).is_ok()); |
| assert_eq!(delay_range.preferred_min, Some(PresentationDelay { microseconds: 1500 })); |
| assert_eq!(delay_range.preferred_max, None); |
| |
| assert!(delay_range.with_preferred(None, Some(2100)).is_err()); |
| assert!(delay_range.with_preferred(Some(1801), Some(1800)).is_err()); |
| assert!(delay_range.with_preferred(Some(1500), Some(1800)).is_ok()); |
| assert_eq!(delay_range.preferred_min, Some(PresentationDelay { microseconds: 1500 })); |
| assert_eq!(delay_range.preferred_max, Some(PresentationDelay { microseconds: 1800 })); |
| |
| let mut encoded = [0; PresentationDelayRange::BYTE_SIZE]; |
| assert!(delay_range.encode(&mut encoded).is_ok()); |
| |
| #[rustfmt::skip] |
| let expected: &[u8; 12] = &[ |
| 0xE8, 0x03, 0x00, // 1000 |
| 0xD0, 0x07, 0x00, // 2000 |
| 0xDC, 0x05, 0x00, // 1500 |
| 0x08, 0x07, 0x00, // 1800 |
| ]; |
| |
| assert_eq!(&encoded, expected); |
| } |
| } |