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

281 lines
10 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.BroadcastReceiver
import android.content.Context
2020-01-22 21:25:31 -08:00
import android.content.Intent
import android.content.IntentFilter
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.google.protobuf.ByteString
import java.nio.charset.Charset
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-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
*/
/**
* 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-22 22:16:30 -08:00
sendBroadcast(intent)
}
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-22 22:16:30 -08:00
sendBroadcast(intent)
}
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) {
RadioInterfaceService.sendToRadio(this, p.build().toByteArray())
}
2020-01-22 21:25:31 -08:00
override fun onBind(intent: Intent): IBinder {
// Return the interface
return binder
}
override fun onCreate() {
super.onCreate()
val filter = IntentFilter(RadioInterfaceService.RECEIVE_FROMRADIO_ACTION)
registerReceiver(radioInterfaceReceiver, filter)
2020-01-24 20:35:42 -08:00
// FIXME - don't do this until after we see that the radio is connected to the phone
// Ask for the current node DB
sendToRadio(ToRadio.newBuilder().apply {
wantNodes = ToRadio.WantNodes.newBuilder().build()
})
}
override fun onDestroy() {
unregisterReceiver(radioInterfaceReceiver)
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 = -1
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
private fun toNodeInfo(n: Int) = nodeDBbyNodeNum.getValue(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) }
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.getValue(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)
}
/// 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
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))
/// 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.SIMPLE_TEXT_VALUE ->
warn(
"TODO ignoring SIMPLE_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-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 ->
updateNodeInfo(fromNum) {
it.user = MeshUser(p.user.id, p.user.longName, p.user.shortName)
// This might have been the first time we know an ID for this node, so also update the by ID map
nodeDBbyID.set(p.user.id, it)
}
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)!!)
2020-01-24 22:22:30 -08:00
info("Received from radio service: $proto")
when (proto.variantCase.number) {
MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket(proto.packet)
MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleReceivedNodeInfo(proto.nodeInfo)
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-22 22:16:30 -08:00
override fun setOwner(myId: String, longName: String, shortName: String) {
error("TODO setOwner $myId : $longName : $shortName")
2020-01-22 21:25:31 -08:00
}
override fun sendOpaque(destId: String, payloadIn: ByteArray, typ: Int) {
2020-01-24 20:35:42 -08:00
info("sendOpaque $destId <- ${payloadIn.size}")
// encapsulate our payload in the proper protobufs and fire it off
val packet = newMeshPacketTo(destId).apply {
payload = MeshProtos.MeshPayload.newBuilder().apply {
addSubPackets(MeshProtos.SubPacket.newBuilder().apply {
data = MeshProtos.Data.newBuilder().also {
it.typ = MeshProtos.Data.Type.SIGNAL_OPAQUE
it.payload = ByteString.copyFrom(payloadIn)
2020-01-24 20:35:42 -08:00
}.build()
}.build())
}.build()
}.build()
sendToRadio(ToRadio.newBuilder().apply {
this.packet = packet
})
2020-01-22 21:25:31 -08:00
}
2020-01-22 22:16:30 -08:00
override fun getOnline(): Array<String> {
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-24 20:46:29 -08:00
return r
2020-01-22 21:25:31 -08:00
}
override fun isConnected(): Boolean {
2020-01-24 20:46:29 -08:00
val r = this@MeshService.isConnected
info("in isConnected=r")
return r
2020-01-22 21:25:31 -08:00
}
}
}