diff --git a/nrf-softdevice-macro/src/lib.rs b/nrf-softdevice-macro/src/lib.rs index 717d119a..adcdc647 100644 --- a/nrf-softdevice-macro/src/lib.rs +++ b/nrf-softdevice-macro/src/lib.rs @@ -71,6 +71,7 @@ pub fn gatt_server(_args: TokenStream, item: TokenStream) -> TokenStream { let mut code_register_init = TokenStream2::new(); let mut code_on_write = TokenStream2::new(); + let mut code_on_notify_complete = TokenStream2::new(); let mut code_event_enum = TokenStream2::new(); let ble = quote!(::nrf_softdevice::ble); @@ -96,6 +97,12 @@ pub fn gatt_server(_args: TokenStream, item: TokenStream) -> TokenStream { return Some(#event_enum_name::#name_pascal(e)); } )); + + code_on_notify_complete.extend(quote_spanned!(span=> + if let Some(e) = self.#name.on_notify_complete(handle) { + return Some(#event_enum_name::#name_pascal(e)); + } + )); } } @@ -127,6 +134,13 @@ pub fn gatt_server(_args: TokenStream, item: TokenStream) -> TokenStream { #code_on_write None } + + fn on_notify_complete(&self, _conn: &::nrf_softdevice::ble::Connection, handle: u16) -> Option { + use #ble::gatt_server::Service; + + #code_on_notify_complete + None + } } }; @@ -213,6 +227,7 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { let mut code_build_chars = TokenStream2::new(); let mut code_struct_init = TokenStream2::new(); let mut code_on_write = TokenStream2::new(); + let mut code_on_notify_complete = TokenStream2::new(); let mut code_event_enum = TokenStream2::new(); let ble = quote!(::nrf_softdevice::ble); @@ -333,6 +348,17 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { } )); + // Emit a per-char DidNotify event when the notification completes + let case_did_notify = format_ident!("{}DidNotify", name_pascal); + code_event_enum.extend(quote_spanned!(ch.span=> + #case_did_notify, + )); + code_on_notify_complete.extend(quote_spanned!(ch.span=> + if handle == self.#value_handle { + return Some(#event_enum_name::#case_did_notify); + } + )); + if !indicate { let case_cccd_write = format_ident!("{}CccdWrite", name_pascal); @@ -433,6 +459,11 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { #code_on_write None } + + fn on_notify_complete(&self, handle: u16) -> Option { + #code_on_notify_complete + None + } } #[allow(unused)] diff --git a/nrf-softdevice/src/ble/gatt_server.rs b/nrf-softdevice/src/ble/gatt_server.rs index 3c183e02..354e84bc 100644 --- a/nrf-softdevice/src/ble/gatt_server.rs +++ b/nrf-softdevice/src/ble/gatt_server.rs @@ -3,6 +3,7 @@ //! Typically the peripheral device is the GATT server, but it is not necessary. //! In a connection any device can be server and client, and even both can be both at the same time. +use core::cell::UnsafeCell; use core::convert::TryFrom; use crate::ble::*; @@ -127,6 +128,15 @@ pub trait Server: Sized { None } + /// Callback for a per-characteristic notification completion. + /// + /// Called once for each completed notification with the characteristic's value handle. + /// Default implementation does nothing. + fn on_notify_complete(&self, conn: &Connection, handle: u16) -> Option { + let _ = (conn, handle); + None + } + /// Callback to indicate that the indication of a characteristic has been received by the client. fn on_indicate_confirm(&self, conn: &Connection, handle: u16) -> Option { let _ = (conn, handle); @@ -149,6 +159,13 @@ pub trait Service: Sized { type Event; fn on_write(&self, handle: u16, data: &[u8]) -> Option; + + /// Handle per-characteristic notification completion. + /// + /// Default implementation does nothing. + fn on_notify_complete(&self, _handle: u16) -> Option { + None + } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -177,6 +194,8 @@ where .wait_many(|ble_evt| unsafe { let ble_evt = &*ble_evt; if u32::from(ble_evt.header.evt_id) == raw::BLE_GAP_EVTS_BLE_GAP_EVT_DISCONNECTED { + // Clear any pending notify tracking for this connection + notify_tracker_clear(conn_handle); return Some(DisconnectedError); } @@ -247,7 +266,20 @@ where } raw::BLE_GATTS_EVTS_BLE_GATTS_EVT_HVN_TX_COMPLETE => { let params = get_union_field(ble_evt, &gatts_evt.params.hvn_tx_complete); - server.on_notify_tx_complete(&conn, params.count) + // Emit per-notification completion events using the queued handles + let count = params.count; + for _ in 0..count { + if let Some(handle) = notify_tracker_pop(gatts_evt.conn_handle) { + if let Some(evt) = server.on_notify_complete(&conn, handle) { + f(evt) + } + } else { + // Queue underrun, nothing to pop + break; + } + } + // Backwards-compatible aggregate callback + server.on_notify_tx_complete(&conn, count) } raw::BLE_GATTS_EVTS_BLE_GATTS_EVT_HVC => { let params = get_union_field(ble_evt, &gatts_evt.params.hvc); @@ -353,6 +385,8 @@ pub fn notify_value(conn: &Connection, handle: u16, val: &[u8]) -> Result<(), No }; let ret = unsafe { raw::sd_ble_gatts_hvx(conn_handle, ¶ms) }; RawError::convert(ret)?; + // Track the handle so we can emit per-characteristic completion events when TX completes. + notify_tracker_push(conn_handle, handle); Ok(()) } @@ -473,3 +507,88 @@ static PORTALS: [Portal<*const raw::ble_evt_t>; CONNS_MAX] = [PORTAL_NEW; CONNS_ pub(crate) fn portal(conn_handle: u16) -> &'static Portal<*const raw::ble_evt_t> { &PORTALS[conn_handle as usize] } + +// Simple per-connection FIFO to track the order of notified handles so we can +// emit per-characteristic completion callbacks upon BLE_GATTS_EVT_HVN_TX_COMPLETE. +const NOTIFY_TRACK_CAPACITY: usize = 16; + +struct NotifyQueue { + buf: [u16; NOTIFY_TRACK_CAPACITY], + head: u8, + tail: u8, +} + +impl NotifyQueue { + const fn new() -> Self { + Self { + buf: [0; NOTIFY_TRACK_CAPACITY], + head: 0, + tail: 0, + } + } + + #[inline(always)] + fn is_full(&self) -> bool { + self.len() == NOTIFY_TRACK_CAPACITY as u8 + } + + #[inline(always)] + fn is_empty(&self) -> bool { + self.head == self.tail + } + + #[inline(always)] + fn len(&self) -> u8 { + self.head.wrapping_sub(self.tail) + } + + #[inline(always)] + fn push(&mut self, handle: u16) { + if self.is_full() { + // Drop oldest to make room + self.pop(); + } + let idx = (self.head as usize) % NOTIFY_TRACK_CAPACITY; + unsafe { *self.buf.get_unchecked_mut(idx) = handle }; + self.head = self.head.wrapping_add(1); + } + + #[inline(always)] + fn pop(&mut self) -> Option { + if self.is_empty() { + return None; + } + let idx = (self.tail as usize) % NOTIFY_TRACK_CAPACITY; + let val = *unsafe { self.buf.get_unchecked(idx) }; + self.tail = self.tail.wrapping_add(1); + Some(val) + } + + #[inline(always)] + fn clear(&mut self) { + self.head = 0; + self.tail = 0; + } +} + +const NOTIFY_QUEUE_INIT: UnsafeCell = UnsafeCell::new(NotifyQueue::new()); +static mut NOTIFY_QUEUES: [UnsafeCell; CONNS_MAX] = [NOTIFY_QUEUE_INIT; CONNS_MAX]; + +fn notify_queue(conn_handle: u16) -> &'static mut NotifyQueue { + unsafe { &mut *NOTIFY_QUEUES[conn_handle as usize].get() } +} + +fn notify_tracker_push(conn_handle: u16, handle: u16) { + let q = notify_queue(conn_handle); + q.push(handle); +} + +fn notify_tracker_pop(conn_handle: u16) -> Option { + let q = notify_queue(conn_handle); + q.pop() +} + +fn notify_tracker_clear(conn_handle: u16) { + let q = notify_queue(conn_handle); + q.clear(); +}