kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: Scrollable Signal Chart (#1505)
* 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.pull/1490/head
rodzic
d14a8de78e
commit
70a08c9d31
|
@ -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.MS_PER_SEC
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
import com.geeksville.mesh.ui.theme.Orange
|
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.plotPoint
|
||||||
import com.geeksville.mesh.util.GraphUtil.createPath
|
import com.geeksville.mesh.util.GraphUtil.createPath
|
||||||
|
|
||||||
|
@ -155,12 +156,12 @@ private fun DeviceMetricsChart(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
val graphColor = MaterialTheme.colors.onSurface
|
val graphColor = MaterialTheme.colors.onSurface
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val screenWidth = configuration.screenWidthDp
|
val screenWidth = configuration.screenWidthDp
|
||||||
val dp by remember(key1 = selectedTime) {
|
val dp by remember(key1 = selectedTime) {
|
||||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong()))
|
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
@ -195,7 +196,6 @@ private fun DeviceMetricsChart(
|
||||||
|
|
||||||
val height = size.height
|
val height = size.height
|
||||||
val width = size.width
|
val width = size.width
|
||||||
val dataPointRadius = 2.dp.toPx()
|
|
||||||
for (i in telemetries.indices) {
|
for (i in telemetries.indices) {
|
||||||
val telemetry = telemetries[i]
|
val telemetry = telemetries[i]
|
||||||
|
|
||||||
|
@ -207,7 +207,6 @@ private fun DeviceMetricsChart(
|
||||||
plotPoint(
|
plotPoint(
|
||||||
drawContext = drawContext,
|
drawContext = drawContext,
|
||||||
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
||||||
radius = dataPointRadius,
|
|
||||||
x = x,
|
x = x,
|
||||||
value = telemetry.deviceMetrics.channelUtilization,
|
value = telemetry.deviceMetrics.channelUtilization,
|
||||||
divisor = MAX_PERCENT_VALUE
|
divisor = MAX_PERCENT_VALUE
|
||||||
|
@ -217,7 +216,6 @@ private fun DeviceMetricsChart(
|
||||||
plotPoint(
|
plotPoint(
|
||||||
drawContext = drawContext,
|
drawContext = drawContext,
|
||||||
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
||||||
radius = dataPointRadius,
|
|
||||||
x = x,
|
x = x,
|
||||||
value = telemetry.deviceMetrics.airUtilTx,
|
value = telemetry.deviceMetrics.airUtilTx,
|
||||||
divisor = MAX_PERCENT_VALUE
|
divisor = MAX_PERCENT_VALUE
|
||||||
|
@ -246,7 +244,7 @@ private fun DeviceMetricsChart(
|
||||||
path = path,
|
path = path,
|
||||||
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
||||||
style = Stroke(
|
style = Stroke(
|
||||||
width = dataPointRadius,
|
width = GraphUtil.RADIUS,
|
||||||
cap = StrokeCap.Round
|
cap = StrokeCap.Round
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,9 +17,8 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui.components
|
package com.geeksville.mesh.ui.components
|
||||||
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
|
@ -46,11 +47,8 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.MeshProtos.MeshPacket
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.model.MetricsViewModel
|
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.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.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
|
import com.geeksville.mesh.util.GraphUtil.plotPoint
|
||||||
|
|
||||||
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
||||||
|
|
||||||
|
@ -108,6 +105,7 @@ fun SignalMetricsScreen(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(fraction = 0.33f),
|
.fillMaxHeight(fraction = 0.33f),
|
||||||
meshPackets = data.reversed(),
|
meshPackets = data.reversed(),
|
||||||
|
selectedTimeFrame,
|
||||||
promptInfoDialog = { displayInfoDialog = true }
|
promptInfoDialog = { displayInfoDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -126,21 +124,30 @@ fun SignalMetricsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun SignalMetricsChart(
|
private fun SignalMetricsChart(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
meshPackets: List<MeshPacket>,
|
meshPackets: List<MeshPacket>,
|
||||||
|
selectedTime: TimeFrame,
|
||||||
promptInfoDialog: () -> Unit
|
promptInfoDialog: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
ChartHeader(amount = meshPackets.size)
|
ChartHeader(amount = meshPackets.size)
|
||||||
if (meshPackets.isEmpty()) {
|
if (meshPackets.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val (oldest, newest) = remember(key1 = meshPackets) {
|
||||||
|
Pair(
|
||||||
|
meshPackets.minBy { it.rxTime },
|
||||||
|
meshPackets.maxBy { it.rxTime }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val timeDiff = newest.rxTime - oldest.rxTime
|
||||||
|
|
||||||
TimeLabels(
|
TimeLabels(
|
||||||
oldest = meshPackets.first().rxTime,
|
oldest = oldest.rxTime,
|
||||||
newest = meshPackets.last().rxTime
|
newest = newest.rxTime
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
@ -149,52 +156,76 @@ private fun SignalMetricsChart(
|
||||||
val snrDiff = Metric.SNR.difference()
|
val snrDiff = Metric.SNR.difference()
|
||||||
val rssiDiff = Metric.RSSI.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(
|
Row {
|
||||||
modifier = modifier,
|
YAxisLabels(
|
||||||
lineColors = List(size = 5) { graphColor },
|
modifier = modifier.weight(weight = .1f),
|
||||||
labelColor = METRICS_COLORS[Metric.SNR.ordinal],
|
METRICS_COLORS[Metric.RSSI.ordinal],
|
||||||
minValue = Metric.SNR.min,
|
minValue = Metric.RSSI.min,
|
||||||
maxValue = Metric.SNR.max,
|
maxValue = Metric.RSSI.max,
|
||||||
leaveSpace = true
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
TimeAxisOverlay(
|
||||||
|
modifier.width(dp),
|
||||||
|
oldest = oldest.rxTime,
|
||||||
|
newest = newest.rxTime,
|
||||||
|
selectedTime.lineInterval()
|
||||||
)
|
)
|
||||||
LeftYLabels(modifier = modifier, labelColor = METRICS_COLORS[Metric.RSSI.ordinal])
|
|
||||||
|
|
||||||
/* Plot SNR and RSSI */
|
/* Plot SNR and RSSI */
|
||||||
Canvas(modifier = modifier) {
|
Canvas(modifier = modifier.width(dp)) {
|
||||||
|
val width = size.width
|
||||||
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 */
|
/* Plot */
|
||||||
val dataPointRadius = 2.dp.toPx()
|
for (packet in meshPackets) {
|
||||||
for ((i, packet) in meshPackets.withIndex()) {
|
|
||||||
|
|
||||||
val x = spacing + i * spacePerEntry
|
val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff
|
||||||
|
val x = xRatio * width
|
||||||
|
|
||||||
/* SNR */
|
/* SNR */
|
||||||
val snrRatio = (packet.rxSnr - Metric.SNR.min) / snrDiff
|
plotPoint(
|
||||||
val ySNR = height - (snrRatio * height)
|
drawContext = drawContext,
|
||||||
drawCircle(
|
|
||||||
color = METRICS_COLORS[Metric.SNR.ordinal],
|
color = METRICS_COLORS[Metric.SNR.ordinal],
|
||||||
radius = dataPointRadius,
|
x = x,
|
||||||
center = Offset(x, ySNR)
|
value = packet.rxSnr - Metric.SNR.min,
|
||||||
|
divisor = snrDiff
|
||||||
)
|
)
|
||||||
|
|
||||||
/* RSSI */
|
/* RSSI */
|
||||||
val rssiRatio = (packet.rxRssi - Metric.RSSI.min) / rssiDiff
|
plotPoint(
|
||||||
val yRssi = height - (rssiRatio * height)
|
drawContext = drawContext,
|
||||||
drawCircle(
|
|
||||||
color = METRICS_COLORS[Metric.RSSI.ordinal],
|
color = METRICS_COLORS[Metric.RSSI.ordinal],
|
||||||
radius = dataPointRadius,
|
x = x,
|
||||||
center = Offset(x, yRssi)
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
@ -203,48 +234,6 @@ private fun SignalMetricsChart(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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
|
@Composable
|
||||||
private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
||||||
val time = meshPacket.rxTime * MS_PER_SEC
|
val time = meshPacket.rxTime * MS_PER_SEC
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.util
|
package com.geeksville.mesh.util
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -25,6 +26,8 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||||
|
|
||||||
object GraphUtil {
|
object GraphUtil {
|
||||||
|
|
||||||
|
val RADIUS = Resources.getSystem().displayMetrics.density * 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param value Must be zero-scaled before passing.
|
* @param value Must be zero-scaled before passing.
|
||||||
* @param divisor The range for the data set.
|
* @param divisor The range for the data set.
|
||||||
|
@ -32,7 +35,6 @@ object GraphUtil {
|
||||||
fun plotPoint(
|
fun plotPoint(
|
||||||
drawContext: DrawContext,
|
drawContext: DrawContext,
|
||||||
color: Color,
|
color: Color,
|
||||||
radius: Float,
|
|
||||||
x: Float,
|
x: Float,
|
||||||
value: Float,
|
value: Float,
|
||||||
divisor: Float,
|
divisor: Float,
|
||||||
|
@ -42,7 +44,7 @@ object GraphUtil {
|
||||||
val y = height - (ratio * height)
|
val y = height - (ratio * height)
|
||||||
drawContext.canvas.drawCircle(
|
drawContext.canvas.drawCircle(
|
||||||
center = Offset(x, y),
|
center = Offset(x, y),
|
||||||
radius = radius,
|
radius = RADIUS,
|
||||||
paint = androidx.compose.ui.graphics.Paint().apply { this.color = color }
|
paint = androidx.compose.ui.graphics.Paint().apply { this.color = color }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue