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 {
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
}
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-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-06-22 17:14:29 -07:00
/ * *
* Convert a version string of the form 1.23 . 57 to a comparable integer of
* the form 12357.
*
* Or throw an exception if the string can not be parsed
* /
fun verStringToInt ( s : String ) : Int {
// Allow 1 to two digits per match
val match =
Regex ( " ( \\ d{1,2}).( \\ d{1,2}).( \\ d{1,2}) " ) . find ( s )
?: throw Exception ( " Can't parse version $s " )
val ( major , minor , build ) = match . destructured
return major . toInt ( ) * 1000 + minor . toInt ( ) * 100 + build . toInt ( )
}
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 ,
swVer : String
2020-06-22 17:14:29 -07:00
) : Boolean = try {
val curVer = verStringToInt ( context . getString ( R . string . cur _firmware _version ) )
val minVer =
verStringToInt ( " 0.7.8 " ) // The oldest device version with a working software update service
2020-05-13 14:47:55 -07:00
// If the user is running a development build we never do an automatic update
2020-06-22 17:14:29 -07:00
val deviceVersion =
verStringToInt ( if ( swVer . isEmpty ( ) || swVer == " unset " ) " 99.99.99 " else swVer )
2020-10-21 17:51:30 +08:00
// (curVer > deviceVersion) && (deviceVersion >= minVer)
true
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
}
/ * * Return the filename this device needs to use as an update ( or null if no update needed )
* /
fun getUpdateFilename (
context : Context ,
mfg : String
2020-06-28 14:55:02 -07:00
) : String ? {
2020-05-13 14:47:55 -07:00
val curver = context . getString ( R . string . cur _firmware _version )
2020-10-21 17:51:30 +08:00
val base = " firmware- $mfg - $curver .bin "
2020-07-02 10:32:47 -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 ( )
return if ( firmwareFiles . contains ( base ) )
" firmware/ $base "
2020-06-28 14:55:02 -07:00
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
* /
fun getUpdateFilename ( context : Context , sync : SafeBluetooth ) : String ? {
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 .
* /
fun doUpdate ( context : Context , sync : SafeBluetooth , assetName : String ) {
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
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
}
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
}
progress = - 1 // success
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
}