mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: network module (#1905)
This commit is contained in:
parent
520d058546
commit
02bb3f02e4
80 changed files with 2165 additions and 15032 deletions
|
|
@ -35,7 +35,8 @@ data class MyNodeInfo(
|
|||
val maxChannels: Int,
|
||||
val hasWifi: Boolean,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float
|
||||
val airUtilTx: Float,
|
||||
val deviceId: String?,
|
||||
) : Parcelable {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String get() = "$model $firmwareVersion"
|
||||
|
|
|
|||
|
|
@ -129,4 +129,20 @@ class Converters : Logging {
|
|||
fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? {
|
||||
return value.toByteArray()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringList(value: String?): List<String>? {
|
||||
if (value == null) {
|
||||
return null
|
||||
}
|
||||
return Json.decodeFromString<List<String>>(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStringList(list: List<String>?): String? {
|
||||
if (list == null) {
|
||||
return null
|
||||
}
|
||||
return Json.encodeToString(list)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import android.app.Application
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
|
|
@ -55,4 +57,14 @@ class DatabaseModule {
|
|||
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao {
|
||||
return database.quickChatActionDao()
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao {
|
||||
return database.deviceHardwareDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao {
|
||||
return database.firmwareReleaseDao()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,15 @@ import androidx.room.Room
|
|||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
|
|
@ -48,6 +52,8 @@ import com.geeksville.mesh.database.entity.ReactionEntity
|
|||
QuickChatAction::class,
|
||||
ReactionEntity::class,
|
||||
MetadataEntity::class,
|
||||
DeviceHardwareEntity::class,
|
||||
FirmwareReleaseEntity::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 3, to = 4),
|
||||
|
|
@ -63,8 +69,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
|
|||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
],
|
||||
version = 16,
|
||||
version = 17,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
@ -73,6 +80,8 @@ abstract class MeshtasticDatabase : RoomDatabase() {
|
|||
abstract fun packetDao(): PacketDao
|
||||
abstract fun meshLogDao(): MeshLogDao
|
||||
abstract fun quickChatActionDao(): QuickChatActionDao
|
||||
abstract fun deviceHardwareDao(): DeviceHardwareDao
|
||||
abstract fun firmwareReleaseDao(): FirmwareReleaseDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase {
|
||||
|
|
@ -82,7 +91,7 @@ abstract class MeshtasticDatabase : RoomDatabase() {
|
|||
MeshtasticDatabase::class.java,
|
||||
"meshtastic_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
|
||||
@Dao
|
||||
interface DeviceHardwareDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(deviceHardware: DeviceHardwareEntity)
|
||||
|
||||
@Query("SELECT * FROM device_hardware WHERE hwModel = :hwModel")
|
||||
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity?
|
||||
|
||||
@Query("DELETE FROM device_hardware")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
|
||||
@Dao
|
||||
interface FirmwareReleaseDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(firmwareReleaseEntity: FirmwareReleaseEntity)
|
||||
|
||||
@Query("DELETE FROM firmware_release")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("SELECT * FROM firmware_release")
|
||||
suspend fun getAllReleases(): List<FirmwareReleaseEntity>?
|
||||
|
||||
@Query("SELECT * FROM firmware_release WHERE release_type = :releaseType")
|
||||
suspend fun getReleasesByType(releaseType: FirmwareReleaseType): List<FirmwareReleaseEntity>?
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.model.DeviceHardware
|
||||
import com.geeksville.mesh.network.model.NetworkDeviceHardware
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "device_hardware")
|
||||
data class DeviceHardwareEntity(
|
||||
@ColumnInfo(name = "actively_supported") val activelySupported: Boolean,
|
||||
val architecture: String,
|
||||
@ColumnInfo(name = "display_name") val displayName: String,
|
||||
@ColumnInfo(name = "has_ink_hud") val hasInkHud: Boolean? = null,
|
||||
@ColumnInfo(name = "has_mui") val hasMui: Boolean? = null,
|
||||
@PrimaryKey val hwModel: Int,
|
||||
@ColumnInfo(name = "hw_model_slug") val hwModelSlug: String,
|
||||
val images: List<String>?,
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "partition_scheme") val partitionScheme: String? = null,
|
||||
@ColumnInfo(name = "platformio_target") val platformioTarget: String,
|
||||
@ColumnInfo(name = "requires_dfu") val requiresDfu: Boolean?,
|
||||
@ColumnInfo(name = "support_level") val supportLevel: Int?,
|
||||
val tags: List<String>?,
|
||||
)
|
||||
|
||||
fun NetworkDeviceHardware.asEntity() = DeviceHardwareEntity(
|
||||
activelySupported = activelySupported,
|
||||
architecture = architecture,
|
||||
displayName = displayName,
|
||||
hasInkHud = hasInkHud,
|
||||
hasMui = hasMui,
|
||||
hwModel = hwModel,
|
||||
hwModelSlug = hwModelSlug,
|
||||
images = images,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
partitionScheme = partitionScheme,
|
||||
platformioTarget = platformioTarget,
|
||||
requiresDfu = requiresDfu,
|
||||
supportLevel = supportLevel,
|
||||
tags = tags,
|
||||
)
|
||||
|
||||
fun DeviceHardwareEntity.asExternalModel() = DeviceHardware(
|
||||
activelySupported = activelySupported,
|
||||
architecture = architecture,
|
||||
displayName = displayName,
|
||||
hasInkHud = hasInkHud,
|
||||
hasMui = hasMui,
|
||||
hwModel = hwModel,
|
||||
hwModelSlug = hwModelSlug,
|
||||
images = images,
|
||||
partitionScheme = partitionScheme,
|
||||
platformioTarget = platformioTarget,
|
||||
requiresDfu = requiresDfu,
|
||||
supportLevel = supportLevel,
|
||||
tags = tags,
|
||||
)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.network.model.NetworkFirmwareRelease
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "firmware_release")
|
||||
data class FirmwareReleaseEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String = "",
|
||||
@ColumnInfo(name = "page_url")
|
||||
val pageUrl: String = "",
|
||||
@ColumnInfo(name = "release_notes")
|
||||
val releaseNotes: String = "",
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String = "",
|
||||
@ColumnInfo(name = "zip_url")
|
||||
val zipUrl: String = "",
|
||||
@ColumnInfo(name = "last_updated")
|
||||
val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "release_type")
|
||||
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
fun NetworkFirmwareRelease.asEntity(releaseType: FirmwareReleaseType) = FirmwareReleaseEntity(
|
||||
id = id,
|
||||
pageUrl = pageUrl,
|
||||
releaseNotes = releaseNotes,
|
||||
title = title,
|
||||
zipUrl = zipUrl,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
releaseType = releaseType,
|
||||
)
|
||||
|
||||
fun FirmwareReleaseEntity.asExternalModel() = FirmwareRelease(
|
||||
id = id,
|
||||
pageUrl = pageUrl,
|
||||
releaseNotes = releaseNotes,
|
||||
title = title,
|
||||
zipUrl = zipUrl,
|
||||
lastUpdated = lastUpdated,
|
||||
releaseType = releaseType,
|
||||
)
|
||||
|
||||
data class FirmwareRelease(
|
||||
val id: String = "",
|
||||
val pageUrl: String = "",
|
||||
val releaseNotes: String = "",
|
||||
val title: String = "",
|
||||
val zipUrl: String = "",
|
||||
val lastUpdated: Long = System.currentTimeMillis(),
|
||||
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
enum class FirmwareReleaseType {
|
||||
STABLE,
|
||||
ALPHA
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ data class MyNodeEntity(
|
|||
val minAppVersion: Int,
|
||||
val maxChannels: Int,
|
||||
val hasWifi: Boolean,
|
||||
val deviceId: String? = "unknown",
|
||||
) {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String get() = "$model $firmwareVersion"
|
||||
|
|
@ -52,5 +53,6 @@ data class MyNodeEntity(
|
|||
hasWifi = hasWifi,
|
||||
channelUtilization = 0f,
|
||||
airUtilTx = 0f,
|
||||
deviceId = deviceId,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.MeshProtos.HardwareModel
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.Position
|
||||
import com.geeksville.mesh.Portnums.PortNum
|
||||
|
|
@ -37,9 +36,12 @@ import com.geeksville.mesh.R
|
|||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.model.map.CustomTileSource
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
||||
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
|
||||
|
|
@ -56,11 +58,9 @@ import kotlinx.coroutines.flow.toList
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -79,6 +79,9 @@ data class MetricsState(
|
|||
val tracerouteResults: List<MeshPacket> = emptyList(),
|
||||
val positionLogs: List<Position> = emptyList(),
|
||||
val deviceHardware: DeviceHardware? = null,
|
||||
val isLocalDevice: Boolean = false,
|
||||
val latestStableFirmware: FirmwareRelease? = null,
|
||||
val latestAlphaFirmware: FirmwareRelease? = null,
|
||||
) {
|
||||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||
|
|
@ -188,6 +191,7 @@ private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
|
|||
null
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class MetricsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
|
@ -195,6 +199,8 @@ class MetricsViewModel @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatchers,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val preferences: SharedPreferences,
|
||||
) : ViewModel(), Logging {
|
||||
private val destNum = savedStateHandle.toRoute<Route.NodeDetail>().destNum
|
||||
|
|
@ -229,25 +235,23 @@ class MetricsViewModel @Inject constructor(
|
|||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
private var deviceHardwareList: List<DeviceHardware> = listOf()
|
||||
|
||||
init {
|
||||
destNum?.let {
|
||||
radioConfigRepository.nodeDBbyNum
|
||||
.mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.onEach { (node, ourNode) ->
|
||||
_state.update { state -> state.copy(node = node, isLocal = destNum == ourNode) }
|
||||
node?.user?.hwModel?.let { hwModel ->
|
||||
val deviceHardware = getDeviceHardwareFromHardwareModel(hwModel)
|
||||
deviceHardware?.let {
|
||||
_state.update { state ->
|
||||
state.copy(deviceHardware = it)
|
||||
}
|
||||
}
|
||||
val deviceHardware = node?.user?.hwModel?.number?.let {
|
||||
deviceHardwareRepository.getDeviceHardwareByModel(it)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
node = node,
|
||||
isLocal = destNum == ourNode,
|
||||
deviceHardware = deviceHardware
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.deviceProfileFlow.onEach { profile ->
|
||||
val moduleConfig = profile.moduleConfig
|
||||
|
|
@ -308,6 +312,18 @@ class MetricsViewModel @Inject constructor(
|
|||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.stableRelease.onEach { latestStable ->
|
||||
_state.update { state ->
|
||||
state.copy(latestStableFirmware = latestStable)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
firmwareReleaseRepository.alphaRelease.onEach { latestAlpha ->
|
||||
_state.update { state ->
|
||||
state.copy(latestAlphaFirmware = latestAlpha)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
debug("MetricsViewModel created")
|
||||
}
|
||||
}
|
||||
|
|
@ -361,22 +377,4 @@ class MetricsViewModel @Inject constructor(
|
|||
errormsg("Can't write file error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceHardwareFromHardwareModel(
|
||||
hwModel: HardwareModel
|
||||
): DeviceHardware? {
|
||||
if (deviceHardwareList.isEmpty()) {
|
||||
try {
|
||||
val json =
|
||||
app.assets.open("device_hardware.json").bufferedReader().use { it.readText() }
|
||||
deviceHardwareList = Json.decodeFromString<List<DeviceHardware>>(json)
|
||||
return deviceHardwareList.find { it.hwModel == hwModel.number }
|
||||
} catch (ex: IOException) {
|
||||
errormsg("Can't read device_hardware.json error: ${ex.message}")
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
errormsg(ex.message.toString())
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import android.app.Application
|
||||
import com.geeksville.mesh.network.model.NetworkDeviceHardware
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareJsonDataSource @Inject constructor(
|
||||
private val application: Application,
|
||||
) {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun loadDeviceHardwareFromJsonAsset(): List<NetworkDeviceHardware> {
|
||||
val inputStream = application.assets.open("device_hardware.json")
|
||||
return Json.decodeFromStream<List<NetworkDeviceHardware>>(inputStream)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.asEntity
|
||||
import com.geeksville.mesh.network.model.NetworkDeviceHardware
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareLocalDataSource @Inject constructor(
|
||||
private val deviceHardwareDaoLazy: dagger.Lazy<DeviceHardwareDao>
|
||||
) {
|
||||
private val deviceHardwareDao by lazy {
|
||||
deviceHardwareDaoLazy.get()
|
||||
}
|
||||
|
||||
suspend fun insertAllDeviceHardware(deviceHardware: List<NetworkDeviceHardware>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
deviceHardware.forEach { deviceHardware ->
|
||||
deviceHardwareDao.insert(deviceHardware.asEntity())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAllDeviceHardware() = withContext(Dispatchers.IO) {
|
||||
deviceHardwareDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity? = withContext(Dispatchers.IO) {
|
||||
deviceHardwareDao.getByHwModel(hwModel)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.asExternalModel
|
||||
import com.geeksville.mesh.model.DeviceHardware
|
||||
import com.geeksville.mesh.network.DeviceHardwareRemoteDataSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareRepository @Inject constructor(
|
||||
private val apiDataSource: DeviceHardwareRemoteDataSource,
|
||||
private val localDataSource: DeviceHardwareLocalDataSource,
|
||||
private val jsonDataSource: DeviceHardwareJsonDataSource,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
// 1 day
|
||||
private const val CACHE_EXPIRATION_TIME_MS = 24 * 60 * 60 * 1000L
|
||||
}
|
||||
|
||||
suspend fun getDeviceHardwareByModel(hwModel: Int, refresh: Boolean = false): DeviceHardware? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
if (cachedHardware != null && !isCacheExpired(cachedHardware.lastUpdated)) {
|
||||
debug("Using recent cached device hardware")
|
||||
val externalModel = cachedHardware.asExternalModel()
|
||||
return@withContext externalModel
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching device hardware from server")
|
||||
localDataSource.insertAllDeviceHardware(apiDataSource.getAllDeviceHardware())
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
return@withContext externalModel
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch device hardware from server: ${e.message}")
|
||||
var cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
if (cachedHardware != null) {
|
||||
debug("Using stale cached device hardware")
|
||||
return@withContext cachedHardware.asExternalModel()
|
||||
}
|
||||
debug("Loading and caching device hardware from local JSON asset")
|
||||
localDataSource.insertAllDeviceHardware(jsonDataSource.loadDeviceHardwareFromJsonAsset())
|
||||
cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
return@withContext externalModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invalidateCache() {
|
||||
localDataSource.deleteAllDeviceHardware()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import android.app.Application
|
||||
import com.geeksville.mesh.network.model.NetworkFirmwareReleases
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseJsonDataSource @Inject constructor(
|
||||
private val application: Application,
|
||||
) {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun loadFirmwareReleaseFromJsonAsset(): NetworkFirmwareReleases {
|
||||
val inputStream = application.assets.open("firmware_releases.json")
|
||||
val result = inputStream.use {
|
||||
Json.decodeFromStream<NetworkFirmwareReleases>(inputStream)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
import com.geeksville.mesh.database.entity.asEntity
|
||||
import com.geeksville.mesh.network.model.NetworkFirmwareRelease
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseLocalDataSource @Inject constructor(
|
||||
private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>
|
||||
) {
|
||||
private val firmwareReleaseDao by lazy {
|
||||
firmwareReleaseDaoLazy.get()
|
||||
}
|
||||
|
||||
suspend fun insertFirmwareReleases(
|
||||
firmwareReleases: List<NetworkFirmwareRelease>,
|
||||
releaseType: FirmwareReleaseType
|
||||
) =
|
||||
withContext(Dispatchers.IO) {
|
||||
firmwareReleases.forEach { firmwareRelease ->
|
||||
firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAllFirmwareReleases() = withContext(Dispatchers.IO) {
|
||||
firmwareReleaseDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun getLatestRelease(releaseType: FirmwareReleaseType): FirmwareReleaseEntity? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val releases = firmwareReleaseDao.getReleasesByType(releaseType)
|
||||
val latestRelease =
|
||||
releases?.firstOrNull()
|
||||
return@withContext latestRelease
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.repository.api
|
||||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
import com.geeksville.mesh.database.entity.asExternalModel
|
||||
import com.geeksville.mesh.network.FirmwareReleaseRemoteDataSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseRepository @Inject constructor(
|
||||
private val apiDataSource: FirmwareReleaseRemoteDataSource,
|
||||
private val localDataSource: FirmwareReleaseLocalDataSource,
|
||||
private val jsonDataSource: FirmwareReleaseJsonDataSource,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
// 1 hour
|
||||
private const val CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000L
|
||||
}
|
||||
|
||||
val stableRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.STABLE)
|
||||
|
||||
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
|
||||
|
||||
private fun getLatestFirmware(
|
||||
releaseType: FirmwareReleaseType,
|
||||
refresh: Boolean = false
|
||||
): Flow<FirmwareRelease?> = flow {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
|
||||
debug("Using recent cached firmware release")
|
||||
val externalModel = cachedRelease.asExternalModel()
|
||||
emit(externalModel)
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching firmware releases from server")
|
||||
val networkFirmwareReleases = apiDataSource.getFirmwareReleases()
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch firmware releases from server: ${e.message}")
|
||||
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invalidateCache() {
|
||||
localDataSource.deleteAllFirmwareReleases()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
}
|
||||
|
|
@ -1604,6 +1604,7 @@ class MeshService : Service(), Logging {
|
|||
minAppVersion = minAppVersion,
|
||||
maxChannels = 8,
|
||||
hasWifi = metadata.hasWifi,
|
||||
deviceId = deviceId.toStringUtf8(),
|
||||
)
|
||||
}
|
||||
serviceScope.handledLaunch {
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ import androidx.compose.material.icons.filled.Share
|
|||
import androidx.compose.material.icons.filled.SignalCellularAlt
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.Thermostat
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.filled.WaterDrop
|
||||
import androidx.compose.material.icons.filled.Work
|
||||
import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.material.icons.outlined.NoCell
|
||||
import androidx.compose.material.icons.twotone.Verified
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -79,6 +79,7 @@ import androidx.compose.ui.draw.rotate
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
|
|
@ -90,7 +91,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.SuccessResult
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.model.DeviceHardware
|
||||
import com.geeksville.mesh.model.MetricsState
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
|
|
@ -207,6 +212,33 @@ private fun NodeDetailList(
|
|||
NodeDetailsContent(node)
|
||||
}
|
||||
}
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
item {
|
||||
PreferenceCategory(stringResource(R.string.firmware)) {
|
||||
val latestStableFirmware = metricsState.latestStableFirmware
|
||||
val latestAlphaFirmware = metricsState.latestAlphaFirmware
|
||||
NodeDetailRow(
|
||||
label = "Installed",
|
||||
icon = Icons.Default.Memory,
|
||||
value = firmwareVersion.substringBeforeLast(".")
|
||||
)
|
||||
latestStableFirmware?.let { stable ->
|
||||
NodeDetailRow(
|
||||
label = "Latest stable",
|
||||
icon = Icons.Default.Memory,
|
||||
value = stable.id.substringBeforeLast(".").replace("v", "")
|
||||
)
|
||||
}
|
||||
latestAlphaFirmware?.let { alpha ->
|
||||
NodeDetailRow(
|
||||
label = "Latest alpha",
|
||||
icon = Icons.Default.Memory,
|
||||
value = alpha.id.substringBeforeLast(".").replace("v", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
DeviceActions(
|
||||
|
|
@ -332,25 +364,19 @@ private fun DeviceDetailsContent(
|
|||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
DeviceHardwareImage(
|
||||
deviceHardware = deviceHardware,
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
)
|
||||
DeviceHardwareImage(deviceHardware, Modifier.fillMaxSize())
|
||||
}
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.hardware),
|
||||
icon = Icons.Default.Router,
|
||||
value = hwModelName
|
||||
)
|
||||
if (isSupported) {
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.supported),
|
||||
icon = Icons.Default.Verified,
|
||||
value = "",
|
||||
iconTint = Color.Green
|
||||
)
|
||||
}
|
||||
NodeDetailRow(
|
||||
label = if (isSupported) stringResource(R.string.supported) else "Supported by Community",
|
||||
icon = if (isSupported) Icons.TwoTone.Verified else ImageVector.vectorResource(R.drawable.unverified),
|
||||
value = "",
|
||||
iconTint = if (isSupported) Color.Green else Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -358,20 +384,37 @@ fun DeviceHardwareImage(
|
|||
deviceHardware: DeviceHardware,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hwImg = deviceHardware.images?.lastOrNull()
|
||||
if (hwImg != null) {
|
||||
val imageUrl = "file:///android_asset/device_hardware/$hwImg"
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder = painterResource(R.drawable.hw_unknown),
|
||||
error = painterResource(R.drawable.hw_unknown),
|
||||
fallback = painterResource(R.drawable.hw_unknown),
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
val hwImg = deviceHardware.images?.get(1) ?: deviceHardware.images?.get(0) ?: "unknown.svg"
|
||||
val imageUrl = "https://flasher.meshtastic.org/img/devices/$hwImg"
|
||||
val listener = object : ImageRequest.Listener {
|
||||
override fun onStart(request: ImageRequest) {
|
||||
super.onStart(request)
|
||||
debug("Image request started")
|
||||
}
|
||||
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
super.onError(request, result)
|
||||
debug("Image request failed: ${result.throwable.message}")
|
||||
}
|
||||
|
||||
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
|
||||
super.onSuccess(request, result)
|
||||
debug("Image request succeeded: ${result.dataSource.name}")
|
||||
}
|
||||
}
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.listener(listener)
|
||||
.data(imageUrl)
|
||||
.build(),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder = painterResource(R.drawable.hw_unknown),
|
||||
error = painterResource(R.drawable.hw_unknown),
|
||||
fallback = painterResource(R.drawable.hw_unknown),
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -435,13 +478,6 @@ private fun NodeDetailsContent(
|
|||
value = formatUptime(node.deviceMetrics.uptimeSeconds)
|
||||
)
|
||||
}
|
||||
if (node.metadata != null) {
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.firmware_version),
|
||||
icon = Icons.Default.Memory,
|
||||
value = node.metadata.firmwareVersion.substringBeforeLast(".")
|
||||
)
|
||||
}
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.node_sort_last_heard),
|
||||
icon = Icons.Default.History,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue