diff --git a/app/build.gradle b/app/build.gradle index d80151c92..f46a402ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,8 @@ android { versionCode Configs.VERSION_CODE // format is Mmmss (where M is 1+the numeric major number versionName Configs.VERSION_NAME testInstrumentationRunner "com.geeksville.mesh.TestRunner" - buildConfigField("String", "MIN_DEVICE_VERSION", "\"${Configs.MIN_DEVICE_VERSION}\"") + buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"") + buildConfigField("String", "ABS_MIN_FW_VERSION", "\"${Configs.ABS_MIN_FW_VERSION}\"") // per https://developer.android.com/studio/write/vector-asset-studio vectorDrawables.useSupportLibrary = true } @@ -111,6 +112,23 @@ android { } } +/* + * 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 . + */ + // per protobuf-gradle-plugin docs, this is recommended for android protobuf { protoc { diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/FirmwareReleaseEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/FirmwareReleaseEntity.kt index baf540a3f..b5646db76 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/FirmwareReleaseEntity.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/FirmwareReleaseEntity.kt @@ -80,6 +80,12 @@ fun FirmwareReleaseEntity.asDeviceVersion(): DeviceVersion { ) } +fun FirmwareRelease.asDeviceVersion(): DeviceVersion { + return DeviceVersion( + id.substringBeforeLast(".").replace("v", "") + ) +} + enum class FirmwareReleaseType { STABLE, ALPHA diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index d97c3cd49..f0d89e528 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -55,6 +55,8 @@ import com.geeksville.mesh.database.QuickChatActionRepository import com.geeksville.mesh.database.entity.MyNodeEntity import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction +import com.geeksville.mesh.database.entity.asDeviceVersion +import com.geeksville.mesh.repository.api.FirmwareReleaseRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.location.LocationRepository import com.geeksville.mesh.repository.radio.RadioInterfaceService @@ -77,6 +79,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -183,6 +186,7 @@ class UIViewModel @Inject constructor( private val packetRepository: PacketRepository, private val quickChatActionRepository: QuickChatActionRepository, private val locationRepository: LocationRepository, + firmwareReleaseRepository: FirmwareReleaseRepository, private val preferences: SharedPreferences ) : ViewModel(), Logging { @@ -560,6 +564,8 @@ class UIViewModel @Inject constructor( showSnackbar(R.string.channel_invalid) } + val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() } + /** * Called immediately after activity observes requestChannelUrl */ diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 333fd377e..de251d1fe 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -193,7 +193,8 @@ class MeshService : Service(), Logging { /** The minimum firmware version we know how to talk to. We'll still be able * to talk to 2.0 firmwares but only well enough to ask them to firmware update. */ - val minDeviceVersion = DeviceVersion(BuildConfig.MIN_DEVICE_VERSION) + val minDeviceVersion = DeviceVersion(BuildConfig.MIN_FW_VERSION) + val absoluteMinDeviceVersion = DeviceVersion(BuildConfig.ABS_MIN_FW_VERSION) } enum class ConnectionState { diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 73970d66c..555e39bc7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -49,6 +49,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -195,6 +196,8 @@ private fun VersionChecks( val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle() val context = LocalContext.current + val latestStableFirmwareRelease by + viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4")) // Check if the device is running an old app version or firmware version LaunchedEffect(connectionState, myNodeInfo) { if (connectionState == MeshService.ConnectionState.CONNECTED) { @@ -211,18 +214,31 @@ private fun VersionChecks( MeshService.changeDeviceAddress(context, service, "n") } ) - } else if (curVer < MeshService.minDeviceVersion) { + } else if (curVer < MeshService.absoluteMinDeviceVersion) { val title = context.getString(R.string.firmware_too_old) val message = context.getString(R.string.firmware_old) viewModel.showAlert( title = title, - message = message, + html = message, dismissable = false, onConfirm = { val service = viewModel.meshService ?: return@showAlert MeshService.changeDeviceAddress(context, service, "n") } ) + } else if (curVer < MeshService.minDeviceVersion) { + val title = context.getString(R.string.should_update_firmware) + val message = + context.getString( + R.string.should_update, + latestStableFirmwareRelease.asString + ) + viewModel.showAlert( + title = title, + message = message, + dismissable = false, + onConfirm = {} + ) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/AlertDialogs.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/AlertDialogs.kt index a80760730..760e21ba7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/AlertDialogs.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/AlertDialogs.kt @@ -55,7 +55,7 @@ fun SimpleAlertDialog( style = SpanStyle( textDecoration = TextDecoration.Underline, fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.tertiary + color = MaterialTheme.colorScheme.primary ) ) ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59923c12a..0c81393e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -170,7 +170,7 @@ Message notifications Alert notifications Protocol stress test - Firmware update required + Firmware update required. The radio firmware is too old to talk to this application. For more information on this see our Firmware Installation guide. OK You must set a region! @@ -667,4 +667,6 @@ By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions. I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT I agree. + Firmware Update Recommended. + To benefit from the latest fixes and features, please update your node firmware.\n\nLatest stable firmware version: %1$s diff --git a/buildSrc/src/main/kotlin/Configs.kt b/buildSrc/src/main/kotlin/Configs.kt index 37e28021b..34d2a19c9 100644 --- a/buildSrc/src/main/kotlin/Configs.kt +++ b/buildSrc/src/main/kotlin/Configs.kt @@ -23,5 +23,6 @@ object Configs { const val VERSION_CODE = 30609 // format is Mmmss (where M is 1+the numeric major number const val VERSION_NAME = "2.6.9" const val USE_CRASHLYTICS = false // Set to true if you want to use Firebase Crashlytics - const val MIN_DEVICE_VERSION = "2.5.14" // Minimum device firmware version supported by this app + const val MIN_FW_VERSION = "2.5.14" // Minimum device firmware version supported by this app + const val ABS_MIN_FW_VERSION = "2.3.15" // Minimum device firmware version supported by this app }