Skip to main content

esp_idf_svc/
ble.rs

1//! Safe wrapper for the ESP-IDF NimBLE BLE host.
2
3use core::cell::UnsafeCell;
4use core::ffi::{c_int, c_void};
5use core::fmt;
6use core::marker::PhantomData;
7use core::sync::atomic::{AtomicBool, Ordering};
8
9use alloc::boxed::Box;
10use alloc::sync::Arc;
11
12use crate::hal::modem::BluetoothModemPeripheral;
13use crate::private::mutex::Mutex;
14use crate::sys::*;
15
16pub mod gap;
17pub mod gatt;
18#[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
19pub mod l2cap;
20#[cfg(any(
21    esp_idf_bt_nimble_gatt_server,
22    esp_idf_bt_nimble_gatt_client,
23    not(esp_idf_bt_nimble_l2cap_coc_max_num = "0")
24))]
25pub mod mbuf;
26
27/// A connection handle (NimBLE's `conn_handle`). A *connection* is a cross-cutting concept — born
28/// at the GAP layer and used by the GATT server, the GATT client, and L2CAP — so it lives at the
29/// root rather than in any one subsystem module.
30pub type ConnHandle = u16;
31
32/// The placeholder connection handle NimBLE uses for accesses that did not originate from a peer
33/// (`BLE_HS_CONN_HANDLE_NONE`) — e.g. the local read that fetches a characteristic value when a
34/// notification is sent without an explicit payload. Attribute permissions are not checked for
35/// those.
36pub const CONN_HANDLE_NONE: ConnHandle = BLE_HS_CONN_HANDLE_NONE as ConnHandle;
37
38/// A BLE UUID, either 16-bit (assigned) or 128-bit (vendor-specific).
39#[derive(Clone, Copy, Debug)]
40pub enum BleUuid {
41    Uuid16(ble_uuid16_t),
42    Uuid128(ble_uuid128_t),
43}
44
45impl BleUuid {
46    pub const fn uuid16(uuid: u16) -> Self {
47        Self::Uuid16(ble_uuid16_t {
48            u: ble_uuid_t {
49                type_: BLE_UUID_TYPE_16 as u8,
50            },
51            value: uuid,
52        })
53    }
54
55    pub const fn uuid128(uuid: u128) -> Self {
56        Self::Uuid128(ble_uuid128_t {
57            u: ble_uuid_t {
58                type_: BLE_UUID_TYPE_128 as u8,
59            },
60            value: uuid.to_le_bytes(),
61        })
62    }
63
64    pub const fn as_ptr(&self) -> *const ble_uuid_t {
65        match self {
66            Self::Uuid16(uuid) => &uuid.u as *const ble_uuid_t,
67            Self::Uuid128(uuid) => &uuid.u as *const ble_uuid_t,
68        }
69    }
70
71    /// # Safety
72    ///
73    /// `uuid` must point to a valid `ble_uuid_t` header and the concrete
74    /// 16-/128-bit body it introduces.
75    pub(crate) unsafe fn from_raw(uuid: *const ble_uuid_t) -> Self {
76        match unsafe { (*uuid).type_ } as u32 {
77            BLE_UUID_TYPE_128 => Self::Uuid128(unsafe { *uuid.cast::<ble_uuid128_t>() }),
78            // Only 16- and 128-bit UUIDs are modelled; anything else reads as 16-bit.
79            _ => Self::Uuid16(unsafe { *uuid.cast::<ble_uuid16_t>() }),
80        }
81    }
82}
83
84impl PartialEq for BleUuid {
85    fn eq(&self, other: &Self) -> bool {
86        unsafe { ble_uuid_cmp(self.as_ptr(), other.as_ptr()) == 0 }
87    }
88}
89
90impl Eq for BleUuid {}
91
92#[derive(Clone, Copy)]
93#[repr(transparent)]
94pub struct BleAddr(ble_addr_t);
95
96impl BleAddr {
97    pub const fn new(kind: u8, val: [u8; 6]) -> Self {
98        Self(ble_addr_t { type_: kind, val })
99    }
100
101    pub const fn raw(&self) -> &ble_addr_t {
102        &self.0
103    }
104
105    pub const fn kind(&self) -> u8 {
106        self.0.type_
107    }
108
109    pub const fn val(&self) -> [u8; 6] {
110        self.0.val
111    }
112}
113
114impl fmt::Display for BleAddr {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        let v = &self.0.val;
117        write!(
118            f,
119            "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
120            v[5], v[4], v[3], v[2], v[1], v[0]
121        )
122    }
123}
124
125impl From<ble_addr_t> for BleAddr {
126    fn from(addr: ble_addr_t) -> Self {
127        Self(addr)
128    }
129}
130
131impl From<BleAddr> for ble_addr_t {
132    fn from(addr: BleAddr) -> Self {
133        addr.0
134    }
135}
136
137/// Attempt to configure at least one BLE address; how this is done is hardware-specific.
138/// If prefer_random is true, prefer using a random address even if a public address is configured.
139pub fn ensure_addr(prefer_random: bool) -> Result<(), BleError> {
140    BleError::from_raw(unsafe { ble_hs_util_ensure_addr(prefer_random as c_int) })
141}
142
143/// Read back the device's identity address of the given type.
144pub fn id_copy_addr(kind: u8) -> Result<BleAddr, BleError> {
145    let mut val = [0u8; 6];
146    BleError::from_raw(unsafe {
147        ble_hs_id_copy_addr(kind, val.as_mut_ptr(), core::ptr::null_mut())
148    })?;
149
150    Ok(BleAddr::new(kind, val))
151}
152
153#[derive(Clone, Copy, PartialEq, Eq)]
154#[repr(transparent)]
155pub struct BleError(c_int);
156
157impl BleError {
158    pub const fn new(rc: c_int) -> Self {
159        Self(rc)
160    }
161
162    pub const fn code(&self) -> c_int {
163        self.0
164    }
165
166    pub fn from_raw(rc: c_int) -> Result<(), Self> {
167        if rc == 0 {
168            Ok(())
169        } else {
170            Err(Self(rc))
171        }
172    }
173
174    fn name(&self) -> &'static str {
175        match self.0 as u32 {
176            BLE_HS_EALREADY => "BLE_HS_EALREADY",
177            BLE_HS_EDONE => "BLE_HS_EDONE",
178            BLE_HS_ENOMEM => "BLE_HS_ENOMEM",
179            BLE_HS_ETIMEOUT => "BLE_HS_ETIMEOUT",
180            _ => "BLE_HS_E*",
181        }
182    }
183}
184
185impl fmt::Debug for BleError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(f, "BleError({}, {})", self.0, self.name())
188    }
189}
190
191impl fmt::Display for BleError {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "NimBLE error {} ({})", self.0, self.name())
194    }
195}
196
197#[cfg(feature = "std")]
198impl std::error::Error for BleError {}
199
200impl From<BleError> for EspError {
201    /// NimBLE host codes (`BLE_HS_E*`) are a separate namespace from `esp_err_t`
202    /// with no faithful mapping, so any [`BleError`] collapses to `ESP_FAIL`. Match
203    /// on the [`BleError`] directly if you need the specific NimBLE code.
204    fn from(_err: BleError) -> Self {
205        EspError::from_infallible::<ESP_FAIL>()
206    }
207}
208
209/// Security Manager (SMP) configuration, applied via
210/// [`BleDriver::set_security`](BleDriver::set_security) before the host starts.
211#[derive(Clone, Copy)]
212pub struct BleSecurity {
213    /// Local IO capabilities (`BLE_HS_IO_*`).
214    pub io_cap: u8,
215    pub oob_data_flag: bool,
216    pub bonding: bool,
217    pub mitm: bool,
218    /// LE Secure Connections.
219    pub secure_connections: bool,
220    /// Restrict pairing to LE Secure Connections only.
221    pub secure_connections_only: bool,
222    pub keypress: bool,
223    /// Minimum GATT security level (`sm_sec_lvl`); 0 is ignored.
224    pub min_sec_level: u8,
225    /// Keys we distribute (`BLE_SM_PAIR_KEY_DIST_*` mask).
226    pub our_key_dist: u8,
227    /// Keys the peer distributes (`BLE_SM_PAIR_KEY_DIST_*` mask).
228    pub their_key_dist: u8,
229}
230
231impl BleSecurity {
232    pub const fn new() -> Self {
233        Self {
234            io_cap: BLE_HS_IO_NO_INPUT_OUTPUT as u8,
235            oob_data_flag: false,
236            bonding: false,
237            mitm: false,
238            secure_connections: false,
239            secure_connections_only: false,
240            keypress: false,
241            min_sec_level: 0,
242            our_key_dist: 0,
243            their_key_dist: 0,
244        }
245    }
246}
247
248impl Default for BleSecurity {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// Host-lifecycle events, delivered to the [`host_subscribe`](BleDriver::host_subscribe) hook.
255pub enum HostEvent {
256    /// The host has synchronized with the controller and is ready for BLE operations. Re-entrant:
257    /// fires again after a [`Reset`](Self::Reset).
258    Sync,
259    /// The host reset (e.g. a fatal controller error), carrying the reason code. A
260    /// [`Sync`](Self::Sync) follows once the stack re-synchronizes.
261    Reset { reason: i32 },
262}
263
264#[allow(dead_code)]
265#[allow(clippy::type_complexity)]
266pub(crate) struct BleCallback<A, R> {
267    callback: Mutex<Option<Arc<UnsafeCell<Box<dyn FnMut(A) -> R>>>>>,
268    default_result: R,
269}
270
271#[allow(dead_code)]
272impl<A, R> BleCallback<A, R>
273where
274    R: Clone,
275{
276    pub const fn new(default_result: R) -> Self {
277        Self {
278            callback: Mutex::new(None),
279            default_result,
280        }
281    }
282
283    pub fn subscribe<F>(&self, callback: F)
284    where
285        F: FnMut(A) -> R + Send + 'static,
286    {
287        unsafe { self.subscribe_nonstatic(callback) }
288    }
289
290    /// # Safety
291    ///
292    /// The stored slot is `'static`; this erases the callback's lifetime. The
293    /// caller must ensure the callback (and everything it borrows) stays valid
294    /// until it is unsubscribed via `unsubscribe`.
295    pub unsafe fn subscribe_nonstatic<'a, F>(&self, callback: F)
296    where
297        F: FnMut(A) -> R + Send + 'a,
298    {
299        let callback: Box<dyn FnMut(A) -> R + 'a> = Box::new(callback);
300        let callback: Box<dyn FnMut(A) -> R + 'static> = unsafe { core::mem::transmute(callback) };
301        *self.callback.lock() = Some(Arc::new(UnsafeCell::new(callback)));
302    }
303
304    pub fn unsubscribe(&self) {
305        *self.callback.lock() = None;
306    }
307
308    /// # Safety
309    ///
310    /// Safe to use only from within the NimBLE host task.
311    pub unsafe fn call(&self, arg: A) -> R {
312        // Clone the callback `Arc` out and drop the lock *before* invoking it. The callback runs on
313        // the NimBLE host task; holding the lock across it would make a `subscribe`/`unsubscribe` on
314        // another thread block until the callback returns, and would deadlock a callback that
315        // re-subscribes itself.
316        let callback = self
317            .callback
318            .lock()
319            .as_ref()
320            .map(|callback| callback.clone());
321        if let Some(callback) = callback {
322            ((callback.get()).as_mut().unwrap())(arg)
323        } else {
324            self.default_result.clone()
325        }
326    }
327}
328
329unsafe impl<A, R> Sync for BleCallback<A, R> {}
330unsafe impl<A, R> Send for BleCallback<A, R> {}
331
332/// The GATT-server hook. Unlike [`BleCallback`], the argument
333/// [`GattsEvent`](gatt::server::GattsEvent) is lifetime-parametrized (its `Read`/`Write` variants
334/// borrow the operation's mbuf, valid only for the duration of the call), so the stored closure is
335/// higher-ranked over that lifetime. The return value is the ATT status for `Read`/`Write` and is
336/// ignored for the registration events.
337#[cfg(esp_idf_bt_nimble_gatt_server)]
338#[allow(clippy::type_complexity)]
339pub(crate) struct GattsCallback {
340    callback: Mutex<
341        Option<Arc<UnsafeCell<Box<dyn for<'a> FnMut(gatt::server::GattsEvent<'a>) -> u8 + Send>>>>,
342    >,
343}
344
345#[cfg(esp_idf_bt_nimble_gatt_server)]
346impl GattsCallback {
347    pub const fn new() -> Self {
348        Self {
349            callback: Mutex::new(None),
350        }
351    }
352
353    /// # Safety
354    ///
355    /// See [`BleCallback::subscribe_nonstatic`]; the stored slot is `'static` and this erases the
356    /// callback's capture lifetime.
357    // `GattsCallback` is `unsafe impl Send + Sync` below (accessed only from the host task); the
358    // `Arc<UnsafeCell<..>>` is the same re-entrancy mechanism as `BleCallback`, which escapes this
359    // lint only because it is generic.
360    #[allow(clippy::arc_with_non_send_sync)]
361    pub unsafe fn subscribe_nonstatic<'a, F>(&self, callback: F)
362    where
363        F: for<'e> FnMut(gatt::server::GattsEvent<'e>) -> u8 + Send + 'a,
364    {
365        let callback: Box<dyn for<'e> FnMut(gatt::server::GattsEvent<'e>) -> u8 + Send + 'a> =
366            Box::new(callback);
367        let callback: Box<dyn for<'e> FnMut(gatt::server::GattsEvent<'e>) -> u8 + Send + 'static> =
368            unsafe { core::mem::transmute(callback) };
369        *self.callback.lock() = Some(Arc::new(UnsafeCell::new(callback)));
370    }
371
372    pub fn unsubscribe(&self) {
373        *self.callback.lock() = None;
374    }
375
376    /// # Safety
377    ///
378    /// Safe to use only from within the NimBLE host task.
379    pub unsafe fn call(&self, event: gatt::server::GattsEvent<'_>) -> u8 {
380        // Drop the lock before invoking; see `BleCallback::call` for why the `let` binding matters.
381        let callback = self
382            .callback
383            .lock()
384            .as_ref()
385            .map(|callback| callback.clone());
386        if let Some(callback) = callback {
387            unsafe { ((callback.get()).as_mut().unwrap())(event) }
388        } else {
389            0
390        }
391    }
392}
393
394#[cfg(esp_idf_bt_nimble_gatt_server)]
395unsafe impl Sync for GattsCallback {}
396#[cfg(esp_idf_bt_nimble_gatt_server)]
397unsafe impl Send for GattsCallback {}
398
399/// The GATT-client hook — the dual of [`GattsCallback`] for the client side. Its argument
400/// [`GattcEvent`](gatt::client::GattcEvent) is likewise higher-ranked (its `ReadComplete`/`Notify`
401/// variants borrow an mbuf). It has no return value: the client produces no ATT responses.
402#[cfg(esp_idf_bt_nimble_gatt_client)]
403#[allow(clippy::type_complexity)]
404pub(crate) struct GattcCallback {
405    callback:
406        Mutex<Option<Arc<UnsafeCell<Box<dyn for<'a> FnMut(gatt::client::GattcEvent<'a>) + Send>>>>>,
407}
408
409#[cfg(esp_idf_bt_nimble_gatt_client)]
410impl GattcCallback {
411    pub const fn new() -> Self {
412        Self {
413            callback: Mutex::new(None),
414        }
415    }
416
417    /// # Safety
418    ///
419    /// See [`GattsCallback::subscribe_nonstatic`].
420    #[allow(clippy::arc_with_non_send_sync)]
421    pub unsafe fn subscribe_nonstatic<'a, F>(&self, callback: F)
422    where
423        F: for<'e> FnMut(gatt::client::GattcEvent<'e>) + Send + 'a,
424    {
425        let callback: Box<dyn for<'e> FnMut(gatt::client::GattcEvent<'e>) + Send + 'a> =
426            Box::new(callback);
427        let callback: Box<dyn for<'e> FnMut(gatt::client::GattcEvent<'e>) + Send + 'static> =
428            unsafe { core::mem::transmute(callback) };
429        *self.callback.lock() = Some(Arc::new(UnsafeCell::new(callback)));
430    }
431
432    pub fn unsubscribe(&self) {
433        *self.callback.lock() = None;
434    }
435
436    /// # Safety
437    ///
438    /// Safe to use only from within the NimBLE host task.
439    pub unsafe fn call(&self, event: gatt::client::GattcEvent<'_>) {
440        // Drop the lock before invoking; see `BleCallback::call` for why the `let` binding matters.
441        let callback = self
442            .callback
443            .lock()
444            .as_ref()
445            .map(|callback| callback.clone());
446        if let Some(callback) = callback {
447            unsafe { ((callback.get()).as_mut().unwrap())(event) }
448        }
449    }
450}
451
452#[cfg(esp_idf_bt_nimble_gatt_client)]
453unsafe impl Sync for GattcCallback {}
454#[cfg(esp_idf_bt_nimble_gatt_client)]
455unsafe impl Send for GattcCallback {}
456
457/// The L2CAP CoC hook. Its argument [`L2capEvent`](l2cap::L2capEvent) is higher-ranked (its
458/// `Received` variant borrows the SDU mbuf). It returns an ATT-style status (`0` = ok), consulted
459/// only for `Accept`, where non-zero rejects the incoming channel.
460#[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
461#[allow(clippy::type_complexity)]
462pub(crate) struct L2capCallback {
463    callback:
464        Mutex<Option<Arc<UnsafeCell<Box<dyn for<'a> FnMut(l2cap::L2capEvent<'a>) -> i32 + Send>>>>>,
465}
466
467#[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
468impl L2capCallback {
469    pub const fn new() -> Self {
470        Self {
471            callback: Mutex::new(None),
472        }
473    }
474
475    /// # Safety
476    ///
477    /// See [`GattsCallback::subscribe_nonstatic`].
478    #[allow(clippy::arc_with_non_send_sync)]
479    pub unsafe fn subscribe_nonstatic<'a, F>(&self, callback: F)
480    where
481        F: for<'e> FnMut(l2cap::L2capEvent<'e>) -> i32 + Send + 'a,
482    {
483        let callback: Box<dyn for<'e> FnMut(l2cap::L2capEvent<'e>) -> i32 + Send + 'a> =
484            Box::new(callback);
485        let callback: Box<dyn for<'e> FnMut(l2cap::L2capEvent<'e>) -> i32 + Send + 'static> =
486            unsafe { core::mem::transmute(callback) };
487        *self.callback.lock() = Some(Arc::new(UnsafeCell::new(callback)));
488    }
489
490    pub fn unsubscribe(&self) {
491        *self.callback.lock() = None;
492    }
493
494    /// # Safety
495    ///
496    /// Safe to use only from within the NimBLE host task.
497    pub unsafe fn call(&self, event: l2cap::L2capEvent<'_>) -> i32 {
498        // Drop the lock before invoking; see `BleCallback::call` for why the `let` binding matters.
499        let callback = self
500            .callback
501            .lock()
502            .as_ref()
503            .map(|callback| callback.clone());
504        if let Some(callback) = callback {
505            unsafe { ((callback.get()).as_mut().unwrap())(event) }
506        } else {
507            0
508        }
509    }
510}
511
512#[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
513unsafe impl Sync for L2capCallback {}
514#[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
515unsafe impl Send for L2capCallback {}
516
517/// The NimBLE stack has several globally-singleton things; we enforce that by
518/// the calling take/release on this. BleSingleton also wraps the globally singleton state
519/// that requires well-known static addresses.
520#[allow(dead_code)]
521pub(crate) struct BleSingleton {
522    initialized: AtomicBool,
523    host: BleCallback<HostEvent, ()>,
524    gap: BleCallback<gap::GapEvent, i32>,
525    #[cfg(esp_idf_bt_nimble_gatt_server)]
526    gatts: GattsCallback,
527    #[cfg(esp_idf_bt_nimble_gatt_client)]
528    gattc: GattcCallback,
529    #[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
530    l2cap: L2capCallback,
531}
532
533#[allow(dead_code)]
534impl BleSingleton {
535    pub const fn new() -> Self {
536        Self {
537            initialized: AtomicBool::new(false),
538            host: BleCallback::new(()),
539            gap: BleCallback::new(0),
540            #[cfg(esp_idf_bt_nimble_gatt_server)]
541            gatts: GattsCallback::new(),
542            #[cfg(esp_idf_bt_nimble_gatt_client)]
543            gattc: GattcCallback::new(),
544            #[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
545            l2cap: L2capCallback::new(),
546        }
547    }
548
549    pub fn take(&self) -> Result<(), EspError> {
550        self.initialized
551            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
552            .map_err(|_| EspError::from_infallible::<ESP_ERR_INVALID_STATE>())?;
553
554        Ok(())
555    }
556
557    pub fn release(&self) -> Result<(), EspError> {
558        self.initialized
559            .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
560            .map_err(|_| EspError::from_infallible::<ESP_ERR_INVALID_STATE>())?;
561
562        Ok(())
563    }
564
565    // The `unsafe extern "C"` trampolines NimBLE calls into. They are grouped here as associated
566    // functions — C callbacks take no `self`, so each reads the one global `SINGLETON` — since they
567    // all dispatch through it. (They cannot live on `BleDriver`, which is generic.)
568
569    unsafe extern "C" fn host_sync_cb() {
570        unsafe { SINGLETON.host.call(HostEvent::Sync) }
571    }
572
573    unsafe extern "C" fn host_reset_cb(reason: i32) {
574        unsafe { SINGLETON.host.call(HostEvent::Reset { reason }) }
575    }
576
577    /// The connection event callback (wired at `adv_start` for a server, at `connect` for a
578    /// client). NimBLE multiplexes role-specific events onto it, so we **demux by role**: the
579    /// role-agnostic connection events go to the GAP hook, the server-role events
580    /// (`Subscribe`/`NotifyComplete`) to the GATTS hook, and the client-role event (`Notify`) to
581    /// the GATTC hook.
582    unsafe extern "C" fn gap_event_cb(event: *mut ble_gap_event, _arg: *mut c_void) -> c_int {
583        let event = unsafe { &*event };
584
585        match event.type_ as u32 {
586            #[cfg(esp_idf_bt_nimble_gatt_server)]
587            BLE_GAP_EVENT_SUBSCRIBE | BLE_GAP_EVENT_NOTIFY_TX => {
588                if let Some(event) = gatt::server::GattsEvent::from_gap(event) {
589                    unsafe { SINGLETON.gatts.call(event) };
590                }
591                0
592            }
593            #[cfg(esp_idf_bt_nimble_gatt_client)]
594            BLE_GAP_EVENT_NOTIFY_RX => {
595                unsafe {
596                    SINGLETON
597                        .gattc
598                        .call(gatt::client::GattcEvent::from_notify_rx(event))
599                };
600                0
601            }
602            _ => unsafe { SINGLETON.gap.call(gap::GapEvent::from(event)) },
603        }
604    }
605
606    #[cfg(esp_idf_bt_nimble_gatt_server)]
607    unsafe extern "C" fn gatts_register_cb(ctxt: *mut ble_gatt_register_ctxt, _arg: *mut c_void) {
608        let event =
609            gatt::server::GattsEvent::Register(gatt::server::BleGattRegister::from(unsafe {
610                &*ctxt
611            }));
612
613        // The registration events carry no reply; the hook's status return is ignored.
614        unsafe {
615            SINGLETON.gatts.call(event);
616        }
617    }
618
619    /// The single access trampoline shared by *every* characteristic — NimBLE dispatches reads and
620    /// writes here, and we route them to the one [`gatts_subscribe`](BleDriver::gatts_subscribe) hook,
621    /// keyed by the (globally unique) `attr_handle`. There are no per-characteristic closures.
622    #[cfg(esp_idf_bt_nimble_gatt_server)]
623    unsafe extern "C" fn gatts_access_cb(
624        conn_handle: u16,
625        attr_handle: u16,
626        ctxt: *mut ble_gatt_access_ctxt,
627        _arg: *mut c_void,
628    ) -> c_int {
629        let mbuf = mbuf::Mbuf::from_raw(unsafe { (*ctxt).om });
630
631        let event = match unsafe { (*ctxt).op } as u32 {
632            BLE_GATT_ACCESS_OP_READ_CHR => {
633                // NimBLE only carries the long-read offset from ESP-IDF 5.3 on; the older
634                // `ble_gatt_access_ctxt` has no such member.
635                #[cfg(esp_idf_version_at_least_5_3_0)]
636                let offset = unsafe { (*ctxt).offset };
637                #[cfg(not(esp_idf_version_at_least_5_3_0))]
638                let offset = 0;
639
640                gatt::server::GattsEvent::Read {
641                    conn_handle,
642                    attr_handle,
643                    offset,
644                    reply: mbuf,
645                }
646            }
647            // Writes are always delivered whole and at offset 0 (NimBLE coalesces long writes), so
648            // there is no offset to report here.
649            BLE_GATT_ACCESS_OP_WRITE_CHR => gatt::server::GattsEvent::Write {
650                conn_handle,
651                attr_handle,
652                data: mbuf,
653            },
654            _ => return BLE_ATT_ERR_UNLIKELY as c_int,
655        };
656
657        unsafe { SINGLETON.gatts.call(event) as c_int }
658    }
659
660    // The GATT-client per-operation completion trampolines. NimBLE's `ble_gattc_*` calls each take
661    // a callback; we pass the matching one of these, and it routes the completion to the single
662    // GATTC hook. Discovery fires one event per item, then a final one with a `None` payload.
663
664    #[cfg(esp_idf_bt_nimble_gatt_client)]
665    unsafe extern "C" fn gattc_disc_svc_cb(
666        conn_handle: u16,
667        error: *const ble_gatt_error,
668        service: *const ble_gatt_svc,
669        _arg: *mut c_void,
670    ) -> c_int {
671        let status = if error.is_null() {
672            0
673        } else {
674            unsafe { (*error).status }
675        };
676        let service =
677            (!service.is_null()).then(|| gatt::client::GattcService::from(unsafe { &*service }));
678
679        unsafe {
680            SINGLETON.gattc.call(gatt::client::GattcEvent::Service {
681                conn_handle,
682                status,
683                service,
684            });
685        }
686        0
687    }
688
689    #[cfg(esp_idf_bt_nimble_gatt_client)]
690    unsafe extern "C" fn gattc_disc_chr_cb(
691        conn_handle: u16,
692        error: *const ble_gatt_error,
693        chr: *const ble_gatt_chr,
694        _arg: *mut c_void,
695    ) -> c_int {
696        let status = if error.is_null() {
697            0
698        } else {
699            unsafe { (*error).status }
700        };
701        let chr = (!chr.is_null()).then(|| gatt::client::GattcChr::from(unsafe { &*chr }));
702
703        unsafe {
704            SINGLETON
705                .gattc
706                .call(gatt::client::GattcEvent::Characteristic {
707                    conn_handle,
708                    status,
709                    chr,
710                });
711        }
712        0
713    }
714
715    #[cfg(esp_idf_bt_nimble_gatt_client)]
716    unsafe extern "C" fn gattc_read_cb(
717        conn_handle: u16,
718        error: *const ble_gatt_error,
719        attr: *mut ble_gatt_attr,
720        _arg: *mut c_void,
721    ) -> c_int {
722        let status = if error.is_null() {
723            0
724        } else {
725            unsafe { (*error).status }
726        };
727        let (attr_handle, om) = if attr.is_null() {
728            (0, core::ptr::null_mut())
729        } else {
730            unsafe { ((*attr).handle, (*attr).om) }
731        };
732
733        unsafe {
734            SINGLETON
735                .gattc
736                .call(gatt::client::GattcEvent::ReadComplete {
737                    conn_handle,
738                    status,
739                    attr_handle,
740                    data: mbuf::Mbuf::from_raw(om),
741                });
742        }
743        0
744    }
745
746    #[cfg(esp_idf_bt_nimble_gatt_client)]
747    unsafe extern "C" fn gattc_write_cb(
748        conn_handle: u16,
749        error: *const ble_gatt_error,
750        attr: *mut ble_gatt_attr,
751        _arg: *mut c_void,
752    ) -> c_int {
753        let status = if error.is_null() {
754            0
755        } else {
756            unsafe { (*error).status }
757        };
758        let attr_handle = if attr.is_null() {
759            0
760        } else {
761            unsafe { (*attr).handle }
762        };
763
764        unsafe {
765            SINGLETON
766                .gattc
767                .call(gatt::client::GattcEvent::WriteComplete {
768                    conn_handle,
769                    status,
770                    attr_handle,
771                });
772        }
773        0
774    }
775
776    /// The L2CAP CoC event callback (wired at `create_server` / `connect`). Unlike the GATT server,
777    /// L2CAP has its own dedicated callback, so there is no demux off the GAP callback. NimBLE hands
778    /// us ownership of a received SDU's mbuf, so we free it once the hook has read it.
779    #[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
780    unsafe extern "C" fn l2cap_event_cb(event: *mut ble_l2cap_event, _arg: *mut c_void) -> c_int {
781        let event = unsafe { &*event };
782
783        // A received SDU's mbuf is ours to free after dispatch; grab the pointer before dispatching.
784        let received_sdu = if event.type_ as u32 == BLE_L2CAP_EVENT_COC_DATA_RECEIVED {
785            Some(unsafe { event.__bindgen_anon_1.receive.sdu_rx })
786        } else {
787            None
788        };
789
790        let status = match l2cap::L2capEvent::from_raw(event) {
791            Some(event) => unsafe { SINGLETON.l2cap.call(event) },
792            None => 0,
793        };
794
795        if let Some(om) = received_sdu {
796            l2cap::free_mbuf(om);
797        }
798
799        status as c_int
800    }
801
802    unsafe extern "C" fn host_task(_arg: *mut c_void) {
803        unsafe {
804            nimble_port_run();
805            nimble_port_freertos_deinit();
806        }
807    }
808}
809
810static SINGLETON: BleSingleton = BleSingleton::new();
811
812/// The NimBLE host handle and primary entrypoint to BLE.
813///
814/// It is **role-agnostic**: the type parameter `S` is the GATT-server service table, defaulting to
815/// `()` (no server). A central or broadcaster uses [`new`](Self::new) (`S = ()`); a GATT server
816/// uses [`new_with_services`](Self::new_with_services), whose `S: Deref<Target = [ble_gatt_svc_def]>`
817/// owns the table and keeps it alive — drop order guarantees `nimble_port_deinit` (in `Drop`) runs
818/// before the table field is freed, so NimBLE never sees a dangling pointer.
819///
820/// The GAP / GATT-server / GATT-client operations are grouped into separate `impl` blocks
821/// (`gap.rs`, `gatt/gatts.rs`, and — later — the client), each mirroring a NimBLE subsystem; the
822/// GATT ones are `#[cfg]`-gated on the corresponding Kconfig. `start` takes `&self` (interior
823/// started-flag).
824pub struct BleDriver<'ble, S = ()> {
825    started: AtomicBool,
826    // Owns the GATT service table (if any). Declared before `_p`; dropped *after* `Drop::drop`
827    // runs `nimble_port_deinit`, so the table outlives NimBLE's pointers into it.
828    //
829    // Only *read* through `AsRef` in `new_with_services`, which is `#[cfg(esp_idf_bt_nimble_gatt_server)]`.
830    // In a server-off build `S` is always `()` and nothing reads it, so suppress the `-Dwarnings`
831    // "never read" lint — the field is still needed to own `S` for the drop-ordering above.
832    #[allow(dead_code)]
833    services: S,
834    _p: PhantomData<&'ble mut ()>,
835}
836
837impl<'ble> BleDriver<'ble, ()> {
838    /// Initialize the NimBLE host with **no GATT server** — the role-agnostic form used by a
839    /// central, a broadcaster, or an observer. Performs `nimble_port_init` and the standard
840    /// GAP/GATT service init, but does **not** start the host task; configure callbacks/security,
841    /// then call [`start`](Self::start).
842    pub fn new<M: BluetoothModemPeripheral + 'ble>(modem: M) -> Result<Self, EspError> {
843        Self::host_init(modem, ())
844    }
845}
846
847#[cfg(esp_idf_bt_nimble_gatt_server)]
848impl<'ble, S> BleDriver<'ble, S>
849where
850    S: AsRef<[ble_gatt_svc_def]>,
851{
852    /// Initialize the NimBLE host as a **GATT server**, registering `services` in NimBLE's
853    /// pre-start window (this is why service registration is a construction concern, not a runtime
854    /// one — see [`ble_gatts_add_svcs`]). `S` may be an owned bundle built at runtime (e.g.
855    /// [`BleGattServices`](gatt::server::BleGattServices)), a `Box<[ble_gatt_svc_def]>`, a
856    /// `&'static [ble_gatt_svc_def]`, or a `&'static` static table built with the
857    /// [`gatt_services!`](crate::gatt_services) macro; whatever it is, it must keep the *entire*
858    /// pointer graph the table references (characteristics, UUIDs) alive and at stable addresses for
859    /// as long as it is held. The driver owns it, so drop order does the rest.
860    ///
861    /// Does not start the host task; hook [`gatts_subscribe`](Self::gatts_subscribe) (to learn the
862    /// assigned attribute handles), configure security/callbacks, then call [`start`](Self::start).
863    pub fn new_with_services<M: BluetoothModemPeripheral + 'ble>(
864        modem: M,
865        services: S,
866    ) -> Result<Self, EspError> {
867        let this = Self::host_init(modem, services)?;
868
869        // Install the GATT-server registration trampoline in the same pre-`start` window as the host
870        // trampolines (see `host_init`). It dispatches into `SINGLETON.gatts` (empty until
871        // `gatts_subscribe`); NimBLE invokes it while assigning attribute handles during host start.
872        unsafe {
873            (*core::ptr::addr_of_mut!(ble_hs_cfg)).gatts_register_cb =
874                Some(BleSingleton::gatts_register_cb);
875        }
876
877        // `?` converts `BleError` to `EspError` via `From<BleError>`.
878        let defs = this.services.as_ref().as_ptr();
879        BleError::from_raw(unsafe { ble_gatts_count_cfg(defs) })?;
880        BleError::from_raw(unsafe { ble_gatts_add_svcs(defs) })?;
881
882        Ok(this)
883    }
884}
885
886impl<'ble, S> BleDriver<'ble, S> {
887    /// Subscribe to host-lifecycle events ([`HostEvent`]): `Sync` when the host and controller are
888    /// synchronized (you must delay BLE operations until then), and `Reset` when the host resets.
889    /// The hook must be re-entrant — a reset is followed by another `Sync` once re-synced.
890    /// See <https://mynewt.apache.org/latest/network/ble_setup/ble_sync_cb.html>
891    pub fn host_subscribe<F>(&self, callback: F)
892    where
893        F: FnMut(HostEvent) + Send + 'static,
894    {
895        unsafe { self.host_subscribe_nonstatic(callback) }
896    }
897
898    /// # Safety
899    ///
900    /// The non-`'static` counterpart of [`host_subscribe`](Self::host_subscribe): the callback may
901    /// borrow variables that live as long as this [`BleDriver`]. It stays registered with the
902    /// running NimBLE host task until the driver is dropped, which un-subscribes it.
903    ///
904    /// Care must be taken NOT to `core::mem::forget` the driver: that skips the un-subscription,
905    /// leaving the host task holding a callback with dangling borrows. This "local borrowing" can
906    /// only be expressed safely once/if `!Leak` types are introduced to Rust.
907    pub unsafe fn host_subscribe_nonstatic<F>(&self, callback: F)
908    where
909        F: FnMut(HostEvent) + Send + 'ble,
910    {
911        // The sync/reset trampolines are installed once at construction (see `host_init`), so
912        // subscribing only swaps the callback into the mutex-guarded `SINGLETON` slot — safe at any
913        // time, including after `start`.
914        unsafe { SINGLETON.host.subscribe_nonstatic(callback) };
915    }
916
917    /// Stop delivering host-lifecycle events to the subscribed hook.
918    pub fn host_unsubscribe(&self) {
919        SINGLETON.host.unsubscribe();
920    }
921
922    /// Configure the Security Manager (SMP) parameters. Must be called **before**
923    /// [`start`](Self::start); the settings take effect once the host task runs.
924    ///
925    /// This writes the global `ble_hs_cfg`, which the running host task reads on its own thread with
926    /// no lock we could share — so it is refused (with `ESP_ERR_INVALID_STATE`) once the host has
927    /// started. It takes `&mut self` rather than `&self` so this write cannot race a concurrent
928    /// config call from another thread; the operational, post-`start` API is all `&self` (and the
929    /// driver is `Sync`).
930    pub fn set_security(&mut self, security: &BleSecurity) -> Result<(), EspError> {
931        // `&mut self` guarantees no other thread holds a `&self` to call `start` concurrently, so the
932        // started-flag cannot flip between this check and the write below.
933        if self.started.load(Ordering::SeqCst) {
934            return Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>());
935        }
936
937        unsafe {
938            let cfg = core::ptr::addr_of_mut!(ble_hs_cfg);
939            (*cfg).sm_io_cap = security.io_cap;
940            (*cfg).set_sm_oob_data_flag(security.oob_data_flag as _);
941            (*cfg).set_sm_bonding(security.bonding as _);
942            (*cfg).set_sm_mitm(security.mitm as _);
943            (*cfg).set_sm_sc(security.secure_connections as _);
944            (*cfg).set_sm_sc_only(security.secure_connections_only as _);
945            (*cfg).set_sm_keypress(security.keypress as _);
946            (*cfg).sm_sec_lvl = security.min_sec_level;
947            (*cfg).sm_our_key_dist = security.our_key_dist;
948            (*cfg).sm_their_key_dist = security.their_key_dist;
949        }
950
951        Ok(())
952    }
953
954    /// Start the NimBLE host task. It runs in the background and calls the
955    /// [`Sync`](HostEvent::Sync) via [`host_subscribe`](Self::host_subscribe) callback once the stack is ready for use. Call this once
956    /// services, security and callbacks are set up; you must retain the driver, as the BLE stack
957    /// is stopped when it drops.
958    ///
959    /// Takes `&self` (flipping an interior started-flag) rather than consuming the driver, so the
960    /// service table it owns and every subscribed callback stay put across the call.
961    pub fn start(&self) -> Result<(), EspError> {
962        // `nimble_port_freertos_init` -> `esp_nimble_enable` unconditionally `xTaskCreate`s the host
963        // task (it does not guard against a repeat call), so a second `start()` would spawn a second
964        // `nimble_host` task running the event loop and leak the first task handle. Guard it: only
965        // the transition from not-started to started spawns the task.
966        if !self.started.swap(true, Ordering::SeqCst) {
967            unsafe { nimble_port_freertos_init(Some(BleSingleton::host_task)) };
968        }
969
970        Ok(())
971    }
972
973    /// Stop the NimBLE host task.
974    ///
975    /// Takes `&self` (flipping an interior started-flag) rather than consuming the driver, so the
976    /// service table it owns and every subscribed callback stay put across the call.
977    pub fn stop(&self) -> Result<(), EspError> {
978        // `nimble_port_freertos_init` -> `esp_nimble_enable` unconditionally `xTaskCreate`s the host
979        // task (it does not guard against a repeat call), so a second `start()` would spawn a second
980        // `nimble_host` task running the event loop and leak the first task handle. Guard it: only
981        // the transition from not-started to started spawns the task.
982        if self.started.swap(false, Ordering::SeqCst) {
983            let _ = unsafe { nimble_port_stop() };
984        }
985
986        Ok(())
987    }
988
989    /// Shared host initialization for both constructors: `nimble_port_init` + the standard GAP/GATT
990    /// service init, gated by the singleton `take`. Does **not** start the host task.
991    fn host_init<M: BluetoothModemPeripheral>(_modem: M, services: S) -> Result<Self, EspError> {
992        SINGLETON.take()?;
993
994        esp!(unsafe { nimble_port_init() })?;
995
996        unsafe {
997            ble_svc_gap_init();
998            ble_svc_gatt_init();
999
1000            // Install the host-lifecycle trampolines once, here in the single-threaded construction
1001            // window (before `start`, and serialized against a second driver by `SINGLETON.take`). They
1002            // dispatch into `SINGLETON.host`, which stays empty until `host_subscribe`, so an
1003            // unsubscribed hook is simply a no-op. Doing this here rather than lazily in `host_subscribe`
1004            // keeps every `ble_hs_cfg` write out of the post-`start` window — where NimBLE's host task
1005            // reads these fields on its own thread with no lock we could share. See the `Sync` note.
1006            let cfg = core::ptr::addr_of_mut!(ble_hs_cfg);
1007            (*cfg).sync_cb = Some(BleSingleton::host_sync_cb);
1008            (*cfg).reset_cb = Some(BleSingleton::host_reset_cb);
1009        }
1010
1011        let mut this = Self {
1012            started: AtomicBool::new(false),
1013            services,
1014            _p: PhantomData,
1015        };
1016
1017        this.set_security(&BleSecurity::new())?;
1018
1019        Ok(this)
1020    }
1021}
1022
1023// SAFETY: `BleDriver` is a handle to the process-wide NimBLE host. For `Send + Sync` to be sound,
1024// no `&self` method may mutate shared state without synchronization. Each one goes through a
1025// thread-safe path:
1026//   * NimBLE's own host API - internally locked (`ble_hs_lock`) and callable from any task;
1027//   * the mutex-guarded `SINGLETON` slots (callback subscribe/unsubscribe/dispatch).
1028// The global `ble_hs_cfg` is the one piece of shared state NimBLE reads with no lock we can share
1029// (its host task reads it directly). We keep every write to it off the `&self` API instead of trying
1030// to lock it against that reader:
1031//   * the callback trampolines are written once at construction (`host_init` /
1032//     `new_with_services`) - single-threaded, before `start`, serialized by `SINGLETON.take`;
1033//   * `set_security` is the only runtime writer, and it takes `&mut self` (so it cannot alias a
1034//     `&self` on another thread) and refuses once `start` has run (so it never races the host task);
1035//   * `Drop` clears the fields under `&mut self`, after `nimble_port_deinit` has torn the host down.
1036// The owned service table `S` is only ever *read* through `AsRef`. Hence both `Send` and `Sync` are
1037// sound even for an `S` (e.g. the heap `BleGattServices`) whose raw pointers otherwise make it
1038// auto-`!Send`/`!Sync`: the driver never hands out a `&S`, and the pointers are consumed only by
1039// NimBLE's own (thread-safe) registration.
1040unsafe impl<S> Send for BleDriver<'_, S> {}
1041unsafe impl<S> Sync for BleDriver<'_, S> {}
1042
1043impl<S> Drop for BleDriver<'_, S> {
1044    fn drop(&mut self) {
1045        let _ = self.stop();
1046
1047        // Tears down the whole host, including the GATT database — after this NimBLE holds no more
1048        // pointers into the `_services` table, which is dropped *after* this `Drop::drop` returns.
1049        esp!(unsafe { nimble_port_deinit() }).unwrap();
1050
1051        unsafe {
1052            let cfg = core::ptr::addr_of_mut!(ble_hs_cfg);
1053            (*cfg).sync_cb = None;
1054            (*cfg).reset_cb = None;
1055            #[cfg(esp_idf_bt_nimble_gatt_server)]
1056            {
1057                (*cfg).gatts_register_cb = None;
1058            }
1059        }
1060
1061        SINGLETON.host.unsubscribe();
1062        SINGLETON.gap.unsubscribe();
1063        #[cfg(esp_idf_bt_nimble_gatt_server)]
1064        SINGLETON.gatts.unsubscribe();
1065        #[cfg(esp_idf_bt_nimble_gatt_client)]
1066        SINGLETON.gattc.unsubscribe();
1067        #[cfg(not(esp_idf_bt_nimble_l2cap_coc_max_num = "0"))]
1068        SINGLETON.l2cap.unsubscribe();
1069        let _ = SINGLETON.release();
1070    }
1071}