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

Commit 8dc51c09 authored by Brian Delwiche's avatar Brian Delwiche
Browse files

Add Bumble test for GATT Characteristic Read/Write

Also includes a basic service discovery test and new Bumble features.

Test: atest BumbleBluetoothTests
Bug: 303248992
Change-Id: I709d945463b3e3fdc83dc2ea24ba2cd9e147b024
parent 35c5e00d
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -31,6 +31,7 @@ android_test_helper_app {
        "opencensus-java-contrib-grpc-metrics",
        "opencensus-java-contrib-grpc-metrics",
        "pandora_experimental-grpc-java",
        "pandora_experimental-grpc-java",
        "pandora_experimental-proto-java",
        "pandora_experimental-proto-java",
        "truth-java8-extension",
    ],
    ],


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


import static com.google.common.truth.Truth.assertThat;
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.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.any;
@@ -45,7 +46,11 @@ import org.mockito.InOrder;
import org.mockito.invocation.Invocation;
import org.mockito.invocation.Invocation;


import java.util.Collection;
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.AdvertiseRequest;
import pandora.HostProto.AdvertiseResponse;
import pandora.HostProto.AdvertiseResponse;
import pandora.HostProto.OwnAddressType;
import pandora.HostProto.OwnAddressType;
@@ -57,6 +62,8 @@ public class GattClientTest {
    private static final int MTU_REQUESTED = 23;
    private static final int MTU_REQUESTED = 23;
    private static final int ANOTHER_MTU_REQUESTED = 42;
    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();
    @ClassRule public static final AdoptShellPermissionsRule PERM = new AdoptShellPermissionsRule();


    @Rule public final PandoraDevice mBumble = new PandoraDevice();
    @Rule public final PandoraDevice mBumble = new PandoraDevice();
@@ -147,6 +154,123 @@ public class GattClientTest {
        verifyNoMoreInteractions(gattCallback);
        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() {
    private void advertiseWithBumble() {
        AdvertiseRequest request =
        AdvertiseRequest request =
                AdvertiseRequest.newBuilder()
                AdvertiseRequest.newBuilder()
+11 −0
Original line number Original line Diff line number Diff line
@@ -30,6 +30,7 @@ import org.junit.rules.ExternalResource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;


import pandora.DckGrpc;
import pandora.DckGrpc;
import pandora.GATTGrpc;
import pandora.HostGrpc;
import pandora.HostGrpc;
import pandora.HostProto;
import pandora.HostProto;
import pandora.SecurityGrpc;
import pandora.SecurityGrpc;
@@ -119,4 +120,14 @@ public final class PandoraDevice extends ExternalResource {
    public SecurityGrpc.SecurityStub security() {
    public SecurityGrpc.SecurityStub security() {
        return SecurityGrpc.newStub(mChannel);
        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 Original line Diff line number Diff line
@@ -16,8 +16,10 @@ import asyncio
import grpc
import grpc
import logging
import logging


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


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


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


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


        return DiscoverServicesResponse(
        return DiscoverServicesResponse(services=[
            services=[
            GattService(
            GattService(
                handle=service.handle,
                handle=service.handle,
                type=int.from_bytes(bytes(service.type), 'little'),
                type=int.from_bytes(bytes(service.type), 'little'),
@@ -136,21 +138,16 @@ class GATTService(GATTServicer):
                                handle=descriptor.handle,  # type: ignore
                                handle=descriptor.handle,  # type: ignore
                                permissions=0,  # TODO
                                permissions=0,  # TODO
                                uuid=str(descriptor.type),  # type: ignore
                                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
    @utils.rpc
    async def DiscoverServices(
    async def DiscoverServices(self, request: DiscoverServicesRequest,
        self, request: DiscoverServicesRequest, context: grpc.ServicerContext
                               context: grpc.ServicerContext) -> DiscoverServicesResponse:
    ) -> DiscoverServicesResponse:
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
        logging.info(f"DiscoverServices: {connection_handle}")
        logging.info(f"DiscoverServices: {connection_handle}")


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


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


        return DiscoverServicesResponse(
        return DiscoverServicesResponse(services=[
            services=[
            GattService(
            GattService(
                handle=service.handle,
                handle=service.handle,
                type=int.from_bytes(bytes(service.type), 'little'),
                type=int.from_bytes(bytes(service.type), 'little'),
@@ -183,16 +179,12 @@ class GATTService(GATTServicer):
                                handle=descriptor.handle,  # type: ignore
                                handle=descriptor.handle,  # type: ignore
                                permissions=0,  # TODO
                                permissions=0,  # TODO
                                uuid=str(descriptor.type),  # type: ignore
                                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`
    # TODO: implement `DiscoverServicesSdp`


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


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


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


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


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


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


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


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


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


        return ReadCharacteristicDescriptorResponse(value=AttValue(value=value), status=status)
        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()