Loading android/pandora/server/proto/pandora_experimental/gatt.proto +22 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,9 @@ service GATT { // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); // Register a GATT service rpc RegisterService(RegisterServiceRequest) returns (RegisterServiceResponse); } enum AttStatusCode { Loading Loading @@ -172,3 +175,22 @@ message ReadCharacteristicDescriptorResponse { AttValue value = 1; AttStatusCode status = 2; } message GattServiceParams { string uuid = 1; repeated GattCharacteristicParams characteristics = 2; } message GattCharacteristicParams { uint32 properties = 1; uint32 permissions = 2; string uuid = 3; } message RegisterServiceRequest { GattServiceParams service = 1; } message RegisterServiceResponse { GattService service = 1; } android/pandora/server/src/com/android/pandora/Gatt.kt +120 −100 Original line number Diff line number Diff line Loading @@ -20,18 +20,20 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted Loading @@ -45,15 +47,15 @@ import pandora.GattProto.* class Gatt(private val context: Context) : GATTImplBase() { private val TAG = "PandoraGatt" private val mScope: CoroutineScope private val flow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default) private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val mBluetoothAdapter = mBluetoothManager.adapter init { mScope = CoroutineScope(Dispatchers.Default) private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) } private val flow: Flow<Intent> init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_UUID) Loading @@ -61,10 +63,14 @@ class Gatt(private val context: Context) : GATTImplBase() { } fun deinit() { serverManager.server.close() mScope.cancel() } override fun exchangeMTU(request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse>) { override fun exchangeMTU( request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse> ) { grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) { val mtu = request.mtu Log.i(TAG, "exchangeMTU MTU=$mtu") Loading @@ -88,24 +94,16 @@ class Gatt(private val context: Context) : GATTImplBase() { if (characteristic == null) { val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking( descriptor, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } else { val valueWrote = gattInstance.writeCharacteristicBlocking( characteristic, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() val valueWrote = gattInstance.writeCharacteristicBlocking(characteristic, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } } } Loading Loading @@ -164,7 +162,10 @@ class Gatt(private val context: Context) : GATTImplBase() { } } override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse>) { override fun clearCache( request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse> ) { grpcUnary<ClearCacheResponse>(mScope, responseObserver) { Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) Loading @@ -185,11 +186,7 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } val readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } Loading @@ -205,10 +202,7 @@ class Gatt(private val context: Context) : GATTImplBase() { tryDiscoverServices(gattInstance) val readValues = gattInstance.readCharacteristicUuidBlocking( UUID.fromString(request.uuid), request.startHandle, request.endHandle ) UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicsFromUuidResponse.newBuilder() .addAllCharacteristicsRead(generateReadValuesList(readValues)) .build() Loading @@ -227,16 +221,46 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } val readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } } override fun registerService( request: RegisterServiceRequest, responseObserver: StreamObserver<RegisterServiceResponse> ) { grpcUnary(mScope, responseObserver) { val service = BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY) for (characteristic in request.service.characteristicsList) { service.addCharacteristic( BluetoothGattCharacteristic( UUID.fromString(characteristic.uuid), characteristic.properties, characteristic.permissions)) } val fullService = coroutineScope { val firstService = mScope.async { serverManager.newServiceFlow.first() } serverManager.server.addService(service) firstService.await() } RegisterServiceResponse.newBuilder() .setService( GattService.newBuilder() .setHandle(fullService.instanceId) .setType(fullService.type) .setUuid(fullService.uuid.toString()) .addAllIncludedServices(generateServicesList(service.includedServices, 1)) .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) .build()) .build() } } /** * Discovers services, then returns characteristic with given handle. BluetoothGatt API is * package-private so we have to redefine it here. Loading Loading @@ -338,11 +362,7 @@ class Gatt(private val context: Context) : GATTImplBase() { for (readValue in readValuesList) { val readValueBuilder = ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) newReadValuesList.add(readValueBuilder.build()) } Loading android/pandora/server/src/com/android/pandora/GattServerManager.kt 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.pandora import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattServerCallback import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map class GattServerManager(bluetoothManager: BluetoothManager, context: Context, globalScope: CoroutineScope) { val services = mutableMapOf<UUID, BluetoothGattService>() val server: BluetoothGattServer val newServiceFlow = MutableSharedFlow<BluetoothGattService>(extraBufferCapacity = 8) init { newServiceFlow.map { services[it.uuid] = it }.launchIn(globalScope) } init { val callback = object : BluetoothGattServerCallback() { override fun onServiceAdded(status: Int, service: BluetoothGattService) { check(status == BluetoothGatt.GATT_SUCCESS) check(newServiceFlow.tryEmit(service)) } } server = bluetoothManager.openGattServer(context, callback) } } Loading
android/pandora/server/proto/pandora_experimental/gatt.proto +22 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,9 @@ service GATT { // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); // Register a GATT service rpc RegisterService(RegisterServiceRequest) returns (RegisterServiceResponse); } enum AttStatusCode { Loading Loading @@ -172,3 +175,22 @@ message ReadCharacteristicDescriptorResponse { AttValue value = 1; AttStatusCode status = 2; } message GattServiceParams { string uuid = 1; repeated GattCharacteristicParams characteristics = 2; } message GattCharacteristicParams { uint32 properties = 1; uint32 permissions = 2; string uuid = 3; } message RegisterServiceRequest { GattServiceParams service = 1; } message RegisterServiceResponse { GattService service = 1; }
android/pandora/server/src/com/android/pandora/Gatt.kt +120 −100 Original line number Diff line number Diff line Loading @@ -20,18 +20,20 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted Loading @@ -45,15 +47,15 @@ import pandora.GattProto.* class Gatt(private val context: Context) : GATTImplBase() { private val TAG = "PandoraGatt" private val mScope: CoroutineScope private val flow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default) private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val mBluetoothAdapter = mBluetoothManager.adapter init { mScope = CoroutineScope(Dispatchers.Default) private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) } private val flow: Flow<Intent> init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_UUID) Loading @@ -61,10 +63,14 @@ class Gatt(private val context: Context) : GATTImplBase() { } fun deinit() { serverManager.server.close() mScope.cancel() } override fun exchangeMTU(request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse>) { override fun exchangeMTU( request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse> ) { grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) { val mtu = request.mtu Log.i(TAG, "exchangeMTU MTU=$mtu") Loading @@ -88,24 +94,16 @@ class Gatt(private val context: Context) : GATTImplBase() { if (characteristic == null) { val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking( descriptor, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } else { val valueWrote = gattInstance.writeCharacteristicBlocking( characteristic, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() val valueWrote = gattInstance.writeCharacteristicBlocking(characteristic, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } } } Loading Loading @@ -164,7 +162,10 @@ class Gatt(private val context: Context) : GATTImplBase() { } } override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse>) { override fun clearCache( request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse> ) { grpcUnary<ClearCacheResponse>(mScope, responseObserver) { Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) Loading @@ -185,11 +186,7 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } val readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } Loading @@ -205,10 +202,7 @@ class Gatt(private val context: Context) : GATTImplBase() { tryDiscoverServices(gattInstance) val readValues = gattInstance.readCharacteristicUuidBlocking( UUID.fromString(request.uuid), request.startHandle, request.endHandle ) UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicsFromUuidResponse.newBuilder() .addAllCharacteristicsRead(generateReadValuesList(readValues)) .build() Loading @@ -227,16 +221,46 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } val readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } } override fun registerService( request: RegisterServiceRequest, responseObserver: StreamObserver<RegisterServiceResponse> ) { grpcUnary(mScope, responseObserver) { val service = BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY) for (characteristic in request.service.characteristicsList) { service.addCharacteristic( BluetoothGattCharacteristic( UUID.fromString(characteristic.uuid), characteristic.properties, characteristic.permissions)) } val fullService = coroutineScope { val firstService = mScope.async { serverManager.newServiceFlow.first() } serverManager.server.addService(service) firstService.await() } RegisterServiceResponse.newBuilder() .setService( GattService.newBuilder() .setHandle(fullService.instanceId) .setType(fullService.type) .setUuid(fullService.uuid.toString()) .addAllIncludedServices(generateServicesList(service.includedServices, 1)) .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) .build()) .build() } } /** * Discovers services, then returns characteristic with given handle. BluetoothGatt API is * package-private so we have to redefine it here. Loading Loading @@ -338,11 +362,7 @@ class Gatt(private val context: Context) : GATTImplBase() { for (readValue in readValuesList) { val readValueBuilder = ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) newReadValuesList.add(readValueBuilder.build()) } Loading
android/pandora/server/src/com/android/pandora/GattServerManager.kt 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.pandora import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattServerCallback import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map class GattServerManager(bluetoothManager: BluetoothManager, context: Context, globalScope: CoroutineScope) { val services = mutableMapOf<UUID, BluetoothGattService>() val server: BluetoothGattServer val newServiceFlow = MutableSharedFlow<BluetoothGattService>(extraBufferCapacity = 8) init { newServiceFlow.map { services[it.uuid] = it }.launchIn(globalScope) } init { val callback = object : BluetoothGattServerCallback() { override fun onServiceAdded(status: Int, service: BluetoothGattService) { check(status == BluetoothGatt.GATT_SUCCESS) check(newServiceFlow.tryEmit(service)) } } server = bluetoothManager.openGattServer(context, callback) } }