Skip to main content

aranet_core/
guard.rs

1//! Connection guard for automatic disconnect on drop.
2//!
3//! This module provides RAII-style connection management for Aranet devices,
4//! ensuring that connections are properly closed when the guard goes out of scope.
5
6use std::ops::{Deref, DerefMut};
7use std::sync::Arc;
8
9use tokio::runtime::Handle;
10use tracing::warn;
11
12use crate::device::Device;
13
14/// A guard that automatically disconnects from the device when dropped.
15///
16/// This provides RAII-style management of BLE connections. When the guard
17/// is dropped, it will attempt to disconnect from the device.
18///
19/// # Example
20///
21/// ```ignore
22/// use aranet_core::{Device, DeviceGuard};
23///
24/// async fn read_with_guard() -> Result<(), Box<dyn std::error::Error>> {
25///     let device = Device::connect("Aranet4 12345").await?;
26///     let guard = DeviceGuard::new(device);
27///
28///     // Use the device through the guard
29///     let reading = guard.read_current().await?;
30///     println!("CO2: {}", reading.co2);
31///
32///     // Device is automatically disconnected when guard goes out of scope
33///     Ok(())
34/// }
35/// ```
36pub struct DeviceGuard {
37    device: Option<Device>,
38}
39
40impl DeviceGuard {
41    /// Create a new device guard.
42    pub fn new(device: Device) -> Self {
43        Self {
44            device: Some(device),
45        }
46    }
47
48    /// Take ownership of the device, preventing automatic disconnect.
49    ///
50    /// After calling this, you are responsible for disconnecting the device.
51    /// This consumes the guard, so the device cannot be "already taken" —
52    /// the `Option` is only `None` during `Drop`.
53    pub fn into_inner(mut self) -> Device {
54        // SAFETY: `device` is always `Some` except inside `Drop`.
55        // Since `into_inner` takes `self` by value, Drop hasn't run yet.
56        self.device
57            .take()
58            .expect("DeviceGuard invariant violated: device is None outside of Drop")
59    }
60
61    /// Get a reference to the device.
62    fn device(&self) -> &Device {
63        // SAFETY: Same invariant as above — always `Some` outside of `Drop`.
64        self.device
65            .as_ref()
66            .expect("DeviceGuard invariant violated: device is None outside of Drop")
67    }
68
69    /// Get a mutable reference to the device.
70    fn device_mut(&mut self) -> &mut Device {
71        self.device
72            .as_mut()
73            .expect("DeviceGuard invariant violated: device is None outside of Drop")
74    }
75}
76
77impl Deref for DeviceGuard {
78    type Target = Device;
79
80    fn deref(&self) -> &Self::Target {
81        self.device()
82    }
83}
84
85impl DerefMut for DeviceGuard {
86    fn deref_mut(&mut self) -> &mut Self::Target {
87        self.device_mut()
88    }
89}
90
91impl Drop for DeviceGuard {
92    fn drop(&mut self) {
93        if let Some(device) = self.device.take() {
94            // Try to get a runtime handle to perform async disconnect
95            if let Ok(handle) = Handle::try_current() {
96                handle.spawn(async move {
97                    if let Err(e) = device.disconnect().await {
98                        warn!("Failed to disconnect device in guard drop: {}", e);
99                    }
100                });
101            } else {
102                // No runtime available, log warning
103                warn!("No tokio runtime available for device disconnect in guard drop");
104            }
105        }
106    }
107}
108
109/// A guard for Arc-wrapped devices.
110///
111/// Similar to `DeviceGuard` but for shared device references.
112pub struct SharedDeviceGuard {
113    device: Arc<Device>,
114}
115
116impl SharedDeviceGuard {
117    /// Create a new shared device guard.
118    pub fn new(device: Arc<Device>) -> Self {
119        Self { device }
120    }
121
122    /// Consume the guard and return the underlying Arc.
123    ///
124    /// After calling this, the device will NOT be automatically disconnected
125    /// when the returned Arc is dropped. You are responsible for managing
126    /// the device lifecycle.
127    pub fn into_inner(self) -> Arc<Device> {
128        // Use ManuallyDrop to prevent Drop from running
129        let guard = std::mem::ManuallyDrop::new(self);
130        Arc::clone(&guard.device)
131    }
132}
133
134impl Deref for SharedDeviceGuard {
135    type Target = Device;
136
137    fn deref(&self) -> &Self::Target {
138        &self.device
139    }
140}
141
142impl Drop for SharedDeviceGuard {
143    fn drop(&mut self) {
144        let device = Arc::clone(&self.device);
145        if let Ok(handle) = Handle::try_current() {
146            handle.spawn(async move {
147                if let Err(e) = device.disconnect().await {
148                    warn!("Failed to disconnect shared device in guard drop: {}", e);
149                }
150            });
151        }
152    }
153}