Traceroute map position snapshots (#4035)

Signed-off-by: Jord <650645+DivineOmega@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jord 2025-12-18 14:14:03 +00:00 committed by GitHub
parent 03fd2bf9ba
commit 9833795864
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1195 additions and 44 deletions

View file

@ -43,6 +43,7 @@ import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.di.CoroutineDispatchers
@ -87,6 +88,7 @@ constructor(
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val nodeRepository: NodeRepository,
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
private val deviceHardwareRepository: DeviceHardwareRepository,
private val firmwareReleaseRepository: FirmwareReleaseRepository,
) : ViewModel() {
@ -146,6 +148,8 @@ constructor(
return overlay
}
fun tracerouteSnapshotPositions(logUuid: String) = tracerouteSnapshotRepository.getSnapshotPositions(logUuid)
fun clearTracerouteResponse() = serviceRepository.clearTracerouteResponse()
fun tracerouteMapAvailability(forwardRoute: List<Int>, returnRoute: List<Int>): TracerouteMapAvailability =

View file

@ -64,6 +64,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.model.toMessageRes
@ -89,7 +90,12 @@ import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.proto.MeshProtos
import java.text.DateFormat
private data class TracerouteDialog(val message: AnnotatedString, val requestId: Int, val overlay: TracerouteOverlay?)
private data class TracerouteDialog(
val message: AnnotatedString,
val requestId: Int,
val responseLogUuid: String,
val overlay: TracerouteOverlay?,
)
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod")
@ -98,7 +104,7 @@ fun TracerouteLogScreen(
modifier: Modifier = Modifier,
viewModel: MetricsViewModel = hiltViewModel(),
onNavigateUp: () -> Unit,
onViewOnMap: (requestId: Int) -> Unit = {},
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> },
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) }
@ -185,10 +191,12 @@ fun TracerouteLogScreen(
AnnotatedString(it)
}
dialogMessage?.let {
val responseLogUuid = result?.uuid ?: return@combinedClickable
showDialog =
TracerouteDialog(
message = it,
requestId = log.fromRadio.packet.id,
responseLogUuid = responseLogUuid,
overlay = overlay,
)
}
@ -211,23 +219,34 @@ private fun TracerouteLogDialogs(
dialog: TracerouteDialog?,
errorMessageRes: StringResource?,
viewModel: MetricsViewModel,
onViewOnMap: (requestId: Int) -> Unit,
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit,
onShowErrorMessageRes: (StringResource) -> Unit,
onDismissDialog: () -> Unit,
onDismissError: () -> Unit,
) {
dialog?.let { dialogState ->
val snapshotPositionsFlow =
remember(dialogState.responseLogUuid) { viewModel.tracerouteSnapshotPositions(dialogState.responseLogUuid) }
val snapshotPositions by snapshotPositionsFlow.collectAsStateWithLifecycle(emptyMap<Int, MeshProtos.Position>())
SimpleAlertDialog(
title = Res.string.traceroute,
text = { SelectionContainer { Text(text = dialogState.message) } },
confirmText = stringResource(Res.string.view_on_map),
onConfirm = {
val positionedNodeNums =
if (snapshotPositions.isNotEmpty()) {
snapshotPositions.keys
} else {
viewModel.positionedNodeNums()
}
val availability =
viewModel.tracerouteMapAvailability(
evaluateTracerouteMapAvailability(
forwardRoute = dialogState.overlay?.forwardRoute.orEmpty(),
returnRoute = dialogState.overlay?.returnRoute.orEmpty(),
positionedNodeNums = positionedNodeNums,
)
availability.toMessageRes()?.let(onShowErrorMessageRes) ?: onViewOnMap(dialogState.requestId)
availability.toMessageRes()?.let(onShowErrorMessageRes)
?: onViewOnMap(dialogState.requestId, dialogState.responseLogUuid)
onDismissDialog()
},
onDismiss = onDismissDialog,

View file

@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.flowOf
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.strings.Res
@ -54,37 +55,59 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.feature.map.MapView
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.MeshProtos
@Composable
fun TracerouteMapScreen(
metricsViewModel: MetricsViewModel = hiltViewModel(),
requestId: Int,
logUuid: String? = null,
onNavigateUp: () -> Unit,
) {
val state by metricsViewModel.state.collectAsStateWithLifecycle()
val nodeTitle = state.node?.user?.longName ?: stringResource(Res.string.traceroute)
val routeDiscovery =
state.tracerouteResults
.find { it.fromRadio.packet.decoded.requestId == requestId }
?.fromRadio
?.packet
?.fullRouteDiscovery
val snapshotPositions by
remember(logUuid) {
logUuid?.let(metricsViewModel::tracerouteSnapshotPositions)
?: flowOf(emptyMap<Int, MeshProtos.Position>())
}
.collectAsStateWithLifecycle(emptyMap<Int, MeshProtos.Position>())
val tracerouteResult =
if (logUuid != null) {
state.tracerouteResults.find { it.uuid == logUuid }
} else {
state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == requestId }
}
val routeDiscovery = tracerouteResult?.fromRadio?.packet?.fullRouteDiscovery
val overlayFromLogs =
remember(routeDiscovery) {
routeDiscovery?.let {
TracerouteOverlay(requestId = requestId, forwardRoute = it.routeList, returnRoute = it.routeBackList)
}
remember(routeDiscovery, requestId) {
routeDiscovery?.let { TracerouteOverlay(requestId, it.routeList, it.routeBackList) }
}
val overlayFromService = remember(requestId) { metricsViewModel.getTracerouteOverlay(requestId) }
val overlay = overlayFromLogs ?: overlayFromService
var tracerouteNodesShown by remember { mutableStateOf(0) }
var tracerouteNodesTotal by remember { mutableStateOf(0) }
LaunchedEffect(Unit) { metricsViewModel.clearTracerouteResponse() }
TracerouteMapScaffold(
title = state.node?.user?.longName ?: stringResource(Res.string.traceroute),
overlay = overlay,
snapshotPositions = snapshotPositions,
onNavigateUp = onNavigateUp,
)
}
@Composable
private fun TracerouteMapScaffold(
title: String,
overlay: TracerouteOverlay?,
snapshotPositions: Map<Int, MeshProtos.Position>,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier,
) {
var tracerouteNodesShown by remember { mutableStateOf(0) }
var tracerouteNodesTotal by remember { mutableStateOf(0) }
Scaffold(
topBar = {
MainAppBar(
title = nodeTitle,
title = title,
ourNode = null,
showNodeChip = false,
canNavigateUp = true,
@ -94,10 +117,11 @@ fun TracerouteMapScreen(
)
},
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
Box(modifier = modifier.fillMaxSize().padding(paddingValues)) {
MapView(
navigateToNodeDetails = {},
tracerouteOverlay = overlay,
tracerouteNodePositions = snapshotPositions,
onTracerouteMappableCountChanged = { shown, total ->
tracerouteNodesShown = shown
tracerouteNodesTotal = total