[bt-pacs] Initial PACS crate Add bt_gatt::client::FromCharacteristic as utility to be used in a later change, as a convenient way to define decoding and updating structures defined directly from characteristic values. Add Characteristic types and deocding. Bug: b/308483293 Test: cargo test Change-Id: If56d06670eb58895d30c63b23194228282ac59cf Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1321 Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-common/src/generic_audio.rs b/rust/bt-common/src/generic_audio.rs index c2f72f0..f6e33dd 100644 --- a/rust/bt-common/src/generic_audio.rs +++ b/rust/bt-common/src/generic_audio.rs
@@ -5,3 +5,148 @@ pub mod codec_capabilities; pub mod metadata_ltv; + +use crate::{codable_as_bitmask, decodable_enum}; + +// Source: +// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/profiles_and_services/generic_audio/context_type.yaml +decodable_enum! { + pub enum ContextType<u16, crate::packet_encoding::Error, OutOfRange> { + Unspecified = 0x0001, + Conversational = 0x0002, + Media = 0x0004, + Game = 0x0008, + Instructional = 0x0010, + VoiceAssistants = 0x0020, + Live = 0x0040, + SoundEffects = 0x0080, + Notifications = 0x0100, + Ringtone = 0x0200, + Alerts = 0x0400, + EmergencyAlarm = 0x0800, + } +} + +codable_as_bitmask!(ContextType, u16); + +// Source: +// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/profiles_and_services/generic_audio/audio_location_definitions.yaml +// Regexp magic for quick variants: +// %s/ - value: \(\S\+\)\n audio_location: \(.*\)\n/\2 = \1\r,/g +// with subsequent removal of Spaces +decodable_enum! { + pub enum AudioLocation<u32, crate::packet_encoding::Error, OutOfRange> { + FrontLeft = 0x00000001, + FrontRight = 0x00000002, + FrontCenter = 0x00000004, + LowFrequencyEffects1 = 0x00000008, + BackLeft = 0x00000010, + BackRight = 0x00000020, + FrontLeftOfCenter = 0x00000040, + FrontRightOfCenter = 0x00000080, + BackCenter = 0x00000100, + LowFrequencyEffects2 = 0x00000200, + SideLeft = 0x00000400, + SideRight = 0x00000800, + TopFrontLeft = 0x00001000, + TopFrontRight = 0x00002000, + TopFrontCenter = 0x00004000, + TopCenter = 0x00008000, + TopBackLeft = 0x00010000, + TopBackRight = 0x00020000, + TopSideLeft = 0x00040000, + TopSideRight = 0x00080000, + TopBackCenter = 0x00100000, + BottomFrontCenter = 0x00200000, + BottomFrontLeft = 0x00400000, + BottomFrontRight = 0x00800000, + FrontLeftWide = 0x01000000, + FrontRightWide = 0x02000000, + LeftSurround = 0x04000000, + RightSurround = 0x08000000, + } +} + +codable_as_bitmask!(AudioLocation, u32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn locations_decodable() { + let five_point_one = 0b111111; + + let locations: std::collections::HashSet<AudioLocation> = + AudioLocation::from_bits(five_point_one).collect(); + + assert_eq!(6, locations.len()); + + let expected_locations = [ + AudioLocation::FrontLeft, + AudioLocation::FrontRight, + AudioLocation::FrontCenter, + AudioLocation::LowFrequencyEffects1, + AudioLocation::BackLeft, + AudioLocation::BackRight, + ] + .into_iter() + .collect(); + + assert_eq!(locations, expected_locations); + + assert_eq!(AudioLocation::try_from(0x4), Ok(AudioLocation::FrontCenter)); + + // Directly decoding a location that is not a single bit is an error. + assert!(AudioLocation::try_from(0b1010101).is_err()); + } + + #[test] + fn locations_encodable() { + let locations_missing_sub = [ + AudioLocation::FrontLeft, + AudioLocation::FrontRight, + AudioLocation::FrontCenter, + AudioLocation::BackLeft, + AudioLocation::BackRight, + ]; + + let value = AudioLocation::to_bits(locations_missing_sub.iter()); + + assert_eq!(0b110111, value); + } + + #[test] + fn context_type_decodable() { + let contexts: Vec<ContextType> = ContextType::from_bits(0b10).collect(); + assert_eq!(contexts.len(), 1); + assert_eq!(contexts[0], ContextType::Conversational); + + let live_and_instructional = 0b1010000; + let contexts: std::collections::HashSet<ContextType> = + ContextType::from_bits(live_and_instructional).collect(); + + assert_eq!(contexts.len(), 2); + assert_eq!(contexts, [ContextType::Live, ContextType::Instructional].into_iter().collect()); + + let alerts_and_conversational = 0x0402; + + let contexts: std::collections::HashSet<ContextType> = + ContextType::from_bits(alerts_and_conversational).collect(); + + assert_eq!(contexts.len(), 2); + assert_eq!( + contexts, + [ContextType::Alerts, ContextType::Conversational].into_iter().collect() + ); + } + + #[test] + fn context_type_encodable() { + let contexts = [ContextType::Notifications, ContextType::SoundEffects, ContextType::Game]; + + let value = ContextType::to_bits(contexts.iter()); + + assert_eq!(0x188, value); + } +}
diff --git a/rust/bt-common/src/packet_encoding.rs b/rust/bt-common/src/packet_encoding.rs index 978430b..10058b8 100644 --- a/rust/bt-common/src/packet_encoding.rs +++ b/rust/bt-common/src/packet_encoding.rs
@@ -26,6 +26,90 @@ fn encode(&self, buf: &mut [u8]) -> ::core::result::Result<(), Self::Error>; } +/// Generates an enum value where each variant can be converted into a constant in the given +/// raw_type. +/// +/// For example: +/// decodable_enum! { +/// pub(crate) enum Color<u8, MyError, Variant> { +/// Red = 1, +/// Blue = 2, +/// Green = 3, +/// } +/// } +/// +/// Color::try_from(2) -> Color::Red +/// u8::from(&Color::Red) -> 1. +#[macro_export] +macro_rules! decodable_enum { + ($(#[$meta:meta])* $visibility:vis enum $name:ident< + $raw_type:ty, + $error_type:ty, + $error_path:ident + > { + $($(#[$variant_meta:meta])* $variant:ident = $val:expr),*, + }) => { + $(#[$meta])* + #[derive( + ::core::clone::Clone, + ::core::marker::Copy, + ::core::fmt::Debug, + ::core::cmp::Eq, + ::core::hash::Hash, + ::core::cmp::PartialEq)] + $visibility enum $name { + $($(#[$variant_meta])* $variant = $val),* + } + + impl $name { + pub const VALUES : &'static [$raw_type] = &[$($val),*,]; + pub const VARIANTS : &'static [$name] = &[$($name::$variant),*,]; + pub fn name(&self) -> &'static ::core::primitive::str { + match self { + $($name::$variant => ::core::stringify!($variant)),* + } + } + } + + impl ::core::convert::From<&$name> for $raw_type { + fn from(v: &$name) -> $raw_type { + match v { + $($name::$variant => $val),*, + } + } + } + + impl ::core::convert::TryFrom<$raw_type> for $name { + type Error = $error_type; + + fn try_from(value: $raw_type) -> ::core::result::Result<Self, $error_type> { + match value { + $($val => ::core::result::Result::Ok($name::$variant)),*, + _ => ::core::result::Result::Err(<$error_type>::$error_path), + } + } + } + } +} + +#[macro_export] +macro_rules! codable_as_bitmask { + ($type:ty, $raw_type:ty) => { + impl $type { + pub fn from_bits(v: $raw_type) -> impl Iterator<Item = $type> { + (0..<$raw_type>::BITS) + .map(|bit| 1 << bit) + .filter(move |val| (v & val) != 0) + .filter_map(|val| val.try_into().ok()) + } + + pub fn to_bits<'a>(it: impl Iterator<Item = &'a $type>) -> $raw_type { + it.fold(0, |acc, item| acc | Into::<$raw_type>::into(item)) + } + } + }; +} + #[derive(Error, Debug, PartialEq)] pub enum Error { #[error("Parameter is not valid: {0}")]
diff --git a/rust/bt-gatt/src/client/mod.rs b/rust/bt-gatt/src/client/mod.rs index 8415f69..e34c8ae 100644 --- a/rust/bt-gatt/src/client/mod.rs +++ b/rust/bt-gatt/src/client/mod.rs
@@ -12,11 +12,12 @@ Secondary, } -// A short definition of a service that is either being published or has been discovered on a -// peer. +// A short definition of a service that is either being published or has been +// discovered on a peer. pub struct PeerServiceDefinition { /// Service Handle - /// When publishing services, unique among all services published with [`Server::publish`]. + /// When publishing services, unique among all services published with + /// [`Server::publish`]. pub id: u64, /// Whether the service is marked as Primary in the GATT server. pub kind: ServiceKind, @@ -25,7 +26,8 @@ } /// GATT Client connected to a particular peer. -/// Holding a struct that implements this should attempt to maintain a LE connection to the peer. +/// Holding a struct that implements this should attempt to maintain a LE +/// connection to the peer. pub trait Client { type PeerServiceHandleT: PeerServiceHandle; type ServiceResultFut: Future<Output = Result<Vec<Self::PeerServiceHandleT>>> + 'static; @@ -34,8 +36,8 @@ fn peer_id(&self) -> PeerId; /// Find services by UUID on the peer. - /// This may cause as much as a full discovery of all services on the peer if the stack deems it - /// appropriate. + /// This may cause as much as a full discovery of all services on the peer + /// if the stack deems it appropriate. /// Service information should be up to date at the time returned. fn find_service(&self, uuid: Uuid) -> Self::ServiceResultFut; } @@ -49,7 +51,25 @@ fn connect(&self) -> Self::ConnectFut; } -#[derive(Debug)] +/// Implement when a type can be deserialized from a characteristic value. +pub trait FromCharacteristic: Sized { + const UUID: Uuid; + + /// Create this type from a Characteristic and an initial value. + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> ::core::result::Result<Self, bt_common::packet_encoding::Error>; + + /// Attempt to update the type when supplied with the `new_value`, which may + /// or may not be the complete value. + fn update( + &mut self, + new_value: &[u8], + ) -> ::core::result::Result<&mut Self, bt_common::packet_encoding::Error>; +} + +#[derive(Debug, Clone)] pub struct CharacteristicNotification { pub handle: Handle, pub value: Vec<u8>, @@ -65,15 +85,17 @@ type WriteFut<'a>: Future<Output = Result<()>> + 'a; /// Discover characteristics on this service. - /// If `uuid` is provided, only the characteristics matching `uuid` will be returned. - /// This operation may use either the Discovery All Characteristics of a Service or - /// Discovery Characteristic by UUID procedures, regardless of `uuid`. + /// If `uuid` is provided, only the characteristics matching `uuid` will be + /// returned. This operation may use either the Discovery All + /// Characteristics of a Service or Discovery Characteristic by UUID + /// procedures, regardless of `uuid`. fn discover_characteristics(&self, uuid: Option<Uuid>) -> Self::CharacteristicsFut; - /// Read a characteristic into a buffer, given the handle within the service. - /// On success, returns the size read and whether the value may have been truncated. - /// By default this will try to use a long read if the `buf` is larger than a normal read will - /// allow (22 bytes) or if the offset is non-zero. + /// Read a characteristic into a buffer, given the handle within the + /// service. On success, returns the size read and whether the value may + /// have been truncated. By default this will try to use a long read if + /// the `buf` is larger than a normal read will allow (22 bytes) or if + /// the offset is non-zero. fn read_characteristic<'a>( &self, handle: &Handle, @@ -104,12 +126,12 @@ ) -> Self::WriteFut<'a>; /// Subscribe to updates on a Characteristic. - /// Either notifications or indications will be enabled depending on the properties available, - /// with indications preferred if they are supported. - /// Fails if the Characteristic doesn't support indications or notifications. - /// Errors are delivered through an Err item in the stream. - /// This will often write to the Client Characteristic Configuration descriptor for the - /// Characteristic subscribed to. + /// Either notifications or indications will be enabled depending on the + /// properties available, with indications preferred if they are + /// supported. Fails if the Characteristic doesn't support indications + /// or notifications. Errors are delivered through an Err item in the + /// stream. This will often write to the Client Characteristic + /// Configuration descriptor for the Characteristic subscribed to. /// Updates sent from the peer wlil be delivered to the Stream returned. fn subscribe(&self, handle: &Handle) -> Self::NotificationStream; } @@ -127,14 +149,7 @@ uuid: Uuid, ) -> Result<Vec<ServiceCharacteristic<'a, PeerServiceT>>> { let chrs = service.discover_characteristics(Some(uuid)).await?; - Ok(chrs - .into_iter() - .map(|characteristic| Self { - service, - characteristic, - uuid, - }) - .collect()) + Ok(chrs.into_iter().map(|characteristic| Self { service, characteristic, uuid }).collect()) } } @@ -154,9 +169,6 @@ impl<'a, PeerServiceT: PeerService> ServiceCharacteristic<'a, PeerServiceT> { pub async fn read(&self, buf: &mut [u8]) -> Result<usize> { - self.service - .read_characteristic(self.handle(), 0, buf) - .await - .map(|(bytes, _)| bytes) + self.service.read_characteristic(self.handle(), 0, buf).await.map(|(bytes, _)| bytes) } }
diff --git a/rust/bt-gatt/src/types/mod.rs b/rust/bt-gatt/src/types/mod.rs index e6eda85..d07c186 100644 --- a/rust/bt-gatt/src/types/mod.rs +++ b/rust/bt-gatt/src/types/mod.rs
@@ -5,8 +5,9 @@ use bt_common::{PeerId, Uuid}; use thiserror::Error; -/// Errors that can be returned from GATT procedures. These errors are sent from the peer. -/// These are defined to match the Bluetooth Core Spec (v5.4, Vol 3, Part F, Sec 3.4.1.1) +/// Errors that can be returned from GATT procedures. These errors are sent from +/// the peer. These are defined to match the Bluetooth Core Spec (v5.4, Vol 3, +/// Part F, Sec 3.4.1.1) #[derive(Debug, Copy, Clone)] pub enum GattError { InvalidHandle = 1, @@ -150,10 +151,10 @@ pub type Result<T> = core::result::Result<T, Error>; /// Handles are used as opaque identifiers for Characteristics and Descriptors. -/// Their value should be treated as opaque by clients of PeerService, and are explicitly not -/// guaranteed to be equal to a peer's attribute handles. -/// Stack implementations should provide unique handles for each Characteristic and Descriptor -/// within a PeerService. +/// Their value should be treated as opaque by clients of PeerService, and are +/// explicitly not guaranteed to be equal to a peer's attribute handles. +/// Stack implementations should provide unique handles for each Characteristic +/// and Descriptor within a PeerService. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Handle(pub u64); @@ -163,8 +164,11 @@ WithoutResponse, } -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u16)] +/// Characteristic Properties, including Extended Properties +/// Determines how the Characteristic Value may be used. +/// +/// Defined in Core Spec 5.3, Vol 3 Part G Sec 3.3.1.1 and 3.3.3.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CharacteristicProperty { Broadcast = 0x01, Read = 0x02, @@ -177,7 +181,7 @@ WritableAuxiliaries = 0x200, } -#[derive(Clone, Debug)] +#[derive(Default, Debug, Clone)] pub struct CharacteristicProperties(pub Vec<CharacteristicProperty>); #[derive(Debug, Clone, Copy, Default)] @@ -200,25 +204,27 @@ #[derive(Debug, Clone, Copy, Default)] pub struct AttributePermissions { - /// If None, then this cannot be read. Otherwise the SecurityLevels given are required to read. + /// If None, then this cannot be read. Otherwise the SecurityLevels given + /// are required to read. pub(crate) read: Option<SecurityLevels>, - /// If None, then this cannot be written. Otherwise the SecurityLevels given are required to - /// write. + /// If None, then this cannot be written. Otherwise the SecurityLevels given + /// are required to write. pub(crate) write: Option<SecurityLevels>, - /// If None, then this cannot be updated. Otherwise the SecurityLevels given are required to - /// update. + /// If None, then this cannot be updated. Otherwise the SecurityLevels given + /// are required to update. pub(crate) update: Option<SecurityLevels>, } -/// 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 using [`PeerService::read_descriptor`]. +/// 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 +/// using [`PeerService::read_descriptor`]. /// -/// Characteristic Extended Properties are included in the CharacteristicProperties of the -/// Characteristic. -/// This is a subset of the descriptors defined in the Core Specification (v5.4, Vol 3 Part G Sec -/// 3.3.3). Missing DescriptorTypes are handled internally and will be omitted from descriptor -/// APIs. +/// Characteristic Extended Properties are included in the +/// CharacteristicProperties of the Characteristic. +/// This is a subset of the descriptors defined in the Core Specification (v5.4, +/// Vol 3 Part G Sec 3.3.3). Missing DescriptorTypes are handled internally and +/// will be omitted from descriptor APIs. #[derive(Clone, Debug)] pub enum DescriptorType { UserDescription(String), @@ -240,14 +246,14 @@ #[derive(Clone, Debug)] pub struct Descriptor { pub handle: Handle, - /// Permissions required needed to interact with this descriptor. May not be accurate on - /// clients until the descriptor is interacted with. + /// Permissions required needed to interact with this descriptor. May not + /// be accurate on clients until the descriptor is interacted with. pub permissions: AttributePermissions, pub r#type: DescriptorType, } -/// A Characteristic on a Service. Each Characteristic has a declaration, value, and zero or more -/// decriptors. +/// A Characteristic on a Service. Each Characteristic has a declaration, value, +/// and zero or more decriptors. #[derive(Clone, Debug)] pub struct Characteristic { pub handle: Handle,
diff --git a/rust/bt-pacs/.gitignore b/rust/bt-pacs/.gitignore new file mode 100644 index 0000000..438e2b1 --- /dev/null +++ b/rust/bt-pacs/.gitignore
@@ -0,0 +1,17 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Vim swap files. +*.swp
diff --git a/rust/bt-pacs/Cargo.toml b/rust/bt-pacs/Cargo.toml new file mode 100644 index 0000000..9a471b2 --- /dev/null +++ b/rust/bt-pacs/Cargo.toml
@@ -0,0 +1,15 @@ +[package] +name = "bt-pacs" +description = "Client and server library for the Published Audio Capabilities Service" +version = "0.0.1" +edition = "2021" +license = "BSD-2-Clause" + +[dependencies] + +### In-tree dependencies +bt-gatt = { path = "../bt-gatt" } +bt-common = { path = "../bt-common" } + +[dev-dependencies] +pretty_assertions = "1"
diff --git a/rust/bt-pacs/LICENSE b/rust/bt-pacs/LICENSE new file mode 100644 index 0000000..e5de3c8 --- /dev/null +++ b/rust/bt-pacs/LICENSE
@@ -0,0 +1,24 @@ +Copyright 2023 Google LLC + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/rust/bt-pacs/PATENTS b/rust/bt-pacs/PATENTS new file mode 100644 index 0000000..3a21f11 --- /dev/null +++ b/rust/bt-pacs/PATENTS
@@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of this crate + +Google hereby grants to you a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, +import, transfer, and otherwise run, modify and propagate the contents +of this implementation, where such license applies only to +those patent claims, both currently owned by Google and acquired in +the future, licensable by Google that are necessarily infringed by +this implementation. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute +or order or agree to the institution of patent litigation or any other +patent enforcement activity against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation +shall terminate as of the date such litigation is filed.
diff --git a/rust/bt-pacs/src/lib.rs b/rust/bt-pacs/src/lib.rs new file mode 100644 index 0000000..b371697 --- /dev/null +++ b/rust/bt-pacs/src/lib.rs
@@ -0,0 +1,581 @@ +// Copyright 2023 Google LLC +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use bt_common::core::ltv::LtValue; +use bt_common::generic_audio::codec_capabilities::CodecCapability; +use bt_common::generic_audio::{AudioLocation, ContextType}; +use bt_common::packet_encoding::Decodable; +use bt_common::Uuid; +use bt_gatt::{client::FromCharacteristic, Characteristic}; + +/// Codec_ID communicated by the Published Audio Capabilities Service +/// From Section 3.1 of the Spec. +/// Used in [`PacRecord`] +#[derive(Debug, Clone, PartialEq)] +pub enum CodecId { + /// From the Assigned Numbers. Format will not be + /// `CodingFormat::VendorSpecific` + Assigned(bt_common::core::CodingFormat), + VendorSpecific { + company_id: bt_common::CompanyId, + vendor_specific_codec_id: u16, + }, +} + +impl bt_common::packet_encoding::Decodable for CodecId { + type Error = bt_common::packet_encoding::Error; + + fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { + if buf.len() < 5 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let format = buf[0].into(); + if format != bt_common::core::CodingFormat::VendorSpecific { + // Maybe don't ignore the company and vendor id, and check if they are wrong. + return Ok((Self::Assigned(format), 5)); + } + let company_id = u16::from_le_bytes([buf[1], buf[2]]).into(); + let vendor_specific_codec_id = u16::from_le_bytes([buf[3], buf[4]]); + Ok((Self::VendorSpecific { company_id, vendor_specific_codec_id }, 5)) + } +} + +/// A Published Audio Capability (PAC) record. +/// Published Audio Capabilities represent the capabilities of a given peer to +/// transmit or receive Audio capabilities, exposed in PAC records, represent +/// the server audio capabilities independent of available resources at any +/// given time. Audio capabilities do not distinguish between unicast +/// Audio Streams or broadcast Audio Streams. +#[derive(Debug, Clone, PartialEq)] +pub struct PacRecord { + pub codec_id: CodecId, + pub codec_specific_capabilities: Vec<CodecCapability>, + // TODO: Actually parse the metadata once Metadata + pub metadata: (), +} + +impl bt_common::packet_encoding::Decodable for PacRecord { + type Error = bt_common::packet_encoding::Error; + + fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { + let mut idx = 0; + let (codec_id, consumed) = CodecId::decode(&buf[idx..])?; + idx += consumed; + let codec_specific_capabilites_length = buf[idx] as usize; + idx += 1; + let (results, consumed) = + CodecCapability::decode_all(&buf[idx..idx + codec_specific_capabilites_length]); + if consumed != codec_specific_capabilites_length { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let codec_specific_capabilities = results.into_iter().filter_map(Result::ok).collect(); + idx += consumed; + + let metadata_length = buf[idx]; + idx += 1; + // TODO: Actually parse the Metadata when the type is included in bt_common + /* + let (results, consumed) = Metadatum::decode_all(&buf[idx..idx + metadata_length]); + if consumed != metadata_length { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let metadata = results.into_iter().filter(Result::ok).collect(); + idx += consumed; + */ + idx += metadata_length as usize; + + Ok((Self { codec_id, codec_specific_capabilities, metadata: () }, idx)) + } +} + +/// 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>, +} + +fn pac_records_from_bytes( + value: &[u8], +) -> Result<Vec<PacRecord>, bt_common::packet_encoding::Error> { + if value.len() < 1 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let num_of_pac_records = value[0] as usize; + let mut next_idx = 1; + let mut capabilities = Vec::with_capacity(num_of_pac_records); + for _ in 0..num_of_pac_records { + let (cap, consumed) = PacRecord::decode(&value[next_idx..])?; + capabilities.push(cap); + next_idx += consumed; + } + Ok(capabilities) +} + +impl FromCharacteristic for SinkPac { + const UUID: Uuid = Uuid::from_u16(0x2BC9); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + let capabilities = pac_records_from_bytes(value)?; + Ok(Self { handle, capabilities }) + } + + fn update(&mut self, new_value: &[u8]) -> Result<&mut Self, bt_common::packet_encoding::Error> { + self.capabilities = pac_records_from_bytes(new_value)?; + Ok(self) + } +} + +/// 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 SourcePac { + pub handle: bt_gatt::types::Handle, + pub capabilities: Vec<PacRecord>, +} + +impl FromCharacteristic for SourcePac { + const UUID: Uuid = Uuid::from_u16(0x2BCB); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + let capabilities = pac_records_from_bytes(value)?; + Ok(Self { handle, capabilities }) + } + + fn update(&mut self, new_value: &[u8]) -> Result<&mut Self, bt_common::packet_encoding::Error> { + self.capabilities = pac_records_from_bytes(new_value)?; + Ok(self) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct AudioLocations { + pub locations: std::collections::HashSet<AudioLocation>, +} + +impl bt_common::packet_encoding::Decodable for AudioLocations { + type Error = bt_common::packet_encoding::Error; + + fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { + if buf.len() != 4 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let locations = + AudioLocation::from_bits(u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])) + .collect(); + Ok((AudioLocations { locations }, 4)) + } +} + +#[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 FromCharacteristic for SourceAudioLocations { + const UUID: Uuid = Uuid::from_u16(0x2BCC); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + let (locations, _) = AudioLocations::decode(value)?; + Ok(Self { handle, locations }) + } + + fn update(&mut self, new_value: &[u8]) -> Result<&mut Self, bt_common::packet_encoding::Error> { + self.locations = AudioLocations::decode(new_value)?.0; + Ok(self) + } +} + +impl FromCharacteristic for SinkAudioLocations { + const UUID: Uuid = Uuid::from_u16(0x2BCA); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + let (locations, _) = AudioLocations::decode(value)?; + Ok(Self { handle, locations }) + } + + fn update(&mut self, new_value: &[u8]) -> Result<&mut Self, bt_common::packet_encoding::Error> { + self.locations = AudioLocations::decode(new_value)?.0; + Ok(self) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum AvailableContexts { + NotAvailable, + Available(std::collections::HashSet<ContextType>), +} + +impl bt_common::packet_encoding::Decodable for AvailableContexts { + type Error = bt_common::packet_encoding::Error; + + fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { + if buf.len() < 2 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let encoded = u16::from_le_bytes([buf[0], buf[1]]); + if encoded == 0 { + Ok((Self::NotAvailable, 2)) + } else { + Ok((Self::Available(ContextType::from_bits(encoded).collect()), 2)) + } + } +} + +#[derive(Debug, Clone)] +pub struct AvailableAudioContexts { + pub handle: bt_gatt::types::Handle, + pub sink: AvailableContexts, + pub source: AvailableContexts, +} + +impl FromCharacteristic for AvailableAudioContexts { + const UUID: Uuid = Uuid::from_u16(0x28CD); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> core::result::Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + if value.len() < 4 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let sink = AvailableContexts::decode(&value[0..2])?.0; + let source = AvailableContexts::decode(&value[2..4])?.0; + Ok(Self { handle, sink, source }) + } + + fn update( + &mut self, + new_value: &[u8], + ) -> core::result::Result<&mut Self, bt_common::packet_encoding::Error> { + if new_value.len() != 4 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let sink = AvailableContexts::decode(&new_value[0..2])?.0; + let source = AvailableContexts::decode(&new_value[2..4])?.0; + self.sink = sink; + self.source = source; + Ok(self) + } +} + +#[derive(Debug, Clone)] +pub struct SupportedAudioContexts { + pub handle: bt_gatt::types::Handle, + pub sink: std::collections::HashSet<ContextType>, + pub source: std::collections::HashSet<ContextType>, +} + +impl FromCharacteristic for SupportedAudioContexts { + const UUID: Uuid = Uuid::from_u16(0x28CE); + + fn from_chr( + characteristic: Characteristic, + value: &[u8], + ) -> core::result::Result<Self, bt_common::packet_encoding::Error> { + let handle = characteristic.handle; + if value.len() < 4 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + let sink = ContextType::from_bits(u16::from_le_bytes([value[0], value[1]])).collect(); + let source = ContextType::from_bits(u16::from_le_bytes([value[2], value[3]])).collect(); + Ok(Self { handle, sink, source }) + } + + fn update( + &mut self, + new_value: &[u8], + ) -> core::result::Result<&mut Self, bt_common::packet_encoding::Error> { + if new_value.len() < 4 { + return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); + } + self.sink = + ContextType::from_bits(u16::from_le_bytes([new_value[0], new_value[1]])).collect(); + self.source = + ContextType::from_bits(u16::from_le_bytes([new_value[2], new_value[3]])).collect(); + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use bt_common::{ + generic_audio::codec_capabilities::{CodecCapabilityType, SamplingFrequency}, + Uuid, + }; + use bt_gatt::{ + types::{AttributePermissions, Handle}, + Characteristic, + }; + + use pretty_assertions::assert_eq; + + const SINGLE_PAC_SIMPLE: [u8; 12] = [ + 0x01, // Num of records; 1 + 0x06, // CodecID: codec LC3, + 0x00, 0x00, 0x00, 0x00, // CodecID: company and specific id (zero) + 0x04, // Len of Codec capabilities + 0x03, 0x01, // lt: supported_sampling_frequencies + 0xC0, 0x02, // Supported: 44.1kHz, 48kHz, 96kHz + 0x00, // Len of metadata + ]; + + const MULTIPLE_PAC_COMPLEX: [u8; 35] = [ + 0x02, // Num of records; 2 + 0x05, // CodecID: codec MSBC, + 0x00, 0x00, 0x00, 0x00, // CodecID: company and specific id (zero) + 0x0A, // Len of Codec capabilities (10) + 0x03, 0x01, // lt: supported_sampling_frequencies + 0xC0, 0x02, // Supported: 44.1kHz, 48kHz, 96kHz + 0x05, 0x04, // lt: Octets per codec frame + 0x11, 0x00, // Minimum: 9 + 0x00, 0x10, // Maximum: 4096 + 0x0A, // Len of metadata: 10 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0A, // Metadata (not parsed at this time) + 0xFF, // CodecId: Vendor specific + 0xE0, 0x00, // Google + 0x01, 0x10, // ID 4097 + 0x00, // Len of codec capabilities (none) + 0x00, // Len of metadata (none) + ]; + + #[track_caller] + fn assert_has_frequencies( + cap: &bt_common::generic_audio::codec_capabilities::CodecCapability, + freqs: &[SamplingFrequency], + ) { + let CodecCapability::SupportedSamplingFrequencies(set) = cap else { + unreachable!(); + }; + + for freq in freqs { + assert!(set.contains(freq)); + } + } + + #[test] + fn simple_sink_pac() { + let pac = SinkPac::from_chr( + Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x2BC9), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + &SINGLE_PAC_SIMPLE, + ) + .expect("should decode correctly"); + + assert_eq!(pac.capabilities.len(), 1); + let cap = &pac.capabilities[0]; + assert_eq!(cap.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Lc3)); + let freq_cap = cap + .codec_specific_capabilities + .iter() + .find(|c| c.into_type() == CodecCapabilityType::SupportedSamplingFrequencies) + .unwrap(); + assert_has_frequencies( + freq_cap, + &[ + SamplingFrequency::F44100Hz, + SamplingFrequency::F48000Hz, + SamplingFrequency::F96000Hz, + ], + ); + } + + #[test] + fn simple_source_pac() { + let pac = SinkPac::from_chr( + Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x2BC9), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + &SINGLE_PAC_SIMPLE, + ) + .expect("should decode correctly"); + + assert_eq!(pac.capabilities.len(), 1); + let cap = &pac.capabilities[0]; + assert_eq!(cap.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Lc3)); + let freq_cap = cap + .codec_specific_capabilities + .iter() + .find(|c| c.into_type() == CodecCapabilityType::SupportedSamplingFrequencies) + .unwrap(); + assert_has_frequencies( + freq_cap, + &[ + SamplingFrequency::F44100Hz, + SamplingFrequency::F48000Hz, + SamplingFrequency::F96000Hz, + ], + ); + } + + #[test] + fn complex_sink_pac() { + let pac = SinkPac::from_chr( + Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x2BC9), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + &MULTIPLE_PAC_COMPLEX, + ) + .expect("should decode correctly"); + assert_eq!(pac.capabilities.len(), 2); + let first = &pac.capabilities[0]; + assert_eq!(first.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Msbc)); + let freq_cap = first + .codec_specific_capabilities + .iter() + .find(|c| c.into_type() == CodecCapabilityType::SupportedSamplingFrequencies) + .unwrap(); + assert_has_frequencies( + freq_cap, + &[ + SamplingFrequency::F44100Hz, + SamplingFrequency::F48000Hz, + SamplingFrequency::F96000Hz, + ], + ); + + let second = &pac.capabilities[1]; + assert_eq!( + second.codec_id, + CodecId::VendorSpecific { + company_id: 0x00E0.into(), + vendor_specific_codec_id: 0x1001_u16, + } + ); + assert_eq!(second.codec_specific_capabilities.len(), 0); + } + + #[test] + fn available_contexts_no_sink() { + let available = AvailableAudioContexts::from_chr( + Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x28CD), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }, + &[0x00, 0x00, 0x02, 0x04], + ) + .expect("should decode correctly"); + assert_eq!(available.handle, Handle(1)); + assert_eq!(available.sink, AvailableContexts::NotAvailable); + let AvailableContexts::Available(a) = available.source else { + panic!("Source should be available"); + }; + assert_eq!(a, [ContextType::Conversational, ContextType::Alerts].into_iter().collect()); + } + + #[test] + fn available_contexts_wrong_size() { + let chr = Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x28CD), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }; + let _ = AvailableAudioContexts::from_chr(chr.clone(), &[0x00, 0x00, 0x02]) + .expect_err("should not decode with too short"); + + let available = + AvailableAudioContexts::from_chr(chr.clone(), &[0x00, 0x00, 0x02, 0x04, 0xCA, 0xFE]) + .expect("should attempt to decode with too long"); + + assert_eq!(available.sink, AvailableContexts::NotAvailable); + let AvailableContexts::Available(a) = available.source else { + panic!("Source should be available"); + }; + assert_eq!(a, [ContextType::Conversational, ContextType::Alerts].into_iter().collect()); + } + + #[test] + fn supported_contexts() { + let chr = Characteristic { + handle: Handle(1), + uuid: Uuid::from_u16(0x28CE), + properties: bt_gatt::types::CharacteristicProperties(vec![ + bt_gatt::types::CharacteristicProperty::Read, + ]), + permissions: AttributePermissions::default(), + descriptors: vec![], + }; + + let supported = + SupportedAudioContexts::from_chr(chr.clone(), &[0x00, 0x00, 0x00, 0x00]).unwrap(); + assert_eq!(supported.sink.len(), 0); + assert_eq!(supported.source.len(), 0); + + let supported = + SupportedAudioContexts::from_chr(chr.clone(), &[0x08, 0x06, 0x06, 0x03]).unwrap(); + assert_eq!(supported.sink.len(), 3); + assert_eq!(supported.source.len(), 4); + assert_eq!( + supported.source, + [ + ContextType::Media, + ContextType::Conversational, + ContextType::Ringtone, + ContextType::Notifications + ] + .into_iter() + .collect() + ); + } +}