1use core::pin::Pin;
6use futures::stream::{
7 abortable, AbortHandle, FusedStream, FuturesUnordered, SelectAll, Stream, StreamExt,
8};
9use futures::FutureExt;
10use std::collections::HashMap;
11use std::future::Future;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14use std::task::Poll;
15
16use bt_bap::types::{BroadcastAudioSourceEndpoint, BroadcastId};
17use bt_common::core::{AdvertisingSetId, PeriodicAdvertisingInterval};
18use bt_common::packet_encoding::Decodable;
19use bt_common::packet_encoding::Error as PacketError;
20use bt_common::PeerId;
21use bt_gatt::central::{AdvertisingDatum, ScanResult};
22use bt_gatt::periodic_advertising::{PeriodicAdvertising, SyncConfiguration, SyncReport};
23use bt_gatt::GattTypes;
24
25use crate::assistant::{
26 DiscoveredBroadcastSources, Error, BASIC_AUDIO_ANNOUNCEMENT_SERVICE,
27 BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE,
28};
29use crate::types::BroadcastSource;
30
31#[derive(Debug)]
32pub enum Event {
33 FoundBroadcastSource {
34 peer: PeerId,
35 advertising_sid: AdvertisingSetId,
36 source: BroadcastSource,
37 },
38 CouldNotParseAdvertisingData {
39 peer: PeerId,
40 error: PacketError,
41 },
42}
43
44type PeriodicAdvertisingSyncResult<T> = Result<
45 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream,
46 bt_gatt::types::Error,
47>;
48
49type PeriodicAdvertisingFuture<T> =
50 Pin<Box<dyn Future<Output = (PeerId, u8, PeriodicAdvertisingSyncResult<T>)>>>;
51
52type PeriodicAdvertisingStream =
53 Pin<Box<dyn Stream<Item = (PeerId, u8, Result<SyncReport, bt_gatt::types::Error>)>>>;
54
55pub struct EventStream<T: bt_gatt::GattTypes> {
59 scan_result_stream: Pin<Box<<T as bt_gatt::GattTypes>::ScanResultStream>>,
60 terminated: bool,
61
62 broadcast_sources: Arc<DiscoveredBroadcastSources>,
63 broadcast_source_scan_started: Arc<AtomicBool>,
64
65 periodic_advertising: Option<T::PeriodicAdvertising>,
66
67 establishing_periodic_advertising_syncs: FuturesUnordered<PeriodicAdvertisingFuture<T>>,
68 active_periodic_advertising_sync_streams: SelectAll<PeriodicAdvertisingStream>,
69 active_syncs: HashMap<(PeerId, u8), Option<AbortHandle>>,
70}
71
72impl<T: bt_gatt::GattTypes> Unpin for EventStream<T> {}
73
74impl<T: bt_gatt::GattTypes> EventStream<T>
75where
76 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
77 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
78{
79 pub(crate) fn new(
80 scan_result_stream: T::ScanResultStream,
81 periodic_advertising: Option<T::PeriodicAdvertising>,
82 broadcast_sources: Arc<DiscoveredBroadcastSources>,
83 broadcast_source_scan_started: Arc<AtomicBool>,
84 ) -> Self {
85 Self {
86 scan_result_stream: Box::pin(scan_result_stream),
87 terminated: false,
88 broadcast_sources,
89 broadcast_source_scan_started,
90 periodic_advertising,
91 establishing_periodic_advertising_syncs: FuturesUnordered::new(),
92 active_periodic_advertising_sync_streams: SelectAll::new(),
93 active_syncs: HashMap::new(),
94 }
95 }
96
97 fn poll_establishing_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<()> {
103 if self.establishing_periodic_advertising_syncs.is_terminated() {
104 return Poll::Pending;
105 }
106
107 match self.establishing_periodic_advertising_syncs.poll_next_unpin(cx) {
108 Poll::Ready(Some((peer_id, sid, Ok(stream)))) => {
109 self.handle_established_sync(peer_id, sid, stream);
110 Poll::Ready(())
111 }
112 Poll::Ready(Some((peer_id, sid, Err(_)))) => {
113 self.active_syncs.remove(&(peer_id, sid));
114 Poll::Ready(())
115 }
116 Poll::Ready(None) | Poll::Pending => Poll::Pending,
117 }
118 }
119
120 fn poll_active_syncs(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Option<Event>> {
130 if self.active_periodic_advertising_sync_streams.is_terminated() {
131 return Poll::Pending;
132 }
133
134 match self.active_periodic_advertising_sync_streams.poll_next_unpin(cx) {
135 Poll::Ready(Some((peer_id, sid, Ok(report)))) => {
136 match self.handle_periodic_advertising_report(peer_id, sid, report) {
137 Some(event) => Poll::Ready(Some(event)),
138 None => Poll::Ready(None), }
140 }
141 Poll::Ready(Some((peer_id, sid, Err(_)))) => {
142 self.active_syncs.remove(&(peer_id, sid));
143 Poll::Ready(None) }
145 Poll::Ready(None) | Poll::Pending => Poll::Pending,
146 }
147 }
148
149 fn try_into_broadcast_source(
153 scan_result: &ScanResult,
154 ) -> Result<Option<BroadcastSource>, PacketError> {
155 let mut source = None;
156 for datum in &scan_result.advertised {
157 match datum {
158 AdvertisingDatum::ServiceData(uuid, data)
159 if *uuid == BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE =>
160 {
161 let bid = BroadcastId::decode(data.as_slice()).0?;
162 source.get_or_insert(BroadcastSource::default()).with_broadcast_id(bid);
163 }
164 AdvertisingDatum::BroadcastName(name) => {
165 source
166 .get_or_insert(BroadcastSource::default())
167 .with_broadcast_name(name.clone());
168 }
169 _ => {}
170 }
171 }
172 if let Some(src) = &mut source {
173 src.periodic_advertising_interval =
174 scan_result.periodic_advertising_interval.map(PeriodicAdvertisingInterval);
175 }
176 Ok(source)
177 }
178
179 fn handle_established_sync(
180 &mut self,
181 peer_id: PeerId,
182 sid: u8,
183 stream: <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream,
184 ) {
185 let mapped_stream = stream.map(move |report| (peer_id, sid, report));
186 let (abortable_stream, abort_handle) = abortable(mapped_stream);
187
188 self.active_periodic_advertising_sync_streams.push(Box::pin(abortable_stream));
189 self.active_syncs.insert((peer_id, sid), Some(abort_handle));
190 }
191
192 fn handle_periodic_advertising_report(
193 &mut self,
194 peer_id: PeerId,
195 sid: u8,
196 report: SyncReport,
197 ) -> Option<Event> {
198 let SyncReport::PeriodicAdvertisingReport(report) = report else {
199 return None;
200 };
201
202 let Some(base) = parse_base_from_advertising_data(&report.data) else {
203 return None;
204 };
205
206 let (broadcast_source, changed) = self.broadcast_sources.merge_broadcast_source_data(
207 &(peer_id, AdvertisingSetId(sid)),
208 &BroadcastSource::default().with_endpoint(base),
209 );
210
211 if broadcast_source.is_ready_to_add() && changed {
212 if let Some(Some(handle)) = self.active_syncs.remove(&(peer_id, sid)) {
213 handle.abort();
214 }
215 return Some(Event::FoundBroadcastSource {
216 peer: peer_id,
217 advertising_sid: AdvertisingSetId(sid),
218 source: broadcast_source,
219 });
220 }
221 None
222 }
223
224 fn handle_scan_result(&mut self, scanned: ScanResult) -> Option<Event> {
225 let found_source = match Self::try_into_broadcast_source(&scanned) {
226 Ok(Some(src)) => src,
227 Ok(None) => return None,
228 Err(e) => {
229 return Some(Event::CouldNotParseAdvertisingData { peer: scanned.id, error: e })
230 }
231 };
232
233 let Some(raw_sid) = scanned.advertising_sid else {
234 return None;
235 };
236 let sid = AdvertisingSetId(raw_sid);
237
238 let (broadcast_source, changed) =
239 self.broadcast_sources.merge_broadcast_source_data(&(scanned.id, sid), &found_source);
240
241 if broadcast_source.is_ready_to_add() && changed {
242 return Some(Event::FoundBroadcastSource {
243 peer: scanned.id,
244 advertising_sid: sid,
245 source: broadcast_source,
246 });
247 }
248
249 if broadcast_source.endpoint.is_some() {
254 return None;
255 }
256
257 let Some(ref pa) = self.periodic_advertising else {
259 return None;
260 };
261
262 let key = (scanned.id, sid.0);
265 if self.active_syncs.contains_key(&key) {
266 return None;
267 }
268
269 self.active_syncs.insert(key, None);
270 let fut = pa.sync_to_advertising_reports(
271 scanned.id,
272 sid.0,
273 SyncConfiguration { filter_duplicates: true },
274 );
275 let mapped_fut = fut.map(move |res| (scanned.id, sid.0, res));
276 self.establishing_periodic_advertising_syncs.push(Box::pin(mapped_fut));
277
278 None
279 }
280}
281
282fn parse_base_from_advertising_data(
283 data: &[AdvertisingDatum],
284) -> Option<BroadcastAudioSourceEndpoint> {
285 for datum in data {
286 let AdvertisingDatum::ServiceData(uuid, service_data) = datum else {
287 continue;
288 };
289 if *uuid != BASIC_AUDIO_ANNOUNCEMENT_SERVICE {
290 continue;
291 }
292 let (Ok(base), _) = BroadcastAudioSourceEndpoint::decode(service_data) else {
293 continue;
294 };
295 return Some(base);
296 }
297 None
298}
299
300impl<T: bt_gatt::GattTypes> Drop for EventStream<T> {
301 fn drop(&mut self) {
302 self.broadcast_source_scan_started.store(false, Ordering::Relaxed);
303 }
304}
305
306impl<T: bt_gatt::GattTypes> FusedStream for EventStream<T>
307where
308 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
309 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
310{
311 fn is_terminated(&self) -> bool {
312 self.terminated
313 }
314}
315
316impl<T: bt_gatt::GattTypes> Stream for EventStream<T>
317where
318 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncStream: 'static,
319 <<T as GattTypes>::PeriodicAdvertising as PeriodicAdvertising>::SyncFut: 'static,
320{
321 type Item = Result<Event, Error>;
322
323 fn poll_next(
324 mut self: std::pin::Pin<&mut Self>,
325 cx: &mut std::task::Context<'_>,
326 ) -> Poll<Option<Self::Item>> {
327 if self.terminated {
328 return Poll::Ready(None);
329 }
330
331 loop {
332 let mut progressed = false;
333
334 if self.poll_establishing_syncs(cx).is_ready() {
335 progressed = true;
336 }
337
338 match self.poll_active_syncs(cx) {
339 Poll::Ready(Some(event)) => return Poll::Ready(Some(Ok(event))),
340 Poll::Ready(None) => progressed = true,
341 Poll::Pending => {}
342 }
343
344 match self.scan_result_stream.poll_next_unpin(cx) {
345 Poll::Ready(Some(Ok(scanned))) => {
346 progressed = true;
347 if let Some(event) = self.handle_scan_result(scanned) {
348 return Poll::Ready(Some(Ok(event)));
349 }
350 }
351 Poll::Ready(None | Some(Err(_))) => {
352 self.terminated = true;
353 self.broadcast_source_scan_started.store(false, Ordering::Relaxed);
354 return Poll::Ready(Some(Err(Error::CentralScanTerminated)));
355 }
356 Poll::Pending => {}
357 }
358
359 if !progressed {
360 break;
361 }
362 }
363
364 Poll::Pending
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 use assert_matches::assert_matches;
373
374 use bt_common::core::{AddressType, AdvertisingSetId};
375 use bt_gatt::central::{AdvertisingDatum, PeerName};
376 use bt_gatt::test_utils::{
377 FakePeriodicAdvertising, FakeTypes, ScannedResultStream, ScannedResultStreamController,
378 };
379 use bt_gatt::types::Error as BtGattError;
380 use bt_gatt::types::GattError;
381
382 fn setup_stream(
383 ) -> (EventStream<FakeTypes>, ScannedResultStreamController, FakePeriodicAdvertising) {
384 let fake_scan_result_stream = ScannedResultStream::new();
385 let controller = fake_scan_result_stream.controller();
386 let broadcast_sources = DiscoveredBroadcastSources::new();
387 let broadcast_source_scan_started = Arc::new(AtomicBool::new(false));
388 let pa = FakePeriodicAdvertising::default();
389
390 (
391 EventStream::<FakeTypes>::new(
392 fake_scan_result_stream,
393 Some(pa.clone()),
394 broadcast_sources,
395 broadcast_source_scan_started,
396 ),
397 controller,
398 pa,
399 )
400 }
401
402 #[test]
403 fn poll_found_broadcast_source_events() {
404 let (mut stream, scan_result_controller, pa) = setup_stream();
405
406 let broadcast_source_pid = PeerId(1005);
408
409 scan_result_controller.add_scanned_result(Ok(ScanResult {
410 id: broadcast_source_pid,
411 connectable: true,
412 name: PeerName::Unknown,
413 advertised: vec![
414 AdvertisingDatum::ServiceData(
415 BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE,
416 vec![0x01, 0x02, 0x03],
417 ),
418 AdvertisingDatum::BroadcastName("Test Broadcast".to_string()),
419 ],
420 advertising_sid: Some(1),
421 periodic_advertising_interval: Some(0x0100),
422 }));
423
424 let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
427 assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
428
429 assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
431
432 let _ = stream.broadcast_sources.merge_broadcast_source_data(
434 &(broadcast_source_pid, AdvertisingSetId(1)),
435 &BroadcastSource::default()
436 .with_address([1, 2, 3, 4, 5, 6])
437 .with_address_type(AddressType::Public),
438 );
439
440 #[rustfmt::skip]
442 let base_data = vec![
443 0x10, 0x20, 0x30, 0x02, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x02, 0x05, 0x08, ];
453
454 let advertising_data =
455 vec![AdvertisingDatum::ServiceData(BASIC_AUDIO_ANNOUNCEMENT_SERVICE, base_data)];
456
457 let periodic_advertising_sender = pa
460 .get_sender(broadcast_source_pid)
461 .expect("should have registered periodic advertising sync sender");
462
463 periodic_advertising_sender
465 .unbounded_send(Ok(SyncReport::PeriodicAdvertisingReport(
466 bt_gatt::periodic_advertising::PeriodicAdvertisingReport {
467 rssi: -50,
468 data: advertising_data,
469 event_counter: None,
470 subevent: None,
471 timestamp: 0,
472 },
473 )))
474 .unwrap();
475
476 let Poll::Ready(Some(Ok(event))) = stream.poll_next_unpin(&mut noop_cx) else {
479 panic!("should have received event");
480 };
481 assert_matches!(event, Event::FoundBroadcastSource { peer, advertising_sid, source } => {
482 assert_eq!(peer, broadcast_source_pid);
483 assert_eq!(advertising_sid, AdvertisingSetId(1));
484 assert_eq!(source.periodic_advertising_interval, Some(PeriodicAdvertisingInterval(0x0100)));
485 assert_eq!(source.address, Some([1, 2, 3, 4, 5, 6]));
486 assert_eq!(source.broadcast_name, Some("Test Broadcast".to_string()));
487 });
488
489 assert!(stream.active_syncs.is_empty());
492
493 assert!(stream.poll_next_unpin(&mut noop_cx).is_pending());
495 }
496
497 #[test]
498 fn central_scan_stream_terminates() {
499 let (mut stream, scan_result_controller, _pa) = setup_stream();
500
501 scan_result_controller.add_scanned_result(Err(BtGattError::Gatt(GattError::InvalidPdu)));
503
504 let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
505 match stream.poll_next_unpin(&mut noop_cx) {
506 Poll::Ready(Some(Err(e))) => assert_matches!(e, Error::CentralScanTerminated),
507 _ => panic!("should have received central scan terminated error"),
508 }
509
510 assert_matches!(stream.poll_next_unpin(&mut noop_cx), Poll::Ready(None));
512 assert_matches!(stream.is_terminated(), true);
513 }
514}