diff --git a/TODO.md b/TODO.md index d5d6c000..643e81fd 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ # High priority * fix startup race conditions in services, allow reads to block as needed +* if radio disconnects, we need to requeue a new connect attempt in RadioService * when notified phone should download messages * have phone use our local node number as its node number (instead of hardwired) * investigate the Signal SMS message flow path, see if I could just make Mesh a third peer to signal & sms? diff --git a/app/src/main/java/com/geeksville/mesh/Constants.kt b/app/src/main/java/com/geeksville/mesh/Constants.kt index 82c15b6d..536e88e6 100644 --- a/app/src/main/java/com/geeksville/mesh/Constants.kt +++ b/app/src/main/java/com/geeksville/mesh/Constants.kt @@ -2,7 +2,9 @@ package com.geeksville.mesh const val prefix = "com.geeksville.mesh" +// 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_ID = "$prefix.Id" diff --git a/app/src/main/java/com/geeksville/mesh/MeshService.kt b/app/src/main/java/com/geeksville/mesh/MeshService.kt index c2ecc04d..ca0a0c5b 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshService.kt @@ -4,7 +4,6 @@ import android.app.Service import android.content.* import android.os.IBinder import com.geeksville.android.Logging -import com.geeksville.concurrent.DeferredExecution import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.util.toOneLineString @@ -12,6 +11,7 @@ import com.geeksville.util.toRemoteExceptions import com.google.protobuf.ByteString import java.nio.charset.Charset +class RadioNotConnectedException() : Exception("Can't find radio") /** * Handles all the communication with android apps. Also keeps an internal model @@ -25,7 +25,6 @@ class MeshService : Service(), Logging { 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") - class RadioNotConnectedException() : Exception("Can't find radio") /// If we haven't yet received a node number from the radio private const val NODE_NUM_UNKNOWN = -2 @@ -74,18 +73,22 @@ class MeshService : Service(), Logging { explicitBroadcast(intent) } - private val toRadioDeferred = DeferredExecution() + /// 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() - val s = radioService - if (s != null) - s.sendToRadio(b) - else - toRadioDeferred.add { radioService!!.sendToRadio(b) } + connectedRadio.sendToRadio(b) } override fun onBind(intent: Intent?): IBinder? { @@ -94,24 +97,8 @@ class MeshService : Service(), Logging { private val radioConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val filter = IntentFilter(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION) - registerReceiver(radioInterfaceReceiver, filter) - radioReceiverRegistered = true - val m = IRadioInterfaceService.Stub.asInterface(service) radioService = m - - // 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 - - // Ask for the current node DB FIXME - /* sendToRadio(ToRadio.newBuilder().apply { - wantNodes = ToRadio.WantNodes.newBuilder().build() - }) */ - - // Now send any packets which had previously been queued for clients - toRadioDeferred.run() } override fun onServiceDisconnected(name: ComponentName?) { @@ -124,6 +111,10 @@ class MeshService : Service(), Logging { info("Creating mesh service") + // we listen for messages from the radio receiver _before_ trying to create the service + val filter = IntentFilter(RadioInterfaceService.RECEIVE_FROMRADIO_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 @@ -132,12 +123,10 @@ class MeshService : Service(), Logging { // the rest of our init will happen once we are in radioConnection.onServiceConnected } - private var radioReceiverRegistered = false override fun onDestroy() { info("Destroying mesh service") - if (radioReceiverRegistered) - unregisterReceiver(radioInterfaceReceiver) + unregisterReceiver(radioInterfaceReceiver) unbindService(radioConnection) radioService = null @@ -201,6 +190,11 @@ class MeshService : Service(), Logging { 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 } /// Generate a new mesh packet builder with our node as the sender, and the specified node num @@ -264,9 +258,6 @@ class MeshService : Service(), Logging { private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User) { updateNodeInfo(fromNum) { it.user = MeshUser(p.id, p.longName, p.shortName) - - // This might have been the first time we know an ID for this node, so also update the by ID map - nodeDBbyID[p.id] = it } } @@ -309,8 +300,49 @@ class MeshService : Service(), Logging { } } - private fun handleReceivedNodeInfo(info: MeshProtos.NodeInfo) { - TODO() + + /// Called when we gain/lose connection to our radio + private fun onConnectionChanged(c: Boolean) { + 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() + } + } + TODO("FIXME - set our owner, get node infos, set our local nodenum, dont process received packets until we have the full node db") } /** @@ -320,17 +352,25 @@ class MeshService : Service(), Logging { private val radioInterfaceReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - 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) - /* - FIXME - handle node info and setting my protonum - MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleReceivedNodeInfo(proto.nodeInfo) - MeshProtos.FromRadio.MY_NODE_NUM_FIELD_NUMBER -> ourNodeNum = proto.myNodeNum - */ - else -> TODO("Unexpected FromRadio variant") + when (intent.action) { + RadioInterfaceService.CONNECTCHANGED_ACTION -> { + onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) + explicitBroadcast(intent) // forward the connection change message to anyone who is listening to us + } + + 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") } } } @@ -358,11 +398,8 @@ class MeshService : Service(), Logging { handleReceivedUser(ourNodeNum, user) } - /* FIXME - set my owner info - sendToRadio(ToRadio.newBuilder().apply { - this.setOwner = user - }) - */ + // set my owner info + connectedRadio.writeOwner(user.toByteArray()) } override fun sendData(destId: String, payloadIn: ByteArray, typ: Int) = diff --git a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt index 30a59b30..d0c27dff 100644 --- a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt @@ -11,6 +11,7 @@ import android.os.IBinder import com.geeksville.android.DebugLogFile import com.geeksville.android.Logging import com.geeksville.concurrent.DeferredExecution +import com.geeksville.util.toRemoteExceptions import com.google.protobuf.util.JsonFormat import java.util.* @@ -89,6 +90,7 @@ class RadioInterfaceService : Service(), Logging { * Payload will be the raw bytes which were contained within a MeshProtos.FromRadio protobuf */ const val RECEIVE_FROMRADIO_ACTION = "$prefix.RECEIVE_FROMRADIO" + const val CONNECTCHANGED_ACTION = "$prefix.CONNECT_CHANGED" private val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") private val BTM_FROMRADIO_CHARACTER = @@ -141,16 +143,14 @@ class RadioInterfaceService : Service(), Logging { // for debug logging only private val jsonPrinter = JsonFormat.printer() - // We have talked to our device and consumed all of the FromRadio packets it had initially - // waiting for us - private var initCompleted = false + 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() - fun broadcastConnectionChanged(isConnected: Boolean) { - val intent = Intent("$prefix.CONNECTION_CHANGED") + private fun broadcastConnectionChanged(isConnected: Boolean) { + val intent = Intent(CONNECTCHANGED_ACTION) intent.putExtra(EXTRA_CONNECTED, isConnected) sendBroadcast(intent) } @@ -173,28 +173,24 @@ class RadioInterfaceService : Service(), Logging { /// Attempt to read from the fromRadio mailbox, if data is found broadcast it to android apps private fun doReadFromRadio() { - safe.asyncReadCharacteristic(fromRadio) { - val b = it.getOrThrow().value + if (!isConnected) + warn("Abandoning fromradio read because we are not connected") + else + safe.asyncReadCharacteristic(fromRadio) { + val b = it.getOrThrow().value - if (b.isNotEmpty()) { - debug("Received ${b.size} bytes from radio") - handleFromRadio(b) + 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") - initCompleted = true - doClientOperations() + // Queue up another read, until we run out of packets + doReadFromRadio() + } else { + debug("Done reading from radio, fromradio is empty") + } } - } } - /// If we are inited send any client requests - private fun doClientOperations() { - if (initCompleted) - clientOperations.run() - } override fun onCreate() { super.onCreate() @@ -235,6 +231,11 @@ class RadioInterfaceService : Service(), Logging { fromRadio = service.getCharacteristic(BTM_FROMRADIO_CHARACTER) fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER) + // Now tell clients they can (finally use the api) + broadcastConnectionChanged(true) + isConnected = true + + // Immediately broadcast any queued packets sitting on the device doReadFromRadio() } } @@ -255,50 +256,60 @@ class RadioInterfaceService : Service(), Logging { } /** - * We allow writes to bluetooth characteristics to be queued up until our init is completed. + * do a synchronous write operation */ - private fun doAsyncWrite(uuid: UUID, a: ByteArray) { - debug("queuing ${a.size} bytes to $uuid") + private fun doWrite(uuid: UUID, a: ByteArray) = toRemoteExceptions { + if (!isConnected) + throw RadioNotConnectedException() + else { + debug("queuing ${a.size} bytes to $uuid") - clientOperations.add { // 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.asyncWriteCharacteristic(toRadio) { - it.getOrThrow() // FIXME, handle the error better - debug("ToRadio write of ${a.size} bytes completed") - } + safe.writeCharacteristic(toRadio) + debug("write of ${a.size} bytes completed") } + } - doClientOperations() + /** + * 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() = doAsyncWrite(BTM_NODEINFO_CHARACTER, ByteArray(0)) + override fun restartNodeInfo() = doWrite(BTM_NODEINFO_CHARACTER, ByteArray(0)) - override fun readMyNode(): ByteArray { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun readMyNode() = doRead(BTM_MYNODE_CHARACTER)!! - override fun sendToRadio(a: ByteArray) = doAsyncWrite(BTM_TORADIO_CHARACTER, a) + override fun sendToRadio(a: ByteArray) = doWrite(BTM_TORADIO_CHARACTER, a) - override fun readRadioConfig(): ByteArray { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun readRadioConfig() = doRead(BTM_RADIO_CHARACTER)!! - override fun readOwner(): ByteArray { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun readOwner() = doRead(BTM_OWNER_CHARACTER)!! - override fun writeOwner(owner: ByteArray) = doAsyncWrite(BTM_OWNER_CHARACTER, owner) + override fun writeOwner(owner: ByteArray) = doWrite(BTM_OWNER_CHARACTER, owner) - override fun writeRadioConfig(config: ByteArray) = doAsyncWrite(BTM_RADIO_CHARACTER, config) + override fun writeRadioConfig(config: ByteArray) = doWrite(BTM_RADIO_CHARACTER, config) - override fun readNodeInfo(): ByteArray { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun readNodeInfo() = doRead(BTM_NODEINFO_CHARACTER) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt index aeb8fe76..e6b61722 100644 --- a/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/SafeBluetooth.kt @@ -22,7 +22,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD Logging { /// Timeout before we declare a bluetooth operation failed - private val timeoutMsec = 30 * 1000L + var timeoutMsec = 5 * 1000L /// Users can access the GATT directly as needed lateinit var gatt: BluetoothGatt