diff --git a/examples/apps/src/ble_bas_central.rs b/examples/apps/src/ble_bas_central.rs index c2ef46d9..c8d57312 100644 --- a/examples/apps/src/ble_bas_central.rs +++ b/examples/apps/src/ble_bas_central.rs @@ -47,7 +47,7 @@ where let service = services.first().unwrap().clone(); info!("Looking for value handle"); - let c = client + let c: Characteristic = client .characteristic_by_uuid(&service, &Uuid::new_short(0x2a19)) .await .unwrap(); diff --git a/examples/apps/src/ble_bas_peripheral.rs b/examples/apps/src/ble_bas_peripheral.rs index 4d1faf69..2c4462c8 100644 --- a/examples/apps/src/ble_bas_peripheral.rs +++ b/examples/apps/src/ble_bas_peripheral.rs @@ -24,10 +24,19 @@ struct Server { // Battery service #[gatt_service(uuid = "180f")] struct BatteryService { - #[characteristic(uuid = "2a19", read, notify)] + #[characteristic(uuid = "2a19", read, write, notify, on_read = battery_level_on_read, on_write = battery_level_on_write)] 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(()) +} + pub async fn run(controller: C) where C: Controller, @@ -46,7 +55,8 @@ where name: "TrouBLE", appearance: &appearance::GENERIC_POWER, }), - ); + ) + .unwrap(); info!("Starting advertising and GATT service"); let _ = join3( @@ -61,16 +71,20 @@ async fn ble_task(mut runner: Runner<'_, C>) -> Result<(), BleHos runner.run().await } -async fn gatt_task(server: &Server<'_,'_, C>) { +async fn gatt_task(server: &Server<'_, '_, C>) { loop { match server.next().await { - Ok(GattEvent::Write { handle, connection: _ }) => { - let _ = server.get(handle, |value| { - info!("[gatt] Write event on {:?}. Value written: {:?}", handle, value); - }); + Ok(GattEvent::Write { + value_handle, + connection: _, + }) => { + info!("[gatt] Write event on {:?}", value_handle); } - Ok(GattEvent::Read { handle, connection: _ }) => { - info!("[gatt] Read event on {:?}", handle); + Ok(GattEvent::Read { + value_handle, + connection: _, + }) => { + info!("[gatt] Read event on {:?}", value_handle); } Err(e) => { error!("[gatt] Error processing GATT events: {:?}", e); @@ -111,7 +125,7 @@ async fn advertise_task( Timer::after(Duration::from_secs(2)).await; tick = tick.wrapping_add(1); info!("[adv] notifying connection of tick {}", tick); - let _ = server.notify(server.battery_service.level, &conn, &[tick]).await; + let _ = server.notify(&server.battery_service.level, &conn, &tick).await; } } } diff --git a/examples/esp32/.cargo/config.toml b/examples/esp32/.cargo/config.toml index bacdf2e1..fe4c6319 100644 --- a/examples/esp32/.cargo/config.toml +++ b/examples/esp32/.cargo/config.toml @@ -3,13 +3,14 @@ runner = "espflash flash --monitor" [env] -ESP_LOG="trouble_host=trace" +ESP_LOGLEVEL = "info" [build] rustflags = [ # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.) # NOTE: May negatively impact performance of produced code - "-C", "force-frame-pointers", + "-C", + "force-frame-pointers", ] target = "riscv32imc-unknown-none-elf" diff --git a/host-macro/src/characteristic.rs b/host-macro/src/characteristic.rs index 0841e705..0b7fd5e7 100644 --- a/host-macro/src/characteristic.rs +++ b/host-macro/src/characteristic.rs @@ -9,8 +9,9 @@ use darling::Error; use darling::FromMeta; use proc_macro2::Span; use syn::parse::Result; -use syn::spanned::Spanned as _; +use syn::spanned::Spanned; use syn::Field; +use syn::Ident; use syn::LitStr; #[derive(Debug)] @@ -47,10 +48,10 @@ pub(crate) struct DescriptorArgs { } /// Characteristic attribute arguments -#[derive(Debug, FromMeta, Default)] +#[derive(Debug, FromMeta)] pub(crate) struct CharacteristicArgs { /// The UUID of the characteristic. - pub uuid: Option, + pub uuid: Uuid, /// If true, the characteristic can be read. #[darling(default)] pub read: bool, @@ -66,20 +67,35 @@ pub(crate) struct CharacteristicArgs { /// 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, /// The initial value of the characteristic. /// This is optional and can be used to set the initial value of the characteristic. #[darling(default)] - pub 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)] - pub _descriptor: Vec, + pub _descriptors: Vec, } impl CharacteristicArgs { /// Parse the arguments of a characteristic attribute pub fn parse(attribute: &syn::Attribute) -> Result { - let mut args = CharacteristicArgs::default(); + let mut uuid: Option = None; + let mut read = false; + let mut write = false; + let mut write_without_response = false; + let mut notify = false; + let mut indicate = false; + let mut on_read = None; + let mut on_write = None; + let mut _default_value: Option = None; + let descriptors: Vec = Vec::new(); attribute.parse_nested_meta(|meta| { match meta.path.get_ident().ok_or(Error::custom("no ident"))?.to_string().as_str() { "uuid" => { @@ -87,18 +103,21 @@ impl CharacteristicArgs { .value() .map_err(|_| Error::custom("uuid must be followed by '= [data]'. i.e. uuid = '0x2A37'".to_string()))?; let uuid_string: LitStr = value.parse()?; - args.uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); + uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); }, - "read" => args.read = true, - "write" => args.write = true, - "write_without_response" => args.write_without_response = true, - "notify" => args.notify = true, - "indicate" => args.indicate = true, + "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()?), "value" => { - let value = meta - .value() - .map_err(|_| Error::custom("value must be followed by '= [data]'. i.e. value = 'hello'".to_string()))?; - args.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( @@ -108,9 +127,17 @@ impl CharacteristicArgs { }; Ok(()) })?; - if args.uuid.is_none() { - return Err(Error::custom("Characteristic must have a UUID").into()); - } - Ok(args) + Ok(Self { + uuid: uuid.ok_or(Error::custom("Characteristic must have a UUID"))?, + read, + write, + write_without_response, + notify, + indicate, + on_read, + on_write, + _default_value, + _descriptors: descriptors, + }) } } diff --git a/host-macro/src/lib.rs b/host-macro/src/lib.rs index 5bba3cfd..a93c5fe6 100644 --- a/host-macro/src/lib.rs +++ b/host-macro/src/lib.rs @@ -16,6 +16,7 @@ use ctxt::Ctxt; use proc_macro::TokenStream; use server::{ServerArgs, ServerBuilder}; use service::{ServiceArgs, ServiceBuilder}; +use syn::parse_macro_input; /// Gatt Service attribute macro. /// @@ -57,37 +58,50 @@ pub fn gatt_server(args: TokenStream, item: TokenStream) -> TokenStream { /// # Example /// /// ```rust +/// use trouble_host::prelude::*; /// use trouble_host_macro::gatt_service; /// -/// #[gatt_service(uuid = "7e701cf1-b1df-42a1-bb5f-6a1028c793b0")] +/// #[gatt_service(uuid = "7e701cf1-b1df-42a1-bb5f-6a1028c793b0", on_read = service_on_read)] /// struct HeartRateService { -/// #[characteristic(uuid = "0x2A37", read, notify, value = 3.14)] +/// #[characteristic(uuid = "0x2A37", read, notify, value = 3.14, on_read = rate_on_read)] /// rate: f32, /// #[characteristic(uuid = "0x2A38", read)] /// location: f32, -/// #[characteristic(uuid = "0x2A39", write)] +/// #[characteristic(uuid = "0x2A39", write, on_write = control_on_write)] /// control: u8, /// #[characteristic(uuid = "0x2A63", read, notify)] /// energy_expended: u16, /// } +/// +/// fn service_on_read(connection: &Connection) { +/// info!("Read callback triggered for {:?}", connection); +/// } +/// +/// fn rate_on_read(connection: &Connection) { +/// info!("Heart rate read on {:?}", connection); +/// } +/// +/// fn control_on_write(connection: &Connection, data: &[u8] -> Result<(), ()> { +/// info!("Write event on control attribute from {:?} with data {:?}", connectioni, data); +/// let control = u8::from_gatt(data).unwrap(); +/// match control { +/// 0 => info!("Control setting 0 selected"), +/// 1 => info!("Control setting 1 selected"), +/// _ => { +/// warn!("Unsupported control setting! Rejecting write request."); +/// return Err(()) +/// } +/// } +/// Ok(()) +/// }) /// ``` #[proc_macro_attribute] pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { - let service_uuid = { - // Get arguments from the gatt_service macro attribute (i.e. uuid) - let service_attributes: ServiceArgs = { - let mut attributes = ServiceArgs::default(); - let arg_parser = syn::meta::parser(|meta| attributes.parse(meta)); - - syn::parse_macro_input!(args with arg_parser); - attributes - }; - service_attributes.uuid - } - .expect("uuid is required for gatt_service"); + // Get arguments from the gatt_service macro attribute + let service_arguments = parse_macro_input!(args as ServiceArgs); // Parse the contents of the struct - let mut service_props = syn::parse_macro_input!(item as syn::ItemStruct); + let mut service_props = parse_macro_input!(item as syn::ItemStruct); let ctxt = Ctxt::new(); // error handling context, must be initialized after parse_macro_input calls. @@ -119,7 +133,7 @@ pub fn gatt_service(args: TokenStream, item: TokenStream) -> TokenStream { } // Build the service struct - let result = ServiceBuilder::new(service_props, service_uuid) + let result = ServiceBuilder::new(service_props, service_arguments) .process_characteristics_and_fields(fields, characteristics) .build(); diff --git a/host-macro/src/server.rs b/host-macro/src/server.rs index 9a84ce84..5a610f8e 100644 --- a/host-macro/src/server.rs +++ b/host-macro/src/server.rs @@ -119,40 +119,44 @@ impl ServerBuilder { /// Create a new Gatt Server instance. /// /// This function will add a Generic GAP Service with the given name. - #visibility fn new_default(stack: Stack<'reference, C>, name: &'values str) -> Self { + /// 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(); - GapConfig::default(name).build(&mut table); + GapConfig::default(name).build(&mut table)?; #code_service_init - Self { + Ok(Self { server: GattServer::new(stack, table), #code_server_populate - } + }) } /// Create a new Gatt Server instance. /// /// This function will add a GAP Service. - #visibility fn new_with_config(stack: Stack<'reference, C>, gap: GapConfig<'values>) -> Self { + /// 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(); - gap.build(&mut table); + gap.build(&mut table)?; #code_service_init - Self { + Ok(Self { server: GattServer::new(stack, table), #code_server_populate - } + }) } - #visibility fn get T, T>(&self, handle: Characteristic, f: F) -> Result { - self.server.server().table().get(handle, f) + #visibility fn get(&self, handle: &Characteristic) -> Result { + self.server.server().table().get(handle) } - #visibility fn set(&self, handle: Characteristic, input: &[u8]) -> Result<(), Error> { + #visibility fn set(&self, handle: &Characteristic, input: &T) -> Result<(), Error> { self.server.server().table().set(handle, input) } } diff --git a/host-macro/src/service.rs b/host-macro/src/service.rs index 7b7be8cd..7b27805b 100644 --- a/host-macro/src/service.rs +++ b/host-macro/src/service.rs @@ -6,34 +6,64 @@ use crate::characteristic::{Characteristic, CharacteristicArgs}; use crate::uuid::Uuid; -use darling::FromMeta; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use darling::{Error, FromMeta}; +use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote, quote_spanned}; -use syn::meta::ParseNestedMeta; use syn::parse::Result; use syn::spanned::Spanned; -use syn::LitStr; +use syn::{Meta, Token}; -#[derive(Debug, Default)] +#[derive(Debug)] pub(crate) struct ServiceArgs { - pub uuid: Option, + pub uuid: Uuid, + pub on_read: Option, } -impl ServiceArgs { - pub fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { - if meta.path.is_ident("uuid") { - let uuid_string: LitStr = meta.value()?.parse()?; - self.uuid = Some(Uuid::from_string(uuid_string.value().as_str())?); - Ok(()) - } else { - Err(meta.error("Unsupported service property, 'uuid' is the only supported property")) +impl syn::parse::Parse for ServiceArgs { + fn parse(input: syn::parse::ParseStream) -> Result { + let mut uuid = None; + let mut on_read = None; + + while !input.is_empty() { + let meta = input.parse()?; + + match &meta { + Meta::NameValue(name_value) => { + match name_value + .path + .get_ident() + .ok_or(Error::custom("Argument name is missing").with_span(&name_value.span()))? + .to_string() + .as_str() + { + "uuid" => uuid = Some(Uuid::from_meta(&meta)?), + "on_read" => on_read = Some(syn::Ident::from_meta(&meta)?), + other => { + return Err(Error::unknown_field(&format!( + "Unsupported service property: '{other}'.\nSupported properties are uuid, on_read" + )) + .with_span(&name_value.span()) + .into()) + } + } + } + _ => return Err(Error::custom("Unexpected argument").with_span(&meta.span()).into()), + } + let _ = input.parse::(); } + + Ok(Self { + uuid: uuid.ok_or(Error::custom( + "Service must have a UUID (i.e. `#[gatt_service(uuid = '1234')]`)", + ))?, + on_read, + }) } } pub(crate) struct ServiceBuilder { properties: syn::ItemStruct, - uuid: Uuid, + args: ServiceArgs, code_impl: TokenStream2, code_build_chars: TokenStream2, code_struct_init: TokenStream2, @@ -41,10 +71,10 @@ pub(crate) struct ServiceBuilder { } impl ServiceBuilder { - pub fn new(properties: syn::ItemStruct, uuid: Uuid) -> Self { + pub fn new(properties: syn::ItemStruct, args: ServiceArgs) -> Self { Self { - uuid, properties, + args, code_struct_init: TokenStream2::new(), code_impl: TokenStream2::new(), code_fields: TokenStream2::new(), @@ -60,7 +90,11 @@ impl ServiceBuilder { let code_impl = self.code_impl; let fields = self.code_fields; let code_build_chars = self.code_build_chars; - let uuid = self.uuid; + let uuid = self.args.uuid; + let read_callback = self + .args + .on_read + .map(|callback| quote!(service.set_read_callback(#callback);)); quote! { #visibility struct #struct_name { @@ -75,6 +109,7 @@ impl ServiceBuilder { M: embassy_sync::blocking_mutex::raw::RawMutex, { let mut service = table.add_service(Service::new(#uuid)); + #read_callback #code_build_chars Self { @@ -88,24 +123,33 @@ impl ServiceBuilder { } /// Construct instructions for adding a characteristic to the service, with static storage. - fn construct_characteristic_static( - &mut self, - name: &str, - span: Span, - ty: &syn::Type, - properties: &Vec, - uuid: Option, - ) { + fn construct_characteristic_static(&mut self, characteristic: Characteristic) { let name_screaming = format_ident!( "{}", - inflector::cases::screamingsnakecase::to_screaming_snake_case(name) + inflector::cases::screamingsnakecase::to_screaming_snake_case(characteristic.name.as_str()) ); - let char_name = format_ident!("{}", name); - self.code_build_chars.extend(quote_spanned! {span=> + 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);)); + + 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 builder = service.add_characteristic(#uuid, &[#(#properties),*], store); + let mut builder = service.add_characteristic(#uuid, &[#(#properties),*], store); + #read_callback + #write_callback // TODO: Descriptors @@ -113,7 +157,7 @@ impl ServiceBuilder { }; }); - self.code_struct_init.extend(quote_spanned!(span=> + self.code_struct_init.extend(quote_spanned!(characteristic.span=> #char_name, )); } @@ -137,33 +181,19 @@ impl ServiceBuilder { // Process characteristic fields for ch in characteristics { let char_name = format_ident!("{}", ch.name); - let uuid = ch.args.uuid; - - // TODO add methods to characteristic - let _get_fn = format_ident!("{}_get", ch.name); - let _set_fn = format_ident!("{}_set", ch.name); - let _notify_fn = format_ident!("{}_notify", ch.name); - let _indicate_fn = format_ident!("{}_indicate", ch.name); - let _fn_vis = &ch.vis; - - let _notify = ch.args.notify; - let _indicate = ch.args.indicate; - let ty = &ch.ty; - let properties = set_access_properties(&ch.args); - // add fields for each characteristic value handle fields.push(syn::Field { ident: Some(char_name.clone()), - ty: syn::Type::Verbatim(quote!(Characteristic)), + ty: syn::Type::Verbatim(quote!(Characteristic<#ty>)), attrs: Vec::new(), colon_token: Default::default(), vis: ch.vis.clone(), mutability: syn::FieldMutability::None, }); - self.construct_characteristic_static(&ch.name, ch.span, ty, &properties, uuid); + self.construct_characteristic_static(ch); } // Processing common to all fields diff --git a/host/Cargo.toml b/host/Cargo.toml index 76b056f5..b0d81a0c 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -18,10 +18,13 @@ embassy-futures = "0.1" futures = { version = "0.3", default-features = false } heapless = "0.8" trouble-host-macros = { path = "../host-macro", optional = true } +static_cell = "2.1.0" # Logging log = { version = "0.4.16", optional = true } defmt = { version = "0.3", optional = true } +num_enum = { version = "0.7.3", default-features = false } +thiserror = { version = "2.0.0", default-features = false } [dev-dependencies] tokio = { version = "1", default-features = false, features = [ @@ -35,7 +38,6 @@ embedded-io = { version = "0.6.1" } tokio-serial = "5.4" env_logger = "0.11" critical-section = { version = "1", features = ["std"] } -static_cell = "2.1.0" rand = "0.8.5" diff --git a/host/src/att.rs b/host/src/att.rs index 4af24b8e..740dd3d1 100644 --- a/host/src/att.rs +++ b/host/src/att.rs @@ -1,3 +1,6 @@ +use num_enum::TryFromPrimitive; +use thiserror::Error; + use crate::codec; use crate::cursor::{ReadCursor, WriteCursor}; use crate::types::uuid::*; @@ -28,70 +31,71 @@ pub(crate) const ATT_READ_BLOB_REQ: u8 = 0x0c; pub(crate) const ATT_READ_BLOB_RSP: u8 = 0x0d; pub(crate) const ATT_HANDLE_VALUE_NTF: u8 = 0x1b; +/// Attribute Error Code +/// +/// This enum type describes the `ATT_ERROR_RSP` PDU from the Bluetooth Core Specification +/// Version 6.0 | Vol 3, Part F (page 1491) #[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, TryFromPrimitive, Error)] #[repr(u8)] pub enum AttErrorCode { - /// Attempted to use an `Handle` that isn't valid on this server. + /// The attribute handle given was not valid on this server. + #[error("Attempted to use an `Handle` that isn't valid on this server.")] InvalidHandle = 0x01, - /// Attribute isn't readable. + /// The attribute cannot be read. + #[error("Attribute isn't readable.")] ReadNotPermitted = 0x02, - /// Attribute isn't writable. + /// The attribute cannot be written. + #[error("Attribute isn't writable.")] WriteNotPermitted = 0x03, - /// Attribute PDU is invalid. + /// The attribute PDU was invalid. + #[error("Attribute PDU is invalid.")] InvalidPdu = 0x04, - /// Authentication needed before attribute can be read/written. + /// The attribute requires authentication before it can be read or written. + #[error("Authentication needed before attribute can be read/written.")] InsufficientAuthentication = 0x05, - /// Server doesn't support this operation. + /// ATT Server does not support the request reveived from the client. + #[error("Server doesn't support this operation.")] RequestNotSupported = 0x06, - /// Offset was past the end of the attribute. + /// Offset specified was past the end of the attribute. + #[error("Offset was past the end of the attribute.")] InvalidOffset = 0x07, - /// Authorization needed before attribute can be read/written. + /// The attribute requires authorisation before it can be read or written. + #[error("Authorization needed before attribute can be read/written.")] InsufficientAuthorization = 0x08, - /// Too many "prepare write" requests have been queued. + /// Too many prepare writes have been queued. + #[error("Too many 'prepare write' requests have been queued.")] PrepareQueueFull = 0x09, - /// No attribute found within the specified attribute handle range. + /// No attribute found within the given attribute handle range. + #[error("No attribute found within the specified attribute handle range.")] AttributeNotFound = 0x0A, - /// Attribute can't be read/written using *Read Key Blob* request. + /// The attribute cannot be read using the ATT_READ_BLOB_REQ PDU. + #[error("Attribute can't be read using *Read Key Blob* request.")] AttributeNotLong = 0x0B, - /// The encryption key in use is too weak to access an attribute. + /// The Encryption Key Size used for encrypting this link is too short. + #[error("The encryption key in use is too weak to access an attribute.")] InsufficientEncryptionKeySize = 0x0C, - /// Attribute value has an incorrect length for the operation. + /// The attribute value length is invalid for the operation. + #[error("Attribute value has an incorrect length for the operation.")] InvalidAttributeValueLength = 0x0D, - /// Request has encountered an "unlikely" error and could not be completed. + /// The attribute request that was requested had encountered an error that was unlikely, and therefore could not be completed as requested. + #[error("Request has encountered an 'unlikely' error and could not be completed.")] UnlikelyError = 0x0E, - /// Attribute cannot be read/written without an encrypted connection. + /// The attribute requires encryption before it can be read or written. + #[error("Attribute cannot be read/written without an encrypted connection.")] InsufficientEncryption = 0x0F, - /// Attribute type is an invalid grouping attribute according to a higher-layer spec. + /// The attribute type is not a supported grouping attribute as defined by a higher layer specification. + #[error("Attribute type is an invalid grouping attribute according to a higher-layer spec.")] UnsupportedGroupType = 0x10, - /// Server didn't have enough resources to complete a request. + /// Insufficient Resources to complete the request. + #[error("Server didn't have enough resources to complete a request.")] InsufficientResources = 0x11, -} - -impl TryFrom for AttErrorCode { - type Error = (); - fn try_from(code: u8) -> Result { - match code { - 0x01 => Ok(Self::InvalidHandle), - 0x02 => Ok(Self::ReadNotPermitted), - 0x03 => Ok(Self::WriteNotPermitted), - 0x04 => Ok(Self::InvalidPdu), - 0x05 => Ok(Self::InsufficientAuthentication), - 0x06 => Ok(Self::RequestNotSupported), - 0x07 => Ok(Self::InvalidOffset), - 0x08 => Ok(Self::InsufficientAuthorization), - 0x09 => Ok(Self::PrepareQueueFull), - 0x0A => Ok(Self::AttributeNotFound), - 0x0B => Ok(Self::AttributeNotLong), - 0x0C => Ok(Self::InsufficientEncryptionKeySize), - 0x0D => Ok(Self::InvalidAttributeValueLength), - 0x0E => Ok(Self::UnlikelyError), - 0x0F => Ok(Self::InsufficientEncryption), - 0x10 => Ok(Self::UnsupportedGroupType), - 0x11 => Ok(Self::InsufficientResources), - _ => Err(()), - } - } + /// The server requests the client to rediscover the database. + #[error("The server requests the client to rediscover the database.")] + DatabaseOutOfSync = 0x12, + /// The attribute parameter value was not allowed. + #[error("The attribute parameter value was not allowed.")] + ValueNotAllowed = 0x13, } #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/host/src/attribute.rs b/host/src/attribute.rs index c8578b0c..f0f7e207 100644 --- a/host/src/attribute.rs +++ b/host/src/attribute.rs @@ -1,12 +1,14 @@ //! Attribute protocol implementation. use core::cell::RefCell; use core::fmt; - +use core::marker::PhantomData; use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::blocking_mutex::Mutex; use crate::att::AttErrorCode; use crate::cursor::{ReadCursor, WriteCursor}; +use crate::prelude::Connection; +use crate::types::gatt_traits::GattValue; pub use crate::types::uuid::Uuid; use crate::Error; @@ -62,16 +64,47 @@ pub enum CharacteristicProp { Extended = 0x80, } +type WriteCallback = fn(&Connection, &[u8]) -> Result<(), ()>; + /// Attribute metadata. pub struct Attribute<'a> { pub(crate) uuid: Uuid, pub(crate) handle: u16, pub(crate) last_handle_in_group: u16, pub(crate) data: AttributeData<'a>, + pub(crate) on_read: Option, + pub(crate) on_write: Option, } impl<'a> Attribute<'a> { const EMPTY: Option> = None; + + pub(crate) fn read(&self, connection: &Connection, offset: usize, data: &mut [u8]) -> Result { + if !self.data.readable() { + return Err(AttErrorCode::ReadNotPermitted); + } + if let Some(callback) = self.on_read { + callback(connection); + } + self.data.read(offset, data) + } + + pub(crate) fn write(&mut self, connection: &Connection, offset: usize, data: &[u8]) -> Result<(), AttErrorCode> { + if !self.data.writable() { + return Err(AttErrorCode::WriteNotPermitted); + } + + let mut callback_result = Ok(()); + if let Some(callback) = self.on_write { + callback_result = callback(connection, data); + } + + if callback_result.is_ok() { + self.data.write(offset, data) + } else { + Err(AttErrorCode::ValueNotAllowed) + } + } } pub(crate) enum AttributeData<'d> { @@ -122,7 +155,7 @@ impl<'d> AttributeData<'d> { } } - pub(crate) fn read(&self, offset: usize, data: &mut [u8]) -> Result { + fn read(&self, offset: usize, data: &mut [u8]) -> Result { if !self.readable() { return Err(AttErrorCode::ReadNotPermitted); } @@ -204,7 +237,7 @@ impl<'d> AttributeData<'d> { } } - pub(crate) fn write(&mut self, offset: usize, data: &[u8]) -> Result<(), AttErrorCode> { + fn write(&mut self, offset: usize, data: &[u8]) -> Result<(), AttErrorCode> { let writable = self.writable(); match self { @@ -270,12 +303,19 @@ impl<'a> defmt::Format for Attribute<'a> { } impl<'a> Attribute<'a> { - pub(crate) fn new(uuid: Uuid, data: AttributeData<'a>) -> Attribute<'a> { + pub(crate) fn new( + uuid: Uuid, + data: AttributeData<'a>, + on_read: Option, + on_write: Option, + ) -> Attribute<'a> { Attribute { uuid, handle: 0, data, last_handle_in_group: 0xffff, + on_read, + on_write, } } } @@ -359,6 +399,8 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeTable<'d, M, MAX> { handle: 0, last_handle_in_group: 0, data: AttributeData::Service { uuid: service.uuid }, + on_read: None, + on_write: None, }); ServiceBuilder { handle: AttributeHandle { handle }, @@ -367,20 +409,47 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeTable<'d, M, MAX> { } } + fn set_read_callback(&mut self, handle: u16, on_read: fn(&Connection)) { + self.iterate(|mut it| { + while let Some(att) = it.next() { + if att.handle == handle { + att.on_read = Some(on_read); + break; + } + } + }) + } + + fn set_write_callback(&mut self, handle: u16, on_write: fn(&Connection, &[u8]) -> Result<(), ()>) { + self.iterate(|mut it| { + while let Some(att) = it.next() { + if att.handle == handle { + att.on_write = Some(on_write); + break; + } + } + }) + } + /// Set the value of a characteristic /// /// The provided data must exactly match the size of the storage for the characteristic, /// otherwise this function will panic. /// - /// If the characteristic for the handle cannot be found, an error is returned. - pub fn set(&self, handle: Characteristic, input: &[u8]) -> Result<(), Error> { + /// If the characteristic for the handle cannot be found, or the shape of the data does not match the type of the characterstic, + /// an error is returned + pub fn set(&self, handle: &Characteristic, input: &T) -> Result<(), Error> { + let gatt_value = input.to_gatt(); self.iterate(|mut it| { while let Some(att) = it.next() { if att.handle == handle.handle { if let AttributeData::Data { props, value } = &mut att.data { - assert_eq!(value.len(), input.len()); - value.copy_from_slice(input); - return Ok(()); + if value.len() == gatt_value.len() { + value.copy_from_slice(gatt_value); + return Ok(()); + } else { + return Err(Error::InvalidValue); + } } } } @@ -393,12 +462,12 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeTable<'d, M, MAX> { /// The return value of the closure is returned in this function and is assumed to be infallible. /// /// If the characteristic for the handle cannot be found, an error is returned. - pub fn get T, T>(&self, handle: Characteristic, mut f: F) -> Result { + pub fn get(&self, handle: &Characteristic) -> Result { self.iterate(|mut it| { while let Some(att) = it.next() { if att.handle == handle.handle { if let AttributeData::Data { props, value } = &mut att.data { - let v = f(value); + let v = ::from_gatt(value).map_err(|_| Error::InvalidValue)?; return Ok(v); } } @@ -407,7 +476,10 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeTable<'d, M, MAX> { }) } - pub(crate) fn find_characteristic_by_value_handle(&self, handle: u16) -> Result { + /// Return the characteristic which corresponds to the supplied value handle + /// + /// If no characteristic corresponding to the given value handle was found, returns an error + pub fn find_characteristic_by_value_handle(&self, handle: u16) -> Result, Error> { self.iterate(|mut it| { while let Some(att) = it.next() { if att.handle == handle { @@ -421,17 +493,20 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeTable<'d, M, MAX> { return Ok(Characteristic { handle, cccd_handle: Some(next.handle), + phantom: PhantomData, }); } else { return Ok(Characteristic { handle, cccd_handle: None, + phantom: PhantomData, }); } } else { return Ok(Characteristic { handle, cccd_handle: None, + phantom: PhantomData, }); } } @@ -462,12 +537,12 @@ pub struct ServiceBuilder<'r, 'd, M: RawMutex, const MAX: usize> { } impl<'r, 'd, M: RawMutex, const MAX: usize> ServiceBuilder<'r, 'd, M, MAX> { - fn add_characteristic_internal( + fn add_characteristic_internal( &mut self, uuid: Uuid, props: CharacteristicProps, data: AttributeData<'d>, - ) -> CharacteristicBuilder<'_, 'd, M, MAX> { + ) -> CharacteristicBuilder<'_, 'd, T, M, MAX> { // First the characteristic declaration let next = self.table.handle + 1; let cccd = self.table.handle + 2; @@ -480,6 +555,8 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> ServiceBuilder<'r, 'd, M, MAX> { handle: next, uuid: uuid.clone(), }, + on_read: None, + on_write: None, }); // Then the value declaration @@ -488,6 +565,8 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> ServiceBuilder<'r, 'd, M, MAX> { handle: 0, last_handle_in_group: 0, data, + on_read: None, + on_write: None, }); // Add optional CCCD handle @@ -500,6 +579,8 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> ServiceBuilder<'r, 'd, M, MAX> { notifications: false, indications: false, }, + on_read: None, + on_write: None, }); Some(cccd) } else { @@ -510,30 +591,43 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> ServiceBuilder<'r, 'd, M, MAX> { handle: Characteristic { handle: next, cccd_handle, + phantom: PhantomData, }, table: self.table, } } /// Add a characteristic to this service with a refererence to a mutable storage buffer. - pub fn add_characteristic>( + pub fn add_characteristic>( &mut self, uuid: U, props: &[CharacteristicProp], storage: &'d mut [u8], - ) -> CharacteristicBuilder<'_, 'd, M, MAX> { + ) -> CharacteristicBuilder<'_, 'd, T, M, MAX> { let props = props.into(); self.add_characteristic_internal(uuid.into(), props, AttributeData::Data { props, value: storage }) } /// Add a characteristic to this service with a refererence to an immutable storage buffer. - pub fn add_characteristic_ro>( + pub fn add_characteristic_ro>( &mut self, uuid: U, - value: &'d [u8], - ) -> CharacteristicBuilder<'_, 'd, M, MAX> { + value: &'d T, + ) -> CharacteristicBuilder<'_, 'd, T, M, MAX> { let props = [CharacteristicProp::Read].into(); - self.add_characteristic_internal(uuid.into(), props, AttributeData::ReadOnlyData { props, value }) + self.add_characteristic_internal( + uuid.into(), + props, + AttributeData::ReadOnlyData { + props, + value: value.to_gatt(), + }, + ) + } + + /// Add a callback to be triggered when the attribute is read + pub fn set_read_callback(&mut self, on_read: fn(&Connection)) { + self.table.set_read_callback(self.handle.handle, on_read); } /// Finish construction of the service and return a handle. @@ -559,23 +653,26 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> Drop for ServiceBuilder<'r, 'd, M, M /// A characteristic in the attribute table. #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[derive(Clone, Copy, Debug, PartialEq)] -pub struct Characteristic { +pub struct Characteristic { pub(crate) cccd_handle: Option, pub(crate) handle: u16, + pub(crate) phantom: PhantomData, } /// Builder for characteristics. -pub struct CharacteristicBuilder<'r, 'd, M: RawMutex, const MAX: usize> { - handle: Characteristic, +pub struct CharacteristicBuilder<'r, 'd, T: GattValue, M: RawMutex, const MAX: usize> { + handle: Characteristic, table: &'r mut AttributeTable<'d, M, MAX>, } -impl<'r, 'd, M: RawMutex, const MAX: usize> CharacteristicBuilder<'r, 'd, M, MAX> { +impl<'r, 'd, T: GattValue, M: RawMutex, const MAX: usize> CharacteristicBuilder<'r, 'd, T, M, MAX> { fn add_descriptor_internal( &mut self, uuid: Uuid, props: CharacteristicProps, data: AttributeData<'d>, + on_read: Option, + on_write: Option, ) -> DescriptorHandle { let handle = self.table.handle; self.table.push(Attribute { @@ -583,6 +680,8 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> CharacteristicBuilder<'r, 'd, M, MAX handle: 0, last_handle_in_group: 0, data, + on_read, + on_write, }); DescriptorHandle { handle } @@ -594,19 +693,48 @@ impl<'r, 'd, M: RawMutex, const MAX: usize> CharacteristicBuilder<'r, 'd, M, MAX uuid: U, props: &[CharacteristicProp], data: &'d mut [u8], + on_read: Option, + on_write: Option, ) -> DescriptorHandle { let props = props.into(); - self.add_descriptor_internal(uuid.into(), props, AttributeData::Data { props, value: data }) + self.add_descriptor_internal( + uuid.into(), + props, + AttributeData::Data { props, value: data }, + on_read, + on_write, + ) } /// Add a read only characteristic descriptor for this characteristic. - pub fn add_descriptor_ro>(&mut self, uuid: U, data: &'d [u8]) -> DescriptorHandle { + pub fn add_descriptor_ro>( + &mut self, + uuid: U, + data: &'d [u8], + on_read: Option, + ) -> DescriptorHandle { let props = [CharacteristicProp::Read].into(); - self.add_descriptor_internal(uuid.into(), props, AttributeData::ReadOnlyData { props, value: data }) + self.add_descriptor_internal( + uuid.into(), + props, + AttributeData::ReadOnlyData { props, value: data }, + on_read, + None, + ) + } + + /// Add a callback to be triggered when a read event occurs + pub fn set_read_callback(&mut self, on_read: fn(&Connection)) { + self.table.set_read_callback(self.handle.handle, on_read); + } + + /// Add a callback to be triggered when a write event occurs + pub fn set_write_callback(&mut self, on_write: WriteCallback) { + self.table.set_write_callback(self.handle.handle, on_write) } /// Return the built characteristic. - pub fn build(self) -> Characteristic { + pub fn build(self) -> Characteristic { self.handle } } diff --git a/host/src/attribute_server.rs b/host/src/attribute_server.rs index 6789dc10..dc9f64a9 100644 --- a/host/src/attribute_server.rs +++ b/host/src/attribute_server.rs @@ -8,6 +8,7 @@ use crate::att::{self, AttErrorCode, AttReq}; use crate::attribute::{AttributeData, AttributeTable}; use crate::codec; use crate::cursor::WriteCursor; +use crate::prelude::Connection; use crate::types::uuid::Uuid; #[derive(Debug, PartialEq)] @@ -74,6 +75,7 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { fn handle_read_by_type_req( &self, + connection: &Connection, buf: &mut [u8], start: u16, end: u16, @@ -91,11 +93,9 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { body.write(att.handle)?; handle = att.handle; - if att.data.readable() { - err = att.data.read(0, body.write_buf()); - if let Ok(len) = &err { - body.commit(*len)?; - } + err = att.read(connection, 0, body.write_buf()); + if let Ok(len) = err { + body.commit(len)?; } // debug!("found! {:?} {}", att.uuid, att.handle); @@ -117,6 +117,7 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { fn handle_read_by_group_type_req( &self, + connection: &Connection, buf: &mut [u8], start: u16, end: u16, @@ -138,11 +139,9 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { body.write(att.handle)?; body.write(att.last_handle_in_group)?; - if att.data.readable() { - err = att.data.read(0, body.write_buf()); - if let Ok(len) = &err { - body.commit(*len)?; - } + err = att.read(connection, 0, body.write_buf()); + if let Ok(len) = err { + body.commit(len)?; } break; } @@ -160,7 +159,7 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { } } - fn handle_read_req(&self, buf: &mut [u8], handle: u16) -> Result { + fn handle_read_req(&self, connection: &Connection, buf: &mut [u8], handle: u16) -> Result { let mut data = WriteCursor::new(buf); data.write(att::ATT_READ_RSP)?; @@ -169,11 +168,9 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { let mut err = Err(AttErrorCode::AttributeNotFound); while let Some(att) = it.next() { if att.handle == handle { - if att.data.readable() { - err = att.data.read(0, data.write_buf()); - if let Ok(len) = err { - data.commit(len)?; - } + err = att.read(connection, 0, data.write_buf()); + if let Ok(len) = err { + data.commit(len)?; } break; } @@ -187,25 +184,29 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { } } - fn handle_write_cmd(&self, buf: &mut [u8], handle: u16, data: &[u8]) -> Result { + fn handle_write_cmd( + &self, + connection: &Connection, + buf: &mut [u8], + handle: u16, + data: &[u8], + ) -> Result { // TODO: Generate event self.table.iterate(|mut it| { while let Some(att) = it.next() { if att.handle == handle { - if att.data.writable() { - // Write commands can't respond with an error. - att.data.write(0, data).unwrap(); - } + // Write commands can't respond with an error. + let _ = att.write(connection, 0, data); break; } } - Ok(0) - }) + }); + Ok(0) } fn handle_write_req( &self, - conn: ConnHandle, + connection: &Connection, buf: &mut [u8], handle: u16, data: &[u8], @@ -214,16 +215,14 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { let mut err = Err(AttErrorCode::AttributeNotFound); while let Some(att) = it.next() { if att.handle == handle { - if att.data.writable() { - err = att.data.write(0, data); - if err.is_ok() { - if let AttributeData::Cccd { - notifications, - indications, - } = att.data - { - self.set_notify(conn, handle, notifications); - } + err = att.write(connection, 0, data); + if err.is_ok() { + if let AttributeData::Cccd { + notifications, + indications, + } = att.data + { + self.set_notify(connection.handle(), handle, notifications); } } break; @@ -335,6 +334,7 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { fn handle_prepare_write( &self, + connection: &Connection, buf: &mut [u8], handle: u16, offset: u16, @@ -349,9 +349,7 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { let mut err = Err(AttErrorCode::AttributeNotFound); while let Some(att) = it.next() { if att.handle == handle { - if att.data.writable() { - err = att.data.write(offset as usize, value); - } + err = att.write(connection, offset as usize, value); w.append(value)?; break; } @@ -371,7 +369,13 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { Ok(w.len()) } - fn handle_read_blob(&self, buf: &mut [u8], handle: u16, offset: u16) -> Result { + fn handle_read_blob( + &self, + connection: &Connection, + buf: &mut [u8], + handle: u16, + offset: u16, + ) -> Result { let mut w = WriteCursor::new(buf); w.write(att::ATT_READ_BLOB_RSP)?; @@ -379,11 +383,9 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { let mut err = Err(AttErrorCode::AttributeNotFound); while let Some(att) = it.next() { if att.handle == handle { - if att.data.readable() { - err = att.data.read(offset as usize, w.write_buf()); - if let Ok(n) = &err { - w.commit(*n)?; - } + err = att.read(connection, offset as usize, w.write_buf()); + if let Ok(n) = err { + w.commit(n)?; } break; } @@ -397,7 +399,12 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { } } - fn handle_read_multiple(&self, buf: &mut [u8], handles: &[u8]) -> Result { + fn handle_read_multiple( + &self, + connection: &Connection, + buf: &mut [u8], + handles: &[u8], + ) -> Result { let w = WriteCursor::new(buf); Self::error_response( w, @@ -408,30 +415,35 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { } /// Process an event and produce a response if necessary - pub fn process(&self, conn: ConnHandle, packet: &AttReq, rx: &mut [u8]) -> Result, codec::Error> { + pub fn process( + &self, + connection: &Connection, + packet: &AttReq, + rx: &mut [u8], + ) -> Result, codec::Error> { let len = match packet { AttReq::ReadByType { start, end, attribute_type, - } => self.handle_read_by_type_req(rx, *start, *end, attribute_type)?, + } => self.handle_read_by_type_req(connection, rx, *start, *end, attribute_type)?, AttReq::ReadByGroupType { start, end, group_type } => { - self.handle_read_by_group_type_req(rx, *start, *end, group_type)? + self.handle_read_by_group_type_req(connection, rx, *start, *end, group_type)? } AttReq::FindInformation { start_handle, end_handle, } => self.handle_find_information(rx, *start_handle, *end_handle)?, - AttReq::Read { handle } => self.handle_read_req(rx, *handle)?, + AttReq::Read { handle } => self.handle_read_req(connection, rx, *handle)?, AttReq::WriteCmd { handle, data } => { - self.handle_write_cmd(rx, *handle, data)?; + self.handle_write_cmd(connection, rx, *handle, data)?; 0 } - AttReq::Write { handle, data } => self.handle_write_req(conn, rx, *handle, data)?, + AttReq::Write { handle, data } => self.handle_write_req(connection, rx, *handle, data)?, AttReq::ExchangeMtu { mtu } => 0, // Done outside, @@ -442,13 +454,15 @@ impl<'d, M: RawMutex, const MAX: usize> AttributeServer<'d, M, MAX> { att_value, } => self.handle_find_type_value(rx, *start_handle, *end_handle, *att_type, att_value)?, - AttReq::PrepareWrite { handle, offset, value } => self.handle_prepare_write(rx, *handle, *offset, value)?, + AttReq::PrepareWrite { handle, offset, value } => { + self.handle_prepare_write(connection, rx, *handle, *offset, value)? + } AttReq::ExecuteWrite { flags } => self.handle_execute_write(rx, *flags)?, - AttReq::ReadBlob { handle, offset } => self.handle_read_blob(rx, *handle, *offset)?, + AttReq::ReadBlob { handle, offset } => self.handle_read_blob(connection, rx, *handle, *offset)?, - AttReq::ReadMultiple { handles } => self.handle_read_multiple(rx, handles)?, + AttReq::ReadMultiple { handles } => self.handle_read_multiple(connection, rx, handles)?, }; if len > 0 { Ok(Some(len)) diff --git a/host/src/gap.rs b/host/src/gap.rs index 4984da09..7a504606 100644 --- a/host/src/gap.rs +++ b/host/src/gap.rs @@ -7,8 +7,22 @@ //! In addition, this profile includes common format requirements for //! parameters accessible on the user interface level. -use crate::prelude::*; use embassy_sync::blocking_mutex::raw::RawMutex; +use heapless::String; +use static_cell::StaticCell; + +use crate::prelude::*; + +// GAP Service UUIDs +const GAP_UUID: u16 = 0x1800; +const GATT_UUID: u16 = 0x1801; + +// GAP Characteristic UUIDs +const DEVICE_NAME_UUID: u16 = 0x2a00; +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; pub mod appearance { //! The representation of the external appearance of the device. @@ -92,31 +106,55 @@ impl<'a> GapConfig<'a> { appearance: &appearance::GENERIC_UNKNOWN, }) } - /// Add the GAP service to the attribute table. - pub fn build(self, table: &mut AttributeTable<'a, M, MAX>) { - // Service UUIDs. These are mandatory services. - const GAP_UUID: u16 = 0x1800; - const GATT_UUID: u16 = 0x1801; - - // Characteristic UUIDs. These are mandatory characteristics. - const DEVICE_NAME_UUID: u16 = 0x2a00; - const APPEARANCE_UUID: u16 = 0x2a01; - let mut gap = table.add_service(Service::new(GAP_UUID)); // GAP UUID (mandatory) + /// Add the GAP config to the attribute table + pub fn build( + self, + table: &mut AttributeTable<'a, M, MAX>, + ) -> Result<(), &'static str> { match self { - GapConfig::Peripheral(config) => { - let id = config.name.as_bytes(); - let _ = gap.add_characteristic_ro(DEVICE_NAME_UUID, id); - let _ = gap.add_characteristic_ro(APPEARANCE_UUID, &config.appearance[..]); - } - GapConfig::Central(config) => { - let id = config.name.as_bytes(); - let _ = gap.add_characteristic_ro(DEVICE_NAME_UUID, id); - let _ = gap.add_characteristic_ro(APPEARANCE_UUID, &config.appearance[..]); - } - }; - gap.build(); - - table.add_service(Service::new(GATT_UUID)); // GATT UUID (mandatory) + GapConfig::Peripheral(config) => config.build(table), + GapConfig::Central(config) => config.build(table), + } + } +} + +impl<'a> PeripheralConfig<'a> { + /// Add the peripheral GAP config to the attribute table + fn build(self, table: &mut AttributeTable<'a, M, MAX>) -> Result<(), &'static str> { + static PERIPHERAL_NAME: StaticCell> = StaticCell::new(); + let peripheral_name = PERIPHERAL_NAME.init(String::new()); + peripheral_name + .push_str(self.name) + .map_err(|_| "Device name is too long. Max length is 22 bytes")?; + + let mut gap_builder = table.add_service(Service::new(GAP_UUID)); + gap_builder.add_characteristic_ro(DEVICE_NAME_UUID, peripheral_name); + gap_builder.add_characteristic_ro(APPEARANCE_UUID, self.appearance); + gap_builder.build(); + + table.add_service(Service::new(GATT_UUID)); + + Ok(()) + } +} + +impl<'a> CentralConfig<'a> { + /// Add the peripheral GAP config to the attribute table + fn build(self, table: &mut AttributeTable<'a, M, MAX>) -> Result<(), &'static str> { + static CENTRAL_NAME: StaticCell> = StaticCell::new(); + let central_name = CENTRAL_NAME.init(String::new()); + central_name + .push_str(self.name) + .map_err(|_| "Device name is too long. Max length is 22 bytes")?; + + let mut gap_builder = table.add_service(Service::new(GAP_UUID)); + gap_builder.add_characteristic_ro(DEVICE_NAME_UUID, central_name); + gap_builder.add_characteristic_ro(APPEARANCE_UUID, self.appearance); + gap_builder.build(); + + table.add_service(Service::new(GATT_UUID)); + + Ok(()) } } diff --git a/host/src/gatt.rs b/host/src/gatt.rs index f6de4481..874914b1 100644 --- a/host/src/gatt.rs +++ b/host/src/gatt.rs @@ -1,6 +1,7 @@ //! GATT server and client implementation. use core::cell::RefCell; use core::future::Future; +use core::marker::PhantomData; use bt_hci::controller::Controller; use bt_hci::param::ConnHandle; @@ -19,6 +20,7 @@ use crate::connection::Connection; use crate::connection_manager::DynamicConnectionManager; use crate::cursor::{ReadCursor, WriteCursor}; use crate::pdu::Pdu; +use crate::types::gatt_traits::GattValue; use crate::types::l2cap::L2capHeader; use crate::{config, BleHostError, Error, Stack}; @@ -62,7 +64,7 @@ impl<'reference, 'values, C: Controller, M: RawMutex, const MAX: usize, const L2 let mut w = WriteCursor::new(&mut tx); let (mut header, mut data) = w.split(4)?; - match self.server.process(handle, &att, data.write_buf()) { + match self.server.process(&connection, &att, data.write_buf()) { Ok(Some(written)) => { let mtu = self.connections.get_att_mtu(handle); data.commit(written)?; @@ -74,17 +76,17 @@ impl<'reference, 'values, C: Controller, M: RawMutex, const MAX: usize, const L2 let event = match att { AttReq::Write { handle, data } => Some(GattEvent::Write { connection, - handle: self.server.table.find_characteristic_by_value_handle(handle)?, + value_handle: handle, }), AttReq::Read { handle } => Some(GattEvent::Read { connection, - handle: self.server.table.find_characteristic_by_value_handle(handle)?, + value_handle: handle, }), AttReq::ReadBlob { handle, offset } => Some(GattEvent::Read { connection, - handle: self.server.table.find_characteristic_by_value_handle(handle)?, + value_handle: handle, }), _ => None, }; @@ -117,11 +119,11 @@ impl<'reference, 'values, C: Controller, M: RawMutex, const MAX: usize, const L2 /// If the provided connection has not subscribed for this characteristic, it will not be notified. /// /// If the characteristic for the handle cannot be found, an error is returned. - pub async fn notify( + pub async fn notify( &self, - handle: Characteristic, + handle: &Characteristic, connection: &Connection<'_>, - value: &[u8], + value: &T, ) -> Result<(), BleHostError> { let conn = connection.handle(); self.server.table.set(handle, value)?; @@ -138,7 +140,7 @@ impl<'reference, 'values, C: Controller, M: RawMutex, const MAX: usize, const L2 let (mut header, mut data) = w.split(4)?; data.write(ATT_HANDLE_VALUE_NTF)?; data.write(handle.handle)?; - data.append(value)?; + data.append(value.to_gatt())?; header.write(data.len() as u16)?; header.write(4_u16)?; @@ -161,14 +163,14 @@ pub enum GattEvent<'reference> { /// Connection that read the characteristic. connection: Connection<'reference>, /// Characteristic handle that was read. - handle: Characteristic, + value_handle: u16, }, /// A characteristic was written. Write { /// Connection that wrote the characteristic. connection: Connection<'reference>, /// Characteristic handle that was written. - handle: Characteristic, + value_handle: u16, }, } @@ -260,14 +262,14 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } } -impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usize> - GattClient<'reference, T, MAX_SERVICES, L2CAP_MTU> +impl<'reference, C: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usize> + GattClient<'reference, C, MAX_SERVICES, L2CAP_MTU> { /// Creates a GATT client capable of processing the GATT protocol using the provided table of attributes. pub async fn new( - stack: Stack<'reference, T>, + stack: Stack<'reference, C>, connection: &Connection<'reference>, - ) -> Result, BleHostError> { + ) -> Result, BleHostError> { let l2cap = L2capHeader { channel: 4, length: 3 }; let mut buf = [0; 7]; let mut w = WriteCursor::new(&mut buf); @@ -296,7 +298,7 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz pub async fn services_by_uuid( &self, uuid: &Uuid, - ) -> Result, BleHostError> { + ) -> Result, BleHostError> { let mut start: u16 = 0x0001; let mut result = Vec::new(); @@ -347,11 +349,11 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } /// Discover characteristics in a given service using a UUID. - pub async fn characteristic_by_uuid( + pub async fn characteristic_by_uuid( &self, service: &ServiceHandle, uuid: &Uuid, - ) -> Result> { + ) -> Result, BleHostError> { let mut start: u16 = service.start; loop { let data = att::AttReq::ReadByType { @@ -382,7 +384,11 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz None }; - return Ok(Characteristic { handle, cccd_handle }); + return Ok(Characteristic { + handle, + cccd_handle, + phantom: PhantomData, + }); } if handle == 0xFFFF { @@ -402,7 +408,7 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } } - async fn get_characteristic_cccd(&self, char_handle: u16) -> Result<(u16, CCCD), BleHostError> { + async fn get_characteristic_cccd(&self, char_handle: u16) -> Result<(u16, CCCD), BleHostError> { let data = att::AttReq::ReadByType { start: char_handle, end: char_handle + 1, @@ -430,11 +436,11 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz /// Read a characteristic described by a handle. /// /// The number of bytes copied into the provided buffer is returned. - pub async fn read_characteristic( + pub async fn read_characteristic( &self, - characteristic: &Characteristic, + characteristic: &Characteristic, dest: &mut [u8], - ) -> Result> { + ) -> Result> { let data = att::AttReq::Read { handle: characteristic.handle, }; @@ -460,7 +466,7 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz service: &ServiceHandle, uuid: &Uuid, dest: &mut [u8], - ) -> Result> { + ) -> Result> { let data = att::AttReq::ReadByType { start: service.start, end: service.end, @@ -485,11 +491,11 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } /// Write to a characteristic described by a handle. - pub async fn write_characteristic( + pub async fn write_characteristic( &self, - handle: &Characteristic, + handle: &Characteristic, buf: &[u8], - ) -> Result<(), BleHostError> { + ) -> Result<(), BleHostError> { let data = att::AttReq::Write { handle: handle.handle, data: buf, @@ -506,11 +512,11 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz /// Subscribe to indication/notification of a given Characteristic /// /// A listener is returned, which has a `next()` method - pub async fn subscribe( + pub async fn subscribe( &self, - characteristic: &Characteristic, + characteristic: &Characteristic, indication: bool, - ) -> Result, BleHostError> { + ) -> Result, BleHostError> { let properties = u16::to_le_bytes(if indication { 0x02 } else { 0x01 }); let data = att::AttReq::Write { @@ -538,7 +544,10 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } /// Unsubscribe from a given Characteristic - pub async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<(), BleHostError> { + pub async fn unsubscribe( + &self, + characteristic: &Characteristic, + ) -> Result<(), BleHostError> { let properties = u16::to_le_bytes(0); let data = att::AttReq::Write { handle: characteristic.cccd_handle.ok_or(Error::NotSupported)?, @@ -556,7 +565,7 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } /// Handle a notification that was received. - async fn handle_notification_packet(&self, data: &[u8]) -> Result<(), BleHostError> { + async fn handle_notification_packet(&self, data: &[u8]) -> Result<(), BleHostError> { let mut r = ReadCursor::new(data); let value_handle: u16 = r.read()?; let value_attr = r.remaining(); @@ -576,7 +585,7 @@ impl<'reference, T: Controller, const MAX_SERVICES: usize, const L2CAP_MTU: usiz } /// Task which handles GATT rx data (needed for notifications to work) - pub async fn task(&self) -> Result<(), BleHostError> { + pub async fn task(&self) -> Result<(), BleHostError> { loop { let (handle, pdu) = self.rx.receive().await; let data = pdu.as_ref(); diff --git a/host/src/lib.rs b/host/src/lib.rs index 2dbbda16..42fa131d 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -60,6 +60,7 @@ pub use peripheral::*; #[allow(missing_docs)] pub mod prelude { + pub use super::att::AttErrorCode; pub use super::{BleHostError, Controller, Error, HostResources, Stack}; #[cfg(feature = "peripheral")] pub use crate::advertise::*; @@ -81,6 +82,8 @@ pub mod prelude { pub use crate::scan::*; pub use crate::Address; #[cfg(feature = "derive")] + pub use heapless::String as HeaplessString; + #[cfg(feature = "derive")] pub use trouble_host_macros::*; } diff --git a/host/src/types/gatt_traits.rs b/host/src/types/gatt_traits.rs new file mode 100644 index 00000000..a4568bb9 --- /dev/null +++ b/host/src/types/gatt_traits.rs @@ -0,0 +1,156 @@ +use core::{mem, slice}; + +use heapless::{String, Vec}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Error type to signify an issue when converting from GATT bytes to a concrete type +pub enum FromGattError { + /// Byte array's length did not match what was expected for the converted type + InvalidLength, + /// Attempt to encode as string failed due to an invalid character representation in the byte array + InvalidCharacter, +} + +/// Trait to allow conversion of a fixed size type to and from a byte slice +pub trait FixedGattValue: Sized { + /// Size of the type in bytes + const SIZE: usize; + + /// Converts from gatt bytes. + /// Must return FromGattError::InvalidLength if data.len != Self::SIZE + fn from_gatt(data: &[u8]) -> Result; + + /// Converts to gatt bytes. + /// Must return a slice of len Self::SIZE + fn to_gatt(&self) -> &[u8]; +} + +/// Trait to allow conversion of a type to and from a byte slice +pub trait GattValue: Sized { + /// The minimum size the type might be + const MIN_SIZE: usize; + /// The maximum size the type might be + const MAX_SIZE: usize; + + /// Converts from gatt bytes. + /// Must return FromGattError::InvalidLength if data.len not in MIN_SIZE..=MAX_SIZE + fn from_gatt(data: &[u8]) -> Result; + + /// Converts to gatt bytes. + /// Must return a slice of len in MIN_SIZE..=MAX_SIZE + fn to_gatt(&self) -> &[u8]; +} + +impl GattValue for T { + const MIN_SIZE: usize = Self::SIZE; + const MAX_SIZE: usize = Self::SIZE; + + fn from_gatt(data: &[u8]) -> Result { + ::from_gatt(data) + } + + fn to_gatt(&self) -> &[u8] { + ::to_gatt(self) + } +} + +trait Primitive: Copy {} +impl Primitive for u8 {} +impl Primitive for u16 {} +impl Primitive for u32 {} +impl Primitive for u64 {} +impl Primitive for i8 {} +impl Primitive for i16 {} +impl Primitive for i32 {} +impl Primitive for i64 {} +impl Primitive for f32 {} +impl Primitive for f64 {} + +impl FixedGattValue for T { + const SIZE: usize = mem::size_of::(); + + fn from_gatt(data: &[u8]) -> Result { + if data.len() != Self::SIZE { + Err(FromGattError::InvalidLength) + } else { + // SAFETY + // - Pointer is considered "valid" as per the rules outlined for validity in std::ptr v1.82.0 + // - Pointer was generated from a slice of bytes matching the size of the type implementing Primitive, and all types implementing Primitive are valid for all possible configurations of bits + // - Primitive trait is constrained to require Copy + unsafe { Ok((data.as_ptr() as *const Self).read_unaligned()) } + } + } + + fn to_gatt(&self) -> &[u8] { + // SAFETY + // - Slice is of type u8 so data is guaranteed valid for reads of any length + // - Data and len are tied to the address and size of the type + unsafe { slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) } + } +} + +impl FixedGattValue for bool { + const SIZE: usize = 1; + + fn from_gatt(data: &[u8]) -> Result { + if data.len() != Self::SIZE { + Err(FromGattError::InvalidLength) + } else { + Ok(data != [0x00]) + } + } + + fn to_gatt(&self) -> &[u8] { + match self { + true => &[0x01], + false => &[0x00], + } + } +} + +impl GattValue for Vec { + const MIN_SIZE: usize = 0; + const MAX_SIZE: usize = N; + + fn from_gatt(data: &[u8]) -> Result { + Self::from_slice(data).map_err(|_| FromGattError::InvalidLength) + } + + fn to_gatt(&self) -> &[u8] { + self + } +} + +impl GattValue for [u8; N] { + const MIN_SIZE: usize = 0; + const MAX_SIZE: usize = N; + + fn from_gatt(data: &[u8]) -> Result { + if data.len() < Self::MAX_SIZE { + let mut actual = [0; N]; + actual[..data.len()].copy_from_slice(data); + Ok(actual) + } else { + data.try_into().map_err(|_| FromGattError::InvalidLength) + } + } + + fn to_gatt(&self) -> &[u8] { + self.as_slice() + } +} + +impl GattValue for String { + const MIN_SIZE: usize = 0; + const MAX_SIZE: usize = N; + + fn from_gatt(data: &[u8]) -> Result { + String::from_utf8(unwrap!(Vec::from_slice(data).map_err(|_| FromGattError::InvalidLength))) + .map_err(|_| FromGattError::InvalidCharacter) + } + + fn to_gatt(&self) -> &[u8] { + self.as_ref() + } +} diff --git a/host/src/types/mod.rs b/host/src/types/mod.rs index 67d471e4..e8b746ae 100644 --- a/host/src/types/mod.rs +++ b/host/src/types/mod.rs @@ -1,4 +1,7 @@ //! Common types. + +/// Traits for conversion between types and their GATT representations +pub mod gatt_traits; pub(crate) mod l2cap; pub(crate) mod primitives; diff --git a/host/tests/gatt.rs b/host/tests/gatt.rs index dab8d5ef..433a4f1a 100644 --- a/host/tests/gatt.rs +++ b/host/tests/gatt.rs @@ -43,7 +43,7 @@ async fn gatt_client_server() { let mut expected = value[0].wrapping_add(1); let mut svc = table.add_service(Service::new(0x1800)); let _ = svc.add_characteristic_ro(0x2a00, id); - let _ = svc.add_characteristic_ro(0x2a01, &appearance[..]); + let _ = svc.add_characteristic_ro(0x2a01, &appearance); svc.build(); // Generic attribute service (mandatory) @@ -54,8 +54,8 @@ async fn gatt_client_server() { .add_characteristic( VALUE_UUID.clone(), &[CharacteristicProp::Read, CharacteristicProp::Write, CharacteristicProp::Notify], - &mut value) - .build(); + &mut value + ).build(); let server = GattServer::::new(stack, table); select! { @@ -68,14 +68,14 @@ async fn gatt_client_server() { match server.next().await { Ok(GattEvent::Write { connection: _, - handle, + value_handle: handle, }) => { - assert_eq!(handle, value_handle); - let _ = server.server().table().get(handle, |value| { - assert_eq!(expected, value[0]); + let characteristic = server.server().table().find_characteristic_by_value_handle(handle).unwrap(); + assert_eq!(characteristic, value_handle); + let value: u8 = server.server().table().get(&characteristic).unwrap(); + assert_eq!(expected, value); expected += 1; writes += 1; - }); if writes == 2 { println!("expected value written twice, test pass"); // NOTE: Ensure that adapter gets polled again @@ -168,7 +168,7 @@ async fn gatt_client_server() { let service = services.first().unwrap().clone(); println!("[central] service discovered successfully"); - let c = client.characteristic_by_uuid(&service, &VALUE_UUID).await.unwrap(); + let c: Characteristic = client.characteristic_by_uuid(&service, &VALUE_UUID).await.unwrap(); let mut data = [0; 1]; client.read_characteristic(&c, &mut data[..]).await.unwrap(); @@ -196,15 +196,15 @@ async fn gatt_client_server() { (Err(e1), Err(e2)) => { println!("Central error: {:?}", e1); println!("Peripheral error: {:?}", e2); - assert!(false); + panic!(); } (Err(e), _) => { println!("Central error: {:?}", e); - assert!(false) + panic!(); } (_, Err(e)) => { println!("Peripheral error: {:?}", e); - assert!(false) + panic!(); } _ => { println!("Test completed successfully"); @@ -212,7 +212,7 @@ async fn gatt_client_server() { }, Err(e) => { println!("Test timed out: {:?}", e); - assert!(false); + panic!(); } } } diff --git a/host/tests/gatt_derive.rs b/host/tests/gatt_derive.rs index 4fbc4d12..6740f4cf 100644 --- a/host/tests/gatt_derive.rs +++ b/host/tests/gatt_derive.rs @@ -1,6 +1,6 @@ -use std::time::Duration; +use std::{cell::RefCell, time::Duration}; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::{raw::NoopRawMutex, CriticalSectionMutex}; use tokio::select; use trouble_host::prelude::*; @@ -23,10 +23,29 @@ struct Server { #[gatt_service(uuid = "408813df-5dd4-1f87-ec11-cdb000100000")] struct CustomService { - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify)] + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", read, write, notify, on_read = value_on_read, on_write = value_on_write)] value: u8, } +static READ_FLAG: CriticalSectionMutex> = CriticalSectionMutex::new(RefCell::new(false)); +static WRITE_FLAG: CriticalSectionMutex> = CriticalSectionMutex::new(RefCell::new(0)); + +fn value_on_read(_: &Connection) { + READ_FLAG.lock(|cell| cell.replace(true)); +} + +fn value_on_write(_: &Connection, _: &[u8]) -> Result<(), ()> { + WRITE_FLAG.lock(|cell| { + let old = cell.replace_with(|&mut old| old + 1); + if old == 0 { + // Return an error on the first write to test accept / reject functionality + Err(()) + } else { + Ok(()) + } + }) +} + #[tokio::test] async fn gatt_client_server() { let _ = env_logger::try_init(); @@ -53,12 +72,13 @@ async fn gatt_client_server() { let server: Server = Server::new_with_config( stack, gap, - ); + ).unwrap(); // Random starting value to 'prove' the incremented value is correct - let value: [u8; 1] = [rand::prelude::random(); 1]; - let mut expected = value[0].wrapping_add(1); - server.set(server.service.value, &value).unwrap(); + let value: u8 = rand::prelude::random(); + // The first write will be rejected by the write callback, so value is not expected to change the first time + let mut expected = value; + server.set(&server.service.value, &value).unwrap(); select! { r = runner.run() => { @@ -70,14 +90,15 @@ async fn gatt_client_server() { match server.next().await { Ok(GattEvent::Write { connection: _, - handle, + value_handle, }) => { - assert_eq!(handle, server.service.value); - let _ = server.get(handle, |value| { - assert_eq!(expected, value[0]); - expected += 1; - writes += 1; - }); + let characteristic = server.server().table().find_characteristic_by_value_handle(value_handle).unwrap(); + assert_eq!(characteristic, server.service.value); + let value = server.get(&characteristic).unwrap(); + assert_eq!(expected, value); + expected += 2; + writes += 1; + if writes == 2 { println!("expected value written twice, test pass"); // NOTE: Ensure that adapter gets polled again @@ -170,17 +191,27 @@ async fn gatt_client_server() { let service = services.first().unwrap().clone(); println!("[central] service discovered successfully"); - let c = client.characteristic_by_uuid(&service, &VALUE_UUID).await.unwrap(); + let c: Characteristic = client.characteristic_by_uuid(&service, &VALUE_UUID).await.unwrap(); let mut data = [0; 1]; client.read_characteristic(&c, &mut data[..]).await.unwrap(); println!("[central] read value: {}", data[0]); data[0] = data[0].wrapping_add(1); println!("[central] write value: {}", data[0]); - client.write_characteristic(&c, &data[..]).await.unwrap(); + if let Err(BleHostError::BleHost(Error::Att(AttErrorCode::ValueNotAllowed))) = client.write_characteristic(&c, &data[..]).await { + println!("[central] Frist write was rejected by write callback as expected."); + } else { + println!("[central] First write was expected to be rejected by server write callback!"); + panic!(); + } data[0] = data[0].wrapping_add(1); println!("[central] write value: {}", data[0]); - client.write_characteristic(&c, &data[..]).await.unwrap(); + if let Ok(()) = client.write_characteristic(&c, &data[..]).await { + println!("[central] Second write accepted by server."); + } else { + println!("[central] Second write was expected to be accepted by the server!"); + panic!(); + } println!("[central] write done"); Ok(()) } => { @@ -209,6 +240,12 @@ async fn gatt_client_server() { panic!(); } _ => { + assert!(READ_FLAG.lock(|cell| cell.take()), "Read callback failed to trigger!"); + let actual_write_count = WRITE_FLAG.lock(|cell| cell.take()); + assert_eq!( + actual_write_count, 2, + "Write callback didn't trigger the expected number of times" + ); println!("Test completed successfully"); } },