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(),
+ }
+ }
+}