| // 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. |
| |
| //! Contains structs and traits that are used to connect and get information for |
| //! Connected Isochronous Groups and Streams. |
| |
| use bt_common::core::CodecId; |
| use bt_common::decodable_enum; |
| use bt_common::packet_encoding::{Decodable, Encodable}; |
| use bt_common::PeerId; |
| use futures::future::{ready, Ready}; |
| use std::future::Future; |
| |
| decodable_enum! { |
| pub enum Framing<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Unframed = 0x00, |
| Framed = 0x01, |
| } |
| } |
| |
| impl Framing { |
| pub 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(()) |
| } |
| } |
| |
| decodable_enum! { |
| pub enum Packing<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Sequential = 0x00, |
| Interleaved = 0x01, |
| } |
| } |
| |
| impl Packing { |
| const BYTE_SIZE: usize = 1; |
| } |
| |
| impl Encodable for Packing { |
| 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(()) |
| } |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub struct CisId(u8); |
| |
| impl CisId { |
| pub 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(()) |
| } |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub struct CigId(u8); |
| |
| impl CigId { |
| pub 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(()) |
| } |
| } |
| |
| /// SDU Interval parameter (in milliseconds) |
| /// 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 { |
| pub const BYTE_SIZE: usize = 3; |
| } |
| |
| impl TryFrom<std::time::Duration> for SduInterval { |
| type Error = bt_common::packet_encoding::Error; |
| fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> { |
| let Ok(microseconds) = u32::try_from(value.as_micros()) else { |
| return Err(Self::Error::OutOfRange); |
| }; |
| if microseconds < 0xFF || microseconds > 0x0FFFFF { |
| return Err(Self::Error::OutOfRange); |
| } |
| Ok(Self(microseconds)) |
| } |
| } |
| |
| impl TryFrom<u32> for SduInterval { |
| type Error = bt_common::packet_encoding::Error; |
| fn try_from(value: u32) -> Result<Self, Self::Error> { |
| if value < 0xFF || value > 0x0FFFFF { |
| return Err(Self::Error::OutOfRange); |
| } |
| Ok(Self(value)) |
| } |
| } |
| |
| 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(()) |
| } |
| } |
| |
| /// Max Transport Latency (in milliseconds) |
| /// 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); |
| }; |
| Self::try_from(milliseconds) |
| } |
| } |
| |
| impl TryFrom<u16> for MaxTransportLatency { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: u16) -> Result<Self, Self::Error> { |
| if !(0x0005..=0x0FA0).contains(&value) { |
| return Err(Self::Error::OutOfRange); |
| } |
| Ok(Self(value)) |
| } |
| } |
| |
| impl MaxTransportLatency { |
| pub const BYTE_SIZE: usize = 2; |
| } |
| |
| pub struct CigParameters { |
| pub sdu_interval_central_to_peripheral: SduInterval, |
| pub sdu_interval_peripheral_to_central: SduInterval, |
| pub packing: Packing, |
| pub framing: Framing, |
| pub max_transport_latency_central_to_peripheral: MaxTransportLatency, |
| pub max_transport_latency_peripheral_to_central: MaxTransportLatency, |
| /// The expected or intended peers to join this CIG, if known. This may be |
| /// used to optimize parameters, but may limit the set of peers that can |
| /// connect to the group. |
| pub expected_peers: Vec<PeerId>, |
| } |
| |
| /// Data Direction |
| /// This is always represented in relation to the controller, so: |
| /// Input is data that goes from the Host to the Controller (to the peer) |
| /// Output is data that goes from the Controller to the Host (from the peer) |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub enum DataDirection { |
| Input, |
| Output, |
| } |
| |
| /// Data Path Id |
| /// Non-HCI Data Paths are vendor defined |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] |
| pub enum DataPathId { |
| Hci, |
| Vendor(core::num::NonZeroU8), |
| } |
| |
| impl DataPathId { |
| pub fn hci() -> Self { |
| Self::Hci |
| } |
| |
| pub fn vendor(path_id: core::num::NonZeroU8) -> Self { |
| Self::Vendor(path_id) |
| } |
| } |
| |
| /// Controller Delay |
| /// Stored in microseconds, only valid up to 4 seconds. |
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct ControllerDelay { |
| microseconds: u32, |
| } |
| |
| impl TryFrom<std::time::Duration> for ControllerDelay { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(duration: std::time::Duration) -> Result<Self, Self::Error> { |
| if duration > Self::MAX_ALLOWED { |
| return Err(bt_common::packet_encoding::Error::OutOfRange); |
| } |
| Ok(Self { microseconds: duration.as_micros().try_into().unwrap() }) |
| } |
| } |
| |
| impl From<ControllerDelay> for std::time::Duration { |
| fn from(delay: ControllerDelay) -> Self { |
| Self::from_micros(delay.microseconds.into()) |
| } |
| } |
| |
| impl ControllerDelay { |
| // Maximum allowed delay, from Core Spec Vol 4, Part E 7.8.109 |
| const MAX_ALLOWED: std::time::Duration = std::time::Duration::from_secs(4); |
| } |
| |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct CisRequestedParameters { |
| pub cis_id: CisId, |
| pub max_sdu_size_outgoing: u16, |
| pub max_sdu_size_incoming: u16, |
| } |
| |
| #[derive(Debug)] |
| pub enum ConnectedIsochronousError { |
| NotSupported, |
| } |
| |
| pub trait ConnectedIsochronous: Sized { |
| type Group: ConnectedIsochronousGroup; |
| type InitializeCigFut: Future< |
| Output = std::result::Result<Self::Group, ConnectedIsochronousError>, |
| >; |
| |
| /// Initialize an isochronous group with the specified parameters. |
| /// Dropping a ConnectedIsochronous has no side-effects, notably, |
| /// initialized groups are not disconnected. |
| fn initialize( |
| &self, |
| group_parameters: CigParameters, |
| streams_requested: &[CisRequestedParameters], |
| ) -> Self::InitializeCigFut; |
| } |
| |
| pub trait ConnectedIsochronousGroup: Sized { |
| type ConnectedIsochronousStream: ConnectedIsochronousStream; |
| type CreateCisFut: Future< |
| Output = std::result::Result< |
| Vec<Self::ConnectedIsochronousStream>, |
| ConnectedIsochronousError, |
| >, |
| >; |
| type RemoveGroupFut: Future<Output = ()>; |
| |
| /// Get the CIG ID that has been assigned to this group. Returns None if the |
| /// Group has not been created yet. |
| fn cig_id(&self) -> CigId; |
| |
| /// Create CISes using existing connections to the listed peers. |
| /// Returns a set of streams which are established and ready for data. |
| fn create_streams(&self, peers: &[(CisId, PeerId)]) -> Self::CreateCisFut; |
| |
| /// Remove this group. All connected streams will be closed and any |
| /// remaining calls to `create` will fail. This future will complete |
| /// when the group has been removed and all streams are disconnected. |
| fn remove(&self) -> Self::RemoveGroupFut; |
| } |
| |
| /// Represents a connected isochronous stream. |
| /// Streams may be readable and writable using platform-specific methods. |
| pub trait ConnectedIsochronousStream: Sized { |
| type SetupDataPathFut: Future<Output = std::result::Result<(), ConnectedIsochronousError>>; |
| |
| /// Get the IDs associated with this stream. |
| fn id(&self) -> (CigId, CisId); |
| |
| /// Setup a data path for this stream. This command may be called multiple |
| /// times for each DataDirection. If it is called multiple times with |
| /// the same direction, the previous data path will be removed before |
| /// the new data path is set up. |
| fn setup_data_path( |
| &self, |
| direction: DataDirection, |
| data_path: DataPathId, |
| codec: CodecId, |
| codec_configuration: &[u8], |
| controller_delay: ControllerDelay, |
| ) -> Self::SetupDataPathFut; |
| } |
| |
| pub struct NotSupported; |
| |
| impl ConnectedIsochronous for NotSupported { |
| type Group = NotSupported; |
| type InitializeCigFut = Ready<std::result::Result<Self::Group, ConnectedIsochronousError>>; |
| |
| fn initialize( |
| &self, |
| _group_parameters: CigParameters, |
| _streams_requested: &[CisRequestedParameters], |
| ) -> Self::InitializeCigFut { |
| ready(Err(ConnectedIsochronousError::NotSupported)) |
| } |
| } |
| |
| impl ConnectedIsochronousGroup for NotSupported { |
| type ConnectedIsochronousStream = NotSupported; |
| type CreateCisFut = Ready< |
| std::result::Result<Vec<Self::ConnectedIsochronousStream>, ConnectedIsochronousError>, |
| >; |
| type RemoveGroupFut = Ready<()>; |
| |
| fn create_streams(&self, _peers: &[(CisId, PeerId)]) -> Self::CreateCisFut { |
| ready(Err(ConnectedIsochronousError::NotSupported)) |
| } |
| |
| fn remove(&self) -> Self::RemoveGroupFut { |
| ready(()) |
| } |
| |
| fn cig_id(&self) -> CigId { |
| unreachable!(); |
| } |
| } |
| |
| impl ConnectedIsochronousStream for NotSupported { |
| type SetupDataPathFut = Ready<std::result::Result<(), ConnectedIsochronousError>>; |
| |
| fn id(&self) -> (CigId, CisId) { |
| unreachable!(); |
| } |
| |
| fn setup_data_path( |
| &self, |
| _direction: DataDirection, |
| _data_path: DataPathId, |
| _codec: CodecId, |
| _codec_configuration: &[u8], |
| _controller_delay: ControllerDelay, |
| ) -> Self::SetupDataPathFut { |
| ready(Err(ConnectedIsochronousError::NotSupported)) |
| } |
| } |