chore: KMP audit — commonize code, centralize utilities, eliminate dead abstractions (#5133)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-04-14 21:17:50 -05:00
committed by GitHub
parent 50ade01e55
commit 72b981f73b
132 changed files with 2186 additions and 916 deletions

View File

@@ -49,7 +49,7 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.Base64Factory
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.formatUptime
@@ -263,7 +263,7 @@ private fun SignalRow(node: Node) {
if (node.snr != Float.MAX_VALUE) {
InfoItem(
label = stringResource(Res.string.snr),
value = formatString("%.1f dB", node.snr),
value = MetricFormatter.snr(node.snr),
icon = MeshtasticIcons.Snr,
modifier = Modifier.weight(1f),
)
@@ -273,7 +273,7 @@ private fun SignalRow(node: Node) {
if (node.rssi != Int.MAX_VALUE) {
InfoItem(
label = stringResource(Res.string.rssi),
value = formatString("%d dBm", node.rssi),
value = MetricFormatter.rssi(node.rssi),
icon = MeshtasticIcons.Rssi,
modifier = Modifier.weight(1f),
)

View File

@@ -46,12 +46,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.isUnmessageableRole
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.air_utilization
@@ -260,14 +259,14 @@ private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Col
icon = MeshtasticIcons.ChannelUtilization,
contentDescription = stringResource(Res.string.channel_utilization),
label = stringResource(Res.string.channel_utilization),
text = formatString("%.1f%%", thatNode.deviceMetrics.channel_utilization),
text = MetricFormatter.percent(thatNode.deviceMetrics.channel_utilization ?: 0f),
contentColor = contentColor,
)
IconInfo(
icon = MeshtasticIcons.AirUtilization,
contentDescription = stringResource(Res.string.air_utilization),
label = stringResource(Res.string.air_utilization),
text = formatString("%.1f%%", thatNode.deviceMetrics.air_util_tx),
text = MetricFormatter.percent(thatNode.deviceMetrics.air_util_tx ?: 0f),
contentColor = contentColor,
)
}
@@ -320,31 +319,24 @@ private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: C
}
if ((env.temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
formatString("%.1f°F", celsiusToFahrenheit(env.temperature ?: 0f))
} else {
formatString("%.1f°C", env.temperature ?: 0f)
}
val temp = MetricFormatter.temperature(env.temperature ?: 0f, tempInFahrenheit)
items.add { TemperatureInfo(temp = temp, contentColor = contentColor) }
}
if ((env.relative_humidity ?: 0f) != 0f) {
items.add {
HumidityInfo(humidity = formatString("%.0f%%", env.relative_humidity ?: 0f), contentColor = contentColor)
HumidityInfo(humidity = MetricFormatter.humidity(env.relative_humidity ?: 0f), contentColor = contentColor)
}
}
if ((env.barometric_pressure ?: 0f) != 0f) {
items.add {
PressureInfo(pressure = formatString("%.1fhPa", env.barometric_pressure ?: 0f), contentColor = contentColor)
PressureInfo(
pressure = MetricFormatter.pressure(env.barometric_pressure ?: 0f),
contentColor = contentColor,
)
}
}
if ((env.soil_temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
formatString("%.1f°F", celsiusToFahrenheit(env.soil_temperature ?: 0f))
} else {
formatString("%.1f°C", env.soil_temperature ?: 0f)
}
val temp = MetricFormatter.temperature(env.soil_temperature ?: 0f, tempInFahrenheit)
items.add { SoilTemperatureInfo(temp = temp, contentColor = contentColor) }
}
if ((env.soil_moisture ?: 0) != 0 && (env.soil_temperature ?: 0f) != 0f) {
@@ -353,7 +345,7 @@ private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: C
if ((env.voltage ?: 0f) != 0f) {
items.add {
PowerInfo(
value = formatString("%.2fV", env.voltage ?: 0f),
value = MetricFormatter.voltage(env.voltage ?: 0f),
label = stringResource(Res.string.voltage),
contentColor = contentColor,
)
@@ -362,7 +354,7 @@ private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: C
if ((env.current ?: 0f) != 0f) {
items.add {
PowerInfo(
value = formatString("%.1fmA", env.current ?: 0f),
value = MetricFormatter.current(env.current ?: 0f),
label = stringResource(Res.string.current),
contentColor = contentColor,
)

View File

@@ -72,7 +72,7 @@ fun NodeListScreen(
onNavigateToChannels: () -> Unit = {},
scrollToTopEvents: Flow<ScrollToTopEvent>? = null,
activeNodeId: Int? = null,
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
val showToast = org.meshtastic.core.ui.util.rememberShowToastResource()
val scope = rememberCoroutineScope()
@@ -125,7 +125,7 @@ fun NodeListScreen(
alignment = androidx.compose.ui.Alignment.BottomEnd,
),
onImport = { uriString ->
onHandleDeepLink(org.meshtastic.core.common.util.MeshtasticUri(uriString)) {
onHandleDeepLink(org.meshtastic.core.common.util.CommonUri.parse(uriString)) {
scope.launch { showToast(Res.string.channel_invalid) }
}
},

View File

@@ -55,6 +55,8 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.TelemetryType
@@ -230,12 +232,13 @@ private fun DeviceMetricsChart(
ChartStyling.rememberMarker(
valueFormatter =
ChartStyling.createColoredMarkerValueFormatter { value, color ->
val formatted = NumberFormatter.format(value, 1)
when (color) {
batteryColor -> formatString(percentValueTemplate, batteryLabel, value)
voltageColor -> formatString(voltageValueTemplate, voltageLabel, value)
chUtilColor -> formatString(percentValueTemplate, channelUtilizationLabel, value)
airUtilColor -> formatString(percentValueTemplate, airUtilizationLabel, value)
else -> formatString(numericValueTemplate, value)
batteryColor -> formatString(percentValueTemplate, batteryLabel, formatted)
voltageColor -> formatString(voltageValueTemplate, voltageLabel, formatted)
chUtilColor -> formatString(percentValueTemplate, channelUtilizationLabel, formatted)
airUtilColor -> formatString(percentValueTemplate, airUtilizationLabel, formatted)
else -> formatString(numericValueTemplate, formatted)
}
},
)
@@ -337,7 +340,7 @@ private fun DeviceMetricsChart(
if (leftLayer != null) {
VerticalAxis.rememberStart(
label = ChartStyling.rememberAxisLabel(color = batteryColor),
valueFormatter = { _, value, _ -> formatString("%.0f%%", value) },
valueFormatter = { _, value, _ -> MetricFormatter.percent(value.toFloat(), 0) },
)
} else {
null
@@ -346,7 +349,7 @@ private fun DeviceMetricsChart(
if (rightLayer != null) {
VerticalAxis.rememberEnd(
label = ChartStyling.rememberAxisLabel(color = voltageColor),
valueFormatter = { _, value, _ -> formatString("%.1f V", value) },
valueFormatter = { _, value, _ -> "${NumberFormatter.format(value.toFloat(), 1)} V" },
)
} else {
null
@@ -441,7 +444,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
formatString(
percentValueTemplate,
channelUtilizationLabel,
deviceMetrics.channel_utilization ?: 0f,
NumberFormatter.format(deviceMetrics.channel_utilization ?: 0f, 1),
),
)
Spacer(Modifier.width(12.dp))
@@ -453,7 +456,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
formatString(
percentValueTemplate,
airUtilizationLabel,
deviceMetrics.air_util_tx ?: 0f,
NumberFormatter.format(deviceMetrics.air_util_tx ?: 0f, 1),
),
)
}

View File

@@ -37,7 +37,7 @@ import okio.ByteString.Companion.decodeBase64
import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.InjectedParam
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.di.CoroutineDispatchers
@@ -333,7 +333,7 @@ open class MetricsViewModel(
* epoch-seconds timestamp extracted by [epochSeconds].
*/
private fun <T> exportCsv(
uri: MeshtasticUri,
uri: CommonUri,
header: String,
rows: List<T>,
epochSeconds: (T) -> Long,
@@ -351,11 +351,10 @@ open class MetricsViewModel(
}
}
fun savePositionCSV(uri: MeshtasticUri, data: List<org.meshtastic.proto.Position>) {
fun savePositionCSV(uri: CommonUri, data: List<org.meshtastic.proto.Position>) {
exportCsv(
uri = uri,
header =
"\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\"," + "\"satsInView\",\"speed\",\"heading\"\n",
header = "\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"\n",
rows = data,
epochSeconds = { it.time.toLong() },
) { pos ->
@@ -366,7 +365,7 @@ open class MetricsViewModel(
}
}
fun saveDeviceMetricsCSV(uri: MeshtasticUri, data: List<Telemetry>) {
fun saveDeviceMetricsCSV(uri: CommonUri, data: List<Telemetry>) {
exportCsv(
uri = uri,
header =
@@ -382,7 +381,7 @@ open class MetricsViewModel(
}
}
fun saveEnvironmentMetricsCSV(uri: MeshtasticUri, data: List<Telemetry>) {
fun saveEnvironmentMetricsCSV(uri: CommonUri, data: List<Telemetry>) {
val oneWireHeaders = (1..ONE_WIRE_SENSOR_COUNT).joinToString(",") { "\"oneWireTemp$it\"" }
exportCsv(
uri = uri,
@@ -405,7 +404,7 @@ open class MetricsViewModel(
}
}
fun saveSignalMetricsCSV(uri: MeshtasticUri, data: List<MeshPacket>) {
fun saveSignalMetricsCSV(uri: CommonUri, data: List<MeshPacket>) {
exportCsv(
uri = uri,
header = "\"date\",\"time\",\"rssi\",\"snr\"\n",
@@ -416,7 +415,7 @@ open class MetricsViewModel(
}
}
fun savePowerMetricsCSV(uri: MeshtasticUri, data: List<Telemetry>) {
fun savePowerMetricsCSV(uri: CommonUri, data: List<Telemetry>) {
exportCsv(
uri = uri,
header =

View File

@@ -54,7 +54,8 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC
import org.meshtastic.core.resources.Res
@@ -194,9 +195,9 @@ private fun PowerMetricsChart(
valueFormatter =
ChartStyling.createColoredMarkerValueFormatter { value, color ->
when (color) {
currentColor -> formatString("Current: %.0f mA", value)
voltageColor -> formatString("Voltage: %.1f V", value)
else -> formatString("%.1f", value)
currentColor -> "Current: ${MetricFormatter.current(value.toFloat(), 0)}"
voltageColor -> "Voltage: ${NumberFormatter.format(value.toFloat(), 1)} V"
else -> NumberFormatter.format(value.toFloat(), 1)
}
},
)
@@ -256,7 +257,7 @@ private fun PowerMetricsChart(
if (currentData.isNotEmpty()) {
VerticalAxis.rememberStart(
label = ChartStyling.rememberAxisLabel(color = currentColor),
valueFormatter = { _, value, _ -> formatString("%.0f mA", value) },
valueFormatter = { _, value, _ -> MetricFormatter.current(value.toFloat(), 0) },
)
} else {
null
@@ -265,7 +266,7 @@ private fun PowerMetricsChart(
if (voltageData.isNotEmpty()) {
VerticalAxis.rememberEnd(
label = ChartStyling.rememberAxisLabel(color = voltageColor),
valueFormatter = { _, value, _ -> formatString("%.1f V", value) },
valueFormatter = { _, value, _ -> "${NumberFormatter.format(value.toFloat(), 1)} V" },
)
} else {
null
@@ -369,8 +370,8 @@ private fun PowerChannelColumn(titleRes: StringResource, voltage: Float, current
style = TextStyle(fontWeight = FontWeight.Bold),
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
MetricValueRow(color = PowerMetric.VOLTAGE.color, text = formatString("%.2fV", voltage))
MetricValueRow(color = PowerMetric.CURRENT.color, text = formatString("%.1fmA", current))
MetricValueRow(color = PowerMetric.VOLTAGE.color, text = MetricFormatter.voltage(voltage))
MetricValueRow(color = PowerMetric.CURRENT.color, text = MetricFormatter.current(current))
}
}

View File

@@ -47,7 +47,7 @@ import com.patrykandpatrick.vico.compose.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.compose.cartesian.data.lineSeries
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC
import org.meshtastic.core.resources.Res
@@ -157,9 +157,9 @@ private fun SignalMetricsChart(
valueFormatter =
ChartStyling.createColoredMarkerValueFormatter { value, color ->
if (color == rssiColor) {
formatString("RSSI: %.0f dBm", value)
"RSSI: ${MetricFormatter.rssi(value.toInt())}"
} else {
formatString("SNR: %.1f dB", value)
"SNR: ${MetricFormatter.snr(value.toFloat())}"
}
},
)
@@ -189,7 +189,7 @@ private fun SignalMetricsChart(
if (rssiData.isNotEmpty()) {
VerticalAxis.rememberStart(
label = ChartStyling.rememberAxisLabel(color = rssiColor),
valueFormatter = { _, value, _ -> formatString("%.0f dBm", value) },
valueFormatter = { _, value, _ -> MetricFormatter.rssi(value.toInt()) },
)
} else {
null
@@ -198,7 +198,7 @@ private fun SignalMetricsChart(
if (snrData.isNotEmpty()) {
VerticalAxis.rememberEnd(
label = ChartStyling.rememberAxisLabel(color = snrColor),
valueFormatter = { _, value, _ -> formatString("%.1f dB", value) },
valueFormatter = { _, value, _ -> MetricFormatter.snr(value.toFloat()) },
)
} else {
null
@@ -234,15 +234,9 @@ private fun SignalMetricsCard(meshPacket: MeshPacket, isSelected: Boolean, onCli
/* SNR and RSSI */
Row(verticalAlignment = Alignment.CenterVertically) {
MetricValueRow(
color = SignalMetric.RSSI.color,
text = formatString("%.0f dBm", meshPacket.rx_rssi.toFloat()),
)
MetricValueRow(color = SignalMetric.RSSI.color, text = MetricFormatter.rssi(meshPacket.rx_rssi))
Spacer(Modifier.width(12.dp))
MetricValueRow(
color = SignalMetric.SNR.color,
text = formatString("%.1f dB", meshPacket.rx_snr),
)
MetricValueRow(color = SignalMetric.SNR.color, text = MetricFormatter.snr(meshPacket.rx_snr))
}
}
}

View File

@@ -56,6 +56,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.model.TracerouteOverlay
import org.meshtastic.core.model.fullRouteDiscovery
@@ -113,7 +114,7 @@ fun TracerouteLogScreen(
val headerTowardsStr = stringResource(Res.string.traceroute_route_towards_dest)
val headerBackStr = stringResource(Res.string.traceroute_route_back_to_us)
val durationTemplate = stringResource(Res.string.traceroute_duration, "%SECS%")
val durationFormatStr = stringResource(Res.string.traceroute_duration)
val threshold = timeFrame.timeThreshold()
val filteredRequests =
@@ -176,7 +177,7 @@ fun TracerouteLogScreen(
getUsername = ::getUsername,
headerTowards = headerTowardsStr,
headerBack = headerBackStr,
durationTemplate = durationTemplate,
durationTemplate = durationFormatStr,
statusGreen = statusGreen,
statusYellow = statusYellow,
statusOrange = statusOrange,
@@ -335,7 +336,7 @@ private fun showTracerouteDetail(
statusYellow = statusYellow,
statusOrange = statusOrange,
)
val durationText = durationTemplate.replace("%SECS%", formatString("%.1f", seconds))
val durationText = formatString(durationTemplate, NumberFormatter.format(seconds, 1))
buildAnnotatedString {
append(annotatedBase)
append("\n\n$durationText")

View File

@@ -31,7 +31,7 @@ import org.meshtastic.feature.node.list.NodeListViewModel
fun AdaptiveNodeListScreen(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
val nodeListViewModel: NodeListViewModel = koinViewModel()

View File

@@ -73,7 +73,7 @@ import kotlin.reflect.KClass
fun EntryProviderScope<NavKey>.nodesGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoute.NodesGraph>(metadata = { ListDetailSceneStrategy.listPane() }) {
AdaptiveNodeListScreen(
@@ -99,7 +99,7 @@ fun EntryProviderScope<NavKey>.nodesGraph(
fun EntryProviderScope<NavKey>.nodeDetailGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
onHandleDeepLink: (org.meshtastic.core.common.util.CommonUri, onInvalid: () -> Unit) -> Unit = { _, _ -> },
) {
entry<NodesRoute.NodeDetailGraph>(metadata = { ListDetailSceneStrategy.listPane() }) { args ->
AdaptiveNodeListScreen(

View File

@@ -35,7 +35,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import okio.Buffer
import okio.BufferedSink
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogRepository
@@ -210,7 +210,7 @@ class MetricsViewModelTest {
awaitItem() // Empty
awaitItem() // with position
val uri = MeshtasticUri("content://test")
val uri = CommonUri.parse("content://test")
vm.savePositionCSV(uri, listOf(testPosition))
runCurrent()