embassy-rs / nrf-softdevice

Apache License 2.0
259 stars 79 forks source link

Receive notifications in gatt_client #102

Closed albertskog closed 11 months ago

albertskog commented 2 years ago

Is there a way for gatt_client to receive notifications? I am trying to understand nrf-softdevice-macro and I see there is some sort of event generated in nrf-softdevice-macro/src/lib.rs:

if notify {
    let case_notification = format_ident!("{}Notification", name_pascal);
    code_event_enum.extend(quote_spanned!(ch.span=>
        #case_notification(#ty),
    ));
}

But how do I capture it, there is no gatt_client::run() like there is with gatt_server? Maybe I'm looking in the wrong places..

Happy to contribute documentation and/or code if I can just get a little hint!

alexmoon commented 1 year ago

It doesn't look like notifications are currently supported. You would need to add handling for the BLE_GATTC_EVT_HVX event to gatt_client.rs and add support for writing to the Client Configuration Characteristic Descriptor to enable/disable notifications. PRs are welcome.

sureshjoshi commented 1 year ago

Is this still an open issue?

As in, if I use the NRF as a client and connect to a peripheral, I cannot receive notifications from that peripheral?

sureshjoshi commented 1 year ago

Okay, just in case anyone runs into this, there is a bit of a hack in the meantime to get some raw data out (I've never used Rust macros, so the "correct" solution will take me some time to wrap my head around before I can PR).

Given this gatt client:

#[nrf_softdevice::gatt_client(uuid = "00726f62-6f74-7061-6a61-6d61732e6361")]
struct MyClient {
    #[characteristic(uuid = "01726f62-6f74-7061-6a61-6d61732e6361", write)]
    tx: u8,
    #[characteristic(uuid = "02726f62-6f74-7061-6a61-6d61732e6361", read, notify)]
    rx: u8,
}

The existing macros will create what you need to set up notifications.

let mut config = central::ConnectConfig::default();
config.scan_config.whitelist = Some(addresses);
let connection = unwrap!(central::connect(sd, &config).await);
let my_client: MyClient = unwrap!(gatt_client::discover(&connection).await);

So, write to the characteristic descriptor with your notification/indication request:

// Write to the generated notify cccd handle with a 16-bit little-endian 0x0001 to setup notifications (0x0002 for indications)
unwrap!(gatt_client::write(&connection, my_client.rx_cccd_handle, &[0x01, 0x00]).await);

Then, use this new function to consume the raw ble_evt_t:

let result = gatt_client::run(&connection, &client, |e| {
    info!("Event: {:?}", e.header.evt_id);
}).await;

In gatt_client.rs, I've created a run function that currently just passes the BLE event straight down for manual consumption (setting this up with macro-generated events Ala gatt_server::run would be better).

pub async fn run<'m, F, C>(conn: &Connection, client: &C, mut f: F) -> DisconnectedError
where
    // HACK: Passing the event straight down
    F: FnMut(&ble_evt_t),
    C: Client,
{
    let conn_handle = match conn.with_state(|state| state.check_connected()) {
        Ok(handle) => handle,
        Err(DisconnectedError) => return DisconnectedError,
    };

    portal(conn_handle)
        .wait_many(|ble_evt| unsafe {
            info!("run: ble_evt={:?}", ble_evt);
            let ble_evt = &*ble_evt;
            if u32::from(ble_evt.header.evt_id) == raw::BLE_GAP_EVTS_BLE_GAP_EVT_DISCONNECTED {
                return Some(DisconnectedError);
            }

            // If evt_id is not BLE_GAP_EVTS_BLE_GAP_EVT_DISCONNECTED, then it must be 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: Option<ble_evt_t> = 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, &params.data, params.len as usize);
                    trace!("GATT_HVX write handle={:?} data={:?}", params.handle, v);
                    None
                }

                _ => None,
            };

            // HACK: Just passing the event straight down
            f(ble_evt);

            None
        })
        .await
}

Hopefully this helps someone, and if anyone would like to provide me some suggestions on how this API should look and how I should consider setting up the macros - I'd appreciate it. I can then run with it and submit a PR to do this properly.