Skip to content

Commit

Permalink
feat: Scrollable Signal Chart (meshtastic#1505)
Browse files Browse the repository at this point in the history
* Removed repeated calculation.

* Centralized the radius used to plot points and draw lines within GraphUtil.kt.

* Updated the signal metrics chart to use the scroll features.

* SignalMetricsChart long method warning suppression.
  • Loading branch information
Robert-0410 authored Jan 3, 2025
1 parent d14a8de commit 70a08c9
Showing 3 changed files with 90 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ import com.geeksville.mesh.ui.BatteryInfo
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.theme.Orange
import com.geeksville.mesh.util.GraphUtil
import com.geeksville.mesh.util.GraphUtil.plotPoint
import com.geeksville.mesh.util.GraphUtil.createPath

@@ -155,12 +156,12 @@ private fun DeviceMetricsChart(
Spacer(modifier = Modifier.height(16.dp))

val graphColor = MaterialTheme.colors.onSurface
val scrollState = rememberScrollState()

val scrollState = rememberScrollState()
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp
val dp by remember(key1 = selectedTime) {
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong()))
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
}

Row {
@@ -195,7 +196,6 @@ private fun DeviceMetricsChart(

val height = size.height
val width = size.width
val dataPointRadius = 2.dp.toPx()
for (i in telemetries.indices) {
val telemetry = telemetries[i]

@@ -207,7 +207,6 @@ private fun DeviceMetricsChart(
plotPoint(
drawContext = drawContext,
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
radius = dataPointRadius,
x = x,
value = telemetry.deviceMetrics.channelUtilization,
divisor = MAX_PERCENT_VALUE
@@ -217,7 +216,6 @@ private fun DeviceMetricsChart(
plotPoint(
drawContext = drawContext,
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
radius = dataPointRadius,
x = x,
value = telemetry.deviceMetrics.airUtilTx,
divisor = MAX_PERCENT_VALUE
@@ -246,7 +244,7 @@ private fun DeviceMetricsChart(
path = path,
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
style = Stroke(
width = dataPointRadius,
width = GraphUtil.RADIUS,
cap = StrokeCap.Round
)
)
175 changes: 82 additions & 93 deletions app/src/main/java/com/geeksville/mesh/ui/components/SignalMetrics.kt
Original file line number Diff line number Diff line change
@@ -17,9 +17,8 @@

package com.geeksville.mesh.ui.components

import android.graphics.Paint
import android.graphics.Typeface
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
@@ -31,8 +30,10 @@ 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.foundation.text.selection.SelectionContainer
import androidx.compose.material.Card
import androidx.compose.material.Surface
@@ -46,11 +47,8 @@ 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.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@@ -60,11 +58,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.R
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.util.GraphUtil.plotPoint

private val METRICS_COLORS = listOf(Color.Green, Color.Blue)

@@ -108,6 +105,7 @@ fun SignalMetricsScreen(
.fillMaxWidth()
.fillMaxHeight(fraction = 0.33f),
meshPackets = data.reversed(),
selectedTimeFrame,
promptInfoDialog = { displayInfoDialog = true }
)

@@ -126,21 +124,30 @@ fun SignalMetricsScreen(
}
}

@Suppress("LongMethod")
@Composable
private fun SignalMetricsChart(
modifier: Modifier = Modifier,
meshPackets: List<MeshPacket>,
selectedTime: TimeFrame,
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 timeDiff = newest.rxTime - oldest.rxTime

TimeLabels(
oldest = meshPackets.first().rxTime,
newest = meshPackets.last().rxTime
oldest = oldest.rxTime,
newest = newest.rxTime
)

Spacer(modifier = Modifier.height(16.dp))
@@ -149,51 +156,75 @@ private fun SignalMetricsChart(
val snrDiff = Metric.SNR.difference()
val rssiDiff = Metric.RSSI.difference()

Box(contentAlignment = Alignment.TopStart) {
val scrollState = rememberScrollState()
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp
val dp by remember(key1 = selectedTime) {
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong()))
}

ChartOverlay(
modifier = modifier,
lineColors = List(size = 5) { graphColor },
labelColor = METRICS_COLORS[Metric.SNR.ordinal],
minValue = Metric.SNR.min,
maxValue = Metric.SNR.max,
leaveSpace = true
Row {
YAxisLabels(
modifier = modifier.weight(weight = .1f),
METRICS_COLORS[Metric.RSSI.ordinal],
minValue = Metric.RSSI.min,
maxValue = Metric.RSSI.max,
)
LeftYLabels(modifier = modifier, labelColor = METRICS_COLORS[Metric.RSSI.ordinal])

/* Plot SNR and RSSI */
Canvas(modifier = modifier) {

val height = size.height
val width = size.width - 28.dp.toPx()
val spacing = LEFT_LABEL_SPACING.dp.toPx()
val spacePerEntry = (width - spacing) / meshPackets.size

/* Plot */
val dataPointRadius = 2.dp.toPx()
for ((i, packet) in meshPackets.withIndex()) {

val x = spacing + i * spacePerEntry
Box(
contentAlignment = Alignment.TopStart,
modifier = Modifier
.horizontalScroll(state = scrollState, reverseScrolling = true)
.weight(1f)
) {
HorizontalLinesOverlay(
modifier.width(dp),
lineColors = List(size = 5) { graphColor },
minValue = Metric.SNR.min,
maxValue = Metric.SNR.max
)

/* SNR */
val snrRatio = (packet.rxSnr - Metric.SNR.min) / snrDiff
val ySNR = height - (snrRatio * height)
drawCircle(
color = METRICS_COLORS[Metric.SNR.ordinal],
radius = dataPointRadius,
center = Offset(x, ySNR)
)
TimeAxisOverlay(
modifier.width(dp),
oldest = oldest.rxTime,
newest = newest.rxTime,
selectedTime.lineInterval()
)

/* RSSI */
val rssiRatio = (packet.rxRssi - Metric.RSSI.min) / rssiDiff
val yRssi = height - (rssiRatio * height)
drawCircle(
color = METRICS_COLORS[Metric.RSSI.ordinal],
radius = dataPointRadius,
center = Offset(x, yRssi)
)
/* Plot SNR and RSSI */
Canvas(modifier = modifier.width(dp)) {
val width = size.width
/* Plot */
for (packet in meshPackets) {

val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff
val x = xRatio * width

/* SNR */
plotPoint(
drawContext = drawContext,
color = METRICS_COLORS[Metric.SNR.ordinal],
x = x,
value = packet.rxSnr - Metric.SNR.min,
divisor = snrDiff
)

/* RSSI */
plotPoint(
drawContext = drawContext,
color = METRICS_COLORS[Metric.RSSI.ordinal],
x = x,
value = packet.rxRssi - Metric.RSSI.min,
divisor = rssiDiff
)
}
}
}
YAxisLabels(
modifier = modifier.weight(weight = .1f),
METRICS_COLORS[Metric.SNR.ordinal],
minValue = Metric.SNR.min,
maxValue = Metric.SNR.max,
)
}

Spacer(modifier = Modifier.height(16.dp))
@@ -203,48 +234,6 @@ private fun SignalMetricsChart(
Spacer(modifier = Modifier.height(16.dp))
}

/**
* Draws a set of Y labels on the left side of the graph.
* Currently only used for the RSSI labels.
*/
@Composable
private fun LeftYLabels(
modifier: Modifier,
labelColor: Color,
) {
val range = Metric.RSSI.difference()
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
}
drawContext.canvas.nativeCanvas.apply {
var label = Metric.RSSI.min
for (i in 0..LINE_LIMIT) {
val ratio = (label - Metric.RSSI.min) / range
val y = height - (ratio * height)
drawText(
"${label.toInt()}",
4.dp.toPx(),
y + 4.dp.toPx(),
textPaint
)
label += verticalSpacing
}
}
}
}

@Composable
private fun SignalMetricsCard(meshPacket: MeshPacket) {
val time = meshPacket.rxTime * MS_PER_SEC
6 changes: 4 additions & 2 deletions app/src/main/java/com/geeksville/mesh/util/GraphUtil.kt
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@

package com.geeksville.mesh.util

import android.content.res.Resources
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -25,14 +26,15 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry

object GraphUtil {

val RADIUS = Resources.getSystem().displayMetrics.density * 2

/**
* @param value Must be zero-scaled before passing.
* @param divisor The range for the data set.
*/
fun plotPoint(
drawContext: DrawContext,
color: Color,
radius: Float,
x: Float,
value: Float,
divisor: Float,
@@ -42,7 +44,7 @@ object GraphUtil {
val y = height - (ratio * height)
drawContext.canvas.drawCircle(
center = Offset(x, y),
radius = radius,
radius = RADIUS,
paint = androidx.compose.ui.graphics.Paint().apply { this.color = color }
)
}

0 comments on commit 70a08c9

Please sign in to comment.