From 99938e97bd481e261c2edfd16bba1f09d5e73b10 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 23:34:03 +1000 Subject: [PATCH] add times to traceroute displays. (#2999) --- .../geeksville/mesh/model/MetricsViewModel.kt | 4 +- .../geeksville/mesh/model/RouteDiscovery.kt | 77 ++++++++++--------- .../geeksville/mesh/service/MeshService.kt | 21 ++++- .../mesh/ui/metrics/TracerouteLog.kt | 15 +++- 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt index c88ee607f..a5f8d2216 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -84,7 +84,7 @@ data class MetricsState( val powerMetrics: List = emptyList(), val hostMetrics: List = emptyList(), val tracerouteRequests: List = emptyList(), - val tracerouteResults: List = emptyList(), + val tracerouteResults: List = emptyList(), val positionLogs: List = emptyList(), val deviceHardware: DeviceHardware? = null, val isLocalDevice: Boolean = false, @@ -321,7 +321,7 @@ constructor( combine( meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE), - meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE), + meshLogRepository.getLogsFrom(destNum ?: 0, PortNum.TRACEROUTE_APP_VALUE), ) { request, response -> _state.update { state -> state.copy( diff --git a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt index 2497e8dcc..59a74881a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt @@ -22,48 +22,51 @@ import com.geeksville.mesh.MeshProtos.RouteDiscovery import com.geeksville.mesh.Portnums val MeshProtos.MeshPacket.fullRouteDiscovery: RouteDiscovery? - get() = with(decoded) { - if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) { - runCatching { RouteDiscovery.parseFrom(payload).toBuilder() }.getOrNull()?.apply { - val fullRoute = listOf(to) + routeList + from - clearRoute() - addAllRoute(fullRoute) + get() = + with(decoded) { + if (hasDecoded() && !wantResponse && portnum == Portnums.PortNum.TRACEROUTE_APP) { + runCatching { RouteDiscovery.parseFrom(payload).toBuilder() } + .getOrNull() + ?.apply { + val fullRoute = listOf(to) + routeList + from + clearRoute() + addAllRoute(fullRoute) - val fullRouteBack = listOf(from) + routeBackList + to - clearRouteBack() - if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid - addAllRouteBack(fullRouteBack) - } - }?.build() - } else { - null + val fullRouteBack = listOf(from) + routeBackList + to + clearRouteBack() + if (hopStart > 0 && snrBackCount > 0) { // otherwise back route is invalid + addAllRouteBack(fullRouteBack) + } + } + ?.build() + } else { + null + } } - } @Suppress("MagicNumber") private fun formatTraceroutePath(nodesList: List, snrList: List): String { // nodesList should include both origin and destination nodes // origin will not have an SNR value, but destination should - val snrStr = if (snrList.size == nodesList.size - 1) { - snrList - } else { - // use unknown SNR for entire route if snrList has invalid size - List(nodesList.size - 1) { -128 } - }.map { snr -> - val str = if (snr == -128) "?" else "${snr / 4f}" - "⇊ $str dB" - } + val snrStr = + if (snrList.size == nodesList.size - 1) { + snrList + } else { + // use unknown SNR for entire route if snrList has invalid size + List(nodesList.size - 1) { -128 } + } + .map { snr -> + val str = if (snr == -128) "?" else "${snr / 4f}" + "⇊ $str dB" + } - return nodesList.map { userName -> - "■ $userName" - }.flatMapIndexed { i, nodeStr -> - if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr) - }.joinToString("\n") + return nodesList + .map { userName -> "■ $userName" } + .flatMapIndexed { i, nodeStr -> if (i == 0) listOf(nodeStr) else listOf(snrStr[i - 1], nodeStr) } + .joinToString("\n") } -private fun RouteDiscovery.getTracerouteResponse( - getUser: (nodeNum: Int) -> String, -): String = buildString { +private fun RouteDiscovery.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String = buildString { if (routeList.isNotEmpty()) { append("Route traced toward destination:\n\n") append(formatTraceroutePath(routeList.map(getUser), snrTowardsList)) @@ -75,6 +78,10 @@ private fun RouteDiscovery.getTracerouteResponse( } } -fun MeshProtos.MeshPacket.getTracerouteResponse( - getUser: (nodeNum: Int) -> String, -): String? = fullRouteDiscovery?.getTracerouteResponse(getUser) +fun MeshProtos.MeshPacket.getTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = + fullRouteDiscovery?.getTracerouteResponse(getUser) + +/** Returns a traceroute response string only when the result is complete (both directions). */ +fun MeshProtos.MeshPacket.getFullTracerouteResponse(getUser: (nodeNum: Int) -> String): String? = fullRouteDiscovery + ?.takeIf { it.routeList.isNotEmpty() && it.routeBackList.isNotEmpty() } + ?.getTracerouteResponse(getUser) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index fe6d4151c..3874b2e25 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -73,7 +73,7 @@ import com.geeksville.mesh.fromRadio import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.NO_DEVICE_SELECTED import com.geeksville.mesh.model.Node -import com.geeksville.mesh.model.getTracerouteResponse +import com.geeksville.mesh.model.getFullTracerouteResponse import com.geeksville.mesh.position import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.location.LocationRepository @@ -148,6 +148,8 @@ class MeshService : @Inject lateinit var meshPrefs: MeshPrefs + private val tracerouteStartTimes = ConcurrentHashMap() + companion object : Logging { // Intents broadcast by MeshService @@ -848,7 +850,21 @@ class MeshService : } Portnums.PortNum.TRACEROUTE_APP_VALUE -> { - radioConfigRepository.setTracerouteResponse(packet.getTracerouteResponse(::getUserName)) + val full = packet.getFullTracerouteResponse(::getUserName) + if (full != null) { + val requestId = packet.decoded.requestId + val start = tracerouteStartTimes.remove(requestId) + val response = + if (start != null) { + val elapsedMs = System.currentTimeMillis() - start + val seconds = elapsedMs / 1000.0 + info("Traceroute $requestId complete in $seconds s") + "$full\n\nDuration: ${"%.1f".format(seconds)} s" + } else { + full + } + radioConfigRepository.setTracerouteResponse(response) + } } else -> debug("No custom processing needed for ${data.portnumValue}") @@ -2376,6 +2392,7 @@ class MeshService : } override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions { + tracerouteStartTimes[requestId] = System.currentTimeMillis() packetHandler.sendToRadio( newMeshPacketTo(destNum).buildMeshPacket( wantAck = true, diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt index 24577db71..8947d40ca 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt @@ -63,6 +63,7 @@ import com.geeksville.mesh.model.fullRouteDiscovery import com.geeksville.mesh.model.getTracerouteResponse import com.geeksville.mesh.ui.common.components.SimpleAlertDialog import com.geeksville.mesh.ui.common.theme.AppTheme +import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import java.text.DateFormat @OptIn(ExperimentalFoundationApi::class) @@ -88,9 +89,9 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod items(state.tracerouteRequests, key = { it.uuid }) { log -> val result = remember(state.tracerouteRequests) { - state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id } + state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == log.fromRadio.packet.id } } - val route = remember(result) { result?.fullRouteDiscovery } + val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery } val time = dateFormat.format(log.received_date) val (text, icon) = route.getTextAndIcon() @@ -103,7 +104,15 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod modifier = Modifier.combinedClickable(onLongClick = { expanded = true }) { if (result != null) { - showDialog = result.getTracerouteResponse(::getUsername) + val full = route + if (full != null && full.routeList.isNotEmpty() && full.routeBackList.isNotEmpty()) { + val elapsedMs = (result.received_date - log.received_date).coerceAtLeast(0) + val seconds = elapsedMs.toDouble() / MS_PER_SEC + val base = result.fromRadio.packet.getTracerouteResponse(::getUsername) + showDialog = "$base\n\nDuration: ${"%.1f".format(seconds)} s" + } else { + showDialog = result.fromRadio.packet.getTracerouteResponse(::getUsername) + } } }, )