diff --git a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt index 15b3b7d29..f4f38c5a3 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt @@ -42,12 +42,6 @@ import com.geeksville.mesh.Portnums.PortNum import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import com.geeksville.mesh.repository.datastore.RadioConfigRepository -import com.google.protobuf.InvalidProtocolBufferException -import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.TelemetryProtos -import com.geeksville.mesh.AdminProtos -import com.geeksville.mesh.PaxcountProtos -import com.geeksville.mesh.StoreAndForwardProtos data class SearchMatch( val logIndex: Int, @@ -162,7 +156,6 @@ class LogFilterManager { } } -@Suppress("TooManyFunctions") @HiltViewModel class DebugViewModel @Inject constructor( private val meshLogRepository: MeshLogRepository, @@ -213,7 +206,6 @@ class DebugViewModel @Inject constructor( messageType = log.message_type, formattedReceivedDate = TIME_FORMAT.format(log.received_date), logMessage = annotateMeshLogMessage(log), - decodedPayload = decodePayloadFromMeshLog(log), ) }.toImmutableList() @@ -221,33 +213,22 @@ class DebugViewModel @Inject constructor( * Transform the input [MeshLog] by enhancing the raw message with annotations. */ private fun annotateMeshLogMessage(meshLog: MeshLog): String { - return when (meshLog.message_type) { + val annotated = when (meshLog.message_type) { "Packet" -> meshLog.meshPacket?.let { packet -> - annotatePacketLog(packet) - } ?: meshLog.raw_message + annotateRawMessage(meshLog.raw_message, packet.from, packet.to) + } + "NodeInfo" -> meshLog.nodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.num) - } ?: meshLog.raw_message + } + "MyNodeInfo" -> meshLog.myNodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum) - } ?: meshLog.raw_message - else -> meshLog.raw_message - } - } + } - private fun annotatePacketLog(packet: MeshProtos.MeshPacket): String { - val builder = packet.toBuilder() - val hasDecoded = builder.hasDecoded() - val decoded = if (hasDecoded) builder.decoded else null - if (hasDecoded) builder.clearDecoded() - val baseText = builder.build().toString().trimEnd() - val result = if (hasDecoded && decoded != null) { - val decodedText = decoded.toString().trimEnd().prependIndent(" ") - "$baseText\ndecoded {\n$decodedText\n}" - } else { - baseText + else -> null } - return annotateRawMessage(result, packet.from, packet.to) + return annotated ?: meshLog.raw_message } /** @@ -293,7 +274,6 @@ class DebugViewModel @Inject constructor( val messageType: String, val formattedReceivedDate: String, val logMessage: String, - val decodedPayload: String? = null, ) companion object { @@ -315,54 +295,4 @@ class DebugViewModel @Inject constructor( } fun setSelectedLogId(id: String?) { _selectedLogId.value = id } - - /** - * Attempts to fully decode the payload of a MeshLog's MeshPacket using the appropriate protobuf definition, - * based on the portnum of the packet. - * - * For known portnums, the payload is parsed into its corresponding proto message and returned as a string. - * For text and alert messages, the payload is interpreted as UTF-8 text. - * For unknown portnums, the payload is shown as a hex string. - * - * @param log The MeshLog containing the packet and payload to decode. - * @return A human-readable string representation of the decoded payload, or an error message if decoding fails, - * or null if the log does not contain a decodable packet. - */ - private fun decodePayloadFromMeshLog(log: MeshLog): String? { - var result: String? = null - val packet = log.meshPacket - if (packet == null || !packet.hasDecoded()) { - result = null - } else { - val portnum = packet.decoded.portnumValue - val payload = packet.decoded.payload.toByteArray() - result = try { - when (portnum) { - PortNum.TEXT_MESSAGE_APP_VALUE, - PortNum.ALERT_APP_VALUE -> - payload.toString(Charsets.UTF_8) - PortNum.POSITION_APP_VALUE -> - MeshProtos.Position.parseFrom(payload).toString() - PortNum.WAYPOINT_APP_VALUE -> - MeshProtos.Waypoint.parseFrom(payload).toString() - PortNum.NODEINFO_APP_VALUE -> - MeshProtos.User.parseFrom(payload).toString() - PortNum.TELEMETRY_APP_VALUE -> - TelemetryProtos.Telemetry.parseFrom(payload).toString() - PortNum.ROUTING_APP_VALUE -> - MeshProtos.Routing.parseFrom(payload).toString() - PortNum.ADMIN_APP_VALUE -> - AdminProtos.AdminMessage.parseFrom(payload).toString() - PortNum.PAXCOUNTER_APP_VALUE -> - PaxcountProtos.Paxcount.parseFrom(payload).toString() - PortNum.STORE_FORWARD_APP_VALUE -> - StoreAndForwardProtos.StoreAndForward.parseFrom(payload).toString() - else -> payload.joinToString(" ") { "%02x".format(it) } - } - } catch (e: InvalidProtocolBufferException) { - "Failed to decode payload: ${e.message}" - } - } - return result - } } 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 10c9cbde4..f53bde640 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt @@ -47,7 +47,6 @@ import com.geeksville.mesh.repository.api.FirmwareReleaseRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.service.ServiceAction import com.geeksville.mesh.ui.map.MAP_STYLE_ID -import com.geeksville.mesh.Portnums import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -89,7 +88,6 @@ data class MetricsState( val isLocalDevice: Boolean = false, val latestStableFirmware: FirmwareRelease = FirmwareRelease(), val latestAlphaFirmware: FirmwareRelease = FirmwareRelease(), - val paxMetrics: List = emptyList(), ) { fun hasDeviceMetrics() = deviceMetrics.isNotEmpty() fun hasSignalMetrics() = signalMetrics.isNotEmpty() @@ -97,7 +95,6 @@ data class MetricsState( fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty() fun hasPositionLogs() = positionLogs.isNotEmpty() fun hasHostMetrics() = hostMetrics.isNotEmpty() - fun hasPaxMetrics() = paxMetrics.isNotEmpty() fun deviceMetricsFiltered(timeFrame: TimeFrame): List { val oldestTime = timeFrame.calculateOldestTime() @@ -267,7 +264,7 @@ class MetricsViewModel @Inject constructor( val timeFrame: StateFlow = _timeFrame init { - if (destNum != null) { + destNum?.let { radioConfigRepository.nodeDBbyNum .mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() } .distinctUntilChanged() @@ -347,12 +344,6 @@ class MetricsViewModel @Inject constructor( } }.launchIn(viewModelScope) - meshLogRepository.getLogsFrom(destNum, Portnums.PortNum.PAXCOUNTER_APP_VALUE).onEach { logs -> - _state.update { state -> - state.copy(paxMetrics = logs) - } - }.launchIn(viewModelScope) - firmwareReleaseRepository.stableRelease.filterNotNull().onEach { latestStable -> _state.update { state -> state.copy(latestStableFirmware = latestStable) @@ -366,8 +357,6 @@ class MetricsViewModel @Inject constructor( }.launchIn(viewModelScope) debug("MetricsViewModel created") - } else { - debug("MetricsViewModel: destNum is null, skipping metrics flows initialization.") } } diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt index f1fca4905..02eb2e06d 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt @@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Memory import androidx.compose.material.icons.filled.PermScanWifi -import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.Power import androidx.compose.material.icons.filled.Router import androidx.compose.runtime.remember @@ -43,7 +42,6 @@ import com.geeksville.mesh.ui.metrics.PositionLogScreen import com.geeksville.mesh.ui.metrics.PowerMetricsScreen import com.geeksville.mesh.ui.metrics.SignalMetricsScreen import com.geeksville.mesh.ui.metrics.TracerouteLogScreen -import com.geeksville.mesh.ui.metrics.PaxMetricsScreen import com.geeksville.mesh.ui.node.NodeDetailScreen import com.geeksville.mesh.ui.node.NodeMapScreen import com.geeksville.mesh.ui.node.NodeScreen @@ -88,9 +86,6 @@ sealed class NodeDetailRoutes { @Serializable data object HostMetricsLog : Route - - @Serializable - data object PaxMetrics : Route } fun NavGraphBuilder.nodesGraph( @@ -166,7 +161,6 @@ fun NavGraphBuilder.nodeDetailGraph( NodeDetailRoute.POWER -> PowerMetricsScreen(hiltViewModel(parentEntry)) NodeDetailRoute.HOST -> HostMetricsLogScreen(hiltViewModel(parentEntry)) - NodeDetailRoute.PAX -> PaxMetricsScreen(hiltViewModel(parentEntry)) } } } @@ -186,5 +180,4 @@ enum class NodeDetailRoute( TRACEROUTE(R.string.traceroute, NodeDetailRoutes.TracerouteLog, Icons.Default.PermScanWifi), POWER(R.string.power, NodeDetailRoutes.PowerMetrics, Icons.Default.Power), HOST(R.string.host, NodeDetailRoutes.HostMetricsLog, Icons.Default.Memory), - PAX(R.string.pax, NodeDetailRoutes.PaxMetrics, Icons.Default.People), } diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index bf8cf1649..fda133ed8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -237,14 +237,6 @@ internal fun DebugItem( color = colorScheme.onSurface ) ) - // Show decoded payload if available - if (!log.decodedPayload.isNullOrBlank()) { - DecodedPayloadBlock( - decodedPayload = log.decodedPayload, - isSelected = isSelected, - colorScheme = colorScheme - ) - } } } } @@ -788,48 +780,3 @@ private suspend fun exportAllLogs(context: Context, logs: List) = wit warn("Error:IOException: " + e.toString()) } } - -@Composable -private fun DecodedPayloadBlock( - decodedPayload: String, - isSelected: Boolean, - colorScheme: ColorScheme -) { - Text( - text = stringResource(id = R.string.debug_decoded_payload), - style = TextStyle( - fontSize = if (isSelected) 10.sp else 8.sp, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) - ) - Text( - text = "{", - style = TextStyle( - fontSize = if (isSelected) 10.sp else 8.sp, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - modifier = Modifier.padding(start = 8.dp, bottom = 2.dp) - ) - Text( - text = decodedPayload, - softWrap = true, - style = TextStyle( - fontSize = if (isSelected) 10.sp else 8.sp, - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.8f) - ), - modifier = Modifier.padding(start = 16.dp, bottom = 0.dp) - ) - Text( - text = "}", - style = TextStyle( - fontSize = if (isSelected) 10.sp else 8.sp, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - modifier = Modifier.padding(start = 8.dp, bottom = 4.dp) - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt deleted file mode 100644 index 6a6bea820..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (c) 2025 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 . - */ - -package com.geeksville.mesh.ui.metrics - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.PaxcountProtos -import com.geeksville.mesh.R -import com.geeksville.mesh.database.entity.MeshLog -import com.geeksville.mesh.model.MetricsViewModel -import com.geeksville.mesh.util.formatUptime -import com.geeksville.mesh.Portnums.PortNum -import java.text.DateFormat -import java.util.Date -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.remember -import com.geeksville.mesh.model.TimeFrame -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import com.geeksville.mesh.ui.common.components.OptionLabel -import com.geeksville.mesh.ui.common.components.SlidingSelector -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.rememberScrollState -import androidx.compose.ui.Alignment -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer - -private const val CHART_WEIGHT = 1f -private const val Y_AXIS_WEIGHT = 0.1f -private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT + Y_AXIS_WEIGHT) - -private enum class PaxSeries(val color: Color, val legendRes: Int) { - PAX(Color.Black, R.string.pax), - BLE(Color.Cyan, R.string.ble_devices), - WIFI(Color.Green, R.string.wifi_devices) -} - -@Suppress("LongMethod") -@Composable -private fun PaxMetricsChart( - modifier: Modifier = Modifier, - totalSeries: List>, - bleSeries: List>, - wifiSeries: List>, - minValue: Float, - maxValue: Float, - timeFrame: TimeFrame, -) { - if (totalSeries.isEmpty()) return - val scrollState = rememberScrollState() - val screenWidth = LocalWindowInfo.current.containerSize.width - val times = totalSeries.map { it.first } - val minTime = times.minOrNull() ?: 0 - val maxTime = times.maxOrNull() ?: 1 - val timeDiff = maxTime - minTime - val dp = remember(timeFrame, screenWidth, timeDiff) { - timeFrame.dp(screenWidth, time = timeDiff.toLong()) - } - // Calculate visible time range based on scroll position and chart width - val visibleTimeRange = run { - val totalWidthPx = with(LocalDensity.current) { dp.toPx() } - val scrollPx = scrollState.value.toFloat() - val visibleWidthPx = screenWidth * CHART_WIDTH_RATIO - val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f) - val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f) - val visibleOldest = minTime + (timeDiff * leftRatio).toInt() - val visibleNewest = minTime + (timeDiff * rightRatio).toInt() - visibleOldest to visibleNewest - } - TimeLabels( - oldest = visibleTimeRange.first, - newest = visibleTimeRange.second - ) - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.33f) - ) { - YAxisLabels( - modifier = Modifier - .weight(Y_AXIS_WEIGHT) - .fillMaxHeight() - .padding(start = 8.dp), - - labelColor = MaterialTheme.colorScheme.onSurface, - minValue = minValue, - maxValue = maxValue - ) - Box( - contentAlignment = Alignment.TopStart, - modifier = Modifier - .horizontalScroll(state = scrollState, reverseScrolling = true) - .weight(CHART_WEIGHT) - ) { - HorizontalLinesOverlay( - modifier.width(dp), - lineColors = List(size = 5) { Color.LightGray }, - ) - TimeAxisOverlay( - modifier.width(dp), - oldest = minTime, - newest = maxTime, - timeFrame.lineInterval() - ) - Canvas(modifier = Modifier.width(dp).fillMaxHeight()) { - val width = size.width - val height = size.height - fun xForTime(t: Int): Float = - if (maxTime == minTime) width / 2 else (t - minTime).toFloat() / (maxTime - minTime) * width - fun yForValue(v: Int): Float = height - (v - minValue) / (maxValue - minValue) * height - fun drawLine(series: List>, color: Color) { - for (i in 1 until series.size) { - drawLine( - color = color, - start = Offset(xForTime(series[i - 1].first), yForValue(series[i - 1].second)), - end = Offset(xForTime(series[i].first), yForValue(series[i].second)), - strokeWidth = 2.dp.toPx() - ) - } - } - drawLine(bleSeries, PaxSeries.BLE.color) - drawLine(wifiSeries, PaxSeries.WIFI.color) - drawLine(totalSeries, PaxSeries.PAX.color) - } - } - YAxisLabels( - modifier = Modifier - .weight(Y_AXIS_WEIGHT) - .fillMaxHeight() - .padding(end = 8.dp), - labelColor = MaterialTheme.colorScheme.onSurface, - minValue = minValue, - maxValue = maxValue - ) - } - Spacer(modifier = Modifier.height(16.dp)) -} - -@Composable -@Suppress("MagicNumber", "LongMethod") -fun PaxMetricsScreen( - metricsViewModel: MetricsViewModel = hiltViewModel(), -) { - val state by metricsViewModel.state.collectAsStateWithLifecycle() - val dateFormat = DateFormat.getDateTimeInstance() - var timeFrame by remember { mutableStateOf(TimeFrame.TWENTY_FOUR_HOURS) } - // Only show logs that can be decoded as PaxcountProtos.Paxcount - val paxMetrics = state.paxMetrics.mapNotNull { log -> - val pax = decodePaxFromLog(log) - if (pax != null) { - Pair(log, pax) - } else { - null - } - } - // Prepare data for graph - val oldestTime = timeFrame.calculateOldestTime() - val graphData = paxMetrics.filter { it.first.received_date / 1000 >= oldestTime } - .map { - val t = (it.first.received_date / 1000).toInt() - Triple(t, it.second.ble, it.second.wifi) - } - .sortedBy { it.first } - val totalSeries = graphData.map { it.first to (it.second + it.third) } - val bleSeries = graphData.map { it.first to it.second } - val wifiSeries = graphData.map { it.first to it.third } - val maxValue = (totalSeries.maxOfOrNull { it.second } ?: 1).toFloat().coerceAtLeast(1f) - val minValue = 0f - val legendData = listOf( - LegendData(PaxSeries.PAX.legendRes, PaxSeries.PAX.color), - LegendData(PaxSeries.BLE.legendRes, PaxSeries.BLE.color), - LegendData(PaxSeries.WIFI.legendRes, PaxSeries.WIFI.color), - ) - - Column(modifier = Modifier.fillMaxSize()) { - // Time frame selector - SlidingSelector( - options = TimeFrame.entries.toList(), - selectedOption = timeFrame, - onOptionSelected = { timeFrame = it } - ) { tf: TimeFrame -> - OptionLabel(stringResource(tf.strRes)) - } - // Graph - if (graphData.isNotEmpty()) { - ChartHeader(graphData.size) - Legend(legendData = legendData) - PaxMetricsChart( - totalSeries = totalSeries, - bleSeries = bleSeries, - wifiSeries = wifiSeries, - minValue = minValue, - maxValue = maxValue, - timeFrame = timeFrame - ) - } - // List - if (paxMetrics.isEmpty()) { - Text( - text = stringResource(R.string.no_pax_metrics_logs), - modifier = Modifier.fillMaxSize().padding(16.dp), - textAlign = TextAlign.Center - ) - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - items(paxMetrics) { (log, pax) -> - PaxMetricsItem(log, pax, dateFormat) - } - } - } - } -} - -@Suppress("MagicNumber", "CyclomaticComplexMethod") -fun decodePaxFromLog(log: MeshLog): PaxcountProtos.Paxcount? { - var result: PaxcountProtos.Paxcount? = null - // First, try to parse from the binary fromRadio field (robust, like telemetry) - try { - val packet = log.fromRadio.packet - if (packet != null && packet.hasDecoded() && - packet.decoded.portnumValue == PortNum.PAXCOUNTER_APP_VALUE) { - val pax = PaxcountProtos.Paxcount.parseFrom(packet.decoded.payload) - if (pax.ble != 0 || pax.wifi != 0 || pax.uptime != 0) result = pax - } - } catch (e: com.google.protobuf.InvalidProtocolBufferException) { - android.util.Log.e("PaxMetrics", "Failed to parse Paxcount from binary data", e) - } catch (e: IllegalArgumentException) { - android.util.Log.e("PaxMetrics", "Invalid argument while parsing Paxcount from binary data", e) - } - // Fallback: Try direct base64 or bytes from raw_message - if (result == null) { - try { - val base64 = log.raw_message.trim() - if (base64.matches(Regex("^[A-Za-z0-9+/=\r\n]+$"))) { - val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT) - val pax = PaxcountProtos.Paxcount.parseFrom(bytes) - result = pax - } else if (base64.matches(Regex("^[0-9a-fA-F]+$")) && base64.length % 2 == 0) { - val bytes = base64.chunked(2).map { it.toInt(16).toByte() }.toByteArray() - val pax = PaxcountProtos.Paxcount.parseFrom(bytes) - result = pax - } - } catch (e: IllegalArgumentException) { - android.util.Log.e("PaxMetrics", "Invalid Base64 or hex input", e) - } catch (e: com.google.protobuf.InvalidProtocolBufferException) { - android.util.Log.e("PaxMetrics", "Failed to parse Paxcount from decoded data", e) - } - } - return result -} - -@Suppress("MagicNumber") -fun unescapeProtoString(escaped: String): ByteArray { - val out = mutableListOf() - var i = 0 - while (i < escaped.length) { - if (escaped[i] == '\\' && i + 3 < escaped.length && escaped[i + 1].isDigit()) { - // Octal escape: \\ddd - val octal = escaped.substring(i + 1, i + 4) - out.add(octal.toInt(8).toByte()) - i += 4 - } else { - out.add(escaped[i].code.toByte()) - i++ - } - } - return out.toByteArray() -} - -@Composable -fun PaxMetricsItem(log: MeshLog, pax: PaxcountProtos.Paxcount, dateFormat: DateFormat) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Text( - text = dateFormat.format(Date(log.received_date)), - style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold), - textAlign = TextAlign.End, - modifier = Modifier.fillMaxWidth() - ) - val total = pax.ble + pax.wifi - val summary = "PAX: $total (B:${pax.ble} W:${pax.wifi})" - Row( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = summary, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f, fill = true) - ) - Text( - text = stringResource(R.string.uptime) + ": " + formatUptime(pax.uptime), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.End, - modifier = Modifier.alignByBaseline() - ) - } - } -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt index a159d6019..0d326a58e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt @@ -53,7 +53,6 @@ import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Map import androidx.compose.material.icons.filled.Memory import androidx.compose.material.icons.filled.Numbers -import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Power import androidx.compose.material.icons.filled.Route @@ -176,7 +175,6 @@ private enum class LogsType( POWER(R.string.power_metrics_log, Icons.Default.Power, NodeDetailRoutes.PowerMetrics), TRACEROUTE(R.string.traceroute_log, Icons.Default.Route, NodeDetailRoutes.TracerouteLog), HOST(R.string.host_metrics_log, Icons.Default.Memory, NodeDetailRoutes.HostMetricsLog), - PAX(R.string.pax_metrics_log, Icons.Default.People, NodeDetailRoutes.PaxMetrics), } @Suppress("LongMethod") @@ -204,7 +202,6 @@ fun NodeDetailScreen( state.hasPowerMetrics(), state.hasTracerouteLogs(), state.hasHostMetrics(), - state.hasPaxMetrics(), // Added for PAX log ) } val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf93013ef..6b36e3489 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,7 +165,6 @@ Text messages This Channel URL is invalid and can not be used Debug Panel - Decoded Payload: Export Logs 500 last messages Filters @@ -790,9 +789,5 @@ Clear selection Message Type a message - PAX Metrics Log - PAX - No PAX metrics logs available. - WiFi Devices - BLE Devices +