1use std::time::Duration;
7
8use async_trait::async_trait;
9use btleplug::api::{Characteristic, Peripheral as _, WriteType};
10use btleplug::platform::{Adapter, Peripheral};
11use tokio::time::timeout;
12use tracing::{debug, info};
13use uuid::Uuid;
14
15use crate::error::{Error, Result};
16use crate::scan::{ScanOptions, find_device};
17use crate::traits::AranetDevice;
18use crate::util::{create_identifier, format_peripheral_id};
19use crate::uuid::{
20 BATTERY_LEVEL, BATTERY_SERVICE, CURRENT_READINGS_DETAIL, CURRENT_READINGS_DETAIL_ALT,
21 DEVICE_INFO_SERVICE, DEVICE_NAME, FIRMWARE_REVISION, GAP_SERVICE, HARDWARE_REVISION,
22 MANUFACTURER_NAME, MODEL_NUMBER, SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD,
23 SERIAL_NUMBER, SOFTWARE_REVISION,
24};
25use aranet_types::{CurrentReading, DeviceInfo, DeviceType};
26
27pub struct Device {
37 #[allow(dead_code)]
43 adapter: Adapter,
44 peripheral: Peripheral,
46 name: Option<String>,
48 address: String,
50 device_type: Option<DeviceType>,
52 services_discovered: bool,
54 notification_handles: tokio::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
56}
57
58impl std::fmt::Debug for Device {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("Device")
64 .field("name", &self.name)
65 .field("address", &self.address)
66 .field("device_type", &self.device_type)
67 .field("services_discovered", &self.services_discovered)
68 .finish_non_exhaustive()
69 }
70}
71
72const READ_TIMEOUT: Duration = Duration::from_secs(10);
74
75const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
77
78impl Device {
79 #[tracing::instrument(level = "info", skip_all, fields(identifier = %identifier))]
94 pub async fn connect(identifier: &str) -> Result<Self> {
95 Self::connect_with_timeout(identifier, Duration::from_secs(15)).await
96 }
97
98 #[tracing::instrument(level = "info", skip_all, fields(identifier = %identifier, timeout_secs = timeout.as_secs()))]
100 pub async fn connect_with_timeout(identifier: &str, timeout: Duration) -> Result<Self> {
101 let options = ScanOptions {
102 duration: timeout,
103 filter_aranet_only: false, };
105
106 let (adapter, peripheral) = match find_device(identifier).await {
108 Ok(result) => result,
109 Err(_) => crate::scan::find_device_with_options(identifier, options).await?,
110 };
111
112 Self::from_peripheral(adapter, peripheral).await
113 }
114
115 #[tracing::instrument(level = "info", skip_all)]
117 pub async fn from_peripheral(adapter: Adapter, peripheral: Peripheral) -> Result<Self> {
118 info!("Connecting to device...");
120 peripheral.connect().await?;
121 info!("Connected!");
122
123 info!("Discovering services...");
125 peripheral.discover_services().await?;
126
127 let services = peripheral.services();
128 debug!("Found {} services", services.len());
129 for service in &services {
130 debug!(" Service: {}", service.uuid);
131 for char in &service.characteristics {
132 debug!(" Characteristic: {}", char.uuid);
133 }
134 }
135
136 let properties = peripheral.properties().await?;
138 let name = properties.as_ref().and_then(|p| p.local_name.clone());
139
140 let address = properties
142 .as_ref()
143 .map(|p| create_identifier(&p.address.to_string(), &peripheral.id()))
144 .unwrap_or_else(|| format_peripheral_id(&peripheral.id()));
145
146 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
148
149 Ok(Self {
150 adapter,
151 peripheral,
152 name,
153 address,
154 device_type,
155 services_discovered: true,
156 notification_handles: tokio::sync::Mutex::new(Vec::new()),
157 })
158 }
159
160 pub async fn is_connected(&self) -> bool {
162 self.peripheral.is_connected().await.unwrap_or(false)
163 }
164
165 #[tracing::instrument(level = "info", skip(self), fields(device_name = ?self.name))]
174 pub async fn disconnect(&self) -> Result<()> {
175 info!("Disconnecting from device...");
176
177 {
179 let mut handles = self.notification_handles.lock().await;
180 for handle in handles.drain(..) {
181 handle.abort();
182 }
183 }
184
185 self.peripheral.disconnect().await?;
186 Ok(())
187 }
188
189 pub fn name(&self) -> Option<&str> {
191 self.name.as_deref()
192 }
193
194 pub fn address(&self) -> &str {
199 &self.address
200 }
201
202 pub fn device_type(&self) -> Option<DeviceType> {
204 self.device_type
205 }
206
207 pub async fn read_rssi(&self) -> Result<i16> {
212 let properties = self.peripheral.properties().await?;
213 properties
214 .and_then(|p| p.rssi)
215 .ok_or_else(|| Error::InvalidData("RSSI not available".to_string()))
216 }
217
218 fn find_characteristic(&self, uuid: Uuid) -> Result<Characteristic> {
220 let services = self.peripheral.services();
221 let service_count = services.len();
222
223 for service in &services {
225 if service.uuid == SAF_TEHNIKA_SERVICE_NEW || service.uuid == SAF_TEHNIKA_SERVICE_OLD {
226 for char in &service.characteristics {
227 if char.uuid == uuid {
228 return Ok(char.clone());
229 }
230 }
231 }
232 }
233
234 for service in &services {
236 if service.uuid == GAP_SERVICE
237 || service.uuid == DEVICE_INFO_SERVICE
238 || service.uuid == BATTERY_SERVICE
239 {
240 for char in &service.characteristics {
241 if char.uuid == uuid {
242 return Ok(char.clone());
243 }
244 }
245 }
246 }
247
248 for service in &services {
250 for char in &service.characteristics {
251 if char.uuid == uuid {
252 return Ok(char.clone());
253 }
254 }
255 }
256
257 Err(Error::characteristic_not_found(
258 uuid.to_string(),
259 service_count,
260 ))
261 }
262
263 pub async fn read_characteristic(&self, uuid: Uuid) -> Result<Vec<u8>> {
268 let characteristic = self.find_characteristic(uuid)?;
269 let data = timeout(READ_TIMEOUT, self.peripheral.read(&characteristic))
270 .await
271 .map_err(|_| Error::Timeout {
272 operation: format!("read characteristic {}", uuid),
273 duration: READ_TIMEOUT,
274 })??;
275 Ok(data)
276 }
277
278 pub async fn write_characteristic(&self, uuid: Uuid, data: &[u8]) -> Result<()> {
283 let characteristic = self.find_characteristic(uuid)?;
284 timeout(
285 WRITE_TIMEOUT,
286 self.peripheral
287 .write(&characteristic, data, WriteType::WithResponse),
288 )
289 .await
290 .map_err(|_| Error::Timeout {
291 operation: format!("write characteristic {}", uuid),
292 duration: WRITE_TIMEOUT,
293 })??;
294 Ok(())
295 }
296
297 #[tracing::instrument(level = "debug", skip(self), fields(device_name = ?self.name, device_type = ?self.device_type))]
303 pub async fn read_current(&self) -> Result<CurrentReading> {
304 let data = match self.read_characteristic(CURRENT_READINGS_DETAIL).await {
306 Ok(data) => data,
307 Err(Error::CharacteristicNotFound { .. }) => {
308 debug!("Primary reading characteristic not found, trying alternative");
310 self.read_characteristic(CURRENT_READINGS_DETAIL_ALT)
311 .await?
312 }
313 Err(e) => return Err(e),
314 };
315
316 match self.device_type {
318 Some(DeviceType::Aranet4) | None => {
319 Ok(CurrentReading::from_bytes(&data)?)
321 }
322 Some(DeviceType::Aranet2) => crate::readings::parse_aranet2_reading(&data),
323 Some(DeviceType::AranetRadon) => crate::readings::parse_aranet_radon_gatt(&data),
324 Some(DeviceType::AranetRadiation) => {
325 crate::readings::parse_aranet_radiation_gatt(&data).map(|ext| ext.reading)
327 }
328 Some(_) => Ok(CurrentReading::from_bytes(&data)?),
330 }
331 }
332
333 #[tracing::instrument(level = "debug", skip(self))]
335 pub async fn read_battery(&self) -> Result<u8> {
336 let data = self.read_characteristic(BATTERY_LEVEL).await?;
337 if data.is_empty() {
338 return Err(Error::InvalidData("Empty battery data".to_string()));
339 }
340 Ok(data[0])
341 }
342
343 #[tracing::instrument(level = "debug", skip(self))]
347 pub async fn read_device_info(&self) -> Result<DeviceInfo> {
348 fn read_string(data: Vec<u8>) -> String {
349 String::from_utf8(data)
350 .unwrap_or_default()
351 .trim_end_matches('\0')
352 .to_string()
353 }
354
355 let (
357 name_result,
358 model_result,
359 serial_result,
360 firmware_result,
361 hardware_result,
362 software_result,
363 manufacturer_result,
364 ) = tokio::join!(
365 self.read_characteristic(DEVICE_NAME),
366 self.read_characteristic(MODEL_NUMBER),
367 self.read_characteristic(SERIAL_NUMBER),
368 self.read_characteristic(FIRMWARE_REVISION),
369 self.read_characteristic(HARDWARE_REVISION),
370 self.read_characteristic(SOFTWARE_REVISION),
371 self.read_characteristic(MANUFACTURER_NAME),
372 );
373
374 let name = name_result
375 .map(read_string)
376 .unwrap_or_else(|_| self.name.clone().unwrap_or_default());
377
378 let model = model_result.map(read_string).unwrap_or_default();
379 let serial = serial_result.map(read_string).unwrap_or_default();
380 let firmware = firmware_result.map(read_string).unwrap_or_default();
381 let hardware = hardware_result.map(read_string).unwrap_or_default();
382 let software = software_result.map(read_string).unwrap_or_default();
383 let manufacturer = manufacturer_result.map(read_string).unwrap_or_default();
384
385 Ok(DeviceInfo {
386 name,
387 model,
388 serial,
389 firmware,
390 hardware,
391 software,
392 manufacturer,
393 })
394 }
395
396 pub async fn subscribe_to_notifications<F>(&self, uuid: Uuid, callback: F) -> Result<()>
402 where
403 F: Fn(&[u8]) + Send + Sync + 'static,
404 {
405 let characteristic = self.find_characteristic(uuid)?;
406
407 self.peripheral.subscribe(&characteristic).await?;
408
409 let mut stream = self.peripheral.notifications().await?;
411 let char_uuid = characteristic.uuid;
412
413 let handle = tokio::spawn(async move {
414 use futures::StreamExt;
415 while let Some(notification) = stream.next().await {
416 if notification.uuid == char_uuid {
417 callback(¬ification.value);
418 }
419 }
420 });
421
422 self.notification_handles.lock().await.push(handle);
424
425 Ok(())
426 }
427
428 pub async fn unsubscribe_from_notifications(&self, uuid: Uuid) -> Result<()> {
430 let characteristic = self.find_characteristic(uuid)?;
431 self.peripheral.unsubscribe(&characteristic).await?;
432 Ok(())
433 }
434}
435
436#[async_trait]
448impl AranetDevice for Device {
449 async fn is_connected(&self) -> bool {
452 Device::is_connected(self).await
453 }
454
455 async fn disconnect(&self) -> Result<()> {
456 Device::disconnect(self).await
457 }
458
459 fn name(&self) -> Option<&str> {
462 Device::name(self)
463 }
464
465 fn address(&self) -> &str {
466 Device::address(self)
467 }
468
469 fn device_type(&self) -> Option<DeviceType> {
470 Device::device_type(self)
471 }
472
473 async fn read_current(&self) -> Result<CurrentReading> {
476 Device::read_current(self).await
477 }
478
479 async fn read_device_info(&self) -> Result<DeviceInfo> {
480 Device::read_device_info(self).await
481 }
482
483 async fn read_rssi(&self) -> Result<i16> {
484 Device::read_rssi(self).await
485 }
486
487 async fn read_battery(&self) -> Result<u8> {
490 Device::read_battery(self).await
491 }
492
493 async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
496 Device::get_history_info(self).await
497 }
498
499 async fn download_history(&self) -> Result<Vec<aranet_types::HistoryRecord>> {
500 Device::download_history(self).await
501 }
502
503 async fn download_history_with_options(
504 &self,
505 options: crate::history::HistoryOptions,
506 ) -> Result<Vec<aranet_types::HistoryRecord>> {
507 Device::download_history_with_options(self, options).await
508 }
509
510 async fn get_interval(&self) -> Result<crate::settings::MeasurementInterval> {
513 Device::get_interval(self).await
514 }
515
516 async fn set_interval(&self, interval: crate::settings::MeasurementInterval) -> Result<()> {
517 Device::set_interval(self, interval).await
518 }
519
520 async fn get_calibration(&self) -> Result<crate::settings::CalibrationData> {
521 Device::get_calibration(self).await
522 }
523}