Skip to content

Commit a7b1da3

Browse files
authored
Start of NimBLE wrapper (#672)
* Start of NimBLE wrapper ESP-IDF ships two bluetooth stacks, one based on Bluedroid and one based on NimBLE. Bluedroid provides a complete "real" Bluetooth stack, while NimBLE provides only BLE but does it with lower resource use. This adds an *incomplete* and *early* safe wrapper for the NimBLE stack, just enough to implement a bare-bones BLE GATT server. The overall structure tries to find a good balance between providing a safe API on top of normal NimBLE routines, without completely diverging from the structure already established by the Bluedroid wrapper. The two APIs are very different, so the new wrapper is built side-by-side, giving this repo `bt.rs` for full bluetooth via Bluedroid and `ble.rs` for light-weight BLE via NimBLE. Structure: ``` src/ ble.rs # top-level entrypoint ble/gap.rs # GAP stuff, wraps ble_gap_XXX and ble_svc_gap_XXX routines ble/gatt.rs # GATT stuff shared between clients (not implemented) and servers ble/gatt/gatts.rs # wraps ble_gatts_XXX ble/mbus.rs # wrappers for NimBLEs buffer pool ``` For GATT servers, NimBLE takes a tree structure of attributes and callback pointers. The proposed API here mirrors that approach, expecting users to allocate and provide a reference to an equivalent Rust structure - note there's quite a bit of dragons there as this ends up being a self-referential structure we give to the NimBLE stack.. I think the way I've structured it now is safe from moves while the ble stack is running but could definintely use some critical eyes. * ble: Address PR feedback blocker B1, support non-ext advertisement * ble: Address PR feedback blocker B2, support bare-bones configuing of security * ble: Address PR feedback S4, missed error handling in id_copy_addr * ble: Fix unsafe val_handle API * ble: Add From<> for BleError -> EspError * fmt * ble: add note about ownership of mbufs in places where we rely on nimble releasing * ble: changelog entry
1 parent d6dd703 commit a7b1da3

8 files changed

Lines changed: 1475 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ remote_component = { name = "espressif/lan87xx", version = "1.*" }
4747
### Added
4848
- Compatibility with ESP-IDF V6.0, and some pre-release 6.0.x.
4949
- Added support for the Generic Ethernet PHY driver: particularly useful on ESP-IDF 6.0+ as it is built-in.
50+
- Added early support for the NimBLE low-resource-use BLE stack, currently only GAP and GATT Server support. See examples/ble_gatt_server.rs
5051

5152
## [0.52.1] - 2026-03-10
5253

examples/ble_gatt_server.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//! Example of a BLE GATT server using the ESP IDF NimBLE bindings.
2+
//!
3+
//! Requires a NimBLE-enabled build.
4+
5+
#![allow(unknown_lints)]
6+
#![allow(unexpected_cfgs)]
7+
8+
#[cfg(all(not(any(esp32s2, esp32p4)), esp_idf_bt_nimble_enabled))]
9+
fn main() -> anyhow::Result<()> {
10+
example::main()
11+
}
12+
13+
#[cfg(not(all(not(any(esp32s2, esp32p4)), esp_idf_bt_nimble_enabled)))]
14+
fn main() -> anyhow::Result<()> {
15+
panic!("This example requires a NimBLE-enabled build (CONFIG_BT_NIMBLE_ENABLED=y) on a chip with a BLE radio");
16+
}
17+
18+
#[cfg(all(not(any(esp32s2, esp32p4)), esp_idf_bt_nimble_enabled))]
19+
mod example {
20+
use core::sync::atomic::{AtomicU16, Ordering};
21+
use std::sync::Mutex;
22+
23+
use esp_idf_svc::ble::gap::{self, BleAdvFields, BleGapEvent};
24+
use esp_idf_svc::ble::gatt::gatts::{
25+
self, BleGattAccess, BleGattCharacteristic, BleGattRegister, BleGattService,
26+
BleGattServices, ConnectionId, GattsSetup,
27+
};
28+
use esp_idf_svc::ble::gatt::BleGattCharFlag;
29+
use esp_idf_svc::ble::{ensure_addr, BleError, BleSetup, BleUuid};
30+
use esp_idf_svc::hal::delay::FreeRtos;
31+
use esp_idf_svc::hal::peripherals::Peripherals;
32+
use esp_idf_svc::log::EspLogger;
33+
34+
use enumset::enum_set;
35+
use log::{info, warn};
36+
37+
const DEVICE_NAME: &str = "esp-nimble";
38+
39+
// Our service UUID
40+
pub const SERVICE_UUID: u128 = 0xad91b201734740479e173bed82d75f9d;
41+
42+
/// Our "recv" characteristic - i.e. where clients can send data.
43+
pub const RECV_CHARACTERISTIC_UUID: u128 = 0xb6fccb5087be44f3ae22f85485ea42c4;
44+
/// Our "indicate" characteristic - i.e. where clients can receive data if they subscribe to it
45+
pub const IND_CHARACTERISTIC_UUID: u128 = 0x503de214868246c4828fd59144da41be;
46+
47+
// Server state. We capture the indicate characteristic's value handle from the
48+
// registration callback (see `on_gatts_register` below); a real server tracking
49+
// several handles would keep a uuid -> handle map instead of a single slot.
50+
static SUBSCRIBERS: Mutex<Vec<ConnectionId>> = Mutex::new(Vec::new());
51+
static IND_VAL_HANDLE: AtomicU16 = AtomicU16::new(0);
52+
53+
pub fn main() -> anyhow::Result<()> {
54+
esp_idf_svc::sys::link_patches();
55+
EspLogger::initialize_default();
56+
57+
let peripherals = Peripherals::take()?;
58+
59+
let services = BleGattServices::new(vec![BleGattService::new(
60+
true,
61+
BleUuid::uuid128(SERVICE_UUID),
62+
vec![
63+
// "recv": clients write here; we just log what arrives.
64+
BleGattCharacteristic::new(
65+
BleUuid::uuid128(RECV_CHARACTERISTIC_UUID),
66+
enum_set!(BleGattCharFlag::Write),
67+
|access| {
68+
if let BleGattAccess::Write { data, .. } = access {
69+
let mut buf = [0u8; 200];
70+
match data.read(&mut buf) {
71+
Ok(n) => info!("recv {n} bytes: {:?}", &buf[..n]),
72+
Err(e) => warn!("recv read failed: {e}"),
73+
}
74+
}
75+
0
76+
},
77+
),
78+
// "indicate": clients subscribe and get the counter pushed from the loop
79+
// below. NimBLE adds the CCCD (0x2902) automatically for this flag, so
80+
// there is no descriptor to declare and no read/write to service here.
81+
BleGattCharacteristic::new(
82+
BleUuid::uuid128(IND_CHARACTERISTIC_UUID),
83+
enum_set!(BleGattCharFlag::Indicate),
84+
|_access| 0,
85+
),
86+
],
87+
)]);
88+
89+
let mut setup = BleSetup::new(peripherals.modem)?;
90+
91+
GattsSetup::new(&mut setup).add_services(&services)?;
92+
93+
// NimBLE assigns attribute handles during registration and reports them here,
94+
// on the host task. We stash the indicate handle so the loop below can push to
95+
// it; matching on the UUID is how we tell our characteristics apart.
96+
setup.on_gatts_register(|event| {
97+
if let BleGattRegister::Characteristic {
98+
uuid, val_handle, ..
99+
} = event
100+
{
101+
if uuid == BleUuid::uuid128(IND_CHARACTERISTIC_UUID) {
102+
IND_VAL_HANDLE.store(val_handle, Ordering::Relaxed);
103+
}
104+
}
105+
});
106+
107+
// We wait until the stack is "in sync" before we can start using it. Note this
108+
// closure needs to handle being called multiple times in case the stack resets.
109+
setup.on_sync(|| match start_advertising() {
110+
Ok(()) => info!("advertising as {DEVICE_NAME:?}"),
111+
Err(e) => warn!("failed to start advertising: {e}"),
112+
});
113+
114+
setup.on_gap_event(|event| {
115+
match event {
116+
BleGapEvent::Connect {
117+
conn_handle,
118+
status,
119+
} => info!("connected (handle {conn_handle}): {status:?}"),
120+
BleGapEvent::Disconnect {
121+
conn_handle,
122+
reason,
123+
} => {
124+
info!("disconnected ({reason}); restarting advertising");
125+
SUBSCRIBERS.lock().unwrap().retain(|&c| c != conn_handle);
126+
if let Err(e) = start_advertising() {
127+
warn!("failed to restart advertising: {e}");
128+
}
129+
}
130+
BleGapEvent::Subscribe {
131+
conn_handle,
132+
attr_handle,
133+
cur_indicate,
134+
..
135+
} => {
136+
if attr_handle == IND_VAL_HANDLE.load(Ordering::Relaxed) {
137+
let mut subs = SUBSCRIBERS.lock().unwrap();
138+
subs.retain(|&c| c != conn_handle);
139+
if cur_indicate {
140+
let _ = subs.push(conn_handle);
141+
}
142+
}
143+
}
144+
_ => {}
145+
}
146+
147+
0
148+
});
149+
150+
let _driver = setup.start()?;
151+
info!("NimBLE host started");
152+
153+
let mut counter: u16 = 0;
154+
loop {
155+
FreeRtos::delay_ms(1000);
156+
157+
// The handle stays 0 until the GATT registration callback sets it to
158+
// whatver val_handle NimBLE assigned.
159+
let ind_handle = IND_VAL_HANDLE.load(Ordering::Relaxed);
160+
if ind_handle == 0 {
161+
continue;
162+
}
163+
164+
counter = counter.wrapping_add(1);
165+
166+
// Copy the subscriber list out so the lock isn't held across `indicate`.
167+
let subs = SUBSCRIBERS.lock().unwrap().clone();
168+
for conn in subs {
169+
if let Err(e) = gatts::indicate(conn, ind_handle, &counter.to_le_bytes()) {
170+
warn!("indicate to {conn} failed: {e}");
171+
}
172+
}
173+
}
174+
}
175+
176+
/// Configure and start a connectable legacy advertisement
177+
/// n.b. NimBLE exposes a mutually-exclusive "extended" advertisement API as well
178+
/// if you set the right build flags
179+
fn start_advertising() -> Result<(), BleError> {
180+
use esp_idf_svc::ble::gap::BleAdvParams;
181+
182+
ensure_addr(false)?;
183+
gap::svc_set_device_name(DEVICE_NAME)?;
184+
185+
let fields = BleAdvFields {
186+
flags: 0x06, // LE General Discoverable, BR/EDR unsupported
187+
name: Some(DEVICE_NAME),
188+
..Default::default()
189+
};
190+
gap::adv_set_fields(&fields)?;
191+
192+
let params = BleAdvParams {
193+
conn_mode: 2, // BLE_GAP_CONN_MODE_UND
194+
disc_mode: 2, // BLE_GAP_DISC_MODE_GEN
195+
itvl_min: 0x30, // 30 ms, in 0.625 ms units
196+
itvl_max: 0x60, // 60 ms
197+
..Default::default()
198+
};
199+
gap::adv_start(0 /* BLE_OWN_ADDR_PUBLIC */, &params)
200+
}
201+
}

0 commit comments

Comments
 (0)