2020-02-10 15:31:56 -08:00
|
|
|
package com.geeksville.mesh.service
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2024-08-26 19:31:41 -03:00
|
|
|
import android.annotation.SuppressLint
|
2020-10-21 17:51:30 +08:00
|
|
|
import android.app.Service
|
2020-02-25 08:10:23 -08:00
|
|
|
import android.content.Context
|
|
|
|
|
import android.content.Intent
|
2023-06-18 17:33:06 -03:00
|
|
|
import android.content.pm.ServiceInfo
|
2020-01-22 21:25:31 -08:00
|
|
|
import android.os.IBinder
|
2020-02-17 18:46:20 -08:00
|
|
|
import android.os.RemoteException
|
2023-01-03 21:02:31 -03:00
|
|
|
import androidx.core.app.ServiceCompat
|
2024-06-13 07:26:56 -03:00
|
|
|
import androidx.core.location.LocationCompat
|
2022-09-04 22:52:40 -03:00
|
|
|
import com.geeksville.mesh.analytics.DataPair
|
|
|
|
|
import com.geeksville.mesh.android.GeeksvilleApplication
|
|
|
|
|
import com.geeksville.mesh.android.Logging
|
|
|
|
|
import com.geeksville.mesh.concurrent.handledLaunch
|
2020-02-10 17:05:51 -08:00
|
|
|
import com.geeksville.mesh.*
|
2022-09-12 19:07:30 -03:00
|
|
|
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
2023-09-11 19:39:49 -03:00
|
|
|
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
2020-01-24 20:35:42 -08:00
|
|
|
import com.geeksville.mesh.MeshProtos.MeshPacket
|
2024-09-16 06:16:06 -06:00
|
|
|
import com.geeksville.mesh.MeshProtos.RouteDiscovery
|
2020-01-24 20:35:42 -08:00
|
|
|
import com.geeksville.mesh.MeshProtos.ToRadio
|
2024-08-26 19:31:41 -03:00
|
|
|
import com.geeksville.mesh.android.hasLocationPermission
|
2022-09-13 22:49:38 -03:00
|
|
|
import com.geeksville.mesh.database.MeshLogRepository
|
2022-09-14 01:54:13 -03:00
|
|
|
import com.geeksville.mesh.database.PacketRepository
|
2022-09-13 22:49:38 -03:00
|
|
|
import com.geeksville.mesh.database.entity.MeshLog
|
2022-09-14 01:54:13 -03:00
|
|
|
import com.geeksville.mesh.database.entity.Packet
|
2021-03-02 15:12:57 +08:00
|
|
|
import com.geeksville.mesh.model.DeviceVersion
|
2023-05-20 11:42:15 -03:00
|
|
|
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
2022-05-20 09:13:59 -03:00
|
|
|
import com.geeksville.mesh.repository.location.LocationRepository
|
2023-10-12 17:52:52 -03:00
|
|
|
import com.geeksville.mesh.repository.network.MQTTRepository
|
2022-04-22 10:22:03 -07:00
|
|
|
import com.geeksville.mesh.repository.radio.BluetoothInterface
|
|
|
|
|
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
2022-04-22 17:39:48 -07:00
|
|
|
import com.geeksville.mesh.repository.radio.RadioServiceConnectionState
|
2022-09-04 22:52:40 -03:00
|
|
|
import com.geeksville.mesh.util.*
|
2020-01-24 20:35:42 -08:00
|
|
|
import com.google.protobuf.ByteString
|
2020-05-11 11:44:24 -07:00
|
|
|
import com.google.protobuf.InvalidProtocolBufferException
|
2022-02-08 13:50:21 -08:00
|
|
|
import dagger.Lazy
|
|
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
2023-02-03 19:41:30 -03:00
|
|
|
import java8.util.concurrent.CompletableFuture
|
2022-12-24 00:20:54 -03:00
|
|
|
import kotlinx.coroutines.CancellationException
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import kotlinx.coroutines.delay
|
2023-10-12 17:52:52 -03:00
|
|
|
import kotlinx.coroutines.flow.catch
|
2022-05-20 09:13:59 -03:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
|
|
|
|
import kotlinx.coroutines.flow.onEach
|
2023-02-18 08:20:36 -03:00
|
|
|
import kotlinx.coroutines.withTimeoutOrNull
|
2020-05-31 11:23:25 -07:00
|
|
|
import java.util.*
|
2023-02-13 18:38:22 -03:00
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue
|
2023-01-17 18:46:04 -03:00
|
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
|
import java.util.concurrent.TimeoutException
|
2022-02-08 13:50:21 -08:00
|
|
|
import javax.inject.Inject
|
2020-05-31 11:23:25 -07:00
|
|
|
import kotlin.math.absoluteValue
|
2020-01-24 17:05:55 -08:00
|
|
|
|
2020-01-23 08:09:50 -08:00
|
|
|
/**
|
2020-01-24 17:05:55 -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-10-01 22:20:19 +02:00
|
|
|
* Warning: do not override toString, it causes infinite recursion on some androids (because contextWrapper.getResources calls to string
|
2020-01-23 08:09:50 -08:00
|
|
|
*/
|
2022-02-08 13:50:21 -08:00
|
|
|
@AndroidEntryPoint
|
2020-01-22 22:16:30 -08:00
|
|
|
class MeshService : Service(), Logging {
|
2022-04-22 17:39:48 -07:00
|
|
|
@Inject
|
|
|
|
|
lateinit var dispatchers: CoroutineDispatchers
|
|
|
|
|
|
2022-09-14 01:54:13 -03:00
|
|
|
@Inject
|
|
|
|
|
lateinit var packetRepository: Lazy<PacketRepository>
|
|
|
|
|
|
2022-02-08 13:50:21 -08:00
|
|
|
@Inject
|
2022-09-13 22:49:38 -03:00
|
|
|
lateinit var meshLogRepository: Lazy<MeshLogRepository>
|
2020-01-22 22:16:30 -08:00
|
|
|
|
2022-04-22 17:39:48 -07:00
|
|
|
@Inject
|
|
|
|
|
lateinit var radioInterfaceService: RadioInterfaceService
|
|
|
|
|
|
2022-05-20 09:13:59 -03:00
|
|
|
@Inject
|
|
|
|
|
lateinit var locationRepository: LocationRepository
|
|
|
|
|
|
2022-06-11 18:36:57 -03:00
|
|
|
@Inject
|
2023-05-20 11:42:15 -03:00
|
|
|
lateinit var radioConfigRepository: RadioConfigRepository
|
2022-09-12 19:07:30 -03:00
|
|
|
|
2023-10-12 17:52:52 -03:00
|
|
|
@Inject
|
|
|
|
|
lateinit var mqttRepository: MQTTRepository
|
|
|
|
|
|
2020-02-12 15:47:06 -08:00
|
|
|
companion object : Logging {
|
2020-02-09 05:52:17 -08:00
|
|
|
|
|
|
|
|
/// Intents broadcast by MeshService
|
2021-01-11 17:15:19 +08:00
|
|
|
|
2021-03-24 13:48:32 +08:00
|
|
|
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
2021-01-11 17:15:19 +08:00
|
|
|
|
|
|
|
|
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
|
|
|
|
fun actionReceived(portNum: Int): String {
|
|
|
|
|
val portType = Portnums.PortNum.forNumber(portNum)
|
|
|
|
|
val portStr = portType?.toString() ?: portNum.toString()
|
|
|
|
|
|
|
|
|
|
return actionReceived(portStr)
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-09 05:52:17 -08:00
|
|
|
const val ACTION_NODE_CHANGE = "$prefix.NODE_CHANGE"
|
2020-02-10 15:31:56 -08:00
|
|
|
const val ACTION_MESH_CONNECTED = "$prefix.MESH_CONNECTED"
|
2022-11-29 17:45:04 -03:00
|
|
|
const val ACTION_MESSAGE_STATUS = "$prefix.MESSAGE_STATUS"
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
open class NodeNotFoundException(reason: String) : Exception(reason)
|
2023-01-12 17:25:28 -03:00
|
|
|
class InvalidNodeIdException(id: String) : NodeNotFoundException("Invalid NodeId $id")
|
2021-02-08 11:22:15 +08:00
|
|
|
class NodeNumNotFoundException(id: Int) : NodeNotFoundException("NodeNum not found $id")
|
|
|
|
|
class IdNotFoundException(id: String) : NodeNotFoundException("ID not found $id")
|
2021-02-04 23:39:44 +08:00
|
|
|
|
2022-11-29 17:47:49 -03:00
|
|
|
class NoDeviceConfigException(message: String = "No radio settings received (is our app too old?)") :
|
|
|
|
|
RadioNotConnectedException(message)
|
|
|
|
|
|
2020-06-07 20:17:47 -07:00
|
|
|
/**
|
|
|
|
|
* Talk to our running service and try to set a new device address. And then immediately
|
|
|
|
|
* call start on the service to possibly promote our service to be a foreground service.
|
|
|
|
|
*/
|
|
|
|
|
fun changeDeviceAddress(context: Context, service: IMeshService, address: String?) {
|
|
|
|
|
service.setDeviceAddress(address)
|
|
|
|
|
startService(context)
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:20:19 +02:00
|
|
|
fun createIntent() = Intent().setClassName(
|
|
|
|
|
"com.geeksville.mesh",
|
|
|
|
|
"com.geeksville.mesh.service.MeshService"
|
|
|
|
|
)
|
2021-03-19 22:49:51 +08:00
|
|
|
|
2023-08-05 07:04:37 -03:00
|
|
|
/** The minimum firmware version we know how to talk to. We'll still be able
|
|
|
|
|
* to talk to 2.0 firmwares but only well enough to ask them to firmware update.
|
2021-03-19 22:49:51 +08:00
|
|
|
*/
|
2024-06-30 08:52:52 -03:00
|
|
|
val minDeviceVersion = DeviceVersion("2.3.2")
|
2020-01-25 10:00:57 -08:00
|
|
|
}
|
|
|
|
|
|
2020-04-19 17:25:20 -07:00
|
|
|
enum class ConnectionState {
|
2020-04-04 15:29:16 -07:00
|
|
|
DISCONNECTED,
|
|
|
|
|
CONNECTED,
|
|
|
|
|
DEVICE_SLEEP // device is in LS sleep state, it will reconnected to us over bluetooth once it has data
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-07 17:38:54 -08:00
|
|
|
private var previousSummary: String? = null
|
|
|
|
|
|
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>()
|
2020-10-01 22:20:19 +02:00
|
|
|
private val serviceNotifications = MeshServiceNotifications(this)
|
2023-10-20 18:31:13 -03:00
|
|
|
private val serviceBroadcasts = MeshServiceBroadcasts(this, clientPackages) {
|
|
|
|
|
connectionState.also { radioConfigRepository.setConnectionState(it) }
|
|
|
|
|
}
|
2020-04-04 14:37:44 -07:00
|
|
|
private val serviceJob = Job()
|
|
|
|
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
2020-04-04 15:29:16 -07:00
|
|
|
private var connectionState = ConnectionState.DISCONNECTED
|
2021-03-03 07:30:05 +08:00
|
|
|
|
2022-05-20 09:13:59 -03:00
|
|
|
private var locationFlow: Job? = null
|
2023-10-12 17:52:52 -03:00
|
|
|
private var mqttMessageFlow: Job? = null
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2021-02-08 12:32:23 +08:00
|
|
|
private fun getSenderName(packet: DataPacket?): String {
|
2021-02-07 17:38:54 -08:00
|
|
|
val name = nodeDBbyID[packet?.from]?.user?.longName
|
2022-12-28 17:37:25 -03:00
|
|
|
return name ?: getString(R.string.unknown_username)
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:20:19 +02:00
|
|
|
private val notificationSummary
|
|
|
|
|
get() = when (connectionState) {
|
|
|
|
|
ConnectionState.CONNECTED -> getString(R.string.connected_count).format(
|
|
|
|
|
numOnlineNodes,
|
|
|
|
|
numNodes
|
|
|
|
|
)
|
|
|
|
|
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
|
|
|
|
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
|
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
|
|
|
|
|
/**
|
2020-02-19 10:53:36 -08:00
|
|
|
* start our location requests (if they weren't already running)
|
2020-02-16 14:22:24 -08:00
|
|
|
*/
|
2022-05-20 09:13:59 -03:00
|
|
|
private fun startLocationRequests() {
|
|
|
|
|
// If we're already observing updates, don't register again
|
|
|
|
|
if (locationFlow?.isActive == true) return
|
|
|
|
|
|
2024-08-26 19:31:41 -03:00
|
|
|
@SuppressLint("MissingPermission")
|
|
|
|
|
if (hasLocationPermission()) {
|
2024-06-13 07:26:56 -03:00
|
|
|
locationFlow = locationRepository.getLocations().onEach { location ->
|
|
|
|
|
sendPosition(
|
|
|
|
|
position {
|
|
|
|
|
latitudeI = Position.degI(location.latitude)
|
|
|
|
|
longitudeI = Position.degI(location.longitude)
|
2024-06-16 07:55:22 -03:00
|
|
|
if (LocationCompat.hasMslAltitude(location)) {
|
|
|
|
|
altitude = LocationCompat.getMslAltitudeMeters(location).toInt()
|
|
|
|
|
}
|
2024-06-13 07:26:56 -03:00
|
|
|
altitudeHae = location.altitude.toInt()
|
|
|
|
|
time = (location.time / 1000).toInt()
|
|
|
|
|
groundSpeed = location.speed.toInt()
|
|
|
|
|
groundTrack = location.bearing.toInt()
|
2024-09-04 09:20:51 -03:00
|
|
|
locationSource = MeshProtos.Position.LocSource.LOC_EXTERNAL
|
2024-06-13 07:26:56 -03:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}.launchIn(serviceScope)
|
2020-02-19 10:53:36 -08:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopLocationRequests() {
|
2022-05-20 09:13:59 -03:00
|
|
|
if (locationFlow?.isActive == true) {
|
2023-10-12 17:52:52 -03:00
|
|
|
info("Stopping location requests")
|
2022-05-20 09:13:59 -03:00
|
|
|
locationFlow?.cancel()
|
2023-01-12 17:25:28 -03:00
|
|
|
locationFlow = null
|
2020-02-19 10:53:36 -08:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
2020-02-10 15:31:56 -08:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
/** Send a command/packet to our radio. But cope with the possibility that we might start up
|
2021-03-27 18:15:57 +08:00
|
|
|
before we are fully bound to the RadioInterfaceService
|
|
|
|
|
*/
|
2022-04-22 17:39:48 -07:00
|
|
|
private fun sendToRadio(p: ToRadio.Builder) {
|
2021-05-09 09:02:53 +08:00
|
|
|
val built = p.build()
|
2021-05-10 08:19:27 +08:00
|
|
|
debug("Sending to radio ${built.toPIIString()}")
|
2021-05-09 09:02:53 +08:00
|
|
|
val b = built.toByteArray()
|
2020-01-27 19:23:34 -08:00
|
|
|
|
2022-04-22 17:39:48 -07:00
|
|
|
radioInterfaceService.sendToRadio(b)
|
2023-02-18 08:20:36 -03:00
|
|
|
changeStatus(p.packet.id, MessageStatus.ENROUTE)
|
2023-01-12 17:34:17 -03:00
|
|
|
|
|
|
|
|
if (p.packet.hasDecoded()) {
|
|
|
|
|
val packetToSave = MeshLog(
|
|
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"Packet",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
p.packet.toString()
|
|
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
|
|
|
|
}
|
2020-01-24 20:35:42 -08:00
|
|
|
}
|
|
|
|
|
|
2020-04-22 07:59:07 -07:00
|
|
|
/**
|
|
|
|
|
* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException
|
|
|
|
|
*/
|
2022-04-22 17:39:48 -07:00
|
|
|
private fun sendToRadio(packet: MeshPacket) {
|
2023-01-17 18:46:04 -03:00
|
|
|
queuedPackets.add(packet)
|
|
|
|
|
startPacketQueue()
|
2020-04-22 07:59:07 -07:00
|
|
|
}
|
|
|
|
|
|
2023-08-25 17:19:39 -03:00
|
|
|
private fun updateMessageNotification(dataPacket: DataPacket) {
|
|
|
|
|
val message: String = when (dataPacket.dataType) {
|
|
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> dataPacket.text!!
|
|
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
|
|
|
|
|
getString(R.string.waypoint_received, dataPacket.waypoint!!.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> return
|
|
|
|
|
}
|
|
|
|
|
serviceNotifications.updateMessageNotification(getSenderName(dataPacket), message)
|
|
|
|
|
}
|
2020-02-28 13:53:16 -08:00
|
|
|
|
|
|
|
|
override fun onCreate() {
|
|
|
|
|
super.onCreate()
|
2020-02-04 13:24:04 -08:00
|
|
|
|
2020-02-28 13:53:16 -08:00
|
|
|
info("Creating mesh service")
|
2020-02-04 13:24:04 -08:00
|
|
|
|
2020-04-19 17:25:20 -07:00
|
|
|
// Switch to the IO thread
|
|
|
|
|
serviceScope.handledLaunch {
|
2022-04-22 17:39:48 -07:00
|
|
|
radioInterfaceService.connect()
|
2020-04-19 17:25:20 -07:00
|
|
|
}
|
2022-12-24 00:20:54 -03:00
|
|
|
radioInterfaceService.connectionState.onEach(::onRadioConnectionState)
|
|
|
|
|
.launchIn(serviceScope)
|
|
|
|
|
radioInterfaceService.receivedData.onEach(::onReceiveFromRadio)
|
|
|
|
|
.launchIn(serviceScope)
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.localConfigFlow.onEach { localConfig = it }
|
2022-12-24 00:20:54 -03:00
|
|
|
.launchIn(serviceScope)
|
2023-09-11 19:39:49 -03:00
|
|
|
radioConfigRepository.moduleConfigFlow.onEach { moduleConfig = it }
|
|
|
|
|
.launchIn(serviceScope)
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.channelSetFlow.onEach { channelSet = it }
|
2023-02-20 21:48:55 -03:00
|
|
|
.launchIn(serviceScope)
|
2022-05-06 12:17:17 -07:00
|
|
|
|
|
|
|
|
// the rest of our init will happen once we are in radioConnection.onServiceConnected
|
2020-01-24 17:05:55 -08:00
|
|
|
}
|
|
|
|
|
|
2020-06-07 17:51:51 -07:00
|
|
|
/**
|
|
|
|
|
* If someone binds to us, this will be called after on create
|
|
|
|
|
*/
|
2022-09-16 18:17:19 -03:00
|
|
|
override fun onBind(intent: Intent?): IBinder {
|
2020-06-07 17:51:51 -07:00
|
|
|
return binder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If someone starts us (or restarts us) this will be called after onCreate)
|
|
|
|
|
*/
|
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
2023-01-02 21:12:57 -03:00
|
|
|
val a = radioInterfaceService.getBondedDeviceAddress()
|
|
|
|
|
val wantForeground = a != null && a != "n"
|
|
|
|
|
|
|
|
|
|
info("Requesting foreground service=$wantForeground")
|
|
|
|
|
|
|
|
|
|
// We always start foreground because that's how our service is always started (if we didn't then android would kill us)
|
|
|
|
|
// but if we don't really need foreground we immediately stop it.
|
|
|
|
|
val notification = serviceNotifications.createServiceStateNotification(notificationSummary)
|
2020-06-07 17:51:51 -07:00
|
|
|
|
2024-07-25 18:04:11 -03:00
|
|
|
try {
|
|
|
|
|
ServiceCompat.startForeground(
|
|
|
|
|
this,
|
2023-06-18 17:33:06 -03:00
|
|
|
serviceNotifications.notifyId,
|
|
|
|
|
notification,
|
2024-07-25 18:04:11 -03:00
|
|
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
2024-09-14 18:24:23 -03:00
|
|
|
if (hasLocationPermission()) {
|
|
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
|
|
|
|
} else {
|
|
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
|
|
|
|
}
|
2024-07-25 18:04:11 -03:00
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
},
|
2023-06-18 17:33:06 -03:00
|
|
|
)
|
2024-07-25 18:04:11 -03:00
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("startForeground failed", ex)
|
|
|
|
|
return START_NOT_STICKY
|
2023-06-18 17:33:06 -03:00
|
|
|
}
|
2023-01-02 21:12:57 -03:00
|
|
|
return if (!wantForeground) {
|
2023-01-03 21:02:31 -03:00
|
|
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
2023-01-02 21:12:57 -03:00
|
|
|
START_NOT_STICKY
|
|
|
|
|
} else {
|
|
|
|
|
START_STICKY
|
|
|
|
|
}
|
2020-06-07 17:51:51 -07:00
|
|
|
}
|
2020-01-27 16:00:00 -08:00
|
|
|
|
2020-01-24 17:05:55 -08:00
|
|
|
override fun onDestroy() {
|
2020-01-25 10:00:57 -08:00
|
|
|
info("Destroying mesh service")
|
2020-04-20 20:44:21 -07:00
|
|
|
|
2023-01-03 21:02:31 -03:00
|
|
|
// Make sure we aren't using the notification first
|
|
|
|
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
2021-02-05 09:31:25 +08:00
|
|
|
serviceNotifications.close()
|
|
|
|
|
|
2020-01-24 17:05:55 -08:00
|
|
|
super.onDestroy()
|
2020-04-04 14:37:44 -07:00
|
|
|
serviceJob.cancel()
|
2020-01-24 17:05:55 -08:00
|
|
|
}
|
|
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
///
|
|
|
|
|
/// BEGINNING OF MODEL - FIXME, move elsewhere
|
|
|
|
|
///
|
|
|
|
|
|
2024-09-13 00:19:21 -03:00
|
|
|
val myNodeInfo: MyNodeInfo? get() = radioConfigRepository.myNodeInfo.value
|
2020-02-16 14:22:24 -08:00
|
|
|
|
2024-06-03 10:17:20 -03:00
|
|
|
private val configTotal by lazy { ConfigProtos.Config.getDescriptor().fields.size }
|
|
|
|
|
private val moduleTotal by lazy { ModuleConfigProtos.ModuleConfig.getDescriptor().fields.size }
|
2024-09-04 09:31:15 -03:00
|
|
|
private var sessionPasskey: ByteString = ByteString.EMPTY
|
2024-06-03 10:17:20 -03:00
|
|
|
|
2022-09-12 19:07:30 -03:00
|
|
|
private var localConfig: LocalConfig = LocalConfig.getDefaultInstance()
|
2023-09-11 19:39:49 -03:00
|
|
|
private var moduleConfig: LocalModuleConfig = LocalModuleConfig.getDefaultInstance()
|
2023-02-20 21:48:55 -03:00
|
|
|
private var channelSet: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance()
|
2021-02-27 14:31:52 +08:00
|
|
|
|
2020-03-30 17:35:33 -07:00
|
|
|
/// True after we've done our initial node db init
|
2020-06-12 17:02:21 -07:00
|
|
|
@Volatile
|
2020-03-30 17:35:33 -07:00
|
|
|
private var haveNodeDB = false
|
|
|
|
|
|
2020-01-24 20:35:42 -08:00
|
|
|
// The database of active nodes, index is the node number
|
2024-09-13 00:19:21 -03:00
|
|
|
private val nodeDBbyNodeNum get() = radioConfigRepository.nodeDBbyNum.value
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2024-09-11 18:59:11 -03:00
|
|
|
// 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).
|
|
|
|
|
private val nodeDBbyID get() = nodeDBbyNodeNum.mapKeys { it.value.user?.id }
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
///
|
|
|
|
|
/// END OF MODEL
|
|
|
|
|
///
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
private val deviceVersion get() = DeviceVersion(myNodeInfo?.firmwareVersion ?: "")
|
|
|
|
|
private val appVersion get() = BuildConfig.VERSION_CODE
|
|
|
|
|
private val minAppVersion get() = myNodeInfo?.minAppVersion ?: 0
|
2020-12-10 09:23:02 +08:00
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
/// Map a nodenum to a node, or throw an exception if not found
|
2023-01-12 17:25:28 -03:00
|
|
|
private fun toNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: throw NodeNumNotFoundException(n)
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
/** Map a nodeNum to the nodeId string
|
2021-02-08 11:22:15 +08:00
|
|
|
If we have a NodeInfo for this ID we prefer to return the string ID inside the user record.
|
|
|
|
|
but some nodes might not have a user record at all (because not yet received), in that case, we return
|
|
|
|
|
a hex version of the ID just based on the number */
|
2023-01-12 17:25:28 -03:00
|
|
|
private fun toNodeID(n: Int): String =
|
|
|
|
|
if (n == DataPacket.NODENUM_BROADCAST) DataPacket.ID_BROADCAST
|
|
|
|
|
else nodeDBbyNodeNum[n]?.user?.id ?: DataPacket.nodeNumToDefaultId(n)
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2024-09-14 17:46:46 -03:00
|
|
|
private fun defaultUser(num: Int) = MeshUser(
|
|
|
|
|
id = DataPacket.nodeNumToDefaultId(num),
|
|
|
|
|
longName = getString(R.string.unknown_username),
|
|
|
|
|
shortName = getString(R.string.unknown_node_short_name),
|
|
|
|
|
hwModel = MeshProtos.HardwareModel.UNSET,
|
2024-09-13 00:19:21 -03:00
|
|
|
)
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2024-09-14 17:46:46 -03:00
|
|
|
// given a nodeNum, return a db entry - creating if necessary
|
|
|
|
|
private fun getOrCreateNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: NodeInfo(n, defaultUser(n))
|
|
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
private val hexIdRegex = """\!([0-9A-Fa-f]+)""".toRegex()
|
2023-10-08 23:38:28 -03:00
|
|
|
private val rangeTestRegex = Regex("seq (\\d{1,10})")
|
2021-02-08 11:22:15 +08:00
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
/// Map a userid to a node/ node num, or throw an exception if not found
|
2021-02-08 11:22:15 +08:00
|
|
|
/// We prefer to find nodes based on their assigned IDs, but if no ID has been assigned to a node, we can also find it based on node number
|
|
|
|
|
private fun toNodeInfo(id: String): NodeInfo {
|
|
|
|
|
// If this is a valid hexaddr will be !null
|
|
|
|
|
val hexStr = hexIdRegex.matchEntire(id)?.groups?.get(1)?.value
|
|
|
|
|
|
|
|
|
|
return nodeDBbyID[id] ?: when {
|
|
|
|
|
id == DataPacket.ID_LOCAL -> toNodeInfo(myNodeNum)
|
|
|
|
|
hexStr != null -> {
|
|
|
|
|
val n = hexStr.toLong(16).toInt()
|
|
|
|
|
nodeDBbyNodeNum[n] ?: throw IdNotFoundException(id)
|
|
|
|
|
}
|
2023-01-12 17:25:28 -03:00
|
|
|
else -> throw InvalidNodeIdException(id)
|
2021-02-08 11:22:15 +08:00
|
|
|
}
|
|
|
|
|
}
|
2020-01-26 15:01:59 -08:00
|
|
|
|
2024-08-18 09:34:47 -03:00
|
|
|
private fun getUserName(num: Int): String {
|
|
|
|
|
val user = nodeDBbyNodeNum[num]?.user
|
|
|
|
|
val longName = user?.longName ?: getString(R.string.unknown_username)
|
|
|
|
|
val shortName = user?.shortName ?: DataPacket.nodeNumToDefaultId(num)
|
|
|
|
|
return "$longName ($shortName)"
|
|
|
|
|
}
|
2024-04-07 16:26:47 -03:00
|
|
|
|
2020-02-25 09:28:47 -08:00
|
|
|
private val numNodes get() = nodeDBbyNodeNum.size
|
|
|
|
|
|
2020-02-19 10:53:36 -08:00
|
|
|
/**
|
|
|
|
|
* How many nodes are currently online (including our local node)
|
|
|
|
|
*/
|
2024-01-10 05:30:02 -03:00
|
|
|
private val numOnlineNodes get() = nodeDBbyNodeNum.values.count { it.isOnline }
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
private fun toNodeNum(id: String): Int = when (id) {
|
|
|
|
|
DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST
|
|
|
|
|
DataPacket.ID_LOCAL -> myNodeNum
|
|
|
|
|
else -> toNodeInfo(id).num
|
|
|
|
|
}
|
2020-01-24 22:22:30 -08:00
|
|
|
|
|
|
|
|
/// A helper function that makes it easy to update node info objects
|
2024-01-17 19:34:55 -03:00
|
|
|
private inline fun updateNodeInfo(
|
2021-03-27 17:29:46 +08:00
|
|
|
nodeNum: Int,
|
|
|
|
|
withBroadcast: Boolean = true,
|
2024-01-17 19:34:55 -03:00
|
|
|
crossinline updateFn: (NodeInfo) -> Unit,
|
2021-03-27 17:29:46 +08:00
|
|
|
) {
|
2020-01-24 22:22:30 -08:00
|
|
|
val info = getOrCreateNodeInfo(nodeNum)
|
2021-03-27 16:14:57 +08:00
|
|
|
updateFn(info)
|
2020-02-04 12:12:29 -08:00
|
|
|
|
|
|
|
|
// This might have been the first time we know an ID for this node, so also update the by ID map
|
2020-02-14 04:41:20 -08:00
|
|
|
val userId = info.user?.id.orEmpty()
|
2023-09-05 08:19:26 -03:00
|
|
|
if (userId.isNotEmpty()) {
|
|
|
|
|
if (haveNodeDB) serviceScope.handledLaunch {
|
|
|
|
|
radioConfigRepository.upsert(info)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2020-02-09 10:18:26 -08:00
|
|
|
// parcelable is busted
|
2021-03-27 17:29:46 +08:00
|
|
|
if (withBroadcast)
|
2021-03-27 16:14:57 +08:00
|
|
|
serviceBroadcasts.broadcastNodeChange(info)
|
2020-01-24 22:22:30 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-17 13:14:53 -08:00
|
|
|
/// My node num
|
2020-04-22 18:34:22 -07:00
|
|
|
private val myNodeNum
|
|
|
|
|
get() = myNodeInfo?.myNodeNum
|
|
|
|
|
?: throw RadioNotConnectedException("We don't yet have our myNodeInfo")
|
2020-02-17 13:14:53 -08:00
|
|
|
|
|
|
|
|
/// My node ID string
|
|
|
|
|
private val myNodeID get() = toNodeID(myNodeNum)
|
|
|
|
|
|
2022-10-10 18:09:20 -03:00
|
|
|
/// Admin channel index
|
2023-05-21 19:46:40 -03:00
|
|
|
private val adminChannelIndex: Int
|
2023-06-20 08:35:08 -03:00
|
|
|
get() = channelSet.settingsList.indexOfFirst { it.name.equals("admin", ignoreCase = true) }
|
2023-05-24 06:17:32 -03:00
|
|
|
.coerceAtLeast(0)
|
2022-10-10 18:09:20 -03:00
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
/// 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-02-16 14:22:24 -08:00
|
|
|
if (myNodeInfo == null)
|
2020-01-25 12:24:53 -08:00
|
|
|
throw RadioNotConnectedException()
|
|
|
|
|
|
2022-05-30 17:43:05 -03:00
|
|
|
from = 0 // don't add myNodeNum
|
2020-06-07 22:15:47 -07:00
|
|
|
|
2021-02-27 10:18:00 +08:00
|
|
|
to = idNum
|
2020-01-24 20:35:42 -08:00
|
|
|
}
|
|
|
|
|
|
2020-02-17 15:39:49 -08:00
|
|
|
/**
|
|
|
|
|
* Generate a new mesh packet builder with our node as the sender, and the specified recipient
|
|
|
|
|
*
|
|
|
|
|
* If id is null we assume a broadcast message
|
|
|
|
|
*/
|
2023-04-22 12:06:25 -03:00
|
|
|
private fun newMeshPacketTo(id: String) = newMeshPacketTo(toNodeNum(id))
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2020-02-17 15:39:49 -08:00
|
|
|
/**
|
|
|
|
|
* Helper to make it easy to build a subpacket in the proper protobufs
|
|
|
|
|
*/
|
2022-05-20 09:12:55 -03:00
|
|
|
private fun MeshPacket.Builder.buildMeshPacket(
|
2020-05-21 17:25:04 -07:00
|
|
|
wantAck: Boolean = false,
|
2021-03-02 22:12:42 +08:00
|
|
|
id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
|
2023-04-25 19:18:03 -03:00
|
|
|
hopLimit: Int = localConfig.lora.hopLimit,
|
2022-09-15 22:24:04 -03:00
|
|
|
channel: Int = 0,
|
2021-03-02 16:27:43 +08:00
|
|
|
priority: MeshPacket.Priority = MeshPacket.Priority.UNSET,
|
2021-02-27 10:18:00 +08:00
|
|
|
initFn: MeshProtos.Data.Builder.() -> Unit
|
2021-03-02 16:27:43 +08:00
|
|
|
): MeshPacket {
|
2020-05-21 17:25:04 -07:00
|
|
|
this.wantAck = wantAck
|
2020-05-30 17:28:00 -07:00
|
|
|
this.id = id
|
2021-02-27 22:28:59 -07:00
|
|
|
this.hopLimit = hopLimit
|
2022-09-15 22:24:04 -03:00
|
|
|
this.channel = channel
|
2021-03-02 16:27:43 +08:00
|
|
|
this.priority = priority
|
2021-02-27 10:18:00 +08:00
|
|
|
decoded = MeshProtos.Data.newBuilder().also {
|
2020-02-02 18:38:01 -08:00
|
|
|
initFn(it)
|
2020-01-25 10:00:57 -08:00
|
|
|
}.build()
|
2021-03-02 16:27:43 +08:00
|
|
|
|
|
|
|
|
return build()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper to make it easy to build a subpacket in the proper protobufs
|
|
|
|
|
*/
|
2022-05-20 09:12:55 -03:00
|
|
|
private fun MeshPacket.Builder.buildAdminPacket(
|
2023-04-22 12:06:25 -03:00
|
|
|
id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
|
2021-03-03 07:30:05 +08:00
|
|
|
wantResponse: Boolean = false,
|
2021-03-02 16:27:43 +08:00
|
|
|
initFn: AdminProtos.AdminMessage.Builder.() -> Unit
|
|
|
|
|
): MeshPacket = buildMeshPacket(
|
2023-04-22 12:06:25 -03:00
|
|
|
id = id,
|
2021-03-02 16:27:43 +08:00
|
|
|
wantAck = true,
|
2022-10-10 18:09:20 -03:00
|
|
|
channel = adminChannelIndex,
|
2021-03-02 16:27:43 +08:00
|
|
|
priority = MeshPacket.Priority.RELIABLE
|
2023-10-12 17:52:52 -03:00
|
|
|
) {
|
2021-03-03 07:30:05 +08:00
|
|
|
this.wantResponse = wantResponse
|
2021-03-02 16:27:43 +08:00
|
|
|
portnumValue = Portnums.PortNum.ADMIN_APP_VALUE
|
|
|
|
|
payload = AdminProtos.AdminMessage.newBuilder().also {
|
|
|
|
|
initFn(it)
|
2024-09-04 09:31:15 -03:00
|
|
|
it.sessionPasskey = sessionPasskey
|
2021-03-02 16:27:43 +08:00
|
|
|
}.build().toByteString()
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-19 11:47:34 -07:00
|
|
|
/// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so
|
|
|
|
|
private fun toDataPacket(packet: MeshPacket): DataPacket? {
|
2021-02-27 10:18:00 +08:00
|
|
|
return if (!packet.hasDecoded()) {
|
2020-04-19 11:47:34 -07:00
|
|
|
// We never convert packets that are not DataPackets
|
|
|
|
|
null
|
|
|
|
|
} else {
|
2021-02-27 10:18:00 +08:00
|
|
|
val data = packet.decoded
|
2020-04-19 11:47:34 -07:00
|
|
|
|
|
|
|
|
// If the rxTime was not set by the device (because device software was old), guess at a time
|
2021-03-02 14:16:57 +08:00
|
|
|
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
2020-04-19 11:47:34 -07:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
DataPacket(
|
2024-01-10 12:18:12 -03:00
|
|
|
from = toNodeID(packet.from),
|
|
|
|
|
to = toNodeID(packet.to),
|
2023-01-12 17:25:28 -03:00
|
|
|
time = rxTime * 1000L,
|
|
|
|
|
id = packet.id,
|
2023-09-11 19:39:49 -03:00
|
|
|
dataType = data.portnumValue,
|
2024-01-10 12:18:12 -03:00
|
|
|
bytes = data.payload.toByteArray(),
|
|
|
|
|
hopLimit = packet.hopLimit,
|
2024-09-16 10:28:42 -03:00
|
|
|
channel = if (packet.pkiEncrypted) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
|
2023-01-12 17:25:28 -03:00
|
|
|
)
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-30 15:48:50 -07:00
|
|
|
private fun toMeshPacket(p: DataPacket): MeshPacket {
|
2021-03-02 16:27:43 +08:00
|
|
|
return newMeshPacketTo(p.to!!).buildMeshPacket(
|
|
|
|
|
id = p.id,
|
|
|
|
|
wantAck = true,
|
2022-09-15 22:24:04 -03:00
|
|
|
hopLimit = p.hopLimit,
|
|
|
|
|
channel = p.channel,
|
2021-03-02 16:27:43 +08:00
|
|
|
) {
|
2021-02-27 10:18:00 +08:00
|
|
|
portnumValue = p.dataType
|
|
|
|
|
payload = ByteString.copyFrom(p.bytes)
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-08 19:24:13 -03:00
|
|
|
private val rememberDataType = setOf(
|
|
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE,
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-28 16:18:16 -03:00
|
|
|
private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) {
|
2023-10-08 19:24:13 -03:00
|
|
|
if (dataPacket.dataType !in rememberDataType) return
|
2023-01-27 16:13:49 -03:00
|
|
|
val fromLocal = dataPacket.from == DataPacket.ID_LOCAL
|
|
|
|
|
val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST
|
|
|
|
|
val contactId = if (fromLocal || toBroadcast) dataPacket.to else dataPacket.from
|
|
|
|
|
|
|
|
|
|
// contactKey: unique contact key filter (channel)+(nodeId)
|
|
|
|
|
val contactKey = "${dataPacket.channel}$contactId"
|
|
|
|
|
|
|
|
|
|
val packetToSave = Packet(
|
|
|
|
|
0L, // autoGenerated
|
2024-06-08 10:25:47 -03:00
|
|
|
myNodeNum,
|
2023-01-27 16:13:49 -03:00
|
|
|
dataPacket.dataType,
|
|
|
|
|
contactKey,
|
|
|
|
|
System.currentTimeMillis(),
|
2024-06-15 12:18:26 -03:00
|
|
|
fromLocal,
|
2023-01-27 16:13:49 -03:00
|
|
|
dataPacket
|
|
|
|
|
)
|
2024-04-28 16:18:16 -03:00
|
|
|
serviceScope.handledLaunch {
|
|
|
|
|
packetRepository.get().apply {
|
|
|
|
|
insert(packetToSave)
|
|
|
|
|
val isMuted = getContactSettings(contactKey).isMuted
|
|
|
|
|
if (updateNotification && !isMuted) updateMessageNotification(dataPacket)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
|
|
|
|
|
2020-01-25 06:16:10 -08:00
|
|
|
/// Update our model and resend as needed for a MeshPacket we just received from the radio
|
2020-04-19 11:47:34 -07:00
|
|
|
private fun handleReceivedData(packet: MeshPacket) {
|
2020-05-31 11:23:25 -07:00
|
|
|
myNodeInfo?.let { myInfo ->
|
2021-02-27 10:18:00 +08:00
|
|
|
val data = packet.decoded
|
2020-05-31 11:23:25 -07:00
|
|
|
val bytes = data.payload.toByteArray()
|
|
|
|
|
val fromId = toNodeID(packet.from)
|
|
|
|
|
val dataPacket = toDataPacket(packet)
|
2020-01-25 06:16:10 -08:00
|
|
|
|
2020-05-31 11:23:25 -07:00
|
|
|
if (dataPacket != null) {
|
2020-01-25 06:16:10 -08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
// We ignore most messages that we sent
|
|
|
|
|
val fromUs = myInfo.myNodeNum == packet.from
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
debug("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
|
2020-02-28 14:07:04 -08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
dataPacket.status = MessageStatus.RECEIVED
|
2021-02-27 10:18:00 +08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
// if (p.hasUser()) handleReceivedUser(fromNum, p.user)
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
/// We tell other apps about most message types, but some may have sensitve data, so that is not shared'
|
|
|
|
|
var shouldBroadcast = !fromUs
|
|
|
|
|
|
|
|
|
|
when (data.portnumValue) {
|
2023-10-08 23:38:28 -03:00
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
|
|
|
|
|
if (fromUs) return
|
|
|
|
|
|
|
|
|
|
// TODO temporary solution to Range Test spam, may be removed in the future
|
|
|
|
|
val isRangeTest = rangeTestRegex.matches(data.payload.toStringUtf8())
|
|
|
|
|
if (!moduleConfig.rangeTest.enabled && isRangeTest) return
|
|
|
|
|
|
|
|
|
|
debug("Received CLEAR_TEXT from $fromId")
|
|
|
|
|
rememberDataPacket(dataPacket)
|
|
|
|
|
}
|
2020-01-25 06:16:10 -08:00
|
|
|
|
2023-01-27 16:13:49 -03:00
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
|
|
|
|
|
val u = MeshProtos.Waypoint.parseFrom(data.payload)
|
|
|
|
|
// Validate locked Waypoints from the original sender
|
|
|
|
|
if (u.lockedTo != 0 && u.lockedTo != packet.from) return
|
2024-04-28 16:18:16 -03:00
|
|
|
rememberDataPacket(dataPacket, u.expire > currentSecond())
|
2023-01-27 16:13:49 -03:00
|
|
|
}
|
|
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
// Handle new style position info
|
|
|
|
|
Portnums.PortNum.POSITION_APP_VALUE -> {
|
2023-05-10 22:17:09 -03:00
|
|
|
if (data.wantResponse) return // ignore data from position requests
|
2024-09-09 19:02:21 -03:00
|
|
|
val u = MeshProtos.Position.parseFrom(data.payload)
|
2021-05-10 08:19:27 +08:00
|
|
|
// debug("position_app ${packet.from} ${u.toOneLineString()}")
|
2021-03-03 07:30:05 +08:00
|
|
|
handleReceivedPosition(packet.from, u, dataPacket.time)
|
|
|
|
|
}
|
2020-12-07 19:50:06 +08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
// Handle new style user info
|
|
|
|
|
Portnums.PortNum.NODEINFO_APP_VALUE ->
|
|
|
|
|
if (!fromUs) {
|
2020-12-07 19:50:06 +08:00
|
|
|
val u = MeshProtos.User.parseFrom(data.payload)
|
2024-02-28 17:52:49 -03:00
|
|
|
.copy { if (packet.viaMqtt) longName = "$longName (MQTT)" }
|
2024-01-17 19:34:55 -03:00
|
|
|
handleReceivedUser(packet.from, u, packet.channel)
|
2020-12-07 19:50:06 +08:00
|
|
|
}
|
2021-02-27 10:18:00 +08:00
|
|
|
|
2022-03-28 15:50:33 -03:00
|
|
|
// Handle new telemetry info
|
|
|
|
|
Portnums.PortNum.TELEMETRY_APP_VALUE -> {
|
2024-09-09 19:02:21 -03:00
|
|
|
val u = TelemetryProtos.Telemetry.parseFrom(data.payload)
|
2022-03-28 15:50:33 -03:00
|
|
|
handleReceivedTelemetry(packet.from, u, dataPacket.time)
|
2022-06-11 18:36:57 -03:00
|
|
|
}
|
2022-03-28 15:50:33 -03:00
|
|
|
|
2021-03-03 07:49:23 +08:00
|
|
|
Portnums.PortNum.ROUTING_APP_VALUE -> {
|
2023-10-12 22:52:54 -03:00
|
|
|
// We always send ACKs to other apps, because they might care about the messages they sent
|
|
|
|
|
shouldBroadcast = true
|
2021-03-03 07:49:23 +08:00
|
|
|
val u = MeshProtos.Routing.parseFrom(data.payload)
|
2023-01-02 22:23:23 -03:00
|
|
|
val isAck = u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE
|
2023-10-12 22:52:54 -03:00
|
|
|
|
|
|
|
|
if (u.errorReason == MeshProtos.Routing.Error.DUTY_CYCLE_LIMIT) {
|
2024-04-07 16:35:04 -03:00
|
|
|
radioConfigRepository.setErrorMessage(getString(R.string.error_duty_cycle))
|
2023-10-12 22:52:54 -03:00
|
|
|
}
|
|
|
|
|
|
2023-01-02 22:23:23 -03:00
|
|
|
handleAckNak(isAck, fromId, data.requestId)
|
2023-01-17 18:46:04 -03:00
|
|
|
queueResponse.remove(data.requestId)?.complete(true)
|
2021-03-03 07:49:23 +08:00
|
|
|
}
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
Portnums.PortNum.ADMIN_APP_VALUE -> {
|
|
|
|
|
val u = AdminProtos.AdminMessage.parseFrom(data.payload)
|
|
|
|
|
handleReceivedAdmin(packet.from, u)
|
|
|
|
|
shouldBroadcast = false
|
2020-12-07 20:33:29 +08:00
|
|
|
}
|
2020-03-04 11:16:43 -08:00
|
|
|
|
2024-02-18 07:37:18 -03:00
|
|
|
Portnums.PortNum.STORE_FORWARD_APP_VALUE -> {
|
|
|
|
|
val u = StoreAndForwardProtos.StoreAndForward.parseFrom(data.payload)
|
|
|
|
|
handleReceivedStoreAndForward(dataPacket, u)
|
|
|
|
|
shouldBroadcast = false
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-11 19:39:49 -03:00
|
|
|
Portnums.PortNum.RANGE_TEST_APP_VALUE -> {
|
|
|
|
|
if (!moduleConfig.rangeTest.enabled) return
|
|
|
|
|
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
|
|
|
|
rememberDataPacket(u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Portnums.PortNum.DETECTION_SENSOR_APP_VALUE -> {
|
|
|
|
|
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
|
|
|
|
rememberDataPacket(u)
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 16:26:47 -03:00
|
|
|
Portnums.PortNum.TRACEROUTE_APP_VALUE -> {
|
2024-04-21 08:48:40 -03:00
|
|
|
if (data.wantResponse) return // ignore data from traceroute requests
|
2024-04-07 16:26:47 -03:00
|
|
|
val parsed = MeshProtos.RouteDiscovery.parseFrom(data.payload)
|
2024-09-16 06:16:06 -06:00
|
|
|
handleReceivedTraceroute(packet, parsed)
|
2024-04-07 16:26:47 -03:00
|
|
|
}
|
|
|
|
|
|
2023-09-11 19:39:49 -03:00
|
|
|
else -> debug("No custom processing needed for ${data.portnumValue}")
|
2021-03-03 07:30:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We always tell other apps when new data packets arrive
|
|
|
|
|
if (shouldBroadcast)
|
|
|
|
|
serviceBroadcasts.broadcastReceivedData(dataPacket)
|
2020-04-22 07:59:07 -07:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"num_data_receive",
|
|
|
|
|
DataPair(1)
|
|
|
|
|
)
|
2020-05-31 11:23:25 -07:00
|
|
|
|
2021-03-03 07:30:05 +08:00
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"data_receive",
|
|
|
|
|
DataPair("num_bytes", bytes.size),
|
|
|
|
|
DataPair("type", data.portnumValue)
|
|
|
|
|
)
|
2020-05-31 11:23:25 -07:00
|
|
|
}
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
2020-01-25 06:16:10 -08:00
|
|
|
}
|
|
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) {
|
|
|
|
|
if (fromNodeNum == myNodeNum) {
|
2022-09-18 18:35:13 -03:00
|
|
|
when (a.payloadVariantCase) {
|
|
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
2022-05-30 17:43:05 -03:00
|
|
|
val response = a.getConfigResponse
|
|
|
|
|
debug("Admin: received config ${response.payloadVariantCase}")
|
2022-06-10 21:55:26 -03:00
|
|
|
setLocalConfig(response)
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
|
2022-09-18 18:35:13 -03:00
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
2021-03-02 22:12:42 +08:00
|
|
|
val mi = myNodeInfo
|
|
|
|
|
if (mi != null) {
|
|
|
|
|
val ch = a.getChannelResponse
|
2021-03-04 09:08:29 +08:00
|
|
|
debug("Admin: Received channel ${ch.index}")
|
2022-07-29 19:46:04 -03:00
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
if (ch.index + 1 < mi.maxChannels) {
|
2022-10-16 19:19:03 -03:00
|
|
|
handleChannel(ch)
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else ->
|
2022-09-18 18:35:13 -03:00
|
|
|
warn("No special processing needed for ${a.payloadVariantCase}")
|
2021-03-02 22:12:42 +08:00
|
|
|
|
|
|
|
|
}
|
2024-09-04 09:31:15 -03:00
|
|
|
} else {
|
|
|
|
|
debug("Admin: Received session_passkey from $fromNodeNum")
|
|
|
|
|
sessionPasskey = a.sessionPasskey
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-25 10:00:57 -08:00
|
|
|
/// Update our DB of users based on someone sending out a User subpacket
|
2024-01-17 19:34:55 -03:00
|
|
|
private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User, channel: Int = 0) {
|
2020-01-25 10:00:57 -08:00
|
|
|
updateNodeInfo(fromNum) {
|
2024-09-13 00:19:21 -03:00
|
|
|
it.user = MeshUser(p)
|
2024-01-17 19:34:55 -03:00
|
|
|
it.channel = channel
|
2020-01-25 10:00:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 14:16:57 +08:00
|
|
|
/** Update our DB of users based on someone sending out a Position subpacket
|
|
|
|
|
* @param defaultTime in msecs since 1970
|
2021-03-02 14:38:55 +08:00
|
|
|
*/
|
2020-08-18 11:25:16 -07:00
|
|
|
private fun handleReceivedPosition(
|
|
|
|
|
fromNum: Int,
|
|
|
|
|
p: MeshProtos.Position,
|
2021-03-02 14:16:57 +08:00
|
|
|
defaultTime: Long = System.currentTimeMillis()
|
2020-08-18 11:25:16 -07:00
|
|
|
) {
|
2023-05-10 22:17:09 -03:00
|
|
|
// Nodes periodically send out position updates, but those updates might not contain a lat & lon (because no GPS lock)
|
|
|
|
|
// We like to look at the local node to see if it has been sending out valid lat/lon, so for the LOCAL node (only)
|
2021-03-27 13:25:01 +08:00
|
|
|
// we don't record these nop position updates
|
2024-06-13 07:26:56 -03:00
|
|
|
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
|
2023-05-10 22:17:09 -03:00
|
|
|
debug("Ignoring nop position update for the local node")
|
2024-06-13 07:26:56 -03:00
|
|
|
} else {
|
2021-03-27 13:25:01 +08:00
|
|
|
updateNodeInfo(fromNum) {
|
2021-05-10 08:19:27 +08:00
|
|
|
debug("update position: ${it.user?.longName?.toPIIString()} with ${p.toPIIString()}")
|
2021-03-27 13:25:01 +08:00
|
|
|
it.position = Position(p, (defaultTime / 1000L).toInt())
|
|
|
|
|
}
|
2024-06-13 07:26:56 -03:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
2022-04-04 19:10:15 -03:00
|
|
|
/// Update our DB of users based on someone sending out a Telemetry subpacket
|
2022-03-28 15:50:33 -03:00
|
|
|
private fun handleReceivedTelemetry(
|
|
|
|
|
fromNum: Int,
|
2022-09-08 19:09:36 -03:00
|
|
|
t: TelemetryProtos.Telemetry,
|
2022-03-28 15:50:33 -03:00
|
|
|
defaultTime: Long = System.currentTimeMillis()
|
|
|
|
|
) {
|
|
|
|
|
updateNodeInfo(fromNum) {
|
2022-11-06 08:39:05 -03:00
|
|
|
if (t.hasDeviceMetrics()) it.deviceMetrics = DeviceMetrics(
|
|
|
|
|
t.deviceMetrics, if (t.time != 0) t.time else (defaultTime / 1000L).toInt()
|
2022-09-08 19:09:36 -03:00
|
|
|
)
|
2022-11-06 08:39:05 -03:00
|
|
|
if (t.hasEnvironmentMetrics()) it.environmentMetrics = EnvironmentMetrics(
|
|
|
|
|
t.environmentMetrics, if (t.time != 0) t.time else (defaultTime / 1000L).toInt()
|
2022-04-04 19:10:15 -03:00
|
|
|
)
|
2022-03-28 15:50:33 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-18 07:37:18 -03:00
|
|
|
private fun handleReceivedStoreAndForward(
|
|
|
|
|
dataPacket: DataPacket,
|
|
|
|
|
s: StoreAndForwardProtos.StoreAndForward,
|
|
|
|
|
) {
|
|
|
|
|
debug("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
|
|
|
|
|
when (s.variantCase) {
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
|
|
|
|
|
val u = dataPacket.copy(
|
|
|
|
|
bytes = s.stats.toString().encodeToByteArray(),
|
|
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
|
|
|
|
|
)
|
|
|
|
|
rememberDataPacket(u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.HISTORY -> {
|
|
|
|
|
val text = """
|
|
|
|
|
Total messages: ${s.history.historyMessages}
|
|
|
|
|
History window: ${s.history.window / 60000} min
|
|
|
|
|
Last request: ${s.history.lastRequest}
|
|
|
|
|
""".trimIndent()
|
|
|
|
|
val u = dataPacket.copy(
|
|
|
|
|
bytes = text.encodeToByteArray(),
|
|
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
|
|
|
|
|
)
|
|
|
|
|
rememberDataPacket(u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.TEXT -> {
|
|
|
|
|
if (s.rr == StoreAndForwardProtos.StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
|
|
|
|
|
dataPacket.to = DataPacket.ID_BROADCAST
|
|
|
|
|
}
|
2024-02-25 07:42:22 -03:00
|
|
|
val u = dataPacket.copy(
|
|
|
|
|
bytes = s.text.toByteArray(),
|
|
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
)
|
2024-02-18 07:37:18 -03:00
|
|
|
rememberDataPacket(u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-16 06:16:06 -06:00
|
|
|
@Suppress("MagicNumber")
|
|
|
|
|
private fun formatTraceroutePath(nodesList: List<Int>, snrList: List<Int>): String {
|
|
|
|
|
// nodesList should include both origin and destination nodes
|
|
|
|
|
// origin will not have an SNR value, but destination should
|
|
|
|
|
val snrStr = if (snrList.size == nodesList.size - 1) snrList else {
|
|
|
|
|
// use unknown SNR for entire route if snrList has invalid size
|
|
|
|
|
List(nodesList.size - 1) { -128 }
|
|
|
|
|
}.map { snr ->
|
|
|
|
|
val str = if (snr == -128) "?" else "${snr / 4}"
|
|
|
|
|
"⇊ $str dB"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nodesList.map { nodeId ->
|
|
|
|
|
"■ ${getUserName(nodeId)}"
|
|
|
|
|
}.flatMapIndexed { i, nodeStr ->
|
|
|
|
|
if (i == 0) listOf(nodeStr) else listOf(snrStr[i-1], nodeStr)
|
|
|
|
|
}.joinToString("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleReceivedTraceroute(packet: MeshPacket, trace: RouteDiscovery) {
|
|
|
|
|
val nodesToward = mutableListOf<Int>()
|
|
|
|
|
nodesToward.add(packet.to)
|
|
|
|
|
nodesToward += trace.routeList
|
|
|
|
|
nodesToward.add(packet.from)
|
|
|
|
|
|
|
|
|
|
val nodesBack = mutableListOf<Int>()
|
|
|
|
|
if (packet.hopStart > 0 && trace.snrBackList.size > 0) { // otherwise back route is invalid
|
|
|
|
|
nodesBack.add(packet.from)
|
|
|
|
|
nodesBack += trace.routeBackList
|
|
|
|
|
nodesBack.add(packet.to)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
radioConfigRepository.setTracerouteResponse(buildString {
|
|
|
|
|
append("Route traced toward destination:\n\n")
|
|
|
|
|
append(formatTraceroutePath(nodesToward, trace.snrTowardsList))
|
|
|
|
|
if (nodesBack.size > 0) {
|
|
|
|
|
append("\n\n")
|
|
|
|
|
append("Route traced back to us:\n\n")
|
|
|
|
|
append(formatTraceroutePath(nodesBack, trace.snrBackList))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
// If apps try to send packets when our radio is sleeping, we queue them here instead
|
|
|
|
|
private val offlineSentPackets = mutableListOf<DataPacket>()
|
|
|
|
|
|
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) {
|
2020-03-30 17:35:33 -07:00
|
|
|
if (haveNodeDB) {
|
|
|
|
|
processReceivedMeshPacket(packet)
|
|
|
|
|
onNodeDBChanged()
|
|
|
|
|
} else {
|
2022-06-20 22:46:45 -03:00
|
|
|
warn("Ignoring early received packet: ${packet.toOneLineString()}")
|
2021-06-11 09:45:04 -07:00
|
|
|
//earlyReceivedPackets.add(packet)
|
|
|
|
|
//logAssert(earlyReceivedPackets.size < 128) // The max should normally be about 32, but if the device is messed up it might try to send forever
|
2020-03-30 17:35:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-13 18:38:22 -03:00
|
|
|
private val queuedPackets = ConcurrentLinkedQueue<MeshPacket>()
|
2023-01-17 18:46:04 -03:00
|
|
|
private val queueResponse = mutableMapOf<Int, CompletableFuture<Boolean>>()
|
|
|
|
|
private var queueJob: Job? = null
|
|
|
|
|
|
|
|
|
|
private fun sendPacket(packet: MeshPacket): CompletableFuture<Boolean> {
|
|
|
|
|
// send the packet to the radio and return a CompletableFuture that will be completed with the result
|
|
|
|
|
val future = CompletableFuture<Boolean>()
|
|
|
|
|
queueResponse[packet.id] = future
|
|
|
|
|
try {
|
2023-02-18 08:20:36 -03:00
|
|
|
if (connectionState != ConnectionState.CONNECTED) throw RadioNotConnectedException()
|
2023-01-17 18:46:04 -03:00
|
|
|
sendToRadio(ToRadio.newBuilder().apply {
|
|
|
|
|
this.packet = packet
|
|
|
|
|
})
|
|
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("sendToRadio error:", ex)
|
|
|
|
|
future.complete(false)
|
|
|
|
|
}
|
|
|
|
|
return future
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun startPacketQueue() {
|
|
|
|
|
if (queueJob?.isActive == true) return
|
|
|
|
|
queueJob = serviceScope.handledLaunch {
|
|
|
|
|
debug("packet queueJob started")
|
|
|
|
|
while (connectionState == ConnectionState.CONNECTED) {
|
|
|
|
|
// take the first packet from the queue head
|
|
|
|
|
val packet = queuedPackets.poll() ?: break
|
2023-10-19 17:12:08 -03:00
|
|
|
try {
|
2023-02-13 18:38:22 -03:00
|
|
|
// send packet to the radio and wait for response
|
|
|
|
|
val response = sendPacket(packet)
|
2023-10-19 17:12:08 -03:00
|
|
|
debug("queueJob packet id=${packet.id.toUInt()} waiting")
|
|
|
|
|
val success = response.get(2, TimeUnit.MINUTES)
|
2023-01-17 18:46:04 -03:00
|
|
|
debug("queueJob packet id=${packet.id.toUInt()} success $success")
|
|
|
|
|
} catch (e: TimeoutException) {
|
2023-10-19 17:12:08 -03:00
|
|
|
debug("queueJob packet id=${packet.id.toUInt()} timeout")
|
|
|
|
|
} catch (e: Exception) {
|
2023-02-13 18:38:22 -03:00
|
|
|
debug("queueJob packet id=${packet.id.toUInt()} failed")
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopPacketQueue() {
|
|
|
|
|
if (queueJob?.isActive == true) {
|
2023-10-12 17:52:52 -03:00
|
|
|
info("Stopping packet queueJob")
|
2023-01-17 18:46:04 -03:00
|
|
|
queueJob?.cancel()
|
|
|
|
|
queueJob = null
|
|
|
|
|
queuedPackets.clear()
|
2023-02-18 08:20:36 -03:00
|
|
|
queueResponse.entries.lastOrNull { !it.value.isDone }?.value?.complete(false)
|
2023-01-17 18:46:04 -03:00
|
|
|
queueResponse.clear()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 14:03:10 -07:00
|
|
|
private fun sendNow(p: DataPacket) {
|
|
|
|
|
val packet = toMeshPacket(p)
|
|
|
|
|
p.time = System.currentTimeMillis() // update time to the actual time we started sending
|
2021-05-10 08:19:27 +08:00
|
|
|
// debug("Sending to radio: ${packet.toPIIString()}")
|
2020-06-11 14:03:10 -07:00
|
|
|
sendToRadio(packet)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
private fun processQueuedPackets() {
|
|
|
|
|
val sentPackets = mutableListOf<DataPacket>()
|
|
|
|
|
offlineSentPackets.forEach { p ->
|
2023-02-18 08:20:36 -03:00
|
|
|
try {
|
|
|
|
|
sendNow(p)
|
2024-07-21 10:30:54 -03:00
|
|
|
sentPackets.add(p)
|
2023-02-18 08:20:36 -03:00
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("Error sending queued message:", ex)
|
2023-01-12 17:47:59 -03:00
|
|
|
}
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|
2024-07-21 10:30:54 -03:00
|
|
|
offlineSentPackets.removeAll(sentPackets)
|
2021-12-25 19:30:45 -03:00
|
|
|
}
|
2020-03-30 17:35:33 -07:00
|
|
|
|
2023-02-18 08:20:36 -03:00
|
|
|
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000) {
|
|
|
|
|
var dataPacket: DataPacket? = null
|
|
|
|
|
while (dataPacket == null) {
|
|
|
|
|
dataPacket = packetRepository.get().getDataPacketById(packetId)
|
|
|
|
|
if (dataPacket == null) delay(100)
|
|
|
|
|
}
|
|
|
|
|
dataPacket
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Change the status on a DataPacket and update watchers
|
|
|
|
|
*/
|
|
|
|
|
private fun changeStatus(packetId: Int, m: MessageStatus) = serviceScope.handledLaunch {
|
|
|
|
|
if (packetId != 0) getDataPacketById(packetId)?.let { p ->
|
|
|
|
|
if (p.status == m) return@handledLaunch
|
|
|
|
|
packetRepository.get().updateMessageStatus(p, m)
|
|
|
|
|
serviceBroadcasts.broadcastMessageStatus(packetId, m)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-30 19:58:36 -07:00
|
|
|
/**
|
|
|
|
|
* Handle an ack/nak packet by updating sent message status
|
|
|
|
|
*/
|
2023-01-20 19:19:13 -03:00
|
|
|
private fun handleAckNak(isAck: Boolean, fromId: String, requestId: Int) {
|
2023-01-02 22:23:23 -03:00
|
|
|
serviceScope.handledLaunch {
|
2023-02-18 08:20:36 -03:00
|
|
|
val p = getDataPacketById(requestId)
|
2023-01-20 19:19:13 -03:00
|
|
|
// distinguish real ACKs coming from the intended receiver
|
|
|
|
|
val m = if (isAck && fromId == p?.to) MessageStatus.RECEIVED
|
|
|
|
|
else if (isAck) MessageStatus.DELIVERED else MessageStatus.ERROR
|
|
|
|
|
if (p != null && p.status != MessageStatus.RECEIVED)
|
|
|
|
|
packetRepository.get().updateMessageStatus(p, m)
|
|
|
|
|
serviceBroadcasts.broadcastMessageStatus(requestId, m)
|
2020-05-30 19:58:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 17:35:33 -07:00
|
|
|
/// Update our model and resend as needed for a MeshPacket we just received from the radio
|
|
|
|
|
private fun processReceivedMeshPacket(packet: MeshPacket) {
|
2020-01-24 22:22:30 -08:00
|
|
|
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)
|
2020-02-29 13:42:15 -08:00
|
|
|
//val toNum = packet.to
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2020-06-13 16:02:57 -07:00
|
|
|
// debug("Recieved: $packet")
|
2021-02-27 10:18:00 +08:00
|
|
|
if (packet.hasDecoded()) {
|
2022-09-13 22:49:38 -03:00
|
|
|
val packetToSave = MeshLog(
|
2021-02-27 10:18:00 +08:00
|
|
|
UUID.randomUUID().toString(),
|
2022-07-29 19:46:04 -03:00
|
|
|
"Packet",
|
2021-02-27 10:18:00 +08:00
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
packet.toString()
|
|
|
|
|
)
|
2022-09-13 22:49:38 -03:00
|
|
|
insertMeshLog(packetToSave)
|
2020-04-19 11:47:34 -07:00
|
|
|
|
2024-04-07 19:50:27 -03:00
|
|
|
serviceScope.handledLaunch {
|
|
|
|
|
radioConfigRepository.emitMeshPacket(packet)
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-27 10:18:00 +08:00
|
|
|
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
|
|
|
|
|
// through our node on the way to the phone that means that local node is also alive in the mesh
|
2020-02-19 11:35:16 -08:00
|
|
|
|
2021-03-27 16:14:57 +08:00
|
|
|
val isOtherNode = myNodeNum != fromNum
|
|
|
|
|
updateNodeInfo(myNodeNum, withBroadcast = isOtherNode) {
|
2021-03-27 10:53:39 +08:00
|
|
|
it.lastHeard = currentSecond()
|
2021-02-27 10:18:00 +08:00
|
|
|
}
|
|
|
|
|
|
2021-03-27 16:14:57 +08:00
|
|
|
// Do not generate redundant broadcasts of node change for this bookkeeping updateNodeInfo call
|
|
|
|
|
// because apps really only care about important updates of node state - which handledReceivedData will give them
|
|
|
|
|
updateNodeInfo(fromNum, withBroadcast = false) {
|
|
|
|
|
// If the rxTime was not set by the device (because device software was old), guess at a time
|
|
|
|
|
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
|
|
|
|
|
2020-07-23 23:12:01 -04:00
|
|
|
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
|
2024-09-08 08:33:39 -03:00
|
|
|
it.lastHeard = rxTime
|
2021-03-19 14:33:55 -07:00
|
|
|
it.snr = packet.rxSnr
|
2021-03-22 21:10:58 -07:00
|
|
|
it.rssi = packet.rxRssi
|
2020-02-16 14:22:24 -08:00
|
|
|
|
2024-04-21 12:14:35 +01:00
|
|
|
// Generate our own hopsAway, comparing hopStart to hopLimit.
|
|
|
|
|
if (packet.hopStart != 0 && packet.hopLimit <= packet.hopStart) {
|
|
|
|
|
it.hopsAway = packet.hopStart - packet.hopLimit
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-19 11:47:34 -07:00
|
|
|
handleReceivedData(packet)
|
2021-02-27 10:18:00 +08:00
|
|
|
}
|
2020-01-24 22:22:30 -08:00
|
|
|
}
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2022-09-13 22:49:38 -03:00
|
|
|
private fun insertMeshLog(packetToSave: MeshLog) {
|
2020-09-23 22:47:45 -04:00
|
|
|
serviceScope.handledLaunch {
|
2021-05-10 08:19:27 +08:00
|
|
|
// Do not log, because might contain PII
|
|
|
|
|
// info("insert: ${packetToSave.message_type} = ${packetToSave.raw_message.toOneLineString()}")
|
2022-09-13 22:49:38 -03:00
|
|
|
meshLogRepository.get().insert(packetToSave)
|
2020-09-23 22:47:45 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 22:46:45 -03:00
|
|
|
private fun setLocalConfig(config: ConfigProtos.Config) {
|
2022-06-11 18:36:57 -03:00
|
|
|
serviceScope.handledLaunch {
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.setLocalConfig(config)
|
2022-06-11 18:36:57 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-22 22:01:37 -03:00
|
|
|
private fun setLocalModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
|
|
|
|
serviceScope.handledLaunch {
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.setLocalModuleConfig(config)
|
2022-11-22 22:01:37 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 22:46:45 -03:00
|
|
|
private fun clearLocalConfig() {
|
|
|
|
|
serviceScope.handledLaunch {
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.clearLocalConfig()
|
|
|
|
|
radioConfigRepository.clearLocalModuleConfig()
|
2022-06-20 22:46:45 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-06 08:08:17 -03:00
|
|
|
private fun updateChannelSettings(ch: ChannelProtos.Channel) = serviceScope.handledLaunch {
|
2023-05-21 19:46:40 -03:00
|
|
|
radioConfigRepository.updateChannelSettings(ch)
|
2022-09-12 19:07:30 -03:00
|
|
|
}
|
|
|
|
|
|
2020-02-19 11:35:16 -08:00
|
|
|
private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt()
|
|
|
|
|
|
2020-04-04 15:29:16 -07:00
|
|
|
|
2020-02-19 10:53:36 -08:00
|
|
|
/// If we just changed our nodedb, we might want to do somethings
|
|
|
|
|
private fun onNodeDBChanged() {
|
2021-02-07 17:38:54 -08:00
|
|
|
maybeUpdateServiceStatusNotification()
|
2021-03-17 21:00:01 -07:00
|
|
|
}
|
2020-02-19 10:53:36 -08:00
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
/**
|
|
|
|
|
* Send in analytics about mesh connection
|
|
|
|
|
*/
|
|
|
|
|
private fun reportConnection() {
|
|
|
|
|
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"mesh_connect",
|
|
|
|
|
DataPair("num_nodes", numNodes),
|
|
|
|
|
DataPair("num_online", numOnlineNodes),
|
|
|
|
|
radioModel
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Once someone connects to hardware start tracking the approximate number of nodes in their mesh
|
|
|
|
|
// this allows us to collect stats on what typical mesh size is and to tell difference between users who just
|
|
|
|
|
// downloaded the app, vs has connected it to some hardware.
|
|
|
|
|
GeeksvilleApplication.analytics.setUserInfo(
|
|
|
|
|
DataPair("num_nodes", numNodes),
|
|
|
|
|
radioModel
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-04 15:29:16 -07:00
|
|
|
private var sleepTimeout: Job? = null
|
|
|
|
|
|
2020-04-21 14:46:52 -07:00
|
|
|
/// msecs since 1970 we started this connection
|
|
|
|
|
private var connectTimeMsec = 0L
|
|
|
|
|
|
2020-02-04 12:12:29 -08:00
|
|
|
/// Called when we gain/lose connection to our radio
|
2020-04-04 15:29:16 -07:00
|
|
|
private fun onConnectionChanged(c: ConnectionState) {
|
2023-10-21 07:24:46 -03:00
|
|
|
debug("onConnectionChanged: $connectionState -> $c")
|
2020-04-04 15:29:16 -07:00
|
|
|
|
|
|
|
|
/// Perform all the steps needed once we start waiting for device sleep to complete
|
|
|
|
|
fun startDeviceSleep() {
|
2023-01-17 18:46:04 -03:00
|
|
|
stopPacketQueue()
|
2020-04-04 15:29:16 -07:00
|
|
|
stopLocationRequests()
|
2023-10-12 17:52:52 -03:00
|
|
|
stopMqttClientProxy()
|
2020-04-04 15:29:16 -07:00
|
|
|
|
2020-04-21 14:46:52 -07:00
|
|
|
if (connectTimeMsec != 0L) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
connectTimeMsec = 0L
|
|
|
|
|
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"connected_seconds",
|
|
|
|
|
DataPair((now - connectTimeMsec) / 1000.0)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 18:31:13 -03:00
|
|
|
// Have our timeout fire in the appropriate number of seconds
|
2020-04-04 15:29:16 -07:00
|
|
|
sleepTimeout = serviceScope.handledLaunch {
|
|
|
|
|
try {
|
|
|
|
|
// If we have a valid timeout, wait that long (+30 seconds) otherwise, just wait 30 seconds
|
2022-05-30 17:43:05 -03:00
|
|
|
val timeout = (localConfig.power?.lsSecs ?: 0) + 30
|
2020-04-04 15:29:16 -07:00
|
|
|
|
|
|
|
|
debug("Waiting for sleeping device, timeout=$timeout secs")
|
|
|
|
|
delay(timeout * 1000L)
|
|
|
|
|
warn("Device timeout out, setting disconnected")
|
|
|
|
|
onConnectionChanged(ConnectionState.DISCONNECTED)
|
|
|
|
|
} catch (ex: CancellationException) {
|
|
|
|
|
debug("device sleep timeout cancelled")
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-31 11:23:25 -07:00
|
|
|
|
|
|
|
|
// broadcast an intent with our new connection state
|
2020-10-01 22:20:19 +02:00
|
|
|
serviceBroadcasts.broadcastConnection()
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun startDisconnect() {
|
2023-01-17 18:46:04 -03:00
|
|
|
stopPacketQueue()
|
2022-05-20 09:13:59 -03:00
|
|
|
stopLocationRequests()
|
2023-10-12 17:52:52 -03:00
|
|
|
stopMqttClientProxy()
|
2022-05-20 09:13:59 -03:00
|
|
|
|
2020-04-04 15:29:16 -07:00
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"mesh_disconnect",
|
|
|
|
|
DataPair("num_nodes", numNodes),
|
|
|
|
|
DataPair("num_online", numOnlineNodes)
|
|
|
|
|
)
|
2020-04-21 14:46:52 -07:00
|
|
|
GeeksvilleApplication.analytics.track("num_nodes", DataPair(numNodes))
|
2020-05-31 11:23:25 -07:00
|
|
|
|
|
|
|
|
// broadcast an intent with our new connection state
|
2020-10-01 22:20:19 +02:00
|
|
|
serviceBroadcasts.broadcastConnection()
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun startConnect() {
|
2020-02-04 12:12:29 -08:00
|
|
|
// Do our startup init
|
2020-02-17 18:46:20 -08:00
|
|
|
try {
|
2020-04-21 14:46:52 -07:00
|
|
|
connectTimeMsec = System.currentTimeMillis()
|
2020-06-05 21:12:15 -07:00
|
|
|
startConfig()
|
2020-05-11 11:44:24 -07:00
|
|
|
} catch (ex: InvalidProtocolBufferException) {
|
|
|
|
|
errormsg(
|
|
|
|
|
"Invalid protocol buffer sent by device - update device software and try again",
|
|
|
|
|
ex
|
|
|
|
|
)
|
2020-04-24 15:59:01 -07:00
|
|
|
} catch (ex: RadioNotConnectedException) {
|
|
|
|
|
// note: no need to call startDeviceSleep(), because this exception could only have reached us if it was already called
|
2020-05-11 11:44:24 -07:00
|
|
|
errormsg("Lost connection to radio during init - waiting for reconnect")
|
2020-02-17 18:46:20 -08:00
|
|
|
} catch (ex: RemoteException) {
|
|
|
|
|
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which
|
|
|
|
|
// causes the phone to try and reconnect. If we fail downloading our initial radio state we don't want to
|
|
|
|
|
// claim we have a valid connection still
|
2020-04-04 15:29:16 -07:00
|
|
|
connectionState = ConnectionState.DEVICE_SLEEP
|
|
|
|
|
startDeviceSleep()
|
2021-03-27 15:50:16 +08:00
|
|
|
throw ex // Important to rethrow so that we don't tell the app all is well
|
2020-02-04 12:12:29 -08:00
|
|
|
}
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
2020-02-25 09:28:47 -08:00
|
|
|
|
2020-04-04 15:29:16 -07:00
|
|
|
// Cancel any existing timeouts
|
|
|
|
|
sleepTimeout?.let {
|
|
|
|
|
it.cancel()
|
|
|
|
|
sleepTimeout = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectionState = c
|
|
|
|
|
when (c) {
|
2023-10-12 17:52:52 -03:00
|
|
|
ConnectionState.CONNECTED -> startConnect()
|
|
|
|
|
ConnectionState.DEVICE_SLEEP -> startDeviceSleep()
|
|
|
|
|
ConnectionState.DISCONNECTED -> startDisconnect()
|
2020-02-04 12:12:29 -08:00
|
|
|
}
|
2020-02-28 13:53:16 -08:00
|
|
|
|
2020-05-31 11:23:25 -07:00
|
|
|
// Update the android notification in the status bar
|
2021-02-07 17:38:54 -08:00
|
|
|
maybeUpdateServiceStatusNotification()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun maybeUpdateServiceStatusNotification() {
|
|
|
|
|
val currentSummary = notificationSummary
|
|
|
|
|
if (previousSummary == null || !previousSummary.equals(currentSummary)) {
|
|
|
|
|
serviceNotifications.updateServiceStateNotification(currentSummary)
|
|
|
|
|
previousSummary = currentSummary
|
|
|
|
|
}
|
2020-05-31 11:23:25 -07:00
|
|
|
}
|
|
|
|
|
|
2022-04-22 17:39:48 -07:00
|
|
|
private fun onRadioConnectionState(state: RadioServiceConnectionState) {
|
2022-10-05 22:06:46 -03:00
|
|
|
// sleep now disabled by default on ESP32, permanent is true unless light sleep enabled
|
|
|
|
|
val isRouter = localConfig.device.role == ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
|
|
|
|
val lsEnabled = localConfig.power.isPowerSaving || isRouter
|
2022-04-22 17:39:48 -07:00
|
|
|
val connected = state.isConnected
|
|
|
|
|
val permanent = state.isPermanent || !lsEnabled
|
|
|
|
|
onConnectionChanged(
|
|
|
|
|
when {
|
|
|
|
|
connected -> ConnectionState.CONNECTED
|
|
|
|
|
permanent -> ConnectionState.DISCONNECTED
|
|
|
|
|
else -> ConnectionState.DEVICE_SLEEP
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
2020-02-04 12:12:29 -08:00
|
|
|
|
2022-04-22 17:39:48 -07:00
|
|
|
private fun onReceiveFromRadio(bytes: ByteArray) {
|
|
|
|
|
try {
|
2022-06-20 22:46:45 -03:00
|
|
|
val proto = MeshProtos.FromRadio.parseFrom(bytes)
|
2022-04-22 17:39:48 -07:00
|
|
|
// info("Received from radio service: ${proto.toOneLineString()}")
|
|
|
|
|
when (proto.payloadVariantCase.number) {
|
2022-06-20 22:46:45 -03:00
|
|
|
MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket(proto.packet)
|
|
|
|
|
MeshProtos.FromRadio.CONFIG_COMPLETE_ID_FIELD_NUMBER -> handleConfigComplete(proto.configCompleteId)
|
2022-04-22 17:39:48 -07:00
|
|
|
MeshProtos.FromRadio.MY_INFO_FIELD_NUMBER -> handleMyInfo(proto.myInfo)
|
|
|
|
|
MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleNodeInfo(proto.nodeInfo)
|
2022-10-16 19:19:03 -03:00
|
|
|
MeshProtos.FromRadio.CHANNEL_FIELD_NUMBER -> handleChannel(proto.channel)
|
2022-06-20 22:46:45 -03:00
|
|
|
MeshProtos.FromRadio.CONFIG_FIELD_NUMBER -> handleDeviceConfig(proto.config)
|
2022-09-13 22:59:50 -03:00
|
|
|
MeshProtos.FromRadio.MODULECONFIG_FIELD_NUMBER -> handleModuleConfig(proto.moduleConfig)
|
2023-01-17 18:46:04 -03:00
|
|
|
MeshProtos.FromRadio.QUEUESTATUS_FIELD_NUMBER -> handleQueueStatus(proto.queueStatus)
|
2023-08-05 07:04:37 -03:00
|
|
|
MeshProtos.FromRadio.METADATA_FIELD_NUMBER -> handleMetadata(proto.metadata)
|
2023-10-12 17:52:52 -03:00
|
|
|
MeshProtos.FromRadio.MQTTCLIENTPROXYMESSAGE_FIELD_NUMBER -> handleMqttProxyMessage(proto.mqttClientProxyMessage)
|
2024-08-25 10:55:55 -03:00
|
|
|
MeshProtos.FromRadio.CLIENTNOTIFICATION_FIELD_NUMBER -> {
|
|
|
|
|
handleClientNotification(proto.clientNotification)
|
|
|
|
|
}
|
2022-04-22 17:39:48 -07:00
|
|
|
else -> errormsg("Unexpected FromRadio variant")
|
2020-01-24 22:22:30 -08:00
|
|
|
}
|
2022-04-22 17:39:48 -07:00
|
|
|
} catch (ex: InvalidProtocolBufferException) {
|
|
|
|
|
errormsg("Invalid Protobuf from radio, len=${bytes.size}", ex)
|
2020-01-24 17:05:55 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
/// A provisional MyNodeInfo that we will install if all of our node config downloads go okay
|
|
|
|
|
private var newMyNodeInfo: MyNodeInfo? = null
|
|
|
|
|
|
|
|
|
|
/// provisional NodeInfos we will install if all goes well
|
|
|
|
|
private val newNodes = mutableListOf<MeshProtos.NodeInfo>()
|
|
|
|
|
|
|
|
|
|
/// Used to make sure we never get foold by old BLE packets
|
|
|
|
|
private var configNonce = 1
|
|
|
|
|
|
2022-06-20 22:46:45 -03:00
|
|
|
private fun handleDeviceConfig(config: ConfigProtos.Config) {
|
|
|
|
|
debug("Received config ${config.toOneLineString()}")
|
2022-09-13 22:49:38 -03:00
|
|
|
val packetToSave = MeshLog(
|
2022-06-20 22:46:45 -03:00
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"Config ${config.payloadVariantCase}",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
config.toString()
|
|
|
|
|
)
|
2022-09-13 22:49:38 -03:00
|
|
|
insertMeshLog(packetToSave)
|
2022-06-20 22:46:45 -03:00
|
|
|
setLocalConfig(config)
|
2024-06-03 10:17:20 -03:00
|
|
|
val configCount = localConfig.allFields.size
|
|
|
|
|
radioConfigRepository.setStatusMessage("Device config ($configCount / $configTotal)")
|
2022-06-20 22:46:45 -03:00
|
|
|
}
|
|
|
|
|
|
2022-11-22 22:01:37 -03:00
|
|
|
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
|
|
|
|
debug("Received moduleConfig ${config.toOneLineString()}")
|
2022-09-13 22:59:50 -03:00
|
|
|
val packetToSave = MeshLog(
|
|
|
|
|
UUID.randomUUID().toString(),
|
2022-11-22 22:01:37 -03:00
|
|
|
"ModuleConfig ${config.payloadVariantCase}",
|
2022-09-13 22:59:50 -03:00
|
|
|
System.currentTimeMillis(),
|
2022-11-22 22:01:37 -03:00
|
|
|
config.toString()
|
2022-09-13 22:59:50 -03:00
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
2022-11-22 22:01:37 -03:00
|
|
|
setLocalModuleConfig(config)
|
2024-06-03 10:17:20 -03:00
|
|
|
val moduleCount = moduleConfig.allFields.size
|
|
|
|
|
radioConfigRepository.setStatusMessage("Module config ($moduleCount / $moduleTotal)")
|
2022-09-13 22:59:50 -03:00
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:46:04 -03:00
|
|
|
private fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
|
|
|
|
|
debug("queueStatus ${queueStatus.toOneLineString()}")
|
|
|
|
|
val (success, isFull, requestId) = with(queueStatus) {
|
2023-03-08 08:07:47 -03:00
|
|
|
Triple(res == 0, free == 0, meshPacketId)
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
if (success && isFull) return // Queue is full, wait for free != 0
|
|
|
|
|
if (requestId != 0) queueResponse.remove(requestId)?.complete(success)
|
|
|
|
|
else queueResponse.entries.lastOrNull { !it.value.isDone }?.value?.complete(success)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-16 19:19:03 -03:00
|
|
|
private fun handleChannel(ch: ChannelProtos.Channel) {
|
|
|
|
|
debug("Received channel ${ch.index}")
|
|
|
|
|
val packetToSave = MeshLog(
|
|
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"Channel",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
ch.toString()
|
|
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
2023-05-06 08:08:17 -03:00
|
|
|
if (ch.role != ChannelProtos.Channel.Role.DISABLED) updateChannelSettings(ch)
|
2024-06-03 10:17:20 -03:00
|
|
|
val maxChannels = myNodeInfo?.maxChannels ?: 8
|
|
|
|
|
radioConfigRepository.setStatusMessage("Channels (${ch.index + 1} / $maxChannels)")
|
2022-10-16 19:19:03 -03:00
|
|
|
}
|
|
|
|
|
|
2024-09-13 00:19:21 -03:00
|
|
|
private fun MeshProtos.NodeInfo.toEntity() = NodeInfo(
|
|
|
|
|
num = num,
|
2024-09-14 17:46:46 -03:00
|
|
|
user = if (hasUser()) {
|
|
|
|
|
MeshUser(user.copy { if (viaMqtt) longName = "$longName (MQTT)" })
|
|
|
|
|
} else {
|
|
|
|
|
defaultUser(num)
|
|
|
|
|
},
|
|
|
|
|
position = if (hasPosition()) {
|
|
|
|
|
Position(position)
|
|
|
|
|
} else {
|
|
|
|
|
null
|
|
|
|
|
},
|
|
|
|
|
snr = snr,
|
2024-09-13 00:19:21 -03:00
|
|
|
lastHeard = lastHeard,
|
2024-09-14 17:46:46 -03:00
|
|
|
deviceMetrics = if(hasDeviceMetrics()) {
|
|
|
|
|
DeviceMetrics(deviceMetrics)
|
|
|
|
|
} else {
|
|
|
|
|
null
|
|
|
|
|
},
|
2024-09-13 00:19:21 -03:00
|
|
|
channel = channel,
|
|
|
|
|
hopsAway = hopsAway,
|
|
|
|
|
)
|
2020-04-22 18:34:22 -07:00
|
|
|
|
|
|
|
|
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
|
2022-04-04 19:10:15 -03:00
|
|
|
debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}, hasDeviceMetrics=${info.hasDeviceMetrics()}")
|
2020-04-22 18:34:22 -07:00
|
|
|
|
2022-09-13 22:49:38 -03:00
|
|
|
val packetToSave = MeshLog(
|
2020-10-01 16:59:34 -04:00
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"NodeInfo",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
info.toString()
|
|
|
|
|
)
|
2022-09-13 22:49:38 -03:00
|
|
|
insertMeshLog(packetToSave)
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
newNodes.add(info)
|
2024-06-03 10:17:20 -03:00
|
|
|
radioConfigRepository.setStatusMessage("Nodes (${newNodes.size} / 100)")
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2021-03-14 11:42:04 +08:00
|
|
|
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null
|
2023-08-05 07:04:37 -03:00
|
|
|
private var rawDeviceMetadata: MeshProtos.DeviceMetadata? = null
|
2021-03-14 11:42:04 +08:00
|
|
|
|
|
|
|
|
/** Regenerate the myNodeInfo model. We call this twice. Once after we receive myNodeInfo from the device
|
|
|
|
|
* and again after we have the node DB (which might allow us a better notion of our HwModel.
|
|
|
|
|
*/
|
|
|
|
|
private fun regenMyNodeInfo() {
|
|
|
|
|
val myInfo = rawMyNodeInfo
|
|
|
|
|
if (myInfo != null) {
|
|
|
|
|
val mi = with(myInfo) {
|
|
|
|
|
MyNodeInfo(
|
2024-09-09 18:57:36 -03:00
|
|
|
myNodeNum = myNodeNum,
|
|
|
|
|
hasGPS = false,
|
|
|
|
|
model = rawDeviceMetadata?.hwModel?.let { hwModel ->
|
|
|
|
|
if (hwModel == MeshProtos.HardwareModel.UNSET) null
|
|
|
|
|
else hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
|
|
|
|
},
|
|
|
|
|
firmwareVersion = rawDeviceMetadata?.firmwareVersion,
|
|
|
|
|
couldUpdate = false,
|
2024-02-25 03:38:51 -07:00
|
|
|
shouldUpdate = false, // TODO add check after re-implementing firmware updates
|
2024-09-09 18:57:36 -03:00
|
|
|
currentPacketId = currentPacketId and 0xffffffffL,
|
|
|
|
|
messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code
|
|
|
|
|
minAppVersion = minAppVersion,
|
|
|
|
|
maxChannels = 8,
|
|
|
|
|
hasWifi = rawDeviceMetadata?.hasWifi ?: false,
|
|
|
|
|
channelUtilization = 0f,
|
|
|
|
|
airUtilTx = 0f,
|
2021-03-14 11:42:04 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
newMyNodeInfo = mi
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun sendAnalytics() {
|
|
|
|
|
val myInfo = rawMyNodeInfo
|
|
|
|
|
val mi = myNodeInfo
|
|
|
|
|
if (myInfo != null && mi != null) {
|
|
|
|
|
/// Track types of devices and firmware versions in use
|
|
|
|
|
GeeksvilleApplication.analytics.setUserInfo(
|
|
|
|
|
DataPair("firmware", mi.firmwareVersion),
|
|
|
|
|
DataPair("hw_model", mi.model),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-30 17:28:00 -07:00
|
|
|
/**
|
2023-08-05 07:04:37 -03:00
|
|
|
* Update MyNodeInfo (called from either new API version or the old one)
|
2020-05-30 17:28:00 -07:00
|
|
|
*/
|
2020-04-22 18:34:22 -07:00
|
|
|
private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
|
2022-09-13 22:49:38 -03:00
|
|
|
val packetToSave = MeshLog(
|
2020-10-01 16:59:34 -04:00
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"MyNodeInfo",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
myInfo.toString()
|
|
|
|
|
)
|
2022-09-13 22:49:38 -03:00
|
|
|
insertMeshLog(packetToSave)
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2021-03-14 11:42:04 +08:00
|
|
|
rawMyNodeInfo = myInfo
|
2020-04-22 18:34:22 -07:00
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
// We'll need to get a new set of channels and settings now
|
2022-09-12 19:07:30 -03:00
|
|
|
serviceScope.handledLaunch {
|
2023-05-20 11:42:15 -03:00
|
|
|
radioConfigRepository.clearChannelSet()
|
|
|
|
|
radioConfigRepository.clearLocalConfig()
|
|
|
|
|
radioConfigRepository.clearLocalModuleConfig()
|
2021-03-24 15:19:26 +08:00
|
|
|
}
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2023-08-05 07:04:37 -03:00
|
|
|
/**
|
|
|
|
|
* Update our DeviceMetadata
|
|
|
|
|
*/
|
|
|
|
|
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
|
2023-08-25 17:00:56 -03:00
|
|
|
debug("Received deviceMetadata ${metadata.toOneLineString()}")
|
2023-08-05 07:04:37 -03:00
|
|
|
val packetToSave = MeshLog(
|
|
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"DeviceMetadata",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
metadata.toString()
|
|
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
|
|
|
|
|
|
|
|
|
rawDeviceMetadata = metadata
|
2024-09-09 18:57:36 -03:00
|
|
|
regenMyNodeInfo()
|
2023-08-05 07:04:37 -03:00
|
|
|
}
|
|
|
|
|
|
2023-10-12 17:52:52 -03:00
|
|
|
/**
|
|
|
|
|
* Publish MqttClientProxyMessage (fromRadio)
|
|
|
|
|
*/
|
|
|
|
|
private fun handleMqttProxyMessage(message: MeshProtos.MqttClientProxyMessage) {
|
|
|
|
|
with(message) {
|
|
|
|
|
when (payloadVariantCase) {
|
|
|
|
|
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.TEXT -> {
|
2023-12-27 19:48:40 -03:00
|
|
|
mqttRepository.publish(topic, text.encodeToByteArray(), retained)
|
2023-10-12 17:52:52 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.DATA -> {
|
|
|
|
|
mqttRepository.publish(topic, data.toByteArray(), retained)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-25 10:55:55 -03:00
|
|
|
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
|
|
|
|
|
debug("Received clientNotification ${notification.toOneLineString()}")
|
|
|
|
|
radioConfigRepository.setErrorMessage(notification.message)
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-12 17:52:52 -03:00
|
|
|
/**
|
|
|
|
|
* Connect, subscribe and receive Flow of MqttClientProxyMessage (toRadio)
|
|
|
|
|
*/
|
|
|
|
|
private fun startMqttClientProxy() {
|
|
|
|
|
if (mqttMessageFlow?.isActive == true) return
|
|
|
|
|
if (moduleConfig.mqtt.enabled && moduleConfig.mqtt.proxyToClientEnabled) {
|
|
|
|
|
mqttMessageFlow = mqttRepository.proxyMessageFlow.onEach { message ->
|
|
|
|
|
sendToRadio(ToRadio.newBuilder().apply { mqttClientProxyMessage = message })
|
|
|
|
|
}.catch { throwable ->
|
2024-04-07 16:35:04 -03:00
|
|
|
radioConfigRepository.setErrorMessage("MqttClientProxy failed: $throwable")
|
2023-10-12 17:52:52 -03:00
|
|
|
}.launchIn(serviceScope)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopMqttClientProxy() {
|
|
|
|
|
if (mqttMessageFlow?.isActive == true) {
|
|
|
|
|
info("Stopping MqttClientProxy")
|
|
|
|
|
mqttMessageFlow?.cancel()
|
|
|
|
|
mqttMessageFlow = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-25 19:30:45 -03:00
|
|
|
/// If we've received our initial config, our radio settings and all of our channels, send any queued packets and broadcast connected to clients
|
2021-03-02 22:12:42 +08:00
|
|
|
private fun onHasSettings() {
|
2021-03-03 07:49:23 +08:00
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
processQueuedPackets() // send any packets that were queued up
|
2023-10-12 17:52:52 -03:00
|
|
|
startMqttClientProxy()
|
2021-03-02 22:12:42 +08:00
|
|
|
|
|
|
|
|
// broadcast an intent with our new connection state
|
|
|
|
|
serviceBroadcasts.broadcastConnection()
|
|
|
|
|
onNodeDBChanged()
|
|
|
|
|
reportConnection()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 00:19:21 -03:00
|
|
|
private fun handleConfigComplete(configCompleteId: Int) = serviceScope.handledLaunch {
|
2020-04-22 18:34:22 -07:00
|
|
|
if (configCompleteId == configNonce) {
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2022-09-13 22:49:38 -03:00
|
|
|
val packetToSave = MeshLog(
|
2020-10-01 16:59:34 -04:00
|
|
|
UUID.randomUUID().toString(),
|
|
|
|
|
"ConfigComplete",
|
|
|
|
|
System.currentTimeMillis(),
|
|
|
|
|
configCompleteId.toString()
|
|
|
|
|
)
|
2022-09-13 22:49:38 -03:00
|
|
|
insertMeshLog(packetToSave)
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
// This was our config request
|
2023-10-20 18:31:13 -03:00
|
|
|
if (newMyNodeInfo == null || newNodes.isEmpty()) {
|
2020-07-01 15:47:58 -07:00
|
|
|
errormsg("Did not receive a valid config")
|
2023-10-20 18:31:13 -03:00
|
|
|
} else {
|
2021-02-01 10:31:39 +08:00
|
|
|
debug("Installing new node DB")
|
2024-09-13 00:19:21 -03:00
|
|
|
radioConfigRepository.installNodeDB(newMyNodeInfo!!, newNodes.map { it.toEntity() })
|
2020-04-22 18:34:22 -07:00
|
|
|
newNodes.clear() // Just to save RAM ;-)
|
|
|
|
|
|
2024-09-13 18:30:55 -03:00
|
|
|
withTimeoutOrNull(timeMillis = 5000) {
|
|
|
|
|
while (myNodeInfo == null) {
|
|
|
|
|
delay(100)
|
|
|
|
|
}
|
|
|
|
|
} ?: errormsg("Timeout: installNodeDB failed!")
|
|
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
haveNodeDB = true // we now have nodes from real hardware
|
2021-03-14 11:42:04 +08:00
|
|
|
|
2024-09-09 19:13:13 -03:00
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
setTimeOnly = currentSecond()
|
|
|
|
|
})
|
2021-03-14 11:42:04 +08:00
|
|
|
sendAnalytics()
|
|
|
|
|
|
2022-08-29 12:50:29 -03:00
|
|
|
if (deviceVersion < minDeviceVersion || appVersion < minAppVersion) {
|
|
|
|
|
info("Device firmware or app is too old, faking config so firmware update can occur")
|
2022-06-20 22:46:45 -03:00
|
|
|
clearLocalConfig()
|
2024-08-25 09:32:31 -03:00
|
|
|
setLocalConfig(config { security = security.copy { isManaged = true } })
|
2022-10-16 19:19:03 -03:00
|
|
|
}
|
|
|
|
|
onHasSettings()
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
2023-10-20 18:31:13 -03:00
|
|
|
} else {
|
2020-04-22 18:34:22 -07:00
|
|
|
warn("Ignoring stale config complete")
|
2023-10-20 18:31:13 -03:00
|
|
|
}
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start the modern (REV2) API configuration flow
|
|
|
|
|
*/
|
|
|
|
|
private fun startConfig() {
|
|
|
|
|
configNonce += 1
|
|
|
|
|
newNodes.clear()
|
|
|
|
|
newMyNodeInfo = null
|
2022-10-12 23:40:54 -03:00
|
|
|
|
|
|
|
|
if (BluetoothInterface.invalidVersion) onHasSettings() // Device firmware is too old
|
|
|
|
|
|
2020-06-09 09:53:32 -07:00
|
|
|
debug("Starting config nonce=$configNonce")
|
2020-04-22 18:34:22 -07:00
|
|
|
|
2020-04-23 11:02:44 -07:00
|
|
|
sendToRadio(ToRadio.newBuilder().apply {
|
|
|
|
|
this.wantConfigId = configNonce
|
|
|
|
|
})
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:20:19 +02:00
|
|
|
/**
|
|
|
|
|
* Send a position (typically from our built in GPS) into the mesh.
|
|
|
|
|
*/
|
2020-02-19 18:51:59 -08:00
|
|
|
private fun sendPosition(
|
2024-06-13 07:26:56 -03:00
|
|
|
position: MeshProtos.Position,
|
2022-11-06 17:46:57 -03:00
|
|
|
destNum: Int? = null,
|
|
|
|
|
wantResponse: Boolean = false
|
2020-02-19 18:51:59 -08:00
|
|
|
) {
|
2021-03-27 15:50:16 +08:00
|
|
|
try {
|
2021-03-28 10:33:59 +08:00
|
|
|
val mi = myNodeInfo
|
2021-03-29 20:33:06 +08:00
|
|
|
if (mi != null) {
|
2022-11-06 17:46:57 -03:00
|
|
|
val idNum = destNum ?: mi.myNodeNum // when null we just send to the local node
|
2024-06-13 07:26:56 -03:00
|
|
|
debug("Sending our position/time to=$idNum ${Position(position)}")
|
2020-02-16 14:22:24 -08:00
|
|
|
|
2024-01-17 19:34:55 -03:00
|
|
|
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
|
2021-03-28 11:05:39 +08:00
|
|
|
handleReceivedPosition(mi.myNodeNum, position)
|
2021-03-02 16:27:43 +08:00
|
|
|
|
2024-06-13 07:26:56 -03:00
|
|
|
sendToRadio(newMeshPacketTo(idNum).buildMeshPacket(
|
2024-01-17 19:34:55 -03:00
|
|
|
channel = if (destNum == null) 0 else nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
|
|
|
|
priority = MeshPacket.Priority.BACKGROUND,
|
|
|
|
|
) {
|
|
|
|
|
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
|
|
|
|
payload = position.toByteString()
|
|
|
|
|
this.wantResponse = wantResponse
|
2024-06-13 07:26:56 -03:00
|
|
|
})
|
2021-03-28 10:33:59 +08:00
|
|
|
}
|
2021-03-20 11:23:31 +08:00
|
|
|
} catch (ex: BLEException) {
|
2021-01-05 14:01:45 +08:00
|
|
|
warn("Ignoring disconnected radio during gps location update")
|
|
|
|
|
}
|
2020-10-01 22:20:19 +02:00
|
|
|
}
|
|
|
|
|
|
2020-04-23 11:18:48 -07:00
|
|
|
/**
|
2023-03-27 15:30:42 -03:00
|
|
|
* Send setOwner admin packet with [MeshProtos.User] protobuf
|
2020-04-23 11:18:48 -07:00
|
|
|
*/
|
2023-10-03 18:05:40 -03:00
|
|
|
private fun setOwner(packetId: Int, user: MeshProtos.User) = with(user) {
|
2023-03-27 15:30:42 -03:00
|
|
|
val dest = nodeDBbyID[id]
|
2023-10-03 18:05:40 -03:00
|
|
|
?: throw Exception("Can't set user without a NodeInfo") // this shouldn't happen
|
|
|
|
|
val old = dest.user!!
|
|
|
|
|
if (longName == old.longName && shortName == old.shortName && isLicensed == old.isLicensed) {
|
|
|
|
|
debug("Ignoring nop owner change")
|
|
|
|
|
} else {
|
|
|
|
|
debug("setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed")
|
2020-06-13 16:02:57 -07:00
|
|
|
|
2023-10-03 18:05:40 -03:00
|
|
|
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
|
|
|
|
|
handleReceivedUser(dest.num, user)
|
2021-02-27 11:13:30 +08:00
|
|
|
|
2023-10-03 18:05:40 -03:00
|
|
|
// encapsulate our payload in the proper protobuf and fire it off
|
|
|
|
|
sendToRadio(newMeshPacketTo(dest.num).buildAdminPacket(id = packetId) {
|
|
|
|
|
setOwner = user
|
|
|
|
|
})
|
|
|
|
|
}
|
2020-04-23 11:18:48 -07:00
|
|
|
}
|
|
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2020-05-30 17:28:00 -07:00
|
|
|
/// Do not use directly, instead call generatePacketId()
|
2021-03-02 22:12:42 +08:00
|
|
|
private var currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue
|
2020-05-30 17:28:00 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate a unique packet ID (if we know enough to do so - otherwise return 0 so the device will do it)
|
|
|
|
|
*/
|
2021-03-02 22:12:42 +08:00
|
|
|
@Synchronized
|
2020-05-30 17:28:00 -07:00
|
|
|
private fun generatePacketId(): Int {
|
2021-03-02 22:12:42 +08:00
|
|
|
val numPacketIds =
|
2022-05-20 09:12:55 -03:00
|
|
|
((1L shl 32) - 1) // A mask for only the valid packet ID bits, either 255 or maxint
|
2020-05-30 17:28:00 -07:00
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
currentPacketId++
|
2021-02-12 13:50:39 +08:00
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits
|
2020-05-30 17:28:00 -07:00
|
|
|
|
2021-03-02 22:12:42 +08:00
|
|
|
// Use modulus and +1 to ensure we skip 0 on any values we return
|
|
|
|
|
return ((currentPacketId % numPacketIds) + 1L).toInt()
|
2020-05-30 17:28:00 -07:00
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
private fun enqueueForSending(p: DataPacket) {
|
|
|
|
|
if (p.dataType in rememberDataType) {
|
|
|
|
|
offlineSentPackets.add(p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 17:47:02 +08:00
|
|
|
private val binder = object : IMeshService.Stub() {
|
2020-04-19 19:23:20 -07:00
|
|
|
|
2020-04-20 15:38:53 -07:00
|
|
|
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
2020-07-07 10:44:06 -07:00
|
|
|
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
2020-07-04 16:54:48 -07:00
|
|
|
|
2022-04-22 17:39:48 -07:00
|
|
|
val res = radioInterfaceService.setDeviceAddress(deviceAddr)
|
2020-07-04 16:54:48 -07:00
|
|
|
if (res) {
|
2024-09-13 18:38:04 -03:00
|
|
|
haveNodeDB = false
|
2022-11-29 17:42:16 -03:00
|
|
|
} else {
|
|
|
|
|
serviceBroadcasts.broadcastConnection()
|
2020-07-04 16:54:48 -07:00
|
|
|
}
|
|
|
|
|
res
|
2020-04-19 19:23:20 -07:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2024-09-09 19:03:30 -03:00
|
|
|
override fun getUpdateStatus(): Int = -4 // ProgressNotStarted
|
2020-05-13 14:47:55 -07:00
|
|
|
|
|
|
|
|
override fun startFirmwareUpdate() = toRemoteExceptions {
|
2024-09-09 19:03:30 -03:00
|
|
|
// TODO reimplement this after we have a new firmware update mechanism
|
2020-05-13 14:47:55 -07:00
|
|
|
}
|
|
|
|
|
|
2021-03-04 09:08:29 +08:00
|
|
|
override fun getMyNodeInfo(): MyNodeInfo? = this@MeshService.myNodeInfo
|
2020-05-13 14:47:55 -07:00
|
|
|
|
2020-02-17 13:14:53 -08:00
|
|
|
override fun getMyId() = toRemoteExceptions { myNodeID }
|
|
|
|
|
|
2023-02-01 12:16:44 -03:00
|
|
|
override fun getPacketId() = toRemoteExceptions { generatePacketId() }
|
|
|
|
|
|
2023-03-27 15:30:42 -03:00
|
|
|
override fun setOwner(user: MeshUser) = toRemoteExceptions {
|
2023-10-03 18:05:40 -03:00
|
|
|
setOwner(generatePacketId(), user.toProto())
|
2023-03-27 15:30:42 -03:00
|
|
|
}
|
2020-01-25 10:00:57 -08:00
|
|
|
|
2023-10-03 18:05:40 -03:00
|
|
|
override fun setRemoteOwner(id: Int, payload: ByteArray) = toRemoteExceptions {
|
2023-04-22 12:06:25 -03:00
|
|
|
val parsed = MeshProtos.User.parseFrom(payload)
|
2023-10-03 18:05:40 -03:00
|
|
|
setOwner(id, parsed)
|
2023-04-22 12:06:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getRemoteOwner(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getOwnerRequest = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:20:19 +02:00
|
|
|
override fun send(p: DataPacket) {
|
2020-01-26 10:44:42 -08:00
|
|
|
toRemoteExceptions {
|
2023-01-12 17:25:28 -03:00
|
|
|
if (p.id == 0) p.id = generatePacketId()
|
2020-05-30 15:48:50 -07:00
|
|
|
|
2020-05-30 19:58:36 -07:00
|
|
|
info("sendData dest=${p.to}, id=${p.id} <- ${p.bytes!!.size} bytes (connectionState=$connectionState)")
|
2020-05-30 15:48:50 -07:00
|
|
|
|
2021-03-14 11:42:04 +08:00
|
|
|
if (p.dataType == 0)
|
2021-03-07 09:57:14 +08:00
|
|
|
throw Exception("Port numbers must be non-zero!") // we are now more strict
|
|
|
|
|
|
2020-12-07 20:33:29 +08:00
|
|
|
if (p.bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
|
2020-11-21 08:41:26 +08:00
|
|
|
p.status = MessageStatus.ERROR
|
|
|
|
|
throw RemoteException("Message too long")
|
2023-01-12 17:25:28 -03:00
|
|
|
} else p.status = MessageStatus.QUEUED
|
2020-11-21 08:41:26 +08:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
if (connectionState == ConnectionState.CONNECTED) try {
|
|
|
|
|
sendNow(p)
|
|
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("Error sending message, so enqueueing", ex)
|
2024-07-21 10:30:54 -03:00
|
|
|
enqueueForSending(p)
|
|
|
|
|
} else {
|
|
|
|
|
enqueueForSending(p)
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
2022-12-10 00:14:32 -03:00
|
|
|
serviceBroadcasts.broadcastMessageStatus(p)
|
|
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
// Keep a record of DataPackets, so GUIs can show proper chat history
|
2024-04-30 19:51:00 -03:00
|
|
|
rememberDataPacket(p, false)
|
2022-12-10 00:14:32 -03:00
|
|
|
|
2020-03-08 15:22:31 -07:00
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"data_send",
|
2020-05-30 17:28:00 -07:00
|
|
|
DataPair("num_bytes", p.bytes.size),
|
2020-05-30 15:48:50 -07:00
|
|
|
DataPair("type", p.dataType)
|
2020-03-08 15:22:31 -07:00
|
|
|
)
|
2020-04-22 07:59:07 -07:00
|
|
|
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"num_data_sent",
|
|
|
|
|
DataPair(1)
|
|
|
|
|
)
|
2020-01-25 10:00:57 -08:00
|
|
|
}
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2022-11-29 17:47:49 -03:00
|
|
|
override fun getConfig(): ByteArray = toRemoteExceptions {
|
|
|
|
|
this@MeshService.localConfig.toByteArray() ?: throw NoDeviceConfigException()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 12:06:25 -03:00
|
|
|
/** Send our current radio config to the device
|
|
|
|
|
*/
|
2022-10-11 16:27:36 -03:00
|
|
|
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
|
2023-10-03 18:05:40 -03:00
|
|
|
setRemoteConfig(generatePacketId(), myNodeNum, payload)
|
2023-04-22 12:06:25 -03:00
|
|
|
}
|
|
|
|
|
|
2023-10-03 18:05:40 -03:00
|
|
|
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
2023-04-22 12:06:25 -03:00
|
|
|
debug("Setting new radio config!")
|
|
|
|
|
val config = ConfigProtos.Config.parseFrom(payload)
|
2023-10-03 18:05:40 -03:00
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
|
|
|
|
|
if (num == myNodeNum) setLocalConfig(config) // Update our local copy
|
2023-04-22 12:06:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getConfigRequestValue = config
|
|
|
|
|
})
|
2021-02-27 11:44:05 +08:00
|
|
|
}
|
|
|
|
|
|
2023-04-22 12:06:25 -03:00
|
|
|
/** Send our current module config to the device
|
|
|
|
|
*/
|
2023-10-03 18:05:40 -03:00
|
|
|
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
2023-04-22 12:06:25 -03:00
|
|
|
debug("Setting new module config!")
|
|
|
|
|
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
2023-10-03 18:05:40 -03:00
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
|
|
|
|
|
if (num == myNodeNum) setLocalModuleConfig(config) // Update our local copy
|
2023-04-22 12:06:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getModuleConfigRequestValue = config
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun setRingtone(destNum: Int, ringtone: String) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
|
|
|
|
setRingtoneMessage = ringtone
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getRingtone(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getRingtoneRequest = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun setCannedMessages(destNum: Int, messages: String) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
|
|
|
|
setCannedMessageModuleMessages = messages
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getCannedMessages(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getCannedMessageModuleMessagesRequest = true
|
|
|
|
|
})
|
2022-11-22 22:01:37 -03:00
|
|
|
}
|
|
|
|
|
|
2022-10-11 16:27:36 -03:00
|
|
|
override fun setChannel(payload: ByteArray?) = toRemoteExceptions {
|
2023-10-03 18:05:40 -03:00
|
|
|
setRemoteChannel(generatePacketId(), myNodeNum, payload)
|
2023-04-29 07:14:30 -03:00
|
|
|
}
|
|
|
|
|
|
2023-10-03 18:05:40 -03:00
|
|
|
override fun setRemoteChannel(id: Int, num: Int, payload: ByteArray?) = toRemoteExceptions {
|
2023-04-29 07:14:30 -03:00
|
|
|
val channel = ChannelProtos.Channel.parseFrom(payload)
|
2023-10-03 18:05:40 -03:00
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setChannel = channel })
|
2023-04-29 07:14:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getRemoteChannel(id: Int, destNum: Int, index: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getChannelRequest = index + 1
|
|
|
|
|
})
|
2021-02-27 13:43:55 +08:00
|
|
|
}
|
|
|
|
|
|
2022-11-29 17:47:49 -03:00
|
|
|
override fun beginEditSettings() = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
beginEditSettings = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun commitEditSettings() = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
commitEditSettings = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-20 21:48:55 -03:00
|
|
|
override fun getChannelSet(): ByteArray = toRemoteExceptions {
|
|
|
|
|
this@MeshService.channelSet.toByteArray()
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-19 11:56:06 -07:00
|
|
|
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
|
2024-09-13 18:37:10 -03:00
|
|
|
val r = nodeDBbyNodeNum.values.toMutableList()
|
2020-01-24 20:46:29 -08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2020-04-04 15:29:16 -07:00
|
|
|
override fun connectionState(): String = toRemoteExceptions {
|
|
|
|
|
val r = this@MeshService.connectionState
|
|
|
|
|
info("in connectionState=$r")
|
|
|
|
|
r.toString()
|
2020-01-22 21:25:31 -08:00
|
|
|
}
|
2022-01-03 21:59:30 -03:00
|
|
|
|
2022-05-20 09:13:59 -03:00
|
|
|
override fun startProvideLocation() = toRemoteExceptions {
|
|
|
|
|
startLocationRequests()
|
2022-01-03 21:59:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun stopProvideLocation() = toRemoteExceptions {
|
|
|
|
|
stopLocationRequests()
|
|
|
|
|
}
|
2024-04-09 05:18:38 -05:00
|
|
|
override fun removeByNodenum(requestId: Int, nodeNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
removeByNodenum = nodeNum
|
|
|
|
|
|
|
|
|
|
})
|
2022-06-06 17:29:09 -03:00
|
|
|
|
2024-04-09 05:18:38 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
override fun requestPosition(destNum: Int, position: Position) = toRemoteExceptions {
|
2024-04-01 19:22:58 -03:00
|
|
|
if (destNum != myNodeNum) {
|
2022-11-15 22:00:29 -03:00
|
|
|
// request position
|
2024-04-21 08:49:09 -03:00
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildMeshPacket(
|
|
|
|
|
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
|
|
|
|
priority = MeshPacket.Priority.BACKGROUND,
|
|
|
|
|
) {
|
|
|
|
|
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
|
|
|
|
wantResponse = true
|
|
|
|
|
})
|
2023-04-22 12:06:25 -03:00
|
|
|
} else {
|
2024-06-13 07:26:56 -03:00
|
|
|
// send fixed position (local only/no remote method)
|
2024-04-01 19:22:58 -03:00
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
|
|
|
|
|
if (position != Position(0.0, 0.0, 0)) {
|
|
|
|
|
setFixedPosition = position {
|
2024-06-13 07:26:56 -03:00
|
|
|
latitudeI = Position.degI(position.latitude)
|
|
|
|
|
longitudeI = Position.degI(position.longitude)
|
|
|
|
|
altitude = position.altitude
|
2024-04-01 19:22:58 -03:00
|
|
|
}
|
2024-06-13 07:26:56 -03:00
|
|
|
.also { sendPosition(it) } // TODO remove after minDeviceVersion >= 2.3.3
|
2024-04-01 19:22:58 -03:00
|
|
|
} else {
|
|
|
|
|
removeFixedPosition = true
|
|
|
|
|
}
|
|
|
|
|
})
|
2022-11-15 22:00:29 -03:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
}
|
2022-11-06 17:46:57 -03:00
|
|
|
|
2023-04-16 06:16:41 -03:00
|
|
|
override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions {
|
2024-01-17 19:34:55 -03:00
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildMeshPacket(
|
|
|
|
|
wantAck = true,
|
|
|
|
|
id = requestId,
|
|
|
|
|
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
|
|
|
|
) {
|
2023-04-16 06:16:41 -03:00
|
|
|
portnumValue = Portnums.PortNum.TRACEROUTE_APP_VALUE
|
|
|
|
|
wantResponse = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-15 17:49:13 -03:00
|
|
|
override fun requestShutdown(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) {
|
2022-11-29 17:47:49 -03:00
|
|
|
shutdownSeconds = 5
|
|
|
|
|
})
|
2022-09-30 15:57:04 -03:00
|
|
|
}
|
|
|
|
|
|
2023-05-15 17:49:13 -03:00
|
|
|
override fun requestReboot(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) {
|
2022-11-29 17:47:49 -03:00
|
|
|
rebootSeconds = 5
|
|
|
|
|
})
|
2022-06-06 17:29:09 -03:00
|
|
|
}
|
|
|
|
|
|
2023-05-15 17:49:13 -03:00
|
|
|
override fun requestFactoryReset(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) {
|
2024-08-18 06:36:24 -03:00
|
|
|
factoryResetDevice = 1
|
2022-11-29 17:47:49 -03:00
|
|
|
})
|
2022-06-06 17:29:09 -03:00
|
|
|
}
|
2022-09-18 18:35:13 -03:00
|
|
|
|
2023-05-15 17:49:13 -03:00
|
|
|
override fun requestNodedbReset(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) {
|
2022-11-29 17:47:49 -03:00
|
|
|
nodedbReset = 1
|
|
|
|
|
})
|
2022-09-18 18:35:13 -03:00
|
|
|
}
|
2020-01-22 21:25:31 -08:00
|
|
|
}
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|