mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
we now fetch any new rxmessages when they arrive at the radio
This commit is contained in:
parent
6244556f8b
commit
10ad07e136
11 changed files with 110 additions and 43 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/com/geeksville/mesh/service/Constants.kt
Normal file
16
app/src/main/java/com/geeksville/mesh/service/Constants.kt
Normal 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"
|
||||
567
app/src/main/java/com/geeksville/mesh/service/MeshService.kt
Normal file
567
app/src/main/java/com/geeksville/mesh/service/MeshService.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
450
app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
Normal file
450
app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue