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}