From e87bd6b7027df658e769767b33a9d282957ec519 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 16:19:15 +0100 Subject: [PATCH] reinit device instead of dropping connection on quick reconnects --- .../cobble/bluetooth/ble/NordicGattServer.kt | 18 +++++- .../bluetooth/ble/PPoGServiceConnection.kt | 56 ++++++++++++++----- .../cobble/bluetooth/ble/PPoGSession.kt | 3 +- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 2 +- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt index fb9e3cb0..8296bc83 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory import org.slf4j.LoggerFactoryFriend import timber.log.Timber import java.io.Closeable +import java.io.IOException import java.util.UUID import kotlin.coroutines.CoroutineContext @@ -102,7 +103,7 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. } val serverScope = CoroutineScope(ioDispatcher) serverScope.coroutineContext.job.invokeOnCompletion { - Timber.v("GattServer scope closed") + Timber.v(it, "GattServer scope closed") close() } server = ServerBleGatt.create( @@ -122,8 +123,13 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. Timber.w("Connection already exists for device ${it.device.address}") return@onEach } - val connection = PPoGServiceConnection(it) - connections[it.device.address] = connection + if (connections[it.device.address]?.isStillValid == true) { + Timber.d("Reinitializing connection for device ${it.device.address}") + connections[it.device.address]?.reinit(it) + } else { + val connection = PPoGServiceConnection(it) + connections[it.device.address] = connection + } } .launchIn(serverScope) } @@ -139,6 +145,12 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. return connection.sendMessage(packet) } + suspend fun resetDevice(deviceAddress: String) { + val connection = connections[deviceAddress] + ?: throw IOException("No connection for device $deviceAddress") + connection.requestReset() + } + fun rxFlowFor(deviceAddress: String): Flow? { return connections[deviceAddress]?.incomingPebblePacketData } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 54c49993..75970e78 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat import no.nordicsemi.android.kotlin.ble.core.errors.GattOperationException @@ -14,9 +15,10 @@ import java.io.Closeable import java.util.UUID @OptIn(FlowPreview::class) -class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattConnection, ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { - private val scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") - private val ppogSession = PPoGSession(scope, serverConnection.device.address, LEConstants.DEFAULT_MTU) +class PPoGServiceConnection(private var serverConnection: ServerBluetoothGattConnection, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { + private var scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") + private val sessionScope = CoroutineScope(ioDispatcher) + CoroutineName("PPoGSession-${serverConnection.device.address}") + private val ppogSession = PPoGSession(sessionScope, serverConnection.device.address, LEConstants.DEFAULT_MTU) val device get() = serverConnection.device @@ -30,14 +32,36 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon private val _incomingPebblePackets = Channel(Channel.BUFFERED) val incomingPebblePacketData: Flow = _incomingPebblePackets.receiveAsFlow() + // Make our own connection state flow that debounces the connection state, as we might recreate the connection but only want to cancel everything if it doesn't reconnect + private val connectionStateDebounced = MutableStateFlow(null) + val isConnected: Boolean get() = scope.isActive + val isStillValid: Boolean + get() = sessionScope.isActive private val notificationsEnabled = MutableStateFlow(false) private var lastNotify: DataByteArray? = null init { - Timber.d("PPoGServiceConnection created with ${serverConnection.device}: PHY (RX ${serverConnection.rxPhy} TX ${serverConnection.txPhy})") + connectionStateDebounced + .filterNotNull() + .debounce(1000) + .onEach { + Timber.v("(${serverConnection.device}) New connection state: ${it.state} ${it.status}") + } + .filter { it.state == GattConnectionState.STATE_DISCONNECTED } + .onEach { + Timber.i("(${serverConnection.device}) Connection lost") + scope.cancel("Connection lost") + sessionScope.cancel("Connection lost") + } + .launchIn(sessionScope) + launchFlows() + } + + private fun launchFlows() { + Timber.d("PPoGServiceConnection created with ${serverConnection.device}") serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) serverConnection.services.findService(ppogServiceUUID)?.let { service -> check(service.findCharacteristic(metaCharacteristicUUID) != null) { "Meta characteristic missing" } @@ -48,8 +72,8 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon characteristic.value .filter { it != lastNotify } // Ignore echo .onEach { - ppogSession.handlePacket(it.value.clone()) - }.launchIn(scope) + ppogSession.handlePacket(it.value.clone()) + }.launchIn(scope) characteristic.findDescriptor(configurationDescriptorUUID)?.value?.onEach { val value = it.getIntValue(IntFormat.FORMAT_UINT8, 0) Timber.i("(${serverConnection.device}) PPOG Notify changed: $value") @@ -78,27 +102,31 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon } }.launchIn(scope) serverConnection.connectionProvider.connectionStateWithStatus - .filterNotNull() - .debounce(1000) // Debounce to ignore quick reconnects - .onEach { - Timber.v("(${serverConnection.device}) New connection state: ${it.state} ${it.status}") - } - .filter { it.state == GattConnectionState.STATE_DISCONNECTED } .onEach { - Timber.i("(${serverConnection.device}) Connection lost") - scope.cancel("Connection lost") + connectionStateDebounced.value = it } .launchIn(scope) } ?: throw IllegalStateException("PPOG Characteristic missing") } ?: throw IllegalStateException("PPOG Service missing") } + fun reinit(serverConnection: ServerBluetoothGattConnection) { + this.serverConnection = serverConnection + scope.cancel("Reinit") + scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") + } + override fun close() { scope.cancel("Closed") + sessionScope.cancel("Closed") } suspend fun sendMessage(packet: ByteArray): Boolean { ppogSession.stateManager.stateFlow.first { it == PPoGSession.State.Open } // Wait for session to open, otherwise packet will be dropped return ppogSession.sendMessage(packet) } + + suspend fun requestReset() { + ppogSession.requestReset() + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 3a7ae9e8..d223f219 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -334,7 +334,8 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: delayedAckJob?.cancel() } - private suspend fun requestReset() { + suspend fun requestReset() { + check(pendingOutboundResetAck == null) { "Tried to request reset while reset ACK is pending" } stateManager.state = State.AwaitingResetAckRequested resetState() packetWriter.rescheduleTimeout(true) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index dbcefa2e..7f7cf46c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -88,7 +88,7 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val } else { if (connection.device.bondState == BluetoothDevice.BOND_BONDED) { Timber.w("Phone is bonded but watch is not paired") - //TODO: Request user to remove bond + BluetoothDevice::class.java.getMethod("removeBond").invoke(connection.device) emit(ConnectorState.PAIRING) requestPairing(connectionStatus) } else {