Skip to main content

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}