mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: firmware bootloader ota warnings (#3846)
This commit is contained in:
parent
b18ad56113
commit
d4a30c0b24
8 changed files with 253 additions and 30 deletions
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 org.meshtastic.core.data.datasource
|
||||
|
||||
import android.app.Application
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.meshtastic.core.model.BootloaderOtaQuirk
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val application: Application) {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun loadBootloaderOtaQuirksFromJsonAsset(): List<BootloaderOtaQuirk> = runCatching {
|
||||
val inputStream = application.assets.open("device_bootloader_ota_quirks.json")
|
||||
inputStream.use { Json.decodeFromStream<ListWrapper>(it).devices }
|
||||
}
|
||||
.onFailure { e -> Timber.w(e, "Failed to load device_bootloader_ota_quirks.json") }
|
||||
.getOrDefault(emptyList())
|
||||
|
||||
@Serializable private data class ListWrapper(val devices: List<BootloaderOtaQuirk> = emptyList())
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ package org.meshtastic.core.data.repository
|
|||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
|
||||
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
|
||||
import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource
|
||||
import org.meshtastic.core.database.entity.DeviceHardwareEntity
|
||||
import org.meshtastic.core.database.entity.asExternalModel
|
||||
import org.meshtastic.core.model.BootloaderOtaQuirk
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
|
||||
import timber.log.Timber
|
||||
|
|
@ -38,6 +40,7 @@ constructor(
|
|||
private val remoteDataSource: DeviceHardwareRemoteDataSource,
|
||||
private val localDataSource: DeviceHardwareLocalDataSource,
|
||||
private val jsonDataSource: DeviceHardwareJsonDataSource,
|
||||
private val bootloaderOtaQuirksJsonDataSource: BootloaderOtaQuirksJsonDataSource,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -53,6 +56,7 @@ constructor(
|
|||
* @param forceRefresh If true, the local cache will be invalidated and data will be fetched remotely.
|
||||
* @return A [Result] containing the [DeviceHardware] on success (or null if not found), or an exception on failure.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d(
|
||||
|
|
@ -61,6 +65,8 @@ constructor(
|
|||
forceRefresh,
|
||||
)
|
||||
|
||||
val quirks = loadQuirks()
|
||||
|
||||
if (forceRefresh) {
|
||||
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
|
||||
localDataSource.deleteAllDeviceHardware()
|
||||
|
|
@ -69,7 +75,9 @@ constructor(
|
|||
val cachedEntity = localDataSource.getByHwModel(hwModel)
|
||||
if (cachedEntity != null && !cachedEntity.isStale()) {
|
||||
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
|
||||
return@withContext Result.success(cachedEntity.asExternalModel())
|
||||
return@withContext Result.success(
|
||||
applyBootloaderQuirk(hwModel, cachedEntity.asExternalModel(), quirks),
|
||||
)
|
||||
}
|
||||
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
|
||||
}
|
||||
|
|
@ -94,7 +102,7 @@ constructor(
|
|||
}
|
||||
.onSuccess {
|
||||
// Successfully fetched and found the model
|
||||
return@withContext Result.success(it)
|
||||
return@withContext Result.success(applyBootloaderQuirk(hwModel, it, quirks))
|
||||
}
|
||||
.onFailure { e ->
|
||||
Timber.w(
|
||||
|
|
@ -107,7 +115,9 @@ constructor(
|
|||
val staleEntity = localDataSource.getByHwModel(hwModel)
|
||||
if (staleEntity != null && !staleEntity.isIncomplete()) {
|
||||
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
|
||||
return@withContext Result.success(staleEntity.asExternalModel())
|
||||
return@withContext Result.success(
|
||||
applyBootloaderQuirk(hwModel, staleEntity.asExternalModel(), quirks),
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Fallback to bundled JSON if cache is empty or incomplete
|
||||
|
|
@ -116,36 +126,38 @@ constructor(
|
|||
if (staleEntity == null) "empty" else "incomplete",
|
||||
hwModel,
|
||||
)
|
||||
return@withContext loadFromBundledJson(hwModel)
|
||||
return@withContext loadFromBundledJson(hwModel, quirks)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadFromBundledJson(hwModel: Int): Result<DeviceHardware?> = runCatching {
|
||||
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
|
||||
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: bundled JSON returned %d device hardware entries",
|
||||
jsonHardware.size,
|
||||
)
|
||||
private suspend fun loadFromBundledJson(hwModel: Int, quirks: List<BootloaderOtaQuirk>): Result<DeviceHardware?> =
|
||||
runCatching {
|
||||
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
|
||||
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: bundled JSON returned %d device hardware entries",
|
||||
jsonHardware.size,
|
||||
)
|
||||
|
||||
localDataSource.insertAllDeviceHardware(jsonHardware)
|
||||
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
|
||||
hwModel,
|
||||
if (fromDb != null) "succeeded" else "returned null",
|
||||
)
|
||||
fromDb
|
||||
}
|
||||
.also { result ->
|
||||
result.exceptionOrNull()?.let { e ->
|
||||
Timber.e(
|
||||
e,
|
||||
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=%d",
|
||||
hwModel,
|
||||
)
|
||||
}
|
||||
localDataSource.insertAllDeviceHardware(jsonHardware)
|
||||
val base = localDataSource.getByHwModel(hwModel)?.asExternalModel()
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
|
||||
hwModel,
|
||||
if (base != null) "succeeded" else "returned null",
|
||||
)
|
||||
|
||||
applyBootloaderQuirk(hwModel, base, quirks)
|
||||
}
|
||||
.also { result ->
|
||||
result.exceptionOrNull()?.let { e ->
|
||||
Timber.e(
|
||||
e,
|
||||
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=%d",
|
||||
hwModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the cached entity is missing important fields and should be refreshed. */
|
||||
private fun DeviceHardwareEntity.isIncomplete(): Boolean =
|
||||
|
|
@ -160,6 +172,40 @@ constructor(
|
|||
private fun DeviceHardwareEntity.isStale(): Boolean =
|
||||
isIncomplete() || (System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
|
||||
|
||||
private fun loadQuirks(): List<BootloaderOtaQuirk> {
|
||||
val quirks = bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset()
|
||||
Timber.d("DeviceHardwareRepository: loaded %d bootloader quirks", quirks.size)
|
||||
return quirks
|
||||
}
|
||||
|
||||
private fun applyBootloaderQuirk(
|
||||
hwModel: Int,
|
||||
base: DeviceHardware?,
|
||||
quirks: List<BootloaderOtaQuirk>,
|
||||
): DeviceHardware? {
|
||||
if (base == null) return null
|
||||
|
||||
val quirk = quirks.firstOrNull { it.hwModel == hwModel }
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: applyBootloaderQuirk for hwModel=%d, quirk found=%b",
|
||||
hwModel,
|
||||
quirk != null,
|
||||
)
|
||||
return if (quirk != null) {
|
||||
Timber.d(
|
||||
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=%b, infoUrl=%s",
|
||||
quirk.requiresBootloaderUpgradeForOta,
|
||||
quirk.infoUrl,
|
||||
)
|
||||
base.copy(
|
||||
requiresBootloaderUpgradeForOta = quirk.requiresBootloaderUpgradeForOta,
|
||||
bootloaderInfoUrl = quirk.infoUrl,
|
||||
)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.DAYS.toMillis(1)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue