diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 7bf1b84..6320df7 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -18,10 +18,8 @@ const MAX_ATTRIBUTES: usize = 10; type Resources = HostResources; -const ATTRIBUTE_DATA_SIZE: usize = 10; - // GATT Server definition -#[gatt_server(attribute_data_size = ATTRIBUTE_DATA_SIZE)] +#[gatt_server] struct Server { battery_service: BatteryService, } @@ -116,20 +114,19 @@ async fn advertise_task( break; } ConnectionEvent::Gatt { event, .. } => match event { - GattEvent::Read { value_handle } => { + GattEvent::Read { value_handle } => { if value_handle == level.handle { let value = server.get(&level); info!("[gatt] Read Event to Level Characteristic: {:?}", value); } - }, + } GattEvent::Write { value_handle } => { if value_handle == level.handle { let value = server.get(&level); info!("[gatt] Write Event to Level Characteristic: {:?}", value); } - }, + } }, - }, Either::Second(_) => { tick = tick.wrapping_add(1); diff --git a/host-macros/src/server.rs b/host-macros/src/server.rs index 61785c3..0a1715b 100644 --- a/host-macros/src/server.rs +++ b/host-macros/src/server.rs @@ -8,15 +8,13 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, quote_spanned}; use syn::{meta::ParseNestedMeta, parse_quote, spanned::Spanned, Expr, Result}; -/// Default size for the memory block storing attribute data in bytes -const DEFAULT_ATTRIBUTE_DATA_SIZE: usize = 32; /// MTU for a legacy BLE packet const LEGACY_BLE_MTU: usize = 27; #[derive(Default)] pub(crate) struct ServerArgs { mutex_type: Option, - attribute_data_size: Option, + attribute_table_size: Option, mtu: Option, } @@ -33,15 +31,15 @@ impl ServerArgs { let buffer = meta.value().map_err(|_| Error::custom("mutex_type must be followed by `= [type]`. e.g. mutex_type = NoopRawMutex".to_string()))?; self.mutex_type = Some(buffer.parse()?); } - "attribute_data_size" => { - let buffer = meta.value().map_err(|_| Error::custom("attribute_data_size msut be followed by `= [size]`. e.g. attribute_data_size = 32".to_string()))?; - self.attribute_data_size = Some(buffer.parse()?); + "attribute_table_size" => { + let buffer = meta.value().map_err(|_| Error::custom("attribute_table_size must be followed by `= [size]`. e.g. attribute_table_size = 32".to_string()))?; + self.attribute_table_size = Some(buffer.parse()?); } "mtu" => { let buffer = meta.value().map_err(|_| Error::custom("mtu must be followed by `= [size]`. e.g. mtu = 27".to_string()))?; self.mtu = Some(buffer.parse()?); } - other => return Err(meta.error(format!("Unsupported server property: '{other}'.\nSupported properties are: mutex_type, attribute_data_size, mtu"))), + other => return Err(meta.error(format!("Unsupported server property: '{other}'.\nSupported properties are: mutex_type, attribute_table_size, mtu"))), } Ok(()) } @@ -65,12 +63,6 @@ impl ServerBuilder { let mutex_type = self.arguments.mutex_type.unwrap_or(syn::Type::Verbatim(quote!( embassy_sync::blocking_mutex::raw::NoopRawMutex ))); - let attribute_data_size = if let Some(value) = self.arguments.attribute_data_size { - value - } else { - let tokens = quote!(#DEFAULT_ATTRIBUTE_DATA_SIZE); - parse_quote!(#tokens) - }; let mtu = if let Some(value) = self.arguments.mtu { value } else { @@ -81,6 +73,7 @@ impl ServerBuilder { let mut code_service_definition = TokenStream2::new(); let mut code_service_init = TokenStream2::new(); let mut code_server_populate = TokenStream2::new(); + let mut code_attribute_summation = TokenStream2::new(); for service in &self.properties.fields { let vis = &service.vis; let service_span = service.span(); @@ -98,12 +91,28 @@ impl ServerBuilder { code_server_populate.extend(quote_spanned! {service_span=> #service_name, }); + + code_attribute_summation.extend(quote_spanned! {service_span=> + + #service_type::ATTRIBUTE_COUNT + }) } + let attribute_table_size = if let Some(value) = self.arguments.attribute_table_size { + value + } else { + parse_quote!(GAP_SERVICE_ATTRIBUTE_COUNT #code_attribute_summation) + }; + quote! { + const _ATTRIBUTE_TABLE_SIZE: usize = #attribute_table_size; + // This pattern causes the assertion to happen at compile time + const _: () = { + core::assert!(_ATTRIBUTE_TABLE_SIZE >= GAP_SERVICE_ATTRIBUTE_COUNT #code_attribute_summation, "Specified attribute table size is insufficient. Please increase attribute_table_size or remove the argument entirely to allow automatic sizing of the attribute table."); + }; + #visibility struct #name<'reference, 'values, C: Controller> { - server: GattServer<'reference, 'values, C, #mutex_type, #attribute_data_size, #mtu>, + server: GattServer<'reference, 'values, C, #mutex_type, _ATTRIBUTE_TABLE_SIZE, #mtu>, #code_service_definition } @@ -112,7 +121,7 @@ impl ServerBuilder { /// Create a new Gatt Server instance. /// /// Requires you to add your own GAP Service. Use `new_default(name)` or `new_with_config(name, gap_config)` if you want to add a GAP Service. - #visibility fn new(stack: Stack<'reference, C>, mut table: AttributeTable<'values, #mutex_type, #attribute_data_size>) -> Self { + #visibility fn new(stack: Stack<'reference, C>, mut table: AttributeTable<'values, #mutex_type, _ATTRIBUTE_TABLE_SIZE>) -> Self { #code_service_init @@ -127,7 +136,7 @@ impl ServerBuilder { /// The maximum length which the name can be is 22 bytes (limited by the size of the advertising packet). /// If a name longer than this is passed, Err() is returned. #visibility fn new_default(stack: Stack<'reference, C>, name: &'values str) -> Result { - let mut table: AttributeTable<'_, #mutex_type, #attribute_data_size> = AttributeTable::new(); + let mut table: AttributeTable<'_, #mutex_type, _ATTRIBUTE_TABLE_SIZE> = AttributeTable::new(); GapConfig::default(name).build(&mut table)?; @@ -145,7 +154,7 @@ impl ServerBuilder { /// The maximum length which the device name can be is 22 bytes (limited by the size of the advertising packet). /// If a name longer than this is passed, Err() is returned. #visibility fn new_with_config(stack: Stack<'reference, C>, gap: GapConfig<'values>) -> Result { - let mut table: AttributeTable<'_, #mutex_type, #attribute_data_size> = AttributeTable::new(); + let mut table: AttributeTable<'_, #mutex_type, _ATTRIBUTE_TABLE_SIZE> = AttributeTable::new(); gap.build(&mut table)?; @@ -168,7 +177,7 @@ impl ServerBuilder { impl<'reference, 'values, C: Controller> core::ops::Deref for #name<'reference, 'values, C> { - type Target = GattServer<'reference, 'values, C, #mutex_type, #attribute_data_size, #mtu>; + type Target = GattServer<'reference, 'values, C, #mutex_type, _ATTRIBUTE_TABLE_SIZE, #mtu>; fn deref(&self) -> &Self::Target { &self.server diff --git a/host-macros/src/service.rs b/host-macros/src/service.rs index 7b27805..d445a4f 100644 --- a/host-macros/src/service.rs +++ b/host-macros/src/service.rs @@ -64,6 +64,7 @@ impl syn::parse::Parse for ServiceArgs { pub(crate) struct ServiceBuilder { properties: syn::ItemStruct, args: ServiceArgs, + attribute_count: usize, code_impl: TokenStream2, code_build_chars: TokenStream2, code_struct_init: TokenStream2, @@ -75,6 +76,7 @@ impl ServiceBuilder { Self { properties, args, + attribute_count: 1, // Service counts as an attribute code_struct_init: TokenStream2::new(), code_impl: TokenStream2::new(), code_fields: TokenStream2::new(), @@ -91,6 +93,7 @@ impl ServiceBuilder { let fields = self.code_fields; let code_build_chars = self.code_build_chars; let uuid = self.args.uuid; + let attribute_count = self.attribute_count; let read_callback = self .args .on_read @@ -104,6 +107,8 @@ impl ServiceBuilder { #[allow(unused)] impl #struct_name { + const ATTRIBUTE_COUNT: usize = #attribute_count; + #visibility fn new(table: &mut AttributeTable<'_, M, MAX_ATTRIBUTES>) -> Self where M: embassy_sync::blocking_mutex::raw::RawMutex, @@ -152,6 +157,7 @@ impl ServiceBuilder { #write_callback // TODO: Descriptors + // NOTE: Descriptors are attributes too - will need to increment self.attribute_count builder.build() }; @@ -193,6 +199,13 @@ impl ServiceBuilder { mutability: syn::FieldMutability::None, }); + // At least two attributes will be added to the attribute table for each characteristic: + // - The characteristic declaration + // - The characteristic's value declaration + // + // If the characteristic has either the notify or indicate property, a Client Characteristic Configuration Descriptor (CCCD) declaration will also be added. + self.attribute_count += if ch.args.notify || ch.args.indicate { 3 } else { 2 }; + self.construct_characteristic_static(ch); } diff --git a/host/src/gap.rs b/host/src/gap.rs index 7a50460..62560c3 100644 --- a/host/src/gap.rs +++ b/host/src/gap.rs @@ -24,6 +24,15 @@ const APPEARANCE_UUID: u16 = 0x2a01; /// Advertising packet is limited to 31 bytes. 9 of these are used by other GAP data, leaving 22 bytes for the Device Name characteristic const DEVICE_NAME_MAX_LENGTH: usize = 22; +/// The number of attributes added by the GAP and GATT services +/// GAP_SERVICE: 1 +/// ├── DEVICE_NAME: 2 +/// └── APPEARANCE: 2 +/// GATT_SERVICE: + 1 +/// --- +/// = 6 +pub const GAP_SERVICE_ATTRIBUTE_COUNT: usize = 6; + pub mod appearance { //! The representation of the external appearance of the device. //! diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 515a5f8..24e4eb3 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_data_size = 10, mtu = 27)] +#[gatt_server(mutex_type = NoopRawMutex, attribute_table_size = 10, mtu = 27)] struct Server { service: CustomService, }