-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PPoGATT connection, session handlers
- Loading branch information
Showing
8 changed files
with
617 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
.../pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package io.rebble.cobble.bluetooth.ble | ||
|
||
import kotlinx.coroutines.channels.Channel | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.consumeAsFlow | ||
|
||
object PPoGLinkStateManager { | ||
private val states = mutableMapOf<String, Channel<PPoGLinkState>>() | ||
|
||
fun getState(deviceAddress: String): Flow<PPoGLinkState> { | ||
return states.getOrPut(deviceAddress) { | ||
Channel(Channel.BUFFERED) | ||
}.consumeAsFlow() | ||
} | ||
|
||
fun removeState(deviceAddress: String) { | ||
states.remove(deviceAddress) | ||
} | ||
|
||
fun updateState(deviceAddress: String, state: PPoGLinkState) { | ||
states.getOrPut(deviceAddress) { | ||
Channel(Channel.BUFFERED) | ||
}.trySend(state) | ||
} | ||
} | ||
|
||
enum class PPoGLinkState { | ||
Closed, | ||
ReadyForSession, | ||
SessionOpen | ||
} |
118 changes: 118 additions & 0 deletions
118
android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package io.rebble.cobble.bluetooth.ble | ||
|
||
import androidx.annotation.RequiresPermission | ||
import io.rebble.libpebblecommon.ble.GATTPacket | ||
import kotlinx.coroutines.* | ||
import timber.log.Timber | ||
import java.io.Closeable | ||
import java.util.LinkedList | ||
import kotlin.jvm.Throws | ||
|
||
class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val serviceConnection: PPoGServiceConnection, private val onTimeout: () -> Unit): Closeable { | ||
private var metaWaitingToSend: GATTPacket? = null | ||
private val dataWaitingToSend: LinkedList<GATTPacket> = LinkedList() | ||
private val inflightPackets: LinkedList<GATTPacket> = LinkedList() | ||
var txWindow = 1 | ||
private var timeoutJob: Job? = null | ||
|
||
companion object { | ||
private const val PACKET_ACK_TIMEOUT_MILLIS = 10_000L | ||
} | ||
|
||
suspend fun sendOrQueuePacket(packet: GATTPacket) { | ||
if (packet.type == GATTPacket.PacketType.DATA) { | ||
dataWaitingToSend.add(packet) | ||
} else { | ||
metaWaitingToSend = packet | ||
} | ||
sendNextPacket() | ||
} | ||
|
||
fun cancelTimeout() { | ||
timeoutJob?.cancel() | ||
} | ||
|
||
suspend fun onAck(packet: GATTPacket) { | ||
require(packet.type == GATTPacket.PacketType.ACK) | ||
for (waitingPacket in dataWaitingToSend.iterator()) { | ||
if (waitingPacket.sequence == packet.sequence) { | ||
dataWaitingToSend.remove(waitingPacket) | ||
break | ||
} | ||
} | ||
if (!inflightPackets.contains(packet)) { | ||
Timber.w("Received ACK for packet not in flight") | ||
return | ||
} | ||
var ackedPacket: GATTPacket? = null | ||
|
||
// remove packets until the acked packet | ||
while (ackedPacket != packet) { | ||
ackedPacket = inflightPackets.poll() | ||
} | ||
sendNextPacket() | ||
rescheduleTimeout() | ||
} | ||
|
||
@Throws(SecurityException::class) | ||
private suspend fun sendNextPacket() { | ||
if (metaWaitingToSend == null && dataWaitingToSend.isEmpty()) { | ||
return | ||
} | ||
|
||
val packet = if (metaWaitingToSend != null) { | ||
metaWaitingToSend | ||
} else { | ||
if (inflightPackets.size > txWindow) { | ||
return | ||
} else { | ||
dataWaitingToSend.peek() | ||
} | ||
} | ||
|
||
if (packet == null) { | ||
return | ||
} | ||
|
||
if (packet.type !in stateManager.state.allowedTxTypes) { | ||
Timber.e("Attempted to send packet of type ${packet.type} in state ${stateManager.state}") | ||
return | ||
} | ||
|
||
if (!sendPacket(packet)) { | ||
return | ||
} | ||
|
||
if (packet.type == GATTPacket.PacketType.DATA) { | ||
dataWaitingToSend.poll() | ||
inflightPackets.offer(packet) | ||
} else { | ||
metaWaitingToSend = null | ||
} | ||
|
||
rescheduleTimeout() | ||
|
||
sendNextPacket() | ||
} | ||
|
||
fun rescheduleTimeout(force: Boolean = false) { | ||
timeoutJob?.cancel() | ||
if (inflightPackets.isNotEmpty() || force) { | ||
timeoutJob = scope.launch { | ||
delay(PACKET_ACK_TIMEOUT_MILLIS) | ||
onTimeout() | ||
} | ||
} | ||
} | ||
|
||
@RequiresPermission("android.permission.BLUETOOTH_CONNECT") | ||
private suspend fun sendPacket(packet: GATTPacket): Boolean { | ||
val data = packet.toByteArray() | ||
require(data.size > stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} | ||
return serviceConnection.writeData(data) | ||
} | ||
|
||
override fun close() { | ||
timeoutJob?.cancel() | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
...le_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package io.rebble.cobble.bluetooth.ble | ||
|
||
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket | ||
import io.rebble.libpebblecommon.structmapper.SUShort | ||
import io.rebble.libpebblecommon.structmapper.StructMapper | ||
import io.rebble.libpebblecommon.util.DataBuffer | ||
import kotlinx.coroutines.flow.flow | ||
import java.nio.ByteBuffer | ||
import kotlin.math.min | ||
|
||
class PPoGPebblePacketAssembler { | ||
private var data: ByteBuffer? = null | ||
|
||
/** | ||
* Emits one or more [PebblePacket]s if the data is complete. | ||
*/ | ||
fun assemble(dataToAdd: ByteArray) = flow { | ||
val dataToAddBuf = ByteBuffer.wrap(dataToAdd) | ||
while (dataToAddBuf.hasRemaining()) { | ||
if (data == null) { | ||
if (dataToAddBuf.remaining() < 4) { | ||
throw PPoGPebblePacketAssemblyException("Not enough data for header") | ||
} | ||
beginAssembly(dataToAddBuf.slice()) | ||
dataToAddBuf.position(dataToAddBuf.position() + 4) | ||
} | ||
|
||
val remaining = data!!.remaining() | ||
val toRead = min(remaining, dataToAddBuf.remaining()) | ||
data!!.put(dataToAddBuf.array(), dataToAddBuf.position(), toRead) | ||
dataToAddBuf.position(dataToAddBuf.position() + toRead) | ||
|
||
if (data!!.remaining() == 0) { | ||
data!!.flip() | ||
val packet = PebblePacket.deserialize(data!!.array().toUByteArray()) | ||
emit(packet) | ||
clear() | ||
} | ||
} | ||
} | ||
|
||
private fun beginAssembly(headerSlice: ByteBuffer) { | ||
val meta = StructMapper() | ||
val length = SUShort(meta) | ||
val ep = SUShort(meta) | ||
meta.fromBytes(DataBuffer(headerSlice.array().asUByteArray())) | ||
val packetLength = length.get() | ||
data = ByteBuffer.allocate(packetLength.toInt()) | ||
} | ||
|
||
fun clear() { | ||
data = null | ||
} | ||
} | ||
|
||
class PPoGPebblePacketAssemblyException(message: String) : Exception(message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.