rust/bt-gatt: Add periodic advertising sync API

Adds a new API to the `Central` trait for synchronizing to periodic
advertising trains. This is the Rust equivalent of the Periodic
Advertisement Scanning FIDL API.

- Adds `sync_to_periodic_advertising` to the `Central` trait
- Adds a `periodic_advertising` module with necessary types
- Adds a `PeriodicAdvertisingTypes` trait for a clean API
- Updates tests and fixes other crates affected by the changes

Bug: b/433283601
Test: cargo build && cargo test
Change-Id: I2c70c352a5f703b50f5036a2d6aa446bc6786380
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2500
Reviewed-by: Marie Janssen <jamuraa@google.com>
diff --git a/rust/bt-broadcast-assistant/src/assistant/event.rs b/rust/bt-broadcast-assistant/src/assistant/event.rs
index 3f781f6..e1e444c 100644
--- a/rust/bt-broadcast-assistant/src/assistant/event.rs
+++ b/rust/bt-broadcast-assistant/src/assistant/event.rs
@@ -165,6 +165,7 @@
                 BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE,
                 vec![0x01, 0x02, 0x03],
             )],
+            advertising_sid: 0,
         }));
 
         // Found broadcast source event shouldn't have been sent since braodcast source
@@ -209,6 +210,7 @@
                 BASIC_AUDIO_ANNOUNCEMENT_SERVICE,
                 base_data.clone(),
             )],
+            advertising_sid: 0,
         }));
 
         // Expect the stream to send out broadcast source found event since information
@@ -231,6 +233,7 @@
                 BASIC_AUDIO_ANNOUNCEMENT_SERVICE,
                 base_data.clone(),
             )],
+            advertising_sid: 0,
         }));
 
         // Shouldn't have gotten the event again since the information remained the
diff --git a/rust/bt-broadcast-assistant/src/assistant/peer.rs b/rust/bt-broadcast-assistant/src/assistant/peer.rs
index ec141e9..5c3b5b5 100644
--- a/rust/bt-broadcast-assistant/src/assistant/peer.rs
+++ b/rust/bt-broadcast-assistant/src/assistant/peer.rs
@@ -3,8 +3,8 @@
 // found in the LICENSE file.
 
 use bt_gatt::pii::GetPeerAddr;
-use futures::Stream;
 use futures::stream::FusedStream;
+use futures::Stream;
 use std::sync::Arc;
 use thiserror::Error;
 
@@ -94,8 +94,8 @@
     ///
     /// * `broadcast_source_pid` - peer id of the braodcast source that's to be
     ///   added to this scan delegator peer
-    /// * `address_lookup` - An implementation of [`GetPeerAddr`] that will
-    ///   be used to look up the peer's address.
+    /// * `address_lookup` - An implementation of [`GetPeerAddr`] that will be
+    ///   used to look up the peer's address.
     /// * `pa_sync` - pa sync mode the peer should attempt to be in
     /// * `bis_sync` - desired BIG to BIS synchronization information. If the
     ///   set is empty, no preference value is used for all the BIGs
@@ -198,7 +198,7 @@
 
     use assert_matches::assert_matches;
     use bt_gatt::pii::StaticPeerAddr;
-    use futures::{FutureExt, pin_mut};
+    use futures::{pin_mut, FutureExt};
     use std::collections::HashSet;
     use std::task::Poll;
 
diff --git a/rust/bt-broadcast-assistant/src/debug.rs b/rust/bt-broadcast-assistant/src/debug.rs
index 02d3cb7..6a4f308 100644
--- a/rust/bt-broadcast-assistant/src/debug.rs
+++ b/rust/bt-broadcast-assistant/src/debug.rs
@@ -263,7 +263,13 @@
                         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, &self.peer_addr_getter, pa_sync, bis_sync).await
+                        peer.add_broadcast_source(
+                            broadcast_source_pid,
+                            &self.peer_addr_getter,
+                            pa_sync,
+                            bis_sync,
+                        )
+                        .await
                     })
                     .await;
                 }
diff --git a/rust/bt-common/src/core.rs b/rust/bt-common/src/core.rs
index 9ee3fa0..d2e2ce5 100644
--- a/rust/bt-common/src/core.rs
+++ b/rust/bt-common/src/core.rs
@@ -177,6 +177,16 @@
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Phy {
+    /// LE 1M PHY
+    Le1m,
+    /// LE 2M PHY
+    Le2m,
+    /// LE Coded PHY
+    LeCoded,
+}
+
 #[cfg(test)]
 mod tests {
     use crate::packet_encoding::Decodable;
diff --git a/rust/bt-gatt/src/central.rs b/rust/bt-gatt/src/central.rs
index 5ca7712..c896717 100644
--- a/rust/bt-gatt/src/central.rs
+++ b/rust/bt-gatt/src/central.rs
@@ -75,6 +75,7 @@
     pub connectable: bool,
     pub name: PeerName,
     pub advertised: Vec<AdvertisingDatum>,
+    pub advertising_sid: u8,
 }
 
 pub trait Central<T: crate::GattTypes> {
@@ -84,4 +85,7 @@
 
     /// Connect to a specific peer.
     fn connect(&self, peer_id: PeerId) -> T::ConnectFuture;
+
+    /// Get API for periodic advertising.
+    fn periodic_advertising(&self) -> crate::Result<T::PeriodicAdvertising>;
 }
diff --git a/rust/bt-gatt/src/lib.rs b/rust/bt-gatt/src/lib.rs
index 74c7f3b..3ca4b1a 100644
--- a/rust/bt-gatt/src/lib.rs
+++ b/rust/bt-gatt/src/lib.rs
@@ -14,6 +14,7 @@
 pub mod central;
 pub use central::Central;
 
+pub mod periodic_advertising;
 pub mod pii;
 
 #[cfg(any(test, feature = "test-utils"))]
@@ -24,6 +25,8 @@
 
 use futures::{Future, Stream};
 
+use crate::periodic_advertising::PeriodicAdvertising;
+
 /// Implementors implement traits with respect to GattTypes.
 /// Implementation crates provide an object which relates a constellation of
 /// types to each other, used to provide concrete types for library crates to
@@ -56,6 +59,8 @@
     /// Future resolving when a characteristic or descriptor has been written.
     /// Returns an error if the value could not be written.
     type WriteFut<'a>: Future<Output = Result<()>> + 'a;
+    /// The implementation of periodic advertising for this GATT implementation.
+    type PeriodicAdvertising: PeriodicAdvertising;
 }
 
 /// Servers and services are defined with respect to ServerTypes.
diff --git a/rust/bt-gatt/src/periodic_advertising.rs b/rust/bt-gatt/src/periodic_advertising.rs
new file mode 100644
index 0000000..c0c227f
--- /dev/null
+++ b/rust/bt-gatt/src/periodic_advertising.rs
@@ -0,0 +1,92 @@
+// Copyright 2025 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Contains traits that are used to synchronize to Periodic Advertisements,
+//! i.e. the GAP Central role role defined in the Bluetooth Core
+//! Specification (5.4, Volume 3 Part C Section 2.2.2)
+//!
+//! These traits should be implemented outside this crate, conforming to the
+//! types and structs here when necessary.
+
+use bt_common::core::Phy;
+use futures::{Future, Stream};
+use thiserror::Error;
+
+use bt_common::PeerId;
+
+#[derive(Error, Debug)]
+#[non_exhaustive]
+pub enum Error {
+    #[error("Periodic Advertising Sync failed to establish")]
+    SyncEstablishFailed,
+    #[error("Periodic Advertising Sync lost")]
+    SyncLost,
+    #[error("I/O error")]
+    Io,
+}
+
+/// A trait for managing a periodic advertising.
+pub trait PeriodicAdvertising {
+    type SyncFut: Future<Output = crate::Result<Self::SyncStream>>;
+    type SyncStream: Stream<Item = crate::Result<SyncReport>>;
+
+    /// Request to sync to periodic advertising resports.
+    /// On success, returns the SyncStream which can be used to receive
+    /// SyncReports.
+    fn sync_to_advertising_reports(
+        peer_id: PeerId,
+        advertising_sid: u8,
+        config: SyncConfiguration,
+    ) -> Self::SyncFut;
+
+    // TODO(b/340885203): Add a method to sync to subevents.
+}
+
+#[derive(Debug, Clone)]
+pub struct SyncConfiguration {
+    /// Filter out duplicate advertising reports.
+    /// Optional.
+    /// Default: true
+    pub filter_duplicates: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct PeriodicAdvertisingReport {
+    pub rssi: i8,
+    pub data: Vec<u8>,
+    /// The event counter of the event that the advertising packet was received
+    /// in.
+    pub event_counter: Option<u16>,
+    /// The subevent number of the report. Only present if the packet was
+    /// received in a subevent.
+    pub subevent: Option<u8>,
+    pub timestamp: i64,
+}
+
+#[derive(Debug, Clone)]
+pub struct BroadcastIsochronousGroupInfo {
+    /// The number of Broadcast Isochronous Streams in this group.
+    /// The specification calls this "num_bis".
+    pub streams_count: u8,
+    /// The time interval of the periodic SDUs.
+    pub sdu_interval: i64,
+    /// The maximum size of an SDU.
+    pub max_sdu_size: u16,
+    /// The PHY used for transmission of data.
+    pub phy: Phy,
+    /// Indicates whether the BIG is encrypted.
+    pub encryption: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct BroadcastIsochronousGroupInfoReport {
+    pub info: BroadcastIsochronousGroupInfo,
+    pub timestamp: i64,
+}
+
+#[derive(Debug, Clone)]
+pub enum SyncReport {
+    PeriodicAdvertisingReport(PeriodicAdvertisingReport),
+    BroadcastIsochronousGroupInfoReport(BroadcastIsochronousGroupInfoReport),
+}
diff --git a/rust/bt-gatt/src/pii.rs b/rust/bt-gatt/src/pii.rs
index f165db6..df17047 100644
--- a/rust/bt-gatt/src/pii.rs
+++ b/rust/bt-gatt/src/pii.rs
@@ -5,8 +5,8 @@
 use std::future::Future;
 
 use bt_common::{
-    PeerId,
     core::{Address, AddressType},
+    PeerId,
 };
 
 use crate::types::*;
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs
index 15ad2fa..c72b395 100644
--- a/rust/bt-gatt/src/test_utils.rs
+++ b/rust/bt-gatt/src/test_utils.rs
@@ -15,9 +15,10 @@
 
 use crate::central::ScanResult;
 use crate::client::CharacteristicNotification;
+use crate::periodic_advertising::{PeriodicAdvertising, SyncReport};
 use crate::pii::GetPeerAddr;
 use crate::server::{self, LocalService, ReadResponder, ServiceDefinition, WriteResponder};
-use crate::{GattTypes, ServerTypes, types::*};
+use crate::{types::*, GattTypes, ServerTypes};
 
 #[derive(Default)]
 struct FakePeerServiceInner {
@@ -293,6 +294,7 @@
     type NotificationStream = UnboundedReceiver<Result<CharacteristicNotification>>;
     type ReadFut<'a> = Ready<Result<(usize, bool)>>;
     type WriteFut<'a> = Ready<Result<()>>;
+    type PeriodicAdvertising = FakePeriodicAdvertising;
 }
 
 impl ServerTypes for FakeTypes {
@@ -306,6 +308,21 @@
     type IndicateConfirmationStream = UnboundedReceiver<Result<server::ConfirmationEvent>>;
 }
 
+pub struct FakePeriodicAdvertising;
+
+impl PeriodicAdvertising for FakePeriodicAdvertising {
+    type SyncFut = Ready<Result<Self::SyncStream>>;
+    type SyncStream = futures::stream::Empty<Result<SyncReport>>;
+
+    fn sync_to_advertising_reports(
+        _peer_id: PeerId,
+        _advertising_sid: u8,
+        _config: crate::periodic_advertising::SyncConfiguration,
+    ) -> Self::SyncFut {
+        unimplemented!()
+    }
+}
+
 #[derive(Default)]
 pub struct FakeCentralInner {
     clients: HashMap<PeerId, FakeClient>,
@@ -339,6 +356,10 @@
         };
         futures::future::ready(res)
     }
+
+    fn periodic_advertising(&self) -> Result<<FakeTypes as GattTypes>::PeriodicAdvertising> {
+        unimplemented!()
+    }
 }
 
 pub enum FakeServerEvent {
diff --git a/rust/bt-gatt/src/tests.rs b/rust/bt-gatt/src/tests.rs
index 896f2cc..cc51dd5 100644
--- a/rust/bt-gatt/src/tests.rs
+++ b/rust/bt-gatt/src/tests.rs
@@ -243,6 +243,7 @@
         connectable: true,
         name: PeerName::CompleteName("Marie's Pixel 7 Pro".to_owned()),
         advertised: vec![AdvertisingDatum::Services(vec![Uuid::from_u16(0x1844)])],
+        advertising_sid: 0,
     };
     let _ = scan_results.set_scanned_result(Ok(scanned_result));
 
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index 2171070..ad182b4 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -153,6 +153,8 @@
     AlreadyPublished(crate::server::ServiceId),
     #[error("Encoding/decoding error: {0}")]
     Encoding(#[from] bt_common::packet_encoding::Error),
+    #[error("Periodic Advertising error: {0}")]
+    PeriodicAdvertising(crate::periodic_advertising::Error),
 }
 
 impl Error {