2020-01-22 21:46:41 -08:00
package com.geeksville.mesh
2020-01-21 09:37:39 -08:00
2020-01-21 10:39:01 -08:00
import android.bluetooth.*
2020-01-21 13:12:01 -08:00
import android.bluetooth.le.*
2020-01-21 09:37:39 -08:00
import android.content.Context
import android.content.Intent
import android.os.Handler
2020-01-21 13:12:01 -08:00
import android.os.ParcelUuid
2020-01-21 09:37:39 -08:00
import android.os.SystemClock
import android.widget.Toast
import androidx.core.app.JobIntentService
2020-01-22 14:27:22 -08:00
import com.geeksville.android.Logging
2020-01-23 12:56:06 -08:00
import java.io.IOException
2020-01-21 12:07:03 -08:00
import java.io.InputStream
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-23 12:56:06 -08:00
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
2020-01-21 09:37:39 -08:00
2020-01-23 12:56:06 -08:00
/ * *
* Uses coroutines to safely access a bluetooth GATT device with a synchronous API
*
* The BTLE API on android is dumb . You can only have one outstanding operation in flight to
* the device . If you try to do something when something is pending , the operation just returns
* false . You are expected to chain your operations from the results callbacks .
*
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations .
* /
class SyncBluetoothDevice ( context : Context , device : BluetoothDevice ) : Logging {
private val gattCallback = object : BluetoothGattCallback ( ) {
override fun onServicesDiscovered ( gatt : BluetoothGatt , status : Int ) {
logAssert ( pendingServiceDesc != null )
if ( status != 0 )
pendingServiceDesc !! . resumeWithException ( IOException ( " Bluetooth status= $status " ) )
else
pendingServiceDesc !! . resume ( Unit )
}
override fun onCharacteristicRead (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic ,
status : Int
) {
logAssert ( pendingReadC != null )
if ( status != 0 )
pendingReadC !! . resumeWithException ( IOException ( " Bluetooth status= $status " ) )
else
pendingReadC !! . resume ( characteristic )
}
override fun onCharacteristicWrite (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic ,
status : Int
) {
logAssert ( pendingWriteC != null )
if ( status != 0 )
pendingWriteC !! . resumeWithException ( IOException ( " Bluetooth status= $status " ) )
else
pendingWriteC !! . resume ( Unit )
}
override fun onMtuChanged ( gatt : BluetoothGatt , mtu : Int , status : Int ) {
logAssert ( pendingMtu != null )
if ( status != 0 )
pendingMtu !! . resumeWithException ( IOException ( " Bluetooth status= $status " ) )
else
pendingMtu !! . resume ( mtu )
}
}
/// Users can access the GATT directly as needed
val gatt = device . connectGatt ( context , true , gattCallback ) !!
private var pendingServiceDesc : Continuation < Unit > ? = null
private var pendingMtu : Continuation < Int > ? = null
private var pendingWriteC : Continuation < Unit > ? = null
private var pendingReadC : Continuation < BluetoothGattCharacteristic > ? = null
suspend fun discoverServices ( c : BluetoothGattCharacteristic ) =
suspendCoroutine < Unit > { cont ->
pendingServiceDesc = cont
logAssert ( gatt . discoverServices ( ) )
}
/// Returns the actual MTU size used
suspend fun requestMtu ( len : Int ) = suspendCoroutine < Int > { cont ->
pendingMtu = cont
logAssert ( gatt . requestMtu ( len ) )
}
suspend fun writeCharacteristic ( c : BluetoothGattCharacteristic ) =
suspendCoroutine < Unit > { cont ->
pendingWriteC = cont
logAssert ( gatt . writeCharacteristic ( c ) )
}
suspend fun readCharacteristic ( c : BluetoothGattCharacteristic ) =
suspendCoroutine < BluetoothGattCharacteristic > { cont ->
pendingReadC = cont
logAssert ( gatt . readCharacteristic ( c ) )
}
fun disconnect ( ) {
gatt . disconnect ( )
}
}
2020-01-21 09:37:39 -08:00
/ * *
2020-01-21 10:39:01 -08:00
* typical flow
*
* startScan
* startUpdate
* sendNextBlock
* finishUpdate
*
* 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-22 14:27:22 -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-01-21 10:39:01 -08:00
fun startUpdate ( ) {
2020-01-23 09:04:06 -08:00
info ( " starting update " )
2020-01-22 16:45:27 -08:00
firmwareStream = assets . open ( " firmware.bin " )
2020-01-23 09:19:53 -08:00
firmwareCrc . reset ( )
2020-01-23 10:39:54 -08:00
firmwareNumSent = 0
firmwareSize = firmwareStream . available ( )
2020-01-21 12:07:03 -08:00
2020-01-23 10:39:54 -08:00
// we begin by setting our MTU size as high as it can go
logAssert ( updateGatt . requestMtu ( 512 ) )
2020-01-21 10:39:01 -08:00
}
// Send the next block of our file to the device
fun sendNextBlock ( ) {
2020-01-23 10:39:54 -08:00
if ( firmwareNumSent < firmwareSize ) {
2020-01-23 12:56:06 -08:00
info ( " sending block ${firmwareNumSent * 100 / firmwareSize} % " )
var blockSize = 512 - 3 // Max size MTU excluding framing
2020-01-21 12:07:03 -08:00
if ( blockSize > firmwareStream . available ( ) )
blockSize = firmwareStream . available ( )
val buffer = ByteArray ( blockSize )
2020-01-21 10:39:01 -08:00
2020-01-21 12:07:03 -08:00
// slightly expensive to keep reallocing this buffer, but whatever
2020-01-22 14:27:22 -08:00
logAssert ( firmwareStream . read ( buffer ) == blockSize )
2020-01-23 09:19:53 -08:00
firmwareCrc . update ( buffer )
2020-01-21 12:07:03 -08:00
// updateGatt.beginReliableWrite()
dataDesc . value = buffer
2020-01-22 14:27:22 -08:00
logAssert ( updateGatt . writeCharacteristic ( dataDesc ) )
2020-01-23 10:39:54 -08:00
firmwareNumSent += blockSize
2020-01-22 16:45:27 -08:00
} else {
2020-01-23 09:19:53 -08:00
// We have finished sending all our blocks, so post the CRC so our state machine can advance
2020-01-23 10:39:54 -08:00
val c = firmwareCrc . value
info ( " Sent all blocks, crc is $c " )
logAssert ( crc32Desc . setValue ( c . toInt ( ) , BluetoothGattCharacteristic . FORMAT _UINT32 , 0 ) )
2020-01-23 09:19:53 -08:00
logAssert ( updateGatt . writeCharacteristic ( crc32Desc ) )
2020-01-21 12:07:03 -08:00
}
2020-01-21 10:39:01 -08:00
}
2020-01-23 09:04:06 -08:00
fun connectToDevice ( device : BluetoothDevice ) {
debug ( " Connect to $device " )
lateinit var bluetoothGatt : BluetoothGatt // late init so we can declare our callback and use this there
//var connectionState = STATE_DISCONNECTED
// Various callback methods defined by the BLE API.
val gattCallback = object : BluetoothGattCallback ( ) {
override fun onConnectionStateChange (
gatt : BluetoothGatt ,
status : Int ,
newState : Int
) {
info ( " new bluetooth connection state $newState " )
//val intentAction: String
when ( newState ) {
BluetoothProfile . STATE _CONNECTED -> {
//intentAction = ACTION_GATT_CONNECTED
//connectionState = STATE_CONNECTED
// broadcastUpdate(intentAction)
logAssert ( bluetoothGatt . discoverServices ( ) )
}
BluetoothProfile . STATE _DISCONNECTED -> {
//intentAction = ACTION_GATT_DISCONNECTED
//connectionState = STATE_DISCONNECTED
// broadcastUpdate(intentAction)
}
}
}
// New services discovered
override fun onServicesDiscovered ( gatt : BluetoothGatt , status : Int ) {
info ( " onServicesDiscovered " )
logAssert ( status == BluetoothGatt . GATT _SUCCESS )
// broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
val service = gatt . services . find { it . uuid == SW _UPDATE _UUID }
logAssert ( service != null )
// FIXME instead of slamming in the target device here, instead make it a param for startUpdate
updateService = service !!
totalSizeDesc = service . getCharacteristic ( SW _UPDATE _TOTALSIZE _CHARACTER )
dataDesc = service . getCharacteristic ( SW _UPDATE _DATA _CHARACTER )
crc32Desc = service . getCharacteristic ( SW _UPDATE _CRC32 _CHARACTER )
updateResultDesc = service . getCharacteristic ( SW _UPDATE _RESULT _CHARACTER )
// FIXME instead of keeping the connection open, make start update just reconnect (needed once user can choose devices)
updateGatt = bluetoothGatt
enqueueWork ( this @SoftwareUpdateService , startUpdateIntent )
}
2020-01-23 10:39:54 -08:00
override fun onMtuChanged ( gatt : BluetoothGatt , mtu : Int , status : Int ) {
debug ( " onMtuChanged $mtu " )
logAssert ( status == BluetoothGatt . GATT _SUCCESS )
// Start the update by writing the # of bytes in the image
2020-01-23 12:56:06 -08:00
logAssert (
totalSizeDesc . setValue (
firmwareSize ,
BluetoothGattCharacteristic . FORMAT _UINT32 ,
0
)
)
2020-01-23 10:39:54 -08:00
logAssert ( updateGatt . writeCharacteristic ( totalSizeDesc ) )
}
2020-01-23 09:04:06 -08:00
// Result of a characteristic read operation
override fun onCharacteristicRead (
gatt : BluetoothGatt ,
characteristic : BluetoothGattCharacteristic ,
status : Int
) {
2020-01-23 09:19:53 -08:00
debug ( " onCharacteristicRead $characteristic " )
2020-01-23 09:04:06 -08:00
logAssert ( status == BluetoothGatt . GATT _SUCCESS )
if ( characteristic == totalSizeDesc ) {
// Our read of this has completed, either fail or continue updating
val readvalue =
characteristic . getIntValue ( BluetoothGattCharacteristic . FORMAT _UINT32 , 0 )
logAssert ( readvalue != 0 ) // FIXME - handle this case
enqueueWork ( this @SoftwareUpdateService , sendNextBlockIntent )
2020-01-23 12:56:06 -08:00
} else if ( characteristic == updateResultDesc ) {
2020-01-23 09:19:53 -08:00
// we just read the update result if !0 we have an error
val readvalue =
characteristic . getIntValue ( BluetoothGattCharacteristic . FORMAT _UINT8 , 0 )
logAssert ( readvalue == 0 ) // FIXME - handle this case
2020-01-23 12:56:06 -08:00
} else {
2020-01-23 09:04:06 -08:00
warn ( " Unexpected read: $characteristic " )
}
// broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}
override fun onCharacteristicWrite (
gatt : BluetoothGatt ? ,
characteristic : BluetoothGattCharacteristic ? ,
status : Int
) {
2020-01-23 09:19:53 -08:00
debug ( " onCharacteristicWrite $characteristic " )
2020-01-23 09:04:06 -08:00
logAssert ( status == BluetoothGatt . GATT _SUCCESS )
2020-01-23 12:56:06 -08:00
if ( characteristic == totalSizeDesc ) {
2020-01-23 09:04:06 -08:00
// Our write completed, queue up a readback
logAssert ( updateGatt . readCharacteristic ( totalSizeDesc ) )
} else if ( characteristic == dataDesc ) {
enqueueWork ( this @SoftwareUpdateService , sendNextBlockIntent )
2020-01-23 09:19:53 -08:00
} else if ( characteristic == crc32Desc ) {
// Now that we wrote the CRC, we should read the result code
logAssert ( updateGatt . readCharacteristic ( updateResultDesc ) )
2020-01-23 12:56:06 -08:00
} else {
2020-01-23 09:04:06 -08:00
warn ( " Unexpected write: $characteristic " )
}
}
}
bluetoothGatt =
device . connectGatt ( this @SoftwareUpdateService . applicationContext , true , gattCallback ) !!
toast ( " Connected to $device " )
// too early to do this here
// logAssert(bluetoothGatt.discoverServices())
}
2020-01-21 13:12:01 -08:00
private val scanCallback = object : ScanCallback ( ) {
override fun onScanFailed ( errorCode : Int ) {
throw NotImplementedError ( )
}
2020-01-21 18:26:28 -08:00
override fun onBatchScanResults ( results : MutableList < ScanResult > ? ) {
throw NotImplementedError ( )
}
2020-01-21 13:12:01 -08:00
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
// check if it is an eligable device and store it in our list of candidates
// if that device later disconnects remove it as a candidate
override fun onScanResult ( callbackType : Int , result : ScanResult ) {
2020-01-23 09:04:06 -08:00
info ( " onScanResult " )
2020-01-21 13:12:01 -08:00
// We don't need any more results now
bluetoothAdapter . bluetoothLeScanner . stopScan ( this )
2020-01-23 09:04:06 -08:00
connectToDevice ( result . device )
2020-01-21 10:39:01 -08:00
}
}
2020-01-23 09:04:06 -08:00
// Until my race condition with scanning is fixed
fun connectToTestDevice ( ) {
connectToDevice ( bluetoothAdapter . getRemoteDevice ( " B4:E6:2D:EA:32:B7 " ) )
}
2020-01-21 13:12:01 -08:00
2020-01-21 10:39:01 -08:00
private fun scanLeDevice ( enable : Boolean ) {
when ( enable ) {
true -> {
// Stops scanning after a pre-defined scan period.
/ * handler . postDelayed ( {
mScanning = false
bluetoothAdapter . stopLeScan ( leScanCallback )
} , SCAN _PERIOD )
mScanning = true * /
2020-01-21 13:12:01 -08:00
val scanner = bluetoothAdapter . bluetoothLeScanner
// filter and only accept devices that have a sw update service
val filter = ScanFilter . Builder ( ) . setServiceUuid ( ParcelUuid ( SW _UPDATE _UUID ) ) . build ( )
2020-01-21 18:26:28 -08:00
/ * ScanSettings . CALLBACK _TYPE _FIRST _MATCH seems to trigger a bug returning an error of
SCAN _FAILED _OUT _OF _HARDWARE _RESOURCES ( error # 5 )
* /
2020-01-22 16:45:27 -08:00
val settings =
ScanSettings . Builder ( ) . setScanMode ( ScanSettings . SCAN _MODE _LOW _LATENCY ) .
// setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT).
// setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH).
build ( )
2020-01-21 13:12:01 -08:00
scanner . startScan ( listOf ( filter ) , settings , scanCallback )
2020-01-21 10:39:01 -08:00
}
else -> {
// mScanning = false
2020-01-21 13:12:01 -08:00
// bluetoothAdapter.stopLeScan(leScanCallback)
2020-01-21 10:39:01 -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-01-23 10:39:54 -08:00
debug ( " Executing work: $intent " )
2020-01-22 16:45:27 -08:00
when ( intent . action ) {
2020-01-23 09:04:06 -08:00
scanDevicesIntent . action -> connectToTestDevice ( ) // FIXME scanLeDevice(true)
2020-01-21 12:07:03 -08:00
startUpdateIntent . action -> startUpdate ( )
sendNextBlockIntent . action -> sendNextBlock ( )
2020-01-22 14:27:22 -08:00
else -> logAssert ( false )
2020-01-21 12:07:03 -08:00
}
2020-01-23 10:39:54 -08:00
debug (
2020-01-22 16:45:27 -08:00
" Completed service @ " + SystemClock . elapsedRealtime ( )
2020-01-21 09:37:39 -08:00
)
}
override fun onDestroy ( ) {
super . onDestroy ( )
2020-01-23 10:39:54 -08:00
// toast("All work complete")
2020-01-21 09:37:39 -08:00
}
val mHandler = Handler ( )
// Helper for showing tests
fun toast ( text : CharSequence ? ) {
mHandler . post {
2020-01-21 10:39:01 -08:00
Toast . makeText ( this @SoftwareUpdateService , text , Toast . LENGTH _SHORT ) . show ( )
2020-01-21 09:37:39 -08:00
}
}
companion object {
/ * *
* Unique job ID for this service . Must be the same for all work .
* /
const val JOB _ID = 1000
2020-01-22 21:46:41 -08:00
val scanDevicesIntent = Intent ( " com.geeksville.com.geeeksville.mesh.SCAN_DEVICES " )
val startUpdateIntent = Intent ( " com.geeksville.com.geeeksville.mesh.START_UPDATE " )
2020-01-23 12:56:06 -08:00
private val sendNextBlockIntent =
Intent ( " com.geeksville.com.geeeksville.mesh.SEND_NEXT_BLOCK " )
2020-01-21 10:39:01 -08:00
private const val SCAN _PERIOD : Long = 10000
//const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
//const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"
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-01-22 16:45:27 -08:00
// FIXME - this is state that really more properly goes with the serice instance, but
// it can go away if our work queue gets empty. So we keep it here instead. Not sure
// if there is a better approach?
lateinit var updateGatt : BluetoothGatt // the gatt api used to talk to our device
lateinit var updateService : BluetoothGattService // The service we are currently talking to to do the update
lateinit var totalSizeDesc : BluetoothGattCharacteristic
lateinit var dataDesc : BluetoothGattCharacteristic
lateinit var crc32Desc : BluetoothGattCharacteristic
lateinit var updateResultDesc : BluetoothGattCharacteristic
lateinit var firmwareStream : InputStream
2020-01-23 09:19:53 -08:00
val firmwareCrc = CRC32 ( )
2020-01-23 10:39:54 -08:00
var firmwareNumSent = 0
var firmwareSize = 0
2020-01-22 16:45:27 -08:00
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-01-21 10:39:01 -08:00
SoftwareUpdateService :: class . java , JOB _ID , work
2020-01-21 09:37:39 -08:00
)
}
}
}