2020-02-10 15:31:56 -08:00
package com.geeksville.mesh.service
2020-01-21 09:37:39 -08:00
2020-01-24 12:49:27 -08:00
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
2020-01-21 09:37:39 -08:00
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
2020-01-22 14:27:22 -08:00
import com.geeksville.android.Logging
2020-02-10 15:31:56 -08:00
import com.geeksville.mesh.MainActivity
2020-02-24 15:47:53 -08:00
import com.geeksville.mesh.R
import com.geeksville.util.exceptionReporter
2020-01-21 10:39:01 -08:00
import java.util.*
2020-01-23 09:19:53 -08:00
import java.util.zip.CRC32
2020-01-21 09:37:39 -08:00
2020-06-13 16:21:26 -07:00
/ * *
* Move this somewhere as a generic network byte order function
* /
fun toNetworkByteArray ( value : Int , formatType : Int ) : ByteArray {
2020-11-16 15:57:40 +08:00
val len = when ( formatType ) {
BluetoothGattCharacteristic . FORMAT _UINT8 -> 1
BluetoothGattCharacteristic . FORMAT _UINT32 -> 4
else -> TODO ( )
}
2020-06-13 16:21:26 -07:00
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 ( )
} * /
2020-11-16 15:57:40 +08:00
BluetoothGattCharacteristic . FORMAT _UINT8 ->
mValue [ 0 ] = ( value and 0xFF ) . toByte ( )
2020-06-13 16:21:26 -07:00
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
}
2020-11-16 15:57:40 +08:00
data class UpdateFilenames ( val appLoad : String ? , val spiffs : String ? )
2020-01-21 09:37:39 -08:00
/ * *
2020-01-21 10:39:01 -08:00
* 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
2020-01-21 09:37:39 -08:00
* /
2020-01-23 23:05:15 -08:00
class SoftwareUpdateService : JobIntentService ( ) , Logging {
2020-01-21 10:39:01 -08:00
2020-05-13 14:47:55 -07:00
2020-01-21 10:39:01 -08:00
private val bluetoothAdapter : BluetoothAdapter by lazy ( LazyThreadSafetyMode . NONE ) {
val bluetoothManager = getSystemService ( Context . BLUETOOTH _SERVICE ) as BluetoothManager
bluetoothManager . adapter !!
}
2020-01-23 10:39:54 -08:00
2020-05-13 14:47:55 -07:00
private fun startUpdate ( macaddr : String ) {
2020-02-24 15:47:53 -08:00
info ( " starting update to $macaddr " )
val device = bluetoothAdapter . getRemoteDevice ( macaddr )
2020-01-21 12:07:03 -08:00
2020-02-10 15:31:56 -08:00
val sync =
SafeBluetooth (
this @SoftwareUpdateService ,
device
)
2020-01-23 21:58:23 -08:00
2020-01-24 12:49:27 -08:00
sync . connect ( )
2020-02-24 15:47:53 -08:00
sync . use { _ ->
// we begin by setting our MTU size as high as it can go
sync . requestMtu ( 512 )
2020-02-04 17:27:10 -08:00
2020-04-22 08:10:23 -07:00
sync . discoverServices ( ) // Get our services
2020-05-13 14:47:55 -07:00
val updateFilename = getUpdateFilename ( this , sync )
2020-02-24 15:47:53 -08:00
if ( updateFilename != null ) {
2020-05-13 14:47:55 -07:00
doUpdate ( this , sync , updateFilename )
2020-02-24 15:47:53 -08:00
} else
warn ( " Device is already up-to-date no update needed. " )
2020-01-21 10:39:01 -08:00
}
}
2020-02-24 15:47:53 -08:00
2020-05-13 14:47:55 -07:00
override fun onHandleWork ( intent : Intent ) {
// We have received work to do. The system or framework is already
2020-01-21 09:37:39 -08:00
// holding a wake lock for us at this point, so we can just go.
2020-02-24 15:47:53 -08:00
// 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 " )
2020-01-23 21:58:23 -08:00
}
2020-01-21 09:37:39 -08:00
}
}
2020-05-13 14:47:55 -07:00
companion object : Logging {
2020-01-21 09:37:39 -08:00
/ * *
* Unique job ID for this service . Must be the same for all work .
* /
2020-01-24 17:05:55 -08:00
private const val JOB _ID = 1000
2020-01-21 09:37:39 -08:00
2020-02-24 15:47:53 -08:00
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 "
2020-01-21 10:39:01 -08:00
private const val SCAN _PERIOD : Long = 10000
2020-01-24 12:49:27 -08:00
2020-01-21 10:39:01 -08:00
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
2020-11-16 15:57:40 +08:00
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)
2020-01-21 10:39:01 -08:00
2020-02-24 15:47:53 -08:00
private val SW _VERSION _CHARACTER = longBLEUUID ( " 2a28 " )
private val MANUFACTURE _CHARACTER = longBLEUUID ( " 2a29 " )
private val HW _VERSION _CHARACTER = longBLEUUID ( " 2a27 " )
2020-05-13 14:47:55 -07:00
/ * *
* % progress through the update
* /
var progress = 0
2020-01-21 09:37:39 -08:00
/ * *
* Convenience method for enqueuing work in to this service .
* /
fun enqueueWork ( context : Context , work : Intent ) {
enqueueWork (
context ,
2020-02-10 15:31:56 -08:00
SoftwareUpdateService :: class . java ,
JOB _ID , work
2020-01-21 09:37:39 -08:00
)
}
2020-05-13 14:47:55 -07:00
2020-12-10 09:23:02 +08:00
2020-06-22 17:14:29 -07:00
2020-05-13 14:47:55 -07:00
/ * * Return true if we thing the firmwarte shoulde be updated
2020-06-22 17:14:29 -07:00
*
* @param swVer the version of the software running on the target
2020-05-13 14:47:55 -07:00
* /
fun shouldUpdate (
context : Context ,
2020-12-10 09:23:02 +08:00
deviceVersion : DeviceVersion
2020-06-22 17:14:29 -07:00
) : Boolean = try {
2020-12-10 09:23:02 +08:00
val curVer = DeviceVersion ( context . getString ( R . string . cur _firmware _version ) )
2020-06-22 17:14:29 -07:00
val minVer =
2020-12-10 09:23:02 +08:00
DeviceVersion ( " 0.7.8 " ) // The oldest device version with a working software update service
2020-06-22 17:14:29 -07:00
2020-10-21 17:53:37 +08:00
( curVer > deviceVersion ) && ( deviceVersion >= minVer )
2020-06-22 17:14:29 -07:00
} catch ( ex : Exception ) {
2020-08-15 11:25:36 -07:00
errormsg ( " Error finding swupdate info " , ex )
2020-06-22 17:14:29 -07:00
false // If we fail parsing our update info
2020-05-13 14:47:55 -07:00
}
2020-11-16 15:57:40 +08:00
/ * * Return a Pair of apploadfilename , spiffs filename this device needs to use as an update ( or null if no update needed )
2020-05-13 14:47:55 -07:00
* /
fun getUpdateFilename (
context : Context ,
mfg : String
2020-11-16 15:57:40 +08:00
) : UpdateFilenames {
2020-12-10 09:23:02 +08:00
val curVer = context . getString ( R . string . cur _firmware _version )
2020-05-13 14:47:55 -07:00
2020-06-28 14:55:02 -07:00
// Check to see if the file exists (some builds might not include update files for size reasons)
2020-07-02 10:32:47 -07:00
val firmwareFiles = context . assets . list ( " firmware " ) ?: arrayOf ( )
2020-11-16 15:57:40 +08:00
2020-12-10 09:23:02 +08:00
val appLoad = " firmware- $mfg - $curVer .bin "
val spiffs = " spiffs- $curVer .bin "
2020-11-16 15:57:40 +08:00
return UpdateFilenames (
if ( firmwareFiles . contains ( appLoad ) )
" firmware/ $appLoad "
else
null ,
if ( firmwareFiles . contains ( spiffs ) )
" firmware/ $spiffs "
else
null
)
2020-05-13 14:47:55 -07:00
}
/ * * Return the filename this device needs to use as an update ( or null if no update needed )
2020-10-21 17:51:30 +08:00
* No longer used , because we get update info inband from our radio API
2020-05-13 14:47:55 -07:00
* /
2020-11-16 15:57:40 +08:00
fun getUpdateFilename ( context : Context , sync : SafeBluetooth ) : UpdateFilenames ? {
2020-05-13 14:47:55 -07:00
val service = sync . gatt !! . services . find { it . uuid == SW _UPDATE _UUID } !!
2020-10-21 17:51:30 +08:00
//val hwVerDesc = service.getCharacteristic(HW_VERSION_CHARACTER)
2020-05-13 14:47:55 -07:00
val mfgDesc = service . getCharacteristic ( MANUFACTURE _CHARACTER )
2020-10-21 17:51:30 +08:00
//val swVerDesc = service.getCharacteristic(SW_VERSION_CHARACTER)
2020-05-13 14:47:55 -07:00
// looks like HELTEC
val mfg = sync . readCharacteristic ( mfgDesc ) . getStringValue ( 0 )
2020-10-21 17:51:30 +08:00
return getUpdateFilename ( context , mfg )
2020-05-13 14:47:55 -07:00
}
/ * *
* A public function so that if you have your own SafeBluetooth connection already open
* you can use it for the software update .
* /
2020-11-16 15:57:40 +08:00
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 ) {
2020-06-22 17:14:29 -07:00
try {
val g = sync . gatt !!
2020-07-04 11:32:51 -07:00
val service = g . services . find { it . uuid == SW _UPDATE _UUID }
?: throw BLEException ( " Couldn't find update service " )
2020-06-22 17:14:29 -07:00
2020-11-16 15:57:40 +08:00
/ * *
* 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 " )
2020-06-22 17:14:29 -07:00
progress = 0
2020-11-16 15:57:40 +08:00
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? " )
}
2020-06-22 17:14:29 -07:00
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 ) {
2020-11-20 07:51:25 +08:00
// 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
2020-06-22 17:14:29 -07:00
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
}
2020-08-30 11:39:26 -07:00
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 )
2020-06-22 17:14:29 -07:00
}
2020-05-13 14:47:55 -07:00
}
2020-06-22 17:14:29 -07:00
} catch ( ex : BLEException ) {
progress = - 3
throw ex // Unexpected BLE exception
2020-05-13 14:47:55 -07:00
}
}
2020-01-21 09:37:39 -08:00
}
2020-05-13 14:47:55 -07:00
}