Skip to content

Commit

Permalink
Allow more types in UUID macro (embassy-rs#218)
Browse files Browse the repository at this point in the history
* use bt_hci constant uuids

* parse uuid from Expr as well as LitStr

* update docstring examples

* add parsing of service uuid from litstr as well as expr

* add parsing Into<Uuid> in gatt_service args

* improve compile time error messages for uuids

* set descriptor min capacity to 16 bytes
  • Loading branch information
jamessizeland authored Jan 5, 2025
1 parent 591c357 commit 5e55ac1
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 95 deletions.
12 changes: 7 additions & 5 deletions examples/apps/src/ble_bas_peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ struct Server {
battery_service: BatteryService,
}

// Battery service
#[gatt_service(uuid = "180f")]
/// Battery service
#[gatt_service(uuid = service::BATTERY)]
struct BatteryService {
/// Battery Level
#[descriptor(uuid = "2b20", read, value = "Battery Level")]
#[descriptor(uuid = "2b21", read, value = [0x12, 0x34])]
#[characteristic(uuid = "2a19", read, notify, value = 10)]
#[descriptor(uuid = descriptors::VALID_RANGE, read, value = [0, 100])]
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, read, value = "Battery Level")]
#[characteristic(uuid = characteristic::BATTERY_LEVEL, read, notify, value = 10)]
level: u8,
#[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", write, read, notify)]
status: bool,
}

/// Run the BLE stack.
Expand Down
89 changes: 41 additions & 48 deletions host-macros/src/characteristic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
//! A characteristic is a data value that can be accessed from a connected client.
use darling::{Error, FromMeta};
use proc_macro2::Span;
use proc_macro2::{Span, TokenStream};
use syn::meta::ParseNestedMeta;
use syn::parse::Result;
use syn::spanned::Spanned;
use syn::{Field, Ident, LitStr};
use syn::{Field, LitStr};

use crate::uuid::Uuid;

#[derive(Debug)]
pub(crate) struct Characteristic {
pub struct Characteristic {
pub name: String,
pub ty: syn::Type,
pub args: CharacteristicArgs,
Expand All @@ -34,60 +34,48 @@ impl Characteristic {
}
}

#[derive(Debug, FromMeta, Default)]
pub(crate) struct AccessArgs {
#[derive(Debug, Default)]
pub struct AccessArgs {
/// If true, the characteristic can be written.
#[darling(default)]
pub read: bool,
/// If true, the characteristic can be written.
#[darling(default)]
pub write: bool,
/// If true, the characteristic can be written without a response.
#[darling(default)]
pub write_without_response: bool,
/// If true, the characteristic can send notifications.
#[darling(default)]
pub notify: bool,
/// If true, the characteristic can send indications.
#[darling(default)]
pub indicate: bool,
}

/// Descriptor attribute arguments.
///
/// Descriptors are optional and can be used to add additional metadata to the characteristic.
#[derive(Debug, FromMeta)]
pub(crate) struct DescriptorArgs {
#[derive(Debug)]
pub struct DescriptorArgs {
/// The UUID of the descriptor.
pub uuid: Uuid,
pub uuid: TokenStream,
/// The initial value of the descriptor (&str).
/// This is optional and can be used to set the initial value of the descriptor.
#[darling(default)]
pub default_value: Option<syn::Expr>,
#[darling(default)]
/// Capacity for writing new descriptors (u8)
pub capacity: Option<syn::Expr>,
#[darling(default)]
pub access: AccessArgs,
}

/// Characteristic attribute arguments
#[derive(Debug, FromMeta)]
pub(crate) struct CharacteristicArgs {
#[derive(Debug)]
pub struct CharacteristicArgs {
/// The UUID of the characteristic.
pub uuid: Uuid,
pub uuid: TokenStream,
/// Starting value for this characteristic.
#[darling(default)]
pub default_value: Option<syn::Expr>,
/// Descriptors for the characteristic.
/// Descriptors are optional and can be used to add additional metadata to the characteristic.
/// Parsed in super::check_for_characteristic.
#[darling(default, multiple)]
pub descriptors: Vec<DescriptorArgs>,
/// Any '///' comments on each field, parsed in super::check_for_characteristic.
#[darling(default)]
pub doc_string: String,
#[darling(default)]
pub access: AccessArgs,
}

Expand All @@ -101,10 +89,34 @@ fn check_multi<T>(arg: &mut Option<T>, name: &str, meta: &ParseNestedMeta<'_>, v
}
}

pub fn parse_uuid(meta: &ParseNestedMeta<'_>) -> Result<TokenStream> {
let parser = meta.value().map_err(|_| {
meta.error(
"uuid must be followed by '= [data]'. i.e. uuid = \"2a37\" or \"0000180f-0000-1000-8000-00805f9b34fb\"",
)
})?;
if let Ok(uuid_string) = parser.parse::<LitStr>() {
// Check if it's a valid UUID from a string before running the code
let uuid = Uuid::from_string(uuid_string.value().as_str()).map_err(|_| {
meta.error("Invalid UUID string. Expect i.e. \"180f\" or \"0000180f-0000-1000-8000-00805f9b34fb\"")
})?;
Ok(quote::quote! { #uuid })
} else {
let expr: syn::Expr = parser.parse()?;
let span = expr.span(); // span will highlight if the value does not impl Into<Uuid>
Ok(quote::quote_spanned! { span =>
{
let uuid: Uuid = #expr.into();
uuid
}
})
}
}

impl CharacteristicArgs {
/// Parse the arguments of a characteristic attribute
pub fn parse(attribute: &syn::Attribute) -> Result<Self> {
let mut uuid: Option<Uuid> = None;
let mut uuid: Option<_> = None;
let mut read: Option<bool> = None;
let mut write: Option<bool> = None;
let mut notify: Option<bool> = None;
Expand All @@ -113,14 +125,7 @@ impl CharacteristicArgs {
let mut write_without_response: Option<bool> = None;
attribute.parse_nested_meta(|meta| {
match meta.path.get_ident().ok_or(meta.error("no ident"))?.to_string().as_str() {
"uuid" => {
let parser = meta
.value()
.map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?;
let uuid_string: LitStr = parser.parse()?;
let value = Uuid::from_string(uuid_string.value().as_str())?;
check_multi(&mut uuid, "uuid", &meta, value)?
},
"uuid" => check_multi(&mut uuid, "uuid", &meta, parse_uuid(&meta)?)?,
"read" => check_multi(&mut read, "read", &meta, true)?,
"write" => check_multi(&mut write, "write", &meta, true)?,
"notify" => check_multi(&mut notify, "notify", &meta, true)?,
Expand Down Expand Up @@ -160,11 +165,9 @@ impl CharacteristicArgs {

impl DescriptorArgs {
pub fn parse(attribute: &syn::Attribute) -> Result<Self> {
let mut uuid: Option<Uuid> = None;
let mut uuid: Option<_> = None;
let mut read: Option<bool> = None;
// let mut write: Option<bool> = None;
let mut on_read: Option<Ident> = None;
// let mut on_write: Option<Ident> = None;
// let mut capacity: Option<syn::Expr> = None;
let mut default_value: Option<syn::Expr> = None;
// let mut write_without_response: Option<bool> = None;
Expand All @@ -176,18 +179,9 @@ impl DescriptorArgs {
.to_string()
.as_str()
{
"uuid" => {
let parser = meta
.value()
.map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?;
let uuid_string: LitStr = parser.parse()?;
let value = Uuid::from_string(uuid_string.value().as_str())?;
check_multi(&mut uuid, "uuid", &meta, value)?
}
"uuid" => check_multi(&mut uuid, "uuid", &meta, parse_uuid(&meta)?)?,
"read" => check_multi(&mut read, "read", &meta, true)?,
// "write" => check_multi(&mut write, "write", &meta, true)?,
"on_read" => check_multi(&mut on_read, "on_read", &meta, meta.value()?.parse()?)?,
// "on_write" => check_multi(&mut on_write, "on_write", &meta, meta.value()?.parse()?)?,
// "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta, true)?,
"value" => {
let value = meta.value().map_err(|_| {
Expand All @@ -202,9 +196,8 @@ impl DescriptorArgs {
"default_value" => return Err(meta.error("use 'value' for default value")),
other => {
return Err(meta.error(format!(
// "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, value,\ncapacity, on_read, on_write"
"Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, value, on_read"
)));
"Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, value"
)));
}
};
Ok(())
Expand Down
41 changes: 9 additions & 32 deletions host-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,45 +63,22 @@ pub fn gatt_server(args: TokenStream, item: TokenStream) -> TokenStream {
///
/// const DESCRIPTOR_VALUE: &str = "Can be specified from a const";
///
/// #[gatt_service(uuid = "7e701cf1-b1df-42a1-bb5f-6a1028c793b0", on_read = service_on_read)]
/// #[gatt_service(uuid = "7e701cf1-b1df-42a1-bb5f-6a1028c793b0")]
/// struct HeartRateService {
/// /// Docstrings can be
/// /// Multiple lines
/// #[descriptor(uuid = "2a20", read, write, notify, capacity = 100)]
/// #[descriptor(uuid = "2a21", read, notify, value = "Demo description")]
/// #[characteristic(uuid = "2a37", read, notify, value = 3.14, on_read = rate_on_read)]
/// #[descriptor(uuid = "2a21", read, value = [0x00,0x01,0x02,0x03])]
/// #[characteristic(uuid = characteristic::HEART_RATE_MEASUREMENT, read, notify, value = 3.14)]
/// rate: f32,
/// #[descriptor(uuid = "2a21", read, write, notify, value = DESCRIPTOR_VALUE, capacity = DESCRIPTOR_VALUE.len() as u8)]
/// #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, read, value = DESCRIPTOR_VALUE)]
/// #[characteristic(uuid = "2a28", read, write, notify, value = 42.0)]
/// /// Can be in any order
/// location: f32,
/// #[characteristic(uuid = "2a39", write, on_write = control_on_write)]
/// #[characteristic(uuid = "2a39", write)]
/// control: u8,
/// #[characteristic(uuid = "2a63", read, notify)]
/// energy_expended: u16,
/// }
///
/// fn service_on_read(connection: &Connection) {
/// info!("Read callback triggered for {:?}", connection);
/// }
///
/// fn rate_on_read(connection: &Connection) {
/// info!("Heart rate read on {:?}", connection);
/// }
///
/// fn control_on_write(connection: &Connection, data: &[u8] -> Result<(), ()> {
/// info!("Write event on control attribute from {:?} with data {:?}", connection, data);
/// let control = u8::from_gatt(data).unwrap();
/// match control {
/// 0 => info!("Control setting 0 selected"),
/// 1 => info!("Control setting 1 selected"),
/// _ => {
/// warn!("Unsupported control setting! Rejecting write request.");
/// return Err(())
/// }
/// }
/// Ok(())
/// })
/// ```
#[proc_macro_attribute]
pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -164,11 +141,11 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream {
/// struct BatteryService {
/// /// Docstrings can be
/// /// Multiple lines
/// #[characteristic(uuid = "2a19", read, write, notify, value = 99, on_read = battery_level_on_read)]
/// #[descriptor(uuid = "2a20", read, write, notify, on_read = battery_level_on_read)]
/// #[descriptor(uuid = "2a20", read, write, notify, value = "Demo description")]
/// #[characteristic(uuid = "2a19", read, write, notify, value = 99)]
/// #[descriptor(uuid = "2a20", read, value = [0x00,0x01,0x02,0x03])]
/// #[descriptor(uuid = "2a20", read, value = "Demo description")]
/// level: u8,
/// #[descriptor(uuid = "2a21", read, write, notify, value = VAL)]
/// #[descriptor(uuid = "2a21", read, value = VAL)]
/// #[characteristic(uuid = "2a22", read, write, notify, value = 42.0)]
/// /// Can be in any order
/// rate_of_discharge: f32,
Expand Down
62 changes: 54 additions & 8 deletions host-macros/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,58 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, quote_spanned};
use syn::parse::Result;
use syn::spanned::Spanned;
use syn::{Meta, Token};
use syn::{Expr, Meta, Token};

use crate::characteristic::{AccessArgs, Characteristic};
use crate::uuid::Uuid;

#[derive(Debug)]
pub(crate) struct ServiceArgs {
pub uuid: Uuid,
pub uuid: TokenStream2,
}

/// Parse the UUID argument of the service attribute.
///
/// The UUID can be specified as a string literal, an integer literal, or an expression that impl Into<Uuid>.
fn parse_arg_uuid(value: &Expr) -> Result<TokenStream2> {
match value {
Expr::Lit(lit) => {
if let syn::Lit::Str(lit_str) = &lit.lit {
let uuid_string = Uuid::from_string(&lit_str.value()).map_err(|_| {
Error::custom(
"Invalid UUID string. Expect i.e. \"180f\" or \"0000180f-0000-1000-8000-00805f9b34fb\"",
)
.with_span(&lit.span())
})?;
Ok(quote::quote! {#uuid_string})
} else if let syn::Lit::Int(lit_int) = &lit.lit {
let uuid_string = Uuid::Uuid16(lit_int.base10_parse::<u16>().map_err(|_| {
Error::custom("Invalid 16bit UUID literal. Expect i.e. \"0x180f\"").with_span(&lit.span())
})?);
Ok(quote::quote! {#uuid_string})
} else {
Err(Error::custom(
"Invalid UUID literal. Expect i.e. \"180f\" or \"0000180f-0000-1000-8000-00805f9b34fb\"",
)
.with_span(&lit.span())
.into())
}
}
other => {
let span = other.span(); // span will highlight if the value does not impl Into<Uuid>
Ok(quote::quote_spanned! { span =>
{
let uuid: Uuid = #other.into();
uuid
}
})
}
}
}

impl syn::parse::Parse for ServiceArgs {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
let mut uuid = None;
let mut uuid: Option<_> = None;

while !input.is_empty() {
let meta = input.parse()?;
Expand All @@ -36,10 +75,17 @@ impl syn::parse::Parse for ServiceArgs {
.to_string()
.as_str()
{
"uuid" => uuid = Some(Uuid::from_meta(&meta)?),
"uuid" => {
if uuid.is_some() {
return Err(Error::custom("UUID cannot be specified more than once")
.with_span(&name_value.span())
.into());
}
uuid = Some(parse_arg_uuid(&name_value.value)?);
}
other => {
return Err(Error::unknown_field(&format!(
"Unsupported service property: '{other}'.\nSupported properties are uuid"
"Unsupported service property: '{other}'.\nSupported properties are: uuid"
))
.with_span(&name_value.span())
.into())
Expand All @@ -53,7 +99,7 @@ impl syn::parse::Parse for ServiceArgs {

Ok(Self {
uuid: uuid.ok_or(Error::custom(
"Service must have a UUID (i.e. `#[gatt_service(uuid = '1234')]`)",
"Service must have a UUID (i.e. `#[gatt_service(uuid = '1234')]` or `#[gatt_service(uuid = service::BATTERY)]`)",
))?,
})
}
Expand Down Expand Up @@ -236,7 +282,7 @@ impl ServiceBuilder {
format_ident!("DESC_{index}_{}", to_screaming_snake_case(characteristic.name.as_str()));
let access = &args.access;
let properties = set_access_properties(access);
let uuid = args.uuid;
let uuid = &args.uuid;
let default_value = match &args.default_value {
Some(val) => quote!(#val), // if set by user
None => quote!(""),
Expand All @@ -251,7 +297,7 @@ impl ServiceBuilder {
quote_spanned! {characteristic.span=>
{
let value = #default_value;
const CAPACITY: u8 = #capacity;
const CAPACITY: u8 = if (#capacity) < 16 { 16 } else { #capacity }; // minimum capacity is 16 bytes
static #name_screaming: static_cell::StaticCell<[u8; CAPACITY as usize]> = static_cell::StaticCell::new();
let store = #name_screaming.init([0; CAPACITY as usize]);
let value = GattValue::to_gatt(&value);
Expand Down
Loading

0 comments on commit 5e55ac1

Please sign in to comment.