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
178static ADAPTER: RwLock<Option<Adapter>> = RwLock::const_new(None);
193
194static PROCESS_RUNTIME: std::sync::Mutex<Option<tokio::runtime::Runtime>> =
197 std::sync::Mutex::new(None);
198
199async fn run_on_process_runtime<F>(future: F) -> Result<F::Output>
202where
203 F: std::future::Future + Send + 'static,
204 F::Output: Send + 'static,
205{
206 let handle = {
207 let mut guard = PROCESS_RUNTIME.lock().unwrap_or_else(|e| e.into_inner());
208 match guard.as_ref() {
209 Some(runtime) => runtime.handle().clone(),
210 None => {
211 let runtime = tokio::runtime::Builder::new_multi_thread()
212 .worker_threads(1)
213 .thread_name("aranet-ble")
214 .enable_all()
215 .build()?;
216 let handle = runtime.handle().clone();
217 *guard = Some(runtime);
218 handle
219 }
220 }
221 };
222 let output = handle.spawn(future).await.map_err(std::io::Error::from)?;
223 Ok(output)
224}
225
226pub async fn get_adapter() -> Result<Adapter> {
233 if cfg!(target_os = "macos") {
234 cached_adapter().await
235 } else {
236 create_adapter().await
237 }
238}
239
240async fn cached_adapter() -> Result<Adapter> {
241 let cached = ADAPTER.read().await.clone();
242 if let Some(adapter) = cached
243 && adapter_thread_is_running(&adapter).await
244 {
245 return Ok(adapter);
246 }
247 let mut guard = ADAPTER.write().await;
248 if let Some(adapter) = guard.as_ref() {
249 if adapter_thread_is_running(adapter).await {
250 return Ok(adapter.clone());
251 }
252 warn!("CoreBluetooth adapter thread has stopped; creating a new adapter");
253 }
254 let adapter = run_on_process_runtime(create_adapter()).await??;
255 *guard = Some(adapter.clone());
256 Ok(adapter)
257}
258
259async fn adapter_thread_is_running(adapter: &Adapter) -> bool {
267 let adapter = adapter.clone();
268 let state = run_on_process_runtime(async move {
271 tokio::time::timeout(Duration::from_secs(2), adapter.adapter_state()).await
272 })
273 .await;
274 match state {
275 Ok(Ok(Err(e))) => {
276 debug!("Cached Bluetooth adapter is unusable: {e}");
277 false
278 }
279 _ => true,
280 }
281}
282
283async fn create_adapter() -> Result<Adapter> {
284 use crate::error::DeviceNotFoundReason;
285
286 #[cfg(target_os = "linux")]
290 crate::bluez_agent::ensure_agent();
291
292 let manager = shared_manager().await?;
293 let adapters = match manager.adapters().await {
294 Ok(a) => a,
295 Err(e) => {
296 reset_manager().await;
299 return Err(e.into());
300 }
301 };
302
303 adapters
304 .into_iter()
305 .next()
306 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
307}
308
309pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
321 scan_with_options(ScanOptions::default()).await
322}
323
324pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
326 let adapter = get_adapter().await?;
327 scan_with_adapter(&adapter, options).await
328}
329
330pub async fn scan_with_retry(
353 options: ScanOptions,
354 max_retries: u32,
355 retry_on_empty: bool,
356) -> Result<Vec<DiscoveredDevice>> {
357 let mut attempt = 0;
358 let mut delay = Duration::from_millis(500);
359
360 loop {
361 match scan_with_options(options.clone()).await {
362 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
363 attempt += 1;
364 warn!(
365 "No devices found, retrying ({}/{})...",
366 attempt, max_retries
367 );
368 sleep(delay).await;
369 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
370 }
371 Ok(devices) => return Ok(devices),
372 Err(e) if attempt < max_retries => {
373 attempt += 1;
374 warn!(
375 "Scan failed ({}), retrying ({}/{})...",
376 e, attempt, max_retries
377 );
378 sleep(delay).await;
379 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
380 }
381 Err(e) => return Err(e),
382 }
383 }
384}
385
386pub async fn scan_with_adapter(
388 adapter: &Adapter,
389 options: ScanOptions,
390) -> Result<Vec<DiscoveredDevice>> {
391 info!(
392 "Starting BLE scan for {} seconds (service_filter={})...",
393 options.duration.as_secs(),
394 options.use_service_filter
395 );
396
397 let scan_filter = if options.use_service_filter {
399 ScanFilter {
400 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
401 }
402 } else {
403 ScanFilter::default()
404 };
405
406 adapter.start_scan(scan_filter).await?;
408
409 sleep(options.duration).await;
411
412 adapter.stop_scan().await?;
414
415 let peripherals = adapter.peripherals().await?;
417 let mut discovered = Vec::new();
418
419 for peripheral in peripherals {
420 match process_peripheral(&peripheral, options.filter_aranet_only).await {
421 Ok(Some(device)) => {
422 info!("Found Aranet device: {:?}", device.name);
423 discovered.push(device);
424 }
425 Ok(None) => {
426 }
428 Err(e) => {
429 debug!("Error processing peripheral: {}", e);
430 }
431 }
432 }
433
434 info!("Scan complete. Found {} device(s)", discovered.len());
435 Ok(discovered)
436}
437
438async fn process_peripheral(
440 peripheral: &Peripheral,
441 filter_aranet_only: bool,
442) -> Result<Option<DiscoveredDevice>> {
443 let properties = peripheral.properties().await?;
444 let properties = match properties {
445 Some(p) => p,
446 None => return Ok(None),
447 };
448
449 let id = peripheral.id();
450 let address = properties.address.to_string();
451 let name = properties.local_name.clone();
452 let rssi = properties.rssi;
453
454 let is_aranet = is_aranet_device(&properties);
456
457 if filter_aranet_only && !is_aranet {
458 return Ok(None);
459 }
460
461 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
463
464 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
466
467 let identifier = create_identifier(&address, &id);
470
471 Ok(Some(DiscoveredDevice {
472 name,
473 id,
474 address,
475 identifier,
476 rssi,
477 device_type,
478 is_aranet,
479 manufacturer_data,
480 }))
481}
482
483fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
485 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
487 return true;
488 }
489
490 for service_uuid in properties.service_data.keys() {
492 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
493 return true;
494 }
495 }
496
497 for service_uuid in &properties.services {
499 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
500 return true;
501 }
502 }
503
504 if let Some(name) = &properties.local_name {
506 let name_lower = name.to_lowercase();
507 if name_lower.contains("aranet") {
508 return true;
509 }
510 }
511
512 false
513}
514
515pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
517 find_device_with_options(identifier, ScanOptions::default()).await
518}
519
520pub async fn find_device_with_options(
529 identifier: &str,
530 options: ScanOptions,
531) -> Result<(Adapter, Peripheral)> {
532 find_device_with_progress(identifier, options, None).await
533}
534
535pub async fn find_device_with_adapter(
540 adapter: &Adapter,
541 identifier: &str,
542 options: ScanOptions,
543) -> Result<Peripheral> {
544 find_device_with_adapter_progress(adapter, identifier, options, None).await
545}
546
547pub async fn find_device_with_adapter_progress(
549 adapter: &Adapter,
550 identifier: &str,
551 options: ScanOptions,
552 progress: Option<ProgressCallback>,
553) -> Result<Peripheral> {
554 let identifier_lower = identifier.to_lowercase();
555
556 info!("Looking for device: {}", identifier);
557
558 if let Some(peripheral) = find_peripheral_by_identifier(adapter, &identifier_lower).await? {
559 info!("Found device in cache (no scan needed)");
560 if let Some(ref cb) = progress {
561 cb(FindProgress::CacheHit);
562 }
563 return Ok(peripheral);
564 }
565
566 let max_attempts: u32 = 3;
567 let base_duration = options.duration.as_millis() as u64 / 2;
568 let base_duration = Duration::from_millis(base_duration.max(2000));
569
570 for attempt in 1..=max_attempts {
571 let scan_duration = base_duration * attempt;
572 let duration_secs = scan_duration.as_secs();
573
574 info!(
575 "Scan attempt {}/{} ({}s)...",
576 attempt, max_attempts, duration_secs
577 );
578
579 if let Some(ref cb) = progress {
580 cb(FindProgress::ScanAttempt {
581 attempt,
582 total: max_attempts,
583 duration_secs,
584 });
585 }
586
587 adapter.start_scan(ScanFilter::default()).await?;
588 sleep(scan_duration).await;
589 adapter.stop_scan().await?;
590
591 if let Some(peripheral) = find_peripheral_by_identifier(adapter, &identifier_lower).await? {
592 info!("Found device on attempt {}", attempt);
593 if let Some(ref cb) = progress {
594 cb(FindProgress::Found { attempt });
595 }
596 return Ok(peripheral);
597 }
598
599 if attempt < max_attempts {
600 warn!("Device not found, retrying...");
601 if let Some(ref cb) = progress {
602 cb(FindProgress::RetryNeeded { attempt });
603 }
604 }
605 }
606
607 warn!(
608 "Device not found after {} attempts: {}",
609 max_attempts, identifier
610 );
611 Err(Error::device_not_found(identifier))
612}
613
614pub async fn find_device_with_progress(
619 identifier: &str,
620 options: ScanOptions,
621 progress: Option<ProgressCallback>,
622) -> Result<(Adapter, Peripheral)> {
623 let adapter = get_adapter().await?;
624 let peripheral =
625 find_device_with_adapter_progress(&adapter, identifier, options, progress).await?;
626 Ok((adapter, peripheral))
627}
628
629async fn find_peripheral_by_identifier(
631 adapter: &Adapter,
632 identifier_lower: &str,
633) -> Result<Option<Peripheral>> {
634 let peripherals = adapter.peripherals().await?;
635
636 for peripheral in peripherals {
637 if let Ok(Some(props)) = peripheral.properties().await {
638 let address = props.address.to_string().to_lowercase();
639 let peripheral_id = format_peripheral_id(&peripheral.id()).to_lowercase();
640
641 if peripheral_id.contains(identifier_lower) {
643 debug!("Matched by peripheral ID: {}", peripheral_id);
644 return Ok(Some(peripheral));
645 }
646
647 if address != "00:00:00:00:00:00"
649 && (address == identifier_lower
650 || address.replace(':', "") == identifier_lower.replace(':', ""))
651 {
652 debug!("Matched by address: {}", address);
653 return Ok(Some(peripheral));
654 }
655
656 if let Some(name) = &props.local_name
658 && name.to_lowercase().contains(identifier_lower)
659 {
660 debug!("Matched by name: {}", name);
661 return Ok(Some(peripheral));
662 }
663 }
664 }
665
666 Ok(None)
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
676 fn test_scan_options_default() {
677 let options = ScanOptions::default();
678 assert_eq!(options.duration, Duration::from_secs(5));
679 assert!(options.filter_aranet_only);
680 }
681
682 #[test]
683 fn test_scan_options_new() {
684 let options = ScanOptions::new();
685 assert_eq!(options.duration, Duration::from_secs(5));
686 assert!(options.filter_aranet_only);
687 }
688
689 #[test]
690 fn test_scan_options_duration() {
691 let options = ScanOptions::new().duration(Duration::from_secs(10));
692 assert_eq!(options.duration, Duration::from_secs(10));
693 }
694
695 #[test]
696 fn test_scan_options_duration_secs() {
697 let options = ScanOptions::new().duration_secs(15);
698 assert_eq!(options.duration, Duration::from_secs(15));
699 }
700
701 #[test]
702 fn test_scan_options_filter_aranet_only() {
703 let options = ScanOptions::new().filter_aranet_only(false);
704 assert!(!options.filter_aranet_only);
705
706 let options = ScanOptions::new().filter_aranet_only(true);
707 assert!(options.filter_aranet_only);
708 }
709
710 #[test]
711 fn test_scan_options_all_devices() {
712 let options = ScanOptions::new().all_devices();
713 assert!(!options.filter_aranet_only);
714 }
715
716 #[test]
717 fn test_scan_options_chaining() {
718 let options = ScanOptions::new()
719 .duration_secs(20)
720 .filter_aranet_only(false);
721
722 assert_eq!(options.duration, Duration::from_secs(20));
723 assert!(!options.filter_aranet_only);
724 }
725
726 #[test]
727 fn test_scan_options_clone() {
728 let options1 = ScanOptions::new().duration_secs(8);
729 let options2 = options1.clone();
730
731 assert_eq!(options1.duration, options2.duration);
732 assert_eq!(options1.filter_aranet_only, options2.filter_aranet_only);
733 }
734
735 #[test]
736 fn test_scan_options_debug() {
737 let options = ScanOptions::new();
738 let debug = format!("{:?}", options);
739 assert!(debug.contains("ScanOptions"));
740 assert!(debug.contains("duration"));
741 assert!(debug.contains("filter_aranet_only"));
742 }
743
744 #[test]
747 fn test_find_progress_cache_hit() {
748 let progress = FindProgress::CacheHit;
749 let debug = format!("{:?}", progress);
750 assert!(debug.contains("CacheHit"));
751 }
752
753 #[test]
754 fn test_find_progress_scan_attempt() {
755 let progress = FindProgress::ScanAttempt {
756 attempt: 2,
757 total: 3,
758 duration_secs: 5,
759 };
760
761 if let FindProgress::ScanAttempt {
762 attempt,
763 total,
764 duration_secs,
765 } = progress
766 {
767 assert_eq!(attempt, 2);
768 assert_eq!(total, 3);
769 assert_eq!(duration_secs, 5);
770 } else {
771 panic!("Expected ScanAttempt variant");
772 }
773 }
774
775 #[test]
776 fn test_find_progress_found() {
777 let progress = FindProgress::Found { attempt: 1 };
778 assert!(matches!(progress, FindProgress::Found { attempt: 1 }));
779 }
780
781 #[test]
782 fn test_find_progress_retry_needed() {
783 let progress = FindProgress::RetryNeeded { attempt: 2 };
784 assert!(matches!(progress, FindProgress::RetryNeeded { attempt: 2 }));
785 }
786
787 #[test]
788 fn test_find_progress_clone() {
789 let progress1 = FindProgress::ScanAttempt {
790 attempt: 1,
791 total: 3,
792 duration_secs: 4,
793 };
794 let progress2 = progress1.clone();
795
796 assert!(matches!(
797 (&progress1, &progress2),
798 (
799 FindProgress::ScanAttempt {
800 attempt: 1,
801 total: 3,
802 duration_secs: 4,
803 },
804 FindProgress::ScanAttempt {
805 attempt: 1,
806 total: 3,
807 duration_secs: 4,
808 },
809 )
810 ));
811 }
812
813 #[test]
816 fn test_tasks_spawned_on_process_runtime_outlive_the_caller_runtime() {
817 let (tx, rx) = std::sync::mpsc::channel();
820 let caller = tokio::runtime::Builder::new_current_thread()
821 .enable_all()
822 .build()
823 .unwrap();
824 caller
825 .block_on(run_on_process_runtime(async move {
826 tokio::spawn(async move {
827 tokio::time::sleep(Duration::from_millis(50)).await;
828 tx.send(()).unwrap();
829 });
830 }))
831 .unwrap();
832 drop(caller);
833
834 rx.recv_timeout(Duration::from_secs(5))
835 .expect("task spawned on the process runtime died with the caller's runtime");
836 }
837
838 }