diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index 9108aa542..73742379a 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -20,9 +20,9 @@ package com.geeksville.mesh import android.graphics.Color import android.os.Parcelable import com.geeksville.mesh.util.GPSFormat +import com.geeksville.mesh.util.anonymize import com.geeksville.mesh.util.bearing import com.geeksville.mesh.util.latLongToMeter -import com.geeksville.mesh.util.anonymize import com.geeksville.mesh.util.onlineTimeThreshold import kotlinx.parcelize.Parcelize @@ -40,33 +40,27 @@ data class MeshUser( val role: Int = 0, ) : Parcelable { - override fun toString(): String { - return "MeshUser(id=${id.anonymize}, " + - "longName=${longName.anonymize}, " + - "shortName=${shortName.anonymize}, " + - "hwModel=$hwModelString, " + - "isLicensed=$isLicensed, " + - "role=$role)" - } + override fun toString(): String = "MeshUser(id=${id.anonymize}, " + + "longName=${longName.anonymize}, " + + "shortName=${shortName.anonymize}, " + + "hwModel=$hwModelString, " + + "isLicensed=$isLicensed, " + + "role=$role)" - /** Create our model object from a protobuf. + /** Create our model object from a protobuf. */ + constructor(p: MeshProtos.User) : this(p.id, p.longName, p.shortName, p.hwModel, p.isLicensed, p.roleValue) + + /** + * a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null + * if unset */ - constructor(p: MeshProtos.User) : this( - p.id, - p.longName, - p.shortName, - p.hwModel, - p.isLicensed, - p.roleValue - ) - - /** a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot - * or null if unset - * */ val hwModelString: String? get() = - if (hwModel == MeshProtos.HardwareModel.UNSET) null - else hwModel.name.replace('_', '-').replace('p', '.').lowercase() + if (hwModel == MeshProtos.HardwareModel.UNSET) { + null + } else { + hwModel.name.replace('_', '-').replace('p', '.').lowercase() + } } @Parcelize @@ -82,16 +76,22 @@ data class Position( ) : Parcelable { companion object { - /// Convert to a double representation of degrees + // / Convert to a double representation of degrees fun degD(i: Int) = i * 1e-7 + fun degI(d: Double) = (d * 1e7).toInt() fun currentTime() = (System.currentTimeMillis() / 1000).toInt() } - /** Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will be used. + /** + * Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will + * be used. */ - constructor(position: MeshProtos.Position, defaultTime: Int = currentTime()) : this( + constructor( + position: MeshProtos.Position, + defaultTime: Int = currentTime(), + ) : this( // We prefer the int version of lat/lon but if not available use the depreciated legacy version degD(position.latitudeI), degD(position.longitudeI), @@ -100,21 +100,20 @@ data class Position( position.satsInView, position.groundSpeed, position.groundTrack, - position.precisionBits + position.precisionBits, ) - /// @return distance in meters to some other node (or null if unknown) + // / @return distance in meters to some other node (or null if unknown) fun distance(o: Position) = latLongToMeter(latitude, longitude, o.latitude, o.longitude) - /// @return bearing to the other position in degrees + // / @return bearing to the other position in degrees fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude) // If GPS gives a crap position don't crash our app - fun isValid(): Boolean { - return latitude != 0.0 && longitude != 0.0 && - (latitude >= -90 && latitude <= 90.0) && - (longitude >= -180 && longitude <= 180) - } + fun isValid(): Boolean = latitude != 0.0 && + longitude != 0.0 && + (latitude >= -90 && latitude <= 90.0) && + (longitude >= -180 && longitude <= 180) fun gpsString(gpsFormat: Int): String = when (gpsFormat) { ConfigProtos.Config.DisplayConfig.GpsCoordinateFormat.DEC_VALUE -> GPSFormat.DEC(this) @@ -124,12 +123,10 @@ data class Position( else -> GPSFormat.DEC(this) } - override fun toString(): String { - return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time})" - } + override fun toString(): String = + "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)" } - @Parcelize data class DeviceMetrics( val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) @@ -143,16 +140,11 @@ data class DeviceMetrics( fun currentTime() = (System.currentTimeMillis() / 1000).toInt() } - /** Create our model object from a protobuf. - */ - constructor(p: TelemetryProtos.DeviceMetrics, telemetryTime: Int = currentTime()) : this( - telemetryTime, - p.batteryLevel, - p.voltage, - p.channelUtilization, - p.airUtilTx, - p.uptimeSeconds, - ) + /** Create our model object from a protobuf. */ + constructor( + p: TelemetryProtos.DeviceMetrics, + telemetryTime: Int = currentTime(), + ) : this(telemetryTime, p.batteryLevel, p.voltage, p.channelUtilization, p.airUtilTx, p.uptimeSeconds) } @Parcelize @@ -184,7 +176,7 @@ data class NodeInfo( var deviceMetrics: DeviceMetrics? = null, var channel: Int = 0, var environmentMetrics: EnvironmentMetrics? = null, - var hopsAway: Int = 0 + var hopsAway: Int = 0, ) : Parcelable { val colors: Pair @@ -196,46 +188,53 @@ data class NodeInfo( return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b) } - val batteryLevel get() = deviceMetrics?.batteryLevel - val voltage get() = deviceMetrics?.voltage - val batteryStr get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else "" + val batteryLevel + get() = deviceMetrics?.batteryLevel - /** - * true if the device was heard from recently - */ + val voltage + get() = deviceMetrics?.voltage + + val batteryStr + get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else "" + + /** true if the device was heard from recently */ val isOnline: Boolean get() { return lastHeard > onlineTimeThreshold() } - /// return the position if it is valid, else null + // / return the position if it is valid, else null val validPosition: Position? get() { return position?.takeIf { it.isValid() } } - /// @return distance in meters to some other node (or null if unknown) + // / @return distance in meters to some other node (or null if unknown) fun distance(o: NodeInfo?): Int? { val p = validPosition val op = o?.validPosition return if (p != null && op != null) p.distance(op).toInt() else null } - /// @return bearing to the other position in degrees + // / @return bearing to the other position in degrees fun bearing(o: NodeInfo?): Int? { val p = validPosition val op = o?.validPosition return if (p != null && op != null) p.bearing(op).toInt() else null } - /// @return a nice human readable string for the distance, or null for unknown + // / @return a nice human readable string for the distance, or null for unknown fun distanceStr(o: NodeInfo?, prefUnits: Int = 0) = distance(o)?.let { dist -> when { dist == 0 -> null // same point - prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist < 1000 -> "%.0f m".format(dist.toDouble()) - prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist >= 1000 -> "%.1f km".format(dist / 1000.0) - prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist < 1609 -> "%.0f ft".format(dist.toDouble()*3.281) - prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist >= 1609 -> "%.1f mi".format(dist / 1609.34) + prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist < 1000 -> + "%.0f m".format(dist.toDouble()) + prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist >= 1000 -> + "%.1f km".format(dist / 1000.0) + prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist < 1609 -> + "%.0f ft".format(dist.toDouble() * 3.281) + prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist >= 1609 -> + "%.1f mi".format(dist / 1609.34) else -> null } } diff --git a/app/src/main/java/com/geeksville/mesh/database/Converters.kt b/app/src/main/java/com/geeksville/mesh/database/Converters.kt index 7917027eb..5099b2cdb 100644 --- a/app/src/main/java/com/geeksville/mesh/database/Converters.kt +++ b/app/src/main/java/com/geeksville/mesh/database/Converters.kt @@ -42,94 +42,64 @@ class Converters : Logging { } @TypeConverter - fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio { - return try { - MeshProtos.FromRadio.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToFromRadio TypeConverter error:", ex) - MeshProtos.FromRadio.getDefaultInstance() - } + fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try { + MeshProtos.FromRadio.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToFromRadio TypeConverter error:", ex) + MeshProtos.FromRadio.getDefaultInstance() } - @TypeConverter - fun fromRadioToBytes(value: MeshProtos.FromRadio): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun fromRadioToBytes(value: MeshProtos.FromRadio): ByteArray? = value.toByteArray() @TypeConverter - fun bytesToUser(bytes: ByteArray): MeshProtos.User { - return try { - MeshProtos.User.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToUser TypeConverter error:", ex) - MeshProtos.User.getDefaultInstance() - } + fun bytesToUser(bytes: ByteArray): MeshProtos.User = try { + MeshProtos.User.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToUser TypeConverter error:", ex) + MeshProtos.User.getDefaultInstance() } - @TypeConverter - fun userToBytes(value: MeshProtos.User): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun userToBytes(value: MeshProtos.User): ByteArray? = value.toByteArray() @TypeConverter - fun bytesToPosition(bytes: ByteArray): MeshProtos.Position { - return try { - MeshProtos.Position.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToPosition TypeConverter error:", ex) - MeshProtos.Position.getDefaultInstance() - } + fun bytesToPosition(bytes: ByteArray): MeshProtos.Position = try { + MeshProtos.Position.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToPosition TypeConverter error:", ex) + MeshProtos.Position.getDefaultInstance() } - @TypeConverter - fun positionToBytes(value: MeshProtos.Position): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun positionToBytes(value: MeshProtos.Position): ByteArray? = value.toByteArray() @TypeConverter - fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry { - return try { - TelemetryProtos.Telemetry.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToTelemetry TypeConverter error:", ex) - TelemetryProtos.Telemetry.getDefaultInstance() - } + fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry = try { + TelemetryProtos.Telemetry.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToTelemetry TypeConverter error:", ex) + TelemetryProtos.Telemetry.getDefaultInstance() } - @TypeConverter - fun telemetryToBytes(value: TelemetryProtos.Telemetry): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun telemetryToBytes(value: TelemetryProtos.Telemetry): ByteArray? = value.toByteArray() @TypeConverter - fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount { - return try { - PaxcountProtos.Paxcount.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToPaxcounter TypeConverter error:", ex) - PaxcountProtos.Paxcount.getDefaultInstance() - } + fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount = try { + PaxcountProtos.Paxcount.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToPaxcounter TypeConverter error:", ex) + PaxcountProtos.Paxcount.getDefaultInstance() } - @TypeConverter - fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? = value.toByteArray() @TypeConverter - fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata { - return try { - MeshProtos.DeviceMetadata.parseFrom(bytes) - } catch (ex: InvalidProtocolBufferException) { - errormsg("bytesToMetadata TypeConverter error:", ex) - MeshProtos.DeviceMetadata.getDefaultInstance() - } + fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata = try { + MeshProtos.DeviceMetadata.parseFrom(bytes) + } catch (ex: InvalidProtocolBufferException) { + errormsg("bytesToMetadata TypeConverter error:", ex) + MeshProtos.DeviceMetadata.getDefaultInstance() } - @TypeConverter - fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? { - return value.toByteArray() - } + @TypeConverter fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? = value.toByteArray() @TypeConverter fun fromStringList(value: String?): List? { @@ -148,12 +118,7 @@ class Converters : Logging { } @TypeConverter - fun bytesToByteString(bytes: ByteArray?): ByteString? { - return if (bytes == null) null else ByteString.copyFrom(bytes) - } + fun bytesToByteString(bytes: ByteArray?): ByteString? = if (bytes == null) null else ByteString.copyFrom(bytes) - @TypeConverter - fun byteStringToBytes(value: ByteString?): ByteArray? { - return value?.toByteArray() - } + @TypeConverter fun byteStringToBytes(value: ByteString?): ByteArray? = value?.toByteArray() } diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt index 53b3dd96a..3167c2b13 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt @@ -84,69 +84,41 @@ data class NodeWithRelations( } } -@Entity( - tableName = "metadata", - indices = [ - Index(value = ["num"]), - ], -) +@Entity(tableName = "metadata", indices = [Index(value = ["num"])]) data class MetadataEntity( @PrimaryKey val num: Int, - @ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) - val proto: MeshProtos.DeviceMetadata, + @ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) val proto: MeshProtos.DeviceMetadata, val timestamp: Long = System.currentTimeMillis(), ) @Suppress("MagicNumber") @Entity(tableName = "nodes") data class NodeEntity( - @PrimaryKey(autoGenerate = false) - val num: Int, // This is immutable, and used as a key - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - var user: MeshProtos.User = MeshProtos.User.getDefaultInstance(), + @PrimaryKey(autoGenerate = false) val num: Int, // This is immutable, and used as a key + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var user: MeshProtos.User = MeshProtos.User.getDefaultInstance(), @ColumnInfo(name = "long_name") var longName: String? = null, @ColumnInfo(name = "short_name") var shortName: String? = null, // used in includeUnknown filter - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(), var latitude: Double = 0.0, var longitude: Double = 0.0, - var snr: Float = Float.MAX_VALUE, var rssi: Int = Int.MAX_VALUE, - - @ColumnInfo(name = "last_heard") - var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 - + @ColumnInfo(name = "last_heard") var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 @ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB) var deviceTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(), - var channel: Int = 0, - - @ColumnInfo(name = "via_mqtt") - var viaMqtt: Boolean = false, - - @ColumnInfo(name = "hops_away") - var hopsAway: Int = -1, - - @ColumnInfo(name = "is_favorite") - var isFavorite: Boolean = false, - - @ColumnInfo(name = "is_ignored", defaultValue = "0") - var isIgnored: Boolean = false, - + @ColumnInfo(name = "via_mqtt") var viaMqtt: Boolean = false, + @ColumnInfo(name = "hops_away") var hopsAway: Int = -1, + @ColumnInfo(name = "is_favorite") var isFavorite: Boolean = false, + @ColumnInfo(name = "is_ignored", defaultValue = "0") var isIgnored: Boolean = false, @ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB) var environmentTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(), - @ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB) var powerTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(), - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(), - - @ColumnInfo(name = "public_key") - var publicKey: ByteString? = null, + @ColumnInfo(name = "public_key") var publicKey: ByteString? = null, ) { val deviceMetrics: TelemetryProtos.DeviceMetrics get() = deviceTelemetry.deviceMetrics @@ -154,8 +126,11 @@ data class NodeEntity( val environmentMetrics: TelemetryProtos.EnvironmentMetrics get() = environmentTelemetry.environmentMetrics - val isUnknownUser get() = user.hwModel == MeshProtos.HardwareModel.UNSET - val hasPKC get() = (publicKey ?: user.publicKey).isNotEmpty() + val isUnknownUser + get() = user.hwModel == MeshProtos.HardwareModel.UNSET + + val hasPKC + get() = (publicKey ?: user.publicKey).isNotEmpty() fun setPosition(p: MeshProtos.Position, defaultTime: Int = currentTime()) { position = p.copy { time = if (p.time != 0) p.time else defaultTime } @@ -163,9 +138,7 @@ data class NodeEntity( longitude = degD(p.longitudeI) } - /** - * true if the device was heard from recently - */ + /** true if the device was heard from recently */ val isOnline: Boolean get() { return lastHeard > onlineTimeThreshold() @@ -174,22 +147,27 @@ data class NodeEntity( companion object { // Convert to a double representation of degrees fun degD(i: Int) = i * 1e-7 + fun degI(d: Double) = (d * 1e7).toInt() val ERROR_BYTE_STRING: ByteString = ByteString.copyFrom(ByteArray(32) { 0 }) + fun currentTime() = (System.currentTimeMillis() / 1000).toInt() } fun toNodeInfo() = NodeInfo( num = num, - user = MeshUser( + user = + MeshUser( id = user.id, longName = user.longName, shortName = user.shortName, hwModel = user.hwModel, role = user.roleValue, - ).takeIf { user.id.isNotEmpty() }, - position = Position( + ) + .takeIf { user.id.isNotEmpty() }, + position = + Position( latitude = latitude, longitude = longitude, altitude = position.altitude, @@ -198,11 +176,13 @@ data class NodeEntity( groundSpeed = position.groundSpeed, groundTrack = position.groundTrack, precisionBits = position.precisionBits, - ).takeIf { it.isValid() }, + ) + .takeIf { it.isValid() }, snr = snr, rssi = rssi, lastHeard = lastHeard, - deviceMetrics = DeviceMetrics( + deviceMetrics = + DeviceMetrics( time = deviceTelemetry.time, batteryLevel = deviceMetrics.batteryLevel, voltage = deviceMetrics.voltage, @@ -211,7 +191,8 @@ data class NodeEntity( uptimeSeconds = deviceMetrics.uptimeSeconds, ), channel = channel, - environmentMetrics = EnvironmentMetrics( + environmentMetrics = + EnvironmentMetrics( time = environmentTelemetry.time, temperature = environmentMetrics.temperature, relativeHumidity = environmentMetrics.relativeHumidity, diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/CommonCharts.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/CommonCharts.kt index 82a3b8e0b..2d4404064 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/CommonCharts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/CommonCharts.kt @@ -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, -) { +fun HorizontalLinesOverlay(modifier: Modifier, lineColors: List) { /* 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, - displayInfoIcon: Boolean = true, - promptInfoDialog: () -> Unit = {} -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { +fun Legend(legendData: List, 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>, 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>, 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 = {}) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt index 6a6bea820..84cc5ca34 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt @@ -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(), ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt index e371ca85e..da0e51576 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt @@ -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, 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) } }