diff options
| author | 2023-03-22 00:07:21 +0000 | |
|---|---|---|
| committer | 2023-03-22 00:07:21 +0000 | |
| commit | 62fa90934c6a0c2f7992f8eec89bbd972e35e29b (patch) | |
| tree | 0a0851f5f3cf7b24db96fe8fa34667dba2112a34 | |
| parent | 5d745396c2f021a48a670f1371a7ec4c3e9c4677 (diff) | |
[Private GATT] Built-in services
GATT/GAP. Implementation of GATT srv_chg indication to follow in the
next CL.
Bug: 255880936
Test: unit
Change-Id: If344709656c3d7e46e94f2644f63f8c221bae08d
| -rw-r--r-- | system/rust/src/gatt/server.rs | 6 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/request_handler.rs | 1 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/services.rs | 18 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/services/gap.rs | 170 | ||||
| -rw-r--r-- | system/rust/src/gatt/server/services/gatt.rs | 261 | ||||
| -rw-r--r-- | system/rust/src/packets.pdl | 9 | ||||
| -rw-r--r-- | system/rust/tests/gatt_server_test.rs | 41 |
7 files changed, 499 insertions, 7 deletions
diff --git a/system/rust/src/gatt/server.rs b/system/rust/src/gatt/server.rs index ec0f01b907..7b5639669c 100644 --- a/system/rust/src/gatt/server.rs +++ b/system/rust/src/gatt/server.rs @@ -6,6 +6,7 @@ pub mod att_server_bearer; pub mod gatt_database; mod indication_handler; mod request_handler; +pub mod services; mod transactions; #[cfg(test)] @@ -22,6 +23,7 @@ use self::{ super::ids::ServerId, att_server_bearer::AttServerBearer, gatt_database::{AttDatabaseImpl, GattServiceWithHandle}, + services::register_builtin_services, }; use super::{callbacks::GattDatastore, channel::AttTransport, ids::AttHandle}; @@ -97,7 +99,9 @@ impl GattModule { /// Open a GATT server pub fn open_gatt_server(&mut self, server_id: ServerId) -> Result<()> { - let old = self.databases.insert(server_id, GattDatabase::new().into()); + let mut db = GattDatabase::new(); + register_builtin_services(&mut db)?; + let old = self.databases.insert(server_id, db.into()); if old.is_some() { bail!("GATT server {server_id:?} already exists but was re-opened, clobbering old value...") } diff --git a/system/rust/src/gatt/server/request_handler.rs b/system/rust/src/gatt/server/request_handler.rs index 8040fd3aec..e6890803d0 100644 --- a/system/rust/src/gatt/server/request_handler.rs +++ b/system/rust/src/gatt/server/request_handler.rs @@ -40,6 +40,7 @@ impl<Db: AttDatabase> AttRequestHandler<Db> { Ok(result) => result, Err(_) => { // parse error, assume it's an unsupported request + // TODO(aryarahul): distinguish between REQUEST_NOT_SUPPORTED and INVALID_PDU AttErrorResponseBuilder { opcode_in_error: packet.get_opcode(), handle_in_error: AttHandle(0).into(), diff --git a/system/rust/src/gatt/server/services.rs b/system/rust/src/gatt/server/services.rs new file mode 100644 index 0000000000..a97db3a441 --- /dev/null +++ b/system/rust/src/gatt/server/services.rs @@ -0,0 +1,18 @@ +//! This module initializes the built-in services included in every +//! GATT server. + +pub mod gap; +pub mod gatt; + +use anyhow::Result; + +use self::{gap::register_gap_service, gatt::register_gatt_service}; + +use super::gatt_database::GattDatabase; + +/// Register all built-in services with the provided database +pub fn register_builtin_services(database: &mut GattDatabase) -> Result<()> { + register_gap_service(database)?; + register_gatt_service(database)?; + Ok(()) +} diff --git a/system/rust/src/gatt/server/services/gap.rs b/system/rust/src/gatt/server/services/gap.rs new file mode 100644 index 0000000000..7538e45c8c --- /dev/null +++ b/system/rust/src/gatt/server/services/gap.rs @@ -0,0 +1,170 @@ +//! The GAP service as defined in Core Spec 5.3 Vol 3C Section 12 + +use std::rc::Rc; + +use anyhow::Result; +use async_trait::async_trait; + +use crate::{ + core::uuid::Uuid, + gatt::{ + callbacks::GattDatastore, + ffi::AttributeBackingType, + ids::{AttHandle, ConnectionId}, + server::gatt_database::{ + AttPermissions, GattCharacteristicWithHandle, GattDatabase, GattServiceWithHandle, + }, + }, + packets::{AttAttributeDataChild, AttAttributeDataView, AttErrorCode}, +}; + +struct GapService; + +// Must lie in the range specified by GATT_GAP_START_HANDLE from legacy stack +const GAP_SERVICE_HANDLE: AttHandle = AttHandle(20); +const DEVICE_NAME_HANDLE: AttHandle = AttHandle(22); +const DEVICE_APPEARANCE_HANDLE: AttHandle = AttHandle(24); + +/// The UUID used for the GAP service (Assigned Numbers 3.4.1 Services by Name) +pub const GAP_SERVICE_UUID: Uuid = Uuid::new(0x1800); +/// The UUID used for the Device Name characteristic (Assigned Numbers 3.8.1 Characteristics by Name) +pub const DEVICE_NAME_UUID: Uuid = Uuid::new(0x2A00); +/// The UUID used for the Device Appearance characteristic (Assigned Numbers 3.8.1 Characteristics by Name) +pub const DEVICE_APPEARANCE_UUID: Uuid = Uuid::new(0x2A01); + +#[async_trait(?Send)] +impl GattDatastore for GapService { + async fn read( + &self, + _: ConnectionId, + handle: AttHandle, + _: AttributeBackingType, + ) -> Result<AttAttributeDataChild, AttErrorCode> { + match handle { + DEVICE_NAME_HANDLE => { + // for non-bonded peers, don't let them read the device name + // TODO(aryarahul): support discoverability, when we make this the main GATT server + Err(AttErrorCode::INSUFFICIENT_AUTHENTICATION) + } + // 0x0000 from AssignedNumbers => "Unknown" + DEVICE_APPEARANCE_HANDLE => Ok(AttAttributeDataChild::RawData([0x00, 0x00].into())), + _ => unreachable!("unexpected handle read"), + } + } + + async fn write( + &self, + _: ConnectionId, + _: AttHandle, + _: AttributeBackingType, + _: AttAttributeDataView<'_>, + ) -> Result<(), AttErrorCode> { + unreachable!("no GAP data should be writable") + } +} + +/// Register the GAP service in the provided GATT database. +pub fn register_gap_service(database: &mut GattDatabase) -> Result<()> { + database.add_service_with_handles( + // GAP Service + GattServiceWithHandle { + handle: GAP_SERVICE_HANDLE, + type_: GAP_SERVICE_UUID, + // Device Name + characteristics: vec![ + GattCharacteristicWithHandle { + handle: DEVICE_NAME_HANDLE, + type_: DEVICE_NAME_UUID, + permissions: AttPermissions::READABLE, + descriptors: vec![], + }, + // Appearance + GattCharacteristicWithHandle { + handle: DEVICE_APPEARANCE_HANDLE, + type_: DEVICE_APPEARANCE_UUID, + permissions: AttPermissions::READABLE, + descriptors: vec![], + }, + ], + }, + Rc::new(GapService), + ) +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::{ + core::shared_box::SharedBox, + gatt::{ + ids::ConnectionId, + server::{ + att_database::{ + AttDatabase, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID, + }, + gatt_database::GattDatabase, + }, + }, + utils::task::block_on_locally, + }; + + const CONN_ID: ConnectionId = ConnectionId(1); + + fn init_dbs() -> (SharedBox<GattDatabase>, impl AttDatabase) { + let mut gatt_database = GattDatabase::new(); + register_gap_service(&mut gatt_database).unwrap(); + let gatt_database = SharedBox::new(gatt_database); + let att_database = gatt_database.get_att_database(CONN_ID); + (gatt_database, att_database) + } + + #[test] + fn test_gap_service_discovery() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: discover all services + let attrs = att_db.list_attributes(); + + // assert: 1 service + (2 characteristics) * (declaration + value attrs) = 5 attrs + assert_eq!(attrs.len(), 5); + // assert: value handles are correct + assert_eq!(attrs[0].handle, GAP_SERVICE_HANDLE); + assert_eq!(attrs[2].handle, DEVICE_NAME_HANDLE); + assert_eq!(attrs[4].handle, DEVICE_APPEARANCE_HANDLE); + // assert: types are correct + assert_eq!(attrs[0].type_, PRIMARY_SERVICE_DECLARATION_UUID); + assert_eq!(attrs[1].type_, CHARACTERISTIC_UUID); + assert_eq!(attrs[2].type_, DEVICE_NAME_UUID); + assert_eq!(attrs[3].type_, CHARACTERISTIC_UUID); + assert_eq!(attrs[4].type_, DEVICE_APPEARANCE_UUID); + // assert: permissions of value attrs are correct + assert_eq!(attrs[2].permissions, AttPermissions::READABLE); + assert_eq!(attrs[4].permissions, AttPermissions::READABLE); + } + + #[test] + fn test_read_device_name_not_discoverable() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: try to read the device name + let name = block_on_locally(att_db.read_attribute(DEVICE_NAME_HANDLE)); + + // assert: the name is not readable + assert_eq!(name, Err(AttErrorCode::INSUFFICIENT_AUTHENTICATION)); + } + + #[test] + fn test_read_device_appearance() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: try to read the device name + let name = block_on_locally(att_db.read_attribute(DEVICE_APPEARANCE_HANDLE)); + + // assert: the name is not readable + assert_eq!(name, Ok(AttAttributeDataChild::RawData([0x00, 0x00].into()))); + } +} diff --git a/system/rust/src/gatt/server/services/gatt.rs b/system/rust/src/gatt/server/services/gatt.rs new file mode 100644 index 0000000000..dd9db3225a --- /dev/null +++ b/system/rust/src/gatt/server/services/gatt.rs @@ -0,0 +1,261 @@ +//! The GATT service as defined in Core Spec 5.3 Vol 3G Section 7 + +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +use anyhow::Result; +use async_trait::async_trait; +use log::warn; + +use crate::{ + core::uuid::Uuid, + gatt::{ + callbacks::GattDatastore, + ffi::AttributeBackingType, + ids::{AttHandle, ConnectionId}, + server::gatt_database::{ + AttPermissions, GattCharacteristicWithHandle, GattDatabase, GattDescriptorWithHandle, + GattServiceWithHandle, + }, + }, + packets::{ + AttAttributeDataChild, AttAttributeDataView, AttClientCharacteristicConfigurationBuilder, + AttClientCharacteristicConfigurationView, AttErrorCode, Packet, + }, +}; + +#[derive(Default)] +struct GattService { + // TODO(aryarahul): clear this on disconnect/reconnect + // TODO(aryarahul): do NOT clear this for bonded devices + // TODO(aryarahul): actually send this on service change + clients_watching_for_service_change_indication: RefCell<HashSet<ConnectionId>>, +} + +// Must lie in the range specified by GATT_GATT_START_HANDLE from legacy stack +const GATT_SERVICE_HANDLE: AttHandle = AttHandle(1); +const SERVICE_CHANGE_HANDLE: AttHandle = AttHandle(3); +const SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE: AttHandle = AttHandle(4); + +/// The UUID used for the GATT service (Assigned Numbers 3.4.1 Services by Name) +pub const GATT_SERVICE_UUID: Uuid = Uuid::new(0x1801); +/// The UUID used for the Service Changed characteristic (Assigned Numbers 3.8.1 Characteristics by Name) +pub const SERVICE_CHANGE_UUID: Uuid = Uuid::new(0x2A05); +/// The UUID used for the Client Characteristic Configuration descriptor (Assigned Numbers 3.7 Descriptors) +pub const CLIENT_CHARACTERISTIC_CONFIGURATION_UUID: Uuid = Uuid::new(0x2902); + +#[async_trait(?Send)] +impl GattDatastore for GattService { + async fn read( + &self, + conn_id: ConnectionId, + handle: AttHandle, + _: AttributeBackingType, + ) -> Result<AttAttributeDataChild, AttErrorCode> { + if handle == SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE { + Ok(AttClientCharacteristicConfigurationBuilder { + notification: 0, + indication: self + .clients_watching_for_service_change_indication + .borrow() + .contains(&conn_id) + .into(), + } + .into()) + } else { + unreachable!() + } + } + + async fn write( + &self, + conn_id: ConnectionId, + handle: AttHandle, + _: AttributeBackingType, + data: AttAttributeDataView<'_>, + ) -> Result<(), AttErrorCode> { + if handle == SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE { + let ccc = AttClientCharacteristicConfigurationView::try_parse(data).map_err(|err| { + warn!("failed to parse CCC descriptor, got: {err:?}"); + AttErrorCode::APPLICATION_ERROR + })?; + if ccc.get_indication() != 0 { + self.clients_watching_for_service_change_indication.borrow_mut().insert(conn_id); + } else { + self.clients_watching_for_service_change_indication.borrow_mut().remove(&conn_id); + } + Ok(()) + } else { + unreachable!() + } + } +} + +/// Register the GATT service in the provided GATT database. +pub fn register_gatt_service(database: &mut GattDatabase) -> Result<()> { + database.add_service_with_handles( + // GATT Service + GattServiceWithHandle { + handle: GATT_SERVICE_HANDLE, + type_: GATT_SERVICE_UUID, + // Service Changed Characteristic + characteristics: vec![GattCharacteristicWithHandle { + handle: SERVICE_CHANGE_HANDLE, + type_: SERVICE_CHANGE_UUID, + permissions: AttPermissions::INDICATE, + descriptors: vec![GattDescriptorWithHandle { + handle: SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE, + type_: CLIENT_CHARACTERISTIC_CONFIGURATION_UUID, + permissions: AttPermissions::READABLE | AttPermissions::WRITABLE, + }], + }], + }, + Rc::new(GattService::default()), + ) +} +#[cfg(test)] +mod test { + use super::*; + + use crate::{ + core::shared_box::SharedBox, + gatt::{ + ids::ConnectionId, + server::{ + att_database::{ + AttDatabase, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID, + }, + gatt_database::GattDatabase, + }, + }, + utils::{ + packet::{build_att_data, build_view_or_crash}, + task::block_on_locally, + }, + }; + + const CONN_ID: ConnectionId = ConnectionId(1); + + fn init_dbs() -> (SharedBox<GattDatabase>, impl AttDatabase) { + let mut gatt_database = GattDatabase::new(); + register_gatt_service(&mut gatt_database).unwrap(); + let gatt_database = SharedBox::new(gatt_database); + let att_database = gatt_database.get_att_database(CONN_ID); + (gatt_database, att_database) + } + + #[test] + fn test_gatt_service_discovery() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: discover all services + let attrs = att_db.list_attributes(); + + // assert: 1 service + 1 char decl + 1 char value + 1 char descriptor = 4 attrs + assert_eq!(attrs.len(), 4); + // assert: value handles are correct + assert_eq!(attrs[0].handle, GATT_SERVICE_HANDLE); + assert_eq!(attrs[2].handle, SERVICE_CHANGE_HANDLE); + // assert: types are correct + assert_eq!(attrs[0].type_, PRIMARY_SERVICE_DECLARATION_UUID); + assert_eq!(attrs[1].type_, CHARACTERISTIC_UUID); + assert_eq!(attrs[2].type_, SERVICE_CHANGE_UUID); + assert_eq!(attrs[3].type_, CLIENT_CHARACTERISTIC_CONFIGURATION_UUID); + // assert: permissions of value attrs are correct + assert_eq!(attrs[2].permissions, AttPermissions::INDICATE); + assert_eq!(attrs[3].permissions, AttPermissions::READABLE | AttPermissions::WRITABLE); + } + + #[test] + fn test_default_indication_subscription() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: try to read the CCC descriptor + let resp = + block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap(); + + // assert: we are not registered for either indications/notifications + let AttAttributeDataChild::AttClientCharacteristicConfiguration(configuration) = resp else { + unreachable!() + }; + assert_eq!( + configuration, + AttClientCharacteristicConfigurationBuilder { notification: 0, indication: 0 } + ); + } + + #[test] + fn test_subscribe_to_indication() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: register for service change indication + block_on_locally( + att_db.write_attribute( + SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE, + build_view_or_crash(build_att_data(AttClientCharacteristicConfigurationBuilder { + notification: 0, + indication: 1, + })) + .view(), + ), + ) + .unwrap(); + // read our registration status + let resp = + block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap(); + + // assert: we are registered for indications + let AttAttributeDataChild::AttClientCharacteristicConfiguration(configuration) = resp else { + unreachable!() + }; + assert_eq!( + configuration, + AttClientCharacteristicConfigurationBuilder { notification: 0, indication: 1 } + ); + } + + #[test] + fn test_unsubscribe_to_indication() { + // arrange + let (_gatt_db, att_db) = init_dbs(); + + // act: register for service change indication + block_on_locally( + att_db.write_attribute( + SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE, + build_view_or_crash(build_att_data(AttClientCharacteristicConfigurationBuilder { + notification: 0, + indication: 1, + })) + .view(), + ), + ) + .unwrap(); + // act: next, unregister from this indication + block_on_locally( + att_db.write_attribute( + SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE, + build_view_or_crash(build_att_data(AttClientCharacteristicConfigurationBuilder { + notification: 0, + indication: 0, + })) + .view(), + ), + ) + .unwrap(); + // read our registration status + let resp = + block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap(); + + // assert: we are not registered for indications + let AttAttributeDataChild::AttClientCharacteristicConfiguration(configuration) = resp else { + unreachable!() + }; + assert_eq!( + configuration, + AttClientCharacteristicConfigurationBuilder { notification: 0, indication: 0 } + ); + } +} diff --git a/system/rust/src/packets.pdl b/system/rust/src/packets.pdl index 85ea01998a..3a1122ae06 100644 --- a/system/rust/src/packets.pdl +++ b/system/rust/src/packets.pdl @@ -59,9 +59,12 @@ enum AttErrorCode : 8 { INVALID_HANDLE = 0x01, READ_NOT_PERMITTED = 0x02, WRITE_NOT_PERMITTED = 0x03, + INVALID_PDU = 0x04, + INSUFFICIENT_AUTHENTICATION = 0x05, REQUEST_NOT_SUPPORTED = 0x06, ATTRIBUTE_NOT_FOUND = 0x0A, UNSUPPORTED_GROUP_TYPE = 0x10, + APPLICATION_ERROR = 0x80, UNLIKELY_ERROR = 0x0E } @@ -135,6 +138,12 @@ struct GattServiceDeclarationValue : AttAttributeData { uuid: Uuid, } +struct AttClientCharacteristicConfiguration : AttAttributeData { + notification: 1, + indication: 1, + _reserved_: 14, +} + struct AttAttributeData { _payload_ } diff --git a/system/rust/tests/gatt_server_test.rs b/system/rust/tests/gatt_server_test.rs index b40edc3a14..807616f42e 100644 --- a/system/rust/tests/gatt_server_test.rs +++ b/system/rust/tests/gatt_server_test.rs @@ -15,14 +15,16 @@ use bluetooth_core::{ AttPermissions, GattCharacteristicWithHandle, GattDescriptorWithHandle, GattServiceWithHandle, }, + services::gap::DEVICE_NAME_UUID, GattModule, IndicationError, }, }, packets::{ - AttAttributeDataChild, AttBuilder, AttErrorCode, AttErrorResponseBuilder, + AttAttributeDataChild, AttBuilder, AttChild, AttErrorCode, AttErrorResponseBuilder, AttHandleValueConfirmationBuilder, AttHandleValueIndicationBuilder, AttOpcode, - AttReadRequestBuilder, AttReadResponseBuilder, AttWriteRequestBuilder, - AttWriteResponseBuilder, GattServiceDeclarationValueBuilder, Serializable, + AttReadByTypeRequestBuilder, AttReadRequestBuilder, AttReadResponseBuilder, + AttWriteRequestBuilder, AttWriteResponseBuilder, GattServiceDeclarationValueBuilder, + Serializable, }, utils::packet::{build_att_data, build_att_view_or_crash}, }; @@ -40,9 +42,9 @@ const ANOTHER_TCB_IDX: TransportIndex = TransportIndex(2); const ANOTHER_SERVER_ID: ServerId = ServerId(3); const ANOTHER_CONN_ID: ConnectionId = ConnectionId::new(ANOTHER_TCB_IDX, ANOTHER_SERVER_ID); -const SERVICE_HANDLE: AttHandle = AttHandle(3); -const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(5); -const DESCRIPTOR_HANDLE: AttHandle = AttHandle(6); +const SERVICE_HANDLE: AttHandle = AttHandle(6); +const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(8); +const DESCRIPTOR_HANDLE: AttHandle = AttHandle(9); const SERVICE_TYPE: Uuid = Uuid::new(0x0102); const CHARACTERISTIC_TYPE: Uuid = Uuid::new(0x0103); @@ -431,3 +433,30 @@ fn test_multiple_servers() { assert_eq!(resp_2._child_.to_vec().unwrap(), ANOTHER_DATA); }) } + +#[test] +fn test_read_device_name() { + start_test(async move { + // arrange + let (mut gatt, mut transport_rx) = start_gatt_module(); + create_server_and_open_connection(&mut gatt); + + // act: try to read the device name + gatt.get_bearer(CONN_ID).unwrap().handle_packet( + build_att_view_or_crash(AttReadByTypeRequestBuilder { + starting_handle: AttHandle(1).into(), + ending_handle: AttHandle(0xFFFF).into(), + attribute_type: DEVICE_NAME_UUID.into(), + }) + .view(), + ); + let (tcb_idx, resp) = transport_rx.recv().await.unwrap(); + + // assert: the name should not be readable + assert_eq!(tcb_idx, TCB_IDX); + let AttChild::AttErrorResponse(resp) = resp._child_ else { + unreachable!("{resp:?}"); + }; + assert_eq!(resp.error_code, AttErrorCode::INSUFFICIENT_AUTHENTICATION); + }); +} |