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}