diff options
| author | 2023-04-17 17:37:25 +0000 | |
|---|---|---|
| committer | 2023-04-17 17:37:25 +0000 | |
| commit | 58aeaefd83bf7a0b25700e0db5338fbc0041dba6 (patch) | |
| tree | e7dba19fbc4c8061e411318e0ccad4d9a2a4da80 | |
| parent | d6707153ed94f55ce5dd62403598eee39cece2af (diff) | |
[Private GATT] Add support for write commands
Test: unit
Bug: 255880936
Change-Id: Ic2c85e68522d2595af6047073b64479de09fa11b
| -rw-r--r-- | system/rust/src/gatt/server.rs | 1 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/att_database.rs | 13 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/att_server_bearer.rs | 12 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/command_handler.rs | 99 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/gatt_database.rs | 170 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/test/test_att_db.rs | 53 | ||||
| -rw-r--r-- | system/rust/src/packets.pdl | 5 | ||||
| -rw-r--r-- | system/rust/src/utils/packet.rs | 1 |
8 files changed, 336 insertions, 18 deletions
diff --git a/system/rust/src/gatt/server.rs b/system/rust/src/gatt/server.rs index 886d9953a0..2fd81cf2a6 100644 --- a/system/rust/src/gatt/server.rs +++ b/system/rust/src/gatt/server.rs @@ -9,6 +9,7 @@ mod request_handler; pub mod services; mod transactions; +mod command_handler; #[cfg(test)] mod test; diff --git a/system/rust/src/gatt/server/att_database.rs b/system/rust/src/gatt/server/att_database.rs index 3b508c3194..10274e896a 100644 --- a/system/rust/src/gatt/server/att_database.rs +++ b/system/rust/src/gatt/server/att_database.rs @@ -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 @@ -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) @@ -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. @@ -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() } diff --git a/system/rust/src/gatt/server/att_server_bearer.rs b/system/rust/src/gatt/server/att_server_bearer.rs index 02c6d6a0bc..d04ba66841 100644 --- a/system/rust/src/gatt/server/att_server_bearer.rs +++ b/system/rust/src/gatt/server/att_server_bearer.rs @@ -27,6 +27,7 @@ use crate::{ use super::{ att_database::AttDatabase, + command_handler::AttCommandHandler, indication_handler::{ConfirmationWatcher, IndicationError, IndicationHandler}, request_handler::AttRequestHandler, }; @@ -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> { @@ -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), } } @@ -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 => { diff --git a/system/rust/src/gatt/server/command_handler.rs b/system/rust/src/gatt/server/command_handler.rs new file mode 100644 index 0000000000..e27eb31695 --- /dev/null +++ b/system/rust/src/gatt/server/command_handler.rs @@ -0,0 +1,99 @@ +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) + } +} diff --git a/system/rust/src/gatt/server/gatt_database.rs b/system/rust/src/gatt/server/gatt_database.rs index 01d8372e44..7c0c34758a 100644 --- a/system/rust/src/gatt/server/gatt_database.rs +++ b/system/rust/src/gatt/server/gatt_database.rs @@ -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::{ @@ -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(), @@ -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()) @@ -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::{ @@ -690,6 +739,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 let (gatt_datastore, mut data_evts) = MockDatastore::new(); @@ -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); + } } diff --git a/system/rust/src/gatt/server/test/test_att_db.rs b/system/rust/src/gatt/server/test/test_att_db.rs index 2128fe81d9..20642b3a8b 100644 --- a/system/rust/src/gatt/server/test/test_att_db.rs +++ b/system/rust/src/gatt/server/test/test_att_db.rs @@ -7,21 +7,31 @@ use crate::{ }; use async_trait::async_trait; -use log::info; -use std::{cell::RefCell, collections::BTreeMap}; +use log::{info, warn}; +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct TestAttDatabase { - attributes: BTreeMap<AttHandle, (AttAttribute, RefCell<Vec<u8>>)>, + attributes: Rc<BTreeMap<AttHandle, TestAttributeWithData>>, +} + +#[derive(Debug)] +struct TestAttributeWithData { + attribute: AttAttribute, + data: RefCell<Vec<u8>>, } impl TestAttDatabase { pub fn new(attributes: Vec<(AttAttribute, Vec<u8>)>) -> Self { Self { - attributes: attributes - .into_iter() - .map(|(att, data)| (att.handle, (att, RefCell::new(data)))) - .collect(), + attributes: Rc::new( + attributes + .into_iter() + .map(|(attribute, data)| { + (attribute.handle, TestAttributeWithData { attribute, data: data.into() }) + }) + .collect(), + ), } } } @@ -34,10 +44,12 @@ impl AttDatabase for TestAttDatabase { ) -> Result<AttAttributeDataChild, AttErrorCode> { info!("reading {handle:?}"); match self.attributes.get(&handle) { - Some((AttAttribute { permissions, .. }, _)) if !permissions.readable() => { + Some(TestAttributeWithData { attribute: AttAttribute { permissions, .. }, .. }) + if !permissions.readable() => + { Err(AttErrorCode::READ_NOT_PERMITTED) } - Some((_, data)) => { + Some(TestAttributeWithData { data, .. }) => { Ok(AttAttributeDataChild::RawData(data.borrow().clone().into_boxed_slice())) } None => Err(AttErrorCode::INVALID_HANDLE), @@ -49,18 +61,33 @@ impl AttDatabase for TestAttDatabase { data: AttAttributeDataView<'_>, ) -> Result<(), AttErrorCode> { match self.attributes.get(&handle) { - Some((AttAttribute { permissions, .. }, _)) if !permissions.writable() => { + Some(TestAttributeWithData { attribute: AttAttribute { permissions, .. }, .. }) + if !permissions.writable() => + { Err(AttErrorCode::WRITE_NOT_PERMITTED) } - Some((_, data_cell)) => { + Some(TestAttributeWithData { data: data_cell, .. }) => { data_cell.replace(data.get_raw_payload().collect()); Ok(()) } None => Err(AttErrorCode::INVALID_HANDLE), } } + fn write_no_response_attribute(&self, handle: AttHandle, data: AttAttributeDataView<'_>) { + match self.attributes.get(&handle) { + Some(TestAttributeWithData { + attribute: AttAttribute { permissions, .. }, + data: data_cell, + }) if !permissions.writable() => { + data_cell.replace(data.get_raw_payload().collect()); + } + _ => { + warn!("rejecting write command to {handle:?}") + } + } + } fn list_attributes(&self) -> Vec<AttAttribute> { - self.attributes.values().map(|(att, _)| *att).collect() + self.attributes.values().map(|attr| attr.attribute).collect() } } diff --git a/system/rust/src/packets.pdl b/system/rust/src/packets.pdl index 48d410c809..c8079bec42 100644 --- a/system/rust/src/packets.pdl +++ b/system/rust/src/packets.pdl @@ -246,3 +246,8 @@ packet AttExchangeMtuRequest : Att(opcode = EXCHANGE_MTU_REQUEST) { packet AttExchangeMtuResponse : Att(opcode = EXCHANGE_MTU_RESPONSE) { mtu: 16, } + +packet AttWriteCommand : Att(opcode = WRITE_COMMAND) { + handle : AttHandle, + value : AttAttributeData, +} diff --git a/system/rust/src/utils/packet.rs b/system/rust/src/utils/packet.rs index 3a5dd51ee4..790be2026b 100644 --- a/system/rust/src/utils/packet.rs +++ b/system/rust/src/utils/packet.rs @@ -42,6 +42,7 @@ pub fn HACK_child_to_opcode(child: &AttChild) -> AttOpcode { AttChild::AttHandleValueConfirmation(_) => AttOpcode::HANDLE_VALUE_CONFIRMATION, AttChild::AttExchangeMtuRequest(_) => AttOpcode::EXCHANGE_MTU_REQUEST, AttChild::AttExchangeMtuResponse(_) => AttOpcode::EXCHANGE_MTU_RESPONSE, + AttChild::AttWriteCommand(_) => AttOpcode::WRITE_COMMAND, } } |