From 6fd57c87222165646c053ba37a5e1f722604d2ad Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 21 Jan 2020 10:39:01 -0800 Subject: [PATCH] move bt code to service --- TODO.md | 1 + .../com/geeksville/meshutil/MainActivity.kt | 131 +------------ .../meshutil/SoftwareUpdateService.kt | 178 ++++++++++++++++-- 3 files changed, 166 insertions(+), 144 deletions(-) diff --git a/TODO.md b/TODO.md index 6dfda74fc..a695a98c1 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ # Medium priority +* add crash reporting * remove example code boilerplate from the service * add analytics (make them optional) diff --git a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt index af6f6203c..efa1f97c3 100644 --- a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt +++ b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt @@ -19,142 +19,13 @@ class MainActivity : AppCompatActivity() { companion object { const val REQUEST_ENABLE_BT = 10 - - 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 const val STATE_DISCONNECTED = 0 - private const val STATE_CONNECTING = 1 - private const val STATE_CONNECTED = 2 - - 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 } - private val BluetoothAdapter.isDisabled: Boolean - get() = !isEnabled - private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager.adapter!! } - private var mScanning: Boolean = false - private val handler = Handler() - - private val leScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ -> - runOnUiThread { - /* - leDeviceListAdapter.addDevice(device) - leDeviceListAdapter.notifyDataSetChanged() - */ - - lateinit var bluetoothGatt: BluetoothGatt - - //var connectionState = STATE_DISCONNECTED - - lateinit var totalSizeDesc: BluetoothGattCharacteristic - - // Send the next block of our file to the device - fun sendNextBlock() { - - } - - // Various callback methods defined by the BLE API. - val gattCallback = object : BluetoothGattCallback() { - override fun onConnectionStateChange( - gatt: BluetoothGatt, - status: Int, - newState: Int - ) { - //val intentAction: String - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - //intentAction = ACTION_GATT_CONNECTED - //connectionState = STATE_CONNECTED - // broadcastUpdate(intentAction) - Log.i(TAG, "Connected to GATT server.") - Log.i(TAG, "Attempting to start service discovery: " + - bluetoothGatt.discoverServices()) - } - BluetoothProfile.STATE_DISCONNECTED -> { - //intentAction = ACTION_GATT_DISCONNECTED - //connectionState = STATE_DISCONNECTED - Log.i(TAG, "Disconnected from GATT server.") - // broadcastUpdate(intentAction) - } - } - } - - // New services discovered - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - when (status) { - BluetoothGatt.GATT_SUCCESS -> { - // broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) - - val updateService = gatt.services.find { it.uuid == SW_UPDATE_UUID } - if(updateService != null) { - - // Start the update by writing the # of bytes in the image - val numBytes = 45 - totalSizeDesc = updateService.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)!! - assert(totalSizeDesc.setValue(numBytes, BluetoothGattCharacteristic.FORMAT_UINT32, 0)) - assert(bluetoothGatt.writeCharacteristic(totalSizeDesc)) - assert(bluetoothGatt.readCharacteristic(totalSizeDesc)) - } - } - else -> Log.w(TAG, "onServicesDiscovered received: $status") - } - } - - // Result of a characteristic read operation - override fun onCharacteristicRead( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - status: Int - ) { - assert(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) - assert(readvalue != 0) // FIXME - handle this case - sendNextBlock() // FIXME, call this in a job queue of the service - } - - // broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) - } - } - bluetoothGatt = device.connectGatt(this, false, gattCallback)!! - } - } - - 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 - bluetoothAdapter.startLeScan(leScanCallback) - } - else -> { - mScanning = false - bluetoothAdapter.stopLeScan(leScanCallback) - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -167,7 +38,7 @@ class MainActivity : AppCompatActivity() { // Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. - bluetoothAdapter.takeIf { it.isDisabled }?.apply { + bluetoothAdapter.takeIf { !it.isEnabled }?.apply { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) } diff --git a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt index 63661aa1a..69a609466 100644 --- a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt @@ -1,18 +1,154 @@ package com.geeksville.meshutil +import android.bluetooth.* import android.content.Context import android.content.Intent import android.os.Handler import android.os.SystemClock import android.util.Log import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity import androidx.core.app.JobIntentService +import java.util.* /** - * Example implementation of a JobIntentService. + * 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 */ -class SimpleJobIntentService : JobIntentService() { +class SoftwareUpdateService : JobIntentService() { + + private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) { + val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothManager.adapter!! + } + + 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 + + fun startUpdate() { + if (updateService != null) { + totalSizeDesc = updateService.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)!! + + // Start the update by writing the # of bytes in the image + val numBytes = 45 + assert(totalSizeDesc.setValue(numBytes, BluetoothGattCharacteristic.FORMAT_UINT32, 0)) + assert(updateGatt.writeCharacteristic(totalSizeDesc)) + assert(updateGatt.readCharacteristic(totalSizeDesc)) + } + } + + // Send the next block of our file to the device + fun sendNextBlock() { + + } + + // 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 + private val leScanCallback = BluetoothAdapter.LeScanCallback { 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 + ) { + //val intentAction: String + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + //intentAction = ACTION_GATT_CONNECTED + //connectionState = STATE_CONNECTED + // broadcastUpdate(intentAction) + Log.i(AppCompatActivity.TAG, "Connected to GATT server.") + Log.i( + AppCompatActivity.TAG, "Attempting to start service discovery: " + ) + assert(bluetoothGatt.discoverServices()) + } + BluetoothProfile.STATE_DISCONNECTED -> { + //intentAction = ACTION_GATT_DISCONNECTED + //connectionState = STATE_DISCONNECTED + Log.i(AppCompatActivity.TAG, "Disconnected from GATT server.") + // broadcastUpdate(intentAction) + } + } + } + + // New services discovered + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + assert(status == BluetoothGatt.GATT_SUCCESS) + + // broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) + + val service = gatt.services.find { it.uuid == SW_UPDATE_UUID } + if (service != null) { + // FIXME instead of slamming in the target device here, instead make it a param for startUpdate + updateService = service + // FIXME instead of keeping the connection open, make start update just reconnect (needed once user can choose devices) + updateGatt = bluetoothGatt + enqueueWork(this@SoftwareUpdateService, startUpdateIntent) + } else { + // drop our connection - we don't care about this device + bluetoothGatt.disconnect() + } + } + + // Result of a characteristic read operation + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + assert(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) + assert(readvalue != 0) // FIXME - handle this case + enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent) + } + + // broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) + } + } + bluetoothGatt = device.connectGatt(this, false, gattCallback)!! + } + + 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 */ + assert(bluetoothAdapter.startLeScan(leScanCallback)) + } + else -> { + // mScanning = false + bluetoothAdapter.stopLeScan(leScanCallback) + } + } + } + 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. Log.i("SimpleJobIntentService", "Executing work: $intent") @@ -21,16 +157,7 @@ class SimpleJobIntentService : JobIntentService() { label = intent.toString() } toast("Executing: $label") - for (i in 0..4) { - Log.i( - "SimpleJobIntentService", "Running service " + (i + 1) - + "/5 @ " + SystemClock.elapsedRealtime() - ) - try { - Thread.sleep(1000) - } catch (e: InterruptedException) { - } - } + Log.i( "SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime() @@ -46,7 +173,7 @@ class SimpleJobIntentService : JobIntentService() { // Helper for showing tests fun toast(text: CharSequence?) { mHandler.post { - Toast.makeText(this@SimpleJobIntentService, text, Toast.LENGTH_SHORT).show() + Toast.makeText(this@SoftwareUpdateService, text, Toast.LENGTH_SHORT).show() } } @@ -56,13 +183,36 @@ class SimpleJobIntentService : JobIntentService() { */ const val JOB_ID = 1000 + val scanDevicesIntent = Intent("com.geeksville.meshutil.SCAN_DEVICES") + val startUpdateIntent = Intent("com.geeksville.meshutil.START_UPDATE") + private val sendNextBlockIntent = Intent("com.geeksville.meshutil.SEND_NEXT_BLOCK") + private val finishUpdateIntent = Intent("com.geeksville.meshutil.FINISH_UPDATE") + + 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 + /** * Convenience method for enqueuing work in to this service. */ fun enqueueWork(context: Context, work: Intent) { enqueueWork( context, - SimpleJobIntentService::class.java, JOB_ID, work + SoftwareUpdateService::class.java, JOB_ID, work ) } }