mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(firmware): add firmware edition handling (#2508)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
45ad973d35
commit
924bd25f60
6 changed files with 598 additions and 557 deletions
|
|
@ -18,6 +18,7 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
|
|
@ -32,68 +33,60 @@ import kotlinx.coroutines.withContext
|
|||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class MeshLogRepository @Inject constructor(
|
||||
class MeshLogRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val meshLogDaoLazy: dagger.Lazy<MeshLogDao>,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val meshLogDao by lazy {
|
||||
meshLogDaoLazy.get()
|
||||
}
|
||||
private val meshLogDao by lazy { meshLogDaoLazy.get() }
|
||||
|
||||
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = meshLogDao.getAllLogs(maxItems)
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
|
||||
meshLogDao.getAllLogs(maxItems).flowOn(dispatchers.io).conflate()
|
||||
|
||||
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
|
||||
meshLogDao.getAllLogsInReceiveOrder(maxItems)
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
meshLogDao.getAllLogsInReceiveOrder(maxItems).flowOn(dispatchers.io).conflate()
|
||||
|
||||
private fun parseTelemetryLog(log: MeshLog): Telemetry? = runCatching {
|
||||
Telemetry.parseFrom(log.fromRadio.packet.decoded.payload)
|
||||
.toBuilder().setTime((log.received_date / MILLIS_TO_SECONDS).toInt()).build()
|
||||
}.getOrNull()
|
||||
.toBuilder()
|
||||
.setTime((log.received_date / MILLIS_TO_SECONDS).toInt())
|
||||
.build()
|
||||
}
|
||||
.getOrNull()
|
||||
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> =
|
||||
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||
.flowOn(dispatchers.io)
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = meshLogDao
|
||||
.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
fun getLogsFrom(
|
||||
nodeNum: Int,
|
||||
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
||||
maxItem: Int = MAX_MESH_PACKETS,
|
||||
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem)
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.io)
|
||||
): Flow<List<MeshLog>> =
|
||||
meshLogDao.getLogsFrom(nodeNum, portNum, maxItem).distinctUntilChanged().flowOn(dispatchers.io)
|
||||
|
||||
/*
|
||||
* Retrieves MeshPackets matching 'nodeNum' and 'portNum'.
|
||||
* If 'portNum' is not specified, returns all MeshPackets. Otherwise, filters by 'portNum'.
|
||||
*/
|
||||
fun getMeshPacketsFrom(
|
||||
nodeNum: Int,
|
||||
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
||||
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum)
|
||||
.mapLatest { list -> list.map { it.fromRadio.packet } }
|
||||
fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE): Flow<List<MeshPacket>> =
|
||||
getLogsFrom(nodeNum, portNum).mapLatest { list -> list.map { it.fromRadio.packet } }.flowOn(dispatchers.io)
|
||||
|
||||
fun getMyNodeInfo(): Flow<MeshProtos.MyNodeInfo?> = getLogsFrom(0, 0)
|
||||
.mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) {
|
||||
meshLogDao.insert(log)
|
||||
}
|
||||
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) { meshLogDao.insert(log) }
|
||||
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) {
|
||||
meshLogDao.deleteAll()
|
||||
}
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { meshLogDao.deleteAll() }
|
||||
|
||||
suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) {
|
||||
meshLogDao.deleteLog(uuid)
|
||||
}
|
||||
suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) { meshLogDao.deleteLog(uuid) }
|
||||
|
||||
suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(dispatchers.io) {
|
||||
meshLogDao.deleteLogs(nodeNum, portNum)
|
||||
}
|
||||
suspend fun deleteLogs(nodeNum: Int, portNum: Int) =
|
||||
withContext(dispatchers.io) { meshLogDao.deleteLogs(nodeNum, portNum) }
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 500
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.geeksville.mesh.DataPacket
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.Position
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Portnums.PortNum
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
|
|
@ -47,7 +48,6 @@ import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
|||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
import com.geeksville.mesh.ui.map.MAP_STYLE_ID
|
||||
import com.geeksville.mesh.Portnums
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -56,6 +56,7 @@ 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
|
||||
|
|
@ -87,16 +88,23 @@ data class MetricsState(
|
|||
val positionLogs: List<Position> = emptyList(),
|
||||
val deviceHardware: DeviceHardware? = null,
|
||||
val isLocalDevice: Boolean = false,
|
||||
val firmwareEdition: MeshProtos.FirmwareEdition? = null,
|
||||
val latestStableFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val latestAlphaFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val paxMetrics: List<MeshLog> = emptyList(),
|
||||
) {
|
||||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||
|
||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||
|
||||
fun hasPowerMetrics() = powerMetrics.isNotEmpty()
|
||||
|
||||
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||
|
||||
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
||||
|
||||
fun hasHostMetrics() = hostMetrics.isNotEmpty()
|
||||
|
||||
fun hasPaxMetrics() = paxMetrics.isNotEmpty()
|
||||
|
||||
fun deviceMetricsFiltered(timeFrame: TimeFrame): List<Telemetry> {
|
||||
|
|
@ -119,20 +127,16 @@ data class MetricsState(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported time frames used to display data.
|
||||
*/
|
||||
/** Supported time frames used to display data. */
|
||||
@Suppress("MagicNumber")
|
||||
enum class TimeFrame(
|
||||
val seconds: Long,
|
||||
@StringRes val strRes: Int
|
||||
) {
|
||||
enum class TimeFrame(val seconds: Long, @StringRes val strRes: Int) {
|
||||
TWENTY_FOUR_HOURS(TimeUnit.DAYS.toSeconds(1), R.string.twenty_four_hours),
|
||||
FORTY_EIGHT_HOURS(TimeUnit.DAYS.toSeconds(2), R.string.forty_eight_hours),
|
||||
ONE_WEEK(TimeUnit.DAYS.toSeconds(7), R.string.one_week),
|
||||
TWO_WEEKS(TimeUnit.DAYS.toSeconds(14), R.string.two_weeks),
|
||||
FOUR_WEEKS(TimeUnit.DAYS.toSeconds(28), R.string.four_weeks),
|
||||
MAX(0L, R.string.max);
|
||||
MAX(0L, R.string.max),
|
||||
;
|
||||
|
||||
fun calculateOldestTime(): Long = if (this == MAX) {
|
||||
MAX.seconds
|
||||
|
|
@ -141,42 +145,29 @@ enum class TimeFrame(
|
|||
}
|
||||
|
||||
/**
|
||||
* The time interval to draw the vertical lines representing
|
||||
* time on the x-axis.
|
||||
* The time interval to draw the vertical lines representing time on the x-axis.
|
||||
*
|
||||
* @return seconds epoch seconds
|
||||
*/
|
||||
fun lineInterval(): Long {
|
||||
return when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal ->
|
||||
TimeUnit.HOURS.toSeconds(6)
|
||||
fun lineInterval(): Long = when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(6)
|
||||
|
||||
FORTY_EIGHT_HOURS.ordinal ->
|
||||
TimeUnit.HOURS.toSeconds(12)
|
||||
FORTY_EIGHT_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(12)
|
||||
|
||||
ONE_WEEK.ordinal,
|
||||
TWO_WEEKS.ordinal ->
|
||||
TimeUnit.DAYS.toSeconds(1)
|
||||
ONE_WEEK.ordinal,
|
||||
TWO_WEEKS.ordinal,
|
||||
-> TimeUnit.DAYS.toSeconds(1)
|
||||
|
||||
else ->
|
||||
TimeUnit.DAYS.toSeconds(7)
|
||||
}
|
||||
else -> TimeUnit.DAYS.toSeconds(7)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to detect a significant time separation between [Telemetry]s.
|
||||
*/
|
||||
fun timeThreshold(): Long {
|
||||
return when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal ->
|
||||
TimeUnit.HOURS.toSeconds(6)
|
||||
/** Used to detect a significant time separation between [Telemetry]s. */
|
||||
fun timeThreshold(): Long = when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(6)
|
||||
|
||||
FORTY_EIGHT_HOURS.ordinal ->
|
||||
TimeUnit.HOURS.toSeconds(12)
|
||||
FORTY_EIGHT_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(12)
|
||||
|
||||
else ->
|
||||
TimeUnit.DAYS.toSeconds(1)
|
||||
}
|
||||
else -> TimeUnit.DAYS.toSeconds(1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,7 +194,9 @@ private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class MetricsViewModel @Inject constructor(
|
||||
class MetricsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val app: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -212,56 +205,50 @@ class MetricsViewModel @Inject constructor(
|
|||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val preferences: SharedPreferences,
|
||||
) : ViewModel(), Logging {
|
||||
) : ViewModel(),
|
||||
Logging {
|
||||
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
|
||||
|
||||
private fun MeshLog.hasValidTraceroute(): Boolean = with(fromRadio.packet) {
|
||||
hasDecoded() && decoded.wantResponse && from == 0 && to == 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.
|
||||
* 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()
|
||||
val defaultUser =
|
||||
MeshProtos.User.newBuilder()
|
||||
.setId(userId)
|
||||
.setLongName(longName)
|
||||
.setShortName(safeUserId)
|
||||
.setHwModel(MeshProtos.HardwareModel.UNSET)
|
||||
.build()
|
||||
|
||||
return Node(
|
||||
num = nodeNum,
|
||||
user = defaultUser,
|
||||
)
|
||||
return Node(num = nodeNum, user = defaultUser)
|
||||
}
|
||||
|
||||
fun getUser(nodeNum: Int) = radioConfigRepository.getUser(nodeNum)
|
||||
val tileSource get() = CustomTileSource.getTileSource(preferences.getInt(MAP_STYLE_ID, 0))
|
||||
|
||||
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) {
|
||||
meshLogRepository.deleteLog(uuid)
|
||||
}
|
||||
val tileSource
|
||||
get() = CustomTileSource.getTileSource(preferences.getInt(MAP_STYLE_ID, 0))
|
||||
|
||||
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)
|
||||
}
|
||||
destNum?.let { meshLogRepository.deleteLogs(it, PortNum.POSITION_APP_VALUE) }
|
||||
}
|
||||
|
||||
fun onServiceAction(action: ServiceAction) = viewModelScope.launch {
|
||||
radioConfigRepository.onServiceAction(action)
|
||||
}
|
||||
fun onServiceAction(action: ServiceAction) = viewModelScope.launch { radioConfigRepository.onServiceAction(action) }
|
||||
|
||||
private val _state = MutableStateFlow(MetricsState.Empty)
|
||||
val state: StateFlow<MetricsState> = _state
|
||||
|
||||
private val _envState = MutableStateFlow(EnvironmentMetricsState())
|
||||
val environmentState: StateFlow<EnvironmentMetricsState> = _envState
|
||||
private val _environmentState = MutableStateFlow(EnvironmentMetricsState())
|
||||
val environmentState: StateFlow<EnvironmentMetricsState> = _environmentState
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
|
@ -274,53 +261,56 @@ class MetricsViewModel @Inject constructor(
|
|||
.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.number.let {
|
||||
deviceHardwareRepository.getDeviceHardwareByModel(it)
|
||||
val deviceHardware =
|
||||
actualNode.user.hwModel.number.let { deviceHardwareRepository.getDeviceHardwareByModel(it) }
|
||||
_state.update { state ->
|
||||
state.copy(node = actualNode, isLocal = destNum == ourNode, deviceHardware = deviceHardware)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.deviceProfileFlow
|
||||
.onEach { profile ->
|
||||
val moduleConfig = profile.moduleConfig
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
node = actualNode,
|
||||
isLocal = destNum == ourNode,
|
||||
deviceHardware = deviceHardware
|
||||
isManaged = profile.config.security.isManaged,
|
||||
isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
displayUnits = profile.config.display.units,
|
||||
)
|
||||
}
|
||||
}.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)
|
||||
.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() },
|
||||
)
|
||||
}
|
||||
_envState.update { state ->
|
||||
state.copy(
|
||||
environmentMetrics = telemetry.filter {
|
||||
it.hasEnvironmentMetrics() &&
|
||||
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.relativeHumidity >= 0f &&
|
||||
!it.environmentMetrics.temperature.isNaN()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets ->
|
||||
_state.update { state ->
|
||||
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
||||
meshLogRepository
|
||||
.getMeshPacketsFrom(destNum)
|
||||
.onEach { meshPackets ->
|
||||
_state.update { state -> state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() }) }
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
combine(
|
||||
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
|
||||
|
|
@ -332,38 +322,46 @@ class MetricsViewModel @Inject constructor(
|
|||
tracerouteResults = response,
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
meshLogRepository.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE)
|
||||
meshLogRepository
|
||||
.getMeshPacketsFrom(destNum, PortNum.POSITION_APP_VALUE)
|
||||
.onEach { packets ->
|
||||
val distinctPositions =
|
||||
packets.mapNotNull { it.toPosition() }.asFlow()
|
||||
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)
|
||||
(old.latitudeI == new.latitudeI && old.longitudeI == new.longitudeI)
|
||||
}
|
||||
.toList()
|
||||
_state.update { state -> state.copy(positionLogs = distinctPositions) }
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.stableRelease.filterNotNull().onEach { latestStable ->
|
||||
_state.update { state ->
|
||||
state.copy(latestStableFirmware = latestStable)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
meshLogRepository
|
||||
.getLogsFrom(destNum, Portnums.PortNum.PAXCOUNTER_APP_VALUE)
|
||||
.onEach { logs -> _state.update { state -> state.copy(paxMetrics = logs) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.alphaRelease.filterNotNull().onEach { latestAlpha ->
|
||||
_state.update { state ->
|
||||
state.copy(latestAlphaFirmware = latestAlpha)
|
||||
}
|
||||
}.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)
|
||||
|
||||
debug("MetricsViewModel created")
|
||||
} else {
|
||||
|
|
@ -380,16 +378,15 @@ class MetricsViewModel @Inject constructor(
|
|||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the persisted Position data out to a CSV file in the specified location.
|
||||
*/
|
||||
/** 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\"")
|
||||
writer.appendLine(
|
||||
"\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"",
|
||||
)
|
||||
|
||||
val dateFormat =
|
||||
SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
|
||||
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
|
||||
|
||||
positions.forEach { position ->
|
||||
val rxDateTime = dateFormat.format(position.time * 1000L)
|
||||
|
|
@ -401,23 +398,23 @@ class MetricsViewModel @Inject constructor(
|
|||
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\"")
|
||||
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) }
|
||||
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}")
|
||||
}
|
||||
} catch (ex: FileNotFoundException) {
|
||||
errormsg("Can't write file error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -78,7 +78,9 @@ import androidx.navigation.NavHostController
|
|||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -293,9 +295,28 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
|
||||
|
||||
val latestStableFirmwareRelease by viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
firmwareEdition?.let { edition ->
|
||||
debug("FirmwareEdition: ${edition.name}")
|
||||
when (edition) {
|
||||
MeshProtos.FirmwareEdition.VANILLA -> {
|
||||
// Handle any specific logic for VANILLA firmware edition if needed
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Handle other firmware editions if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the device is running an old app version or firmware version
|
||||
LaunchedEffect(connectionState, myNodeInfo) {
|
||||
LaunchedEffect(connectionState, myNodeInfo, firmwareEdition) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
myNodeInfo?.let { info ->
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
|
|
|
|||
|
|
@ -255,13 +255,16 @@ private fun handleNodeAction(
|
|||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
||||
navigateToMessages("$channel${node.user.id}")
|
||||
}
|
||||
|
||||
is NodeMenuAction.Remove -> {
|
||||
uiViewModel.handleNodeMenuAction(menuAction)
|
||||
onNavigateUp()
|
||||
}
|
||||
|
||||
else -> uiViewModel.handleNodeMenuAction(menuAction)
|
||||
}
|
||||
}
|
||||
|
||||
is NodeDetailAction.ShareContact -> {
|
||||
/* Handled in NodeDetailContent */
|
||||
}
|
||||
|
|
@ -438,11 +441,19 @@ private fun AdministrationSection(
|
|||
}
|
||||
}
|
||||
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
val latestStable = metricsState.latestStableFirmware
|
||||
val latestAlpha = metricsState.latestAlphaFirmware
|
||||
PreferenceCategory(stringResource(R.string.firmware)) {
|
||||
val firmwareEdition = metricsState.firmwareEdition
|
||||
firmwareEdition?.let {
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.firmware_edition),
|
||||
icon = Icons.Default.Download,
|
||||
value = it.name,
|
||||
)
|
||||
}
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
val latestStable = metricsState.latestStableFirmware
|
||||
val latestAlpha = metricsState.latestAlphaFirmware
|
||||
|
||||
PreferenceCategory(stringResource(R.string.firmware)) {
|
||||
val deviceVersion = DeviceVersion(firmwareVersion.substringBeforeLast("."))
|
||||
val statusColor = deviceVersion.determineFirmwareStatusColor(latestStable, latestAlpha)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue