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,