1use parking_lot::Mutex;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use thiserror::Error;
10
11use bt_bap::types::BroadcastId;
12use bt_bass::client::error::Error as BassClientError;
13use bt_bass::client::BroadcastAudioScanServiceClient;
14use bt_common::{core::AdvertisingSetId, PeerId, Uuid};
15use bt_gatt::central::*;
16use bt_gatt::client::PeerServiceHandle;
17use bt_gatt::types::Error as GattError;
18use bt_gatt::Client;
19
20pub mod event;
21use event::*;
22pub mod peer;
23pub use peer::Peer;
24
25use crate::types::*;
26
27pub const BROADCAST_AUDIO_SCAN_SERVICE: Uuid = Uuid::from_u16(0x184F);
28pub const BASIC_AUDIO_ANNOUNCEMENT_SERVICE: Uuid = Uuid::from_u16(0x1851);
29pub const BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE: Uuid = Uuid::from_u16(0x1852);
30
31pub type BroadcastSourceKey = (PeerId, AdvertisingSetId);
32
33#[derive(Debug, Error)]
34pub enum Error {
35 #[error("GATT operation error: {0:?}")]
36 Gatt(#[from] GattError),
37
38 #[error("Broadcast Audio Scan Service client error at peer ({0}): {1:?}")]
39 BassClient(PeerId, BassClientError),
40
41 #[error("Not connected to Broadcast Audio Scan Service at peer ({0})")]
42 NotConnectedToBass(PeerId),
43
44 #[error("Central scanning terminated unexpectedly")]
45 CentralScanTerminated,
46
47 #[error("Failed to connect to service ({1}) at peer ({0})")]
48 ConnectionFailure(PeerId, Uuid),
49
50 #[error("Broadcast Assistant was already started previously. It cannot be started twice")]
51 AlreadyStarted,
52
53 #[error("Failed due to error: {0}")]
54 Generic(String),
55}
56
57#[derive(Debug)]
60pub(crate) struct DiscoveredBroadcastSources(Mutex<HashMap<BroadcastSourceKey, BroadcastSource>>);
61
62impl DiscoveredBroadcastSources {
63 pub fn new() -> Arc<Self> {
65 Arc::new(Self(Mutex::new(HashMap::new())))
66 }
67
68 pub(crate) fn merge_broadcast_source_data(
72 &self,
73 key: &BroadcastSourceKey,
74 data: &BroadcastSource,
75 ) -> (BroadcastSource, bool) {
76 let mut lock = self.0.lock();
77 let source = lock.entry(*key).or_default();
78 let before = source.clone();
79
80 source.merge(data);
81
82 let after = source.clone();
83 let changed = before != after;
84
85 (after, changed)
86 }
87
88 pub(crate) fn get_by_key(
90 &self,
91 peer_id: PeerId,
92 advertising_sid: AdvertisingSetId,
93 ) -> Option<BroadcastSource> {
94 let lock = self.0.lock();
95 lock.get(&(peer_id, advertising_sid)).cloned()
96 }
97
98 fn get_by_broadcast_id(&self, broadcast_id: &BroadcastId) -> Option<BroadcastSource> {
100 let lock = self.0.lock();
101 let info = lock.values().find(|v| v.broadcast_id == Some(*broadcast_id));
102 info.cloned()
103 }
104}
105
106pub struct BroadcastAssistant<T: bt_gatt::GattTypes> {
107 central: T::Central,
108 broadcast_sources: Arc<DiscoveredBroadcastSources>,
109 broadcast_source_scan_started: Arc<AtomicBool>,
110}
111
112impl<T: bt_gatt::GattTypes + 'static> BroadcastAssistant<T> {
113 pub fn new(central: T::Central) -> Self {
117 Self {
118 central,
119 broadcast_sources: DiscoveredBroadcastSources::new(),
120 broadcast_source_scan_started: Arc::new(AtomicBool::new(false)),
121 }
122 }
123
124 fn scan_filters() -> Vec<ScanFilter> {
130 vec![Filter::HasServiceData(BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE).into()]
131 }
132
133 pub fn start(&mut self) -> Result<EventStream<T>, Error> {
137 if self.is_started() {
138 return Err(Error::AlreadyStarted);
139 }
140 let scan_result_stream = self.central.scan(&Self::scan_filters());
141 self.broadcast_source_scan_started.store(true, Ordering::Relaxed);
142
143 let periodic_advertising = self
144 .central
145 .periodic_advertising()
146 .inspect_err(|e| {
147 log::warn!(
150 "Periodic Advertising not supported by platform: {:?}. Falling back to EA-only scanning.",
151 e
152 );
153 })
154 .ok();
155
156 Ok(EventStream::<T>::new(
157 scan_result_stream,
158 periodic_advertising,
159 self.broadcast_sources.clone(),
160 self.broadcast_source_scan_started.clone(),
161 ))
162 }
163
164 fn is_started(&self) -> bool {
166 self.broadcast_source_scan_started.load(Ordering::Relaxed)
167 }
168
169 pub fn scan_for_scan_delegators(&mut self) -> Result<T::ScanResultStream, Error> {
170 if self.is_started() {
171 return Err(Error::Generic(format!(
172 "Cannot scan for scan delegators while scanning for broadcast sources"
173 )));
174 }
175 Ok(self.central.scan(&vec![Filter::HasServiceData(BROADCAST_AUDIO_SCAN_SERVICE).into()]))
179 }
180
181 pub async fn connect_to_scan_delegator(&self, peer_id: PeerId) -> Result<Peer<T>, Error>
182 where
183 <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
184 {
185 let client = self.central.connect(peer_id).await?;
186 let service_handles = client.find_service(BROADCAST_AUDIO_SCAN_SERVICE).await?;
187
188 for handle in service_handles {
189 if handle.uuid() != BROADCAST_AUDIO_SCAN_SERVICE || !handle.is_primary() {
190 continue;
191 }
192 let service = handle.connect().await?;
193 let bass = BroadcastAudioScanServiceClient::<T>::create(service)
194 .await
195 .map_err(|e| Error::BassClient(peer_id, e))?;
196
197 let connected_peer =
198 Peer::<T>::new(peer_id, client, bass, self.broadcast_sources.clone());
199 return Ok(connected_peer);
200 }
201 Err(Error::ConnectionFailure(peer_id, BROADCAST_AUDIO_SCAN_SERVICE))
202 }
203
204 #[cfg(any(test, feature = "debug"))]
206 pub fn force_discover_broadcast_source(
207 &self,
208 peer_id: PeerId,
209 address: [u8; 6],
210 address_type: bt_common::core::AddressType,
211 advertising_sid: bt_common::core::AdvertisingSetId,
212 ) -> Result<BroadcastSource, Error> {
213 let broadcast_source = BroadcastSource {
214 address: Some(address),
215 address_type: Some(address_type),
216 broadcast_id: None,
217 periodic_advertising_interval: None,
218 endpoint: None,
219 broadcast_name: None,
220 };
221
222 Ok(self
223 .broadcast_sources
224 .merge_broadcast_source_data(&(peer_id, advertising_sid), &broadcast_source)
225 .0)
226 }
227
228 #[cfg(any(test, feature = "debug"))]
230 pub fn force_discover_broadcast_source_metadata(
231 &self,
232 peer_id: PeerId,
233 advertising_sid: bt_common::core::AdvertisingSetId,
234 big_metadata: Vec<Vec<bt_common::generic_audio::metadata_ltv::Metadata>>,
235 ) -> Result<BroadcastSource, Error> {
236 use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastIsochronousGroup};
237 use bt_common::core::CodecId;
238
239 let mut big = Vec::new();
240 for metadata in big_metadata {
241 let group = BroadcastIsochronousGroup {
242 codec_id: CodecId::Assigned(bt_common::core::CodingFormat::ALawLog), codec_specific_configs: vec![],
244 metadata,
245 bis: vec![],
246 };
247 big.push(group);
248 }
249
250 let endpoint = BroadcastAudioSourceEndpoint { presentation_delay_ms: 0, big };
251
252 let broadcast_source = BroadcastSource {
253 address: None,
254 address_type: None,
255 broadcast_id: None,
256 periodic_advertising_interval: None,
257 endpoint: Some(endpoint),
258 broadcast_name: None,
259 };
260
261 Ok(self
262 .broadcast_sources
263 .merge_broadcast_source_data(&(peer_id, advertising_sid), &broadcast_source)
264 .0)
265 }
266
267 pub fn known_broadcast_sources(
270 &self,
271 ) -> std::collections::HashMap<BroadcastSourceKey, BroadcastSource> {
272 self.broadcast_sources.0.lock().clone()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 use futures::{pin_mut, FutureExt};
281 use std::task::Poll;
282
283 use bt_bap::types::*;
284 use bt_common::core::{AddressType, AdvertisingSetId, PeriodicAdvertisingInterval};
285 use bt_common::generic_audio::metadata_ltv::Metadata;
286 use bt_gatt::test_utils::{FakeCentral, FakeClient, FakeTypes};
287
288 use crate::assistant::peer::tests::fake_bass_service;
289
290 #[test]
291 fn merge_broadcast_source() {
292 let discovered = DiscoveredBroadcastSources::new();
293 let bid1 = BroadcastId::try_from(1001).unwrap();
294 let key1 = (PeerId(1001), AdvertisingSetId(1));
295
296 let (bs, changed) = discovered.merge_broadcast_source_data(
298 &key1,
299 &BroadcastSource::default()
300 .with_address([1, 2, 3, 4, 5, 6])
301 .with_address_type(AddressType::Public)
302 .with_broadcast_id(bid1),
303 );
304 assert!(changed);
305 assert_eq!(
306 bs,
307 BroadcastSource {
308 address: Some([1, 2, 3, 4, 5, 6]),
309 address_type: Some(AddressType::Public),
310 broadcast_id: Some(bid1),
311 periodic_advertising_interval: None,
312 endpoint: None,
313 broadcast_name: None,
314 }
315 );
316
317 let (bs, changed) = discovered.merge_broadcast_source_data(
319 &key1,
320 &BroadcastSource::default()
321 .with_address_type(AddressType::Random)
322 .with_endpoint(BroadcastAudioSourceEndpoint {
323 presentation_delay_ms: 32,
324 big: vec![],
325 })
326 .with_periodic_advertising_interval(PeriodicAdvertisingInterval(0x0100)),
327 );
328 assert!(changed);
329 assert_eq!(
330 bs,
331 BroadcastSource {
332 address: Some([1, 2, 3, 4, 5, 6]),
333 address_type: Some(AddressType::Random),
334 broadcast_id: Some(bid1),
335 periodic_advertising_interval: Some(PeriodicAdvertisingInterval(0x0100)),
336 endpoint: Some(BroadcastAudioSourceEndpoint {
337 presentation_delay_ms: 32,
338 big: vec![]
339 }),
340 broadcast_name: None,
341 }
342 );
343
344 let (_, changed) = discovered.merge_broadcast_source_data(
346 &key1,
347 &BroadcastSource::default().with_address_type(AddressType::Random).with_endpoint(
348 BroadcastAudioSourceEndpoint { presentation_delay_ms: 32, big: vec![] },
349 ),
350 );
351 assert!(!changed);
352
353 let bid2 = BroadcastId::try_from(1002).unwrap();
356 let key2 = (PeerId(1001), AdvertisingSetId(2));
357 let (bs, changed) = discovered.merge_broadcast_source_data(
358 &key2,
359 &BroadcastSource::default()
360 .with_address([1, 2, 3, 4, 5, 6])
361 .with_address_type(AddressType::Public)
362 .with_broadcast_id(bid2),
363 );
364 assert!(changed);
365 assert_eq!(
366 bs,
367 BroadcastSource {
368 address: Some([1, 2, 3, 4, 5, 6]),
369 address_type: Some(AddressType::Public),
370 broadcast_id: Some(bid2),
371 periodic_advertising_interval: None,
372 endpoint: None,
373 broadcast_name: None,
374 }
375 );
376
377 assert!(discovered.get_by_key(key1.0, key1.1).is_some());
379 assert!(discovered.get_by_key(key2.0, key2.1).is_some());
380
381 let lock = discovered.0.lock();
383 let entry1 = lock.iter().find(|(_, v)| v.broadcast_id == Some(bid1)).unwrap();
384 assert_eq!(entry1.0 .1, AdvertisingSetId(1));
385 let entry2 = lock.iter().find(|(_, v)| v.broadcast_id == Some(bid2)).unwrap();
386 assert_eq!(entry2.0 .1, AdvertisingSetId(2));
387 }
388
389 #[test]
390 fn start_stream() {
391 let mut assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
392 let stream = assistant.start().expect("can start stream");
393
394 assert!(assistant.is_started());
396 assert!(assistant.start().is_err());
397
398 drop(stream);
400 assert!(!assistant.is_started());
401 assert!(assistant.start().is_ok());
402 }
403
404 #[test]
405 fn connect_to_scan_delegator() {
406 let mut central = FakeCentral::new();
408 let mut client = FakeClient::new();
409 central.add_client(PeerId(1004), client.clone());
410 let service = fake_bass_service();
411 client.add_service(BROADCAST_AUDIO_SCAN_SERVICE, true, service.clone());
412
413 let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
414 let assistant = BroadcastAssistant::<FakeTypes>::new(central);
415 let conn_fut = assistant.connect_to_scan_delegator(PeerId(1004));
416 pin_mut!(conn_fut);
417 let polled = conn_fut.poll_unpin(&mut noop_cx);
418 let Poll::Ready(res) = polled else {
419 panic!("should be ready");
420 };
421 let _ = res.expect("should be ok");
422 }
423
424 #[test]
425 fn force_discover_broadcast_source_test() {
426 let assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
427 let peer_id = PeerId(1);
428 let address = [1, 2, 3, 4, 5, 6];
429 let address_type = AddressType::Public;
430 let sid = AdvertisingSetId(1);
431
432 let source =
433 assistant.force_discover_broadcast_source(peer_id, address, address_type, sid).unwrap();
434
435 assert_eq!(source.address, Some(address));
436 assert_eq!(source.address_type, Some(address_type));
437
438 let known = assistant.known_broadcast_sources();
440 assert_eq!(known.get(&(peer_id, sid)).unwrap().address, Some(address));
441 }
442
443 #[test]
444 fn force_discover_broadcast_source_metadata_test() {
445 let assistant = BroadcastAssistant::<FakeTypes>::new(FakeCentral::new());
446 let peer_id = PeerId(1);
447 let metadata = vec![vec![Metadata::BroadcastAudioImmediateRenderingFlag]];
448 let sid = AdvertisingSetId(1);
449
450 let source = assistant
451 .force_discover_broadcast_source_metadata(peer_id, sid, metadata.clone())
452 .unwrap();
453
454 let endpoint = source.endpoint.unwrap();
455 assert_eq!(endpoint.big.len(), 1);
456 assert_eq!(endpoint.big[0].metadata, metadata[0]);
457
458 let known = assistant.known_broadcast_sources();
460 assert!(known.contains_key(&(peer_id, sid)));
461 }
462}