Skip to content

Commit

Permalink
QEMU support
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Oct 12, 2023
1 parent 15d21e3 commit 7434e77
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package io.rebble.cobble.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import io.rebble.cobble.BuildConfig
import io.rebble.cobble.bluetooth.classic.BlueSerialDriver
import io.rebble.cobble.bluetooth.classic.SocketSerialDriver
import io.rebble.cobble.bluetooth.scan.BleScanner
import io.rebble.cobble.bluetooth.scan.ClassicScanner
import io.rebble.cobble.datasources.FlutterPreferences
Expand All @@ -30,30 +32,38 @@ class BlueCommon @Inject constructor(
fun startSingleWatchConnection(macAddress: String): Flow<SingleConnectionStatus> {
bleScanner.stopScan()
classicScanner.stopScan()

val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
val bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress)

Timber.d("Found Pebble device $bluetoothDevice'")
val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) {
PebbleBluetoothDevice(null, true, macAddress)
} else {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
PebbleBluetoothDevice(bluetoothAdapter.getRemoteDevice(macAddress))
}

val driver = getTargetTransport(bluetoothDevice)
this@BlueCommon.driver = driver

return driver.startSingleWatchConnection(bluetoothDevice)
}

fun getTargetTransport(device: BluetoothDevice): BlueIO {
private fun getTargetTransport(pebbleDevice: PebbleBluetoothDevice): BlueIO {
val btDevice = pebbleDevice.bluetoothDevice
return when {
device.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device
pebbleDevice.emulated -> {
SocketSerialDriver(
protocolHandler,
incomingPacketsListener
)
}
btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device
BlueLEDriver(context, protocolHandler, flutterPreferences, incomingPacketsListener)
}
device.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE
btDevice?.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE
BlueSerialDriver(
protocolHandler,
incomingPacketsListener
)
}
else -> throw IllegalArgumentException("Unknown device type: ${device.type}") // Can't contact device
else -> throw IllegalArgumentException("Unknown device type: ${btDevice?.type}") // Can't contact device
}
}
}
19 changes: 16 additions & 3 deletions android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@ import kotlinx.coroutines.flow.Flow

interface BlueIO {
@FlowPreview
fun startSingleWatchConnection(device: BluetoothDevice): Flow<SingleConnectionStatus>
fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow<SingleConnectionStatus>
}

data class PebbleBluetoothDevice (
val bluetoothDevice: BluetoothDevice?,
val emulated: Boolean,
val address: String
) {
constructor(bluetoothDevice: BluetoothDevice?, emulated: Boolean = false) :
this(
bluetoothDevice,
emulated,
bluetoothDevice?.address ?: throw IllegalArgumentException()
)
}

sealed class SingleConnectionStatus {
class Connecting(val watch: BluetoothDevice) : SingleConnectionStatus()
class Connected(val watch: BluetoothDevice) : SingleConnectionStatus()
class Connecting(val watch: PebbleBluetoothDevice) : SingleConnectionStatus()
class Connected(val watch: PebbleBluetoothDevice) : SingleConnectionStatus()
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ class BlueLEDriver(
}

@FlowPreview
override fun startSingleWatchConnection(device: BluetoothDevice): Flow<SingleConnectionStatus> = flow {
override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow<SingleConnectionStatus> = flow {
require(!device.emulated)
require(device.bluetoothDevice != null)
try {
coroutineScope {
if (device.type == BluetoothDevice.DEVICE_TYPE_CLASSIC || device.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN) {
if (device.bluetoothDevice.type == BluetoothDevice.DEVICE_TYPE_CLASSIC || device.bluetoothDevice.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN) {
throw IllegalArgumentException("Non-LE device should not use LE driver")
}

if (connectionState == LEConnectionState.CONNECTED && device.address == this@BlueLEDriver.targetPebble.address) {
if (connectionState == LEConnectionState.CONNECTED && device.bluetoothDevice.address == this@BlueLEDriver.targetPebble.address) {
Timber.w("startSingleWatchConnection called on already connected driver")
emit(SingleConnectionStatus.Connected(device))
} else if (connectionState != LEConnectionState.IDLE) { // If not in idle state this is a stale instance
Expand All @@ -145,10 +147,10 @@ class BlueLEDriver(

protocolHandler.openProtocol()

this@BlueLEDriver.targetPebble = device
this@BlueLEDriver.targetPebble = device.bluetoothDevice

val server = BlueGATTServer(
device,
device.bluetoothDevice,
context,
this,
protocolHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ class ConnectionLooper @Inject constructor(
Timber.d("Bluetooth is off. Waiting until it is on Cancel connection attempt.")

_connectionState.value = ConnectionState.WaitingForBluetoothToEnable(
BluetoothAdapter.getDefaultAdapter()?.getRemoteDevice(macAddress)
BluetoothAdapter.getDefaultAdapter()?.getRemoteDevice(macAddress)?.let { PebbleBluetoothDevice(it) }
)

getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn == true }
getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn }
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import android.bluetooth.BluetoothDevice

sealed class ConnectionState {
object Disconnected : ConnectionState()
class WaitingForBluetoothToEnable(val watch: BluetoothDevice?) : ConnectionState()
class WaitingForReconnect(val watch: BluetoothDevice?) : ConnectionState()
class Connecting(val watch: BluetoothDevice?) : ConnectionState()
class Connected(val watch: BluetoothDevice) : ConnectionState()
class WaitingForBluetoothToEnable(val watch: PebbleBluetoothDevice?) : ConnectionState()
class WaitingForReconnect(val watch: PebbleBluetoothDevice?) : ConnectionState()
class Connecting(val watch: PebbleBluetoothDevice?) : ConnectionState()
class Connected(val watch: PebbleBluetoothDevice) : ConnectionState()
}

val ConnectionState.watchOrNull: BluetoothDevice?
val ConnectionState.watchOrNull: PebbleBluetoothDevice?
get() {
return when (this) {
is ConnectionState.Connecting -> watch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ package io.rebble.cobble.bluetooth.classic

import android.bluetooth.BluetoothDevice
import io.rebble.cobble.bluetooth.BlueIO
import io.rebble.cobble.bluetooth.PebbleBluetoothDevice
import io.rebble.cobble.bluetooth.ProtocolIO
import io.rebble.cobble.bluetooth.SingleConnectionStatus
import io.rebble.cobble.datasources.IncomingPacketsListener
import io.rebble.libpebblecommon.ProtocolHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.*

Expand All @@ -22,13 +20,16 @@ class BlueSerialDriver(
) : BlueIO {
private var protocolIO: ProtocolIO? = null

override fun startSingleWatchConnection(device: BluetoothDevice): Flow<SingleConnectionStatus> = flow {
@FlowPreview
override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow<SingleConnectionStatus> = flow {
require(!device.emulated)
require(device.bluetoothDevice != null)
coroutineScope {
emit(SingleConnectionStatus.Connecting(device))

val btSerialUUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
val serialSocket = withContext(Dispatchers.IO) {
device.createRfcommSocketToServiceRecord(btSerialUUID).also {
device.bluetoothDevice.createRfcommSocketToServiceRecord(btSerialUUID).also {
it.connect()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.rebble.cobble.bluetooth.classic

import io.rebble.cobble.bluetooth.BlueIO
import io.rebble.cobble.bluetooth.PebbleBluetoothDevice
import io.rebble.cobble.bluetooth.SingleConnectionStatus
import io.rebble.cobble.bluetooth.readFully
import io.rebble.cobble.datasources.IncomingPacketsListener
import io.rebble.libpebblecommon.ProtocolHandler
import io.rebble.libpebblecommon.packets.QemuPacket
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.coroutines.coroutineContext

/**
* Used for testing app via a qemu pebble
*/
class SocketSerialDriver(
private val protocolHandler: ProtocolHandler,
private val incomingPacketsListener: IncomingPacketsListener
): BlueIO {

private var inputStream: InputStream? = null
private var outputStream: OutputStream? = null

private suspend fun readLoop() {
try {
val buf: ByteBuffer = ByteBuffer.allocate(8192)

while (coroutineContext.isActive) {
val inputStream = inputStream ?: break
/* READ PACKET META */
inputStream.readFully(buf, 0, 4)

val qemuPacket = QemuPacket.deserialize(buf.array().asUByteArray())
if (qemuPacket.protocol.get() != UShort.MAX_VALUE) {
Timber.d("QEMU packet ${qemuPacket.protocol.get()}")
}
val sppPacket = qemuPacket as? QemuPacket.QemuSPP ?: continue

buf.rewind()
inputStream.readFully(buf, 4, sppPacket.length.get().toInt())
buf.rewind()

val metBuf = ByteBuffer.wrap(buf.array())
metBuf.order(ByteOrder.BIG_ENDIAN)
val length = metBuf.short
val endpoint = metBuf.short
if (length < 0 || length > buf.capacity()) {
Timber.w("Invalid length in packet (EP ${endpoint.toUShort()}): got ${length.toUShort()}")
continue
}

Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}")

buf.rewind()
val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES))
buf.get(packet, 0, packet.size)
incomingPacketsListener.receivedPackets.emit(packet)
protocolHandler.receivePacket(packet.toUByteArray())
}
} finally {
Timber.e("Read loop returning")
try {
withContext(Dispatchers.IO) {
inputStream?.close()
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
inputStream = null
}

try {
withContext(Dispatchers.IO) {
outputStream?.close()
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
outputStream = null
}
}
}

@FlowPreview
override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow<SingleConnectionStatus> = flow {
val host = device.address
coroutineScope {
emit(SingleConnectionStatus.Connecting(device))

val serialSocket = withContext(Dispatchers.IO) {
Socket(host, 12344)
}

delay(8000)

val sendLoop = launch {
protocolHandler.startPacketSendingLoop(::sendPacket)
}

inputStream = serialSocket.inputStream
outputStream = serialSocket.outputStream

readLoop()
try {
withContext(Dispatchers.IO) {
serialSocket.close()
}
} catch (_: IOException) {
}
sendLoop.cancel()
}
}

private suspend fun sendPacket(bytes: UByteArray): Boolean {
//Timber.d("Sending packet of EP ${PebblePacket(bytes.toUByteArray()).endpoint}")
val qemuPacket = QemuPacket.QemuSPP(bytes)
val outputStream = outputStream ?: return false
withContext(Dispatchers.IO) {
outputStream.write(qemuPacket.serialize().toByteArray())
}
return true
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package io.rebble.cobble.bridges.common

import android.bluetooth.le.ScanResult
import io.rebble.cobble.BuildConfig
import io.rebble.cobble.bluetooth.BluePebbleDevice
import io.rebble.cobble.bluetooth.scan.BleScanner
import io.rebble.cobble.bluetooth.scan.ClassicScanner
import io.rebble.cobble.bridges.FlutterBridge
import io.rebble.cobble.bridges.ui.BridgeLifecycleController
import io.rebble.cobble.pigeons.ListWrapper
import io.rebble.cobble.pigeons.Pigeons
import io.rebble.cobble.pigeons.Pigeons.PebbleScanDevicePigeon
import io.rebble.cobble.pigeons.toMapExt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
Expand All @@ -30,6 +34,16 @@ class ScanFlutterBridge @Inject constructor(
coroutineScope.launch {
scanCallbacks.onScanStarted { }

if (BuildConfig.DEBUG) {
scanCallbacks.onScanUpdate(ListWrapper(listOf(PebbleScanDevicePigeon().also {
it.address = "10.0.2.2" //TODO: make configurable
it.name = "Emulator"
it.firstUse = false
it.runningPRF = false
it.serialNumber = "EMULATOR"
}.toMapExt()))) {}
}

bleScanner.getScanFlow().collect { foundDevices ->
scanCallbacks.onScanUpdate(
ListWrapper(foundDevices.map { it.toPigeon().toMapExt() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.IntentSender
import android.os.Build
import io.rebble.cobble.BuildConfig
import io.rebble.cobble.MainActivity
import io.rebble.cobble.bluetooth.ConnectionLooper
import io.rebble.cobble.bridges.FlutterBridge
Expand Down Expand Up @@ -88,6 +89,11 @@ class ConnectionUiFlutterBridge @Inject constructor(

@TargetApi(Build.VERSION_CODES.O)
private fun associateWithCompanionDeviceManager(macAddress: String) {
if (BuildConfig.DEBUG && !macAddress.contains(":")) {
openConnectionToWatch(macAddress)
return
}

val companionDeviceManager =
activity.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager

Expand Down
Loading

0 comments on commit 7434e77

Please sign in to comment.