From cd9167f19f8498b00b593d23bab4ee25705d8b9a Mon Sep 17 00:00:00 2001 From: Robert-0410 <62630290+Robert-0410@users.noreply.github.com> Date: Sat, 10 May 2025 06:03:14 -0700 Subject: [PATCH] refactor: Node detail work (#1836) --- .../java/com/geeksville/mesh/ui/NodeDetail.kt | 49 +++++-------------- .../mesh/ui/components/EnvironmentMetrics.kt | 7 +-- .../mesh/ui/components/PositionLog.kt | 14 +++--- .../components/EditChannelDialog.kt | 2 - .../mesh/util/DistanceExtensions.kt | 38 ++++++++------ .../geeksville/mesh/util/UnitConversions.kt | 46 +++++++++++++++++ app/src/main/res/values/strings.xml | 10 ++++ 7 files changed, 96 insertions(+), 70 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/util/UnitConversions.kt diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt index ebba0abe4..4f0942990 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt @@ -15,15 +15,12 @@ * along with this program. If not, see . */ -@file:Suppress("TooManyFunctions") - package com.geeksville.mesh.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -93,7 +90,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import com.geeksville.mesh.R import com.geeksville.mesh.model.DeviceHardware import com.geeksville.mesh.model.MetricsState @@ -104,11 +100,12 @@ import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider import com.geeksville.mesh.ui.radioconfig.NavCard import com.geeksville.mesh.ui.theme.AppTheme -import com.geeksville.mesh.util.DistanceUnit +import com.geeksville.mesh.util.UnitConversions.calculateDewPoint +import com.geeksville.mesh.util.UnitConversions.toTempString import com.geeksville.mesh.util.formatAgo import com.geeksville.mesh.util.formatUptime import com.geeksville.mesh.util.thenIf -import kotlin.math.ln +import com.geeksville.mesh.util.toSpeedString private enum class LogsType( val titleRes: Int, @@ -286,13 +283,13 @@ private fun DeviceDetailsContent( ) } NodeDetailRow( - label = "Hardware", + label = stringResource(R.string.hardware), icon = Icons.Default.Router, value = hwModelName ) if (isSupported) { NodeDetailRow( - label = "Supported", + label = stringResource(R.string.supported), icon = Icons.Default.Verified, value = "", iconTint = Color.Green @@ -349,36 +346,36 @@ private fun NodeDetailsContent( Spacer(Modifier.height(16.dp)) } NodeDetailRow( - label = "Node Number", + label = stringResource(R.string.node_number), icon = Icons.Default.Numbers, value = node.num.toUInt().toString() ) NodeDetailRow( - label = "User Id", + label = stringResource(R.string.user_id), icon = Icons.Default.Person, value = node.user.id ) NodeDetailRow( - label = "Role", + label = stringResource(R.string.role), icon = Icons.Default.Work, value = node.user.role.name ) if (node.deviceMetrics.uptimeSeconds > 0) { NodeDetailRow( - label = "Uptime", + label = stringResource(R.string.uptime), icon = Icons.Default.CheckCircle, value = formatUptime(node.deviceMetrics.uptimeSeconds) ) } if (node.metadata != null) { NodeDetailRow( - label = "Firmware version", + label = stringResource(R.string.firmware_version), icon = Icons.Default.Memory, value = node.metadata.firmwareVersion.substringBeforeLast(".") ) } NodeDetailRow( - label = "Last heard", + label = stringResource(R.string.node_sort_last_heard), icon = Icons.Default.History, value = formatAgo(node.lastHeard) ) @@ -431,7 +428,6 @@ private fun InfoCard( } } -@OptIn(ExperimentalLayoutApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun EnvironmentMetrics( @@ -541,29 +537,6 @@ private fun EnvironmentMetrics( } } -@Suppress("MagicNumber") -private fun Float.toTempString(isFahrenheit: Boolean) = if (isFahrenheit) { - val fahrenheit = this * 1.8F + 32 - "%.0f°F".format(fahrenheit) -} else { - "%.0f°C".format(this) -} - -@Suppress("MagicNumber") -private fun Float.toSpeedString() = when (DistanceUnit.getFromLocale()) { - DisplayUnits.METRIC -> "%.0f km/h".format(this * 3.6) - else -> "%.0f mph".format(this * 2.23694f) -} - -// Magnus-Tetens approximation -@Suppress("MagicNumber") -private fun calculateDewPoint(tempCelsius: Float, humidity: Float): Float { - val (a, b) = 17.27f to 237.7f - val alpha = (a * tempCelsius) / (b + tempCelsius) + ln(humidity / 100f) - return (b * alpha) / (a - alpha) -} - -@OptIn(ExperimentalLayoutApi::class) @Composable private fun PowerMetrics(node: Node) = with(node.powerMetrics) { FlowRow( diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt index 375ba7811..b46837f13 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EnvironmentMetrics.kt @@ -66,6 +66,7 @@ import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.util.GraphUtil.createPath import com.geeksville.mesh.util.GraphUtil.drawPathWithGradient +import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit private val LEGEND_DATA_1 = listOf( LegendData( @@ -102,12 +103,6 @@ fun EnvironmentMetricsScreen( val graphData = environmentState.environmentMetricsFiltered(selectedTimeFrame) val data = graphData.metrics - /* Convert Celsius to Fahrenheit */ - @Suppress("MagicNumber") - fun celsiusToFahrenheit(celsius: Float): Float { - return (celsius * 1.8F) + 32 - } - val processedTelemetries: List = if (state.isFahrenheit) { data.map { telemetry -> val temperatureFahrenheit = diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/PositionLog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/PositionLog.kt index 4cb43aa9d..8a789550a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/PositionLog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/PositionLog.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -98,15 +97,15 @@ private fun HeaderItem(compactWidth: Boolean) { .padding(8.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - PositionText("Latitude", Weight20) - PositionText("Longitude", Weight20) - PositionText("Sats", Weight10) - PositionText("Alt", Weight15) + PositionText(stringResource(R.string.latitude), Weight20) + PositionText(stringResource(R.string.longitude), Weight20) + PositionText(stringResource(R.string.sats), Weight10) + PositionText(stringResource(R.string.alt), Weight15) if (!compactWidth) { PositionText("Speed", Weight15) - PositionText("Heading", Weight15) + PositionText(stringResource(R.string.heading), Weight15) } - PositionText("Timestamp", Weight40) + PositionText(stringResource(R.string.timestamp), Weight40) } } @@ -155,7 +154,6 @@ private fun formatPositionTime( return timeText } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun ActionButtons( clearButtonEnabled: Boolean, diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt index 73d531bd7..ee1c06811 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/EditChannelDialog.kt @@ -19,7 +19,6 @@ package com.geeksville.mesh.ui.radioconfig.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -55,7 +54,6 @@ import com.geeksville.mesh.ui.components.PositionPrecisionPreference import com.geeksville.mesh.ui.components.SwitchPreference @Suppress("LongMethod") -@OptIn(ExperimentalLayoutApi::class) @Composable fun EditChannelDialog( channelSettings: ChannelProtos.ChannelSettings, diff --git a/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt index 9e1c71983..830e00cdf 100644 --- a/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt +++ b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt @@ -19,7 +19,7 @@ package com.geeksville.mesh.util import android.icu.util.LocaleData import android.icu.util.ULocale -import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig +import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import java.util.Locale enum class DistanceUnit( @@ -27,23 +27,23 @@ enum class DistanceUnit( val multiplier: Float, val system: Int ) { - METER("m", multiplier = 1F, DisplayConfig.DisplayUnits.METRIC_VALUE), - KILOMETER("km", multiplier = 0.001F, DisplayConfig.DisplayUnits.METRIC_VALUE), - FOOT("ft", multiplier = 3.28084F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE), - MILE("mi", multiplier = 0.000621371F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE), + METER("m", multiplier = 1F, DisplayUnits.METRIC_VALUE), + KILOMETER("km", multiplier = 0.001F, DisplayUnits.METRIC_VALUE), + FOOT("ft", multiplier = 3.28084F, DisplayUnits.IMPERIAL_VALUE), + MILE("mi", multiplier = 0.000621371F, DisplayUnits.IMPERIAL_VALUE), ; companion object { - fun getFromLocale(locale: Locale = Locale.getDefault()): DisplayConfig.DisplayUnits { + fun getFromLocale(locale: Locale = Locale.getDefault()): DisplayUnits { return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) { - LocaleData.MeasurementSystem.SI -> DisplayConfig.DisplayUnits.METRIC - else -> DisplayConfig.DisplayUnits.IMPERIAL + LocaleData.MeasurementSystem.SI -> DisplayUnits.METRIC + else -> DisplayUnits.IMPERIAL } } else { when (locale.country.uppercase(locale)) { - "US", "LR", "MM", "GB" -> DisplayConfig.DisplayUnits.IMPERIAL - else -> DisplayConfig.DisplayUnits.METRIC + "US", "LR", "MM", "GB" -> DisplayUnits.IMPERIAL + else -> DisplayUnits.METRIC } } } @@ -54,9 +54,9 @@ fun Int.metersIn(unit: DistanceUnit): Float { return this * unit.multiplier } -fun Int.metersIn(system: DisplayConfig.DisplayUnits): Float { +fun Int.metersIn(system: DisplayUnits): Float { val unit = when (system.number) { - DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT + DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT else -> DistanceUnit.METER } return this.metersIn(unit) @@ -70,9 +70,9 @@ fun Float.toString(unit: DistanceUnit): String { }.format(this, unit.symbol) } -fun Float.toString(system: DisplayConfig.DisplayUnits): String { +fun Float.toString(system: DisplayUnits): String { val unit = when (system.number) { - DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT + DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT else -> DistanceUnit.METER } return this.toString(unit) @@ -80,8 +80,8 @@ fun Float.toString(system: DisplayConfig.DisplayUnits): String { private const val KILOMETER_THRESHOLD = 1000 private const val MILE_THRESHOLD = 1609 -fun Int.toDistanceString(system: DisplayConfig.DisplayUnits): String { - val unit = if (system.number == DisplayConfig.DisplayUnits.METRIC_VALUE) { +fun Int.toDistanceString(system: DisplayUnits): String { + val unit = if (system.number == DisplayUnits.METRIC_VALUE) { if (this < KILOMETER_THRESHOLD) DistanceUnit.METER else DistanceUnit.KILOMETER } else { if (this < MILE_THRESHOLD) DistanceUnit.FOOT else DistanceUnit.MILE @@ -89,3 +89,9 @@ fun Int.toDistanceString(system: DisplayConfig.DisplayUnits): String { val valueInUnit = this * unit.multiplier return valueInUnit.toString(unit) } + +@Suppress("MagicNumber") +fun Float.toSpeedString() = when (DistanceUnit.getFromLocale()) { + DisplayUnits.METRIC -> "%.0f km/h".format(this * 3.6) + else -> "%.0f mph".format(this * 2.23694f) +} diff --git a/app/src/main/java/com/geeksville/mesh/util/UnitConversions.kt b/app/src/main/java/com/geeksville/mesh/util/UnitConversions.kt new file mode 100644 index 000000000..f2406109f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/util/UnitConversions.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.util + +import kotlin.math.ln + +object UnitConversions { + + @Suppress("MagicNumber") + fun celsiusToFahrenheit(celsius: Float): Float { + return (celsius * 1.8F) + 32 + } + + fun Float.toTempString(isFahrenheit: Boolean) = if (isFahrenheit) { + val fahrenheit = celsiusToFahrenheit(this) + "%.0f°F".format(fahrenheit) + } else { + "%.0f°C".format(this) + } + + /** + * Calculated the dew point based on the Magnus-Tetens approximation which is a widely used + * formula for calculating dew point temperature. + */ + @Suppress("MagicNumber") + fun calculateDewPoint(tempCelsius: Float, humidity: Float): Float { + val (a, b) = 17.27f to 237.7f + val alpha = (a * tempCelsius) / (b + tempCelsius) + ln(humidity / 100f) + return (b * alpha) / (a - alpha) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f532ccd76..ff5830f7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -595,4 +595,14 @@ Import configuration Export configuration + Hardware + Supported + Node Number + User ID + Uptime + Firmware version + Timestamp + Heading + Sats + Alt