Skip to main content

esp_idf_svc/ble/
gap.rs

1//! NimBLE GAP: device name, connection events, and advertising.
2
3use core::ffi::c_int;
4use core::ptr;
5
6use alloc::ffi::CString;
7
8use crate::sys::*;
9
10use super::{BleAddr, BleDriver, BleError, ConnHandle};
11
12// Advertising parameter and field types.
13//
14// NimBLE exposes two mutually-exclusive advertising APIs. The legacy API is the default; the
15// extended API (the `ext_adv_*` methods) is only compiled when the controller is built with
16// `CONFIG_BT_NIMBLE_EXT_ADV=y`.
17
18/// Parameters for a legacy advertising procedure (safe version of `ble_gap_adv_params`).
19#[cfg(not(esp_idf_bt_nimble_ext_adv))]
20#[derive(Clone, Copy, Default)]
21pub struct BleAdvParams {
22    pub conn_mode: u8,
23    pub disc_mode: u8,
24    pub itvl_min: u16,
25    pub itvl_max: u16,
26    pub channel_map: u8,
27    pub filter_policy: u8,
28    pub high_duty_cycle: bool,
29}
30
31#[cfg(not(esp_idf_bt_nimble_ext_adv))]
32impl From<&BleAdvParams> for ble_gap_adv_params {
33    fn from(params: &BleAdvParams) -> Self {
34        let mut raw: ble_gap_adv_params = unsafe { core::mem::zeroed() };
35
36        raw.conn_mode = params.conn_mode;
37        raw.disc_mode = params.disc_mode;
38        raw.itvl_min = params.itvl_min;
39        raw.itvl_max = params.itvl_max;
40        raw.channel_map = params.channel_map;
41        raw.filter_policy = params.filter_policy;
42        raw.set_high_duty_cycle(params.high_duty_cycle as _);
43
44        raw
45    }
46}
47
48/// Parameters for an extended advertising instance (safe version of `ble_gap_ext_adv_params`).
49///
50/// Only available when the controller is built with `CONFIG_BT_NIMBLE_EXT_ADV=y`;
51/// the default build exposes the legacy [`BleAdvParams`].
52#[cfg(esp_idf_bt_nimble_ext_adv)]
53#[derive(Clone, Copy, Default)]
54pub struct BleExtAdvParams {
55    pub connectable: bool,
56    pub scannable: bool,
57    pub legacy_pdu: bool,
58    pub directed: bool,
59    pub anonymous: bool,
60    pub high_duty_directed: bool,
61    pub include_tx_power: bool,
62    pub itvl_min: u32,
63    pub itvl_max: u32,
64    pub channel_map: u8,
65    pub own_addr_type: u8,
66    pub primary_phy: u8,
67    pub secondary_phy: u8,
68    pub tx_power: i8,
69    pub sid: u8,
70}
71
72#[cfg(esp_idf_bt_nimble_ext_adv)]
73impl From<&BleExtAdvParams> for ble_gap_ext_adv_params {
74    fn from(params: &BleExtAdvParams) -> Self {
75        let mut raw: ble_gap_ext_adv_params = unsafe { core::mem::zeroed() };
76
77        raw.set_connectable(params.connectable as _);
78        raw.set_scannable(params.scannable as _);
79        raw.set_legacy_pdu(params.legacy_pdu as _);
80        raw.set_directed(params.directed as _);
81        raw.set_anonymous(params.anonymous as _);
82        raw.set_high_duty_directed(params.high_duty_directed as _);
83        raw.set_include_tx_power(params.include_tx_power as _);
84
85        raw.itvl_min = params.itvl_min;
86        raw.itvl_max = params.itvl_max;
87        raw.channel_map = params.channel_map;
88        raw.own_addr_type = params.own_addr_type;
89        raw.primary_phy = params.primary_phy;
90        raw.secondary_phy = params.secondary_phy;
91        raw.tx_power = params.tx_power;
92        raw.sid = params.sid;
93
94        raw
95    }
96}
97
98/// Structured advertising payload (safe version of `ble_hs_adv_fields`).
99///
100/// Which raw-payload setter the driver offers depends on whether the build has extended advertising
101/// enabled; in this build it is
102#[cfg_attr(esp_idf_bt_nimble_ext_adv, doc = "[`BleDriver::ext_adv_set_data`],")]
103#[cfg_attr(not(esp_idf_bt_nimble_ext_adv), doc = "[`BleDriver::adv_set_data`],")]
104/// which you can use to set the payload bytes yourself.
105#[derive(Clone, Copy, Default)]
106pub struct BleAdvFields<'a> {
107    pub flags: u8,
108    pub name: Option<&'a str>,
109    pub tx_power_level: Option<i8>,
110    pub appearance: Option<u16>,
111    pub service_data_uuid16: Option<&'a [u8]>,
112    pub manufacturer_data: Option<&'a [u8]>,
113}
114
115impl From<&BleAdvFields<'_>> for ble_hs_adv_fields {
116    fn from(fields: &BleAdvFields) -> Self {
117        let mut raw: ble_hs_adv_fields = unsafe { core::mem::zeroed() };
118
119        raw.flags = fields.flags;
120
121        if let Some(name) = fields.name {
122            raw.name = name.as_ptr();
123            raw.name_len = name.len() as _;
124            raw.set_name_is_complete(1);
125        }
126
127        if let Some(tx_power_level) = fields.tx_power_level {
128            raw.tx_pwr_lvl = tx_power_level;
129            raw.set_tx_pwr_lvl_is_present(1);
130        }
131
132        if let Some(appearance) = fields.appearance {
133            raw.appearance = appearance;
134            raw.set_appearance_is_present(1);
135        }
136
137        if let Some(service_data) = fields.service_data_uuid16 {
138            raw.svc_data_uuid16 = service_data.as_ptr();
139            raw.svc_data_uuid16_len = service_data.len() as _;
140        }
141
142        if let Some(manufacturer_data) = fields.manufacturer_data {
143            raw.mfg_data = manufacturer_data.as_ptr();
144            raw.mfg_data_len = manufacturer_data.len() as _;
145        }
146
147        raw
148    }
149}
150
151/// Role-agnostic connection events. NimBLE multiplexes *role-specific* events (server:
152/// `Subscribe`/`NotifyComplete`; client: `Notify`) onto the same connection callback, but those are
153/// demuxed to [`GattsEvent`](super::gatt::server::GattsEvent) /
154/// [`GattcEvent`](super::gatt::client::GattcEvent) — so they are not part of this enum.
155pub enum GapEvent {
156    Connect {
157        conn_handle: ConnHandle,
158        status: Result<(), BleError>,
159    },
160    Disconnect {
161        conn_handle: ConnHandle,
162        reason: BleError,
163    },
164    Mtu {
165        conn_handle: ConnHandle,
166        value: u16,
167    },
168    Other,
169}
170
171impl From<&ble_gap_event> for GapEvent {
172    fn from(event: &ble_gap_event) -> Self {
173        let anon = &event.__bindgen_anon_1;
174
175        match event.type_ as u32 {
176            BLE_GAP_EVENT_CONNECT => {
177                let connect = unsafe { &anon.connect };
178                Self::Connect {
179                    conn_handle: connect.conn_handle,
180                    status: BleError::from_raw(connect.status),
181                }
182            }
183            BLE_GAP_EVENT_DISCONNECT => {
184                let disconnect = unsafe { &anon.disconnect };
185                Self::Disconnect {
186                    conn_handle: disconnect.conn.conn_handle,
187                    reason: BleError::new(disconnect.reason),
188                }
189            }
190            BLE_GAP_EVENT_MTU => {
191                let mtu = unsafe { &anon.mtu };
192                Self::Mtu {
193                    conn_handle: mtu.conn_handle,
194                    value: mtu.value,
195                }
196            }
197            _ => Self::Other,
198        }
199    }
200}
201
202#[derive(Clone, Copy)]
203#[repr(transparent)]
204pub struct BleConnDesc(ble_gap_conn_desc);
205
206impl BleConnDesc {
207    pub const fn peer_addr(&self) -> BleAddr {
208        BleAddr::new(self.0.peer_id_addr.type_, self.0.peer_id_addr.val)
209    }
210}
211
212/// Look up a connection descriptor by handle.
213///
214/// A stateless query against the running host, so it is a free function rather than a [`BleDriver`]
215/// method — it needs no live handle and is convenient to call from within a GAP event callback.
216pub fn conn_find(conn_handle: ConnHandle) -> Result<BleConnDesc, BleError> {
217    let mut desc: ble_gap_conn_desc = unsafe { core::mem::zeroed() };
218    BleError::from_raw(unsafe { ble_gap_conn_find(conn_handle, &mut desc) })?;
219
220    Ok(BleConnDesc(desc))
221}
222
223/// GAP operations on the [`BleDriver`]: advertising, the device name, and GAP event
224/// subscription. Available for any role (`S`). `&self`, so callable re-entrantly from within the
225/// GAP event callback.
226impl<'d, S> BleDriver<'d, S> {
227    /// Set the device name exposed via the GAP service.
228    pub fn set_device_name(&self, name: &str) -> Result<(), BleError> {
229        let name = CString::new(name).map_err(|_| BleError::new(BLE_HS_EINVAL as c_int))?;
230
231        // NimBLE copies the name into its own buffer, so the drop is safe
232        BleError::from_raw(unsafe { ble_svc_gap_device_name_set(name.as_ptr()) })
233    }
234
235    /// Subscribe to GAP events (connect / disconnect / subscribe / MTU / notify-tx / — for a
236    /// client — notify-rx). The callback runs on the NimBLE host task and returns the GAP status
237    /// code (`0` on success). The trampoline is wired at [`adv_start`](Self::adv_start) (server) or
238    /// at connect time (client), so this only needs to be set before whichever of those you use.
239    pub fn gap_subscribe<F>(&self, callback: F)
240    where
241        F: FnMut(GapEvent) -> i32 + Send + 'static,
242    {
243        unsafe { self.gap_subscribe_nonstatic(callback) }
244    }
245
246    /// # Safety
247    ///
248    /// The non-`'static` counterpart of [`gap_subscribe`](Self::gap_subscribe): the callback may
249    /// borrow data that lives as long as the [`BleDriver`]. It stays registered until the driver is
250    /// dropped, which un-subscribes it, so the driver must not be `core::mem::forget`-ten. See
251    /// [`BleDriver::host_subscribe_nonstatic`](crate::ble::BleDriver::host_subscribe_nonstatic).
252    pub unsafe fn gap_subscribe_nonstatic<F>(&self, callback: F)
253    where
254        F: FnMut(GapEvent) -> i32 + Send + 'd,
255    {
256        unsafe { super::SINGLETON.gap.subscribe_nonstatic(callback) };
257    }
258
259    /// Stop delivering GAP events to the subscribed callback.
260    pub fn gap_unsubscribe(&self) {
261        super::SINGLETON.gap.unsubscribe();
262    }
263
264    /// Set the raw advertising payload.
265    #[cfg(not(esp_idf_bt_nimble_ext_adv))]
266    pub fn adv_set_data(&self, data: &[u8]) -> Result<(), BleError> {
267        // NimBLE copies the payload into its own buffer, so `data` need not outlive the call.
268        BleError::from_raw(unsafe { ble_gap_adv_set_data(data.as_ptr(), data.len() as c_int) })
269    }
270
271    /// Encode `fields` into the advertising payload.
272    #[cfg(not(esp_idf_bt_nimble_ext_adv))]
273    pub fn adv_set_fields(&self, fields: &BleAdvFields) -> Result<(), BleError> {
274        let raw: ble_hs_adv_fields = fields.into();
275
276        BleError::from_raw(unsafe { ble_gap_adv_set_fields(&raw) })
277    }
278
279    /// Start a legacy advertising procedure. Drive this from an [`host_subscribe`] closure once the host
280    /// has synced, and restart it from a [`GapEvent::Disconnect`] handler. Events for the
281    /// resulting connection are delivered to the [`gap_subscribe`](Self::gap_subscribe) callback.
282    ///
283    /// [`host_subscribe`]: crate::ble::BleDriver::host_subscribe
284    #[cfg(not(esp_idf_bt_nimble_ext_adv))]
285    pub fn adv_start(&self, own_addr_type: u8, params: &BleAdvParams) -> Result<(), BleError> {
286        let raw: ble_gap_adv_params = params.into();
287
288        // bindgen does not emit `BLE_HS_FOREVER`, as its C macro expands to `INT32_MAX` rather than
289        // to an integer literal. Advertise with no timeout.
290        const BLE_HS_FOREVER: c_int = i32::MAX;
291
292        let rc = unsafe {
293            ble_gap_adv_start(
294                own_addr_type,
295                ptr::null(),
296                BLE_HS_FOREVER as _,
297                &raw,
298                Some(super::BleSingleton::gap_event_cb),
299                ptr::null_mut(),
300            )
301        };
302        if rc == BLE_HS_EALREADY as c_int {
303            return Ok(());
304        }
305
306        BleError::from_raw(rc)
307    }
308
309    #[cfg(not(esp_idf_bt_nimble_ext_adv))]
310    pub fn adv_stop(&self) -> Result<(), BleError> {
311        BleError::from_raw(unsafe { ble_gap_adv_stop() })
312    }
313
314    #[cfg(esp_idf_bt_nimble_ext_adv)]
315    pub fn ext_adv_configure(
316        &self,
317        instance: u8,
318        params: &BleExtAdvParams,
319    ) -> Result<i8, BleError> {
320        let raw: ble_gap_ext_adv_params = params.into();
321        let mut selected_tx_power: i8 = 0;
322
323        BleError::from_raw(unsafe {
324            ble_gap_ext_adv_configure(
325                instance,
326                &raw,
327                &mut selected_tx_power,
328                Some(super::BleSingleton::gap_event_cb),
329                ptr::null_mut(),
330            )
331        })?;
332
333        Ok(selected_tx_power)
334    }
335
336    #[cfg(esp_idf_bt_nimble_ext_adv)]
337    pub fn ext_adv_set_addr(&self, instance: u8, addr: &BleAddr) -> Result<(), BleError> {
338        BleError::from_raw(unsafe { ble_gap_ext_adv_set_addr(instance, addr.raw()) })
339    }
340
341    #[cfg(esp_idf_bt_nimble_ext_adv)]
342    pub fn ext_adv_set_data(&self, instance: u8, data: &[u8]) -> Result<(), BleError> {
343        let om = super::mbuf::mbuf_from_slice(data)?;
344
345        // `ble_gap_ext_adv_set_data` takes ownership of `om` and frees it on all paths (no leak, no double-free).
346        BleError::from_raw(unsafe { ble_gap_ext_adv_set_data(instance, om) })
347    }
348
349    /// Encode `fields` into an advertising payload and install it on `instance`.
350    #[cfg(esp_idf_bt_nimble_ext_adv)]
351    pub fn ext_adv_set_fields(&self, instance: u8, fields: &BleAdvFields) -> Result<(), BleError> {
352        let raw: ble_hs_adv_fields = fields.into();
353
354        let mut buf = [0u8; BLE_HS_ADV_MAX_SZ as usize];
355        let mut len: u8 = 0;
356        BleError::from_raw(unsafe {
357            ble_hs_adv_set_fields(&raw, buf.as_mut_ptr(), &mut len, buf.len() as u8)
358        })?;
359
360        self.ext_adv_set_data(instance, &buf[..len as usize])
361    }
362
363    #[cfg(esp_idf_bt_nimble_ext_adv)]
364    pub fn ext_adv_start(&self, instance: u8) -> Result<(), BleError> {
365        let rc = unsafe { ble_gap_ext_adv_start(instance, 0, 0) };
366        if rc == BLE_HS_EALREADY as c_int {
367            return Ok(());
368        }
369
370        BleError::from_raw(rc)
371    }
372
373    #[cfg(esp_idf_bt_nimble_ext_adv)]
374    pub fn ext_adv_stop(&self, instance: u8) -> Result<(), BleError> {
375        BleError::from_raw(unsafe { ble_gap_ext_adv_stop(instance) })
376    }
377}