Meshtastic-Android/app/src/main/java/com/geeksville/mesh/MeshService.kt

387 lines
14 KiB
Kotlin
Raw Normal View History

2020-01-22 21:46:41 -08:00
package com.geeksville.mesh
2020-01-22 21:25:31 -08:00
import android.app.Service
import android.content.*
2020-01-22 21:25:31 -08:00
import android.os.IBinder
2020-01-22 22:16:30 -08:00
import com.geeksville.android.Logging
2020-01-24 20:35:42 -08:00
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.util.toOneLineString
import com.geeksville.util.toRemoteExceptions
2020-01-24 20:35:42 -08:00
import com.google.protobuf.ByteString
import java.nio.charset.Charset
2020-01-26 11:33:51 -08:00
2020-01-23 08:09:50 -08:00
/**
* Handles all the communication with android apps. Also keeps an internal model
* of the network state.
*
2020-01-23 08:09:50 -08:00
* Note: this service will go away once all clients are unbound from it.
*/
2020-01-22 22:16:30 -08:00
class MeshService : Service(), Logging {
2020-01-25 10:00:57 -08:00
companion object {
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
/// If the radio hasn't yet joined a mesh (i.e. no nodenum assigned)
private const val NODE_NUM_NO_MESH = -1
2020-01-25 10:00:57 -08:00
}
2020-01-26 11:33:51 -08:00
/// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf<String, String>()
private var radioService: IRadioInterfaceService? = null
2020-01-22 22:16:30 -08:00
/*
2020-01-23 06:34:15 -08:00
see com.geeksville.mesh broadcast intents
2020-01-22 22:16:30 -08:00
// RECEIVED_OPAQUE for data received from other nodes
// NODE_CHANGE for new IDs appearing or disappearing
// CONNECTION_CHANGED for losing/gaining connection to the packet radio
*/
2020-01-26 11:33:51 -08:00
private fun explicitBroadcast(intent: Intent) {
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
2020-01-25 06:33:30 -08:00
* Type will be the Data.Type enum code for this payload
*/
2020-01-25 06:33:30 -08:00
private fun broadcastReceivedOpaque(senderId: String, payload: ByteArray, typ: Int) {
2020-01-22 22:16:30 -08:00
val intent = Intent("$prefix.RECEIVED_OPAQUE")
intent.putExtra(EXTRA_SENDER, senderId)
intent.putExtra(EXTRA_PAYLOAD, payload)
2020-01-25 06:33:30 -08:00
intent.putExtra(EXTRA_TYP, typ)
2020-01-26 11:33:51 -08:00
explicitBroadcast(intent)
2020-01-22 22:16:30 -08:00
}
2020-01-25 06:33:30 -08:00
private fun broadcastNodeChange(nodeId: String, isOnline: Boolean) {
2020-01-22 22:16:30 -08:00
val intent = Intent("$prefix.NODE_CHANGE")
intent.putExtra(EXTRA_ID, nodeId)
intent.putExtra(EXTRA_ONLINE, isOnline)
2020-01-26 11:33:51 -08:00
explicitBroadcast(intent)
2020-01-22 22:16:30 -08:00
}
2020-01-22 21:25:31 -08:00
2020-01-24 20:35:42 -08:00
/// Send a command/packet to our radio
private fun sendToRadio(p: ToRadio.Builder) {
2020-01-27 16:24:38 -08:00
val s = radioService
if (s != null)
s.sendToRadio(p.build().toByteArray())
else
error("FIXME! dropped sent packet because radio interface not yet fully connected")
2020-01-24 20:35:42 -08:00
}
2020-01-26 11:33:51 -08:00
override fun onBind(intent: Intent?): IBinder? {
2020-01-22 21:25:31 -08:00
return binder
}
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
sendToRadio(ToRadio.newBuilder().apply {
wantNodes = ToRadio.WantNodes.newBuilder().build()
})
}
override fun onServiceDisconnected(name: ComponentName?) {
radioService = null
}
}
override fun onCreate() {
super.onCreate()
2020-01-25 10:00:57 -08:00
info("Creating mesh service")
2020-01-24 20:35:42 -08:00
// 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))
2020-01-24 20:35:42 -08:00
// the rest of our init will happen once we are in radioConnection.onServiceConnected
}
private var radioReceiverRegistered = false
override fun onDestroy() {
2020-01-25 10:00:57 -08:00
info("Destroying mesh service")
if (radioReceiverRegistered)
unregisterReceiver(radioInterfaceReceiver)
unbindService(radioConnection)
radioService = null
super.onDestroy()
}
2020-01-24 20:35:42 -08:00
// 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(
2020-01-24 22:22:30 -08:00
val num: Int, // This is immutable, and used as a key
var user: MeshUser? = null,
var position: Position? = null,
var lastSeen: Long? = null
2020-01-24 20:35:42 -08:00
)
2020-01-24 22:22:30 -08:00
///
/// 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
2020-01-24 22:22:30 -08:00
2020-01-24 20:35:42 -08:00
// 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
2020-01-24 22:22:30 -08:00
/// 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.
2020-01-24 20:35:42 -08:00
private val nodeDBbyID = mutableMapOf<String, NodeInfo>()
2020-01-24 22:22:30 -08:00
///
/// END OF MODEL
///
2020-01-24 20:35:42 -08:00
2020-01-24 22:22:30 -08:00
/// Map a nodenum to a node, or throw an exception if not found
2020-01-25 10:00:57 -08:00
private fun toNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: throw NodeNumNotFoundException(n)
2020-01-24 22:22:30 -08:00
/// 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) }
2020-01-24 22:22:30 -08:00
/// Map a userid to a node/ node num, or throw an exception if not found
private fun toNodeInfo(id: String) =
nodeDBbyID[id]
2020-01-26 15:01:59 -08:00
?: throw IdNotFoundException(id)
// ?: getOrCreateNodeInfo(10) // FIXME hack for now - throw IdNotFoundException(id)
2020-01-24 22:22:30 -08:00
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)
}
/// 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 {
2020-01-24 20:35:42 -08:00
from = ourNodeNum
if (from == NODE_NUM_NO_MESH)
throw NotInMeshException()
else if (from == NODE_NUM_UNKNOWN)
throw RadioNotConnectedException()
2020-01-24 20:35:42 -08:00
to = idNum
}
/// Generate a new mesh packet builder with our node as the sender, and the specified recipient
2020-01-24 22:22:30 -08:00
private fun newMeshPacketTo(id: String) = newMeshPacketTo(toNodeNum(id))
2020-01-25 10:00:57 -08:00
// 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.MeshPayload.newBuilder().apply {
addSubPackets(MeshProtos.SubPacket.newBuilder().also {
initFn(it)
}.build())
}.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()
when (data.typValue) {
MeshProtos.Data.Type.CLEAR_TEXT_VALUE ->
warn(
"TODO ignoring CLEAR_TEXT from $fromString: ${bytes.toString(
Charset.forName("UTF-8")
)}"
)
MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
warn(
"TODO ignoring CLEAR_READACK from $fromString"
)
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}")
2020-01-25 06:33:30 -08:00
broadcastReceivedOpaque(fromId, bytes, data.typValue)
}
else -> TODO()
}
}
2020-01-25 10:00:57 -08:00
/// 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)
// 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
}
}
2020-01-24 22:22:30 -08:00
/// 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 payload = packet.payload
payload.subPacketsList.forEach { p ->
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.TIME_FIELD_NUMBER ->
updateNodeInfo(fromNum) {
it.lastSeen = p.time.msecs
}
MeshProtos.SubPacket.DATA_FIELD_NUMBER ->
handleReceivedData(fromNum, p.data)
2020-01-24 22:22:30 -08:00
MeshProtos.SubPacket.USER_FIELD_NUMBER ->
2020-01-25 10:00:57 -08:00
handleReceivedUser(fromNum, p.user)
2020-01-24 22:22:30 -08:00
MeshProtos.SubPacket.WANT_NODE_FIELD_NUMBER -> {
// This is managed by the radio on its own
debug("Ignoring WANT_NODE from $fromNum")
}
MeshProtos.SubPacket.DENY_NODE_FIELD_NUMBER -> {
// This is managed by the radio on its own
debug("Ignoring DENY_NODE from $fromNum to $toNum")
}
else -> TODO("Unexpected SubPacket variant")
}
}
}
2020-01-24 20:35:42 -08:00
private fun handleReceivedNodeInfo(info: MeshProtos.NodeInfo) {
TODO()
}
/**
* 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() {
override fun onReceive(context: Context, intent: Intent) {
val proto = MeshProtos.FromRadio.parseFrom(intent.getByteArrayExtra(EXTRA_PAYLOAD)!!)
info("Received from radio service: ${proto.toOneLineString()}")
2020-01-24 22:22:30 -08:00
when (proto.variantCase.number) {
MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket(proto.packet)
MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleReceivedNodeInfo(proto.nodeInfo)
MeshProtos.FromRadio.MY_NODE_NUM_FIELD_NUMBER -> ourNodeNum = proto.myNodeNum
2020-01-24 22:22:30 -08:00
else -> TODO("Unexpected FromRadio variant")
}
}
}
2020-01-22 21:25:31 -08:00
private val binder = object : IMeshService.Stub() {
2020-01-25 10:00:57 -08:00
// 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
2020-01-26 11:33:51 -08:00
override fun subscribeReceiver(packageName: String, receiverName: String) =
toRemoteExceptions {
clientPackages[receiverName] = packageName
}
2020-01-22 21:25:31 -08:00
2020-01-25 10:00:57 -08:00
override fun setOwner(myId: String, longName: String, shortName: String) =
toRemoteExceptions {
2020-01-25 10:00:57 -08:00
error("TODO setOwner $myId : $longName : $shortName")
val user = MeshProtos.User.newBuilder().also {
it.id = myId
it.longName = longName
it.shortName = shortName
2020-01-24 20:35:42 -08:00
}.build()
2020-01-25 10:00:57 -08:00
// Also update our own map for our nodenum, by handling the packet just like packets from other users
if (ourNodeNum >= 0) {
2020-01-25 10:00:57 -08:00
handleReceivedUser(ourNodeNum, user)
}
sendToRadio(ToRadio.newBuilder().apply {
this.setOwner = user
})
}
override fun sendData(destId: String, payloadIn: ByteArray, typ: Int) =
toRemoteExceptions {
2020-01-25 10:00:57 -08:00
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
})
}
2020-01-22 21:25:31 -08:00
override fun getOnline(): Array<String> = toRemoteExceptions {
2020-01-24 20:46:29 -08:00
val r = nodeDBbyID.keys.toTypedArray()
info("in getOnline, count=${r.size}")
2020-01-24 20:35:42 -08:00
// return arrayOf("+16508675309")
2020-01-25 10:00:57 -08:00
r
2020-01-22 21:25:31 -08:00
}
override fun isConnected(): Boolean = toRemoteExceptions {
2020-01-24 20:46:29 -08:00
val r = this@MeshService.isConnected
info("in isConnected=r")
2020-01-25 10:00:57 -08:00
r
2020-01-22 21:25:31 -08:00
}
}
}