1use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::time::Duration;
9
10use async_trait::async_trait;
11use btleplug::api::{Characteristic, Peripheral as _, WriteType};
12use btleplug::platform::{Adapter, Peripheral};
13use tokio::sync::RwLock;
14use tokio::time::timeout;
15use tracing::{debug, info, warn};
16use uuid::Uuid;
17
18use crate::error::{Error, Result};
19use crate::scan::{ScanOptions, find_device};
20use crate::traits::AranetDevice;
21use crate::util::{create_identifier, format_peripheral_id};
22use crate::uuid::{
23 BATTERY_LEVEL, BATTERY_SERVICE, CURRENT_READINGS_DETAIL, CURRENT_READINGS_DETAIL_ALT,
24 DEVICE_INFO_SERVICE, DEVICE_NAME, FIRMWARE_REVISION, GAP_SERVICE, HARDWARE_REVISION,
25 MANUFACTURER_NAME, MODEL_NUMBER, SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD,
26 SERIAL_NUMBER, SOFTWARE_REVISION,
27};
28use aranet_types::{CurrentReading, DeviceInfo, DeviceType};
29
30pub struct Device {
46 #[allow(dead_code)]
52 adapter: Adapter,
53 peripheral: Peripheral,
55 name: Option<String>,
57 address: String,
59 device_type: Option<DeviceType>,
61 services_discovered: bool,
63 characteristics_cache: RwLock<HashMap<Uuid, Characteristic>>,
66 notification_handles: tokio::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
68 disconnected: AtomicBool,
70 config: ConnectionConfig,
72}
73
74impl std::fmt::Debug for Device {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("Device")
80 .field("name", &self.name)
81 .field("address", &self.address)
82 .field("device_type", &self.device_type)
83 .field("services_discovered", &self.services_discovered)
84 .finish_non_exhaustive()
85 }
86}
87
88const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
90
91const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(10);
93
94const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
96
97const DEFAULT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(10);
99
100const DEFAULT_VALIDATION_TIMEOUT: Duration = Duration::from_secs(3);
102
103#[derive(Debug, Clone)]
121pub struct ConnectionConfig {
122 pub connection_timeout: Duration,
124 pub read_timeout: Duration,
126 pub write_timeout: Duration,
128 pub discovery_timeout: Duration,
130 pub validation_timeout: Duration,
132}
133
134impl Default for ConnectionConfig {
135 fn default() -> Self {
136 Self {
137 connection_timeout: DEFAULT_CONNECT_TIMEOUT,
138 read_timeout: DEFAULT_READ_TIMEOUT,
139 write_timeout: DEFAULT_WRITE_TIMEOUT,
140 discovery_timeout: DEFAULT_DISCOVERY_TIMEOUT,
141 validation_timeout: DEFAULT_VALIDATION_TIMEOUT,
142 }
143 }
144}
145
146impl ConnectionConfig {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn for_current_platform() -> Self {
154 let platform = crate::platform::PlatformConfig::for_current_platform();
155 Self {
156 connection_timeout: platform.recommended_connection_timeout,
157 read_timeout: platform.recommended_operation_timeout,
158 write_timeout: platform.recommended_operation_timeout,
159 discovery_timeout: platform.recommended_operation_timeout,
160 validation_timeout: DEFAULT_VALIDATION_TIMEOUT,
161 }
162 }
163
164 pub fn challenging_environment() -> Self {
169 Self {
170 connection_timeout: Duration::from_secs(25),
171 read_timeout: Duration::from_secs(15),
172 write_timeout: Duration::from_secs(15),
173 discovery_timeout: Duration::from_secs(15),
174 validation_timeout: Duration::from_secs(5),
175 }
176 }
177
178 pub fn fast() -> Self {
183 Self {
184 connection_timeout: Duration::from_secs(8),
185 read_timeout: Duration::from_secs(5),
186 write_timeout: Duration::from_secs(5),
187 discovery_timeout: Duration::from_secs(5),
188 validation_timeout: Duration::from_secs(2),
189 }
190 }
191
192 #[must_use]
194 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
195 self.connection_timeout = timeout;
196 self
197 }
198
199 #[must_use]
201 pub fn read_timeout(mut self, timeout: Duration) -> Self {
202 self.read_timeout = timeout;
203 self
204 }
205
206 #[must_use]
208 pub fn write_timeout(mut self, timeout: Duration) -> Self {
209 self.write_timeout = timeout;
210 self
211 }
212
213 #[must_use]
215 pub fn discovery_timeout(mut self, timeout: Duration) -> Self {
216 self.discovery_timeout = timeout;
217 self
218 }
219
220 #[must_use]
222 pub fn validation_timeout(mut self, timeout: Duration) -> Self {
223 self.validation_timeout = timeout;
224 self
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
230pub enum SignalQuality {
231 Poor,
233 Fair,
235 Good,
237 Excellent,
239}
240
241impl SignalQuality {
242 pub fn from_rssi(rssi: i16) -> Self {
252 match rssi {
253 r if r > -60 => SignalQuality::Excellent,
254 r if r > -75 => SignalQuality::Good,
255 r if r > -85 => SignalQuality::Fair,
256 _ => SignalQuality::Poor,
257 }
258 }
259
260 pub fn description(&self) -> &'static str {
262 match self {
263 SignalQuality::Excellent => "Excellent signal",
264 SignalQuality::Good => "Good signal",
265 SignalQuality::Fair => "Fair signal - connection may be unstable",
266 SignalQuality::Poor => "Poor signal - consider moving closer",
267 }
268 }
269
270 pub fn recommended_read_delay(&self) -> Duration {
272 match self {
273 SignalQuality::Excellent => Duration::from_millis(30),
274 SignalQuality::Good => Duration::from_millis(50),
275 SignalQuality::Fair => Duration::from_millis(100),
276 SignalQuality::Poor => Duration::from_millis(200),
277 }
278 }
279
280 pub fn is_usable(&self) -> bool {
282 matches!(
283 self,
284 SignalQuality::Excellent | SignalQuality::Good | SignalQuality::Fair
285 )
286 }
287}
288
289impl Device {
290 #[tracing::instrument(level = "info", skip_all, fields(identifier = %identifier))]
305 pub async fn connect(identifier: &str) -> Result<Self> {
306 Self::connect_with_config(identifier, ConnectionConfig::default()).await
307 }
308
309 #[tracing::instrument(level = "info", skip_all, fields(identifier = %identifier, timeout_secs = scan_timeout.as_secs()))]
311 pub async fn connect_with_timeout(identifier: &str, scan_timeout: Duration) -> Result<Self> {
312 let config = ConnectionConfig::default().connection_timeout(scan_timeout);
313 Self::connect_with_config(identifier, config).await
314 }
315
316 #[tracing::instrument(level = "info", skip_all, fields(identifier = %identifier))]
336 pub async fn connect_with_config(identifier: &str, config: ConnectionConfig) -> Result<Self> {
337 let options = ScanOptions {
338 duration: config.connection_timeout,
339 filter_aranet_only: false, use_service_filter: false,
341 };
342
343 let (adapter, peripheral) = match find_device(identifier).await {
345 Ok(result) => result,
346 Err(_) => crate::scan::find_device_with_options(identifier, options).await?,
347 };
348
349 Self::from_peripheral_with_config(adapter, peripheral, config).await
350 }
351
352 #[tracing::instrument(level = "info", skip_all)]
354 pub async fn from_peripheral(adapter: Adapter, peripheral: Peripheral) -> Result<Self> {
355 Self::from_peripheral_with_config(adapter, peripheral, ConnectionConfig::default()).await
356 }
357
358 #[tracing::instrument(level = "info", skip_all, fields(timeout_secs = connect_timeout.as_secs()))]
360 pub async fn from_peripheral_with_timeout(
361 adapter: Adapter,
362 peripheral: Peripheral,
363 connect_timeout: Duration,
364 ) -> Result<Self> {
365 let config = ConnectionConfig::default().connection_timeout(connect_timeout);
366 Self::from_peripheral_with_config(adapter, peripheral, config).await
367 }
368
369 #[tracing::instrument(level = "info", skip_all, fields(connect_timeout = ?config.connection_timeout))]
371 pub async fn from_peripheral_with_config(
372 adapter: Adapter,
373 peripheral: Peripheral,
374 config: ConnectionConfig,
375 ) -> Result<Self> {
376 info!("Connecting to device...");
378 timeout(config.connection_timeout, peripheral.connect())
379 .await
380 .map_err(|_| Error::Timeout {
381 operation: "connect to device".to_string(),
382 duration: config.connection_timeout,
383 })??;
384 info!("Connected!");
385
386 info!("Discovering services...");
388 timeout(config.discovery_timeout, peripheral.discover_services())
389 .await
390 .map_err(|_| Error::Timeout {
391 operation: "discover services".to_string(),
392 duration: config.discovery_timeout,
393 })??;
394
395 let services = peripheral.services();
396 debug!("Found {} services", services.len());
397
398 let mut characteristics_cache = HashMap::new();
400 for service in &services {
401 debug!(" Service: {}", service.uuid);
402 for char in &service.characteristics {
403 debug!(" Characteristic: {}", char.uuid);
404 characteristics_cache.insert(char.uuid, char.clone());
405 }
406 }
407 debug!(
408 "Cached {} characteristics for fast lookup",
409 characteristics_cache.len()
410 );
411
412 let properties = peripheral.properties().await?;
414 let name = properties.as_ref().and_then(|p| p.local_name.clone());
415
416 let address = properties
418 .as_ref()
419 .map(|p| create_identifier(&p.address.to_string(), &peripheral.id()))
420 .unwrap_or_else(|| format_peripheral_id(&peripheral.id()));
421
422 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
424
425 Ok(Self {
426 adapter,
427 peripheral,
428 name,
429 address,
430 device_type,
431 services_discovered: true,
432 characteristics_cache: RwLock::new(characteristics_cache),
433 notification_handles: tokio::sync::Mutex::new(Vec::new()),
434 disconnected: AtomicBool::new(false),
435 config,
436 })
437 }
438
439 pub async fn is_connected(&self) -> bool {
444 self.peripheral.is_connected().await.unwrap_or(false)
445 }
446
447 pub async fn validate_connection(&self) -> bool {
460 timeout(self.config.validation_timeout, self.read_battery())
461 .await
462 .map(|r| r.is_ok())
463 .unwrap_or(false)
464 }
465
466 pub async fn is_connection_alive(&self) -> bool {
480 self.validate_connection().await
481 }
482
483 pub fn config(&self) -> &ConnectionConfig {
485 &self.config
486 }
487
488 pub async fn signal_quality(&self) -> Option<SignalQuality> {
492 self.read_rssi().await.ok().map(SignalQuality::from_rssi)
493 }
494
495 #[tracing::instrument(level = "info", skip(self), fields(device_name = ?self.name))]
504 pub async fn disconnect(&self) -> Result<()> {
505 info!("Disconnecting from device...");
506 self.disconnected.store(true, Ordering::SeqCst);
507
508 {
510 let mut handles = self.notification_handles.lock().await;
511 for handle in handles.drain(..) {
512 handle.abort();
513 }
514 }
515
516 self.peripheral.disconnect().await?;
517 Ok(())
518 }
519
520 pub fn name(&self) -> Option<&str> {
522 self.name.as_deref()
523 }
524
525 pub fn address(&self) -> &str {
530 &self.address
531 }
532
533 pub fn device_type(&self) -> Option<DeviceType> {
535 self.device_type
536 }
537
538 pub async fn read_rssi(&self) -> Result<i16> {
543 let properties = self.peripheral.properties().await?;
544 properties
545 .and_then(|p| p.rssi)
546 .ok_or_else(|| Error::InvalidData("RSSI not available".to_string()))
547 }
548
549 async fn find_characteristic(&self, uuid: Uuid) -> Result<Characteristic> {
555 {
557 let cache = self.characteristics_cache.read().await;
558 if let Some(char) = cache.get(&uuid) {
559 return Ok(char.clone());
560 }
561
562 if !cache.is_empty() {
564 return Err(Error::characteristic_not_found(
565 uuid.to_string(),
566 self.peripheral.services().len(),
567 ));
568 }
569 }
570
571 warn!(
573 "Characteristics cache empty, falling back to service search for {}",
574 uuid
575 );
576 let services = self.peripheral.services();
577 let service_count = services.len();
578
579 for service in &services {
581 if service.uuid == SAF_TEHNIKA_SERVICE_NEW || service.uuid == SAF_TEHNIKA_SERVICE_OLD {
582 for char in &service.characteristics {
583 if char.uuid == uuid {
584 return Ok(char.clone());
585 }
586 }
587 }
588 }
589
590 for service in &services {
592 if service.uuid == GAP_SERVICE
593 || service.uuid == DEVICE_INFO_SERVICE
594 || service.uuid == BATTERY_SERVICE
595 {
596 for char in &service.characteristics {
597 if char.uuid == uuid {
598 return Ok(char.clone());
599 }
600 }
601 }
602 }
603
604 for service in &services {
606 for char in &service.characteristics {
607 if char.uuid == uuid {
608 return Ok(char.clone());
609 }
610 }
611 }
612
613 Err(Error::characteristic_not_found(
614 uuid.to_string(),
615 service_count,
616 ))
617 }
618
619 pub async fn read_characteristic(&self, uuid: Uuid) -> Result<Vec<u8>> {
624 let characteristic = self.find_characteristic(uuid).await?;
625 let data = timeout(
626 self.config.read_timeout,
627 self.peripheral.read(&characteristic),
628 )
629 .await
630 .map_err(|_| Error::Timeout {
631 operation: format!("read characteristic {}", uuid),
632 duration: self.config.read_timeout,
633 })??;
634 Ok(data)
635 }
636
637 pub async fn read_characteristic_with_timeout(
642 &self,
643 uuid: Uuid,
644 read_timeout: Duration,
645 ) -> Result<Vec<u8>> {
646 let characteristic = self.find_characteristic(uuid).await?;
647 let data = timeout(read_timeout, self.peripheral.read(&characteristic))
648 .await
649 .map_err(|_| Error::Timeout {
650 operation: format!("read characteristic {}", uuid),
651 duration: read_timeout,
652 })??;
653 Ok(data)
654 }
655
656 pub async fn write_characteristic(&self, uuid: Uuid, data: &[u8]) -> Result<()> {
661 let characteristic = self.find_characteristic(uuid).await?;
662 timeout(
663 self.config.write_timeout,
664 self.peripheral
665 .write(&characteristic, data, WriteType::WithResponse),
666 )
667 .await
668 .map_err(|_| Error::Timeout {
669 operation: format!("write characteristic {}", uuid),
670 duration: self.config.write_timeout,
671 })??;
672 Ok(())
673 }
674
675 pub async fn write_characteristic_with_timeout(
677 &self,
678 uuid: Uuid,
679 data: &[u8],
680 write_timeout: Duration,
681 ) -> Result<()> {
682 let characteristic = self.find_characteristic(uuid).await?;
683 timeout(
684 write_timeout,
685 self.peripheral
686 .write(&characteristic, data, WriteType::WithResponse),
687 )
688 .await
689 .map_err(|_| Error::Timeout {
690 operation: format!("write characteristic {}", uuid),
691 duration: write_timeout,
692 })??;
693 Ok(())
694 }
695
696 #[tracing::instrument(level = "debug", skip(self), fields(device_name = ?self.name, device_type = ?self.device_type))]
702 pub async fn read_current(&self) -> Result<CurrentReading> {
703 let data = match self.read_characteristic(CURRENT_READINGS_DETAIL).await {
705 Ok(data) => data,
706 Err(Error::CharacteristicNotFound { .. }) => {
707 debug!("Primary reading characteristic not found, trying alternative");
709 self.read_characteristic(CURRENT_READINGS_DETAIL_ALT)
710 .await?
711 }
712 Err(e) => return Err(e),
713 };
714
715 match self.device_type {
717 Some(DeviceType::Aranet4) | None => {
718 Ok(CurrentReading::from_bytes(&data)?)
720 }
721 Some(DeviceType::Aranet2) => crate::readings::parse_aranet2_reading(&data),
722 Some(DeviceType::AranetRadon) => crate::readings::parse_aranet_radon_gatt(&data),
723 Some(DeviceType::AranetRadiation) => {
724 crate::readings::parse_aranet_radiation_gatt(&data).map(|ext| ext.reading)
726 }
727 Some(_) => Ok(CurrentReading::from_bytes(&data)?),
729 }
730 }
731
732 #[tracing::instrument(level = "debug", skip(self))]
734 pub async fn read_battery(&self) -> Result<u8> {
735 let data = self.read_characteristic(BATTERY_LEVEL).await?;
736 if data.is_empty() {
737 return Err(Error::InvalidData("Empty battery data".to_string()));
738 }
739 Ok(data[0])
740 }
741
742 #[tracing::instrument(level = "debug", skip(self))]
746 pub async fn read_device_info(&self) -> Result<DeviceInfo> {
747 fn read_string(data: Vec<u8>) -> String {
748 String::from_utf8(data)
749 .unwrap_or_default()
750 .trim_end_matches('\0')
751 .to_string()
752 }
753
754 let (
756 name_result,
757 model_result,
758 serial_result,
759 firmware_result,
760 hardware_result,
761 software_result,
762 manufacturer_result,
763 ) = tokio::join!(
764 self.read_characteristic(DEVICE_NAME),
765 self.read_characteristic(MODEL_NUMBER),
766 self.read_characteristic(SERIAL_NUMBER),
767 self.read_characteristic(FIRMWARE_REVISION),
768 self.read_characteristic(HARDWARE_REVISION),
769 self.read_characteristic(SOFTWARE_REVISION),
770 self.read_characteristic(MANUFACTURER_NAME),
771 );
772
773 let name = name_result
774 .map(read_string)
775 .unwrap_or_else(|_| self.name.clone().unwrap_or_default());
776
777 let model = model_result.map(read_string).unwrap_or_default();
778 let serial = serial_result.map(read_string).unwrap_or_default();
779 let firmware = firmware_result.map(read_string).unwrap_or_default();
780 let hardware = hardware_result.map(read_string).unwrap_or_default();
781 let software = software_result.map(read_string).unwrap_or_default();
782 let manufacturer = manufacturer_result.map(read_string).unwrap_or_default();
783
784 Ok(DeviceInfo {
785 name,
786 model,
787 serial,
788 firmware,
789 hardware,
790 software,
791 manufacturer,
792 })
793 }
794
795 #[tracing::instrument(level = "debug", skip(self))]
801 pub async fn read_device_info_essential(&self) -> Result<DeviceInfo> {
802 fn read_string(data: Vec<u8>) -> String {
803 String::from_utf8(data)
804 .unwrap_or_default()
805 .trim_end_matches('\0')
806 .to_string()
807 }
808
809 let (name_result, serial_result, firmware_result) = tokio::join!(
811 self.read_characteristic(DEVICE_NAME),
812 self.read_characteristic(SERIAL_NUMBER),
813 self.read_characteristic(FIRMWARE_REVISION),
814 );
815
816 let name = name_result
817 .map(read_string)
818 .unwrap_or_else(|_| self.name.clone().unwrap_or_default());
819 let serial = serial_result.map(read_string).unwrap_or_default();
820 let firmware = firmware_result.map(read_string).unwrap_or_default();
821
822 Ok(DeviceInfo {
823 name,
824 model: String::new(),
825 serial,
826 firmware,
827 hardware: String::new(),
828 software: String::new(),
829 manufacturer: String::new(),
830 })
831 }
832
833 pub async fn subscribe_to_notifications<F>(&self, uuid: Uuid, callback: F) -> Result<()>
839 where
840 F: Fn(&[u8]) + Send + Sync + 'static,
841 {
842 let characteristic = self.find_characteristic(uuid).await?;
843
844 self.peripheral.subscribe(&characteristic).await?;
845
846 let mut stream = self.peripheral.notifications().await?;
848 let char_uuid = characteristic.uuid;
849
850 let handle = tokio::spawn(async move {
851 use futures::StreamExt;
852 while let Some(notification) = stream.next().await {
853 if notification.uuid == char_uuid {
854 callback(¬ification.value);
855 }
856 }
857 });
858
859 self.notification_handles.lock().await.push(handle);
861
862 Ok(())
863 }
864
865 pub async fn unsubscribe_from_notifications(&self, uuid: Uuid) -> Result<()> {
867 let characteristic = self.find_characteristic(uuid).await?;
868 self.peripheral.unsubscribe(&characteristic).await?;
869 Ok(())
870 }
871
872 pub async fn cached_characteristic_count(&self) -> usize {
876 self.characteristics_cache.read().await.len()
877 }
878}
879
880impl Drop for Device {
893 fn drop(&mut self) {
894 if !self.disconnected.load(Ordering::SeqCst) {
895 self.disconnected.store(true, Ordering::SeqCst);
897
898 warn!(
900 device_name = ?self.name,
901 device_address = %self.address,
902 "Device dropped without calling disconnect() - performing best-effort cleanup. \
903 For reliable cleanup, call device.disconnect().await before dropping."
904 );
905
906 if let Ok(mut handles) = self.notification_handles.try_lock() {
909 for handle in handles.drain(..) {
910 handle.abort();
911 }
912 }
913
914 let peripheral = self.peripheral.clone();
917 let address = self.address.clone();
918
919 if let Ok(handle) = tokio::runtime::Handle::try_current() {
921 handle.spawn(async move {
922 if let Err(e) = peripheral.disconnect().await {
923 debug!(
924 device_address = %address,
925 error = %e,
926 "Best-effort disconnect failed (device may already be disconnected)"
927 );
928 } else {
929 debug!(
930 device_address = %address,
931 "Best-effort disconnect completed"
932 );
933 }
934 });
935 }
936 }
937 }
938}
939
940#[async_trait]
941impl AranetDevice for Device {
942 async fn is_connected(&self) -> bool {
945 Device::is_connected(self).await
946 }
947
948 async fn disconnect(&self) -> Result<()> {
949 Device::disconnect(self).await
950 }
951
952 fn name(&self) -> Option<&str> {
955 Device::name(self)
956 }
957
958 fn address(&self) -> &str {
959 Device::address(self)
960 }
961
962 fn device_type(&self) -> Option<DeviceType> {
963 Device::device_type(self)
964 }
965
966 async fn read_current(&self) -> Result<CurrentReading> {
969 Device::read_current(self).await
970 }
971
972 async fn read_device_info(&self) -> Result<DeviceInfo> {
973 Device::read_device_info(self).await
974 }
975
976 async fn read_rssi(&self) -> Result<i16> {
977 Device::read_rssi(self).await
978 }
979
980 async fn read_battery(&self) -> Result<u8> {
983 Device::read_battery(self).await
984 }
985
986 async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
989 Device::get_history_info(self).await
990 }
991
992 async fn download_history(&self) -> Result<Vec<aranet_types::HistoryRecord>> {
993 Device::download_history(self).await
994 }
995
996 async fn download_history_with_options(
997 &self,
998 options: crate::history::HistoryOptions,
999 ) -> Result<Vec<aranet_types::HistoryRecord>> {
1000 Device::download_history_with_options(self, options).await
1001 }
1002
1003 async fn get_interval(&self) -> Result<crate::settings::MeasurementInterval> {
1006 Device::get_interval(self).await
1007 }
1008
1009 async fn set_interval(&self, interval: crate::settings::MeasurementInterval) -> Result<()> {
1010 Device::set_interval(self, interval).await
1011 }
1012
1013 async fn get_calibration(&self) -> Result<crate::settings::CalibrationData> {
1014 Device::get_calibration(self).await
1015 }
1016}