gestura_core_ble/
scanner.rs

1use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
2use btleplug::platform::{Adapter, Manager, Peripheral};
3use std::time::Duration;
4use uuid::Uuid;
5
6/// Initializes the BLE manager, begins a scan targeting the provided `service_uuid`,
7/// and reliably polls until the peripheral is matched or bounds exhaust.
8pub async fn find_device_by_service_uuid(
9    service_uuid: Uuid,
10    polling_attempts: u8,
11    polling_interval: Duration,
12) -> Result<(Adapter, Peripheral), String> {
13    let manager = Manager::new()
14        .await
15        .map_err(|e| format!("Failed to initialize BLE Manager: {}", e))?;
16    let adapters = manager
17        .adapters()
18        .await
19        .map_err(|e| format!("Failed to get BLE adapters: {}", e))?;
20    let adapter = adapters
21        .into_iter()
22        .next()
23        .ok_or("No Bluetooth adapters found")?;
24
25    adapter
26        .start_scan(ScanFilter {
27            services: vec![service_uuid],
28        })
29        .await
30        .map_err(|e| format!("Failed to start scan: {}", e))?;
31
32    tracing::info!("Scanning for device bounding service UUID {}", service_uuid);
33
34    // Track the last adapter-level error and whether any poll ever succeeded.
35    // Used at the end to distinguish "scan infrastructure failed" (adapter
36    // error) from "scan worked but no matching peripheral was found", so
37    // callers can react to each case appropriately instead of seeing a
38    // misleading "device not found" when the real problem is the adapter.
39    let mut last_scan_error: Option<String> = None;
40    let mut successful_polls: u32 = 0;
41
42    for _ in 0..polling_attempts {
43        tokio::time::sleep(polling_interval).await;
44
45        let peripherals = match adapter.peripherals().await {
46            Ok(ps) => {
47                successful_polls += 1;
48                ps
49            }
50            Err(e) => {
51                tracing::warn!(
52                    "Failed to list peripherals during BLE scan (will retry): {}",
53                    e
54                );
55                last_scan_error = Some(e.to_string());
56                continue;
57            }
58        };
59
60        for p in peripherals {
61            let properties = match p.properties().await {
62                Ok(Some(props)) => props,
63                Ok(None) => continue,
64                Err(e) => {
65                    tracing::warn!(
66                        "Failed to read peripheral properties (skipping peripheral): {}",
67                        e
68                    );
69                    continue;
70                }
71            };
72            if properties.services.contains(&service_uuid) {
73                let _ = adapter.stop_scan().await;
74                return Ok((adapter, p));
75            }
76        }
77    }
78
79    let _ = adapter.stop_scan().await;
80
81    // If not a single peripheral listing succeeded the adapter itself is
82    // broken; propagate the last error so callers see "scan failed" rather
83    // than a misleading "device not found".
84    if successful_polls == 0
85        && let Some(err) = last_scan_error
86    {
87        return Err(format!(
88            "BLE scan failed for service {} – adapter could not list peripherals: {}",
89            service_uuid, err
90        ));
91    }
92
93    Err(format!(
94        "Device mapped to BLE service {} not found across bounds",
95        service_uuid
96    ))
97}