mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(feature/node): Extract MetricsViewModel to commonMain
This commit is contained in:
parent
8dd1113dfd
commit
52c2f6ef08
4 changed files with 41 additions and 123 deletions
|
|
@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import org.jetbrains.compose.resources.StringResource
|
import org.jetbrains.compose.resources.StringResource
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.meshtastic.app.map.node.NodeMapScreen
|
import org.meshtastic.app.map.node.NodeMapScreen
|
||||||
import org.meshtastic.app.node.AndroidMetricsViewModel
|
import org.meshtastic.feature.node.metrics.MetricsViewModel
|
||||||
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
|
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
|
||||||
import org.meshtastic.core.navigation.ContactsRoutes
|
import org.meshtastic.core.navigation.ContactsRoutes
|
||||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||||
|
|
@ -116,7 +116,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<NodeDetailRoutes.TracerouteLog> { args ->
|
entry<NodeDetailRoutes.TracerouteLog> { args ->
|
||||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
val metricsViewModel = koinViewModel<MetricsViewModel>()
|
||||||
metricsViewModel.setNodeId(args.destNum)
|
metricsViewModel.setNodeId(args.destNum)
|
||||||
|
|
||||||
TracerouteLogScreen(
|
TracerouteLogScreen(
|
||||||
|
|
@ -135,7 +135,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<NodeDetailRoutes.TracerouteMap> { args ->
|
entry<NodeDetailRoutes.TracerouteMap> { args ->
|
||||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
val metricsViewModel = koinViewModel<MetricsViewModel>()
|
||||||
metricsViewModel.setNodeId(args.destNum)
|
metricsViewModel.setNodeId(args.destNum)
|
||||||
|
|
||||||
TracerouteMapScreen(
|
TracerouteMapScreen(
|
||||||
|
|
@ -177,7 +177,7 @@ private inline fun <reified R : Route> EntryProviderScope<NavKey>.addNodeDetailS
|
||||||
crossinline getDestNum: (R) -> Int,
|
crossinline getDestNum: (R) -> Int,
|
||||||
) {
|
) {
|
||||||
entry<R> { args ->
|
entry<R> { args ->
|
||||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
val metricsViewModel = koinViewModel<MetricsViewModel>()
|
||||||
val destNum = getDestNum(args)
|
val destNum = getDestNum(args)
|
||||||
metricsViewModel.setNodeId(destNum)
|
metricsViewModel.setNodeId(destNum)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
* 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.app.node
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import co.touchlab.kermit.Logger
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.annotation.KoinViewModel
|
|
||||||
import org.meshtastic.core.common.util.toDate
|
|
||||||
import org.meshtastic.core.common.util.toInstant
|
|
||||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
|
||||||
import org.meshtastic.core.di.CoroutineDispatchers
|
|
||||||
import org.meshtastic.core.repository.MeshLogRepository
|
|
||||||
import org.meshtastic.core.repository.NodeRepository
|
|
||||||
import org.meshtastic.core.repository.ServiceRepository
|
|
||||||
import org.meshtastic.core.ui.util.AlertManager
|
|
||||||
import org.meshtastic.feature.node.detail.NodeRequestActions
|
|
||||||
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
|
||||||
import org.meshtastic.feature.node.metrics.MetricsViewModel
|
|
||||||
import java.io.BufferedWriter
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.FileWriter
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@KoinViewModel
|
|
||||||
class AndroidMetricsViewModel(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val app: Application,
|
|
||||||
dispatchers: CoroutineDispatchers,
|
|
||||||
meshLogRepository: MeshLogRepository,
|
|
||||||
serviceRepository: ServiceRepository,
|
|
||||||
nodeRepository: NodeRepository,
|
|
||||||
tracerouteSnapshotRepository: TracerouteSnapshotRepository,
|
|
||||||
nodeRequestActions: NodeRequestActions,
|
|
||||||
alertManager: AlertManager,
|
|
||||||
getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
|
||||||
) : MetricsViewModel(
|
|
||||||
savedStateHandle.get<Int>("destNum") ?: 0,
|
|
||||||
dispatchers,
|
|
||||||
meshLogRepository,
|
|
||||||
serviceRepository,
|
|
||||||
nodeRepository,
|
|
||||||
tracerouteSnapshotRepository,
|
|
||||||
nodeRequestActions,
|
|
||||||
alertManager,
|
|
||||||
getNodeDetailsUseCase,
|
|
||||||
) {
|
|
||||||
override fun savePositionCSV(uri: Any) {
|
|
||||||
if (uri is Uri) {
|
|
||||||
savePositionCSVAndroid(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun savePositionCSVAndroid(uri: Uri) = viewModelScope.launch(dispatchers.main) {
|
|
||||||
val positions = state.value.positionLogs
|
|
||||||
writeToUri(uri) { writer ->
|
|
||||||
writer.appendLine(
|
|
||||||
"\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"",
|
|
||||||
)
|
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
|
|
||||||
|
|
||||||
positions.forEach { position ->
|
|
||||||
val rxDateTime = dateFormat.format((position.time.toLong() * 1000L).toInstant().toDate())
|
|
||||||
val latitude = (position.latitude_i ?: 0) * 1e-7
|
|
||||||
val longitude = (position.longitude_i ?: 0) * 1e-7
|
|
||||||
val altitude = position.altitude
|
|
||||||
val satsInView = position.sats_in_view
|
|
||||||
val speed = position.ground_speed
|
|
||||||
val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
|
|
||||||
|
|
||||||
writer.appendLine(
|
|
||||||
"$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) =
|
|
||||||
withContext(dispatchers.io) {
|
|
||||||
try {
|
|
||||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
|
||||||
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
|
|
||||||
BufferedWriter(fileWriter).use { writer -> block.invoke(writer) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: FileNotFoundException) {
|
|
||||||
Logger.e(ex) { "Can't write file error" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun decodeBase64(base64: String): ByteArray =
|
|
||||||
android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.node.metrics
|
package org.meshtastic.feature.node.metrics
|
||||||
|
|
||||||
|
import org.meshtastic.core.common.util.toMeshtasticUri
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
|
@ -119,7 +121,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
|
||||||
val exportPositionLauncher =
|
val exportPositionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
it.data?.data?.let { uri -> viewModel.savePositionCSV(uri) }
|
it.data?.data?.let { uri -> viewModel.savePositionCSV(uri.toMeshtasticUri()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,13 @@ import org.meshtastic.proto.PortNum
|
||||||
import org.meshtastic.proto.Telemetry
|
import org.meshtastic.proto.Telemetry
|
||||||
import org.meshtastic.proto.Paxcount as ProtoPaxcount
|
import org.meshtastic.proto.Paxcount as ProtoPaxcount
|
||||||
|
|
||||||
|
import org.meshtastic.core.repository.FileService
|
||||||
|
import org.meshtastic.core.common.util.MeshtasticUri
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import okio.ByteString.Companion.decodeBase64
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node.
|
* ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node.
|
||||||
*/
|
*/
|
||||||
|
|
@ -81,6 +88,7 @@ open class MetricsViewModel(
|
||||||
private val nodeRequestActions: NodeRequestActions,
|
private val nodeRequestActions: NodeRequestActions,
|
||||||
private val alertManager: AlertManager,
|
private val alertManager: AlertManager,
|
||||||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||||
|
private val fileService: FileService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val nodeIdFromRoute: Int?
|
private val nodeIdFromRoute: Int?
|
||||||
|
|
@ -315,8 +323,30 @@ open class MetricsViewModel(
|
||||||
Logger.d { "MetricsViewModel cleared" }
|
Logger.d { "MetricsViewModel cleared" }
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun savePositionCSV(uri: Any) {
|
fun savePositionCSV(uri: MeshtasticUri) {
|
||||||
// To be implemented in platform-specific subclass
|
viewModelScope.launch(dispatchers.main) {
|
||||||
|
val positions = state.value.positionLogs
|
||||||
|
fileService.write(uri) { sink ->
|
||||||
|
sink.writeUtf8("\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"\n")
|
||||||
|
|
||||||
|
positions.forEach { position ->
|
||||||
|
val localDateTime = Instant.fromEpochSeconds(position.time.toLong())
|
||||||
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
val rxDateTime = "\"${localDateTime.date}\",\"${localDateTime.time}\""
|
||||||
|
|
||||||
|
val latitude = (position.latitude_i ?: 0) * 1e-7
|
||||||
|
val longitude = (position.longitude_i ?: 0) * 1e-7
|
||||||
|
val altitude = position.altitude
|
||||||
|
val satsInView = position.sats_in_view
|
||||||
|
val speed = position.ground_speed
|
||||||
|
// Kotlin string format is available in common code on 1.9.20+ via String.format,
|
||||||
|
// but we can just do basic string manipulation if needed.
|
||||||
|
val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
|
||||||
|
|
||||||
|
sink.writeUtf8("$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount")
|
@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount")
|
||||||
|
|
@ -347,8 +377,7 @@ open class MetricsViewModel(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun decodeBase64(base64: String): ByteArray {
|
protected fun decodeBase64(base64: String): ByteArray {
|
||||||
// To be overridden in platform-specific subclass or use KMP library
|
return base64.decodeBase64()?.toByteArray() ?: ByteArray(0)
|
||||||
return ByteArray(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue