diff --git a/examples/src/bin/ble_bas_central.rs b/examples/src/bin/ble_bas_central.rs index b8475c4b..1e8f0752 100644 --- a/examples/src/bin/ble_bas_central.rs +++ b/examples/src/bin/ble_bas_central.rs @@ -82,4 +82,15 @@ async fn main(spawner: Spawner) { // Read to check it's changed let val = unwrap!(client.battery_level_read().await); info!("read battery level: {}", val); + + // Enable battery level notifications from the peripheral + client.battery_level_cccd_write(true).await.unwrap(); + + // Receive notifications + gatt_client::run(&conn, &client, |event| match event { + BatteryServiceClientEvent::BatteryLevelNotification(val) => { + info!("battery level notification: {}", val); + } + }) + .await; } diff --git a/nrf-softdevice-macro/src/lib.rs b/nrf-softdevice-macro/src/lib.rs index abcf9907..717d119a 100644 --- a/nrf-softdevice-macro/src/lib.rs +++ b/nrf-softdevice-macro/src/lib.rs @@ -517,6 +517,7 @@ pub fn gatt_client(args: TokenStream, item: TokenStream) -> TokenStream { let mut code_disc_char = TokenStream2::new(); let mut code_disc_done = TokenStream2::new(); let mut code_event_enum = TokenStream2::new(); + let mut code_on_hvx = TokenStream2::new(); let ble = quote!(::nrf_softdevice::ble); @@ -537,6 +538,7 @@ pub fn gatt_client(args: TokenStream, item: TokenStream) -> TokenStream { let write_fn = format_ident!("{}_write", ch.name); let write_wor_fn = format_ident!("{}_write_without_response", ch.name); let write_try_wor_fn = format_ident!("{}_try_write_without_response", ch.name); + let cccd_write_fn = format_ident!("{}_cccd_write", ch.name); let fn_vis = ch.vis.clone(); let uuid = ch.args.uuid; @@ -647,6 +649,55 @@ pub fn gatt_client(args: TokenStream, item: TokenStream) -> TokenStream { code_event_enum.extend(quote_spanned!(ch.span=> #case_notification(#ty), )); + code_on_hvx.extend(quote_spanned!(ch.span=> + if handle == self.#value_handle && type_ == ::nrf_softdevice::ble::gatt_client::HvxType::Notification { + if data.len() < #ty_as_val::MIN_SIZE { + return None; + } else { + return Some(#event_enum_name::#case_notification(#ty_as_val::from_gatt(data))); + } + } + )); + + if !indicate { + code_impl.extend(quote_spanned!(ch.span=> + #fn_vis async fn #cccd_write_fn(&self, notifications: bool) -> Result<(), #ble::gatt_client::WriteError> { + #ble::gatt_client::write(&self.conn, self.#cccd_handle, &[if notifications { 0x01 } else { 0x00 }, 0x00]).await + } + )); + } + } + + if indicate { + let case_indication = format_ident!("{}Indication", name_pascal); + code_event_enum.extend(quote_spanned!(ch.span=> + #case_indication(#ty), + )); + code_on_hvx.extend(quote_spanned!(ch.span=> + if handle == self.#value_handle && type_ == ::nrf_softdevice::ble::gatt_client::HvxType::Indication { + if data.len() < #ty_as_val::MIN_SIZE { + return None; + } else { + return Some(#event_enum_name::#case_indication(#ty_as_val::from_gatt(data))); + } + } + )); + + if !notify { + code_impl.extend(quote_spanned!(ch.span=> + #fn_vis async fn #cccd_write_fn(&self, indications: bool) -> Result<(), #ble::gatt_client::WriteError> { + #ble::gatt_client::write(&self.conn, self.#cccd_handle, &[if indications { 0x02 } else { 0x00 }, 0x00]).await + } + )); + } + } + + if indicate && notify { + code_impl.extend(quote_spanned!(ch.span=> + #fn_vis async fn #cccd_write_fn(&self, indications: bool, notifications: bool) -> Result<(), #ble::gatt_client::WriteError> { + #ble::gatt_client::write(&self.conn, self.#cccd_handle, &[if indications { 0x02 } else { 0x00 } | if notifications { 0x01 } else { 0x00 }, 0x00]).await + } + )); } } @@ -662,7 +713,14 @@ pub fn gatt_client(args: TokenStream, item: TokenStream) -> TokenStream { } impl #ble::gatt_client::Client for #struct_name { - //type Event = #event_enum_name; + type Event = #event_enum_name; + + fn on_hvx(&self, _conn: &::nrf_softdevice::ble::Connection, type_: ::nrf_softdevice::ble::gatt_client::HvxType, handle: u16, data: &[u8]) -> Option { + use #ble::gatt_client::Client; + + #code_on_hvx + None + } fn uuid() -> #ble::Uuid { #uuid diff --git a/nrf-softdevice/src/ble/gatt_client.rs b/nrf-softdevice/src/ble/gatt_client.rs index d854a957..c80e8202 100644 --- a/nrf-softdevice/src/ble/gatt_client.rs +++ b/nrf-softdevice/src/ble/gatt_client.rs @@ -21,8 +21,38 @@ pub struct Descriptor { pub handle: u16, } +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum HvxType { + Invalid = 0, + Notification, + Indication, +} + +pub struct InvalidHvxTypeError; + +impl TryFrom for HvxType { + type Error = InvalidHvxTypeError; + + fn try_from(value: u8) -> Result { + match u32::from(value) { + raw::BLE_GATT_HVX_INVALID => Ok(HvxType::Invalid), + raw::BLE_GATT_HVX_NOTIFICATION => Ok(HvxType::Notification), + raw::BLE_GATT_HVX_INDICATION => Ok(HvxType::Indication), + _ => Err(InvalidHvxTypeError), + } + } +} + /// Trait for implementing GATT clients. pub trait Client { + type Event; + + /// Handles notification and indication events from the GATT server. + fn on_hvx(&self, conn: &Connection, type_: HvxType, handle: u16, data: &[u8]) -> Option; + /// Get the UUID of the GATT service. This is used by [`discover`] to search for the /// service in the GATT server. fn uuid() -> Uuid; @@ -579,3 +609,54 @@ 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] } + +pub async fn run<'a, F, C>(conn: &Connection, client: &C, mut f: F) -> DisconnectedError +where + F: FnMut(C::Event), + C: Client, +{ + let handle = match conn.with_state(|state| state.check_connected()) { + Ok(handle) => handle, + Err(e) => return e, + }; + + portal(handle) + .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 { + return Some(DisconnectedError); + } + + // We have a GATTC event + let gattc_evt = get_union_field(ble_evt, &ble_evt.evt.gattc_evt); + let conn = unwrap!(Connection::from_handle(gattc_evt.conn_handle)); + let evt = match ble_evt.header.evt_id as u32 { + raw::BLE_GATTC_EVTS_BLE_GATTC_EVT_HVX => { + let params = get_union_field(ble_evt, &gattc_evt.params.hvx); + let v = get_flexarray(ble_evt, ¶ms.data, params.len as usize); + trace!( + "GATT_HVX write handle={:?} type={:?} data={:?}", + params.handle, + params.type_, + v + ); + + match params.type_.try_into() { + Ok(type_) => client.on_hvx(&conn, type_, params.handle, v), + Err(_) => { + error!("gatt_client invalid hvx type: {}", params.type_); + None + } + } + } + _ => None, + }; + + if let Some(evt) = evt { + f(evt); + } + + None + }) + .await +}