Run spotless ahead of 2523 to make the diff easier (#2571)

This commit is contained in:
DaneEvans 2025-07-30 18:42:34 +10:00 committed by GitHub
parent 64ead16d83
commit d336f23486
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 282 additions and 483 deletions

View file

@ -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 = {})
}

View file

@ -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(),
)
}
}

View file

@ -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)
}
}