refactor: Remove AIDL API and modernize service architecture (#5586)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-06-03 21:28:17 -05:00
committed by James Rich
parent b9a00b4223
commit 4833acefd2
264 changed files with 4203 additions and 6748 deletions

View File

@@ -51,9 +51,9 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.Base64Factory
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.a11y_label_value
@@ -214,7 +214,7 @@ private fun NodeIdentificationRow(node: Node) {
Row(modifier = Modifier.fillMaxWidth()) {
InfoItem(
label = stringResource(Res.string.node_id),
value = DataPacket.nodeNumToDefaultId(node.num),
value = NodeAddress.numToDefaultId(node.num),
icon = MeshtasticIcons.DeviceNumbers,
modifier = Modifier.weight(1f),
)

View File

@@ -17,18 +17,15 @@
package org.meshtastic.feature.node.detail
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.neighbor_info
@@ -62,60 +59,50 @@ constructor(
snackbarManager.showSnackbar(message = text.resolve())
}
override fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(ioDispatcher) {
Logger.i { "Requesting UserInfo for '$destNum'" }
radioController.requestUserInfo(destNum)
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName))
}
override suspend fun requestUserInfo(destNum: Int, longName: String) {
Logger.i { "Requesting UserInfo for '$destNum'" }
radioController.requestUserInfo(destNum)
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName))
}
override fun requestNeighborInfo(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(ioDispatcher) {
Logger.i { "Requesting NeighborInfo for '$destNum'" }
val packetId = radioController.getPacketId()
radioController.requestNeighborInfo(packetId, destNum)
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName))
}
override suspend fun requestNeighborInfo(destNum: Int, longName: String) {
Logger.i { "Requesting NeighborInfo for '$destNum'" }
val packetId = radioController.generatePacketId()
radioController.requestNeighborInfo(packetId, destNum)
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName))
}
override fun requestPosition(scope: CoroutineScope, destNum: Int, longName: String, position: Position) {
scope.launch(ioDispatcher) {
Logger.i { "Requesting position for '$destNum'" }
radioController.requestPosition(destNum, position)
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.position, longName))
}
override suspend fun requestPosition(destNum: Int, longName: String, position: Position) {
Logger.i { "Requesting position for '$destNum'" }
radioController.requestPosition(destNum, position)
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.position, longName))
}
override fun requestTelemetry(scope: CoroutineScope, destNum: Int, longName: String, type: TelemetryType) {
scope.launch(ioDispatcher) {
Logger.i { "Requesting telemetry for '$destNum'" }
val packetId = radioController.getPacketId()
radioController.requestTelemetry(packetId, destNum, type.ordinal)
override suspend fun requestTelemetry(destNum: Int, longName: String, type: TelemetryType) {
Logger.i { "Requesting telemetry for '$destNum'" }
val packetId = radioController.generatePacketId()
radioController.requestTelemetry(packetId, destNum, type.ordinal)
val typeRes =
when (type) {
TelemetryType.DEVICE -> Res.string.request_device_metrics
TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics
TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics
TelemetryType.POWER -> Res.string.request_power_metrics
TelemetryType.LOCAL_STATS -> Res.string.signal_quality
TelemetryType.HOST -> Res.string.request_host_metrics
TelemetryType.PAX -> Res.string.request_pax_metrics
}
val typeRes =
when (type) {
TelemetryType.DEVICE -> Res.string.request_device_metrics
TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics
TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics
TelemetryType.POWER -> Res.string.request_power_metrics
TelemetryType.LOCAL_STATS -> Res.string.signal_quality
TelemetryType.HOST -> Res.string.request_host_metrics
TelemetryType.PAX -> Res.string.request_pax_metrics
}
showFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName))
}
showFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName))
}
override fun requestTraceroute(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(ioDispatcher) {
Logger.i { "Requesting traceroute for '$destNum'" }
val packetId = radioController.getPacketId()
radioController.requestTraceroute(packetId, destNum)
_lastTracerouteTime.value = nowMillis
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName))
}
override suspend fun requestTraceroute(destNum: Int, longName: String) {
Logger.i { "Requesting traceroute for '$destNum'" }
val packetId = radioController.generatePacketId()
radioController.requestTraceroute(packetId, destNum)
_lastTracerouteTime.value = nowMillis
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName))
}
}

View File

@@ -37,8 +37,6 @@ internal fun handleNodeAction(
when (action) {
is NodeDetailAction.Navigate -> onNavigate(action.route)
is NodeDetailAction.TriggerServiceAction -> viewModel.onServiceAction(action.action)
is NodeDetailAction.OpenRemoteAdmin -> viewModel.openRemoteAdmin(action.nodeNum)
is NodeDetailAction.RefreshMetadata -> viewModel.refreshMetadata(action.nodeNum)

View File

@@ -1,83 +0,0 @@
/*
* Copyright (c) 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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 org.meshtastic.feature.node.detail
import kotlinx.coroutines.CoroutineScope
import org.koin.core.annotation.Single
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.feature.node.component.NodeMenuAction
@Single
class NodeDetailActions
constructor(
private val nodeManagementActions: NodeManagementActions,
private val nodeRequestActions: NodeRequestActions,
) {
fun handleNodeMenuAction(scope: CoroutineScope, action: NodeMenuAction) {
when (action) {
is NodeMenuAction.Remove -> nodeManagementActions.removeNode(scope, action.node.num)
is NodeMenuAction.Ignore -> nodeManagementActions.ignoreNode(scope, action.node)
is NodeMenuAction.Mute -> nodeManagementActions.muteNode(scope, action.node)
is NodeMenuAction.Favorite -> nodeManagementActions.favoriteNode(scope, action.node)
is NodeMenuAction.RequestUserInfo ->
nodeRequestActions.requestUserInfo(scope, action.node.num, action.node.user.long_name)
is NodeMenuAction.RequestNeighborInfo ->
nodeRequestActions.requestNeighborInfo(scope, action.node.num, action.node.user.long_name)
is NodeMenuAction.RequestPosition ->
nodeRequestActions.requestPosition(scope, action.node.num, action.node.user.long_name)
is NodeMenuAction.RequestTelemetry ->
nodeRequestActions.requestTelemetry(scope, action.node.num, action.node.user.long_name, action.type)
is NodeMenuAction.TraceRoute ->
nodeRequestActions.requestTraceroute(scope, action.node.num, action.node.user.long_name)
else -> {}
}
}
fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) {
nodeManagementActions.setNodeNotes(scope, nodeNum, notes)
}
fun requestPosition(scope: CoroutineScope, destNum: Int, longName: String, position: Position) {
nodeRequestActions.requestPosition(scope, destNum, longName, position)
}
fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String) {
nodeRequestActions.requestUserInfo(scope, destNum, longName)
}
fun requestNeighborInfo(scope: CoroutineScope, destNum: Int, longName: String) {
nodeRequestActions.requestNeighborInfo(scope, destNum, longName)
}
fun requestTelemetry(scope: CoroutineScope, destNum: Int, longName: String, type: TelemetryType) {
nodeRequestActions.requestTelemetry(scope, destNum, longName, type)
}
fun requestTraceroute(scope: CoroutineScope, destNum: Int, longName: String) {
nodeRequestActions.requestTraceroute(scope, destNum, longName)
}
}

View File

@@ -34,13 +34,12 @@ import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.domain.usecase.session.EnsureRemoteAdminSessionUseCase
import org.meshtastic.core.domain.usecase.session.EnsureSessionResult
import org.meshtastic.core.domain.usecase.session.ObserveRemoteAdminSessionStatusUseCase
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.connect_radio_for_remote_admin
@@ -82,7 +81,7 @@ class NodeDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val nodeManagementActions: NodeManagementActions,
private val nodeRequestActions: NodeRequestActions,
private val serviceRepository: ServiceRepository,
private val queryController: QueryController,
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase,
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase,
@@ -144,39 +143,47 @@ class NodeDetailViewModel(
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)
is NodeMenuAction.RequestUserInfo ->
nodeRequestActions.requestUserInfo(viewModelScope, action.node.num, action.node.user.long_name)
viewModelScope.launch {
nodeRequestActions.requestUserInfo(action.node.num, action.node.user.long_name)
}
is NodeMenuAction.RequestNeighborInfo ->
nodeRequestActions.requestNeighborInfo(viewModelScope, action.node.num, action.node.user.long_name)
viewModelScope.launch {
nodeRequestActions.requestNeighborInfo(action.node.num, action.node.user.long_name)
}
is NodeMenuAction.RequestPosition ->
nodeRequestActions.requestPosition(viewModelScope, action.node.num, action.node.user.long_name)
viewModelScope.launch {
nodeRequestActions.requestPosition(action.node.num, action.node.user.long_name)
}
is NodeMenuAction.RequestTelemetry ->
nodeRequestActions.requestTelemetry(
viewModelScope,
action.node.num,
action.node.user.long_name,
action.type,
)
viewModelScope.launch {
nodeRequestActions.requestTelemetry(action.node.num, action.node.user.long_name, action.type)
}
is NodeMenuAction.TraceRoute ->
nodeRequestActions.requestTraceroute(viewModelScope, action.node.num, action.node.user.long_name)
viewModelScope.launch {
nodeRequestActions.requestTraceroute(action.node.num, action.node.user.long_name)
}
else -> {}
}
}
fun onServiceAction(action: ServiceAction) = viewModelScope.launch { serviceRepository.onServiceAction(action) }
/**
* Re-fetch device metadata (firmware/edition/role) for [destNum]. Refreshes the session passkey as a side effect.
*/
fun refreshMetadata(destNum: Int) = viewModelScope.launch { queryController.refreshMetadata(destNum) }
/**
* Ensure a remote-admin session passkey is fresh, then request navigation to the remote-admin screen. Surfaces a
* snackbar with the appropriate guidance on [EnsureSessionResult.Disconnected] or [EnsureSessionResult.Timeout].
*/
fun openRemoteAdmin(destNum: Int) {
if (isEnsuringSession.value) return
// Atomic check-and-flip prevents a double-tap from queuing two passkey exchanges + two navigation events.
if (!isEnsuringSession.compareAndSet(expect = false, update = true)) return
viewModelScope.launch {
isEnsuringSession.value = true
try {
when (ensureRemoteAdminSession(destNum)) {
EnsureSessionResult.AlreadyActive,
@@ -199,19 +206,14 @@ class NodeDetailViewModel(
}
}
/**
* Re-fetch device metadata (firmware/edition/role) for [destNum]. Refreshes the session passkey as a side effect.
*/
fun refreshMetadata(destNum: Int) = onServiceAction(ServiceAction.GetDeviceMetadata(destNum))
fun setNodeNotes(nodeNum: Int, notes: String) {
nodeManagementActions.setNodeNotes(viewModelScope, nodeNum, notes)
viewModelScope.launch { nodeManagementActions.setNodeNotes(nodeNum, notes) }
}
/** Returns the type-safe navigation route for a direct message to this node. */
fun getDirectMessageRoute(node: Node, ourNode: Node?): String {
val hasPKC = ourNode?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val channel = if (hasPKC) NodeAddress.PKC_CHANNEL_INDEX else node.channel
return "${channel}${node.user.id}"
}
}

View File

@@ -21,12 +21,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.favorite
import org.meshtastic.core.resources.favorite_add
@@ -41,12 +38,12 @@ import org.meshtastic.core.resources.remove
import org.meshtastic.core.resources.remove_node_text
import org.meshtastic.core.resources.unmute
import org.meshtastic.core.ui.util.AlertManager
import kotlin.coroutines.cancellation.CancellationException
@Single
open class NodeManagementActions
constructor(
private val nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val alertManager: AlertManager,
) {
@@ -55,19 +52,17 @@ constructor(
titleRes = Res.string.remove,
messageRes = Res.string.remove_node_text,
onConfirm = {
removeNode(scope, node.num)
scope.launch { removeNode(node.num) }
onAfterRemove()
},
)
}
open fun removeNode(scope: CoroutineScope, nodeNum: Int) {
scope.launch(ioDispatcher) {
Logger.i { "Removing node '$nodeNum'" }
val packetId = radioController.getPacketId()
radioController.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
}
open suspend fun removeNode(nodeNum: Int) {
Logger.i { "Removing node '$nodeNum'" }
val packetId = radioController.generatePacketId()
radioController.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
}
open fun requestIgnoreNode(scope: CoroutineScope, node: Node) {
@@ -77,13 +72,13 @@ constructor(
alertManager.showAlert(
titleRes = Res.string.ignore,
message = message,
onConfirm = { ignoreNode(scope, node) },
onConfirm = { scope.launch { setIgnored(node.num, !node.isIgnored) } },
)
}
}
open fun ignoreNode(scope: CoroutineScope, node: Node) {
scope.launch(ioDispatcher) { serviceRepository.onServiceAction(ServiceAction.Ignore(node)) }
open suspend fun setIgnored(nodeNum: Int, ignored: Boolean) {
radioController.setIgnored(nodeNum, ignored)
}
open fun requestMuteNode(scope: CoroutineScope, node: Node) {
@@ -93,13 +88,13 @@ constructor(
alertManager.showAlert(
titleRes = if (node.isMuted) Res.string.unmute else Res.string.mute_notifications,
message = message,
onConfirm = { muteNode(scope, node) },
onConfirm = { scope.launch { toggleMuted(node.num) } },
)
}
}
open fun muteNode(scope: CoroutineScope, node: Node) {
scope.launch(ioDispatcher) { serviceRepository.onServiceAction(ServiceAction.Mute(node)) }
open suspend fun toggleMuted(nodeNum: Int) {
radioController.toggleMuted(nodeNum)
}
open fun requestFavoriteNode(scope: CoroutineScope, node: Node) {
@@ -112,22 +107,22 @@ constructor(
alertManager.showAlert(
titleRes = Res.string.favorite,
message = message,
onConfirm = { favoriteNode(scope, node) },
onConfirm = { scope.launch { setFavorite(node.num, !node.isFavorite) } },
)
}
}
open fun favoriteNode(scope: CoroutineScope, node: Node) {
scope.launch(ioDispatcher) { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) }
open suspend fun setFavorite(nodeNum: Int, favorite: Boolean) {
radioController.setFavorite(nodeNum, favorite)
}
open fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) {
scope.launch(ioDispatcher) {
try {
nodeRepository.setNodeNotes(nodeNum, notes)
} catch (ex: Exception) {
Logger.e(ex) { "Set node notes error" }
}
open suspend fun setNodeNotes(nodeNum: Int, notes: String) {
try {
nodeRepository.setNodeNotes(nodeNum, notes)
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Logger.e(ex) { "Set node notes error" }
}
}
}

View File

@@ -16,7 +16,6 @@
*/
package org.meshtastic.feature.node.detail
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
@@ -26,18 +25,13 @@ interface NodeRequestActions {
val lastTracerouteTime: StateFlow<Long?>
val lastRequestNeighborTimes: StateFlow<Map<Int, Long>>
fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String)
suspend fun requestUserInfo(destNum: Int, longName: String)
fun requestNeighborInfo(scope: CoroutineScope, destNum: Int, longName: String)
suspend fun requestNeighborInfo(destNum: Int, longName: String)
fun requestPosition(
scope: CoroutineScope,
destNum: Int,
longName: String,
position: Position = Position(0.0, 0.0, 0),
)
suspend fun requestPosition(destNum: Int, longName: String, position: Position = Position(0.0, 0.0, 0))
fun requestTelemetry(scope: CoroutineScope, destNum: Int, longName: String, type: TelemetryType)
suspend fun requestTelemetry(destNum: Int, longName: String, type: TelemetryType)
fun requestTraceroute(scope: CoroutineScope, destNum: Int, longName: String)
suspend fun requestTraceroute(destNum: Int, longName: String)
}

View File

@@ -28,17 +28,17 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.NodeListDensity
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.AdminController
import org.meshtastic.core.repository.ConnectionStateProvider
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.node.detail.NodeManagementActions
import org.meshtastic.feature.node.detail.NodeRequestActions
@@ -52,8 +52,8 @@ class NodeListViewModel(
private val savedStateHandle: SavedStateHandle,
private val nodeRepository: NodeRepository,
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val connectionStateProvider: ConnectionStateProvider,
private val adminController: AdminController,
private val radioInterfaceService: RadioInterfaceService,
private val deviceHardwareRepository: DeviceHardwareRepository,
val nodeManagementActions: NodeManagementActions,
@@ -68,7 +68,7 @@ class NodeListViewModel(
val totalNodeCount = nodeRepository.totalNodeCount.stateInWhileSubscribed(initialValue = 0)
val connectionState = serviceRepository.connectionState
val connectionState = connectionStateProvider.connectionState
val deviceType: StateFlow<DeviceType?> =
radioInterfaceService.currentDeviceAddressFlow
@@ -184,7 +184,7 @@ class NodeListViewModel(
radioConfigRepository.replaceAllSettings(channelSet.settings)
val newLoraConfig = channelSet.lora_config
if (newLoraConfig != null) {
radioController.setLocalConfig(Config(lora = newLoraConfig))
adminController.setLocalConfig(Config(lora = newLoraConfig))
}
}
@@ -200,13 +200,13 @@ class NodeListViewModel(
fun getDirectMessageRoute(node: Node): String {
val ourNode = ourNodeInfo.value
val hasPKC = ourNode?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val channel = if (hasPKC) NodeAddress.PKC_CHANNEL_INDEX else node.channel
return "${channel}${node.user.id}"
}
/** Initiates a trace route request to the specified node. */
fun traceRoute(node: Node) {
nodeRequestActions.requestTraceroute(viewModelScope, node.num, node.user.long_name)
viewModelScope.launch { nodeRequestActions.requestTraceroute(node.num, node.user.long_name) }
}
companion object {

View File

@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import okio.ByteString.Companion.decodeBase64
@@ -51,7 +52,7 @@ import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TracerouteResponseProvider
import org.meshtastic.core.repository.TracerouteSnapshotRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.okay
@@ -80,7 +81,7 @@ open class MetricsViewModel(
@InjectedParam val destNum: Int,
protected val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository,
private val serviceRepository: ServiceRepository,
private val tracerouteResponseProvider: TracerouteResponseProvider,
private val nodeRepository: NodeRepository,
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
private val nodeRequestActions: NodeRequestActions,
@@ -191,7 +192,7 @@ open class MetricsViewModel(
if (cached != null) return cached
val overlay =
serviceRepository.tracerouteResponse.value
tracerouteResponseProvider.tracerouteResponse.value
?.takeIf { it.requestId == requestId }
?.let { response ->
TracerouteOverlay(
@@ -211,7 +212,7 @@ open class MetricsViewModel(
fun tracerouteSnapshotPositions(logUuid: String) = tracerouteSnapshotRepository.getSnapshotPositions(logUuid)
fun clearTracerouteResponse() = serviceRepository.clearTracerouteResponse()
fun clearTracerouteResponse() = tracerouteResponseProvider.clearTracerouteResponse()
fun positionedNodeNums(): Set<Int> =
nodeRepository.nodeDBbyNum.value.values.filter { it.validPosition != null }.numSet()
@@ -220,7 +221,7 @@ open class MetricsViewModel(
init {
safeLaunch(tag = "tracerouteCollector") {
serviceRepository.tracerouteResponse.filterNotNull().collect { response ->
tracerouteResponseProvider.tracerouteResponse.filterNotNull().collect { response ->
val overlay =
TracerouteOverlay(
requestId = response.requestId,
@@ -243,25 +244,29 @@ open class MetricsViewModel(
fun requestPosition() {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
nodeRequestActions.requestPosition(viewModelScope, it, state.value.node?.user?.long_name ?: "")
viewModelScope.launch { nodeRequestActions.requestPosition(it, state.value.node?.user?.long_name ?: "") }
}
}
fun requestTelemetry(type: TelemetryType) {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
nodeRequestActions.requestTelemetry(viewModelScope, it, state.value.node?.user?.long_name ?: "", type)
viewModelScope.launch {
nodeRequestActions.requestTelemetry(it, state.value.node?.user?.long_name ?: "", type)
}
}
}
fun requestTraceroute() {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
nodeRequestActions.requestTraceroute(viewModelScope, it, state.value.node?.user?.long_name ?: "")
viewModelScope.launch { nodeRequestActions.requestTraceroute(it, state.value.node?.user?.long_name ?: "") }
}
}
fun requestNeighborInfo() {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
nodeRequestActions.requestNeighborInfo(viewModelScope, it, state.value.node?.user?.long_name ?: "")
viewModelScope.launch {
nodeRequestActions.requestNeighborInfo(it, state.value.node?.user?.long_name ?: "")
}
}
}

View File

@@ -17,7 +17,6 @@
package org.meshtastic.feature.node.model
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.Route
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.proto.Config
@@ -25,8 +24,6 @@ import org.meshtastic.proto.Config
sealed interface NodeDetailAction {
data class Navigate(val route: Route) : NodeDetailAction
data class TriggerServiceAction(val action: ServiceAction) : NodeDetailAction
data class HandleNodeMenuAction(val action: NodeMenuAction) : NodeDetailAction
/** Open the remote-administration screen, ensuring a fresh session passkey first. */

View File

@@ -34,7 +34,7 @@ import org.meshtastic.core.domain.usecase.session.EnsureRemoteAdminSessionUseCas
import org.meshtastic.core.domain.usecase.session.ObserveRemoteAdminSessionStatusUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.ui.util.SnackbarManager
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
@@ -51,7 +51,7 @@ class HandleNodeActionTest {
private val testDispatcher = UnconfinedTestDispatcher()
private val nodeManagementActions: NodeManagementActions = mock()
private val nodeRequestActions: NodeRequestActions = mock()
private val serviceRepository: ServiceRepository = mock()
private val queryController: QueryController = mock()
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase = mock()
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase = mock()
@@ -93,7 +93,7 @@ class HandleNodeActionTest {
savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)),
nodeManagementActions = nodeManagementActions,
nodeRequestActions = nodeRequestActions,
serviceRepository = serviceRepository,
queryController = queryController,
getNodeDetailsUseCase = getNodeDetailsUseCase,
ensureRemoteAdminSession = ensureRemoteAdminSession,
observeRemoteAdminSessionStatus = observeRemoteAdminSessionStatus,

View File

@@ -42,7 +42,7 @@ import org.meshtastic.core.domain.usecase.session.ObserveRemoteAdminSessionStatu
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.connect_radio_for_remote_admin
@@ -64,7 +64,7 @@ class NodeDetailViewModelTest {
private lateinit var viewModel: NodeDetailViewModel
private val nodeManagementActions: NodeManagementActions = mock()
private val nodeRequestActions: NodeRequestActions = mock()
private val serviceRepository: ServiceRepository = mock()
private val queryController: QueryController = mock()
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase = mock()
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase = mock()
@@ -97,7 +97,7 @@ class NodeDetailViewModelTest {
savedStateHandle = SavedStateHandle(if (nodeId != null) mapOf("destNum" to nodeId) else emptyMap()),
nodeManagementActions = nodeManagementActions,
nodeRequestActions = nodeRequestActions,
serviceRepository = serviceRepository,
queryController = queryController,
getNodeDetailsUseCase = getNodeDetailsUseCase,
ensureRemoteAdminSession = ensureRemoteAdminSession,
observeRemoteAdminSessionStatus = observeRemoteAdminSessionStatus,
@@ -158,11 +158,11 @@ class NodeDetailViewModelTest {
@Test
fun `handleNodeMenuAction delegates to nodeRequestActions for Traceroute`() = runTest(testDispatcher) {
val node = Node(num = 1234, user = User(id = "!1234", long_name = "Test Node"))
every { nodeRequestActions.requestTraceroute(any(), any(), any()) } returns Unit
everySuspend { nodeRequestActions.requestTraceroute(any(), any()) } returns Unit
viewModel.handleNodeMenuAction(NodeMenuAction.TraceRoute(node))
verify { nodeRequestActions.requestTraceroute(any(), 1234, "Test Node") }
verifySuspend { nodeRequestActions.requestTraceroute(1234, "Test Node") }
}
@Test

View File

@@ -24,7 +24,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.FakeRadioController
import org.meshtastic.core.ui.util.AlertManager
@@ -36,7 +35,6 @@ import kotlin.test.assertTrue
class NodeManagementActionsTest {
private val nodeRepository = FakeNodeRepository()
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
private val radioController = FakeRadioController()
private val alertManager = mock<AlertManager>(MockMode.autofill)
private val testDispatcher = StandardTestDispatcher()
@@ -45,7 +43,6 @@ class NodeManagementActionsTest {
private val actions =
NodeManagementActions(
nodeRepository = nodeRepository,
serviceRepository = serviceRepository,
radioController = radioController,
alertManager = alertManager,
)
@@ -77,7 +74,6 @@ class NodeManagementActionsTest {
val actionsWithRealAlert =
NodeManagementActions(
nodeRepository = nodeRepository,
serviceRepository = serviceRepository,
radioController = radioController,
alertManager = realAlertManager,
)

View File

@@ -28,8 +28,8 @@ import kotlinx.coroutines.test.runTest
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.ConnectionStateProvider
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.testing.FakeDeviceHardwareRepository
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.FakeRadioController
@@ -50,7 +50,7 @@ class NodeListViewModelTest {
private lateinit var radioController: FakeRadioController
private lateinit var radioInterfaceService: FakeRadioInterfaceService
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
private val connectionStateProvider: ConnectionStateProvider = mock(MockMode.autofill)
private val nodeFilterPreferences: NodeFilterPreferences = mock(MockMode.autofill)
private val nodeManagementActions: NodeManagementActions = mock(MockMode.autofill)
private val nodeRequestActions: NodeRequestActions = mock(MockMode.autofill)
@@ -64,7 +64,7 @@ class NodeListViewModelTest {
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(org.meshtastic.proto.LocalConfig())
every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(org.meshtastic.proto.DeviceProfile())
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
every { nodeFilterPreferences.nodeSortOption } returns MutableStateFlow(NodeSortOption.LAST_HEARD)
every { nodeFilterPreferences.includeUnknown } returns MutableStateFlow(true)
@@ -83,8 +83,8 @@ class NodeListViewModelTest {
savedStateHandle = SavedStateHandle(),
nodeRepository = nodeRepository,
radioConfigRepository = radioConfigRepository,
serviceRepository = serviceRepository,
radioController = radioController,
connectionStateProvider = connectionStateProvider,
adminController = radioController,
radioInterfaceService = radioInterfaceService,
deviceHardwareRepository = FakeDeviceHardwareRepository(),
nodeManagementActions = nodeManagementActions,
@@ -120,7 +120,7 @@ class NodeListViewModelTest {
@Test
fun `connectionState reflects serviceRepository state`() = runTest {
val stateFlow = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
every { serviceRepository.connectionState } returns stateFlow
every { connectionStateProvider.connectionState } returns stateFlow
val vm = createViewModel()
vm.connectionState.test {

View File

@@ -40,7 +40,7 @@ import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TracerouteResponseProvider
import org.meshtastic.core.repository.TracerouteSnapshotRepository
import org.meshtastic.feature.node.detail.NodeDetailUiState
import org.meshtastic.feature.node.detail.NodeRequestActions
@@ -66,7 +66,7 @@ class MetricsViewModelTest {
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
private val meshLogRepository: MeshLogRepository = mock()
private val serviceRepository: ServiceRepository = mock()
private val tracerouteResponseProvider: TracerouteResponseProvider = mock()
private val nodeRepository: NodeRepository = mock()
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository = mock()
private val nodeRequestActions: NodeRequestActions = mock()
@@ -81,7 +81,7 @@ class MetricsViewModelTest {
Dispatchers.setMain(testDispatcher)
// Default setup for flows
every { serviceRepository.tracerouteResponse } returns MutableStateFlow(null)
every { tracerouteResponseProvider.tracerouteResponse } returns MutableStateFlow(null)
every { nodeRequestActions.lastTracerouteTime } returns MutableStateFlow(null)
every { nodeRequestActions.lastRequestNeighborTimes } returns MutableStateFlow(emptyMap())
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())
@@ -96,7 +96,7 @@ class MetricsViewModelTest {
destNum = destNum,
dispatchers = dispatchers,
meshLogRepository = meshLogRepository,
serviceRepository = serviceRepository,
tracerouteResponseProvider = tracerouteResponseProvider,
nodeRepository = nodeRepository,
tracerouteSnapshotRepository = tracerouteSnapshotRepository,
nodeRequestActions = nodeRequestActions,