BLE sw update kinda works again

1.2-legacy
geeksville 2020-02-24 15:47:53 -08:00
rodzic 7ed5a3efac
commit 601aeb83d7
3 zmienionych plików z 163 dodań i 151 usunięć

Wyświetl plik

@ -178,7 +178,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
completeWork(status, mtu)
// Alas, passing back an Int mtu isn't working and since I don't really care what MTU
// the device was willing to let us have I'm just punting and returning Unit
completeWork(status, Unit)
}
/**
@ -378,19 +380,18 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
private fun queueRequestMtu(
len: Int,
cont: Continuation<Int>
cont: Continuation<Unit>
) = queueWork("reqMtu", cont) { gatt!!.requestMtu(len) }
fun asyncRequestMtu(
len: Int,
cb: (Result<Int>) -> Unit
cb: (Result<Unit>) -> Unit
) {
logAssert(workQueue.isEmpty() && currentWork == null) // I don't think anything should be able to sneak in front
queueRequestMtu(len, CallbackContinuation(cb))
}
fun requestMtu(len: Int): Int =
makeSync { queueRequestMtu(len, it) }
fun requestMtu(len: Int): Unit = makeSync { queueRequestMtu(len, it) }
private fun queueWriteCharacteristic(
c: BluetoothGattCharacteristic,

Wyświetl plik

@ -1,19 +1,15 @@
package com.geeksville.mesh.service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
import android.os.ParcelUuid
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
@ -36,10 +32,11 @@ class SoftwareUpdateService : JobIntentService(), Logging {
bluetoothManager.adapter!!
}
lateinit var device: BluetoothDevice
fun startUpdate() {
info("starting update")
fun startUpdate(macaddr: String) {
info("starting update to $macaddr")
val device = bluetoothAdapter.getRemoteDevice(macaddr)
val sync =
SafeBluetooth(
@ -47,147 +44,141 @@ class SoftwareUpdateService : JobIntentService(), Logging {
device
)
val firmwareStream = assets.open("firmware.bin")
val firmwareCrc = CRC32()
var firmwareNumSent = 0
val firmwareSize = firmwareStream.available()
sync.connect()
sync.use { _ ->
sync.discoverServices() // Get our services
sync.discoverServices() // Get our services
// we begin by setting our MTU size as high as it can go
sync.requestMtu(512)
// we begin by setting our MTU size as high as it can go
sync.requestMtu(512)
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
fun doFirmwareUpdate(assetName: String) {
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)
info("Starting firmware update for $assetName")
// Start the update by writing the # of bytes in the image
logAssert(
totalSizeDesc.setValue(
firmwareSize,
BluetoothGattCharacteristic.FORMAT_UINT32,
0
)
)
sync.writeCharacteristic(totalSizeDesc)
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)
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
logAssert(
totalSizeDesc.setValue(
firmwareSize,
BluetoothGattCharacteristic.FORMAT_UINT32,
0
)
)
sync.writeCharacteristic(totalSizeDesc)
// 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")
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) {
info("sending block ${firmwareNumSent * 100 / firmwareSize}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
// Send all the blocks
while (firmwareNumSent < firmwareSize) {
debug("sending block ${firmwareNumSent * 100 / firmwareSize}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
if (blockSize > firmwareStream.available())
blockSize = firmwareStream.available()
val buffer = ByteArray(blockSize)
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)
// slightly expensive to keep reallocing this buffer, but whatever
logAssert(firmwareStream.read(buffer) == blockSize)
firmwareCrc.update(buffer)
// updateGatt.beginReliableWrite()
dataDesc.value = buffer
sync.writeCharacteristic(dataDesc)
firmwareNumSent += blockSize
}
// updateGatt.beginReliableWrite()
dataDesc.value = buffer
sync.writeCharacteristic(dataDesc)
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")
logAssert(crc32Desc.setValue(c.toInt(), BluetoothGattCharacteristic.FORMAT_UINT32, 0))
sync.writeCharacteristic(crc32Desc)
// 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")
logAssert(
crc32Desc.setValue(
c.toInt(),
BluetoothGattCharacteristic.FORMAT_UINT32,
0
)
)
sync.writeCharacteristic(crc32Desc)
// 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) // FIXME - handle this case
throw Exception("Device update failed, reason=$updateResult")
// 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) // FIXME - handle this case
throw Exception("Device update failed, reason=$updateResult")
// FIXME perhaps ask device to reboot
}
private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
throw NotImplementedError()
}
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
throw NotImplementedError()
}
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
// check if it is an eligable device and store it in our list of candidates
// if that device later disconnects remove it as a candidate
override fun onScanResult(callbackType: Int, result: ScanResult) {
info("onScanResult")
// We don't need any more results now
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
device = result.device
}
}
// Until my race condition with scanning is fixed
fun connectToTestDevice() {
device = bluetoothAdapter.getRemoteDevice("B4:E6:2D:EA:32:B7")
}
private fun scanLeDevice(enable: Boolean) {
when (enable) {
true -> {
// Stops scanning after a pre-defined scan period.
/* handler.postDelayed({
mScanning = false
bluetoothAdapter.stopLeScan(leScanCallback)
}, SCAN_PERIOD)
mScanning = true */
val scanner = bluetoothAdapter.bluetoothLeScanner
// filter and only accept devices that have a sw update service
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SW_UPDATE_UUID)).build()
/* ScanSettings.CALLBACK_TYPE_FIRST_MATCH seems to trigger a bug returning an error of
SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES (error #5)
*/
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).
// setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT).
// setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH).
build()
scanner.startScan(listOf(filter), settings, scanCallback)
// FIXME perhaps ask device to reboot
}
}
else -> {
// mScanning = false
// bluetoothAdapter.stopLeScan(leScanCallback)
/// Return the filename this device needs to use as an update (or null if no update needed)
fun getUpdateFilename(): String? {
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
val 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)
val curver = getString(R.string.cur_firmware_version)
// FIXME, instead compare version strings carefully to see if less than
val needsUpdate = (curver != swVer)
return if (!needsUpdate)
null
else {
val regionRegex = Regex(".+-(.+)")
val (region) = regionRegex.find(hwVer)?.destructured
?: throw Exception("Malformed hw version")
"firmware/firmware-$mfg-$region-$curver.bin"
}
}
val updateFilename = getUpdateFilename()
if (updateFilename != null) {
doFirmwareUpdate(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.
debug("Executing work: $intent")
when (intent.action) {
scanDevicesIntent.action -> scanLeDevice(true)
startUpdateIntent.action -> {
connectToTestDevice() // FIXME, pass in as an intent arg instead
startUpdate()
// 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")
}
else -> TODO("Unhandled case")
}
}
@ -197,8 +188,16 @@ class SoftwareUpdateService : JobIntentService(), Logging {
*/
private const val JOB_ID = 1000
val scanDevicesIntent = Intent("$prefix.SCAN_DEVICES")
val startUpdateIntent = Intent("$prefix.START_UPDATE")
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
@ -215,6 +214,10 @@ 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_VERSION_CHARACTER = longBLEUUID("2a28")
private val MANUFACTURE_CHARACTER = longBLEUUID("2a29")
private val HW_VERSION_CHARACTER = longBLEUUID("2a27")
/**
* Convenience method for enqueuing work in to this service.
*/

Wyświetl plik

@ -1,8 +1,10 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.ambient
import androidx.compose.state
import androidx.ui.animation.Crossfade
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Text
import androidx.ui.layout.Column
import androidx.ui.layout.Container
@ -16,6 +18,8 @@ import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.mesh.service.SoftwareUpdateService
object UILog : Logging
@ -40,13 +44,33 @@ fun HomeContent() {
)
}
Text(if (UIState.isConnected.value) "Connected" else "Not Connected")
if (UIState.isConnected.value) {
Column {
Text("Connected")
/// Create a software update button
val context = ambient(ContextAmbient)
RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
Button(text = "Update firmware",
onClick = {
SoftwareUpdateService.enqueueWork(
context,
SoftwareUpdateService.startUpdateIntent(macAddress)
)
}
)
}
}
} else {
Text("Not Connected")
}
}
NodeDB.nodes.values.forEach {
NodeInfoCard(it)
}
/* FIXME - doens't work yet - probably because I'm not using release keys
// If account is null, then show the signin button, otherwise
val context = ambient(ContextAmbient)
@ -62,22 +86,6 @@ fun HomeContent() {
})
}
} */
/*
Button(text = "Start scan",
onClick = {
if (bluetoothAdapter != null) {
// Note: We don't want this service to die just because our activity goes away (because it is doing a software update)
// So we use the application context instead of the activity
SoftwareUpdateService.enqueueWork(
applicationContext,
SoftwareUpdateService.startUpdateIntent
)
}
})
Button(text = "send packets",
onClick = { sendTestPackets() }) */
}
}