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 let manager = Manager::new().await?;
144 let adapters = manager.adapters().await?;
145
146 adapters
147 .into_iter()
148 .next()
149 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
150}
151
152pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
164 scan_with_options(ScanOptions::default()).await
165}
166
167pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
169 let adapter = get_adapter().await?;
170 scan_with_adapter(&adapter, options).await
171}
172
173pub async fn scan_with_retry(
196 options: ScanOptions,
197 max_retries: u32,
198 retry_on_empty: bool,
199) -> Result<Vec<DiscoveredDevice>> {
200 let mut attempt = 0;
201 let mut delay = Duration::from_millis(500);
202
203 loop {
204 match scan_with_options(options.clone()).await {
205 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
206 attempt += 1;
207 warn!(
208 "No devices found, retrying ({}/{})...",
209 attempt, max_retries
210 );
211 sleep(delay).await;
212 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
213 }
214 Ok(devices) => return Ok(devices),
215 Err(e) if attempt < max_retries => {
216 attempt += 1;
217 warn!(
218 "Scan failed ({}), retrying ({}/{})...",
219 e, attempt, max_retries
220 );
221 sleep(delay).await;
222 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
223 }
224 Err(e) => return Err(e),
225 }
226 }
227}
228
229pub async fn scan_with_adapter(
231 adapter: &Adapter,
232 options: ScanOptions,
233) -> Result<Vec<DiscoveredDevice>> {
234 info!(
235 "Starting BLE scan for {} seconds (service_filter={})...",
236 options.duration.as_secs(),
237 options.use_service_filter
238 );
239
240 let scan_filter = if options.use_service_filter {
242 ScanFilter {
243 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
244 }
245 } else {
246 ScanFilter::default()
247 };
248
249 adapter.start_scan(scan_filter).await?;
251
252 sleep(options.duration).await;
254
255 adapter.stop_scan().await?;
257
258 let peripherals = adapter.peripherals().await?;
260 let mut discovered = Vec::new();
261
262 for peripheral in peripherals {
263 match process_peripheral(&peripheral, options.filter_aranet_only).await {
264 Ok(Some(device)) => {
265 info!("Found Aranet device: {:?}", device.name);
266 discovered.push(device);
267 }
268 Ok(None) => {
269 }
271 Err(e) => {
272 debug!("Error processing peripheral: {}", e);
273 }
274 }
275 }
276
277 info!("Scan complete. Found {} device(s)", discovered.len());
278 Ok(discovered)
279}
280
281async fn process_peripheral(
283 peripheral: &Peripheral,
284 filter_aranet_only: bool,
285) -> Result<Option<DiscoveredDevice>> {
286 let properties = peripheral.properties().await?;
287 let properties = match properties {
288 Some(p) => p,
289 None => return Ok(None),
290 };
291
292 let id = peripheral.id();
293 let address = properties.address.to_string();
294 let name = properties.local_name.clone();
295 let rssi = properties.rssi;
296
297 let is_aranet = is_aranet_device(&properties);
299
300 if filter_aranet_only && !is_aranet {
301 return Ok(None);
302 }
303
304 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
306
307 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
309
310 let identifier = create_identifier(&address, &id);
313
314 Ok(Some(DiscoveredDevice {
315 name,
316 id,
317 address,
318 identifier,
319 rssi,
320 device_type,
321 is_aranet,
322 manufacturer_data,
323 }))
324}
325
326fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
328 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
330 return true;
331 }
332
333 for service_uuid in properties.service_data.keys() {
335 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
336 return true;
337 }
338 }
339
340 for service_uuid in &properties.services {
342 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
343 return true;
344 }
345 }
346
347 if let Some(name) = &properties.local_name {
349 let name_lower = name.to_lowercase();
350 if name_lower.contains("aranet") {
351 return true;
352 }
353 }
354
355 false
356}
357
358pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
360 find_device_with_options(identifier, ScanOptions::default()).await
361}
362
363pub async fn find_device_with_options(
372 identifier: &str,
373 options: ScanOptions,
374) -> Result<(Adapter, Peripheral)> {
375 find_device_with_progress(identifier, options, None).await
376}
377
378pub async fn find_device_with_progress(
383 identifier: &str,
384 options: ScanOptions,
385 progress: Option<ProgressCallback>,
386) -> Result<(Adapter, Peripheral)> {
387 let adapter = get_adapter().await?;
388 let identifier_lower = identifier.to_lowercase();
389
390 info!("Looking for device: {}", identifier);
391
392 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await? {
394 info!("Found device in cache (no scan needed)");
395 if let Some(ref cb) = progress {
396 cb(FindProgress::CacheHit);
397 }
398 return Ok((adapter, peripheral));
399 }
400
401 let max_attempts: u32 = 3;
404 let base_duration = options.duration.as_millis() as u64 / 2;
405 let base_duration = Duration::from_millis(base_duration.max(2000)); for attempt in 1..=max_attempts {
408 let scan_duration = base_duration * attempt;
409 let duration_secs = scan_duration.as_secs();
410
411 info!(
412 "Scan attempt {}/{} ({}s)...",
413 attempt, max_attempts, duration_secs
414 );
415
416 if let Some(ref cb) = progress {
417 cb(FindProgress::ScanAttempt {
418 attempt,
419 total: max_attempts,
420 duration_secs,
421 });
422 }
423
424 adapter.start_scan(ScanFilter::default()).await?;
426 sleep(scan_duration).await;
427 adapter.stop_scan().await?;
428
429 if let Some(peripheral) = find_peripheral_by_identifier(&adapter, &identifier_lower).await?
431 {
432 info!("Found device on attempt {}", attempt);
433 if let Some(ref cb) = progress {
434 cb(FindProgress::Found { attempt });
435 }
436 return Ok((adapter, peripheral));
437 }
438
439 if attempt < max_attempts {
440 warn!("Device not found, retrying...");
441 if let Some(ref cb) = progress {
442 cb(FindProgress::RetryNeeded { attempt });
443 }
444 }
445 }
446
447 warn!(
448 "Device not found after {} attempts: {}",
449 max_attempts, identifier
450 );
451 Err(Error::device_not_found(identifier))
452}
453
454async fn find_peripheral_by_identifier(
456 adapter: &Adapter,
457 identifier_lower: &str,
458) -> Result<Option<Peripheral>> {
459 let peripherals = adapter.peripherals().await?;
460
461 for peripheral in peripherals {
462 if let Ok(Some(props)) = peripheral.properties().await {
463 let address = props.address.to_string().to_lowercase();
464 let peripheral_id = format_peripheral_id(&peripheral.id()).to_lowercase();
465
466 if peripheral_id.contains(identifier_lower) {
468 debug!("Matched by peripheral ID: {}", peripheral_id);
469 return Ok(Some(peripheral));
470 }
471
472 if address != "00:00:00:00:00:00"
474 && (address == identifier_lower
475 || address.replace(':', "") == identifier_lower.replace(':', ""))
476 {
477 debug!("Matched by address: {}", address);
478 return Ok(Some(peripheral));
479 }
480
481 if let Some(name) = &props.local_name
483 && name.to_lowercase().contains(identifier_lower)
484 {
485 debug!("Matched by name: {}", name);
486 return Ok(Some(peripheral));
487 }
488 }
489 }
490
491 Ok(None)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
501 fn test_scan_options_default() {
502 let options = ScanOptions::default();
503 assert_eq!(options.duration, Duration::from_secs(5));
504 assert!(options.filter_aranet_only);
505 }
506
507 #[test]
508 fn test_scan_options_new() {
509 let options = ScanOptions::new();
510 assert_eq!(options.duration, Duration::from_secs(5));
511 assert!(options.filter_aranet_only);
512 }
513
514 #[test]
515 fn test_scan_options_duration() {
516 let options = ScanOptions::new().duration(Duration::from_secs(10));
517 assert_eq!(options.duration, Duration::from_secs(10));
518 }
519
520 #[test]
521 fn test_scan_options_duration_secs() {
522 let options = ScanOptions::new().duration_secs(15);
523 assert_eq!(options.duration, Duration::from_secs(15));
524 }
525
526 #[test]
527 fn test_scan_options_filter_aranet_only() {
528 let options = ScanOptions::new().filter_aranet_only(false);
529 assert!(!options.filter_aranet_only);
530
531 let options = ScanOptions::new().filter_aranet_only(true);
532 assert!(options.filter_aranet_only);
533 }
534
535 #[test]
536 fn test_scan_options_all_devices() {
537 let options = ScanOptions::new().all_devices();
538 assert!(!options.filter_aranet_only);
539 }
540
541 #[test]
542 fn test_scan_options_chaining() {
543 let options = ScanOptions::new()
544 .duration_secs(20)
545 .filter_aranet_only(false);
546
547 assert_eq!(options.duration, Duration::from_secs(20));
548 assert!(!options.filter_aranet_only);
549 }
550
551 #[test]
552 fn test_scan_options_clone() {
553 let options1 = ScanOptions::new().duration_secs(8);
554 let options2 = options1.clone();
555
556 assert_eq!(options1.duration, options2.duration);
557 assert_eq!(options1.filter_aranet_only, options2.filter_aranet_only);
558 }
559
560 #[test]
561 fn test_scan_options_debug() {
562 let options = ScanOptions::new();
563 let debug = format!("{:?}", options);
564 assert!(debug.contains("ScanOptions"));
565 assert!(debug.contains("duration"));
566 assert!(debug.contains("filter_aranet_only"));
567 }
568
569 #[test]
572 fn test_find_progress_cache_hit() {
573 let progress = FindProgress::CacheHit;
574 let debug = format!("{:?}", progress);
575 assert!(debug.contains("CacheHit"));
576 }
577
578 #[test]
579 fn test_find_progress_scan_attempt() {
580 let progress = FindProgress::ScanAttempt {
581 attempt: 2,
582 total: 3,
583 duration_secs: 5,
584 };
585
586 if let FindProgress::ScanAttempt {
587 attempt,
588 total,
589 duration_secs,
590 } = progress
591 {
592 assert_eq!(attempt, 2);
593 assert_eq!(total, 3);
594 assert_eq!(duration_secs, 5);
595 } else {
596 panic!("Unexpected variant");
597 }
598 }
599
600 #[test]
601 fn test_find_progress_found() {
602 let progress = FindProgress::Found { attempt: 1 };
603 if let FindProgress::Found { attempt } = progress {
604 assert_eq!(attempt, 1);
605 } else {
606 panic!("Unexpected variant");
607 }
608 }
609
610 #[test]
611 fn test_find_progress_retry_needed() {
612 let progress = FindProgress::RetryNeeded { attempt: 2 };
613 if let FindProgress::RetryNeeded { attempt } = progress {
614 assert_eq!(attempt, 2);
615 } else {
616 panic!("Unexpected variant");
617 }
618 }
619
620 #[test]
621 fn test_find_progress_clone() {
622 let progress1 = FindProgress::ScanAttempt {
623 attempt: 1,
624 total: 3,
625 duration_secs: 4,
626 };
627 let progress2 = progress1.clone();
628
629 if let (
630 FindProgress::ScanAttempt {
631 attempt: a1,
632 total: t1,
633 duration_secs: d1,
634 },
635 FindProgress::ScanAttempt {
636 attempt: a2,
637 total: t2,
638 duration_secs: d2,
639 },
640 ) = (progress1, progress2)
641 {
642 assert_eq!(a1, a2);
643 assert_eq!(t1, t2);
644 assert_eq!(d1, d2);
645 } else {
646 panic!("Clone should preserve variant");
647 }
648 }
649
650 }