bt-gatt: Add ConnectedIsochronousTypes and refactor traits Change-Id: I3d533ada59cc4f97136af07fd0db40c86a6a6964 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3000
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 89cfefc..caff88c 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -8,6 +8,7 @@ use bt_common::packet_encoding::Decodable; use bt_common::Uuid; use bt_gatt::client::{CharacteristicNotification, PeerService, ServiceCharacteristic}; +use bt_gatt::connected_isochronous::{CigId, CisId, SduInterval}; use bt_gatt::types::Handle; use crate::types::*; @@ -661,6 +662,7 @@ use super::*; use bt_common::core::CodecId; use bt_common::generic_audio::metadata_ltv::Metadata; + use bt_gatt::connected_isochronous::{Framing, MaxTransportLatency}; use bt_gatt::test_utils::{FakePeerService, FakeTypes}; use bt_gatt::types::{AttributePermissions, CharacteristicProperties, CharacteristicProperty}; use bt_gatt::Characteristic;
diff --git a/rust/bt-ascs/src/debug.rs b/rust/bt-ascs/src/debug.rs index c8f176f..d65b023 100644 --- a/rust/bt-ascs/src/debug.rs +++ b/rust/bt-ascs/src/debug.rs
@@ -7,6 +7,7 @@ use bt_common::gen_commandset; use std::sync::Mutex; +use bt_gatt::connected_isochronous::{CigId, CisId, SduInterval}; use bt_gatt::{client::PeerServiceHandle, Client}; use crate::client::{AudioStreamControlServiceClient, QosConfigurationRequest};
diff --git a/rust/bt-ascs/src/server.rs b/rust/bt-ascs/src/server.rs index 6c4ae42..fa8b3f3 100644 --- a/rust/bt-ascs/src/server.rs +++ b/rust/bt-ascs/src/server.rs
@@ -5,6 +5,7 @@ use bt_common::core::CodecId; use bt_common::generic_audio::metadata_ltv::Metadata; use bt_common::{PeerId, Uuid}; +use bt_gatt::connected_isochronous::{CigId, CisId, Framing, MaxTransportLatency}; use bt_gatt::server::{LocalService, Server, ServiceDefinition, ServiceId}; use bt_gatt::server::{ReadResponder, WriteResponder}; use bt_gatt::types::{
diff --git a/rust/bt-ascs/src/tests.rs b/rust/bt-ascs/src/tests.rs index 60ca82c..36941fb 100644 --- a/rust/bt-ascs/src/tests.rs +++ b/rust/bt-ascs/src/tests.rs
@@ -3,6 +3,7 @@ // found in the LICENSE file. use bt_common::{PeerId, Uuid}; +use bt_gatt::connected_isochronous::{Framing, MaxTransportLatency}; use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes}; use bt_gatt::types::Handle;
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs index a783bf3..ed8341f 100644 --- a/rust/bt-ascs/src/types.rs +++ b/rust/bt-ascs/src/types.rs
@@ -12,6 +12,7 @@ use bt_common::core::ltv::LtValue; use bt_common::core::CodecId; use bt_common::generic_audio::metadata_ltv::Metadata; +use bt_gatt::connected_isochronous::{CigId, CisId, Framing, MaxTransportLatency, SduInterval}; use bt_gatt::types::Error as BtGattError; /// Error type @@ -1269,172 +1270,6 @@ } } -#[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 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(()) - } -} - -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. @@ -1481,59 +1316,6 @@ } } -/// 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. @@ -1922,7 +1704,10 @@ use std::collections::HashSet; + use bt_common::core::ltv::LtValue; use bt_common::generic_audio::{codec_configuration, AudioLocation}; + use bt_common::packet_encoding::Encodable; + use bt_gatt::connected_isochronous::{CigId, CisId}; #[test] fn ase_control_operation_ase_ids() { @@ -2137,14 +1922,14 @@ 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.cig_id, CigId::try_from(0).unwrap()); + assert_eq!(qos_config.cis_id, CisId::try_from(0).unwrap()); + assert_eq!(qos_config.sdu_interval, SduInterval::try_from(10000).unwrap()); 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.max_transport_latency, MaxTransportLatency::try_from(500).unwrap()); assert_eq!(qos_config.presentation_delay, PresentationDelay { microseconds: 8388632 }); }
diff --git a/rust/bt-gatt/src/central.rs b/rust/bt-gatt/src/central.rs index 0bf0802..7c9bedf 100644 --- a/rust/bt-gatt/src/central.rs +++ b/rust/bt-gatt/src/central.rs
@@ -91,4 +91,7 @@ /// Get API for periodic advertising. fn periodic_advertising(&self) -> crate::Result<T::PeriodicAdvertising>; + + /// Get API for connected isochronous groups / streams. + fn connected_isochronous(&self) -> crate::Result<T::ConnectedIsochronous>; }
diff --git a/rust/bt-gatt/src/connected_isochronous.rs b/rust/bt-gatt/src/connected_isochronous.rs new file mode 100644 index 0000000..c6c4ce2 --- /dev/null +++ b/rust/bt-gatt/src/connected_isochronous.rs
@@ -0,0 +1,463 @@ +// 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)) + } +}
diff --git a/rust/bt-gatt/src/lib.rs b/rust/bt-gatt/src/lib.rs index 3ca4b1a..8163976 100644 --- a/rust/bt-gatt/src/lib.rs +++ b/rust/bt-gatt/src/lib.rs
@@ -17,6 +17,9 @@ pub mod periodic_advertising; pub mod pii; +pub mod connected_isochronous; +pub use connected_isochronous::{ConnectedIsochronousGroup, ConnectedIsochronousStream}; + #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; @@ -61,6 +64,10 @@ type WriteFut<'a>: Future<Output = Result<()>> + 'a; /// The implementation of periodic advertising for this GATT implementation. type PeriodicAdvertising: PeriodicAdvertising; + /// The implementation of connected isochronous groups. + /// Note: The provided `NotSupported` can be used here to indicate that it + /// is not supported. + type ConnectedIsochronous: connected_isochronous::ConnectedIsochronous; } /// Servers and services are defined with respect to ServerTypes.
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs index 858362b..b767e2a 100644 --- a/rust/bt-gatt/src/test_utils.rs +++ b/rust/bt-gatt/src/test_utils.rs
@@ -16,6 +16,7 @@ use crate::central::ScanResult; use crate::client::CharacteristicNotification; +use crate::connected_isochronous::CigId; use crate::periodic_advertising::{PeriodicAdvertising, SyncReport}; use crate::pii::GetPeerAddr; use crate::server::{ @@ -320,6 +321,7 @@ type ReadFut<'a> = Ready<Result<(usize, bool)>>; type WriteFut<'a> = Ready<Result<()>>; type PeriodicAdvertising = FakePeriodicAdvertising; + type ConnectedIsochronous = FakeConnectedIsochronous; } impl ServerTypes for FakeTypes { @@ -369,6 +371,88 @@ } } +pub struct FakeConnectedIsochronousStream { + cig_id: crate::connected_isochronous::CigId, + cis_id: crate::connected_isochronous::CisId, +} + +impl crate::connected_isochronous::ConnectedIsochronousStream for FakeConnectedIsochronousStream { + type SetupDataPathFut = + Ready<std::result::Result<(), crate::connected_isochronous::ConnectedIsochronousError>>; + fn id(&self) -> (crate::connected_isochronous::CigId, crate::connected_isochronous::CisId) { + (self.cig_id, self.cis_id) + } + + fn setup_data_path( + &self, + _direction: crate::connected_isochronous::DataDirection, + _data_path: crate::connected_isochronous::DataPathId, + _codec: bt_common::core::CodecId, + _codec_configuration: &[u8], + _controller_delay: crate::connected_isochronous::ControllerDelay, + ) -> Ready<std::result::Result<(), crate::connected_isochronous::ConnectedIsochronousError>> + { + ready(Ok(())) + } +} + +pub struct FakeConnectedIsochronous {} + +impl crate::connected_isochronous::ConnectedIsochronous for FakeConnectedIsochronous { + type Group = FakeConnectedIsochronousGroup; + type InitializeCigFut = Ready< + std::result::Result<Self::Group, crate::connected_isochronous::ConnectedIsochronousError>, + >; + + fn initialize( + &self, + _group_parameters: crate::connected_isochronous::CigParameters, + _streams_requested: &[crate::connected_isochronous::CisRequestedParameters], + ) -> Self::InitializeCigFut { + ready(Ok(FakeConnectedIsochronousGroup {})) + } +} + +pub struct FakeConnectedIsochronousGroup {} + +impl crate::connected_isochronous::ConnectedIsochronousGroup for FakeConnectedIsochronousGroup { + type ConnectedIsochronousStream = FakeConnectedIsochronousStream; + type CreateCisFut = Ready< + std::result::Result< + Vec<Self::ConnectedIsochronousStream>, + crate::connected_isochronous::ConnectedIsochronousError, + >, + >; + type RemoveGroupFut = Ready<()>; + + fn cig_id(&self) -> CigId { + CigId::try_from(1).unwrap() + } + + fn create_streams( + &self, + peers: &[(crate::connected_isochronous::CisId, PeerId)], + ) -> Ready< + std::result::Result< + Vec<FakeConnectedIsochronousStream>, + crate::connected_isochronous::ConnectedIsochronousError, + >, + > { + let streams = peers + .iter() + .map(|(cis_id, _peer_id)| FakeConnectedIsochronousStream { + cig_id: self.cig_id(), + cis_id: *cis_id, + }) + .collect(); + ready(Ok(streams)) + } + + fn remove(&self) -> Ready<()> { + ready(()) + } +} + #[derive(Default)] pub struct FakeCentralInner { clients: HashMap<PeerId, FakeClient>, @@ -407,6 +491,12 @@ fn periodic_advertising(&self) -> Result<<FakeTypes as GattTypes>::PeriodicAdvertising> { Ok(self.inner.lock().periodic_advertising.clone()) } + + fn connected_isochronous( + &self, + ) -> crate::Result<<FakeTypes as GattTypes>::ConnectedIsochronous> { + Ok(FakeConnectedIsochronous {}) + } } #[derive(Debug)]