hive_btle/platform/linux/
connection.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! BlueZ connection wrapper
17
18use bluer::Device;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21use tokio::sync::RwLock;
22
23use crate::config::BlePhy;
24use crate::error::{BleError, Result};
25use crate::transport::BleConnection;
26use crate::NodeId;
27
28/// Internal connection state
29struct ConnectionState {
30    /// Whether the connection is alive
31    alive: bool,
32    /// Negotiated MTU
33    mtu: u16,
34    /// Current PHY
35    phy: BlePhy,
36    /// Last RSSI reading
37    rssi: Option<i8>,
38}
39
40/// BlueZ connection wrapper
41///
42/// Wraps a `bluer::Device` with connection state tracking.
43#[derive(Clone)]
44pub struct BluerConnection {
45    /// Remote peer ID
46    peer_id: NodeId,
47    /// BlueZ device handle
48    device: Device,
49    /// Connection state
50    state: Arc<RwLock<ConnectionState>>,
51    /// When the connection was established
52    connected_at: Instant,
53}
54
55impl BluerConnection {
56    /// Create a new connection wrapper
57    pub(crate) async fn new(peer_id: NodeId, device: Device) -> Result<Self> {
58        // Get initial MTU
59        // BlueZ doesn't expose MTU directly, use default
60        let mtu = 23; // Will be updated after MTU exchange
61
62        let state = ConnectionState {
63            alive: true,
64            mtu,
65            phy: BlePhy::Le1M, // Default PHY
66            rssi: None,
67        };
68
69        let conn = Self {
70            peer_id,
71            device,
72            state: Arc::new(RwLock::new(state)),
73            connected_at: Instant::now(),
74        };
75
76        // Try to get initial RSSI
77        conn.update_rssi().await;
78
79        Ok(conn)
80    }
81
82    /// Get the underlying BlueZ device
83    pub fn device(&self) -> &Device {
84        &self.device
85    }
86
87    /// Update RSSI from device
88    pub async fn update_rssi(&self) {
89        if let Ok(Some(rssi)) = self.device.rssi().await {
90            let mut state = self.state.write().await;
91            state.rssi = Some(rssi as i8);
92        }
93    }
94
95    /// Update MTU
96    pub async fn set_mtu(&self, mtu: u16) {
97        let mut state = self.state.write().await;
98        state.mtu = mtu;
99    }
100
101    /// Update PHY
102    pub async fn set_phy(&self, phy: BlePhy) {
103        let mut state = self.state.write().await;
104        state.phy = phy;
105    }
106
107    /// Mark connection as dead
108    pub async fn mark_dead(&self) {
109        let mut state = self.state.write().await;
110        state.alive = false;
111    }
112
113    /// Disconnect from the device
114    pub async fn disconnect(&self) -> Result<()> {
115        self.device
116            .disconnect()
117            .await
118            .map_err(|e| BleError::ConnectionFailed(format!("Failed to disconnect: {}", e)))?;
119        self.mark_dead().await;
120        Ok(())
121    }
122
123    /// Discover GATT services
124    pub async fn discover_services(&self) -> Result<()> {
125        // Trigger service discovery
126        // In bluer, services are discovered automatically on connect
127        // but we can force a refresh
128        let _ = self.device.services().await;
129        Ok(())
130    }
131
132    /// Get GATT services
133    pub async fn services(&self) -> Result<Vec<bluer::gatt::remote::Service>> {
134        self.device
135            .services()
136            .await
137            .map_err(|e| BleError::GattError(format!("Failed to get services: {}", e)))
138    }
139
140    /// Find a service by UUID
141    pub async fn find_service(
142        &self,
143        uuid: uuid::Uuid,
144    ) -> Result<Option<bluer::gatt::remote::Service>> {
145        let services = self.services().await?;
146        for service in services {
147            if service.uuid().await.ok() == Some(uuid) {
148                return Ok(Some(service));
149            }
150        }
151        Ok(None)
152    }
153
154    /// Read a characteristic value
155    pub async fn read_characteristic(
156        &self,
157        service_uuid: uuid::Uuid,
158        char_uuid: uuid::Uuid,
159    ) -> Result<Vec<u8>> {
160        let service = self
161            .find_service(service_uuid)
162            .await?
163            .ok_or_else(|| BleError::ServiceNotFound(service_uuid.to_string()))?;
164
165        let characteristics = service
166            .characteristics()
167            .await
168            .map_err(|e| BleError::GattError(format!("Failed to get characteristics: {}", e)))?;
169
170        for char in characteristics {
171            if char.uuid().await.ok() == Some(char_uuid) {
172                return char.read().await.map_err(|e| {
173                    BleError::GattError(format!("Failed to read characteristic: {}", e))
174                });
175            }
176        }
177
178        Err(BleError::CharacteristicNotFound(char_uuid.to_string()))
179    }
180
181    /// Write a characteristic value
182    pub async fn write_characteristic(
183        &self,
184        service_uuid: uuid::Uuid,
185        char_uuid: uuid::Uuid,
186        value: &[u8],
187    ) -> Result<()> {
188        let service = self
189            .find_service(service_uuid)
190            .await?
191            .ok_or_else(|| BleError::ServiceNotFound(service_uuid.to_string()))?;
192
193        let characteristics = service
194            .characteristics()
195            .await
196            .map_err(|e| BleError::GattError(format!("Failed to get characteristics: {}", e)))?;
197
198        for char in characteristics {
199            if char.uuid().await.ok() == Some(char_uuid) {
200                return char.write(value).await.map_err(|e| {
201                    BleError::GattError(format!("Failed to write characteristic: {}", e))
202                });
203            }
204        }
205
206        Err(BleError::CharacteristicNotFound(char_uuid.to_string()))
207    }
208
209    /// Subscribe to characteristic notifications
210    pub async fn subscribe_characteristic(
211        &self,
212        service_uuid: uuid::Uuid,
213        char_uuid: uuid::Uuid,
214    ) -> Result<impl tokio_stream::Stream<Item = Vec<u8>>> {
215        let service = self
216            .find_service(service_uuid)
217            .await?
218            .ok_or_else(|| BleError::ServiceNotFound(service_uuid.to_string()))?;
219
220        let characteristics = service
221            .characteristics()
222            .await
223            .map_err(|e| BleError::GattError(format!("Failed to get characteristics: {}", e)))?;
224
225        for char in characteristics {
226            if char.uuid().await.ok() == Some(char_uuid) {
227                return char.notify().await.map_err(|e| {
228                    BleError::GattError(format!("Failed to subscribe to notifications: {}", e))
229                });
230            }
231        }
232
233        Err(BleError::CharacteristicNotFound(char_uuid.to_string()))
234    }
235}
236
237impl BleConnection for BluerConnection {
238    fn peer_id(&self) -> &NodeId {
239        &self.peer_id
240    }
241
242    fn is_alive(&self) -> bool {
243        // Try to read state without blocking
244        if let Ok(state) = self.state.try_read() {
245            state.alive
246        } else {
247            // If we can't get the lock, assume alive
248            true
249        }
250    }
251
252    fn mtu(&self) -> u16 {
253        if let Ok(state) = self.state.try_read() {
254            state.mtu
255        } else {
256            23 // Default BLE MTU
257        }
258    }
259
260    fn phy(&self) -> BlePhy {
261        if let Ok(state) = self.state.try_read() {
262            state.phy
263        } else {
264            BlePhy::Le1M
265        }
266    }
267
268    fn rssi(&self) -> Option<i8> {
269        if let Ok(state) = self.state.try_read() {
270            state.rssi
271        } else {
272            None
273        }
274    }
275
276    fn connected_duration(&self) -> Duration {
277        self.connected_at.elapsed()
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    // Integration tests require actual Bluetooth hardware
284    // and a connected device
285}