diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 96c39ec0..afa9502a 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -24,7 +24,10 @@ struct Server { // Battery service #[gatt_service(uuid = "180f")] struct BatteryService { - #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write)] + /// Battery Level + #[descriptor(uuid = "2b20", read, value = "Battery Level", on_read = battery_level_on_read)] + #[descriptor(uuid = "2b21", read, value = [0x12, 0x34])] + #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write, value = 10)] level: u8, } diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 0b7fd5e7..e4abf751 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -2,12 +2,13 @@ //! //! This module contains the parsing and handling of the characteristic attribute. //! The characteristic attribute is used to define a characteristic in a service. -//! A characteristic is a data value that can be read, written, or notified. +//! A characteristic is a data value that can be accessed from a connected client. use crate::uuid::Uuid; use darling::Error; use darling::FromMeta; use proc_macro2::Span; +use syn::meta::ParseNestedMeta; use syn::parse::Result; use syn::spanned::Spanned; use syn::Field; @@ -35,24 +36,9 @@ impl Characteristic { } } -/// Descriptor attribute arguments. -/// -/// Descriptors are optional and can be used to add additional metadata to the characteristic. -#[derive(Debug, FromMeta)] -pub(crate) struct DescriptorArgs { - /// The UUID of the descriptor. - _uuid: Uuid, - /// The value of the descriptor. - #[darling(default)] - _value: Option, -} - -/// Characteristic attribute arguments -#[derive(Debug, FromMeta)] -pub(crate) struct CharacteristicArgs { - /// The UUID of the characteristic. - pub uuid: Uuid, - /// If true, the characteristic can be read. +#[derive(Debug, FromMeta, Default)] +pub(crate) struct AccessArgs { + /// If true, the characteristic can be written. #[darling(default)] pub read: bool, /// If true, the characteristic can be written. @@ -73,71 +59,184 @@ pub(crate) struct CharacteristicArgs { /// Optional callback to be triggered on a write event #[darling(default)] pub on_write: Option, - /// The initial value of the characteristic. - /// This is optional and can be used to set the initial value of the characteristic. +} + +/// Descriptor attribute arguments. +/// +/// Descriptors are optional and can be used to add additional metadata to the characteristic. +#[derive(Debug, FromMeta)] +pub(crate) struct DescriptorArgs { + /// The UUID of the descriptor. + pub uuid: Uuid, + /// 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, + #[darling(default)] + /// Capacity for writing new descriptors (u8) + pub capacity: Option, + #[darling(default)] + pub access: AccessArgs, +} + +/// Characteristic attribute arguments +#[derive(Debug, FromMeta)] +pub(crate) struct CharacteristicArgs { + /// The UUID of the characteristic. + pub uuid: Uuid, + /// Starting value for this characteristic. #[darling(default)] - pub _default_value: Option, - // /// Descriptors for the characteristic. - // /// Descriptors are optional and can be used to add additional metadata to the characteristic. + pub default_value: Option, + /// 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, + pub descriptors: Vec, + /// Any '///' comments on each field, parsed in super::check_for_characteristic. + #[darling(default)] + pub doc_string: String, + #[darling(default)] + pub access: AccessArgs, +} + +/// Check if this bool type has been specified more than once. +fn check_multi(arg: &mut Option, name: &str, meta: &ParseNestedMeta<'_>, value: T) -> Result<()> { + if arg.is_none() { + *arg = Some(value); + Ok(()) + } else { + Err(meta.error(format!("'{name}' should not be specified more than once"))) + } } impl CharacteristicArgs { /// Parse the arguments of a characteristic attribute pub fn parse(attribute: &syn::Attribute) -> Result { let mut uuid: Option = None; - let mut read = false; - let mut write = false; - let mut write_without_response = false; - let mut notify = false; - let mut indicate = false; - let mut on_read = None; - let mut on_write = None; - let mut _default_value: Option = None; - let descriptors: Vec = Vec::new(); + let mut read: Option = None; + let mut write: Option = None; + let mut notify: Option = None; + let mut indicate: Option = None; + let mut on_read: Option = None; + let mut on_write: Option = None; + let mut default_value: Option = None; + let mut write_without_response: Option = None; attribute.parse_nested_meta(|meta| { - match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { + match meta.path.get_ident().ok_or(meta.error("no ident"))?.to_string().as_str() { "uuid" => { - let value = meta + let parser = meta .value() - .map_err(|_| Error::custom("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'".to_string()))?; - let uuid_string: LitStr = value.parse()?; - uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); + .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)? }, - "read" => read = true, - "write" => write = true, - "write_without_response" => write_without_response = true, - "notify" => notify = true, - "indicate" => indicate = true, - "on_read" => on_read = Some(meta.value()?.parse()?), - "on_write" => on_write = Some(meta.value()?.parse()?), + "read" => check_multi(&mut read, "read", &meta, true)?, + "write" => check_multi(&mut write, "write", &meta, true)?, + "notify" => check_multi(&mut notify, "notify", &meta, true)?, + "indicate" => check_multi(&mut indicate, "indicate", &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" => { - return Err(Error::custom("Default value is currently unsupported").with_span(&meta.path.span()).into()) - // let value = meta - // .value() - // .map_err(|_| Error::custom("value must be followed by '= [data]'. i.e. value = 'hello'".to_string()))?; - // default_value = Some(value.parse()?); - }, + let value = meta + .value() + .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"42\""))?; + check_multi(&mut default_value, "value", &meta, value.parse()?)? + } + "default_value" => return Err(meta.error("Use 'value' for default value")), + "descriptor" => return Err(meta.error("Descriptors are added as separate tags i.e. #[descriptor(uuid = \"1234\", value = 42, read, write, notify, indicate)]")), other => return Err( meta.error( format!( - "Unsupported characteristic property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, notify, indicate, value" + "Unsupported characteristic property: '{other}'.\nSupported properties are:\nuuid, read, write, write_without_response, notify, indicate, value\non_read, on_write" ))), }; Ok(()) })?; Ok(Self { uuid: uuid.ok_or(Error::custom("Characteristic must have a UUID"))?, - read, - write, - write_without_response, - notify, - indicate, - on_read, - on_write, - _default_value, - _descriptors: descriptors, + doc_string: String::new(), + descriptors: Vec::new(), + default_value, + access: AccessArgs { + write_without_response: write_without_response.unwrap_or_default(), + indicate: indicate.unwrap_or_default(), + notify: notify.unwrap_or_default(), + write: write.unwrap_or_default(), + read: read.unwrap_or_default(), + on_write, + on_read, + }, + }) + } +} + +impl DescriptorArgs { + pub fn parse(attribute: &syn::Attribute) -> Result { + let mut uuid: Option = None; + let mut read: Option = None; + // let mut write: Option = None; + let mut on_read: Option = None; + // let mut on_write: Option = None; + // let mut capacity: Option = None; + let mut default_value: Option = None; + // let mut write_without_response: Option = 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)? + } + "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(|_| { + meta.error("'value' must be followed by '= [data]'. i.e. value = \"Hello World\"") + })?; + check_multi(&mut default_value, "value", &meta, value.parse()?)? + } + // "capacity" => { + // let value = meta.value().map_err(|_| meta.error("'capacity' must be followed by '= [data]'. i.e. value = 100"))?; + // check_multi(&mut capacity, "capacity", &meta, value.parse()?)? + // } + "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" + ))); + } + }; + Ok(()) + })?; + + Ok(Self { + uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, + default_value, + capacity: None, + access: AccessArgs { + indicate: false, // not possible for descriptor + notify: false, // not possible for descriptor + read: read.unwrap_or_default(), + write_without_response: false, + on_write: None, + write: false, + on_read, + }, }) } } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index a93c5fe6..80f7cdf1 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -11,12 +11,12 @@ mod server; mod service; mod uuid; -use characteristic::{Characteristic, CharacteristicArgs}; +use characteristic::{Characteristic, CharacteristicArgs, DescriptorArgs}; use ctxt::Ctxt; use proc_macro::TokenStream; use server::{ServerArgs, ServerBuilder}; use service::{ServiceArgs, ServiceBuilder}; -use syn::parse_macro_input; +use syn::{parse_macro_input, spanned::Spanned, Error}; /// Gatt Service attribute macro. /// @@ -57,19 +57,26 @@ pub fn gatt_server(args: TokenStream, item: TokenStream) -> TokenStream { /// /// # Example /// -/// ```rust +/// ```rust no_run /// use trouble_host::prelude::*; -/// use trouble_host_macro::gatt_service; +/// +/// const DESCRIPTOR_VALUE: &str = "Can be specified from a const"; /// /// #[gatt_service(uuid = "7e701cf1-b1df-42a1-bb5f-6a1028c793b0", on_read = service_on_read)] /// struct HeartRateService { -/// #[characteristic(uuid = "0x2A37", read, notify, value = 3.14, on_read = rate_on_read)] +/// /// 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)] /// rate: f32, -/// #[characteristic(uuid = "0x2A38", read)] +/// #[descriptor(uuid = "2a21", read, write, notify, value = DESCRIPTOR_VALUE, capacity = DESCRIPTOR_VALUE.len() as u8)] +/// #[characteristic(uuid = "2a28", read, write, notify, value = 42.0)] +/// /// Can be in any order /// location: f32, -/// #[characteristic(uuid = "0x2A39", write, on_write = control_on_write)] +/// #[characteristic(uuid = "2a39", write, on_write = control_on_write)] /// control: u8, -/// #[characteristic(uuid = "0x2A63", read, notify)] +/// #[characteristic(uuid = "2a63", read, notify)] /// energy_expended: u16, /// } /// @@ -82,7 +89,7 @@ pub fn gatt_server(args: TokenStream, item: TokenStream) -> TokenStream { /// } /// /// fn control_on_write(connection: &Connection, data: &[u8] -> Result<(), ()> { -/// info!("Write event on control attribute from {:?} with data {:?}", connectioni, data); +/// 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"), @@ -127,7 +134,7 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { let desc = err.to_string(); ctxt.error_spanned_by( err.into_compile_error(), - format!("Parsing characteristics was unsuccessful: {}", desc), + format!("Parsing characteristics was unsuccessful:\n{}", desc), ); return ctxt.check().unwrap_err().into(); } @@ -144,6 +151,28 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { } /// Check if a field has a characteristic attribute and parse it. +/// +/// If so also check if that field has descriptors and/or docstrings. +/// +/// # Example +/// +/// ```rust +/// use trouble_host::prelude::*; +/// +/// #[gatt_service(uuid = "180f")] +/// 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")] +/// level: u8, +/// #[descriptor(uuid = "2a21", read, write, notify, value = VAL)] +/// #[characteristic(uuid = "2a22", read, write, notify, value = 42.0)] +/// /// Can be in any order +/// rate_of_discharge: f32, +///} +/// ``` fn check_for_characteristic( field: &syn::Field, err: &mut Option, @@ -151,18 +180,76 @@ fn check_for_characteristic( ) -> bool { const RETAIN: bool = true; const REMOVE: bool = false; - let Some(attr) = field.attrs.iter().find(|attr| { - attr.path().segments.len() == 1 && attr.path().segments.first().unwrap().ident == "characteristic" - }) else { + + let Some(attr) = field.attrs.iter().find(|attr| attr.path().is_ident("characteristic")) else { return RETAIN; // If the field does not have a characteristic attribute, retain it. }; - let args = match CharacteristicArgs::parse(attr) { + let mut descriptors = Vec::new(); + let mut doc_string = String::new(); + let mut characteristic_checked = false; + for attr in &field.attrs { + if let Some(ident) = attr.path().get_ident() { + match ident.to_string().as_str() { + "doc" => { + if let Ok(meta_name_value) = attr.meta.require_name_value() { + if let syn::Expr::Lit(value) = &meta_name_value.value { + if let Some(text) = &value.lit.span().source_text() { + let text: Vec<&str> = text.split("///").collect(); + if let Some(text) = text.get(1) { + if !doc_string.is_empty() { + doc_string.push('\n'); + } + doc_string.push_str(text); + } + } + } + } + } + "descriptor" => match DescriptorArgs::parse(attr) { + Ok(args) => descriptors.push(args), + Err(e) => { + *err = Some(e); + return REMOVE; // If there was an error parsing the descriptor, remove the field. + } + }, + "characteristic" => { + // make sure we only have one characteristic meta tag + if characteristic_checked { + *err = Some(Error::new( + attr.path().span(), + "only one characteristic tag should be applied per field", + )); + return REMOVE; // If there was an error parsing the descriptor, remove the field. + } else { + characteristic_checked = true; + } + } + "descriptors" => { + *err = Some(Error::new( + attr.path().span(), + "specify a descriptor like: #[descriptor(uuid = \"1234\", value = \"Hello World\", read, write, notify)]\nCan be specified multiple times.", + )); + return REMOVE; // If there was an error parsing the descriptor, remove the field. + } + _ => { + *err = Some(Error::new( + attr.path().span(), + "only doc (///), descriptor and characteristic tags are supported.", + )); + return REMOVE; // If there was an error parsing the descriptor, remove the field. + } + } + } + } + let mut args = match CharacteristicArgs::parse(attr) { Ok(args) => args, Err(e) => { *err = Some(e); return REMOVE; // If there was an error parsing the characteristic, remove the field. } }; + args.doc_string = doc_string; + args.descriptors = descriptors; characteristics.push(Characteristic::new(field, args)); REMOVE // Successfully parsed, remove the field from the fields vec. } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 69c30862..8300dcae 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -4,9 +4,10 @@ //! The struct definition is used to define the characteristics of the service, and the ServiceBuilder is used to //! generate the code required to create the service. -use crate::characteristic::{Characteristic, CharacteristicArgs}; +use crate::characteristic::{AccessArgs, Characteristic}; use crate::uuid::Uuid; use darling::{Error, FromMeta}; +use inflector::cases::screamingsnakecase::to_screaming_snake_case; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote, quote_spanned}; use syn::parse::Result; @@ -83,6 +84,18 @@ impl ServiceBuilder { code_build_chars: TokenStream2::new(), } } + /// Increment the number of access arguments required for this characteristic + /// + /// At least two attributes will be added to the attribute table for each characteristic: + /// - The characteristic declaration + /// - The characteristic's value declaration + /// + /// If the characteristic has either the notify or indicate property, + /// a Client Characteristic Configuration Descriptor (CCCD) declaration will also be added. + fn increment_attributes(&mut self, access: &AccessArgs) -> usize { + self.attribute_count += if access.notify || access.indicate { 3 } else { 2 }; + self.attribute_count + } /// Construct the macro blueprint for the service struct. pub fn build(self) -> TokenStream2 { let properties = self.properties; @@ -101,8 +114,8 @@ impl ServiceBuilder { quote! { #visibility struct #struct_name { - handle: AttributeHandle, #fields + handle: AttributeHandle, } #[allow(unused)] @@ -129,35 +142,40 @@ impl ServiceBuilder { /// Construct instructions for adding a characteristic to the service, with static storage. fn construct_characteristic_static(&mut self, characteristic: Characteristic) { - let name_screaming = format_ident!( - "{}", - inflector::cases::screamingsnakecase::to_screaming_snake_case(characteristic.name.as_str()) - ); + let descriptors = self.build_descriptors(&characteristic); + let name_screaming = format_ident!("{}", to_screaming_snake_case(characteristic.name.as_str())); let char_name = format_ident!("{}", characteristic.name); let ty = characteristic.ty; - let properties = set_access_properties(&characteristic.args); + let access = &characteristic.args.access; + let properties = set_access_properties(access); let uuid = characteristic.args.uuid; - let read_callback = characteristic - .args + let read_callback = access .on_read .as_ref() .map(|callback| quote!(builder.set_read_callback(#callback);)); - let write_callback = characteristic - .args + let write_callback = access .on_write .as_ref() .map(|callback| quote!(builder.set_write_callback(#callback);)); + let default_value = match characteristic.args.default_value { + Some(val) => quote!(#val), // if set by user + None => quote!(<#ty>::default()), // or default otherwise + }; self.code_build_chars.extend(quote_spanned! {characteristic.span=> let #char_name = { static #name_screaming: static_cell::StaticCell<[u8; size_of::<#ty>()]> = static_cell::StaticCell::new(); - let store = #name_screaming.init([0; size_of::<#ty>()]); - let mut builder = service.add_characteristic(#uuid, &[#(#properties),*], store); + let store = #name_screaming.init(Default::default()); + let mut val = <#ty>::default(); // constrain the type of the value here + val = #default_value; // update the temporary value with our new default + let bytes = GattValue::to_gatt(&val); + store[..bytes.len()].copy_from_slice(bytes); + let mut builder = service + .add_characteristic(#uuid, &[#(#properties),*], store); #read_callback #write_callback - // TODO: Descriptors - // NOTE: Descriptors are attributes too - will need to increment self.attribute_count + #descriptors builder.build() }; @@ -176,19 +194,20 @@ impl ServiceBuilder { characteristics: Vec, ) -> Self { // Processing specific to non-characteristic fields + let mut doc_strings: Vec = Vec::new(); for field in &fields { let ident = field.ident.as_ref().expect("All fields should have names"); let ty = &field.ty; + let vis = &field.vis; self.code_struct_init.extend(quote_spanned! {field.span() => - #ident: #ty::default(), - }) + #vis #ident: #ty::default(), + }); + doc_strings.push(String::new()); // not supporting docstrings here yet } - // Process characteristic fields for ch in characteristics { let char_name = format_ident!("{}", ch.name); let ty = &ch.ty; - // add fields for each characteristic value handle fields.push(syn::Field { ident: Some(char_name.clone()), @@ -198,28 +217,87 @@ impl ServiceBuilder { vis: ch.vis.clone(), mutability: syn::FieldMutability::None, }); + doc_strings.push(ch.args.doc_string.to_owned()); - // At least two attributes will be added to the attribute table for each characteristic: - // - The characteristic declaration - // - The characteristic's value declaration - // - // If the characteristic has either the notify or indicate property, a Client Characteristic Configuration Descriptor (CCCD) declaration will also be added. - self.attribute_count += if ch.args.notify || ch.args.indicate { 3 } else { 2 }; + self.increment_attributes(&ch.args.access); self.construct_characteristic_static(ch); } - + assert_eq!(fields.len(), doc_strings.len()); // Processing common to all fields - for field in fields { + for (field, doc_string) in fields.iter().zip(doc_strings) { + let docs: TokenStream2 = doc_string + .lines() + .map(|line| { + let span = field.span(); + quote_spanned!(span=> + #[doc = #line] + ) + }) + .collect(); let ident = field.ident.clone(); let ty = field.ty.clone(); let vis = &field.vis; self.code_fields.extend(quote_spanned! {field.span()=> + #docs #vis #ident: #ty, }) } self } + + /// Generate token stream for any descriptors tagged against this characteristic. + fn build_descriptors(&mut self, characteristic: &Characteristic) -> TokenStream2 { + characteristic + .args + .descriptors + .iter() + .enumerate() + .map(|(index, args)| { + let name_screaming = + 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 read_callback = match &access.on_read { + Some(callback) => quote!(Some(#callback)), + None => quote!(None), + }; + let write_callback = match &access.on_write { + Some(callback) => quote!(Some(#callback)), + None => quote!(None), + }; + let default_value = match &args.default_value { + Some(val) => quote!(#val), // if set by user + None => quote!(""), + }; + let capacity = match &args.capacity { + Some(cap) => quote!(#cap), + None => quote!(#default_value.len() as u8) + }; + + self.attribute_count += 1; // descriptors should always only be one attribute. + + quote_spanned! {characteristic.span=> + { + let value = #default_value; + const CAPACITY: u8 = #capacity; + 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); + store[..value.len()].copy_from_slice(value); + builder.add_descriptor( + #uuid, + &[#(#properties),*], + store, + #read_callback, + #write_callback, + ); + }; + } + }) + .collect() + } } fn parse_property_into_list(property: bool, variant: TokenStream2, properties: &mut Vec) { @@ -229,7 +307,7 @@ fn parse_property_into_list(property: bool, variant: TokenStream2, properties: & } /// Parse the properties of a characteristic and return a list of properties -fn set_access_properties(args: &CharacteristicArgs) -> Vec { +fn set_access_properties(args: &AccessArgs) -> Vec { let mut properties = Vec::new(); parse_property_into_list(args.read, quote! {CharacteristicProp::Read}, &mut properties); parse_property_into_list(args.write, quote! {CharacteristicProp::Write}, &mut properties); diff --git a/host/Cargo.toml b/host/Cargo.toml index 2015bef9..0fea94c8 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -39,6 +39,7 @@ tokio-serial = "5.4" env_logger = "0.11" critical-section = { version = "1", features = ["std"] } rand = "0.8.5" +heapless = "0.8.0" [features] diff --git a/host/src/types/gatt_traits.rs b/host/src/types/gatt_traits.rs index d02e3b46..97edd272 100644 --- a/host/src/types/gatt_traits.rs +++ b/host/src/types/gatt_traits.rs @@ -68,6 +68,7 @@ impl Primitive for i64 {} impl Primitive for f32 {} impl Primitive for f64 {} impl Primitive for BluetoothUuid16 {} // ok as this is just a NewType(u16) +impl Primitive for &'_ str {} impl FixedGattValue for T { const SIZE: usize = mem::size_of::(); diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index bc9d06b8..eb61db02 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -18,15 +18,30 @@ const VALUE_UUID: Uuid = Uuid::new_long([ 0x00, 0x00, 0x10, 0x01, 0xb0, 0xcd, 0x11, 0xec, 0x87, 0x1f, 0xd4, 0x5d, 0xdf, 0x13, 0x88, 0x40, ]); -#[gatt_server(mutex_type = NoopRawMutex, attribute_table_size = 10)] +#[gatt_server(mutex_type = NoopRawMutex, attribute_table_size = 22)] struct Server { service: CustomService, } #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write)] - value: u8, + #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read, on_read = value_on_read)] + /// Battery Level + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", value = 42, read, write, notify, on_read = value_on_read, on_write = value_on_write)] + #[descriptor(uuid = "2b21", value = [0x01,0x02,0x03], read)] + pub value: u8, + #[characteristic(uuid = "408814df-5dd4-1f87-ec11-cdb001100000", value = 123.321, read, write, notify, on_read = value_on_read, on_write = value_on_write)] + /// Order doesn't matter + #[descriptor(uuid = "2b20", read, value = 42u16.to_le_bytes(), on_read = value_on_read)] // empty descriptor + pub second: f32, + /// Multi + /// + /// Line + /// Comment + #[characteristic(uuid = "408815df-5dd4-1f87-ec11-cdb001100000", value = [0,1], read, write, notify)] + pub third: [u8; 2], + #[characteristic(uuid = "408816df-5dd4-1f87-ec11-cdb001100000", read, write, notify)] + pub fourth: heapless::Vec, } static READ_FLAG: CriticalSectionMutex> = CriticalSectionMutex::new(RefCell::new(false));