Skip to main content

esp_idf_svc/ble/gatt/
client.rs

1//! NimBLE GATT client: connection, discovery, read/write, and received notifications, as client
2//! operations on the [`BleDriver`].
3//!
4//! Every `ble_gattc_*` operation is initiate-now, complete-later: you start it, and NimBLE invokes
5//! a callback when it finishes. We route *all* of those per-operation callbacks through one shared
6//! [`gattc_subscribe`](BleDriver::gattc_subscribe) hook, correlated by `conn_handle` (GATT serializes
7//! one transaction per connection). Received notifications/indications ([`GattcEvent::Notify`])
8//! arrive on the connection's GAP callback and are demuxed here.
9
10use core::ffi::{c_int, c_void};
11use core::ptr;
12
13use crate::sys::*;
14
15use super::super::mbuf::Mbuf;
16use super::super::{BleAddr, BleDriver, BleError, BleUuid, ConnHandle};
17use super::AttrHandle;
18
19/// A GATT-client event, delivered on the host task to the single
20/// [`gattc_subscribe`](BleDriver::gattc_subscribe) hook.
21///
22/// The discovery variants fire once per discovered item and then once more with a `None` payload
23/// to signal completion. `status` is the raw ATT/`BLE_HS_*` status (`0` on success).
24pub enum GattcEvent<'a> {
25    /// A service discovered by [`discover_services`](BleDriver::discover_services).
26    Service {
27        conn_handle: ConnHandle,
28        status: u16,
29        service: Option<GattcService>,
30    },
31    /// A characteristic discovered by
32    /// [`discover_characteristics`](BleDriver::discover_characteristics).
33    Characteristic {
34        conn_handle: ConnHandle,
35        status: u16,
36        chr: Option<GattcChr>,
37    },
38    /// Completion of a [`read`](BleDriver::read). On success `data` holds the value; check `status`
39    /// before reading it.
40    ReadComplete {
41        conn_handle: ConnHandle,
42        status: u16,
43        attr_handle: AttrHandle,
44        data: Mbuf<'a>,
45    },
46    /// Completion of a [`write`](BleDriver::write).
47    WriteComplete {
48        conn_handle: ConnHandle,
49        status: u16,
50        attr_handle: AttrHandle,
51    },
52    /// A notification or indication pushed by the peer (after subscribing by writing its CCCD).
53    Notify {
54        conn_handle: ConnHandle,
55        attr_handle: AttrHandle,
56        indication: bool,
57        data: Mbuf<'a>,
58    },
59}
60
61impl<'a> GattcEvent<'a> {
62    /// Build [`Notify`](Self::Notify) from a raw GAP event. Called from the GAP trampoline's
63    /// demux for `BLE_GAP_EVENT_NOTIFY_RX`.
64    pub(crate) fn from_notify_rx(event: &'a ble_gap_event) -> Self {
65        let notify_rx = unsafe { &event.__bindgen_anon_1.notify_rx };
66
67        Self::Notify {
68            conn_handle: notify_rx.conn_handle,
69            attr_handle: notify_rx.attr_handle,
70            indication: notify_rx.indication() != 0,
71            data: Mbuf::from_raw(notify_rx.om),
72        }
73    }
74}
75
76/// A remote service discovered on a peer (safe view of `ble_gatt_svc`).
77pub struct GattcService {
78    pub start_handle: AttrHandle,
79    pub end_handle: AttrHandle,
80    pub uuid: BleUuid,
81}
82
83impl From<&ble_gatt_svc> for GattcService {
84    fn from(svc: &ble_gatt_svc) -> Self {
85        Self {
86            start_handle: svc.start_handle,
87            end_handle: svc.end_handle,
88            // `ble_uuid_any_t`'s first union member is the `ble_uuid_t` header, at offset 0.
89            uuid: unsafe { BleUuid::from_raw((&svc.uuid as *const ble_uuid_any_t).cast()) },
90        }
91    }
92}
93
94/// A remote characteristic discovered on a peer (safe view of `ble_gatt_chr`).
95pub struct GattcChr {
96    pub def_handle: AttrHandle,
97    pub val_handle: AttrHandle,
98    /// Characteristic properties bitmask (`BLE_GATT_CHR_PROP_*`).
99    pub properties: u8,
100    pub uuid: BleUuid,
101}
102
103impl From<&ble_gatt_chr> for GattcChr {
104    fn from(chr: &ble_gatt_chr) -> Self {
105        Self {
106            def_handle: chr.def_handle,
107            val_handle: chr.val_handle,
108            properties: chr.properties,
109            uuid: unsafe { BleUuid::from_raw((&chr.uuid as *const ble_uuid_any_t).cast()) },
110        }
111    }
112}
113
114/// GATT-client operations on the [`BleDriver`]. Available for any role (`S`) — a device can be
115/// both a server and a client. `&self`, so callable re-entrantly from within the client callback.
116impl<'d, S> BleDriver<'d, S> {
117    /// Subscribe to GATT-client events ([`GattcEvent`]): per-operation completions plus received
118    /// notifications/indications.
119    pub fn gattc_subscribe<F>(&self, callback: F)
120    where
121        F: for<'a> FnMut(GattcEvent<'a>) + Send + 'static,
122    {
123        unsafe { self.gattc_subscribe_nonstatic(callback) }
124    }
125
126    /// # Safety
127    ///
128    /// The non-`'static` counterpart of [`gattc_subscribe`](Self::gattc_subscribe). See
129    /// [`BleDriver::host_subscribe_nonstatic`](crate::ble::BleDriver::host_subscribe_nonstatic) for the borrowing
130    /// rules and the `core::mem::forget` hazard.
131    pub unsafe fn gattc_subscribe_nonstatic<F>(&self, callback: F)
132    where
133        F: for<'a> FnMut(GattcEvent<'a>) + Send + 'd,
134    {
135        unsafe { super::super::SINGLETON.gattc.subscribe_nonstatic(callback) };
136    }
137
138    /// Stop delivering GATT-client events to the subscribed hook.
139    pub fn gattc_unsubscribe(&self) {
140        super::super::SINGLETON.gattc.unsubscribe();
141    }
142
143    /// Initiate a connection to `peer`. The connect/disconnect outcome arrives on the GAP hook
144    /// ([`gap_subscribe`](BleDriver::gap_subscribe)); the connection's received notifications arrive
145    /// on the GATTC hook.
146    pub fn connect(&self, own_addr_type: u8, peer: &BleAddr) -> Result<(), BleError> {
147        // bindgen does not emit `BLE_HS_FOREVER` (its C macro is `INT32_MAX`); inline it.
148        const BLE_HS_FOREVER: c_int = i32::MAX;
149
150        BleError::from_raw(unsafe {
151            ble_gap_connect(
152                own_addr_type,
153                peer.raw() as *const _,
154                BLE_HS_FOREVER,
155                ptr::null(),
156                Some(super::super::BleSingleton::gap_event_cb),
157                ptr::null_mut(),
158            )
159        })
160    }
161
162    /// Terminate the connection `conn_handle`.
163    pub fn disconnect(&self, conn_handle: ConnHandle) -> Result<(), BleError> {
164        // 0x13 = BLE_ERR_REM_USER_CONN_TERM ("remote user terminated connection").
165        BleError::from_raw(unsafe { ble_gap_terminate(conn_handle, 0x13) })
166    }
167
168    /// Discover all of the peer's primary services. Results arrive as [`GattcEvent::Service`].
169    pub fn discover_services(&self, conn_handle: ConnHandle) -> Result<(), BleError> {
170        BleError::from_raw(unsafe {
171            ble_gattc_disc_all_svcs(
172                conn_handle,
173                Some(super::super::BleSingleton::gattc_disc_svc_cb),
174                ptr::null_mut(),
175            )
176        })
177    }
178
179    /// Discover the peer's characteristics in the attribute-handle range `[start_handle,
180    /// end_handle]` (e.g. a service's range). Results arrive as [`GattcEvent::Characteristic`].
181    pub fn discover_characteristics(
182        &self,
183        conn_handle: ConnHandle,
184        start_handle: AttrHandle,
185        end_handle: AttrHandle,
186    ) -> Result<(), BleError> {
187        BleError::from_raw(unsafe {
188            ble_gattc_disc_all_chrs(
189                conn_handle,
190                start_handle,
191                end_handle,
192                Some(super::super::BleSingleton::gattc_disc_chr_cb),
193                ptr::null_mut(),
194            )
195        })
196    }
197
198    /// Read the value of `attr_handle` on the peer (a Read Request). The result arrives as
199    /// [`GattcEvent::ReadComplete`].
200    pub fn read(&self, conn_handle: ConnHandle, attr_handle: AttrHandle) -> Result<(), BleError> {
201        BleError::from_raw(unsafe {
202            ble_gattc_read(
203                conn_handle,
204                attr_handle,
205                Some(super::super::BleSingleton::gattc_read_cb),
206                ptr::null_mut(),
207            )
208        })
209    }
210
211    /// Write `data` to `attr_handle` on the peer as a **Write Request** (acknowledged): the peer
212    /// sends a Write Response, and its completion arrives as [`GattcEvent::WriteComplete`].
213    pub fn write(
214        &self,
215        conn_handle: ConnHandle,
216        attr_handle: AttrHandle,
217        data: &[u8],
218    ) -> Result<(), BleError> {
219        // NimBLE copies `data` into its own mbuf, so it need not outlive the call.
220        BleError::from_raw(unsafe {
221            ble_gattc_write_flat(
222                conn_handle,
223                attr_handle,
224                data.as_ptr() as *const c_void,
225                data.len() as u16,
226                Some(super::super::BleSingleton::gattc_write_cb),
227                ptr::null_mut(),
228            )
229        })
230    }
231
232    /// Write `data` to `attr_handle` on the peer as a **Write Command** (unacknowledged): the peer
233    /// sends no response, so this is fire-and-forget — there is **no** completion event. The
234    /// returned `Result` only reflects whether the command was accepted for transmission.
235    pub fn write_cmd(
236        &self,
237        conn_handle: ConnHandle,
238        attr_handle: AttrHandle,
239        data: &[u8],
240    ) -> Result<(), BleError> {
241        BleError::from_raw(unsafe {
242            ble_gattc_write_no_rsp_flat(
243                conn_handle,
244                attr_handle,
245                data.as_ptr() as *const c_void,
246                data.len() as u16,
247            )
248        })
249    }
250}