Skip to content

Commit 69ca714

Browse files
committed
fix(discovery): fix scan engine bugs and nav crash
- Add use_preset=true and use setLocalConfig to shift presets. - Fix ConcurrentModificationExceptions in persistCurrentDwellResults by locking. - Only collect packets while dwelling, preventing misattributions during shift. - Increase reconnect timeout to 60s for slow radios. - Fetch neighbor names from NodeDB for better neighbor matching. - Add DiscoveryRoute to MeshtasticNavSavedStateConfig to fix serialization crash.
1 parent 5e71598 commit 69ca714

2 files changed

Lines changed: 84 additions & 59 deletions

File tree

core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ val MeshtasticNavSavedStateConfig = SavedStateConfiguration {
4040
subclassesOfSealed<SettingsRoute>()
4141
subclassesOfSealed<FirmwareRoute>()
4242
subclassesOfSealed<WifiProvisionRoute>()
43+
subclassesOfSealed<DiscoveryRoute>()
4344
}
4445
}
4546
}

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

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class DiscoveryScanEngine(
201201
// region DiscoveryPacketCollector
202202

203203
override suspend fun onPacketReceived(meshPacket: MeshPacket, dataPacket: DataPacket) {
204-
if (!isActive) return
204+
if (_scanState.value !is DiscoveryScanState.Dwell) return
205205
val fromNum = meshPacket.from.toLong()
206206
val portNum = meshPacket.decoded?.portnum ?: return
207207

@@ -222,12 +222,14 @@ class DiscoveryScanEngine(
222222
}
223223
}
224224

225-
// Try to fill in names from the live NodeDB
226-
if (node.shortName == null || node.longName == null) {
227-
val dbNode = nodeRepository.nodeDBbyNum.value[fromNum.toInt()]
228-
if (dbNode != null) {
229-
node.shortName = dbNode.user.short_name.ifBlank { null }
230-
node.longName = dbNode.user.long_name.ifBlank { null }
225+
// Ensure all nodes in the collection have names if available in the NodeDB
226+
collectedNodes.values.forEach { n ->
227+
if (n.shortName == null || n.longName == null) {
228+
val dbNode = nodeRepository.nodeDBbyNum.value[n.nodeNum.toInt()]
229+
if (dbNode != null) {
230+
n.shortName = dbNode.user.short_name.ifBlank { null }
231+
n.longName = dbNode.user.long_name.ifBlank { null }
232+
}
231233
}
232234
}
233235
}
@@ -243,8 +245,10 @@ class DiscoveryScanEngine(
243245
if (!isActive) return
244246

245247
currentPresetName = preset.name
246-
mutex.withLock { collectedNodes.clear() }
247-
deviceMetricsLog.clear()
248+
mutex.withLock {
249+
collectedNodes.clear()
250+
deviceMetricsLog.clear()
251+
}
248252
totalDwellSeconds = dwellDurationSeconds
249253

250254
// Shift to the new preset
@@ -263,7 +267,14 @@ class DiscoveryScanEngine(
263267
}
264268

265269
// Dwell
266-
runDwell(preset.name, dwellDurationSeconds)
270+
val dwellCompleted = runDwell(preset.name, dwellDurationSeconds)
271+
if (!dwellCompleted) {
272+
cancelScanInternal()
273+
restoreHomePreset()
274+
finalizeSession("paused")
275+
_scanState.value = DiscoveryScanState.Idle
276+
return
277+
}
267278
if (!isActive) return
268279

269280
// Persist this preset's results
@@ -279,11 +290,13 @@ class DiscoveryScanEngine(
279290
}
280291

281292
private suspend fun shiftPreset(preset: ChannelOption) {
282-
val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: return
283-
val loraConfig = Config.LoRaConfig(modem_preset = preset.modemPreset)
293+
val loraConfig = Config.LoRaConfig(use_preset = true, modem_preset = preset.modemPreset)
284294
val config = Config(lora = loraConfig)
285-
radioController.setConfig(myNodeNum, config, radioController.getPacketId())
286-
Logger.i { "DiscoveryScanEngine: shifted to ${preset.name}" }
295+
radioController.setLocalConfig(config)
296+
Logger.i { "DiscoveryScanEngine: shifted to ${preset.name} (use_preset=true)" }
297+
// The firmware often restarts the radio or reboots after a LoRa config change.
298+
// Wait a short moment to ensure we don't consider it 'connected' right before it drops.
299+
delay(3000)
287300
}
288301

289302
private suspend fun waitForConnection(): Boolean {
@@ -294,9 +307,17 @@ class DiscoveryScanEngine(
294307
return result != null
295308
}
296309

297-
private suspend fun runDwell(presetName: String, durationSeconds: Long) {
310+
private suspend fun runDwell(presetName: String, durationSeconds: Long): Boolean {
298311
var remaining = durationSeconds
299312
while (remaining > 0 && isActive) {
313+
val isConnected = serviceRepository.connectionState.value is ConnectionState.Connected
314+
if (!isConnected) {
315+
_scanState.value = DiscoveryScanState.Reconnecting(presetName)
316+
val reconnected = waitForConnection()
317+
if (!reconnected) return false
318+
continue
319+
}
320+
300321
_scanState.value =
301322
DiscoveryScanState.Dwell(
302323
presetName = presetName,
@@ -306,6 +327,7 @@ class DiscoveryScanEngine(
306327
delay(TICK_INTERVAL_MS)
307328
remaining--
308329
}
330+
return true
309331
}
310332

311333
// endregion
@@ -384,55 +406,57 @@ class DiscoveryScanEngine(
384406

385407
private suspend fun persistCurrentDwellResults() {
386408
if (sessionId == 0L) return
387-
if (collectedNodes.isEmpty()) {
388-
// Persist a zero-result entry so the preset appears in reports
389-
val emptyResult =
409+
mutex.withLock {
410+
if (collectedNodes.isEmpty()) {
411+
// Persist a zero-result entry so the preset appears in reports
412+
val emptyResult =
413+
DiscoveryPresetResultEntity(
414+
sessionId = sessionId,
415+
presetName = currentPresetName,
416+
dwellDurationSeconds = totalDwellSeconds,
417+
)
418+
discoveryDao.insertPresetResult(emptyResult)
419+
return
420+
}
421+
422+
val (avgChannelUtil, avgAirUtil) = computeAverageMetrics()
423+
val directCount = collectedNodes.values.count { it.neighborType == "direct" }
424+
val meshCount = collectedNodes.values.count { it.neighborType == "mesh" }
425+
426+
val presetResult =
390427
DiscoveryPresetResultEntity(
391428
sessionId = sessionId,
392429
presetName = currentPresetName,
393430
dwellDurationSeconds = totalDwellSeconds,
431+
uniqueNodes = collectedNodes.size,
432+
directNeighborCount = directCount,
433+
meshNeighborCount = meshCount,
434+
messageCount = collectedNodes.values.sumOf { it.messageCount },
435+
sensorPacketCount = collectedNodes.values.sumOf { it.sensorPacketCount },
436+
avgChannelUtilization = avgChannelUtil,
437+
avgAirtimeRate = avgAirUtil,
394438
)
395-
discoveryDao.insertPresetResult(emptyResult)
396-
return
439+
val presetResultId = discoveryDao.insertPresetResult(presetResult)
440+
441+
val nodeEntities =
442+
collectedNodes.values.map { data ->
443+
DiscoveredNodeEntity(
444+
presetResultId = presetResultId,
445+
nodeNum = data.nodeNum,
446+
shortName = data.shortName,
447+
longName = data.longName,
448+
neighborType = data.neighborType,
449+
latitude = data.latitude,
450+
longitude = data.longitude,
451+
hopCount = data.hopCount,
452+
snr = data.snr,
453+
rssi = data.rssi,
454+
messageCount = data.messageCount,
455+
sensorPacketCount = data.sensorPacketCount,
456+
)
457+
}
458+
discoveryDao.insertDiscoveredNodes(nodeEntities)
397459
}
398-
399-
val (avgChannelUtil, avgAirUtil) = computeAverageMetrics()
400-
val directCount = collectedNodes.values.count { it.neighborType == "direct" }
401-
val meshCount = collectedNodes.values.count { it.neighborType == "mesh" }
402-
403-
val presetResult =
404-
DiscoveryPresetResultEntity(
405-
sessionId = sessionId,
406-
presetName = currentPresetName,
407-
dwellDurationSeconds = totalDwellSeconds,
408-
uniqueNodes = collectedNodes.size,
409-
directNeighborCount = directCount,
410-
meshNeighborCount = meshCount,
411-
messageCount = collectedNodes.values.sumOf { it.messageCount },
412-
sensorPacketCount = collectedNodes.values.sumOf { it.sensorPacketCount },
413-
avgChannelUtilization = avgChannelUtil,
414-
avgAirtimeRate = avgAirUtil,
415-
)
416-
val presetResultId = discoveryDao.insertPresetResult(presetResult)
417-
418-
val nodeEntities =
419-
collectedNodes.values.map { data ->
420-
DiscoveredNodeEntity(
421-
presetResultId = presetResultId,
422-
nodeNum = data.nodeNum,
423-
shortName = data.shortName,
424-
longName = data.longName,
425-
neighborType = data.neighborType,
426-
latitude = data.latitude,
427-
longitude = data.longitude,
428-
hopCount = data.hopCount,
429-
snr = data.snr,
430-
rssi = data.rssi,
431-
messageCount = data.messageCount,
432-
sensorPacketCount = data.sensorPacketCount,
433-
)
434-
}
435-
discoveryDao.insertDiscoveredNodes(nodeEntities)
436460
}
437461

438462
/**
@@ -502,7 +526,7 @@ class DiscoveryScanEngine(
502526
// endregion
503527

504528
companion object {
505-
private const val RECONNECT_TIMEOUT_MS = 30_000L
529+
private const val RECONNECT_TIMEOUT_MS = 60_000L
506530
private const val TICK_INTERVAL_MS = 1_000L
507531
private const val POSITION_DIVISOR = 1e7
508532
private const val MIN_DEVICE_METRICS_PACKETS = 2

0 commit comments

Comments
 (0)