Loading system/rust/src/gatt/server.rs +1 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ mod request_handler; pub mod services; mod transactions; mod command_handler; #[cfg(test)] mod test; Loading system/rust/src/gatt/server/att_database.rs +13 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,8 @@ bitflags! { pub struct AttPermissions : u8 { /// Attribute can be read using READ_REQ const READABLE = 0x02; /// Attribute can be written to using WRITE_CMD const WRITABLE_WITHOUT_RESPONSE = 0x04; /// Attribute can be written to using WRITE_REQ const WRITABLE = 0x08; /// Attribute value may be sent using indications Loading @@ -53,6 +55,10 @@ impl AttPermissions { pub fn writable(&self) -> bool { self.contains(AttPermissions::WRITABLE) } /// Attribute can be written to using WRITE_CMD pub fn writable_without_response(&self) -> bool { self.contains(AttPermissions::WRITABLE_WITHOUT_RESPONSE) } /// Attribute value may be sent using indications pub fn indicate(&self) -> bool { self.contains(AttPermissions::INDICATE) Loading @@ -74,6 +80,9 @@ pub trait AttDatabase { data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode>; /// Write to an attribute by handle fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>); /// List all the attributes in this database. /// /// Expected to return them in sorted order. Loading Loading @@ -122,6 +131,10 @@ impl AttDatabase for SnapshottedAttDatabase<'_> { self.backing.write_attribute(handle, data).await } fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>) { self.backing.write_no_response_attribute(handle, data); } fn list_attributes(&self) -> Vec<AttAttribute> { self.attributes.clone() } Loading system/rust/src/gatt/server/att_server_bearer.rs +9 −3 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ use crate::{ use super::{ att_database::AttDatabase, command_handler::AttCommandHandler, indication_handler::{ConfirmationWatcher, IndicationError, IndicationHandler}, request_handler::AttRequestHandler, }; Loading Loading @@ -59,6 +60,9 @@ pub struct AttServerBearer<T: AttDatabase> { // indication state indication_handler: SharedMutex<IndicationHandler<T>>, pending_confirmation: ConfirmationWatcher, // command handler (across all bearers) command_handler: AttCommandHandler<T>, } impl<T: AttDatabase + Clone + 'static> AttServerBearer<T> { Loading @@ -73,10 +77,12 @@ impl<T: AttDatabase + Clone + 'static> AttServerBearer<T> { send_packet: Box::new(send_packet), mtu: AttMtu::new(), curr_request: AttRequestState::Idle(AttRequestHandler::new(db)).into(), curr_request: AttRequestState::Idle(AttRequestHandler::new(db.clone())).into(), indication_handler: SharedMutex::new(indication_handler), pending_confirmation, command_handler: AttCommandHandler::new(db), } } Loading @@ -93,10 +99,10 @@ impl<T: AttDatabase + Clone + 'static> WeakBoxRef<'_, AttServerBearer<T>> { pub fn handle_packet(&self, packet: AttView<'_>) { match classify_opcode(packet.get_opcode()) { OperationType::Command => { error!("dropping ATT command (currently unsupported)"); self.command_handler.process_packet(packet); } OperationType::Request => { Self::handle_request(self, packet); self.handle_request(packet); } OperationType::Confirmation => self.pending_confirmation.on_confirmation(), OperationType::Response | OperationType::Notification | OperationType::Indication => { Loading system/rust/src/gatt/server/command_handler.rs 0 → 100644 +99 −0 Original line number Diff line number Diff line use log::warn; use crate::packets::{AttOpcode, AttView, AttWriteCommandView, Packet}; use super::att_database::AttDatabase; /// This struct handles all ATT commands. pub struct AttCommandHandler<Db: AttDatabase> { db: Db, } impl<Db: AttDatabase> AttCommandHandler<Db> { pub fn new(db: Db) -> Self { Self { db } } pub fn process_packet(&self, packet: AttView<'_>) { let snapshotted_db = self.db.snapshot(); match packet.get_opcode() { AttOpcode::WRITE_COMMAND => { let Ok(packet) = AttWriteCommandView::try_parse(packet) else { warn!("failed to parse WRITE_COMMAND packet"); return; }; snapshotted_db .write_no_response_attribute(packet.get_handle().into(), packet.get_value()); } _ => { warn!("Dropping unsupported opcode {:?}", packet.get_opcode()); } } } } #[cfg(test)] mod test { use crate::{ core::uuid::Uuid, gatt::{ ids::AttHandle, server::{ att_database::{AttAttribute, AttDatabase}, command_handler::AttCommandHandler, gatt_database::AttPermissions, test::test_att_db::TestAttDatabase, }, }, packets::{ AttAttributeDataChild, AttErrorCode, AttErrorResponseBuilder, AttOpcode, AttWriteCommandBuilder, }, utils::{ packet::{build_att_data, build_att_view_or_crash}, task::block_on_locally, }, }; #[test] fn test_write_command() { // arrange let db = TestAttDatabase::new(vec![( AttAttribute { handle: AttHandle(3), type_: Uuid::new(0x1234), permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITHOUT_RESPONSE, }, vec![1, 2, 3], )]); let handler = AttCommandHandler { db: db.clone() }; let data = AttAttributeDataChild::RawData([1, 2].into()); // act: send write command let att_view = build_att_view_or_crash(AttWriteCommandBuilder { handle: AttHandle(3).into(), value: build_att_data(data.clone()), }); handler.process_packet(att_view.view()); // assert: the db has been updated assert_eq!(block_on_locally(db.read_attribute(AttHandle(3))).unwrap(), data); } #[test] fn test_unsupported_command() { // arrange let db = TestAttDatabase::new(vec![]); let handler = AttCommandHandler { db }; // act: send a packet that should not be handled here let att_view = build_att_view_or_crash(AttErrorResponseBuilder { opcode_in_error: AttOpcode::EXCHANGE_MTU_REQUEST, handle_in_error: AttHandle(1).into(), error_code: AttErrorCode::UNLIKELY_ERROR, }); handler.process_packet(att_view.view()); // assert: nothing happens (we crash if anything is unhandled within a mock) } } system/rust/src/gatt/server/gatt_database.rs +168 −2 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ use std::{cell::RefCell, collections::BTreeMap, ops::RangeInclusive, rc::Rc}; use anyhow::{bail, Result}; use async_trait::async_trait; use log::error; use log::{error, warn}; use crate::{ core::{ Loading Loading @@ -211,7 +211,10 @@ impl GattDatabase { properties: GattCharacteristicPropertiesBuilder { broadcast: 0, read: characteristic.permissions.readable().into(), write_without_response: 0, write_without_response: characteristic .permissions .writable_without_response() .into(), write: characteristic.permissions.writable().into(), notify: 0, indicate: characteristic.permissions.indicate().into(), Loading Loading @@ -432,6 +435,51 @@ impl AttDatabase for AttDatabaseImpl { } } 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.conn_id, handle, AttributeBackingType::Characteristic, data, ); } AttAttributeBackingValue::DynamicDescriptor(datastore) => { datastore.write_no_response( self.conn_id, 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()) Loading Loading @@ -472,6 +520,7 @@ mod test { gatt::mocks::{ mock_database_callbacks::{MockCallbackEvents, MockCallbacks}, mock_datastore::{MockDatastore, MockDatastoreEvents}, mock_raw_datastore::{MockRawDatastore, MockRawDatastoreEvents}, }, packets::Packet, utils::{ Loading Loading @@ -689,6 +738,54 @@ mod test { ); } #[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(CONN_ID); // 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 Loading Loading @@ -1381,4 +1478,73 @@ mod test { 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(CONN_ID); 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(CONN_ID, 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, descriptors: vec![], }], }, Rc::new(gatt_datastore), ) .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: 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); } } Loading
system/rust/src/gatt/server.rs +1 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ mod request_handler; pub mod services; mod transactions; mod command_handler; #[cfg(test)] mod test; Loading
system/rust/src/gatt/server/att_database.rs +13 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,8 @@ bitflags! { pub struct AttPermissions : u8 { /// Attribute can be read using READ_REQ const READABLE = 0x02; /// Attribute can be written to using WRITE_CMD const WRITABLE_WITHOUT_RESPONSE = 0x04; /// Attribute can be written to using WRITE_REQ const WRITABLE = 0x08; /// Attribute value may be sent using indications Loading @@ -53,6 +55,10 @@ impl AttPermissions { pub fn writable(&self) -> bool { self.contains(AttPermissions::WRITABLE) } /// Attribute can be written to using WRITE_CMD pub fn writable_without_response(&self) -> bool { self.contains(AttPermissions::WRITABLE_WITHOUT_RESPONSE) } /// Attribute value may be sent using indications pub fn indicate(&self) -> bool { self.contains(AttPermissions::INDICATE) Loading @@ -74,6 +80,9 @@ pub trait AttDatabase { data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode>; /// Write to an attribute by handle fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>); /// List all the attributes in this database. /// /// Expected to return them in sorted order. Loading Loading @@ -122,6 +131,10 @@ impl AttDatabase for SnapshottedAttDatabase<'_> { self.backing.write_attribute(handle, data).await } fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>) { self.backing.write_no_response_attribute(handle, data); } fn list_attributes(&self) -> Vec<AttAttribute> { self.attributes.clone() } Loading
system/rust/src/gatt/server/att_server_bearer.rs +9 −3 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ use crate::{ use super::{ att_database::AttDatabase, command_handler::AttCommandHandler, indication_handler::{ConfirmationWatcher, IndicationError, IndicationHandler}, request_handler::AttRequestHandler, }; Loading Loading @@ -59,6 +60,9 @@ pub struct AttServerBearer<T: AttDatabase> { // indication state indication_handler: SharedMutex<IndicationHandler<T>>, pending_confirmation: ConfirmationWatcher, // command handler (across all bearers) command_handler: AttCommandHandler<T>, } impl<T: AttDatabase + Clone + 'static> AttServerBearer<T> { Loading @@ -73,10 +77,12 @@ impl<T: AttDatabase + Clone + 'static> AttServerBearer<T> { send_packet: Box::new(send_packet), mtu: AttMtu::new(), curr_request: AttRequestState::Idle(AttRequestHandler::new(db)).into(), curr_request: AttRequestState::Idle(AttRequestHandler::new(db.clone())).into(), indication_handler: SharedMutex::new(indication_handler), pending_confirmation, command_handler: AttCommandHandler::new(db), } } Loading @@ -93,10 +99,10 @@ impl<T: AttDatabase + Clone + 'static> WeakBoxRef<'_, AttServerBearer<T>> { pub fn handle_packet(&self, packet: AttView<'_>) { match classify_opcode(packet.get_opcode()) { OperationType::Command => { error!("dropping ATT command (currently unsupported)"); self.command_handler.process_packet(packet); } OperationType::Request => { Self::handle_request(self, packet); self.handle_request(packet); } OperationType::Confirmation => self.pending_confirmation.on_confirmation(), OperationType::Response | OperationType::Notification | OperationType::Indication => { Loading
system/rust/src/gatt/server/command_handler.rs 0 → 100644 +99 −0 Original line number Diff line number Diff line use log::warn; use crate::packets::{AttOpcode, AttView, AttWriteCommandView, Packet}; use super::att_database::AttDatabase; /// This struct handles all ATT commands. pub struct AttCommandHandler<Db: AttDatabase> { db: Db, } impl<Db: AttDatabase> AttCommandHandler<Db> { pub fn new(db: Db) -> Self { Self { db } } pub fn process_packet(&self, packet: AttView<'_>) { let snapshotted_db = self.db.snapshot(); match packet.get_opcode() { AttOpcode::WRITE_COMMAND => { let Ok(packet) = AttWriteCommandView::try_parse(packet) else { warn!("failed to parse WRITE_COMMAND packet"); return; }; snapshotted_db .write_no_response_attribute(packet.get_handle().into(), packet.get_value()); } _ => { warn!("Dropping unsupported opcode {:?}", packet.get_opcode()); } } } } #[cfg(test)] mod test { use crate::{ core::uuid::Uuid, gatt::{ ids::AttHandle, server::{ att_database::{AttAttribute, AttDatabase}, command_handler::AttCommandHandler, gatt_database::AttPermissions, test::test_att_db::TestAttDatabase, }, }, packets::{ AttAttributeDataChild, AttErrorCode, AttErrorResponseBuilder, AttOpcode, AttWriteCommandBuilder, }, utils::{ packet::{build_att_data, build_att_view_or_crash}, task::block_on_locally, }, }; #[test] fn test_write_command() { // arrange let db = TestAttDatabase::new(vec![( AttAttribute { handle: AttHandle(3), type_: Uuid::new(0x1234), permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITHOUT_RESPONSE, }, vec![1, 2, 3], )]); let handler = AttCommandHandler { db: db.clone() }; let data = AttAttributeDataChild::RawData([1, 2].into()); // act: send write command let att_view = build_att_view_or_crash(AttWriteCommandBuilder { handle: AttHandle(3).into(), value: build_att_data(data.clone()), }); handler.process_packet(att_view.view()); // assert: the db has been updated assert_eq!(block_on_locally(db.read_attribute(AttHandle(3))).unwrap(), data); } #[test] fn test_unsupported_command() { // arrange let db = TestAttDatabase::new(vec![]); let handler = AttCommandHandler { db }; // act: send a packet that should not be handled here let att_view = build_att_view_or_crash(AttErrorResponseBuilder { opcode_in_error: AttOpcode::EXCHANGE_MTU_REQUEST, handle_in_error: AttHandle(1).into(), error_code: AttErrorCode::UNLIKELY_ERROR, }); handler.process_packet(att_view.view()); // assert: nothing happens (we crash if anything is unhandled within a mock) } }
system/rust/src/gatt/server/gatt_database.rs +168 −2 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ use std::{cell::RefCell, collections::BTreeMap, ops::RangeInclusive, rc::Rc}; use anyhow::{bail, Result}; use async_trait::async_trait; use log::error; use log::{error, warn}; use crate::{ core::{ Loading Loading @@ -211,7 +211,10 @@ impl GattDatabase { properties: GattCharacteristicPropertiesBuilder { broadcast: 0, read: characteristic.permissions.readable().into(), write_without_response: 0, write_without_response: characteristic .permissions .writable_without_response() .into(), write: characteristic.permissions.writable().into(), notify: 0, indicate: characteristic.permissions.indicate().into(), Loading Loading @@ -432,6 +435,51 @@ impl AttDatabase for AttDatabaseImpl { } } 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.conn_id, handle, AttributeBackingType::Characteristic, data, ); } AttAttributeBackingValue::DynamicDescriptor(datastore) => { datastore.write_no_response( self.conn_id, 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()) Loading Loading @@ -472,6 +520,7 @@ mod test { gatt::mocks::{ mock_database_callbacks::{MockCallbackEvents, MockCallbacks}, mock_datastore::{MockDatastore, MockDatastoreEvents}, mock_raw_datastore::{MockRawDatastore, MockRawDatastoreEvents}, }, packets::Packet, utils::{ Loading Loading @@ -689,6 +738,54 @@ mod test { ); } #[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(CONN_ID); // 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 Loading Loading @@ -1381,4 +1478,73 @@ mod test { 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(CONN_ID); 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(CONN_ID, 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, descriptors: vec![], }], }, Rc::new(gatt_datastore), ) .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: 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); } }