embassy-rs / trouble

A Rust Host BLE stack with a future goal of qualification.
Apache License 2.0
118 stars 23 forks source link

Example how to handle events #72

Closed Syphixs closed 3 months ago

Syphixs commented 4 months ago

Hey! Thank you for your great work. I am coming from bleps and wanted to understand how I can handle events in trouble. For example how do i call a function when a write on a specific characteristic happens? I am closely looking at the ble_bas_peripheral.rs example in examples/nrf-sdc but I don't understand when the event is fired as this

    info!("Starting advertising and GATT service");
    let _ = join3(
        ble.run(),
        async {
            loop {
                info!("Reading next");
                match server.next().await {
                    Ok(event) => {
                        info!("Gatt event: {:?}", event);
                    }
                    Err(e) => {
                        error!("Error processing GATT events: {:?}", e);
                    }
                }
            }
        }, .....

only fires a single time at the beginning and never again. I thought i can just define a new custom service and characteristic like this:

    let mut custom_value = [0u8; 4]; // 4-byte value
    let custom_handle = {
        let mut svc = table.add_service(Service::new(0xFFFF)); 

        svc.add_characteristic(
            0xFFFE, // Custom 16-bit UUID for characteristic
            &[CharacteristicProp::Read, CharacteristicProp::Write],
            &mut custom_value,
        )
        .build()
    };

and then on a write event, which does work (as i can read the updated value) the event fires. maybe I am misunderstanding something here but the example sadly does not tell me how to use it properly yet. I would very much appreciate some guidance.

Syphixs commented 4 months ago

And currently the GattEvent Enum just handles Write. Is this planned to include Read as well in some time?

#[derive(Clone)]
pub enum GattEvent<'reference, 'values> {
    Write {
        connection: &'reference Connection<'reference>,
        handle: CharacteristicHandle,
        value: &'values [u8],
    },
}
lulf commented 4 months ago

@Syphixs Thanks for raising the issue. I think this is probably a bug, the write event should fire on every write as you expected. I will try to reproduce it and work on a fix.

I wasn't planning on including a read event, as it would just read from the attribute table and not require any logic in the app itself. What's the use case you have in mind for the read event? I modeled this after nrf-softdevice which really only has events for Write and 'Enable Notifications' (which is handled a bit differently in trouble, because you don't have to know if it is enabled or not).

Syphixs commented 4 months ago

Thank you!

Well i was thinking about a simple store which holds the last x events on the chip and waits till at least 2 devices have read an entry for example. If that happened it can safely remove the entry. Maybe this is a bad idea altogether but in bleps the store example works quite well actually. I have not yet included the second device logic for now its just a queue where on an read event i push it to the Store and on an write event with the correct message id it gets deleted, so basically just an ack.

use heapless::Vec;
...

const MAX_EVENTS: usize = 16;
#[derive(Copy, Clone, Debug)]
struct EventData {
    msg_uuid: [u8; 16],
    timestamp: u64,
    task_uuid: [u8; 16],
}

impl EventData {
    fn new(msg_uuid_bytes: [u8; 16], timestamp: u64, task_uuid_bytes: [u8; 16]) -> Self {
        Self {
            msg_uuid: msg_uuid_bytes,
            timestamp,
            task_uuid: task_uuid_bytes,
        }
    }
    fn packet(&self) -> [u8; 40] {
        let mut data = [0u8; 40];
        data[0..16].copy_from_slice(&self.msg_uuid);
        data[16..24].copy_from_slice(&self.timestamp.to_le_bytes());
        data[24..40].copy_from_slice(&self.task_uuid);
        data
    }
}

struct EventStorage {
    events: Vec<EventData, MAX_EVENTS>,
}

impl EventStorage {
    fn new() -> Self {
        Self { events: Vec::new() }
    }

    fn add_event(&mut self, event: EventData) {
        if self.events.len() == MAX_EVENTS {
            self.events.remove(0);
        }
        self.events.push(event).unwrap();
    }

    fn remove_event(&mut self, msg_uuid: &[u8; 16]) -> Option<EventData> {
        for i in 0..self.events.len() {
            if self.events[i].msg_uuid == *msg_uuid {
                return Some(self.events.remove(i));
            }
        }
        None
    }

    fn get_oldest_event(&self) -> Option<&EventData> {
        self.events.first()
    }

    fn clear_events(&mut self) {
        self.events.clear();
    }
}

If you have a simpler more elegant solution I am all ears :D

Syphixs commented 4 months ago

And here is my simple example code for easier reproducibility. Do mind that this is just for testing so I did not use everything and commented out a lot. Oh and I am using the nrf52833DK for testing.

main.rs

#![no_std]
#![no_main]
#![feature(impl_trait_in_assoc_type)]

use core::cell::RefCell;

use defmt::{error, info, unwrap};
use embassy_executor::Spawner;
use embassy_futures::join::join3;
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
use embassy_nrf::{bind_interrupts, pac};
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_time::{Duration, Timer};
use heapless::Vec;
use nrf_sdc::mpsl::MultiprotocolServiceLayer;
use nrf_sdc::{self as sdc, mpsl};
use sdc::rng_pool::RngPool;
use static_cell::StaticCell;
use trouble_host::advertise::{
    AdStructure, Advertisement, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE,
};
use trouble_host::attribute::{
    AttributeTable, CharacteristicHandle, CharacteristicProp, Service, Uuid,
};
use trouble_host::gatt::GattEvent;
use trouble_host::{Address, BleHost, BleHostResources, PacketQos};
use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(struct Irqs {
    RNG => nrf_sdc::rng_pool::InterruptHandler;
    SWI0_EGU0 => nrf_sdc::mpsl::LowPrioInterruptHandler;
    POWER_CLOCK => nrf_sdc::mpsl::ClockInterruptHandler;
    RADIO => nrf_sdc::mpsl::HighPrioInterruptHandler;
    TIMER0 => nrf_sdc::mpsl::HighPrioInterruptHandler;
    RTC0 => nrf_sdc::mpsl::HighPrioInterruptHandler;
});

const DEVICE_NAME_CHAR: u16 = 0x2a00;
const APPEARANCE_CHAR: u16 = 0x2a01;
const GENERAL_SERVICE: u16 = 0x1800;
const BATTERY_SERVICE: u16 = 0x180F;
const BATTERY_LEVEL_CHAR: u16 = 0x2a19;
const EVENT_SERVICE: u16 = 0xFFFF;
const EVENT_CHAR: u16 = 0xFFFE;

const MAX_EVENTS: usize = 16;
#[derive(Copy, Clone, Debug)]
struct EventData {
    msg_uuid: [u8; 16],
    timestamp: u64,
    task_uuid: [u8; 16],
}

impl EventData {
    fn new(msg_uuid_bytes: [u8; 16], timestamp: u64, task_uuid_bytes: [u8; 16]) -> Self {
        Self {
            msg_uuid: msg_uuid_bytes,
            timestamp,
            task_uuid: task_uuid_bytes,
        }
    }
    fn packet(&self) -> [u8; 40] {
        let mut data = [0u8; 40];
        data[0..16].copy_from_slice(&self.msg_uuid);
        data[16..24].copy_from_slice(&self.timestamp.to_le_bytes());
        data[24..40].copy_from_slice(&self.task_uuid);
        data
    }
}

struct EventStorage {
    events: Vec<EventData, MAX_EVENTS>,
}

impl EventStorage {
    fn new() -> Self {
        Self { events: Vec::new() }
    }

    fn add_event(&mut self, event: EventData) {
        if self.events.len() == MAX_EVENTS {
            self.events.remove(0);
        }
        self.events.push(event).unwrap();
    }

    fn remove_event(&mut self, msg_uuid: &[u8; 16]) -> Option<EventData> {
        for i in 0..self.events.len() {
            if self.events[i].msg_uuid == *msg_uuid {
                return Some(self.events.remove(i));
            }
        }
        None
    }

    fn get_oldest_event(&self) -> Option<&EventData> {
        self.events.first()
    }

    fn clear_events(&mut self) {
        self.events.clear();
    }
}

#[embassy_executor::task]
async fn mpsl_task(mpsl: &'static MultiprotocolServiceLayer<'static>) -> ! {
    mpsl.run().await
}

#[embassy_executor::task(pool_size = 1)]
async fn button_task(n: usize, mut pin: Input<'static>, mut led: Output<'static>) {
    loop {
        pin.wait_for_low().await;
        info!("Button {:?} pressed!", n);
        led.set_low();
        pin.wait_for_high().await;
        info!("Button {:?} released!", n);
        led.set_high();
    }
}

fn my_addr() -> Address {
    unsafe {
        let ficr = &*pac::FICR::ptr();
        let high = u64::from((ficr.deviceaddr[1].read().bits() & 0x0000ffff) | 0x0000c000);
        let addr = high << 32 | u64::from(ficr.deviceaddr[0].read().bits());
        Address::random(unwrap!(addr.to_le_bytes()[..6].try_into()))
    }
}

/// Size of L2CAP packets (ATT MTU is this - 4)
const L2CAP_MTU: usize = 44;

/// Max number of connections
const CONNECTIONS_MAX: usize = 1;

/// Max number of L2CAP channels.
const L2CAP_CHANNELS_MAX: usize = 2; // Signal + att

fn build_sdc<'d, const N: usize>(
    p: nrf_sdc::Peripherals<'d>,
    rng: &'d RngPool,
    mpsl: &'d MultiprotocolServiceLayer,
    mem: &'d mut sdc::Mem<N>,
) -> Result<nrf_sdc::SoftdeviceController<'d>, nrf_sdc::Error> {
    sdc::Builder::new()?
        .support_adv()?
        .support_peripheral()?
        .peripheral_count(1)?
        .build(p, rng, mpsl, mem)
}

struct GattData {
    id: [u8; 10],
    appearance: [u8; 2],
    event: [u8; 40],
    battery_level: [u8; 1],
}

// Structure to hold characteristic handles
struct GattCharacteristics {
    event: CharacteristicHandle,
    battery_level: CharacteristicHandle,
}

fn setup_gatt_table<'a>(
    table: &mut AttributeTable<'a, NoopRawMutex, 20>,
    data: &'a mut GattData,
) -> GattCharacteristics {
    // Generic Access Service (mandatory)
    let mut svc = table.add_service(Service::new(GENERAL_SERVICE));
    let _ = svc.add_characteristic_ro(DEVICE_NAME_CHAR, &data.id);
    let _ = svc.add_characteristic_ro(APPEARANCE_CHAR, &data.appearance[..]);
    svc.build();

    let battery_char = {
        // Generic attribute service (mandatory)
        table.add_service(Service::new(0x1801));
        // Battery service
        let mut svc = table.add_service(Service::new(BATTERY_SERVICE));

        svc.add_characteristic(
            BATTERY_LEVEL_CHAR,
            &[CharacteristicProp::Read, CharacteristicProp::Notify],
            &mut data.battery_level,
        )
        .build()
    };

    let event_char = {
        let mut svc = table.add_service(Service::new(EVENT_SERVICE)); // Custom 16-bit UUID

        svc.add_characteristic(
            EVENT_CHAR,
            &[CharacteristicProp::Read, CharacteristicProp::Write],
            &mut data.event,
        )
        .build()
    };

    GattCharacteristics {
        event: event_char,
        battery_level: battery_char,
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());

    let mut led = Output::new(p.P0_13, Level::High, OutputDrive::Standard);
    let mut led2 = Output::new(p.P0_14, Level::High, OutputDrive::Standard);
    // let led2_ref = RefCell::new(led2_pin);
    let btn1 = Input::new(p.P0_11, Pull::Up);
    let btn2 = Input::new(p.P0_12, Pull::Up);
    let btn3 = Input::new(p.P0_24, Pull::Up);
    let btn4 = Input::new(p.P0_25, Pull::Up);

    // button.wait_for_rising_edge().await;
    // info!("Button pressed?");

    let pac_p = pac::Peripherals::take().unwrap();

    let mpsl_p = mpsl::Peripherals::new(
        pac_p.CLOCK,
        pac_p.RADIO,
        p.RTC0,
        p.TIMER0,
        p.TEMP,
        p.PPI_CH19,
        p.PPI_CH30,
        p.PPI_CH31,
    );
    let lfclk_cfg = mpsl::raw::mpsl_clock_lfclk_cfg_t {
        source: mpsl::raw::MPSL_CLOCK_LF_SRC_RC as u8,
        rc_ctiv: mpsl::raw::MPSL_RECOMMENDED_RC_CTIV as u8,
        rc_temp_ctiv: mpsl::raw::MPSL_RECOMMENDED_RC_TEMP_CTIV as u8,
        accuracy_ppm: mpsl::raw::MPSL_DEFAULT_CLOCK_ACCURACY_PPM as u16,
        skip_wait_lfclk_started: mpsl::raw::MPSL_DEFAULT_SKIP_WAIT_LFCLK_STARTED != 0,
    };
    static MPSL: StaticCell<MultiprotocolServiceLayer> = StaticCell::new();
    let mpsl = MPSL.init(unwrap!(mpsl::MultiprotocolServiceLayer::new(
        mpsl_p, Irqs, lfclk_cfg
    )));
    // Start all tasks
    spawner.must_spawn(mpsl_task(&*mpsl));
    unwrap!(spawner.spawn(button_task(1, btn1, led2)));

    let sdc_p = sdc::Peripherals::new(
        pac_p.ECB, pac_p.AAR, p.PPI_CH17, p.PPI_CH18, p.PPI_CH20, p.PPI_CH21, p.PPI_CH22,
        p.PPI_CH23, p.PPI_CH24, p.PPI_CH25, p.PPI_CH26, p.PPI_CH27, p.PPI_CH28, p.PPI_CH29,
    );

    let mut pool = [0; 256];
    let rng = sdc::rng_pool::RngPool::new(p.RNG, Irqs, &mut pool, 64);

    let mut sdc_mem = sdc::Mem::<3312>::new();
    let sdc = unwrap!(build_sdc(sdc_p, &rng, mpsl, &mut sdc_mem));

    info!("Our address = {:02x}", my_addr());
    Timer::after(Duration::from_millis(200)).await;

    static HOST_RESOURCES: StaticCell<
        BleHostResources<CONNECTIONS_MAX, L2CAP_CHANNELS_MAX, L2CAP_MTU>,
    > = StaticCell::new();
    let host_resources = HOST_RESOURCES.init(BleHostResources::new(PacketQos::None));

    let mut ble: BleHost<'_, _> = BleHost::new(sdc, host_resources);
    ble.set_random_address(my_addr());

    let event_storage = RefCell::new(EventStorage::new());

    // Set up GATT Table
    let id = b"NewTrouble";
    let mut gatt_data = GattData {
        id: *id,
        appearance: [0x80, 0x07],
        event: [0u8; 40],
        battery_level: [0u8; 1],
    };

    let mut table: AttributeTable<'_, NoopRawMutex, 20> = AttributeTable::new();
    let characteristics = setup_gatt_table(&mut table, &mut gatt_data);

    let server = ble.gatt_server(&table);
    let mut adv_data = [0; 31];
    unwrap!(AdStructure::encode_slice(
        &[
            AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
            AdStructure::ServiceUuids16(&[Uuid::Uuid16([0x0f, 0x18])]),
            AdStructure::CompleteLocalName(b"NewTrouble"),
        ],
        &mut adv_data[..],
    ));

    info!("Starting advertising and GATT service");
    let _ = join3(
        ble.run(),
        async {
            loop {
                info!("Reading next");
                match server.next().await {
                    Ok(event) => {
                        info!("Gatt event: {:?}", event);
                        match event {
                            GattEvent::Write {
                                connection,
                                handle,
                                value,
                            } => {
                                info!("event matched on write with connection");

                                // if handle == characteristics.event {
                                //     // Handle event write
                                //     if data.len() == 40 {
                                //         let mut msg_uuid = [0u8; 16];
                                //         let mut timestamp = [0u8; 8];
                                //         let mut task_uuid = [0u8; 16];
                                //         msg_uuid.copy_from_slice(&data[0..16]);
                                //         timestamp.copy_from_slice(&data[16..24]);
                                //         task_uuid.copy_from_slice(&data[24..40]);
                                //         let event_data = EventData::new(
                                //             msg_uuid,
                                //             u64::from_le_bytes(timestamp),
                                //             task_uuid,
                                //         );
                                //         event_storage.borrow_mut().add_event(event_data);
                                //     }
                                // }
                            }
                        }
                    }
                    Err(e) => {
                        error!("Error processing GATT events: {:?}", e);
                    }
                }
            }
        },
        async {
            let mut advertiser = unwrap!(
                ble.advertise(
                    &Default::default(),
                    Advertisement::ConnectableScannableUndirected {
                        adv_data: &adv_data[..],
                        scan_data: &[],
                    }
                )
                .await
            );
            let conn = unwrap!(advertiser.accept().await);
            // Keep connection alive
            let mut tick: u8 = 0;
            loop {
                Timer::after(Duration::from_secs(10)).await;
                info!("Tick happened");
                tick += 1;
                unwrap!(
                    server
                        .notify(characteristics.battery_level, &conn, &[tick])
                        .await
                );
                led.set_low();
                Timer::after_millis(300).await;
                led.set_high();
            }
        },
    )
    .await;
}

cargo.toml

[package]
name = "nrf52833dk"
version = "0.1.0"
edition = "2021"

[dependencies]
heapless = { version = "*" }
embassy-executor = { version = "0.5", default-features = false, features = [
  "nightly",
  "arch-cortex-m",
  "executor-thread",
  "defmt",
  "integrated-timers",
  "executor-interrupt",
] }
embassy-time = { version = "0.3.0", default-features = false, features = [
  "defmt",
  "defmt-timestamp-uptime",
] }
embassy-nrf = { version = "0.1.0", default-features = false, features = [
  "defmt",
  "nrf52833",
  "time-driver-rtc1",
  "gpiote",
  "unstable-pac",
  "rt",
] }
embassy-futures = "0.1.1"
embassy-sync = { version = "0.6", features = ["defmt"] }

futures = { version = "0.3", default-features = false, features = [
  "async-await",
] }
nrf-sdc = { version = "0.1.0", default-features = false, features = [
  "defmt",
  "nrf52833",
  "peripheral",
  "central",
] }
nrf-mpsl = { version = "0.1.0", default-features = false, features = [
  "defmt",
  "critical-section-impl",
] }
bt-hci = { version = "0.1.0", default-features = false, features = ["defmt"] }

trouble-host = { version = "*", default-features = false, features = [
  "defmt",
  "gatt",
] }

defmt = "0.3"
defmt-rtt = "0.4.0"

cortex-m = { version = "0.7.6" }
cortex-m-rt = "0.7.0"
panic-probe = { version = "0.3", features = ["print-defmt"] }
static_cell = "2"

[patch.crates-io]
embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-nrf = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-futures = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-time-driver = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" }
nrf-sdc = { git = "https://github.com/alexmoon/nrf-sdc.git", branch = "main" }
nrf-mpsl = { git = "https://github.com/alexmoon/nrf-sdc.git", branch = "main" }
bt-hci = { git = "https://github.com/alexmoon/bt-hci.git", branch = "main" }
trouble-host = { git = "https://github.com/embassy-rs/trouble.git", branch = "main" }

[profile.release]
debug = 2

.cargo/config.toml

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# replace nRF82840_xxAA with your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip nRF52833_xxAA"

# rustflags = ["-C", "link-arg=-Tlink.x", "-C", "link-arg=-Tdefmt.x"]

[build]
target = "thumbv7em-none-eabi"
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi"    # Cortex-M3
# target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

[env]
DEFMT_LOG = "trace"

memory.x and build.rs are the example ones ofc with the correct settings for nrf52833

lulf commented 3 months ago

@Syphixs Thanks for the example! I'll create an integration test for gatt so that we can be sure it will continue to work.

Regarding the use case for the Read event, that seems valid, I just haven't seen GATT used in this way before. It should be fairly easy to add!

lulf commented 3 months ago

@Syphixs I've added an integration test for GATT. Turns out that events were never returned :see_no_evil: I've added read events as well now, and a minor mod to write events not returning the value. You can read that from the AttributeTable you pass to the gatt server (see ble_bas_peripheral for example).

Good news is that to write the integration test I also implemented a basic GATT client, so that's now supported as well along with an example (ble_bas_central).

That should solve the original issue, so I'm closing that. Feel free to reopen if it's not working for you.