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;