bluetooth cleanup wip

pull/8/head
geeksville 2020-01-27 14:54:35 -08:00
rodzic 52440d499e
commit 6edc89e2aa
7 zmienionych plików z 339 dodań i 150 usunięć

11
TODO.md
Wyświetl plik

@ -1,11 +1,17 @@
# High priority
* fix // FIXME hack for now - throw IdNotFoundException(id) in MeshService
* implement android side of mesh radio bluetooth link
* investigate the Signal SMS message flow path, see if I could just make Mesh a third peer to signal & sms?
* make signal work when there is no internet up
* make Signal rx path work
* send Signal message type. It seems to be? " public static final int WHISPER_TYPE = 2;
public static final int PREKEY_TYPE = 3;
public static final int SENDERKEY_TYPE = 4;
public static final int SENDERKEY_DISTRIBUTION_TYPE = 5;"
* optionally turn off crypto in signal
* clean up sw update code in device side
* change signal package ID
* make compose based access show mesh state
* add real messaging code/protobufs
@ -29,7 +35,7 @@ nanopb binaries available here: https://jpa.kapsi.fi/nanopb/download/ use nanopb
* use platform theme (dark or light)
* remove mixpanel analytics
* require user auth to pair with the device (i.e. press button on device to allow a new phone to pair with it).
Don't leave device discoverable. Don't let unpaired users do thing with device
Don't leave device discoverable. Don't let unpaired users do things with device
* remove example code boilerplate from the service
* switch from protobuf-java to protobuf-javalite - much faster and smaller, just no JSON debug printing
@ -64,3 +70,4 @@ Don't leave device discoverable. Don't let unpaired users do thing with device
* DONE add broadcasters for use by signal (node changes and packet received)
* DONE have signal declare receivers: https://developer.android.com/guide/components/broadcasts#manifest-declared-receivers
* fix // FIXME hack for now - throw IdNotFoundException(id) in MeshService

Wyświetl plik

@ -36,7 +36,6 @@ class MeshService : Service(), Logging {
private const val NODE_NUM_NO_MESH = -1
}
/// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf<String, String>()

Wyświetl plik

@ -1,11 +1,61 @@
package com.geeksville.mesh
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
import com.geeksville.android.DebugLogFile
import com.geeksville.android.Logging
import com.google.protobuf.util.JsonFormat
import java.util.*
/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface.
MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd
FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best
FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there.
FIXME - make sure this protocol is guaranteed robust and won't drop packets
"According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)).
In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes."
MAXPACKET is 256? look into what the lora lib uses. FIXME
Characteristics:
UUID
properties
description
8ba2bcc2-ee02-4a55-a531-c525c5e454d5
read
fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet).
After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this
mailbox.
f75c76d2-129e-4dad-a1dd-7866124401e7
write
toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len)
ed9da18c-a800-4f66-a670-aa7547e34453
read|notify|write
fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages
until it catches up with this number.
The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32
callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio.
When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio.
Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted.
Re: queue management
Not all messages are kept in the fromradio queue (filtered based on SubPacket):
* only the most recent Position and User messages for a particular node are kept
* all Data SubPackets are kept
* No WantNodeNum / DenyNodeNum messages are kept
A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging)
*/
/**
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
@ -36,6 +86,14 @@ class RadioInterfaceService : JobIntentService(), Logging {
*/
const val RECEIVE_FROMRADIO_ACTION = "$prefix.RECEIVE_FROMRADIO"
private val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
private val BTM_FROMRADIO_CHARACTER =
UUID.fromString("8ba2bcc2-ee02-4a55-a531-c525c5e454d5")
private val BTM_TORADIO_CHARACTER =
UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7")
private val BTM_FROMNUM_CHARACTER =
UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")
/**
* Convenience method for enqueuing work in to this service.
*/
@ -64,6 +122,15 @@ class RadioInterfaceService : JobIntentService(), Logging {
}
}
private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter!!
}
// Both of these are created in onCreate()
private lateinit var device: BluetoothDevice
private lateinit var safe: SafeBluetooth
lateinit var sentPacketsLog: DebugLogFile // inited in onCreate
fun broadcastConnectionChanged(isConnected: Boolean) {
@ -91,6 +158,23 @@ class RadioInterfaceService : JobIntentService(), Logging {
override fun onCreate() {
super.onCreate()
// FIXME, the lifecycle is wrong for jobintentservice, change to a regular service
// FIXME, let user GUI select which device we are talking to
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
device = bluetoothAdapter.getRemoteDevice("B4:E6:2D:EA:32:B7")
// Note this constructor also does no comm
safe = SafeBluetooth(this, device)
// FIXME, pass in true for autoconnect - so we will autoconnect whenever the radio
// comes in range (even if we made this connect call long ago when we got powered on)
// see https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble for
// more info
// FIXME, can't use sync connect here - because it could take a LONG time
// FIXME, don't use sync api at all - because our operations are so simple and atomic
safe.connect(true)
sentPacketsLog = DebugLogFile(this, "sent_log.json")
}

Wyświetl plik

@ -0,0 +1,232 @@
package com.geeksville.mesh
import android.bluetooth.*
import android.content.Context
import com.geeksville.android.Logging
import com.geeksville.concurrent.CallbackContinuation
import com.geeksville.concurrent.Continuation
import com.geeksville.concurrent.SyncContinuation
import java.io.IOException
/**
* Uses coroutines to safely access a bluetooth GATT device with a synchronous API
*
* The BTLE API on android is dumb. You can only have one outstanding operation in flight to
* the device. If you try to do something when something is pending, the operation just returns
* false. You are expected to chain your operations from the results callbacks.
*
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations.
*/
class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) :
Logging {
/// Timeout before we declare a bluetooth operation failed
private val timeoutMsec = 30 * 1000L
/// Users can access the GATT directly as needed
lateinit var gatt: BluetoothGatt
var state = BluetoothProfile.STATE_DISCONNECTED
private var currentWork: BluetoothContinuation? = null
private val workQueue = mutableListOf<BluetoothContinuation>()
/**
* a schedulable bit of bluetooth work, includes both the closure to call to start the operation
* and the completion (either async or sync) to call when it completes
*/
class BluetoothContinuation(
val completion: com.geeksville.concurrent.Continuation<*>,
private val startWorkFn: () -> Boolean
) {
/// Start running a queued bit of work, return true for success or false for fatal bluetooth error
fun startWork() = startWorkFn()
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
info("new bluetooth connection state $newState")
state = newState
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
//logAssert(workQueue.isNotEmpty())
//val work = workQueue.removeAt(0)
completeWork(status, Unit)
}
BluetoothProfile.STATE_DISCONNECTED -> {
// cancel any ops
failAllWork(IOException("Lost connection"))
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
completeWork(status, Unit)
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
completeWork(status, characteristic)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
completeWork(status, characteristic)
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
completeWork(status, mtu)
}
}
/// If we have work we can do, start doing it.
private fun startNewWork() {
logAssert(currentWork == null)
if (workQueue.isNotEmpty()) {
val newWork = workQueue.removeAt(0)
currentWork = newWork
newWork.startWork()
}
}
private fun <T> queueWork(cont: Continuation<T>, initFn: () -> Boolean) {
val btCont = BluetoothContinuation(cont, initFn)
synchronized(workQueue) {
workQueue.add(btCont)
// if we don't have any outstanding operations, run first item in queue
if (currentWork == null)
startNewWork()
}
}
/**
* Called from our big GATT callback, completes the current job and then schedules a new one
*/
private fun <T : Any> completeWork(status: Int, res: T) {
// startup next job in queue before calling the completion handler
val work =
synchronized(workQueue) {
val w = currentWork!! // will throw if null, which is helpful
currentWork = null // We are now no longer working on anything
startNewWork()
w
}
if (status != 0)
work.completion.resumeWithException(IOException("Bluetooth status=$status"))
else
work.completion.resume(Result.success(res) as Result<Nothing>) // FIXME, will this work?
}
/**
* Something went wrong, abort all queued
*/
private fun failAllWork(ex: Exception) {
synchronized(workQueue) {
workQueue.forEach {
it.completion.resumeWithException(ex)
}
workQueue.clear()
}
}
/// helper glue to make sync continuations and then wait for the result
private fun <T> makeSync(wrappedFn: (SyncContinuation<T>) -> Unit): T {
val cont = SyncContinuation<T>()
wrappedFn(cont)
return cont.await(timeoutMsec)
}
// FIXME, pass in true for autoconnect - so we will autoconnect whenever the radio
// comes in range (even if we made this connect call long ago when we got powered on)
// see https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble for
// more info.
// Otherwise if you pass in false, it will try to connect now and will timeout and fail in 30 seconds.
private fun queueConnect(autoConnect: Boolean = false, cont: Continuation<Unit>) {
queueWork(cont) {
val g = device.connectGatt(context, autoConnect, gattCallback)
if (g != null)
gatt = g
g != null
}
}
fun asyncConnect(autoConnect: Boolean = false, cb: (Result<Unit>) -> Unit) {
queueConnect(autoConnect, CallbackContinuation(cb))
}
fun connect(autoConnect: Boolean = false) = makeSync<Unit> { queueConnect(autoConnect, it) }
private fun queueReadCharacteristic(
c: BluetoothGattCharacteristic,
cont: Continuation<BluetoothGattCharacteristic>
) = queueWork(cont) { gatt.readCharacteristic(c) }
fun asyncReadCharacteristic(
c: BluetoothGattCharacteristic,
cb: (Result<BluetoothGattCharacteristic>) -> Unit
) = queueReadCharacteristic(c, CallbackContinuation(cb))
fun readCharacteristic(c: BluetoothGattCharacteristic): BluetoothGattCharacteristic =
makeSync { queueReadCharacteristic(c, it) }
private fun queueDiscoverServices(cont: Continuation<Unit>) {
queueWork(cont) {
gatt.discoverServices()
}
}
fun asyncDiscoverServices(cb: (Result<Unit>) -> Unit) {
queueDiscoverServices(CallbackContinuation(cb))
}
fun discoverServices() = makeSync<Unit> { queueDiscoverServices(it) }
private fun queueRequestMtu(
len: Int,
cont: Continuation<Int>
) = queueWork(cont) { gatt.requestMtu(len) }
fun asyncRequestMtu(
len: Int,
cb: (Result<Int>) -> Unit
) = queueRequestMtu(len, CallbackContinuation(cb))
fun requestMtu(len: Int): Int =
makeSync { queueRequestMtu(len, it) }
private fun queueWriteCharacteristic(
c: BluetoothGattCharacteristic,
cont: Continuation<BluetoothGattCharacteristic>
) = queueWork(cont) { gatt.writeCharacteristic(c) }
fun asyncWriteCharacteristic(
c: BluetoothGattCharacteristic,
cb: (Result<BluetoothGattCharacteristic>) -> Unit
) = queueWriteCharacteristic(c, CallbackContinuation(cb))
fun writeCharacteristic(c: BluetoothGattCharacteristic): BluetoothGattCharacteristic =
makeSync { queueWriteCharacteristic(c, it) }
fun disconnect() {
gatt.disconnect()
}
}

Wyświetl plik

@ -40,7 +40,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
fun startUpdate() {
info("starting update")
val sync = SyncBluetoothDevice(this@SoftwareUpdateService, device)
val sync = SafeBluetooth(this@SoftwareUpdateService, device)
val firmwareStream = assets.open("firmware.bin")
val firmwareCrc = CRC32()

Wyświetl plik

@ -1,142 +0,0 @@
package com.geeksville.mesh
import android.bluetooth.*
import android.content.Context
import com.geeksville.android.Logging
import com.geeksville.concurrent.SyncContinuation
import com.geeksville.concurrent.suspend
import java.io.IOException
/**
* Uses coroutines to safely access a bluetooth GATT device with a synchronous API
*
* The BTLE API on android is dumb. You can only have one outstanding operation in flight to
* the device. If you try to do something when something is pending, the operation just returns
* false. You are expected to chain your operations from the results callbacks.
*
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations.
*/
class SyncBluetoothDevice(private val context: Context, private val device: BluetoothDevice) :
Logging {
private var pendingServiceDesc: SyncContinuation<Unit>? = null
private var pendingMtu: SyncContinuation<Int>? = null
private var pendingWriteC: SyncContinuation<Unit>? = null
private var pendingReadC: SyncContinuation<BluetoothGattCharacteristic>? = null
private var pendingConnect: SyncContinuation<Unit>? = null
/// Timeout before we declare a bluetooth operation failed
private val timeoutMsec = 30 * 1000L
var state = BluetoothProfile.STATE_DISCONNECTED
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
info("new bluetooth connection state $newState")
state = newState
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
if (pendingConnect != null) { // If someone was waiting to connect unblock them
pendingConnect!!.resume(Unit)
pendingConnect = null
}
}
BluetoothProfile.STATE_DISCONNECTED -> {
// cancel any ops
val pendings = listOf(
pendingMtu,
pendingServiceDesc,
pendingWriteC,
pendingReadC,
pendingConnect
)
pendings.filterNotNull().forEach {
it.resumeWithException(IOException("Lost connection"))
}
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status != 0)
pendingServiceDesc!!.resumeWithException(IOException("Bluetooth status=$status"))
else
pendingServiceDesc!!.resume(Unit)
pendingServiceDesc = null
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status != 0)
pendingReadC!!.resumeWithException(IOException("Bluetooth status=$status"))
else
pendingReadC!!.resume(characteristic)
pendingReadC = null
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status != 0)
pendingWriteC!!.resumeWithException(IOException("Bluetooth status=$status"))
else
pendingWriteC!!.resume(Unit)
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
if (status != 0)
pendingMtu!!.resumeWithException(IOException("Bluetooth status=$status"))
else
pendingMtu!!.resume(mtu)
pendingMtu = null
}
}
/// Users can access the GATT directly as needed
lateinit var gatt: BluetoothGatt
fun connect() =
suspend<Unit>(timeoutMsec) { cont ->
pendingConnect = cont
gatt = device.connectGatt(context, false, gattCallback)!!
}
fun discoverServices() =
suspend<Unit>(timeoutMsec) { cont ->
pendingServiceDesc = cont
logAssert(gatt.discoverServices())
}
/// Returns the actual MTU size used
fun requestMtu(len: Int) = suspend<Int>(timeoutMsec) { cont ->
pendingMtu = cont
logAssert(gatt.requestMtu(len))
}
fun writeCharacteristic(c: BluetoothGattCharacteristic) =
suspend<Unit>(timeoutMsec) { cont ->
pendingWriteC = cont
logAssert(gatt.writeCharacteristic(c))
}
fun readCharacteristic(c: BluetoothGattCharacteristic) =
suspend<BluetoothGattCharacteristic>(timeoutMsec) { cont ->
pendingReadC = cont
logAssert(gatt.readCharacteristic(c))
}
fun disconnect() {
gatt.disconnect()
}
}

Wyświetl plik

@ -45,6 +45,7 @@ message Position {
double latitude = 1;
double longitude = 2;
int32 altitude = 3;
int32 battery_level = 4; // 0-100
}
// Times are typically not sent over the mesh, but they will be added to any Packet (chain of SubPacket)
@ -121,6 +122,12 @@ message MeshPacket {
// Full settings (center freq, spread factor, pre-shared secret key etc...) needed to configure a radio
message RadioConfig {
// FIXME
// If true, radio should not try to be smart about what packets to queue to the phone
bool keep_all_packets = 100;
// If true, we will try to capture all the packets sent on the mesh, not just the ones destined to our node.
bool promiscuous_mode = 101;
}
/**
@ -149,7 +156,6 @@ SET_CONFIG (switches device to a new set of radio params and preshared key, drop
message NodeInfo {
int32 num = 1; // the node number
User user = 2;
int32 battery_level = 3; // 0-100
Position position = 4;
Time last_seen = 5;
}
@ -159,14 +165,17 @@ message NodeInfo {
// it will sit in that descriptor until consumed by the phone, at which point the next item in the FIFO
// will be populated. FIXME
message FromRadio {
// The packet num, used to allow the phone to request missing read packets from the FIFO, see our bluetooth docs
uint32 num = 1;
oneof variant {
MeshPacket packet = 1;
MeshPacket packet = 2;
/// Tells the phone what our node number is, can be -1 if we've not yet joined a mesh.
sint32 my_node_num = 2;
sint32 my_node_num = 3;
/// One packet is sent for each node in the on radio DB
NodeInfo node_info = 3;
NodeInfo node_info = 4;
}
}