diff --git a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt index 657aaf963..4ee38cd1b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt @@ -1,5 +1,8 @@ package com.geeksville.mesh.service import java.io.IOException +import java.util.* -open class BLEException(msg: String) : IOException(msg) \ No newline at end of file +open class BLEException(msg: String) : IOException(msg) + +open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid") \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt index 5f4d2bb45..a1e1eda66 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -489,6 +489,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String * Get a chracteristic, but in a safe manner because some buggy BLE implementations might return null */ private fun getCharacteristic(uuid: UUID) = - bservice.getCharacteristic(uuid) ?: throw BLEException("Can't get characteristic $uuid") + bservice.getCharacteristic(uuid) ?: throw BLECharacteristicNotFoundException(uuid) } 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 2cd528cf3..8e7cfab81 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1334,7 +1334,7 @@ class MeshService : Service(), Logging { return 0 // We don't have mynodeinfo yet, so just let the radio eventually assign an ID } - var firmwareUpdateFilename: String? = null + var firmwareUpdateFilename: UpdateFilenames? = null /*** * Return the filename we will install on the device 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 1c955064f..88d3beb6b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -18,7 +18,12 @@ import java.util.zip.CRC32 */ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray { - val len: Int = 4 // getTypeLen(formatType) + val len = when (formatType) { + BluetoothGattCharacteristic.FORMAT_UINT8 -> 1 + BluetoothGattCharacteristic.FORMAT_UINT32 -> 4 + else -> TODO() + } + val mValue = ByteArray(len) when (formatType) { @@ -44,6 +49,9 @@ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray { mValue.get(offset++) = (value shr 16 and 0xFF).toByte() mValue.get(offset) = (value shr 24 and 0xFF).toByte() } */ + BluetoothGattCharacteristic.FORMAT_UINT8 -> + mValue[0] = (value and 0xFF).toByte() + BluetoothGattCharacteristic.FORMAT_UINT32 -> { mValue[0] = (value and 0xFF).toByte() mValue[1] = (value shr 8 and 0xFF).toByte() @@ -55,6 +63,9 @@ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray { return mValue } + +data class UpdateFilenames(val appLoad: String?, val spiffs: String?) + /** * typical flow * @@ -152,6 +163,8 @@ class SoftwareUpdateService : JobIntentService(), Logging { UUID.fromString("4826129c-c22a-43a3-b066-ce8f0d5bacc6") // write crc32, write last - writing this will complete the OTA operation, now you can read result 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) private val SW_VERSION_CHARACTER = longBLEUUID("2a28") private val MANUFACTURE_CHARACTER = longBLEUUID("2a29") @@ -210,28 +223,36 @@ class SoftwareUpdateService : JobIntentService(), Logging { false // If we fail parsing our update info } - /** Return the filename this device needs to use as an update (or null if no update needed) + /** Return a Pair of apploadfilename, spiffs filename this device needs to use as an update (or null if no update needed) */ fun getUpdateFilename( context: Context, mfg: String - ): String? { + ): UpdateFilenames { val curver = context.getString(R.string.cur_firmware_version) - val base = "firmware-$mfg-$curver.bin" - // Check to see if the file exists (some builds might not include update files for size reasons) val firmwareFiles = context.assets.list("firmware") ?: arrayOf() - return if (firmwareFiles.contains(base)) - "firmware/$base" - else - null + + val appLoad = "firmware-$mfg-$curver.bin" + val spiffs = "spiffs-$curver.bin" + + return UpdateFilenames( + if (firmwareFiles.contains(appLoad)) + "firmware/$appLoad" + else + null, + if (firmwareFiles.contains(spiffs)) + "firmware/$spiffs" + else + null + ) } /** Return the filename this device needs to use as an update (or null if no update needed) * No longer used, because we get update info inband from our radio API */ - fun getUpdateFilename(context: Context, sync: SafeBluetooth): String? { + fun getUpdateFilename(context: Context, sync: SafeBluetooth): UpdateFilenames? { val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!! //val hwVerDesc = service.getCharacteristic(HW_VERSION_CHARACTER) @@ -248,19 +269,66 @@ class SoftwareUpdateService : JobIntentService(), Logging { * A public function so that if you have your own SafeBluetooth connection already open * you can use it for the software update. */ - fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String) { + fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) { + // we must attempt spiffs first, because if we update the appload the device will reboot afterwards + try { + assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) } + } + 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") + } + assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) } + progress = -1 // success + } + + // writable region codes in the ESP32 update code + private val FLASH_REGION_APPLOAD = 0 + private val FLASH_REGION_SPIFFS = 100 + + /** + * A public function so that if you have your own SafeBluetooth connection already open + * you can use it for the software update. + */ + private fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String, flashRegion: Int = FLASH_REGION_APPLOAD) { try { val g = sync.gatt!! val service = g.services.find { it.uuid == SW_UPDATE_UUID } ?: throw BLEException("Couldn't find update service") - info("Starting firmware update for $assetName") + /** + * Get a chracteristic, but in a safe manner because some buggy BLE implementations might return null + */ + fun getCharacteristic(uuid: UUID) = + service.getCharacteristic(uuid) + ?: throw BLECharacteristicNotFoundException(uuid) + + info("Starting firmware update for $assetName, flash region $flashRegion") progress = 0 - val totalSizeDesc = service.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER) - val dataDesc = service.getCharacteristic(SW_UPDATE_DATA_CHARACTER) - val crc32Desc = service.getCharacteristic(SW_UPDATE_CRC32_CHARACTER) - val updateResultDesc = service.getCharacteristic(SW_UPDATE_RESULT_CHARACTER) + val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER) + val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER) + 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) + /// Old apploads don't have this feature, but we only fail if the user was trying to set a + /// spiffs - otherwise we assume appload. + try { + val updateRegionDesc = getCharacteristic(SW_UPDATE_REGION_CHARACTER) + sync.writeCharacteristic( + updateRegionDesc, + toNetworkByteArray(flashRegion, BluetoothGattCharacteristic.FORMAT_UINT8) + ) + } + catch(ex: BLECharacteristicNotFoundException) { + errormsg("Can't set flash programming region (old appload?") + if(flashRegion != FLASH_REGION_APPLOAD) { + errormsg("Can't set flash programming region (old appload?)") + throw ex + } + warn("Ignoring setting appload flashRegion (old appload?") + } context.assets.open(assetName).use { firmwareStream -> val firmwareCrc = CRC32() @@ -320,8 +388,6 @@ class SoftwareUpdateService : JobIntentService(), Logging { // We might get SyncContinuation timeout on the final write, assume the device simply rebooted to run the new load and we missed it errormsg("Assuming successful update", ex) } - - progress = -1 // success } } catch (ex: BLEException) { progress = -3