rust/bt-broadcast-assistant: Add support for command line tool
Implement Debug and CommandRunner traits to support easier
integration with command line tools.
Change-Id: I53510a9860fcf15d743fe5ac36623acb6534ad9e
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/1940
Reviewed-by: Marie Janssen <jamuraa@google.com>
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 4fbcdbd..401b1f4 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -25,6 +25,7 @@
futures = "=0.3.31"
lazy_static = "1.4"
log = { version = "0.4.22", features = [ "kv", "std" ] }
+num = "0.4.0" # 0.4.0 in Fuchsia
parking_lot = "0.12.0"
pretty_assertions = "1.2.1"
thiserror = "2.0.11"
diff --git a/rust/bt-bass/src/client.rs b/rust/bt-bass/src/client.rs
index 0e7d18c..b48d8ed 100644
--- a/rust/bt-bass/src/client.rs
+++ b/rust/bt-bass/src/client.rs
@@ -109,7 +109,7 @@
broadcast_sources: Arc<Mutex<KnownBroadcastSources>>,
/// Keeps track of the broadcast codes that were sent to the remote BASS
/// server.
- broadcast_codes: HashMap<SourceId, [u8; 16]>,
+ broadcast_codes: Arc<Mutex<HashMap<SourceId, [u8; 16]>>>,
// GATT notification streams for BRS characteristic value changes.
notification_streams: Option<
SelectAll<BoxStream<'static, Result<CharacteristicNotification, bt_gatt::types::Error>>>,
@@ -123,7 +123,7 @@
gatt_client,
audio_scan_control_point,
broadcast_sources: Default::default(),
- broadcast_codes: HashMap::new(),
+ broadcast_codes: Arc::new(Mutex::new(HashMap::new())),
notification_streams: Some(SelectAll::new()),
}
}
@@ -152,7 +152,7 @@
gatt_client,
audio_scan_control_point: bascp_handle,
broadcast_sources: Arc::new(Mutex::new(KnownBroadcastSources::new(brs_chars))),
- broadcast_codes: HashMap::new(),
+ broadcast_codes: Arc::new(Mutex::new(HashMap::new())),
notification_streams: None,
};
c.register_notifications();
@@ -266,21 +266,21 @@
/// broadcast sources on behalf of it. If the scan delegator that serves
/// the BASS server is collocated with a broadcast sink, this may or may
/// not change the scanning behaviour of the the broadcast sink.
- pub async fn remote_scan_started(&mut self) -> Result<(), Error> {
+ pub async fn remote_scan_started(&self) -> Result<(), Error> {
let op = RemoteScanStartedOperation;
self.write_to_bascp(op).await
}
/// Indicates to the remote BASS server that we have stopped scanning for
/// broadcast sources on behalf of it.
- pub async fn remote_scan_stopped(&mut self) -> Result<(), Error> {
+ pub async fn remote_scan_stopped(&self) -> Result<(), Error> {
let op = RemoteScanStoppedOperation;
self.write_to_bascp(op).await
}
/// Provides the BASS server with information regarding a Broadcast Source.
pub async fn add_broadcast_source(
- &mut self,
+ &self,
broadcast_id: BroadcastId,
address_type: AddressType,
advertiser_address: [u8; ADDRESS_BYTE_SIZE],
@@ -317,7 +317,7 @@
/// * `metadata_map` - map of updated metadata for BIGs. If a mapping does
/// not exist for a BIG, that BIG's metadata is not updated
pub async fn modify_broadcast_source(
- &mut self,
+ &self,
broadcast_id: BroadcastId,
pa_sync: PaSync,
pa_interval: Option<PaInterval>,
@@ -372,10 +372,7 @@
self.write_to_bascp(op).await
}
- pub async fn remove_broadcast_source(
- &mut self,
- broadcast_id: BroadcastId,
- ) -> Result<(), Error> {
+ pub async fn remove_broadcast_source(&self, broadcast_id: BroadcastId) -> Result<(), Error> {
let source_id = self.get_source_id(&broadcast_id)?;
let op = RemoveSourceOperation::new(source_id);
@@ -384,7 +381,7 @@
/// Sets the broadcast code for a particular broadcast stream.
pub async fn set_broadcast_code(
- &mut self,
+ &self,
broadcast_id: BroadcastId,
broadcast_code: [u8; 16],
) -> Result<(), Error> {
@@ -394,7 +391,7 @@
self.write_to_bascp(op).await?;
// Save the broadcast code we sent.
- self.broadcast_codes.insert(source_id, broadcast_code);
+ self.broadcast_codes.lock().insert(source_id, broadcast_code);
Ok(())
}
@@ -715,7 +712,7 @@
#[test]
fn remote_scan_started() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
fake_peer_service.expect_characteristic_value(&AUDIO_SCAN_CONTROL_POINT_HANDLE, vec![0x01]);
@@ -728,7 +725,7 @@
#[test]
fn remote_scan_stopped() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
fake_peer_service.expect_characteristic_value(&AUDIO_SCAN_CONTROL_POINT_HANDLE, vec![0x00]);
@@ -741,7 +738,7 @@
#[test]
fn add_broadcast_source() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
fake_peer_service.expect_characteristic_value(
&AUDIO_SCAN_CONTROL_POINT_HANDLE,
@@ -768,7 +765,7 @@
#[test]
fn modify_broadcast_source() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
// Manually update the broadcast source tracker for testing purposes.
// In practice, this would have been updated from BRS value change notification.
@@ -810,7 +807,7 @@
#[test]
fn modify_broadcast_source_updates_groups() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
// Manually update the broadcast source tracker for testing purposes.
// In practice, this would have been updated from BRS value change notification.
@@ -862,7 +859,7 @@
#[test]
fn modify_broadcast_source_fail() {
- let (mut client, _fake_peer_service) = setup_client();
+ let (client, _fake_peer_service) = setup_client();
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
// Broadcast source wasn't previously added.
@@ -880,7 +877,7 @@
#[test]
fn remove_broadcast_source() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
let bid = BroadcastId::try_from(0x11).expect("should not fail");
// Manually update the broadcast source tracker for testing purposes.
@@ -912,7 +909,7 @@
#[test]
fn remove_broadcast_source_fail() {
- let (mut client, _fake_peer_service) = setup_client();
+ let (client, _fake_peer_service) = setup_client();
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
// Broadcast source wasn't previously added.
@@ -924,7 +921,7 @@
#[test]
fn set_broadcast_code() {
- let (mut client, mut fake_peer_service) = setup_client();
+ let (client, mut fake_peer_service) = setup_client();
// Manually update the broadcast source tracker for testing purposes.
// In practice, this would have been updated from BRS value change notification.
@@ -957,7 +954,7 @@
#[test]
fn set_broadcast_code_fails() {
- let (mut client, _) = setup_client();
+ let (client, _) = setup_client();
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
let set_code_fut =
diff --git a/rust/bt-broadcast-assistant/Cargo.toml b/rust/bt-broadcast-assistant/Cargo.toml
index 824bb18..b4d62fb 100644
--- a/rust/bt-broadcast-assistant/Cargo.toml
+++ b/rust/bt-broadcast-assistant/Cargo.toml
@@ -4,15 +4,21 @@
edition.workspace = true
license.workspace = true
+[features]
+default = []
+debug = []
+
[dependencies]
bt-bap.workspace = true
bt-bass.workspace = true
bt-common.workspace = true
bt-gatt.workspace = true
futures.workspace = true
+num.workspace = true
parking_lot.workspace = true
thiserror.workspace = true
[dev-dependencies]
assert_matches.workspace = true
bt-bass = { workspace = true, features = ["test-utils"] }
+bt-broadcast-assistant = { workspace = true, features = ["debug"] }
diff --git a/rust/bt-broadcast-assistant/src/assistant.rs b/rust/bt-broadcast-assistant/src/assistant.rs
index 2297dba..3fd08cb 100644
--- a/rust/bt-broadcast-assistant/src/assistant.rs
+++ b/rust/bt-broadcast-assistant/src/assistant.rs
@@ -143,7 +143,7 @@
self.central.scan(&vec![Filter::HasServiceData(BROADCAST_AUDIO_SCAN_SERVICE).into()])
}
- pub async fn connect_to_scan_delegator(&mut self, peer_id: PeerId) -> Result<Peer<T>, Error>
+ pub async fn connect_to_scan_delegator(&self, peer_id: PeerId) -> Result<Peer<T>, Error>
where
<T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
{
@@ -165,6 +165,91 @@
}
Err(Error::ConnectionFailure(peer_id, BROADCAST_AUDIO_SCAN_SERVICE))
}
+
+ // Manually adds broadcast source information for debugging purposes.
+ #[cfg(any(test, feature = "debug"))]
+ pub fn force_discover_broadcast_source(
+ &self,
+ peer_id: PeerId,
+ address: [u8; 6],
+ raw_address_type: u8,
+ raw_advertising_sid: u8,
+ ) -> Result<BroadcastSource, Error> {
+ use bt_common::core::{AddressType, AdvertisingSetId};
+ let broadcast_source = BroadcastSource {
+ address: Some(address),
+ address_type: Some(
+ AddressType::try_from(raw_address_type)
+ .map_err(|e| Error::Generic(e.to_string()))?,
+ ),
+ advertising_sid: Some(AdvertisingSetId(raw_advertising_sid)),
+ broadcast_id: None,
+ pa_interval: None,
+ endpoint: None,
+ };
+
+ Ok(self.broadcast_sources.merge_broadcast_source_data(&peer_id, &broadcast_source).0)
+ }
+
+ // Manually adds broadcast source information for debugging purposes.
+ #[cfg(any(test, feature = "debug"))]
+ pub fn force_discover_broadcast_source_metadata(
+ &self,
+ peer_id: PeerId,
+ raw_metadata: Vec<Vec<u8>>,
+ ) -> Result<BroadcastSource, Error> {
+ use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastIsochronousGroup};
+ use bt_common::core::ltv::LtValue;
+ use bt_common::core::CodecId;
+ use bt_common::generic_audio::metadata_ltv::Metadata;
+
+ let mut big = Vec::new();
+ for bytes in raw_metadata {
+ let metadata = {
+ if bytes.len() > 0 {
+ let (decoded_metadata, consumed_len) = Metadata::decode_all(bytes.as_slice());
+ if consumed_len != bytes.len() {
+ return Err(Error::Generic("Metadata length is not valid".to_string()));
+ }
+ decoded_metadata.into_iter().filter_map(Result::ok).collect()
+ } else {
+ vec![]
+ }
+ };
+
+ let group = BroadcastIsochronousGroup {
+ codec_id: CodecId::Assigned(bt_common::core::CodingFormat::ALawLog), // mock.
+ codec_specific_configs: vec![],
+ metadata,
+ bis: vec![],
+ };
+ big.push(group);
+ }
+
+ let endpoint = BroadcastAudioSourceEndpoint { presentation_delay_ms: 0, big };
+
+ let broadcast_source = BroadcastSource {
+ address: None,
+ address_type: None,
+ advertising_sid: None,
+ broadcast_id: None,
+ pa_interval: None,
+ endpoint: Some(endpoint),
+ };
+
+ Ok(self.broadcast_sources.merge_broadcast_source_data(&peer_id, &broadcast_source).0)
+ }
+
+ // Gets the broadcast sources currently known by the broadcast
+ // assistant.
+ pub fn known_broadcast_sources(&self) -> std::collections::HashMap<PeerId, BroadcastSource> {
+ let lock = self.broadcast_sources.0.lock();
+ let mut m = HashMap::new();
+ for (pid, source) in lock.iter() {
+ m.insert(*pid, source.clone());
+ }
+ m
+ }
}
#[cfg(test)]
@@ -255,7 +340,7 @@
client.add_service(BROADCAST_AUDIO_SCAN_SERVICE, true, service.clone());
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
- let mut assistant = BroadcastAssistant::<FakeTypes>::new(central);
+ let assistant = BroadcastAssistant::<FakeTypes>::new(central);
let conn_fut = assistant.connect_to_scan_delegator(PeerId(1004));
pin_mut!(conn_fut);
let polled = conn_fut.poll_unpin(&mut noop_cx);
diff --git a/rust/bt-broadcast-assistant/src/assistant/peer.rs b/rust/bt-broadcast-assistant/src/assistant/peer.rs
index 5131604..ec141e9 100644
--- a/rust/bt-broadcast-assistant/src/assistant/peer.rs
+++ b/rust/bt-broadcast-assistant/src/assistant/peer.rs
@@ -12,10 +12,14 @@
use bt_bass::client::error::Error as BassClientError;
use bt_bass::client::event::Event as BassEvent;
use bt_bass::client::{BigToBisSync, BroadcastAudioScanServiceClient};
+#[cfg(any(test, feature = "debug"))]
+use bt_bass::types::BroadcastReceiveState;
use bt_bass::types::PaSync;
use bt_common::core::PaInterval;
use bt_common::packet_encoding::Error as PacketError;
use bt_common::PeerId;
+#[cfg(any(test, feature = "debug"))]
+use bt_gatt::types::Handle;
use crate::assistant::DiscoveredBroadcastSources;
@@ -77,7 +81,7 @@
/// Send broadcast code for a particular broadcast.
pub async fn send_broadcast_code(
- &mut self,
+ &self,
broadcast_id: BroadcastId,
broadcast_code: [u8; 16],
) -> Result<(), Error> {
@@ -96,7 +100,7 @@
/// * `bis_sync` - desired BIG to BIS synchronization information. If the
/// set is empty, no preference value is used for all the BIGs
pub async fn add_broadcast_source(
- &mut self,
+ &self,
source_peer_id: PeerId,
address_lookup: &impl GetPeerAddr,
pa_sync: PaSync,
@@ -141,7 +145,7 @@
/// in.
/// * `bis_sync` - desired BIG to BIS synchronization information
pub async fn update_broadcast_source_sync(
- &mut self,
+ &self,
broadcast_id: BroadcastId,
pa_sync: PaSync,
bis_sync: BigToBisSync,
@@ -164,24 +168,28 @@
///
/// * `broadcast_id` - broadcast id of the braodcast source that's to be
/// removed from the scan delegator
- pub async fn remove_broadcast_source(
- &mut self,
- broadcast_id: BroadcastId,
- ) -> Result<(), Error> {
+ pub async fn remove_broadcast_source(&self, broadcast_id: BroadcastId) -> Result<(), Error> {
self.bass.remove_broadcast_source(broadcast_id).await.map_err(Into::into)
}
/// Sends a command to inform the scan delegator peer that we have
/// started scanning for broadcast sources on behalf of it.
- pub async fn inform_remote_scan_started(&mut self) -> Result<(), Error> {
+ pub async fn inform_remote_scan_started(&self) -> Result<(), Error> {
self.bass.remote_scan_started().await.map_err(Into::into)
}
/// Sends a command to inform the scan delegator peer that we have
/// stopped scanning for broadcast sources on behalf of it.
- pub async fn inform_remote_scan_stopped(&mut self) -> Result<(), Error> {
+ pub async fn inform_remote_scan_stopped(&self) -> Result<(), Error> {
self.bass.remote_scan_stopped().await.map_err(Into::into)
}
+
+ /// Returns a list of BRS characteristics' latest values the scan delegator
+ /// has received.
+ #[cfg(any(test, feature = "debug"))]
+ pub fn get_broadcast_receive_states(&self) -> Vec<(Handle, BroadcastReceiveState)> {
+ self.bass.known_broadcast_sources()
+ }
}
#[cfg(test)]
@@ -267,7 +275,7 @@
#[test]
fn add_broadcast_source_fail() {
- let (mut peer, _peer_service, broadcast_source) = setup();
+ let (peer, _peer_service, broadcast_source) = setup();
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
diff --git a/rust/bt-broadcast-assistant/src/debug.rs b/rust/bt-broadcast-assistant/src/debug.rs
new file mode 100644
index 0000000..9939810
--- /dev/null
+++ b/rust/bt-broadcast-assistant/src/debug.rs
@@ -0,0 +1,500 @@
+// Copyright 2024 Google LLC
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use bt_bap::types::BroadcastId;
+use bt_bass::client::error::Error as BassClientError;
+use bt_bass::client::event::Event as BassEvent;
+use bt_bass::client::BigToBisSync;
+use bt_bass::types::PaSync;
+use bt_common::debug_command::CommandRunner;
+use bt_common::debug_command::CommandSet;
+use bt_common::gen_commandset;
+use bt_common::PeerId;
+
+use futures::stream::FusedStream;
+use futures::Future;
+use futures::Stream;
+use num::Num;
+use parking_lot::Mutex;
+use std::collections::HashSet;
+use std::num::ParseIntError;
+use std::sync::Arc;
+
+use crate::assistant::event::*;
+use crate::assistant::peer::Peer;
+use crate::assistant::Error;
+use crate::*;
+
+gen_commandset! {
+ AssistantCmd {
+ Info = ("info", [], [], "Print information from broadcast assistant"),
+ Connect = ("connect", [], ["peer_id"], "Attempt connection to scan delegator"),
+ Disconnect = ("disconnect", [], [], "Disconnect from connected scan delegator"),
+ SendBroadcastCode = ("set-broadcast-code", [], ["broadcast_id", "broadcast_code"], "Attempt to send decryption key for a particular broadcast source to the scan delegator"),
+ AddBroadcastSource = ("add-broadcast-source", [], ["broadcast_source_pid", "pa_sync", "[bis_sync]"], "Attempt to add a particular broadcast source to the scan delegator"),
+ UpdatePaSync = ("update-pa-sync", [], ["broadcast_id", "pa_sync", "[bis_sync]"], "Attempt to update the scan delegator's desired pa sync to a particular broadcast source"),
+ RemoveBroadcastSource = ("remove-broadcast-source", [], ["broadcast_id"], "Attempt to remove a particular broadcast source to the scan delegator"),
+ RemoteScanStarted = ("inform-scan-started", [], [], "Inform the scan delegator that we have started scanning on behalf of it"),
+ RemoteScanStopped = ("inform-scan-stopped", [], [], "Inform the scan delegator that we have stopped scanning on behalf of it"),
+ // TODO(http://b/433285146): Once PA scanning is implemented, remove bottom 3 commands.
+ ForceDiscoverBroadcastSource = ("force-discover-broadcast-source", [], ["broadcast_source_pid", "address", "address_type", "advertising_sid"], "Force the broadcast assistant to become aware of the provided broadcast source"),
+ ForceDiscoverSourceMetadata = ("force-discover-source-metadata", [], ["broadcast_source_pid", "comma_separated_raw_metadata"], "Force the broadcast assistant to become aware of the provided metadata, each BIG's metadata is comma separated"),
+ ForceDiscoverEmptySourceMetadata = ("force-discover-empty-source-metadata", [], ["broadcast_source_pid", "num_big"], "Force the broadcast assistant to become aware of the provided empty metadata, as many as # BIGs specified"),
+ }
+}
+
+pub struct AssistantDebug<T: bt_gatt::GattTypes> {
+ assistant: BroadcastAssistant<T>,
+ connected_peer: Mutex<Option<Arc<Peer<T>>>>,
+ started: bool,
+}
+
+impl<T: bt_gatt::GattTypes + 'static> AssistantDebug<T> {
+ pub fn new(central: T::Central) -> Self
+ where
+ <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
+ {
+ Self {
+ assistant: BroadcastAssistant::<T>::new(central),
+ connected_peer: Mutex::new(None),
+ started: false,
+ }
+ }
+
+ pub fn start(&mut self) -> Result<EventStream<T>, Error> {
+ let event_stream = self.assistant.start()?;
+ self.started = true;
+ Ok(event_stream)
+ }
+
+ pub fn look_for_scan_delegators(&mut self) -> T::ScanResultStream {
+ self.assistant.scan_for_scan_delegators()
+ }
+
+ pub fn take_connected_peer_event_stream(
+ &mut self,
+ ) -> Result<impl Stream<Item = Result<BassEvent, BassClientError>> + FusedStream, Error> {
+ let mut lock = self.connected_peer.lock();
+ let Some(peer_arc) = lock.as_mut() else {
+ return Err(Error::Generic(format!("not connected to any scan delegator peer")));
+ };
+ let Some(peer) = Arc::get_mut(peer_arc) else {
+ return Err(Error::Generic(format!(
+ "cannot get mutable peer reference, it is shared elsewhere"
+ )));
+ };
+ peer.take_event_stream().map_err(|e| Error::Generic(format!("{e:?}")))
+ }
+
+ async fn with_peer<F, Fut>(&self, f: F)
+ where
+ F: FnOnce(Arc<Peer<T>>) -> Fut,
+ Fut: Future<Output = Result<(), crate::assistant::peer::Error>>,
+ {
+ let Some(peer) = self.connected_peer.lock().clone() else {
+ eprintln!("not connected to a scan delegator");
+ return;
+ };
+ if let Err(e) = f(peer).await {
+ eprintln!("failed to perform oepration: {e:?}");
+ }
+ }
+}
+
+/// Attempt to parse a string into an integer. If the string begins with 0x,
+/// treat the rest of the string as a hex value, otherwise treat it as decimal.
+pub(crate) fn parse_int<N>(input: &str) -> Result<N, ParseIntError>
+where
+ N: Num<FromStrRadixErr = ParseIntError>,
+{
+ if input.starts_with("0x") {
+ N::from_str_radix(&input[2..], 16)
+ } else {
+ N::from_str_radix(input, 10)
+ }
+}
+
+fn parse_peer_id(input: &str) -> Result<PeerId, String> {
+ let raw_id = match parse_int(input) {
+ Err(_) => return Err(format!("falied to parse int from {input}")),
+ Ok(i) => i,
+ };
+
+ Ok(PeerId(raw_id))
+}
+
+#[cfg(any(test, feature = "debug"))]
+fn parse_bd_addr(input: &str) -> Result<[u8; 6], String> {
+ let tokens: Vec<u8> =
+ input.split(':').map(|t| u8::from_str_radix(t, 16)).filter_map(Result::ok).collect();
+ if tokens.len() != 6 {
+ return Err(format!("failed to parse bd address from {input}"));
+ }
+ tokens.try_into().map_err(|e| format!("{e:?}"))
+}
+
+fn parse_broadcast_id(input: &str) -> Result<BroadcastId, String> {
+ let raw_id: u32 = match parse_int(input) {
+ Err(_) => return Err(format!("falied to parse int from {input}")),
+ Ok(i) => i,
+ };
+ raw_id.try_into().map_err(|e| format!("{e:?}"))
+}
+
+fn parse_bis_sync(input: &str) -> BigToBisSync {
+ input.split(',').filter_map(|t| {
+ let parts: Vec<_> = t.split('-').collect();
+ if parts.len() != 2 {
+ eprintln!("invalid big-bis sync info {t}. should be in <Ith_BIG>-<BIS_INDEX> format, will be ignored");
+ return None;
+ }
+ let ith_big = parse_int(parts[0]).ok()?;
+ let bis_index = parse_int(parts[1]).ok()?;
+ Some((ith_big, bis_index))
+ }).collect()
+}
+
+impl<T: bt_gatt::GattTypes + 'static> CommandRunner for AssistantDebug<T>
+where
+ <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
+{
+ type Set = AssistantCmd;
+
+ fn run(
+ &self,
+ cmd: Self::Set,
+ args: Vec<String>,
+ ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
+ async move {
+ match cmd {
+ AssistantCmd::Info => {
+ let known = self.assistant.known_broadcast_sources();
+ eprintln!("Known Broadcast Sources:");
+ for (id, s) in known {
+ eprintln!("PeerId ({id}), source: {s:?}");
+ }
+ }
+ AssistantCmd::Connect => {
+ if self.connected_peer.lock().is_some() {
+ eprintln!(
+ "peer already connected. Call `disconnect` first: {}",
+ AssistantCmd::Disconnect.help_simple()
+ );
+ return Ok(());
+ }
+ if args.len() != 1 {
+ eprintln!("usage: {}", AssistantCmd::Connect.help_simple());
+ return Ok(());
+ }
+
+ let Ok(peer_id) = parse_peer_id(&args[0]) else {
+ eprintln!("invalid peer id: {}", args[0]);
+ return Ok(());
+ };
+
+ let peer = self.assistant.connect_to_scan_delegator(peer_id).await;
+ match peer {
+ Ok(peer) => {
+ *self.connected_peer.lock() = Some(Arc::new(peer));
+ }
+ Err(e) => {
+ eprintln!("failed to connect to scan delegator: {e:?}");
+ }
+ };
+ }
+ AssistantCmd::Disconnect => {
+ if self.connected_peer.lock().take().is_none() {
+ eprintln!("not connected to a scan delegator");
+ }
+ }
+ AssistantCmd::SendBroadcastCode => {
+ if args.len() != 2 {
+ eprintln!("usage: {}", AssistantCmd::SendBroadcastCode.help_simple());
+ return Ok(());
+ }
+
+ let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
+ eprintln!("invalid broadcast id: {}", args[0]);
+ return Ok(());
+ };
+
+ let code = args[1].as_bytes();
+ if code.len() > 16 {
+ eprintln!(
+ "invalid broadcast code: {}. should be at max length 16",
+ args[1]
+ );
+ return Ok(());
+ }
+ let mut passcode_vec = vec![0; 16];
+ passcode_vec[16 - code.len()..16].copy_from_slice(code);
+ self.with_peer(|peer| async move {
+ peer.send_broadcast_code(broadcast_id, passcode_vec.try_into().unwrap())
+ .await
+ })
+ .await;
+ }
+ AssistantCmd::AddBroadcastSource => {
+ if args.len() < 2 {
+ eprintln!("usage: {}", AssistantCmd::AddBroadcastSource.help_simple());
+ return Ok(());
+ }
+
+ let Ok(broadcast_source_pid) = parse_peer_id(&args[0]) else {
+ eprintln!("invalid broadcast id: {}", args[0]);
+ return Ok(());
+ };
+
+ let pa_sync = match parse_int::<u8>(&args[1]) {
+ Ok(raw_val) if PaSync::try_from(raw_val).is_ok() => {
+ PaSync::try_from(raw_val).unwrap()
+ }
+ _ => {
+ eprintln!("invalid pa_sync: {}", args[1]);
+ return Ok(());
+ }
+ };
+
+ let bis_sync =
+ if args.len() == 3 { parse_bis_sync(&args[2]) } else { HashSet::new() };
+
+ self.with_peer(|peer| async move {
+ peer.add_broadcast_source(broadcast_source_pid, pa_sync, bis_sync).await
+ })
+ .await;
+ }
+ AssistantCmd::UpdatePaSync => {
+ if args.len() < 2 {
+ eprintln!("usage: {}", AssistantCmd::UpdatePaSync.help_simple());
+ return Ok(());
+ }
+
+ let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
+ eprintln!("invalid broadcast id: {}", args[0]);
+ return Ok(());
+ };
+
+ let pa_sync = match parse_int::<u8>(&args[1]) {
+ Ok(raw_val) if PaSync::try_from(raw_val).is_ok() => {
+ PaSync::try_from(raw_val).unwrap()
+ }
+ _ => {
+ eprintln!("invalid pa_sync: {}", args[1]);
+ return Ok(());
+ }
+ };
+
+ let bis_sync =
+ if args.len() == 3 { parse_bis_sync(&args[2]) } else { HashSet::new() };
+
+ self.with_peer(|peer| async move {
+ peer.update_broadcast_source_sync(broadcast_id, pa_sync, bis_sync).await
+ })
+ .await;
+ }
+ AssistantCmd::RemoveBroadcastSource => {
+ if args.len() != 1 {
+ eprintln!("usage: {}", AssistantCmd::RemoveBroadcastSource.help_simple());
+ return Ok(());
+ }
+
+ let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
+ eprintln!("invalid broadcast id: {}", args[0]);
+ return Ok(());
+ };
+
+ self.with_peer(|peer| async move {
+ peer.remove_broadcast_source(broadcast_id).await
+ })
+ .await;
+ }
+ AssistantCmd::RemoteScanStarted => {
+ self.with_peer(|peer| async move { peer.inform_remote_scan_started().await })
+ .await;
+ }
+ AssistantCmd::RemoteScanStopped => {
+ self.with_peer(|peer| async move { peer.inform_remote_scan_stopped().await })
+ .await;
+ }
+ #[cfg(feature = "debug")]
+ AssistantCmd::ForceDiscoverBroadcastSource => {
+ if args.len() != 4 {
+ eprintln!(
+ "usage: {}",
+ AssistantCmd::ForceDiscoverBroadcastSource.help_simple()
+ );
+ return Ok(());
+ }
+
+ let Ok(peer_id) = parse_peer_id(&args[0]) else {
+ eprintln!("invalid peer id: {}", args[0]);
+ return Ok(());
+ };
+
+ let Ok(address) = parse_bd_addr(&args[1]) else {
+ eprintln!("invalid address: {}", args[1]);
+ return Ok(());
+ };
+
+ let Ok(raw_addr_type) = parse_int::<u8>(&args[2]) else {
+ eprintln!("invalid address type: {}", args[2]);
+ return Ok(());
+ };
+
+ let Ok(raw_ad_sid) = parse_int::<u8>(&args[3]) else {
+ eprintln!("invalid advertising sid: {}", args[3]);
+ return Ok(());
+ };
+
+ match self.assistant.force_discover_broadcast_source(
+ peer_id,
+ address,
+ raw_addr_type,
+ raw_ad_sid,
+ ) {
+ Ok(source) => {
+ eprintln!("broadcast source after additional info: {source:?}")
+ }
+ Err(e) => {
+ eprintln!("failed to enter in broadcast source information: {e:?}")
+ }
+ }
+ }
+ #[cfg(feature = "debug")]
+ AssistantCmd::ForceDiscoverSourceMetadata => {
+ if args.len() < 2 {
+ eprintln!(
+ "usage: {}",
+ AssistantCmd::ForceDiscoverSourceMetadata.help_simple()
+ );
+ return Ok(());
+ }
+
+ let Ok(peer_id) = parse_peer_id(&args[0]) else {
+ println!("invalid peer id: {}", args[0]);
+ return Ok(());
+ };
+
+ let mut raw_metadata = Vec::new();
+ for i in 1..args.len() {
+ let ith_metadata: Vec<u8> = args[i]
+ .split(',')
+ .map(|t| parse_int(t))
+ .filter_map(Result::ok)
+ .collect();
+ raw_metadata.push(ith_metadata);
+ }
+
+ match self
+ .assistant
+ .force_discover_broadcast_source_metadata(peer_id, raw_metadata)
+ {
+ Ok(source) => eprintln!("broadcast source with metadata: {source:?}"),
+ Err(e) => eprintln!("failed to enter in broadcast source metadata: {e:?}"),
+ }
+ }
+ #[cfg(feature = "debug")]
+ AssistantCmd::ForceDiscoverEmptySourceMetadata => {
+ if args.len() != 2 {
+ eprintln!(
+ "usage: {}",
+ AssistantCmd::ForceDiscoverEmptySourceMetadata.help_simple()
+ );
+ return Ok(());
+ }
+
+ let Ok(peer_id) = parse_peer_id(&args[0]) else {
+ eprintln!("invalid peer id: {}", args[0]);
+ return Ok(());
+ };
+
+ let Ok(num_big) = parse_int::<usize>(&args[1]) else {
+ eprintln!("invalid # of bigs: {}", args[1]);
+ return Ok(());
+ };
+
+ let mut raw_metadata = Vec::new();
+ for _i in 0..num_big {
+ raw_metadata.push(vec![]);
+ }
+
+ match self
+ .assistant
+ .force_discover_broadcast_source_metadata(peer_id, raw_metadata)
+ {
+ Ok(source) => eprintln!("broadcast source with metadata: {source:?}"),
+ Err(e) => {
+ eprintln!("failed to enter in empty broadcast source metadata: {e:?}")
+ }
+ }
+ }
+ #[cfg(not(feature = "debug"))]
+ c => eprintln!("unknown command: {c:?}"),
+ }
+ Ok::<(), Error>(())
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_peer_id() {
+ // In hex string.
+ assert_eq!(parse_peer_id("0x678abc").expect("should be ok"), PeerId(0x678abc));
+ // Decimal equivalent.
+ assert_eq!(parse_peer_id("6785724").expect("should be ok"), PeerId(0x678abc));
+
+ // Invalid peer id.
+ let _ = parse_peer_id("0123zzz").expect_err("should fail");
+ }
+
+ #[test]
+ fn test_parse_bd_addr() {
+ assert_eq!(
+ parse_bd_addr("3c:80:f1:ed:32:2c").expect("should be ok"),
+ [0x3c, 0x80, 0xf1, 0xed, 0x32, 0x2c]
+ );
+ // Address with 5 parts is invalid.
+ let _ = parse_bd_addr("3c:80:f1:ed:32").expect_err("should fail");
+ // Address with 6 parts but one of them empty is invalid.
+ let _ = parse_bd_addr("3c:80:f1::32:2c").expect_err("should fail");
+ let _ = parse_bd_addr(":80:f1:ed:32:2c").expect_err("should fail");
+ let _ = parse_bd_addr("3c:80:f1:ed:32:").expect_err("should fail");
+ // Address not delimited by : is invalid.
+ let _ = parse_bd_addr("3c.80.f1.ed.32.2c").expect_err("should fail");
+ }
+
+ #[test]
+ fn test_parse_broadcast_id() {
+ assert_eq!(parse_broadcast_id("0xABCD").expect("should work"), 0xABCD.try_into().unwrap());
+ assert_eq!(parse_broadcast_id("123456").expect("should work"), 123456.try_into().unwrap());
+
+ // Invalid string cannot be parsed.
+ let _ = parse_broadcast_id("0xABYZ").expect_err("should fail");
+
+ // Broadcast ID is actually a 3 byte long number.
+ let _ = parse_broadcast_id("16777216").expect_err("should fail");
+ }
+
+ #[test]
+ fn test_parse_bis_sync() {
+ let bis_sync = parse_bis_sync("0-1,0-2,1-1");
+ assert_eq!(bis_sync.len(), 3);
+ bis_sync.contains(&(0, 1));
+ bis_sync.contains(&(0, 2));
+ bis_sync.contains(&(1, 1));
+
+ // Will ignore invalid values.
+ let bis_sync = parse_bis_sync("0-1,0-2,1:1,1-1-1,");
+ assert_eq!(bis_sync.len(), 2);
+ bis_sync.contains(&(0, 1));
+ bis_sync.contains(&(0, 2));
+
+ let bis_sync = parse_bis_sync("hellothisistoallynotvalid");
+ assert_eq!(bis_sync.len(), 0);
+ }
+}
diff --git a/rust/bt-broadcast-assistant/src/lib.rs b/rust/bt-broadcast-assistant/src/lib.rs
index 7d7af50..bf0aeb4 100644
--- a/rust/bt-broadcast-assistant/src/lib.rs
+++ b/rust/bt-broadcast-assistant/src/lib.rs
@@ -4,4 +4,5 @@
pub mod assistant;
pub use assistant::BroadcastAssistant;
+pub mod debug;
pub mod types;