| //! This module converts a GattDatastore to an AttDatabase, |
| //! by converting a registry of services into a list of attributes, and proxying |
| //! ATT read/write requests into characteristic reads/writes |
| |
| use std::{cell::RefCell, collections::BTreeMap, ops::RangeInclusive, rc::Rc}; |
| |
| use anyhow::{bail, Result}; |
| use async_trait::async_trait; |
| use log::{error, warn}; |
| |
| use crate::{ |
| core::{ |
| shared_box::{SharedBox, WeakBox, WeakBoxRef}, |
| uuid::Uuid, |
| }, |
| gatt::{ |
| callbacks::{GattWriteRequestType, RawGattDatastore}, |
| ffi::AttributeBackingType, |
| ids::{AttHandle, TransportIndex}, |
| }, |
| packets::{ |
| AttAttributeDataChild, AttAttributeDataView, AttErrorCode, |
| GattCharacteristicDeclarationValueBuilder, GattCharacteristicPropertiesBuilder, |
| GattServiceDeclarationValueBuilder, UuidBuilder, |
| }, |
| }; |
| |
| use super::{ |
| att_database::{AttAttribute, AttDatabase}, |
| att_server_bearer::AttServerBearer, |
| }; |
| |
| pub use super::att_database::AttPermissions; |
| |
| /// Primary Service Declaration from Bluetooth Assigned Numbers 3.5 Declarations |
| pub const PRIMARY_SERVICE_DECLARATION_UUID: Uuid = Uuid::new(0x2800); |
| /// Secondary Service Declaration from Bluetooth Assigned Numbers 3.5 Declarations |
| pub const SECONDARY_SERVICE_DECLARATION_UUID: Uuid = Uuid::new(0x2801); |
| /// Characteristic Declaration from Bluetooth Assigned Numbers 3.5 Declarations |
| pub const CHARACTERISTIC_UUID: Uuid = Uuid::new(0x2803); |
| |
| /// A GattService (currently, only primary services are supported) has an |
| /// identifying UUID and a list of contained characteristics, as well as a |
| /// handle (indicating the attribute where the service declaration will live) |
| #[derive(Debug, Clone)] |
| pub struct GattServiceWithHandle { |
| /// The handle of the service declaration |
| pub handle: AttHandle, |
| /// The type of the service |
| pub type_: Uuid, |
| /// A list of contained characteristics (that must have handles between the |
| /// service declaration handle, and that of the next service) |
| pub characteristics: Vec<GattCharacteristicWithHandle>, |
| } |
| |
| /// A GattCharacteristic consists of a handle (where the value attribute lives), |
| /// a UUID identifying its type, and permissions indicating what operations can |
| /// be performed |
| #[derive(Debug, Clone)] |
| pub struct GattCharacteristicWithHandle { |
| /// The handle of the characteristic value attribute. The characteristic |
| /// declaration is one before this handle. |
| pub handle: AttHandle, |
| /// The UUID representing the type of the characteristic value. |
| pub type_: Uuid, |
| /// The permissions (read/write) indicate what operations can be performed. |
| pub permissions: AttPermissions, |
| /// The descriptors associated with this characteristic |
| pub descriptors: Vec<GattDescriptorWithHandle>, |
| } |
| |
| /// A GattDescriptor consists of a handle, type_, and permissions (similar to a |
| /// GattCharacteristic) It is guaranteed that the handle of the GattDescriptor |
| /// is after the handle of the characteristic value attribute, and before the |
| /// next characteristic/service declaration |
| #[derive(Debug, Clone)] |
| pub struct GattDescriptorWithHandle { |
| /// The handle of the descriptor. |
| pub handle: AttHandle, |
| /// The UUID representing the type of the descriptor. |
| pub type_: Uuid, |
| /// The permissions (read/write) indicate what operations can be performed. |
| pub permissions: AttPermissions, |
| } |
| |
| /// The GattDatabase implements AttDatabase, and converts attribute reads/writes |
| /// into GATT operations to be sent to the upper layers |
| #[derive(Default)] |
| pub struct GattDatabase { |
| schema: RefCell<GattDatabaseSchema>, |
| listeners: RefCell<Vec<Rc<dyn GattDatabaseCallbacks>>>, |
| } |
| |
| #[derive(Default)] |
| struct GattDatabaseSchema { |
| attributes: BTreeMap<AttHandle, AttAttributeWithBackingValue>, |
| } |
| |
| #[derive(Clone)] |
| enum AttAttributeBackingValue { |
| Static(AttAttributeDataChild), |
| DynamicCharacteristic(Rc<dyn RawGattDatastore>), |
| DynamicDescriptor(Rc<dyn RawGattDatastore>), |
| } |
| |
| #[derive(Clone)] |
| struct AttAttributeWithBackingValue { |
| attribute: AttAttribute, |
| value: AttAttributeBackingValue, |
| } |
| |
| /// Callbacks that can be registered on the GattDatabase to watch for |
| /// events of interest. |
| /// |
| /// Note: if the GattDatabase is dropped (e.g. due to unregistration), these |
| /// callbacks will not be invoked, even if the relevant event occurs later. |
| /// e.g. if we open the db, connect, close the db, then disconnect, then on_le_disconnect() |
| /// will NEVER be invoked. |
| pub trait GattDatabaseCallbacks { |
| /// A peer device on the given bearer has connected to this database (and can see its attributes) |
| fn on_le_connect( |
| &self, |
| tcb_idx: TransportIndex, |
| bearer: WeakBoxRef<AttServerBearer<AttDatabaseImpl>>, |
| ); |
| /// A peer device has disconnected from this database |
| fn on_le_disconnect(&self, tcb_idx: TransportIndex); |
| /// The attributes in the specified range have changed |
| fn on_service_change(&self, range: RangeInclusive<AttHandle>); |
| } |
| |
| impl GattDatabase { |
| /// Constructor, wrapping a GattDatastore |
| pub fn new() -> Self { |
| Default::default() |
| } |
| |
| /// Register an event listener |
| pub fn register_listener(&self, callbacks: Rc<dyn GattDatabaseCallbacks>) { |
| self.listeners.borrow_mut().push(callbacks); |
| } |
| |
| /// When a connection has been made with access to this database. |
| /// The supplied bearer is guaranteed to be ready for use. |
| pub fn on_bearer_ready( |
| &self, |
| tcb_idx: TransportIndex, |
| bearer: WeakBoxRef<AttServerBearer<AttDatabaseImpl>>, |
| ) { |
| for listener in self.listeners.borrow().iter() { |
| listener.on_le_connect(tcb_idx, bearer.clone()); |
| } |
| } |
| |
| /// When the connection has dropped. |
| pub fn on_bearer_dropped(&self, tcb_idx: TransportIndex) { |
| for listener in self.listeners.borrow().iter() { |
| listener.on_le_disconnect(tcb_idx); |
| } |
| } |
| |
| /// Add a service with pre-allocated handles (for co-existence with C++) backed by the supplied datastore |
| /// Assumes that the characteristic DECLARATION handles are one less than |
| /// the characteristic handles. |
| /// Returns failure if handles overlap with ones already allocated |
| pub fn add_service_with_handles( |
| &self, |
| service: GattServiceWithHandle, |
| datastore: Rc<dyn RawGattDatastore>, |
| ) -> Result<()> { |
| let mut attributes = BTreeMap::new(); |
| let mut attribute_cnt = 0; |
| |
| let mut add_attribute = |attribute: AttAttribute, value: AttAttributeBackingValue| { |
| attribute_cnt += 1; |
| attributes.insert(attribute.handle, AttAttributeWithBackingValue { attribute, value }) |
| }; |
| |
| let mut characteristics = vec![]; |
| |
| // service definition |
| add_attribute( |
| AttAttribute { |
| handle: service.handle, |
| type_: PRIMARY_SERVICE_DECLARATION_UUID, |
| permissions: AttPermissions::READABLE, |
| }, |
| AttAttributeBackingValue::Static( |
| GattServiceDeclarationValueBuilder { uuid: UuidBuilder::from(service.type_) } |
| .into(), |
| ), |
| ); |
| |
| // characteristics |
| for characteristic in service.characteristics { |
| characteristics.push(characteristic.clone()); |
| |
| // declaration |
| // Recall that we assume the declaration handle is one less than the value |
| // handle |
| let declaration_handle = AttHandle(characteristic.handle.0 - 1); |
| |
| add_attribute( |
| AttAttribute { |
| handle: declaration_handle, |
| type_: CHARACTERISTIC_UUID, |
| permissions: AttPermissions::READABLE, |
| }, |
| AttAttributeBackingValue::Static( |
| GattCharacteristicDeclarationValueBuilder { |
| properties: GattCharacteristicPropertiesBuilder { |
| broadcast: 0, |
| read: characteristic.permissions.readable().into(), |
| write_without_response: characteristic |
| .permissions |
| .writable_without_response() |
| .into(), |
| write: characteristic.permissions.writable_with_response().into(), |
| notify: 0, |
| indicate: characteristic.permissions.indicate().into(), |
| authenticated_signed_writes: 0, |
| extended_properties: 0, |
| }, |
| handle: characteristic.handle.into(), |
| uuid: characteristic.type_.into(), |
| } |
| .into(), |
| ), |
| ); |
| |
| // value |
| add_attribute( |
| AttAttribute { |
| handle: characteristic.handle, |
| type_: characteristic.type_, |
| permissions: characteristic.permissions, |
| }, |
| AttAttributeBackingValue::DynamicCharacteristic(datastore.clone()), |
| ); |
| |
| // descriptors |
| for descriptor in characteristic.descriptors { |
| add_attribute( |
| AttAttribute { |
| handle: descriptor.handle, |
| type_: descriptor.type_, |
| permissions: descriptor.permissions, |
| }, |
| AttAttributeBackingValue::DynamicDescriptor(datastore.clone()), |
| ); |
| } |
| } |
| |
| // validate attributes for overlap |
| let mut static_data = self.schema.borrow_mut(); |
| |
| for handle in attributes.keys() { |
| if static_data.attributes.contains_key(handle) { |
| bail!("duplicate handle detected"); |
| } |
| } |
| if attributes.len() != attribute_cnt { |
| bail!("duplicate handle detected"); |
| } |
| |
| // if we made it here, we successfully loaded the new service |
| static_data.attributes.extend(attributes.clone()); |
| |
| // re-entrancy via the listeners is possible, so we prevent it by dropping here |
| drop(static_data); |
| |
| // notify listeners if any attribute changed |
| let added_handles = attributes.into_iter().map(|attr| attr.0).collect::<Vec<_>>(); |
| if !added_handles.is_empty() { |
| for listener in self.listeners.borrow().iter() { |
| listener.on_service_change( |
| *added_handles.iter().min().unwrap()..=*added_handles.iter().max().unwrap(), |
| ); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Remove a previously-added service by service handle |
| pub fn remove_service_at_handle(&self, service_handle: AttHandle) -> Result<()> { |
| let mut static_data = self.schema.borrow_mut(); |
| |
| // find next service |
| let next_service_handle = static_data |
| .attributes |
| .values() |
| .find(|attribute| { |
| attribute.attribute.handle > service_handle |
| && attribute.attribute.type_ == PRIMARY_SERVICE_DECLARATION_UUID |
| }) |
| .map(|service| service.attribute.handle); |
| |
| // predicate matching all handles in our service |
| let in_service_pred = |handle: AttHandle| { |
| service_handle <= handle && next_service_handle.map(|x| handle < x).unwrap_or(true) |
| }; |
| |
| // record largest attribute matching predicate |
| let largest_service_handle = |
| static_data.attributes.keys().filter(|handle| in_service_pred(**handle)).max().cloned(); |
| |
| // clear out attributes |
| static_data.attributes.retain(|curr_handle, _| !in_service_pred(*curr_handle)); |
| |
| // re-entrancy via the listeners is possible, so we prevent it by dropping here |
| drop(static_data); |
| |
| // notify listeners if any attribute changed |
| if let Some(largest_service_handle) = largest_service_handle { |
| for listener in self.listeners.borrow().iter() { |
| listener.on_service_change(service_handle..=largest_service_handle); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl SharedBox<GattDatabase> { |
| /// Generate an impl AttDatabase from a backing GattDatabase, associated |
| /// with a given connection. |
| /// |
| /// Note: After the AttDatabaseImpl is constructed, we MUST call on_bearer_ready() with |
| /// the resultant bearer, so that the listeners get the correct sequence of callbacks. |
| pub fn get_att_database(&self, tcb_idx: TransportIndex) -> AttDatabaseImpl { |
| AttDatabaseImpl { gatt_db: self.downgrade(), tcb_idx } |
| } |
| } |
| |
| /// An implementation of AttDatabase wrapping an underlying GattDatabase |
| pub struct AttDatabaseImpl { |
| gatt_db: WeakBox<GattDatabase>, |
| tcb_idx: TransportIndex, |
| } |
| |
| #[async_trait(?Send)] |
| impl AttDatabase for AttDatabaseImpl { |
| async fn read_attribute( |
| &self, |
| handle: AttHandle, |
| ) -> Result<AttAttributeDataChild, AttErrorCode> { |
| let value = self.gatt_db.with(|gatt_db| { |
| let Some(gatt_db) = gatt_db else { |
| // db must have been closed |
| return Err(AttErrorCode::INVALID_HANDLE); |
| }; |
| let services = gatt_db.schema.borrow(); |
| let Some(attr) = services.attributes.get(&handle) else { |
| return Err(AttErrorCode::INVALID_HANDLE); |
| }; |
| if !attr.attribute.permissions.readable() { |
| return Err(AttErrorCode::READ_NOT_PERMITTED); |
| } |
| Ok(attr.value.clone()) |
| })?; |
| |
| match value { |
| AttAttributeBackingValue::Static(val) => return Ok(val), |
| AttAttributeBackingValue::DynamicCharacteristic(datastore) => { |
| datastore |
| .read( |
| self.tcb_idx, |
| handle, |
| /* offset */ 0, |
| AttributeBackingType::Characteristic, |
| ) |
| .await |
| } |
| AttAttributeBackingValue::DynamicDescriptor(datastore) => { |
| datastore |
| .read( |
| self.tcb_idx, |
| handle, |
| /* offset */ 0, |
| AttributeBackingType::Descriptor, |
| ) |
| .await |
| } |
| } |
| } |
| |
| async fn write_attribute( |
| &self, |
| handle: AttHandle, |
| data: AttAttributeDataView<'_>, |
| ) -> Result<(), AttErrorCode> { |
| let value = self.gatt_db.with(|gatt_db| { |
| let Some(gatt_db) = gatt_db else { |
| // db must have been closed |
| return Err(AttErrorCode::INVALID_HANDLE); |
| }; |
| let services = gatt_db.schema.borrow(); |
| let Some(attr) = services.attributes.get(&handle) else { |
| return Err(AttErrorCode::INVALID_HANDLE); |
| }; |
| if !attr.attribute.permissions.writable_with_response() { |
| return Err(AttErrorCode::WRITE_NOT_PERMITTED); |
| } |
| Ok(attr.value.clone()) |
| })?; |
| |
| match value { |
| AttAttributeBackingValue::Static(val) => { |
| error!("A static attribute {val:?} is marked as writable - ignoring it and rejecting the write..."); |
| return Err(AttErrorCode::WRITE_NOT_PERMITTED); |
| } |
| AttAttributeBackingValue::DynamicCharacteristic(datastore) => { |
| datastore |
| .write( |
| self.tcb_idx, |
| handle, |
| AttributeBackingType::Characteristic, |
| GattWriteRequestType::Request, |
| data, |
| ) |
| .await |
| } |
| AttAttributeBackingValue::DynamicDescriptor(datastore) => { |
| datastore |
| .write( |
| self.tcb_idx, |
| handle, |
| AttributeBackingType::Descriptor, |
| GattWriteRequestType::Request, |
| data, |
| ) |
| .await |
| } |
| } |
| } |
| |
| fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>) { |
| let value = self.gatt_db.with(|gatt_db| { |
| let Some(gatt_db) = gatt_db else { |
| // db must have been closed |
| return None; |
| }; |
| let services = gatt_db.schema.borrow(); |
| let Some(attr) = services.attributes.get(&handle) else { |
| warn!("cannot find handle {handle:?}"); |
| return None; |
| }; |
| if !attr.attribute.permissions.writable_without_response() { |
| warn!("trying to write without response to {handle:?}, which doesn't support it"); |
| return None; |
| } |
| Some(attr.value.clone()) |
| }); |
| |
| let Some(value) = value else { |
| return; |
| }; |
| |
| match value { |
| AttAttributeBackingValue::Static(val) => { |
| error!("A static attribute {val:?} is marked as writable - ignoring it and rejecting the write..."); |
| } |
| AttAttributeBackingValue::DynamicCharacteristic(datastore) => { |
| datastore.write_no_response( |
| self.tcb_idx, |
| handle, |
| AttributeBackingType::Characteristic, |
| data, |
| ); |
| } |
| AttAttributeBackingValue::DynamicDescriptor(datastore) => { |
| datastore.write_no_response( |
| self.tcb_idx, |
| handle, |
| AttributeBackingType::Descriptor, |
| data, |
| ); |
| } |
| }; |
| } |
| |
| fn list_attributes(&self) -> Vec<AttAttribute> { |
| self.gatt_db.with(|db| { |
| db.map(|db| db.schema.borrow().attributes.values().map(|attr| attr.attribute).collect()) |
| .unwrap_or_default() |
| }) |
| } |
| } |
| |
| impl Clone for AttDatabaseImpl { |
| fn clone(&self) -> Self { |
| Self { gatt_db: self.gatt_db.clone(), tcb_idx: self.tcb_idx } |
| } |
| } |
| |
| impl AttDatabaseImpl { |
| /// When the bearer owning this AttDatabase is invalidated, |
| /// we must notify the listeners tied to our GattDatabase. |
| /// |
| /// Note: AttDatabases referring to the backing GattDatabase |
| /// may still exist after bearer invalidation, but the bearer will |
| /// no longer exist (so packets can no longer be sent/received). |
| pub fn on_bearer_dropped(&self) { |
| self.gatt_db.with(|db| { |
| db.map(|db| { |
| for listener in db.listeners.borrow().iter() { |
| listener.on_le_disconnect(self.tcb_idx) |
| } |
| }) |
| }); |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use tokio::{join, sync::mpsc::error::TryRecvError, task::spawn_local}; |
| |
| use crate::{ |
| gatt::mocks::{ |
| mock_database_callbacks::{MockCallbackEvents, MockCallbacks}, |
| mock_datastore::{MockDatastore, MockDatastoreEvents}, |
| mock_raw_datastore::{MockRawDatastore, MockRawDatastoreEvents}, |
| }, |
| packets::Packet, |
| utils::{ |
| packet::{build_att_data, build_view_or_crash}, |
| task::block_on_locally, |
| }, |
| }; |
| |
| use super::*; |
| |
| const SERVICE_HANDLE: AttHandle = AttHandle(1); |
| const SERVICE_TYPE: Uuid = Uuid::new(0x1234); |
| |
| const CHARACTERISTIC_DECLARATION_HANDLE: AttHandle = AttHandle(2); |
| const CHARACTERISTIC_VALUE_HANDLE: AttHandle = AttHandle(3); |
| const CHARACTERISTIC_TYPE: Uuid = Uuid::new(0x5678); |
| |
| const DESCRIPTOR_HANDLE: AttHandle = AttHandle(4); |
| const DESCRIPTOR_TYPE: Uuid = Uuid::new(0x9ABC); |
| |
| const TCB_IDX: TransportIndex = TransportIndex(1); |
| |
| #[test] |
| fn test_read_empty_db() { |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| |
| let resp = tokio_test::block_on(att_db.read_attribute(AttHandle(1))); |
| |
| assert_eq!(resp, Err(AttErrorCode::INVALID_HANDLE)) |
| } |
| |
| #[test] |
| fn test_single_service() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| |
| let attrs = att_db.list_attributes(); |
| let service_value = tokio_test::block_on(att_db.read_attribute(SERVICE_HANDLE)); |
| |
| assert_eq!( |
| attrs, |
| vec![AttAttribute { |
| handle: SERVICE_HANDLE, |
| type_: PRIMARY_SERVICE_DECLARATION_UUID, |
| permissions: AttPermissions::READABLE |
| }] |
| ); |
| assert_eq!( |
| service_value, |
| Ok(AttAttributeDataChild::GattServiceDeclarationValue( |
| GattServiceDeclarationValueBuilder { uuid: SERVICE_TYPE.into() } |
| )) |
| ); |
| } |
| |
| #[test] |
| fn test_service_removal() { |
| // arrange three services, each with a single characteristic |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_datastore = Rc::new(gatt_datastore); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(1), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(3), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| gatt_datastore.clone(), |
| ) |
| .unwrap(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| gatt_datastore.clone(), |
| ) |
| .unwrap(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(7), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(9), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| gatt_datastore, |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| assert_eq!(att_db.list_attributes().len(), 9); |
| |
| // act: remove the middle service |
| gatt_db.remove_service_at_handle(AttHandle(4)).unwrap(); |
| let attrs = att_db.list_attributes(); |
| |
| // assert that the middle service is gone |
| assert_eq!(attrs.len(), 6, "{attrs:?}"); |
| |
| // assert the other two old services are still there |
| assert_eq!( |
| attrs[0], |
| AttAttribute { |
| handle: AttHandle(1), |
| type_: PRIMARY_SERVICE_DECLARATION_UUID, |
| permissions: AttPermissions::READABLE |
| } |
| ); |
| assert_eq!( |
| attrs[3], |
| AttAttribute { |
| handle: AttHandle(7), |
| type_: PRIMARY_SERVICE_DECLARATION_UUID, |
| permissions: AttPermissions::READABLE |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_single_characteristic_declaration() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE |
| | AttPermissions::WRITABLE_WITH_RESPONSE |
| | AttPermissions::INDICATE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| |
| let attrs = att_db.list_attributes(); |
| let characteristic_decl = |
| tokio_test::block_on(att_db.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE)); |
| |
| assert_eq!(attrs.len(), 3, "{attrs:?}"); |
| assert_eq!(attrs[0].type_, PRIMARY_SERVICE_DECLARATION_UUID); |
| assert_eq!( |
| attrs[1], |
| AttAttribute { |
| handle: CHARACTERISTIC_DECLARATION_HANDLE, |
| type_: CHARACTERISTIC_UUID, |
| permissions: AttPermissions::READABLE |
| } |
| ); |
| assert_eq!( |
| attrs[2], |
| AttAttribute { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE |
| | AttPermissions::WRITABLE_WITH_RESPONSE |
| | AttPermissions::INDICATE |
| } |
| ); |
| |
| assert_eq!( |
| characteristic_decl, |
| Ok(AttAttributeDataChild::GattCharacteristicDeclarationValue( |
| GattCharacteristicDeclarationValueBuilder { |
| properties: GattCharacteristicPropertiesBuilder { |
| read: 1, |
| broadcast: 0, |
| write_without_response: 0, |
| write: 1, |
| notify: 0, |
| indicate: 1, |
| authenticated_signed_writes: 0, |
| extended_properties: 0, |
| }, |
| handle: CHARACTERISTIC_VALUE_HANDLE.into(), |
| uuid: CHARACTERISTIC_TYPE.into() |
| } |
| )) |
| ); |
| } |
| |
| #[test] |
| fn test_all_characteristic_permissions() { |
| // arrange |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| |
| // act: add a characteristic with all permission bits set |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::all(), |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| |
| // assert: the characteristic declaration has all the bits we support set |
| let characteristic_decl = |
| tokio_test::block_on(att_db.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE)); |
| assert_eq!( |
| characteristic_decl, |
| Ok(AttAttributeDataChild::GattCharacteristicDeclarationValue( |
| GattCharacteristicDeclarationValueBuilder { |
| properties: GattCharacteristicPropertiesBuilder { |
| read: 1, |
| broadcast: 0, |
| write_without_response: 1, |
| write: 1, |
| notify: 0, |
| indicate: 1, |
| authenticated_signed_writes: 0, |
| extended_properties: 0, |
| }, |
| handle: CHARACTERISTIC_VALUE_HANDLE.into(), |
| uuid: CHARACTERISTIC_TYPE.into() |
| } |
| )) |
| ); |
| } |
| |
| #[test] |
| fn test_single_characteristic_value() { |
| // arrange: create a database with a single characteristic |
| let (gatt_datastore, mut data_evts) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = AttAttributeDataChild::RawData(Box::new([1, 2])); |
| |
| // act: read from the database, and supply a value from the backing datastore |
| let characteristic_value = tokio_test::block_on(async { |
| join!( |
| async { |
| let MockDatastoreEvents::Read( |
| TCB_IDX, |
| CHARACTERISTIC_VALUE_HANDLE, |
| AttributeBackingType::Characteristic, |
| reply, |
| ) = data_evts.recv().await.unwrap() else { |
| unreachable!() |
| }; |
| reply.send(Ok(data.clone())).unwrap(); |
| }, |
| att_db.read_attribute(CHARACTERISTIC_VALUE_HANDLE) |
| ) |
| .1 |
| }); |
| |
| // assert: the supplied value matches what the att datastore returned |
| assert_eq!(characteristic_value, Ok(data)); |
| } |
| |
| #[test] |
| fn test_unreadable_characteristic() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::empty(), |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| |
| let characteristic_value = tokio_test::block_on( |
| gatt_db.get_att_database(TCB_IDX).read_attribute(CHARACTERISTIC_VALUE_HANDLE), |
| ); |
| |
| assert_eq!(characteristic_value, Err(AttErrorCode::READ_NOT_PERMITTED)); |
| } |
| |
| #[test] |
| fn test_handle_clash() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| |
| let result = gatt_db.add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::WRITABLE_WITH_RESPONSE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ); |
| |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_handle_clash_with_existing() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_datastore = Rc::new(gatt_datastore); |
| let gatt_db = Rc::new(GattDatabase::new()); |
| |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![], |
| }, |
| gatt_datastore.clone(), |
| ) |
| .unwrap(); |
| |
| let result = gatt_db.add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![], |
| }, |
| gatt_datastore, |
| ); |
| |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_write_single_characteristic_callback_invoked() { |
| // arrange: create a database with a single characteristic |
| let (gatt_datastore, mut data_evts) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::WRITABLE_WITH_RESPONSE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| // act: write to the database |
| let recv_data = block_on_locally(async { |
| // start write task |
| let cloned_data = data.view().to_owned_packet(); |
| spawn_local(async move { |
| att_db |
| .write_attribute(CHARACTERISTIC_VALUE_HANDLE, cloned_data.view()) |
| .await |
| .unwrap(); |
| }); |
| |
| let MockDatastoreEvents::Write( |
| TCB_IDX, |
| CHARACTERISTIC_VALUE_HANDLE, |
| AttributeBackingType::Characteristic, |
| recv_data, |
| _, |
| ) = data_evts.recv().await.unwrap() else { |
| unreachable!(); |
| }; |
| recv_data |
| }); |
| |
| // assert: the received value matches what we supplied |
| assert_eq!( |
| recv_data.view().get_raw_payload().collect::<Vec<_>>(), |
| data.view().get_raw_payload().collect::<Vec<_>>() |
| ); |
| } |
| |
| #[test] |
| fn test_write_single_characteristic_recv_response() { |
| // arrange: create a database with a single characteristic |
| let (gatt_datastore, mut data_evts) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::WRITABLE_WITH_RESPONSE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| // act: write to the database |
| let res = tokio_test::block_on(async { |
| join!( |
| async { |
| let MockDatastoreEvents::Write(_,_,_,_,reply) = data_evts.recv().await.unwrap() else { |
| unreachable!(); |
| }; |
| reply.send(Err(AttErrorCode::UNLIKELY_ERROR)).unwrap(); |
| }, |
| att_db.write_attribute(CHARACTERISTIC_VALUE_HANDLE, data.view()) |
| ) |
| .1 |
| }); |
| |
| // assert: the supplied value matches what the att datastore returned |
| assert_eq!(res, Err(AttErrorCode::UNLIKELY_ERROR)); |
| } |
| |
| #[test] |
| fn test_unwriteable_characteristic() { |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| let characteristic_value = tokio_test::block_on( |
| gatt_db |
| .get_att_database(TCB_IDX) |
| .write_attribute(CHARACTERISTIC_VALUE_HANDLE, data.view()), |
| ); |
| |
| assert_eq!(characteristic_value, Err(AttErrorCode::WRITE_NOT_PERMITTED)); |
| } |
| |
| #[test] |
| fn test_single_descriptor_declaration() { |
| let (gatt_datastore, mut data_evts) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![GattDescriptorWithHandle { |
| handle: DESCRIPTOR_HANDLE, |
| type_: DESCRIPTOR_TYPE, |
| permissions: AttPermissions::READABLE, |
| }], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = AttAttributeDataChild::RawData(Box::new([1, 2])); |
| |
| let descriptor_value = block_on_locally(async { |
| // start write task |
| let pending_read = |
| spawn_local(async move { att_db.read_attribute(DESCRIPTOR_HANDLE).await.unwrap() }); |
| |
| let MockDatastoreEvents::Read( |
| TCB_IDX, |
| DESCRIPTOR_HANDLE, |
| AttributeBackingType::Descriptor, |
| reply, |
| ) = data_evts.recv().await.unwrap() else { |
| unreachable!(); |
| }; |
| |
| reply.send(Ok(data.clone())).unwrap(); |
| |
| pending_read.await.unwrap() |
| }); |
| |
| assert_eq!(descriptor_value, data); |
| } |
| |
| #[test] |
| fn test_write_descriptor() { |
| // arrange: db with a writable descriptor |
| let (gatt_datastore, mut data_evts) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![GattDescriptorWithHandle { |
| handle: DESCRIPTOR_HANDLE, |
| type_: DESCRIPTOR_TYPE, |
| permissions: AttPermissions::WRITABLE_WITH_RESPONSE, |
| }], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| // act: write, and wait for the callback to be invoked |
| block_on_locally(async { |
| // start write task |
| spawn_local(async move { |
| att_db.write_attribute(DESCRIPTOR_HANDLE, data.view()).await.unwrap() |
| }); |
| |
| let MockDatastoreEvents::Write( |
| TCB_IDX, |
| DESCRIPTOR_HANDLE, |
| AttributeBackingType::Descriptor, |
| _, |
| _, |
| ) = data_evts.recv().await.unwrap() else { |
| unreachable!(); |
| }; |
| }); |
| |
| // assert: nothing, if we reach this far we are OK |
| } |
| |
| #[test] |
| fn test_multiple_descriptors() { |
| // arrange: a database with some characteristics and descriptors |
| let (gatt_datastore, _) = MockDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(1), |
| type_: SERVICE_TYPE, |
| characteristics: vec![ |
| GattCharacteristicWithHandle { |
| handle: AttHandle(3), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![GattDescriptorWithHandle { |
| handle: AttHandle(4), |
| type_: DESCRIPTOR_TYPE, |
| permissions: AttPermissions::READABLE, |
| }], |
| }, |
| GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![ |
| GattDescriptorWithHandle { |
| handle: AttHandle(7), |
| type_: DESCRIPTOR_TYPE, |
| permissions: AttPermissions::WRITABLE_WITH_RESPONSE, |
| }, |
| GattDescriptorWithHandle { |
| handle: AttHandle(8), |
| type_: DESCRIPTOR_TYPE, |
| permissions: AttPermissions::READABLE |
| | AttPermissions::WRITABLE_WITH_RESPONSE, |
| }, |
| ], |
| }, |
| ], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| |
| // act: get the attributes |
| let attributes = gatt_db.get_att_database(TCB_IDX).list_attributes(); |
| |
| // assert: check the attributes are in the correct order |
| assert_eq!(attributes.len(), 8); |
| assert_eq!(attributes[0].type_, PRIMARY_SERVICE_DECLARATION_UUID); |
| assert_eq!(attributes[1].type_, CHARACTERISTIC_UUID); |
| assert_eq!(attributes[2].type_, CHARACTERISTIC_TYPE); |
| assert_eq!(attributes[3].type_, DESCRIPTOR_TYPE); |
| assert_eq!(attributes[4].type_, CHARACTERISTIC_UUID); |
| assert_eq!(attributes[5].type_, CHARACTERISTIC_TYPE); |
| assert_eq!(attributes[6].type_, DESCRIPTOR_TYPE); |
| assert_eq!(attributes[7].type_, DESCRIPTOR_TYPE); |
| // assert: check the handles of the descriptors are correct |
| assert_eq!(attributes[3].handle, AttHandle(4)); |
| assert_eq!(attributes[6].handle, AttHandle(7)); |
| assert_eq!(attributes[7].handle, AttHandle(8)); |
| // assert: check the permissions of the descriptors are correct |
| assert_eq!(attributes[3].permissions, AttPermissions::READABLE); |
| assert_eq!(attributes[6].permissions, AttPermissions::WRITABLE_WITH_RESPONSE); |
| assert_eq!( |
| attributes[7].permissions, |
| AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE |
| ); |
| } |
| |
| #[test] |
| fn test_multiple_datastores() { |
| // arrange: create a database with two services backed by different datastores |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| |
| let (gatt_datastore_1, mut data_evts_1) = MockDatastore::new(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(1), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(3), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore_1), |
| ) |
| .unwrap(); |
| |
| let (gatt_datastore_2, mut data_evts_2) = MockDatastore::new(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore_2), |
| ) |
| .unwrap(); |
| |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = AttAttributeDataChild::RawData(Box::new([1, 2])); |
| |
| // act: read from the second characteristic and supply a response from the second datastore |
| let characteristic_value = tokio_test::block_on(async { |
| join!( |
| async { |
| let MockDatastoreEvents::Read( |
| TCB_IDX, |
| AttHandle(6), |
| AttributeBackingType::Characteristic, |
| reply, |
| ) = data_evts_2.recv().await.unwrap() else { |
| unreachable!() |
| }; |
| reply.send(Ok(data.clone())).unwrap(); |
| }, |
| att_db.read_attribute(AttHandle(6)) |
| ) |
| .1 |
| }); |
| |
| // assert: the supplied value matches what the att datastore returned |
| assert_eq!(characteristic_value, Ok(data)); |
| // the first datastore received no events |
| assert_eq!(data_evts_1.try_recv().unwrap_err(), TryRecvError::Empty); |
| // the second datastore has no remaining events |
| assert_eq!(data_evts_2.try_recv().unwrap_err(), TryRecvError::Empty); |
| } |
| |
| fn make_bearer( |
| gatt_db: &SharedBox<GattDatabase>, |
| ) -> SharedBox<AttServerBearer<AttDatabaseImpl>> { |
| SharedBox::new(AttServerBearer::new(gatt_db.get_att_database(TCB_IDX), |_| { |
| unreachable!(); |
| })) |
| } |
| |
| #[test] |
| fn test_connection_listener() { |
| // arrange: db with a listener |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| gatt_db.register_listener(Rc::new(callbacks)); |
| let bearer = make_bearer(&gatt_db); |
| |
| // act: open a connection |
| gatt_db.on_bearer_ready(TCB_IDX, bearer.as_ref()); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| assert!(matches!(event, MockCallbackEvents::OnLeConnect(TCB_IDX, _))); |
| } |
| |
| #[test] |
| fn test_disconnection_listener() { |
| // arrange: db with a listener |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| gatt_db.register_listener(Rc::new(callbacks)); |
| |
| // act: disconnect |
| gatt_db.on_bearer_dropped(TCB_IDX); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| assert!(matches!(event, MockCallbackEvents::OnLeDisconnect(TCB_IDX))); |
| } |
| |
| #[test] |
| fn test_multiple_listeners() { |
| // arrange: db with two listeners |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks1, mut rx1) = MockCallbacks::new(); |
| gatt_db.register_listener(Rc::new(callbacks1)); |
| let (callbacks2, mut rx2) = MockCallbacks::new(); |
| gatt_db.register_listener(Rc::new(callbacks2)); |
| |
| // act: disconnect |
| gatt_db.on_bearer_dropped(TCB_IDX); |
| |
| // assert: we got the callback on both listeners |
| let event = rx1.blocking_recv().unwrap(); |
| assert!(matches!(event, MockCallbackEvents::OnLeDisconnect(TCB_IDX))); |
| let event = rx2.blocking_recv().unwrap(); |
| assert!(matches!(event, MockCallbackEvents::OnLeDisconnect(TCB_IDX))); |
| } |
| |
| #[test] |
| fn test_add_service_changed_listener() { |
| // arrange: db with a listener |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| let (datastore, _) = MockDatastore::new(); |
| |
| // act: start listening and add a new service |
| gatt_db.register_listener(Rc::new(callbacks)); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::empty(), |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(datastore), |
| ) |
| .unwrap(); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| let MockCallbackEvents::OnServiceChange(range) = event else { |
| unreachable!(); |
| }; |
| assert_eq!(*range.start(), AttHandle(4)); |
| assert_eq!(*range.end(), AttHandle(6)); |
| } |
| |
| #[test] |
| fn test_partial_remove_service_changed_listener() { |
| // arrange: db with two services and a listener |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| let (datastore, _) = MockDatastore::new(); |
| let datastore = Rc::new(datastore); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::empty(), |
| descriptors: vec![], |
| }], |
| }, |
| datastore.clone(), |
| ) |
| .unwrap(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(8), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(10), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::empty(), |
| descriptors: vec![], |
| }], |
| }, |
| datastore, |
| ) |
| .unwrap(); |
| |
| // act: start listening and remove the first service |
| gatt_db.register_listener(Rc::new(callbacks)); |
| gatt_db.remove_service_at_handle(AttHandle(4)).unwrap(); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| let MockCallbackEvents::OnServiceChange(range) = event else { |
| unreachable!(); |
| }; |
| assert_eq!(*range.start(), AttHandle(4)); |
| assert_eq!(*range.end(), AttHandle(6)); |
| } |
| |
| #[test] |
| fn test_full_remove_service_changed_listener() { |
| // arrange: db with a listener and a service |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| let (datastore, _) = MockDatastore::new(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: AttHandle(6), |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::empty(), |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(datastore), |
| ) |
| .unwrap(); |
| |
| // act: start listening and remove the service |
| gatt_db.register_listener(Rc::new(callbacks)); |
| gatt_db.remove_service_at_handle(AttHandle(4)).unwrap(); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| let MockCallbackEvents::OnServiceChange(range) = event else { |
| unreachable!(); |
| }; |
| assert_eq!(*range.start(), AttHandle(4)); |
| assert_eq!(*range.end(), AttHandle(6)); |
| } |
| |
| #[test] |
| fn test_trivial_remove_service_changed_listener() { |
| // arrange: db with a listener and a trivial service |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| let (callbacks, mut rx) = MockCallbacks::new(); |
| let (datastore, _) = MockDatastore::new(); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: AttHandle(4), |
| type_: SERVICE_TYPE, |
| characteristics: vec![], |
| }, |
| Rc::new(datastore), |
| ) |
| .unwrap(); |
| |
| // act: start listening and remove the service |
| gatt_db.register_listener(Rc::new(callbacks)); |
| gatt_db.remove_service_at_handle(AttHandle(4)).unwrap(); |
| |
| // assert: we got the callback |
| let event = rx.blocking_recv().unwrap(); |
| let MockCallbackEvents::OnServiceChange(range) = event else { |
| unreachable!(); |
| }; |
| assert_eq!(*range.start(), AttHandle(4)); |
| assert_eq!(*range.end(), AttHandle(4)); |
| } |
| |
| #[test] |
| fn test_write_no_response_single_characteristic() { |
| // arrange: create a database with a single characteristic |
| let (gatt_datastore, mut data_evts) = MockRawDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::WRITABLE_WITHOUT_RESPONSE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| // act: write without response to the database |
| att_db.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, data.view()); |
| |
| // assert: we got a callback |
| let event = data_evts.blocking_recv().unwrap(); |
| let MockRawDatastoreEvents::WriteNoResponse(TCB_IDX, CHARACTERISTIC_VALUE_HANDLE, AttributeBackingType::Characteristic, recv_data) = event else { |
| unreachable!("{event:?}"); |
| }; |
| assert_eq!( |
| recv_data.view().get_raw_payload().collect::<Vec<_>>(), |
| data.view().get_raw_payload().collect::<Vec<_>>() |
| ); |
| } |
| |
| #[test] |
| fn test_unwriteable_without_response_characteristic() { |
| // arrange: db with a characteristic that is writable, but not writable-without-response |
| let (gatt_datastore, mut data_events) = MockRawDatastore::new(); |
| let gatt_db = SharedBox::new(GattDatabase::new()); |
| gatt_db |
| .add_service_with_handles( |
| GattServiceWithHandle { |
| handle: SERVICE_HANDLE, |
| type_: SERVICE_TYPE, |
| characteristics: vec![GattCharacteristicWithHandle { |
| handle: CHARACTERISTIC_VALUE_HANDLE, |
| type_: CHARACTERISTIC_TYPE, |
| permissions: AttPermissions::READABLE |
| | AttPermissions::WRITABLE_WITH_RESPONSE, |
| descriptors: vec![], |
| }], |
| }, |
| Rc::new(gatt_datastore), |
| ) |
| .unwrap(); |
| let att_db = gatt_db.get_att_database(TCB_IDX); |
| let data = |
| build_view_or_crash(build_att_data(AttAttributeDataChild::RawData(Box::new([1, 2])))); |
| |
| // act: try writing without response to this characteristic |
| att_db.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, data.view()); |
| |
| // assert: no callback was sent |
| assert_eq!(data_events.try_recv().unwrap_err(), TryRecvError::Empty); |
| } |
| } |