mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
267 lines
11 KiB
Kotlin
267 lines
11 KiB
Kotlin
/*
|
|
* Copyright (c) 2025-2026 Meshtastic LLC
|
|
*
|
|
* 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/>.
|
|
*/
|
|
package com.geeksville.mesh.service
|
|
|
|
import android.app.Notification
|
|
import co.touchlab.kermit.Logger
|
|
import com.geeksville.mesh.concurrent.handledLaunch
|
|
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
|
import com.meshtastic.core.strings.getString
|
|
import kotlinx.coroutines.CancellationException
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.flow.launchIn
|
|
import kotlinx.coroutines.flow.onEach
|
|
import org.meshtastic.core.analytics.DataPair
|
|
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
|
import org.meshtastic.core.data.repository.NodeRepository
|
|
import org.meshtastic.core.data.repository.RadioConfigRepository
|
|
import org.meshtastic.core.prefs.ui.UiPrefs
|
|
import org.meshtastic.core.service.ConnectionState
|
|
import org.meshtastic.core.service.MeshServiceNotifications
|
|
import org.meshtastic.core.strings.Res
|
|
import org.meshtastic.core.strings.connected_count
|
|
import org.meshtastic.core.strings.connecting
|
|
import org.meshtastic.core.strings.device_sleeping
|
|
import org.meshtastic.core.strings.disconnected
|
|
import org.meshtastic.proto.ConfigProtos
|
|
import org.meshtastic.proto.MeshProtos.ToRadio
|
|
import org.meshtastic.proto.TelemetryProtos
|
|
import javax.inject.Inject
|
|
import javax.inject.Singleton
|
|
import kotlin.time.Duration.Companion.seconds
|
|
|
|
@Suppress("LongParameterList", "TooManyFunctions")
|
|
@Singleton
|
|
class MeshConnectionManager
|
|
@Inject
|
|
constructor(
|
|
private val radioInterfaceService: RadioInterfaceService,
|
|
private val connectionStateHolder: ConnectionStateHandler,
|
|
private val serviceBroadcasts: MeshServiceBroadcasts,
|
|
private val serviceNotifications: MeshServiceNotifications,
|
|
private val uiPrefs: UiPrefs,
|
|
private val packetHandler: PacketHandler,
|
|
private val nodeRepository: NodeRepository,
|
|
private val locationManager: MeshLocationManager,
|
|
private val mqttManager: MeshMqttManager,
|
|
private val historyManager: MeshHistoryManager,
|
|
private val radioConfigRepository: RadioConfigRepository,
|
|
private val commandSender: MeshCommandSender,
|
|
private val nodeManager: MeshNodeManager,
|
|
private val analytics: PlatformAnalytics,
|
|
) {
|
|
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
private var sleepTimeout: Job? = null
|
|
private var locationRequestsJob: Job? = null
|
|
private var connectTimeMsec = 0L
|
|
|
|
fun start(scope: CoroutineScope) {
|
|
this.scope = scope
|
|
radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope)
|
|
|
|
nodeRepository.myNodeInfo
|
|
.onEach { myNodeEntity ->
|
|
locationRequestsJob?.cancel()
|
|
if (myNodeEntity != null) {
|
|
locationRequestsJob =
|
|
uiPrefs
|
|
.shouldProvideNodeLocation(myNodeEntity.myNodeNum)
|
|
.onEach { shouldProvide ->
|
|
if (shouldProvide) {
|
|
locationManager.start(scope) { pos -> commandSender.sendPosition(pos) }
|
|
} else {
|
|
locationManager.stop()
|
|
}
|
|
}
|
|
.launchIn(scope)
|
|
}
|
|
}
|
|
.launchIn(scope)
|
|
}
|
|
|
|
private fun onRadioConnectionState(newState: ConnectionState) {
|
|
scope.handledLaunch {
|
|
val localConfig = radioConfigRepository.localConfigFlow.first()
|
|
val isRouter = localConfig.device.role == ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
|
val lsEnabled = localConfig.power.isPowerSaving || isRouter
|
|
|
|
val effectiveState =
|
|
when (newState) {
|
|
is ConnectionState.Connected -> ConnectionState.Connected
|
|
is ConnectionState.DeviceSleep ->
|
|
if (lsEnabled) ConnectionState.DeviceSleep else ConnectionState.Disconnected
|
|
is ConnectionState.Connecting -> ConnectionState.Connecting
|
|
is ConnectionState.Disconnected -> ConnectionState.Disconnected
|
|
}
|
|
onConnectionChanged(effectiveState)
|
|
}
|
|
}
|
|
|
|
private fun onConnectionChanged(c: ConnectionState) {
|
|
if (connectionStateHolder.connectionState.value == c && c !is ConnectionState.Connected) return
|
|
Logger.d { "onConnectionChanged: ${connectionStateHolder.connectionState.value} -> $c" }
|
|
|
|
sleepTimeout?.cancel()
|
|
sleepTimeout = null
|
|
|
|
when (c) {
|
|
is ConnectionState.Connecting -> connectionStateHolder.setState(ConnectionState.Connecting)
|
|
is ConnectionState.Connected -> handleConnected()
|
|
is ConnectionState.DeviceSleep -> handleDeviceSleep()
|
|
is ConnectionState.Disconnected -> handleDisconnected()
|
|
}
|
|
updateStatusNotification()
|
|
}
|
|
|
|
private fun handleConnected() {
|
|
connectionStateHolder.setState(ConnectionState.Connecting)
|
|
serviceBroadcasts.broadcastConnection()
|
|
Logger.d { "Starting connect" }
|
|
connectTimeMsec = System.currentTimeMillis()
|
|
startConfigOnly()
|
|
}
|
|
|
|
private fun handleDeviceSleep() {
|
|
connectionStateHolder.setState(ConnectionState.DeviceSleep)
|
|
packetHandler.stopPacketQueue()
|
|
locationManager.stop()
|
|
mqttManager.stop()
|
|
|
|
if (connectTimeMsec != 0L) {
|
|
val now = System.currentTimeMillis()
|
|
val duration = now - connectTimeMsec
|
|
connectTimeMsec = 0L
|
|
analytics.track(
|
|
EVENT_CONNECTED_SECONDS,
|
|
DataPair(EVENT_CONNECTED_SECONDS, duration / MILLISECONDS_IN_SECOND),
|
|
)
|
|
}
|
|
|
|
sleepTimeout =
|
|
scope.handledLaunch {
|
|
try {
|
|
val localConfig = radioConfigRepository.localConfigFlow.first()
|
|
val timeout = (localConfig.power?.lsSecs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS
|
|
Logger.d { "Waiting for sleeping device, timeout=$timeout secs" }
|
|
delay(timeout.seconds)
|
|
Logger.w { "Device timeout out, setting disconnected" }
|
|
onConnectionChanged(ConnectionState.Disconnected)
|
|
} catch (_: CancellationException) {
|
|
Logger.d { "device sleep timeout cancelled" }
|
|
}
|
|
}
|
|
|
|
serviceBroadcasts.broadcastConnection()
|
|
}
|
|
|
|
private fun handleDisconnected() {
|
|
connectionStateHolder.setState(ConnectionState.Disconnected)
|
|
packetHandler.stopPacketQueue()
|
|
locationManager.stop()
|
|
mqttManager.stop()
|
|
|
|
analytics.track(
|
|
EVENT_MESH_DISCONNECT,
|
|
DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size),
|
|
DataPair(KEY_NUM_ONLINE, nodeManager.nodeDBbyNodeNum.values.count { it.isOnline }),
|
|
)
|
|
analytics.track(EVENT_NUM_NODES, DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size))
|
|
|
|
serviceBroadcasts.broadcastConnection()
|
|
}
|
|
|
|
fun startConfigOnly() {
|
|
packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = CONFIG_ONLY_NONCE })
|
|
}
|
|
|
|
fun startNodeInfoOnly() {
|
|
packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = NODE_INFO_NONCE })
|
|
}
|
|
|
|
fun onHasSettings() {
|
|
commandSender.processQueuedPackets()
|
|
|
|
// Start MQTT if enabled
|
|
scope.handledLaunch {
|
|
val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
|
|
mqttManager.start(scope, moduleConfig.mqtt.enabled, moduleConfig.mqtt.proxyToClientEnabled)
|
|
}
|
|
|
|
reportConnection()
|
|
|
|
val myNodeNum = nodeManager.myNodeNum ?: 0
|
|
// Request history
|
|
scope.handledLaunch {
|
|
val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
|
|
historyManager.requestHistoryReplay("onHasSettings", myNodeNum, moduleConfig.storeForward, "Unknown")
|
|
}
|
|
|
|
// Set time
|
|
commandSender.sendAdmin(myNodeNum) {
|
|
setTimeOnly = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
|
|
}
|
|
}
|
|
|
|
private fun reportConnection() {
|
|
val myNode = nodeManager.getMyNodeInfo()
|
|
val radioModel = DataPair(KEY_RADIO_MODEL, myNode?.model ?: "unknown")
|
|
analytics.track(
|
|
EVENT_MESH_CONNECT,
|
|
DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size),
|
|
DataPair(KEY_NUM_ONLINE, nodeManager.nodeDBbyNodeNum.values.count { it.isOnline }),
|
|
radioModel,
|
|
)
|
|
}
|
|
|
|
fun updateTelemetry(telemetry: TelemetryProtos.Telemetry) {
|
|
updateStatusNotification(telemetry)
|
|
}
|
|
|
|
fun updateStatusNotification(telemetry: TelemetryProtos.Telemetry? = null): Notification {
|
|
val summary =
|
|
when (connectionStateHolder.connectionState.value) {
|
|
is ConnectionState.Connected ->
|
|
getString(Res.string.connected_count)
|
|
.format(nodeManager.nodeDBbyNodeNum.values.count { it.isOnline })
|
|
is ConnectionState.Disconnected -> getString(Res.string.disconnected)
|
|
is ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping)
|
|
is ConnectionState.Connecting -> getString(Res.string.connecting)
|
|
}
|
|
return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry)
|
|
}
|
|
|
|
companion object {
|
|
private const val CONFIG_ONLY_NONCE = 69420
|
|
private const val NODE_INFO_NONCE = 69421
|
|
private const val MILLISECONDS_IN_SECOND = 1000.0
|
|
private const val DEVICE_SLEEP_TIMEOUT_SECONDS = 30
|
|
|
|
private const val EVENT_CONNECTED_SECONDS = "connected_seconds"
|
|
private const val EVENT_MESH_DISCONNECT = "mesh_disconnect"
|
|
private const val EVENT_NUM_NODES = "num_nodes"
|
|
private const val EVENT_MESH_CONNECT = "mesh_connect"
|
|
|
|
private const val KEY_NUM_NODES = "num_nodes"
|
|
private const val KEY_NUM_ONLINE = "num_online"
|
|
private const val KEY_RADIO_MODEL = "radio_model"
|
|
}
|
|
}
|