diff options
author | 2023-03-08 03:22:38 +0000 | |
---|---|---|
committer | 2023-03-09 01:26:28 +0000 | |
commit | e8b68ea94aeb440e63c255bab747c3dcc6565276 (patch) | |
tree | 385846a4b814aff86e1f93fbd28a1eb2cfb4ebd3 | |
parent | 26c1bc3fa4def19ddaa87ae90756991dd7289fd3 (diff) |
[Private GATT] Add support for descriptors
Bug: 255880936
Test: unit
Change-Id: I62a0f715cbf27e5a895dcdb39cd9f06233fb19ea
-rw-r--r-- | system/rust/src/gatt/callbacks.rs | 21 | ||||
-rw-r--r-- | system/rust/src/gatt/callbacks/callback_transaction_manager.rs | 13 | ||||
-rw-r--r-- | system/rust/src/gatt/ffi.rs | 168 | ||||
-rw-r--r-- | system/rust/src/gatt/ffi/gatt_shim.cc | 60 | ||||
-rw-r--r-- | system/rust/src/gatt/ffi/gatt_shim.h | 23 | ||||
-rw-r--r-- | system/rust/src/gatt/mocks/mock_callbacks.rs | 19 | ||||
-rw-r--r-- | system/rust/src/gatt/mocks/mock_datastore.rs | 18 | ||||
-rw-r--r-- | system/rust/src/gatt/server/att_server_bearer.rs | 5 | ||||
-rw-r--r-- | system/rust/src/gatt/server/gatt_database.rs | 273 | ||||
-rw-r--r-- | system/rust/src/gatt/server/transactions/helpers/payload_accumulator.rs | 7 | ||||
-rw-r--r-- | system/rust/tests/gatt_callbacks_test.rs | 38 | ||||
-rw-r--r-- | system/rust/tests/gatt_server_test.rs | 162 |
12 files changed, 635 insertions, 172 deletions
diff --git a/system/rust/src/gatt/callbacks.rs b/system/rust/src/gatt/callbacks.rs index 3d969122a9..d60bd5a0c8 100644 --- a/system/rust/src/gatt/callbacks.rs +++ b/system/rust/src/gatt/callbacks.rs @@ -11,6 +11,7 @@ use async_trait::async_trait; use crate::packets::{AttAttributeDataChild, AttAttributeDataView, AttErrorCode}; use super::{ + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId, TransactionId}, server::IndicationError, }; @@ -18,25 +19,27 @@ use super::{ /// These callbacks are expected to be made available to the GattModule from /// JNI. pub trait GattCallbacks { - /// Invoked when a client tries to read a characteristic. Expects a response - /// using bluetooth::gatt::send_response(); - fn on_server_read_characteristic( + /// Invoked when a client tries to read a characteristic/descriptor. Expects + /// a response using bluetooth::gatt::send_response(); + fn on_server_read( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, is_long: bool, ); - /// Invoked when a client tries to write a characteristic. Expects a - /// response using bluetooth::gatt::send_response(); + /// Invoked when a client tries to write a characteristic/descriptor. + /// Expects a response using bluetooth::gatt::send_response(); #[allow(clippy::too_many_arguments)] // needed to match the C++ interface - fn on_server_write_characteristic( + fn on_server_write( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, need_response: bool, is_prepare: bool, @@ -64,17 +67,19 @@ pub trait GattDatastore { fn remove_connection(&self, conn_id: ConnectionId); /// Read a characteristic from the specified connection at the given handle. - async fn read_characteristic( + async fn read( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, ) -> Result<AttAttributeDataChild, AttErrorCode>; /// Write data to a given characteristic on the specified connection. - async fn write_characteristic( + async fn write( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode>; } diff --git a/system/rust/src/gatt/callbacks/callback_transaction_manager.rs b/system/rust/src/gatt/callbacks/callback_transaction_manager.rs index 0406a1e000..da50be9bd4 100644 --- a/system/rust/src/gatt/callbacks/callback_transaction_manager.rs +++ b/system/rust/src/gatt/callbacks/callback_transaction_manager.rs @@ -12,7 +12,7 @@ use crate::{ packets::{AttAttributeDataChild, AttAttributeDataView, AttErrorCode}, }; -use super::GattDatastore; +use super::{AttributeBackingType, GattDatastore}; struct PendingTransaction { response: oneshot::Sender<Result<AttAttributeDataChild, AttErrorCode>>, @@ -121,15 +121,16 @@ impl GattDatastore for CallbackTransactionManager { assert!(old_conn.is_some(), "Received unexpected connection ID, something has gone wrong") } - async fn read_characteristic( + async fn read( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, ) -> Result<AttAttributeDataChild, AttErrorCode> { let (trans_id, rx) = self.pending_transactions.borrow_mut().start_new_transaction(conn_id)?; - self.callbacks.on_server_read_characteristic(conn_id, trans_id, handle, 0, false); + self.callbacks.on_server_read(conn_id, trans_id, handle, attr_type, 0, false); if let Ok(value) = rx.await { value @@ -139,17 +140,17 @@ impl GattDatastore for CallbackTransactionManager { } } - async fn write_characteristic( + async fn write( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode> { let (trans_id, rx) = self.pending_transactions.borrow_mut().start_new_transaction(conn_id)?; - self.callbacks - .on_server_write_characteristic(conn_id, trans_id, handle, 0, true, false, data); + self.callbacks.on_server_write(conn_id, trans_id, handle, attr_type, 0, true, false, data); if let Ok(value) = rx.await { value.map(|_| ()) // the data passed back is irrelevant for write diff --git a/system/rust/src/gatt/ffi.rs b/system/rust/src/gatt/ffi.rs index cb93597aec..32dbb6c8c2 100644 --- a/system/rust/src/gatt/ffi.rs +++ b/system/rust/src/gatt/ffi.rs @@ -1,6 +1,8 @@ //! FFI interfaces for the GATT module. Some structs are exported so that //! core::init can instantiate and pass them into the main loop. +use std::iter::Peekable; + use anyhow::{bail, Result}; use bt_common::init_flags::{ always_use_private_gatt_for_debugging_is_enabled, rust_event_loop_is_enabled, @@ -23,8 +25,10 @@ use super::{ channel::AttTransport, ids::{AdvertiserId, AttHandle, ConnectionId, ServerId, TransactionId, TransportIndex}, server::{ - att_server_bearer::AttServerBearer, - gatt_database::{AttPermissions, GattCharacteristicWithHandle, GattServiceWithHandle}, + gatt_database::{ + AttPermissions, GattCharacteristicWithHandle, GattDescriptorWithHandle, + GattServiceWithHandle, + }, IndicationError, }, GattCallbacks, @@ -44,33 +48,49 @@ mod inner { type Uuid = crate::core::uuid::Uuid; } + /// The GATT entity backing the value of a user-controlled + /// attribute + #[derive(Debug)] + #[namespace = "bluetooth::gatt"] + enum AttributeBackingType { + /// A GATT characteristic + #[cxx_name = "CHARACTERISTIC"] + Characteristic = 0u32, + /// A GATT descriptor + #[cxx_name = "DESCRIPTOR"] + Descriptor = 1u32, + } + #[namespace = "bluetooth::gatt"] unsafe extern "C++" { include!("src/gatt/ffi/gatt_shim.h"); + type AttributeBackingType; /// This contains the callbacks from Rust into C++ JNI needed for GATT type GattServerCallbacks; - /// This callback is invoked when reading a characteristic - the client + /// This callback is invoked when reading - the client /// must reply using SendResponse - #[cxx_name = "OnServerReadCharacteristic"] - fn on_server_read_characteristic( + #[cxx_name = "OnServerRead"] + fn on_server_read( self: &GattServerCallbacks, conn_id: u16, trans_id: u32, attr_handle: u16, + attr_type: AttributeBackingType, offset: u32, is_long: bool, ); - /// This callback is invoked when writing a characteristic - the client + /// This callback is invoked when writing - the client /// must reply using SendResponse - #[cxx_name = "OnServerWriteCharacteristic"] - fn on_server_write_characteristic( + #[cxx_name = "OnServerWrite"] + fn on_server_write( self: &GattServerCallbacks, conn_id: u16, trans_id: u32, attr_handle: u16, + attr_type: AttributeBackingType, offset: u32, need_response: bool, is_prepare: bool, @@ -80,7 +100,7 @@ mod inner { /// This callback is invoked when an indication has been sent and the /// peer device has confirmed it, or if some error occurred. #[cxx_name = "OnIndicationSentConfirmation"] - fn on_indication_sent_confirmation(&self, conn_id: u16, status: i32); + fn on_indication_sent_confirmation(self: &GattServerCallbacks, conn_id: u16, status: i32); } /// What action the arbiter should take in response to an incoming packet @@ -160,34 +180,37 @@ mod inner { pub struct GattCallbacksImpl(pub UniquePtr<GattServerCallbacks>); impl GattCallbacks for GattCallbacksImpl { - fn on_server_read_characteristic( + fn on_server_read( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, is_long: bool, ) { self.0 .as_ref() .unwrap() - .on_server_read_characteristic(conn_id.0, trans_id.0, handle.0, offset, is_long); + .on_server_read(conn_id.0, trans_id.0, handle.0, attr_type, offset, is_long); } - fn on_server_write_characteristic( + fn on_server_write( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, need_response: bool, is_prepare: bool, value: AttAttributeDataView, ) { - self.0.as_ref().unwrap().on_server_write_characteristic( + self.0.as_ref().unwrap().on_server_write( conn_id.0, trans_id.0, handle.0, + attr_type, offset, need_response, is_prepare, @@ -262,11 +285,33 @@ fn close_server(server_id: u8) { }) } +fn consume_descriptors<'a>( + records: &mut Peekable<impl Iterator<Item = &'a GattRecord>>, +) -> Vec<GattDescriptorWithHandle> { + let mut out = vec![]; + while let Some(GattRecord { uuid, attribute_handle, permissions, .. }) = + records.next_if(|record| record.record_type == GattRecordType::Descriptor) + { + let mut att_permissions = AttPermissions::empty(); + att_permissions.set(AttPermissions::READABLE, permissions & 0x01 != 0); + att_permissions.set(AttPermissions::WRITABLE, permissions & 0x10 != 0); + + out.push(GattDescriptorWithHandle { + handle: AttHandle(*attribute_handle), + type_: *uuid, + permissions: att_permissions, + }) + } + out +} + fn records_to_service(service_records: &[GattRecord]) -> Result<GattServiceWithHandle> { let mut characteristics = vec![]; let mut service_handle_uuid = None; - for record in service_records { + let mut service_records = service_records.iter().peekable(); + + while let Some(record) = service_records.next() { match record.record_type { GattRecordType::PrimaryService => { if service_handle_uuid.is_some() { @@ -274,11 +319,17 @@ fn records_to_service(service_records: &[GattRecord]) -> Result<GattServiceWithH } service_handle_uuid = Some((record.attribute_handle, record.uuid)); } - GattRecordType::Characteristic => characteristics.push(GattCharacteristicWithHandle { - handle: AttHandle(record.attribute_handle), - type_: record.uuid, - permissions: AttPermissions::from_bits_truncate(record.properties), - }), + GattRecordType::Characteristic => { + characteristics.push(GattCharacteristicWithHandle { + handle: AttHandle(record.attribute_handle), + type_: record.uuid, + permissions: AttPermissions::from_bits_truncate(record.properties), + descriptors: consume_descriptors(&mut service_records), + }); + } + GattRecordType::Descriptor => { + bail!("Got unexpected descriptor outside of characteristic declaration") + } _ => { warn!("ignoring unsupported database entry of type {:?}", record.record_type) } @@ -424,7 +475,10 @@ mod test { const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(2); const CHARACTERISTIC_UUID: Uuid = Uuid::new(0x5678); - const ANOTHER_CHARACTERISTIC_HANDLE: AttHandle = AttHandle(3); + const DESCRIPTOR_UUID: Uuid = Uuid::new(0x4321); + const ANOTHER_DESCRIPTOR_UUID: Uuid = Uuid::new(0x5432); + + const ANOTHER_CHARACTERISTIC_HANDLE: AttHandle = AttHandle(10); const ANOTHER_CHARACTERISTIC_UUID: Uuid = Uuid::new(0x9ABC); fn make_service_record(uuid: Uuid, handle: AttHandle) -> GattRecord { @@ -449,6 +503,17 @@ mod test { } } + fn make_descriptor_record(uuid: Uuid, handle: AttHandle, permissions: u16) -> GattRecord { + GattRecord { + uuid, + record_type: GattRecordType::Descriptor, + attribute_handle: handle.0, + properties: 0, + extended_properties: 0, + permissions, + } + } + #[test] fn test_empty_records() { let res = records_to_service(&[]); @@ -546,4 +611,67 @@ mod test { AttPermissions::READABLE | AttPermissions::WRITABLE ); } + + #[test] + fn test_multiple_descriptors() { + let service = records_to_service(&[ + make_service_record(SERVICE_UUID, AttHandle(1)), + make_characteristic_record(CHARACTERISTIC_UUID, AttHandle(2), 0), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(3), 0), + make_descriptor_record(ANOTHER_DESCRIPTOR_UUID, AttHandle(4), 0), + ]) + .unwrap(); + + assert_eq!(service.characteristics[0].descriptors.len(), 2); + assert_eq!(service.characteristics[0].descriptors[0].handle, AttHandle(3)); + assert_eq!(service.characteristics[0].descriptors[0].type_, DESCRIPTOR_UUID); + assert_eq!(service.characteristics[0].descriptors[1].handle, AttHandle(4)); + assert_eq!(service.characteristics[0].descriptors[1].type_, ANOTHER_DESCRIPTOR_UUID); + } + + #[test] + fn test_descriptor_permissions() { + let service = records_to_service(&[ + make_service_record(SERVICE_UUID, AttHandle(1)), + make_characteristic_record(CHARACTERISTIC_UUID, AttHandle(2), 0), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(3), 0x01), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(4), 0x10), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(5), 0x11), + ]) + .unwrap(); + + assert_eq!(service.characteristics[0].descriptors[0].permissions, AttPermissions::READABLE); + assert_eq!(service.characteristics[0].descriptors[1].permissions, AttPermissions::WRITABLE); + assert_eq!( + service.characteristics[0].descriptors[2].permissions, + AttPermissions::READABLE | AttPermissions::WRITABLE + ); + } + + #[test] + fn test_descriptors_multiple_characteristics() { + let service = records_to_service(&[ + make_service_record(SERVICE_UUID, AttHandle(1)), + make_characteristic_record(CHARACTERISTIC_UUID, AttHandle(2), 0), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(3), 0), + make_characteristic_record(CHARACTERISTIC_UUID, AttHandle(4), 0), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(5), 0), + ]) + .unwrap(); + + assert_eq!(service.characteristics[0].descriptors.len(), 1); + assert_eq!(service.characteristics[0].descriptors[0].handle, AttHandle(3)); + assert_eq!(service.characteristics[1].descriptors.len(), 1); + assert_eq!(service.characteristics[1].descriptors[0].handle, AttHandle(5)); + } + + #[test] + fn test_unexpected_descriptor() { + let res = records_to_service(&[ + make_service_record(SERVICE_UUID, AttHandle(1)), + make_descriptor_record(DESCRIPTOR_UUID, AttHandle(3), 0), + ]); + + assert!(res.is_err()); + } } diff --git a/system/rust/src/gatt/ffi/gatt_shim.cc b/system/rust/src/gatt/ffi/gatt_shim.cc index 7949195f04..3957188844 100644 --- a/system/rust/src/gatt/ffi/gatt_shim.cc +++ b/system/rust/src/gatt/ffi/gatt_shim.cc @@ -50,11 +50,10 @@ std::optional<RawAddress> AddressOfConnection(uint16_t conn_id) { namespace bluetooth { namespace gatt { -void GattServerCallbacks::OnServerReadCharacteristic(uint16_t conn_id, - uint32_t trans_id, - uint16_t attr_handle, - uint32_t offset, - bool is_long) const { +void GattServerCallbacks::OnServerRead(uint16_t conn_id, uint32_t trans_id, + uint16_t attr_handle, + AttributeBackingType attr_type, + uint32_t offset, bool is_long) const { auto addr = AddressOfConnection(conn_id); if (!addr.has_value()) { LOG_WARN( @@ -63,16 +62,28 @@ void GattServerCallbacks::OnServerReadCharacteristic(uint16_t conn_id, return; } - do_in_jni_thread( - FROM_HERE, - base::Bind(callbacks.request_read_characteristic_cb, conn_id, trans_id, - addr.value(), attr_handle, offset, is_long)); + switch (attr_type) { + case AttributeBackingType::CHARACTERISTIC: + do_in_jni_thread( + FROM_HERE, + base::Bind(callbacks.request_read_characteristic_cb, conn_id, + trans_id, addr.value(), attr_handle, offset, is_long)); + break; + case AttributeBackingType::DESCRIPTOR: + do_in_jni_thread( + FROM_HERE, + base::Bind(callbacks.request_read_descriptor_cb, conn_id, trans_id, + addr.value(), attr_handle, offset, is_long)); + break; + default: + LOG_ALWAYS_FATAL("Unexpected backing type %d", attr_type); + } } -void GattServerCallbacks::OnServerWriteCharacteristic( - uint16_t conn_id, uint32_t trans_id, uint16_t attr_handle, uint32_t offset, - bool need_response, bool is_prepare, - ::rust::Slice<const uint8_t> value) const { +void GattServerCallbacks::OnServerWrite( + uint16_t conn_id, uint32_t trans_id, uint16_t attr_handle, + AttributeBackingType attr_type, uint32_t offset, bool need_response, + bool is_prepare, ::rust::Slice<const uint8_t> value) const { auto addr = AddressOfConnection(conn_id); if (!addr.has_value()) { LOG_WARN( @@ -84,11 +95,24 @@ void GattServerCallbacks::OnServerWriteCharacteristic( auto buf = new uint8_t[value.size()]; std::copy(value.begin(), value.end(), buf); - do_in_jni_thread( - FROM_HERE, - base::Bind(callbacks.request_write_characteristic_cb, conn_id, trans_id, - addr.value(), attr_handle, offset, need_response, is_prepare, - base::Owned(buf), value.size())); + switch (attr_type) { + case AttributeBackingType::CHARACTERISTIC: + do_in_jni_thread( + FROM_HERE, + base::Bind(callbacks.request_write_characteristic_cb, conn_id, + trans_id, addr.value(), attr_handle, offset, need_response, + is_prepare, base::Owned(buf), value.size())); + break; + case AttributeBackingType::DESCRIPTOR: + do_in_jni_thread( + FROM_HERE, + base::Bind(callbacks.request_write_descriptor_cb, conn_id, trans_id, + addr.value(), attr_handle, offset, need_response, + is_prepare, base::Owned(buf), value.size())); + break; + default: + LOG_ALWAYS_FATAL("Unexpected backing type %hhu", attr_type); + } } void GattServerCallbacks::OnIndicationSentConfirmation(uint16_t conn_id, diff --git a/system/rust/src/gatt/ffi/gatt_shim.h b/system/rust/src/gatt/ffi/gatt_shim.h index 1457cc3a03..5c42ee2c02 100644 --- a/system/rust/src/gatt/ffi/gatt_shim.h +++ b/system/rust/src/gatt/ffi/gatt_shim.h @@ -25,19 +25,28 @@ namespace bluetooth { namespace gatt { +/// The GATT entity backing the value of a user-controlled +/// attribute +enum class AttributeBackingType { + /// A GATT characteristic + CHARACTERISTIC, + /// A GATT descriptor + DESCRIPTOR, +}; + class GattServerCallbacks { public: GattServerCallbacks(const btgatt_server_callbacks_t& callbacks) : callbacks(callbacks){}; - void OnServerReadCharacteristic(uint16_t conn_id, uint32_t trans_id, - uint16_t attr_handle, uint32_t offset, - bool is_long) const; + void OnServerRead(uint16_t conn_id, uint32_t trans_id, uint16_t attr_handle, + AttributeBackingType attr_type, uint32_t offset, + bool is_long) const; - void OnServerWriteCharacteristic(uint16_t conn_id, uint32_t trans_id, - uint16_t attr_handle, uint32_t offset, - bool need_response, bool is_prepare, - ::rust::Slice<const uint8_t> value) const; + void OnServerWrite(uint16_t conn_id, uint32_t trans_id, uint16_t attr_handle, + AttributeBackingType attr_type, uint32_t offset, + bool need_response, bool is_prepare, + ::rust::Slice<const uint8_t> value) const; void OnIndicationSentConfirmation(uint16_t conn_id, int status) const; diff --git a/system/rust/src/gatt/mocks/mock_callbacks.rs b/system/rust/src/gatt/mocks/mock_callbacks.rs index efc44063c8..3c05ad2c9c 100644 --- a/system/rust/src/gatt/mocks/mock_callbacks.rs +++ b/system/rust/src/gatt/mocks/mock_callbacks.rs @@ -2,6 +2,7 @@ use crate::{ gatt::{ + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId, TransactionId}, server::IndicationError, GattCallbacks, @@ -25,12 +26,13 @@ impl MockCallbacks { #[derive(Debug)] pub enum MockCallbackEvents { /// GattCallbacks#on_server_read_characteristic invoked - OnServerReadCharacteristic(ConnectionId, TransactionId, AttHandle, u32, bool), + OnServerRead(ConnectionId, TransactionId, AttHandle, AttributeBackingType, u32, bool), /// GattCallbacks#on_server_write_characteristic invoked - OnServerWriteCharacteristic( + OnServerWrite( ConnectionId, TransactionId, AttHandle, + AttributeBackingType, u32, bool, bool, @@ -41,36 +43,39 @@ pub enum MockCallbackEvents { } impl GattCallbacks for MockCallbacks { - fn on_server_read_characteristic( + fn on_server_read( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, is_long: bool, ) { self.0 - .send(MockCallbackEvents::OnServerReadCharacteristic( - conn_id, trans_id, handle, offset, is_long, + .send(MockCallbackEvents::OnServerRead( + conn_id, trans_id, handle, attr_type, offset, is_long, )) .unwrap(); } - fn on_server_write_characteristic( + fn on_server_write( &self, conn_id: ConnectionId, trans_id: TransactionId, handle: AttHandle, + attr_type: AttributeBackingType, offset: u32, need_response: bool, is_prepare: bool, value: AttAttributeDataView, ) { self.0 - .send(MockCallbackEvents::OnServerWriteCharacteristic( + .send(MockCallbackEvents::OnServerWrite( conn_id, trans_id, handle, + attr_type, offset, need_response, is_prepare, diff --git a/system/rust/src/gatt/mocks/mock_datastore.rs b/system/rust/src/gatt/mocks/mock_datastore.rs index 7e7eac2465..6a1fc27dd5 100644 --- a/system/rust/src/gatt/mocks/mock_datastore.rs +++ b/system/rust/src/gatt/mocks/mock_datastore.rs @@ -3,6 +3,7 @@ use crate::{ gatt::{ callbacks::GattDatastore, + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId}, }, packets::{ @@ -37,16 +38,18 @@ pub enum MockDatastoreEvents { RemoveConnection(ConnectionId), /// A characteristic was read on a given handle. The oneshot is used to /// return the value read. - ReadCharacteristic( + Read( ConnectionId, AttHandle, + AttributeBackingType, oneshot::Sender<Result<AttAttributeDataChild, AttErrorCode>>, ), /// A characteristic was written to on a given handle. The oneshot is used /// to return whether the write succeeded. - WriteCharacteristic( + Write( ConnectionId, AttHandle, + AttributeBackingType, OwnedAttAttributeDataView, oneshot::Sender<Result<(), AttErrorCode>>, ), @@ -62,29 +65,32 @@ impl GattDatastore for MockDatastore { self.0.send(MockDatastoreEvents::RemoveConnection(conn_id)).unwrap(); } - async fn read_characteristic( + async fn read( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, ) -> Result<AttAttributeDataChild, AttErrorCode> { let (tx, rx) = oneshot::channel(); - self.0.send(MockDatastoreEvents::ReadCharacteristic(conn_id, handle, tx)).unwrap(); + self.0.send(MockDatastoreEvents::Read(conn_id, handle, attr_type, tx)).unwrap(); let resp = rx.await.unwrap(); info!("sending {resp:?} down from upper tester"); resp } - async fn write_characteristic( + async fn write( &self, conn_id: ConnectionId, handle: AttHandle, + attr_type: AttributeBackingType, data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode> { let (tx, rx) = oneshot::channel(); self.0 - .send(MockDatastoreEvents::WriteCharacteristic( + .send(MockDatastoreEvents::Write( conn_id, handle, + attr_type, data.to_owned_packet(), tx, )) diff --git a/system/rust/src/gatt/server/att_server_bearer.rs b/system/rust/src/gatt/server/att_server_bearer.rs index 02ade701a2..4534cc54a7 100644 --- a/system/rust/src/gatt/server/att_server_bearer.rs +++ b/system/rust/src/gatt/server/att_server_bearer.rs @@ -203,6 +203,7 @@ mod test { core::{shared_box::SharedBox, uuid::Uuid}, gatt::{ callbacks::GattDatastore, + ffi::AttributeBackingType, ids::ConnectionId, mocks::mock_datastore::{MockDatastore, MockDatastoreEvents}, server::{ @@ -314,11 +315,13 @@ mod test { handle: VALID_HANDLE, type_: Uuid::new(2), permissions: AttPermissions::READABLE, + descriptors: vec![], }, GattCharacteristicWithHandle { handle: ANOTHER_VALID_HANDLE, type_: Uuid::new(2), permissions: AttPermissions::READABLE, + descriptors: vec![], }, ], }) @@ -344,7 +347,7 @@ mod test { }); conn.as_ref().handle_packet(req2.view()); // handle first reply - let MockDatastoreEvents::ReadCharacteristic(CONN_ID, VALID_HANDLE, data_resp) = + let MockDatastoreEvents::Read(CONN_ID, VALID_HANDLE, AttributeBackingType::Characteristic, data_resp) = data_rx.recv().await.unwrap() else { unreachable!(); }; diff --git a/system/rust/src/gatt/server/gatt_database.rs b/system/rust/src/gatt/server/gatt_database.rs index 619672a20b..f1f448d12e 100644 --- a/system/rust/src/gatt/server/gatt_database.rs +++ b/system/rust/src/gatt/server/gatt_database.rs @@ -4,8 +4,9 @@ use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use async_trait::async_trait; +use log::error; use crate::{ core::{ @@ -14,6 +15,7 @@ use crate::{ }, gatt::{ callbacks::GattDatastore, + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId}, }, packets::{ @@ -55,6 +57,22 @@ pub struct GattCharacteristicWithHandle { 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 @@ -67,14 +85,14 @@ pub struct GattDatabase<T: ?Sized> { #[derive(Default)] struct GattDatabaseSchema { - services: Vec<GattServiceWithHandle>, attributes: BTreeMap<AttHandle, AttAttributeWithBackingValue>, } #[derive(Clone)] enum AttAttributeBackingValue { Static(AttAttributeDataChild), - Dynamic, + DynamicCharacteristic, + DynamicDescriptor, } #[derive(Clone)] @@ -120,11 +138,7 @@ impl<T: GattDatastore + ?Sized> GattDatabase<T> { // characteristics for characteristic in service.characteristics { - characteristics.push(GattCharacteristicWithHandle { - handle: characteristic.handle, - type_: characteristic.type_, - permissions: characteristic.permissions, - }); + characteristics.push(characteristic.clone()); // declaration // Recall that we assume the declaration handle is one less than the value @@ -163,8 +177,20 @@ impl<T: GattDatastore + ?Sized> GattDatabase<T> { type_: characteristic.type_, permissions: characteristic.permissions, }, - AttAttributeBackingValue::Dynamic, + AttAttributeBackingValue::DynamicCharacteristic, ); + + // descriptors + for descriptor in characteristic.descriptors { + add_attribute( + AttAttribute { + handle: descriptor.handle, + type_: descriptor.type_, + permissions: descriptor.permissions, + }, + AttAttributeBackingValue::DynamicDescriptor, + ); + } } // validate attributes for overlap @@ -180,9 +206,6 @@ impl<T: GattDatastore + ?Sized> GattDatabase<T> { } // if we made it here, we successfully loaded the new service - let service = - GattServiceWithHandle { handle: service.handle, type_: service.type_, characteristics }; - static_data.services.push(service); static_data.attributes.extend(attributes.into_iter()); Ok(()) } @@ -191,16 +214,6 @@ impl<T: GattDatastore + ?Sized> GattDatabase<T> { pub fn remove_service_at_handle(&self, service_handle: AttHandle) -> Result<()> { let mut static_data = self.schema.borrow_mut(); - // remove old service - static_data - .services - .iter() - .position(|service| service.handle == service_handle) - .map(|index| static_data.services.remove(index)) - .ok_or_else(|| { - anyhow!("service at handle {service_handle:?} not found, cannot remove") - })?; - // find next service let next_service_handle = static_data .attributes @@ -244,7 +257,7 @@ where &self, handle: AttHandle, ) -> Result<AttAttributeDataChild, AttErrorCode> { - let value = self.gatt_db.with(|gatt_db| { + let (value, datastore) = self.gatt_db.with(|gatt_db| { let Some(gatt_db) = gatt_db else { // db must have been closed return Err(AttErrorCode::INVALID_HANDLE); @@ -256,21 +269,16 @@ where if !attr.attribute.permissions.readable() { return Err(AttErrorCode::READ_NOT_PERMITTED); } - Ok(attr.value.clone()) + Ok((attr.value.clone(), gatt_db.datastore.clone())) })?; match value { AttAttributeBackingValue::Static(val) => return Ok(val), - AttAttributeBackingValue::Dynamic => { - self.gatt_db - .with(|gatt_db| { - gatt_db - .expect("no await points since last access, must be non-null") - .datastore - .clone() - }) - .read_characteristic(self.conn_id, handle) - .await + AttAttributeBackingValue::DynamicCharacteristic => { + datastore.read(self.conn_id, handle, AttributeBackingType::Characteristic).await + } + AttAttributeBackingValue::DynamicDescriptor => { + datastore.read(self.conn_id, handle, AttributeBackingType::Descriptor).await } } } @@ -280,7 +288,7 @@ where handle: AttHandle, data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode> { - let datastore = self.gatt_db.with(|gatt_db| { + let (value, datastore) = self.gatt_db.with(|gatt_db| { let Some(gatt_db) = gatt_db else { // db must have been closed return Err(AttErrorCode::INVALID_HANDLE); @@ -292,16 +300,29 @@ where if !attr.attribute.permissions.writable() { return Err(AttErrorCode::WRITE_NOT_PERMITTED); } - Ok(gatt_db.datastore.clone()) + Ok((attr.value.clone(), gatt_db.datastore.clone())) })?; - datastore.write_characteristic(self.conn_id, handle, data).await + 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 + .write(self.conn_id, handle, AttributeBackingType::Characteristic, data) + .await + } + AttAttributeBackingValue::DynamicDescriptor => { + datastore.write(self.conn_id, handle, AttributeBackingType::Descriptor, data).await + } + } } 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(vec![]) + .unwrap_or_default() }) } } @@ -334,6 +355,9 @@ mod test { 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 CONN_ID: ConnectionId = ConnectionId(1); #[test] @@ -393,6 +417,7 @@ mod test { handle: AttHandle(3), type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE, + descriptors: vec![], }], }) .unwrap(); @@ -404,6 +429,7 @@ mod test { handle: AttHandle(6), type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE, + descriptors: vec![], }], }) .unwrap(); @@ -415,6 +441,7 @@ mod test { handle: AttHandle(9), type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE, + descriptors: vec![], }], }) .unwrap(); @@ -461,6 +488,7 @@ mod test { permissions: AttPermissions::READABLE | AttPermissions::WRITABLE | AttPermissions::INDICATE, + descriptors: vec![], }], }) .unwrap(); @@ -525,6 +553,7 @@ mod test { handle: CHARACTERISTIC_VALUE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE, + descriptors: vec![], }], }) .unwrap(); @@ -535,9 +564,10 @@ mod test { let characteristic_value = tokio_test::block_on(async { join!( async { - let MockDatastoreEvents::ReadCharacteristic( + let MockDatastoreEvents::Read( CONN_ID, CHARACTERISTIC_VALUE_HANDLE, + AttributeBackingType::Characteristic, reply, ) = data_evts.recv().await.unwrap() else { unreachable!() @@ -565,6 +595,7 @@ mod test { handle: CHARACTERISTIC_VALUE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::empty(), + descriptors: vec![], }], }) .unwrap(); @@ -588,6 +619,7 @@ mod test { handle: SERVICE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::WRITABLE, + descriptors: vec![], }], }); @@ -629,6 +661,7 @@ mod test { handle: CHARACTERISTIC_VALUE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::WRITABLE, + descriptors: vec![], }], }) .unwrap(); @@ -647,9 +680,10 @@ mod test { .unwrap(); }); - let MockDatastoreEvents::WriteCharacteristic( + let MockDatastoreEvents::Write( CONN_ID, CHARACTERISTIC_VALUE_HANDLE, + AttributeBackingType::Characteristic, recv_data, _, ) = data_evts.recv().await.unwrap() else { @@ -678,6 +712,7 @@ mod test { handle: CHARACTERISTIC_VALUE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::WRITABLE, + descriptors: vec![], }], }) .unwrap(); @@ -689,7 +724,7 @@ mod test { let res = tokio_test::block_on(async { join!( async { - let MockDatastoreEvents::WriteCharacteristic(_,_,_,reply) = data_evts.recv().await.unwrap() else { + let MockDatastoreEvents::Write(_,_,_,_,reply) = data_evts.recv().await.unwrap() else { unreachable!(); }; reply.send(Err(AttErrorCode::UNLIKELY_ERROR)).unwrap(); @@ -715,6 +750,7 @@ mod test { handle: CHARACTERISTIC_VALUE_HANDLE, type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE, + descriptors: vec![], }], }) .unwrap(); @@ -729,4 +765,159 @@ mod test { 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_datastore.into())); + 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, + }], + }], + }) + .unwrap(); + let att_db = gatt_db.get_att_database(CONN_ID); + 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( + CONN_ID, + 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_datastore.into())); + 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, + }], + }], + }) + .unwrap(); + let att_db = gatt_db.get_att_database(CONN_ID); + 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( + CONN_ID, + 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_datastore.into())); + 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, + }, + GattDescriptorWithHandle { + handle: AttHandle(8), + type_: DESCRIPTOR_TYPE, + permissions: AttPermissions::READABLE | AttPermissions::WRITABLE, + }, + ], + }, + ], + }) + .unwrap(); + + // act: get the attributes + let attributes = gatt_db.get_att_database(CONN_ID).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); + assert_eq!(attributes[7].permissions, AttPermissions::READABLE | AttPermissions::WRITABLE); + } } diff --git a/system/rust/src/gatt/server/transactions/helpers/payload_accumulator.rs b/system/rust/src/gatt/server/transactions/helpers/payload_accumulator.rs index 40a30f0de5..72aa46b367 100644 --- a/system/rust/src/gatt/server/transactions/helpers/payload_accumulator.rs +++ b/system/rust/src/gatt/server/transactions/helpers/payload_accumulator.rs @@ -11,6 +11,7 @@ impl<T: Builder> PayloadAccumulator<T> { Self { curr: 0, lim: size * 8, elems: vec![] } } + #[must_use] pub fn push(&mut self, builder: T) -> bool { // if serialization fails we WANT to continue, to get a clean SerializeError at // the end @@ -47,11 +48,12 @@ mod test { fn test_nonempty() { let mut accumulator = PayloadAccumulator::new(128); - accumulator.push(AttBuilder { + let ok = accumulator.push(AttBuilder { opcode: AttOpcode::WRITE_RESPONSE, _child_: AttChild::RawData([1, 2].into()), }); + assert!(ok); assert!(!accumulator.is_empty()) } @@ -59,11 +61,12 @@ mod test { fn test_push_serialize() { let mut accumulator = PayloadAccumulator::new(128); - accumulator.push(AttBuilder { + let ok = accumulator.push(AttBuilder { opcode: AttOpcode::WRITE_RESPONSE, _child_: AttChild::RawData([1, 2].into()), }); + assert!(ok); assert_eq!( accumulator.into_boxed_slice().as_ref(), [AttBuilder { diff --git a/system/rust/tests/gatt_callbacks_test.rs b/system/rust/tests/gatt_callbacks_test.rs index 0fde0db941..60e0b6c61b 100644 --- a/system/rust/tests/gatt_callbacks_test.rs +++ b/system/rust/tests/gatt_callbacks_test.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use bluetooth_core::{ gatt::{ callbacks::{CallbackResponseError, CallbackTransactionManager, GattDatastore}, + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId, ServerId, TransactionId, TransportIndex}, mocks::mock_callbacks::{MockCallbackEvents, MockCallbacks}, }, @@ -22,6 +23,7 @@ const CONN_ID: ConnectionId = ConnectionId::new(TCB_IDX, SERVER_ID); const ANOTHER_CONN_ID: ConnectionId = ConnectionId(10); const HANDLE_1: AttHandle = AttHandle(3); +const BACKING_TYPE: AttributeBackingType = AttributeBackingType::Descriptor; fn initialize_manager_with_connection( ) -> (Rc<CallbackTransactionManager>, UnboundedReceiver<MockCallbackEvents>) { @@ -34,8 +36,8 @@ fn initialize_manager_with_connection( async fn pull_trans_id(events_rx: &mut UnboundedReceiver<MockCallbackEvents>) -> TransactionId { match events_rx.recv().await.unwrap() { - MockCallbackEvents::OnServerReadCharacteristic(_, trans_id, _, _, _) => trans_id, - MockCallbackEvents::OnServerWriteCharacteristic(_, trans_id, _, _, _, _, _) => trans_id, + MockCallbackEvents::OnServerRead(_, trans_id, _, _, _, _) => trans_id, + MockCallbackEvents::OnServerWrite(_, trans_id, _, _, _, _, _, _) => trans_id, _ => unimplemented!(), } } @@ -47,11 +49,11 @@ fn test_read_characteristic_callback() { let (callback_manager, mut callbacks_rx) = initialize_manager_with_connection(); // act: start read operation - spawn_local(async move { callback_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { callback_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // assert: verify the read callback is received - let MockCallbackEvents::OnServerReadCharacteristic( - CONN_ID, _, HANDLE_1, 0, false, + let MockCallbackEvents::OnServerRead( + CONN_ID, _, HANDLE_1, BACKING_TYPE, 0, false, ) = callbacks_rx.recv().await.unwrap() else { unreachable!() }; @@ -68,7 +70,7 @@ fn test_read_characteristic_response() { // act: start read operation let cloned_manager = callback_manager.clone(); let pending_read = - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // provide a response let trans_id = pull_trans_id(&mut callbacks_rx).await; callback_manager.send_response(CONN_ID, trans_id, data.clone()).unwrap(); @@ -89,7 +91,7 @@ fn test_sequential_reads() { // act: start read operation let cloned_manager = callback_manager.clone(); let pending_read_1 = - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // respond to first let trans_id = pull_trans_id(&mut callbacks_rx).await; callback_manager.send_response(CONN_ID, trans_id, data1.clone()).unwrap(); @@ -97,7 +99,7 @@ fn test_sequential_reads() { // do a second read operation let cloned_manager = callback_manager.clone(); let pending_read_2 = - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // respond to second let trans_id = pull_trans_id(&mut callbacks_rx).await; callback_manager.send_response(CONN_ID, trans_id, data2.clone()).unwrap(); @@ -119,12 +121,12 @@ fn test_concurrent_reads() { // act: start read operation let cloned_manager = callback_manager.clone(); let pending_read_1 = - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // do a second read operation let cloned_manager = callback_manager.clone(); let pending_read_2 = - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // respond to first let trans_id = pull_trans_id(&mut callbacks_rx).await; @@ -148,9 +150,9 @@ fn test_distinct_transaction_ids() { // act: start two read operations concurrently let cloned_manager = callback_manager.clone(); - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); let cloned_manager = callback_manager.clone(); - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // pull both trans_ids let trans_id_1 = pull_trans_id(&mut callbacks_rx).await; @@ -170,7 +172,7 @@ fn test_invalid_conn_id() { // act: start a read operation let cloned_manager = callback_manager.clone(); - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // respond with the correct trans_id but an invalid conn_id let trans_id = pull_trans_id(&mut callbacks_rx).await; let err = callback_manager.send_response(ANOTHER_CONN_ID, trans_id, data).unwrap_err(); @@ -189,7 +191,7 @@ fn test_invalid_trans_id() { // act: start a read operation let cloned_manager = callback_manager.clone(); - spawn_local(async move { cloned_manager.read_characteristic(CONN_ID, HANDLE_1).await }); + spawn_local(async move { cloned_manager.read(CONN_ID, HANDLE_1, BACKING_TYPE).await }); // respond with the correct conn_id but an invalid trans_id let trans_id = pull_trans_id(&mut callbacks_rx).await; let invalid_trans_id = TransactionId(trans_id.0 + 1); @@ -211,12 +213,12 @@ fn test_write_characteristic_callback() { build_view_or_crash(build_att_data(AttAttributeDataChild::RawData([1, 2].into()))); let cloned_data = data.view().to_owned_packet(); spawn_local(async move { - callback_manager.write_characteristic(CONN_ID, HANDLE_1, cloned_data.view()).await + callback_manager.write(CONN_ID, HANDLE_1, BACKING_TYPE, cloned_data.view()).await }); // assert: verify the write callback is received - let MockCallbackEvents::OnServerWriteCharacteristic( - CONN_ID, _, HANDLE_1, 0, /* needs_response = */ true, false, recv_data + let MockCallbackEvents::OnServerWrite( + CONN_ID, _, HANDLE_1, BACKING_TYPE, 0, /* needs_response = */ true, false, recv_data ) = callbacks_rx.recv().await.unwrap() else { unreachable!() }; @@ -238,7 +240,7 @@ fn test_write_characteristic_response() { build_view_or_crash(build_att_data(AttAttributeDataChild::RawData([1, 2].into()))); let cloned_manager = callback_manager.clone(); let pending_write = spawn_local(async move { - cloned_manager.write_characteristic(CONN_ID, HANDLE_1, data.view()).await + cloned_manager.write(CONN_ID, HANDLE_1, BACKING_TYPE, data.view()).await }); // provide a response with some error code let trans_id = pull_trans_id(&mut callbacks_rx).await; diff --git a/system/rust/tests/gatt_server_test.rs b/system/rust/tests/gatt_server_test.rs index eafeec569e..2677e9d1de 100644 --- a/system/rust/tests/gatt_server_test.rs +++ b/system/rust/tests/gatt_server_test.rs @@ -4,13 +4,17 @@ use bluetooth_core::{ core::uuid::Uuid, gatt::{ self, + ffi::AttributeBackingType, ids::{AttHandle, ConnectionId, ServerId, TransportIndex}, mocks::{ mock_datastore::{MockDatastore, MockDatastoreEvents}, mock_transport::MockAttTransport, }, server::{ - gatt_database::{AttPermissions, GattCharacteristicWithHandle, GattServiceWithHandle}, + gatt_database::{ + AttPermissions, GattCharacteristicWithHandle, GattDescriptorWithHandle, + GattServiceWithHandle, + }, GattModule, IndicationError, }, }, @@ -31,10 +35,15 @@ mod utils; const TCB_IDX: TransportIndex = TransportIndex(1); const SERVER_ID: ServerId = ServerId(2); const CONN_ID: ConnectionId = ConnectionId::new(TCB_IDX, SERVER_ID); -const HANDLE_1: AttHandle = AttHandle(3); -const HANDLE_2: AttHandle = AttHandle(5); -const UUID_1: Uuid = Uuid::new(0x0102); -const UUID_2: Uuid = Uuid::new(0x0103); + +const SERVICE_HANDLE: AttHandle = AttHandle(3); +const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(5); +const DESCRIPTOR_HANDLE: AttHandle = AttHandle(6); + +const SERVICE_TYPE: Uuid = Uuid::new(0x0102); +const CHARACTERISTIC_TYPE: Uuid = Uuid::new(0x0103); +const DESCRIPTOR_TYPE: Uuid = Uuid::new(0x0104); + const DATA: [u8; 4] = [1, 2, 3, 4]; fn start_gatt_module() -> ( @@ -54,14 +63,19 @@ fn create_server_and_open_connection(gatt: &mut GattModule) { gatt.register_gatt_service( SERVER_ID, GattServiceWithHandle { - handle: HANDLE_1, - type_: UUID_1, + handle: SERVICE_HANDLE, + type_: SERVICE_TYPE, characteristics: vec![GattCharacteristicWithHandle { - handle: HANDLE_2, - type_: UUID_2, + handle: CHARACTERISTIC_HANDLE, + type_: CHARACTERISTIC_TYPE, permissions: AttPermissions::READABLE | AttPermissions::WRITABLE | AttPermissions::INDICATE, + descriptors: vec![GattDescriptorWithHandle { + handle: DESCRIPTOR_HANDLE, + type_: DESCRIPTOR_TYPE, + permissions: AttPermissions::READABLE | AttPermissions::WRITABLE, + }], }], }, ) @@ -78,7 +92,11 @@ fn test_connection_creation() { gatt.open_gatt_server(SERVER_ID).unwrap(); gatt.register_gatt_service( SERVER_ID, - GattServiceWithHandle { handle: HANDLE_1, type_: UUID_1, characteristics: vec![] }, + GattServiceWithHandle { + handle: SERVICE_HANDLE, + type_: SERVICE_TYPE, + characteristics: vec![], + }, ) .unwrap(); @@ -101,7 +119,11 @@ fn test_disconnection() { gatt.open_gatt_server(SERVER_ID).unwrap(); gatt.register_gatt_service( SERVER_ID, - GattServiceWithHandle { handle: HANDLE_1, type_: UUID_1, characteristics: vec![] }, + GattServiceWithHandle { + handle: SERVICE_HANDLE, + type_: SERVICE_TYPE, + characteristics: vec![], + }, ) .unwrap(); gatt.on_le_connect(CONN_ID).unwrap(); @@ -129,8 +151,10 @@ fn test_service_read() { // act gatt.get_bearer(CONN_ID).unwrap().handle_packet( - build_att_view_or_crash(AttReadRequestBuilder { attribute_handle: HANDLE_1.into() }) - .view(), + build_att_view_or_crash(AttReadRequestBuilder { + attribute_handle: SERVICE_HANDLE.into(), + }) + .view(), ); let (tcb_idx, resp) = transport_rx.recv().await.unwrap(); @@ -142,7 +166,7 @@ fn test_service_read() { opcode: AttOpcode::READ_RESPONSE, _child_: AttReadResponseBuilder { value: build_att_data(GattServiceDeclarationValueBuilder { - uuid: UUID_1.into() + uuid: SERVICE_TYPE.into() }) } .into() @@ -165,8 +189,10 @@ fn test_server_closed_while_connected() { // act: read from the closed server gatt.get_bearer(CONN_ID).unwrap().handle_packet( - build_att_view_or_crash(AttReadRequestBuilder { attribute_handle: HANDLE_1.into() }) - .view(), + build_att_view_or_crash(AttReadRequestBuilder { + attribute_handle: SERVICE_HANDLE.into(), + }) + .view(), ); let (_, resp) = transport_rx.recv().await.unwrap(); @@ -176,7 +202,7 @@ fn test_server_closed_while_connected() { resp._child_, AttErrorResponseBuilder { opcode_in_error: AttOpcode::READ_REQUEST, - handle_in_error: HANDLE_1.into(), + handle_in_error: SERVICE_HANDLE.into(), error_code: AttErrorCode::INVALID_HANDLE } .into() @@ -197,11 +223,17 @@ fn test_characteristic_read() { // act gatt.get_bearer(CONN_ID).unwrap().handle_packet( - build_att_view_or_crash(AttReadRequestBuilder { attribute_handle: HANDLE_2.into() }) - .view(), + build_att_view_or_crash(AttReadRequestBuilder { + attribute_handle: CHARACTERISTIC_HANDLE.into(), + }) + .view(), ); - let tx = if let MockDatastoreEvents::ReadCharacteristic(CONN_ID, HANDLE_2, tx) = - data_rx.recv().await.unwrap() + let tx = if let MockDatastoreEvents::Read( + CONN_ID, + CHARACTERISTIC_HANDLE, + AttributeBackingType::Characteristic, + tx, + ) = data_rx.recv().await.unwrap() { tx } else { @@ -236,19 +268,23 @@ fn test_characteristic_write() { // act gatt.get_bearer(CONN_ID).unwrap().handle_packet( build_att_view_or_crash(AttWriteRequestBuilder { - handle: HANDLE_2.into(), + handle: CHARACTERISTIC_HANDLE.into(), value: build_att_data(data.clone()), }) .view(), ); - let (tx, written_data) = - if let MockDatastoreEvents::WriteCharacteristic(CONN_ID, HANDLE_2, written_data, tx) = - data_rx.recv().await.unwrap() - { - (tx, written_data) - } else { - unreachable!() - }; + let (tx, written_data) = if let MockDatastoreEvents::Write( + CONN_ID, + CHARACTERISTIC_HANDLE, + AttributeBackingType::Characteristic, + written_data, + tx, + ) = data_rx.recv().await.unwrap() + { + (tx, written_data) + } else { + unreachable!() + }; tx.send(Ok(())).unwrap(); let (tcb_idx, resp) = transport_rx.recv().await.unwrap(); @@ -280,8 +316,9 @@ fn test_send_indication() { data_rx.recv().await.unwrap(); // act - let pending_indication = - spawn_local(gatt.get_bearer(CONN_ID).unwrap().send_indication(HANDLE_2, data.clone())); + let pending_indication = spawn_local( + gatt.get_bearer(CONN_ID).unwrap().send_indication(CHARACTERISTIC_HANDLE, data.clone()), + ); let (tcb_idx, resp) = transport_rx.recv().await.unwrap(); @@ -297,7 +334,7 @@ fn test_send_indication() { AttBuilder { opcode: AttOpcode::HANDLE_VALUE_INDICATION, _child_: AttHandleValueIndicationBuilder { - handle: HANDLE_2.into(), + handle: CHARACTERISTIC_HANDLE.into(), value: build_att_data(data), } .into() @@ -316,11 +353,10 @@ fn test_send_indication_and_disconnect() { data_rx.recv().await.unwrap(); // act: send an indication, then disconnect - let pending_indication = spawn_local( - gatt.get_bearer(CONN_ID) - .unwrap() - .send_indication(HANDLE_2, AttAttributeDataChild::RawData([1, 2, 3, 4].into())), - ); + let pending_indication = spawn_local(gatt.get_bearer(CONN_ID).unwrap().send_indication( + CHARACTERISTIC_HANDLE, + AttAttributeDataChild::RawData([1, 2, 3, 4].into()), + )); transport_rx.recv().await.unwrap(); gatt.on_le_disconnect(CONN_ID); @@ -331,3 +367,53 @@ fn test_send_indication_and_disconnect() { )); }) } + +#[test] +fn test_write_to_descriptor() { + start_test(async move { + // arrange + let (mut gatt, mut data_rx, mut transport_rx) = start_gatt_module(); + + let data = AttAttributeDataChild::RawData(DATA.into()); + + create_server_and_open_connection(&mut gatt); + data_rx.recv().await.unwrap(); + + // act + gatt.get_bearer(CONN_ID).unwrap().handle_packet( + build_att_view_or_crash(AttWriteRequestBuilder { + handle: DESCRIPTOR_HANDLE.into(), + value: build_att_data(data.clone()), + }) + .view(), + ); + let (tx, written_data) = if let MockDatastoreEvents::Write( + CONN_ID, + DESCRIPTOR_HANDLE, + AttributeBackingType::Descriptor, + written_data, + tx, + ) = data_rx.recv().await.unwrap() + { + (tx, written_data) + } else { + unreachable!() + }; + tx.send(Ok(())).unwrap(); + let (tcb_idx, resp) = transport_rx.recv().await.unwrap(); + + // assert + assert_eq!(tcb_idx, TCB_IDX); + assert_eq!( + resp, + AttBuilder { + opcode: AttOpcode::WRITE_RESPONSE, + _child_: AttWriteResponseBuilder {}.into() + } + ); + assert_eq!( + data.to_vec().unwrap(), + written_data.view().get_raw_payload().collect::<Vec<_>>() + ) + }) +} |