[bt-common] LtValue trait, CodecCapabilities
Add LtValue trait and helper functions to help with defining types
that are length-type-value structure.
Add CompanyId, CodingFormat from core spec.
Migrate core::fmt::Display to std::fmt::Display for PeerId to not
conflict with core mod.
Add packet_encoding mod.
Bug: b/308483293
Tests: cargo test
Change-Id: Ie4d4e26f83f632ee93853b62a2c1f41dc2b5c8a8
Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1320
Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-common/Cargo.toml b/rust/bt-common/Cargo.toml
index 0b1c788..497acbb 100644
--- a/rust/bt-common/Cargo.toml
+++ b/rust/bt-common/Cargo.toml
@@ -4,10 +4,10 @@
edition = "2021"
license = "BSD-2-Clause"
-[dependencies.uuid]
-version = "1.4.1"
-features = ["macro-diagnostics"]
-
[dependencies]
bitflags = "2.4.0"
thiserror = "1.0"
+
+[dependencies.uuid]
+version = "1.4.1"
+features = ["macro-diagnostics"]
diff --git a/rust/bt-common/src/company_id.rs b/rust/bt-common/src/company_id.rs
new file mode 100644
index 0000000..78addec
--- /dev/null
+++ b/rust/bt-common/src/company_id.rs
@@ -0,0 +1,28 @@
+// Copyright 2023 Google LLC
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// A Bluetooth Company ID, which is assigned by the Bluetotoh SIG.
+/// Company identifiers are unique numbers assigned by the Bluetooth SIG to
+/// member companies requesting one. Referenced in the Bluetooth Core Spec in
+/// many commands and formats. See the Assigned Number Document for a reference.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct CompanyId(u16);
+
+impl From<u16> for CompanyId {
+ fn from(value: u16) -> Self {
+ Self(value)
+ }
+}
+
+impl From<CompanyId> for u16 {
+ fn from(value: CompanyId) -> Self {
+ value.0
+ }
+}
+
+impl core::fmt::Display for CompanyId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "Company (ID {:02x})", self.0)
+ }
+}
diff --git a/rust/bt-common/src/core.rs b/rust/bt-common/src/core.rs
index 16bb6ec..f7d8c7b 100644
--- a/rust/bt-common/src/core.rs
+++ b/rust/bt-common/src/core.rs
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+/// Traits and utilities to handle length-type-value structures.
+pub mod ltv;
+
use crate::packet_encoding::{Encodable, Error as PacketError};
/// Represents the Advertising Set ID which is 1 byte long.
@@ -45,6 +48,74 @@
}
}
+/// Coding Format as defined by the Assigned Numbers Document. Section 2.11.
+/// Referenced in the Core Spec 5.3, Volume 4, Part E, Section 7 as well as
+/// various other profile specifications.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub enum CodingFormat {
+ MuLawLog,
+ ALawLog,
+ Cvsd,
+ Transparent,
+ LinearPcm,
+ Msbc,
+ Lc3,
+ G729a,
+ VendorSpecific,
+ Unrecognized(u8),
+}
+
+impl From<u8> for CodingFormat {
+ fn from(value: u8) -> Self {
+ match value {
+ 0x00 => Self::MuLawLog,
+ 0x01 => Self::ALawLog,
+ 0x02 => Self::Cvsd,
+ 0x03 => Self::Transparent,
+ 0x04 => Self::LinearPcm,
+ 0x05 => Self::Msbc,
+ 0x06 => Self::Lc3,
+ 0x07 => Self::G729a,
+ 0xFF => Self::VendorSpecific,
+ x => Self::Unrecognized(x),
+ }
+ }
+}
+
+impl From<CodingFormat> for u8 {
+ fn from(value: CodingFormat) -> Self {
+ match value {
+ CodingFormat::MuLawLog => 0x00,
+ CodingFormat::ALawLog => 0x01,
+ CodingFormat::Cvsd => 0x02,
+ CodingFormat::Transparent => 0x03,
+ CodingFormat::LinearPcm => 0x04,
+ CodingFormat::Msbc => 0x05,
+ CodingFormat::Lc3 => 0x06,
+ CodingFormat::G729a => 0x07,
+ CodingFormat::VendorSpecific => 0xFF,
+ CodingFormat::Unrecognized(x) => x,
+ }
+ }
+}
+
+impl core::fmt::Display for CodingFormat {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ CodingFormat::MuLawLog => write!(f, "µ-law log"),
+ CodingFormat::ALawLog => write!(f, "A-law log"),
+ CodingFormat::Cvsd => write!(f, "CVSD"),
+ CodingFormat::Transparent => write!(f, "Transparent"),
+ CodingFormat::LinearPcm => write!(f, "Linear PCM"),
+ CodingFormat::Msbc => write!(f, "mSBC"),
+ CodingFormat::Lc3 => write!(f, "LC3"),
+ CodingFormat::G729a => write!(f, "G.729A"),
+ CodingFormat::VendorSpecific => write!(f, "Vendor Specific"),
+ CodingFormat::Unrecognized(x) => write!(f, "Unrecognized ({x})"),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -60,7 +131,7 @@
#[test]
fn encode_pa_interval_fails() {
- let mut buf = [0; 1]; // Not enough buffer space.
+ let mut buf = [0; 1]; // Not enough buffer space.
let interval = PaInterval(0x1004);
interval.encode(&mut buf[..]).expect_err("should fail");
diff --git a/rust/bt-common/src/core/ltv.rs b/rust/bt-common/src/core/ltv.rs
new file mode 100644
index 0000000..5baab7d
--- /dev/null
+++ b/rust/bt-common/src/core/ltv.rs
@@ -0,0 +1,313 @@
+// 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 crate::packet_encoding::{Decodable, Encodable};
+
+/// Implement Ltv when a collection of types is represented in the Bluetooth
+/// specifications as a length-type-value structure. They should have an
+/// associated type which can be retrieved from a type byte.
+pub trait LtValue: Sized {
+ type Type: Into<u8> + Copy;
+
+ const NAME: &'static str;
+
+ /// Given a type octet, return the associated Type if it is possible.
+ /// Returns None if the value is unrecognized.
+ fn type_from_octet(x: u8) -> Option<Self::Type>;
+
+ /// Returns length bounds for the type indicated, **including** the type
+ /// byte. Note that the assigned numbers from the Bluetooth SIG include
+ /// the type byte in their Length specifications.
+ // TODO: use impl std::ops::RangeBounds when RPITIT is sufficiently stable
+ fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8>;
+
+ /// Retrieve the type of the current value.
+ fn into_type(&self) -> Self::Type;
+
+ /// The length of the encoded value, without the length and type byte.
+ /// This cannot be 255 in practice, as the length byte is only one octet
+ /// long.
+ fn value_encoded_len(&self) -> u8;
+
+ /// Decodes the value from a buffer, which does not include the type or
+ /// length bytes. The `buf` slice length is exactly what was specified
+ /// for this value in the encoded source.
+ fn decode_value(ty: &Self::Type, buf: &[u8]) -> Result<Self, crate::packet_encoding::Error>;
+
+ /// Encodes a value into `buf`, which is verified to be the correct length
+ /// as indicated by [LtValue::value_encoded_len].
+ fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error>;
+
+ /// Decode a collection of LtValue structures that are present in a buffer.
+ /// If it is possible to continue decoding after encountering an error, does
+ /// so and includes the error. If an unrecoverable error occurs, does
+ /// not consume the final item and the last element in the result is the
+ /// error.
+ fn decode_all(buf: &[u8]) -> (Vec<Result<Self, crate::packet_encoding::Error>>, usize) {
+ let mut results = Vec::new();
+ let mut total_consumed = 0;
+ loop {
+ if buf.len() <= total_consumed {
+ return (results, std::cmp::min(buf.len(), total_consumed));
+ }
+ let indicated_len = buf[total_consumed] as usize;
+ match Self::decode(&buf[total_consumed..=total_consumed + indicated_len]) {
+ Ok((item, consumed)) => {
+ results.push(Ok(item));
+ total_consumed += consumed;
+ }
+ Err(e @ crate::packet_encoding::Error::UnexpectedDataLength) => {
+ results.push(Err(e));
+ return (results, total_consumed);
+ }
+ Err(e) => {
+ results.push(Err(e));
+ // Consume the bytes
+ total_consumed += indicated_len + 1;
+ }
+ }
+ }
+ }
+}
+
+impl<T: LtValue> Encodable for T {
+ type Error = crate::packet_encoding::Error;
+
+ fn encoded_len(&self) -> core::primitive::usize {
+ 2 + self.value_encoded_len() as usize
+ }
+
+ fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> {
+ if buf.len() < self.encoded_len() {
+ return Err(crate::packet_encoding::Error::BufferTooSmall);
+ }
+ buf[0] = self.value_encoded_len() + 1;
+ buf[1] = self.into_type().into();
+ self.encode_value(&mut buf[2..self.encoded_len()])?;
+ Ok(())
+ }
+}
+
+impl<T: LtValue> Decodable for T {
+ type Error = crate::packet_encoding::Error;
+
+ fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> {
+ if buf.len() < 2 {
+ return Err(crate::packet_encoding::Error::UnexpectedDataLength);
+ }
+ let indicated_len = buf[0] as usize;
+ if buf.len() < indicated_len + 1 {
+ return Err(crate::packet_encoding::Error::UnexpectedDataLength);
+ }
+
+ let Some(ty) = Self::type_from_octet(buf[1]) else {
+ return Err(crate::packet_encoding::Error::UnrecognizedType(
+ Self::NAME.to_owned(),
+ buf[1],
+ ));
+ };
+
+ if !Self::length_range_from_type(ty).contains(&((buf.len() - 1) as u8)) {
+ return Err(crate::packet_encoding::Error::UnexpectedDataLength);
+ }
+
+ Ok((Self::decode_value(&ty, &buf[2..=indicated_len])?, indicated_len + 1))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[derive(Copy, Clone, PartialEq, Debug)]
+ enum TestType {
+ OneByte,
+ TwoBytes,
+ TwoBytesLittleEndian,
+ UnicodeString,
+ AlwaysError,
+ }
+
+ impl From<TestType> for u8 {
+ fn from(value: TestType) -> Self {
+ match value {
+ TestType::OneByte => 1,
+ TestType::TwoBytes => 2,
+ TestType::TwoBytesLittleEndian => 3,
+ TestType::UnicodeString => 4,
+ TestType::AlwaysError => 0xFF,
+ }
+ }
+ }
+
+ #[derive(PartialEq, Debug)]
+ enum TestValues {
+ OneByte(u8),
+ TwoBytes(u16),
+ TwoBytesLittleEndian(u16),
+ UnicodeString(String),
+ AlwaysError,
+ }
+
+ impl LtValue for TestValues {
+ type Type = TestType;
+
+ const NAME: &'static str = "TestValues";
+
+ fn type_from_octet(x: u8) -> Option<Self::Type> {
+ match x {
+ 1 => Some(TestType::OneByte),
+ 2 => Some(TestType::TwoBytes),
+ 3 => Some(TestType::TwoBytesLittleEndian),
+ 4 => Some(TestType::UnicodeString),
+ 0xFF => Some(TestType::AlwaysError),
+ _ => None,
+ }
+ }
+
+ fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8> {
+ match ty {
+ TestType::OneByte => 2..=2,
+ TestType::TwoBytes => 3..=3,
+ TestType::TwoBytesLittleEndian => 3..=3,
+ TestType::UnicodeString => 2..=255,
+ // AlwaysError fields can be any length (value will be thrown away)
+ TestType::AlwaysError => 1..=255,
+ }
+ }
+
+ fn into_type(&self) -> Self::Type {
+ match self {
+ TestValues::TwoBytes(_) => TestType::TwoBytes,
+ TestValues::TwoBytesLittleEndian(_) => TestType::TwoBytesLittleEndian,
+ TestValues::OneByte(_) => TestType::OneByte,
+ TestValues::UnicodeString(_) => TestType::UnicodeString,
+ TestValues::AlwaysError => TestType::AlwaysError,
+ }
+ }
+
+ fn value_encoded_len(&self) -> u8 {
+ match self {
+ TestValues::TwoBytes(_) => 2,
+ TestValues::TwoBytesLittleEndian(_) => 2,
+ TestValues::OneByte(_) => 1,
+ TestValues::UnicodeString(s) => s.len() as u8,
+ TestValues::AlwaysError => 0,
+ }
+ }
+
+ fn decode_value(
+ ty: &Self::Type,
+ buf: &[u8],
+ ) -> Result<Self, crate::packet_encoding::Error> {
+ match ty {
+ TestType::OneByte => Ok(TestValues::OneByte(buf[0])),
+ TestType::TwoBytes => {
+ Ok(TestValues::TwoBytes(u16::from_be_bytes([buf[0], buf[1]])))
+ }
+ TestType::TwoBytesLittleEndian => {
+ Ok(TestValues::TwoBytesLittleEndian(u16::from_le_bytes([buf[0], buf[1]])))
+ }
+ TestType::UnicodeString => {
+ Ok(TestValues::UnicodeString(String::from_utf8_lossy(buf).into_owned()))
+ }
+ TestType::AlwaysError => Err(crate::packet_encoding::Error::OutOfRange),
+ }
+ }
+
+ fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error> {
+ match self {
+ TestValues::TwoBytes(x) => {
+ [buf[0], buf[1]] = x.to_be_bytes();
+ }
+ TestValues::TwoBytesLittleEndian(x) => {
+ [buf[0], buf[1]] = x.to_le_bytes();
+ }
+ TestValues::OneByte(x) => buf[0] = *x,
+ TestValues::UnicodeString(s) => {
+ buf.copy_from_slice(s.as_bytes());
+ }
+ TestValues::AlwaysError => {
+ return Err(crate::packet_encoding::Error::InvalidParameter("test".to_owned()));
+ }
+ }
+ Ok(())
+ }
+ }
+
+ #[test]
+ fn decode_twobytes() {
+ let encoded = [0x03, 0x02, 0x10, 0x01, 0x03, 0x03, 0x10, 0x01];
+ let (decoded, consumed) = TestValues::decode_all(&encoded);
+ assert_eq!(consumed, encoded.len());
+ assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097)));
+ assert_eq!(decoded[1], Ok(TestValues::TwoBytesLittleEndian(272)));
+ }
+
+ #[test]
+ fn decode_unrecognized() {
+ let encoded = [0x03, 0x02, 0x10, 0x01, 0x03, 0x06, 0x10, 0x01];
+ let (decoded, consumed) = TestValues::decode_all(&encoded);
+ assert_eq!(consumed, encoded.len());
+ assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097)));
+ assert_eq!(
+ decoded[1],
+ Err(crate::packet_encoding::Error::UnrecognizedType("TestValues".to_owned(), 6))
+ );
+ }
+
+ #[track_caller]
+ fn u8char(c: char) -> u8 {
+ c.try_into().unwrap()
+ }
+
+ #[test]
+ fn decode_variable_lengths() {
+ let encoded = [
+ 0x03,
+ 0x02,
+ 0x10,
+ 0x01,
+ 0x0A,
+ 0x04,
+ u8char('B'),
+ u8char('l'),
+ u8char('u'),
+ u8char('e'),
+ u8char('t'),
+ u8char('o'),
+ u8char('o'),
+ u8char('t'),
+ u8char('h'),
+ 0x02,
+ 0x01,
+ 0x01,
+ ];
+ let (decoded, consumed) = TestValues::decode_all(&encoded);
+ assert_eq!(consumed, encoded.len());
+ assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097)));
+ assert_eq!(decoded[1], Ok(TestValues::UnicodeString("Bluetooth".to_owned())));
+ assert_eq!(decoded[2], Ok(TestValues::OneByte(1)));
+ }
+
+ #[test]
+ fn decode_with_error() {
+ let encoded = [0x03, 0x02, 0x10, 0x01, 0x02, 0xFF, 0xFF, 0x02, 0x01, 0x03];
+ let (decoded, consumed) = TestValues::decode_all(&encoded);
+ assert_eq!(consumed, encoded.len());
+ assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097)));
+ assert_eq!(decoded[1], Err(crate::packet_encoding::Error::OutOfRange),);
+ assert_eq!(decoded[2], Ok(TestValues::OneByte(3)));
+ }
+
+ #[test]
+ fn encode_with_error() {
+ let mut buf = [0; 10];
+ let value = TestValues::AlwaysError;
+ assert!(matches!(
+ value.encode(&mut buf),
+ Err(crate::packet_encoding::Error::InvalidParameter(_)),
+ ));
+ }
+}
diff --git a/rust/bt-common/src/generic_audio.rs b/rust/bt-common/src/generic_audio.rs
index 904506c..c2f72f0 100644
--- a/rust/bt-common/src/generic_audio.rs
+++ b/rust/bt-common/src/generic_audio.rs
@@ -2,4 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+pub mod codec_capabilities;
+
pub mod metadata_ltv;
diff --git a/rust/bt-common/src/generic_audio/codec_capabilities.rs b/rust/bt-common/src/generic_audio/codec_capabilities.rs
new file mode 100644
index 0000000..eeff6cd
--- /dev/null
+++ b/rust/bt-common/src/generic_audio/codec_capabilities.rs
@@ -0,0 +1,256 @@
+// 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 std::collections::HashSet;
+use std::ops::RangeBounds;
+
+use crate::core::ltv::LtValue;
+
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
+pub enum CodecCapabilityType {
+ SupportedSamplingFrequencies,
+ SupportedFrameDurations,
+ SupportedAudioChannelCounts,
+ SupportedOctetsPerCodecFrame,
+ SupportedMaxCodecFramesPerSdu,
+}
+
+impl From<CodecCapabilityType> for u8 {
+ fn from(value: CodecCapabilityType) -> Self {
+ match value {
+ CodecCapabilityType::SupportedSamplingFrequencies => 1,
+ CodecCapabilityType::SupportedFrameDurations => 2,
+ CodecCapabilityType::SupportedAudioChannelCounts => 3,
+ CodecCapabilityType::SupportedOctetsPerCodecFrame => 4,
+ CodecCapabilityType::SupportedMaxCodecFramesPerSdu => 5,
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub enum FrameDurationSupport {
+ SevenFiveMs,
+ TenMs,
+ BothNoPreference,
+ PreferSevenFiveMs,
+ PreferTenMs,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub enum SamplingFrequency {
+ F8000Hz,
+ F11025Hz,
+ F16000Hz,
+ F22050Hz,
+ F24000Hz,
+ F32000Hz,
+ F44100Hz,
+ F48000Hz,
+ F88200Hz,
+ F96000Hz,
+ F176400Hz,
+ F192000Hz,
+ F384000Hz,
+}
+
+impl SamplingFrequency {
+ fn bitpos(&self) -> u8 {
+ match self {
+ SamplingFrequency::F8000Hz => 0,
+ SamplingFrequency::F11025Hz => 1,
+ SamplingFrequency::F16000Hz => 2,
+ SamplingFrequency::F22050Hz => 3,
+ SamplingFrequency::F24000Hz => 4,
+ SamplingFrequency::F32000Hz => 5,
+ SamplingFrequency::F44100Hz => 6,
+ SamplingFrequency::F48000Hz => 7,
+ SamplingFrequency::F88200Hz => 8,
+ SamplingFrequency::F96000Hz => 9,
+ SamplingFrequency::F176400Hz => 10,
+ SamplingFrequency::F192000Hz => 11,
+ SamplingFrequency::F384000Hz => 12,
+ }
+ }
+
+ fn from_bitpos(value: u8) -> Option<Self> {
+ match value {
+ 0 => Some(SamplingFrequency::F8000Hz),
+ 1 => Some(SamplingFrequency::F11025Hz),
+ 2 => Some(SamplingFrequency::F16000Hz),
+ 3 => Some(SamplingFrequency::F22050Hz),
+ 4 => Some(SamplingFrequency::F24000Hz),
+ 5 => Some(SamplingFrequency::F32000Hz),
+ 6 => Some(SamplingFrequency::F44100Hz),
+ 7 => Some(SamplingFrequency::F48000Hz),
+ 8 => Some(SamplingFrequency::F88200Hz),
+ 9 => Some(SamplingFrequency::F96000Hz),
+ 10 => Some(SamplingFrequency::F176400Hz),
+ 11 => Some(SamplingFrequency::F192000Hz),
+ 12 => Some(SamplingFrequency::F384000Hz),
+ _ => None,
+ }
+ }
+}
+
+/// Codec Capability LTV Structures
+///
+/// Defined in Assigned Numbers Section 6.12.4.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum CodecCapability {
+ SupportedSamplingFrequencies(std::collections::HashSet<SamplingFrequency>),
+ SupportedFrameDurations(FrameDurationSupport),
+ SupportedAudioChannelCounts(std::collections::HashSet<u8>),
+ SupportedMaxCodecFramesPerSdu(u8),
+ SupportedOctetsPerCodecFrame { min: u16, max: u16 },
+}
+
+impl LtValue for CodecCapability {
+ type Type = CodecCapabilityType;
+
+ const NAME: &'static str = "Codec Compatability";
+
+ fn type_from_octet(x: u8) -> Option<Self::Type> {
+ match x {
+ 1 => Some(CodecCapabilityType::SupportedSamplingFrequencies),
+ 2 => Some(CodecCapabilityType::SupportedFrameDurations),
+ 3 => Some(CodecCapabilityType::SupportedAudioChannelCounts),
+ 4 => Some(CodecCapabilityType::SupportedOctetsPerCodecFrame),
+ 5 => Some(CodecCapabilityType::SupportedMaxCodecFramesPerSdu),
+ _ => None,
+ }
+ }
+
+ fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8> {
+ match ty {
+ CodecCapabilityType::SupportedAudioChannelCounts => 2..=2,
+ CodecCapabilityType::SupportedFrameDurations => 2..=2,
+ CodecCapabilityType::SupportedMaxCodecFramesPerSdu => 2..=2,
+ CodecCapabilityType::SupportedOctetsPerCodecFrame => 5..=5,
+ CodecCapabilityType::SupportedSamplingFrequencies => 3..=3,
+ }
+ }
+
+ fn into_type(&self) -> Self::Type {
+ match self {
+ CodecCapability::SupportedAudioChannelCounts(_) => {
+ CodecCapabilityType::SupportedAudioChannelCounts
+ }
+ CodecCapability::SupportedFrameDurations(_) => {
+ CodecCapabilityType::SupportedFrameDurations
+ }
+ CodecCapability::SupportedMaxCodecFramesPerSdu(_) => {
+ CodecCapabilityType::SupportedMaxCodecFramesPerSdu
+ }
+ CodecCapability::SupportedOctetsPerCodecFrame { .. } => {
+ CodecCapabilityType::SupportedOctetsPerCodecFrame
+ }
+ CodecCapability::SupportedSamplingFrequencies(_) => {
+ CodecCapabilityType::SupportedSamplingFrequencies
+ }
+ }
+ }
+
+ fn value_encoded_len(&self) -> u8 {
+ // All the CodecCapabilities are constant length. Remove the type octet.
+ let range = Self::length_range_from_type(self.into_type());
+ let std::ops::Bound::Included(len) = range.start_bound() else {
+ unreachable!();
+ };
+ (len - 1).into()
+ }
+
+ fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error> {
+ match self {
+ CodecCapability::SupportedAudioChannelCounts(counts) => {
+ buf[0] = counts
+ .iter()
+ .fold(0, |acc, count| if *count > 8 { acc } else { acc | (1 << (*count - 1)) });
+ }
+ CodecCapability::SupportedFrameDurations(support) => {
+ buf[0] = match support {
+ FrameDurationSupport::SevenFiveMs => 0b000001,
+ FrameDurationSupport::TenMs => 0b000010,
+ FrameDurationSupport::BothNoPreference => 0b000011,
+ FrameDurationSupport::PreferSevenFiveMs => 0b010011,
+ FrameDurationSupport::PreferTenMs => 0b100011,
+ };
+ }
+ CodecCapability::SupportedMaxCodecFramesPerSdu(max) => {
+ buf[0] = *max;
+ }
+ CodecCapability::SupportedOctetsPerCodecFrame { min, max } => {
+ let min_bytes = min.to_le_bytes();
+ buf[0..=1].copy_from_slice(&min_bytes);
+
+ let max_bytes = max.to_le_bytes();
+ buf[2..=3].copy_from_slice(&max_bytes);
+ }
+ CodecCapability::SupportedSamplingFrequencies(supported) => {
+ let sup_bytes = supported
+ .iter()
+ .fold(0u16, |acc, freq| acc | (1 << freq.bitpos()))
+ .to_le_bytes();
+ buf[0..=1].copy_from_slice(&sup_bytes)
+ }
+ };
+ Ok(())
+ }
+
+ fn decode_value(
+ ty: &CodecCapabilityType,
+ buf: &[u8],
+ ) -> Result<Self, crate::packet_encoding::Error> {
+ match ty {
+ CodecCapabilityType::SupportedAudioChannelCounts => {
+ let mut supported = HashSet::new();
+ for bitpos in 0..8 {
+ if (buf[0] & (1 << bitpos)) != 0 {
+ supported.insert(bitpos + 1);
+ }
+ }
+ Ok(Self::SupportedAudioChannelCounts(supported))
+ }
+ CodecCapabilityType::SupportedFrameDurations => {
+ let support = match buf[0] & 0b110011 {
+ 0b000001 => FrameDurationSupport::SevenFiveMs,
+ 0b000010 => FrameDurationSupport::TenMs,
+ 0b000011 => FrameDurationSupport::BothNoPreference,
+ 0b010011 => FrameDurationSupport::PreferSevenFiveMs,
+ 0b100011 => FrameDurationSupport::PreferTenMs,
+ _ => {
+ return Err(crate::packet_encoding::Error::InvalidParameter(format!(
+ "Unrecognized bit pattern: {:#b}",
+ buf[0]
+ )));
+ }
+ };
+ Ok(Self::SupportedFrameDurations(support))
+ }
+ CodecCapabilityType::SupportedMaxCodecFramesPerSdu => {
+ Ok(Self::SupportedMaxCodecFramesPerSdu(buf[0]))
+ }
+ CodecCapabilityType::SupportedOctetsPerCodecFrame => {
+ let min = u16::from_le_bytes([buf[0], buf[1]]);
+ let max = u16::from_le_bytes([buf[2], buf[3]]);
+ Ok(Self::SupportedOctetsPerCodecFrame { min, max })
+ }
+ CodecCapabilityType::SupportedSamplingFrequencies => {
+ let bitflags = u16::from_le_bytes([buf[0], buf[1]]);
+ let mut supported = HashSet::new();
+ for bitpos in 0..13 {
+ if bitflags & (1 << bitpos) != 0 {
+ let Some(f) = SamplingFrequency::from_bitpos(bitpos) else {
+ continue;
+ };
+ let _ = supported.insert(f);
+ }
+ }
+ Ok(Self::SupportedSamplingFrequencies(supported))
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {}
diff --git a/rust/bt-common/src/generic_audio/metadata_ltv.rs b/rust/bt-common/src/generic_audio/metadata_ltv.rs
index e29b636..3630f4e 100644
--- a/rust/bt-common/src/generic_audio/metadata_ltv.rs
+++ b/rust/bt-common/src/generic_audio/metadata_ltv.rs
@@ -31,10 +31,10 @@
} else {
Err(PacketError::UnexpectedDataLength)
}
- }
+ }
- // Total size range including all Length, Type, and Value parameters.
- fn total_size_range(&self) -> std::ops::Range<usize> {
+ // Total size range including all Length, Type, and Value parameters.
+ fn total_size_range(&self) -> std::ops::Range<usize> {
match self {
Type::PreferredAudioContexts | Type::StreamingAudioContexts => 4..5,
Type::ProgramInfo | Type::CCIDList | Type::ProgramInfoURI => 2..usize::MAX,
@@ -44,7 +44,7 @@
Type::AudioActiveState => 3..4,
Type::BroadcastAudioImmediateRenderingFlag => 2..3,
}
- }
+ }
}
impl TryFrom<u8> for Type {
@@ -92,13 +92,13 @@
BroadcastAudioImmediateRenderingFlag,
}
-
impl Metadatum {
const LENGTH_BYTE_SIZE: usize = 1;
const TYPE_BYTE_SIZE: usize = 1;
const MIN_PACKET_SIZE: usize = Self::LENGTH_BYTE_SIZE + Self::TYPE_BYTE_SIZE;
- // Size of this metadatum's Value parameter. Does not include the Length or Type size.
+ // Size of this metadatum's Value parameter. Does not include the Length or Type
+ // size.
pub fn value_encoded_len(&self) -> usize {
match self {
Metadatum::PreferredAudioContexts(_) => 2,
@@ -124,10 +124,12 @@
Metadatum::CCIDList(_) => Type::CCIDList,
Metadatum::ParentalRating(_) => Type::ParentalRating,
Metadatum::ProgramInfoURI(_) => Type::ProgramInfoURI,
- Metadatum::ExtendedMetadata{..} => Type::ExtendedMetadata,
- Metadatum::VendorSpecific{..} => Type::VendorSpecific,
+ Metadatum::ExtendedMetadata { .. } => Type::ExtendedMetadata,
+ Metadatum::VendorSpecific { .. } => Type::VendorSpecific,
Metadatum::AudioActiveState(_) => Type::AudioActiveState,
- Metadatum::BroadcastAudioImmediateRenderingFlag => Type::BroadcastAudioImmediateRenderingFlag,
+ Metadatum::BroadcastAudioImmediateRenderingFlag => {
+ Type::BroadcastAudioImmediateRenderingFlag
+ }
}
}
}
@@ -148,15 +150,17 @@
let _ = type_.length_check(total_len)?;
let m: Metadatum = match type_ {
Type::PreferredAudioContexts | Type::StreamingAudioContexts => {
- let context_type = ContextType::try_from(u16::from_le_bytes(buf[2..4].try_into().unwrap()))?;
+ let context_type =
+ ContextType::try_from(u16::from_le_bytes(buf[2..4].try_into().unwrap()))?;
if type_ == Type::PreferredAudioContexts {
Self::PreferredAudioContexts(context_type)
- } else {
+ } else {
Self::StreamingAudioContexts(context_type)
}
- },
+ }
Type::ProgramInfo | Type::ProgramInfoURI => {
- let value = String::from_utf8(buf[2..total_len].to_vec()).map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
+ let value = String::from_utf8(buf[2..total_len].to_vec())
+ .map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
if type_ == Type::ProgramInfo {
Self::ProgramInfo(value)
} else {
@@ -164,55 +168,49 @@
}
}
Type::Language => {
- let code = String::from_utf8(buf[2..total_len].to_vec()).map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
+ let code = String::from_utf8(buf[2..total_len].to_vec())
+ .map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
Self::LanguageCode(code)
- },
+ }
Type::CCIDList => Self::CCIDList(buf[2..total_len].to_vec()),
- Type::ParentalRating => {
- Self::ParentalRating(Rating::decode(&buf[2..3])?.0)
- },
+ Type::ParentalRating => Self::ParentalRating(Rating::decode(&buf[2..3])?.0),
Type::ExtendedMetadata | Type::VendorSpecific => {
let type_or_id = u16::from_le_bytes(buf[2..4].try_into().unwrap());
- let data = if buf.len() >= 5 {
- buf[4..total_len].to_vec()
- } else {
- vec![]
- };
+ let data = if buf.len() >= 5 { buf[4..total_len].to_vec() } else { vec![] };
if type_ == Type::ExtendedMetadata {
- Self::ExtendedMetadata{
- type_: type_or_id,
- metadata: data,
- }
+ Self::ExtendedMetadata { type_: type_or_id, metadata: data }
} else {
- Self::VendorSpecific{
- company_id: type_or_id,
- metadata: data,
- }
+ Self::VendorSpecific { company_id: type_or_id, metadata: data }
}
- },
+ }
Type::AudioActiveState => {
if buf[2] > 1 {
return Err(PacketError::UnexpectedDataLength);
}
Self::AudioActiveState(buf[2] != 0)
- },
- Type::BroadcastAudioImmediateRenderingFlag => Self::BroadcastAudioImmediateRenderingFlag,
+ }
+ Type::BroadcastAudioImmediateRenderingFlag => {
+ Self::BroadcastAudioImmediateRenderingFlag
+ }
};
- return Ok((m, total_len))
+ return Ok((m, total_len));
}
}
impl Encodable for Metadatum {
type Error = PacketError;
- /// Encodes the object into a provided buffer in a Metadata LTV structure. Bytes are in LSO.
- // See Assigned Numbers section 6.12.6 for details about Metadata LTV structures.
+ /// Encodes the object into a provided buffer in a Metadata LTV structure.
+ /// Bytes are in LSO.
+ // See Assigned Numbers section 6.12.6 for details about Metadata LTV
+ // structures.
fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> {
if buf.len() < self.encoded_len() {
return Err(PacketError::BufferTooSmall);
}
- // Length parameter value excludes the length of Length parameter itself (which is 1 byte).
+ // Length parameter value excludes the length of Length parameter itself (which
+ // is 1 byte).
buf[0] = (self.encoded_len() - 1)
.try_into()
.map_err(|_| PacketError::InvalidParameter("Metadata too big".to_string()))?;
@@ -231,7 +229,7 @@
Self::LanguageCode(value) => buf[2..5].copy_from_slice(&value.as_bytes()[..3]),
Self::CCIDList(value) => buf[2..2 + value.len()].copy_from_slice(&value.as_slice()),
Self::ParentalRating(value) => buf[2] = value.into(),
- Self::ExtendedMetadata{type_, metadata} => {
+ Self::ExtendedMetadata { type_, metadata } => {
buf[2..4].copy_from_slice(&type_.to_le_bytes());
buf[4..4 + metadata.len()].copy_from_slice(&metadata.as_slice());
}
@@ -261,7 +259,7 @@
Metadatum::CCIDList(v) => write!(f, "CCID List: {v:?}"),
Metadatum::ParentalRating(v) => write!(f, "Parental Rating: {v:?}"),
Metadatum::ProgramInfoURI(v) => write!(f, "Program Info URI: {v:?}"),
- Metadatum::ExtendedMetadata{type_, metadata} => {
+ Metadatum::ExtendedMetadata { type_, metadata } => {
write!(f, "Extended Metadata: type(0x{type_:02x}) metadata({metadata:?})")
}
Metadatum::VendorSpecific { company_id, metadata } => {
@@ -303,7 +301,6 @@
}
Ok(ContextType::from_bits_truncate(value))
}
-
}
/// Represents recommended minimum age of the viewer.
@@ -337,7 +334,9 @@
pub fn min_recommended(age: u8) -> Result<Self, PacketError> {
if age < Self::MIN_RECOMMENDED_AGE {
- return Err(PacketError::InvalidParameter(format!("minimum recommended age must be at least 5. Got {age}")));
+ return Err(PacketError::InvalidParameter(format!(
+ "minimum recommended age must be at least 5. Got {age}"
+ )));
}
Ok(Rating::Age(age))
}
@@ -364,10 +363,9 @@
0x00 => Rating::NoRating,
0x01 => Rating::AllAge,
value => {
- let age = value.checked_add(Self::AGE_OFFSET)
- .ok_or(PacketError::OutOfRange)?;
+ let age = value.checked_add(Self::AGE_OFFSET).ok_or(PacketError::OutOfRange)?;
Rating::min_recommended(age).unwrap()
- },
+ }
};
Ok((rating, 1))
}
@@ -482,7 +480,7 @@
#[test]
fn metadatum_vendor_specific() {
// Encoding.
- let test = Metadatum::VendorSpecific{company_id: 0x00E0, metadata: vec![0x01, 0x02]};
+ let test = Metadatum::VendorSpecific { company_id: 0x00E0, metadata: vec![0x01, 0x02] };
assert_eq!(test.encoded_len(), 0x06);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
diff --git a/rust/bt-common/src/lib.rs b/rust/bt-common/src/lib.rs
index 67e9360..232ec7d 100644
--- a/rust/bt-common/src/lib.rs
+++ b/rust/bt-common/src/lib.rs
@@ -4,22 +4,33 @@
extern crate core as rust_core;
-/// Peers are identified by ids, which should be treated as opaque by service libraries.
-/// Stack implementations should ensure that each PeerId identifies a single peer over a single
-/// instance of the stack - a [`bt_gatt::Central::connect`] should always attempt to connect to the
-/// same peer as long as the PeerId was retrieved after the `Central` was instantiated.
-/// PeerIds can be valid longer than that (often if the peer is bonded)
+/// Peers are identified by ids, which should be treated as opaque by service
+/// libraries. Stack implementations should ensure that each PeerId identifies a
+/// single peer over a single instance of the stack - a
+/// [`bt_gatt::Central::connect`] should always attempt to connect to the
+/// same peer as long as the PeerId was retrieved after the `Central` was
+/// instantiated. PeerIds can be valid longer than that (often if the peer is
+/// bonded)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PeerId(pub u64);
impl rust_core::fmt::Display for PeerId {
- fn fmt(&self, f: &mut rust_core::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ fn fmt(
+ &self,
+ f: &mut rust_core::fmt::Formatter<'_>,
+ ) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{:x}", self.0)
}
}
pub mod core;
+
+pub mod company_id;
+pub use company_id::CompanyId;
+
pub mod generic_audio;
+
pub mod packet_encoding;
+
pub mod uuid;
pub use crate::uuid::Uuid;
diff --git a/rust/bt-common/src/packet_encoding.rs b/rust/bt-common/src/packet_encoding.rs
index fa01309..978430b 100644
--- a/rust/bt-common/src/packet_encoding.rs
+++ b/rust/bt-common/src/packet_encoding.rs
@@ -9,12 +9,13 @@
pub trait Decodable: ::core::marker::Sized {
type Error;
- /// Decodes into a new object with the number of bytes that were decoded, or returns an error.
+ /// Decodes into a new object with the number of bytes that were decoded, or
+ /// returns an error.
fn decode(buf: &[u8]) -> ::core::result::Result<(Self, usize), Self::Error>;
}
/// An encodable type can write itself into a byte buffer.
-pub trait Encodable: ::core::marker::Sized {
+pub trait Encodable {
type Error;
/// Returns the number of bytes necessary to encode |self|.
@@ -38,4 +39,7 @@
#[error("Buffer being decoded is invalid length")]
UnexpectedDataLength,
+
+ #[error("Unrecognized type for {0}: {1}")]
+ UnrecognizedType(String, u8),
}
diff --git a/rust/bt-common/src/uuid.rs b/rust/bt-common/src/uuid.rs
index a47f790..2f11ed8 100644
--- a/rust/bt-common/src/uuid.rs
+++ b/rust/bt-common/src/uuid.rs
@@ -7,7 +7,8 @@
pub struct Uuid(uuid::Uuid);
impl Uuid {
- // Non-changing parts of the Bluetooth Base UUID, for easy comparison and construction.
+ // Non-changing parts of the Bluetooth Base UUID, for easy comparison and
+ // construction.
const BASE_UUID_B_PART: u16 = 0x0000;
const BASE_UUID_C_PART: u16 = 0x1000;
const BASE_UUID_D_PART: [u8; 8] = [0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB];