1use std::time::Duration;
7
8use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
9use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
10use tokio::sync::RwLock;
11use tokio::time::sleep;
12use tracing::{debug, info, warn};
13
14static MANAGER: RwLock<Option<Manager>> = RwLock::const_new(None);
20
21async fn shared_manager() -> Result<Manager> {
23 {
25 let guard = MANAGER.read().await;
26 if let Some(m) = guard.as_ref() {
27 return Ok(m.clone());
28 }
29 }
30 let mut guard = MANAGER.write().await;
32 if let Some(m) = guard.as_ref() {
34 return Ok(m.clone());
35 }
36 let m = Manager::new().await?;
37 *guard = Some(m.clone());
38 Ok(m)
39}
40
41async fn reset_manager() {
46 let mut guard = MANAGER.write().await;
47 if guard.take().is_some() {
48 warn!("BLE manager reset — next operation will create a new D-Bus connection");
49 }
50}
51
52use crate::error::{Error, Result};
53use crate::util::{create_identifier, format_peripheral_id};
54use crate::uuid::{MANUFACTURER_ID, SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD};
55use aranet_types::DeviceType;
56
57#[derive(Debug, Clone)]
59pub enum FindProgress {
60 CacheHit,
62 ScanAttempt {
64 attempt: u32,
66 total: u32,
68 duration_secs: u64,
70 },
71 Found { attempt: u32 },
73 RetryNeeded { attempt: u32 },
75}
76
77pub type ProgressCallback = Box<dyn Fn(FindProgress) + Send + Sync>;
79
80#[derive(Debug, Clone)]
82pub struct DiscoveredDevice {
83 pub name: Option<String>,
85 pub id: PeripheralId,
87 pub address: String,
89 pub identifier: String,
91 pub rssi: Option<i16>,
93 pub device_type: Option<DeviceType>,
95 pub is_aranet: bool,
97 pub manufacturer_data: Option<Vec<u8>>,
99}
100
101#[derive(Debug, Clone)]
103pub struct ScanOptions {
104 pub duration: Duration,
106 pub filter_aranet_only: bool,
108 pub use_service_filter: bool,
111}
112
113impl Default for ScanOptions {
114 fn default() -> Self {
115 Self {
116 duration: Duration::from_secs(5),
117 filter_aranet_only: true,
118 use_service_filter: false,
121 }
122 }
123}
124
125impl ScanOptions {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn duration(mut self, duration: Duration) -> Self {
133 self.duration = duration;
134 self
135 }
136
137 pub fn duration_secs(mut self, secs: u64) -> Self {
139 self.duration = Duration::from_secs(secs);
140 self
141 }
142
143 pub fn filter_aranet_only(mut self, filter: bool) -> Self {
145 self.filter_aranet_only = filter;
146 self
147 }
148
149 pub fn all_devices(self) -> Self {
151 self.filter_aranet_only(false)
152 }
153
154 pub fn use_service_filter(mut self, enable: bool) -> Self {
162 self.use_service_filter = enable;
163 self
164 }
165
166 pub fn optimized() -> Self {
170 Self {
171 duration: Duration::from_secs(3),
172 filter_aranet_only: true,
173 use_service_filter: true,
174 }
175 }
176}
177
178pub async fn get_adapter() -> Result<Adapter> {
180 use crate::error::DeviceNotFoundReason;
181
182 #[cfg(target_os = "linux")]
186 crate::bluez_agent::ensure_agent();
187
188 let manager = shared_manager().await?;
189 let adapters = match manager.adapters().await {
190 Ok(a) => a,
191 Err(e) => {
192 reset_manager().await;
195 return Err(e.into());
196 }
197 };
198
199 adapters
200 .into_iter()
201 .next()
202 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
203}
204
205pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
217 scan_with_options(ScanOptions::default()).await
218}
219
220pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
222 let adapter = get_adapter().await?;
223 scan_with_adapter(&adapter, options).await
224}
225
226pub async fn scan_with_retry(
249 options: ScanOptions,
250 max_retries: u32,
251 retry_on_empty: bool,
252) -> Result<Vec<DiscoveredDevice>> {
253 let mut attempt = 0;
254 let mut delay = Duration::from_millis(500);
255
256 loop {
257 match scan_with_options(options.clone()).await {
258 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
259 attempt += 1;
260 warn!(
261 "No devices found, retrying ({}/{})...",
262 attempt, max_retries
263 );
264 sleep(delay).await;
265 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
266 }
267 Ok(devices) => return Ok(devices),
268 Err(e) if attempt < max_retries => {
269 attempt += 1;
270 warn!(
271 "Scan failed ({}), retrying ({}/{})...",
272 e, attempt, max_retries
273 );
274 sleep(delay).await;
275 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
276 }
277 Err(e) => return Err(e),
278 }
279 }
280}
281
282pub async fn scan_with_adapter(
284 adapter: &Adapter,
285 options: ScanOptions,
286) -> Result<Vec<DiscoveredDevice>> {
287 info!(
288 "Starting BLE scan for {} seconds (service_filter={})...",
289 options.duration.as_secs(),
290 options.use_service_filter
291 );
292
293 let scan_filter = if options.use_service_filter {
295 ScanFilter {
296 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
297 }
298 } else {
299 ScanFilter::default()
300 };
301
302 adapter.start_scan(scan_filter).await?;
304
305 sleep(options.duration).await;
307
308 adapter.stop_scan().await?;
310
311 let peripherals = adapter.peripherals().await?;
313 let mut discovered = Vec::new();
314
315 for peripheral in peripherals {
316 match process_peripheral(&peripheral, options.filter_aranet_only).await {
317 Ok(Some(device)) => {
318 info!("Found Aranet device: {:?}", device.name);
319 discovered.push(device);
320 }
321 Ok(None) => {
322 }
324 Err(e) => {
325 debug!("Error processing peripheral: {}", e);
326 }
327 }
328 }
329
330 info!("Scan complete. Found {} device(s)", discovered.len());
331 Ok(discovered)
332}
333
334async fn process_peripheral(
336 peripheral: &Peripheral,
337 filter_aranet_only: bool,
338) -> Result<Option<DiscoveredDevice>> {
339 let properties = peripheral.properties().await?;
340 let properties = match properties {
341 Some(p) => p,
342 None => return Ok(None),
343 };
344
345 let id = peripheral.id();
346 let address = properties.address.to_string();
347 let name = properties.local_name.clone();
348 let rssi = properties.rssi;
349
350 let is_aranet = is_aranet_device(&properties);
352
353 if filter_aranet_only && !is_aranet {
354 return Ok(None);
355 }
356
357 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
359
360 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
362
363 let identifier = create_identifier(&address, &id);
366
367 Ok(Some(DiscoveredDevice {
368 name,
369 id,
370 address,
371 identifier,
372 rssi,
373 device_type,
374 is_aranet,
375 manufacturer_data,
376 }))
377}
378
379fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
381 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
383 return true;
384 }
385
386 for service_uuid in properties.service_data.keys() {
388 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
389 return true;
390 }
391 }
392
393 for service_uuid in &properties.services {
395 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
396 return true;
397 }
398 }
399
400 if let Some(name) = &properties.local_name {
402 let name_lower = name.to_lowercase();
403 if name_lower.contains("aranet") {
404 return true;
405 }
406 }
407
408 false
409}
410
411pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
413 find_device_with_options(identifier, ScanOptions::default()).await
414}
415
416pub async fn find_device_with_options(
425 identifier: &str,
426 options: ScanOptions,
427) -> Result<(Adapter, Peripheral)> {
428 find_device_with_progress(identifier, options, None).await
429}
430
431pub async fn find_device_with_adapter(
436 adapter: &Adapter,
437 identifier: &str,
438 options: ScanOptions,
439) -> Result<Peripheral> {
440 find_device_with_adapter_progress(adapter, identifier, options, None).await
441}
442
443pub async fn find_device_with_adapter_progress(
445 adapter: &Adapter,
446 identifier: &str,
447 options: ScanOptions,
448 progress: Option<ProgressCallback>,
449) -> Result<Peripheral> {
450 let identifier_lower = identifier.to_lowercase();
451
452 info!("Looking for device: {}", identifier);
453
454 if let Some(peripheral) = find_peripheral_by_identifier(adapter, &identifier_lower).await? {
455 info!("Found device in cache (no scan needed)");
456 if let Some(ref cb) = progress {
457 cb(FindProgress::CacheHit);
458 }
459 return Ok(peripheral);
460 }
461
462 let max_attempts: u32 = 3;
463 let base_duration = options.duration.as_millis() as u64 / 2;
464 let base_duration = Duration::from_millis(base_duration.max(2000));
465
466 for attempt in 1..=max_attempts {
467 let scan_duration = base_duration * attempt;
468 let duration_secs = scan_duration.as_secs();
469
470 info!(
471 "Scan attempt {}/{} ({}s)...",
472 attempt, max_attempts, duration_secs
473 );
474
475 if let Some(ref cb) = progress {
476 cb(FindProgress::ScanAttempt {
477 attempt,
478 total: max_attempts,
479 duration_secs,
480 });
481 }
482
483 adapter.start_scan(ScanFilter::default()).await?;
484 sleep(scan_duration).await;
485 adapter.stop_scan().await?;
486
487 if let Some(peripheral) = find_peripheral_by_identifier(adapter, &identifier_lower).await? {
488 info!("Found device on attempt {}", attempt);
489 if let Some(ref cb) = progress {
490 cb(FindProgress::Found { attempt });
491 }
492 return Ok(peripheral);
493 }
494
495 if attempt < max_attempts {
496 warn!("Device not found, retrying...");
497 if let Some(ref cb) = progress {
498 cb(FindProgress::RetryNeeded { attempt });
499 }
500 }
501 }
502
503 warn!(
504 "Device not found after {} attempts: {}",
505 max_attempts, identifier
506 );
507 Err(Error::device_not_found(identifier))
508}
509
510pub async fn find_device_with_progress(
515 identifier: &str,
516 options: ScanOptions,
517 progress: Option<ProgressCallback>,
518) -> Result<(Adapter, Peripheral)> {
519 let adapter = get_adapter().await?;
520 let peripheral =
521 find_device_with_adapter_progress(&adapter, identifier, options, progress).await?;
522 Ok((adapter, peripheral))
523}
524
525async fn find_peripheral_by_identifier(
527 adapter: &Adapter,
528 identifier_lower: &str,
529) -> Result<Option<Peripheral>> {
530 let peripherals = adapter.peripherals().await?;
531
532 for peripheral in peripherals {
533 if let Ok(Some(props)) = peripheral.properties().await {
534 let address = props.address.to_string().to_lowercase();
535 let peripheral_id = format_peripheral_id(&peripheral.id()).to_lowercase();
536
537 if peripheral_id.contains(identifier_lower) {
539 debug!("Matched by peripheral ID: {}", peripheral_id);
540 return Ok(Some(peripheral));
541 }
542
543 if address != "00:00:00:00:00:00"
545 && (address == identifier_lower
546 || address.replace(':', "") == identifier_lower.replace(':', ""))
547 {
548 debug!("Matched by address: {}", address);
549 return Ok(Some(peripheral));
550 }
551
552 if let Some(name) = &props.local_name
554 && name.to_lowercase().contains(identifier_lower)
555 {
556 debug!("Matched by name: {}", name);
557 return Ok(Some(peripheral));
558 }
559 }
560 }
561
562 Ok(None)
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
572 fn test_scan_options_default() {
573 let options = ScanOptions::default();
574 assert_eq!(options.duration, Duration::from_secs(5));
575 assert!(options.filter_aranet_only);
576 }
577
578 #[test]
579 fn test_scan_options_new() {
580 let options = ScanOptions::new();
581 assert_eq!(options.duration, Duration::from_secs(5));
582 assert!(options.filter_aranet_only);
583 }
584
585 #[test]
586 fn test_scan_options_duration() {
587 let options = ScanOptions::new().duration(Duration::from_secs(10));
588 assert_eq!(options.duration, Duration::from_secs(10));
589 }
590
591 #[test]
592 fn test_scan_options_duration_secs() {
593 let options = ScanOptions::new().duration_secs(15);
594 assert_eq!(options.duration, Duration::from_secs(15));
595 }
596
597 #[test]
598 fn test_scan_options_filter_aranet_only() {
599 let options = ScanOptions::new().filter_aranet_only(false);
600 assert!(!options.filter_aranet_only);
601
602 let options = ScanOptions::new().filter_aranet_only(true);
603 assert!(options.filter_aranet_only);
604 }
605
606 #[test]
607 fn test_scan_options_all_devices() {
608 let options = ScanOptions::new().all_devices();
609 assert!(!options.filter_aranet_only);
610 }
611
612 #[test]
613 fn test_scan_options_chaining() {
614 let options = ScanOptions::new()
615 .duration_secs(20)
616 .filter_aranet_only(false);
617
618 assert_eq!(options.duration, Duration::from_secs(20));
619 assert!(!options.filter_aranet_only);
620 }
621
622 #[test]
623 fn test_scan_options_clone() {
624 let options1 = ScanOptions::new().duration_secs(8);
625 let options2 = options1.clone();
626
627 assert_eq!(options1.duration, options2.duration);
628 assert_eq!(options1.filter_aranet_only, options2.filter_aranet_only);
629 }
630
631 #[test]
632 fn test_scan_options_debug() {
633 let options = ScanOptions::new();
634 let debug = format!("{:?}", options);
635 assert!(debug.contains("ScanOptions"));
636 assert!(debug.contains("duration"));
637 assert!(debug.contains("filter_aranet_only"));
638 }
639
640 #[test]
643 fn test_find_progress_cache_hit() {
644 let progress = FindProgress::CacheHit;
645 let debug = format!("{:?}", progress);
646 assert!(debug.contains("CacheHit"));
647 }
648
649 #[test]
650 fn test_find_progress_scan_attempt() {
651 let progress = FindProgress::ScanAttempt {
652 attempt: 2,
653 total: 3,
654 duration_secs: 5,
655 };
656
657 if let FindProgress::ScanAttempt {
658 attempt,
659 total,
660 duration_secs,
661 } = progress
662 {
663 assert_eq!(attempt, 2);
664 assert_eq!(total, 3);
665 assert_eq!(duration_secs, 5);
666 } else {
667 panic!("Expected ScanAttempt variant");
668 }
669 }
670
671 #[test]
672 fn test_find_progress_found() {
673 let progress = FindProgress::Found { attempt: 1 };
674 assert!(matches!(progress, FindProgress::Found { attempt: 1 }));
675 }
676
677 #[test]
678 fn test_find_progress_retry_needed() {
679 let progress = FindProgress::RetryNeeded { attempt: 2 };
680 assert!(matches!(progress, FindProgress::RetryNeeded { attempt: 2 }));
681 }
682
683 #[test]
684 fn test_find_progress_clone() {
685 let progress1 = FindProgress::ScanAttempt {
686 attempt: 1,
687 total: 3,
688 duration_secs: 4,
689 };
690 let progress2 = progress1.clone();
691
692 assert!(matches!(
693 (&progress1, &progress2),
694 (
695 FindProgress::ScanAttempt {
696 attempt: 1,
697 total: 3,
698 duration_secs: 4,
699 },
700 FindProgress::ScanAttempt {
701 attempt: 1,
702 total: 3,
703 duration_secs: 4,
704 },
705 )
706 ));
707 }
708
709 }