Meshtastic-Android/app/src/main/java/com/geeksville/mesh/SoftwareUpdateService.kt

317 lines
13 KiB
Kotlin
Raw Normal View History

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-21 12:07:03 -08:00
import java.io.InputStream
2020-01-21 10:39:01 -08:00
import java.util.*
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!!
}
fun startUpdate() {
info("starting update")
2020-01-22 16:45:27 -08:00
firmwareStream = assets.open("firmware.bin")
2020-01-21 12:07:03 -08:00
2020-01-22 16:45:27 -08:00
// Start the update by writing the # of bytes in the image
val numBytes = firmwareStream.available()
logAssert(totalSizeDesc.setValue(numBytes, BluetoothGattCharacteristic.FORMAT_UINT32, 0))
logAssert(updateGatt.writeCharacteristic(totalSizeDesc))
2020-01-21 10:39:01 -08:00
}
// Send the next block of our file to the device
fun sendNextBlock() {
info("sending next block")
2020-01-22 16:45:27 -08:00
if (firmwareStream.available() > 0) {
2020-01-21 12:07:03 -08:00
var blockSize = 512
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-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-22 16:45:27 -08:00
} else {
2020-01-22 14:27:22 -08:00
logAssert(false) // fixme
2020-01-21 12:07:03 -08:00
}
2020-01-21 10:39:01 -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)
}
// Result of a characteristic read operation
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
debug("onCharacteristicRead")
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)
}
else {
warn("Unexpected read: $characteristic")
}
// broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
status: Int
) {
debug("onCharacteristicWrite")
logAssert(status == BluetoothGatt.GATT_SUCCESS)
if(characteristic == totalSizeDesc) {
// Our write completed, queue up a readback
logAssert(updateGatt.readCharacteristic(totalSizeDesc))
} else if (characteristic == dataDesc) {
enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
}
else {
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) {
info("onScanResult")
2020-01-21 13:12:01 -08:00
// We don't need any more results now
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
connectToDevice(result.device)
2020-01-21 10:39:01 -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-22 14:27:22 -08:00
info("Executing work: $intent")
2020-01-21 09:37:39 -08:00
var label = intent.getStringExtra("label")
if (label == null) {
label = intent.toString()
}
toast("Executing: $label")
2020-01-21 10:39:01 -08:00
2020-01-22 16:45:27 -08:00
when (intent.action) {
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-22 16:45:27 -08:00
info(
"Completed service @ " + SystemClock.elapsedRealtime()
2020-01-21 09:37:39 -08:00
)
}
override fun onDestroy() {
super.onDestroy()
toast("All work complete")
}
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")
private val sendNextBlockIntent = Intent("com.geeksville.com.geeeksville.mesh.SEND_NEXT_BLOCK")
private val finishUpdateIntent = Intent("com.geeksville.com.geeeksville.mesh.FINISH_UPDATE")
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-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
)
}
}
}