mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-05 05:12:51 -05:00
Revert "Revert "Feat/1919 pax graphs"" (#2480)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -47,6 +47,7 @@ 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
|
||||
@@ -88,6 +89,7 @@ data class MetricsState(
|
||||
val isLocalDevice: Boolean = false,
|
||||
val latestStableFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val latestAlphaFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val paxMetrics: List<MeshLog> = emptyList(),
|
||||
) {
|
||||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||
@@ -95,6 +97,7 @@ data class MetricsState(
|
||||
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
||||
fun hasHostMetrics() = hostMetrics.isNotEmpty()
|
||||
fun hasPaxMetrics() = paxMetrics.isNotEmpty()
|
||||
|
||||
fun deviceMetricsFiltered(timeFrame: TimeFrame): List<Telemetry> {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
@@ -264,7 +267,7 @@ class MetricsViewModel @Inject constructor(
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
init {
|
||||
destNum?.let {
|
||||
if (destNum != null) {
|
||||
radioConfigRepository.nodeDBbyNum
|
||||
.mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
@@ -344,6 +347,12 @@ 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)
|
||||
@@ -357,6 +366,8 @@ class MetricsViewModel @Inject constructor(
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
debug("MetricsViewModel created")
|
||||
} else {
|
||||
debug("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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
|
||||
@@ -42,6 +43,7 @@ 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
|
||||
@@ -86,6 +88,9 @@ sealed class NodeDetailRoutes {
|
||||
|
||||
@Serializable
|
||||
data object HostMetricsLog : Route
|
||||
|
||||
@Serializable
|
||||
data object PaxMetrics : Route
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.nodesGraph(
|
||||
@@ -161,6 +166,7 @@ fun NavGraphBuilder.nodeDetailGraph(
|
||||
|
||||
NodeDetailRoute.POWER -> PowerMetricsScreen(hiltViewModel(parentEntry))
|
||||
NodeDetailRoute.HOST -> HostMetricsLogScreen(hiltViewModel(parentEntry))
|
||||
NodeDetailRoute.PAX -> PaxMetricsScreen(hiltViewModel(parentEntry))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,4 +186,5 @@ 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),
|
||||
}
|
||||
|
||||
@@ -795,6 +795,7 @@ private fun DecodedPayloadBlock(
|
||||
isSelected: Boolean,
|
||||
colorScheme: ColorScheme
|
||||
) {
|
||||
|
||||
val commonTextStyle = TextStyle(
|
||||
fontSize = if (isSelected) 10.sp else 8.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
||||
346
app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
Normal file
346
app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
Normal file
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Pair<Int, Int>>,
|
||||
bleSeries: List<Pair<Int, Int>>,
|
||||
wifiSeries: List<Pair<Int, Int>>,
|
||||
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<Pair<Int, Int>>, 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<Byte>()
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ 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
|
||||
@@ -175,6 +176,7 @@ 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")
|
||||
@@ -202,6 +204,7 @@ fun NodeDetailScreen(
|
||||
state.hasPowerMetrics(),
|
||||
state.hasTracerouteLogs(),
|
||||
state.hasHostMetrics(),
|
||||
state.hasPaxMetrics(), // Added for PAX log
|
||||
)
|
||||
}
|
||||
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -790,6 +790,11 @@
|
||||
<string name="clear_selection">Clear selection</string>
|
||||
<string name="message_input_label">Message</string>
|
||||
<string name="type_a_message">Type a message</string>
|
||||
<string name="pax_metrics_log">PAX Metrics Log</string>
|
||||
<string name="pax">PAX</string>
|
||||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user