mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-26 06:25:24 -04:00
feat(firmware): link OTAFIX bootloader from slow-DFU success screen (#5917)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,7 @@ import org.meshtastic.core.resources.firmware_update_release_notes
|
||||
import org.meshtastic.core.resources.firmware_update_retry
|
||||
import org.meshtastic.core.resources.firmware_update_save_dfu_file
|
||||
import org.meshtastic.core.resources.firmware_update_select_file
|
||||
import org.meshtastic.core.resources.firmware_update_slow_bootloader_hint
|
||||
import org.meshtastic.core.resources.firmware_update_source_local
|
||||
import org.meshtastic.core.resources.firmware_update_stable
|
||||
import org.meshtastic.core.resources.firmware_update_success
|
||||
@@ -150,6 +151,14 @@ import org.meshtastic.core.ui.util.rememberSaveFileLauncher
|
||||
|
||||
private const val CYCLE_DELAY_MS = 4500L
|
||||
|
||||
/**
|
||||
* Flashing instructions for the OTAFIX 2.1 bootloader, which lifts the BLE DFU MTU cap (20-byte → 244-byte packets,
|
||||
* ~10× faster updates). Offered on the Success screen after a low-speed transfer — never mid-upload, where leaving the
|
||||
* app could drop the DFU link and brick the device.
|
||||
*/
|
||||
private const val OTAFIX_BOOTLOADER_URL =
|
||||
"https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX#changes-in-otafix-21"
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateViewModel, modifier: Modifier = Modifier) {
|
||||
@@ -328,7 +337,8 @@ private fun FirmwareUpdateContent(
|
||||
|
||||
is FirmwareUpdateState.Error -> ErrorState(error = state.error, onRetry = actions.onRetry)
|
||||
|
||||
is FirmwareUpdateState.Success -> SuccessState(onDone = actions.onDone)
|
||||
is FirmwareUpdateState.Success ->
|
||||
SuccessState(onDone = actions.onDone, wasLowSpeedTransfer = state.wasLowSpeedTransfer)
|
||||
|
||||
is FirmwareUpdateState.AwaitingFileSave -> AwaitingFileSaveState(state, actions.onSaveFile)
|
||||
}
|
||||
@@ -866,8 +876,9 @@ internal fun ErrorState(error: UiText, onRetry: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SuccessState(onDone: () -> Unit) {
|
||||
internal fun SuccessState(wasLowSpeedTransfer: Boolean = false, onDone: () -> Unit) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val openUrl = rememberOpenUrl()
|
||||
LaunchedEffect(Unit) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -884,6 +895,18 @@ internal fun SuccessState(onDone: () -> Unit) {
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
// The just-finished transfer was MTU-capped (stock bootloader). Now that the device is back and it's safe to
|
||||
// leave the app, offer a one-time OTAFIX upgrade tip for faster future updates.
|
||||
if (wasLowSpeedTransfer) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.firmware_update_slow_bootloader_hint),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
TextButton(onClick = { openUrl(OTAFIX_BOOTLOADER_URL) }) { Text(stringResource(Res.string.learn_more)) }
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
val largeHeight = ButtonDefaults.LargeContainerHeight
|
||||
|
||||
@@ -72,8 +72,13 @@ sealed interface FirmwareUpdateState {
|
||||
/** An error occurred at any stage of the update pipeline. */
|
||||
data class Error(val error: UiText) : FirmwareUpdateState
|
||||
|
||||
/** The firmware update completed and the device reconnected successfully. */
|
||||
data object Success : FirmwareUpdateState
|
||||
/**
|
||||
* The firmware update completed and the device reconnected successfully.
|
||||
*
|
||||
* @property wasLowSpeedTransfer True if the upload ran at the MTU-capped low speed (stock bootloader), so the
|
||||
* Success screen can offer a one-time OTAFIX upgrade tip for faster future updates.
|
||||
*/
|
||||
data class Success(val wasLowSpeedTransfer: Boolean = false) : FirmwareUpdateState
|
||||
|
||||
/** UF2 file is ready; waiting for the user to choose a save location (USB flow). */
|
||||
data class AwaitingFileSave(val uf2Artifact: FirmwareArtifact, val fileName: String) : FirmwareUpdateState
|
||||
|
||||
@@ -237,8 +237,9 @@ class FirmwareUpdateViewModel(
|
||||
updateState = { _state.value = it },
|
||||
)
|
||||
|
||||
when (_state.value) {
|
||||
is FirmwareUpdateState.Success -> verifyUpdateResult(originalDeviceAddress)
|
||||
when (val finalState = _state.value) {
|
||||
is FirmwareUpdateState.Success ->
|
||||
verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer)
|
||||
|
||||
is FirmwareUpdateState.Error -> {
|
||||
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
|
||||
@@ -325,8 +326,9 @@ class FirmwareUpdateViewModel(
|
||||
)
|
||||
tempFirmwareFile = updateArtifact ?: extractedFile
|
||||
|
||||
when (_state.value) {
|
||||
is FirmwareUpdateState.Success -> verifyUpdateResult(originalDeviceAddress)
|
||||
when (val finalState = _state.value) {
|
||||
is FirmwareUpdateState.Success ->
|
||||
verifyUpdateResult(originalDeviceAddress, finalState.wasLowSpeedTransfer)
|
||||
|
||||
is FirmwareUpdateState.Error -> {
|
||||
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
|
||||
@@ -355,7 +357,7 @@ class FirmwareUpdateViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verifyUpdateResult(address: String?) {
|
||||
private suspend fun verifyUpdateResult(address: String?, wasLowSpeedTransfer: Boolean = false) {
|
||||
_state.value = FirmwareUpdateState.Verifying
|
||||
|
||||
// Trigger a fresh connection attempt by MeshService using the original prefixed address
|
||||
@@ -378,7 +380,7 @@ class FirmwareUpdateViewModel(
|
||||
Logger.w { "Post-update verification timed out for $address" }
|
||||
_state.value = FirmwareUpdateState.VerificationFailed
|
||||
} else {
|
||||
_state.value = FirmwareUpdateState.Success
|
||||
_state.value = FirmwareUpdateState.Success(wasLowSpeedTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -351,6 +351,6 @@ class Esp32OtaUpdateHandler(
|
||||
.getOrThrow()
|
||||
Logger.i { "ESP32 OTA: Firmware stream completed" }
|
||||
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ class SecureDfuHandler(
|
||||
)
|
||||
|
||||
completed = true
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success(wasLowSpeedTransfer = transport.isLowSpeedTransfer))
|
||||
zipFile
|
||||
} finally {
|
||||
// Send ABORT if cancelled mid-transfer, then always clean up.
|
||||
|
||||
@@ -129,7 +129,7 @@ class FirmwareUpdateIntegrationTest {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
|
||||
updateState(FirmwareUpdateState.Updating(ProgressState()))
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success())
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ class FirmwareUpdateViewModelTest {
|
||||
.calls {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success())
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ class FirmwareUpdateViewModelFileTest {
|
||||
.calls {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success())
|
||||
null
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ class FirmwareUpdateViewModelFileTest {
|
||||
.calls {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val updateState = it.args[3] as (FirmwareUpdateState) -> Unit
|
||||
updateState(FirmwareUpdateState.Success)
|
||||
updateState(FirmwareUpdateState.Success())
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user