Meshtastic-Android/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
2020-12-10 09:23:02 +08:00

385 lines
16 KiB
Kotlin

package com.geeksville.mesh.service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
import com.geeksville.android.Logging
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.util.exceptionReporter
import java.util.*
import java.util.zip.CRC32
/**
* Move this somewhere as a generic network byte order function
*/
fun toNetworkByteArray(value: Int, formatType: Int): ByteArray {
val len = when (formatType) {
BluetoothGattCharacteristic.FORMAT_UINT8 -> 1
BluetoothGattCharacteristic.FORMAT_UINT32 -> 4
else -> TODO()
}
val mValue = ByteArray(len)
when (formatType) {
/* BluetoothGattCharacteristic.FORMAT_SINT8 -> {
value = intToSignedBits(value, 8)
mValue.get(offset) = (value and 0xFF).toByte()
}
BluetoothGattCharacteristic.FORMAT_UINT8 -> mValue.get(offset) =
(value and 0xFF).toByte()
BluetoothGattCharacteristic.FORMAT_SINT16 -> {
value = intToSignedBits(value, 16)
mValue.get(offset++) = (value and 0xFF).toByte()
mValue.get(offset) = (value shr 8 and 0xFF).toByte()
}
BluetoothGattCharacteristic.FORMAT_UINT16 -> {
mValue.get(offset++) = (value and 0xFF).toByte()
mValue.get(offset) = (value shr 8 and 0xFF).toByte()
}
BluetoothGattCharacteristic.FORMAT_SINT32 -> {
value = intToSignedBits(value, 32)
mValue.get(offset++) = (value and 0xFF).toByte()
mValue.get(offset++) = (value shr 8 and 0xFF).toByte()
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()
mValue[2] = (value shr 16 and 0xFF).toByte()
mValue[3] = (value shr 24 and 0xFF).toByte()
}
else -> TODO()
}
return mValue
}
data class UpdateFilenames(val appLoad: String?, val spiffs: String?)
/**
* typical flow
*
* startScan
* startUpdate
*
* stopScan
*
* FIXME - if we don't find a device stop our scan
* FIXME - broadcast when we found devices, made progress sending blocks or when the update is complete
* FIXME - make the user decide to start an update on a particular device
*/
class SoftwareUpdateService : JobIntentService(), Logging {
private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter!!
}
private fun startUpdate(macaddr: String) {
info("starting update to $macaddr")
val device = bluetoothAdapter.getRemoteDevice(macaddr)
val sync =
SafeBluetooth(
this@SoftwareUpdateService,
device
)
sync.connect()
sync.use { _ ->
// we begin by setting our MTU size as high as it can go
sync.requestMtu(512)
sync.discoverServices() // Get our services
val updateFilename = getUpdateFilename(this, sync)
if (updateFilename != null) {
doUpdate(this, sync, updateFilename)
} else
warn("Device is already up-to-date no update needed.")
}
}
override fun onHandleWork(intent: Intent) {
// We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
// Report failures but do not crash the app
exceptionReporter {
debug("Executing work: $intent")
when (intent.action) {
ACTION_START_UPDATE -> {
val addr = intent.getStringExtra(EXTRA_MACADDR)
?: throw Exception("EXTRA_MACADDR not specified")
startUpdate(addr) // FIXME, pass in as an intent arg instead
}
else -> TODO("Unhandled case")
}
}
}
companion object : Logging {
/**
* Unique job ID for this service. Must be the same for all work.
*/
private const val JOB_ID = 1000
fun startUpdateIntent(macAddress: String): Intent {
val i = Intent(ACTION_START_UPDATE)
i.putExtra(EXTRA_MACADDR, macAddress)
return i
}
const val ACTION_START_UPDATE = "$prefix.START_UPDATE"
const val EXTRA_MACADDR = "macaddr"
private const val SCAN_PERIOD: Long = 10000
private val TAG =
MainActivity::class.java.simpleName // FIXME - use my logging class instead
private val SW_UPDATE_UUID = UUID.fromString("cb0b9a0b-a84c-4c0d-bdbb-442e3144ee30")
private val SW_UPDATE_TOTALSIZE_CHARACTER =
UUID.fromString("e74dd9c0-a301-4a6f-95a1-f0e1dbea8e1e") // write|read total image size, 32 bit, write this first, then read read back to see if it was acceptable (0 mean not accepted)
private val SW_UPDATE_DATA_CHARACTER =
UUID.fromString("e272ebac-d463-4b98-bc84-5cc1a39ee517") // write data, variable sized, recommended 512 bytes, write one for each block of file
private val SW_UPDATE_CRC32_CHARACTER =
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")
private val HW_VERSION_CHARACTER = longBLEUUID("2a27")
/**
* % progress through the update
*/
var progress = 0
/**
* Convenience method for enqueuing work in to this service.
*/
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(
context,
SoftwareUpdateService::class.java,
JOB_ID, work
)
}
/** Return true if we thing the firmwarte shoulde be updated
*
* @param swVer the version of the software running on the target
*/
fun shouldUpdate(
context: Context,
deviceVersion: DeviceVersion
): Boolean = try {
val curVer = DeviceVersion(context.getString(R.string.cur_firmware_version))
val minVer =
DeviceVersion("0.7.8") // The oldest device version with a working software update service
(curVer > deviceVersion) && (deviceVersion >= minVer)
} catch (ex: Exception) {
errormsg("Error finding swupdate info", ex)
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)
*/
fun getUpdateFilename(
context: Context,
mfg: String
): UpdateFilenames {
val curVer = context.getString(R.string.cur_firmware_version)
// Check to see if the file exists (some builds might not include update files for size reasons)
val firmwareFiles = context.assets.list("firmware") ?: arrayOf()
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): UpdateFilenames? {
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
//val hwVerDesc = service.getCharacteristic(HW_VERSION_CHARACTER)
val mfgDesc = service.getCharacteristic(MANUFACTURE_CHARACTER)
//val swVerDesc = service.getCharacteristic(SW_VERSION_CHARACTER)
// looks like HELTEC
val mfg = sync.readCharacteristic(mfgDesc).getStringValue(0)
return getUpdateFilename(context, mfg)
}
/**
* 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, 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")
/**
* 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 = 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()
var firmwareNumSent = 0
val firmwareSize = firmwareStream.available()
// Start the update by writing the # of bytes in the image
sync.writeCharacteristic(
totalSizeDesc,
toNetworkByteArray(firmwareSize, BluetoothGattCharacteristic.FORMAT_UINT32)
)
// Our write completed, queue up a readback
val totalSizeReadback = sync.readCharacteristic(totalSizeDesc)
.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0)
if (totalSizeReadback == 0) // FIXME - handle this case
throw Exception("Device rejected file size")
// Send all the blocks
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
progress = firmwareNumSent * maxProgress / firmwareSize
debug("sending block ${progress}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
if (blockSize > firmwareStream.available())
blockSize = firmwareStream.available()
val buffer = ByteArray(blockSize)
// slightly expensive to keep reallocing this buffer, but whatever
logAssert(firmwareStream.read(buffer) == blockSize)
firmwareCrc.update(buffer)
sync.writeCharacteristic(dataDesc, buffer)
firmwareNumSent += blockSize
}
try {
// We have finished sending all our blocks, so post the CRC so our state machine can advance
val c = firmwareCrc.value
info("Sent all blocks, crc is $c")
sync.writeCharacteristic(
crc32Desc,
toNetworkByteArray(c.toInt(), BluetoothGattCharacteristic.FORMAT_UINT32)
)
// we just read the update result if !0 we have an error
val updateResult =
sync.readCharacteristic(updateResultDesc)
.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)
if (updateResult != 0) {
progress = -2
throw Exception("Device update failed, reason=$updateResult")
}
// Device will now reboot
} catch (ex: BLEException) {
// 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)
}
}
} catch (ex: BLEException) {
progress = -3
throw ex // Unexpected BLE exception
}
}
}
}