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}