we now fetch any new rxmessages when they arrive at the radio

This commit is contained in:
geeksville 2020-02-10 15:31:56 -08:00
parent 6244556f8b
commit 10ad07e136
11 changed files with 110 additions and 43 deletions

View file

@ -0,0 +1,14 @@
package com.geeksville.mesh.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(mContext: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// FIXME - start listening for bluetooth messages from our device
}
}
}

View file

@ -0,0 +1,16 @@
package com.geeksville.mesh.service
const val prefix = "com.geeksville.mesh"
//
// standard EXTRA bundle definitions
//
// a bool true means now connected, false means not
const val EXTRA_CONNECTED = "$prefix.Connected"
const val EXTRA_PAYLOAD = "$prefix.Payload"
const val EXTRA_SENDER = "$prefix.Sender"
const val EXTRA_NODEINFO = "$prefix.NodeInfo"
const val EXTRA_TYP = "$prefix.Typ"

View file

@ -0,0 +1,567 @@
package com.geeksville.mesh.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.*
import android.graphics.Color
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MIN
import com.geeksville.android.Logging
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.IRadioInterfaceService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.util.exceptionReporter
import com.geeksville.util.toOneLineString
import com.geeksville.util.toRemoteExceptions
import com.google.protobuf.ByteString
import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
class RadioNotConnectedException() : Exception("Can't find radio")
// model objects that directly map to the corresponding protobufs
@Parcelize
data class MeshUser(val id: String, val longName: String, val shortName: String) :
Parcelable
@Parcelize
data class Position(val latitude: Double, val longitude: Double, val altitude: Int) :
Parcelable
@Parcelize
data class NodeInfo(
val num: Int, // This is immutable, and used as a key
var user: MeshUser? = null,
var position: Position? = null,
var lastSeen: Long? = null
) : Parcelable
/**
* Handles all the communication with android apps. Also keeps an internal model
* of the network state.
*
* Note: this service will go away once all clients are unbound from it.
*/
class MeshService : Service(), Logging {
companion object {
/// Intents broadcast by MeshService
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA"
const val ACTION_NODE_CHANGE = "$prefix.NODE_CHANGE"
const val ACTION_MESH_CONNECTED = "$prefix.MESH_CONNECTED"
class IdNotFoundException(id: String) : Exception("ID not found $id")
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
class NotInMeshException() : Exception("We are not yet in a mesh")
/// If we haven't yet received a node number from the radio
private const val NODE_NUM_UNKNOWN = -2
/// If the radio hasn't yet joined a mesh (i.e. no nodenum assigned)
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>()
private var radioService: IRadioInterfaceService? = null
/*
see com.geeksville.mesh broadcast intents
// RECEIVED_OPAQUE for data received from other nodes
// NODE_CHANGE for new IDs appearing or disappearing
// ACTION_MESH_CONNECTED for losing/gaining connection to the packet radio (note, this is not
the same as RadioInterfaceService.RADIO_CONNECTED_ACTION, because it implies we have assembled a valid
node db.
*/
private fun explicitBroadcast(intent: Intent) {
sendBroadcast(intent) // We also do a regular (not explicit broadcast) so any context-registered rceivers will work
clientPackages.forEach {
intent.setClassName(it.value, it.key)
sendBroadcast(intent)
}
}
/**
* The RECEIVED_OPAQUE:
* Payload will be the raw bytes which were contained within a MeshPacket.Opaque field
* Sender will be a user ID string
* Type will be the Data.Type enum code for this payload
*/
private fun broadcastReceivedData(senderId: String, payload: ByteArray, typ: Int) {
val intent = Intent(ACTION_RECEIVED_DATA)
intent.putExtra(EXTRA_SENDER, senderId)
intent.putExtra(EXTRA_PAYLOAD, payload)
intent.putExtra(EXTRA_TYP, typ)
explicitBroadcast(intent)
}
private fun broadcastNodeChange(info: NodeInfo) {
debug("Broadcasting node change $info")
val intent = Intent(ACTION_NODE_CHANGE)
intent.putExtra(EXTRA_NODEINFO, info)
explicitBroadcast(intent)
}
/// Safely access the radio service, if not connected an exception will be thrown
private val connectedRadio: IRadioInterfaceService
get() {
val s = radioService
if (s == null || !isConnected)
throw RadioNotConnectedException()
return s
}
/// Send a command/packet to our radio. But cope with the possiblity that we might start up
/// before we are fully bound to the RadioInterfaceService
private fun sendToRadio(p: ToRadio.Builder) {
val b = p.build().toByteArray()
connectedRadio.sendToRadio(b)
}
override fun onBind(intent: Intent?): IBinder? {
return binder
}
private val radioConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val m = IRadioInterfaceService.Stub.asInterface(
service
)
radioService = m
}
override fun onServiceDisconnected(name: ComponentName?) {
radioService = null
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = "my_service"
val channelName = "My Background Service"
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_HIGH
)
chan.lightColor = Color.BLUE
chan.importance = NotificationManager.IMPORTANCE_NONE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
private fun startForeground() {
// val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val notificationBuilder = NotificationCompat.Builder(this, channelId)
val notification = notificationBuilder.setOngoing(true)
.setPriority(PRIORITY_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
//.setContentTitle("Meshtastic") // leave this off for now so our notification looks smaller
//.setContentText("Listening for mesh...")
.build()
startForeground(101, notification)
}
override fun onCreate() {
super.onCreate()
info("Creating mesh service")
/*
// This intent will be used if the user clicks on the item in the status bar
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0,
notificationIntent, 0
)
val notification: Notification = NotificationCompat.Builder(this)
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.setContentTitle("Meshtastic")
.setContentText("Listening for mesh...")
.setContentIntent(pendingIntent).build()
// We are required to call this within a few seconds of create
startForeground(1337, notification)
*/
startForeground()
// we listen for messages from the radio receiver _before_ trying to create the service
val filter = IntentFilter()
filter.addAction(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
filter.addAction(RadioInterfaceService.RADIO_CONNECTED_ACTION)
registerReceiver(radioInterfaceReceiver, filter)
// We in turn need to use the radiointerface service
val intent = Intent(this, RadioInterfaceService::class.java)
// intent.action = IMeshService::class.java.name
logAssert(bindService(intent, radioConnection, Context.BIND_AUTO_CREATE))
// the rest of our init will happen once we are in radioConnection.onServiceConnected
}
override fun onDestroy() {
info("Destroying mesh service")
unregisterReceiver(radioInterfaceReceiver)
unbindService(radioConnection)
radioService = null
super.onDestroy()
}
///
/// BEGINNING OF MODEL - FIXME, move elsewhere
///
/// Is our radio connected to the phone?
private var isConnected = false
/// We learn this from the node db sent by the device - it is stable for the entire session
private var ourNodeNum =
NODE_NUM_UNKNOWN
// The database of active nodes, index is the node number
private val nodeDBbyNodeNum = mutableMapOf<Int, NodeInfo>()
/// The database of active nodes, index is the node user ID string
/// NOTE: some NodeInfos might be in only nodeDBbyNodeNum (because we don't yet know
/// an ID). But if a NodeInfo is in both maps, it must be one instance shared by
/// both datastructures.
private val nodeDBbyID = mutableMapOf<String, NodeInfo>()
///
/// END OF MODEL
///
/// Map a nodenum to a node, or throw an exception if not found
private fun toNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: throw NodeNumNotFoundException(
n
)
/// Map a nodenum to the nodeid string, or throw an exception if not present
private fun toNodeID(n: Int) = toNodeInfo(n).user?.id
/// given a nodenum, return a db entry - creating if necessary
private fun getOrCreateNodeInfo(n: Int) =
nodeDBbyNodeNum.getOrPut(n) { -> NodeInfo(n) }
/// Map a userid to a node/ node num, or throw an exception if not found
private fun toNodeInfo(id: String) =
nodeDBbyID[id]
?: throw IdNotFoundException(
id
)
// ?: getOrCreateNodeInfo(10) // FIXME hack for now - throw IdNotFoundException(id)
private fun toNodeNum(id: String) = toNodeInfo(id).num
/// A helper function that makes it easy to update node info objects
private fun updateNodeInfo(nodeNum: Int, updatefn: (NodeInfo) -> Unit) {
val info = getOrCreateNodeInfo(nodeNum)
updatefn(info)
// This might have been the first time we know an ID for this node, so also update the by ID map
val userId = info.user?.id
if (userId != null)
nodeDBbyID[userId] = info
// parcelable is busted
// broadcastNodeChange(info)
}
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply {
from = ourNodeNum
if (from == NODE_NUM_NO_MESH)
throw NotInMeshException()
else if (from == NODE_NUM_UNKNOWN)
throw RadioNotConnectedException()
to = idNum
}
/// Generate a new mesh packet builder with our node as the sender, and the specified recipient
private fun newMeshPacketTo(id: String) = newMeshPacketTo(toNodeNum(id))
// Helper to make it easy to build a subpacket in the proper protobufs
private fun buildMeshPacket(
destId: String,
initFn: MeshProtos.SubPacket.Builder.() -> Unit
): MeshPacket = newMeshPacketTo(destId).apply {
payload = MeshProtos.SubPacket.newBuilder().also {
initFn(it)
}.build()
}.build()
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedData(fromNum: Int, data: MeshProtos.Data) {
val bytes = data.payload.toByteArray()
val fromId = toNodeID(fromNum)
/// the sending node ID if possible, else just its number
val fromString = fromId ?: fromId.toString()
fun forwardData() {
if (fromId == null)
warn("Ignoring data from $fromNum because we don't yet know its ID")
else {
debug("Received data from $fromId ${bytes.size}")
broadcastReceivedData(fromId, bytes, data.typValue)
}
}
when (data.typValue) {
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
debug(
"FIXME - don't long this: Received CLEAR_TEXT from $fromString: ${bytes.toString(
Charset.forName("UTF-8")
)}"
)
forwardData()
}
MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
warn(
"TODO ignoring CLEAR_READACK from $fromString"
)
MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE ->
forwardData()
else -> TODO()
}
}
/// Update our DB of users based on someone sending out a User subpacket
private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User) {
updateNodeInfo(fromNum) {
it.user = MeshUser(
p.id,
p.longName,
p.shortName
)
}
}
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedMeshPacket(packet: MeshPacket) {
val fromNum = packet.from
// FIXME, perhaps we could learn our node ID by looking at any to packets the radio
// decided to pass through to us (except for broadcast packets)
val toNum = packet.to
val p = packet.payload
// Update our last seen based on any valid timestamps
if (packet.rxTime != 0L) {
updateNodeInfo(fromNum) {
it.lastSeen = packet.rxTime
}
}
when (p.variantCase.number) {
MeshProtos.SubPacket.POSITION_FIELD_NUMBER ->
updateNodeInfo(fromNum) {
it.position = Position(
p.position.latitude,
p.position.longitude,
p.position.altitude
)
}
MeshProtos.SubPacket.DATA_FIELD_NUMBER ->
handleReceivedData(fromNum, p.data)
MeshProtos.SubPacket.USER_FIELD_NUMBER ->
handleReceivedUser(fromNum, p.user)
else -> TODO("Unexpected SubPacket variant")
}
}
/// Called when we gain/lose connection to our radio
private fun onConnectionChanged(c: Boolean) {
debug("onConnectionChanged connected=$c")
isConnected = c
if (c) {
// Do our startup init
// FIXME - don't do this until after we see that the radio is connected to the phone
//val sim = SimRadio(this@MeshService)
//sim.start() // Fake up our node id info and some past packets from other nodes
val myInfo = MeshProtos.MyNodeInfo.parseFrom(
connectedRadio.readMyNode()
)
ourNodeNum = myInfo.myNodeNum
// Ask for the current node DB
connectedRadio.restartNodeInfo()
// read all the infos until we get back null
var infoBytes = connectedRadio.readNodeInfo()
while (infoBytes != null) {
val info =
MeshProtos.NodeInfo.parseFrom(infoBytes)
debug("Received initial nodeinfo $info")
// Just replace/add any entry
updateNodeInfo(info.num) {
if (info.hasUser())
it.user =
MeshUser(
info.user.id,
info.user.longName,
info.user.shortName
)
if (info.hasPosition())
it.position = Position(
info.position.latitude,
info.position.longitude,
info.position.altitude
)
it.lastSeen = info.lastSeen
}
// advance to next
infoBytes = connectedRadio.readNodeInfo()
}
}
}
/**
* Receives messages from our BT radio service and processes them to update our model
* and send to clients as needed.
*/
private val radioInterfaceReceiver = object : BroadcastReceiver() {
// Important to never throw exceptions out of onReceive
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
debug("Received broadcast ${intent.action}")
when (intent.action) {
RadioInterfaceService.RADIO_CONNECTED_ACTION -> {
onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false))
// forward the connection change message to anyone who is listening to us. but change the action
// to prevent an infinite loop from us receiving our own broadcast. ;-)
intent.action = ACTION_MESH_CONNECTED
explicitBroadcast(intent)
}
RadioInterfaceService.RECEIVE_FROMRADIO_ACTION -> {
val proto =
MeshProtos.FromRadio.parseFrom(
intent.getByteArrayExtra(
EXTRA_PAYLOAD
)!!
)
info("Received from radio service: ${proto.toOneLineString()}")
when (proto.variantCase.number) {
MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket(
proto.packet
)
else -> TODO("Unexpected FromRadio variant")
}
}
else -> TODO("Unexpected radio interface broadcast")
}
}
}
private val binder = object : IMeshService.Stub() {
// Note: bound methods don't get properly exception caught/logged, so do that with a wrapper
// per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63
override fun subscribeReceiver(packageName: String, receiverName: String) =
toRemoteExceptions {
clientPackages[receiverName] = packageName
}
override fun setOwner(myId: String, longName: String, shortName: String) =
toRemoteExceptions {
error("TODO setOwner $myId : $longName : $shortName")
val user = MeshProtos.User.newBuilder().also {
it.id = myId
it.longName = longName
it.shortName = shortName
}.build()
// Also update our own map for our nodenum, by handling the packet just like packets from other users
if (ourNodeNum >= 0) {
handleReceivedUser(ourNodeNum, user)
}
// set my owner info
connectedRadio.writeOwner(user.toByteArray())
}
override fun sendData(destId: String, payloadIn: ByteArray, typ: Int) =
toRemoteExceptions {
info("sendData $destId <- ${payloadIn.size} bytes")
// encapsulate our payload in the proper protobufs and fire it off
val packet = buildMeshPacket(destId) {
data = MeshProtos.Data.newBuilder().also {
it.typ =
MeshProtos.Data.Type.SIGNAL_OPAQUE
it.payload = ByteString.copyFrom(payloadIn)
}.build()
}
sendToRadio(ToRadio.newBuilder().apply {
this.packet = packet
})
}
override fun getOnline(): Array<String> = toRemoteExceptions {
val r = nodeDBbyID.keys.toTypedArray()
info("in getOnline, count=${r.size}")
// return arrayOf("+16508675309")
r
}
override fun isConnected(): Boolean = toRemoteExceptions {
val r = this@MeshService.isConnected
info("in isConnected=$r")
r
}
}
}

View file

@ -0,0 +1,342 @@
package com.geeksville.mesh.service
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.os.IBinder
import com.geeksville.android.BinaryLogFile
import com.geeksville.android.Logging
import com.geeksville.concurrent.DeferredExecution
import com.geeksville.mesh.IRadioInterfaceService
import com.geeksville.util.toRemoteExceptions
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.
meshMyNodeCharacteristic("ea9f3f82-8dc4-4733-9452-1f6da28892a2", BLECharacteristic::PROPERTY_READ)
mynode - read/write this to access a MyNodeInfo protobuf
meshNodeInfoCharacteristic("d31e02e0-c8ab-4d3f-9cc9-0b8466bdabe8", BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_READ),
nodeinfo - read this to get a series of node infos (ending with a null empty record), write to this to restart the read statemachine that returns all the node infos
meshRadioCharacteristic("b56786c8-839a-44a1-b98e-a1724c4a0262", BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_READ),
radio - read/write this to access a RadioConfig protobuf
meshOwnerCharacteristic("6ff1d8b6-e2de-41e3-8c0b-8fa384f64eb6", BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_READ)
owner - read/write this to access a User protobuf
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,
* just does bluetooth comms etc...
*
* This service is not exposed outside of this process.
*
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
*/
class RadioInterfaceService : Service(), Logging {
companion object {
/**
* The RECEIVED_FROMRADIO
* Payload will be the raw bytes which were contained within a MeshProtos.FromRadio protobuf
*/
const val RECEIVE_FROMRADIO_ACTION = "$prefix.RECEIVE_FROMRADIO"
/**
* This is broadcast when connection state changed
*/
const val RADIO_CONNECTED_ACTION = "$prefix.CONNECT_CHANGED"
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")
/// mynode - read/write this to access a MyNodeInfo protobuf
private val BTM_MYNODE_CHARACTER =
UUID.fromString("ea9f3f82-8dc4-4733-9452-1f6da28892a2")
/// nodeinfo - read this to get a series of node infos (ending with a null empty record), write to this to restart the read statemachine that returns all the node infos
private val BTM_NODEINFO_CHARACTER =
UUID.fromString("d31e02e0-c8ab-4d3f-9cc9-0b8466bdabe8")
/// radio - read/write this to access a RadioConfig protobuf
private val BTM_RADIO_CHARACTER =
UUID.fromString("b56786c8-839a-44a1-b98e-a1724c4a0262")
/// owner - read/write this to access a User protobuf
private val BTM_OWNER_CHARACTER =
UUID.fromString("6ff1d8b6-e2de-41e3-8c0b-8fa384f64eb6")
/// This is public only so that SimRadio can bootstrap our message flow
fun broadcastReceivedFromRadio(context: Context, payload: ByteArray) {
val intent = Intent(RECEIVE_FROMRADIO_ACTION)
intent.putExtra(EXTRA_PAYLOAD, payload)
context.sendBroadcast(intent)
}
}
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 var safe: SafeBluetooth? = null
val service get() = safe!!.gatt!!.services.find { it.uuid == BTM_SERVICE_UUID }!!
private lateinit var fromNum: BluetoothGattCharacteristic
private val logSends = false
lateinit var sentPacketsLog: BinaryLogFile // inited in onCreate
private var isConnected = false
/// Work that users of our service want done, which might get deferred until after
/// we have completed our initial connection
private val clientOperations = DeferredExecution()
private fun broadcastConnectionChanged(isConnected: Boolean) {
debug("Broadcasting connection=$isConnected")
val intent = Intent(RADIO_CONNECTED_ACTION)
intent.putExtra(EXTRA_CONNECTED, isConnected)
sendBroadcast(intent)
}
/// Send a packet/command out the radio link
private fun handleSendToRadio(p: ByteArray) {
// For debugging/logging purposes ONLY we convert back into a protobuf for readability
// al proto = MeshProtos.ToRadio.parseFrom(p)
debug("sending to radio")
doWrite(BTM_TORADIO_CHARACTER, p)
if (logSends) {
sentPacketsLog.write(p)
sentPacketsLog.flush()
}
}
// Handle an incoming packet from the radio, broadcasts it as an android intent
private fun handleFromRadio(p: ByteArray) {
broadcastReceivedFromRadio(
this,
p
)
}
/// Attempt to read from the fromRadio mailbox, if data is found broadcast it to android apps
private fun doReadFromRadio() {
if (!isConnected)
warn("Abandoning fromradio read because we are not connected")
else {
val fromRadio = service.getCharacteristic(BTM_FROMRADIO_CHARACTER)
safe!!.asyncReadCharacteristic(fromRadio) {
val b = it.getOrThrow().value
if (b.isNotEmpty()) {
debug("Received ${b.size} bytes from radio")
handleFromRadio(b)
// Queue up another read, until we run out of packets
doReadFromRadio()
} else {
debug("Done reading from radio, fromradio is empty")
}
}
}
}
private fun onDisconnect() {
broadcastConnectionChanged(false)
isConnected = false
}
private fun onConnect(connRes: Result<Unit>) {
// This callback is invoked after we are connected
connRes.getOrThrow() // FIXME, instead just try to reconnect?
info("Connected to radio!")
// FIXME - no need to discover services more than once - instead use lazy() to use them in future attempts
safe!!.asyncDiscoverServices { discRes ->
discRes.getOrThrow() // FIXME, instead just try to reconnect?
debug("Discovered services!")
// we begin by setting our MTU size as high as it can go
safe!!.asyncRequestMtu(512) { mtuRes ->
debug("requested MTU result=$mtuRes")
mtuRes.getOrThrow() // FIXME - why sometimes is the result Unit!?!
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)
safe!!.setNotify(fromNum, true) {
debug("fromNum changed, so we are reading new messages")
doReadFromRadio()
}
// Now tell clients they can (finally use the api)
broadcastConnectionChanged(true)
isConnected = true
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio()
}
}
}
override fun onCreate() {
super.onCreate()
// 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)
val usetbeam = false
val address = if (usetbeam) "B4:E6:2D:EA:32:B7" else "24:6F:28:96:C9:2A"
val device = bluetoothAdapter?.getRemoteDevice(address)
if (device != null) {
info("Creating radio interface service. device=$address")
// Note this constructor also does no comm
val s = SafeBluetooth(this, device)
safe = s
// 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
s.asyncConnect(true, ::onConnect, ::onDisconnect)
} else {
error("Bluetooth adapter not found, assuming running on the emulator!")
}
if (logSends)
sentPacketsLog = BinaryLogFile(this, "sent_log.pb")
}
override fun onDestroy() {
info("Destroying radio interface service")
if (logSends)
sentPacketsLog.close()
safe?.disconnect()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
return binder;
}
/**
* do a synchronous write operation
*/
private fun doWrite(uuid: UUID, a: ByteArray) = toRemoteExceptions {
if (!isConnected)
throw RadioNotConnectedException()
else {
debug("queuing ${a.size} bytes to $uuid")
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
val toRadio = service.getCharacteristic(uuid)
toRadio.value = a
safe!!.writeCharacteristic(toRadio)
debug("write of ${a.size} bytes completed")
}
}
/**
* do a synchronous read operation
*/
private fun doRead(uuid: UUID): ByteArray? = toRemoteExceptions {
if (!isConnected)
throw RadioNotConnectedException()
else {
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
val toRadio = service.getCharacteristic(uuid)
var a = safe!!.readCharacteristic(toRadio).value
debug("Read of $uuid got ${a.size} bytes")
if (a.isEmpty()) // An empty bluetooth response is converted to a null response for our clients
a = null
a
}
}
private val binder = object : IRadioInterfaceService.Stub() {
// A write of any size to nodeinfo means restart reading
override fun restartNodeInfo() = doWrite(BTM_NODEINFO_CHARACTER, ByteArray(0))
override fun readMyNode() = doRead(BTM_MYNODE_CHARACTER)!!
override fun sendToRadio(a: ByteArray) = handleSendToRadio(a)
override fun readRadioConfig() = doRead(BTM_RADIO_CHARACTER)!!
override fun readOwner() = doRead(BTM_OWNER_CHARACTER)!!
override fun writeOwner(owner: ByteArray) = doWrite(BTM_OWNER_CHARACTER, owner)
override fun writeRadioConfig(config: ByteArray) = doWrite(BTM_RADIO_CHARACTER, config)
override fun readNodeInfo() = doRead(BTM_NODEINFO_CHARACTER)
}
}

View file

@ -0,0 +1,450 @@
package com.geeksville.mesh.service
import android.bluetooth.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.geeksville.android.Logging
import com.geeksville.concurrent.CallbackContinuation
import com.geeksville.concurrent.Continuation
import com.geeksville.concurrent.SyncContinuation
import com.geeksville.util.exceptionReporter
import java.io.IOException
import java.util.*
/**
* 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
var timeoutMsec = 30 * 1000L
/// Users can access the GATT directly as needed
var gatt: BluetoothGatt? = null
var state = BluetoothProfile.STATE_DISCONNECTED
private var currentWork: BluetoothContinuation? = null
private val workQueue = mutableListOf<BluetoothContinuation>()
// Called for reconnection attemps
private var connectionCallback: ((Result<Unit>) -> Unit)? = null
private var lostConnectCallback: (() -> Unit)? = null
/// from characteristic UUIDs to the handler function for notfies
private val notifyHandlers = mutableMapOf<UUID, (BluetoothGattCharacteristic) -> Unit>()
/// When we see the BT stack getting disabled/renabled we handle that as a connect/disconnect event
private val btStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val newstate = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
when (newstate) {
// Simulate a disconnection if the user disables bluetooth entirely
BluetoothAdapter.STATE_OFF -> {
if (state == BluetoothProfile.STATE_CONNECTED)
gattCallback.onConnectionStateChange(
gatt!!,
0,
BluetoothProfile.STATE_DISCONNECTED
)
else
debug("We were not connected, so ignoring bluetooth shutdown")
}
BluetoothAdapter.STATE_ON -> {
warn("FIXME - requeue a connect anytime bluetooth is reenabled?")
}
}
}
}
}
// 0x2902 org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
private val configurationDescriptorUUID =
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
init {
context.registerReceiver(
btStateReceiver,
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
)
}
/**
* 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
*/
private class BluetoothContinuation(
val tag: String,
val completion: com.geeksville.concurrent.Continuation<*>,
val startWorkFn: () -> Boolean
) : Logging {
/// Start running a queued bit of work, return true for success or false for fatal bluetooth error
fun startWork(): Boolean {
debug("Starting work: $tag")
return startWorkFn()
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
g: BluetoothGatt,
status: Int,
newState: Int
) = exceptionReporter {
info("new bluetooth connection state $newState, status $status")
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
state =
newState // we only care about connected/disconnected - not the transitional states
//logAssert(workQueue.isNotEmpty())
//val work = workQueue.removeAt(0)
completeWork(status, Unit)
}
BluetoothProfile.STATE_DISCONNECTED -> {
// cancel any queued ops if we were already connected
val oldstate = state
state = newState
if (oldstate == BluetoothProfile.STATE_CONNECTED) {
info("Lost connection - aborting current work")
/*
Supposedly this reconnect attempt happens automatically
"If the connection was established through an auto connect, Android will
automatically try to reconnect to the remote device when it gets disconnected
until you manually call disconnect() or close(). Once a connection established
through direct connect disconnects, no attempt is made to reconnect to the remote device."
https://stackoverflow.com/questions/37965337/what-exactly-does-androids-bluetooth-autoconnect-parameter-do?rq=1
closeConnection()
*/
failAllWork(IOException("Lost connection"))
// Cancel any notifications - because when the device comes back it might have forgotten about us
notifyHandlers.clear()
debug("calling lostConnect handler")
lostConnectCallback?.invoke()
// Queue a new connection attempt
val cb = connectionCallback
if (cb != null) {
debug("queuing a reconnection callback")
assert(currentWork == null)
// note - we don't need an init fn (because that would normally redo the connectGatt call - which we don't need
queueWork("reconnect", CallbackContinuation(cb)) { -> true }
} else {
debug("No connectionCallback registered")
}
}
}
}
}
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)
}
/**
* Callback triggered as a result of a remote characteristic notification.
*
* @param gatt GATT client the characteristic is associated with
* @param characteristic Characteristic that has been updated as a result of a remote
* notification event.
*/
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val handler = notifyHandlers.get(characteristic.uuid)
if (handler == null)
warn("Received notification from $characteristic, but no handler registered")
else {
exceptionReporter {
handler(characteristic)
}
}
}
/**
* Callback indicating the result of a descriptor write operation.
*
* @param gatt GATT client invoked [BluetoothGatt.writeDescriptor]
* @param descriptor Descriptor that was writte to the associated remote device.
* @param status The result of the write operation [BluetoothGatt.GATT_SUCCESS] if the
* operation succeeds.
*/
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
completeWork(status, descriptor)
}
/**
* Callback reporting the result of a descriptor read operation.
*
* @param gatt GATT client invoked [BluetoothGatt.readDescriptor]
* @param descriptor Descriptor that was read from the associated remote device.
* @param status [BluetoothGatt.GATT_SUCCESS] if the read operation was completed
* successfully
*/
override fun onDescriptorRead(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
completeWork(status, descriptor)
}
}
/// 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
logAssert(newWork.startWork())
}
}
private fun <T> queueWork(tag: String, cont: Continuation<T>, initFn: () -> Boolean) {
val btCont =
BluetoothContinuation(
tag,
cont,
initFn
)
synchronized(workQueue) {
debug("Enqueuing work: ${btCont.tag}")
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
}
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
if (status != 0)
work.completion.resumeWithException(IOException("Bluetooth status=$status"))
else
work.completion.resume(Result.success(res) as Result<Nothing>)
}
/**
* Something went wrong, abort all queued
*/
private fun failAllWork(ex: Exception) {
synchronized(workQueue) {
workQueue.forEach {
it.completion.resumeWithException(ex)
}
workQueue.clear()
currentWork = null
}
}
/// 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>) {
assert(gatt == null);
queueWork("connect", cont) {
val g = device.connectGatt(context, autoConnect, gattCallback)
if (g != null)
gatt = g
g != null
}
}
/**
* start a connection attempt.
*
* Note: if autoConnect is true, the callback you provide will be kept around _even after the connection is complete.
* If we ever lose the connection, this class will immediately requque the attempt (after canceling
* any outstanding queued operations).
*
* So you should expect your callback might be called multiple times, each time to reestablish a new connection.
*/
fun asyncConnect(
autoConnect: Boolean = false,
cb: (Result<Unit>) -> Unit,
lostConnectCb: () -> Unit
) {
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
lostConnectCallback = lostConnectCb
connectionCallback = if (autoConnect)
cb
else
null
queueConnect(autoConnect, CallbackContinuation(cb))
}
fun connect(autoConnect: Boolean = false) = makeSync<Unit> { queueConnect(autoConnect, it) }
private fun queueReadCharacteristic(
c: BluetoothGattCharacteristic,
cont: Continuation<BluetoothGattCharacteristic>
) = queueWork("readC ${c.uuid}", 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("discover", cont) {
gatt!!.discoverServices()
}
}
fun asyncDiscoverServices(cb: (Result<Unit>) -> Unit) {
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
queueDiscoverServices(CallbackContinuation(cb))
}
fun discoverServices() = makeSync<Unit> { queueDiscoverServices(it) }
private fun queueRequestMtu(
len: Int,
cont: Continuation<Int>
) = queueWork("reqMtu", cont) { gatt!!.requestMtu(len) }
fun asyncRequestMtu(
len: Int,
cb: (Result<Int>) -> Unit
) {
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
queueRequestMtu(len, CallbackContinuation(cb))
}
fun requestMtu(len: Int): Int =
makeSync { queueRequestMtu(len, it) }
private fun queueWriteCharacteristic(
c: BluetoothGattCharacteristic,
cont: Continuation<BluetoothGattCharacteristic>
) = queueWork("writeC ${c.uuid}", 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) }
private fun queueWriteDescriptor(
c: BluetoothGattDescriptor,
cont: Continuation<BluetoothGattDescriptor>
) = queueWork("writeD", cont) { gatt!!.writeDescriptor(c) }
fun asyncWriteDescriptor(
c: BluetoothGattDescriptor,
cb: (Result<BluetoothGattDescriptor>) -> Unit
) = queueWriteDescriptor(c, CallbackContinuation(cb))
private fun closeConnection() {
failAllWork(IOException("Connection closing"))
if (gatt != null) {
info("Closing our GATT connection")
gatt!!.disconnect()
gatt!!.close()
gatt = null
}
}
fun disconnect() {
closeConnection()
context.unregisterReceiver(btStateReceiver)
}
/// asyncronously turn notification on/off for a characteristic
fun setNotify(
c: BluetoothGattCharacteristic,
enable: Boolean,
onChanged: (BluetoothGattCharacteristic) -> Unit
) {
debug("starting setNotify(${c.uuid}, $enable)")
notifyHandlers[c.uuid] = onChanged
// c.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt!!.setCharacteristicNotification(c, enable)
// per https://stackoverflow.com/questions/27068673/subscribe-to-a-ble-gatt-notification-android
val descriptor: BluetoothGattDescriptor = c.getDescriptor(configurationDescriptorUUID)!!
descriptor.value =
if (enable) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
asyncWriteDescriptor(descriptor) {
debug("Notify enable=$enable completed")
}
}
}

View file

@ -0,0 +1,52 @@
package com.geeksville.mesh
import android.content.Context
import com.google.protobuf.util.JsonFormat
class SimRadio(private val context: Context) {
private val jsonParser = JsonFormat.parser()
/**
* When simulating we parse these MeshPackets as if they arrived at startup
* Send broadcast them after we receive a ToRadio.WantNodes message.
*
* Our fake net has three nodes
*
* +16508675309, nodenum 9 - our node
* +16508675310, nodenum 10 - some other node, name Bob One/BO
* (eventually) +16508675311, nodenum 11 - some other node
*/
private val simInitPackets =
arrayOf(
""" { "from": 10, "to": 9, "payload": { "user": { "id": "+16508675310", "longName": "Bob One", "shortName": "BO" }}} """,
""" { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 0 }}} """, // SIGNAL_OPAQUE
""" { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 1 }}} """, // CLEAR_TEXT
""" { "from": 10, "to": 9, "payload": { "data": { "payload": "", "typ": 2 }}} """ // CLEAR_READACK
)
fun start() {
// FIXME, do this sim startup elsewhere, because waiting for a packet from MeshService
// isn't right, because that service can't successfully send radio packets until it knows
// our node num
// Instead a separate sim radio thing can come in at startup and force these broadcasts to happen
// at the right time
// Send a fake my_node_num response
/* FIXME - change to use new radio info message
RadioInterfaceService.broadcastReceivedFromRadio(
context,
MeshProtos.FromRadio.newBuilder().apply {
myNodeNum = 9
}.build().toByteArray()
) */
simInitPackets.forEach { json ->
val fromRadio = MeshProtos.FromRadio.newBuilder().apply {
packet = MeshProtos.MeshPacket.newBuilder().apply {
jsonParser.merge(json, this)
}.build()
}.build()
RadioInterfaceService.broadcastReceivedFromRadio(context, fromRadio.toByteArray())
}
}
}

View file

@ -0,0 +1,229 @@
package com.geeksville.mesh.service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
import android.os.ParcelUuid
import androidx.core.app.JobIntentService
import com.geeksville.android.Logging
import com.geeksville.mesh.MainActivity
import java.util.*
import java.util.zip.CRC32
/**
* typical flow
*
* startScan
* startUpdate
*
* stopScan
*
* FIXME - if we don't find a device stop our scan
* FIXME - broadcast when we found devices, made progress sending blocks or when the update is complete
* FIXME - make the user decide to start an update on a particular device
*/
class SoftwareUpdateService : JobIntentService(), Logging {
private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter!!
}
lateinit var device: BluetoothDevice
fun startUpdate() {
info("starting update")
val sync =
SafeBluetooth(
this@SoftwareUpdateService,
device
)
val firmwareStream = assets.open("firmware.bin")
val firmwareCrc = CRC32()
var firmwareNumSent = 0
val firmwareSize = firmwareStream.available()
sync.connect()
sync.discoverServices() // Get our services
// we begin by setting our MTU size as high as it can go
sync.requestMtu(512)
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
val totalSizeDesc = service.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
val dataDesc = service.getCharacteristic(SW_UPDATE_DATA_CHARACTER)
val crc32Desc = service.getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
val updateResultDesc = service.getCharacteristic(SW_UPDATE_RESULT_CHARACTER)
// Start the update by writing the # of bytes in the image
logAssert(
totalSizeDesc.setValue(
firmwareSize,
BluetoothGattCharacteristic.FORMAT_UINT32,
0
)
)
sync.writeCharacteristic(totalSizeDesc)
// Our write completed, queue up a readback
val totalSizeReadback = sync.readCharacteristic(totalSizeDesc)
.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0)
if (totalSizeReadback == 0) // FIXME - handle this case
throw Exception("Device rejected file size")
// Send all the blocks
while (firmwareNumSent < firmwareSize) {
info("sending block ${firmwareNumSent * 100 / firmwareSize}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
if (blockSize > firmwareStream.available())
blockSize = firmwareStream.available()
val buffer = ByteArray(blockSize)
// slightly expensive to keep reallocing this buffer, but whatever
logAssert(firmwareStream.read(buffer) == blockSize)
firmwareCrc.update(buffer)
// updateGatt.beginReliableWrite()
dataDesc.value = buffer
sync.writeCharacteristic(dataDesc)
firmwareNumSent += blockSize
}
// We have finished sending all our blocks, so post the CRC so our state machine can advance
val c = firmwareCrc.value
info("Sent all blocks, crc is $c")
logAssert(crc32Desc.setValue(c.toInt(), BluetoothGattCharacteristic.FORMAT_UINT32, 0))
sync.writeCharacteristic(crc32Desc)
// we just read the update result if !0 we have an error
val updateResult =
sync.readCharacteristic(updateResultDesc)
.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)
if (updateResult != 0) // FIXME - handle this case
throw Exception("Device update failed, reason=$updateResult")
// FIXME perhaps ask device to reboot
}
private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
throw NotImplementedError()
}
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
throw NotImplementedError()
}
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
// check if it is an eligable device and store it in our list of candidates
// if that device later disconnects remove it as a candidate
override fun onScanResult(callbackType: Int, result: ScanResult) {
info("onScanResult")
// We don't need any more results now
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
device = result.device
}
}
// Until my race condition with scanning is fixed
fun connectToTestDevice() {
device = bluetoothAdapter.getRemoteDevice("B4:E6:2D:EA:32:B7")
}
private fun scanLeDevice(enable: Boolean) {
when (enable) {
true -> {
// Stops scanning after a pre-defined scan period.
/* handler.postDelayed({
mScanning = false
bluetoothAdapter.stopLeScan(leScanCallback)
}, SCAN_PERIOD)
mScanning = true */
val scanner = bluetoothAdapter.bluetoothLeScanner
// filter and only accept devices that have a sw update service
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SW_UPDATE_UUID)).build()
/* ScanSettings.CALLBACK_TYPE_FIRST_MATCH seems to trigger a bug returning an error of
SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES (error #5)
*/
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).
// setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT).
// setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH).
build()
scanner.startScan(listOf(filter), settings, scanCallback)
}
else -> {
// mScanning = false
// bluetoothAdapter.stopLeScan(leScanCallback)
}
}
}
override fun onHandleWork(intent: Intent) { // We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
debug("Executing work: $intent")
when (intent.action) {
scanDevicesIntent.action -> scanLeDevice(true)
startUpdateIntent.action -> {
connectToTestDevice() // FIXME, pass in as an intent arg instead
startUpdate()
}
else -> TODO("Unhandled case")
}
}
companion object {
/**
* Unique job ID for this service. Must be the same for all work.
*/
private const val JOB_ID = 1000
val scanDevicesIntent = Intent("$prefix.SCAN_DEVICES")
val startUpdateIntent = Intent("$prefix.START_UPDATE")
private const val SCAN_PERIOD: Long = 10000
private val TAG =
MainActivity::class.java.simpleName // FIXME - use my logging class instead
private val SW_UPDATE_UUID = UUID.fromString("cb0b9a0b-a84c-4c0d-bdbb-442e3144ee30")
private val SW_UPDATE_TOTALSIZE_CHARACTER =
UUID.fromString("e74dd9c0-a301-4a6f-95a1-f0e1dbea8e1e") // write|read total image size, 32 bit, write this first, then read read back to see if it was acceptable (0 mean not accepted)
private val SW_UPDATE_DATA_CHARACTER =
UUID.fromString("e272ebac-d463-4b98-bc84-5cc1a39ee517") // write data, variable sized, recommended 512 bytes, write one for each block of file
private val SW_UPDATE_CRC32_CHARACTER =
UUID.fromString("4826129c-c22a-43a3-b066-ce8f0d5bacc6") // write crc32, write last - writing this will complete the OTA operation, now you can read result
private val SW_UPDATE_RESULT_CHARACTER =
UUID.fromString("5e134862-7411-4424-ac4a-210937432c77") // read|notify result code, readable but will notify when the OTA operation completes
/**
* Convenience method for enqueuing work in to this service.
*/
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(
context,
SoftwareUpdateService::class.java,
JOB_ID, work
)
}
}
}