diff --git a/examples/src/bin/ble_keyboard_peripheral_builder_macro.rs b/examples/src/bin/ble_keyboard_peripheral_builder_macro.rs new file mode 100644 index 00000000..e3484461 --- /dev/null +++ b/examples/src/bin/ble_keyboard_peripheral_builder_macro.rs @@ -0,0 +1,420 @@ +#![no_std] +#![no_main] + +macro_rules! count { + () => { 0u8 }; + ($x:tt $($xs:tt)*) => {1u8 + count!($($xs)*)}; +} + +macro_rules! hid { + ($(( $($xs:tt),*)),+ $(,)?) => { &[ $( (count!($($xs)*)-1) | $($xs),* ),* ] }; +} + +#[path = "../example_common.rs"] +mod example_common; + +use core::mem; + +use defmt::{info, *}; +use embassy_executor::Spawner; +use nrf_softdevice::ble::advertisement_builder::{ + AdvertisementDataType, Flag, LegacyAdvertisementBuilder, LegacyAdvertisementPayload, ServiceList, ServiceUuid16, +}; +use nrf_softdevice::ble::gatt_server::builder::ServiceBuilder; +use nrf_softdevice::ble::gatt_server::characteristic::{Attribute, Metadata, Presentation, Properties}; +use nrf_softdevice::ble::gatt_server::{CharacteristicHandles, RegisterError, Service, WriteOp}; +use nrf_softdevice::ble::security::SecurityHandler; +use nrf_softdevice::ble::{gatt_server, peripheral, Connection, Uuid}; +use nrf_softdevice::{raw, Softdevice}; + +const DEVICE_INFORMATION: Uuid = Uuid::new_16(0x180a); +const BATTERY_SERVICE: Uuid = Uuid::new_16(0x180f); + +const BATTERY_LEVEL: Uuid = Uuid::new_16(0x2a19); +const MODEL_NUMBER: Uuid = Uuid::new_16(0x2a24); +const SERIAL_NUMBER: Uuid = Uuid::new_16(0x2a25); +const FIRMWARE_REVISION: Uuid = Uuid::new_16(0x2a26); +const HARDWARE_REVISION: Uuid = Uuid::new_16(0x2a27); +const SOFTWARE_REVISION: Uuid = Uuid::new_16(0x2a28); +const MANUFACTURER_NAME: Uuid = Uuid::new_16(0x2a29); +const PNP_ID: Uuid = Uuid::new_16(0x2a50); + +// Main items +pub const HIDINPUT: u8 = 0x80; +pub const HIDOUTPUT: u8 = 0x90; +pub const FEATURE: u8 = 0xb0; +pub const COLLECTION: u8 = 0xa0; +pub const END_COLLECTION: u8 = 0xc0; + +// Global items +pub const USAGE_PAGE: u8 = 0x04; +pub const LOGICAL_MINIMUM: u8 = 0x14; +pub const LOGICAL_MAXIMUM: u8 = 0x24; +pub const PHYSICAL_MINIMUM: u8 = 0x34; +pub const PHYSICAL_MAXIMUM: u8 = 0x44; +pub const UNIT_EXPONENT: u8 = 0x54; +pub const UNIT: u8 = 0x64; +pub const REPORT_SIZE: u8 = 0x74; //bits +pub const REPORT_ID: u8 = 0x84; +pub const REPORT_COUNT: u8 = 0x94; //bytes +pub const PUSH: u8 = 0xa4; +pub const POP: u8 = 0xb4; + +// Local items +pub const USAGE: u8 = 0x08; +pub const USAGE_MINIMUM: u8 = 0x18; +pub const USAGE_MAXIMUM: u8 = 0x28; +pub const DESIGNATOR_INDEX: u8 = 0x38; +pub const DESIGNATOR_MINIMUM: u8 = 0x48; +pub const DESIGNATOR_MAXIMUM: u8 = 0x58; +pub const STRING_INDEX: u8 = 0x78; +pub const STRING_MINIMUM: u8 = 0x88; +pub const STRING_MAXIMUM: u8 = 0x98; +pub const DELIMITER: u8 = 0xa8; + +const KEYBOARD_ID: u8 = 0x01; +const MEDIA_KEYS_ID: u8 = 0x02; + +const HID_REPORT_DESCRIPTOR: &[u8] = hid!( + (USAGE_PAGE, 0x01), // USAGE_PAGE (Generic Desktop Ctrls) + (USAGE, 0x06), // USAGE (Keyboard) + (COLLECTION, 0x01), // COLLECTION (Application) + // ------------------------------------------------- Keyboard + (REPORT_ID, KEYBOARD_ID), // REPORT_ID (1) + (USAGE_PAGE, 0x07), // USAGE_PAGE (Kbrd/Keypad) + (USAGE_MINIMUM, 0xE0), // USAGE_MINIMUM (0xE0) + (USAGE_MAXIMUM, 0xE7), // USAGE_MAXIMUM (0xE7) + (LOGICAL_MINIMUM, 0x00), // LOGICAL_MINIMUM (0) + (LOGICAL_MAXIMUM, 0x01), // Logical Maximum (1) + (REPORT_SIZE, 0x01), // REPORT_SIZE (1) + (REPORT_COUNT, 0x08), // REPORT_COUNT (8) + (HIDINPUT, 0x02), // INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + (REPORT_COUNT, 0x01), // REPORT_COUNT (1) ; 1 byte (Reserved) + (REPORT_SIZE, 0x08), // REPORT_SIZE (8) + (HIDINPUT, 0x01), // INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + (REPORT_COUNT, 0x05), // REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana) + (REPORT_SIZE, 0x01), // REPORT_SIZE (1) + (USAGE_PAGE, 0x08), // USAGE_PAGE (LEDs) + (USAGE_MINIMUM, 0x01), // USAGE_MINIMUM (0x01) ; Num Lock + (USAGE_MAXIMUM, 0x05), // USAGE_MAXIMUM (0x05) ; Kana + (HIDOUTPUT, 0x02), // OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + (REPORT_COUNT, 0x01), // REPORT_COUNT (1) ; 3 bits (Padding) + (REPORT_SIZE, 0x03), // REPORT_SIZE (3) + (HIDOUTPUT, 0x01), // OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + (REPORT_COUNT, 0x06), // REPORT_COUNT (6) ; 6 bytes (Keys) + (REPORT_SIZE, 0x08), // REPORT_SIZE(8) + (LOGICAL_MINIMUM, 0x00), // LOGICAL_MINIMUM(0) + (LOGICAL_MAXIMUM, 0x65), // LOGICAL_MAXIMUM(0x65) ; 101 keys + (USAGE_PAGE, 0x07), // USAGE_PAGE (Kbrd/Keypad) + (USAGE_MINIMUM, 0x00), // USAGE_MINIMUM (0) + (USAGE_MAXIMUM, 0x65), // USAGE_MAXIMUM (0x65) + (HIDINPUT, 0x00), // INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + (END_COLLECTION), // END_COLLECTION + // ------------------------------------------------- Media Keys + (USAGE_PAGE, 0x0C), // USAGE_PAGE (Consumer) + (USAGE, 0x01), // USAGE (Consumer Control) + (COLLECTION, 0x01), // COLLECTION (Application) + (REPORT_ID, MEDIA_KEYS_ID), // REPORT_ID (2) + (USAGE_PAGE, 0x0C), // USAGE_PAGE (Consumer) + (LOGICAL_MINIMUM, 0x00), // LOGICAL_MINIMUM (0) + (LOGICAL_MAXIMUM, 0x01), // LOGICAL_MAXIMUM (1) + (REPORT_SIZE, 0x01), // REPORT_SIZE (1) + (REPORT_COUNT, 0x10), // REPORT_COUNT (16) + (USAGE, 0xB5), // USAGE (Scan Next Track) ; bit 0: 1 + (USAGE, 0xB6), // USAGE (Scan Previous Track) ; bit 1: 2 + (USAGE, 0xB7), // USAGE (Stop) ; bit 2: 4 + (USAGE, 0xCD), // USAGE (Play/Pause) ; bit 3: 8 + (USAGE, 0xE2), // USAGE (Mute) ; bit 4: 16 + (USAGE, 0xE9), // USAGE (Volume Increment) ; bit 5: 32 + (USAGE, 0xEA), // USAGE (Volume Decrement) ; bit 6: 64 + (USAGE, 0x23, 0x02), // Usage (WWW Home) ; bit 7: 128 + (USAGE, 0x94, 0x01), // Usage (My Computer) ; bit 0: 1 + (USAGE, 0x92, 0x01), // Usage (Calculator) ; bit 1: 2 + (USAGE, 0x2A, 0x02), // Usage (WWW fav) ; bit 2: 4 + (USAGE, 0x21, 0x02), // Usage (WWW search) ; bit 3: 8 + (USAGE, 0x26, 0x02), // Usage (WWW stop) ; bit 4: 16 + (USAGE, 0x24, 0x02), // Usage (WWW back) ; bit 5: 32 + (USAGE, 0x83, 0x01), // Usage (Media sel) ; bit 6: 64 + (USAGE, 0x8A, 0x01), // Usage (Mail) ; bit 7: 128 + (HIDINPUT, 0x02), // INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + (END_COLLECTION), // END_COLLECTION +); + +#[embassy_executor::task] +async fn softdevice_task(sd: &'static Softdevice) -> ! { + sd.run().await +} + +#[repr(u8)] +#[derive(Clone, Copy)] +pub enum VidSource { + BluetoothSIG = 1, + UsbIF = 2, +} + +#[repr(C, packed)] +#[derive(Clone, Copy)] +pub struct PnPID { + pub vid_source: VidSource, + pub vendor_id: u16, + pub product_id: u16, + pub product_version: u16, +} + +#[derive(Debug, Default, defmt::Format)] +pub struct DeviceInformation { + pub manufacturer_name: Option<&'static str>, + pub model_number: Option<&'static str>, + pub serial_number: Option<&'static str>, + pub hw_rev: Option<&'static str>, + pub fw_rev: Option<&'static str>, + pub sw_rev: Option<&'static str>, +} + +pub struct DeviceInformationService {} + +impl DeviceInformationService { + pub fn new(sd: &mut Softdevice, pnp_id: &PnPID, info: DeviceInformation) -> Result { + let mut sb = ServiceBuilder::new(sd, DEVICE_INFORMATION)?; + + Self::add_pnp_characteristic(&mut sb, pnp_id)?; + Self::add_opt_str_characteristic(&mut sb, MANUFACTURER_NAME, info.manufacturer_name)?; + Self::add_opt_str_characteristic(&mut sb, MODEL_NUMBER, info.model_number)?; + Self::add_opt_str_characteristic(&mut sb, SERIAL_NUMBER, info.serial_number)?; + Self::add_opt_str_characteristic(&mut sb, HARDWARE_REVISION, info.hw_rev)?; + Self::add_opt_str_characteristic(&mut sb, FIRMWARE_REVISION, info.fw_rev)?; + Self::add_opt_str_characteristic(&mut sb, SOFTWARE_REVISION, info.sw_rev)?; + + let _service_handle = sb.build(); + + Ok(DeviceInformationService {}) + } + + fn add_opt_str_characteristic( + sb: &mut ServiceBuilder, + uuid: Uuid, + val: Option<&'static str>, + ) -> Result, RegisterError> { + if let Some(val) = val { + let attr = Attribute::new(val); + let md = Metadata::new(Properties::new().read()); + Ok(Some(sb.add_characteristic(uuid, attr, md)?.build())) + } else { + Ok(None) + } + } + + fn add_pnp_characteristic(sb: &mut ServiceBuilder, pnp_id: &PnPID) -> Result { + // SAFETY: `PnPID` is `repr(C, packed)` so viewing it as an immutable slice of bytes is safe. + let val = + unsafe { core::slice::from_raw_parts(pnp_id as *const _ as *const u8, core::mem::size_of::()) }; + + let attr = Attribute::new(val); + let md = Metadata::new(Properties::new().read()); + Ok(sb.add_characteristic(PNP_ID, attr, md)?.build()) + } +} + +pub struct BatteryService { + value_handle: u16, + cccd_handle: u16, +} + +impl BatteryService { + pub fn new(sd: &mut Softdevice) -> Result { + let mut service_builder = ServiceBuilder::new(sd, BATTERY_SERVICE)?; + + let attr = Attribute::new(&[0u8]); + let metadata = Metadata::new(Properties::new().read().notify()).presentation(Presentation { + format: raw::BLE_GATT_CPF_FORMAT_UINT8 as u8, + exponent: 0, /* Value * 10 ^ 0 */ + unit: 0x27AD, /* Percentage */ + name_space: raw::BLE_GATT_CPF_NAMESPACE_BTSIG as u8, + description: raw::BLE_GATT_CPF_NAMESPACE_DESCRIPTION_UNKNOWN as u16, + }); + let characteristic_builder = service_builder.add_characteristic(BATTERY_LEVEL, attr, metadata)?; + let characteristic_handles = characteristic_builder.build(); + + let _service_handle = service_builder.build(); + + Ok(BatteryService { + value_handle: characteristic_handles.value_handle, + cccd_handle: characteristic_handles.cccd_handle, + }) + } + + pub fn battery_level_get(&self, sd: &Softdevice) -> Result { + let buf = &mut [0u8]; + gatt_server::get_value(sd, self.value_handle, buf)?; + Ok(buf[0]) + } + + pub fn battery_level_set(&self, sd: &Softdevice, val: u8) -> Result<(), gatt_server::SetValueError> { + gatt_server::set_value(sd, self.value_handle, &[val]) + } + pub fn battery_level_notify(&self, conn: &Connection, val: u8) -> Result<(), gatt_server::NotifyValueError> { + gatt_server::notify_value(conn, self.value_handle, &[val]) + } + + pub fn on_write(&self, handle: u16, data: &[u8]) { + if handle == self.cccd_handle && !data.is_empty() { + info!("battery notifications: {}", (data[0] & 0x01) != 0); + } + } +} + +#[nrf_softdevice::gatt_service(uuid = "1812")] +pub struct HidService { + // If you have multiple descriptors, just add them all + #[characteristic( + uuid = "2A4D", + security = "justworks", + read, + write, + notify, + value = "[0u8, 1u8]", + descriptor(uuid = "2908", security = "justworks", value = "[0, 1]"), + descriptor(uuid = "2902", security = "justworks", value = "[0, 1]") + )] + pub input_report: [u8; 8], + + #[characteristic(uuid = "2A4A", security = "justworks", read, value = "[0x1, 0x1, 0x0, 0x03]")] + pub hid_info: u8, + + #[characteristic(uuid = "2A4B", security = "justworks", read, value = "HID_REPORT_DESCRIPTOR")] + pub report_map: [u8; HID_REPORT_DESCRIPTOR.len()], +} + +struct Server { + _dis: DeviceInformationService, + bas: BatteryService, + hid: HidService, +} + +impl Server { + pub fn new(sd: &mut Softdevice, serial_number: &'static str) -> Result { + let dis = DeviceInformationService::new( + sd, + &PnPID { + vid_source: VidSource::UsbIF, + vendor_id: 0xDEAD, + product_id: 0xBEEF, + product_version: 0x0000, + }, + DeviceInformation { + manufacturer_name: Some("Embassy"), + model_number: Some("M1234"), + serial_number: Some(serial_number), + ..Default::default() + }, + )?; + + let bas = BatteryService::new(sd)?; + + let hid = HidService::new(sd)?; + + Ok(Self { _dis: dis, bas, hid }) + } +} + +impl gatt_server::Server for Server { + type Event = (); + + fn on_write( + &self, + _conn: &Connection, + handle: u16, + _op: WriteOp, + _offset: usize, + data: &[u8], + ) -> Option { + self.hid.on_write(handle, data); + self.bas.on_write(handle, data); + None + } +} + +struct HidSecurityHandler {} + +impl SecurityHandler for HidSecurityHandler {} + +#[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: 16, + rc_temp_ctiv: 2, + accuracy: raw::NRF_CLOCK_LF_ACCURACY_500_PPM as u8, + }), + 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: 3, + central_role_count: 3, + 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, "12345678")); + unwrap!(spawner.spawn(softdevice_task(sd))); + + static ADV_DATA: LegacyAdvertisementPayload = LegacyAdvertisementBuilder::new() + .flags(&[Flag::GeneralDiscovery, Flag::LE_Only]) + .services_16( + ServiceList::Incomplete, + &[ServiceUuid16::BATTERY, ServiceUuid16::HUMAN_INTERFACE_DEVICE], + ) + .full_name("HelloRust") + // Change the appearance (icon of the bluetooth device) to a keyboard + .raw(AdvertisementDataType::APPEARANCE, &[0xC1, 0x03]) + .build(); + + static SCAN_DATA: LegacyAdvertisementPayload = LegacyAdvertisementBuilder::new() + .services_16( + ServiceList::Complete, + &[ + ServiceUuid16::DEVICE_INFORMATION, + ServiceUuid16::BATTERY, + ServiceUuid16::HUMAN_INTERFACE_DEVICE, + ], + ) + .build(); + + static SEC: HidSecurityHandler = HidSecurityHandler {}; + + loop { + let config = peripheral::Config::default(); + let adv = peripheral::ConnectableAdvertisement::ScannableUndirected { + adv_data: &ADV_DATA, + scan_data: &SCAN_DATA, + }; + let conn = peripheral::advertise_pairable(sd, adv, &config, &SEC).await.unwrap(); + + info!("advertising done!"); + + // Run the GATT server on the connection. This returns when the connection gets disconnected. + let e = gatt_server::run(&conn, &server, |_| {}).await; + + info!("gatt_server run exited with error: {:?}", e); + } +} diff --git a/nrf-softdevice-macro/src/lib.rs b/nrf-softdevice-macro/src/lib.rs index 717d119a..a07632df 100644 --- a/nrf-softdevice-macro/src/lib.rs +++ b/nrf-softdevice-macro/src/lib.rs @@ -20,6 +20,15 @@ struct ServiceArgs { uuid: Uuid, } +#[derive(Debug, FromMeta)] +struct DescriptorArgs { + uuid: Uuid, + #[darling(default)] + security: Option, + #[darling(default)] + value: Option, +} + #[derive(Debug, FromMeta)] struct CharacteristicArgs { uuid: Uuid, @@ -35,6 +44,10 @@ struct CharacteristicArgs { indicate: bool, #[darling(default)] security: Option, + #[darling(default)] + value: Option, + #[darling(default, multiple)] + descriptor: Vec, } #[derive(Debug)] @@ -236,6 +249,31 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { let indicate = ch.args.indicate; let ty = &ch.ty; let ty_as_val = quote!(<#ty as #ble::GattValue>); + let value = match &ch.args.value { + Some(v) => quote! { #v }, + None => quote! { [123u8; #ty_as_val::MIN_SIZE] }, + }; + + let descriptors: Vec = ch + .args + .descriptor + .iter() + .map(|descriptor_args| { + let descriptor_uuid = descriptor_args.uuid; + let security = descriptor_args.security.as_ref().unwrap(); + let value = descriptor_args.value.as_ref().unwrap(); + + quote! { + let _ = cb + .add_descriptor( + #descriptor_uuid, + #ble::gatt_server::characteristic::Attribute::new(#value) + .security(#security) + .variable_len(#value.len() as u16) + )?; + } + }) + .collect(); let security = if let Some(security) = ch.args.security { let security_inner = security.to_token_stream(); @@ -254,7 +292,7 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { code_build_chars.extend(quote_spanned!(ch.span=> let #char_name = { - let val = [123u8; #ty_as_val::MIN_SIZE]; + let val = #value; let mut attr = #ble::gatt_server::characteristic::Attribute::new(&val); if #ty_as_val::MAX_SIZE != #ty_as_val::MIN_SIZE { attr = attr.variable_len(#ty_as_val::MAX_SIZE as u16); @@ -269,7 +307,11 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { ..Default::default() }; let metadata = #ble::gatt_server::characteristic::Metadata::new(props); - service_builder.add_characteristic(#uuid, attr, metadata)?.build() + let mut cb = service_builder.add_characteristic(#uuid, attr, metadata)?; + + #(#descriptors)* + + cb.build() }; ));