@@ -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