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
/ * * 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 )
2020-05-15 10:18:15 -07:00
return needsUpdate && !is DevBuild && false // temporarily disabled because it fails occasionally
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 ,
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 " )
2020-05-15 10:18:15 -07:00
progress = 0
2020-05-13 14:47:55 -07:00
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
2020-06-13 16:21:26 -07:00
sync . writeCharacteristic (
totalSizeDesc ,
toNetworkByteArray ( firmwareSize , BluetoothGattCharacteristic . FORMAT _UINT32 )
2020-05-13 14:47:55 -07:00
)
2020-05-15 10:18:15 -07:00
// Our write completed, queue up a readback
2020-05-13 14:47:55 -07:00
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 )
2020-06-13 16:21:26 -07:00
sync . writeCharacteristic ( dataDesc , buffer )
2020-05-13 14:47:55 -07:00
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 " )
2020-06-13 16:21:26 -07:00
sync . writeCharacteristic (
crc32Desc ,
toNetworkByteArray ( c . toInt ( ) , BluetoothGattCharacteristic . FORMAT _UINT32 )
2020-05-13 14:47:55 -07:00
)
// 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
}
}
2020-01-21 09:37:39 -08:00
}
2020-05-13 14:47:55 -07:00
}