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