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;