mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Run spotless ahead of 2523 to make the diff easier (#2571)
This commit is contained in:
parent
64ead16d83
commit
d336f23486
6 changed files with 282 additions and 483 deletions
|
|
@ -57,14 +57,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_MINUTE_FORMAT
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.MAX_PERCENT_VALUE
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_MINUTE_FORMAT
|
||||
import java.text.DateFormat
|
||||
|
||||
object CommonCharts {
|
||||
val DATE_TIME_FORMAT: DateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
val DATE_TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
val TIME_MINUTE_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
val DATE_TIME_MINUTE_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||
const val MS_PER_SEC = 1000L
|
||||
|
|
@ -86,13 +85,13 @@ fun ChartHeader(amount: Int) {
|
|||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "$amount ${stringResource(R.string.logs)}",
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,14 +102,10 @@ fun ChartHeader(amount: Int) {
|
|||
* @param lineColors A list of 5 [Color]s for the chart lines, 0 being the lowest line on the chart.
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalLinesOverlay(
|
||||
modifier: Modifier,
|
||||
lineColors: List<Color>,
|
||||
) {
|
||||
fun HorizontalLinesOverlay(modifier: Modifier, lineColors: List<Color>) {
|
||||
/* 100 is a good number to divide into quarters */
|
||||
val verticalSpacing = MAX_PERCENT_VALUE / LINE_LIMIT
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val lineStart = 0f
|
||||
val height = size.height
|
||||
val width = size.width
|
||||
|
|
@ -125,72 +120,51 @@ fun HorizontalLinesOverlay(
|
|||
color = lineColors[i],
|
||||
strokeWidth = 1.dp.toPx(),
|
||||
cap = StrokeCap.Round,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f)
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f),
|
||||
)
|
||||
lineY += verticalSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws labels on the Y-axis with respect to the range. Defined by (`maxValue` - `minValue`).
|
||||
*/
|
||||
/** Draws labels on the Y-axis with respect to the range. Defined by (`maxValue` - `minValue`). */
|
||||
@Composable
|
||||
fun YAxisLabels(
|
||||
modifier: Modifier,
|
||||
labelColor: Color,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
) {
|
||||
fun YAxisLabels(modifier: Modifier, labelColor: Color, minValue: Float, maxValue: Float) {
|
||||
val range = maxValue - minValue
|
||||
val verticalSpacing = range / LINE_LIMIT
|
||||
val density = LocalDensity.current
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val height = size.height
|
||||
|
||||
/* Y Labels */
|
||||
val textPaint = Paint().apply {
|
||||
color = labelColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = density.run { 12.dp.toPx() }
|
||||
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||
alpha = TEXT_PAINT_ALPHA
|
||||
}
|
||||
val textPaint =
|
||||
Paint().apply {
|
||||
color = labelColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = density.run { 12.dp.toPx() }
|
||||
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||
alpha = TEXT_PAINT_ALPHA
|
||||
}
|
||||
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
var label = minValue
|
||||
repeat(LINE_LIMIT + 1) {
|
||||
val ratio = (label - minValue) / range
|
||||
val y = height - (ratio * height)
|
||||
drawText(
|
||||
"${label.toInt()}",
|
||||
0f,
|
||||
y + 4.dp.toPx(),
|
||||
textPaint
|
||||
)
|
||||
drawText("${label.toInt()}", 0f, y + 4.dp.toPx(), textPaint)
|
||||
label += verticalSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the vertical lines to help the user relate the plotted data within a time frame.
|
||||
*/
|
||||
/** Draws the vertical lines to help the user relate the plotted data within a time frame. */
|
||||
@Composable
|
||||
fun TimeAxisOverlay(
|
||||
modifier: Modifier,
|
||||
oldest: Int,
|
||||
newest: Int,
|
||||
timeInterval: Long
|
||||
) {
|
||||
|
||||
fun TimeAxisOverlay(modifier: Modifier, oldest: Int, newest: Int, timeInterval: Long) {
|
||||
val range = newest - oldest
|
||||
val density = LocalDensity.current
|
||||
val lineColor = MaterialTheme.colorScheme.onSurface
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val height = size.height
|
||||
val width = size.width
|
||||
|
||||
|
|
@ -200,13 +174,14 @@ fun TimeAxisOverlay(
|
|||
current -= timeRemaining
|
||||
current += timeInterval
|
||||
|
||||
val textPaint = Paint().apply {
|
||||
color = lineColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = density.run { 12.dp.toPx() }
|
||||
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||
alpha = TEXT_PAINT_ALPHA
|
||||
}
|
||||
val textPaint =
|
||||
Paint().apply {
|
||||
color = lineColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = density.run { 12.dp.toPx() }
|
||||
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||
alpha = TEXT_PAINT_ALPHA
|
||||
}
|
||||
|
||||
/* Vertical Lines with labels */
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
|
|
@ -219,39 +194,22 @@ fun TimeAxisOverlay(
|
|||
color = lineColor,
|
||||
strokeWidth = 1.dp.toPx(),
|
||||
cap = StrokeCap.Round,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f)
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f),
|
||||
)
|
||||
|
||||
/* Time */
|
||||
drawText(
|
||||
TIME_FORMAT.format(current * MS_PER_SEC),
|
||||
x,
|
||||
0f,
|
||||
textPaint
|
||||
)
|
||||
drawText(TIME_FORMAT.format(current * MS_PER_SEC), x, 0f, textPaint)
|
||||
/* Date */
|
||||
drawText(
|
||||
DATE_FORMAT.format(current * MS_PER_SEC),
|
||||
x,
|
||||
DATE_Y,
|
||||
textPaint
|
||||
)
|
||||
drawText(DATE_FORMAT.format(current * MS_PER_SEC), x, DATE_Y, textPaint)
|
||||
current += timeInterval
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the `oldest` and `newest` times for the respective telemetry data.
|
||||
* Expects time in seconds.
|
||||
*/
|
||||
/** Draws the `oldest` and `newest` times for the respective telemetry data. Expects time in seconds. */
|
||||
@Composable
|
||||
fun TimeLabels(
|
||||
oldest: Int,
|
||||
newest: Int,
|
||||
) {
|
||||
|
||||
fun TimeLabels(oldest: Int, newest: Int) {
|
||||
Row {
|
||||
Text(
|
||||
text = DATE_TIME_MINUTE_FORMAT.format(oldest * MS_PER_SEC),
|
||||
|
|
@ -264,7 +222,7 @@ fun TimeLabels(
|
|||
text = DATE_TIME_MINUTE_FORMAT.format(newest * MS_PER_SEC),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = 12.sp
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -276,22 +234,11 @@ fun TimeLabels(
|
|||
* @param promptInfoDialog Executes when the user presses the info icon.
|
||||
*/
|
||||
@Composable
|
||||
fun Legend(
|
||||
legendData: List<LegendData>,
|
||||
displayInfoIcon: Boolean = true,
|
||||
promptInfoDialog: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
fun Legend(legendData: List<LegendData>, displayInfoIcon: Boolean = true, promptInfoDialog: () -> Unit = {}) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
legendData.forEachIndexed { index, data ->
|
||||
LegendLabel(
|
||||
text = stringResource(data.nameRes),
|
||||
color = data.color,
|
||||
isLine = data.isLine
|
||||
)
|
||||
LegendLabel(text = stringResource(data.nameRes), color = data.color, isLine = data.isLine)
|
||||
|
||||
if (index != legendData.lastIndex) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
|
@ -302,7 +249,7 @@ fun Legend(
|
|||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
modifier = Modifier.clickable { promptInfoDialog() },
|
||||
contentDescription = stringResource(R.string.info)
|
||||
contentDescription = stringResource(R.string.info),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -320,11 +267,7 @@ fun Legend(
|
|||
fun LegendInfoDialog(pairedRes: List<Pair<Int, Int>>, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.info),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(text = stringResource(R.string.info), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
|
|
@ -332,32 +275,23 @@ fun LegendInfoDialog(pairedRes: List<Pair<Int, Int>>, onDismiss: () -> Unit) {
|
|||
Text(
|
||||
text = stringResource(pair.first),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
Text(
|
||||
text = stringResource(pair.second),
|
||||
style = TextStyle.Default,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
Text(text = stringResource(pair.second), style = TextStyle.Default)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.close))
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.close)) } },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LegendLabel(text: String, color: Color, isLine: Boolean = false) {
|
||||
Canvas(
|
||||
modifier = Modifier.size(4.dp)
|
||||
) {
|
||||
Canvas(modifier = Modifier.size(4.dp)) {
|
||||
if (isLine) {
|
||||
drawLine(
|
||||
color = color,
|
||||
|
|
@ -367,9 +301,7 @@ private fun LegendLabel(text: String, color: Color, isLine: Boolean = false) {
|
|||
cap = StrokeCap.Round,
|
||||
)
|
||||
} else {
|
||||
drawCircle(
|
||||
color = color
|
||||
)
|
||||
drawCircle(color = color)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
|
@ -383,9 +315,10 @@ private fun LegendLabel(text: String, color: Color, isLine: Boolean = false) {
|
|||
@Preview
|
||||
@Composable
|
||||
private fun LegendPreview() {
|
||||
val data = listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = Color.Red),
|
||||
LegendData(nameRes = R.string.snr, color = Color.Green)
|
||||
)
|
||||
val data =
|
||||
listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = Color.Red),
|
||||
LegendData(nameRes = R.string.snr, color = Color.Green),
|
||||
)
|
||||
Legend(legendData = data, promptInfoDialog = {})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,35 @@
|
|||
package com.geeksville.mesh.ui.metrics
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -38,32 +54,16 @@ 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.Portnums.PortNum
|
||||
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
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
private const val CHART_WEIGHT = 1f
|
||||
private const val Y_AXIS_WEIGHT = 0.1f
|
||||
|
|
@ -72,7 +72,7 @@ private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIG
|
|||
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)
|
||||
WIFI(Color.Green, R.string.wifi_devices),
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -93,9 +93,7 @@ private fun PaxMetricsChart(
|
|||
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())
|
||||
}
|
||||
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() }
|
||||
|
|
@ -107,42 +105,21 @@ private fun PaxMetricsChart(
|
|||
val visibleNewest = minTime + (timeDiff * rightRatio).toInt()
|
||||
visibleOldest to visibleNewest
|
||||
}
|
||||
TimeLabels(
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second
|
||||
)
|
||||
TimeLabels(oldest = visibleTimeRange.first, newest = visibleTimeRange.second)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.33f)
|
||||
) {
|
||||
Row(modifier = modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f)) {
|
||||
YAxisLabels(
|
||||
modifier = Modifier
|
||||
.weight(Y_AXIS_WEIGHT)
|
||||
.fillMaxHeight()
|
||||
.padding(start = 8.dp),
|
||||
|
||||
modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(start = 8.dp),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
minValue = minValue,
|
||||
maxValue = maxValue
|
||||
maxValue = maxValue,
|
||||
)
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.horizontalScroll(state = scrollState, reverseScrolling = true)
|
||||
.weight(CHART_WEIGHT)
|
||||
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()
|
||||
)
|
||||
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
|
||||
|
|
@ -155,7 +132,7 @@ private fun PaxMetricsChart(
|
|||
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()
|
||||
strokeWidth = 2.dp.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -165,13 +142,10 @@ private fun PaxMetricsChart(
|
|||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = Modifier
|
||||
.weight(Y_AXIS_WEIGHT)
|
||||
.fillMaxHeight()
|
||||
.padding(end = 8.dp),
|
||||
modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(end = 8.dp),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
minValue = minValue,
|
||||
maxValue = maxValue
|
||||
maxValue = maxValue,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
@ -179,46 +153,48 @@ private fun PaxMetricsChart(
|
|||
|
||||
@Composable
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
fun PaxMetricsScreen(
|
||||
metricsViewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
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
|
||||
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 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),
|
||||
)
|
||||
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 }
|
||||
onOptionSelected = { timeFrame = it },
|
||||
) { tf: TimeFrame ->
|
||||
OptionLabel(stringResource(tf.strRes))
|
||||
}
|
||||
|
|
@ -232,7 +208,7 @@ fun PaxMetricsScreen(
|
|||
wifiSeries = wifiSeries,
|
||||
minValue = minValue,
|
||||
maxValue = maxValue,
|
||||
timeFrame = timeFrame
|
||||
timeFrame = timeFrame,
|
||||
)
|
||||
}
|
||||
// List
|
||||
|
|
@ -240,16 +216,11 @@ fun PaxMetricsScreen(
|
|||
Text(
|
||||
text = stringResource(R.string.no_pax_metrics_logs),
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
items(paxMetrics) { (log, pax) ->
|
||||
PaxMetricsItem(log, pax, dateFormat)
|
||||
}
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp)) {
|
||||
items(paxMetrics) { (log, pax) -> PaxMetricsItem(log, pax, dateFormat) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -261,8 +232,7 @@ fun decodePaxFromLog(log: MeshLog): PaxcountProtos.Paxcount? {
|
|||
// 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) {
|
||||
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
|
||||
}
|
||||
|
|
@ -313,33 +283,26 @@ fun unescapeProtoString(escaped: String): ByteArray {
|
|||
|
||||
@Composable
|
||||
fun PaxMetricsItem(log: MeshLog, pax: PaxcountProtos.Paxcount, dateFormat: DateFormat) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
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()
|
||||
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
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f, fill = true)
|
||||
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()
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ import com.geeksville.mesh.util.GraphUtil.plotPoint
|
|||
@Suppress("MagicNumber")
|
||||
private enum class Metric(val color: Color, val min: Float, val max: Float) {
|
||||
SNR(Color.Green, -20f, 12f), /* Selected 12 as the max to get 4 equal vertical sections. */
|
||||
RSSI(Color.Blue, -140f, -20f);
|
||||
/**
|
||||
* Difference between the metrics `max` and `min` values.
|
||||
*/
|
||||
RSSI(Color.Blue, -140f, -20f),
|
||||
;
|
||||
|
||||
/** Difference between the metrics `max` and `min` values. */
|
||||
fun difference() = max - min
|
||||
}
|
||||
|
||||
|
|
@ -82,54 +82,44 @@ 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 val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = Metric.RSSI.color),
|
||||
LegendData(nameRes = R.string.snr, color = Metric.SNR.color)
|
||||
)
|
||||
private val LEGEND_DATA =
|
||||
listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = Metric.RSSI.color),
|
||||
LegendData(nameRes = R.string.snr, color = Metric.SNR.color),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SignalMetricsScreen(
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
var displayInfoDialog by remember { mutableStateOf(false) }
|
||||
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
||||
val data = state.signalMetricsFiltered(selectedTimeFrame)
|
||||
|
||||
Column {
|
||||
|
||||
if (displayInfoDialog) {
|
||||
LegendInfoDialog(
|
||||
pairedRes = listOf(
|
||||
Pair(R.string.snr, R.string.snr_definition),
|
||||
Pair(R.string.rssi, R.string.rssi_definition)
|
||||
),
|
||||
onDismiss = { displayInfoDialog = false }
|
||||
pairedRes =
|
||||
listOf(Pair(R.string.snr, R.string.snr_definition), Pair(R.string.rssi, R.string.rssi_definition)),
|
||||
onDismiss = { displayInfoDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
SignalMetricsChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.33f),
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
|
||||
meshPackets = data.reversed(),
|
||||
selectedTimeFrame,
|
||||
promptInfoDialog = { displayInfoDialog = true }
|
||||
promptInfoDialog = { displayInfoDialog = true },
|
||||
)
|
||||
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedTimeFrame,
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) },
|
||||
) {
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(data) { meshPacket -> SignalMetricsCard(meshPacket) }
|
||||
}
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { meshPacket -> SignalMetricsCard(meshPacket) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,26 +129,23 @@ private fun SignalMetricsChart(
|
|||
modifier: Modifier = Modifier,
|
||||
meshPackets: List<MeshPacket>,
|
||||
selectedTime: TimeFrame,
|
||||
promptInfoDialog: () -> Unit
|
||||
promptInfoDialog: () -> Unit,
|
||||
) {
|
||||
ChartHeader(amount = meshPackets.size)
|
||||
if (meshPackets.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val (oldest, newest) = remember(key1 = meshPackets) {
|
||||
Pair(
|
||||
meshPackets.minBy { it.rxTime },
|
||||
meshPackets.maxBy { it.rxTime }
|
||||
)
|
||||
}
|
||||
val (oldest, newest) =
|
||||
remember(key1 = meshPackets) { Pair(meshPackets.minBy { it.rxTime }, meshPackets.maxBy { it.rxTime }) }
|
||||
val timeDiff = newest.rxTime - oldest.rxTime
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val screenWidth = LocalWindowInfo.current.containerSize.width
|
||||
val dp by remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong()))
|
||||
}
|
||||
val dp by
|
||||
remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong()))
|
||||
}
|
||||
|
||||
// Calculate visible time range based on scroll position and chart width
|
||||
val visibleTimeRange = run {
|
||||
|
|
@ -174,10 +161,7 @@ private fun SignalMetricsChart(
|
|||
visibleOldest to visibleNewest
|
||||
}
|
||||
|
||||
TimeLabels(
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second
|
||||
)
|
||||
TimeLabels(oldest = visibleTimeRange.first, newest = visibleTimeRange.second)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
|
|
@ -194,20 +178,15 @@ private fun SignalMetricsChart(
|
|||
)
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.horizontalScroll(state = scrollState, reverseScrolling = true)
|
||||
.weight(1f)
|
||||
modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(1f),
|
||||
) {
|
||||
HorizontalLinesOverlay(
|
||||
modifier.width(dp),
|
||||
lineColors = List(size = 5) { graphColor },
|
||||
)
|
||||
HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })
|
||||
|
||||
TimeAxisOverlay(
|
||||
modifier.width(dp),
|
||||
oldest = oldest.rxTime,
|
||||
newest = newest.rxTime,
|
||||
selectedTime.lineInterval()
|
||||
selectedTime.lineInterval(),
|
||||
)
|
||||
|
||||
/* Plot SNR and RSSI */
|
||||
|
|
@ -215,7 +194,6 @@ private fun SignalMetricsChart(
|
|||
val width = size.width
|
||||
/* Plot */
|
||||
for (packet in meshPackets) {
|
||||
|
||||
val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff
|
||||
val x = xRatio * width
|
||||
|
||||
|
|
@ -225,7 +203,7 @@ private fun SignalMetricsChart(
|
|||
color = Metric.SNR.color,
|
||||
x = x,
|
||||
value = packet.rxSnr - Metric.SNR.min,
|
||||
divisor = snrDiff
|
||||
divisor = snrDiff,
|
||||
)
|
||||
|
||||
/* RSSI */
|
||||
|
|
@ -234,7 +212,7 @@ private fun SignalMetricsChart(
|
|||
color = Metric.RSSI.color,
|
||||
x = x,
|
||||
value = packet.rxRssi - Metric.RSSI.min,
|
||||
divisor = rssiDiff
|
||||
divisor = rssiDiff,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -257,35 +235,19 @@ private fun SignalMetricsChart(
|
|||
@Composable
|
||||
private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
||||
val time = meshPacket.rxTime * MS_PER_SEC
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Surface {
|
||||
SelectionContainer {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
/* Data */
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(weight = 5f)
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.weight(weight = 5f).height(IntrinsicSize.Min)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
/* Time */
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
text = DATE_TIME_FORMAT.format(time),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -297,11 +259,7 @@ private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
|||
}
|
||||
|
||||
/* Signal Indicator */
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(weight = 3f)
|
||||
.height(IntrinsicSize.Max)
|
||||
) {
|
||||
Box(modifier = Modifier.weight(weight = 3f).height(IntrinsicSize.Max)) {
|
||||
LoraSignalIndicator(meshPacket.rxSnr, meshPacket.rxRssi)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue