diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index 4e9c112ae..381176608 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -8,6 +8,8 @@ package com.geeksville.mesh; */ interface IMeshService { /// Tell the service where to send its broadcasts of received packets + /// This call is only required for manifest declared receivers. If your receiver is context-registered + /// you don't need this. void subscribeReceiver(String packageName, String receiverName); /** diff --git a/app/src/main/java/com/geeksville/mesh/Constants.kt b/app/src/main/java/com/geeksville/mesh/Constants.kt index 536e88e6e..36f67984f 100644 --- a/app/src/main/java/com/geeksville/mesh/Constants.kt +++ b/app/src/main/java/com/geeksville/mesh/Constants.kt @@ -2,11 +2,15 @@ package com.geeksville.mesh 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_ID = "$prefix.Id" -const val EXTRA_ONLINE = "$prefix.Online" +const val EXTRA_NODEINFO = "$prefix.NodeInfo" const val EXTRA_TYP = "$prefix.Typ" diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 35cda8c1b..053686ae8 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -3,10 +3,7 @@ package com.geeksville.mesh import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection +import android.content.* import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -31,6 +28,8 @@ import androidx.ui.tooling.preview.Preview import com.geeksville.android.Logging import com.geeksville.util.exceptionReporter import com.google.firebase.crashlytics.FirebaseCrashlytics +import java.nio.charset.Charset +import java.util.* class MainActivity : AppCompatActivity(), Logging { @@ -173,16 +172,79 @@ class MainActivity : AppCompatActivity(), Logging { } requestPermission() + + val filter = IntentFilter() + filter.addAction("") + registerReceiver(meshServiceReceiver, filter) + } + + override fun onDestroy() { + unregisterReceiver(meshServiceReceiver) + super.onDestroy() + } + + /// A map from nodeid to to nodeinfo + private val nodes = mutableMapOf() + + data class TextMessage(val date: Date, val from: String, val text: String) + + private val messages = mutableListOf() + + /// Are we connected to our radio device + private var isConnected = false + + private val utf8 = Charset.forName("UTF-8") + + private val meshServiceReceiver = object : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) = exceptionReporter { + debug("Received from mesh service $intent") + + when (intent.action) { + MeshService.ACTION_NODE_CHANGE -> { + warn("TODO nodechange") + val info: NodeInfo = intent.getParcelableExtra(EXTRA_NODEINFO)!! + + // We only care about nodes that have user info + info.user?.id?.let { + nodes[it] = info + } + } + MeshService.ACTION_RECEIVED_DATA -> { + warn("TODO rxopaqe") + val sender = intent.getStringExtra(EXTRA_SENDER)!! + val payload = intent.getByteArrayExtra(EXTRA_PAYLOAD)!! + val typ = intent.getIntExtra(EXTRA_TYP, -1)!! + + when (typ) { + MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> { + // FIXME - use the real time from the packet + messages.add(TextMessage(Date(), sender, payload.toString(utf8))) + } + else -> TODO() + } + } + RadioInterfaceService.CONNECTCHANGED_ACTION -> { + isConnected = intent.getBooleanExtra(EXTRA_CONNECTED, false) + debug("connchange $isConnected") + } + else -> TODO() + } + } } private var meshService: IMeshService? = null private var isBound = false private var serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, service: IBinder) { + override fun onServiceConnected(name: ComponentName, service: IBinder) = exceptionReporter { val m = IMeshService.Stub.asInterface(service) meshService = m + // FIXME - do actions for when we connect to the service + // FIXME - do actions for when we connect to the service + debug("did connect") + // FIXME: this still can't work this early because the send to +6508675310 // requires a DB lookup which isn't yet populated (until the sim test packets // from the radio arrive) diff --git a/app/src/main/java/com/geeksville/mesh/MeshService.kt b/app/src/main/java/com/geeksville/mesh/MeshService.kt index c12e6207b..4f79c973c 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshService.kt @@ -8,6 +8,7 @@ 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 @@ -18,11 +19,29 @@ 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. @@ -32,6 +51,11 @@ class RadioNotConnectedException() : Exception("Can't find radio") 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" + 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") @@ -41,6 +65,8 @@ class MeshService : Service(), Logging { /// 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 @@ -56,6 +82,7 @@ class MeshService : Service(), Logging { */ 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) @@ -68,18 +95,18 @@ class MeshService : Service(), Logging { * Sender will be a user ID string * Type will be the Data.Type enum code for this payload */ - private fun broadcastReceivedOpaque(senderId: String, payload: ByteArray, typ: Int) { - val intent = Intent("$prefix.RECEIVED_OPAQUE") + 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(nodeId: String, isOnline: Boolean) { - val intent = Intent("$prefix.NODE_CHANGE") - intent.putExtra(EXTRA_ID, nodeId) - intent.putExtra(EXTRA_ONLINE, isOnline) + private fun broadcastNodeChange(info: NodeInfo) { + debug("Broadcasting node change $info") + val intent = Intent(ACTION_NODE_CHANGE) + intent.putExtra(EXTRA_NODEINFO, info) explicitBroadcast(intent) } @@ -134,7 +161,7 @@ class MeshService : Service(), Logging { private fun startForeground() { - val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + // val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel() @@ -204,17 +231,6 @@ class MeshService : Service(), Logging { super.onDestroy() } - // model objects that directly map to the corresponding protobufs - data class MeshUser(val id: String, val longName: String, val shortName: String) - - data class Position(val latitude: Double, val longitude: Double, val altitude: Int) - 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 - ) - /// /// BEGINNING OF MODEL - FIXME, move elsewhere /// @@ -266,6 +282,8 @@ class MeshService : Service(), Logging { val userId = info.user?.id if (userId != null) nodeDBbyID[userId] = info + + broadcastNodeChange(info) } /// Generate a new mesh packet builder with our node as the sender, and the specified node num @@ -301,13 +319,24 @@ class MeshService : Service(), Logging { /// 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 -> - warn( - "TODO ignoring CLEAR_TEXT from $fromString: ${bytes.toString( + 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( @@ -315,12 +344,8 @@ class MeshService : Service(), Logging { ) MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE -> - if (fromId == null) - error("Ignoring opaque from $fromNum because we don't yet know its ID") - else { - debug("Received opaque from $fromId ${bytes.size}") - broadcastReceivedOpaque(fromId, bytes, data.typValue) - } + forwardData() + else -> TODO() } } @@ -341,6 +366,14 @@ class MeshService : Service(), Logging { 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) { @@ -350,10 +383,6 @@ class MeshService : Service(), Logging { p.position.altitude ) } - MeshProtos.SubPacket.TIME_FIELD_NUMBER -> - updateNodeInfo(fromNum) { - it.lastSeen = p.time - } MeshProtos.SubPacket.DATA_FIELD_NUMBER -> handleReceivedData(fromNum, p.data) @@ -378,7 +407,7 @@ class MeshService : Service(), Logging { val myInfo = MeshProtos.MyNodeInfo.parseFrom(connectedRadio.readMyNode()) ourNodeNum = myInfo.myNodeNum - // Ask for the current node DB + // Ask for the current node DB connectedRadio.restartNodeInfo() // read all the infos until we get back null @@ -390,7 +419,8 @@ class MeshService : Service(), Logging { // Just replace/add any entry updateNodeInfo(info.num) { if (info.hasUser()) - it.user = MeshUser(info.user.id, info.user.longName, info.user.shortName) + it.user = + MeshUser(info.user.id, info.user.longName, info.user.shortName) if (info.hasPosition()) it.position = Position( @@ -429,7 +459,9 @@ class MeshService : Service(), Logging { 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) + MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket( + proto.packet + ) else -> TODO("Unexpected FromRadio variant") } diff --git a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt index 0a12b616b..2bdfe5fa7 100644 --- a/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/RadioInterfaceService.kt @@ -89,6 +89,10 @@ 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" + + /** + * This is broadcast when connection state changed (it is also rebroadcast by the MeshService) + */ const val CONNECTCHANGED_ACTION = "$prefix.CONNECT_CHANGED" private val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") diff --git a/app/src/main/proto/mesh.proto b/app/src/main/proto/mesh.proto index dc24097e1..7b77f4d81 100644 --- a/app/src/main/proto/mesh.proto +++ b/app/src/main/proto/mesh.proto @@ -116,9 +116,6 @@ message SubPacket { oneof variant { Position position = 1; - // Times are typically not sent over the mesh, but they will be added to any Packet (chain of SubPacket) - // sent to the phone (so the phone can know exact time of reception) - uint64 time = 2; // msecs since 1970 Data data = 3; User user = 4; } @@ -141,6 +138,11 @@ message MeshPacket { // See note above: // MeshPayload payloads = 4; SubPacket payload = 3; + + /// The time this message was received by the esp32 (msecs since 1970). Note: this field is _never_ sent on the radio link itself (to save space) + /// Times are typically not sent over the mesh, but they will be added to any Packet (chain of SubPacket) + /// sent to the phone (so the phone can know exact time of reception) + uint64 rx_time = 4; } // Full settings (center freq, spread factor, pre-shared secret key etc...) needed to configure a radio