summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Rahul Arya <aryarahul@google.com> 2023-04-17 17:37:25 +0000
committer Rahul Arya <aryarahul@google.com> 2023-04-17 17:37:25 +0000
commit58aeaefd83bf7a0b25700e0db5338fbc0041dba6 (patch)
treee7dba19fbc4c8061e411318e0ccad4d9a2a4da80
parentd6707153ed94f55ce5dd62403598eee39cece2af (diff)
[Private GATT] Add support for write commands
Test: unit Bug: 255880936 Change-Id: Ic2c85e68522d2595af6047073b64479de09fa11b
-rw-r--r--system/rust/src/gatt/server.rs1
-rw-r--r--system/rust/src/gatt/server/att_database.rs13
-rw-r--r--system/rust/src/gatt/server/att_server_bearer.rs12
-rw-r--r--system/rust/src/gatt/server/command_handler.rs99
-rw-r--r--system/rust/src/gatt/server/gatt_database.rs170
-rw-r--r--system/rust/src/gatt/server/test/test_att_db.rs53
-rw-r--r--system/rust/src/packets.pdl5
-rw-r--r--system/rust/src/utils/packet.rs1
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,
}
}