rust/bt-pacs: Initial server implementation Published Audio Service server implementation that can be initialized with all the mandatory characteristics with mandatory properties. Read and write functionalities are implemented. Change-Id: I4d10298dd505f0dd543802a6b414f819229134e5 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2000 Reviewed-by: Marie Janssen <jamuraa@google.com>
diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 4ce19aa..650f02c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml
@@ -31,3 +31,4 @@ pretty_assertions = "1.2.1" thiserror = "2.0.12" uuid = { version = "1.1.2", features = ["serde", "v4"] } +pin-project = "1.0.11"
diff --git a/rust/bt-common/src/core.rs b/rust/bt-common/src/core.rs index d2e2ce5..f7d94e5 100644 --- a/rust/bt-common/src/core.rs +++ b/rust/bt-common/src/core.rs
@@ -177,6 +177,32 @@ } } +impl Encodable for CodecId { + type Error = PacketError; + + 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); + } + match self { + CodecId::Assigned(format) => { + buf[0] = (*format).into(); + buf[1..5].fill(0); + } + CodecId::VendorSpecific { company_id, vendor_specific_codec_id } => { + buf[0] = 0xFF; + [buf[1], buf[2]] = u16::from(*company_id).to_le_bytes(); + [buf[3], buf[4]] = vendor_specific_codec_id.to_le_bytes(); + } + } + Ok(()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Phy { /// LE 1M PHY @@ -226,4 +252,21 @@ } ); } + + #[test] + fn encode_codec_id() { + let assigned = [0x01, 0x00, 0x00, 0x00, 0x00]; + let (codec_id, _) = CodecId::decode(&assigned[..]).expect("should succeed"); + assert_eq!(codec_id, CodecId::Assigned(CodingFormat::ALawLog)); + + let vendor_specific = [0xFF, 0x36, 0xFD, 0x11, 0x22]; + let (codec_id, _) = CodecId::decode(&vendor_specific[..]).expect("should succeed"); + assert_eq!( + codec_id, + CodecId::VendorSpecific { + company_id: (0xFD36 as u16).into(), + vendor_specific_codec_id: 0x2211 + } + ); + } }
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs index c72b395..873689f 100644 --- a/rust/bt-gatt/src/test_utils.rs +++ b/rust/bt-gatt/src/test_utils.rs
@@ -403,7 +403,7 @@ sender: UnboundedSender<FakeServerEvent>, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct FakeServer { inner: Arc<Mutex<FakeServerInner>>, }
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs index 5644c31..aefbb78 100644 --- a/rust/bt-gatt/src/types.rs +++ b/rust/bt-gatt/src/types.rs
@@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use std::collections::HashSet; + use bt_common::{PeerId, Uuid}; use thiserror::Error; @@ -260,6 +262,18 @@ } } +impl CharacteristicProperties { + pub fn is_disjoint(&self, other: &Self) -> bool { + let this_properties: HashSet<_> = HashSet::from_iter(self.0.iter()); + let other_properties: HashSet<_> = HashSet::from_iter(other.0.iter()); + this_properties.is_disjoint(&other_properties) + } + + pub fn contains(&self, property: CharacteristicProperty) -> bool { + self.0.contains(&property) + } +} + #[derive(Debug, Clone, Copy, Default)] pub struct SecurityLevels { /// Encryption is required / provided @@ -276,6 +290,10 @@ && (!self.authentication || provided.authentication) && (!self.authorization || provided.encryption) } + + pub const fn encryption_required() -> Self { + Self { encryption: true, authentication: false, authorization: false } + } } #[derive(Debug, Clone, Copy, Default)] @@ -291,6 +309,17 @@ pub update: Option<SecurityLevels>, } +impl AttributePermissions { + pub fn with_levels(properties: &CharacteristicProperties, levels: &SecurityLevels) -> Self { + let notify_properties = CharacteristicProperty::Notify | CharacteristicProperty::Indicate; + Self { + read: properties.contains(CharacteristicProperty::Read).then_some(levels.clone()), + write: properties.contains(CharacteristicProperty::Write).then_some(levels.clone()), + update: (!properties.is_disjoint(¬ify_properties)).then_some(levels.clone()), + } + } +} + /// The different types of well-known Descriptors are defined here and should be /// automatically read during service characteristic discovery by Stack /// Implementations and included. Other Descriptor attribute values can be read
diff --git a/rust/bt-pacs/Cargo.toml b/rust/bt-pacs/Cargo.toml index 52d4737..dd975d8 100644 --- a/rust/bt-pacs/Cargo.toml +++ b/rust/bt-pacs/Cargo.toml
@@ -12,6 +12,9 @@ ### Others futures.workspace = true +pin-project.workspace = true +thiserror.workspace = true [dev-dependencies] +bt-gatt = { workspace = true, features = ["test-utils"] } pretty_assertions.workspace = true
diff --git a/rust/bt-pacs/src/lib.rs b/rust/bt-pacs/src/lib.rs index e926716..b77a577 100644 --- a/rust/bt-pacs/src/lib.rs +++ b/rust/bt-pacs/src/lib.rs
@@ -7,11 +7,14 @@ use bt_common::generic_audio::codec_capabilities::CodecCapability; use bt_common::generic_audio::metadata_ltv::Metadata; use bt_common::generic_audio::{AudioLocation, ContextType}; -use bt_common::packet_encoding::Decodable; +use bt_common::packet_encoding::{Decodable, Encodable}; use bt_common::Uuid; use bt_gatt::{client::FromCharacteristic, Characteristic}; +use std::collections::HashSet; + pub mod debug; +pub mod server; /// UUID from Assigned Numbers section 3.4. pub const PACS_UUID: Uuid = Uuid::from_u16(0x1850); @@ -26,11 +29,10 @@ pub struct PacRecord { pub codec_id: CodecId, pub codec_specific_capabilities: Vec<CodecCapability>, - // TODO: Actually parse the metadata once Metadata pub metadata: Vec<Metadata>, } -impl bt_common::packet_encoding::Decodable for PacRecord { +impl Decodable for PacRecord { type Error = bt_common::packet_encoding::Error; fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { @@ -66,14 +68,48 @@ } } -/// One Sink Published Audio Capability Characteristic, or Sink PAC, exposed on -/// a service. More than one Sink PAC can exist on a given PACS service. If -/// multiple are exposed, they are returned separately and can be notified by -/// the server separately. -#[derive(Debug, PartialEq, Clone)] -pub struct SinkPac { - pub handle: bt_gatt::types::Handle, - pub capabilities: Vec<PacRecord>, +impl Encodable for PacRecord { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> core::primitive::usize { + 5usize + + self.codec_specific_capabilities.iter().fold(0, |a, x| a + x.encoded_len()) + + self.metadata.iter().fold(0, |a, x| a + x.encoded_len()) + + 2 + } + + fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(Self::Error::BufferTooSmall); + } + self.codec_id.encode(&mut buf[0..]).unwrap(); + let codec_capabilities_len = + self.codec_specific_capabilities.iter().fold(0, |a, x| a + x.encoded_len()); + buf[5] = codec_capabilities_len as u8; + LtValue::encode_all(self.codec_specific_capabilities.clone().into_iter(), &mut buf[6..])?; + let metadata_len = self.metadata.iter().fold(0, |a, x| a + x.encoded_len()); + buf[6 + codec_capabilities_len] = metadata_len as u8; + if metadata_len != 0 { + LtValue::encode_all( + self.metadata.clone().into_iter(), + &mut buf[6 + codec_capabilities_len + 1..], + )?; + } + Ok(()) + } +} + +fn pac_records_into_char_value(records: &Vec<PacRecord>) -> Vec<u8> { + let mut val = Vec::new(); + val.push(records.len() as u8); + let mut idx = 1; + for record in records { + let record_len = record.encoded_len(); + val.resize(val.len() + record_len, 0); + record.encode(&mut val[idx..]).unwrap(); + idx += record_len; + } + val } fn pac_records_from_bytes( @@ -93,6 +129,16 @@ Ok(capabilities) } +/// One Sink Published Audio Capability Characteristic, or Sink PAC, exposed on +/// a service. More than one Sink PAC can exist on a given PACS service. If +/// multiple are exposed, they are returned separately and can be notified by +/// the server separately. +#[derive(Debug, PartialEq, Clone)] +pub struct SinkPac { + pub handle: bt_gatt::types::Handle, + pub capabilities: Vec<PacRecord>, +} + impl FromCharacteristic for SinkPac { /// UUID from Assigned Numbers section 3.8. const UUID: Uuid = Uuid::from_u16(0x2BC9); @@ -141,12 +187,12 @@ } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Default)] pub struct AudioLocations { - pub locations: std::collections::HashSet<AudioLocation>, + pub locations: HashSet<AudioLocation>, } -impl bt_common::packet_encoding::Decodable for AudioLocations { +impl Decodable for AudioLocations { type Error = bt_common::packet_encoding::Error; fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { @@ -160,16 +206,36 @@ } } +impl Encodable for AudioLocations { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> core::primitive::usize { + 4 + } + + fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { + if buf.len() < 4 { + return Err(Self::Error::BufferTooSmall); + } + [buf[0], buf[1], buf[2], buf[3]] = + AudioLocation::to_bits(self.locations.iter()).to_le_bytes(); + Ok(()) + } +} + #[derive(Debug, PartialEq, Clone)] pub struct SourceAudioLocations { pub handle: bt_gatt::types::Handle, pub locations: AudioLocations, } -#[derive(Debug, PartialEq, Clone)] -pub struct SinkAudioLocations { - pub handle: bt_gatt::types::Handle, - pub locations: AudioLocations, +impl SourceAudioLocations { + fn into_char_value(&self) -> Vec<u8> { + let mut val = Vec::with_capacity(self.locations.encoded_len()); + val.resize(self.locations.encoded_len(), 0); + self.locations.encode(&mut val[..]).unwrap(); + val + } } impl FromCharacteristic for SourceAudioLocations { @@ -191,6 +257,21 @@ } } +#[derive(Debug, PartialEq, Clone)] +pub struct SinkAudioLocations { + pub handle: bt_gatt::types::Handle, + pub locations: AudioLocations, +} + +impl SinkAudioLocations { + fn into_char_value(&self) -> Vec<u8> { + let mut val = Vec::with_capacity(self.locations.encoded_len()); + val.resize(self.locations.encoded_len(), 0); + self.locations.encode(&mut val[..]).unwrap(); + val + } +} + impl FromCharacteristic for SinkAudioLocations { const UUID: Uuid = Uuid::from_u16(0x2BCA); @@ -212,10 +293,10 @@ #[derive(Debug, PartialEq, Clone)] pub enum AvailableContexts { NotAvailable, - Available(std::collections::HashSet<ContextType>), + Available(HashSet<ContextType>), } -impl bt_common::packet_encoding::Decodable for AvailableContexts { +impl Decodable for AvailableContexts { type Error = bt_common::packet_encoding::Error; fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { @@ -231,6 +312,36 @@ } } +impl Encodable for AvailableContexts { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> core::primitive::usize { + 2 + } + + fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { + if buf.len() < 2 { + return Err(Self::Error::BufferTooSmall); + } + match self { + AvailableContexts::NotAvailable => [buf[0], buf[1]] = [0x00, 0x00], + AvailableContexts::Available(set) => { + [buf[0], buf[1]] = ContextType::to_bits(set.iter()).to_le_bytes() + } + } + Ok(()) + } +} + +impl From<&HashSet<ContextType>> for AvailableContexts { + fn from(value: &HashSet<ContextType>) -> Self { + if value.is_empty() { + return AvailableContexts::NotAvailable; + } + AvailableContexts::Available(value.clone()) + } +} + #[derive(Debug, Clone)] pub struct AvailableAudioContexts { pub handle: bt_gatt::types::Handle, @@ -238,6 +349,16 @@ pub source: AvailableContexts, } +impl AvailableAudioContexts { + fn into_char_value(&self) -> Vec<u8> { + let mut val = Vec::with_capacity(self.sink.encoded_len() + self.source.encoded_len()); + val.resize(self.sink.encoded_len() + self.source.encoded_len(), 0); + self.sink.encode(&mut val[0..]).unwrap(); + self.source.encode(&mut val[2..]).unwrap(); + val + } +} + impl FromCharacteristic for AvailableAudioContexts { /// UUID from Assigned Numbers section 3.8. const UUID: Uuid = Uuid::from_u16(0x2BCD); @@ -273,8 +394,34 @@ #[derive(Debug, Clone)] pub struct SupportedAudioContexts { pub handle: bt_gatt::types::Handle, - pub sink: std::collections::HashSet<ContextType>, - pub source: std::collections::HashSet<ContextType>, + pub sink: HashSet<ContextType>, + pub source: HashSet<ContextType>, +} + +impl SupportedAudioContexts { + fn into_char_value(&self) -> Vec<u8> { + let mut val = Vec::with_capacity(self.encoded_len()); + val.resize(self.encoded_len(), 0); + self.encode(&mut val[0..]).unwrap(); + val + } +} + +impl Encodable for SupportedAudioContexts { + type Error = bt_common::packet_encoding::Error; + + fn encoded_len(&self) -> core::primitive::usize { + 4 + } + + fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { + if buf.len() < 4 { + return Err(Self::Error::BufferTooSmall); + } + [buf[0], buf[1]] = ContextType::to_bits(self.sink.iter()).to_le_bytes(); + [buf[2], buf[3]] = ContextType::to_bits(self.source.iter()).to_le_bytes(); + Ok(()) + } } impl FromCharacteristic for SupportedAudioContexts {
diff --git a/rust/bt-pacs/src/server.rs b/rust/bt-pacs/src/server.rs new file mode 100644 index 0000000..5531187 --- /dev/null +++ b/rust/bt-pacs/src/server.rs
@@ -0,0 +1,640 @@ +// 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. + +//! Implements the Published Audio Capabilities Service server role. +//! +//! Use the `ServerBuilder` to define a new `Server` instance with the specified +//! characteristics. The server isn't published to GATT until +//! `Server::publish` method is called. +//! Once the `Server` is published, poll on it to receive events from the +//! `Server`, which are created as it processes incoming client requests. +//! +//! For example: +//! +//! // Set up a GATT Server which implements `bt_gatt::ServerTypes::Server`. +//! let gatt_server = ...; +//! // Define supported and available audio contexts for this PACS. +//! let supported = AudioContexts::new(...); +//! let available = AudioContexts::new(...); +//! let pacs_server = ServerBuilder::new(supported, +//! available).with_sources(...).with_sinks(...).build()?; +//! +//! // Publish the server. +//! pacs_server.publish(gatt_server).expect("publishes fine"); +//! // Process events from the PACS server. +//! while let Some(event) = pacs_server.next().await { +//! // Do something with `event` +//! } + +use bt_common::generic_audio::ContextType; +use bt_gatt::server::LocalService; +use bt_gatt::server::{ReadResponder, ServiceDefinition, WriteResponder}; +use bt_gatt::types::{GattError, Handle}; +use bt_gatt::Server as _; +use futures::task::{Poll, Waker}; +use futures::{Future, Stream}; +use pin_project::pin_project; +use std::collections::HashMap; +use thiserror::Error; + +use crate::{ + AudioLocations, AvailableAudioContexts, PacRecord, SinkAudioLocations, SourceAudioLocations, + SupportedAudioContexts, +}; + +mod types; +use crate::server::types::*; + +#[pin_project(project = LocalServiceProj)] +enum LocalServiceState<T: bt_gatt::ServerTypes> { + NotPublished { + waker: Option<Waker>, + }, + Preparing { + #[pin] + fut: T::LocalServiceFut, + }, + Published { + service: T::LocalService, + #[pin] + events: T::ServiceEventStream, + }, + Terminated, +} + +impl<T: bt_gatt::ServerTypes> Default for LocalServiceState<T> { + fn default() -> Self { + Self::NotPublished { waker: None } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Service is already published")] + AlreadyPublished, + #[error("Issue publishing service: {0}")] + PublishError(#[from] bt_gatt::types::Error), + #[error("Service should support at least one of Sink or Source PAC characteristics")] + MissingPac, + #[error("Available audio contexts are not supported: {0:?}")] + UnsupportedAudioContexts(Vec<ContextType>), +} + +impl<T: bt_gatt::ServerTypes> Stream for LocalServiceState<T> { + type Item = Result<bt_gatt::server::ServiceEvent<T>, Error>; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll<Option<Self::Item>> { + // SAFETY: + // - Wakers are Unpin + // - We re-pin the structurally pinned futures in Preparing and Published + // (service is untouched) + // - Terminated is empty + loop { + match self.as_mut().project() { + LocalServiceProj::Terminated => return Poll::Ready(None), + LocalServiceProj::NotPublished { .. } => { + self.as_mut() + .set(LocalServiceState::NotPublished { waker: Some(cx.waker().clone()) }); + return Poll::Pending; + } + LocalServiceProj::Preparing { fut } => { + let service_result = futures::ready!(fut.poll(cx)); + let Ok(service) = service_result else { + return Poll::Ready(Some(Err(Error::PublishError( + service_result.err().unwrap(), + )))); + }; + let events = service.publish(); + self.as_mut().set(LocalServiceState::Published { service, events }); + continue; + } + LocalServiceProj::Published { service: _, events } => { + let item = futures::ready!(events.poll_next(cx)); + let Some(gatt_result) = item else { + self.as_mut().set(LocalServiceState::Terminated); + return Poll::Ready(None); + }; + let Ok(event) = gatt_result else { + self.as_mut().set(LocalServiceState::Terminated); + return Poll::Ready(Some(Err(Error::PublishError( + gatt_result.err().unwrap(), + )))); + }; + return Poll::Ready(Some(Ok(event))); + } + } + } + } +} + +impl<T: bt_gatt::ServerTypes> LocalServiceState<T> { + fn is_published(&self) -> bool { + if let LocalServiceState::NotPublished { .. } = self { false } else { true } + } +} + +#[derive(Default)] +pub struct ServerBuilder { + source_pacs: Vec<Vec<PacRecord>>, + source_audio_locations: Option<AudioLocations>, + sink_pacs: Vec<Vec<PacRecord>>, + sink_audio_locations: Option<AudioLocations>, +} + +impl ServerBuilder { + pub fn new() -> ServerBuilder { + ServerBuilder::default() + } + + /// Adds a source PAC characteristic to the builder. + /// Each call adds a new characteristic. + /// `capabilities` represents the records for a single PAC characteristic. + /// If `capabilities` is empty, it will be ignored. + pub fn add_source(mut self, capabilities: Vec<PacRecord>) -> Self { + if !capabilities.is_empty() { + self.source_pacs.push(capabilities); + } + self + } + + /// Sets the audio locations for the source. + /// This corresponds to a single Source Audio Locations characteristic. + pub fn set_source_locations(mut self, audio_locations: AudioLocations) -> Self { + self.source_audio_locations = Some(audio_locations); + self + } + + /// Adds a sink PAC characteristic to the builder. + /// Each call adds a new characteristic. + /// `capabilities` represents the records for a single PAC characteristic. + /// If `capabilities` is empty, it will be ignored. + pub fn add_sink(mut self, capabilities: Vec<PacRecord>) -> Self { + if !capabilities.is_empty() { + self.sink_pacs.push(capabilities); + } + self + } + + /// Sets the audio locations for the sink. + /// This corresponds to a single Sink Audio Locations characteristic. + pub fn set_sink_locations(mut self, audio_locations: AudioLocations) -> Self { + self.sink_audio_locations = Some(audio_locations); + self + } + + fn verify_characteristics( + &self, + supported: &AudioContexts, + available: &AudioContexts, + ) -> Result<(), Error> { + // If the corresponding bit in the supported audio contexts is + // not set to 0b1, we shall not set a bit to 0b1 in the + // available audio contexts. See PACS v1.0.1 section 3.5.1. + let diff: Vec<ContextType> = available.sink.difference(&supported.sink).cloned().collect(); + if diff.len() != 0 { + return Err(Error::UnsupportedAudioContexts(diff)); + } + let diff: Vec<ContextType> = + available.source.difference(&supported.source).cloned().collect(); + if diff.len() != 0 { + return Err(Error::UnsupportedAudioContexts(diff)); + } + + // PACS server must have at least one Sink or Source PACS record. + if self.source_pacs.len() == 0 && self.sink_pacs.len() == 0 { + return Err(Error::MissingPac); + } + Ok(()) + } + + /// Builds a server after verifying all the defined characteristics + /// for this server (see PACS v1.0.1 section 3 for details). + pub fn build<T>( + mut self, + mut supported: AudioContexts, + available: AudioContexts, + ) -> Result<Server<T>, Error> + where + T: bt_gatt::ServerTypes, + { + let _ = self.verify_characteristics(&supported, &available)?; + + let mut service_def = ServiceDefinition::new( + bt_gatt::server::ServiceId::new(1), + crate::PACS_UUID, + bt_gatt::types::ServiceKind::Primary, + ); + + let supported = SupportedAudioContexts { + handle: SUPPORTED_AUDIO_CONTEXTS_HANDLE, + sink: supported.sink.drain().collect(), + source: supported.source.drain().collect(), + }; + let _ = service_def.add_characteristic((&supported).into()); + + let available = AvailableAudioContexts { + handle: AVAILABLE_AUDIO_CONTEXTS_HANDLE, + sink: (&available.sink).into(), + source: (&available.source).into(), + }; + let _ = service_def.add_characteristic((&available).into()); + + let mut next_handle_iter = (HANDLE_OFFSET..).map(|x| Handle(x)); + let mut audio_capabilities = HashMap::new(); + + // Sink audio locations characteristic may exist iff it's defined + // and there are valid sink PAC characteristics. + let sink_audio_locations = match self.sink_audio_locations.take() { + Some(locations) if self.sink_pacs.len() > 0 => { + let sink = + SinkAudioLocations { handle: next_handle_iter.next().unwrap(), locations }; + let _ = service_def.add_characteristic((&sink).into()); + Some(sink) + } + _ => None, + }; + for capabilities in self.sink_pacs.drain(..) { + let handle = next_handle_iter.next().unwrap(); + let pac = PublishedAudioCapability::new_sink(handle, capabilities); + let _ = service_def.add_characteristic((&pac).into()); + audio_capabilities.insert(handle, pac); + } + + // Source audio locations characteristic may exist iff it's defined + // and there are valid source PAC characteristics. + let source_audio_locations = match self.source_audio_locations.take() { + Some(locations) if self.source_pacs.len() > 0 => { + let source = + SourceAudioLocations { handle: next_handle_iter.next().unwrap(), locations }; + let _ = service_def.add_characteristic((&source).into()); + Some(source) + } + _ => None, + }; + for capabilities in self.source_pacs.drain(..) { + let handle = next_handle_iter.next().unwrap(); + let pac = PublishedAudioCapability::new_source(handle, capabilities); + let _ = service_def.add_characteristic((&pac).into()); + audio_capabilities.insert(handle, pac); + } + + let server = Server { + service_def, + local_service: Default::default(), + published_audio_capabilities: audio_capabilities, + source_audio_locations, + sink_audio_locations, + available_audio_contexts: available, + supported_audio_contexts: supported, + }; + Ok(server) + } +} + +#[pin_project] +pub struct Server<T: bt_gatt::ServerTypes> { + service_def: ServiceDefinition, + #[pin] + local_service: LocalServiceState<T>, + published_audio_capabilities: HashMap<Handle, PublishedAudioCapability>, + source_audio_locations: Option<SourceAudioLocations>, + sink_audio_locations: Option<SinkAudioLocations>, + available_audio_contexts: AvailableAudioContexts, + supported_audio_contexts: SupportedAudioContexts, +} + +impl<T: bt_gatt::ServerTypes> Server<T> { + pub fn publish(&mut self, server: T::Server) -> Result<(), Error> { + if self.local_service.is_published() { + return Err(Error::AlreadyPublished); + } + + let LocalServiceState::NotPublished { waker } = std::mem::replace( + &mut self.local_service, + LocalServiceState::Preparing { fut: server.prepare(self.service_def.clone()) }, + ) else { + unreachable!(); + }; + waker.map(Waker::wake); + Ok(()) + } + + fn is_source_locations_handle(&self, handle: Handle) -> bool { + self.source_audio_locations.as_ref().map_or(false, |locations| locations.handle == handle) + } + + fn is_sink_locations_handle(&self, handle: Handle) -> bool { + self.sink_audio_locations.as_ref().map_or(false, |locations| locations.handle == handle) + } +} + +impl<T: bt_gatt::ServerTypes> Stream for Server<T> { + type Item = Result<(), Error>; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll<Option<Self::Item>> { + loop { + let mut this = self.as_mut().project(); + let gatt_event = match futures::ready!(this.local_service.as_mut().poll_next(cx)) { + None => return Poll::Ready(None), + Some(Err(e)) => return Poll::Ready(Some(Err(e))), + Some(Ok(event)) => event, + }; + use bt_gatt::server::ServiceEvent::*; + match gatt_event { + Read { handle, offset, responder, .. } => { + let offset = offset as usize; + let value = match handle { + x if x == AVAILABLE_AUDIO_CONTEXTS_HANDLE => { + self.available_audio_contexts.into_char_value() + } + x if x == SUPPORTED_AUDIO_CONTEXTS_HANDLE => { + self.supported_audio_contexts.into_char_value() + } + x if self.is_source_locations_handle(x) => { + self.source_audio_locations.as_ref().unwrap().into_char_value() + } + x if self.is_sink_locations_handle(x) => { + self.sink_audio_locations.as_ref().unwrap().into_char_value() + } + pac_handle => { + let Some(ref pac) = self.published_audio_capabilities.get(&pac_handle) + else { + responder.error(GattError::InvalidHandle); + continue; + }; + pac.encode() + } + }; + responder.respond(&value[offset..]); + continue; + } + // TODO(b/309015071): support optional writes. + Write { responder, .. } => { + responder.error(GattError::WriteNotPermitted); + continue; + } + // TODO(b/309015071): implement notify since it's mandatory. + ClientConfiguration { .. } => { + unimplemented!(); + } + _ => continue, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use bt_common::core::{CodecId, CodingFormat}; + use bt_common::generic_audio::codec_capabilities::*; + use bt_common::generic_audio::AudioLocation; + use bt_common::PeerId; + use bt_gatt::server; + use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes}; + use bt_gatt::types::ServiceKind; + use futures::{FutureExt, StreamExt}; + + use std::collections::HashSet; + + use crate::AvailableContexts; + + // Builder for a server with: + // - 1 sink and 1 source PAC characteristics + // - sink audio locations + fn default_server_builder() -> ServerBuilder { + let builder = ServerBuilder::new() + .add_sink(vec![PacRecord { + codec_id: CodecId::Assigned(CodingFormat::ALawLog), + codec_specific_capabilities: vec![CodecCapability::SupportedFrameDurations( + FrameDurationSupport::BothNoPreference, + )], + metadata: vec![], + }]) + .set_sink_locations(AudioLocations { + locations: HashSet::from([AudioLocation::FrontLeft, AudioLocation::FrontRight]), + }) + .add_source(vec![ + PacRecord { + codec_id: CodecId::Assigned(CodingFormat::ALawLog), + codec_specific_capabilities: vec![CodecCapability::SupportedFrameDurations( + FrameDurationSupport::BothNoPreference, + )], + metadata: vec![], + }, + PacRecord { + codec_id: CodecId::Assigned(CodingFormat::MuLawLog), + codec_specific_capabilities: vec![CodecCapability::SupportedFrameDurations( + FrameDurationSupport::BothNoPreference, + )], + metadata: vec![], + }, + ]) + .add_source(vec![]); + builder + } + + #[test] + fn build_server() { + let server = default_server_builder() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Conversational, ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new(HashSet::from([ContextType::Media]), HashSet::new()), + ) + .expect("should succeed"); + assert_eq!(server.published_audio_capabilities.len(), 2); + + assert_eq!(server.supported_audio_contexts.handle.0, 1); + assert_eq!( + server.supported_audio_contexts.sink, + HashSet::from([ContextType::Conversational, ContextType::Media]) + ); + assert_eq!(server.supported_audio_contexts.source, HashSet::from([ContextType::Media])); + + assert_eq!(server.available_audio_contexts.handle.0, 2); + assert_eq!( + server.available_audio_contexts.sink, + AvailableContexts::Available(HashSet::from([ContextType::Media])) + ); + assert_eq!(server.available_audio_contexts.source, AvailableContexts::NotAvailable); + + // Should have 1 sink PAC characteristic with audio locations. + let location_char = server.sink_audio_locations.as_ref().expect("should exist"); + assert_eq!(location_char.handle.0, 3); + assert_eq!( + location_char.locations.locations, + HashSet::from([AudioLocation::FrontLeft, AudioLocation::FrontRight]) + ); + + let mut sink_iter = + server.published_audio_capabilities.iter().filter(|(_handle, pac)| pac.is_sink()); + let sink_char = sink_iter.next().expect("should exist"); + assert_eq!(sink_char.0, &Handle(4)); + assert_eq!(sink_char.1.pac_records().len(), 1); + assert!(sink_iter.next().is_none()); + + // Should have 1 source PAC characteristic w/o audio locations. + assert!(server.source_audio_locations.is_none()); + let mut source_iter = + server.published_audio_capabilities.iter().filter(|(_handle, pac)| pac.is_source()); + let source_char = source_iter.next().expect("should exist"); + assert_eq!(source_char.0, &Handle(5)); + assert_eq!(source_char.1.pac_records().len(), 2); + assert_eq!(source_iter.next(), None); + } + + #[test] + fn build_server_error() { + // No sink or source PACs. + assert!( + ServerBuilder::new() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Conversational, ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new(HashSet::from([ContextType::Media]), HashSet::new()), + ) + .is_err() + ); + + // Sink audio context in available not in supported. + assert!( + default_server_builder() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Conversational, ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new(HashSet::from([ContextType::Alerts]), HashSet::new()), + ) + .is_err() + ); + + // Sink audio context in available not in supported. + assert!( + default_server_builder() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Conversational, ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new( + HashSet::from([]), + HashSet::from([ContextType::EmergencyAlarm]) + ), + ) + .is_err() + ); + } + + #[test] + fn publish_server() { + let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); + + let mut server = default_server_builder() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new(HashSet::new(), HashSet::new()), + ) + .unwrap(); + + // Server should be pending still since GATT server not establihsed. + let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else { + panic!("Should be pending"); + }; + + let (fake_gatt_server, mut event_receiver) = FakeServer::new(); + + // Event stream should be pending still since service not published. + let mut event_stream = event_receiver.next(); + let Poll::Pending = event_stream.poll_unpin(&mut noop_cx) else { + panic!("Should be pending"); + }; + + let _ = server.publish(fake_gatt_server).expect("should succeed"); + + // Server should poll on local server state. + let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else { + panic!("Should be pending"); + }; + + // Should receive event that GATT service was published. + let Poll::Ready(Some(FakeServerEvent::Published { id, definition })) = + event_stream.poll_unpin(&mut noop_cx) + else { + panic!("Should be published"); + }; + assert_eq!(id, server::ServiceId::new(1)); + assert_eq!(definition.characteristics().collect::<Vec<_>>().len(), 5); + assert_eq!(definition.kind(), ServiceKind::Primary); + assert_eq!(definition.uuid(), crate::PACS_UUID); + + // Server can only be published once. + let (fake_gatt_server, _) = FakeServer::new(); + assert!(server.publish(fake_gatt_server).is_err()); + } + + #[test] + fn read_from_server() { + let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); + + let mut server = default_server_builder() + .build::<FakeTypes>( + AudioContexts::new( + HashSet::from([ContextType::Media]), + HashSet::from([ContextType::Media]), + ), + AudioContexts::new(HashSet::from([ContextType::Media]), HashSet::new()), + ) + .unwrap(); + + let (fake_gatt_server, mut event_receiver) = FakeServer::new(); + let _ = server.publish(fake_gatt_server.clone()).expect("should succeed"); + + // Server should poll on local server state. + let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else { + panic!("Should be pending"); + }; + + // Should receive event that GATT service was published. + let mut event_stream = event_receiver.next(); + let Poll::Ready(Some(FakeServerEvent::Published { id, .. })) = + event_stream.poll_unpin(&mut noop_cx) + else { + panic!("Should be published"); + }; + + // Fake an incoming read from a remote peer. + let available_char_handle = server.available_audio_contexts.handle; + fake_gatt_server.incoming_read(PeerId(0x01), id, available_char_handle, 0); + + // Server should still be pending. + let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else { + panic!("Should be pending"); + }; + + // We should received read response. + let Poll::Ready(Some(FakeServerEvent::ReadResponded { handle, value, .. })) = + event_stream.poll_unpin(&mut noop_cx) + else { + panic!("Should be published"); + }; + assert_eq!(handle, available_char_handle); + assert_eq!(value.expect("should be ok"), vec![0x04, 0x00, 0x00, 0x00]); + } +}
diff --git a/rust/bt-pacs/src/server/types.rs b/rust/bt-pacs/src/server/types.rs new file mode 100644 index 0000000..db45e52 --- /dev/null +++ b/rust/bt-pacs/src/server/types.rs
@@ -0,0 +1,200 @@ +// 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. + +//! Define types and trait implementations specific to the PACS server. + +use bt_gatt::client::FromCharacteristic; +use bt_gatt::types::{ + AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, SecurityLevels, +}; +use bt_gatt::Characteristic; + +use std::collections::HashSet; + +use crate::*; + +pub(crate) const SUPPORTED_AUDIO_CONTEXTS_HANDLE: Handle = Handle(1); +pub(crate) const AVAILABLE_AUDIO_CONTEXTS_HANDLE: Handle = Handle(2); +pub(crate) const HANDLE_OFFSET: u64 = 3; + +impl From<&SupportedAudioContexts> for Characteristic { + fn from(_value: &SupportedAudioContexts) -> Self { + // TODO(b/309015071): implement optional properties. + let properties: CharacteristicProperties = CharacteristicProperty::Read.into(); + + Characteristic { + handle: SUPPORTED_AUDIO_CONTEXTS_HANDLE, + uuid: <SupportedAudioContexts as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +impl From<&AvailableAudioContexts> for Characteristic { + fn from(_value: &AvailableAudioContexts) -> Self { + let properties = CharacteristicProperty::Read | CharacteristicProperty::Notify; + + Characteristic { + handle: AVAILABLE_AUDIO_CONTEXTS_HANDLE, + uuid: <AvailableAudioContexts as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +impl From<&SourcePac> for Characteristic { + fn from(value: &SourcePac) -> Self { + // TODO(b/309015071): implement optional properties. + let properties: CharacteristicProperties = CharacteristicProperty::Read.into(); + + Characteristic { + handle: value.handle, + uuid: <SourcePac as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +impl From<&SinkPac> for Characteristic { + fn from(value: &SinkPac) -> Self { + // TODO(b/309015071): implement optional properties. + let properties: CharacteristicProperties = CharacteristicProperty::Read.into(); + + Characteristic { + handle: value.handle, + uuid: <SinkPac as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +impl From<&SourceAudioLocations> for Characteristic { + fn from(value: &SourceAudioLocations) -> Self { + // TODO(b/309015071): implement optional properties. + let properties: CharacteristicProperties = CharacteristicProperty::Read.into(); + + Characteristic { + handle: value.handle, + uuid: <SourceAudioLocations as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +impl From<&SinkAudioLocations> for Characteristic { + fn from(value: &SinkAudioLocations) -> Self { + // TODO(b/309015071): implement optional properties. + let properties: CharacteristicProperties = CharacteristicProperty::Read.into(); + + Characteristic { + handle: value.handle, + uuid: <SinkAudioLocations as FromCharacteristic>::UUID, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } + } +} + +#[derive(Default)] +pub struct AudioContexts { + pub(crate) sink: HashSet<ContextType>, + pub(crate) source: HashSet<ContextType>, +} + +impl AudioContexts { + #[cfg(test)] + pub fn new(sink: HashSet<ContextType>, source: HashSet<ContextType>) -> Self { + AudioContexts { sink, source } + } +} + +/// A single PAC characteristic consists of 1 or more PAC records. +pub type PacRecords = Vec<PacRecord>; + +#[derive(Debug, PartialEq)] +pub(crate) enum PublishedAudioCapability { + Sink(SinkPac), + Source(SourcePac), +} + +impl PublishedAudioCapability { + pub fn new_sink(handle: Handle, records: PacRecords) -> Self { + Self::Sink(SinkPac { handle: handle, capabilities: records }) + } + + pub fn new_source(handle: Handle, records: PacRecords) -> Self { + Self::Source(SourcePac { handle: handle, capabilities: records }) + } + + #[cfg(test)] + pub fn is_sink(&self) -> bool { + match self { + PublishedAudioCapability::Sink(_) => true, + PublishedAudioCapability::Source(_) => false, + } + } + + #[cfg(test)] + pub fn is_source(&self) -> bool { + match self { + PublishedAudioCapability::Sink(_) => false, + PublishedAudioCapability::Source(_) => true, + } + } + + #[cfg(test)] + pub fn pac_records(&self) -> &Vec<PacRecord> { + match self { + PublishedAudioCapability::Sink(pac) => &pac.capabilities, + PublishedAudioCapability::Source(pac) => &pac.capabilities, + } + } + + /// Encode into PAC characteristic format as defined in PACS v1.0.1 + /// Table 3.2/3.4. + pub(crate) fn encode(&self) -> Vec<u8> { + match self { + PublishedAudioCapability::Sink(pac) => pac_records_into_char_value(&pac.capabilities), + PublishedAudioCapability::Source(pac) => pac_records_into_char_value(&pac.capabilities), + } + } +} + +impl From<&PublishedAudioCapability> for Characteristic { + fn from(value: &PublishedAudioCapability) -> Self { + match value { + PublishedAudioCapability::Sink(pac) => pac.into(), + PublishedAudioCapability::Source(pac) => pac.into(), + } + } +}