Loading android/pandora/mmi2grpc/mmi2grpc/gatt.py +247 −3 Original line number Diff line number Diff line Loading @@ -13,12 +13,14 @@ # limitations under the License. import re import sys from mmi2grpc._helpers import assert_description from mmi2grpc._proxy import ProfileProxy from pandora.gatt_grpc import GATT from pandora.host_grpc import Host from pandora.gatt_pb2 import AttStatusCode # Tests that need GATT cache cleared before discovering services. NEEDS_CACHE_CLEARED = { Loading @@ -37,6 +39,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None self.read_value = None @assert_description def MMI_IUT_INITIATE_CONNECTION(self, test, pts_addr: bytes, **kwargs): Loading Loading @@ -69,6 +72,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None self.read_value = None return "OK" @assert_description Loading Loading @@ -229,7 +233,7 @@ class GATTProxy(ProfileProxy): assert self.connection is not None assert self.services is not None for service in self.services: assert len(service.included_services) is 0 assert len(service.included_services) == 0 return "OK" def MMI_CONFIRM_INCLUDE_SERVICE(self, description: str, **kwargs): Loading Loading @@ -262,8 +266,9 @@ class GATTProxy(ProfileProxy): stringHandleToInt(all_matches[i + 1]),\ formatUuid(all_matches[i + 3])): found_services += 1 assert found_services == (len(all_matches) / 4) return "OK" if found_services == (len(all_matches) / 4): return "Yes" return "No" def MMI_IUT_DISCOVER_SERVICE_UUID(self, description: str, **kwargs): """ Loading Loading @@ -405,6 +410,245 @@ class GATTProxy(ProfileProxy): return "Yes" return "No" def MMI_IUT_SEND_READ_CHARACTERISTIC_HANDLE(self, description: str, **kwargs): """ Please send read characteristic handle = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromHandle(\ connection=self.connection, handle=handle) return "OK" @assert_description def MMI_IUT_CONFIRM_READ_INVALID_HANDLE(self, **kwargs): """ Please confirm IUT received Invalid handle error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Invalid handle error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.INVALID_HANDLE: return "Yes" return "No" @assert_description def MMI_IUT_CONFIRM_READ_NOT_PERMITTED(self, **kwargs): """ Please confirm IUT received read is not permitted error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate read is not permitted error when read a characteristic. """ assert self.read_value is not None # Android read error doesn't return an error code so we have to also # compare to the generic error code here. if self.read_value.status == AttStatusCode.READ_NOT_PERMITTED or\ self.read_value.status == AttStatusCode.UNKNOWN_ERROR: return "Yes" return "No" @assert_description def MMI_IUT_CONFIRM_READ_AUTHENTICATION(self, **kwargs): """ Please confirm IUT received authentication error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate authentication error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.INSUFFICIENT_AUTHENTICATION: return "Yes" return "No" def MMI_IUT_SEND_READ_CHARACTERISTIC_UUID(self, description: str, **kwargs): """ Please send read using characteristic UUID = 'XXXX'O handle range = 'XXXX'O to 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic by UUID. """ assert self.connection is not None matches = re.findall("'([a0-Z9]*)'O", description) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=formatUuid(matches[0]),\ start_handle=stringHandleToInt(matches[1]),\ end_handle=stringHandleToInt(matches[2])) return "OK" @assert_description def MMI_IUT_CONFIRM_ATTRIBUTE_NOT_FOUND(self, **kwargs): """ Please confirm IUT received attribute not found error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate attribute not found error when read a characteristic. """ assert self.read_value is not None # Android read error doesn't return an error code so we have to also # compare to the generic error code here. if self.read_value.status == AttStatusCode.ATTRIBUTE_NOT_FOUND or\ self.read_value.status == AttStatusCode.UNKNOWN_ERROR: return "Yes" return "No" def MMI_IUT_SEND_READ_GREATER_OFFSET(self, description: str, **kwargs): """ Please send read to handle = 'XXXX'O and offset greater than 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read with invalid offset. """ # Android handles the read offset internally, so we just do read with handle here. # Unfortunately for testing, this will always work. assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromHandle(\ connection=self.connection, handle=handle) return "OK" @assert_description def MMI_IUT_CONFIRM_READ_INVALID_OFFSET(self, **kwargs): """ Please confirm IUT received Invalid offset error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Invalid offset error when read a characteristic. """ # Android handles read offset internally, so we can't read with wrong offset. return "Yes" @assert_description def MMI_IUT_CONFIRM_READ_APPLICATION(self, **kwargs): """ Please confirm IUT received Application error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Application error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.APPLICATION_ERROR: return "Yes" return "No" def MMI_IUT_CONFIRM_READ_CHARACTERISTIC_VALUE(self, description: str, **kwargs): """ Please confirm IUT received characteristic value='XX'O in random selected adopted database. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic to PTS random select adopted database. """ assert self.read_value is not None characteristic_value = bytes.fromhex(re.findall("'([a0-Z9]*)'O", description)[0]) if characteristic_value[0] in self.read_value.value: return "Yes" return "No" def MMI_IUT_READ_BY_TYPE_UUID(self, description: str, **kwargs): """ Please send read by type characteristic UUID = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None matches = re.findall("'([a0-Z9]*)'O", description) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=formatUuid(matches[0]),\ start_handle=0x0001,\ end_handle=0xffff) return "OK" def MMI_IUT_READ_BY_TYPE_UUID_ALT(self, description: str, **kwargs): """ Please send read by type characteristic UUID = 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None uuid = formatUuid(re.findall("'([a0-Z9-]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=uuid, start_handle=0x0001, end_handle=0xffff) return "OK" def MMI_IUT_CONFIRM_READ_HANDLE_VALUE(self, description: str, **kwargs): """ Please confirm IUT Handle='XX'O characteristic value='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'O in random selected adopted database. Click Yes if it matches the IUT, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read long characteristic to PTS random select adopted database. """ assert self.read_value is not None bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) if self.read_value.value == bytes_value: return "Yes" return "No" def MMI_IUT_SEND_READ_DESCIPTOR_HANDLE(self, description: str, **kwargs): """ Please send read characteristic descriptor handle = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic descriptor. """ assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicDescriptorFromHandle(\ connection=self.connection, handle=handle) return "OK" def MMI_IUT_CONFIRM_READ_DESCRIPTOR_VALUE(self, description: str, **kwargs): """ Please confirm IUT received Descriptor value='XXXXXXXX'O in random selected adopted database. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read Descriptor to PTS random select adopted database. """ assert self.read_value is not None bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) if self.read_value.value == bytes_value: return "Yes" return "No" common_uuid = "0000XXXX-0000-1000-8000-00805f9b34fb" Loading android/pandora/server/configs/PtsBotTest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ <option name="profile" value="AVRCP" /> <option name="profile" value="GATT/CL/GAC" /> <option name="profile" value="GATT/CL/GAD" /> <option name="profile" value="GATT/CL/GAR" /> <option name="profile" value="HFP/AG/DIS" /> <option name="profile" value="HFP/AG/HFI" /> <option name="profile" value="HFP/AG/SLC" /> Loading android/pandora/server/configs/pts_bot_tests_config.json +19 −0 Original line number Diff line number Diff line Loading @@ -59,6 +59,19 @@ "GATT/CL/GAD/BV-06-C", "GATT/CL/GAD/BV-07-C", "GATT/CL/GAD/BV-08-C", "GATT/CL/GAR/BI-01-C", "GATT/CL/GAR/BI-02-C", "GATT/CL/GAR/BI-06-C", "GATT/CL/GAR/BI-07-C", "GATT/CL/GAR/BI-12-C", "GATT/CL/GAR/BI-13-C", "GATT/CL/GAR/BI-14-C", "GATT/CL/GAR/BI-35-C", "GATT/CL/GAR/BV-01-C", "GATT/CL/GAR/BV-03-C", "GATT/CL/GAR/BV-04-C", "GATT/CL/GAR/BV-06-C", "GATT/CL/GAR/BV-07-C", "HFP/AG/DIS/BV-01-I", "HFP/AG/HFI/BI-03-I", "HFP/AG/HFI/BV-02-I", Loading Loading @@ -182,6 +195,12 @@ "AVRCP/CT/CRC/BV-01-I", "AVRCP/TG/CRC/BV-02-I", "AVRCP/CT/CEC/BV-01-I", "GATT/CL/GAR/BI-04-C", "GATT/CL/GAR/BI-05-C", "GATT/CL/GAR/BI-10-C", "GATT/CL/GAR/BI-11-C", "GATT/CL/GAR/BI-16-C", "GATT/CL/GAR/BI-17-C", "HFP/AG/SLC/BV-01-C", "HFP/AG/SLC/BV-02-C", "HFP/AG/SLC/BV-03-C", Loading android/pandora/server/proto/pandora/gatt.proto +53 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,26 @@ service GATT { // Clears DUT GATT cache. rpc ClearCache(ClearCacheRequest) returns (google.protobuf.Empty); // Reads characteristic with given handle. rpc ReadCharacteristicFromHandle(ReadCharacteristicRequest) returns (ReadCharacteristicResponse); // Reads characteristic with given uuid, start and end handles. rpc ReadCharacteristicFromUuid(ReadCharacteristicFromUuidRequest) returns (ReadCharacteristicResponse); // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); } enum AttStatusCode { SUCCESS = 0x00; UNKNOWN_ERROR = 0x101; INVALID_HANDLE = 0x01; READ_NOT_PERMITTED = 0x02; INSUFFICIENT_AUTHENTICATION = 0x05; INVALID_OFFSET = 0x07; ATTRIBUTE_NOT_FOUND = 0x0A; APPLICATION_ERROR = 0x80; } // A message representing a GATT service. Loading Loading @@ -58,7 +78,7 @@ message ExchangeMTURequest { int32 mtu = 2; } // Request for the `writeCharacteristicFromHandle` rpc. // Request for the `WriteCharacteristicFromHandle` rpc. message WriteCharacteristicRequest { Connection connection = 1; uint32 handle = 2; Loading Loading @@ -95,3 +115,35 @@ message DiscoverServicesSdpResponse { message ClearCacheRequest { Connection connection = 1; } // Request for the `ReadCharacteristicFromHandle` rpc. message ReadCharacteristicRequest { Connection connection = 1; uint32 handle = 2; } // Request for the `ReadCharacteristicFromUuid` rpc. message ReadCharacteristicFromUuidRequest { Connection connection = 1; string uuid = 2; uint32 start_handle = 3; uint32 end_handle = 4; } // Response for the `ReadCharacteristicFromHandle` and `ReadCharacteristicFromUuid` rpc. message ReadCharacteristicResponse { bytes value = 1; AttStatusCode status = 2; } // Request for the `ReadCharacteristicDescriptorFromHandle` rpc. message ReadCharacteristicDescriptorRequest { Connection connection = 1; uint32 handle = 2; } // Response for the `ReadCharacteristicDescriptorFromHandle` rpc. message ReadCharacteristicDescriptorResponse { bytes value = 1; AttStatusCode status = 2; } android/pandora/server/src/com/android/pandora/Gatt.kt +103 −21 Original line number Diff line number Diff line Loading @@ -18,16 +18,20 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothStatusCodes import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import com.google.protobuf.ByteString import io.grpc.Status import io.grpc.stub.StreamObserver Loading Loading @@ -74,9 +78,9 @@ class Gatt(private val context: Context) : GATTImplBase() { responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val mtu = request.mtu val addr = request.connection.cookie.toByteArray().decodeToString() if (!GattInstance.get(addr).mGatt.requestMtu(mtu)) { Log.e(TAG, "Error on requesting MTU for $addr") Log.i(TAG, "exchangeMTU MTU=$mtu") if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) { Log.e(TAG, "Error on requesting MTU $mtu") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() Loading @@ -86,16 +90,15 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun writeCharacteristicFromHandle(request: WriteCharacteristicRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) val gattInstance = GattInstance.get(request.connection.cookie) val characteristic: BluetoothGattCharacteristic? = getCharacteristicWithHandle(request.handle, gattInstance) if (characteristic != null) { Log.i(TAG, "writeCharacteristicFromHandle handle=${request.handle}") gattInstance.mGatt.writeCharacteristic(characteristic, request.value.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) } else { Log.e(TAG, "Error while writing characteristic for $gattInstance") Log.e(TAG, "Characteristic handle ${request.handle} not found.") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() Loading @@ -105,12 +108,13 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServiceByUuid(request: DiscoverServiceByUuidRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) val gattInstance = GattInstance.get(request.connection.cookie) Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}") // In some cases, GATT starts a discovery immediately after being connected, so // we need to wait until the service discovery is finished to be able to discover again. // This takes between 20s and 28s, and there is no way to know if the service is busy or not. delay(30000L) // Delay was originally 30s, but due to flakyness increased to 32s. delay(32000L) check(gattInstance.mGatt.discoverServiceByUuid(UUID.fromString(request.uuid))) // BluetoothGatt#discoverServiceByUuid does not trigger any callback and does not return // any service, the API was made for PTS testing only. Loading @@ -121,8 +125,8 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServices(request: DiscoverServicesRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) Log.i(TAG, "discoverServices") val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.discoverServices()) gattInstance.waitForDiscoveryEnd() DiscoverServicesResponse.newBuilder() Loading @@ -133,6 +137,7 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServicesSdp(request: DiscoverServicesSdpRequest, responseObserver: StreamObserver<DiscoverServicesSdpResponse>) { grpcUnary<DiscoverServicesSdpResponse>(mScope, responseObserver) { Log.i(TAG, "discoverServicesSdp") val bluetoothDevice = request.address.toBluetoothDevice(mBluetoothAdapter) check(bluetoothDevice.fetchUuidsWithSdp()) flow Loading @@ -151,21 +156,70 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.refresh()) Empty.getDefaultInstance() } } override fun readCharacteristicFromHandle(request: ReadCharacteristicRequest, responseObserver: StreamObserver<ReadCharacteristicResponse>) { grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}") val gattInstance = GattInstance.get(request.connection.cookie) val characteristic: BluetoothGattCharacteristic? = getCharacteristicWithHandle(request.handle, gattInstance) val readValue: GattInstance.GattInstanceValueRead? checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } override fun readCharacteristicFromUuid(request: ReadCharacteristicFromUuidRequest, responseObserver: StreamObserver<ReadCharacteristicResponse>) { grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicFromUuid uuid=${request.uuid}") val gattInstance = GattInstance.get(request.connection.cookie) tryDiscoverServices(gattInstance) val readValue = gattInstance.readCharacteristicUuidBlocking(UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } override fun readCharacteristicDescriptorFromHandle(request: ReadCharacteristicDescriptorRequest, responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse>) { grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}") val gattInstance = GattInstance.get(request.connection.cookie) val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) val readValue: GattInstance.GattInstanceValueRead? checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } /** * Discovers services, then returns characteristic with given handle. * BluetoothGatt API is package-private so we have to redefine it here. */ private suspend fun getCharacteristicWithHandle(handle: Int, gattInstance: GattInstance): BluetoothGattCharacteristic? { if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { Log.e(TAG, "Error on discovering services for $gattInstance") throw Status.UNKNOWN.asException() } else { gattInstance.waitForDiscoveryEnd() } tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { for (characteristic: BluetoothGattCharacteristic in service.characteristics) { if (characteristic.instanceId == handle) { Loading @@ -176,6 +230,25 @@ class Gatt(private val context: Context) : GATTImplBase() { return null } /** * Discovers services, then returns descriptor with given handle. * BluetoothGatt API is package-private so we have to redefine it here. */ private suspend fun getDescriptorWithHandle(handle: Int, gattInstance: GattInstance): BluetoothGattDescriptor? { tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { for (characteristic: BluetoothGattCharacteristic in service.characteristics) { for (descriptor: BluetoothGattDescriptor in characteristic.descriptors) { if (descriptor.getInstanceId() == handle) { return descriptor } } } } return null } /** * Generates a list of GattService from a list of BluetoothGattService. */ Loading Loading @@ -227,4 +300,13 @@ class Gatt(private val context: Context) : GATTImplBase() { } return newDescriptorsList } private suspend fun tryDiscoverServices(gattInstance: GattInstance) { if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { Log.e(TAG, "Error on discovering services for $gattInstance") throw Status.UNKNOWN.asException() } else { gattInstance.waitForDiscoveryEnd() } } } No newline at end of file Loading
android/pandora/mmi2grpc/mmi2grpc/gatt.py +247 −3 Original line number Diff line number Diff line Loading @@ -13,12 +13,14 @@ # limitations under the License. import re import sys from mmi2grpc._helpers import assert_description from mmi2grpc._proxy import ProfileProxy from pandora.gatt_grpc import GATT from pandora.host_grpc import Host from pandora.gatt_pb2 import AttStatusCode # Tests that need GATT cache cleared before discovering services. NEEDS_CACHE_CLEARED = { Loading @@ -37,6 +39,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None self.read_value = None @assert_description def MMI_IUT_INITIATE_CONNECTION(self, test, pts_addr: bytes, **kwargs): Loading Loading @@ -69,6 +72,7 @@ class GATTProxy(ProfileProxy): self.services = None self.characteristics = None self.descriptors = None self.read_value = None return "OK" @assert_description Loading Loading @@ -229,7 +233,7 @@ class GATTProxy(ProfileProxy): assert self.connection is not None assert self.services is not None for service in self.services: assert len(service.included_services) is 0 assert len(service.included_services) == 0 return "OK" def MMI_CONFIRM_INCLUDE_SERVICE(self, description: str, **kwargs): Loading Loading @@ -262,8 +266,9 @@ class GATTProxy(ProfileProxy): stringHandleToInt(all_matches[i + 1]),\ formatUuid(all_matches[i + 3])): found_services += 1 assert found_services == (len(all_matches) / 4) return "OK" if found_services == (len(all_matches) / 4): return "Yes" return "No" def MMI_IUT_DISCOVER_SERVICE_UUID(self, description: str, **kwargs): """ Loading Loading @@ -405,6 +410,245 @@ class GATTProxy(ProfileProxy): return "Yes" return "No" def MMI_IUT_SEND_READ_CHARACTERISTIC_HANDLE(self, description: str, **kwargs): """ Please send read characteristic handle = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromHandle(\ connection=self.connection, handle=handle) return "OK" @assert_description def MMI_IUT_CONFIRM_READ_INVALID_HANDLE(self, **kwargs): """ Please confirm IUT received Invalid handle error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Invalid handle error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.INVALID_HANDLE: return "Yes" return "No" @assert_description def MMI_IUT_CONFIRM_READ_NOT_PERMITTED(self, **kwargs): """ Please confirm IUT received read is not permitted error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate read is not permitted error when read a characteristic. """ assert self.read_value is not None # Android read error doesn't return an error code so we have to also # compare to the generic error code here. if self.read_value.status == AttStatusCode.READ_NOT_PERMITTED or\ self.read_value.status == AttStatusCode.UNKNOWN_ERROR: return "Yes" return "No" @assert_description def MMI_IUT_CONFIRM_READ_AUTHENTICATION(self, **kwargs): """ Please confirm IUT received authentication error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate authentication error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.INSUFFICIENT_AUTHENTICATION: return "Yes" return "No" def MMI_IUT_SEND_READ_CHARACTERISTIC_UUID(self, description: str, **kwargs): """ Please send read using characteristic UUID = 'XXXX'O handle range = 'XXXX'O to 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic by UUID. """ assert self.connection is not None matches = re.findall("'([a0-Z9]*)'O", description) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=formatUuid(matches[0]),\ start_handle=stringHandleToInt(matches[1]),\ end_handle=stringHandleToInt(matches[2])) return "OK" @assert_description def MMI_IUT_CONFIRM_ATTRIBUTE_NOT_FOUND(self, **kwargs): """ Please confirm IUT received attribute not found error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate attribute not found error when read a characteristic. """ assert self.read_value is not None # Android read error doesn't return an error code so we have to also # compare to the generic error code here. if self.read_value.status == AttStatusCode.ATTRIBUTE_NOT_FOUND or\ self.read_value.status == AttStatusCode.UNKNOWN_ERROR: return "Yes" return "No" def MMI_IUT_SEND_READ_GREATER_OFFSET(self, description: str, **kwargs): """ Please send read to handle = 'XXXX'O and offset greater than 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read with invalid offset. """ # Android handles the read offset internally, so we just do read with handle here. # Unfortunately for testing, this will always work. assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromHandle(\ connection=self.connection, handle=handle) return "OK" @assert_description def MMI_IUT_CONFIRM_READ_INVALID_OFFSET(self, **kwargs): """ Please confirm IUT received Invalid offset error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Invalid offset error when read a characteristic. """ # Android handles read offset internally, so we can't read with wrong offset. return "Yes" @assert_description def MMI_IUT_CONFIRM_READ_APPLICATION(self, **kwargs): """ Please confirm IUT received Application error. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) indicate Application error when read a characteristic. """ assert self.read_value is not None if self.read_value.status == AttStatusCode.APPLICATION_ERROR: return "Yes" return "No" def MMI_IUT_CONFIRM_READ_CHARACTERISTIC_VALUE(self, description: str, **kwargs): """ Please confirm IUT received characteristic value='XX'O in random selected adopted database. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic to PTS random select adopted database. """ assert self.read_value is not None characteristic_value = bytes.fromhex(re.findall("'([a0-Z9]*)'O", description)[0]) if characteristic_value[0] in self.read_value.value: return "Yes" return "No" def MMI_IUT_READ_BY_TYPE_UUID(self, description: str, **kwargs): """ Please send read by type characteristic UUID = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None matches = re.findall("'([a0-Z9]*)'O", description) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=formatUuid(matches[0]),\ start_handle=0x0001,\ end_handle=0xffff) return "OK" def MMI_IUT_READ_BY_TYPE_UUID_ALT(self, description: str, **kwargs): """ Please send read by type characteristic UUID = 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic. """ assert self.connection is not None uuid = formatUuid(re.findall("'([a0-Z9-]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicFromUuid(\ connection=self.connection, uuid=uuid, start_handle=0x0001, end_handle=0xffff) return "OK" def MMI_IUT_CONFIRM_READ_HANDLE_VALUE(self, description: str, **kwargs): """ Please confirm IUT Handle='XX'O characteristic value='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'O in random selected adopted database. Click Yes if it matches the IUT, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read long characteristic to PTS random select adopted database. """ assert self.read_value is not None bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) if self.read_value.value == bytes_value: return "Yes" return "No" def MMI_IUT_SEND_READ_DESCIPTOR_HANDLE(self, description: str, **kwargs): """ Please send read characteristic descriptor handle = 'XXXX'O to the PTS. Description: Verify that the Implementation Under Test (IUT) can send Read characteristic descriptor. """ assert self.connection is not None handle = stringHandleToInt(re.findall("'([a0-Z9]*)'O", description)[0]) self.read_value = self.gatt.ReadCharacteristicDescriptorFromHandle(\ connection=self.connection, handle=handle) return "OK" def MMI_IUT_CONFIRM_READ_DESCRIPTOR_VALUE(self, description: str, **kwargs): """ Please confirm IUT received Descriptor value='XXXXXXXX'O in random selected adopted database. Click Yes if IUT received it, otherwise click No. Description: Verify that the Implementation Under Test (IUT) can send Read Descriptor to PTS random select adopted database. """ assert self.read_value is not None bytes_value = bytes.fromhex(re.search("value='(.*)'O", description)[1]) if self.read_value.value == bytes_value: return "Yes" return "No" common_uuid = "0000XXXX-0000-1000-8000-00805f9b34fb" Loading
android/pandora/server/configs/PtsBotTest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ <option name="profile" value="AVRCP" /> <option name="profile" value="GATT/CL/GAC" /> <option name="profile" value="GATT/CL/GAD" /> <option name="profile" value="GATT/CL/GAR" /> <option name="profile" value="HFP/AG/DIS" /> <option name="profile" value="HFP/AG/HFI" /> <option name="profile" value="HFP/AG/SLC" /> Loading
android/pandora/server/configs/pts_bot_tests_config.json +19 −0 Original line number Diff line number Diff line Loading @@ -59,6 +59,19 @@ "GATT/CL/GAD/BV-06-C", "GATT/CL/GAD/BV-07-C", "GATT/CL/GAD/BV-08-C", "GATT/CL/GAR/BI-01-C", "GATT/CL/GAR/BI-02-C", "GATT/CL/GAR/BI-06-C", "GATT/CL/GAR/BI-07-C", "GATT/CL/GAR/BI-12-C", "GATT/CL/GAR/BI-13-C", "GATT/CL/GAR/BI-14-C", "GATT/CL/GAR/BI-35-C", "GATT/CL/GAR/BV-01-C", "GATT/CL/GAR/BV-03-C", "GATT/CL/GAR/BV-04-C", "GATT/CL/GAR/BV-06-C", "GATT/CL/GAR/BV-07-C", "HFP/AG/DIS/BV-01-I", "HFP/AG/HFI/BI-03-I", "HFP/AG/HFI/BV-02-I", Loading Loading @@ -182,6 +195,12 @@ "AVRCP/CT/CRC/BV-01-I", "AVRCP/TG/CRC/BV-02-I", "AVRCP/CT/CEC/BV-01-I", "GATT/CL/GAR/BI-04-C", "GATT/CL/GAR/BI-05-C", "GATT/CL/GAR/BI-10-C", "GATT/CL/GAR/BI-11-C", "GATT/CL/GAR/BI-16-C", "GATT/CL/GAR/BI-17-C", "HFP/AG/SLC/BV-01-C", "HFP/AG/SLC/BV-02-C", "HFP/AG/SLC/BV-03-C", Loading
android/pandora/server/proto/pandora/gatt.proto +53 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,26 @@ service GATT { // Clears DUT GATT cache. rpc ClearCache(ClearCacheRequest) returns (google.protobuf.Empty); // Reads characteristic with given handle. rpc ReadCharacteristicFromHandle(ReadCharacteristicRequest) returns (ReadCharacteristicResponse); // Reads characteristic with given uuid, start and end handles. rpc ReadCharacteristicFromUuid(ReadCharacteristicFromUuidRequest) returns (ReadCharacteristicResponse); // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); } enum AttStatusCode { SUCCESS = 0x00; UNKNOWN_ERROR = 0x101; INVALID_HANDLE = 0x01; READ_NOT_PERMITTED = 0x02; INSUFFICIENT_AUTHENTICATION = 0x05; INVALID_OFFSET = 0x07; ATTRIBUTE_NOT_FOUND = 0x0A; APPLICATION_ERROR = 0x80; } // A message representing a GATT service. Loading Loading @@ -58,7 +78,7 @@ message ExchangeMTURequest { int32 mtu = 2; } // Request for the `writeCharacteristicFromHandle` rpc. // Request for the `WriteCharacteristicFromHandle` rpc. message WriteCharacteristicRequest { Connection connection = 1; uint32 handle = 2; Loading Loading @@ -95,3 +115,35 @@ message DiscoverServicesSdpResponse { message ClearCacheRequest { Connection connection = 1; } // Request for the `ReadCharacteristicFromHandle` rpc. message ReadCharacteristicRequest { Connection connection = 1; uint32 handle = 2; } // Request for the `ReadCharacteristicFromUuid` rpc. message ReadCharacteristicFromUuidRequest { Connection connection = 1; string uuid = 2; uint32 start_handle = 3; uint32 end_handle = 4; } // Response for the `ReadCharacteristicFromHandle` and `ReadCharacteristicFromUuid` rpc. message ReadCharacteristicResponse { bytes value = 1; AttStatusCode status = 2; } // Request for the `ReadCharacteristicDescriptorFromHandle` rpc. message ReadCharacteristicDescriptorRequest { Connection connection = 1; uint32 handle = 2; } // Response for the `ReadCharacteristicDescriptorFromHandle` rpc. message ReadCharacteristicDescriptorResponse { bytes value = 1; AttStatusCode status = 2; }
android/pandora/server/src/com/android/pandora/Gatt.kt +103 −21 Original line number Diff line number Diff line Loading @@ -18,16 +18,20 @@ package com.android.pandora import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothStatusCodes import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import com.google.protobuf.ByteString import io.grpc.Status import io.grpc.stub.StreamObserver Loading Loading @@ -74,9 +78,9 @@ class Gatt(private val context: Context) : GATTImplBase() { responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val mtu = request.mtu val addr = request.connection.cookie.toByteArray().decodeToString() if (!GattInstance.get(addr).mGatt.requestMtu(mtu)) { Log.e(TAG, "Error on requesting MTU for $addr") Log.i(TAG, "exchangeMTU MTU=$mtu") if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) { Log.e(TAG, "Error on requesting MTU $mtu") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() Loading @@ -86,16 +90,15 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun writeCharacteristicFromHandle(request: WriteCharacteristicRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) val gattInstance = GattInstance.get(request.connection.cookie) val characteristic: BluetoothGattCharacteristic? = getCharacteristicWithHandle(request.handle, gattInstance) if (characteristic != null) { Log.i(TAG, "writeCharacteristicFromHandle handle=${request.handle}") gattInstance.mGatt.writeCharacteristic(characteristic, request.value.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) } else { Log.e(TAG, "Error while writing characteristic for $gattInstance") Log.e(TAG, "Characteristic handle ${request.handle} not found.") throw Status.UNKNOWN.asException() } Empty.getDefaultInstance() Loading @@ -105,12 +108,13 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServiceByUuid(request: DiscoverServiceByUuidRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) val gattInstance = GattInstance.get(request.connection.cookie) Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}") // In some cases, GATT starts a discovery immediately after being connected, so // we need to wait until the service discovery is finished to be able to discover again. // This takes between 20s and 28s, and there is no way to know if the service is busy or not. delay(30000L) // Delay was originally 30s, but due to flakyness increased to 32s. delay(32000L) check(gattInstance.mGatt.discoverServiceByUuid(UUID.fromString(request.uuid))) // BluetoothGatt#discoverServiceByUuid does not trigger any callback and does not return // any service, the API was made for PTS testing only. Loading @@ -121,8 +125,8 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServices(request: DiscoverServicesRequest, responseObserver: StreamObserver<DiscoverServicesResponse>) { grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) Log.i(TAG, "discoverServices") val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.discoverServices()) gattInstance.waitForDiscoveryEnd() DiscoverServicesResponse.newBuilder() Loading @@ -133,6 +137,7 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun discoverServicesSdp(request: DiscoverServicesSdpRequest, responseObserver: StreamObserver<DiscoverServicesSdpResponse>) { grpcUnary<DiscoverServicesSdpResponse>(mScope, responseObserver) { Log.i(TAG, "discoverServicesSdp") val bluetoothDevice = request.address.toBluetoothDevice(mBluetoothAdapter) check(bluetoothDevice.fetchUuidsWithSdp()) flow Loading @@ -151,21 +156,70 @@ class Gatt(private val context: Context) : GATTImplBase() { override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<Empty>) { grpcUnary<Empty>(mScope, responseObserver) { val addr = request.connection.cookie.toByteArray().decodeToString() val gattInstance = GattInstance.get(addr) Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) check(gattInstance.mGatt.refresh()) Empty.getDefaultInstance() } } override fun readCharacteristicFromHandle(request: ReadCharacteristicRequest, responseObserver: StreamObserver<ReadCharacteristicResponse>) { grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}") val gattInstance = GattInstance.get(request.connection.cookie) val characteristic: BluetoothGattCharacteristic? = getCharacteristicWithHandle(request.handle, gattInstance) val readValue: GattInstance.GattInstanceValueRead? checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } override fun readCharacteristicFromUuid(request: ReadCharacteristicFromUuidRequest, responseObserver: StreamObserver<ReadCharacteristicResponse>) { grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicFromUuid uuid=${request.uuid}") val gattInstance = GattInstance.get(request.connection.cookie) tryDiscoverServices(gattInstance) val readValue = gattInstance.readCharacteristicUuidBlocking(UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } override fun readCharacteristicDescriptorFromHandle(request: ReadCharacteristicDescriptorRequest, responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse>) { grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) { Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}") val gattInstance = GattInstance.get(request.connection.cookie) val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) val readValue: GattInstance.GattInstanceValueRead? checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setStatus(AttStatusCode.forNumber(readValue.status)) .setValue(ByteString.copyFrom(readValue.value)).build() } } /** * Discovers services, then returns characteristic with given handle. * BluetoothGatt API is package-private so we have to redefine it here. */ private suspend fun getCharacteristicWithHandle(handle: Int, gattInstance: GattInstance): BluetoothGattCharacteristic? { if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { Log.e(TAG, "Error on discovering services for $gattInstance") throw Status.UNKNOWN.asException() } else { gattInstance.waitForDiscoveryEnd() } tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { for (characteristic: BluetoothGattCharacteristic in service.characteristics) { if (characteristic.instanceId == handle) { Loading @@ -176,6 +230,25 @@ class Gatt(private val context: Context) : GATTImplBase() { return null } /** * Discovers services, then returns descriptor with given handle. * BluetoothGatt API is package-private so we have to redefine it here. */ private suspend fun getDescriptorWithHandle(handle: Int, gattInstance: GattInstance): BluetoothGattDescriptor? { tryDiscoverServices(gattInstance) for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) { for (characteristic: BluetoothGattCharacteristic in service.characteristics) { for (descriptor: BluetoothGattDescriptor in characteristic.descriptors) { if (descriptor.getInstanceId() == handle) { return descriptor } } } } return null } /** * Generates a list of GattService from a list of BluetoothGattService. */ Loading Loading @@ -227,4 +300,13 @@ class Gatt(private val context: Context) : GATTImplBase() { } return newDescriptorsList } private suspend fun tryDiscoverServices(gattInstance: GattInstance) { if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) { Log.e(TAG, "Error on discovering services for $gattInstance") throw Status.UNKNOWN.asException() } else { gattInstance.waitForDiscoveryEnd() } } } No newline at end of file