embassy-rs / nrf-softdevice

Apache License 2.0
268 stars 80 forks source link

Do Advertising Extensions work? #156

Closed Ardelean-Calin closed 1 year ago

Ardelean-Calin commented 1 year ago

Used hardware: nRF52832 with 512kB flash and 64kB RAM. Used softdevice: S132 v7.3.0 Used example code: Slightly modified version of ble_bas_peripheral.rs, I will attach the code at the end of this issue.

As far as I could see in the code, it should be supported to configure and enable extended advertising extensions to have advertise data longer than 31 bytes. However, whenever I try it, it doesn't seem to work and I can't figure out why. I looked at the official advertising sequence chart on Nordic's website and the program flow seems to be all right... Why do I always get _NRF_ERROR_INVALIDLENGTH errors, then? What am I missing?

0.000000 WARN  sd_ble_gap_adv_set_configure err InvalidLength
└─ nrf_softdevice::ble::peripheral::start_adv::{closure#4} @ /home/calin/Projects/rustybutt-fw/nrf-softdevice/nrf-softdevice/src/fmt.rs:151
0.000000 WARN  sd_ble_gap_adv_stop: InvalidState
└─ nrf_softdevice::ble::peripheral::advertise_inner::{async_fn#0}::{closure#0} @ /home/calin/Projects/rustybutt-fw/nrf-softdevice/nrf-softdevice/src/fmt.rs:151
0.000000 ERROR panicked at 'unwrap failed: peripheral :: advertise_connectable(sd, adv, & config).await'
error: `Raw(InvalidLength)`
└─ ble_bas_peripheral::____embassy_main_task::{async_fn#0} @ src/bin/ble_bas_peripheral.rs:90
0.000000 ERROR panicked at 'explicit panic', /home/calin/.cargo/registry/src/github.com-1ecc6299db9ec823/defmt-0.3.2/src/lib.rs:367:5
└─ panic_probe::print_defmt::print @ /home/calin/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.3.0/src/lib.rs:91
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

#[path = "../example_common.rs"]
mod example_common;

use core::mem;

use defmt::{info, *};
use embassy_executor::Spawner;
use nrf_softdevice::ble::{gatt_server, peripheral};
use nrf_softdevice::{raw, Softdevice};

#[embassy_executor::task]
async fn softdevice_task(sd: &'static Softdevice) -> ! {
    sd.run().await
}

#[nrf_softdevice::gatt_service(uuid = "180f")]
struct BatteryService {
    #[characteristic(uuid = "2a19", read, notify)]
    battery_level: u8,
}

#[nrf_softdevice::gatt_service(uuid = "9e7312e0-2354-11eb-9f10-fbc30a62cf38")]
struct FooService {
    #[characteristic(uuid = "9e7312e0-2354-11eb-9f10-fbc30a63cf38", read, write, notify, indicate)]
    foo: u16,
}

#[nrf_softdevice::gatt_server]
struct Server {
    bas: BatteryService,
    foo: FooService,
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    info!("Hello World!");

    let config = nrf_softdevice::Config {
        clock: Some(raw::nrf_clock_lf_cfg_t {
            source: raw::NRF_CLOCK_LF_SRC_RC as u8,
            rc_ctiv: 4,
            rc_temp_ctiv: 2,
            accuracy: 7,
        }),
        conn_gap: Some(raw::ble_gap_conn_cfg_t {
            conn_count: 6,
            event_length: 24,
        }),
        conn_gatt: Some(raw::ble_gatt_conn_cfg_t { att_mtu: 256 }),
        gatts_attr_tab_size: Some(raw::ble_gatts_cfg_attr_tab_size_t {
            attr_tab_size: raw::BLE_GATTS_ATTR_TAB_SIZE_DEFAULT,
        }),
        gap_role_count: Some(raw::ble_gap_cfg_role_count_t {
            adv_set_count: 1,
            periph_role_count: 1,
            central_role_count: 0,
            central_sec_count: 0,
            _bitfield_1: raw::ble_gap_cfg_role_count_t::new_bitfield_1(0),
        }),
        gap_device_name: Some(raw::ble_gap_cfg_device_name_t {
            p_value: b"HelloRust" as *const u8 as _,
            current_len: 9,
            max_len: 9,
            write_perm: unsafe { mem::zeroed() },
            _bitfield_1: raw::ble_gap_cfg_device_name_t::new_bitfield_1(raw::BLE_GATTS_VLOC_STACK as u8),
        }),
        ..Default::default()
    };

    let sd = Softdevice::enable(&config);
    let server = unwrap!(Server::new(sd));
    unwrap!(spawner.spawn(softdevice_task(sd)));

    #[rustfmt::skip]
    let adv_data = &mut[
        0x02, 0x01, raw::BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE as u8, // Flags
        0x0A, 0x16, 0xD2, 0xFC, 0x40, // Identifies BTHome; Called Service data
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // My actual data. placeholder for now
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // To be filled later
        0x0a, 0x09, b'R', b'u', b's', b't', b'y', b'B', b'u', b't', b't',
    ];

    loop {
        let config = peripheral::Config::default();
        let adv = peripheral::ConnectableAdvertisement::ExtendedNonscannableUndirected { set_id: 0, adv_data };
        let conn = unwrap!(peripheral::advertise_connectable(sd, adv, &config).await);

        info!("advertising done!");

        // Run the GATT server on the connection. This returns when the connection gets disconnected.
        //
        // Event enums (ServerEvent's) are generated by nrf_softdevice::gatt_server
        // proc macro when applied to the Server struct above
        let res = gatt_server::run(&conn, &server, |e| match e {
            ServerEvent::Bas(e) => match e {
                BatteryServiceEvent::BatteryLevelCccdWrite { notifications } => {
                    info!("battery notifications: {}", notifications)
                }
            },
            ServerEvent::Foo(e) => match e {
                FooServiceEvent::FooWrite(val) => {
                    info!("wrote foo: {}", val);
                    if let Err(e) = server.foo.foo_notify(&conn, val + 1) {
                        info!("send notification error: {:?}", e);
                    }
                }
                FooServiceEvent::FooCccdWrite {
                    indications,
                    notifications,
                } => {
                    info!("foo indications: {}, notifications: {}", indications, notifications)
                }
            },
        })
        .await;

        if let Err(e) = res {
            info!("gatt_server run exited with error: {:?}", e);
        }
    }
}
Ardelean-Calin commented 1 year ago

I've had one more finding: Even if running the example ble_bas_peripheral unmodified, and just deleting one byte from the advertisment data gives the same invalid length error. So I cannot have any advertisment length other than 18 bytes? Why? Every example uses the same advertisment payload.

Ardelean-Calin commented 1 year ago

Nevermind, I figured it out! I will post a comment with the solution to the problem and close the issue.

Ardelean-Calin commented 1 year ago

So, it had nothing to do with Extended Advertising. That actually works and is as simple as using ConnectableAdvertisement::ExtendedNonscannableUndirected. The problem was with my advertisement payload. Apparently it needs to be formatted in a very specific way. It needs to be formatted into Advertising Data (AD) elements.

So the way we need to change the payload above is as follows:

let adv_data = &mut[
    0x02, 0x01, raw::BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE as u8, // Flags
    0x19, 0x16, 0xD2, 0xFC, 0x40, // The BTHome AD element. Has a length of 25 bytes.
        // My actual data. placeholder for now. To be filled later
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
        0xFF, 
    0x0A, 0x09, b'D', b'u', b's', b't', b'y', b'B', b'u', b't', b't',
];

Here we have 3 AD elements, indicated by each line (the indented lines are just payload data of an AD element). The first byte in each line is the length of the AD element. The second byte is the AD element identifier as given by this document. While the rest of the bytes are payload.

For example, the last line in the code above is a 10-byte long AD element with identifier 0x09 - Complete Local Name. If I delete a byte from the end of the name, I would have to change the length 0x0A to 0x09, otherwise I would get InvalidLength errors.

So this is why I got the errors, I failed to correctly structure the advertisement payload into AD elements.