Skip to main content

feagi_hal/hal/
bluetooth.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bluetooth Low Energy (BLE) Hardware Abstraction Layer
5//!
6//! This module defines the platform-agnostic trait for BLE functionality.
7//! Platform implementations (ESP32, nRF52, STM32WB) must implement this trait
8//! to provide BLE capabilities.
9//!
10//! ## Architecture
11//!
12//! ```text
13//! ┌──────────────────────────────────────────────┐
14//! │ Application (firmware)                       │
15//! └─────────────────┬────────────────────────────┘
16//!                   │ uses
17//! ┌─────────────────▼────────────────────────────┐
18//! │ BluetoothProvider trait (THIS FILE)          │
19//! │ - start_advertising()                        │
20//! │ - is_connected()                             │
21//! │ - send() / receive()                         │
22//! └─────────────────┬────────────────────────────┘
23//!                   │ implements
24//! ┌─────────────────▼────────────────────────────┐
25//! │ Platform Implementation                      │
26//! │ - Esp32Bluetooth (esp-idf BLE)              │
27//! │ - Nrf52Bluetooth (TrouBLE/nrf-softdevice)   │
28//! │ - Stm32wbBluetooth (ST BLE stack)           │
29//! └──────────────────────────────────────────────┘
30//! ```
31//!
32//! ## Usage
33//!
34//! ```rust,no_run
35//! use feagi_hal::hal::BluetoothProvider;
36//! # use feagi_hal::platforms::Esp32Bluetooth;
37//!
38//! // Platform layer provides the implementation
39//! let mut ble: Esp32Bluetooth = /* platform init */;
40//!
41//! // Start advertising
42//! ble.start_advertising("FEAGI-robot").unwrap();
43//!
44//! // Wait for connection
45//! while !ble.is_connected() {
46//!     // Poll or sleep
47//! }
48//!
49//! // Send/receive data
50//! ble.send(b"Hello FEAGI").unwrap();
51//! let mut buf = [0u8; 64];
52//! if let Ok(len) = ble.receive(&mut buf) {
53//!     // Process received data
54//! }
55//! ```
56
57/// Connection status for BLE
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub enum ConnectionStatus {
61    /// Not connected, not advertising
62    Disconnected,
63    /// Advertising, waiting for connection
64    Advertising,
65    /// Connected to a client
66    Connected,
67    /// Error state (e.g., init failed, advertising failed)
68    Error,
69}
70
71/// Bluetooth Low Energy provider trait
72///
73/// This trait must be implemented by each platform to provide BLE capabilities.
74/// The trait is designed to be simple and compatible with both async and sync
75/// implementations.
76///
77/// ## Design Principles
78///
79/// 1. **Minimal API**: Only essential operations
80/// 2. **Error transparency**: Platform errors are exposed
81/// 3. **No callbacks**: Use polling or async/await at the platform level
82/// 4. **Buffer-based I/O**: Caller manages buffers
83///
84/// ## Thread Safety
85///
86/// Implementations do NOT need to be `Send` or `Sync` - embedded BLE
87/// typically runs in a single executor/thread.
88pub trait BluetoothProvider {
89    /// Platform-specific error type
90    type Error: core::fmt::Debug;
91
92    /// Start BLE advertising with the given device name
93    ///
94    /// This should set up the BLE stack (if not already initialized) and
95    /// begin advertising. The device becomes discoverable with the given name.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if:
100    /// - BLE stack initialization fails
101    /// - Device name is invalid (e.g., too long)
102    /// - Advertising setup fails
103    ///
104    /// # Blocking Behavior
105    ///
106    /// This method MAY block until advertising is successfully started.
107    /// On some platforms (e.g., TrouBLE), this might block until a connection
108    /// is made. Check platform documentation for blocking behavior.
109    fn start_advertising(&mut self, device_name: &str) -> Result<(), Self::Error>;
110
111    /// Stop BLE advertising
112    ///
113    /// If already connected, this may also disconnect.
114    fn stop_advertising(&mut self) -> Result<(), Self::Error>;
115
116    /// Check if BLE is currently connected to a client
117    ///
118    /// Returns `true` if a client is connected and data can be exchanged.
119    fn is_connected(&self) -> bool;
120
121    /// Get current connection status
122    ///
123    /// Provides more detail than `is_connected()`.
124    fn connection_status(&self) -> ConnectionStatus;
125
126    /// Send data over BLE to the connected client
127    ///
128    /// On most platforms, this uses the TX characteristic (Notify).
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if:
133    /// - Not connected (`is_connected() == false`)
134    /// - Data is too large for MTU
135    /// - BLE stack error
136    ///
137    /// # Blocking Behavior
138    ///
139    /// This method MAY block until the data is queued for transmission.
140    /// It does NOT wait for acknowledgment from the client.
141    fn send(&mut self, data: &[u8]) -> Result<(), Self::Error>;
142
143    /// Receive data from the connected client
144    ///
145    /// On most platforms, this reads from the RX characteristic (Write).
146    ///
147    /// # Returns
148    ///
149    /// - `Ok(n)` where `n` is the number of bytes written to `buffer`
150    /// - `Err(e)` if no data available or BLE error
151    ///
152    /// # Non-blocking
153    ///
154    /// This method SHOULD NOT block. If no data is available, return
155    /// an error or `Ok(0)`.
156    fn receive(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
157
158    /// Flush any pending transmit data
159    ///
160    /// This is optional and may be a no-op on some platforms.
161    fn flush(&mut self) -> Result<(), Self::Error> {
162        Ok(()) // Default: no-op
163    }
164}
165
166/// Helper trait for platforms that support async BLE operations
167///
168/// This is optional and only used by platforms with async runtimes (embassy, tokio, etc.).
169#[cfg(feature = "async")]
170pub trait AsyncBluetoothProvider: BluetoothProvider {
171    /// Async version of `start_advertising`
172    async fn start_advertising_async(&mut self, device_name: &str) -> Result<(), Self::Error>;
173
174    /// Async version of `send`
175    async fn send_async(&mut self, data: &[u8]) -> Result<(), Self::Error>;
176
177    /// Async version of `receive`
178    async fn receive_async(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    // Mock BLE implementation for testing
186    struct MockBluetooth {
187        connected: bool,
188        send_buffer: heapless::Vec<u8, 256>,
189        receive_buffer: heapless::Vec<u8, 256>,
190    }
191
192    impl MockBluetooth {
193        fn new() -> Self {
194            Self {
195                connected: false,
196                send_buffer: heapless::Vec::new(),
197                receive_buffer: heapless::Vec::new(),
198            }
199        }
200    }
201
202    impl BluetoothProvider for MockBluetooth {
203        type Error = &'static str;
204
205        fn start_advertising(&mut self, _device_name: &str) -> Result<(), Self::Error> {
206            self.connected = false;
207            Ok(())
208        }
209
210        fn stop_advertising(&mut self) -> Result<(), Self::Error> {
211            self.connected = false;
212            Ok(())
213        }
214
215        fn is_connected(&self) -> bool {
216            self.connected
217        }
218
219        fn connection_status(&self) -> ConnectionStatus {
220            if self.connected {
221                ConnectionStatus::Connected
222            } else {
223                ConnectionStatus::Disconnected
224            }
225        }
226
227        fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
228            if !self.connected {
229                return Err("Not connected");
230            }
231            self.send_buffer.clear();
232            for &byte in data {
233                self.send_buffer
234                    .push(byte)
235                    .map_err(|_| "Send buffer full")?;
236            }
237            Ok(())
238        }
239
240        fn receive(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
241            if !self.connected {
242                return Err("Not connected");
243            }
244            let len = self.receive_buffer.len().min(buffer.len());
245            buffer[..len].copy_from_slice(&self.receive_buffer[..len]);
246            self.receive_buffer.clear();
247            Ok(len)
248        }
249    }
250
251    #[test]
252    fn test_mock_bluetooth_advertising() {
253        let mut ble = MockBluetooth::new();
254        assert!(!ble.is_connected());
255        assert!(ble.start_advertising("test").is_ok());
256    }
257
258    #[test]
259    fn test_mock_bluetooth_send_not_connected() {
260        let mut ble = MockBluetooth::new();
261        assert!(ble.send(b"test").is_err());
262    }
263
264    #[test]
265    fn test_mock_bluetooth_send_connected() {
266        let mut ble = MockBluetooth::new();
267        ble.connected = true; // Simulate connection
268        assert!(ble.send(b"test").is_ok());
269        assert_eq!(ble.send_buffer.as_slice(), b"test");
270    }
271
272    #[test]
273    fn test_mock_bluetooth_receive_not_connected() {
274        let mut ble = MockBluetooth::new();
275        let mut buf = [0u8; 16];
276        assert!(ble.receive(&mut buf).is_err());
277    }
278
279    #[test]
280    fn test_mock_bluetooth_receive_connected() {
281        let mut ble = MockBluetooth::new();
282        ble.connected = true;
283        ble.receive_buffer.extend_from_slice(b"data").unwrap();
284
285        let mut buf = [0u8; 16];
286        let len = ble.receive(&mut buf).unwrap();
287        assert_eq!(len, 4);
288        assert_eq!(&buf[..len], b"data");
289    }
290}