From b8b2a8a86ca299a927fd34fd5b310f2b4f52bd33 Mon Sep 17 00:00:00 2001 From: andrekir Date: Fri, 11 Mar 2022 00:12:48 -0300 Subject: [PATCH] improve firmware update --- .../geeksville/mesh/service/MeshService.kt | 11 ++-- .../mesh/service/SoftwareUpdateService.kt | 55 +++++++++++-------- .../geeksville/mesh/ui/SettingsFragment.kt | 13 +++++ 3 files changed, 50 insertions(+), 29 deletions(-) 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 58f927590..f3da61bb1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1304,13 +1304,14 @@ class MeshService : Service(), Logging { myInfo.myNodeNum // Note: can't use the normal property because myNodeInfo not yet setup val ni = nodeDBbyNodeNum[nodeNum] // can't use toNodeInfo because too early val hwModelStr = ni?.user?.hwModelString + setFirmwareUpdateFilename(hwModelStr) val mi = with(myInfo) { MyNodeInfo( myNodeNum, hasGps, hwModelStr, firmwareVersion, - firmwareUpdateFilename != null, + firmwareUpdateFilename?.appLoad != null && firmwareUpdateFilename?.littlefs != null, isBluetoothInterface && SoftwareUpdateService.shouldUpdate( this@MeshService, DeviceVersion(firmwareVersion) @@ -1323,9 +1324,7 @@ class MeshService : Service(), Logging { airUtilTx ) } - newMyNodeInfo = mi - setFirmwareUpdateFilename(mi) } } @@ -1641,12 +1640,12 @@ class MeshService : Service(), Logging { /*** * Return the filename we will install on the device */ - private fun setFirmwareUpdateFilename(info: MyNodeInfo) { + private fun setFirmwareUpdateFilename(model: String?) { firmwareUpdateFilename = try { - if (info.firmwareVersion != null && info.model != null) + if (model != null) SoftwareUpdateService.getUpdateFilename( this, - info.model + model ) else null diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt index 8326a5168..0eba401eb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -70,7 +70,7 @@ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray { } -data class UpdateFilenames(val appLoad: String?, val spiffs: String?) +data class UpdateFilenames(val appLoad: String?, val littlefs: String?) /** * typical flow @@ -172,7 +172,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { private val SW_UPDATE_RESULT_CHARACTER = UUID.fromString("5e134862-7411-4424-ac4a-210937432c77") // read|notify result code, readable but will notify when the OTA operation completes private val SW_UPDATE_REGION_CHARACTER = - UUID.fromString("5e134862-7411-4424-ac4a-210937432c67") // write - used to set the region we are setting (appload vs spiffs) + UUID.fromString("5e134862-7411-4424-ac4a-210937432c67") // write - used to set the region we are setting (appload vs littlefs) private val SW_VERSION_CHARACTER = longBLEUUID("2a28") private val MANUFACTURE_CHARACTER = longBLEUUID("2a29") @@ -208,11 +208,11 @@ class SoftwareUpdateService : JobIntentService(), Logging { /** * Update our progress indication for GUIs * - * @param isAppload if false, we don't report failure indications (because we consider spiffs non critical for now). But do report to analytics + * @param isAppload if false, we don't report failure indications (because we consider littlefs non critical for now). But do report to analytics */ fun sendProgress(context: Context, p: Int, isAppload: Boolean) { if (!isAppload && p < 0) - errormsg("Error while writing spiffs $p") // treat errors writing spiffs as non fatal for now (user partition probably missized and most people don't need it) + errormsg("Error while writing littlefs $p") // treat errors writing littlefs as non fatal for now (user partition probably missized and most people don't need it) else if (progress != p) { progress = p @@ -243,7 +243,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { false // If we fail parsing our update info } - /** Return a Pair of apploadfilename, spiffs filename this device needs to use as an update (or null if no update needed) + /** Return a Pair of appload filename, littlefs filename this device needs to use as an update (or null if no update needed) */ fun getUpdateFilename( context: Context, @@ -255,15 +255,15 @@ class SoftwareUpdateService : JobIntentService(), Logging { val firmwareFiles = context.assets.list("firmware") ?: arrayOf() val appLoad = "firmware-$mfg-$curVer.bin" - val spiffs = "spiffs-$curVer.bin" + val littlefs = "littlefs-$curVer.bin" return UpdateFilenames( if (firmwareFiles.contains(appLoad)) "firmware/$appLoad" else null, - if (firmwareFiles.contains(spiffs)) - "firmware/$spiffs" + if (firmwareFiles.contains(littlefs)) + "firmware/$littlefs" else null ) @@ -290,24 +290,30 @@ class SoftwareUpdateService : JobIntentService(), Logging { * you can use it for the software update. */ fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) { - // we must attempt spiffs first, because if we update the appload the device will reboot afterwards + // calculate total firmware size (littlefs + appLoad) + var totalFirmwareSize = 0 + if (assets.appLoad != null && assets.littlefs != null) { + totalFirmwareSize += context.assets.open(assets.appLoad).available() + totalFirmwareSize += context.assets.open(assets.littlefs).available() + } + // we must attempt littlefs first, because if we update the appload the device will reboot afterwards try { - assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) } + assets.littlefs?.let { doUpdate(context, sync, it, FLASH_REGION_LITTLEFS, totalFirmwareSize) } } catch (_: BLECharacteristicNotFoundException) { - // If we can't update spiffs (because not supported by target), do not fail - errormsg("Ignoring failure to update spiffs on old appload") + // If we can't update littlefs (because not supported by target), do not fail + errormsg("Ignoring failure to update littlefs on old appload") } catch (_: DeviceRejectedException) { // the spi filesystem of this device is malformatted, fail silently because most users don't need the web server - errormsg("Device rejected invalid spiffs partition") + errormsg("Device rejected invalid littlefs partition") } - assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) } + assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD, totalFirmwareSize) } sendProgress(context, ProgressSuccess, true) } // writable region codes in the ESP32 update code private val FLASH_REGION_APPLOAD = 0 - private val FLASH_REGION_SPIFFS = 100 + private val FLASH_REGION_LITTLEFS = 100 /** * A public function so that if you have your own SafeBluetooth connection already open @@ -317,7 +323,8 @@ class SoftwareUpdateService : JobIntentService(), Logging { context: Context, sync: SafeBluetooth, assetName: String, - flashRegion: Int = FLASH_REGION_APPLOAD + flashRegion: Int = FLASH_REGION_APPLOAD, + totalFirmwareSize: Int = 0 ) { val isAppload = flashRegion == FLASH_REGION_APPLOAD @@ -341,9 +348,9 @@ class SoftwareUpdateService : JobIntentService(), Logging { val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER) val updateResultDesc = getCharacteristic(SW_UPDATE_RESULT_CHARACTER) - /// Try to set the destination region for programming (spiffs vs appload etc) + /// Try to set the destination region for programming (littlefs vs appload etc) /// Old apploads don't have this feature, but we only fail if the user was trying to set a - /// spiffs - otherwise we assume appload. + /// littlefs - otherwise we assume appload. try { val updateRegionDesc = getCharacteristic(SW_UPDATE_REGION_CHARACTER) sync.writeCharacteristic( @@ -378,13 +385,15 @@ class SoftwareUpdateService : JobIntentService(), Logging { // Send all the blocks var oldProgress = -1 // used to limit # of log spam while (firmwareNumSent < firmwareSize) { - // If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done - // yet - val maxProgress = if (flashRegion != FLASH_REGION_APPLOAD) - 50 else 100 + // If we are doing the littlefs partition, we limit progress to a max of maxProgress + // when updating the appload partition, progress from (100 - maxProgress) to 100% + // maxProgress = littlefs% = 100% - appLoad%; (int * 10 + 5) / 10 used for rounding + val maxProgress = ((firmwareSize * 1000 / totalFirmwareSize) + 5) / 10 + val minProgress = if (flashRegion != FLASH_REGION_APPLOAD) + 0 else (100 - maxProgress) sendProgress( context, - firmwareNumSent * maxProgress / firmwareSize, + minProgress + firmwareNumSent * maxProgress / firmwareSize, isAppload ) if (progress != oldProgress) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 89717f296..83fd0561f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -24,6 +24,7 @@ import android.widget.* import androidx.fragment.app.activityViewModels import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard @@ -474,6 +475,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { model.meshService?.let { service -> debug("User started firmware update") + GeeksvilleApplication.analytics.track( + "firmware_update", + DataPair("content_type", "start") + ) binding.updateFirmwareButton.isEnabled = false // Disable until things complete binding.updateProgressBar.visibility = View.VISIBLE binding.updateProgressBar.progress = 0 // start from scratch @@ -515,6 +520,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } else when (progress) { ProgressSuccess -> { + GeeksvilleApplication.analytics.track( + "firmware_update", + DataPair("content_type", "success") + ) binding.scanStatusText.setText(R.string.update_successful) binding.updateProgressBar.visibility = View.GONE } @@ -523,6 +532,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { binding.updateProgressBar.visibility = View.GONE } else -> { + GeeksvilleApplication.analytics.track( + "firmware_update", + DataPair("content_type", "failure") + ) binding.scanStatusText.setText(R.string.update_failed) binding.updateProgressBar.visibility = View.VISIBLE }