Skip to content

Commit 7a61af0

Browse files
committed
feat(discovery): improve ux and reliability of scans
- Add `KeepScreenOn(isScanning)` and a user toggle to prevent Android Doze mode from sleeping the CPU and dropping packets during long scans. - Suppress detekt warnings for complex engine processing paths. - Add `reset()` hook to break Jetpack Compose LaunchedEffect recomposition loop when returning from the summary screen. - Dynamically label the user's active preset with a '(Home)' suffix so they know what the radio will return to after scanning. - Remove unused `SavedStateHandle` imports after Koin refactor.
1 parent 099d342 commit 7a61af0

8 files changed

Lines changed: 70 additions & 23 deletions

File tree

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryDetailViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717
package org.meshtastic.feature.discovery
1818

19-
import androidx.lifecycle.SavedStateHandle
2019
import androidx.lifecycle.ViewModel
2120
import kotlinx.coroutines.flow.MutableStateFlow
2221
import kotlinx.coroutines.flow.StateFlow

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryMapViewModel.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717
package org.meshtastic.feature.discovery
1818

19-
import androidx.lifecycle.SavedStateHandle
2019
import androidx.lifecycle.ViewModel
2120
import kotlinx.coroutines.flow.MutableStateFlow
2221
import kotlinx.coroutines.flow.StateFlow
@@ -30,10 +29,8 @@ import org.meshtastic.core.ui.viewmodel.safeLaunch
3029
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
3130

3231
@KoinViewModel
33-
class DiscoveryMapViewModel(
34-
@InjectedParam private val sessionId: Long,
35-
private val discoveryDao: DiscoveryDao,
36-
) : ViewModel() {
32+
class DiscoveryMapViewModel(@InjectedParam private val sessionId: Long, private val discoveryDao: DiscoveryDao) :
33+
ViewModel() {
3734

3835
val session: StateFlow<DiscoverySessionEntity?> =
3936
discoveryDao.getSessionFlow(sessionId).stateInWhileSubscribed(initialValue = null)

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,7 @@ class DiscoveryScanEngine(
197197
_scanState.value = DiscoveryScanState.Idle
198198

199199
// Restore home preset in the background so we don't block the UI with the connection wait
200-
CoroutineScope(Dispatchers.Default).launch {
201-
restoreHomePreset()
202-
}
200+
CoroutineScope(Dispatchers.Default).launch { restoreHomePreset() }
203201
}
204202

205203
/** Resets engine state after the UI has acknowledged completion. */
@@ -212,6 +210,7 @@ class DiscoveryScanEngine(
212210

213211
// region DiscoveryPacketCollector
214212

213+
@Suppress("CyclomaticComplexMethod", "ComplexCondition")
215214
override suspend fun onPacketReceived(meshPacket: MeshPacket, dataPacket: DataPacket) {
216215
if (_scanState.value !is DiscoveryScanState.Dwell) return
217216
val fromNum = meshPacket.from.toLong()
@@ -234,14 +233,20 @@ class DiscoveryScanEngine(
234233
}
235234
}
236235

237-
// Ensure all nodes in the collection have names if available in the NodeDB
236+
// Ensure all nodes in the collection have names and position if available in the NodeDB
238237
collectedNodes.values.forEach { n ->
239-
if (n.shortName == null || n.longName == null) {
240-
val dbNode = nodeRepository.nodeDBbyNum.value[n.nodeNum.toInt()]
241-
if (dbNode != null) {
238+
val dbNode = nodeRepository.nodeDBbyNum.value[n.nodeNum.toInt()]
239+
if (dbNode != null) {
240+
if (n.shortName == null || n.longName == null) {
242241
n.shortName = dbNode.user.short_name.ifBlank { null }
243242
n.longName = dbNode.user.long_name.ifBlank { null }
244243
}
244+
if (n.latitude == null || n.longitude == null || (n.latitude == 0.0 && n.longitude == 0.0)) {
245+
val dbLat = dbNode.position.latitude_i
246+
val dbLon = dbNode.position.longitude_i
247+
if (dbLat != null && dbLat != 0) n.latitude = dbLat.toDouble() / POSITION_DIVISOR
248+
if (dbLon != null && dbLon != 0) n.longitude = dbLon.toDouble() / POSITION_DIVISOR
249+
}
245250
}
246251
}
247252
}

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717
package org.meshtastic.feature.discovery
1818

19-
import androidx.lifecycle.SavedStateHandle
2019
import androidx.lifecycle.ViewModel
2120
import kotlinx.coroutines.flow.MutableStateFlow
2221
import kotlinx.coroutines.flow.StateFlow

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.meshtastic.core.database.dao.DiscoveryDao
2727
import org.meshtastic.core.database.entity.DiscoverySessionEntity
2828
import org.meshtastic.core.model.ChannelOption
2929
import org.meshtastic.core.model.ConnectionState
30+
import org.meshtastic.core.repository.RadioConfigRepository
3031
import org.meshtastic.core.repository.ServiceRepository
3132
import org.meshtastic.core.ui.viewmodel.safeLaunch
3233
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
@@ -36,13 +37,22 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
3637
class DiscoveryViewModel(
3738
private val scanEngine: DiscoveryScanEngine,
3839
private val serviceRepository: ServiceRepository,
40+
radioConfigRepository: RadioConfigRepository,
3941
discoveryDao: DiscoveryDao,
4042
) : ViewModel() {
4143

4244
val scanState: StateFlow<DiscoveryScanState> = scanEngine.scanState
4345
val currentSession: StateFlow<DiscoverySessionEntity?> = scanEngine.currentSession
4446
val connectionState: StateFlow<ConnectionState> = serviceRepository.connectionState
4547

48+
val homePreset: StateFlow<ChannelOption> =
49+
radioConfigRepository.localConfigFlow
50+
.map { localConfig ->
51+
val presetEnum = localConfig.lora?.modem_preset
52+
ChannelOption.entries.firstOrNull { it.modemPreset == presetEnum } ?: ChannelOption.DEFAULT
53+
}
54+
.stateInWhileSubscribed(initialValue = ChannelOption.DEFAULT)
55+
4656
private val _selectedPresets = MutableStateFlow<Set<ChannelOption>>(emptySet())
4757
val selectedPresets: StateFlow<Set<ChannelOption>> = _selectedPresets.asStateFlow()
4858

@@ -78,6 +88,10 @@ class DiscoveryViewModel(
7888
safeLaunch(tag = "stopScan") { scanEngine.stopScan() }
7989
}
8090

91+
fun reset() {
92+
scanEngine.reset()
93+
}
94+
8195
companion object {
8296
private const val DEFAULT_DWELL_MINUTES = 15
8397
private const val SECONDS_PER_MINUTE = 60L

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryHistoryDetailScreen.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ import androidx.compose.ui.unit.dp
4444
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4545
import org.meshtastic.core.database.entity.DiscoverySessionEntity
4646
import org.meshtastic.core.ui.icon.ArrowBack
47-
import org.meshtastic.core.ui.icon.MeshtasticIcons
4847
import org.meshtastic.core.ui.icon.Map
48+
import org.meshtastic.core.ui.icon.MeshtasticIcons
4949
import org.meshtastic.feature.discovery.DiscoveryHistoryDetailViewModel
5050
import org.meshtastic.feature.discovery.ui.component.PresetResultCard
5151

@@ -69,7 +69,10 @@ fun DiscoveryHistoryDetailScreen(
6969
},
7070
actions = {
7171
val s = session
72-
val hasAnyMappableNodes = nodesByPreset.values.flatten().any { it.latitude != null && it.longitude != null && it.latitude != 0.0 }
72+
val hasAnyMappableNodes =
73+
nodesByPreset.values.flatten().any {
74+
it.latitude != null && it.longitude != null && it.latitude != 0.0
75+
}
7376
if (s != null && (s.userLatitude != 0.0 || hasAnyMappableNodes)) {
7477
IconButton(onClick = { onNavigateToMap(s.id) }) {
7578
Icon(MeshtasticIcons.Map, contentDescription = "View map")
@@ -88,10 +91,7 @@ fun DiscoveryHistoryDetailScreen(
8891
if (presetResults.isNotEmpty()) {
8992
Text(text = "Preset Results", style = MaterialTheme.typography.titleMedium)
9093
presetResults.forEach { result ->
91-
PresetResultCard(
92-
result = result,
93-
nodes = nodesByPreset[result.id].orEmpty(),
94-
)
94+
PresetResultCard(result = result, nodes = nodesByPreset[result.id].orEmpty())
9595
}
9696
}
9797
}

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,20 @@ import androidx.compose.runtime.LaunchedEffect
4848
import androidx.compose.runtime.getValue
4949
import androidx.compose.runtime.mutableStateOf
5050
import androidx.compose.runtime.remember
51+
import androidx.compose.runtime.saveable.rememberSaveable
5152
import androidx.compose.runtime.setValue
5253
import androidx.compose.ui.Alignment
5354
import androidx.compose.ui.Modifier
5455
import androidx.compose.ui.unit.dp
5556
import androidx.lifecycle.compose.collectAsStateWithLifecycle
57+
import org.meshtastic.core.ui.component.SwitchPreference
5658
import org.meshtastic.core.ui.icon.ArrowBack
5759
import org.meshtastic.core.ui.icon.Close
5860
import org.meshtastic.core.ui.icon.History
5961
import org.meshtastic.core.ui.icon.MeshtasticIcons
6062
import org.meshtastic.core.ui.icon.PlayArrow
6163
import org.meshtastic.core.ui.icon.Warning
64+
import org.meshtastic.core.ui.util.KeepScreenOn
6265
import org.meshtastic.feature.discovery.DiscoveryScanState
6366
import org.meshtastic.feature.discovery.DiscoveryViewModel
6467
import org.meshtastic.feature.discovery.ui.component.DwellProgressIndicator
@@ -84,11 +87,21 @@ fun DiscoveryScanScreen(
8487
val dwellMinutes by viewModel.dwellDurationMinutes.collectAsStateWithLifecycle()
8588
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle()
8689
val currentSession by viewModel.currentSession.collectAsStateWithLifecycle()
90+
val homePreset by viewModel.homePreset.collectAsStateWithLifecycle()
91+
92+
var keepScreenAwake by rememberSaveable { mutableStateOf(true) }
93+
val isScanning = scanState !is DiscoveryScanState.Idle
94+
95+
// Keep screen awake while a scan is in progress
96+
KeepScreenOn(isScanning && keepScreenAwake)
8797

8898
// Navigate to summary when scan completes
8999
LaunchedEffect(scanState) {
90100
if (scanState is DiscoveryScanState.Complete) {
91-
currentSession?.id?.let(onNavigateToSummary)
101+
currentSession?.id?.let { sessionId ->
102+
viewModel.reset()
103+
onNavigateToSummary(sessionId)
104+
}
92105
}
93106
}
94107

@@ -129,7 +142,6 @@ fun DiscoveryScanScreen(
129142
}
130143
},
131144
) { padding ->
132-
val isScanning = scanState !is DiscoveryScanState.Idle
133145
LazyColumn(
134146
contentPadding = padding,
135147
verticalArrangement = Arrangement.spacedBy(SECTION_SPACING),
@@ -145,6 +157,7 @@ fun DiscoveryScanScreen(
145157
item(key = "preset_picker") {
146158
PresetPickerCard(
147159
selectedPresets = selectedPresets,
160+
homePreset = homePreset,
148161
onTogglePreset = viewModel::togglePreset,
149162
enabled = true,
150163
)
@@ -158,6 +171,11 @@ fun DiscoveryScanScreen(
158171
enabled = true,
159172
)
160173
}
174+
175+
// Keep awake toggle
176+
item(key = "keep_awake_toggle") {
177+
KeepAwakeToggleCard(keepAwake = keepScreenAwake, onToggle = { keepScreenAwake = it })
178+
}
161179
}
162180

163181
// Scan progress section
@@ -171,6 +189,19 @@ fun DiscoveryScanScreen(
171189
}
172190
}
173191

192+
@Composable
193+
private fun KeepAwakeToggleCard(keepAwake: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier) {
194+
ElevatedCard(modifier = modifier.fillMaxWidth()) {
195+
SwitchPreference(
196+
title = "Keep screen awake",
197+
summary = "Prevents Android Doze mode from dropping radio packets during long scans. Recommended.",
198+
checked = keepAwake,
199+
enabled = true,
200+
onCheckedChange = onToggle,
201+
)
202+
}
203+
}
204+
174205
@Composable
175206
private fun ConnectionWarningCard(modifier: Modifier = Modifier) {
176207
ElevatedCard(modifier = modifier.fillMaxWidth()) {

feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/PresetPickerCard.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal fun ChannelOption.displayName(): String =
4949
@Composable
5050
fun PresetPickerCard(
5151
selectedPresets: Set<ChannelOption>,
52+
homePreset: ChannelOption,
5253
onTogglePreset: (ChannelOption) -> Unit,
5354
enabled: Boolean,
5455
modifier: Modifier = Modifier,
@@ -69,10 +70,11 @@ fun PresetPickerCard(
6970
) {
7071
ChannelOption.entries.forEach { preset ->
7172
val selected = preset in selectedPresets
73+
val isHome = preset == homePreset
7274
FilterChip(
7375
selected = selected,
7476
onClick = { onTogglePreset(preset) },
75-
label = { Text(preset.displayName()) },
77+
label = { Text(if (isHome) "${preset.displayName()} (Home)" else preset.displayName()) },
7678
enabled = enabled,
7779
leadingIcon =
7880
if (selected) {

0 commit comments

Comments
 (0)