2024-11-26 08:38:12 -03:00
|
|
|
/*
|
2025-01-02 06:50:26 -03:00
|
|
|
* Copyright (c) 2025 Meshtastic LLC
|
2024-11-26 08:38:12 -03:00
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-02-10 15:31:56 -08:00
|
|
|
package com.geeksville.mesh.service
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
import android.Manifest
|
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
|
2025-05-30 13:17:09 -05:00
|
|
|
import android.content.SharedPreferences
|
2023-06-18 17:33:06 -03:00
|
|
|
import android.content.pm.ServiceInfo
|
2025-07-29 09:42:36 -05:00
|
|
|
import android.os.Build
|
2020-01-22 21:25:31 -08:00
|
|
|
import android.os.IBinder
|
2020-02-17 18:46:20 -08:00
|
|
|
import android.os.RemoteException
|
2025-08-08 16:59:54 -05:00
|
|
|
import androidx.annotation.RequiresPermission
|
2023-01-03 21:02:31 -03:00
|
|
|
import androidx.core.app.ServiceCompat
|
2025-05-30 13:17:09 -05:00
|
|
|
import androidx.core.content.edit
|
2024-06-13 07:26:56 -03:00
|
|
|
import androidx.core.location.LocationCompat
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.AdminProtos
|
|
|
|
|
import com.geeksville.mesh.AppOnlyProtos
|
|
|
|
|
import com.geeksville.mesh.BuildConfig
|
|
|
|
|
import com.geeksville.mesh.ChannelProtos
|
|
|
|
|
import com.geeksville.mesh.ConfigProtos
|
|
|
|
|
import com.geeksville.mesh.CoroutineDispatchers
|
|
|
|
|
import com.geeksville.mesh.DataPacket
|
2025-08-05 18:41:03 -05:00
|
|
|
import com.geeksville.mesh.DeviceUIProtos
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.IMeshService
|
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
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.MeshProtos
|
2020-01-24 20:35:42 -08:00
|
|
|
import com.geeksville.mesh.MeshProtos.MeshPacket
|
|
|
|
|
import com.geeksville.mesh.MeshProtos.ToRadio
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.MeshUser
|
|
|
|
|
import com.geeksville.mesh.MessageStatus
|
|
|
|
|
import com.geeksville.mesh.ModuleConfigProtos
|
|
|
|
|
import com.geeksville.mesh.MyNodeInfo
|
|
|
|
|
import com.geeksville.mesh.NodeInfo
|
|
|
|
|
import com.geeksville.mesh.PaxcountProtos
|
|
|
|
|
import com.geeksville.mesh.Portnums
|
|
|
|
|
import com.geeksville.mesh.Position
|
|
|
|
|
import com.geeksville.mesh.R
|
|
|
|
|
import com.geeksville.mesh.StoreAndForwardProtos
|
|
|
|
|
import com.geeksville.mesh.TelemetryProtos
|
2024-10-13 06:10:28 -05:00
|
|
|
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
|
|
|
|
import com.geeksville.mesh.analytics.DataPair
|
|
|
|
|
import com.geeksville.mesh.android.GeeksvilleApplication
|
|
|
|
|
import com.geeksville.mesh.android.Logging
|
2024-08-26 19:31:41 -03:00
|
|
|
import com.geeksville.mesh.android.hasLocationPermission
|
2024-10-13 06:10:28 -05:00
|
|
|
import com.geeksville.mesh.concurrent.handledLaunch
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.copy
|
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
|
2024-10-02 06:18:30 -03:00
|
|
|
import com.geeksville.mesh.database.entity.MyNodeEntity
|
2024-09-16 17:57:30 -03:00
|
|
|
import com.geeksville.mesh.database.entity.NodeEntity
|
2022-09-14 01:54:13 -03:00
|
|
|
import com.geeksville.mesh.database.entity.Packet
|
2024-12-03 05:57:35 -06:00
|
|
|
import com.geeksville.mesh.database.entity.ReactionEntity
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.fromRadio
|
2021-03-02 15:12:57 +08:00
|
|
|
import com.geeksville.mesh.model.DeviceVersion
|
2025-05-30 13:17:09 -05:00
|
|
|
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.model.Node
|
2024-10-23 19:05:43 -03:00
|
|
|
import com.geeksville.mesh.model.getTracerouteResponse
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.position
|
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.RadioInterfaceService
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.telemetry
|
|
|
|
|
import com.geeksville.mesh.user
|
|
|
|
|
import com.geeksville.mesh.util.anonymize
|
2025-06-29 12:45:09 +00:00
|
|
|
import com.geeksville.mesh.util.ignoreException
|
2025-01-02 06:38:33 -03:00
|
|
|
import com.geeksville.mesh.util.toOneLineString
|
|
|
|
|
import com.geeksville.mesh.util.toPIIString
|
|
|
|
|
import com.geeksville.mesh.util.toRemoteExceptions
|
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.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import kotlinx.coroutines.delay
|
2025-05-30 13:17:09 -05:00
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
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
|
2025-08-08 16:59:54 -05:00
|
|
|
import kotlinx.coroutines.isActive
|
2023-02-18 08:20:36 -03:00
|
|
|
import kotlinx.coroutines.withTimeoutOrNull
|
2024-11-03 06:48:32 -03:00
|
|
|
import java.util.Random
|
|
|
|
|
import java.util.UUID
|
2024-09-19 17:52:38 -03:00
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
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
|
2025-08-08 16:59:54 -05:00
|
|
|
import java.util.concurrent.atomic.AtomicLong
|
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
|
|
|
|
2024-11-21 20:30:25 -03:00
|
|
|
sealed class ServiceAction {
|
2025-01-02 06:38:33 -03:00
|
|
|
data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
|
2025-07-29 09:42:36 -05:00
|
|
|
|
2025-02-16 06:02:02 -06:00
|
|
|
data class Favorite(val node: Node) : ServiceAction()
|
2025-07-29 09:42:36 -05:00
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
data class Ignore(val node: Node) : ServiceAction()
|
2025-07-29 09:42:36 -05:00
|
|
|
|
|
|
|
|
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-05-20 13:36:11 -05:00
|
|
|
data class AddSharedContact(val contact: AdminProtos.SharedContact) : ServiceAction()
|
2024-11-21 20:30:25 -03:00
|
|
|
}
|
|
|
|
|
|
2020-01-23 08:09:50 -08:00
|
|
|
/**
|
2025-08-08 16:59:54 -05:00
|
|
|
* Handles all communication with android apps and the Meshtastic device. It maintains an internal model of the network
|
|
|
|
|
* state, manages device configurations, and processes incoming/outgoing packets.
|
2020-01-24 17:05:55 -08:00
|
|
|
*
|
2025-08-08 16:59:54 -05:00
|
|
|
* Note: This service will go away once all clients are unbound from it. Warning: Do not override toString, it causes
|
|
|
|
|
* infinite recursion on some Android versions (because contextWrapper.getResources calls toString).
|
2020-01-23 08:09:50 -08:00
|
|
|
*/
|
2025-08-08 16:59:54 -05:00
|
|
|
@Suppress("MagicNumber")
|
2022-02-08 13:50:21 -08:00
|
|
|
@AndroidEntryPoint
|
2025-07-29 09:42:36 -05:00
|
|
|
class MeshService :
|
|
|
|
|
Service(),
|
|
|
|
|
Logging {
|
|
|
|
|
@Inject lateinit var dispatchers: CoroutineDispatchers
|
2022-04-22 17:39:48 -07:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var packetRepository: Lazy<PacketRepository>
|
2022-09-14 01:54:13 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var meshLogRepository: Lazy<MeshLogRepository>
|
2020-01-22 22:16:30 -08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var radioInterfaceService: RadioInterfaceService
|
2022-04-22 17:39:48 -07:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var locationRepository: LocationRepository
|
2022-05-20 09:13:59 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var radioConfigRepository: RadioConfigRepository
|
2022-09-12 19:07:30 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var mqttRepository: MQTTRepository
|
2023-10-12 17:52:52 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
@Inject lateinit var serviceNotifications: MeshServiceNotifications
|
2025-06-09 19:45:20 +02:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
@Inject lateinit var connectionRouter: ConnectionRouter
|
|
|
|
|
|
2020-02-12 15:47:06 -08:00
|
|
|
companion object : Logging {
|
2025-08-08 16:59:54 -05:00
|
|
|
private const val MESH_PREFS_NAME = "mesh-prefs"
|
|
|
|
|
private const val DEVICE_ADDRESS_KEY = "device_address"
|
|
|
|
|
private const val ADMIN_CHANNEL_NAME = "admin"
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2024-10-13 23:02:05 -03:00
|
|
|
// Intents broadcast by MeshService
|
2021-03-24 13:48:32 +08:00
|
|
|
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
2021-01-11 17:15:19 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
/** Generates a RECEIVED action filter string for a given port number. */
|
2021-01-11 17:15:19 +08:00
|
|
|
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)
|
2025-07-29 09:42:36 -05:00
|
|
|
|
2023-01-12 17:25:28 -03:00
|
|
|
class InvalidNodeIdException(id: String) : NodeNotFoundException("Invalid NodeId $id")
|
2025-07-29 09:42:36 -05:00
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
class NodeNumNotFoundException(id: Int) : NodeNotFoundException("NodeNum not found $id")
|
2025-07-29 09:42:36 -05:00
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
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)
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
/** Initiates a device address change and starts the service. */
|
2020-06-07 20:17:47 -07:00
|
|
|
fun changeDeviceAddress(context: Context, service: IMeshService, address: String?) {
|
|
|
|
|
service.setDeviceAddress(address)
|
2025-08-08 16:59:54 -05:00
|
|
|
startService(context) // Ensure service is started/foregrounded if needed
|
2020-06-07 20:17:47 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
fun createIntent(context: Context): Intent = Intent(context, MeshService::class.java)
|
2021-03-19 22:49:51 +08:00
|
|
|
|
2025-06-03 19:58:21 -05:00
|
|
|
val minDeviceVersion = DeviceVersion(BuildConfig.MIN_FW_VERSION)
|
|
|
|
|
val absoluteMinDeviceVersion = DeviceVersion(BuildConfig.ABS_MIN_FW_VERSION)
|
2020-01-25 10:00:57 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private const val CONFIG_ONLY_NONCE = 69420
|
|
|
|
|
private const val NODE_INFO_ONLY_NONCE = 69421
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
|
|
|
|
|
2021-02-07 17:38:54 -08:00
|
|
|
private var previousSummary: String? = null
|
2024-10-13 06:10:28 -05:00
|
|
|
private var previousStats: LocalStats? = null
|
2021-02-07 17:38:54 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val clientPackages = ConcurrentHashMap<String, String>()
|
|
|
|
|
private val serviceBroadcasts by lazy {
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshServiceBroadcasts(this, clientPackages) {
|
2025-08-08 16:59:54 -05:00
|
|
|
connectionRouter.connectionState.value.also { radioConfigRepository.setConnectionState(it) }
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
}
|
2020-04-04 14:37:44 -07:00
|
|
|
private val serviceJob = Job()
|
|
|
|
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
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
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
// Battery thresholds and cooldowns
|
2025-03-08 04:52:30 -06:00
|
|
|
private val batteryPercentUnsupported = 0.0
|
2025-03-19 04:39:52 -05:00
|
|
|
private val batteryPercentLowThreshold = 20
|
|
|
|
|
private val batteryPercentLowDivisor = 5
|
|
|
|
|
private val batteryPercentCriticalThreshold = 5
|
2025-08-08 16:59:54 -05:00
|
|
|
private val batteryPercentCooldownSeconds = 1500L
|
|
|
|
|
private val batteryPercentCooldowns = ConcurrentHashMap<Int, Long>()
|
2025-03-08 04:52:30 -06:00
|
|
|
|
2021-02-08 12:32:23 +08:00
|
|
|
private fun getSenderName(packet: DataPacket?): String {
|
2025-08-08 16:59:54 -05:00
|
|
|
val nodeId = packet?.from ?: return getString(R.string.unknown_username)
|
|
|
|
|
return nodeDBbyID[nodeId]?.user?.longName ?: getString(R.string.unknown_username)
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val notificationSummary: String
|
2025-07-29 09:42:36 -05:00
|
|
|
get() =
|
2025-08-08 16:59:54 -05:00
|
|
|
when (connectionRouter.connectionState.value) {
|
2025-08-09 11:55:42 -05:00
|
|
|
ConnectionState.CONNECTED -> getString(R.string.connected_count, numOnlineNodes.toString())
|
2025-07-29 09:42:36 -05:00
|
|
|
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
|
|
|
|
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
2025-08-08 16:59:54 -05:00
|
|
|
ConnectionState.CONNECTING -> getString(R.string.connecting_to_device)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
|
2024-10-13 06:10:28 -05:00
|
|
|
private var localStatsTelemetry: TelemetryProtos.Telemetry? = null
|
2025-07-29 09:42:36 -05:00
|
|
|
private val localStats: LocalStats?
|
|
|
|
|
get() = localStatsTelemetry?.localStats
|
2024-10-13 06:10:28 -05:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
private val localStatsUpdatedAtMillis: Long?
|
|
|
|
|
get() = localStatsTelemetry?.time?.let { it * 1000L }
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
/** Starts location requests if permissions are granted and not already active. */
|
|
|
|
|
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
2022-05-20 09:13:59 -03:00
|
|
|
private fun startLocationRequests() {
|
|
|
|
|
if (locationFlow?.isActive == true) return
|
|
|
|
|
|
2024-08-26 19:31:41 -03:00
|
|
|
if (hasLocationPermission()) {
|
2025-07-29 09:42:36 -05:00
|
|
|
locationFlow =
|
|
|
|
|
locationRepository
|
|
|
|
|
.getLocations()
|
|
|
|
|
.onEach { location ->
|
2025-08-08 16:59:54 -05:00
|
|
|
val positionBuilder = position {
|
|
|
|
|
latitudeI = Position.degI(location.latitude)
|
|
|
|
|
longitudeI = Position.degI(location.longitude)
|
|
|
|
|
if (LocationCompat.hasMslAltitude(location)) {
|
|
|
|
|
altitude = LocationCompat.getMslAltitudeMeters(location).toInt()
|
|
|
|
|
}
|
|
|
|
|
altitudeHae = location.altitude.toInt()
|
|
|
|
|
time = (location.time / 1000).toInt()
|
|
|
|
|
groundSpeed = location.speed.toInt()
|
|
|
|
|
groundTrack = location.bearing.toInt()
|
|
|
|
|
locationSource = MeshProtos.Position.LocSource.LOC_EXTERNAL
|
|
|
|
|
}
|
|
|
|
|
sendPosition(positionBuilder)
|
2024-06-13 07:26:56 -03:00
|
|
|
}
|
2025-07-29 09:42:36 -05:00
|
|
|
.launchIn(serviceScope)
|
2020-02-19 10:53:36 -08:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopLocationRequests() {
|
2025-08-08 16:59:54 -05:00
|
|
|
locationFlow
|
|
|
|
|
?.takeIf { it.isActive }
|
|
|
|
|
?.let {
|
|
|
|
|
info("Stopping location requests")
|
|
|
|
|
it.cancel()
|
|
|
|
|
locationFlow = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun sendToRadio(toRadioBuilder: ToRadio.Builder) {
|
|
|
|
|
val builtProto = toRadioBuilder.build()
|
|
|
|
|
debug("Sending to radio: ${builtProto.toPIIString()}")
|
|
|
|
|
radioInterfaceService.sendToRadio(builtProto.toByteArray())
|
|
|
|
|
|
|
|
|
|
if (toRadioBuilder.hasPacket()) {
|
|
|
|
|
val packet = toRadioBuilder.packet
|
|
|
|
|
changeStatus(packet.id, MessageStatus.ENROUTE)
|
|
|
|
|
if (packet.hasDecoded()) {
|
|
|
|
|
insertMeshLog(
|
|
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "PacketSent", // Clarified type
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = packet.toString(),
|
|
|
|
|
fromNum = myNodeNum, // Correctly use myNodeNum for sent packets
|
|
|
|
|
portNum = packet.decoded.portnumValue,
|
|
|
|
|
fromRadio = fromRadio { this.packet = packet },
|
|
|
|
|
),
|
2025-07-29 09:42:36 -05:00
|
|
|
)
|
2025-08-08 16:59:54 -05:00
|
|
|
}
|
2023-01-12 17:34:17 -03:00
|
|
|
}
|
2020-01-24 20:35:42 -08:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2025-03-05 07:28:52 -06:00
|
|
|
private fun showAlertNotification(contactKey: String, dataPacket: DataPacket) {
|
|
|
|
|
serviceNotifications.showAlertNotification(
|
|
|
|
|
contactKey,
|
|
|
|
|
getSenderName(dataPacket),
|
2025-07-29 09:42:36 -05:00
|
|
|
dataPacket.alert ?: getString(R.string.critical_alert),
|
2025-03-05 07:28:52 -06:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-04 16:05:39 -06:00
|
|
|
private fun updateMessageNotification(contactKey: String, dataPacket: DataPacket) {
|
2025-07-29 09:42:36 -05:00
|
|
|
val message: String =
|
|
|
|
|
when (dataPacket.dataType) {
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> dataPacket.text ?: return
|
|
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE ->
|
|
|
|
|
getString(R.string.waypoint_received, dataPacket.waypoint?.name ?: "")
|
2023-08-25 17:19:39 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
else -> return
|
|
|
|
|
}
|
2025-05-30 13:17:09 -05:00
|
|
|
serviceNotifications.updateMessageNotification(
|
|
|
|
|
contactKey,
|
|
|
|
|
getSenderName(dataPacket),
|
2025-06-22 14:41:17 +02:00
|
|
|
message,
|
2025-07-29 09:42:36 -05:00
|
|
|
isBroadcast = dataPacket.to == DataPacket.ID_BROADCAST,
|
2025-05-30 13:17:09 -05:00
|
|
|
)
|
2023-08-25 17:19:39 -03:00
|
|
|
}
|
2020-02-28 13:53:16 -08:00
|
|
|
|
|
|
|
|
override fun onCreate() {
|
|
|
|
|
super.onCreate()
|
2025-08-08 16:59:54 -05:00
|
|
|
sharedPreferences = getSharedPreferences(MESH_PREFS_NAME, Context.MODE_PRIVATE)
|
|
|
|
|
_lastAddress.value = sharedPreferences.getString(DEVICE_ADDRESS_KEY, null) ?: NO_DEVICE_SELECTED
|
2020-02-04 13:24:04 -08:00
|
|
|
|
2020-02-28 13:53:16 -08:00
|
|
|
info("Creating mesh service")
|
2025-03-05 07:28:52 -06:00
|
|
|
serviceNotifications.initChannels()
|
2025-08-08 16:59:54 -05:00
|
|
|
connectionRouter.start()
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
serviceScope.handledLaunch { radioInterfaceService.connect() }
|
2025-08-08 16:59:54 -05:00
|
|
|
|
|
|
|
|
connectionRouter.connectionState
|
|
|
|
|
.onEach { state ->
|
|
|
|
|
when (state) {
|
|
|
|
|
ConnectionState.CONNECTED -> startConnect()
|
|
|
|
|
ConnectionState.DEVICE_SLEEP -> startDeviceSleep()
|
|
|
|
|
ConnectionState.DISCONNECTED -> startDisconnect()
|
|
|
|
|
else -> Unit
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.launchIn(serviceScope)
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
radioInterfaceService.receivedData.onEach(::onReceiveFromRadio).launchIn(serviceScope)
|
|
|
|
|
radioConfigRepository.localConfigFlow.onEach { localConfig = it }.launchIn(serviceScope)
|
|
|
|
|
radioConfigRepository.moduleConfigFlow.onEach { moduleConfig = it }.launchIn(serviceScope)
|
|
|
|
|
radioConfigRepository.channelSetFlow.onEach { channelSet = it }.launchIn(serviceScope)
|
|
|
|
|
radioConfigRepository.serviceAction.onEach(::onServiceAction).launchIn(serviceScope)
|
2022-05-06 12:17:17 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
loadSettings()
|
2020-01-24 17:05:55 -08:00
|
|
|
}
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun onBind(intent: Intent?): IBinder = binder
|
2020-06-07 17:51:51 -07:00
|
|
|
|
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
2025-08-08 16:59:54 -05:00
|
|
|
val deviceAddress = radioInterfaceService.getBondedDeviceAddress()
|
|
|
|
|
val wantForeground = deviceAddress != null && deviceAddress != NO_DEVICE_SELECTED
|
2023-01-02 21:12:57 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
info("Requesting foreground service: $wantForeground")
|
2023-01-02 21:12:57 -03:00
|
|
|
|
|
|
|
|
val notification = serviceNotifications.createServiceStateNotification(notificationSummary)
|
2025-08-08 16:59:54 -05:00
|
|
|
val foregroundServiceType =
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
if (hasLocationPermission()) {
|
|
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
2024-07-25 18:04:11 -03:00
|
|
|
} else {
|
2025-08-08 16:59:54 -05:00
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
|
|
|
|
}
|
2025-07-29 09:42:36 -05:00
|
|
|
} else {
|
2025-08-08 16:59:54 -05:00
|
|
|
0
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
ServiceCompat.startForeground(this, serviceNotifications.notifyId, notification, foregroundServiceType)
|
|
|
|
|
} catch (ex: SecurityException) {
|
|
|
|
|
val errorMessage =
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
|
|
|
"startForeground failed, likely due to missing POST_NOTIFICATIONS permission on Android 13+"
|
|
|
|
|
} else {
|
|
|
|
|
"startForeground failed"
|
|
|
|
|
}
|
|
|
|
|
errormsg(errorMessage, ex)
|
|
|
|
|
return START_NOT_STICKY // Prevent service becoming sticky in a broken state
|
2023-06-18 17:33:06 -03:00
|
|
|
}
|
2025-08-08 16:59:54 -05: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")
|
2023-01-03 21:02:31 -03:00
|
|
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
2020-01-24 17:05:55 -08:00
|
|
|
super.onDestroy()
|
2020-04-04 14:37:44 -07:00
|
|
|
serviceJob.cancel()
|
2025-08-08 16:59:54 -05:00
|
|
|
connectionRouter.stop()
|
2020-01-24 17:05:55 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
// Node Database and Model Management
|
2025-01-02 06:38:33 -03:00
|
|
|
private fun loadSettings() = serviceScope.handledLaunch {
|
2025-08-08 16:59:54 -05:00
|
|
|
resetState() // Clear previous state
|
2024-09-19 17:52:38 -03:00
|
|
|
myNodeInfo = radioConfigRepository.myNodeInfo.value
|
2025-08-08 16:59:54 -05:00
|
|
|
val nodesFromDb = radioConfigRepository.getNodeDBbyNum()
|
|
|
|
|
nodeDBbyNodeNum.putAll(nodesFromDb)
|
|
|
|
|
nodesFromDb.values.forEach { nodeEntity ->
|
|
|
|
|
if (nodeEntity.user.id.isNotEmpty()) {
|
|
|
|
|
_nodeDBbyID[nodeEntity.user.id] = nodeEntity
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-19 17:52:38 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
/**
|
|
|
|
|
* Resets all relevant service state variables to their defaults or clears collections. This is crucial when
|
|
|
|
|
* switching to a new device connection to prevent state from a previous session from affecting the new one. It
|
|
|
|
|
* ensures a clean slate for node information, configurations, pending operations, and cached data.
|
|
|
|
|
*/
|
2025-08-09 11:55:42 -05:00
|
|
|
private fun resetState() = serviceScope.handledLaunch {
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Discarding NodeDB and resetting all service state for new device connection")
|
|
|
|
|
clearDatabases()
|
|
|
|
|
// Core Node and Config data
|
2024-09-19 17:52:38 -03:00
|
|
|
myNodeInfo = null
|
2025-08-08 16:59:54 -05:00
|
|
|
rawMyNodeInfo = null
|
|
|
|
|
|
2024-09-19 17:52:38 -03:00
|
|
|
nodeDBbyNodeNum.clear()
|
2025-08-08 16:59:54 -05:00
|
|
|
_nodeDBbyID.clear()
|
|
|
|
|
|
|
|
|
|
localStatsTelemetry = null
|
|
|
|
|
sessionPasskey = ByteString.EMPTY
|
|
|
|
|
|
|
|
|
|
currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue
|
|
|
|
|
packetIdGenerator.set(Random(System.currentTimeMillis()).nextLong().absoluteValue)
|
|
|
|
|
|
|
|
|
|
offlineSentPackets.clear()
|
|
|
|
|
stopPacketQueue()
|
|
|
|
|
|
|
|
|
|
connectTimeMsec = 0L
|
|
|
|
|
|
|
|
|
|
stopLocationRequests()
|
|
|
|
|
stopMqttClientProxy()
|
|
|
|
|
|
|
|
|
|
previousSummary = null
|
|
|
|
|
previousStats = null
|
|
|
|
|
|
|
|
|
|
batteryPercentCooldowns.clear()
|
|
|
|
|
|
2025-08-09 11:55:42 -05:00
|
|
|
radioConfigRepository.clearChannelSet()
|
|
|
|
|
radioConfigRepository.clearLocalConfig()
|
|
|
|
|
radioConfigRepository.clearLocalModuleConfig()
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
info("MeshService state has been reset for a new device session.")
|
2024-09-19 17:52:38 -03:00
|
|
|
}
|
|
|
|
|
|
2024-10-02 06:18:30 -03:00
|
|
|
private var myNodeInfo: MyNodeEntity? = null
|
2025-08-08 16:59:54 -05:00
|
|
|
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null
|
|
|
|
|
private var currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue
|
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
|
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
|
|
|
|
2024-09-19 17:52:38 -03:00
|
|
|
private val nodeDBbyNodeNum = ConcurrentHashMap<Int, NodeEntity>()
|
2025-08-08 16:59:54 -05:00
|
|
|
private val _nodeDBbyID = ConcurrentHashMap<String, NodeEntity>() // Cached map for ID lookups
|
|
|
|
|
val nodeDBbyID: Map<String, NodeEntity>
|
|
|
|
|
get() = _nodeDBbyID // Expose immutable view if needed externally
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun toNodeInfo(nodeNum: Int): NodeEntity =
|
|
|
|
|
nodeDBbyNodeNum[nodeNum] ?: throw NodeNumNotFoundException(nodeNum)
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun toNodeID(nodeNum: Int): String = when (nodeNum) {
|
|
|
|
|
DataPacket.NODENUM_BROADCAST -> DataPacket.ID_BROADCAST
|
|
|
|
|
else -> nodeDBbyNodeNum[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun getOrCreateNodeInfo(nodeNum: Int, channel: Int = 0): NodeEntity = nodeDBbyNodeNum.getOrPut(nodeNum) {
|
|
|
|
|
val userId = DataPacket.nodeNumToDefaultId(nodeNum)
|
2024-09-19 17:52:38 -03:00
|
|
|
val defaultUser = user {
|
|
|
|
|
id = userId
|
2025-08-08 16:59:54 -05:00
|
|
|
longName = "Meshtastic ${userId.takeLast(4)}"
|
|
|
|
|
shortName = userId.takeLast(4)
|
2024-09-19 17:52:38 -03:00
|
|
|
hwModel = MeshProtos.HardwareModel.UNSET
|
|
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
NodeEntity(
|
|
|
|
|
num = nodeNum,
|
|
|
|
|
user = defaultUser,
|
|
|
|
|
longName = defaultUser.longName,
|
|
|
|
|
channel = channel,
|
|
|
|
|
).also { newEntity ->
|
|
|
|
|
if (newEntity.user.id.isNotEmpty()) {
|
|
|
|
|
_nodeDBbyID[newEntity.user.id] = newEntity
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-19 17:52:38 -03:00
|
|
|
}
|
2024-09-14 17:46:46 -03:00
|
|
|
|
2021-02-08 11:22:15 +08:00
|
|
|
private val hexIdRegex = """\!([0-9A-Fa-f]+)""".toRegex()
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun toNodeInfo(id: String): NodeEntity = _nodeDBbyID[id]
|
|
|
|
|
?: run {
|
|
|
|
|
val hexStr = hexIdRegex.matchEntire(id)?.groups?.get(1)?.value
|
|
|
|
|
when {
|
2025-07-29 09:42:36 -05:00
|
|
|
id == DataPacket.ID_LOCAL -> toNodeInfo(myNodeNum)
|
|
|
|
|
hexStr != null -> {
|
2025-08-08 16:59:54 -05:00
|
|
|
val nodeNum = hexStr.toLong(16).toInt()
|
|
|
|
|
nodeDBbyNodeNum[nodeNum] ?: throw IdNotFoundException(id)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
else -> throw InvalidNodeIdException(id)
|
|
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
}
|
2020-01-26 15:01:59 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun getUserName(num: Int): String =
|
|
|
|
|
radioConfigRepository.getUser(num).let { "${it.longName} (${it.shortName})" }
|
2024-04-07 16:26:47 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val numNodes: Int
|
2025-07-29 09:42:36 -05:00
|
|
|
get() = nodeDBbyNodeNum.size
|
2020-02-25 09:28:47 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val numOnlineNodes: Int
|
2025-07-29 09:42:36 -05:00
|
|
|
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
|
|
|
|
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,
|
2025-03-17 17:47:48 -05:00
|
|
|
channel: Int = 0,
|
2024-09-16 17:57:30 -03:00
|
|
|
crossinline updateFn: (NodeEntity) -> Unit,
|
2021-03-27 17:29:46 +08:00
|
|
|
) {
|
2025-03-17 17:47:48 -05:00
|
|
|
val info = getOrCreateNodeInfo(nodeNum, channel)
|
2025-08-08 16:59:54 -05:00
|
|
|
val oldUserId = info.user.id
|
|
|
|
|
|
2021-03-27 16:14:57 +08:00
|
|
|
updateFn(info)
|
2020-02-04 12:12:29 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
val newUserId = info.user.id
|
|
|
|
|
if (oldUserId.isNotEmpty() && oldUserId != newUserId) {
|
|
|
|
|
_nodeDBbyID.remove(oldUserId)
|
|
|
|
|
}
|
|
|
|
|
if (newUserId.isNotEmpty()) {
|
|
|
|
|
_nodeDBbyID[newUserId] = info
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.user.id.isNotEmpty()) {
|
2025-07-29 09:42:36 -05:00
|
|
|
serviceScope.handledLaunch { radioConfigRepository.upsert(info) }
|
2023-09-05 08:19:26 -03:00
|
|
|
}
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2024-10-13 23:02:05 -03:00
|
|
|
if (withBroadcast) {
|
2024-09-16 17:57:30 -03:00
|
|
|
serviceBroadcasts.broadcastNodeChange(info.toNodeInfo())
|
2024-10-13 23:02:05 -03:00
|
|
|
}
|
2020-01-24 22:22:30 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val myNodeNum: Int
|
|
|
|
|
get() = myNodeInfo?.myNodeNum ?: throw RadioNotConnectedException("Local node information not yet available")
|
2020-02-17 13:14:53 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val myNodeID: String
|
2025-07-29 09:42:36 -05:00
|
|
|
get() = toNodeID(myNodeNum)
|
2020-02-17 13:14:53 -08:00
|
|
|
|
2024-10-08 07:47:25 -03:00
|
|
|
private val MeshPacket.Builder.adminChannelIndex: Int
|
2025-07-29 09:42:36 -05:00
|
|
|
get() =
|
|
|
|
|
when {
|
2025-08-08 16:59:54 -05:00
|
|
|
myNodeNum == to -> 0 // Admin channel to self is 0
|
2025-07-29 09:42:36 -05:00
|
|
|
nodeDBbyNodeNum[myNodeNum]?.hasPKC == true && nodeDBbyNodeNum[to]?.hasPKC == true ->
|
|
|
|
|
DataPacket.PKC_CHANNEL_INDEX
|
|
|
|
|
|
|
|
|
|
else ->
|
2025-08-08 16:59:54 -05:00
|
|
|
channelSet.settingsList
|
|
|
|
|
.indexOfFirst { it.name.equals(ADMIN_CHANNEL_NAME, ignoreCase = true) }
|
|
|
|
|
.coerceAtLeast(0)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2022-10-10 18:09:20 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun newMeshPacketTo(nodeNum: Int): MeshPacket.Builder = MeshPacket.newBuilder().apply {
|
|
|
|
|
from = 0 // Device sets this to myNodeNum
|
|
|
|
|
to = nodeNum
|
2020-01-24 20:35:42 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun newMeshPacketTo(id: String): MeshPacket.Builder = newMeshPacketTo(toNodeNum(id))
|
2020-01-24 22:22:30 -08:00
|
|
|
|
2022-05-20 09:12:55 -03:00
|
|
|
private fun MeshPacket.Builder.buildMeshPacket(
|
2020-05-21 17:25:04 -07:00
|
|
|
wantAck: Boolean = false,
|
2025-08-08 16:59:54 -05:00
|
|
|
id: Int = generatePacketId(),
|
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,
|
2025-07-29 09:42:36 -05: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
|
2021-03-02 16:27:43 +08:00
|
|
|
this.priority = priority
|
2025-08-08 16:59:54 -05:00
|
|
|
this.decoded = MeshProtos.Data.newBuilder().apply(initFn).build()
|
2024-09-22 23:01:33 -03:00
|
|
|
if (channel == DataPacket.PKC_CHANNEL_INDEX) {
|
2024-09-30 15:27:55 -03:00
|
|
|
pkiEncrypted = true
|
2025-08-08 16:59:54 -05:00
|
|
|
nodeDBbyNodeNum[to]?.user?.publicKey?.let { this.publicKey = it }
|
2024-09-30 15:27:55 -03:00
|
|
|
} else {
|
|
|
|
|
this.channel = channel
|
2024-09-16 17:57:30 -03:00
|
|
|
}
|
2021-03-02 16:27:43 +08:00
|
|
|
return build()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 09:12:55 -03:00
|
|
|
private fun MeshPacket.Builder.buildAdminPacket(
|
2025-08-08 16:59:54 -05:00
|
|
|
id: Int = generatePacketId(),
|
2021-03-03 07:30:05 +08:00
|
|
|
wantResponse: Boolean = false,
|
2025-07-29 09:42:36 -05:00
|
|
|
initFn: AdminProtos.AdminMessage.Builder.() -> Unit,
|
|
|
|
|
): MeshPacket =
|
|
|
|
|
buildMeshPacket(id = id, wantAck = true, channel = adminChannelIndex, priority = MeshPacket.Priority.RELIABLE) {
|
|
|
|
|
this.wantResponse = wantResponse
|
2025-08-08 16:59:54 -05:00
|
|
|
this.portnumValue = Portnums.PortNum.ADMIN_APP_VALUE
|
|
|
|
|
this.payload =
|
2025-07-29 09:42:36 -05:00
|
|
|
AdminProtos.AdminMessage.newBuilder()
|
2025-08-08 16:59:54 -05:00
|
|
|
.apply {
|
|
|
|
|
initFn(this)
|
|
|
|
|
this.sessionPasskey = this@MeshService.sessionPasskey
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
|
|
|
|
.build()
|
|
|
|
|
.toByteString()
|
|
|
|
|
}
|
2021-03-02 16:27:43 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun toDataPacket(packet: MeshPacket): DataPacket? {
|
|
|
|
|
if (!packet.hasDecoded()) return null
|
2025-07-29 09:42:36 -05:00
|
|
|
val data = packet.decoded
|
2025-08-08 16:59:54 -05:00
|
|
|
return DataPacket(
|
2025-07-29 09:42:36 -05:00
|
|
|
from = toNodeID(packet.from),
|
|
|
|
|
to = toNodeID(packet.to),
|
|
|
|
|
time = packet.rxTime * 1000L,
|
|
|
|
|
id = packet.id,
|
|
|
|
|
dataType = data.portnumValue,
|
|
|
|
|
bytes = data.payload.toByteArray(),
|
|
|
|
|
hopLimit = packet.hopLimit,
|
|
|
|
|
channel = if (packet.pkiEncrypted) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
|
|
|
|
|
wantAck = packet.wantAck,
|
|
|
|
|
hopStart = packet.hopStart,
|
|
|
|
|
snr = packet.rxSnr,
|
|
|
|
|
rssi = packet.rxRssi,
|
|
|
|
|
replyId = data.replyId,
|
|
|
|
|
)
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun toMeshPacket(dataPacket: DataPacket): MeshPacket = newMeshPacketTo(dataPacket.to!!).buildMeshPacket(
|
|
|
|
|
id = dataPacket.id,
|
|
|
|
|
wantAck = dataPacket.wantAck,
|
|
|
|
|
hopLimit = dataPacket.hopLimit,
|
|
|
|
|
channel = dataPacket.channel,
|
2025-07-29 09:42:36 -05:00
|
|
|
) {
|
2025-08-08 16:59:54 -05:00
|
|
|
portnumValue = dataPacket.dataType
|
|
|
|
|
payload = ByteString.copyFrom(dataPacket.bytes)
|
|
|
|
|
dataPacket.replyId?.takeIf { it != 0 }?.let { this.replyId = it }
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val rememberableDataTypes =
|
2025-07-29 09:42:36 -05:00
|
|
|
setOf(
|
|
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
Portnums.PortNum.ALERT_APP_VALUE,
|
|
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE,
|
|
|
|
|
)
|
2023-10-08 19:24:13 -03:00
|
|
|
|
2024-12-03 05:57:35 -06:00
|
|
|
private fun rememberReaction(packet: MeshPacket) = serviceScope.handledLaunch {
|
2025-07-29 09:42:36 -05:00
|
|
|
val reaction =
|
|
|
|
|
ReactionEntity(
|
|
|
|
|
replyId = packet.decoded.replyId,
|
|
|
|
|
userId = toNodeID(packet.from),
|
|
|
|
|
emoji = packet.decoded.payload.toByteArray().decodeToString(),
|
|
|
|
|
timestamp = System.currentTimeMillis(),
|
|
|
|
|
)
|
2024-12-03 05:57:35 -06:00
|
|
|
packetRepository.get().insertReaction(reaction)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (dataPacket.dataType !in rememberableDataTypes) 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
|
|
|
|
|
val contactKey = "${dataPacket.channel}$contactId"
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
val packetToSave =
|
|
|
|
|
Packet(
|
|
|
|
|
uuid = 0L, // autoGenerated
|
|
|
|
|
myNodeNum = myNodeNum,
|
|
|
|
|
packetId = dataPacket.id,
|
|
|
|
|
port_num = dataPacket.dataType,
|
|
|
|
|
contact_key = contactKey,
|
|
|
|
|
received_time = System.currentTimeMillis(),
|
|
|
|
|
read = fromLocal,
|
|
|
|
|
data = dataPacket,
|
|
|
|
|
snr = dataPacket.snr,
|
|
|
|
|
rssi = dataPacket.rssi,
|
|
|
|
|
hopsAway = dataPacket.hopsAway,
|
|
|
|
|
replyId = dataPacket.replyId ?: 0,
|
|
|
|
|
)
|
2024-04-28 16:18:16 -03:00
|
|
|
serviceScope.handledLaunch {
|
|
|
|
|
packetRepository.get().apply {
|
|
|
|
|
insert(packetToSave)
|
|
|
|
|
val isMuted = getContactSettings(contactKey).isMuted
|
2025-03-05 07:28:52 -06:00
|
|
|
if (packetToSave.port_num == Portnums.PortNum.ALERT_APP_VALUE && !isMuted) {
|
|
|
|
|
showAlertNotification(contactKey, dataPacket)
|
|
|
|
|
} else if (updateNotification && !isMuted) {
|
|
|
|
|
updateMessageNotification(contactKey, dataPacket)
|
|
|
|
|
}
|
2024-04-28 16:18:16 -03:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
// region Received Data Handlers
|
2020-04-19 11:47:34 -07:00
|
|
|
private fun handleReceivedData(packet: MeshPacket) {
|
2025-08-08 16:59:54 -05:00
|
|
|
val currentMyNodeInfo = myNodeInfo ?: return // Early exit if no local node info
|
2020-01-25 06:16:10 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
val decodedData = packet.decoded
|
|
|
|
|
val fromNodeId = toNodeID(packet.from)
|
|
|
|
|
val appDataPacket = toDataPacket(packet) ?: return // Not a processable data packet
|
2020-02-09 05:52:17 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
val fromThisDevice = currentMyNodeInfo.myNodeNum == packet.from
|
|
|
|
|
debug("Received data from $fromNodeId, portnum=${decodedData.portnum} ${decodedData.payload.size()} bytes")
|
|
|
|
|
appDataPacket.status = MessageStatus.RECEIVED
|
2020-02-28 14:07:04 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
var shouldBroadcastToClients = !fromThisDevice
|
2021-02-27 10:18:00 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
when (decodedData.portnumValue) {
|
|
|
|
|
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> handleReceivedText(packet, appDataPacket, fromNodeId)
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.ALERT_APP_VALUE -> handleReceivedAlert(appDataPacket, fromNodeId)
|
|
|
|
|
Portnums.PortNum.WAYPOINT_APP_VALUE -> handleReceivedWaypoint(packet, appDataPacket)
|
|
|
|
|
Portnums.PortNum.POSITION_APP_VALUE -> handleReceivedPositionApp(packet, decodedData, appDataPacket)
|
2021-03-03 07:30:05 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.NODEINFO_APP_VALUE -> if (!fromThisDevice) handleReceivedNodeInfoApp(packet, decodedData)
|
2020-01-25 06:16:10 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.TELEMETRY_APP_VALUE -> handleReceivedTelemetryApp(packet, decodedData, appDataPacket)
|
2023-01-27 16:13:49 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.ROUTING_APP_VALUE -> {
|
|
|
|
|
shouldBroadcastToClients = true
|
|
|
|
|
handleReceivedRoutingApp(decodedData, fromNodeId)
|
|
|
|
|
}
|
2020-12-07 19:50:06 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.ADMIN_APP_VALUE -> {
|
|
|
|
|
handleReceivedAdmin(packet.from, AdminProtos.AdminMessage.parseFrom(decodedData.payload))
|
|
|
|
|
shouldBroadcastToClients = false
|
|
|
|
|
}
|
2021-02-27 10:18:00 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.PAXCOUNTER_APP_VALUE -> {
|
|
|
|
|
handleReceivedPaxcounter(packet.from, PaxcountProtos.Paxcount.parseFrom(decodedData.payload))
|
|
|
|
|
shouldBroadcastToClients = false
|
|
|
|
|
}
|
2022-03-28 15:50:33 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.STORE_FORWARD_APP_VALUE -> {
|
|
|
|
|
handleReceivedStoreAndForward(
|
|
|
|
|
appDataPacket,
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.parseFrom(decodedData.payload),
|
|
|
|
|
)
|
|
|
|
|
shouldBroadcastToClients = false
|
|
|
|
|
}
|
2023-10-12 22:52:54 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.RANGE_TEST_APP_VALUE -> handleReceivedRangeTest(appDataPacket)
|
|
|
|
|
Portnums.PortNum.DETECTION_SENSOR_APP_VALUE -> handleReceivedDetectionSensor(appDataPacket)
|
2023-10-12 22:52:54 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
Portnums.PortNum.TRACEROUTE_APP_VALUE ->
|
|
|
|
|
radioConfigRepository.setTracerouteResponse(packet.getTracerouteResponse(::getUserName))
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
else -> debug("No custom processing needed for ${decodedData.portnumValue}")
|
|
|
|
|
}
|
2020-03-04 11:16:43 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
if (shouldBroadcastToClients) {
|
|
|
|
|
serviceBroadcasts.broadcastReceivedData(appDataPacket)
|
|
|
|
|
}
|
|
|
|
|
trackDataReceptionAnalytics(decodedData.portnumValue, decodedData.payload.size())
|
|
|
|
|
}
|
2024-09-16 17:57:30 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedText(meshPacket: MeshPacket, dataPacket: DataPacket, fromId: String) {
|
|
|
|
|
val decodedPayload = meshPacket.decoded
|
|
|
|
|
when {
|
|
|
|
|
decodedPayload.replyId != 0 && decodedPayload.emoji == 0 -> { // Text reply
|
|
|
|
|
debug("Received REPLY from $fromId")
|
|
|
|
|
rememberDataPacket(dataPacket)
|
|
|
|
|
}
|
2024-02-18 07:37:18 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
decodedPayload.replyId != 0 && decodedPayload.emoji != 0 -> { // Emoji reaction
|
|
|
|
|
debug("Received EMOJI from $fromId")
|
|
|
|
|
rememberReaction(meshPacket)
|
|
|
|
|
}
|
2023-09-11 19:39:49 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
else -> { // Standard text message
|
|
|
|
|
debug("Received CLEAR_TEXT from $fromId")
|
|
|
|
|
rememberDataPacket(dataPacket)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-11 19:39:49 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedAlert(dataPacket: DataPacket, fromId: String) {
|
|
|
|
|
debug("Received ALERT_APP from $fromId")
|
|
|
|
|
rememberDataPacket(dataPacket)
|
|
|
|
|
}
|
2024-04-07 16:26:47 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedWaypoint(meshPacket: MeshPacket, dataPacket: DataPacket) {
|
|
|
|
|
val waypointProto = MeshProtos.Waypoint.parseFrom(meshPacket.decoded.payload)
|
|
|
|
|
// Validate locked Waypoints from the original sender
|
|
|
|
|
if (waypointProto.lockedTo != 0 && waypointProto.lockedTo != meshPacket.from) return
|
|
|
|
|
rememberDataPacket(dataPacket, waypointProto.expire > currentSecond())
|
|
|
|
|
}
|
2021-03-03 07:30:05 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedPositionApp(
|
|
|
|
|
meshPacket: MeshPacket,
|
|
|
|
|
decodedData: MeshProtos.Data,
|
|
|
|
|
dataPacket: DataPacket,
|
|
|
|
|
) {
|
|
|
|
|
val positionProto = MeshProtos.Position.parseFrom(decodedData.payload)
|
|
|
|
|
if (decodedData.wantResponse && positionProto.latitudeI == 0 && positionProto.longitudeI == 0) {
|
|
|
|
|
debug("Ignoring nop position update from position request")
|
|
|
|
|
} else {
|
|
|
|
|
handleReceivedPosition(meshPacket.from, positionProto, dataPacket.time)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-22 07:59:07 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedNodeInfoApp(meshPacket: MeshPacket, decodedData: MeshProtos.Data) {
|
|
|
|
|
val userProto =
|
|
|
|
|
MeshProtos.User.parseFrom(decodedData.payload).copy {
|
|
|
|
|
if (isLicensed) clearPublicKey()
|
|
|
|
|
if (meshPacket.viaMqtt) longName = "$longName (MQTT)"
|
|
|
|
|
}
|
|
|
|
|
handleReceivedUser(meshPacket.from, userProto, meshPacket.channel)
|
|
|
|
|
}
|
2020-05-31 11:23:25 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedTelemetryApp(
|
|
|
|
|
meshPacket: MeshPacket,
|
|
|
|
|
decodedData: MeshProtos.Data,
|
|
|
|
|
dataPacket: DataPacket,
|
|
|
|
|
) {
|
|
|
|
|
val telemetryProto =
|
|
|
|
|
TelemetryProtos.Telemetry.parseFrom(decodedData.payload).copy {
|
|
|
|
|
if (time == 0) time = (dataPacket.time / 1000L).toInt()
|
2020-05-31 11:23:25 -07:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
handleReceivedTelemetry(meshPacket.from, telemetryProto)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleReceivedRoutingApp(decodedData: MeshProtos.Data, fromId: String) {
|
|
|
|
|
val routingProto = MeshProtos.Routing.parseFrom(decodedData.payload)
|
|
|
|
|
if (routingProto.errorReason == MeshProtos.Routing.Error.DUTY_CYCLE_LIMIT) {
|
|
|
|
|
radioConfigRepository.setErrorMessage(getString(R.string.error_duty_cycle))
|
2020-04-19 11:47:34 -07:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
handleAckNak(decodedData.requestId, fromId, routingProto.errorReasonValue)
|
|
|
|
|
queueResponse.remove(decodedData.requestId)?.complete(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleReceivedRangeTest(dataPacket: DataPacket) {
|
|
|
|
|
if (!moduleConfig.rangeTest.enabled) return
|
|
|
|
|
val textDataPacket = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
|
|
|
|
rememberDataPacket(textDataPacket)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleReceivedDetectionSensor(dataPacket: DataPacket) {
|
|
|
|
|
val textDataPacket = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
|
|
|
|
rememberDataPacket(textDataPacket)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun trackDataReceptionAnalytics(portNum: Int, bytesSize: Int) {
|
|
|
|
|
GeeksvilleApplication.analytics.track("num_data_receive", DataPair(1))
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"data_receive",
|
|
|
|
|
DataPair("num_bytes", bytesSize),
|
|
|
|
|
DataPair("type", portNum),
|
|
|
|
|
)
|
2020-01-25 06:16:10 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
@Suppress("NestedBlockDepth")
|
|
|
|
|
private fun handleReceivedAdmin(fromNodeNum: Int, adminMessage: AdminProtos.AdminMessage) {
|
|
|
|
|
when (adminMessage.payloadVariantCase) {
|
2025-01-02 06:38:33 -03:00
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
|
|
|
|
if (fromNodeNum == myNodeNum) {
|
2025-08-08 16:59:54 -05:00
|
|
|
val response = adminMessage.getConfigResponse
|
2022-05-30 17:43:05 -03:00
|
|
|
debug("Admin: received config ${response.payloadVariantCase}")
|
2022-06-10 21:55:26 -03:00
|
|
|
setLocalConfig(response)
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
2025-01-02 06:38:33 -03:00
|
|
|
}
|
2021-03-02 22:12:42 +08:00
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
|
|
|
|
if (fromNodeNum == myNodeNum) {
|
2025-08-08 16:59:54 -05:00
|
|
|
myNodeInfo?.let {
|
|
|
|
|
val ch = adminMessage.getChannelResponse
|
2021-03-04 09:08:29 +08:00
|
|
|
debug("Admin: Received channel ${ch.index}")
|
2025-08-08 16:59:54 -05:00
|
|
|
if (ch.index + 1 < it.maxChannels) {
|
2022-10-16 19:19:03 -03:00
|
|
|
handleChannel(ch)
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-02 06:38:33 -03:00
|
|
|
|
|
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
|
|
|
|
|
debug("Admin: received DeviceMetadata from $fromNodeNum")
|
|
|
|
|
serviceScope.handledLaunch {
|
2025-08-08 16:59:54 -05:00
|
|
|
radioConfigRepository.insertMetadata(fromNodeNum, adminMessage.getDeviceMetadataResponse)
|
2025-01-02 06:38:33 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
AdminProtos.AdminMessage.PayloadVariantCase.PAYLOADVARIANT_NOT_SET,
|
|
|
|
|
null,
|
|
|
|
|
-> warn("Received admin message with no payload variant set.")
|
|
|
|
|
|
|
|
|
|
else -> warn("No special processing needed for admin payload ${adminMessage.payloadVariantCase}")
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
2025-01-02 06:38:33 -03:00
|
|
|
debug("Admin: Received session_passkey from $fromNodeNum")
|
2025-08-08 16:59:54 -05:00
|
|
|
sessionPasskey = adminMessage.sessionPasskey
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedUser(fromNum: Int, userProto: MeshProtos.User, channel: Int = 0) {
|
|
|
|
|
updateNodeInfo(fromNum, channel = channel) { nodeEntity ->
|
|
|
|
|
val isNewNode = (nodeEntity.isUnknownUser && userProto.hwModel != MeshProtos.HardwareModel.UNSET)
|
|
|
|
|
val keyMatch = !nodeEntity.hasPKC || nodeEntity.user.publicKey == userProto.publicKey
|
2024-10-15 04:07:22 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
nodeEntity.user =
|
2025-07-29 09:42:36 -05:00
|
|
|
if (keyMatch) {
|
2025-08-08 16:59:54 -05:00
|
|
|
userProto
|
2025-07-29 09:42:36 -05:00
|
|
|
} else {
|
2025-08-08 16:59:54 -05:00
|
|
|
userProto.copy {
|
|
|
|
|
warn("Public key mismatch from ${userProto.longName} (${userProto.shortName})")
|
2025-07-29 09:42:36 -05:00
|
|
|
publicKey = NodeEntity.ERROR_BYTE_STRING
|
|
|
|
|
}
|
2025-04-21 13:52:41 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
nodeEntity.longName = userProto.longName
|
|
|
|
|
nodeEntity.shortName = userProto.shortName
|
|
|
|
|
if (isNewNode) {
|
|
|
|
|
serviceNotifications.showNewNodeSeenNotification(nodeEntity)
|
2024-10-15 04:07:22 -05:00
|
|
|
}
|
2020-01-25 10:00:57 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-18 11:25:16 -07:00
|
|
|
private fun handleReceivedPosition(
|
|
|
|
|
fromNum: Int,
|
2025-08-08 16:59:54 -05:00
|
|
|
positionProto: MeshProtos.Position,
|
|
|
|
|
defaultTimeMillis: Long = System.currentTimeMillis(),
|
2020-08-18 11:25:16 -07:00
|
|
|
) {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (myNodeNum == fromNum && positionProto.latitudeI == 0 && positionProto.longitudeI == 0) {
|
2023-05-10 22:17:09 -03:00
|
|
|
debug("Ignoring nop position update for the local node")
|
2025-08-08 16:59:54 -05:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
updateNodeInfo(fromNum) {
|
|
|
|
|
debug("update position: ${it.longName?.toPIIString()} with ${positionProto.toPIIString()}")
|
|
|
|
|
it.setPosition(positionProto, (defaultTimeMillis / 1000L).toInt())
|
2024-06-13 07:26:56 -03:00
|
|
|
}
|
2020-02-16 14:22:24 -08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedTelemetry(fromNum: Int, telemetryProto: TelemetryProtos.Telemetry) {
|
2025-06-22 03:21:14 +02:00
|
|
|
val isRemote = (fromNum != myNodeNum)
|
2025-08-08 16:59:54 -05:00
|
|
|
if (!isRemote && telemetryProto.hasLocalStats()) {
|
|
|
|
|
localStatsTelemetry = telemetryProto
|
2024-10-13 23:02:05 -03:00
|
|
|
maybeUpdateServiceStatusNotification()
|
2024-10-13 06:10:28 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
updateNodeInfo(fromNum) { nodeEntity ->
|
2024-09-16 17:57:30 -03:00
|
|
|
when {
|
2025-08-08 16:59:54 -05:00
|
|
|
telemetryProto.hasDeviceMetrics() -> {
|
|
|
|
|
nodeEntity.deviceTelemetry = telemetryProto
|
|
|
|
|
if (fromNum == myNodeNum || (isRemote && nodeEntity.isFavorite)) {
|
|
|
|
|
val metrics = telemetryProto.deviceMetrics
|
2025-07-29 09:42:36 -05:00
|
|
|
if (
|
2025-08-08 16:59:54 -05:00
|
|
|
metrics.voltage > batteryPercentUnsupported &&
|
|
|
|
|
metrics.batteryLevel <= batteryPercentLowThreshold
|
2025-03-19 04:39:52 -05:00
|
|
|
) {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (shouldBatteryNotificationShow(fromNum, telemetryProto)) {
|
|
|
|
|
serviceNotifications.showOrUpdateLowBatteryNotification(nodeEntity, isRemote)
|
2025-03-19 04:39:52 -05:00
|
|
|
}
|
2025-03-12 05:02:14 -05:00
|
|
|
} else {
|
2025-08-08 16:59:54 -05:00
|
|
|
batteryPercentCooldowns.remove(fromNum)
|
|
|
|
|
serviceNotifications.cancelLowBatteryNotification(nodeEntity)
|
2025-03-08 04:52:30 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
telemetryProto.hasEnvironmentMetrics() -> nodeEntity.environmentTelemetry = telemetryProto
|
|
|
|
|
|
|
|
|
|
telemetryProto.hasPowerMetrics() -> nodeEntity.powerTelemetry = telemetryProto
|
2024-09-16 17:57:30 -03:00
|
|
|
}
|
2022-03-28 15:50:33 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun shouldBatteryNotificationShow(fromNum: Int, telemetry: TelemetryProtos.Telemetry): Boolean {
|
2025-03-19 04:39:52 -05:00
|
|
|
val isRemote = (fromNum != myNodeNum)
|
2025-08-08 16:59:54 -05:00
|
|
|
val batteryLevel = telemetry.deviceMetrics.batteryLevel
|
2025-03-19 04:39:52 -05:00
|
|
|
var shouldDisplay = false
|
|
|
|
|
var forceDisplay = false
|
2025-08-08 16:59:54 -05:00
|
|
|
|
2025-03-19 04:39:52 -05:00
|
|
|
when {
|
2025-08-08 16:59:54 -05:00
|
|
|
batteryLevel <= batteryPercentCriticalThreshold -> {
|
2025-03-19 04:39:52 -05:00
|
|
|
shouldDisplay = true
|
|
|
|
|
forceDisplay = true
|
|
|
|
|
}
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
batteryLevel == batteryPercentLowThreshold -> shouldDisplay = true
|
|
|
|
|
batteryLevel % batteryPercentLowDivisor == 0 && !isRemote -> shouldDisplay = true
|
|
|
|
|
isRemote -> shouldDisplay = true // For remote favorites, show if low
|
2025-03-19 04:39:52 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
|
2025-03-19 04:39:52 -05:00
|
|
|
if (shouldDisplay) {
|
2025-08-08 16:59:54 -05:00
|
|
|
val nowSeconds = System.currentTimeMillis() / 1000
|
|
|
|
|
val lastNotificationTime = batteryPercentCooldowns[fromNum] ?: 0L
|
|
|
|
|
if ((nowSeconds - lastNotificationTime) >= batteryPercentCooldownSeconds || forceDisplay) {
|
|
|
|
|
batteryPercentCooldowns[fromNum] = nowSeconds
|
2025-03-19 04:39:52 -05:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedPaxcounter(fromNum: Int, paxcountProto: PaxcountProtos.Paxcount) {
|
|
|
|
|
updateNodeInfo(fromNum) { it.paxcounter = paxcountProto }
|
2024-09-16 17:57:30 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleReceivedStoreAndForward(
|
|
|
|
|
dataPacket: DataPacket,
|
|
|
|
|
storeAndForwardProto: StoreAndForwardProtos.StoreAndForward,
|
|
|
|
|
) {
|
|
|
|
|
debug("StoreAndForward: ${storeAndForwardProto.variantCase} ${storeAndForwardProto.rr} from ${dataPacket.from}")
|
|
|
|
|
when (storeAndForwardProto.variantCase) {
|
2024-02-18 07:37:18 -03:00
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
|
2025-08-08 16:59:54 -05:00
|
|
|
val textPacket =
|
2025-07-29 09:42:36 -05:00
|
|
|
dataPacket.copy(
|
2025-08-08 16:59:54 -05:00
|
|
|
bytes = storeAndForwardProto.stats.toString().encodeToByteArray(),
|
2025-07-29 09:42:36 -05:00
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
)
|
2025-08-08 16:59:54 -05:00
|
|
|
rememberDataPacket(textPacket)
|
2024-02-18 07:37:18 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.HISTORY -> {
|
2025-07-29 09:42:36 -05:00
|
|
|
val text =
|
|
|
|
|
"""
|
2025-08-08 16:59:54 -05:00
|
|
|
Total messages: ${storeAndForwardProto.history.historyMessages}
|
|
|
|
|
History window: ${storeAndForwardProto.history.window / 60000} min
|
|
|
|
|
Last request: ${storeAndForwardProto.history.lastRequest}
|
2025-07-29 09:42:36 -05:00
|
|
|
"""
|
|
|
|
|
.trimIndent()
|
2025-08-08 16:59:54 -05:00
|
|
|
val textPacket =
|
2025-07-29 09:42:36 -05:00
|
|
|
dataPacket.copy(
|
|
|
|
|
bytes = text.encodeToByteArray(),
|
|
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
)
|
2025-08-08 16:59:54 -05:00
|
|
|
rememberDataPacket(textPacket)
|
2024-02-18 07:37:18 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.TEXT -> {
|
2025-08-08 16:59:54 -05:00
|
|
|
var actualTo = dataPacket.to
|
|
|
|
|
if (
|
|
|
|
|
storeAndForwardProto.rr ==
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST
|
|
|
|
|
) {
|
|
|
|
|
actualTo = DataPacket.ID_BROADCAST
|
2024-02-18 07:37:18 -03:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
val textPacket =
|
|
|
|
|
dataPacket.copy(
|
|
|
|
|
to = actualTo,
|
|
|
|
|
bytes = storeAndForwardProto.text.toByteArray(),
|
|
|
|
|
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
|
|
|
|
)
|
|
|
|
|
rememberDataPacket(textPacket)
|
2024-02-18 07:37:18 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.VARIANT_NOT_SET,
|
|
|
|
|
null,
|
|
|
|
|
-> Unit
|
|
|
|
|
|
|
|
|
|
StoreAndForwardProtos.StoreAndForward.VariantCase.HEARTBEAT -> {}
|
2024-02-18 07:37:18 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
private val offlineSentPackets = mutableListOf<DataPacket>()
|
|
|
|
|
|
2020-01-24 22:22:30 -08:00
|
|
|
private fun handleReceivedMeshPacket(packet: MeshPacket) {
|
2025-08-08 16:59:54 -05:00
|
|
|
val processedPacket =
|
|
|
|
|
packet
|
|
|
|
|
.toBuilder()
|
|
|
|
|
.apply {
|
|
|
|
|
if (rxTime == 0) setRxTime(currentSecond()) // Ensure rxTime is set
|
|
|
|
|
}
|
|
|
|
|
.build()
|
|
|
|
|
processReceivedMeshPacketInternal(processedPacket)
|
|
|
|
|
onNodeDBChanged()
|
2020-03-30 17:35:33 -07:00
|
|
|
}
|
|
|
|
|
|
2023-02-13 18:38:22 -03:00
|
|
|
private val queuedPackets = ConcurrentLinkedQueue<MeshPacket>()
|
2025-08-08 16:59:54 -05:00
|
|
|
private val queueResponse = ConcurrentHashMap<Int, CompletableFuture<Boolean>>()
|
2023-01-17 18:46:04 -03:00
|
|
|
private var queueJob: Job? = null
|
|
|
|
|
|
|
|
|
|
private fun sendPacket(packet: MeshPacket): CompletableFuture<Boolean> {
|
|
|
|
|
val future = CompletableFuture<Boolean>()
|
|
|
|
|
queueResponse[packet.id] = future
|
|
|
|
|
try {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (connectionRouter.connectionState.value != ConnectionState.CONNECTED) {
|
|
|
|
|
throw RadioNotConnectedException("Cannot send packet, radio not connected.")
|
|
|
|
|
}
|
|
|
|
|
sendToRadio(ToRadio.newBuilder().setPacket(packet))
|
2023-01-17 18:46:04 -03:00
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("sendToRadio error:", ex)
|
2025-08-08 16:59:54 -05:00
|
|
|
queueResponse.remove(packet.id) // Clean up if send failed immediately
|
|
|
|
|
future.completeExceptionally(ex) // Complete with exception
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
return future
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun startPacketQueue() {
|
|
|
|
|
if (queueJob?.isActive == true) return
|
2025-07-29 09:42:36 -05:00
|
|
|
queueJob =
|
|
|
|
|
serviceScope.handledLaunch {
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Packet queueJob started")
|
|
|
|
|
while (
|
|
|
|
|
connectionRouter.connectionState.value == ConnectionState.CONNECTED && queuedPackets.isNotEmpty()
|
|
|
|
|
) {
|
|
|
|
|
val packet = queuedPackets.poll() ?: break // Should not be null if loop condition met
|
2025-07-29 09:42:36 -05:00
|
|
|
try {
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Queue: Sending packet id=${packet.id.toUInt()}")
|
|
|
|
|
val success = sendPacket(packet).get(2, TimeUnit.MINUTES)
|
|
|
|
|
debug("Queue: Packet id=${packet.id.toUInt()} sent, success=$success")
|
2025-07-29 09:42:36 -05:00
|
|
|
} catch (e: TimeoutException) {
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Queue: Packet id=${packet.id.toUInt()} timed out: ${e.message}")
|
|
|
|
|
queueResponse.remove(packet.id)?.complete(false)
|
2025-07-29 09:42:36 -05:00
|
|
|
} catch (e: Exception) {
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Queue: Packet id=${packet.id.toUInt()} failed: ${e.message}")
|
|
|
|
|
queueResponse.remove(packet.id)?.complete(false)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
debug("Packet queueJob finished or radio disconnected")
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopPacketQueue() {
|
2025-08-08 16:59:54 -05:00
|
|
|
queueJob
|
|
|
|
|
?.takeIf { it.isActive }
|
|
|
|
|
?.let {
|
|
|
|
|
info("Stopping packet queueJob")
|
|
|
|
|
it.cancel()
|
|
|
|
|
queueJob = null
|
|
|
|
|
queuedPackets.clear()
|
|
|
|
|
queueResponse.values.forEach { future -> if (!future.isDone) future.complete(false) }
|
|
|
|
|
queueResponse.clear()
|
|
|
|
|
}
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun sendNow(dataPacket: DataPacket) {
|
|
|
|
|
val meshPacket = toMeshPacket(dataPacket)
|
|
|
|
|
dataPacket.time = System.currentTimeMillis() // Update time to actual send time
|
|
|
|
|
sendToRadio(meshPacket)
|
2020-06-11 14:03:10 -07:00
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
private fun processQueuedPackets() {
|
2025-08-08 16:59:54 -05:00
|
|
|
val packetsToSend = ArrayList(offlineSentPackets) // Avoid ConcurrentModificationException
|
|
|
|
|
offlineSentPackets.clear()
|
|
|
|
|
|
|
|
|
|
packetsToSend.forEach { p ->
|
2023-02-18 08:20:36 -03:00
|
|
|
try {
|
|
|
|
|
sendNow(p)
|
|
|
|
|
} catch (ex: Exception) {
|
2025-08-08 16:59:54 -05:00
|
|
|
errormsg("Error sending queued message, re-queuing:", ex)
|
|
|
|
|
offlineSentPackets.add(p) // Re-queue if sending failed
|
2023-01-12 17:47:59 -03:00
|
|
|
}
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|
2021-12-25 19:30:45 -03:00
|
|
|
}
|
2020-03-30 17:35:33 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000L) {
|
2023-02-18 08:20:36 -03:00
|
|
|
var dataPacket: DataPacket? = null
|
2025-08-08 16:59:54 -05:00
|
|
|
while (dataPacket == null && isActive) { // check coroutine isActive
|
2024-09-24 19:39:20 -03:00
|
|
|
dataPacket = packetRepository.get().getPacketById(packetId)?.data
|
2025-08-08 16:59:54 -05:00
|
|
|
if (dataPacket == null) delay(100L)
|
2023-02-18 08:20:36 -03:00
|
|
|
}
|
|
|
|
|
dataPacket
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun changeStatus(packetId: Int, status: MessageStatus) = serviceScope.handledLaunch {
|
|
|
|
|
if (packetId == 0) return@handledLaunch // Ignore packets with no ID
|
|
|
|
|
|
|
|
|
|
getDataPacketById(packetId)?.let { p ->
|
|
|
|
|
if (p.status == status) return@handledLaunch
|
|
|
|
|
packetRepository.get().updateMessageStatus(p, status)
|
|
|
|
|
serviceBroadcasts.broadcastMessageStatus(packetId, status)
|
2023-02-18 08:20:36 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-24 19:39:20 -03:00
|
|
|
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int) {
|
2023-01-02 22:23:23 -03:00
|
|
|
serviceScope.handledLaunch {
|
2024-09-24 19:39:20 -03:00
|
|
|
val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE
|
2025-08-08 16:59:54 -05:00
|
|
|
val packetEntity = packetRepository.get().getPacketById(requestId)
|
|
|
|
|
|
|
|
|
|
packetEntity?.data?.let { dataPacket ->
|
|
|
|
|
// Distinguish real ACKs coming from the intended receiver
|
|
|
|
|
val newStatus =
|
|
|
|
|
when {
|
|
|
|
|
isAck && fromId == dataPacket.to -> MessageStatus.RECEIVED
|
|
|
|
|
isAck -> MessageStatus.DELIVERED
|
|
|
|
|
else -> MessageStatus.ERROR
|
|
|
|
|
}
|
|
|
|
|
if (dataPacket.status != MessageStatus.RECEIVED) { // Don't override final RECEIVED
|
|
|
|
|
dataPacket.status = newStatus
|
|
|
|
|
packetRepository.get().update(packetEntity.copy(routingError = routingError, data = dataPacket))
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
serviceBroadcasts.broadcastMessageStatus(requestId, newStatus)
|
2024-09-24 19:39:20 -03:00
|
|
|
}
|
2020-05-30 19:58:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun processReceivedMeshPacketInternal(packet: MeshPacket) {
|
|
|
|
|
if (!packet.hasDecoded()) return
|
|
|
|
|
|
|
|
|
|
insertMeshLog(
|
|
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "PacketReceived", // Clarified type
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = packet.toString(),
|
|
|
|
|
fromNum = packet.from,
|
|
|
|
|
portNum = packet.decoded.portnumValue,
|
|
|
|
|
fromRadio = fromRadio { this.packet = packet },
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
serviceScope.handledLaunch { radioConfigRepository.emitMeshPacket(packet) }
|
|
|
|
|
|
|
|
|
|
val isOtherNode = myNodeNum != packet.from
|
|
|
|
|
// Update our own node's lastHeard as we are clearly active to receive this
|
|
|
|
|
updateNodeInfo(myNodeNum, withBroadcast = isOtherNode) { it.lastHeard = currentSecond() }
|
|
|
|
|
|
|
|
|
|
updateNodeInfo(packet.from, withBroadcast = false, channel = packet.channel) {
|
|
|
|
|
it.lastHeard = packet.rxTime
|
|
|
|
|
it.snr = packet.rxSnr
|
|
|
|
|
it.rssi = packet.rxRssi
|
|
|
|
|
it.hopsAway =
|
|
|
|
|
if (packet.hopStart == 0 || packet.hopLimit > packet.hopStart) {
|
|
|
|
|
-1 // Unknown or direct
|
|
|
|
|
} else {
|
|
|
|
|
packet.hopStart - packet.hopLimit
|
|
|
|
|
}
|
2021-02-27 10:18:00 +08:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
handleReceivedData(packet)
|
2020-01-24 22:22:30 -08:00
|
|
|
}
|
2020-01-24 20:35:42 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun insertMeshLog(meshLog: MeshLog) {
|
|
|
|
|
serviceScope.handledLaunch { meshLogRepository.get().insert(meshLog) }
|
2020-09-23 22:47:45 -04:00
|
|
|
}
|
|
|
|
|
|
2022-06-20 22:46:45 -03:00
|
|
|
private fun setLocalConfig(config: ConfigProtos.Config) {
|
2025-07-29 09:42:36 -05:00
|
|
|
serviceScope.handledLaunch { 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) {
|
2025-07-29 09:42:36 -05:00
|
|
|
serviceScope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) }
|
2022-11-22 22:01:37 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun updateChannelSettings(channel: ChannelProtos.Channel) =
|
|
|
|
|
serviceScope.handledLaunch { radioConfigRepository.updateChannelSettings(channel) }
|
2022-09-12 19:07:30 -03:00
|
|
|
|
2020-02-19 11:35:16 -08:00
|
|
|
private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt()
|
|
|
|
|
|
2020-02-19 10:53:36 -08:00
|
|
|
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
|
|
|
private fun reportConnection() {
|
|
|
|
|
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"mesh_connect",
|
|
|
|
|
DataPair("num_nodes", numNodes),
|
|
|
|
|
DataPair("num_online", numOnlineNodes),
|
2025-07-29 09:42:36 -05:00
|
|
|
radioModel,
|
2020-04-22 18:34:22 -07:00
|
|
|
)
|
2025-07-29 09:42:36 -05:00
|
|
|
GeeksvilleApplication.analytics.setUserInfo(DataPair("num_nodes", numNodes), radioModel)
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-21 14:46:52 -07:00
|
|
|
private var connectTimeMsec = 0L
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun startConnect() {
|
|
|
|
|
try {
|
|
|
|
|
connectTimeMsec = System.currentTimeMillis()
|
|
|
|
|
sendConfigOnlyRequest()
|
|
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
when (ex) {
|
|
|
|
|
is InvalidProtocolBufferException,
|
|
|
|
|
is RadioNotConnectedException,
|
|
|
|
|
is RemoteException,
|
|
|
|
|
-> {
|
|
|
|
|
errormsg("Failed to start connection sequence: ${ex.message}", ex)
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
2020-05-31 11:23:25 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
else -> throw ex
|
2020-02-04 12:12:29 -08:00
|
|
|
}
|
2020-04-04 15:29:16 -07:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
}
|
2020-02-25 09:28:47 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun startDeviceSleep() {
|
|
|
|
|
stopPacketQueue()
|
|
|
|
|
stopLocationRequests()
|
|
|
|
|
stopMqttClientProxy()
|
2020-04-04 15:29:16 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
if (connectTimeMsec != 0L) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
GeeksvilleApplication.analytics.track("connected_seconds", DataPair((now - connectTimeMsec) / 1000.0))
|
|
|
|
|
connectTimeMsec = 0L
|
2020-02-04 12:12:29 -08:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
serviceBroadcasts.broadcastConnection()
|
|
|
|
|
}
|
2020-02-28 13:53:16 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun startDisconnect() {
|
|
|
|
|
stopPacketQueue()
|
|
|
|
|
stopLocationRequests()
|
|
|
|
|
stopMqttClientProxy()
|
|
|
|
|
|
|
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"mesh_disconnect",
|
|
|
|
|
DataPair("num_nodes", numNodes),
|
|
|
|
|
DataPair("num_online", numOnlineNodes),
|
|
|
|
|
)
|
|
|
|
|
GeeksvilleApplication.analytics.track("num_nodes", DataPair(numNodes))
|
|
|
|
|
serviceBroadcasts.broadcastConnection()
|
2021-02-07 17:38:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun maybeUpdateServiceStatusNotification() {
|
|
|
|
|
val currentSummary = notificationSummary
|
2024-10-13 06:10:28 -05:00
|
|
|
val currentStats = localStats
|
|
|
|
|
val currentStatsUpdatedAtMillis = localStatsUpdatedAtMillis
|
2025-08-08 16:59:54 -05:00
|
|
|
|
|
|
|
|
val summaryChanged = currentSummary.isNotBlank() && previousSummary != currentSummary
|
|
|
|
|
val statsChanged = currentStats != null && previousStats != currentStats
|
|
|
|
|
|
|
|
|
|
if (summaryChanged || statsChanged) {
|
2021-02-07 17:38:54 -08:00
|
|
|
previousSummary = currentSummary
|
2024-10-13 06:10:28 -05:00
|
|
|
previousStats = currentStats
|
|
|
|
|
serviceNotifications.updateServiceStateNotification(
|
|
|
|
|
summaryString = currentSummary,
|
|
|
|
|
localStats = currentStats,
|
2025-07-29 09:42:36 -05:00
|
|
|
currentStatsUpdatedAtMillis = currentStatsUpdatedAtMillis,
|
2024-10-13 06:10:28 -05:00
|
|
|
)
|
2021-02-07 17:38:54 -08:00
|
|
|
}
|
2020-05-31 11:23:25 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
@SuppressLint("CheckResult")
|
2025-08-01 16:10:57 -05:00
|
|
|
@Suppress("CyclomaticComplexMethod")
|
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)
|
2025-08-08 16:59:54 -05:00
|
|
|
when (proto.payloadVariantCase) {
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.PACKET -> handleReceivedMeshPacket(proto.packet)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.CONFIG_COMPLETE_ID ->
|
|
|
|
|
handleConfigComplete(proto.configCompleteId)
|
|
|
|
|
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.MY_INFO -> handleMyInfo(proto.myInfo)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.NODE_INFO -> handleNodeInfo(proto.nodeInfo)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.CHANNEL -> handleChannel(proto.channel)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.CONFIG -> handleDeviceConfig(proto.config)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.MODULECONFIG -> handleModuleConfig(proto.moduleConfig)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.QUEUESTATUS -> handleQueueStatus(proto.queueStatus)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.METADATA -> handleMetadata(proto.metadata)
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.MQTTCLIENTPROXYMESSAGE ->
|
2025-07-29 09:42:36 -05:00
|
|
|
handleMqttProxyMessage(proto.mqttClientProxyMessage)
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.DEVICEUICONFIG -> handleDeviceUiConfig(proto.deviceuiConfig)
|
2025-05-30 13:17:09 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.FILEINFO -> handleFileInfo(proto.fileInfo)
|
2025-08-05 18:41:03 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.CLIENTNOTIFICATION ->
|
|
|
|
|
handleClientNotification(proto.clientNotification)
|
2025-08-05 18:41:03 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.LOG_RECORD -> {}
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.REBOOTED -> {}
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.XMODEMPACKET -> {}
|
|
|
|
|
MeshProtos.FromRadio.PayloadVariantCase.PAYLOADVARIANT_NOT_SET,
|
|
|
|
|
null,
|
|
|
|
|
-> 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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 22:46:45 -03:00
|
|
|
private fun handleDeviceConfig(config: ConfigProtos.Config) {
|
|
|
|
|
debug("Received config ${config.toOneLineString()}")
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "Config ${config.payloadVariantCase}",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = config.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.config = config },
|
2025-08-08 16:59:54 -05:00
|
|
|
),
|
|
|
|
|
)
|
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()}")
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "ModuleConfig ${config.payloadVariantCase}",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = config.toString(),
|
2025-08-08 16:59:54 -05:00
|
|
|
fromRadio = fromRadio { this.moduleConfig = config },
|
|
|
|
|
),
|
|
|
|
|
)
|
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()}")
|
2025-07-29 09:42:36 -05:00
|
|
|
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, meshPacketId) }
|
2025-08-08 16:59:54 -05:00
|
|
|
if (success && isFull) return // Queue is full, wait for next update
|
|
|
|
|
|
|
|
|
|
val future =
|
|
|
|
|
if (requestId != 0) {
|
|
|
|
|
queueResponse.remove(requestId)
|
|
|
|
|
} else {
|
|
|
|
|
// This is a bit of a guess, but for now we assume it's for the last request that isn't done.
|
|
|
|
|
// A more robust solution would involve matching something other than packetId.
|
|
|
|
|
queueResponse.entries.lastOrNull { !it.value.isDone }?.also { queueResponse.remove(it.key) }?.value
|
|
|
|
|
}
|
|
|
|
|
future?.complete(success)
|
2023-01-17 18:46:04 -03:00
|
|
|
}
|
|
|
|
|
|
2022-10-16 19:19:03 -03:00
|
|
|
private fun handleChannel(ch: ChannelProtos.Channel) {
|
|
|
|
|
debug("Received channel ${ch.index}")
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "Channel",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = ch.toString(),
|
|
|
|
|
fromRadio = fromRadio { channel = ch },
|
2025-08-08 16:59:54 -05:00
|
|
|
),
|
|
|
|
|
)
|
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-19 17:52:38 -03:00
|
|
|
private fun installNodeInfo(info: MeshProtos.NodeInfo) {
|
|
|
|
|
updateNodeInfo(info.num) {
|
|
|
|
|
if (info.hasUser()) {
|
2025-07-29 09:42:36 -05:00
|
|
|
it.user =
|
|
|
|
|
info.user.copy {
|
|
|
|
|
if (isLicensed) clearPublicKey()
|
|
|
|
|
if (info.viaMqtt) longName = "$longName (MQTT)"
|
|
|
|
|
}
|
2024-11-15 06:46:37 -03:00
|
|
|
it.longName = it.user.longName
|
|
|
|
|
it.shortName = it.user.shortName
|
2024-09-19 17:52:38 -03:00
|
|
|
}
|
|
|
|
|
if (info.hasPosition()) {
|
|
|
|
|
it.position = info.position
|
|
|
|
|
it.latitude = Position.degD(info.position.latitudeI)
|
|
|
|
|
it.longitude = Position.degD(info.position.longitudeI)
|
|
|
|
|
}
|
|
|
|
|
it.lastHeard = info.lastHeard
|
|
|
|
|
if (info.hasDeviceMetrics()) {
|
|
|
|
|
it.deviceTelemetry = telemetry { deviceMetrics = info.deviceMetrics }
|
|
|
|
|
}
|
|
|
|
|
it.channel = info.channel
|
|
|
|
|
it.viaMqtt = info.viaMqtt
|
2025-08-08 16:59:54 -05:00
|
|
|
it.hopsAway = if (info.hasHopsAway()) info.hopsAway else -1
|
2024-09-19 17:52:38 -03:00
|
|
|
it.isFavorite = info.isFavorite
|
2024-12-07 08:09:41 -03:00
|
|
|
it.isIgnored = info.isIgnored
|
2024-09-19 17:52:38 -03:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-22 18:34:22 -07:00
|
|
|
|
|
|
|
|
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
|
2025-07-29 09:42:36 -05:00
|
|
|
debug(
|
2025-08-08 16:59:54 -05:00
|
|
|
"Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, " +
|
|
|
|
|
"hasPosition=${info.hasPosition()}, hasDeviceMetrics=${info.hasDeviceMetrics()}",
|
2020-10-01 16:59:34 -04:00
|
|
|
)
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "NodeInfo",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = info.toString(),
|
|
|
|
|
fromRadio = fromRadio { nodeInfo = info },
|
2025-08-08 16:59:54 -05:00
|
|
|
),
|
|
|
|
|
)
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
installNodeInfo(info)
|
|
|
|
|
onNodeDBChanged()
|
|
|
|
|
radioConfigRepository.setStatusMessage("Nodes ($numNodes)")
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata) {
|
2021-03-14 11:42:04 +08:00
|
|
|
val myInfo = rawMyNodeInfo
|
|
|
|
|
if (myInfo != null) {
|
2025-07-29 09:42:36 -05:00
|
|
|
val mi =
|
|
|
|
|
with(myInfo) {
|
|
|
|
|
MyNodeEntity(
|
|
|
|
|
myNodeNum = myNodeNum,
|
|
|
|
|
model =
|
|
|
|
|
when (val hwModel = metadata.hwModel) {
|
|
|
|
|
null,
|
|
|
|
|
MeshProtos.HardwareModel.UNSET,
|
|
|
|
|
-> null
|
|
|
|
|
|
|
|
|
|
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
|
|
|
|
},
|
|
|
|
|
firmwareVersion = metadata.firmwareVersion,
|
|
|
|
|
couldUpdate = false,
|
|
|
|
|
shouldUpdate = false, // TODO add check after re-implementing firmware updates
|
|
|
|
|
currentPacketId = currentPacketId and 0xffffffffL,
|
|
|
|
|
messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code
|
|
|
|
|
minAppVersion = minAppVersion,
|
|
|
|
|
maxChannels = 8,
|
|
|
|
|
hasWifi = metadata.hasWifi,
|
|
|
|
|
deviceId = deviceId.toStringUtf8(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
serviceScope.handledLaunch {
|
|
|
|
|
radioConfigRepository.installMyNodeInfo(mi)
|
|
|
|
|
radioConfigRepository.insertMetadata(mi.myNodeNum, metadata)
|
|
|
|
|
}
|
|
|
|
|
myNodeInfo = mi
|
|
|
|
|
onConnected()
|
2021-03-14 11:42:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun sendAnalytics() {
|
2025-08-08 16:59:54 -05:00
|
|
|
myNodeInfo?.let {
|
2021-03-14 11:42:04 +08:00
|
|
|
GeeksvilleApplication.analytics.setUserInfo(
|
2025-08-08 16:59:54 -05:00
|
|
|
DataPair("firmware", it.firmwareVersion),
|
|
|
|
|
DataPair("hw_model", it.model),
|
2021-03-14 11:42:04 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-22 18:34:22 -07:00
|
|
|
private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "MyNodeInfo",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = myInfo.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.myInfo = myInfo },
|
2025-08-08 16:59:54 -05:00
|
|
|
),
|
|
|
|
|
)
|
2021-03-14 11:42:04 +08:00
|
|
|
rawMyNodeInfo = myInfo
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleDeviceUiConfig(deviceuiConfig: DeviceUIProtos.DeviceUIConfig) {
|
2025-08-05 18:41:03 -05:00
|
|
|
debug("Received DeviceUIConfig ${deviceuiConfig.toOneLineString()}")
|
|
|
|
|
val packetToSave =
|
|
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "DeviceUIConfig",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = deviceuiConfig.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.deviceuiConfig = deviceuiConfig },
|
|
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleFileInfo(fileInfo: MeshProtos.FileInfo) {
|
|
|
|
|
debug("Received FileInfo ${fileInfo.toOneLineString()}")
|
|
|
|
|
val packetToSave =
|
|
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "FileInfo",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = fileInfo.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.fileInfo = fileInfo },
|
|
|
|
|
)
|
|
|
|
|
insertMeshLog(packetToSave)
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-05 07:04:37 -03:00
|
|
|
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
|
2023-08-25 17:00:56 -03:00
|
|
|
debug("Received deviceMetadata ${metadata.toOneLineString()}")
|
2025-08-08 16:59:54 -05:00
|
|
|
insertMeshLog(
|
2025-07-29 09:42:36 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "DeviceMetadata",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
|
|
|
|
raw_message = metadata.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.metadata = metadata },
|
2025-08-08 16:59:54 -05:00
|
|
|
),
|
|
|
|
|
)
|
2025-01-02 06:38:33 -03:00
|
|
|
regenMyNodeInfo(metadata)
|
2023-08-05 07:04:37 -03:00
|
|
|
}
|
|
|
|
|
|
2023-10-12 17:52:52 -03:00
|
|
|
private fun handleMqttProxyMessage(message: MeshProtos.MqttClientProxyMessage) {
|
|
|
|
|
with(message) {
|
|
|
|
|
when (payloadVariantCase) {
|
2025-08-08 16:59:54 -05:00
|
|
|
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
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.DATA ->
|
2023-10-12 17:52:52 -03:00
|
|
|
mqttRepository.publish(topic, data.toByteArray(), retained)
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
else -> Unit
|
2023-10-12 17:52:52 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-25 10:55:55 -03:00
|
|
|
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
|
|
|
|
|
debug("Received clientNotification ${notification.toOneLineString()}")
|
2025-06-13 01:00:28 +00:00
|
|
|
radioConfigRepository.setClientNotification(notification)
|
|
|
|
|
serviceNotifications.showClientNotification(notification)
|
2024-09-28 21:43:18 -06:00
|
|
|
queueResponse.remove(notification.replyId)?.complete(false)
|
2024-08-25 10:55:55 -03:00
|
|
|
}
|
|
|
|
|
|
2023-10-12 17:52:52 -03:00
|
|
|
private fun startMqttClientProxy() {
|
|
|
|
|
if (mqttMessageFlow?.isActive == true) return
|
|
|
|
|
if (moduleConfig.mqtt.enabled && moduleConfig.mqtt.proxyToClientEnabled) {
|
2025-07-29 09:42:36 -05:00
|
|
|
mqttMessageFlow =
|
|
|
|
|
mqttRepository.proxyMessageFlow
|
2025-08-08 16:59:54 -05:00
|
|
|
.onEach { message -> sendToRadio(ToRadio.newBuilder().setMqttClientProxyMessage(message)) }
|
2025-07-29 09:42:36 -05:00
|
|
|
.catch { throwable -> radioConfigRepository.setErrorMessage("MqttClientProxy failed: $throwable") }
|
|
|
|
|
.launchIn(serviceScope)
|
2023-10-12 17:52:52 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopMqttClientProxy() {
|
2025-08-08 16:59:54 -05:00
|
|
|
mqttMessageFlow
|
|
|
|
|
?.takeIf { it.isActive }
|
|
|
|
|
?.let {
|
|
|
|
|
info("Stopping MqttClientProxy")
|
|
|
|
|
it.cancel()
|
|
|
|
|
mqttMessageFlow = null
|
|
|
|
|
}
|
2023-10-12 17:52:52 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun onConnected() {
|
|
|
|
|
// Start sending queued packets and other tasks
|
|
|
|
|
processQueuedPackets()
|
2023-10-12 17:52:52 -03:00
|
|
|
startMqttClientProxy()
|
2025-08-08 16:59:54 -05:00
|
|
|
onNodeDBChanged()
|
2021-03-02 22:12:42 +08:00
|
|
|
serviceBroadcasts.broadcastConnection()
|
2025-08-01 16:10:57 -05:00
|
|
|
sendAnalytics()
|
2021-03-02 22:12:42 +08:00
|
|
|
reportConnection()
|
2025-08-08 16:59:54 -05:00
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() })
|
2021-03-02 22:12:42 +08:00
|
|
|
}
|
|
|
|
|
|
2024-09-19 17:52:38 -03:00
|
|
|
private fun handleConfigComplete(configCompleteId: Int) {
|
2025-08-08 16:59:54 -05:00
|
|
|
when (configCompleteId) {
|
|
|
|
|
CONFIG_ONLY_NONCE -> handleConfigOnlyNonceResponse()
|
|
|
|
|
NODE_INFO_ONLY_NONCE -> handleNodeInfoNonceResponse()
|
|
|
|
|
else -> warn("Received unexpected config complete id $configCompleteId")
|
2025-08-01 16:10:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-18 19:46:39 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleConfigOnlyNonceResponse() {
|
|
|
|
|
debug("Received config only complete for nonce $CONFIG_ONLY_NONCE")
|
|
|
|
|
insertMeshLog(
|
2025-08-01 16:10:57 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "ConfigOnlyComplete",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
2025-08-08 16:59:54 -05:00
|
|
|
raw_message = CONFIG_ONLY_NONCE.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.configCompleteId = CONFIG_ONLY_NONCE },
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
// we have recieved the response to our ConfigOnly request
|
|
|
|
|
// send a heartbeat, then request NodeInfoOnly to get the nodeDb from the radio
|
|
|
|
|
serviceScope.handledLaunch { radioInterfaceService.keepAlive() }
|
|
|
|
|
sendNodeInfoOnlyRequest()
|
2025-08-01 16:10:57 -05:00
|
|
|
}
|
2024-09-19 17:52:38 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun handleNodeInfoNonceResponse() {
|
|
|
|
|
debug("Received node info complete for nonce $NODE_INFO_ONLY_NONCE")
|
|
|
|
|
insertMeshLog(
|
2025-08-01 16:10:57 -05:00
|
|
|
MeshLog(
|
|
|
|
|
uuid = UUID.randomUUID().toString(),
|
|
|
|
|
message_type = "NodeInfoComplete",
|
|
|
|
|
received_date = System.currentTimeMillis(),
|
2025-08-08 16:59:54 -05:00
|
|
|
raw_message = NODE_INFO_ONLY_NONCE.toString(),
|
|
|
|
|
fromRadio = fromRadio { this.configCompleteId = NODE_INFO_ONLY_NONCE },
|
|
|
|
|
),
|
|
|
|
|
)
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun sendConfigOnlyRequest() {
|
|
|
|
|
resetState()
|
|
|
|
|
debug("Starting config only with nonce=$CONFIG_ONLY_NONCE")
|
|
|
|
|
sendToRadio(ToRadio.newBuilder().setWantConfigId(CONFIG_ONLY_NONCE))
|
2025-08-01 16:10:57 -05:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun sendNodeInfoOnlyRequest() {
|
|
|
|
|
debug("Starting node info with nonce=$NODE_INFO_ONLY_NONCE")
|
|
|
|
|
sendToRadio(ToRadio.newBuilder().setWantConfigId(NODE_INFO_ONLY_NONCE))
|
2020-04-22 18:34:22 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
private fun sendPosition(position: MeshProtos.Position, destNum: Int? = null, wantResponse: Boolean = false) {
|
2021-03-27 15:50:16 +08:00
|
|
|
try {
|
2025-08-08 16:59:54 -05:00
|
|
|
myNodeInfo?.let { mi ->
|
|
|
|
|
val targetNodeNum = destNum ?: mi.myNodeNum
|
|
|
|
|
debug("Sending our position/time to=$targetNodeNum ${Position(position)}")
|
2020-02-16 14:22:24 -08:00
|
|
|
|
2024-09-28 08:09:42 -03:00
|
|
|
if (!localConfig.position.fixedPosition) {
|
|
|
|
|
handleReceivedPosition(mi.myNodeNum, position)
|
|
|
|
|
}
|
2021-03-02 16:27:43 +08:00
|
|
|
|
2025-05-30 13:17:09 -05:00
|
|
|
sendToRadio(
|
2025-08-08 16:59:54 -05:00
|
|
|
newMeshPacketTo(targetNodeNum).buildMeshPacket(
|
|
|
|
|
channel = if (destNum == null) 0 else (nodeDBbyNodeNum[destNum]?.channel ?: 0),
|
2025-05-30 13:17:09 -05:00
|
|
|
priority = MeshPacket.Priority.BACKGROUND,
|
|
|
|
|
) {
|
|
|
|
|
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
|
|
|
|
payload = position.toByteString()
|
|
|
|
|
this.wantResponse = wantResponse
|
2025-07-29 09:42:36 -05:00
|
|
|
},
|
|
|
|
|
)
|
2021-03-28 10:33:59 +08:00
|
|
|
}
|
2021-03-20 11:23:31 +08:00
|
|
|
} catch (ex: BLEException) {
|
2025-08-08 16:59:54 -05:00
|
|
|
warn("Ignoring disconnected radio during gps location update: ${ex.message}")
|
2021-01-05 14:01:45 +08:00
|
|
|
}
|
2020-10-01 22:20:19 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private fun setOwner(packetId: Int, user: MeshProtos.User) {
|
|
|
|
|
val dest = nodeDBbyID[user.id] ?: throw Exception("Can't set user without a NodeInfo")
|
|
|
|
|
if (user == dest.user) {
|
2023-10-03 18:05:40 -03:00
|
|
|
debug("Ignoring nop owner change")
|
2025-08-08 16:59:54 -05:00
|
|
|
return
|
2023-10-03 18:05:40 -03:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
|
|
|
|
|
debug("setOwner Id: ${user.id} longName: ${user.longName.anonymize} shortName: ${user.shortName}")
|
|
|
|
|
handleReceivedUser(dest.num, user)
|
|
|
|
|
sendToRadio(newMeshPacketTo(dest.num).buildAdminPacket(id = packetId) { setOwner = user })
|
2020-04-23 11:18:48 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
private val packetIdGenerator = AtomicLong(Random().nextLong())
|
2020-05-30 17:28:00 -07:00
|
|
|
|
|
|
|
|
private fun generatePacketId(): Int {
|
2025-08-08 16:59:54 -05:00
|
|
|
// We need a 32 bit unsigned integer, but since Java doesn't have unsigned,
|
|
|
|
|
// we can use a long and mask it. To ensure it's never 0, we add 1 after masking.
|
|
|
|
|
return (packetIdGenerator.incrementAndGet() and 0xFFFFFFFFL).toInt().let { if (it == 0) 1 else it }
|
2020-05-30 17:28:00 -07:00
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:30:54 -03:00
|
|
|
private fun enqueueForSending(p: DataPacket) {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (p.dataType in rememberableDataTypes) {
|
2024-07-21 10:30:54 -03:00
|
|
|
offlineSentPackets.add(p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
private fun onServiceAction(action: ServiceAction) {
|
2025-06-29 12:45:09 +00:00
|
|
|
ignoreException {
|
|
|
|
|
when (action) {
|
|
|
|
|
is ServiceAction.GetDeviceMetadata -> getDeviceMetadata(action.destNum)
|
|
|
|
|
is ServiceAction.Favorite -> favoriteNode(action.node)
|
|
|
|
|
is ServiceAction.Ignore -> ignoreNode(action.node)
|
|
|
|
|
is ServiceAction.Reaction -> sendReaction(action)
|
|
|
|
|
is ServiceAction.AddSharedContact -> importContact(action.contact)
|
|
|
|
|
}
|
2025-01-02 06:38:33 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 13:36:11 -05:00
|
|
|
private fun importContact(contact: AdminProtos.SharedContact) {
|
2025-07-29 09:42:36 -05:00
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { addContact = contact })
|
|
|
|
|
handleReceivedUser(contact.nodeNum, contact.user)
|
2025-05-20 13:36:11 -05:00
|
|
|
}
|
|
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions {
|
2025-07-29 09:42:36 -05:00
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(wantResponse = true) { getDeviceMetadataRequest = true })
|
2025-01-02 06:38:33 -03:00
|
|
|
}
|
|
|
|
|
|
2025-02-16 06:02:02 -06:00
|
|
|
private fun favoriteNode(node: Node) = toRemoteExceptions {
|
2025-07-29 09:42:36 -05:00
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
if (node.isFavorite) {
|
|
|
|
|
debug("removing node ${node.num} from favorite list")
|
|
|
|
|
removeFavoriteNode = node.num
|
|
|
|
|
} else {
|
|
|
|
|
debug("adding node ${node.num} to favorite list")
|
|
|
|
|
setFavoriteNode = node.num
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
|
2025-02-16 06:02:02 -06:00
|
|
|
}
|
|
|
|
|
|
2025-01-02 06:38:33 -03:00
|
|
|
private fun ignoreNode(node: Node) = toRemoteExceptions {
|
2025-07-29 09:42:36 -05:00
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(myNodeNum).buildAdminPacket {
|
|
|
|
|
if (node.isIgnored) {
|
|
|
|
|
debug("removing node ${node.num} from ignore list")
|
|
|
|
|
removeIgnoredNode = node.num
|
|
|
|
|
} else {
|
|
|
|
|
debug("adding node ${node.num} to ignore list")
|
|
|
|
|
setIgnoredNode = node.num
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
2024-12-07 08:09:41 -03:00
|
|
|
}
|
|
|
|
|
|
2024-12-03 05:57:35 -06:00
|
|
|
private fun sendReaction(reaction: ServiceAction.Reaction) = toRemoteExceptions {
|
|
|
|
|
val channel = reaction.contactKey[0].digitToInt()
|
2025-08-08 16:59:54 -05:00
|
|
|
val destId = reaction.contactKey.substring(1)
|
2024-11-21 20:30:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
val packet =
|
2025-08-08 16:59:54 -05:00
|
|
|
newMeshPacketTo(destId).buildMeshPacket(channel = channel, priority = MeshPacket.Priority.BACKGROUND) {
|
2025-07-29 09:42:36 -05:00
|
|
|
emoji = 1
|
|
|
|
|
replyId = reaction.replyId
|
|
|
|
|
portnumValue = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
|
|
|
|
|
payload = ByteString.copyFrom(reaction.emoji.encodeToByteArray())
|
|
|
|
|
}
|
2024-12-03 05:57:35 -06:00
|
|
|
sendToRadio(packet)
|
2025-08-08 16:59:54 -05:00
|
|
|
rememberReaction(packet.toBuilder().setFrom(myNodeNum).build())
|
2024-11-21 20:30:25 -03:00
|
|
|
}
|
|
|
|
|
|
2025-05-30 13:17:09 -05:00
|
|
|
private val _lastAddress: MutableStateFlow<String?> = MutableStateFlow(null)
|
|
|
|
|
val lastAddress: StateFlow<String?>
|
|
|
|
|
get() = _lastAddress.asStateFlow()
|
|
|
|
|
|
|
|
|
|
lateinit var sharedPreferences: SharedPreferences
|
|
|
|
|
|
|
|
|
|
fun clearDatabases() = serviceScope.handledLaunch {
|
2025-06-03 19:58:52 -05:00
|
|
|
debug("Clearing nodeDB")
|
2025-05-30 13:17:09 -05:00
|
|
|
radioConfigRepository.clearNodeDB()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateLastAddress(deviceAddr: String?) {
|
2025-08-08 16:59:54 -05:00
|
|
|
val currentAddr = lastAddress.value
|
|
|
|
|
debug("setDeviceAddress: New: ${deviceAddr.anonymize}, Old: ${currentAddr.anonymize}")
|
|
|
|
|
|
|
|
|
|
if (deviceAddr != currentAddr) {
|
|
|
|
|
_lastAddress.value = deviceAddr ?: NO_DEVICE_SELECTED
|
|
|
|
|
sharedPreferences.edit { putString(DEVICE_ADDRESS_KEY, deviceAddr) }
|
|
|
|
|
clearNotifications()
|
|
|
|
|
clearDatabases()
|
|
|
|
|
resetState()
|
2025-05-30 13:17:09 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-28 14:50:05 +00:00
|
|
|
|
|
|
|
|
private fun clearNotifications() {
|
|
|
|
|
serviceNotifications.clearNotifications()
|
|
|
|
|
}
|
2020-04-19 19:23:20 -07:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
private val binder =
|
|
|
|
|
object : IMeshService.Stub() {
|
|
|
|
|
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
|
|
|
|
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
|
|
|
|
updateLastAddress(deviceAddr)
|
2025-08-08 16:59:54 -05:00
|
|
|
sharedPreferences.edit { putString("device_address", deviceAddr) }
|
|
|
|
|
connectionRouter.setDeviceAddress(deviceAddr)
|
2020-07-04 16:54:48 -07:00
|
|
|
}
|
2020-04-19 19:23:20 -07:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun subscribeReceiver(packageName: String, receiverName: String) = toRemoteExceptions {
|
2020-01-26 11:33:51 -08:00
|
|
|
clientPackages[receiverName] = packageName
|
|
|
|
|
}
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun getUpdateStatus(): Int = -4 // ProgressNotStarted (DEPRECATED)
|
2020-05-13 14:47:55 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun startFirmwareUpdate() = toRemoteExceptions {}
|
2020-05-13 14:47:55 -07:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getMyNodeInfo(): MyNodeInfo? = this@MeshService.myNodeInfo?.toMyNodeInfo()
|
2020-05-13 14:47:55 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun getMyId(): String = toRemoteExceptions { myNodeID }
|
2020-02-17 13:14:53 -08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun getPacketId(): Int = toRemoteExceptions { generatePacketId() }
|
2023-02-01 12:16:44 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun setOwner(user: MeshUser) = toRemoteExceptions {
|
|
|
|
|
setOwner(
|
|
|
|
|
generatePacketId(),
|
|
|
|
|
user {
|
|
|
|
|
id = user.id
|
|
|
|
|
longName = user.longName
|
|
|
|
|
shortName = user.shortName
|
|
|
|
|
isLicensed = user.isLicensed
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2020-01-25 10:00:57 -08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun setRemoteOwner(id: Int, payload: ByteArray) = toRemoteExceptions {
|
|
|
|
|
val parsed = MeshProtos.User.parseFrom(payload)
|
|
|
|
|
setOwner(id, parsed)
|
|
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getRemoteOwner(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) { getOwnerRequest = true },
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun send(p: DataPacket) {
|
|
|
|
|
toRemoteExceptions {
|
|
|
|
|
if (p.id == 0) p.id = generatePacketId()
|
|
|
|
|
info(
|
2025-08-08 16:59:54 -05:00
|
|
|
"sendData dest=${p.to}, id=${p.id} <- ${p.bytes?.size} bytes " +
|
|
|
|
|
"(connectionState=${connectionRouter.connectionState.value})",
|
2025-07-29 09:42:36 -05:00
|
|
|
)
|
2020-05-30 15:48:50 -07:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
if (p.dataType == 0) throw InvalidProtocolBufferException("Port numbers must be non-zero")
|
|
|
|
|
if ((p.bytes?.size ?: 0) >= MeshProtos.Constants.DATA_PAYLOAD_LEN_VALUE) {
|
2025-07-29 09:42:36 -05:00
|
|
|
p.status = MessageStatus.ERROR
|
|
|
|
|
throw RemoteException("Message too long")
|
|
|
|
|
} else {
|
|
|
|
|
p.status = MessageStatus.QUEUED
|
|
|
|
|
}
|
2020-11-21 08:41:26 +08:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
if (connectionRouter.connectionState.value == ConnectionState.CONNECTED) {
|
2025-07-29 09:42:36 -05:00
|
|
|
try {
|
|
|
|
|
sendNow(p)
|
|
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
errormsg("Error sending message, so enqueueing", ex)
|
|
|
|
|
enqueueForSending(p)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
enqueueForSending(p)
|
|
|
|
|
}
|
|
|
|
|
serviceBroadcasts.broadcastMessageStatus(p)
|
|
|
|
|
rememberDataPacket(p, false)
|
2022-12-10 00:14:32 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
GeeksvilleApplication.analytics.track(
|
|
|
|
|
"data_send",
|
2025-08-08 16:59:54 -05:00
|
|
|
DataPair("num_bytes", p.bytes?.size),
|
2025-07-29 09:42:36 -05:00
|
|
|
DataPair("type", p.dataType),
|
|
|
|
|
)
|
|
|
|
|
GeeksvilleApplication.analytics.track("num_data_sent", DataPair(1))
|
|
|
|
|
}
|
2020-01-25 10:00:57 -08:00
|
|
|
}
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getConfig(): ByteArray = toRemoteExceptions {
|
|
|
|
|
this@MeshService.localConfig.toByteArray() ?: throw NoDeviceConfigException()
|
|
|
|
|
}
|
2022-11-29 17:47:49 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
|
|
|
|
|
setRemoteConfig(generatePacketId(), myNodeNum, payload)
|
|
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
|
|
|
|
debug("Setting new radio config!")
|
|
|
|
|
val config = ConfigProtos.Config.parseFrom(payload)
|
|
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
|
2025-08-08 16:59:54 -05:00
|
|
|
if (num == myNodeNum) setLocalConfig(config)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
2025-08-08 16:59:54 -05:00
|
|
|
getConfigRequestValue = config
|
2025-07-29 09:42:36 -05:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-02-27 11:44:05 +08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
|
|
|
|
debug("Setting new module config!")
|
|
|
|
|
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
|
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
|
2025-08-08 16:59:54 -05:00
|
|
|
if (num == myNodeNum) setLocalModuleConfig(config)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getModuleConfigRequestValue = config
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-08-09 11:55:42 -05:00
|
|
|
override fun setRingtone(destNum: Int, ringtone: String) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setRingtoneMessage = ringtone })
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-08-09 11:55:42 -05:00
|
|
|
override fun getRingtone(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getRingtoneRequest = true
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-08-09 11:55:42 -05:00
|
|
|
override fun setCannedMessages(destNum: Int, messages: String) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setCannedMessageModuleMessages = messages })
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-08-09 11:55:42 -05:00
|
|
|
override fun getCannedMessages(id: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
|
|
|
|
getCannedMessageModuleMessagesRequest = true
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2022-11-22 22:01:37 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun setChannel(payload: ByteArray) = toRemoteExceptions {
|
2025-07-29 09:42:36 -05:00
|
|
|
setRemoteChannel(generatePacketId(), myNodeNum, payload)
|
|
|
|
|
}
|
2023-04-29 07:14:30 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun setRemoteChannel(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
2025-07-29 09:42:36 -05:00
|
|
|
val channel = ChannelProtos.Channel.parseFrom(payload)
|
|
|
|
|
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setChannel = channel })
|
|
|
|
|
}
|
2023-04-29 07:14:30 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getRemoteChannel(id: Int, destNum: Int, index: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket(id = id, wantResponse = true) {
|
2025-08-08 16:59:54 -05:00
|
|
|
getChannelRequest = index + 1 // API is 1-based
|
2025-07-29 09:42:36 -05:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-02-27 13:43:55 +08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun beginEditSettings() = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { beginEditSettings = true })
|
|
|
|
|
}
|
2022-11-29 17:47:49 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun commitEditSettings() = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { commitEditSettings = true })
|
|
|
|
|
}
|
2022-11-29 17:47:49 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getChannelSet(): ByteArray = toRemoteExceptions { this@MeshService.channelSet.toByteArray() }
|
2023-02-20 21:48:55 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
|
2025-08-08 16:59:54 -05:00
|
|
|
nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2020-01-22 21:25:31 -08:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun connectionState(): String = toRemoteExceptions {
|
2025-08-08 16:59:54 -05:00
|
|
|
this@MeshService.connectionRouter.connectionState.value.toString()
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2022-01-03 21:59:30 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
override fun startProvideLocation() = toRemoteExceptions {
|
|
|
|
|
@SuppressLint("MissingPermission")
|
|
|
|
|
startLocationRequests()
|
|
|
|
|
}
|
2022-01-03 21:59:30 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun stopProvideLocation() = toRemoteExceptions { stopLocationRequests() }
|
2024-09-24 07:53:09 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun removeByNodenum(requestId: Int, nodeNum: Int) = toRemoteExceptions {
|
2025-08-08 16:59:54 -05:00
|
|
|
nodeDBbyNodeNum.remove(nodeNum)?.let { removedNode ->
|
|
|
|
|
if (removedNode.user.id.isNotEmpty()) {
|
|
|
|
|
_nodeDBbyID.remove(removedNode.user.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { this.removeByNodenum = nodeNum })
|
2024-10-03 03:02:13 -05:00
|
|
|
}
|
2025-06-19 11:42:48 -05:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestUserInfo(destNum: Int) = toRemoteExceptions {
|
|
|
|
|
if (destNum != myNodeNum) {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildMeshPacket(channel = nodeDBbyNodeNum[destNum]?.channel ?: 0) {
|
|
|
|
|
portnumValue = Portnums.PortNum.NODEINFO_APP_VALUE
|
|
|
|
|
wantResponse = true
|
2025-08-08 16:59:54 -05:00
|
|
|
payload = nodeDBbyNodeNum[myNodeNum]?.user?.toByteString() ?: ByteString.EMPTY
|
2025-07-29 09:42:36 -05:00
|
|
|
},
|
|
|
|
|
)
|
2025-06-19 11:42:48 -05:00
|
|
|
}
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-06-19 11:42:48 -05:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestPosition(destNum: Int, position: Position) = toRemoteExceptions {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (destNum == myNodeNum) return@toRemoteExceptions
|
2025-06-19 11:42:48 -05:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
val provideLocation = sharedPreferences.getBoolean("provide-location-$myNodeNum", false)
|
|
|
|
|
val currentPosition =
|
|
|
|
|
when {
|
|
|
|
|
provideLocation && position.isValid() -> position
|
|
|
|
|
else -> nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
if (currentPosition == null) {
|
|
|
|
|
debug("Position request skipped - no valid position available")
|
|
|
|
|
return@toRemoteExceptions
|
|
|
|
|
}
|
2024-09-18 19:50:33 -03:00
|
|
|
|
2025-08-08 16:59:54 -05:00
|
|
|
val meshPosition = position {
|
|
|
|
|
latitudeI = Position.degI(currentPosition.latitude)
|
|
|
|
|
longitudeI = Position.degI(currentPosition.longitude)
|
|
|
|
|
altitude = currentPosition.altitude
|
|
|
|
|
time = currentSecond()
|
2025-07-29 09:42:36 -05:00
|
|
|
}
|
2025-08-08 16:59:54 -05:00
|
|
|
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildMeshPacket(
|
|
|
|
|
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
|
|
|
|
priority = MeshPacket.Priority.BACKGROUND,
|
|
|
|
|
) {
|
|
|
|
|
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
|
|
|
|
payload = meshPosition.toByteString()
|
|
|
|
|
wantResponse = true
|
|
|
|
|
},
|
|
|
|
|
)
|
2022-11-15 22:00:29 -03:00
|
|
|
}
|
2025-07-29 09:42:36 -05:00
|
|
|
|
|
|
|
|
override fun setFixedPosition(destNum: Int, position: Position) = toRemoteExceptions {
|
|
|
|
|
val pos = position {
|
|
|
|
|
latitudeI = Position.degI(position.latitude)
|
|
|
|
|
longitudeI = Position.degI(position.longitude)
|
|
|
|
|
altitude = position.altitude
|
2024-09-18 19:50:33 -03:00
|
|
|
}
|
2025-07-29 09:42:36 -05:00
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildAdminPacket {
|
2025-08-08 16:59:54 -05:00
|
|
|
if (position.latitude != 0.0 || position.longitude != 0.0 || position.altitude != 0) {
|
2025-07-29 09:42:36 -05:00
|
|
|
setFixedPosition = pos
|
|
|
|
|
} else {
|
|
|
|
|
removeFixedPosition = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
updateNodeInfo(destNum) { it.setPosition(pos, currentSecond()) }
|
2024-09-28 08:09:42 -03:00
|
|
|
}
|
2022-11-06 17:46:57 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(
|
|
|
|
|
newMeshPacketTo(destNum).buildMeshPacket(
|
|
|
|
|
wantAck = true,
|
|
|
|
|
id = requestId,
|
|
|
|
|
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
|
|
|
|
) {
|
|
|
|
|
portnumValue = Portnums.PortNum.TRACEROUTE_APP_VALUE
|
|
|
|
|
wantResponse = true
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-04-16 06:16:41 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestShutdown(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { shutdownSeconds = 5 })
|
|
|
|
|
}
|
2022-09-30 15:57:04 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestReboot(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { rebootSeconds = 5 })
|
|
|
|
|
}
|
2022-06-06 17:29:09 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestFactoryReset(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { factoryResetDevice = 1 })
|
|
|
|
|
}
|
2022-09-18 18:35:13 -03:00
|
|
|
|
2025-07-29 09:42:36 -05:00
|
|
|
override fun requestNodedbReset(requestId: Int, destNum: Int) = toRemoteExceptions {
|
|
|
|
|
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(id = requestId) { nodedbReset = 1 })
|
|
|
|
|
}
|
2022-09-18 18:35:13 -03:00
|
|
|
}
|
2020-05-30 15:48:50 -07:00
|
|
|
}
|