Skip to main content

esp_idf_svc/ble/gatt/
server.rs

1//! NimBLE GATT server: the service table, its events, and server operations on the [`BleDriver`].
2
3use core::ptr;
4
5use alloc::boxed::Box;
6use alloc::vec::Vec;
7
8use enumset::EnumSet;
9
10use crate::sys::*;
11
12use super::super::mbuf::{mbuf_from_slice, Mbuf};
13use super::super::{BleDriver, BleError, BleUuid, ConnHandle};
14use super::{flags_to_repr, AttrHandle, BleGattCharFlag};
15
16/// A GATT-server event, delivered on the host task to the single
17/// [`gatts_subscribe`](BleDriver::gatts_subscribe) hook.
18///
19/// There are no per-characteristic callbacks: NimBLE dispatches *every* characteristic read and
20/// write through one shared trampoline, and they arrive here as [`Read`](Self::Read) /
21/// [`Write`](Self::Write), keyed by the globally-unique `attr_handle`. The
22/// [`Register`](Self::Register) variants fire as the service table is registered (at `start`).
23/// [`SubscriptionChanged`](Self::SubscriptionChanged) and [`NotifyComplete`](Self::NotifyComplete)
24/// are server-role connection events that NimBLE delivers on the GAP callback and we demux here.
25///
26/// The hook returns the ATT status (`0` on success) for `Read`/`Write`; the return is ignored for
27/// the others.
28pub enum GattsEvent<'a> {
29    Register(BleGattRegister),
30    /// A peer is reading one of our characteristics; append the value to `reply`.
31    ///
32    /// This covers every ATT read (Read Request, Read Blob Request, Read By Type Request, Read
33    /// Multiple Request) — NimBLE reports them all the same way — as well as *local* reads, which
34    /// carry [`CONN_HANDLE_NONE`](crate::ble::CONN_HANDLE_NONE) instead of a real connection.
35    Read {
36        conn_handle: ConnHandle,
37        attr_handle: AttrHandle,
38        /// Non-zero only for a long read (ATT Read Blob Request), where it is the offset the peer
39        /// is asking to continue from.
40        ///
41        /// **Always append the whole value regardless of this field**: NimBLE hands a long read a
42        /// scratch buffer and slices `[offset..]` out of it itself. The offset is informational —
43        /// use it to tell a continuation from a fresh read (e.g. to snapshot the value at offset
44        /// `0` so a multi-blob read stays coherent).
45        ///
46        /// Always `0` on ESP-IDF < 5.3, where NimBLE does not report the offset at all.
47        offset: u16,
48        reply: Mbuf<'a>,
49    },
50    /// A peer wrote one of our characteristics.
51    ///
52    /// This covers every ATT write — Write Request, Write Command (write-without-response), Signed
53    /// Write Command, and a completed Prepare/Execute long write (NimBLE coalesces the queued
54    /// fragments into one event) — as NimBLE does not report which opcode carried the write. It
55    /// need not be distinguished: for a Write Request the status returned by the hook becomes the
56    /// ATT error response, and for a Write Command (which has no response) it is discarded. Local
57    /// writes arrive here too, with [`CONN_HANDLE_NONE`](crate::ble::CONN_HANDLE_NONE).
58    Write {
59        conn_handle: ConnHandle,
60        attr_handle: AttrHandle,
61        data: Mbuf<'a>,
62    },
63    /// A peer's subscription state for one of our characteristics changed: it wrote the CCCD, the
64    /// connection is going down, or a bond was restored — see `reason`. The `prev_*` / `cur_*`
65    /// pairs give the edge, so no shadow state is needed to tell a subscribe from an unsubscribe.
66    SubscriptionChanged {
67        conn_handle: ConnHandle,
68        attr_handle: AttrHandle,
69        reason: SubscribeReason,
70        prev_notify: bool,
71        cur_notify: bool,
72        prev_indicate: bool,
73        cur_indicate: bool,
74    },
75    /// An indication/notification we sent completed (for an indication, `status` is the peer's
76    /// confirmation result).
77    NotifyComplete {
78        conn_handle: ConnHandle,
79        attr_handle: AttrHandle,
80        indication: bool,
81        status: i32,
82    },
83}
84
85impl GattsEvent<'static> {
86    /// Build the server-role `SubscriptionChanged` / `NotifyComplete` events from a raw GAP event.
87    /// Returns `None` for any other event type. Called from the GAP trampoline's demux.
88    pub(crate) fn from_gap(event: &ble_gap_event) -> Option<Self> {
89        let anon = &event.__bindgen_anon_1;
90
91        match event.type_ as u32 {
92            BLE_GAP_EVENT_SUBSCRIBE => {
93                let subscribe = unsafe { &anon.subscribe };
94                Some(Self::SubscriptionChanged {
95                    conn_handle: subscribe.conn_handle,
96                    attr_handle: subscribe.attr_handle,
97                    reason: SubscribeReason::from_raw(subscribe.reason),
98                    prev_notify: subscribe.prev_notify() != 0,
99                    cur_notify: subscribe.cur_notify() != 0,
100                    prev_indicate: subscribe.prev_indicate() != 0,
101                    cur_indicate: subscribe.cur_indicate() != 0,
102                })
103            }
104            BLE_GAP_EVENT_NOTIFY_TX => {
105                let notify_tx = unsafe { &anon.notify_tx };
106                Some(Self::NotifyComplete {
107                    conn_handle: notify_tx.conn_handle,
108                    attr_handle: notify_tx.attr_handle,
109                    indication: notify_tx.indication() != 0,
110                    status: notify_tx.status,
111                })
112            }
113            _ => None,
114        }
115    }
116}
117
118/// Why a peer's subscription state changed (the `reason` of
119/// [`GattsEvent::SubscriptionChanged`]).
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
121pub enum SubscribeReason {
122    /// The peer wrote the characteristic's CCCD.
123    Write,
124    /// The connection is about to be terminated, so NimBLE is clearing the peer's subscription
125    /// state. The `cur_*` flags are therefore `false` — this is *not* the peer opting out.
126    Term,
127    /// A bond was restored from persistence and the peer's subscription state came back with it.
128    /// Nothing was written on the wire; the peer is subscribed as of now.
129    Restore,
130    /// A reason code unknown to this version of the crate.
131    Other(u8),
132}
133
134impl SubscribeReason {
135    fn from_raw(reason: u8) -> Self {
136        match reason as u32 {
137            BLE_GAP_SUBSCRIBE_REASON_WRITE => Self::Write,
138            BLE_GAP_SUBSCRIBE_REASON_TERM => Self::Term,
139            BLE_GAP_SUBSCRIBE_REASON_RESTORE => Self::Restore,
140            _ => Self::Other(reason),
141        }
142    }
143}
144
145/// A GATT registration event (the payload of [`GattsEvent::Register`]). Capture the value handles
146/// you need (matching on `uuid`) here.
147pub enum BleGattRegister {
148    Service {
149        uuid: BleUuid,
150        handle: AttrHandle,
151    },
152    Characteristic {
153        uuid: BleUuid,
154        def_handle: AttrHandle,
155        val_handle: AttrHandle,
156    },
157    Descriptor {
158        uuid: BleUuid,
159        handle: AttrHandle,
160    },
161    Other,
162}
163
164impl From<&ble_gatt_register_ctxt> for BleGattRegister {
165    fn from(ctxt: &ble_gatt_register_ctxt) -> Self {
166        let anon = &ctxt.__bindgen_anon_1;
167
168        match ctxt.op as u32 {
169            BLE_GATT_REGISTER_OP_SVC => {
170                let svc = unsafe { &anon.svc };
171                Self::Service {
172                    uuid: unsafe { BleUuid::from_raw((*svc.svc_def).uuid) },
173                    handle: svc.handle,
174                }
175            }
176            BLE_GATT_REGISTER_OP_CHR => {
177                let chr = unsafe { &anon.chr };
178                Self::Characteristic {
179                    uuid: unsafe { BleUuid::from_raw((*chr.chr_def).uuid) },
180                    def_handle: chr.def_handle,
181                    val_handle: chr.val_handle,
182                }
183            }
184            BLE_GATT_REGISTER_OP_DSC => {
185                let dsc = unsafe { &anon.dsc };
186                Self::Descriptor {
187                    uuid: unsafe { BleUuid::from_raw((*dsc.dsc_def).uuid) },
188                    handle: dsc.handle,
189                }
190            }
191            _ => Self::Other,
192        }
193    }
194}
195
196/// A characteristic in a [`BleGattService`] — just its UUID and flags. Reads and writes are
197/// serviced by the single [`gatts_subscribe`](BleDriver::gatts_subscribe) hook (dispatched by the
198/// value handle reported via [`BleGattRegister`]), so there is no per-characteristic closure and
199/// no per-characteristic allocation.
200pub struct BleGattCharacteristic {
201    uuid: BleUuid,
202    flags: EnumSet<BleGattCharFlag>,
203}
204
205impl BleGattCharacteristic {
206    pub fn new(uuid: BleUuid, flags: EnumSet<BleGattCharFlag>) -> Self {
207        Self { uuid, flags }
208    }
209}
210
211/// A GATT service definition.
212pub struct BleGattService {
213    primary: bool,
214    uuid: BleUuid,
215    characteristics: Vec<BleGattCharacteristic>,
216}
217
218impl BleGattService {
219    pub fn new(primary: bool, uuid: BleUuid, characteristics: Vec<BleGattCharacteristic>) -> Self {
220        Self {
221            primary,
222            uuid,
223            characteristics,
224        }
225    }
226}
227
228/// A GATT service table built **at runtime** (heap-allocated), as the raw NimBLE
229/// `ble_gatt_svc_def` tree, ready to hand to
230/// [`BleDriver::new_with_services`](crate::ble::BleDriver::new_with_services). Implements
231/// `AsRef<[ble_gatt_svc_def]>`, so it is one valid `S`; a `&'static` table or a
232/// `Box<[ble_gatt_svc_def]>` are others. For a **compile-time** table (no heap), see the
233/// [`gatt_services!`](crate::gatt_services) macro.
234pub struct BleGattServices {
235    // There are dragons here. The C def arrays hold raw pointers into `_services` (UUIDs) and into
236    // `_chr_defs`. Those targets are heap-allocated, so they stay put when this struct's handle
237    // moves — which is what keeps the pointers valid. All characteristics share the crate's single
238    // access trampoline (`gatts_access_cb`); `arg` and `val_handle` are null.
239    _services: Vec<BleGattService>,
240    _chr_defs: Vec<Box<[ble_gatt_chr_def]>>,
241    svc_defs: Box<[ble_gatt_svc_def]>,
242}
243
244impl BleGattServices {
245    pub fn new(services: Vec<BleGattService>) -> Self {
246        let mut chr_storage: Vec<Box<[ble_gatt_chr_def]>> = Vec::with_capacity(services.len());
247        let mut svc_defs: Vec<ble_gatt_svc_def> = Vec::with_capacity(services.len() + 1);
248
249        for service in &services {
250            let mut chr_defs: Vec<ble_gatt_chr_def> =
251                Vec::with_capacity(service.characteristics.len() + 1);
252
253            for chr in &service.characteristics {
254                chr_defs.push(ble_gatt_chr_def {
255                    uuid: chr.uuid.as_ptr(),
256                    // The one trampoline for every characteristic; `attr_handle` disambiguates.
257                    access_cb: Some(super::super::BleSingleton::gatts_access_cb),
258                    arg: ptr::null_mut(),
259                    flags: flags_to_repr(chr.flags),
260                    // Handles are captured from the registration event, not written back here.
261                    val_handle: ptr::null_mut(),
262                    ..Default::default()
263                });
264            }
265            chr_defs.push(ble_gatt_chr_def::default());
266
267            let chr_defs = chr_defs.into_boxed_slice();
268            let chr_ptr = chr_defs.as_ptr();
269            chr_storage.push(chr_defs);
270
271            svc_defs.push(ble_gatt_svc_def {
272                type_: if service.primary {
273                    BLE_GATT_SVC_TYPE_PRIMARY as u8
274                } else {
275                    BLE_GATT_SVC_TYPE_SECONDARY as u8
276                },
277                uuid: service.uuid.as_ptr(),
278                includes: ptr::null_mut(),
279                characteristics: chr_ptr,
280            });
281        }
282        svc_defs.push(ble_gatt_svc_def::default());
283
284        Self {
285            _services: services,
286            _chr_defs: chr_storage,
287            svc_defs: svc_defs.into_boxed_slice(),
288        }
289    }
290}
291
292impl AsRef<[ble_gatt_svc_def]> for BleGattServices {
293    fn as_ref(&self) -> &[ble_gatt_svc_def] {
294        &self.svc_defs
295    }
296}
297
298/// GATT-server operations on the [`BleDriver`], available only when the driver was built with a
299/// service table (`S: AsRef<[ble_gatt_svc_def]>`) via
300/// [`new_with_services`](BleDriver::new_with_services). `&self`, so callable re-entrantly.
301#[cfg(esp_idf_bt_nimble_gatt_server)]
302impl<'d, S> BleDriver<'d, S>
303where
304    S: AsRef<[ble_gatt_svc_def]>,
305{
306    /// Subscribe to GATT-server events ([`GattsEvent`]). Set this **before**
307    /// [`start`](BleDriver::start): the `Register` events (carrying the attribute handles NimBLE
308    /// assigned) fire during host start.
309    pub fn gatts_subscribe<F>(&self, callback: F)
310    where
311        F: for<'a> FnMut(GattsEvent<'a>) -> u8 + Send + 'static,
312    {
313        unsafe { self.gatts_subscribe_nonstatic(callback) }
314    }
315
316    /// # Safety
317    ///
318    /// The non-`'static` counterpart of [`gatts_subscribe`](Self::gatts_subscribe). See
319    /// [`BleDriver::host_subscribe_nonstatic`](crate::ble::BleDriver::host_subscribe_nonstatic) for the borrowing
320    /// rules and the `core::mem::forget` hazard.
321    pub unsafe fn gatts_subscribe_nonstatic<F>(&self, callback: F)
322    where
323        F: for<'a> FnMut(GattsEvent<'a>) -> u8 + Send + 'd,
324    {
325        // The `gatts_register_cb` trampoline is installed once at construction (see
326        // `BleDriver::new_with_services`), so subscribing only swaps the mutex-guarded `SINGLETON`
327        // slot — no `ble_hs_cfg` write here.
328        unsafe { super::super::SINGLETON.gatts.subscribe_nonstatic(callback) };
329    }
330
331    /// Stop delivering GATT-server events to the subscribed hook.
332    pub fn gatts_unsubscribe(&self) {
333        super::super::SINGLETON.gatts.unsubscribe();
334    }
335
336    /// Send a "free-form" characteristic indication to `conn_handle`.
337    pub fn indicate(
338        &self,
339        conn_handle: ConnHandle,
340        val_handle: AttrHandle,
341        data: &[u8],
342    ) -> Result<(), BleError> {
343        let om = mbuf_from_slice(data)?;
344
345        // `ble_gatts_indicate_custom` takes ownership of `om` and frees it on all paths (no leak, no double-free).
346        BleError::from_raw(unsafe { ble_gatts_indicate_custom(conn_handle, val_handle, om) })
347    }
348}
349
350// -------------------------------------------------------------------------------------------------
351// Static (compile-time) service tables — see the `gatt_services!` macro.
352//
353// The C def structs hold raw pointers, so they are `!Sync` and cannot go in a `static` directly.
354// These `#[repr(transparent)]` wrappers add the `unsafe impl Sync`; the macro then builds the
355// null-terminated tree bottom-up out of *block-scoped inner statics*, which is what gives every
356// pointer target a stable `'static` address (a `const` has no stable address; a `static` does).
357// -------------------------------------------------------------------------------------------------
358
359/// A **static** (compile-time) GATT service table: a null-terminated `ble_gatt_svc_def` array
360/// wrapped so it can live in a `static`. Build one with the [`gatt_services!`](crate::gatt_services)
361/// macro and pass `&MY_SERVICES` to [`new_with_services`](BleDriver::new_with_services) — it needs
362/// no heap and lands in flash. `N` (services + terminator) is inferred by the macro; you never write
363/// it.
364#[repr(transparent)]
365pub struct GattServices<const N: usize>([ble_gatt_svc_def; N]);
366
367// SAFETY: the raw pointers in the table only address other items of the same `'static` tree, and
368// NimBLE consumes the table read-only (assigned handles are reported via the register events, not
369// written back into it).
370unsafe impl<const N: usize> Sync for GattServices<N> {}
371
372impl<const N: usize> GattServices<N> {
373    /// Wrap a fully-built, null-terminated service array. Prefer the
374    /// [`gatt_services!`](crate::gatt_services) macro over calling this directly.
375    #[doc(hidden)]
376    pub const fn new(defs: [ble_gatt_svc_def; N]) -> Self {
377        Self(defs)
378    }
379}
380
381impl<const N: usize> AsRef<[ble_gatt_svc_def]> for GattServices<N> {
382    fn as_ref(&self) -> &[ble_gatt_svc_def] {
383        &self.0
384    }
385}
386
387/// The characteristics of one service, as a null-terminated `ble_gatt_chr_def` array wrapped for
388/// `static` storage. An implementation detail of [`gatt_services!`](crate::gatt_services).
389#[doc(hidden)]
390#[repr(transparent)]
391pub struct GattChrs<const N: usize>([ble_gatt_chr_def; N]);
392
393// SAFETY: as for `GattServices`.
394unsafe impl<const N: usize> Sync for GattChrs<N> {}
395
396impl<const N: usize> GattChrs<N> {
397    pub const fn new(defs: [ble_gatt_chr_def; N]) -> Self {
398        Self(defs)
399    }
400
401    pub const fn as_ptr(&self) -> *const ble_gatt_chr_def {
402        self.0.as_ptr()
403    }
404}
405
406// All-zero templates. A zeroed `ble_gatt_*_def` is exactly the null terminator, and is the base the
407// builders below fill in — the `const` analog of the runtime path's `..Default::default()`, so that
408// fields we do not set (and any the bindings gain across ESP-IDF versions, e.g. `cpfd`) are zeroed
409// without having to enumerate them. Every field is a pointer / fn-pointer / integer, so all-zero is
410// a valid, meaningful value.
411const ZERO_SVC: ble_gatt_svc_def = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
412const ZERO_CHR: ble_gatt_chr_def = unsafe { core::mem::MaybeUninit::zeroed().assume_init() };
413
414/// The service-array terminator. Public only for the macro.
415#[doc(hidden)]
416pub const SVC_SENTINEL: ble_gatt_svc_def = ZERO_SVC;
417
418/// The characteristic-array terminator. Public only for the macro.
419#[doc(hidden)]
420pub const CHR_SENTINEL: ble_gatt_chr_def = ZERO_CHR;
421
422/// Build one characteristic def for a static table. Public only for the macro. Wires the crate's
423/// single access trampoline (as the runtime path does); the assigned handle is learned from the
424/// register events, so `val_handle` is null.
425#[doc(hidden)]
426pub const fn make_chr(uuid: *const ble_uuid_t, flags: ble_gatt_chr_flags) -> ble_gatt_chr_def {
427    ble_gatt_chr_def {
428        uuid,
429        access_cb: Some(super::super::BleSingleton::gatts_access_cb),
430        flags,
431        ..ZERO_CHR
432    }
433}
434
435/// Build one service def for a static table. Public only for the macro.
436#[doc(hidden)]
437pub const fn make_svc(
438    primary: bool,
439    uuid: *const ble_uuid_t,
440    characteristics: *const ble_gatt_chr_def,
441) -> ble_gatt_svc_def {
442    ble_gatt_svc_def {
443        type_: if primary {
444            BLE_GATT_SVC_TYPE_PRIMARY as u8
445        } else {
446            BLE_GATT_SVC_TYPE_SECONDARY as u8
447        },
448        uuid,
449        characteristics,
450        ..ZERO_SVC
451    }
452}
453
454/// Define a **static** (compile-time, heap-free) GATT service table.
455///
456/// Expands to a `static NAME` holding the null-terminated NimBLE `ble_gatt_svc_def` tree (services,
457/// characteristics, UUIDs), all in flash. Pass `&NAME` to
458/// [`new_with_services`](crate::ble::BleDriver::new_with_services). Reads and writes are serviced
459/// the same way as the runtime table — through the single
460/// [`gatts_subscribe`](crate::ble::BleDriver::gatts_subscribe) hook, keyed by the value handle
461/// reported in the `Register` events (so learn handles there, exactly as the runtime example does).
462///
463/// Each UUID slot is any `const` [`BleUuid`](crate::ble::BleUuid) expression — declare your UUIDs as
464/// `const FOO: BleUuid = BleUuid::uuid128(..)` (or `uuid16`) and name them. Characteristic flags are
465/// any `|`-separated [`BleGattCharFlag`](crate::ble::gatt::BleGattCharFlag) variants (`Read`,
466/// `Write`, `Notify`, `Indicate`). Services are `primary(..)` or `secondary(..)`.
467///
468/// The `NAME` and the body live inside the one macro group (a macro invocation is a single token
469/// tree), so it reads `gatt_services!(NAME { .. });` — use `!{ .. }` to drop the trailing `;`.
470///
471/// ```ignore
472/// use esp_idf_svc::ble::BleUuid;
473/// use esp_idf_svc::gatt_services;
474///
475/// const SVC: BleUuid = BleUuid::uuid128(0xad91b201_73474047_9e173bed_82d75f9d);
476/// const RECV: BleUuid = BleUuid::uuid128(0xb6fccb50_87be44f3_ae22f854_85ea42c4);
477/// const HR: BleUuid = BleUuid::uuid16(0x2A37);
478///
479/// gatt_services!(SERVICES {
480///     primary(SVC) {
481///         chr(RECV, Write);
482///         chr(HR, Notify | Indicate);
483///     }
484/// });
485/// // ... let driver = BleDriver::new_with_services(modem, &SERVICES)?;
486/// ```
487#[macro_export]
488macro_rules! gatt_services {
489    // --- internal helpers (matched before the public arm) ---
490
491    // a `*const ble_uuid_t` backed by a fresh block-scoped `'static` (stable address)
492    (@uuid_ptr $uuid:expr) => {{
493        static U: $crate::ble::BleUuid = $uuid;
494        U.as_ptr()
495    }};
496
497    // one characteristic def
498    (@chr $uuid:expr, $($flag:ident)|+ ) => {
499        $crate::ble::gatt::server::make_chr(
500            $crate::gatt_services!(@uuid_ptr $uuid),
501            0 $( | $crate::ble::gatt::BleGattCharFlag::$flag.repr() )+,
502        )
503    };
504
505    // map `primary`/`secondary` to a bool; a unit token used only for counting repetitions
506    (@primary primary) => { true };
507    (@primary secondary) => { false };
508    (@unit $_t:expr) => { () };
509
510    // --- public entry: `gatt_services!(NAME { primary(uuid) { chr(uuid, Flags); .. } .. })` ---
511    (
512        $vis:vis $NAME:ident {
513            $(
514                $kind:ident ( $svc_uuid:expr ) {
515                    $( chr ( $chr_uuid:expr, $($flag:ident)|+ ) ; )*
516                }
517            )+
518        }
519    ) => {
520        $vis static $NAME: $crate::ble::gatt::server::GattServices<
521            { <[()]>::len(&[ $( $crate::gatt_services!(@unit $svc_uuid) ),+ ]) + 1 }
522        > = $crate::ble::gatt::server::GattServices::new([
523            $(
524                {
525                    static CHRS: $crate::ble::gatt::server::GattChrs<
526                        { <[()]>::len(&[ $( $crate::gatt_services!(@unit $chr_uuid) ),* ]) + 1 }
527                    > = $crate::ble::gatt::server::GattChrs::new([
528                        $( $crate::gatt_services!(@chr $chr_uuid, $($flag)|+), )*
529                        $crate::ble::gatt::server::CHR_SENTINEL
530                    ]);
531                    $crate::ble::gatt::server::make_svc(
532                        $crate::gatt_services!(@primary $kind),
533                        $crate::gatt_services!(@uuid_ptr $svc_uuid),
534                        CHRS.as_ptr(),
535                    )
536                },
537            )+
538            $crate::ble::gatt::server::SVC_SENTINEL
539        ]);
540    };
541}