From d5097f57def88c481f77647266e38f3ff201d7ff Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 21 May 2026 20:24:25 -0500 Subject: [PATCH] =?UTF-8?q?refactor(car):=20consolidate=20shared=20utiliti?= =?UTF-8?q?es=20=E2=80=94=20eliminate=20duplicated=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace ConnectionStatus enum with core:model/ConnectionState directly - Fix signal quality thresholds: use core's LoRa-correct SNR (-7/-15) and RSSI (-115/-126) instead of wrong 10/5/0 thresholds - Replace manual formatLastHeard() with DateFormatter.formatRelativeTime() from core:common (already a dependency) - Replace MeshStatusPanel time formatting with DateFormatter - Remove unused time format string resources (car_time_just_now, etc.) - Handle DeviceSleep as disconnected state in car UI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../feature/car/model/CarUiModels.kt | 10 ++---- .../feature/car/panels/MeshStatusPanel.kt | 31 +++++------------ .../car/panels/MeshStatusSessionWiring.kt | 4 +-- .../feature/car/screens/HomeScreen.kt | 4 +-- .../feature/car/screens/NodeDetailScreen.kt | 20 ++--------- .../car/service/CarStateCoordinator.kt | 34 +++++++++---------- feature/car/src/main/res/values/strings.xml | 4 --- 7 files changed, 34 insertions(+), 73 deletions(-) diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/model/CarUiModels.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/model/CarUiModels.kt index be846c461..8b4e6d601 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/model/CarUiModels.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/model/CarUiModels.kt @@ -16,20 +16,16 @@ */ package org.meshtastic.feature.car.model +import org.meshtastic.core.model.ConnectionState + data class CarSessionState( - val connectionStatus: ConnectionStatus, + val connectionStatus: ConnectionState, val onlineNodeCount: Int, val lastMessageTime: Long?, val activeEmergencies: List, val meshName: String?, ) -enum class ConnectionStatus { - CONNECTED, - CONNECTING, - DISCONNECTED, -} - data class MessagingUiState( val channels: List, val selectedChannelIndex: Int, diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusPanel.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusPanel.kt index afda6c2f8..4299cb1a1 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusPanel.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusPanel.kt @@ -20,8 +20,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.DateFormatter +import org.meshtastic.core.model.ConnectionState import org.meshtastic.feature.car.model.CarSessionState -import org.meshtastic.feature.car.model.ConnectionStatus /** * Manages persistent mesh status state for the car display. Provides connection status, node count, and last message @@ -33,7 +34,7 @@ class MeshStatusPanel { private val _state = MutableStateFlow( CarSessionState( - connectionStatus = ConnectionStatus.DISCONNECTED, + connectionStatus = ConnectionState.Disconnected, onlineNodeCount = 0, lastMessageTime = null, activeEmergencies = emptyList(), @@ -42,7 +43,7 @@ class MeshStatusPanel { ) val state: StateFlow = _state.asStateFlow() - fun updateConnectionStatus(status: ConnectionStatus) { + fun updateConnectionStatus(status: ConnectionState) { _state.value = _state.value.copy(connectionStatus = status) } @@ -61,29 +62,15 @@ class MeshStatusPanel { fun getStatusTitle(): String { val state = _state.value return when (state.connectionStatus) { - ConnectionStatus.CONNECTED -> "${state.onlineNodeCount} nodes online" - ConnectionStatus.CONNECTING -> "Connecting..." - ConnectionStatus.DISCONNECTED -> "Disconnected" + ConnectionState.Connected -> "${state.onlineNodeCount} nodes online" + ConnectionState.Connecting -> "Connecting..." + else -> "Disconnected" } } fun getStatusSubtitle(): String? { val state = _state.value - val lastMsg = state.lastMessageTime ?: return null - val elapsed = System.currentTimeMillis() - lastMsg - val timeAgo = - when { - elapsed < MILLIS_PER_MINUTE -> "just now" - elapsed < MILLIS_PER_HOUR -> "${elapsed / MILLIS_PER_MINUTE}m ago" - elapsed < MILLIS_PER_DAY -> "${elapsed / MILLIS_PER_HOUR}h ago" - else -> "${elapsed / MILLIS_PER_DAY}d ago" - } - return "Last msg: $timeAgo" - } - - companion object { - private const val MILLIS_PER_MINUTE = 60_000L - private const val MILLIS_PER_HOUR = 3_600_000L - private const val MILLIS_PER_DAY = 86_400_000L + val lastMsg = state.lastMessageTime?.takeIf { it != 0L } ?: return null + return "Last msg: ${DateFormatter.formatRelativeTime(lastMsg)}" } } diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusSessionWiring.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusSessionWiring.kt index 4ea6192c6..52d4e0f95 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusSessionWiring.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusSessionWiring.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -import org.meshtastic.feature.car.model.ConnectionStatus +import org.meshtastic.core.model.ConnectionState /** Wires MeshStatusPanel to data sources during a car session. Attach in onCreateScreen, detach in onDestroy. */ class MeshStatusSessionWiring(private val panel: MeshStatusPanel) { @@ -30,7 +30,7 @@ class MeshStatusSessionWiring(private val panel: MeshStatusPanel) { fun attach( scope: CoroutineScope, - connectionFlow: Flow, + connectionFlow: Flow, nodeCountFlow: Flow, lastMessageTimeFlow: Flow, meshNameFlow: Flow, diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/HomeScreen.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/HomeScreen.kt index a605d4496..9f049a966 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/HomeScreen.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/HomeScreen.kt @@ -39,8 +39,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import org.meshtastic.core.model.ConnectionState import org.meshtastic.feature.car.R -import org.meshtastic.feature.car.model.ConnectionStatus import org.meshtastic.feature.car.model.NodeUi import org.meshtastic.feature.car.model.SignalQuality import org.meshtastic.feature.car.service.CarStateCoordinator @@ -73,7 +73,7 @@ class HomeScreen(carContext: CarContext, private val stateCoordinator: CarStateC @Suppress("ReturnCount") override fun onGetTemplate(): Template { val connectionStatus = stateCoordinator.sessionState.value.connectionStatus - if (connectionStatus == ConnectionStatus.DISCONNECTED) { + if (connectionStatus == ConnectionState.Disconnected || connectionStatus == ConnectionState.DeviceSleep) { return buildDisconnectedTemplate() } val messaging = stateCoordinator.messagingState.value diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDetailScreen.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDetailScreen.kt index 864b41b4a..64b2379c2 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDetailScreen.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDetailScreen.kt @@ -24,6 +24,7 @@ import androidx.car.app.model.Pane import androidx.car.app.model.PaneTemplate import androidx.car.app.model.Row import androidx.car.app.model.Template +import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.feature.car.R import org.meshtastic.feature.car.model.NodeUi import org.meshtastic.feature.car.model.SignalQuality @@ -107,23 +108,6 @@ class NodeDetailScreen( private fun formatLastHeard(epochMillis: Long): String { if (epochMillis == 0L) return carContext.getString(R.string.car_time_never) - val elapsed = System.currentTimeMillis() - epochMillis - return when { - elapsed < MILLIS_PER_MINUTE -> carContext.getString(R.string.car_time_just_now) - - elapsed < MILLIS_PER_HOUR -> - carContext.getString(R.string.car_time_minutes_ago, (elapsed / MILLIS_PER_MINUTE).toInt()) - - elapsed < MILLIS_PER_DAY -> - carContext.getString(R.string.car_time_hours_ago, (elapsed / MILLIS_PER_HOUR).toInt()) - - else -> carContext.getString(R.string.car_time_days_ago, (elapsed / MILLIS_PER_DAY).toInt()) - } - } - - companion object { - private const val MILLIS_PER_MINUTE = 60_000L - private const val MILLIS_PER_HOUR = 3_600_000L - private const val MILLIS_PER_DAY = 86_400_000L + return DateFormatter.formatRelativeTime(epochMillis) } } diff --git a/feature/car/src/main/kotlin/org/meshtastic/feature/car/service/CarStateCoordinator.kt b/feature/car/src/main/kotlin/org/meshtastic/feature/car/service/CarStateCoordinator.kt index edfc585be..090a72cbf 100644 --- a/feature/car/src/main/kotlin/org/meshtastic/feature/car/service/CarStateCoordinator.kt +++ b/feature/car/src/main/kotlin/org/meshtastic/feature/car/service/CarStateCoordinator.kt @@ -41,7 +41,6 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.feature.car.model.CarSessionState import org.meshtastic.feature.car.model.ChannelUi -import org.meshtastic.feature.car.model.ConnectionStatus import org.meshtastic.feature.car.model.ConversationUi import org.meshtastic.feature.car.model.MessagingUiState import org.meshtastic.feature.car.model.NodeDashboardUiState @@ -80,7 +79,7 @@ class CarStateCoordinator( private val _sessionState = MutableStateFlow( CarSessionState( - connectionStatus = ConnectionStatus.DISCONNECTED, + connectionStatus = ConnectionState.Disconnected, onlineNodeCount = 0, lastMessageTime = null, activeEmergencies = emptyList(), @@ -179,13 +178,7 @@ class CarStateCoordinator( private fun collectConnectionState() { scope.launch { serviceRepository.connectionState.collect { state -> - val status = - when (state) { - ConnectionState.Connected -> ConnectionStatus.CONNECTED - ConnectionState.Connecting -> ConnectionStatus.CONNECTING - else -> ConnectionStatus.DISCONNECTED - } - _sessionState.value = _sessionState.value.copy(connectionStatus = status) + _sessionState.value = _sessionState.value.copy(connectionStatus = state) } } } @@ -272,7 +265,7 @@ class CarStateCoordinator( nodeNum = num, longName = user.long_name.ifEmpty { "Unknown" }, shortName = user.short_name.ifEmpty { "?" }, - signalQuality = snrToSignalQuality(snr), + signalQuality = determineSignalQuality(snr, rssi), batteryPercent = batteryLevel?.takeIf { it in 1..BATTERY_MAX_PERCENT }, isOnline = isOnline, lastHeard = lastHeard.toLong() * SECONDS_TO_MILLIS, @@ -286,15 +279,20 @@ class CarStateCoordinator( private const val DATA_TYPE_TEXT = 1 private const val SECONDS_TO_MILLIS = 1000L private const val BATTERY_MAX_PERCENT = 100 - private const val SNR_EXCELLENT = 10f - private const val SNR_GOOD = 5f - private const val SNR_FAIR = 0f - private fun snrToSignalQuality(snr: Float): SignalQuality = when { - snr == Float.MAX_VALUE -> SignalQuality.UNKNOWN - snr >= SNR_EXCELLENT -> SignalQuality.EXCELLENT - snr >= SNR_GOOD -> SignalQuality.GOOD - snr >= SNR_FAIR -> SignalQuality.FAIR + // Thresholds aligned with core/ui LoraSignalIndicator.kt + private const val SNR_GOOD_THRESHOLD = -7f + private const val SNR_FAIR_THRESHOLD = -15f + private const val RSSI_GOOD_THRESHOLD = -115 + private const val RSSI_FAIR_THRESHOLD = -126 + + @Suppress("MagicNumber") + private fun determineSignalQuality(snr: Float, rssi: Int): SignalQuality = when { + snr == Float.MAX_VALUE || rssi == Int.MAX_VALUE -> SignalQuality.UNKNOWN + snr > SNR_GOOD_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> SignalQuality.EXCELLENT + snr > SNR_GOOD_THRESHOLD && rssi > RSSI_FAIR_THRESHOLD -> SignalQuality.GOOD + snr > SNR_FAIR_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> SignalQuality.GOOD + snr > SNR_FAIR_THRESHOLD -> SignalQuality.FAIR else -> SignalQuality.POOR } } diff --git a/feature/car/src/main/res/values/strings.xml b/feature/car/src/main/res/values/strings.xml index b0a99500e..55c1e0f59 100644 --- a/feature/car/src/main/res/values/strings.xml +++ b/feature/car/src/main/res/values/strings.xml @@ -36,10 +36,6 @@ Status Messages Nodes - %dd ago - %dh ago - Just now - %dm ago Never %d unread Reply