feat: settings rework part 2, domain and usecase abstraction, tests (#4680)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-02 12:15:33 -06:00 committed by GitHub
parent 5f31df96d8
commit 8c6bd8ab7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 5245 additions and 1332 deletions

View file

@ -208,6 +208,10 @@ constructor(
onDisconnected(state)
}
}
.catch { e ->
Logger.w(e) { "[$address] bleConnection.connectionState flow crashed!" }
service.onDisconnect(BleError.from(e))
}
.launchIn(connectionScope)
val p = retryBleOperation(tag = address) { findPeripheral() }

View file

@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -47,9 +48,9 @@ import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toRemoteExceptions
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
import javax.inject.Inject
@ -127,6 +128,7 @@ constructor(
stopInterface()
}
}
.catch { Logger.e(it) { "bluetoothRepository.state flow crashed!" } }
.launchIn(processLifecycle.coroutineScope)
networkRepository.networkAvailable
@ -137,6 +139,7 @@ constructor(
stopInterface()
}
}
.catch { Logger.e(it) { "networkRepository.networkAvailable flow crashed!" } }
.launchIn(processLifecycle.coroutineScope)
}
}

View file

@ -18,7 +18,7 @@ package com.geeksville.mesh.service
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.model.ConnectionState
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -30,12 +30,12 @@ import okio.ByteString.Companion.toByteString
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.isWithinSizeLimit
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Constants
@ -47,7 +47,6 @@ import org.meshtastic.proto.NeighborInfo
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
@ -68,7 +67,6 @@ constructor(
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue)
private val sessionPasskey = AtomicReference(ByteString.EMPTY)
private val offlineSentPackets = CopyOnWriteArrayList<DataPacket>()
val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
val neighborInfoStartTimes = ConcurrentHashMap<Int, Long>()
@ -77,17 +75,6 @@ constructor(
@Volatile var lastNeighborInfo: NeighborInfo? = null
private val rememberDataType =
setOf(
PortNum.TEXT_MESSAGE_APP.value,
PortNum.ALERT_APP.value,
PortNum.WAYPOINT_APP.value,
PortNum.ATAK_PLUGIN.value,
PortNum.ATAK_FORWARDER.value,
PortNum.DETECTION_SENSOR_APP.value,
PortNum.PRIVATE_APP.value,
)
fun start(scope: CoroutineScope) {
this.scope = scope
radioConfigRepository?.localConfigFlow?.onEach { localConfig.value = it }?.launchIn(scope)
@ -154,14 +141,9 @@ constructor(
}
if (connectionStateHolder?.connectionState?.value == ConnectionState.Connected) {
try {
sendNow(p)
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
Logger.e(ex) { "Error sending message, so enqueueing" }
enqueueForSending(p)
}
sendNow(p)
} else {
enqueueForSending(p)
error("Radio is not connected")
}
}
@ -185,25 +167,6 @@ constructor(
packetHandler?.sendToRadio(meshPacket)
}
private fun enqueueForSending(p: DataPacket) {
if (p.dataType in rememberDataType) {
offlineSentPackets.add(p)
}
}
fun processQueuedPackets() {
val sentPackets = mutableListOf<DataPacket>()
offlineSentPackets.forEach { p ->
try {
sendNow(p)
sentPackets.add(p)
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
Logger.e(ex) { "Error sending queued message:" }
}
}
offlineSentPackets.removeAll(sentPackets)
}
fun sendAdmin(
destNum: Int,
requestId: Int = generatePacketId(),

View file

@ -27,7 +27,7 @@ import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Heartbeat

View file

@ -19,6 +19,10 @@ package com.geeksville.mesh.service
import android.app.Notification
import android.content.Context
import androidx.glance.appwidget.updateAll
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.widget.LocalStatsWidget
@ -40,7 +44,9 @@ import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.resources.Res
@ -50,8 +56,8 @@ import org.meshtastic.core.resources.device_sleeping
import org.meshtastic.core.resources.disconnected
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.meshtastic_app_name
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry
@ -82,6 +88,8 @@ constructor(
private val commandSender: MeshCommandSender,
private val nodeManager: MeshNodeManager,
private val analytics: PlatformAnalytics,
private val packetRepository: PacketRepository,
private val workManager: WorkManager,
) {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var sleepTimeout: Job? = null
@ -255,7 +263,25 @@ constructor(
}
fun onRadioConfigLoaded() {
commandSender.processQueuedPackets()
scope.handledLaunch {
val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList()
queuedPackets.forEach { packet ->
try {
val workRequest =
OneTimeWorkRequestBuilder<SendMessageWorker>()
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id))
.build()
workManager.enqueueUniqueWork(
"${SendMessageWorker.WORK_NAME_PREFIX}${packet.id}",
ExistingWorkPolicy.REPLACE,
workRequest,
)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Logger.e(e) { "Failed to enqueue queued packet worker" }
}
}
}
val myNodeNum = nodeManager.myNodeNum ?: 0
// Set time

View file

@ -60,7 +60,7 @@ constructor(
private val logInsertJobByPacketId = ConcurrentHashMap<Int, Job>()
private val earlyReceivedPackets = ArrayDeque<MeshPacket>()
private val maxEarlyPacketBuffer = 128
private val maxEarlyPacketBuffer = 10240
fun clearEarlyPackets() {
synchronized(earlyReceivedPackets) { earlyReceivedPackets.clear() }

View file

@ -78,7 +78,7 @@ constructor(
fun loadCachedNodeDB() {
scope.handledLaunch {
val nodes = nodeRepository?.getNodeDBbyNum()?.first() ?: emptyMap()
val nodes = nodeRepository?.getNodeEntityDBbyNumFlow()?.first() ?: emptyMap()
nodeDBbyNodeNum.putAll(nodes)
nodes.values.forEach { nodeDBbyID[it.user.id] = it }
myNodeNum = nodeRepository?.myNodeInfo?.value?.myNodeNum

View file

@ -21,11 +21,11 @@ import android.content.Intent
import android.os.Parcelable
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.ServiceRepository
import java.util.Locale
import javax.inject.Inject

View file

@ -309,7 +309,7 @@ constructor(
if (myNodeNum != null) {
// We use runBlocking here because this is called from MeshConnectionManager's synchronous methods,
// and we only do this once if the cache is empty.
val nodes = runBlocking { repo.getNodeDBbyNum().first() }
val nodes = runBlocking { repo.getNodeEntityDBbyNumFlow().first() }
nodes[myNodeNum]?.let { entity ->
if (cachedDeviceMetrics == null) {
cachedDeviceMetrics = entity.deviceTelemetry.device_metrics

View file

@ -32,11 +32,11 @@ import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.QueueStatus

View file

@ -96,6 +96,7 @@ import no.nordicsemi.android.common.permissions.notification.RequestNotification
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
@ -123,7 +124,6 @@ import org.meshtastic.core.resources.should_update
import org.meshtastic.core.resources.should_update_firmware
import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.view_on_map
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.icon.Conversations

View file

@ -59,6 +59,7 @@ import com.geeksville.mesh.ui.connections.components.UsbDevices
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.resources.Res
@ -71,7 +72,6 @@ import org.meshtastic.core.resources.must_set_region
import org.meshtastic.core.resources.no_device_selected
import org.meshtastic.core.resources.not_connected
import org.meshtastic.core.resources.set_your_region
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.TitledCard

View file

@ -37,9 +37,9 @@ import no.nordicsemi.android.common.scanner.view.ScannerView
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bluetooth_available_devices
import org.meshtastic.core.service.ConnectionState
/**
* Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
import androidx.compose.animation.Crossfade
@ -39,7 +38,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.ui.connections.DeviceType
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.ui.icon.Device
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.NoDevice

View file

@ -56,12 +56,12 @@ import com.geeksville.mesh.model.DeviceListEntry
import kotlinx.coroutines.delay
import no.nordicsemi.android.common.ui.view.RssiIcon
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add
import org.meshtastic.core.resources.bluetooth
import org.meshtastic.core.resources.network
import org.meshtastic.core.resources.serial
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.NodeChip
private const val RSSI_UPDATE_RATE_MS = 2000L

View file

@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.model.ConnectionState
@Composable
fun List<DeviceListEntry>.DeviceListSection(

View file

@ -53,6 +53,7 @@ import com.geeksville.mesh.ui.connections.ScannerViewModel
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.isValidAddress
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add_network_device
import org.meshtastic.core.resources.address
@ -63,7 +64,6 @@ import org.meshtastic.core.resources.forget_connection
import org.meshtastic.core.resources.ip_port
import org.meshtastic.core.resources.no_network_devices
import org.meshtastic.core.resources.recent_network_devices
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.MeshtasticResourceDialog
import org.meshtastic.core.ui.theme.AppTheme

View file

@ -27,9 +27,9 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.ui.connections.ScannerViewModel
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.no_usb_devices
import org.meshtastic.core.service.ConnectionState
@Composable
fun UsbDevices(

View file

@ -71,6 +71,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.toPlatformUri
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.qrCode
import org.meshtastic.core.navigation.Route
@ -88,7 +89,6 @@ import org.meshtastic.core.resources.replace
import org.meshtastic.core.resources.reset
import org.meshtastic.core.resources.reset_to_defaults
import org.meshtastic.core.resources.share_channels_qr
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.AdaptiveTwoPane
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.component.MainAppBar

View file

@ -69,6 +69,7 @@ import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.air_utilization
@ -92,7 +93,6 @@ import org.meshtastic.core.resources.powered
import org.meshtastic.core.resources.refresh
import org.meshtastic.core.resources.updated
import org.meshtastic.core.resources.uptime
import org.meshtastic.core.service.ConnectionState
class LocalStatsWidget : GlanceAppWidget() {

View file

@ -30,8 +30,8 @@ import kotlinx.coroutines.flow.stateIn
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.util.onlineTimeThreshold
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.LocalStats
import javax.inject.Inject