rust/bt-common: Add interactive debug traits
Define two debug traits:
- CommandSet trait allows a set of commands to be provided
- CommandRunner provides a way to run those commands
Intended to be used for interactive debugging and testing.
Add commands for bt-pacs.
Add FromCharacteristic::try_read, attempting to read and decode data from a
characteristic if it matches the UUID of the type.
Bug: fxbug.dev/308483257
Change-Id: I56062f0604a05b94c8579f17b07991cf9b452c08
Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1460
Reviewed-by: Ani Ramakrishnan <aniramakri@google.com>
Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-common/Cargo.toml b/rust/bt-common/Cargo.toml
index 3006f41..67db0da 100644
--- a/rust/bt-common/Cargo.toml
+++ b/rust/bt-common/Cargo.toml
@@ -8,3 +8,4 @@
thiserror.workspace = true
lazy_static.workspace = true
uuid.workspace = true
+futures.workspace = true
diff --git a/rust/bt-common/src/debug_command.rs b/rust/bt-common/src/debug_command.rs
new file mode 100644
index 0000000..54ea04a
--- /dev/null
+++ b/rust/bt-common/src/debug_command.rs
@@ -0,0 +1,143 @@
+// Copyright 2023 Google LLC
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+///! Debug command traits and helpers for defining commands for integration
+/// into a debug tool.
+use std::str::FromStr;
+
+/// A CommandSet is a set of commands (usually an enum) that each represent an
+/// action that can be performed. i.e. 'list', 'volume' etc. Each command can
+/// take zero or more arguments and have zero or more flags.
+/// Typically an Enum of commands would implement CommandSet trait.
+pub trait CommandSet: FromStr + ::core::fmt::Display {
+ /// Returns a vector of strings that are the commands supported by this.
+ fn variants() -> Vec<String>;
+
+ /// Returns a string listing the arguments that this command takes, in <>
+ /// brackets
+ fn arguments(&self) -> &'static str;
+
+ /// Returns a string displaying the flags that this command supports, in []
+ /// brackets
+ fn flags(&self) -> &'static str;
+
+ /// Returns a short description of this command
+ fn desc(&self) -> &'static str;
+
+ /// Help string for this variant (build from Display, arguments and flags by
+ /// default)
+ fn help_simple(&self) -> String {
+ format!("{self} {} {} -- {}", self.flags(), self.arguments(), self.desc())
+ }
+
+ /// Possibly multi-line help string for all variants of this set.
+ fn help_all() -> String {
+ Self::variants()
+ .into_iter()
+ .filter_map(|s| FromStr::from_str(&s).ok())
+ .map(|s: Self| format!("{}\n", s))
+ .collect()
+ }
+}
+
+/// Macro to help build CommandSets
+#[macro_export]
+macro_rules! gen_commandset {
+ ($name:ident {
+ $($variant:ident = ($val:expr, [$($flag:expr),*], [$($arg:expr),*], $help:expr)),*,
+ }) => {
+ /// Enum of all possible commands
+ #[derive(PartialEq, Debug)]
+ pub enum $name {
+ $($variant),*
+ }
+
+ impl CommandSet for $name {
+ fn variants() -> Vec<String> {
+ let mut variants = Vec::new();
+ $(variants.push($val.to_string());)*
+ variants
+ }
+
+ fn arguments(&self) -> &'static str {
+ match self {
+ $(
+ $name::$variant => concat!($("<", $arg, "> ",)*)
+ ),*
+ }
+ }
+
+ fn flags(&self) -> &'static str {
+ match self {
+ $(
+ $name::$variant => concat!($("[", $flag, "] ",)*)
+ ),*
+ }
+ }
+
+ fn desc(&self) -> &'static str {
+ match self {
+ $(
+ $name::$variant => $help
+ ),*
+ }
+ }
+ }
+
+ impl ::core::fmt::Display for $name {
+ fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
+ match *self {
+ $($name::$variant => write!(f, $val)),* ,
+ }
+ }
+ }
+
+ impl ::std::str::FromStr for $name {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<$name, ()> {
+ match s {
+ $($val => Ok($name::$variant)),* ,
+ _ => Err(()),
+ }
+ }
+ }
+ }
+}
+
+/// CommandRunner is used to perform a specific task based on the
+pub trait CommandRunner {
+ type Set: CommandSet;
+ fn run(
+ &self,
+ cmd: Self::Set,
+ args: Vec<String>,
+ ) -> impl futures::Future<Output = Result<(), impl ::std::error::Error>>;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn gen_commandset_simple() {
+ gen_commandset! {
+ TestCmd {
+ One = ("one", [], [], "First Command"),
+ WithFlags = ("with-flags", ["-1","-2"], [], "Command with flags"),
+ WithArgs = ("with-args", [], ["arg", "two"], "Command with args"),
+ WithBoth = ("with-both", ["-w"], ["simple"], "Command with both flags and args"),
+ }
+ }
+
+ let cmd: TestCmd = "one".parse().unwrap();
+
+ assert_eq!(cmd, TestCmd::One);
+
+ let cmd2: TestCmd = "with-flags".parse().unwrap();
+
+ assert_eq!(cmd2.arguments(), "");
+ assert_eq!(cmd2.flags(), "[-1] [-2] ");
+ }
+}
diff --git a/rust/bt-common/src/lib.rs b/rust/bt-common/src/lib.rs
index 2956e3e..382567d 100644
--- a/rust/bt-common/src/lib.rs
+++ b/rust/bt-common/src/lib.rs
@@ -36,3 +36,5 @@
pub mod uuids;
pub use crate::uuids::Uuid;
+
+pub mod debug_command;
diff --git a/rust/bt-gatt/src/client.rs b/rust/bt-gatt/src/client.rs
index d27a448..94af837 100644
--- a/rust/bt-gatt/src/client.rs
+++ b/rust/bt-gatt/src/client.rs
@@ -42,6 +42,38 @@
&mut self,
new_value: &[u8],
) -> ::core::result::Result<&mut Self, bt_common::packet_encoding::Error>;
+
+ /// Attempt to read a characteristic if it matches the provided
+ /// characteristic UUID.
+ fn try_read<T: crate::GattTypes>(
+ characteristic: Characteristic,
+ service: &T::PeerService,
+ ) -> impl futures::Future<Output = ::core::result::Result<Self, Error>> {
+ async move {
+ if characteristic.uuid != Self::UUID {
+ return Err(Error::ScanFailed("Wrong UUID".to_owned()));
+ }
+ let mut buf = [0; 128];
+ let (bytes, mut truncated) =
+ service.read_characteristic(&characteristic.handle, 0, &mut buf).await?;
+ let mut vec;
+ let buf_ptr = if truncated {
+ vec = Vec::with_capacity(bytes);
+ vec.copy_from_slice(&buf[..bytes]);
+ while truncated {
+ let (bytes, still_truncated) = service
+ .read_characteristic(&characteristic.handle, vec.len() as u16, &mut buf)
+ .await?;
+ vec.extend_from_slice(&buf[..bytes]);
+ truncated = still_truncated;
+ }
+ &vec[..]
+ } else {
+ &buf[..bytes]
+ };
+ Self::from_chr(characteristic, buf_ptr).map_err(Into::into)
+ }
+ }
}
#[derive(Debug, Clone)]
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index 3e3dc06..fdc9cfd 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -151,6 +151,14 @@
DuplicateHandle(Vec<Handle>),
#[error("service for {0:?} already published")]
AlreadyPublished(crate::server::ServiceId),
+ #[error("Encoding/decoding error: {0}")]
+ Encoding(#[from] bt_common::packet_encoding::Error),
+}
+
+impl Error {
+ pub fn other(e: impl std::error::Error + Send + Sync + 'static) -> Self {
+ Self::Other(Box::new(e))
+ }
}
impl From<String> for Error {
diff --git a/rust/bt-pacs/Cargo.toml b/rust/bt-pacs/Cargo.toml
index 0fd767d..52d4737 100644
--- a/rust/bt-pacs/Cargo.toml
+++ b/rust/bt-pacs/Cargo.toml
@@ -10,5 +10,8 @@
bt-gatt.workspace = true
bt-common.workspace = true
+### Others
+futures.workspace = true
+
[dev-dependencies]
pretty_assertions.workspace = true
diff --git a/rust/bt-pacs/src/debug.rs b/rust/bt-pacs/src/debug.rs
new file mode 100644
index 0000000..f5ab520
--- /dev/null
+++ b/rust/bt-pacs/src/debug.rs
@@ -0,0 +1,92 @@
+// 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::debug_command::CommandRunner;
+use bt_common::debug_command::CommandSet;
+use bt_common::gen_commandset;
+
+use bt_gatt::{
+ client::{PeerService, PeerServiceHandle},
+ Client,
+};
+
+use crate::*;
+
+gen_commandset! {
+ PacsCmd {
+ Print = ("print", [], [], "Print the current PACS status"),
+ }
+}
+
+pub struct PacsDebug<T: bt_gatt::GattTypes> {
+ client: T::Client,
+}
+
+impl<T: bt_gatt::GattTypes> PacsDebug<T> {
+ pub fn new(client: T::Client) -> Self {
+ Self { client }
+ }
+}
+
+impl<T: bt_gatt::GattTypes> CommandRunner for PacsDebug<T> {
+ type Set = PacsCmd;
+
+ fn run(
+ &self,
+ _cmd: Self::Set,
+ _args: Vec<String>,
+ ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
+ async {
+ // Since there is only one command, Print, we just print
+ // all the characteristics that are at the remote PACS server.
+ let handles = self.client.find_service(PACS_UUID).await?;
+ for handle in handles {
+ let service = handle.connect().await?;
+
+ let chrs = service.discover_characteristics(None).await?;
+ for chr in chrs {
+ let mut buf = [0; 120];
+ match chr.uuid {
+ SourcePac::UUID => {
+ let source_pac = SourcePac::try_read::<T>(chr, &service).await?;
+ println!("{source_pac:?}");
+ }
+ SinkPac::UUID => {
+ let sink_pac = SinkPac::try_read::<T>(chr, &service).await?;
+ println!("{sink_pac:?}");
+ }
+ SinkAudioLocations::UUID => {
+ let locations =
+ SinkAudioLocations::try_read::<T>(chr, &service).await?;
+ println!("{locations:?}");
+ }
+ SourceAudioLocations::UUID => {
+ let (bytes, _trunc) =
+ service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+ let locations = SourceAudioLocations::from_chr(chr, &buf[..bytes])
+ .map_err(bt_gatt::types::Error::other)?;
+ println!("{locations:?}");
+ }
+ AvailableAudioContexts::UUID => {
+ let (bytes, _trunc) =
+ service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+ let contexts = AvailableAudioContexts::from_chr(chr, &buf[..bytes])
+ .map_err(bt_gatt::types::Error::other)?;
+ println!("{contexts:?}");
+ }
+ SupportedAudioContexts::UUID => {
+ let (bytes, _trunc) =
+ service.read_characteristic(&chr.handle, 0, &mut buf).await?;
+ let contexts = SupportedAudioContexts::from_chr(chr, &buf[..bytes])
+ .map_err(bt_gatt::types::Error::other)?;
+ println!("{contexts:?}");
+ }
+ _x => println!("Unrecognized Chr {}", chr.uuid.recognize()),
+ }
+ }
+ }
+ Ok::<(), bt_gatt::types::Error>(())
+ }
+ }
+}
diff --git a/rust/bt-pacs/src/lib.rs b/rust/bt-pacs/src/lib.rs
index 38efa40..e926716 100644
--- a/rust/bt-pacs/src/lib.rs
+++ b/rust/bt-pacs/src/lib.rs
@@ -11,6 +11,11 @@
use bt_common::Uuid;
use bt_gatt::{client::FromCharacteristic, Characteristic};
+pub mod debug;
+
+/// UUID from Assigned Numbers section 3.4.
+pub const PACS_UUID: Uuid = Uuid::from_u16(0x1850);
+
/// 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
@@ -34,6 +39,9 @@
idx += consumed;
let codec_specific_capabilites_length = buf[idx] as usize;
idx += 1;
+ if idx + codec_specific_capabilites_length > buf.len() {
+ return Err(bt_common::packet_encoding::Error::UnexpectedDataLength);
+ }
let (results, consumed) =
CodecCapability::decode_all(&buf[idx..idx + codec_specific_capabilites_length]);
if consumed != codec_specific_capabilites_length {
@@ -86,6 +94,7 @@
}
impl FromCharacteristic for SinkPac {
+ /// UUID from Assigned Numbers section 3.8.
const UUID: Uuid = Uuid::from_u16(0x2BC9);
fn from_chr(
@@ -114,6 +123,7 @@
}
impl FromCharacteristic for SourcePac {
+ /// UUID from Assigned Numbers section 3.8.
const UUID: Uuid = Uuid::from_u16(0x2BCB);
fn from_chr(
@@ -163,6 +173,7 @@
}
impl FromCharacteristic for SourceAudioLocations {
+ /// UUID from Assigned Numbers section 3.8.
const UUID: Uuid = Uuid::from_u16(0x2BCC);
fn from_chr(
@@ -228,7 +239,8 @@
}
impl FromCharacteristic for AvailableAudioContexts {
- const UUID: Uuid = Uuid::from_u16(0x28CD);
+ /// UUID from Assigned Numbers section 3.8.
+ const UUID: Uuid = Uuid::from_u16(0x2BCD);
fn from_chr(
characteristic: Characteristic,
@@ -266,7 +278,7 @@
}
impl FromCharacteristic for SupportedAudioContexts {
- const UUID: Uuid = Uuid::from_u16(0x28CE);
+ const UUID: Uuid = Uuid::from_u16(0x2BCE);
fn from_chr(
characteristic: Characteristic,