mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
308 lines
12 KiB
Kotlin
308 lines
12 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: Int = 4 // getTypeLen(formatType)
|
|
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_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
|
|
}
|
|
|
|
/**
|
|
* 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_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
|
|
*/
|
|
fun shouldUpdate(
|
|
context: Context,
|
|
swVer: String
|
|
): Boolean {
|
|
val curver = context.getString(R.string.cur_firmware_version)
|
|
|
|
// If the user is running a development build we never do an automatic update
|
|
val isDevBuild = swVer.isEmpty() || swVer == "unset"
|
|
|
|
// FIXME, instead compare version strings carefully to see if less than
|
|
val needsUpdate = (curver != swVer)
|
|
|
|
return needsUpdate && !isDevBuild && false // temporarily disabled because it fails occasionally
|
|
}
|
|
|
|
/** Return the filename this device needs to use as an update (or null if no update needed)
|
|
*/
|
|
fun getUpdateFilename(
|
|
context: Context,
|
|
hwVerIn: String,
|
|
swVer: String,
|
|
mfg: String
|
|
): String {
|
|
val curver = context.getString(R.string.cur_firmware_version)
|
|
|
|
val regionRegex = Regex(".+-(.+)")
|
|
|
|
val hwVer = if (hwVerIn.isEmpty() || hwVerIn == "unset")
|
|
"1.0-US" else hwVerIn // lie and claim US, because that's what the device load will be doing without hwVer set
|
|
|
|
val (region) = regionRegex.find(hwVer)?.destructured
|
|
?: throw Exception("Malformed hw version")
|
|
|
|
return "firmware/firmware-$mfg-$region-$curver.bin"
|
|
}
|
|
|
|
/** Return the filename this device needs to use as an update (or null if no update needed)
|
|
*/
|
|
fun getUpdateFilename(context: Context, sync: SafeBluetooth): String? {
|
|
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 1.0-US
|
|
var hwVer = sync.readCharacteristic(hwVerDesc).getStringValue(0)
|
|
|
|
// looks like HELTEC
|
|
val mfg = sync.readCharacteristic(mfgDesc).getStringValue(0)
|
|
|
|
// looks like 0.0.12
|
|
val swVer = sync.readCharacteristic(swVerDesc).getStringValue(0)
|
|
|
|
return getUpdateFilename(context, hwVer, swVer, 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, assetName: String) {
|
|
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
|
|
|
|
info("Starting firmware update for $assetName")
|
|
|
|
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)
|
|
|
|
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) {
|
|
progress = firmwareNumSent * 100 / 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
|
|
}
|
|
|
|
// 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
|
|
|
|
progress = -1 // success
|
|
}
|
|
}
|
|
}
|
|
}
|