1use std::time::Duration;
7
8use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
9use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
10use tokio::time::sleep;
11use tracing::{debug, info, warn};
12
13use crate::error::{Error, Result};
14use crate::util::{create_identifier, format_peripheral_id};
15use crate::uuid::{MANUFACTURER_ID, SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD};
16use aranet_types::DeviceType;
17
18#[derive(Debug, Clone)]
20pub enum FindProgress {
21 CacheHit,
23 ScanAttempt {
25 attempt: u32,
27 total: u32,
29 duration_secs: u64,
31 },
32 Found { attempt: u32 },
34 RetryNeeded { attempt: u32 },
36}
37
38pub type ProgressCallback = Box<dyn Fn(FindProgress) + Send + Sync>;
40
41#[derive(Debug, Clone)]
43pub struct DiscoveredDevice {
44 pub name: Option<String>,
46 pub id: PeripheralId,
48 pub address: String,
50 pub identifier: String,
52 pub rssi: Option<i16>,
54 pub device_type: Option<DeviceType>,
56 pub is_aranet: bool,
58 pub manufacturer_data: Option<Vec<u8>>,
60}
61
62#[derive(Debug, Clone)]
64pub struct ScanOptions {
65 pub duration: Duration,
67 pub filter_aranet_only: bool,
69}
70
71impl Default for ScanOptions {
72 fn default() -> Self {
73 Self {
74 duration: Duration::from_secs(5),
75 filter_aranet_only: true,
76 }
77 }
78}
79
80impl ScanOptions {
81 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn duration(mut self, duration: Duration) -> Self {
88 self.duration = duration;
89 self
90 }
91
92 pub fn duration_secs(mut self, secs: u64) -> Self {
94 self.duration = Duration::from_secs(secs);
95 self
96 }
97
98 pub fn filter_aranet_only(mut self, filter: bool) -> Self {
100 self.filter_aranet_only = filter;
101 self
102 }
103
104 pub fn all_devices(self) -> Self {
106 self.filter_aranet_only(false)
107 }
108}
109
110pub async fn get_adapter() -> Result<Adapter> {
112 use crate::error::DeviceNotFoundReason;
113
114 let manager = Manager::new().await?;
115 let adapters = manager.adapters().await?;
116
117 adapters
118 .into_iter()
119 .next()
120 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
121}
122
123pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
135 scan_with_options(ScanOptions::default()).await
136}
137
138pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
140 let adapter = get_adapter().await?;
141 scan_with_adapter(&adapter, options).await
142}
143
144pub async fn scan_with_retry(
167 options: ScanOptions,
168 max_retries: u32,
169 retry_on_empty: bool,
170) -> Result<Vec<DiscoveredDevice>> {
171 let mut attempt = 0;
172 let mut delay = Duration::from_millis(500);
173
174 loop {
175 match scan_with_options(options.clone()).await {
176 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
177 attempt += 1;
178 warn!(
179 "No devices found, retrying ({}/{})...",
180 attempt, max_retries
181 );
182 sleep(delay).await;
183 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
184 }
185 Ok(devices) => return Ok(devices),
186 Err(e) if attempt < max_retries => {
187 attempt += 1;
188 warn!(
189 "Scan failed ({}), retrying ({}/{})...",
190 e, attempt, max_retries
191 );
192 sleep(delay).await;
193 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
194 }
195 Err(e) => return Err(e),
196 }
197 }
198}
199
200pub async fn scan_with_adapter(
202 adapter: &Adapter,
203 options: ScanOptions,
204) -> Result<Vec<DiscoveredDevice>> {
205 info!(
206 "Starting BLE scan for {} seconds...",
207 options.duration.as_secs()
208 );
209
210 adapter.start_scan(ScanFilter::default()).await?;
212
213 sleep(options.duration).await;
215
216 adapter.stop_scan().await?;
218
219 let peripherals = adapter.peripherals().await?;
221 let mut discovered = Vec::new();
222
223 for peripheral in peripherals {
224 match process_peripheral(&peripheral, options.filter_aranet_only).await {
225 Ok(Some(device)) => {
226 info!("Found Aranet device: {:?}", device.name);
227 discovered.push(device);
228 }
229 Ok(None) => {
230 }
232 Err(e) => {
233 debug!("Error processing peripheral: {}", e);
234 }
235 }
236 }
237
238 info!("Scan complete. Found {} device(s)", discovered.len());
239 Ok(discovered)
240}
241
242async fn process_peripheral(
244 peripheral: &Peripheral,
245 filter_aranet_only: bool,
246) -> Result<Option<DiscoveredDevice>> {
247 let properties = peripheral.properties().await?;
248 let properties = match properties {
249 Some(p) => p,
250 None => return Ok(None),
251 };
252
253 let id = peripheral.id();
254 let address = properties.address.to_string();
255 let name = properties.local_name.clone();
256 let rssi = properties.rssi;
257
258 let is_aranet = is_aranet_device(&properties);
260
261 if filter_aranet_only && !is_aranet {
262 return Ok(None);
263 }
264
265 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
267
268 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
270
271 let identifier = create_identifier(&address, &id);
274
275 Ok(Some(DiscoveredDevice {
276 name,
277 id,
278 address,
279 identifier,
280 rssi,
281 device_type,
282 is_aranet,
283 manufacturer_data,
284 }))
285}
286
287fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
289 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
291 return true;
292 }
293
294 for service_uuid in properties.service_data.keys() {
296 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
297 return true;
298 }
299 }
300
301 for service_uuid in &properties.services {
303 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
304 return true;
305 }
306 }
307
308 if let Some(name) = &properties.local_name {
310 let name_lower = name.to_lowercase();
311 if name_lower.contains("aranet") {
312 return true;
313 }
314 }
315
316 false
317}
318
319pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
321 find_device_with_options(identifier, ScanOptions::default()).await
322}
323
324pub async fn find_device_with_options(
333 identifier: &str,
334 options: ScanOptions,
335) -> Result<(Adapter, Peripheral)> {
336 find_device_with_progress(identifier, options, None).await
337}
338
339pub async fn find_device_with_progress(
344 identifier: &str,
345 options: ScanOptions,
346 progress: Option<ProgressCallback>,
347) -> Result<(Adapter, Peripheral)> {
348 let adapter = get_adapter().await?;
349 let identifier_lower = identifier.to_lowercase();
350
351 info!("Looking for device: {}", identifier);
352
353 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await? {
355 info!("Found device in cache (no scan needed)");
356 if let Some(ref cb) = progress {
357 cb(FindProgress::CacheHit);
358 }
359 return Ok((adapter, peripheral));
360 }
361
362 let max_attempts: u32 = 3;
365 let base_duration = options.duration.as_millis() as u64 / 2;
366 let base_duration = Duration::from_millis(base_duration.max(2000)); for attempt in 1..=max_attempts {
369 let scan_duration = base_duration * attempt;
370 let duration_secs = scan_duration.as_secs();
371
372 info!(
373 "Scan attempt {}/{} ({}s)...",
374 attempt, max_attempts, duration_secs
375 );
376
377 if let Some(ref cb) = progress {
378 cb(FindProgress::ScanAttempt {
379 attempt,
380 total: max_attempts,
381 duration_secs,
382 });
383 }
384
385 adapter.start_scan(ScanFilter::default()).await?;
387 sleep(scan_duration).await;
388 adapter.stop_scan().await?;
389
390 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await?
392 {
393 info!("Found device on attempt {}", attempt);
394 if let Some(ref cb) = progress {
395 cb(FindProgress::Found { attempt });
396 }
397 return Ok((adapter, peripheral));
398 }
399
400 if attempt < max_attempts {
401 warn!("Device not found, retrying...");
402 if let Some(ref cb) = progress {
403 cb(FindProgress::RetryNeeded { attempt });
404 }
405 }
406 }
407
408 warn!(
409 "Device not found after {} attempts: {}",
410 max_attempts, identifier
411 );
412 Err(Error::device_not_found(identifier))
413}
414
415async fn find_peripheral_by_identifier(
417 adapter: &Adapter,
418 identifier_lower: &str,
419) -> Result<Option<Peripheral>> {
420 let peripherals = adapter.peripherals().await?;
421
422 for peripheral in peripherals {
423 if let Ok(Some(props)) = peripheral.properties().await {
424 let address = props.address.to_string().to_lowercase();
425 let peripheral_id = format_peripheral_id(&peripheral.id()).to_lowercase();
426
427 if peripheral_id.contains(identifier_lower) {
429 debug!("Matched by peripheral ID: {}", peripheral_id);
430 return Ok(Some(peripheral));
431 }
432
433 if address != "00:00:00:00:00:00"
435 && (address == identifier_lower
436 || address.replace(':', "") == identifier_lower.replace(':', ""))
437 {
438 debug!("Matched by address: {}", address);
439 return Ok(Some(peripheral));
440 }
441
442 if let Some(name) = &props.local_name
444 && name.to_lowercase().contains(identifier_lower)
445 {
446 debug!("Matched by name: {}", name);
447 return Ok(Some(peripheral));
448 }
449 }
450 }
451
452 Ok(None)
453}