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