mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(transport): complete transport architecture overhaul — extract callback, wire BleReconnectPolicy, fix safety issues (#5080)
This commit is contained in:
parent
962c619c4c
commit
e85300531e
64 changed files with 1184 additions and 1018 deletions
|
|
@ -39,18 +39,26 @@ import org.meshtastic.core.repository.PacketHandler
|
|||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.AirQualityMetrics
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Constants
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HostMetrics
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Neighbor
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.PowerMetrics
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
@Suppress("TooManyFunctions", "CyclomaticComplexMethod")
|
||||
@Single
|
||||
|
|
@ -68,10 +76,6 @@ class CommandSenderImpl(
|
|||
private val localConfig = MutableStateFlow(LocalConfig())
|
||||
private val channelSet = MutableStateFlow(ChannelSet())
|
||||
|
||||
// We'll need a way to track connection state in shared code,
|
||||
// maybe via ServiceRepository or similar.
|
||||
// For now I'll assume it's injected or available.
|
||||
|
||||
init {
|
||||
radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope)
|
||||
radioConfigRepository.channelSetFlow.onEach { channelSet.value = it }.launchIn(scope)
|
||||
|
|
@ -141,14 +145,11 @@ class CommandSenderImpl(
|
|||
if (!Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value)) {
|
||||
val actualSize = Data.ADAPTER.encodedSize(data)
|
||||
p.status = MessageStatus.ERROR
|
||||
// throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})")
|
||||
// RemoteException is Android specific. For KMP we might want a custom exception.
|
||||
error("Message too long: $actualSize bytes")
|
||||
} else {
|
||||
p.status = MessageStatus.QUEUED
|
||||
}
|
||||
|
||||
// TODO: Check connection state
|
||||
sendNow(p)
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +192,7 @@ class CommandSenderImpl(
|
|||
return packetHandler.sendToRadioAndAwait(packet)
|
||||
}
|
||||
|
||||
override fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int?, wantResponse: Boolean) {
|
||||
override fun sendPosition(pos: ProtoPosition, destNum: Int?, wantResponse: Boolean) {
|
||||
val myNum = nodeManager.myNodeNum.value ?: return
|
||||
val idNum = destNum ?: myNum
|
||||
Logger.d { "Sending our position/time to=$idNum $pos" }
|
||||
|
|
@ -217,7 +218,7 @@ class CommandSenderImpl(
|
|||
|
||||
override fun requestPosition(destNum: Int, currentPosition: Position) {
|
||||
val meshPosition =
|
||||
org.meshtastic.proto.Position(
|
||||
ProtoPosition(
|
||||
latitude_i = Position.degI(currentPosition.latitude),
|
||||
longitude_i = Position.degI(currentPosition.longitude),
|
||||
altitude = currentPosition.altitude,
|
||||
|
|
@ -240,7 +241,7 @@ class CommandSenderImpl(
|
|||
|
||||
override fun setFixedPosition(destNum: Int, pos: Position) {
|
||||
val meshPos =
|
||||
org.meshtastic.proto.Position(
|
||||
ProtoPosition(
|
||||
latitude_i = Position.degI(pos.latitude),
|
||||
longitude_i = Position.degI(pos.longitude),
|
||||
altitude = pos.altitude,
|
||||
|
|
@ -293,21 +294,17 @@ class CommandSenderImpl(
|
|||
|
||||
if (type == TelemetryType.PAX) {
|
||||
portNum = PortNum.PAXCOUNTER_APP
|
||||
payloadBytes = org.meshtastic.proto.Paxcount().encode().toByteString()
|
||||
payloadBytes = Paxcount().encode().toByteString()
|
||||
} else {
|
||||
portNum = PortNum.TELEMETRY_APP
|
||||
payloadBytes =
|
||||
Telemetry(
|
||||
device_metrics =
|
||||
if (type == TelemetryType.DEVICE) org.meshtastic.proto.DeviceMetrics() else null,
|
||||
environment_metrics =
|
||||
if (type == TelemetryType.ENVIRONMENT) org.meshtastic.proto.EnvironmentMetrics() else null,
|
||||
air_quality_metrics =
|
||||
if (type == TelemetryType.AIR_QUALITY) org.meshtastic.proto.AirQualityMetrics() else null,
|
||||
power_metrics = if (type == TelemetryType.POWER) org.meshtastic.proto.PowerMetrics() else null,
|
||||
local_stats =
|
||||
if (type == TelemetryType.LOCAL_STATS) org.meshtastic.proto.LocalStats() else null,
|
||||
host_metrics = if (type == TelemetryType.HOST) org.meshtastic.proto.HostMetrics() else null,
|
||||
device_metrics = if (type == TelemetryType.DEVICE) DeviceMetrics() else null,
|
||||
environment_metrics = if (type == TelemetryType.ENVIRONMENT) EnvironmentMetrics() else null,
|
||||
air_quality_metrics = if (type == TelemetryType.AIR_QUALITY) AirQualityMetrics() else null,
|
||||
power_metrics = if (type == TelemetryType.POWER) PowerMetrics() else null,
|
||||
local_stats = if (type == TelemetryType.LOCAL_STATS) LocalStats() else null,
|
||||
host_metrics = if (type == TelemetryType.HOST) HostMetrics() else null,
|
||||
)
|
||||
.encode()
|
||||
.toByteString()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
|
|
@ -199,7 +200,7 @@ class MeshActionHandlerImpl(
|
|||
commandSender.sendData(p)
|
||||
serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN)
|
||||
dataHandler.value.rememberDataPacket(p, myNodeNum, false)
|
||||
val bytes = p.bytes ?: okio.ByteString.EMPTY
|
||||
val bytes = p.bytes ?: ByteString.EMPTY
|
||||
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +357,7 @@ class MeshActionHandlerImpl(
|
|||
override fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {
|
||||
val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA
|
||||
val otaEvent =
|
||||
AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: okio.ByteString.EMPTY)
|
||||
AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: ByteString.EMPTY)
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okio.ByteString
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
|
|
@ -221,7 +222,7 @@ class MeshConnectionManagerImpl(
|
|||
|
||||
private fun tearDownConnection() {
|
||||
packetHandler.stopPacketQueue()
|
||||
commandSender.setSessionPasskey(okio.ByteString.EMPTY) // Prevent stale passkey on reconnect.
|
||||
commandSender.setSessionPasskey(ByteString.EMPTY) // Prevent stale passkey on reconnect.
|
||||
locationManager.stop()
|
||||
mqttManager.stop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.ByteString
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
|
|
@ -247,7 +248,7 @@ class MeshDataHandlerImpl(
|
|||
val payload = packet.decoded?.payload ?: return
|
||||
val u =
|
||||
User.ADAPTER.decode(payload)
|
||||
.let { if (it.is_licensed == true) it.copy(public_key = okio.ByteString.EMPTY) else it }
|
||||
.let { if (it.is_licensed == true) it.copy(public_key = ByteString.EMPTY) else it }
|
||||
.let {
|
||||
if (packet.via_mqtt == true && !it.long_name.endsWith(" (MQTT)")) {
|
||||
it.copy(long_name = "${it.long_name} (MQTT)")
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class MeshMessageProcessorImpl(
|
|||
@Volatile private var lastLocalNodeRefreshMs = 0L
|
||||
|
||||
private val earlyMutex = Mutex()
|
||||
private val earlyReceivedPackets = kotlin.collections.ArrayDeque<MeshPacket>()
|
||||
private val earlyReceivedPackets = ArrayDeque<MeshPacket>()
|
||||
private val maxEarlyPacketBuffer = 10240
|
||||
|
||||
override fun clearEarlyPackets() {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class TracerouteHandlerImpl(
|
|||
routeDiscovery.getTracerouteResponse(
|
||||
getUser = { num ->
|
||||
nodeManager.nodeDBbyNodeNum[num]?.let { "${it.user.long_name} (${it.user.short_name})" }
|
||||
?: "Unknown" // TODO: Use core:resources once available in core:data
|
||||
?: "Unknown"
|
||||
},
|
||||
headerTowards = "Route towards destination:",
|
||||
headerBack = "Route back to us:",
|
||||
|
|
|
|||
|
|
@ -132,13 +132,10 @@ class MeshConnectionManagerImplTest {
|
|||
scope,
|
||||
)
|
||||
|
||||
@AfterTest fun tearDown() {}
|
||||
@AfterTest fun tearDown() = Unit
|
||||
|
||||
@Test
|
||||
fun `Connected state triggers broadcast and config start`() = runTest(testDispatcher) {
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
|
||||
manager = createManager(backgroundScope)
|
||||
radioConnectionState.value = ConnectionState.Connected
|
||||
advanceUntilIdle()
|
||||
|
|
@ -153,16 +150,6 @@ class MeshConnectionManagerImplTest {
|
|||
|
||||
@Test
|
||||
fun `Disconnected state stops services`() = runTest(testDispatcher) {
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
manager = createManager(backgroundScope)
|
||||
// Transition to Connected first so that Disconnected actually does something
|
||||
|
|
@ -191,11 +178,6 @@ class MeshConnectionManagerImplTest {
|
|||
device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT),
|
||||
)
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
|
||||
manager = createManager(backgroundScope)
|
||||
|
|
@ -216,11 +198,6 @@ class MeshConnectionManagerImplTest {
|
|||
// Power saving enabled
|
||||
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true))
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
|
||||
manager = createManager(backgroundScope)
|
||||
advanceUntilIdle()
|
||||
|
|
@ -280,11 +257,6 @@ class MeshConnectionManagerImplTest {
|
|||
device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER),
|
||||
)
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
|
||||
manager = createManager(backgroundScope)
|
||||
|
|
@ -317,11 +289,6 @@ class MeshConnectionManagerImplTest {
|
|||
// Power saving enabled so DeviceSleep is preserved (not mapped to Disconnected)
|
||||
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true))
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
|
||||
// Record every state transition so we can verify ordering
|
||||
|
|
@ -367,11 +334,6 @@ class MeshConnectionManagerImplTest {
|
|||
// Power saving enabled with a short ls_secs so the sleep timeout fires quickly
|
||||
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true, ls_secs = 1))
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
|
||||
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
|
||||
val observed = mutableListOf<ConnectionState>()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import dev.mokkery.answering.returns
|
|||
import dev.mokkery.every
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verifySuspend
|
||||
import io.kotest.property.Arb
|
||||
import io.kotest.property.arbitrary.int
|
||||
|
|
@ -84,6 +85,8 @@ class PacketHandlerImplTest {
|
|||
val toRadio = ToRadio(packet = MeshPacket(id = 123))
|
||||
|
||||
handler.sendToRadio(toRadio)
|
||||
|
||||
verify { radioInterfaceService.sendToRadio(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -93,6 +96,8 @@ class PacketHandlerImplTest {
|
|||
|
||||
handler.sendToRadio(packet)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
verify { radioInterfaceService.sendToRadio(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue