peat_btle/platform/mod.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//! Platform abstraction layer for BLE
17//!
18//! This module defines the traits that platform-specific implementations
19//! must implement to provide BLE functionality.
20//!
21//! ## Supported Platforms
22//!
23//! - **Linux**: BlueZ via D-Bus (`bluer` crate)
24//! - **Android**: JNI to Android Bluetooth APIs
25//! - **macOS/iOS**: CoreBluetooth
26//! - **Windows**: WinRT Bluetooth APIs
27//! - **Embedded**: ESP-IDF NimBLE
28//!
29//! ## Architecture
30//!
31//! Each platform provides an implementation of `BleAdapter` that handles:
32//! - Adapter initialization and power management
33//! - Discovery (scanning and advertising)
34//! - GATT server and client operations
35//! - Connection management
36
37#[cfg(not(feature = "std"))]
38use alloc::{boxed::Box, format, string::String, string::ToString, vec::Vec};
39
40use async_trait::async_trait;
41
42use crate::config::{BleConfig, BlePhy, DiscoveryConfig};
43use crate::error::Result;
44use crate::transport::BleConnection;
45use crate::NodeId;
46
47// Platform-specific modules (conditionally compiled)
48#[cfg(all(feature = "linux", target_os = "linux"))]
49pub mod linux;
50
51#[cfg(feature = "android")]
52pub mod android;
53
54#[cfg(any(feature = "macos", feature = "ios"))]
55pub mod apple;
56
57#[cfg(feature = "windows")]
58pub mod windows;
59
60#[cfg(feature = "embedded")]
61pub mod embedded;
62
63#[cfg(feature = "esp32")]
64pub mod esp32;
65
66// Mock adapter for testing (always available in std builds)
67#[cfg(feature = "std")]
68pub mod mock;
69
70/// Discovered BLE device
71#[derive(Debug, Clone)]
72pub struct DiscoveredDevice {
73 /// Device address (MAC or platform-specific)
74 pub address: String,
75 /// Device name (if available)
76 pub name: Option<String>,
77 /// RSSI in dBm
78 pub rssi: i8,
79 /// Is this a Peat node?
80 pub is_peat_node: bool,
81 /// Parsed Peat node ID (if Peat node)
82 pub node_id: Option<NodeId>,
83 /// Raw advertising data
84 pub adv_data: Vec<u8>,
85}
86
87/// Callback for discovered devices
88pub type DiscoveryCallback = std::sync::Arc<dyn Fn(DiscoveredDevice) + Send + Sync>;
89
90/// Callback for connection events
91pub type ConnectionCallback = std::sync::Arc<dyn Fn(NodeId, ConnectionEvent) + Send + Sync>;
92
93/// Connection event types
94#[derive(Debug, Clone)]
95pub enum ConnectionEvent {
96 /// Connection established
97 Connected {
98 /// Negotiated MTU
99 mtu: u16,
100 /// Connection PHY
101 phy: BlePhy,
102 },
103 /// Connection lost
104 Disconnected {
105 /// Reason for disconnection
106 reason: DisconnectReason,
107 },
108 /// GATT services discovered
109 ServicesDiscovered {
110 /// Whether the Peat service was found
111 has_peat_service: bool,
112 },
113 /// Data received from peer (characteristic read or notification)
114 DataReceived {
115 /// The received data
116 data: Vec<u8>,
117 },
118 /// MTU changed
119 MtuChanged {
120 /// New MTU value
121 mtu: u16,
122 },
123 /// PHY changed
124 PhyChanged {
125 /// New PHY
126 phy: BlePhy,
127 },
128 /// RSSI updated
129 RssiUpdated {
130 /// New RSSI value in dBm
131 rssi: i8,
132 },
133}
134
135/// Reason for disconnection
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum DisconnectReason {
138 /// Disconnected by local request
139 LocalRequest,
140 /// Disconnected by remote device
141 RemoteRequest,
142 /// Connection timeout
143 Timeout,
144 /// Link loss (device out of range)
145 LinkLoss,
146 /// Connection failed
147 ConnectionFailed,
148 /// Unknown reason
149 Unknown,
150}
151
152/// Platform-specific BLE adapter
153///
154/// This is the main abstraction trait that each platform must implement.
155/// It provides all BLE functionality needed by the transport layer.
156#[async_trait]
157pub trait BleAdapter: Send + Sync {
158 /// Initialize the adapter with the given configuration
159 async fn init(&mut self, config: &BleConfig) -> Result<()>;
160
161 /// Start the adapter (begin advertising and/or scanning)
162 async fn start(&self) -> Result<()>;
163
164 /// Stop the adapter
165 async fn stop(&self) -> Result<()>;
166
167 /// Check if the adapter is powered on
168 fn is_powered(&self) -> bool;
169
170 /// Get the adapter's Bluetooth address
171 fn address(&self) -> Option<String>;
172
173 // === Discovery ===
174
175 /// Start scanning for devices
176 async fn start_scan(&self, config: &DiscoveryConfig) -> Result<()>;
177
178 /// Stop scanning
179 async fn stop_scan(&self) -> Result<()>;
180
181 /// Start advertising
182 async fn start_advertising(&self, config: &DiscoveryConfig) -> Result<()>;
183
184 /// Stop advertising
185 async fn stop_advertising(&self) -> Result<()>;
186
187 /// Set callback for discovered devices
188 fn set_discovery_callback(&mut self, callback: Option<DiscoveryCallback>);
189
190 // === Connections ===
191
192 /// Connect to a peer by node ID
193 async fn connect(&self, peer_id: &NodeId) -> Result<Box<dyn BleConnection>>;
194
195 /// Disconnect from a peer
196 async fn disconnect(&self, peer_id: &NodeId) -> Result<()>;
197
198 /// Get an existing connection
199 fn get_connection(&self, peer_id: &NodeId) -> Option<Box<dyn BleConnection>>;
200
201 /// Get the number of connected peers
202 fn peer_count(&self) -> usize;
203
204 /// Get list of connected peer IDs
205 fn connected_peers(&self) -> Vec<NodeId>;
206
207 /// Set callback for connection events
208 fn set_connection_callback(&mut self, callback: Option<ConnectionCallback>);
209
210 /// Minimal per-peer link info for ADR-032 §Amendment A consumers.
211 ///
212 /// Returns `None` if this adapter has no record of the peer
213 /// (never seen, no connection history, or the adapter doesn't
214 /// track per-peer state). A peer with a known-disconnected state
215 /// record should still return `Some` with `state` set
216 /// accordingly — the visualization layer treats "disconnected
217 /// but known" as meaningful, not absent.
218 ///
219 /// Default impl returns `None`. Adapters that already track per-
220 /// peer state (advertisement RSSI, GATT lifecycle) should
221 /// override to surface that data so peat-mesh's
222 /// `PeatBleTransport::peer_link_state` can synthesise a
223 /// `LinkState` for the visualization layer.
224 fn peer_link_info(&self, _peer_id: &NodeId) -> Option<crate::peer::BlePeerLinkInfo> {
225 None
226 }
227
228 // === GATT ===
229
230 /// Register the Peat GATT service
231 async fn register_gatt_service(&self) -> Result<()>;
232
233 /// Unregister the Peat GATT service
234 async fn unregister_gatt_service(&self) -> Result<()>;
235
236 // === Data ===
237
238 /// Write data to a peer's GATT characteristic
239 ///
240 /// This is the low-level send primitive used by `MeshTransport::send_to()`
241 /// to transmit data fragments over BLE.
242 ///
243 /// # Arguments
244 /// * `peer_id` - Target peer node ID
245 /// * `char_uuid` - GATT characteristic UUID to write to
246 /// * `data` - Bytes to write (must fit within negotiated MTU)
247 async fn write_to_peer(
248 &self,
249 peer_id: &NodeId,
250 char_uuid: uuid::Uuid,
251 data: &[u8],
252 ) -> Result<()> {
253 let _ = (peer_id, char_uuid, data);
254 Err(crate::error::BleError::NotSupported(
255 "write_to_peer not implemented for this adapter".into(),
256 ))
257 }
258
259 // === Capabilities ===
260
261 /// Check if Coded PHY is supported
262 fn supports_coded_phy(&self) -> bool;
263
264 /// Check if extended advertising is supported
265 fn supports_extended_advertising(&self) -> bool;
266
267 /// Get maximum supported MTU
268 fn max_mtu(&self) -> u16;
269
270 /// Get maximum number of connections
271 fn max_connections(&self) -> u8;
272}
273
274/// Stub adapter for testing and platforms without BLE
275#[derive(Debug, Default)]
276pub struct StubAdapter {
277 powered: bool,
278}
279
280#[async_trait]
281impl BleAdapter for StubAdapter {
282 async fn init(&mut self, _config: &BleConfig) -> Result<()> {
283 self.powered = true;
284 Ok(())
285 }
286
287 async fn start(&self) -> Result<()> {
288 Ok(())
289 }
290
291 async fn stop(&self) -> Result<()> {
292 Ok(())
293 }
294
295 fn is_powered(&self) -> bool {
296 self.powered
297 }
298
299 fn address(&self) -> Option<String> {
300 Some("00:00:00:00:00:00".to_string())
301 }
302
303 async fn start_scan(&self, _config: &DiscoveryConfig) -> Result<()> {
304 Ok(())
305 }
306
307 async fn stop_scan(&self) -> Result<()> {
308 Ok(())
309 }
310
311 async fn start_advertising(&self, _config: &DiscoveryConfig) -> Result<()> {
312 Ok(())
313 }
314
315 async fn stop_advertising(&self) -> Result<()> {
316 Ok(())
317 }
318
319 fn set_discovery_callback(&mut self, _callback: Option<DiscoveryCallback>) {}
320
321 async fn connect(&self, peer_id: &NodeId) -> Result<Box<dyn BleConnection>> {
322 Err(crate::error::BleError::NotSupported(format!(
323 "Stub adapter cannot connect to {}",
324 peer_id
325 )))
326 }
327
328 async fn disconnect(&self, _peer_id: &NodeId) -> Result<()> {
329 Ok(())
330 }
331
332 fn get_connection(&self, _peer_id: &NodeId) -> Option<Box<dyn BleConnection>> {
333 None
334 }
335
336 fn peer_count(&self) -> usize {
337 0
338 }
339
340 fn connected_peers(&self) -> Vec<NodeId> {
341 Vec::new()
342 }
343
344 fn set_connection_callback(&mut self, _callback: Option<ConnectionCallback>) {}
345
346 async fn register_gatt_service(&self) -> Result<()> {
347 Ok(())
348 }
349
350 async fn unregister_gatt_service(&self) -> Result<()> {
351 Ok(())
352 }
353
354 fn supports_coded_phy(&self) -> bool {
355 false
356 }
357
358 fn supports_extended_advertising(&self) -> bool {
359 false
360 }
361
362 fn max_mtu(&self) -> u16 {
363 23
364 }
365
366 fn max_connections(&self) -> u8 {
367 0
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[tokio::test]
376 async fn test_stub_adapter() {
377 let mut adapter = StubAdapter::default();
378 assert!(!adapter.is_powered());
379
380 adapter.init(&BleConfig::default()).await.unwrap();
381 assert!(adapter.is_powered());
382 assert_eq!(adapter.peer_count(), 0);
383 assert!(!adapter.supports_coded_phy());
384 }
385}