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 pub use_service_filter: bool,
72}
73
74impl Default for ScanOptions {
75 fn default() -> Self {
76 Self {
77 duration: Duration::from_secs(5),
78 filter_aranet_only: true,
79 use_service_filter: false,
82 }
83 }
84}
85
86impl ScanOptions {
87 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn duration(mut self, duration: Duration) -> Self {
94 self.duration = duration;
95 self
96 }
97
98 pub fn duration_secs(mut self, secs: u64) -> Self {
100 self.duration = Duration::from_secs(secs);
101 self
102 }
103
104 pub fn filter_aranet_only(mut self, filter: bool) -> Self {
106 self.filter_aranet_only = filter;
107 self
108 }
109
110 pub fn all_devices(self) -> Self {
112 self.filter_aranet_only(false)
113 }
114
115 pub fn use_service_filter(mut self, enable: bool) -> Self {
123 self.use_service_filter = enable;
124 self
125 }
126
127 pub fn optimized() -> Self {
131 Self {
132 duration: Duration::from_secs(3),
133 filter_aranet_only: true,
134 use_service_filter: true,
135 }
136 }
137}
138
139pub async fn get_adapter() -> Result<Adapter> {
141 use crate::error::DeviceNotFoundReason;
142
143 #[cfg(target_os = "linux")]
147 crate::bluez_agent::ensure_agent();
148
149 let manager = Manager::new().await?;
150 let adapters = manager.adapters().await?;
151
152 adapters
153 .into_iter()
154 .next()
155 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
156}
157
158pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
170 scan_with_options(ScanOptions::default()).await
171}
172
173pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
175 let adapter = get_adapter().await?;
176 scan_with_adapter(&adapter, options).await
177}
178
179pub async fn scan_with_retry(
202 options: ScanOptions,
203 max_retries: u32,
204 retry_on_empty: bool,
205) -> Result<Vec<DiscoveredDevice>> {
206 let mut attempt = 0;
207 let mut delay = Duration::from_millis(500);
208
209 loop {
210 match scan_with_options(options.clone()).await {
211 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
212 attempt += 1;
213 warn!(
214 "No devices found, retrying ({}/{})...",
215 attempt, max_retries
216 );
217 sleep(delay).await;
218 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
219 }
220 Ok(devices) => return Ok(devices),
221 Err(e) if attempt < max_retries => {
222 attempt += 1;
223 warn!(
224 "Scan failed ({}), retrying ({}/{})...",
225 e, attempt, max_retries
226 );
227 sleep(delay).await;
228 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
229 }
230 Err(e) => return Err(e),
231 }
232 }
233}
234
235pub async fn scan_with_adapter(
237 adapter: &Adapter,
238 options: ScanOptions,
239) -> Result<Vec<DiscoveredDevice>> {
240 info!(
241 "Starting BLE scan for {} seconds (service_filter={})...",
242 options.duration.as_secs(),
243 options.use_service_filter
244 );
245
246 let scan_filter = if options.use_service_filter {
248 ScanFilter {
249 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
250 }
251 } else {
252 ScanFilter::default()
253 };
254
255 adapter.start_scan(scan_filter).await?;
257
258 sleep(options.duration).await;
260
261 adapter.stop_scan().await?;
263
264 let peripherals = adapter.peripherals().await?;
266 let mut discovered = Vec::new();
267
268 for peripheral in peripherals {
269 match process_peripheral(&peripheral, options.filter_aranet_only).await {
270 Ok(Some(device)) => {
271 info!("Found Aranet device: {:?}", device.name);
272 discovered.push(device);
273 }
274 Ok(None) => {
275 }
277 Err(e) => {
278 debug!("Error processing peripheral: {}", e);
279 }
280 }
281 }
282
283 info!("Scan complete. Found {} device(s)", discovered.len());
284 Ok(discovered)
285}
286
287async fn process_peripheral(
289 peripheral: &Peripheral,
290 filter_aranet_only: bool,
291) -> Result<Option<DiscoveredDevice>> {
292 let properties = peripheral.properties().await?;
293 let properties = match properties {
294 Some(p) => p,
295 None => return Ok(None),
296 };
297
298 let id = peripheral.id();
299 let address = properties.address.to_string();
300 let name = properties.local_name.clone();
301 let rssi = properties.rssi;
302
303 let is_aranet = is_aranet_device(&properties);
305
306 if filter_aranet_only && !is_aranet {
307 return Ok(None);
308 }
309
310 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
312
313 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
315
316 let identifier = create_identifier(&address, &id);
319
320 Ok(Some(DiscoveredDevice {
321 name,
322 id,
323 address,
324 identifier,
325 rssi,
326 device_type,
327 is_aranet,
328 manufacturer_data,
329 }))
330}
331
332fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
334 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
336 return true;
337 }
338
339 for service_uuid in properties.service_data.keys() {
341 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
342 return true;
343 }
344 }
345
346 for service_uuid in &properties.services {
348 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
349 return true;
350 }
351 }
352
353 if let Some(name) = &properties.local_name {
355 let name_lower = name.to_lowercase();
356 if name_lower.contains("aranet") {
357 return true;
358 }
359 }
360
361 false
362}
363
364pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
366 find_device_with_options(identifier, ScanOptions::default()).await
367}
368
369pub async fn find_device_with_options(
378 identifier: &str,
379 options: ScanOptions,
380) -> Result<(Adapter, Peripheral)> {
381 find_device_with_progress(identifier, options, None).await
382}
383
384pub async fn find_device_with_progress(
389 identifier: &str,
390 options: ScanOptions,
391 progress: Option<ProgressCallback>,
392) -> Result<(Adapter, Peripheral)> {
393 let adapter = get_adapter().await?;
394 let identifier_lower = identifier.to_lowercase();
395
396 info!("Looking for device: {}", identifier);
397
398 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await? {
400 info!("Found device in cache (no scan needed)");
401 if let Some(ref cb) = progress {
402 cb(FindProgress::CacheHit);
403 }
404 return Ok((adapter, peripheral));
405 }
406
407 let max_attempts: u32 = 3;
410 let base_duration = options.duration.as_millis() as u64 / 2;
411 let base_duration = Duration::from_millis(base_duration.max(2000)); for attempt in 1..=max_attempts {
414 let scan_duration = base_duration * attempt;
415 let duration_secs = scan_duration.as_secs();
416
417 info!(
418 "Scan attempt {}/{} ({}s)...",
419 attempt, max_attempts, duration_secs
420 );
421
422 if let Some(ref cb) = progress {
423 cb(FindProgress::ScanAttempt {
424 attempt,
425 total: max_attempts,
426 duration_secs,
427 });
428 }
429
430 adapter.start_scan(ScanFilter::default()).await?;
432 sleep(scan_duration).await;
433 adapter.stop_scan().await?;
434
435 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await?
437 {
438 info!("Found device on attempt {}", attempt);
439 if let Some(ref cb) = progress {
440 cb(FindProgress::Found { attempt });
441 }
442 return Ok((adapter, peripheral));
443 }
444
445 if attempt < max_attempts {
446 warn!("Device not found, retrying...");
447 if let Some(ref cb) = progress {
448 cb(FindProgress::RetryNeeded { attempt });
449 }
450 }
451 }
452
453 warn!(
454 "Device not found after {} attempts: {}",
455 max_attempts, identifier
456 );
457 Err(Error::device_not_found(identifier))
458}
459
460async fn find_peripheral_by_identifier(
462 adapter: &Adapter,
463 identifier_lower: &str,
464) -> Result<Option<Peripheral>> {
465 let peripherals = adapter.peripherals().await?;
466
467 for peripheral in peripherals {
468 if let Ok(Some(props)) = peripheral.properties().await {
469 let address = props.address.to_string().to_lowercase();
470 let peripheral_id = format_peripheral_id(&peripheral.id()).to_lowercase();
471
472 if peripheral_id.contains(identifier_lower) {
474 debug!("Matched by peripheral ID: {}", peripheral_id);
475 return Ok(Some(peripheral));
476 }
477
478 if address != "00:00:00:00:00:00"
480 && (address == identifier_lower
481 || address.replace(':', "") == identifier_lower.replace(':', ""))
482 {
483 debug!("Matched by address: {}", address);
484 return Ok(Some(peripheral));
485 }
486
487 if let Some(name) = &props.local_name
489 && name.to_lowercase().contains(identifier_lower)
490 {
491 debug!("Matched by name: {}", name);
492 return Ok(Some(peripheral));
493 }
494 }
495 }
496
497 Ok(None)
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
507 fn test_scan_options_default() {
508 let options = ScanOptions::default();
509 assert_eq!(options.duration, Duration::from_secs(5));
510 assert!(options.filter_aranet_only);
511 }
512
513 #[test]
514 fn test_scan_options_new() {
515 let options = ScanOptions::new();
516 assert_eq!(options.duration, Duration::from_secs(5));
517 assert!(options.filter_aranet_only);
518 }
519
520 #[test]
521 fn test_scan_options_duration() {
522 let options = ScanOptions::new().duration(Duration::from_secs(10));
523 assert_eq!(options.duration, Duration::from_secs(10));
524 }
525
526 #[test]
527 fn test_scan_options_duration_secs() {
528 let options = ScanOptions::new().duration_secs(15);
529 assert_eq!(options.duration, Duration::from_secs(15));
530 }
531
532 #[test]
533 fn test_scan_options_filter_aranet_only() {
534 let options = ScanOptions::new().filter_aranet_only(false);
535 assert!(!options.filter_aranet_only);
536
537 let options = ScanOptions::new().filter_aranet_only(true);
538 assert!(options.filter_aranet_only);
539 }
540
541 #[test]
542 fn test_scan_options_all_devices() {
543 let options = ScanOptions::new().all_devices();
544 assert!(!options.filter_aranet_only);
545 }
546
547 #[test]
548 fn test_scan_options_chaining() {
549 let options = ScanOptions::new()
550 .duration_secs(20)
551 .filter_aranet_only(false);
552
553 assert_eq!(options.duration, Duration::from_secs(20));
554 assert!(!options.filter_aranet_only);
555 }
556
557 #[test]
558 fn test_scan_options_clone() {
559 let options1 = ScanOptions::new().duration_secs(8);
560 let options2 = options1.clone();
561
562 assert_eq!(options1.duration, options2.duration);
563 assert_eq!(options1.filter_aranet_only, options2.filter_aranet_only);
564 }
565
566 #[test]
567 fn test_scan_options_debug() {
568 let options = ScanOptions::new();
569 let debug = format!("{:?}", options);
570 assert!(debug.contains("ScanOptions"));
571 assert!(debug.contains("duration"));
572 assert!(debug.contains("filter_aranet_only"));
573 }
574
575 #[test]
578 fn test_find_progress_cache_hit() {
579 let progress = FindProgress::CacheHit;
580 let debug = format!("{:?}", progress);
581 assert!(debug.contains("CacheHit"));
582 }
583
584 #[test]
585 fn test_find_progress_scan_attempt() {
586 let progress = FindProgress::ScanAttempt {
587 attempt: 2,
588 total: 3,
589 duration_secs: 5,
590 };
591
592 if let FindProgress::ScanAttempt {
593 attempt,
594 total,
595 duration_secs,
596 } = progress
597 {
598 assert_eq!(attempt, 2);
599 assert_eq!(total, 3);
600 assert_eq!(duration_secs, 5);
601 } else {
602 panic!("Expected ScanAttempt variant");
603 }
604 }
605
606 #[test]
607 fn test_find_progress_found() {
608 let progress = FindProgress::Found { attempt: 1 };
609 assert!(matches!(progress, FindProgress::Found { attempt: 1 }));
610 }
611
612 #[test]
613 fn test_find_progress_retry_needed() {
614 let progress = FindProgress::RetryNeeded { attempt: 2 };
615 assert!(matches!(progress, FindProgress::RetryNeeded { attempt: 2 }));
616 }
617
618 #[test]
619 fn test_find_progress_clone() {
620 let progress1 = FindProgress::ScanAttempt {
621 attempt: 1,
622 total: 3,
623 duration_secs: 4,
624 };
625 let progress2 = progress1.clone();
626
627 assert!(matches!(
628 (&progress1, &progress2),
629 (
630 FindProgress::ScanAttempt {
631 attempt: 1,
632 total: 3,
633 duration_secs: 4,
634 },
635 FindProgress::ScanAttempt {
636 attempt: 1,
637 total: 3,
638 duration_secs: 4,
639 },
640 )
641 ));
642 }
643
644 }