Skip to main content

hap_ble/
sleepy.rs

1//! The cold-arm connect seam: obtain a ready, post-Pair-Verify accessory for a
2//! stored BLE pairing, keyed by HAP device id. Injectable so the cold-arm path
3//! is testable with a mock, above the live crypto handshake.
4
5use crate::accessory::BleAccessory;
6use crate::error::Result;
7use hap_crypto::{AccessoryPairing, ControllerKeypair};
8use std::sync::Arc;
9use std::time::Duration;
10
11/// Format a 6-byte HAP device id as lowercase colon-separated hex, matching
12/// [`crate::discovery::DiscoveredBleAccessory::device_id`]'s format. `hap-ble`
13/// has no dependency on `hap-pairing` (dependencies flow strictly downward and
14/// no new ones are added here), so this is a small local equivalent of
15/// `hap_pairing::format_device_id`.
16fn format_device_id(id: [u8; 6]) -> String {
17    use std::fmt::Write as _;
18    id.iter().fold(String::new(), |mut s, b| {
19        if !s.is_empty() {
20            s.push(':');
21        }
22        let _ = write!(s, "{b:02x}");
23        s
24    })
25}
26
27/// Establishes a connected, verified [`BleAccessory`] for a stored pairing.
28#[async_trait::async_trait]
29pub trait SleepyConnector: Send + Sync {
30    /// Scan for `device_id`, connect, run Pair Verify with `pairing`, and return
31    /// a ready accessory with its advert source set. Blocks until the device
32    /// advertises.
33    ///
34    /// # Errors
35    /// [`crate::error::BleError`] on scan/connect/verify failure.
36    async fn connect(
37        &self,
38        device_id: [u8; 6],
39        pairing: &AccessoryPairing,
40        broadcast: Option<crate::broadcast_state::BleBroadcastState>,
41    ) -> Result<BleAccessory>;
42}
43
44/// The real bluest-backed connector: retrying scan-by-device-id, then
45/// `connect_gatt` -> `BleController::connect` -> `set_advert_source`.
46///
47/// Retries until the device advertises: a transient scan error (e.g. an
48/// adapter hiccup) does not abort the connect, it backs off and rescans.
49pub struct BluestSleepyConnector {
50    keypair: ControllerKeypair,
51    /// How long each scan-for-the-device attempt runs before retrying.
52    scan_window: Duration,
53}
54
55impl BluestSleepyConnector {
56    /// Create a connector using this controller's long-term identity.
57    #[must_use]
58    pub fn new(keypair: ControllerKeypair) -> Self {
59        Self {
60            keypair,
61            scan_window: Duration::from_secs(15),
62        }
63    }
64}
65
66#[async_trait::async_trait]
67impl SleepyConnector for BluestSleepyConnector {
68    async fn connect(
69        &self,
70        device_id: [u8; 6],
71        pairing: &AccessoryPairing,
72        broadcast: Option<crate::broadcast_state::BleBroadcastState>,
73    ) -> Result<BleAccessory> {
74        let wanted = format_device_id(device_id);
75        // Retry scan-by-device-id until the sleepy device advertises. One scan
76        // at a time (each attempt drops its stream before the next); the scan is
77        // the "wait for first advert".
78        loop {
79            // A transient adapter hiccup must not kill the watch — back off
80            // briefly and rescan rather than propagating the error.
81            let Ok(scanned) = crate::scan(self.scan_window).await else {
82                tokio::time::sleep(Duration::from_secs(2)).await;
83                continue;
84            };
85            if let Some(found) = scanned
86                .into_iter()
87                .find(|d| d.device_id.eq_ignore_ascii_case(&wanted))
88            {
89                let conn = crate::connect_gatt(&found).await?;
90                let advert: Arc<dyn crate::gatt::AdvertSource> = conn.clone();
91                let ble = crate::BleController::new(self.keypair.clone());
92                let mut accessory = ble
93                    .connect(
94                        conn as Arc<dyn crate::gatt::GattConnection>,
95                        pairing,
96                        broadcast,
97                    )
98                    .await?;
99                accessory.set_advert_source(advert);
100                return Ok(accessory);
101            }
102            // not seen this window — loop and scan again
103        }
104    }
105}