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}