Skip to main content

bt_gatt_fuchsia/
periodic_advertising.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use async_utils::hanging_get::client::HangingGetStream;
6use bt_common::PeerId;
7use fidl_fuchsia_bluetooth_le as fidl_le;
8use futures::stream::Stream;
9use futures::{Future, StreamExt, TryStreamExt};
10use std::pin::Pin;
11
12#[cfg(test)]
13use crate::to_fidl_uuid;
14use crate::{to_fidl_peer_id, to_gatt_scan_data};
15
16fn to_gatt_sync_error(err: fidl_le::PeriodicAdvertisingSyncError) -> bt_gatt::types::Error {
17    let gatt_err = match err {
18        fidl_le::PeriodicAdvertisingSyncError::InitialSynchronizationFailed => {
19            bt_gatt::periodic_advertising::Error::SyncEstablishFailed
20        }
21        fidl_le::PeriodicAdvertisingSyncError::SynchronizationLost => {
22            bt_gatt::periodic_advertising::Error::SyncLost
23        }
24        _ => bt_gatt::periodic_advertising::Error::Io,
25    };
26    bt_gatt::types::Error::Other(Box::new(gatt_err))
27}
28
29fn to_gatt_phy(phy: fidl_le::PhysicalLayer) -> bt_common::core::Phy {
30    match phy {
31        fidl_le::PhysicalLayer::Le1M => bt_common::core::Phy::Le1m,
32        fidl_le::PhysicalLayer::Le2M => bt_common::core::Phy::Le2m,
33        fidl_le::PhysicalLayer::LeCoded => bt_common::core::Phy::LeCoded,
34        _ => bt_common::core::Phy::Le1m,
35    }
36}
37
38fn to_gatt_big_info(
39    info: &fidl_le::BroadcastIsochronousGroupInfo,
40) -> bt_gatt::periodic_advertising::BroadcastIsochronousGroupInfo {
41    bt_gatt::periodic_advertising::BroadcastIsochronousGroupInfo {
42        streams_count: info.streams_count.unwrap_or(0),
43        sdu_interval: 0, // TODO(https://fxbug.dev/429213165): Missing in FIDL
44        max_sdu_size: info.max_sdu_size.unwrap_or(0),
45        phy: info.phy.map(to_gatt_phy).unwrap_or(bt_common::core::Phy::Le1m),
46        encryption: info.encryption.unwrap_or(false),
47    }
48}
49
50fn to_gatt_periodic_advertising_report(
51    report: fidl_le::PeriodicAdvertisingReport,
52) -> bt_gatt::periodic_advertising::PeriodicAdvertisingReport {
53    bt_gatt::periodic_advertising::PeriodicAdvertisingReport {
54        rssi: report.rssi.unwrap_or(0),
55        data: report.data.map(to_gatt_scan_data).unwrap_or_default(),
56        event_counter: report.event_counter,
57        subevent: report.subevent,
58        timestamp: report.timestamp.unwrap_or(0),
59    }
60}
61
62fn to_gatt_sync_report(
63    report: fidl_le::SyncReport,
64) -> bt_gatt::Result<Option<bt_gatt::periodic_advertising::SyncReport>> {
65    match report {
66        fidl_le::SyncReport::PeriodicAdvertisingReport(r) => {
67            Ok(Some(bt_gatt::periodic_advertising::SyncReport::PeriodicAdvertisingReport(
68                to_gatt_periodic_advertising_report(r),
69            )))
70        }
71        fidl_le::SyncReport::BroadcastIsochronousGroupInfoReport(r) => {
72            match &r.info {
73                Some(info) => {
74                    let info = to_gatt_big_info(info);
75                    Ok(Some(bt_gatt::periodic_advertising::SyncReport::BroadcastIsochronousGroupInfoReport(
76                        bt_gatt::periodic_advertising::BroadcastIsochronousGroupInfoReport {
77                            info,
78                            timestamp: r.timestamp.unwrap_or(0),
79                        },
80                    )))
81                }
82                None => Ok(None),
83            }
84        }
85        _ => Err(bt_gatt::types::Error::Other(Box::new(std::io::Error::new(
86            std::io::ErrorKind::InvalidData,
87            "unknown SyncReport variant",
88        )))),
89    }
90}
91
92fn create_sync_stream(
93    sync_proxy: fidl_le::PeriodicAdvertisingSyncProxy,
94    event_stream: fidl_le::PeriodicAdvertisingSyncEventStream,
95) -> <PeriodicAdvertising as bt_gatt::periodic_advertising::PeriodicAdvertising>::SyncStream {
96    let hanging_get_stream =
97        HangingGetStream::new_eager(sync_proxy, |p| p.watch_advertising_report());
98
99    let reports_stream = hanging_get_stream
100        .map_err(|e| bt_gatt::types::Error::Other(Box::new(e)))
101        .map(|res| match res {
102            Ok(response) => {
103                let reports = response.reports.unwrap_or_default();
104                let gatt_reports: Vec<bt_gatt::Result<bt_gatt::periodic_advertising::SyncReport>> =
105                    reports
106                        .into_iter()
107                        .filter_map(|r| match to_gatt_sync_report(r) {
108                            Ok(Some(report)) => Some(Ok(report)),
109                            Ok(None) => None,
110                            Err(e) => Some(Err(e)),
111                        })
112                        .collect();
113                futures::stream::iter(gatt_reports)
114            }
115            Err(e) => futures::stream::iter(vec![Err(e)]),
116        })
117        .flatten();
118
119    let event_errors = event_stream.filter_map(|event_res| {
120        let res = match event_res {
121            Ok(fidl_le::PeriodicAdvertisingSyncEvent::OnError { error }) => {
122                Some(Err(to_gatt_sync_error(error)))
123            }
124            Ok(_) => Some(Err(bt_gatt::types::Error::Other(Box::new(std::io::Error::new(
125                std::io::ErrorKind::InvalidData,
126                "unexpected event after establishment",
127            ))))),
128            Err(e) => Some(Err(bt_gatt::types::Error::Other(Box::new(e)))),
129        };
130        futures::future::ready(res)
131    });
132
133    let merged_stream = futures::stream::select(reports_stream, event_errors);
134
135    Box::pin(merged_stream)
136}
137
138#[derive(Clone)]
139pub struct PeriodicAdvertising {
140    pub(crate) proxy: fidl_le::CentralProxy,
141}
142
143impl bt_gatt::periodic_advertising::PeriodicAdvertising for PeriodicAdvertising {
144    type SyncFut = Pin<Box<dyn Future<Output = bt_gatt::Result<Self::SyncStream>> + Send>>;
145    type SyncStream = Pin<
146        Box<
147            dyn Stream<Item = bt_gatt::Result<bt_gatt::periodic_advertising::SyncReport>>
148                + Send
149                + 'static,
150        >,
151    >;
152
153    fn sync_to_advertising_reports(
154        &self,
155        peer_id: PeerId,
156        adv_sid: u8,
157        config: bt_gatt::periodic_advertising::SyncConfiguration,
158    ) -> Self::SyncFut {
159        let proxy = self.proxy.clone();
160
161        Box::pin(async move {
162            let (sync_proxy, server_end) =
163                fidl::endpoints::create_proxy::<fidl_le::PeriodicAdvertisingSyncMarker>();
164
165            let fidl_config = fidl_le::PeriodicAdvertisingSyncConfiguration {
166                filter_duplicates: Some(config.filter_duplicates),
167                ..Default::default()
168            };
169
170            proxy
171                .sync_to_periodic_advertising(fidl_le::CentralSyncToPeriodicAdvertisingRequest {
172                    peer_id: Some(to_fidl_peer_id(&peer_id)),
173                    advertising_sid: Some(adv_sid),
174                    sync: Some(server_end),
175                    config: Some(fidl_config),
176                    ..Default::default()
177                })
178                .map_err(|e| bt_gatt::types::Error::Other(Box::new(e)))?;
179
180            let mut event_stream = sync_proxy.take_event_stream();
181
182            match event_stream.next().await {
183                Some(Ok(fidl_le::PeriodicAdvertisingSyncEvent::OnEstablished { .. })) => {
184                    let stream = create_sync_stream(sync_proxy, event_stream);
185                    Ok(stream)
186                }
187                Some(Ok(fidl_le::PeriodicAdvertisingSyncEvent::OnError { error })) => {
188                    Err(to_gatt_sync_error(error))
189                }
190                Some(Ok(_)) => Err(bt_gatt::types::Error::Other(Box::new(std::io::Error::new(
191                    std::io::ErrorKind::InvalidData,
192                    "unknown event received during establishment",
193                )))),
194                Some(Err(e)) => Err(bt_gatt::types::Error::Other(Box::new(e))),
195                None => Err(bt_gatt::types::Error::Other(Box::new(std::io::Error::new(
196                    std::io::ErrorKind::UnexpectedEof,
197                    "Event stream closed before establishment",
198                )))),
199            }
200        })
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use bt_common::Uuid;
208    use bt_gatt::central::AdvertisingDatum;
209
210    #[test]
211    fn test_to_gatt_sync_error() {
212        let err = fidl_le::PeriodicAdvertisingSyncError::InitialSynchronizationFailed;
213        let gatt_err = to_gatt_sync_error(err);
214        assert!(format!("{gatt_err:?}").contains("SyncEstablishFailed"));
215
216        let err = fidl_le::PeriodicAdvertisingSyncError::SynchronizationLost;
217        let gatt_err = to_gatt_sync_error(err);
218        assert!(format!("{gatt_err:?}").contains("SyncLost"));
219
220        let err = fidl_le::PeriodicAdvertisingSyncError::NotSupportedLocal;
221        let gatt_err = to_gatt_sync_error(err);
222        assert!(format!("{gatt_err:?}").contains("Io"));
223    }
224
225    #[test]
226    fn test_to_gatt_phy() {
227        assert_eq!(to_gatt_phy(fidl_le::PhysicalLayer::Le1M), bt_common::core::Phy::Le1m);
228        assert_eq!(to_gatt_phy(fidl_le::PhysicalLayer::Le2M), bt_common::core::Phy::Le2m);
229        assert_eq!(to_gatt_phy(fidl_le::PhysicalLayer::LeCoded), bt_common::core::Phy::LeCoded);
230    }
231
232    #[test]
233    fn test_to_gatt_big_info() {
234        let fidl_info = fidl_le::BroadcastIsochronousGroupInfo {
235            streams_count: Some(5),
236            max_sdu_size: Some(200),
237            phy: Some(fidl_le::PhysicalLayer::LeCoded),
238            encryption: Some(true),
239            ..Default::default()
240        };
241        let gatt_info = to_gatt_big_info(&fidl_info);
242        assert_eq!(gatt_info.streams_count, 5);
243        assert_eq!(gatt_info.sdu_interval, 0); // Placeholder
244        assert_eq!(gatt_info.max_sdu_size, 200);
245        assert_eq!(gatt_info.phy, bt_common::core::Phy::LeCoded);
246        assert_eq!(gatt_info.encryption, true);
247    }
248
249    #[test]
250    fn test_to_gatt_scan_data() {
251        let service_uuid = Uuid::from_u16(0x1852);
252        let scan_data = fidl_le::ScanData {
253            tx_power: Some(-15),
254            service_uuids: Some(vec![to_fidl_uuid(&service_uuid)]),
255            service_data: Some(vec![fidl_le::ServiceData {
256                uuid: to_fidl_uuid(&service_uuid),
257                data: vec![4, 5, 6],
258            }]),
259            manufacturer_data: Some(vec![fidl_le::ManufacturerData {
260                company_id: 0x00E0,
261                data: vec![7, 8],
262            }]),
263            uris: Some(vec!["https://example.com".to_string()]),
264            broadcast_name: Some("My Broadcast".to_string()),
265            resolvable_set_identifier: Some([1, 2, 3, 4, 5, 6]),
266            ..Default::default()
267        };
268
269        let advertised = to_gatt_scan_data(scan_data);
270        assert_eq!(advertised.len(), 7);
271
272        assert!(advertised.iter().any(|d| matches!(d, AdvertisingDatum::TxPowerLevel(-15))));
273
274        assert!(advertised.iter().any(|d| match d {
275            AdvertisingDatum::Services(uuids) => uuids == &[service_uuid],
276            _ => false,
277        }));
278
279        assert!(advertised.iter().any(|d| match d {
280            AdvertisingDatum::ServiceData(uuid, data) =>
281                *uuid == service_uuid && data == &[4, 5, 6],
282            _ => false,
283        }));
284
285        assert!(advertised.iter().any(|d| match d {
286            AdvertisingDatum::ManufacturerData(company_id, data) =>
287                *company_id == 0x00E0 && data == &[7, 8],
288            _ => false,
289        }));
290
291        assert!(advertised.iter().any(|d| match d {
292            AdvertisingDatum::Uri(uri) => uri == "https://example.com",
293            _ => false,
294        }));
295
296        assert!(advertised.iter().any(|d| match d {
297            AdvertisingDatum::BroadcastName(name) => name == "My Broadcast",
298            _ => false,
299        }));
300
301        assert!(advertised.iter().any(|d| match d {
302            AdvertisingDatum::ResolvableSetIdentifier(rsi) => rsi == &[1, 2, 3, 4, 5, 6],
303            _ => false,
304        }));
305    }
306}