mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-24 23:01:22 -04:00
refactor(car): consolidate shared utilities — eliminate duplicated logic
- 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>
This commit is contained in:
@@ -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<EmergencyAlert>,
|
||||
val meshName: String?,
|
||||
)
|
||||
|
||||
enum class ConnectionStatus {
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
}
|
||||
|
||||
data class MessagingUiState(
|
||||
val channels: List<ChannelUi>,
|
||||
val selectedChannelIndex: Int,
|
||||
|
||||
@@ -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<CarSessionState> = _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)}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ConnectionStatus>,
|
||||
connectionFlow: Flow<ConnectionState>,
|
||||
nodeCountFlow: Flow<Int>,
|
||||
lastMessageTimeFlow: Flow<Long>,
|
||||
meshNameFlow: Flow<String?>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@
|
||||
<string name="car_status_status">Status</string>
|
||||
<string name="car_tab_messages">Messages</string>
|
||||
<string name="car_tab_nodes">Nodes</string>
|
||||
<string name="car_time_days_ago">%dd ago</string>
|
||||
<string name="car_time_hours_ago">%dh ago</string>
|
||||
<string name="car_time_just_now">Just now</string>
|
||||
<string name="car_time_minutes_ago">%dm ago</string>
|
||||
<string name="car_time_never">Never</string>
|
||||
<string name="car_unread_badge">%d unread</string>
|
||||
<string name="car_voice_reply">Reply</string>
|
||||
|
||||
Reference in New Issue
Block a user