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-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
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-02-24 15:47:53 -08:00
fun startUpdate ( macaddr : String ) {
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 { _ ->
sync . discoverServices ( ) // Get our services
2020-01-27 16:24:38 -08:00
2020-02-24 15:47:53 -08:00
// 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-02-24 15:47:53 -08:00
val service = sync . gatt !! . services . find { it . uuid == SW _UPDATE _UUID } !!
2020-01-21 12:07:03 -08:00
2020-02-24 15:47:53 -08:00
fun doFirmwareUpdate ( assetName : String ) {
2020-01-21 10:39:01 -08:00
2020-02-24 15:47:53 -08:00
info ( " Starting firmware update for $assetName " )
2020-01-21 12:07:03 -08:00
2020-02-24 15:47:53 -08: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 )
2020-01-23 09:04:06 -08:00
2020-02-24 15:47:53 -08:00
assets . open ( assetName ) . use { firmwareStream ->
val firmwareCrc = CRC32 ( )
var firmwareNumSent = 0
val firmwareSize = firmwareStream . available ( )
2020-01-23 09:04:06 -08:00
2020-02-24 15:47:53 -08:00
// Start the update by writing the # of bytes in the image
logAssert (
totalSizeDesc . setValue (
firmwareSize ,
BluetoothGattCharacteristic . FORMAT _UINT32 ,
0
)
)
sync . writeCharacteristic ( totalSizeDesc )
2020-01-23 09:04:06 -08:00
2020-02-24 15:47:53 -08:00
// 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
}
}
2020-01-23 21:58:23 -08:00
2020-02-24 15:47:53 -08:00
/// 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 )
2020-01-21 13:12:01 -08:00
2020-02-24 15:47:53 -08:00
// looks like 1.0-US
val hwVer = sync . readCharacteristic ( hwVerDesc ) . getStringValue ( 0 )
2020-01-21 18:26:28 -08:00
2020-02-24 15:47:53 -08:00
// looks like HELTEC
val mfg = sync . readCharacteristic ( mfgDesc ) . getStringValue ( 0 )
2020-01-21 13:12:01 -08:00
2020-02-24 15:47:53 -08:00
// looks like 0.0.12
val swVer = sync . readCharacteristic ( swVerDesc ) . getStringValue ( 0 )
2020-01-23 09:04:06 -08:00
2020-02-24 15:47:53 -08:00
val curver = getString ( R . string . cur _firmware _version )
2020-01-21 13:12:01 -08:00
2020-02-24 15:47:53 -08:00
// FIXME, instead compare version strings carefully to see if less than
val needsUpdate = ( curver != swVer )
2020-01-21 10:39:01 -08:00
2020-02-24 15:47:53 -08:00
return if ( ! needsUpdate )
null
else {
val regionRegex = Regex ( " .+-(.+) " )
val ( region ) = regionRegex . find ( hwVer ) ?. destructured
?: throw Exception ( " Malformed hw version " )
2020-01-21 13:12:01 -08:00
2020-02-24 15:47:53 -08:00
" firmware/firmware- $mfg - $region - $curver .bin "
}
2020-01-21 10:39:01 -08:00
}
2020-02-24 15:47:53 -08:00
val updateFilename = getUpdateFilename ( )
if ( updateFilename != null ) {
doFirmwareUpdate ( updateFilename )
} 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-01-21 09:37:39 -08:00
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.
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
}
}
companion object {
/ * *
* 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-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
)
}
}
}