Skip to content

Commit

Permalink
feat: position logs
Browse files Browse the repository at this point in the history
  • Loading branch information
andrekir committed Nov 2, 2024
1 parent 26f2100 commit adbe595
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
meshLogDao.deleteLog(uuid)
}

suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(Dispatchers.IO) {
meshLogDao.deleteLogs(nodeNum, portNum)
}

companion object {
private const val MAX_ITEMS = 500
private const val MAX_MESH_PACKETS = 10000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ interface MeshLogDao {

@Query("DELETE FROM log WHERE uuid = :uuid")
fun deleteLog(uuid: String)

@Query("DELETE FROM log WHERE from_num = :fromNum AND port_num = :portNum")
fun deleteLogs(fromNum: Int, portNum: Int)
}
80 changes: 77 additions & 3 deletions app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.geeksville.mesh.model

import android.app.Application
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
Expand All @@ -20,38 +24,54 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject

data class MetricsState(
val isManaged: Boolean = true,
val isFahrenheit: Boolean = false,
val displayUnits: DisplayUnits = DisplayUnits.METRIC,
val deviceMetrics: List<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(),
val tracerouteRequests: List<MeshLog> = emptyList(),
val tracerouteResults: List<MeshPacket> = emptyList(),
val positionLogs: List<Position> = emptyList(),
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
fun hasPositionLogs() = positionLogs.isNotEmpty()

companion object {
val Empty = MetricsState()
}
}

private fun MeshPacket.hasValidSignal(): Boolean =
rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0)

private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
runCatching { Position.parseFrom(decoded.payload) }.getOrNull()
} else {
null
}

@HiltViewModel
class MetricsViewModel @Inject constructor(
private val app: Application,
private val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository,
private val radioConfigRepository: RadioConfigRepository,
) : ViewModel(), Logging {
private val destNum = MutableStateFlow(0)

private fun MeshPacket.hasValidSignal(): Boolean =
rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0)

private fun MeshLog.hasValidTraceroute(): Boolean = with(fromRadio.packet) {
hasDecoded() && decoded.wantResponse && from == 0 && to == destNum.value
}
Expand All @@ -62,6 +82,10 @@ class MetricsViewModel @Inject constructor(
meshLogRepository.deleteLog(uuid)
}

fun clearPosition() = viewModelScope.launch(dispatchers.io) {
meshLogRepository.deleteLogs(destNum.value, PortNum.POSITION_APP_VALUE)
}

private val _state = MutableStateFlow(MetricsState.Empty)
val state: StateFlow<MetricsState> = _state

Expand Down Expand Up @@ -114,6 +138,15 @@ class MetricsViewModel @Inject constructor(
}
}.launchIn(viewModelScope)

@OptIn(ExperimentalCoroutinesApi::class)
destNum.flatMapLatest { destNum ->
meshLogRepository.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE).onEach { packets ->
_state.update { state ->
state.copy(positionLogs = packets.mapNotNull { it.toPosition() })
}
}
}.launchIn(viewModelScope)

debug("MetricsViewModel created")
}

Expand All @@ -128,4 +161,45 @@ class MetricsViewModel @Inject constructor(
fun setSelectedNode(nodeNum: Int) {
destNum.value = nodeNum
}

/**
* Write the persisted Position data out to a CSV file in the specified location.
*/
fun savePositionCSV(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 * 1000L)
val latitude = position.latitudeI * 1e-7
val longitude = position.longitudeI * 1e-7
val altitude = position.altitude
val satsInView = position.satsInView
val speed = position.groundSpeed
val heading = "%.2f".format(position.groundTrack * 1e-5)

// date,time,latitude,longitude,altitude,satsInView,speed,heading
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) {
errormsg("Can't write file error: ${ex.message}")
}
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/com/geeksville/mesh/ui/NavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen
Expand Down Expand Up @@ -250,6 +251,10 @@ fun NavGraph(
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
DeviceMetricsScreen(hiltViewModel<MetricsViewModel>(parentEntry))
}
composable("PositionLog") {
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
PositionLogScreen(hiltViewModel<MetricsViewModel>(parentEntry))
}
composable("EnvironmentMetrics") {
val parentEntry = remember { navController.getBackStackEntry("NodeDetails") }
EnvironmentMetricsScreen(hiltViewModel<MetricsViewModel>(parentEntry))
Expand Down
15 changes: 11 additions & 4 deletions app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.material.icons.filled.ChargingStation
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
Expand Down Expand Up @@ -135,31 +136,37 @@ private fun NodeDetailList(

item {
NavCard(
title = stringResource(R.string.device_metrics_logs),
title = stringResource(R.string.device_metrics_log),
icon = Icons.Default.ChargingStation,
enabled = metricsState.hasDeviceMetrics()
) {
onNavigate("DeviceMetrics")
}

NavCard(
title = stringResource(R.string.env_metrics_logs),
title = stringResource(R.string.position_log),
icon = Icons.Default.LocationOn,
enabled = metricsState.hasPositionLogs()
) { onNavigate("PositionLog") }

NavCard(
title = stringResource(R.string.env_metrics_log),
icon = Icons.Default.Thermostat,
enabled = metricsState.hasEnvironmentMetrics()
) {
onNavigate("EnvironmentMetrics")
}

NavCard(
title = stringResource(R.string.sig_metrics_logs),
title = stringResource(R.string.sig_metrics_log),
icon = Icons.Default.SignalCellularAlt,
enabled = metricsState.hasSignalMetrics()
) {
onNavigate("SignalMetrics")
}

NavCard(
title = stringResource(R.string.traceroute_logs),
title = stringResource(R.string.traceroute_log),
icon = Icons.Default.Route,
enabled = metricsState.hasTracerouteLogs()
) {
Expand Down
Loading

0 comments on commit adbe595

Please sign in to comment.