From 8592fd31aa6eee979a931f9fdc2ee9c9cf9825d0 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Mon, 9 Dec 2024 17:42:56 +0000 Subject: [PATCH 01/26] add default value --- host-macros/src/characteristic.rs | 16 ++++++++-------- host-macros/src/service.rs | 10 ++++++++-- host/tests/gatt_derive.rs | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 0b7fd5e7..291ce2a8 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -76,7 +76,7 @@ pub(crate) struct CharacteristicArgs { /// The initial value of the characteristic. /// This is optional and can be used to set the initial value of the characteristic. #[darling(default)] - pub _default_value: Option, + pub default_value: Option, // /// Descriptors for the characteristic. // /// Descriptors are optional and can be used to add additional metadata to the characteristic. #[darling(default, multiple)] @@ -94,7 +94,7 @@ impl CharacteristicArgs { let mut indicate = false; let mut on_read = None; let mut on_write = None; - let mut _default_value: Option = None; + let mut default_value: Option = None; let descriptors: Vec = Vec::new(); attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { @@ -113,11 +113,11 @@ impl CharacteristicArgs { "on_read" => on_read = Some(meta.value()?.parse()?), "on_write" => on_write = Some(meta.value()?.parse()?), "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()?); + // 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()?); }, other => return Err( meta.error( @@ -136,7 +136,7 @@ impl CharacteristicArgs { indicate, on_read, on_write, - _default_value, + default_value, _descriptors: descriptors, }) } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 69c30862..a32a7f05 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -147,11 +147,17 @@ impl ServiceBuilder { .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 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 + store.copy_from_slice(GattValue::to_gatt(&val)); // convert to bytes let mut builder = service.add_characteristic(#uuid, &[#(#properties),*], store); #read_callback #write_callback diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 53090ba7..9f799ed9 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -25,7 +25,7 @@ struct Server { #[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)] + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 42)] value: u8, } From 9f1089ba069a675566f931de99109a3548e5836d Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Mon, 9 Dec 2024 17:46:21 +0000 Subject: [PATCH 02/26] update parse comment --- host-macros/src/characteristic.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 291ce2a8..94c3a5c9 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -113,11 +113,10 @@ impl CharacteristicArgs { "on_read" => on_read = Some(meta.value()?.parse()?), "on_write" => on_write = Some(meta.value()?.parse()?), "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()?); + default_value = Some(value.parse()?); // type checking done in construct_characteristic_static }, other => return Err( meta.error( From fc7733bf537956b0e4556997c4ed5abcb1a34911 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Mon, 9 Dec 2024 23:45:53 +0000 Subject: [PATCH 03/26] parse in docstrings and descriptors --- host-macros/src/characteristic.rs | 76 ++++++++++++++++++++++++++++--- host-macros/src/lib.rs | 37 +++++++++++++-- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 94c3a5c9..b425de2f 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -41,10 +41,23 @@ impl Characteristic { #[derive(Debug, FromMeta)] pub(crate) struct DescriptorArgs { /// The UUID of the descriptor. - _uuid: Uuid, - /// The value of the descriptor. + pub uuid: Uuid, + /// The initial value of the descriptor. + /// This is optional and can be used to set the initial value of the descriptor. + #[darling(default)] + pub default_value: Option, + /// If true, the descriptor can be written. + #[darling(default)] + pub read: bool, + /// If true, the descriptor can be written. + #[darling(default)] + pub write: bool, + /// If true, the descriptor can be written without a response. #[darling(default)] - _value: Option, + pub write_without_response: bool, + /// If true, the descriptor can send notifications. + #[darling(default)] + pub notify: bool, } /// Characteristic attribute arguments @@ -77,10 +90,12 @@ pub(crate) struct CharacteristicArgs { /// This is optional and can be used to set the initial value of the 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. + /// Descriptors for the characteristic. + /// Descriptors are optional and can be used to add additional metadata to the characteristic. #[darling(default, multiple)] - pub _descriptors: Vec, + pub descriptors: Vec, + #[darling(default)] + pub doc_string: String, } impl CharacteristicArgs { @@ -136,7 +151,54 @@ impl CharacteristicArgs { on_read, on_write, default_value, - _descriptors: descriptors, + descriptors, + doc_string: String::new(), + }) + } +} + +impl DescriptorArgs { + pub fn parse(attribute: &syn::Attribute) -> Result { + let mut uuid: Option = None; + let mut read = false; + let mut write = false; + let mut notify = false; + let mut default_value: Option = None; + let mut write_without_response = false; + attribute.parse_nested_meta(|meta| { + match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { + "uuid" => { + let value = 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())?); + }, + "read" => read = true, + "write" => write = true, + "notify" => notify = true, + "write_without_response" => write_without_response = true, + "value" => { + 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()?); // type checking done in construct_characteristic_static + }, + other => return Err( + meta.error( + format!( + "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, notify, value" + ))), + }; + Ok(()) + })?; + Ok(Self { + uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, + read, + write, + notify, + default_value, + write_without_response, }) } } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index a93c5fe6..cd042a10 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -11,7 +11,7 @@ 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}; @@ -144,6 +144,8 @@ 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. fn check_for_characteristic( field: &syn::Field, err: &mut Option, @@ -151,18 +153,43 @@ 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(); + for attr in &field.attrs { + if attr.path().is_ident("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() { + if !doc_string.is_empty() { + doc_string.push('\n'); + } + doc_string.push_str(text); + } + } + } + } else if attr.path().is_ident("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. + } + } + } + } + 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. } From fde1c1f220a2db8091eeaf199f6bbb6913f5d6e0 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Tue, 10 Dec 2024 16:58:00 +0000 Subject: [PATCH 04/26] add extra compile time checks to characteristic parsing --- host-macros/src/characteristic.rs | 92 +++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index b425de2f..b6b9dadb 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -8,6 +8,7 @@ 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; @@ -98,15 +99,25 @@ pub(crate) struct CharacteristicArgs { pub doc_string: String, } +/// Check if this bool type has been specified more than once. +fn check_multi(val: &mut Option, name: &str, meta: &ParseNestedMeta<'_>) -> Result<()> { + if val.is_none() { + *val = Some(true); + 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 read: Option = None; + let mut write: Option = None; + let mut write_without_response: Option = None; + let mut notify: Option = None; + let mut indicate: Option = None; let mut on_read = None; let mut on_write = None; let mut default_value: Option = None; @@ -114,25 +125,41 @@ impl CharacteristicArgs { attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { "uuid" => { + if uuid.is_some() { + return Err(meta.error("'uuid' should not be specified more than once")) + } let value = 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())?); }, - "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)?, + "write" => check_multi(&mut write, "write", &meta)?, + "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, + "notify" => check_multi(&mut notify, "notify", &meta)?, + "indicate" => check_multi(&mut indicate, "indicate", &meta)?, + "on_read" => if on_read.is_none() { + on_read = Some(meta.value()?.parse()?) + } else { + return Err(meta.error("'on_read' should not be specified more than once")) + }, + "on_write" => if on_write.is_none() { + on_write = Some(meta.value()?.parse()?) + } else { + return Err(meta.error("'on_write' should not be specified more than once")) + }, "value" => { + if default_value.is_some() { + return Err(meta.error("'value' should not be specified more than once")) + } 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()?); // type checking done in construct_characteristic_static }, + "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)]")), other => return Err( meta.error( format!( @@ -143,11 +170,11 @@ impl CharacteristicArgs { })?; Ok(Self { uuid: uuid.ok_or(Error::custom("Characteristic must have a UUID"))?, - read, - write, - write_without_response, - notify, - indicate, + read: read.unwrap_or_default(), + write: write.unwrap_or_default(), + write_without_response: write_without_response.unwrap_or_default(), + notify: notify.unwrap_or_default(), + indicate: indicate.unwrap_or_default(), on_read, on_write, default_value, @@ -160,30 +187,37 @@ impl CharacteristicArgs { impl DescriptorArgs { pub fn parse(attribute: &syn::Attribute) -> Result { let mut uuid: Option = None; - let mut read = false; - let mut write = false; - let mut notify = false; + let mut read: Option = None; + let mut write: Option = None; + let mut notify: Option = None; let mut default_value: Option = None; - let mut write_without_response = false; + 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() { "uuid" => { + if uuid.is_some() { + return Err(meta.error("'uuid' should not be specified more than once")) + } let value = 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())?); }, - "read" => read = true, - "write" => write = true, - "notify" => notify = true, - "write_without_response" => write_without_response = true, + "read" => check_multi(&mut read, "read", &meta)?, + "write" => check_multi(&mut write, "write", &meta)?, + "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, + "notify" => check_multi(&mut notify, "notify", &meta)?, "value" => { + if default_value.is_some() { + return Err(meta.error("'value' should not be specified more than once")) + } 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()?); // type checking done in construct_characteristic_static }, + "default_value" => return Err(meta.error("use 'value' for default value")), other => return Err( meta.error( format!( @@ -194,11 +228,11 @@ impl DescriptorArgs { })?; Ok(Self { uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, - read, - write, - notify, + read: read.unwrap_or_default(), + write: write.unwrap_or_default(), + write_without_response: write_without_response.unwrap_or_default(), + notify: notify.unwrap_or_default(), default_value, - write_without_response, }) } } From b9e35a12f42cc724787d7e9366e1a26d84f7ff16 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Tue, 10 Dec 2024 19:24:10 +0000 Subject: [PATCH 05/26] parse docstrings into output struct --- examples/apps/src/ble_bas_peripheral.rs | 1 + host-macros/src/characteristic.rs | 3 +-- host-macros/src/lib.rs | 9 ++++++--- host-macros/src/service.rs | 20 +++++++++++++++----- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 2ec6178f..2d33e184 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -24,6 +24,7 @@ struct Server { // Battery service #[gatt_service(uuid = "180f")] struct BatteryService { + /// Battery Level #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write)] level: u8, } diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index b6b9dadb..cd1a9847 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -121,7 +121,6 @@ impl CharacteristicArgs { let mut on_read = None; let mut on_write = None; let mut default_value: Option = None; - let descriptors: Vec = Vec::new(); attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { "uuid" => { @@ -178,7 +177,7 @@ impl CharacteristicArgs { on_read, on_write, default_value, - descriptors, + descriptors: Vec::new(), doc_string: String::new(), }) } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index cd042a10..af301660 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -164,10 +164,13 @@ fn check_for_characteristic( 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() { - if !doc_string.is_empty() { - doc_string.push('\n'); + 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); } - doc_string.push_str(text); } } } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index a32a7f05..6379e338 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -185,16 +185,16 @@ impl ServiceBuilder { 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(), }) } - + let mut doc_strings = Vec::new(); // 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()), @@ -204,6 +204,7 @@ 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 @@ -214,13 +215,22 @@ impl ServiceBuilder { self.construct_characteristic_static(ch); } - // 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, }) } From abe8e7c4cd64f27887d90f6ba068f7e794d21ee3 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 11:55:40 +0000 Subject: [PATCH 06/26] building descriptors --- host-macros/src/characteristic.rs | 106 +++++++++++++++++++----------- host-macros/src/service.rs | 78 +++++++++++++++++++--- 2 files changed, 135 insertions(+), 49 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index cd1a9847..733520d9 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -59,6 +59,15 @@ pub(crate) struct DescriptorArgs { /// If true, the descriptor can send notifications. #[darling(default)] pub notify: bool, + /// If true, the characteristic can send indications. + #[darling(default)] + pub indicate: bool, + // /// Optional callback to be triggered on a read event + // #[darling(default)] + // pub on_read: Option, + // /// Optional callback to be triggered on a write event + // #[darling(default)] + // pub on_write: Option, } /// Characteristic attribute arguments @@ -115,21 +124,21 @@ impl CharacteristicArgs { let mut uuid: Option = None; let mut read: Option = None; let mut write: Option = None; - let mut write_without_response: Option = None; let mut notify: Option = None; let mut indicate: Option = None; - let mut on_read = None; - let mut on_write = 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" => { if uuid.is_some() { return Err(meta.error("'uuid' should not be specified more than once")) } let value = meta .value() - .map_err(|_| Error::custom("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'".to_string()))?; + .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'"))?; let uuid_string: LitStr = value.parse()?; uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); }, @@ -138,47 +147,46 @@ impl CharacteristicArgs { "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, "notify" => check_multi(&mut notify, "notify", &meta)?, "indicate" => check_multi(&mut indicate, "indicate", &meta)?, - "on_read" => if on_read.is_none() { - on_read = Some(meta.value()?.parse()?) - } else { + "on_read" => if on_read.is_some() { return Err(meta.error("'on_read' should not be specified more than once")) - }, - "on_write" => if on_write.is_none() { - on_write = Some(meta.value()?.parse()?) } else { + on_read = Some(meta.value()?.parse()?) + }, + "on_write" => if on_write.is_some() { return Err(meta.error("'on_write' should not be specified more than once")) + } else { + on_write = Some(meta.value()?.parse()?) }, - "value" => { - if default_value.is_some() { + "value" => if default_value.is_some() { return Err(meta.error("'value' should not be specified more than once")) - } - 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()?); // type checking done in construct_characteristic_static - }, + } else { + let value = meta + .value() + .map_err(|_| meta.error("value must be followed by '= [data]'. i.e. value = 'hello'"))?; + default_value = Some(value.parse()?); // type checking done in construct_characteristic_static + }, "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)]")), + "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: read.unwrap_or_default(), - write: write.unwrap_or_default(), write_without_response: write_without_response.unwrap_or_default(), - notify: notify.unwrap_or_default(), indicate: indicate.unwrap_or_default(), - on_read, - on_write, - default_value, - descriptors: Vec::new(), + notify: notify.unwrap_or_default(), + write: write.unwrap_or_default(), + read: read.unwrap_or_default(), doc_string: String::new(), + descriptors: Vec::new(), + default_value, + on_write, + on_read, }) } } @@ -189,17 +197,20 @@ impl DescriptorArgs { 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" => { if uuid.is_some() { return Err(meta.error("'uuid' should not be specified more than once")) } let value = meta .value() - .map_err(|_| Error::custom("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'".to_string()))?; + .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'"))?; let uuid_string: LitStr = value.parse()?; uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); }, @@ -207,15 +218,26 @@ impl DescriptorArgs { "write" => check_multi(&mut write, "write", &meta)?, "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, "notify" => check_multi(&mut notify, "notify", &meta)?, - "value" => { - if default_value.is_some() { + "indicate" => check_multi(&mut indicate, "indicate", &meta)?, + // TODO Parse read and write callbacks for descriptors + // "on_read" => if on_read.is_some() { + // return Err(meta.error("'on_read' should not be specified more than once")) + // } else { + // on_read = Some(meta.value()?.parse()?) + // }, + // "on_write" => if on_write.is_some() { + // return Err(meta.error("'on_write' should not be specified more than once")) + // } else { + // on_write = Some(meta.value()?.parse()?) + // }, + "value" => if default_value.is_some() { return Err(meta.error("'value' should not be specified more than once")) - } - 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()?); // type checking done in construct_characteristic_static - }, + } else { + let value = meta + .value() + .map_err(|_| meta.error("value must be followed by '= [data]'. i.e. value = 'hello'"))?; + default_value = Some(value.parse()?); // type checking done in construct_characteristic_static + }, "default_value" => return Err(meta.error("use 'value' for default value")), other => return Err( meta.error( @@ -227,11 +249,15 @@ impl DescriptorArgs { })?; Ok(Self { uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, - read: read.unwrap_or_default(), - write: write.unwrap_or_default(), 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(), default_value, + // on_write, + // on_read, + }) } } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 6379e338..10c496aa 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::{Characteristic, CharacteristicArgs, DescriptorArgs}; 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; @@ -129,10 +130,7 @@ 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 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); @@ -151,6 +149,52 @@ impl ServiceBuilder { Some(val) => quote!(#val), // if set by user None => quote!(#ty::default()), // or default otherwise }; + let descriptors: 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 properties = set_desc_access_properties(&args); + let uuid = args.uuid; + // let read_callback = args.on_read.as_ref(); + // let write_callback = args.on_write.as_ref(); + let writeable = args.write || args.write_without_response; + let (default_value, length) = match &args.default_value { + Some(val) => (quote!(#val), quote!(#val.len())), // if set by user + None => { + // if the descriptor is writeable, give it some capacity otherwise set it to zero. + let length: usize = if writeable { 100 } else { 0 }; + (quote!(""), quote!(#length)) + } + }; + // 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 args.notify { 3 } else { 2 }; + quote_spanned! {characteristic.span=> + { + const CAPACITY: usize = #length; + static #name_screaming: static_cell::StaticCell<[u8; CAPACITY]> = static_cell::StaticCell::new(); + let store = #name_screaming.init([0; CAPACITY]); + store.copy_from_slice(#default_value.as_bytes()); + builder.add_descriptor( + #uuid, + &[#(#properties),*], + store, + None, + None, + ); + }; + } + }) + .collect(); + 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(); @@ -158,12 +202,12 @@ impl ServiceBuilder { let mut val = #ty::default(); // constrain the type of the value here val = #default_value; // update the temporary value with our new default store.copy_from_slice(GattValue::to_gatt(&val)); // convert to bytes - let mut builder = service.add_characteristic(#uuid, &[#(#properties),*], store); + 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() }; @@ -210,7 +254,8 @@ impl ServiceBuilder { // - 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. + // 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.construct_characteristic_static(ch); @@ -258,3 +303,18 @@ fn set_access_properties(args: &CharacteristicArgs) -> Vec { parse_property_into_list(args.indicate, quote! {CharacteristicProp::Indicate}, &mut properties); properties } + +/// Parse the properties of a characteristic and return a list of properties +fn set_desc_access_properties(args: &DescriptorArgs) -> 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); + parse_property_into_list( + args.write_without_response, + quote! {CharacteristicProp::WriteWithoutResponse}, + &mut properties, + ); + parse_property_into_list(args.notify, quote! {CharacteristicProp::Notify}, &mut properties); + parse_property_into_list(args.indicate, quote! {CharacteristicProp::Indicate}, &mut properties); + properties +} From ce03795a670f5bd48d9f37e047200dd2c3810237 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 14:28:31 +0000 Subject: [PATCH 07/26] improve error handling --- host-macros/src/characteristic.rs | 37 ++++++++--- host-macros/src/lib.rs | 104 +++++++++++++++++++++++------- host-macros/src/service.rs | 62 ++++++++++-------- 3 files changed, 141 insertions(+), 62 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 733520d9..6aa70892 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -43,7 +43,7 @@ impl Characteristic { pub(crate) struct DescriptorArgs { /// The UUID of the descriptor. pub uuid: Uuid, - /// The initial value of the descriptor. + /// 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, @@ -62,6 +62,9 @@ pub(crate) struct DescriptorArgs { /// If true, the characteristic can send indications. #[darling(default)] pub indicate: bool, + /// Capacity for writing new descriptors (u8) + #[darling(default)] + pub capacity: Option, // /// Optional callback to be triggered on a read event // #[darling(default)] // pub on_read: Option, @@ -138,7 +141,7 @@ impl CharacteristicArgs { } let value = meta .value() - .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'"))?; + .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?; let uuid_string: LitStr = value.parse()?; uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); }, @@ -162,11 +165,11 @@ impl CharacteristicArgs { } else { let value = meta .value() - .map_err(|_| meta.error("value must be followed by '= [data]'. i.e. value = 'hello'"))?; + .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"42\""))?; default_value = Some(value.parse()?); // type checking done in construct_characteristic_static }, - "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)]")), + "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!( @@ -200,6 +203,7 @@ impl DescriptorArgs { let mut indicate: 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| { @@ -210,7 +214,7 @@ impl DescriptorArgs { } let value = meta .value() - .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'"))?; + .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?; let uuid_string: LitStr = value.parse()?; uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); }, @@ -230,33 +234,46 @@ impl DescriptorArgs { // } else { // on_write = Some(meta.value()?.parse()?) // }, + "capacity" => if capacity.is_some() { + return Err(meta.error("'capacity' should not be specified more than once")) + } else { + let value = meta.value().map_err(|_| meta.error("'capacity' must be followed by '= [data]'. i.e. value = 100"))?; + capacity = Some(value.parse()?); + } "value" => if default_value.is_some() { return Err(meta.error("'value' should not be specified more than once")) } else { let value = meta .value() - .map_err(|_| meta.error("value must be followed by '= [data]'. i.e. value = 'hello'"))?; + .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"hello\""))?; default_value = Some(value.parse()?); // type checking done in construct_characteristic_static }, "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, notify, value" + "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, notify, value, capacity" ))), }; Ok(()) })?; + let write = write.unwrap_or_default(); + let write_without_response = write_without_response.unwrap_or_default(); + if (write || write_without_response) && capacity.is_none() { + return Err(syn::Error::new(attribute.meta.span(), "'capacity' must be specified for a writeable descriptor")); + } + Ok(Self { uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, - 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(), + write_without_response, default_value, // on_write, // on_read, + capacity, + write, }) } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index af301660..2b2d36da 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -16,7 +16,7 @@ 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(); } @@ -146,6 +153,26 @@ 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)] +/// #[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, @@ -159,26 +186,55 @@ fn check_for_characteristic( }; let mut descriptors = Vec::new(); let mut doc_string = String::new(); + let mut characteristic_count = 0; for attr in &field.attrs { - if attr.path().is_ident("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'); + 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); + } } - doc_string.push_str(text); } } } - } - } else if attr.path().is_ident("descriptor") { - match DescriptorArgs::parse(attr) { - Ok(args) => descriptors.push(args), - Err(e) => { - *err = Some(e); + "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 + characteristic_count += 1; + if characteristic_count > 1 { + *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. + } + } + "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. } } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 10c496aa..5aa53ebe 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -130,25 +130,6 @@ 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!("{}", 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 uuid = characteristic.args.uuid; - let read_callback = characteristic - .args - .on_read - .as_ref() - .map(|callback| quote!(builder.set_read_callback(#callback);)); - let write_callback = characteristic - .args - .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 - }; let descriptors: TokenStream2 = characteristic .args .descriptors @@ -162,13 +143,18 @@ impl ServiceBuilder { // let read_callback = args.on_read.as_ref(); // let write_callback = args.on_write.as_ref(); let writeable = args.write || args.write_without_response; - let (default_value, length) = match &args.default_value { - Some(val) => (quote!(#val), quote!(#val.len())), // if set by user + let capacity = match &args.capacity { + Some(cap) => quote!(#cap), None => { - // if the descriptor is writeable, give it some capacity otherwise set it to zero. - let length: usize = if writeable { 100 } else { 0 }; - (quote!(""), quote!(#length)) - } + if writeable { + panic!("'capacity' must be specified for writeable descriptors"); + } + quote!(0u8) + }, + }; + let default_value = match &args.default_value { + Some(val) => quote!(#val), // if set by user + None => quote!(""), }; // At least two attributes will be added to the attribute table for each characteristic: // - The characteristic declaration @@ -179,9 +165,9 @@ impl ServiceBuilder { self.attribute_count += if args.notify { 3 } else { 2 }; quote_spanned! {characteristic.span=> { - const CAPACITY: usize = #length; - static #name_screaming: static_cell::StaticCell<[u8; CAPACITY]> = static_cell::StaticCell::new(); - let store = #name_screaming.init([0; CAPACITY]); + 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]); store.copy_from_slice(#default_value.as_bytes()); builder.add_descriptor( #uuid, @@ -195,6 +181,26 @@ impl ServiceBuilder { }) .collect(); + 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 uuid = characteristic.args.uuid; + let read_callback = characteristic + .args + .on_read + .as_ref() + .map(|callback| quote!(builder.set_read_callback(#callback);)); + let write_callback = characteristic + .args + .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(); From 9ab7eb8b90daef5f32b0dad5c603c4d9b6b1c224 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 14:29:15 +0000 Subject: [PATCH 08/26] update example for new tags --- examples/apps/src/ble_bas_peripheral.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 2d33e184..4f2312b3 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,7 +25,9 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write)] + #[descriptor(uuid = "2b20", read, write, notify, value = "Battery Level", capacity = 100)] + #[descriptor(uuid = "2b21", read, write, notify, value = "Other Descriptor", capacity = 255)] + #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write, value = 10)] level: u8, } From ca6f8bd869eeca2c539c1765ae37cc21e5ac428f Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 14:35:07 +0000 Subject: [PATCH 09/26] fix copy from slice for strings --- host-macros/src/service.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 5aa53ebe..5a4df9ca 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -168,7 +168,9 @@ impl ServiceBuilder { 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]); - store.copy_from_slice(#default_value.as_bytes()); + if !#default_value.is_empty() { + store[..#default_value.len()].copy_from_slice(#default_value.as_bytes()); + } builder.add_descriptor( #uuid, &[#(#properties),*], From 9c720206f156bf20647948c1ea7006456dd32631 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 15:58:43 +0000 Subject: [PATCH 10/26] fixes from PR feedback --- host-macros/src/characteristic.rs | 109 ++++++++++++------------------ host-macros/src/lib.rs | 7 +- host-macros/src/service.rs | 2 +- 3 files changed, 47 insertions(+), 71 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 6aa70892..6a165ed8 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -112,9 +112,9 @@ pub(crate) struct CharacteristicArgs { } /// Check if this bool type has been specified more than once. -fn check_multi(val: &mut Option, name: &str, meta: &ParseNestedMeta<'_>) -> Result<()> { - if val.is_none() { - *val = Some(true); +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"))) @@ -136,38 +136,26 @@ impl CharacteristicArgs { attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(meta.error("no ident"))?.to_string().as_str() { "uuid" => { - if uuid.is_some() { - return Err(meta.error("'uuid' should not be specified more than once")) - } - let value = meta + let parser = meta .value() .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?; - let uuid_string: LitStr = value.parse()?; - uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); + 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)?, - "write" => check_multi(&mut write, "write", &meta)?, - "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, - "notify" => check_multi(&mut notify, "notify", &meta)?, - "indicate" => check_multi(&mut indicate, "indicate", &meta)?, - "on_read" => if on_read.is_some() { - return Err(meta.error("'on_read' should not be specified more than once")) - } else { - on_read = Some(meta.value()?.parse()?) - }, - "on_write" => if on_write.is_some() { - return Err(meta.error("'on_write' should not be specified more than once")) - } else { - on_write = Some(meta.value()?.parse()?) - }, - "value" => if default_value.is_some() { - return Err(meta.error("'value' should not be specified more than once")) - } else { - let value = meta + "read" => check_multi(&mut read, "read", &meta, true)?, + "write" => check_multi(&mut write, "write", &meta, true)?, + "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &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()?)?, + "value" => { + let value = meta .value() .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"42\""))?; - default_value = Some(value.parse()?); // type checking done in construct_characteristic_static - }, + 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( @@ -209,45 +197,30 @@ impl DescriptorArgs { attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(meta.error("no ident"))?.to_string().as_str() { "uuid" => { - if uuid.is_some() { - return Err(meta.error("'uuid' should not be specified more than once")) - } - let value = meta + let parser = meta .value() .map_err(|_| meta.error("uuid must be followed by '= [data]'. i.e. uuid = \"2a37\""))?; - let uuid_string: LitStr = value.parse()?; - uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); + 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)?, - "write" => check_multi(&mut write, "write", &meta)?, - "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &meta)?, - "notify" => check_multi(&mut notify, "notify", &meta)?, - "indicate" => check_multi(&mut indicate, "indicate", &meta)?, - // TODO Parse read and write callbacks for descriptors - // "on_read" => if on_read.is_some() { - // return Err(meta.error("'on_read' should not be specified more than once")) - // } else { - // on_read = Some(meta.value()?.parse()?) - // }, - // "on_write" => if on_write.is_some() { - // return Err(meta.error("'on_write' should not be specified more than once")) - // } else { - // on_write = Some(meta.value()?.parse()?) - // }, - "capacity" => if capacity.is_some() { - return Err(meta.error("'capacity' should not be specified more than once")) - } else { - let value = meta.value().map_err(|_| meta.error("'capacity' must be followed by '= [data]'. i.e. value = 100"))?; - capacity = Some(value.parse()?); - } - "value" => if default_value.is_some() { - return Err(meta.error("'value' should not be specified more than once")) - } else { - let value = meta + "read" => check_multi(&mut read, "read", &meta, true)?, + "write" => check_multi(&mut write, "write", &meta, true)?, + "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &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()?)?, + "value" => { + let value = meta .value() - .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"hello\""))?; - default_value = Some(value.parse()?); // type checking done in construct_characteristic_static - }, + .map_err(|_| meta.error("'value' must be followed by '= [data]'. i.e. value = \"42\""))?; + 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( @@ -260,7 +233,10 @@ impl DescriptorArgs { let write = write.unwrap_or_default(); let write_without_response = write_without_response.unwrap_or_default(); if (write || write_without_response) && capacity.is_none() { - return Err(syn::Error::new(attribute.meta.span(), "'capacity' must be specified for a writeable descriptor")); + return Err(syn::Error::new( + attribute.meta.span(), + "'capacity' must be specified for a writeable descriptor", + )); } Ok(Self { @@ -274,7 +250,6 @@ impl DescriptorArgs { // on_read, capacity, write, - }) } } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index 2b2d36da..d358f7c4 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -186,7 +186,7 @@ fn check_for_characteristic( }; let mut descriptors = Vec::new(); let mut doc_string = String::new(); - let mut characteristic_count = 0; + let mut characteristic_checked = false; for attr in &field.attrs { if let Some(ident) = attr.path().get_ident() { match ident.to_string().as_str() { @@ -214,13 +214,14 @@ fn check_for_characteristic( }, "characteristic" => { // make sure we only have one characteristic meta tag - characteristic_count += 1; - if characteristic_count > 1 { + 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" => { diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 5a4df9ca..cfd81946 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -312,7 +312,7 @@ fn set_access_properties(args: &CharacteristicArgs) -> Vec { properties } -/// Parse the properties of a characteristic and return a list of properties +/// Parse the properties of a descriptor and return a list of properties fn set_desc_access_properties(args: &DescriptorArgs) -> Vec { let mut properties = Vec::new(); parse_property_into_list(args.read, quote! {CharacteristicProp::Read}, &mut properties); From 803713b79ce6ff67db429dc6e93d4deb77b306a5 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 16:07:46 +0000 Subject: [PATCH 11/26] add comment allowing indicate --- host-macros/src/characteristic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 6a165ed8..371e3f7b 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -225,7 +225,7 @@ impl DescriptorArgs { other => return Err( meta.error( format!( - "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, notify, value, capacity" + "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, indicate, write_without_response, notify, value, capacity" ))), }; Ok(()) From af8ef93f6f8e0157552d904d595cc32e4e8c5c78 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 16:20:58 +0000 Subject: [PATCH 12/26] add on_read and on_write to descriptors --- examples/apps/src/ble_bas_peripheral.rs | 4 ++-- host-macros/src/characteristic.rs | 26 ++++++++++++------------- host-macros/src/lib.rs | 2 +- host-macros/src/service.rs | 14 +++++++++---- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 4f2312b3..e510c112 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,8 +25,8 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[descriptor(uuid = "2b20", read, write, notify, value = "Battery Level", capacity = 100)] - #[descriptor(uuid = "2b21", read, write, notify, value = "Other Descriptor", capacity = 255)] + #[descriptor(uuid = "2b20", read, write, notify, value = "Battery Level", capacity = 100, on_read = battery_level_on_read)] + #[descriptor(uuid = "2b21", read, write, notify, value = "Other Descriptor", capacity = 255, on_write = battery_level_on_write)] #[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 371e3f7b..b1b9131a 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -65,12 +65,12 @@ pub(crate) struct DescriptorArgs { /// Capacity for writing new descriptors (u8) #[darling(default)] pub capacity: Option, - // /// Optional callback to be triggered on a read event - // #[darling(default)] - // pub on_read: Option, - // /// Optional callback to be triggered on a write event - // #[darling(default)] - // pub on_write: Option, + /// Optional callback to be triggered on a read event + #[darling(default)] + pub on_read: Option, + /// Optional callback to be triggered on a write event + #[darling(default)] + pub on_write: Option, } /// Characteristic attribute arguments @@ -189,8 +189,8 @@ impl DescriptorArgs { 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 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; @@ -209,8 +209,8 @@ impl DescriptorArgs { "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &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()?)?, + "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()?)?, "value" => { let value = meta .value() @@ -225,7 +225,7 @@ impl DescriptorArgs { other => return Err( meta.error( format!( - "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, indicate, write_without_response, notify, value, capacity" + "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, indicate, write_without_response, notify, value,\ncapacity, on_read, on_write" ))), }; Ok(()) @@ -246,9 +246,9 @@ impl DescriptorArgs { read: read.unwrap_or_default(), write_without_response, default_value, - // on_write, - // on_read, capacity, + on_write, + on_read, write, }) } diff --git a/host-macros/src/lib.rs b/host-macros/src/lib.rs index d358f7c4..80f7cdf1 100644 --- a/host-macros/src/lib.rs +++ b/host-macros/src/lib.rs @@ -164,7 +164,7 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { /// /// 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)] +/// #[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)] diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index cfd81946..e47fe7d7 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -140,8 +140,14 @@ impl ServiceBuilder { format_ident!("DESC_{}_{index}", to_screaming_snake_case(characteristic.name.as_str())); let properties = set_desc_access_properties(&args); let uuid = args.uuid; - // let read_callback = args.on_read.as_ref(); - // let write_callback = args.on_write.as_ref(); + let read_callback = match &args.on_read { + Some(callback) => quote!(Some(#callback)), + None => quote!(None), + }; + let write_callback = match &args.on_write { + Some(callback) => quote!(Some(#callback)), + None => quote!(None), + }; let writeable = args.write || args.write_without_response; let capacity = match &args.capacity { Some(cap) => quote!(#cap), @@ -175,8 +181,8 @@ impl ServiceBuilder { #uuid, &[#(#properties),*], store, - None, - None, + #read_callback, + #write_callback, ); }; } From 8e749aa7ce14da6e87bb49389cbaa25f1c16a440 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 11 Dec 2024 16:31:45 +0000 Subject: [PATCH 13/26] tidy up common access arguments --- host-macros/src/characteristic.rs | 95 ++++++++++++++----------------- host-macros/src/service.rs | 42 +++++--------- 2 files changed, 56 insertions(+), 81 deletions(-) diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index b1b9131a..e8cad6b5 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -36,17 +36,8 @@ 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. - 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, +#[derive(Debug, FromMeta, Default)] +pub(crate) struct AccessArgs { /// If true, the descriptor can be written. #[darling(default)] pub read: bool, @@ -62,9 +53,6 @@ pub(crate) struct DescriptorArgs { /// If true, the characteristic can send indications. #[darling(default)] pub indicate: bool, - /// Capacity for writing new descriptors (u8) - #[darling(default)] - pub capacity: Option, /// Optional callback to be triggered on a read event #[darling(default)] pub on_read: Option, @@ -73,34 +61,29 @@ pub(crate) struct DescriptorArgs { pub on_write: Option, } -/// Characteristic attribute arguments +/// Descriptor attribute arguments. +/// +/// Descriptors are optional and can be used to add additional metadata to the characteristic. #[derive(Debug, FromMeta)] -pub(crate) struct CharacteristicArgs { - /// The UUID of the characteristic. +pub(crate) struct DescriptorArgs { + /// The UUID of the descriptor. pub uuid: Uuid, - /// If true, the characteristic can be read. - #[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. + /// 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 indicate: bool, - /// Optional callback to be triggered on a read event + pub default_value: Option, #[darling(default)] - pub on_read: Option, - /// Optional callback to be triggered on a write event + /// Capacity for writing new descriptors (u8) + pub capacity: Option, #[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. + pub access: AccessArgs, +} + +/// Characteristic attribute arguments +#[derive(Debug, FromMeta)] +pub(crate) struct CharacteristicArgs { + /// The UUID of the characteristic. + pub uuid: Uuid, #[darling(default)] pub default_value: Option, /// Descriptors for the characteristic. @@ -109,6 +92,8 @@ pub(crate) struct CharacteristicArgs { pub descriptors: Vec, #[darling(default)] pub doc_string: String, + #[darling(default)] + pub access: AccessArgs, } /// Check if this bool type has been specified more than once. @@ -145,11 +130,11 @@ impl CharacteristicArgs { }, "read" => check_multi(&mut read, "read", &meta, true)?, "write" => check_multi(&mut write, "write", &meta, true)?, - "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &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" => { let value = meta .value() @@ -168,16 +153,18 @@ impl CharacteristicArgs { })?; Ok(Self { uuid: uuid.ok_or(Error::custom("Characteristic must have a UUID"))?, - 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(), doc_string: String::new(), descriptors: Vec::new(), default_value, - on_write, - on_read, + 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, + }, }) } } @@ -206,11 +193,11 @@ impl DescriptorArgs { }, "read" => check_multi(&mut read, "read", &meta, true)?, "write" => check_multi(&mut write, "write", &meta, true)?, - "write_without_response" => check_multi(&mut write_without_response, "write_without_response", &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" => { let value = meta .value() @@ -241,15 +228,17 @@ impl DescriptorArgs { Ok(Self { uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, - indicate: indicate.unwrap_or_default(), - notify: notify.unwrap_or_default(), - read: read.unwrap_or_default(), - write_without_response, default_value, capacity, - on_write, - on_read, - write, + access: AccessArgs { + indicate: indicate.unwrap_or_default(), + notify: notify.unwrap_or_default(), + read: read.unwrap_or_default(), + write_without_response, + on_write, + on_read, + write, + }, }) } } diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index e47fe7d7..1b18c30b 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -4,7 +4,7 @@ //! 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, DescriptorArgs}; +use crate::characteristic::{AccessArgs, Characteristic}; use crate::uuid::Uuid; use darling::{Error, FromMeta}; use inflector::cases::screamingsnakecase::to_screaming_snake_case; @@ -138,17 +138,18 @@ impl ServiceBuilder { .map(|(index, args)| { let name_screaming = format_ident!("DESC_{}_{index}", to_screaming_snake_case(characteristic.name.as_str())); - let properties = set_desc_access_properties(&args); + let access = &args.access; + let properties = set_access_properties(access); let uuid = args.uuid; - let read_callback = match &args.on_read { + let read_callback = match &access.on_read { Some(callback) => quote!(Some(#callback)), None => quote!(None), }; - let write_callback = match &args.on_write { + let write_callback = match &access.on_write { Some(callback) => quote!(Some(#callback)), None => quote!(None), }; - let writeable = args.write || args.write_without_response; + let writeable = access.write || access.write_without_response; let capacity = match &args.capacity { Some(cap) => quote!(#cap), None => { @@ -168,7 +169,7 @@ impl ServiceBuilder { // // 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 args.notify { 3 } else { 2 }; + self.attribute_count += if access.notify { 3 } else { 2 }; quote_spanned! {characteristic.span=> { const CAPACITY: u8 = #capacity; @@ -192,15 +193,14 @@ impl ServiceBuilder { 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);)); @@ -270,7 +270,8 @@ impl ServiceBuilder { // // 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 }; + let access = &ch.args.access; + self.attribute_count += if access.notify || access.indicate { 3 } else { 2 }; self.construct_characteristic_static(ch); } @@ -304,22 +305,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 { - 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); - parse_property_into_list( - args.write_without_response, - quote! {CharacteristicProp::WriteWithoutResponse}, - &mut properties, - ); - parse_property_into_list(args.notify, quote! {CharacteristicProp::Notify}, &mut properties); - parse_property_into_list(args.indicate, quote! {CharacteristicProp::Indicate}, &mut properties); - properties -} - -/// Parse the properties of a descriptor and return a list of properties -fn set_desc_access_properties(args: &DescriptorArgs) -> 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); From 86c14e20e9b17b8069d15b1b8130e1df9730b03d Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Thu, 12 Dec 2024 15:49:29 +0000 Subject: [PATCH 14/26] fix default value for [u8;N] and descriptor capacity --- host-macros/src/service.rs | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 1b18c30b..b582c161 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -84,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; @@ -137,7 +149,7 @@ impl ServiceBuilder { .enumerate() .map(|(index, args)| { let name_screaming = - format_ident!("DESC_{}_{index}", to_screaming_snake_case(characteristic.name.as_str())); + 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; @@ -150,26 +162,22 @@ impl ServiceBuilder { None => quote!(None), }; let writeable = access.write || access.write_without_response; + 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 => { if writeable { panic!("'capacity' must be specified for writeable descriptors"); } - quote!(0u8) + quote!(#default_value.len() as u8) }, }; - let default_value = match &args.default_value { - Some(val) => quote!(#val), // if set by user - None => quote!(""), - }; - // 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 access.notify { 3 } else { 2 }; + + self.increment_attributes(access); + quote_spanned! {characteristic.span=> { const CAPACITY: u8 = #capacity; @@ -213,7 +221,7 @@ impl ServiceBuilder { let #char_name = { static #name_screaming: static_cell::StaticCell<[u8; size_of::<#ty>()]> = static_cell::StaticCell::new(); let store = #name_screaming.init(Default::default()); - let mut val = #ty::default(); // constrain the type of the value here + let mut val = <#ty>::default(); // constrain the type of the value here val = #default_value; // update the temporary value with our new default store.copy_from_slice(GattValue::to_gatt(&val)); // convert to bytes let mut builder = service @@ -264,14 +272,7 @@ impl ServiceBuilder { }); 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. - let access = &ch.args.access; - self.attribute_count += if access.notify || access.indicate { 3 } else { 2 }; + self.increment_attributes(&ch.args.access); self.construct_characteristic_static(ch); } From ec7c2e70c2518703b639d793d1c2af77ae87ca1d Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Thu, 12 Dec 2024 16:29:19 +0000 Subject: [PATCH 15/26] add descriptors and defaults to test --- host/tests/gatt_derive.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 9f799ed9..5bc6f94f 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -25,8 +25,21 @@ struct Server { #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { + #[descriptor(uuid = "2b20", read, value = "Read Only Descriptor", on_read = value_on_read)] + /// Battery Level #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 42)] - value: u8, + #[descriptor(uuid = "2b21", read, write, notify, value = "Writeable Descriptor requires capacity", capacity = 255, on_write = value_on_write)] + first: u8, + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 123.321)] + /// Order doesn't matter + #[descriptor(uuid = "2b20", read, write, notify, capacity = 100, on_read = value_on_read)] // empty descriptor + second: f32, + /// Multi + /// + /// Line + /// Comment + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, value = [0,1])] + third: [u8; 2], } static READ_FLAG: CriticalSectionMutex> = CriticalSectionMutex::new(RefCell::new(false)); From 7f4116ae0f9cd74cd2babf69333a159d6d4164e0 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Thu, 12 Dec 2024 23:03:32 +0000 Subject: [PATCH 16/26] remove notify and indicate from descriptors --- examples/apps/src/ble_bas_peripheral.rs | 4 ++-- host-macros/src/characteristic.rs | 23 +++++++++++------------ host/tests/gatt_derive.rs | 10 +++++----- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index e510c112..2c94c54d 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,8 +25,8 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[descriptor(uuid = "2b20", read, write, notify, value = "Battery Level", capacity = 100, on_read = battery_level_on_read)] - #[descriptor(uuid = "2b21", read, write, notify, value = "Other Descriptor", capacity = 255, on_write = battery_level_on_write)] + #[descriptor(uuid = "2b20", read, write, value = "Battery Level", capacity = 100, on_read = battery_level_on_read)] + #[descriptor(uuid = "2b21", read, write, value = "Other Descriptor", capacity = 255, on_write = battery_level_on_write)] #[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 e8cad6b5..3c6a29f2 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -2,7 +2,7 @@ //! //! 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; @@ -38,16 +38,16 @@ impl Characteristic { #[derive(Debug, FromMeta, Default)] pub(crate) struct AccessArgs { - /// If true, the descriptor can be written. + /// If true, the characteristic can be written. #[darling(default)] pub read: bool, - /// If true, the descriptor can be written. + /// If true, the characteristic can be written. #[darling(default)] pub write: bool, - /// If true, the descriptor can be written without a response. + /// If true, the characteristic can be written without a response. #[darling(default)] pub write_without_response: bool, - /// If true, the descriptor can send notifications. + /// If true, the characteristic can send notifications. #[darling(default)] pub notify: bool, /// If true, the characteristic can send indications. @@ -84,12 +84,15 @@ pub(crate) struct DescriptorArgs { 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. + /// Parsed in super::check_for_characteristic. #[darling(default, multiple)] pub descriptors: Vec, + /// Any '///' comments on each field, parsed in super::check_for_characteristic. #[darling(default)] pub doc_string: String, #[darling(default)] @@ -174,8 +177,6 @@ impl DescriptorArgs { let mut uuid: Option = None; 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 capacity: Option = None; @@ -193,8 +194,6 @@ impl DescriptorArgs { }, "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)?, @@ -212,7 +211,7 @@ impl DescriptorArgs { other => return Err( meta.error( format!( - "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, indicate, write_without_response, notify, value,\ncapacity, on_read, on_write" + "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, write, write_without_response, value,\ncapacity, on_read, on_write" ))), }; Ok(()) @@ -231,8 +230,8 @@ impl DescriptorArgs { default_value, capacity, access: AccessArgs { - indicate: indicate.unwrap_or_default(), - notify: notify.unwrap_or_default(), + indicate: false, // not possible for descriptor + notify: false, // not possible for descriptor read: read.unwrap_or_default(), write_without_response, on_write, diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 5bc6f94f..caee12c3 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -28,18 +28,18 @@ struct CustomService { #[descriptor(uuid = "2b20", read, value = "Read Only Descriptor", on_read = value_on_read)] /// Battery Level #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 42)] - #[descriptor(uuid = "2b21", read, write, notify, value = "Writeable Descriptor requires capacity", capacity = 255, on_write = value_on_write)] - first: u8, + #[descriptor(uuid = "2b21", read, write, value = "Writeable Descriptor requires capacity", capacity = 255, on_write = value_on_write)] + pub first: u8, #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 123.321)] /// Order doesn't matter - #[descriptor(uuid = "2b20", read, write, notify, capacity = 100, on_read = value_on_read)] // empty descriptor - second: f32, + #[descriptor(uuid = "2b20", read, write, capacity = 100, on_read = value_on_read)] // empty descriptor + pub second: f32, /// Multi /// /// Line /// Comment #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, value = [0,1])] - third: [u8; 2], + pub third: [u8; 2], } static READ_FLAG: CriticalSectionMutex> = CriticalSectionMutex::new(RefCell::new(false)); From 8de2421b11c13cdaa93dbfde190bc71096daa87a Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Thu, 12 Dec 2024 23:04:19 +0000 Subject: [PATCH 17/26] tidy up descriptor builder --- host-macros/src/service.rs | 119 +++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index b582c161..26750718 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -114,8 +114,8 @@ impl ServiceBuilder { quote! { #visibility struct #struct_name { - handle: AttributeHandle, #fields + handle: AttributeHandle, } #[allow(unused)] @@ -142,62 +142,7 @@ impl ServiceBuilder { /// Construct instructions for adding a characteristic to the service, with static storage. fn construct_characteristic_static(&mut self, characteristic: Characteristic) { - let descriptors: 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 writeable = access.write || access.write_without_response; - 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 => { - if writeable { - panic!("'capacity' must be specified for writeable descriptors"); - } - quote!(#default_value.len() as u8) - }, - }; - - self.increment_attributes(access); - - quote_spanned! {characteristic.span=> - { - 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]); - if !#default_value.is_empty() { - store[..#default_value.len()].copy_from_slice(#default_value.as_bytes()); - } - builder.add_descriptor( - #uuid, - &[#(#properties),*], - store, - #read_callback, - #write_callback, - ); - }; - } - }) - .collect(); - + 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; @@ -297,6 +242,66 @@ impl ServiceBuilder { } 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 writeable = access.write || access.write_without_response; + 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 => { + if writeable { + panic!("'capacity' must be specified for writeable descriptors"); + } + 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]); + if !value.is_empty() { + store[..value.len()].copy_from_slice(value.as_bytes()); + } + builder.add_descriptor( + #uuid, + &[#(#properties),*], + store, + #read_callback, + #write_callback, + ); + }; + } + }) + .collect() + } } fn parse_property_into_list(property: bool, variant: TokenStream2, properties: &mut Vec) { From 6edd9a5ce0fd93378a38b60ce612daf29ca96039 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Sun, 15 Dec 2024 18:03:17 +0000 Subject: [PATCH 18/26] update starting value to fix heapless vec initialization --- host-macros/src/service.rs | 7 ++++--- host/Cargo.toml | 1 + host/tests/gatt_derive.rs | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 26750718..627841a4 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -158,8 +158,8 @@ impl ServiceBuilder { .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 + Some(val) => quote!(#val), // if set by user + None => quote!(<#ty>::default()), // or default otherwise }; self.code_build_chars.extend(quote_spanned! {characteristic.span=> @@ -168,7 +168,8 @@ impl ServiceBuilder { 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 - store.copy_from_slice(GattValue::to_gatt(&val)); // convert to bytes + let bytes = GattValue::to_gatt(&val); + store[..bytes.len()].copy_from_slice(bytes); let mut builder = service .add_characteristic(#uuid, &[#(#properties),*], store); #read_callback diff --git a/host/Cargo.toml b/host/Cargo.toml index ebe721bf..e699b736 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/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index caee12c3..a1e2608f 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -25,12 +25,12 @@ struct Server { #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { - #[descriptor(uuid = "2b20", read, value = "Read Only Descriptor", on_read = value_on_read)] + #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read, on_read = value_on_read)] /// Battery Level - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 42)] - #[descriptor(uuid = "2b21", read, write, value = "Writeable Descriptor requires capacity", capacity = 255, on_write = value_on_write)] + #[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 = "Writeable Descriptor requires capacity", read, write, capacity = 255, on_write = value_on_write)] pub first: u8, - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write, value = 123.321)] + #[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, write, capacity = 100, on_read = value_on_read)] // empty descriptor pub second: f32, @@ -38,8 +38,10 @@ struct CustomService { /// /// Line /// Comment - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, value = [0,1])] + #[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)); From fdb41074c4a77bc07393b9adc3d6082187fdf570 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 16:05:14 +0000 Subject: [PATCH 19/26] suppress write options for descriptors --- examples/apps/src/ble_bas_peripheral.rs | 4 +- host-macros/src/characteristic.rs | 69 ++++++++++++------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 04ced4b5..627d155e 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,8 +25,8 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[descriptor(uuid = "2b20", read, write, value = "Battery Level", capacity = 100, on_read = battery_level_on_read)] - #[descriptor(uuid = "2b21", read, write, value = "Other Descriptor", capacity = 255, on_write = battery_level_on_write)] + #[descriptor(uuid = "2b20", read, value = "Battery Level", on_read = battery_level_on_read)] + #[descriptor(uuid = "2b21", read, value = "Other Descriptor")] #[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 3c6a29f2..e4abf751 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -176,67 +176,66 @@ 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 write: Option = None; let mut on_read: Option = None; - let mut on_write: Option = None; - let mut capacity: 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; + // 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() { + 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\""))?; + .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)?, + // "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)?, + // "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 = \"42\""))?; + 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()?)? - } + // "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" - ))), + 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(()) })?; - let write = write.unwrap_or_default(); - let write_without_response = write_without_response.unwrap_or_default(); - if (write || write_without_response) && capacity.is_none() { - return Err(syn::Error::new( - attribute.meta.span(), - "'capacity' must be specified for a writeable descriptor", - )); - } Ok(Self { uuid: uuid.ok_or(Error::custom("Descriptor must have a UUID"))?, default_value, - capacity, + capacity: None, access: AccessArgs { indicate: false, // not possible for descriptor notify: false, // not possible for descriptor read: read.unwrap_or_default(), - write_without_response, - on_write, + write_without_response: false, + on_write: None, + write: false, on_read, - write, }, }) } From 02e1e6d4edc2aace53411390a4fa0a7ee256b5c0 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 16:07:26 +0000 Subject: [PATCH 20/26] allow descriptors to be defined as byte array or string literal --- examples/apps/src/ble_bas_peripheral.rs | 2 +- host-macros/src/service.rs | 13 +++---------- host/src/types/gatt_traits.rs | 1 + 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 627d155e..afa9502a 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -26,7 +26,7 @@ struct Server { struct BatteryService { /// Battery Level #[descriptor(uuid = "2b20", read, value = "Battery Level", on_read = battery_level_on_read)] - #[descriptor(uuid = "2b21", read, value = "Other Descriptor")] + #[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/service.rs b/host-macros/src/service.rs index 627841a4..c015e929 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -265,19 +265,13 @@ impl ServiceBuilder { Some(callback) => quote!(Some(#callback)), None => quote!(None), }; - let writeable = access.write || access.write_without_response; 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 => { - if writeable { - panic!("'capacity' must be specified for writeable descriptors"); - } - quote!(#default_value.len() as u8) - }, + None => quote!(#default_value.len() as u8) }; self.attribute_count += 1; // descriptors should always only be one attribute. @@ -288,9 +282,8 @@ impl ServiceBuilder { 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]); - if !value.is_empty() { - store[..value.len()].copy_from_slice(value.as_bytes()); - } + let value = GattValue::to_gatt(&value); + store[..value.len()].copy_from_slice(value); builder.add_descriptor( #uuid, &[#(#properties),*], 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::(); From 8116571247b0e174aa7858ea8be0faa8fd70b4da Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 16:46:08 +0000 Subject: [PATCH 21/26] fix bug on docstring vec length mismatch with non-characteristic fields --- host-macros/src/service.rs | 6 ++++-- host/tests/gatt_derive.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index c015e929..8300dcae 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -194,15 +194,16 @@ 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() => #vis #ident: #ty::default(), - }) + }); + doc_strings.push(String::new()); // not supporting docstrings here yet } - let mut doc_strings = Vec::new(); // Process characteristic fields for ch in characteristics { let char_name = format_ident!("{}", ch.name); @@ -222,6 +223,7 @@ impl ServiceBuilder { self.construct_characteristic_static(ch); } + assert_eq!(fields.len(), doc_strings.len()); // Processing common to all fields for (field, doc_string) in fields.iter().zip(doc_strings) { let docs: TokenStream2 = doc_string diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index e0953831..29bdd8ce 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -28,11 +28,11 @@ struct CustomService { #[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 = "Writeable Descriptor requires capacity", read, write, capacity = 255, on_write = value_on_write)] + #[descriptor(uuid = "2b21", value = [0x01,0x02,0x03], read)] pub first: 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, write, capacity = 100, on_read = value_on_read)] // empty descriptor + #[descriptor(uuid = "2b20", read, value = 42u16.to_le_bytes(), on_read = value_on_read)] // empty descriptor pub second: f32, /// Multi /// From 76e965c400963edeca4e6f70f6895abbe5670db7 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 17:27:36 +0000 Subject: [PATCH 22/26] fix gatt_derive manual table size --- host/tests/gatt_derive.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 29bdd8ce..eb61db02 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -18,7 +18,7 @@ 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, } @@ -29,7 +29,7 @@ struct CustomService { /// 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 first: u8, + 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 From bc66c5e40283510048c7a2e35d832080f98f8c62 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 17:59:26 +0000 Subject: [PATCH 23/26] remove on_read and on_write --- examples/apps/src/ble_bas_peripheral.rs | 13 ++----------- host-macros/src/characteristic.rs | 23 ++--------------------- host-macros/src/service.rs | 22 ++-------------------- host/tests/gatt_derive.rs | 8 ++++---- 4 files changed, 10 insertions(+), 56 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index afa9502a..d5e85a00 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,21 +25,12 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[descriptor(uuid = "2b20", read, value = "Battery Level", on_read = battery_level_on_read)] + #[descriptor(uuid = "2b20", read, value = "Battery Level")] #[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)] + #[characteristic(uuid = "2a19", read, write, notify, value = 10)] level: u8, } -fn battery_level_on_read(_connection: &Connection) { - info!("[gatt] Read event on battery level characteristic"); -} - -fn battery_level_on_write(_connection: &Connection, data: &[u8]) -> Result<(), ()> { - info!("[gatt] Write event on battery level characteristic: {:?}", data); - Ok(()) -} - /// Run the BLE stack. pub async fn run(controller: C) where diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index e4abf751..68eb125e 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -12,7 +12,6 @@ use syn::meta::ParseNestedMeta; use syn::parse::Result; use syn::spanned::Spanned; use syn::Field; -use syn::Ident; use syn::LitStr; #[derive(Debug)] @@ -53,12 +52,6 @@ pub(crate) struct AccessArgs { /// If true, the characteristic can send indications. #[darling(default)] pub indicate: bool, - /// Optional callback to be triggered on a read event - #[darling(default)] - pub on_read: Option, - /// Optional callback to be triggered on a write event - #[darling(default)] - pub on_write: Option, } /// Descriptor attribute arguments. @@ -117,8 +110,6 @@ impl CharacteristicArgs { 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| { @@ -135,8 +126,6 @@ impl CharacteristicArgs { "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" => { let value = meta @@ -149,7 +138,7 @@ impl CharacteristicArgs { other => return Err( meta.error( format!( - "Unsupported characteristic property: '{other}'.\nSupported properties are:\nuuid, read, write, write_without_response, notify, indicate, value\non_read, on_write" + "Unsupported characteristic property: '{other}'.\nSupported properties are:\nuuid, read, write, write_without_response, notify, indicate, value" ))), }; Ok(()) @@ -165,8 +154,6 @@ impl CharacteristicArgs { notify: notify.unwrap_or_default(), write: write.unwrap_or_default(), read: read.unwrap_or_default(), - on_write, - on_read, }, }) } @@ -177,8 +164,6 @@ impl DescriptorArgs { 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; @@ -200,8 +185,6 @@ impl DescriptorArgs { } "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(|_| { @@ -216,7 +199,7 @@ 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, write, write_without_response, value, capacity" "Unsupported descriptor property: '{other}'.\nSupported properties are: uuid, read, value, on_read" ))); } @@ -233,9 +216,7 @@ impl DescriptorArgs { 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/service.rs b/host-macros/src/service.rs index 8300dcae..5d8ad07f 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -149,14 +149,6 @@ impl ServiceBuilder { let access = &characteristic.args.access; let properties = set_access_properties(access); let uuid = characteristic.args.uuid; - let read_callback = access - .on_read - .as_ref() - .map(|callback| quote!(builder.set_read_callback(#callback);)); - 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 @@ -172,8 +164,6 @@ impl ServiceBuilder { store[..bytes.len()].copy_from_slice(bytes); let mut builder = service .add_characteristic(#uuid, &[#(#properties),*], store); - #read_callback - #write_callback #descriptors @@ -259,14 +249,6 @@ impl ServiceBuilder { 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!(""), @@ -290,8 +272,8 @@ impl ServiceBuilder { #uuid, &[#(#properties),*], store, - #read_callback, - #write_callback, + None, + None, ); }; } diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index eb61db02..b2402a9b 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -25,14 +25,14 @@ struct Server { #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { - #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read, on_read = value_on_read)] + #[descriptor(uuid = "2b20", value = "Read Only Descriptor", 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)] + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", value = 42, read, write, notify)] #[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)] + #[characteristic(uuid = "408814df-5dd4-1f87-ec11-cdb001100000", value = 123.321, read, write, notify)] /// Order doesn't matter - #[descriptor(uuid = "2b20", read, value = 42u16.to_le_bytes(), on_read = value_on_read)] // empty descriptor + #[descriptor(uuid = "2b20", read, value = 42u16.to_le_bytes())] // empty descriptor pub second: f32, /// Multi /// From 4f2f5f1698176ad1c90759bba875088f18c584d4 Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 18:43:42 +0000 Subject: [PATCH 24/26] remove starting value --- host/tests/gatt_derive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index b2402a9b..cf19752d 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -27,7 +27,7 @@ struct Server { struct CustomService { #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read)] /// Battery Level - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", value = 42, read, write, notify)] + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify)] #[descriptor(uuid = "2b21", value = [0x01,0x02,0x03], read)] pub value: u8, #[characteristic(uuid = "408814df-5dd4-1f87-ec11-cdb001100000", value = 123.321, read, write, notify)] From 33964e14074d8ec563bd198c74fd9133ca51430e Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 19:35:19 +0000 Subject: [PATCH 25/26] Revert "remove starting value" This reverts commit 4f2f5f1698176ad1c90759bba875088f18c584d4. --- host/tests/gatt_derive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index cf19752d..b2402a9b 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -27,7 +27,7 @@ struct Server { struct CustomService { #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read)] /// Battery Level - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify)] + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", value = 42, read, write, notify)] #[descriptor(uuid = "2b21", value = [0x01,0x02,0x03], read)] pub value: u8, #[characteristic(uuid = "408814df-5dd4-1f87-ec11-cdb001100000", value = 123.321, read, write, notify)] From 9fd6f7e4f61e55a05bcea266bb93cbd8c0523f3d Mon Sep 17 00:00:00 2001 From: James Sizeland Date: Wed, 18 Dec 2024 19:35:28 +0000 Subject: [PATCH 26/26] Revert "remove on_read and on_write" This reverts commit bc66c5e40283510048c7a2e35d832080f98f8c62. --- examples/apps/src/ble_bas_peripheral.rs | 13 +++++++++++-- host-macros/src/characteristic.rs | 23 +++++++++++++++++++++-- host-macros/src/service.rs | 22 ++++++++++++++++++++-- host/tests/gatt_derive.rs | 8 ++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index d5e85a00..afa9502a 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -25,12 +25,21 @@ struct Server { #[gatt_service(uuid = "180f")] struct BatteryService { /// Battery Level - #[descriptor(uuid = "2b20", read, value = "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, value = 10)] + #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write, value = 10)] level: u8, } +fn battery_level_on_read(_connection: &Connection) { + info!("[gatt] Read event on battery level characteristic"); +} + +fn battery_level_on_write(_connection: &Connection, data: &[u8]) -> Result<(), ()> { + info!("[gatt] Write event on battery level characteristic: {:?}", data); + Ok(()) +} + /// Run the BLE stack. pub async fn run(controller: C) where diff --git a/host-macros/src/characteristic.rs b/host-macros/src/characteristic.rs index 68eb125e..e4abf751 100644 --- a/host-macros/src/characteristic.rs +++ b/host-macros/src/characteristic.rs @@ -12,6 +12,7 @@ use syn::meta::ParseNestedMeta; use syn::parse::Result; use syn::spanned::Spanned; use syn::Field; +use syn::Ident; use syn::LitStr; #[derive(Debug)] @@ -52,6 +53,12 @@ pub(crate) struct AccessArgs { /// If true, the characteristic can send indications. #[darling(default)] pub indicate: bool, + /// Optional callback to be triggered on a read event + #[darling(default)] + pub on_read: Option, + /// Optional callback to be triggered on a write event + #[darling(default)] + pub on_write: Option, } /// Descriptor attribute arguments. @@ -110,6 +117,8 @@ impl CharacteristicArgs { 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| { @@ -126,6 +135,8 @@ impl CharacteristicArgs { "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" => { let value = meta @@ -138,7 +149,7 @@ impl CharacteristicArgs { other => return Err( meta.error( format!( - "Unsupported characteristic property: '{other}'.\nSupported properties are:\nuuid, 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(()) @@ -154,6 +165,8 @@ impl CharacteristicArgs { notify: notify.unwrap_or_default(), write: write.unwrap_or_default(), read: read.unwrap_or_default(), + on_write, + on_read, }, }) } @@ -164,6 +177,8 @@ impl DescriptorArgs { 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; @@ -185,6 +200,8 @@ impl DescriptorArgs { } "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(|_| { @@ -199,7 +216,7 @@ 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, capacity" + // "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" ))); } @@ -216,7 +233,9 @@ impl DescriptorArgs { 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/service.rs b/host-macros/src/service.rs index 5d8ad07f..8300dcae 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -149,6 +149,14 @@ impl ServiceBuilder { let access = &characteristic.args.access; let properties = set_access_properties(access); let uuid = characteristic.args.uuid; + let read_callback = access + .on_read + .as_ref() + .map(|callback| quote!(builder.set_read_callback(#callback);)); + 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 @@ -164,6 +172,8 @@ impl ServiceBuilder { store[..bytes.len()].copy_from_slice(bytes); let mut builder = service .add_characteristic(#uuid, &[#(#properties),*], store); + #read_callback + #write_callback #descriptors @@ -249,6 +259,14 @@ impl ServiceBuilder { 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!(""), @@ -272,8 +290,8 @@ impl ServiceBuilder { #uuid, &[#(#properties),*], store, - None, - None, + #read_callback, + #write_callback, ); }; } diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index b2402a9b..eb61db02 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -25,14 +25,14 @@ struct Server { #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { - #[descriptor(uuid = "2b20", value = "Read Only Descriptor", read)] + #[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)] + #[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)] + #[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())] // empty descriptor + #[descriptor(uuid = "2b20", read, value = 42u16.to_le_bytes(), on_read = value_on_read)] // empty descriptor pub second: f32, /// Multi ///