mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Modularize remaining nodes code (#3599)
This commit is contained in:
parent
89bc9528c5
commit
315950b7c6
23 changed files with 125 additions and 141 deletions
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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 com.geeksville.mesh.model
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Green
|
||||
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.LightGreen
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Magenta
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Orange
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Pink
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Purple
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Red
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Yellow
|
||||
import org.meshtastic.feature.node.model.TimeFrame
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class Environment(val color: Color) {
|
||||
TEMPERATURE(Red) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.temperature
|
||||
},
|
||||
HUMIDITY(InfantryBlue) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.relativeHumidity
|
||||
},
|
||||
SOIL_TEMPERATURE(Pink) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.soilTemperature
|
||||
},
|
||||
SOIL_MOISTURE(Purple) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) =
|
||||
telemetry.environmentMetrics.soilMoisture?.toFloat()
|
||||
},
|
||||
BAROMETRIC_PRESSURE(Green) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.barometricPressure
|
||||
},
|
||||
GAS_RESISTANCE(Yellow) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.gasResistance
|
||||
},
|
||||
IAQ(Magenta) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.iaq?.toFloat()
|
||||
},
|
||||
LUX(LightGreen) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.lux
|
||||
},
|
||||
UV_LUX(Orange) {
|
||||
override fun getValue(telemetry: TelemetryProtos.Telemetry) = telemetry.environmentMetrics.uvLux
|
||||
}, ;
|
||||
|
||||
abstract fun getValue(telemetry: TelemetryProtos.Telemetry): Float?
|
||||
}
|
||||
|
||||
/**
|
||||
* @param metrics the filtered [List]
|
||||
* @param shouldPlot a [List] the size of [Environment] used to determine if a metric should be plotted
|
||||
* @param leftMinMax [Pair] with the min and max of the barometric pressure
|
||||
* @param rightMinMax [Pair] with the combined min and max of: the temperature, humidity, and IAQ
|
||||
* @param times [Pair] with the oldest and newest times in that order
|
||||
*/
|
||||
data class EnvironmentGraphingData(
|
||||
val metrics: List<TelemetryProtos.Telemetry>,
|
||||
val shouldPlot: List<Boolean>,
|
||||
val leftMinMax: Pair<Float, Float> = Pair(0f, 0f),
|
||||
val rightMinMax: Pair<Float, Float> = Pair(0f, 0f),
|
||||
val times: Pair<Int, Int> = Pair(0, 0),
|
||||
)
|
||||
|
||||
data class EnvironmentMetricsState(val environmentMetrics: List<TelemetryProtos.Telemetry> = emptyList()) {
|
||||
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
|
||||
|
||||
/**
|
||||
* Filters [environmentMetrics] based on a [org.meshtastic.feature.node.model.TimeFrame].
|
||||
*
|
||||
* @param timeFrame used to filter
|
||||
* @return [EnvironmentGraphingData]
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
|
||||
fun environmentMetricsFiltered(timeFrame: TimeFrame, useFahrenheit: Boolean = false): EnvironmentGraphingData {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
val telemetries = environmentMetrics.filter { it.time >= oldestTime }
|
||||
val shouldPlot = BooleanArray(Environment.entries.size) { false }
|
||||
if (telemetries.isEmpty()) {
|
||||
return EnvironmentGraphingData(metrics = telemetries, shouldPlot = shouldPlot.toList())
|
||||
}
|
||||
|
||||
/* Grab the combined min and max for temp, humidity, soil_Temperature, soilMoisture and iaq. */
|
||||
val minValues = mutableListOf<Float>()
|
||||
val maxValues = mutableListOf<Float>()
|
||||
|
||||
// Temperature
|
||||
val temperatures = telemetries.mapNotNull { it.environmentMetrics.temperature?.takeIf { !it.isNaN() } }
|
||||
if (temperatures.isNotEmpty()) {
|
||||
var minTempValue = temperatures.minOf { it }
|
||||
var maxTempValue = temperatures.maxOf { it }
|
||||
if (useFahrenheit) {
|
||||
minTempValue = UnitConversions.celsiusToFahrenheit(minTempValue)
|
||||
maxTempValue = UnitConversions.celsiusToFahrenheit(maxTempValue)
|
||||
}
|
||||
minValues.add(minTempValue)
|
||||
maxValues.add(maxTempValue)
|
||||
shouldPlot[Environment.TEMPERATURE.ordinal] = true
|
||||
}
|
||||
|
||||
// Relative Humidity
|
||||
val humidities =
|
||||
telemetries.mapNotNull { it.environmentMetrics.relativeHumidity?.takeIf { !it.isNaN() && it != 0.0f } }
|
||||
if (humidities.isNotEmpty()) {
|
||||
minValues.add(humidities.minOf { it })
|
||||
maxValues.add(humidities.maxOf { it })
|
||||
shouldPlot[Environment.HUMIDITY.ordinal] = true
|
||||
}
|
||||
|
||||
// Soil Temperature
|
||||
val soilTemperatures = telemetries.mapNotNull { it.environmentMetrics.soilTemperature?.takeIf { !it.isNaN() } }
|
||||
if (soilTemperatures.isNotEmpty()) {
|
||||
var minSoilTemperatureValue = soilTemperatures.minOf { it }
|
||||
var maxSoilTemperatureValue = soilTemperatures.maxOf { it }
|
||||
if (useFahrenheit) {
|
||||
minSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(minSoilTemperatureValue)
|
||||
maxSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(maxSoilTemperatureValue)
|
||||
}
|
||||
minValues.add(minSoilTemperatureValue)
|
||||
maxValues.add(maxSoilTemperatureValue)
|
||||
shouldPlot[Environment.SOIL_TEMPERATURE.ordinal] = true
|
||||
}
|
||||
|
||||
// Soil Moisture
|
||||
val soilMoistures =
|
||||
telemetries.mapNotNull { it.environmentMetrics.soilMoisture?.takeIf { it != Int.MIN_VALUE } }
|
||||
if (soilMoistures.isNotEmpty()) {
|
||||
minValues.add(soilMoistures.minOf { it.toFloat() })
|
||||
maxValues.add(soilMoistures.maxOf { it.toFloat() })
|
||||
shouldPlot[Environment.SOIL_MOISTURE.ordinal] = true
|
||||
}
|
||||
|
||||
// IAQ
|
||||
val iaqs = telemetries.mapNotNull { it.environmentMetrics.iaq?.takeIf { it != Int.MIN_VALUE } }
|
||||
if (iaqs.isNotEmpty()) {
|
||||
minValues.add(iaqs.minOf { it.toFloat() })
|
||||
maxValues.add(iaqs.maxOf { it.toFloat() })
|
||||
shouldPlot[Environment.IAQ.ordinal] = true
|
||||
}
|
||||
|
||||
// Barometric Pressure
|
||||
val pressures = telemetries.mapNotNull { it.environmentMetrics.barometricPressure?.takeIf { !it.isNaN() } }
|
||||
var minPressureValue = 0f
|
||||
var maxPressureValue = 0f
|
||||
if (pressures.isNotEmpty()) {
|
||||
minPressureValue = pressures.minOf { it }
|
||||
maxPressureValue = pressures.maxOf { it }
|
||||
shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] = true
|
||||
}
|
||||
|
||||
// Lux
|
||||
val luxValues = telemetries.mapNotNull { it.environmentMetrics.lux?.takeIf { !it.isNaN() } }
|
||||
if (luxValues.isNotEmpty()) {
|
||||
minValues.add(luxValues.minOf { it })
|
||||
maxValues.add(luxValues.maxOf { it })
|
||||
shouldPlot[Environment.LUX.ordinal] = true
|
||||
}
|
||||
|
||||
// UVLux
|
||||
val uvLuxValues = telemetries.mapNotNull { it.environmentMetrics.uvLux?.takeIf { !it.isNaN() } }
|
||||
if (uvLuxValues.isNotEmpty()) {
|
||||
minValues.add(uvLuxValues.minOf { it })
|
||||
maxValues.add(uvLuxValues.maxOf { it })
|
||||
shouldPlot[Environment.UV_LUX.ordinal] = true
|
||||
}
|
||||
|
||||
val min = if (minValues.isEmpty()) 0f else minValues.minOf { it }
|
||||
val max = if (maxValues.isEmpty()) 1f else maxValues.maxOf { it }
|
||||
|
||||
val (oldest, newest) = Pair(telemetries.minBy { it.time }, telemetries.maxBy { it.time })
|
||||
|
||||
return EnvironmentGraphingData(
|
||||
metrics = telemetries,
|
||||
shouldPlot = shouldPlot.toList(),
|
||||
leftMinMax = Pair(minPressureValue, maxPressureValue),
|
||||
rightMinMax = Pair(min, max),
|
||||
times = Pair(oldest.time, newest.time),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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 com.geeksville.mesh.model
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import com.geeksville.mesh.util.safeNumber
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.proto.toPosition
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.feature.map.model.CustomTileSource
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.TimeFrame
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.Portnums.PortNum
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEFAULT_ID_SUFFIX_LENGTH = 4
|
||||
|
||||
private fun MeshPacket.hasValidSignal(): Boolean =
|
||||
rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class MetricsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val app: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val mapPrefs: MapPrefs,
|
||||
) : ViewModel() {
|
||||
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
|
||||
|
||||
private fun MeshLog.hasValidTraceroute(): Boolean =
|
||||
with(fromRadio.packet) { hasDecoded() && decoded.wantResponse && from == 0 && to == destNum }
|
||||
|
||||
/**
|
||||
* Creates a fallback node for hidden clients or nodes not yet in the database. This prevents the detail screen from
|
||||
* freezing when viewing unknown nodes.
|
||||
*/
|
||||
private fun createFallbackNode(nodeNum: Int): Node {
|
||||
val userId = DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
val safeUserId = userId.padStart(DEFAULT_ID_SUFFIX_LENGTH, '0').takeLast(DEFAULT_ID_SUFFIX_LENGTH)
|
||||
val longName = app.getString(R.string.fallback_node_name) + " $safeUserId"
|
||||
val defaultUser =
|
||||
MeshProtos.User.newBuilder()
|
||||
.setId(userId)
|
||||
.setLongName(longName)
|
||||
.setShortName(safeUserId)
|
||||
.setHwModel(MeshProtos.HardwareModel.UNSET)
|
||||
.build()
|
||||
|
||||
return Node(num = nodeNum, user = defaultUser)
|
||||
}
|
||||
|
||||
fun getUser(nodeNum: Int) = nodeRepository.getUser(nodeNum)
|
||||
|
||||
val tileSource
|
||||
get() = CustomTileSource.getTileSource(mapPrefs.mapStyle)
|
||||
|
||||
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) { meshLogRepository.deleteLog(uuid) }
|
||||
|
||||
fun clearPosition() = viewModelScope.launch(dispatchers.io) {
|
||||
destNum?.let { meshLogRepository.deleteLogs(it, PortNum.POSITION_APP_VALUE) }
|
||||
}
|
||||
|
||||
fun onServiceAction(action: ServiceAction) = viewModelScope.launch { serviceRepository.onServiceAction(action) }
|
||||
|
||||
private val _state = MutableStateFlow(MetricsState.Empty)
|
||||
val state: StateFlow<MetricsState> = _state
|
||||
|
||||
private val _environmentState = MutableStateFlow(EnvironmentMetricsState())
|
||||
val environmentState: StateFlow<EnvironmentMetricsState> = _environmentState
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
init {
|
||||
if (destNum != null) {
|
||||
nodeRepository.nodeDBbyNum
|
||||
.mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.onEach { (node, ourNode) ->
|
||||
// Create a fallback node if not found in database (for hidden clients, etc.)
|
||||
val actualNode = node ?: createFallbackNode(destNum)
|
||||
val deviceHardware =
|
||||
actualNode.user.hwModel.safeNumber().let {
|
||||
deviceHardwareRepository.getDeviceHardwareByModel(it)
|
||||
}
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
node = actualNode,
|
||||
isLocal = destNum == ourNode,
|
||||
deviceHardware = deviceHardware.getOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.deviceProfileFlow
|
||||
.onEach { profile ->
|
||||
val moduleConfig = profile.moduleConfig
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
isManaged = profile.config.security.isManaged,
|
||||
isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
displayUnits = profile.config.display.units,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository
|
||||
.getTelemetryFrom(destNum)
|
||||
.onEach { telemetry ->
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
||||
powerMetrics = telemetry.filter { it.hasPowerMetrics() },
|
||||
hostMetrics = telemetry.filter { it.hasHostMetrics() },
|
||||
)
|
||||
}
|
||||
_environmentState.update { state ->
|
||||
state.copy(
|
||||
environmentMetrics =
|
||||
telemetry.filter {
|
||||
it.hasEnvironmentMetrics() &&
|
||||
it.environmentMetrics.hasRelativeHumidity() &&
|
||||
it.environmentMetrics.hasTemperature() &&
|
||||
!it.environmentMetrics.temperature.isNaN()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository
|
||||
.getMeshPacketsFrom(destNum)
|
||||
.onEach { meshPackets ->
|
||||
_state.update { state -> state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() }) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
combine(
|
||||
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
|
||||
meshLogRepository.getLogsFrom(destNum ?: 0, PortNum.TRACEROUTE_APP_VALUE),
|
||||
) { request, response ->
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
tracerouteRequests = request.filter { it.hasValidTraceroute() },
|
||||
tracerouteResults = response,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository
|
||||
.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE)
|
||||
.onEach { packets ->
|
||||
val distinctPositions =
|
||||
packets
|
||||
.mapNotNull { it.toPosition() }
|
||||
.asFlow()
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.time == new.time ||
|
||||
(old.latitudeI == new.latitudeI && old.longitudeI == new.longitudeI)
|
||||
}
|
||||
.toList()
|
||||
_state.update { state -> state.copy(positionLogs = distinctPositions) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository
|
||||
.getLogsFrom(destNum, Portnums.PortNum.PAXCOUNTER_APP_VALUE)
|
||||
.onEach { logs -> _state.update { state -> state.copy(paxMetrics = logs) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.stableRelease
|
||||
.filterNotNull()
|
||||
.onEach { latestStable -> _state.update { state -> state.copy(latestStableFirmware = latestStable) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.alphaRelease
|
||||
.filterNotNull()
|
||||
.onEach { latestAlpha -> _state.update { state -> state.copy(latestAlphaFirmware = latestAlpha) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository
|
||||
.getMyNodeInfo()
|
||||
.map { it?.firmwareEdition }
|
||||
.distinctUntilChanged()
|
||||
.onEach { firmwareEdition -> _state.update { state -> state.copy(firmwareEdition = firmwareEdition) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
Timber.d("MetricsViewModel created")
|
||||
} else {
|
||||
Timber.d("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Timber.d("MetricsViewModel cleared")
|
||||
}
|
||||
|
||||
fun setTimeFrame(timeFrame: TimeFrame) {
|
||||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
Timber.e(ex, "Can't write file error")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue