Skip to main content

esp_idf_svc/ble/
l2cap.rs

1//! NimBLE L2CAP connection-oriented channels (CoC): a credit-based data pipe that runs *parallel*
2//! to GATT (both over a GAP connection), exposed as operations on the [`BleDriver`].
3//!
4//! Requires a NimBLE build with CoC enabled (`CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM > 0`); the whole
5//! module is `#[cfg]`-gated on that. A channel is opened either by listening on a PSM
6//! ([`l2cap_create_server`](BleDriver::l2cap_create_server)) or by connecting to a peer's PSM
7//! ([`l2cap_connect`](BleDriver::l2cap_connect)). Both, plus received SDUs and flow-control
8//! notifications, are delivered to the single [`l2cap_subscribe`](BleDriver::l2cap_subscribe) hook.
9//!
10//! Flow control is credit-based and manual: after handling a [`L2capEvent::Received`] you replenish
11//! the peer's credits with [`l2cap_recv_ready`](BleDriver::l2cap_recv_ready); a
12//! [`l2cap_send`](BleDriver::l2cap_send) that
13//! runs out of credits reports [`SendOutcome::Stalled`] and resumes on [`L2capEvent::TxUnstalled`].
14
15use core::ffi::{c_int, c_void};
16
17use crate::sys::*;
18
19use super::mbuf::Mbuf;
20use super::{BleDriver, BleError, ConnHandle};
21
22// See `mbuf.rs`: on chips whose BLE controller lives in ROM (`SOC_ESP_NIMBLE_CONTROLLER`: the
23// c2/c5/c6/c61/h2), the low-level os_mbuf / os_msys primitives are ROM-aliased, so `r_<name>` is the
24// only name bindgen emits there. (`ble_hs_mbuf_*` are host functions and are *not* aliased.)
25#[cfg(all(esp_idf_soc_esp_nimble_controller, esp_idf_bt_controller_enabled))]
26use crate::sys::r_os_mbuf_free_chain as os_mbuf_free_chain;
27#[cfg(all(esp_idf_soc_esp_nimble_controller, esp_idf_bt_controller_enabled))]
28use crate::sys::r_os_msys_get_pkthdr as os_msys_get_pkthdr;
29
30/// An opaque handle to an open L2CAP channel (wraps `*mut ble_l2cap_chan`).
31///
32/// # Validity
33///
34/// A handle is valid only while its channel is open — from [`L2capEvent::Connected`] /
35/// [`L2capEvent::Accept`] until the matching [`L2capEvent::Disconnected`]. Using it afterwards is
36/// undefined behavior (it dereferences freed NimBLE state). It is `Send`/`Sync` so it can be stashed
37/// and used from another task (NimBLE serializes internally with its own lock) — which is exactly
38/// why honoring the "not after `Disconnected`" rule is the caller's responsibility.
39#[derive(Clone, Copy)]
40pub struct L2capChan(*mut ble_l2cap_chan);
41
42// The pointer is an opaque NimBLE handle; all access goes through NimBLE's internally-locked API.
43unsafe impl Send for L2capChan {}
44unsafe impl Sync for L2capChan {}
45
46/// The result of a non-erroring [`l2cap_send`](BleDriver::l2cap_send).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SendOutcome {
49    /// The whole SDU was handed to the controller.
50    Sent,
51    /// Ran out of peer credits mid-SDU; the remainder is queued and transmission resumes once
52    /// [`L2capEvent::TxUnstalled`] fires. Do not send again until then.
53    Stalled,
54}
55
56/// An L2CAP CoC event, delivered on the host task to the single
57/// [`l2cap_subscribe`](BleDriver::l2cap_subscribe) hook. The hook returns an ATT-style status
58/// (`0` = ok); it is only consulted for [`Accept`](Self::Accept), where non-zero rejects the peer.
59pub enum L2capEvent<'a> {
60    /// A channel opened by [`l2cap_connect`](BleDriver::l2cap_connect) finished connecting; check
61    /// `status` (`0` = success) before using `chan`.
62    Connected {
63        conn_handle: ConnHandle,
64        status: i32,
65        chan: L2capChan,
66    },
67    /// A channel disconnected. `chan` must not be used after this event.
68    Disconnected {
69        conn_handle: ConnHandle,
70        chan: L2capChan,
71    },
72    /// An incoming connection to one of our [`l2cap_create_server`](BleDriver::l2cap_create_server)
73    /// PSMs. The handler must provide the first receive buffer by calling
74    /// [`l2cap_recv_ready`](BleDriver::l2cap_recv_ready) on `chan`, and may reject the peer by
75    /// returning non-zero from the hook.
76    Accept {
77        conn_handle: ConnHandle,
78        peer_sdu_size: u16,
79        chan: L2capChan,
80    },
81    /// An SDU was received. `data` is valid only for the duration of the call. After handling it,
82    /// replenish the peer's credits with [`l2cap_recv_ready`](BleDriver::l2cap_recv_ready).
83    Received {
84        conn_handle: ConnHandle,
85        chan: L2capChan,
86        data: Mbuf<'a>,
87    },
88    /// A previously [`Stalled`](SendOutcome::Stalled) send can continue (credits replenished).
89    /// `status` is non-zero only on an allocation error mid-SDU.
90    TxUnstalled {
91        conn_handle: ConnHandle,
92        status: i32,
93        chan: L2capChan,
94    },
95    /// A channel MTU reconfiguration completed. `by_peer` is `false` for a local reconfigure
96    /// completing (`RECONFIG_COMPLETED`) and `true` when the peer reconfigured us
97    /// (`PEER_RECONFIGURED`).
98    Reconfigured {
99        conn_handle: ConnHandle,
100        status: i32,
101        chan: L2capChan,
102        by_peer: bool,
103    },
104}
105
106impl<'a> L2capEvent<'a> {
107    /// Build an [`L2capEvent`] from a raw NimBLE `ble_l2cap_event`. Returns `None` for event types
108    /// this wrapper does not model.
109    pub(crate) fn from_raw(event: &'a ble_l2cap_event) -> Option<Self> {
110        let anon = &event.__bindgen_anon_1;
111
112        Some(match event.type_ as u32 {
113            BLE_L2CAP_EVENT_COC_CONNECTED => {
114                let e = unsafe { &anon.connect };
115                Self::Connected {
116                    conn_handle: e.conn_handle,
117                    status: e.status,
118                    chan: L2capChan(e.chan),
119                }
120            }
121            BLE_L2CAP_EVENT_COC_DISCONNECTED => {
122                let e = unsafe { &anon.disconnect };
123                Self::Disconnected {
124                    conn_handle: e.conn_handle,
125                    chan: L2capChan(e.chan),
126                }
127            }
128            BLE_L2CAP_EVENT_COC_ACCEPT => {
129                let e = unsafe { &anon.accept };
130                Self::Accept {
131                    conn_handle: e.conn_handle,
132                    peer_sdu_size: e.peer_sdu_size,
133                    chan: L2capChan(e.chan),
134                }
135            }
136            BLE_L2CAP_EVENT_COC_DATA_RECEIVED => {
137                let e = unsafe { &anon.receive };
138                Self::Received {
139                    conn_handle: e.conn_handle,
140                    chan: L2capChan(e.chan),
141                    data: Mbuf::from_raw(e.sdu_rx),
142                }
143            }
144            BLE_L2CAP_EVENT_COC_TX_UNSTALLED => {
145                let e = unsafe { &anon.tx_unstalled };
146                Self::TxUnstalled {
147                    conn_handle: e.conn_handle,
148                    status: e.status,
149                    chan: L2capChan(e.chan),
150                }
151            }
152            BLE_L2CAP_EVENT_COC_RECONFIG_COMPLETED | BLE_L2CAP_EVENT_COC_PEER_RECONFIGURED => {
153                let e = unsafe { &anon.reconfigured };
154                Self::Reconfigured {
155                    conn_handle: e.conn_handle,
156                    status: e.status,
157                    chan: L2capChan(e.chan),
158                    by_peer: event.type_ as u32 == BLE_L2CAP_EVENT_COC_PEER_RECONFIGURED,
159                }
160            }
161            _ => return None,
162        })
163    }
164}
165
166/// Free an mbuf whose ownership NimBLE handed to us (a received SDU, or a buffer a failing call did
167/// not take). Null-safe.
168pub(crate) fn free_mbuf(om: *mut os_mbuf) {
169    if !om.is_null() {
170        unsafe { os_mbuf_free_chain(om) };
171    }
172}
173
174/// Allocate a receive/transmit SDU buffer of `size` bytes from the system mbuf pool (`os_msys`).
175fn alloc_sdu(size: u16) -> Result<*mut os_mbuf, BleError> {
176    let om = unsafe { os_msys_get_pkthdr(size, 0) };
177    if om.is_null() {
178        Err(BleError::new(BLE_HS_ENOMEM as c_int))
179    } else {
180        Ok(om)
181    }
182}
183
184/// L2CAP CoC operations on the [`BleDriver`]. Available for any role (`S`); a channel just needs a
185/// GAP connection underneath. `&self`, so callable re-entrantly from within the L2CAP hook (e.g.
186/// calling [`l2cap_recv_ready`](Self::l2cap_recv_ready) from an [`Accept`](L2capEvent::Accept)).
187impl<'d, S> BleDriver<'d, S> {
188    /// Subscribe to L2CAP CoC events ([`L2capEvent`]) — connection lifecycle, received SDUs, and
189    /// flow-control notifications for every channel.
190    pub fn l2cap_subscribe<F>(&self, callback: F)
191    where
192        F: for<'a> FnMut(L2capEvent<'a>) -> i32 + Send + 'static,
193    {
194        unsafe { self.l2cap_subscribe_nonstatic(callback) }
195    }
196
197    /// # Safety
198    ///
199    /// The non-`'static` counterpart of [`l2cap_subscribe`](Self::l2cap_subscribe). See
200    /// [`BleDriver::host_subscribe_nonstatic`](crate::ble::BleDriver::host_subscribe_nonstatic) for
201    /// the borrowing rules and the `core::mem::forget` hazard.
202    pub unsafe fn l2cap_subscribe_nonstatic<F>(&self, callback: F)
203    where
204        F: for<'a> FnMut(L2capEvent<'a>) -> i32 + Send + 'd,
205    {
206        unsafe { super::SINGLETON.l2cap.subscribe_nonstatic(callback) };
207    }
208
209    /// Stop delivering L2CAP events to the subscribed hook.
210    pub fn l2cap_unsubscribe(&self) {
211        super::SINGLETON.l2cap.unsubscribe();
212    }
213
214    /// Listen for incoming L2CAP CoC connections on `psm`, negotiating an MTU of `mtu`. Incoming
215    /// connections arrive as [`L2capEvent::Accept`] on the L2CAP hook. May be called at runtime
216    /// (unlike a GATT service table, which is fixed at construction).
217    pub fn l2cap_create_server(&self, psm: u16, mtu: u16) -> Result<(), BleError> {
218        BleError::from_raw(unsafe {
219            ble_l2cap_create_server(
220                psm,
221                mtu,
222                Some(super::BleSingleton::l2cap_event_cb),
223                core::ptr::null_mut(),
224            )
225        })
226    }
227
228    /// Open an L2CAP CoC to the peer's `psm` over the existing connection `conn_handle`, negotiating
229    /// an MTU of `mtu`. The outcome arrives as [`L2capEvent::Connected`]. The initial receive buffer
230    /// is allocated internally (`mtu` bytes from `os_msys`).
231    pub fn l2cap_connect(
232        &self,
233        conn_handle: ConnHandle,
234        psm: u16,
235        mtu: u16,
236    ) -> Result<(), BleError> {
237        let sdu_rx = alloc_sdu(mtu)?;
238
239        let rc = unsafe {
240            ble_l2cap_connect(
241                conn_handle,
242                psm,
243                mtu,
244                sdu_rx,
245                Some(super::BleSingleton::l2cap_event_cb),
246                core::ptr::null_mut(),
247            )
248        };
249
250        // `ble_l2cap_connect` only takes ownership of `sdu_rx` once it has allocated the channel;
251        // its two pre-allocation failure paths are `EINVAL` (null args — impossible here) and
252        // `ENOTCONN`. Every later error path frees the buffer via `ble_l2cap_chan_free`, and its
253        // `ENOMEM` is returned from *both* sides of that line, so it is not safe to free on. Free
254        // only on the unambiguous pre-allocation codes — this avoids a double-free at the cost of a
255        // possible leak on the rare mid-connect allocation failure.
256        if rc == BLE_HS_ENOTCONN as c_int || rc == BLE_HS_EINVAL as c_int {
257            free_mbuf(sdu_rx);
258        }
259
260        BleError::from_raw(rc)
261    }
262
263    /// Send `data` as one SDU over `chan`. On success returns [`SendOutcome::Sent`]; if the peer's
264    /// credits run out mid-SDU it returns [`SendOutcome::Stalled`] and the remainder resumes on
265    /// [`L2capEvent::TxUnstalled`] (do not call again until then).
266    pub fn l2cap_send(&self, chan: L2capChan, data: &[u8]) -> Result<SendOutcome, BleError> {
267        // NimBLE fragments/copies the SDU into its own buffers, so `data` need not outlive the call.
268        let sdu =
269            unsafe { ble_hs_mbuf_from_flat(data.as_ptr() as *const c_void, data.len() as u16) };
270        if sdu.is_null() {
271            return Err(BleError::new(BLE_HS_ENOMEM as c_int));
272        }
273
274        let rc = unsafe { ble_l2cap_send(chan.0, sdu) };
275
276        match rc as u32 {
277            0 => Ok(SendOutcome::Sent),
278            BLE_HS_ESTALLED => Ok(SendOutcome::Stalled),
279            // `ble_l2cap_coc_send` takes ownership of `sdu` except on its two pre-ownership returns
280            // `EBADDATA` (SDU larger than the channel MTU) and `EBUSY`; free it back on those.
281            BLE_HS_EBADDATA | BLE_HS_EBUSY => {
282                free_mbuf(sdu);
283                BleError::from_raw(rc).map(|()| SendOutcome::Sent)
284            }
285            _ => BleError::from_raw(rc).map(|()| SendOutcome::Sent),
286        }
287    }
288
289    /// Signal readiness to receive another SDU of up to `sdu_size` bytes on `chan`, replenishing the
290    /// peer's credits. Call this to provide the first buffer on [`L2capEvent::Accept`] and to
291    /// re-arm after each [`L2capEvent::Received`]. The buffer is allocated internally from `os_msys`.
292    pub fn l2cap_recv_ready(&self, chan: L2capChan, sdu_size: u16) -> Result<(), BleError> {
293        let sdu_rx = alloc_sdu(sdu_size)?;
294
295        let rc = unsafe { ble_l2cap_recv_ready(chan.0, sdu_rx) };
296
297        // `ble_l2cap_coc_recv_ready` stores `sdu_rx` before its `ENOENT` return, so that path owns
298        // it; only its pre-store `EINVAL` (null — impossible here) and `EBUSY` returns leave it with
299        // us to free.
300        if rc == BLE_HS_EBUSY as c_int || rc == BLE_HS_EINVAL as c_int {
301            free_mbuf(sdu_rx);
302        }
303
304        BleError::from_raw(rc)
305    }
306
307    /// Disconnect the L2CAP channel `chan`. The completion arrives as [`L2capEvent::Disconnected`].
308    pub fn l2cap_disconnect(&self, chan: L2capChan) -> Result<(), BleError> {
309        BleError::from_raw(unsafe { ble_l2cap_disconnect(chan.0) })
310    }
311}