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

Commit aef382e2 authored by Brian Delwiche's avatar Brian Delwiche Committed by Gerrit Code Review
Browse files

Merge "Add Bumble test for GATT Characteristic Read/Write" into main

parents 75c1c6bd 8dc51c09
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ android_test_helper_app {
        "opencensus-java-contrib-grpc-metrics",
        "pandora_experimental-grpc-java",
        "pandora_experimental-proto-java",
        "truth-java8-extension",
    ],

    // Include all test java and kotlin files.
+124 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.bluetooth;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.any;
@@ -45,7 +46,11 @@ import org.mockito.InOrder;
import org.mockito.invocation.Invocation;

import java.util.Collection;
import java.util.UUID;

import pandora.GattProto.GattCharacteristicParams;
import pandora.GattProto.GattServiceParams;
import pandora.GattProto.RegisterServiceRequest;
import pandora.HostProto.AdvertiseRequest;
import pandora.HostProto.AdvertiseResponse;
import pandora.HostProto.OwnAddressType;
@@ -57,6 +62,8 @@ public class GattClientTest {
    private static final int MTU_REQUESTED = 23;
    private static final int ANOTHER_MTU_REQUESTED = 42;

    private static final UUID GAP_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");

    @ClassRule public static final AdoptShellPermissionsRule PERM = new AdoptShellPermissionsRule();

    @Rule public final PandoraDevice mBumble = new PandoraDevice();
@@ -131,6 +138,123 @@ public class GattClientTest {
        verifyNoMoreInteractions(gattCallback);
    }

    @Test
    public void clientGattDiscoverServices() throws Exception {

        BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class);
        BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback);

        try {
            verify(gattCallback, timeout(1000))
                    .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED));

            gatt.discoverServices();
            verify(gattCallback, timeout(10000))
                    .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS));

            assertThat(gatt.getServices().stream().map(BluetoothGattService::getUuid))
                    .contains(GAP_UUID);

        } finally {
            disconnectAndWaitDisconnection(gatt, gattCallback);
        }
    }

    @Test
    public void clientGattReadCharacteristics() throws Exception {

        BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class);
        BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback);

        try {
            verify(gattCallback, timeout(1000))
                    .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED));

            gatt.discoverServices();
            verify(gattCallback, timeout(10000))
                    .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS));

            BluetoothGattService firstService = gatt.getServices().get(0);

            BluetoothGattCharacteristic firstCharacteristic =
                    firstService.getCharacteristics().get(0);

            gatt.readCharacteristic(firstCharacteristic);

            verify(gattCallback, timeout(5000)).onCharacteristicRead(any(), any(), any(), anyInt());

        } finally {
            disconnectAndWaitDisconnection(gatt, gattCallback);
        }
    }

    @Test
    public void clientGattWriteCharacteristic() throws Exception {
        registerWritableGattService();

        BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class);
        BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback);

        try {
            verify(gattCallback, timeout(1000))
                    .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED));

            gatt.discoverServices();
            verify(gattCallback, timeout(10000))
                    .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS));

            BluetoothGattCharacteristic characteristic = null;

            outer:
            for (BluetoothGattService candidateService : gatt.getServices()) {
                for (BluetoothGattCharacteristic candidateCharacteristic :
                        candidateService.getCharacteristics()) {
                    if ((candidateCharacteristic.getProperties()
                                    & BluetoothGattCharacteristic.PROPERTY_WRITE)
                            != 0) {
                        characteristic = candidateCharacteristic;
                        break outer;
                    }
                }
            }

            byte[] newValue = new byte[] {13};

            gatt.writeCharacteristic(
                    characteristic, newValue, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);

            verify(gattCallback, timeout(5000))
                    .onCharacteristicWrite(
                            any(), eq(characteristic), eq(BluetoothGatt.GATT_SUCCESS));

        } finally {
            disconnectAndWaitDisconnection(gatt, gattCallback);
        }
    }

    private void registerWritableGattService() {

        String characteristicUuidString = "11111111-1111-1111-1111-111111111111";
        String serviceUuidString = "00000000-0000-0000-0000-000000000000";

        GattCharacteristicParams characteristicParams =
                GattCharacteristicParams.newBuilder()
                        .setProperties(BluetoothGattCharacteristic.PROPERTY_WRITE)
                        .setUuid(characteristicUuidString)
                        .build();

        GattServiceParams serviceParams =
                GattServiceParams.newBuilder()
                        .addCharacteristics(characteristicParams)
                        .setUuid(serviceUuidString)
                        .build();

        RegisterServiceRequest request =
                RegisterServiceRequest.newBuilder().setService(serviceParams).build();

        mBumble.gattBlocking().registerService(request);
    }

    private void advertiseWithBumble() {
        AdvertiseRequest request =
                AdvertiseRequest.newBuilder()
+11 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import org.junit.rules.ExternalResource;
import java.util.concurrent.TimeUnit;

import pandora.DckGrpc;
import pandora.GATTGrpc;
import pandora.HostGrpc;
import pandora.HostProto;
import pandora.SecurityGrpc;
@@ -119,4 +120,14 @@ public final class PandoraDevice extends ExternalResource {
    public SecurityGrpc.SecurityStub security() {
        return SecurityGrpc.newStub(mChannel);
    }

    /** Get Pandora GATT service */
    public GATTGrpc.GATTStub gatt() {
        return GATTGrpc.newStub(mChannel);
    }

    /** Get Pandora GATT blocking service */
    public GATTGrpc.GATTBlockingStub gattBlocking() {
        return GATTGrpc.newBlockingStub(mChannel);
    }
}
+89 −77
Original line number Diff line number Diff line
@@ -16,8 +16,10 @@ import asyncio
import grpc
import logging

from bumble.att import Attribute
from bumble.core import ProtocolError
from bumble.device import Connection as BumbleConnection, Device, Peer
from bumble.gatt import Characteristic, Descriptor, Service
from bumble.gatt_client import CharacteristicProxy, ServiceProxy
from bumble.pandora import utils
from pandora_experimental.gatt_grpc_aio import GATTServicer
@@ -41,6 +43,8 @@ from pandora_experimental.gatt_pb2 import (
    ReadCharacteristicResponse,
    ReadCharacteristicsFromUuidRequest,
    ReadCharacteristicsFromUuidResponse,
    RegisterServiceRequest,
    RegisterServiceResponse,
    WriteRequest,
    WriteResponse,
)
@@ -100,9 +104,8 @@ class GATTService(GATTServicer):
        return WriteResponse(handle=request.handle, status=status)

    @utils.rpc
    async def DiscoverServiceByUuid(
        self, request: DiscoverServiceByUuidRequest, context: grpc.ServicerContext
    ) -> DiscoverServicesResponse:
    async def DiscoverServiceByUuid(self, request: DiscoverServiceByUuidRequest,
                                    context: grpc.ServicerContext) -> DiscoverServicesResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"DiscoverServiceByUuid: {connection_handle}")

@@ -119,8 +122,7 @@ class GATTService(GATTServicer):

        await asyncio.gather(*(feed_service(service) for service in services))

        return DiscoverServicesResponse(
            services=[
        return DiscoverServicesResponse(services=[
            GattService(
                handle=service.handle,
                type=int.from_bytes(bytes(service.type), 'little'),
@@ -136,21 +138,16 @@ class GATTService(GATTServicer):
                                handle=descriptor.handle,  # type: ignore
                                permissions=0,  # TODO
                                uuid=str(descriptor.type),  # type: ignore
                                )
                                for descriptor in characteristic.descriptors  # type: ignore
                            ) for descriptor in characteristic.descriptors  # type: ignore
                        ],
                        )
                        for characteristic in service.characteristics  # type: ignore
                    ) for characteristic in service.characteristics  # type: ignore
                ],
                )
                for service in services
            ]
        )
            ) for service in services
        ])

    @utils.rpc
    async def DiscoverServices(
        self, request: DiscoverServicesRequest, context: grpc.ServicerContext
    ) -> DiscoverServicesResponse:
    async def DiscoverServices(self, request: DiscoverServicesRequest,
                               context: grpc.ServicerContext) -> DiscoverServicesResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"DiscoverServices: {connection_handle}")

@@ -166,8 +163,7 @@ class GATTService(GATTServicer):

        await asyncio.gather(*(feed_service(service) for service in services))

        return DiscoverServicesResponse(
            services=[
        return DiscoverServicesResponse(services=[
            GattService(
                handle=service.handle,
                type=int.from_bytes(bytes(service.type), 'little'),
@@ -183,16 +179,12 @@ class GATTService(GATTServicer):
                                handle=descriptor.handle,  # type: ignore
                                permissions=0,  # TODO
                                uuid=str(descriptor.type),  # type: ignore
                                )
                                for descriptor in characteristic.descriptors  # type: ignore
                            ) for descriptor in characteristic.descriptors  # type: ignore
                        ],
                        )
                        for characteristic in service.characteristics  # type: ignore
                    ) for characteristic in service.characteristics  # type: ignore
                ],
                )
                for service in services
            ]
        )
            ) for service in services
        ])

    # TODO: implement `DiscoverServicesSdp`

@@ -202,9 +194,8 @@ class GATTService(GATTServicer):
        return ClearCacheResponse()

    @utils.rpc
    async def ReadCharacteristicFromHandle(
        self, request: ReadCharacteristicRequest, context: grpc.ServicerContext
    ) -> ReadCharacteristicResponse:
    async def ReadCharacteristicFromHandle(self, request: ReadCharacteristicRequest,
                                           context: grpc.ServicerContext) -> ReadCharacteristicResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"ReadCharacteristicFromHandle: {connection_handle}")

@@ -222,9 +213,8 @@ class GATTService(GATTServicer):
        return ReadCharacteristicResponse(value=AttValue(value=value), status=status)

    @utils.rpc
    async def ReadCharacteristicsFromUuid(
        self, request: ReadCharacteristicsFromUuidRequest, context: grpc.ServicerContext
    ) -> ReadCharacteristicsFromUuidResponse:
    async def ReadCharacteristicsFromUuid(self, request: ReadCharacteristicsFromUuidRequest,
                                          context: grpc.ServicerContext) -> ReadCharacteristicsFromUuidResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"ReadCharacteristicsFromUuid: {connection_handle}")

@@ -237,15 +227,12 @@ class GATTService(GATTServicer):
        try:
            characteristics = await peer.read_characteristics_by_uuid(request.uuid, service_mock)  # type: ignore

            return ReadCharacteristicsFromUuidResponse(
                characteristics_read=[
            return ReadCharacteristicsFromUuidResponse(characteristics_read=[
                ReadCharacteristicResponse(
                    value=AttValue(value=value, handle=handle),  # type: ignore
                    status=SUCCESS,
                    )
                    for handle, value in characteristics  # type: ignore
                ]
            )
                ) for handle, value in characteristics  # type: ignore
            ])

        except ProtocolError as e:
            return ReadCharacteristicsFromUuidResponse(
@@ -254,8 +241,8 @@ class GATTService(GATTServicer):

    @utils.rpc
    async def ReadCharacteristicDescriptorFromHandle(
        self, request: ReadCharacteristicDescriptorRequest, context: grpc.ServicerContext
    ) -> ReadCharacteristicDescriptorResponse:
            self, request: ReadCharacteristicDescriptorRequest,
            context: grpc.ServicerContext) -> ReadCharacteristicDescriptorResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"ReadCharacteristicDescriptorFromHandle: {connection_handle}")

@@ -271,3 +258,28 @@ class GATTService(GATTServicer):
            status = e.error_code  # type: ignore

        return ReadCharacteristicDescriptorResponse(value=AttValue(value=value), status=status)

    @utils.rpc
    def RegisterService(self, request: RegisterServiceRequest,
                        context: grpc.ServicerContext) -> RegisterServiceResponse:
        logging.info(f"RegisterService")

        serviceUUID = request.service.uuid
        characteristics = [
            Characteristic(
                properties=Characteristic.Properties(characteristicParam.properties),
                permissions=Attribute.Permissions(characteristicParam.permissions),
                uuid=characteristicParam.uuid,
                descriptors=[
                    Descriptor(
                        attribute_type=descParam.uuid,
                        permissions=Attribute.Permissions(descParam.permissions),
                    ) for descParam in characteristicParam.descriptors
                ],
            ) for characteristicParam in request.service.characteristics
        ]
        service = Service(serviceUUID, characteristics)
        self.device.add_service(service)  # type: ignore[no-untyped-call]

        logging.info(f"RegisterService complete")
        return RegisterServiceResponse()