diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index f107b5bb..f96176b7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -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 + cont: Continuation ) = queueWork("reqMtu", cont) { gatt!!.requestMtu(len) } fun asyncRequestMtu( len: Int, - cb: (Result) -> Unit + cb: (Result) -> 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, 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 1aa3f58c..229304ed 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -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?) { - 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. */ diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index dfe0b5b3..dd94507b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -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() }) */ } }