Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 58aeaefd authored by Rahul Arya's avatar Rahul Arya
Browse files

[Private GATT] Add support for write commands

Test: unit
Bug: 255880936
Change-Id: Ic2c85e68522d2595af6047073b64479de09fa11b
parent d6707153
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ mod request_handler;
pub mod services;
mod transactions;

mod command_handler;
#[cfg(test)]
mod test;

+13 −0
Original line number Diff line number Diff line
@@ -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()
    }
+9 −3
Original line number Diff line number Diff line
@@ -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 => {
+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)
    }
}
+168 −2
Original line number Diff line number Diff line
@@ -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::{
@@ -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
@@ -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