From 6576f5eab52145cae8898c8b981122f2f6600202 Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 13 May 2020 14:47:55 -0700 Subject: [PATCH] backend updated to reneable firmware update --- .../com/geeksville/mesh/IMeshService.aidl | 12 + .../java/com/geeksville/mesh/MyNodeInfo.kt | 52 ++++ .../geeksville/mesh/service/MeshService.kt | 100 ++++--- .../mesh/service/RadioInterfaceService.kt | 9 +- .../mesh/service/SoftwareUpdateService.kt | 265 ++++++++++-------- geeksville-androidlib | 2 +- 6 files changed, 285 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index c912895b..75f0780b 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -4,6 +4,7 @@ package com.geeksville.mesh; // Declare any non-default types here with import statements parcelable DataPacket; parcelable NodeInfo; +parcelable MyNodeInfo; /** * Note - these calls might throw RemoteException to indicate mesh error states @@ -62,6 +63,17 @@ interface IMeshService { /// Users should not call this directly, only used internally by the MeshUtil activity void setDeviceAddress(String deviceAddr); + /// Get basic device hardware info about our connected radio + MyNodeInfo getMyNodeInfo(); + + /// Start updating the radios firmware + void startFirmwareUpdate(); + + /** + Return a number 0-100 for progress. -1 for completed and success, -2 for failure + */ + int getUpdateStatus(); + // see com.geeksville.com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes. payload will contain a DataPacket // NODE_CHANGE for new IDs appearing or disappearing diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt new file mode 100644 index 00000000..8c9c5596 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt @@ -0,0 +1,52 @@ +package com.geeksville.mesh + +import android.os.Parcel +import android.os.Parcelable +import kotlinx.serialization.Serializable + +// MyNodeInfo sent via special protobuf from radio +@Serializable +data class MyNodeInfo( + val myNodeNum: Int, + val hasGPS: Boolean, + val region: String?, + val model: String?, + val firmwareVersion: String?, + val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want + val shouldUpdate: Boolean // this device has old firmware +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readInt(), + parcel.readByte() != 0.toByte(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readByte() != 0.toByte(), + parcel.readByte() != 0.toByte() + ) { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(myNodeNum) + parcel.writeByte(if (hasGPS) 1 else 0) + parcel.writeString(region) + parcel.writeString(model) + parcel.writeString(firmwareVersion) + parcel.writeByte(if (couldUpdate) 1 else 0) + parcel.writeByte(if (shouldUpdate) 1 else 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MyNodeInfo { + return MyNodeInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file 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 a3c742dc..b43820cd 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -474,16 +474,6 @@ class MeshService : Service(), Logging { /// special broadcast address val NODENUM_BROADCAST = 255 - // MyNodeInfo sent via special protobuf from radio - @Serializable - data class MyNodeInfo( - val myNodeNum: Int, - val hasGPS: Boolean, - val region: String, - val model: String, - val firmwareVersion: String - ) - /// Our saved preferences as stored on disk @Serializable private data class SavedSettings( @@ -862,35 +852,10 @@ class MeshService : Service(), Logging { connectedRadio.readMyNode() ) - val mi = with(myInfo) { - MyNodeInfo(myNodeNum, hasGps, region, hwModel, firmwareVersion) - } - - myNodeInfo = mi + handleMyInfo(myInfo) radioConfig = MeshProtos.RadioConfig.parseFrom(connectedRadio.readRadioConfig()) - /// Track types of devices and firmware versions in use - GeeksvilleApplication.analytics.setUserInfo( - DataPair("region", mi.region), - DataPair("firmware", mi.firmwareVersion), - DataPair("has_gps", mi.hasGPS), - DataPair("hw_model", mi.model), - DataPair("dev_error_count", myInfo.errorCount) - ) - - if (myInfo.errorCode != 0) { - GeeksvilleApplication.analytics.track( - "dev_error", - DataPair("code", myInfo.errorCode), - DataPair("address", myInfo.errorAddress), - - // We also include this info, because it is required to correctly decode address from the map file - DataPair("firmware", mi.firmwareVersion), - DataPair("hw_model", mi.model), - DataPair("region", mi.region) - ) - } // Ask for the current node DB connectedRadio.restartNodeInfo() @@ -1168,7 +1133,15 @@ class MeshService : Service(), Logging { private fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) { val mi = with(myInfo) { - MyNodeInfo(myNodeNum, hasGps, region, hwModel, firmwareVersion) + MyNodeInfo( + myNodeNum, + hasGps, + region, + hwModel, + firmwareVersion, + false, + false + ) } newMyNodeInfo = mi @@ -1311,7 +1284,41 @@ class MeshService : Service(), Logging { } - private val binder = object : IMeshService.Stub() { + /*** + * Return the filename we will install on the device + */ + val firmwareUpdateFilename: String? + get() = + try { + myNodeInfo?.let { + if (it.region != null && it.firmwareVersion != null && it.model != null) + SoftwareUpdateService.getUpdateFilename( + this, + it.region, + it.firmwareVersion, + it.model + ) + else + null + } + } catch (ex: Exception) { + errormsg("Unable to update", ex) + null + } + + private fun doFirmwareUpdate() { + // Run in the IO thread + val filename = firmwareUpdateFilename ?: throw Exception("No update filename") + val safe = + RadioInterfaceService.safe ?: throw Exception("Can't update - no bluetooth connected") + + serviceScope.handledLaunch { + SoftwareUpdateService.doUpdate(this@MeshService, safe, filename) + } + } + + private + val binder = object : IMeshService.Stub() { override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions { debug("Passing through device change to radio service: $deviceAddr") @@ -1330,6 +1337,14 @@ class MeshService : Service(), Logging { return recentDataPackets } + override fun getUpdateStatus(): Int = SoftwareUpdateService.progress + + override fun startFirmwareUpdate() = toRemoteExceptions { + doFirmwareUpdate() + } + + override fun getMyNodeInfo(): MyNodeInfo? = this@MeshService.myNodeInfo + override fun getMyId() = toRemoteExceptions { myNodeID } override fun setOwner(myId: String?, longName: String, shortName: String) = @@ -1337,7 +1352,11 @@ class MeshService : Service(), Logging { this@MeshService.setOwner(myId, longName, shortName) } - override fun sendData(destId: String?, payloadIn: ByteArray, typ: Int): Boolean = + override fun sendData( + destId: String?, + payloadIn: ByteArray, + typ: Int + ): Boolean = toRemoteExceptions { info("sendData dest=$destId <- ${payloadIn.size} bytes (connectionState=$connectionState)") @@ -1376,7 +1395,8 @@ class MeshService : Service(), Logging { } override fun getRadioConfig(): ByteArray = toRemoteExceptions { - this@MeshService.radioConfig?.toByteArray() ?: throw RadioNotConnectedException() + this@MeshService.radioConfig?.toByteArray() + ?: throw RadioNotConnectedException() } override fun setRadioConfig(payload: ByteArray) = toRemoteExceptions { diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index f99d7e91..d9aa9c4a 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -208,12 +208,15 @@ class RadioInterfaceService : Service(), Logging { warn("CompanionDevice API not available, falling back to classic scan") false } */ + + /** + * this is created in onCreate() + * We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case + */ + var safe: SafeBluetooth? = null } - // Both of these are created in onCreate() - private var safe: SafeBluetooth? = null - /// Our BLE device val device get() = safe!!.gatt!! 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 339278b1..ab334bc3 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -27,13 +27,14 @@ import java.util.zip.CRC32 */ class SoftwareUpdateService : JobIntentService(), Logging { + private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager.adapter!! } - fun startUpdate(macaddr: String) { + private fun startUpdate(macaddr: String) { info("starting update to $macaddr") val device = bluetoothAdapter.getRemoteDevice(macaddr) @@ -51,121 +52,17 @@ class SoftwareUpdateService : JobIntentService(), Logging { sync.discoverServices() // Get our services - val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!! - - fun doFirmwareUpdate(assetName: String) { - - info("Starting firmware update for $assetName") - - 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") - - // 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) - - // 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 - } - - // 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") - - // FIXME perhaps ask device to reboot - } - } - - /// 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() + val updateFilename = getUpdateFilename(this, sync) if (updateFilename != null) { - doFirmwareUpdate(updateFilename) + 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 + 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 @@ -182,7 +79,7 @@ class SoftwareUpdateService : JobIntentService(), Logging { } } - companion object { + companion object : Logging { /** * Unique job ID for this service. Must be the same for all work. */ @@ -218,6 +115,11 @@ class SoftwareUpdateService : JobIntentService(), Logging { 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. */ @@ -228,5 +130,146 @@ class SoftwareUpdateService : JobIntentService(), Logging { 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 + } + + /** 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") + + 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 + 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") + + // 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) + + // 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 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 + } + } } -} \ No newline at end of file +} diff --git a/geeksville-androidlib b/geeksville-androidlib index c55dcf40..1c729d80 160000 --- a/geeksville-androidlib +++ b/geeksville-androidlib @@ -1 +1 @@ -Subproject commit c55dcf40797a1fc2d157860849b5cad2595a1940 +Subproject commit 1c729d801162380f2db9c2a3c681458f4a719bd1