Skip to content

Commit

Permalink
reinit device instead of dropping connection on quick reconnects
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Jun 10, 2024
1 parent c2be935 commit e87bd6b
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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)
}
Expand All @@ -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<ByteArray>? {
return connections[deviceAddress]?.incomingPebblePacketData
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -30,14 +32,36 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon
private val _incomingPebblePackets = Channel<ByteArray>(Channel.BUFFERED)
val incomingPebblePacketData: Flow<ByteArray> = _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<GattConnectionStateWithStatus?>(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" }
Expand All @@ -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")
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e87bd6b

Please sign in to comment.