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}