From 397640151fb9216d67ce4b40cb19650df6751a0e Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 22 Jan 2020 16:45:27 -0800 Subject: [PATCH] bluetooth better --- TODO.md | 5 +- .../com/geeksville/meshutil/MainActivity.kt | 34 +++++---- .../meshutil/SoftwareUpdateService.kt | 71 +++++++++++-------- 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/TODO.md b/TODO.md index 4b8e35dd..bef3149c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,8 @@ - +* fix bluetooth update +* add real messaging code/protobufs * use https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/#4 to show service state -*fix bluetooth +* connect to bluetooth device automatically using minimum power # Medium priority diff --git a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt index b9e7b650..9bfb5036 100644 --- a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt +++ b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt @@ -27,7 +27,6 @@ import androidx.ui.tooling.preview.Preview import com.geeksville.android.Logging - class MainActivity : AppCompatActivity(), Logging { companion object { @@ -43,13 +42,20 @@ class MainActivity : AppCompatActivity(), Logging { fun requestPermission() { debug("Checking permissions") - val perms = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, + val perms = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION, Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.WAKE_LOCK) + Manifest.permission.WAKE_LOCK + ) - val missingPerms = perms.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } + val missingPerms = perms.filter { + ContextCompat.checkSelfPermission( + this, + it + ) != PackageManager.PERMISSION_GRANTED + } if (missingPerms.isNotEmpty()) { missingPerms.forEach { // Permission is not granted @@ -73,25 +79,24 @@ class MainActivity : AppCompatActivity(), Logging { } } + @Preview @Composable fun composeView() { MaterialTheme { Column { - Text(text = "MeshUtil Ugly UI", modifier = Spacing(16.dp)) + Text(text = "MeshUtil Ugly UI", modifier = Spacing(8.dp)) Button(text = "Start scan", onClick = { if (bluetoothAdapter != null) { - SoftwareUpdateService.enqueueWork(this@MainActivity, SoftwareUpdateService.scanDevicesIntent) + SoftwareUpdateService.enqueueWork( + this@MainActivity, + SoftwareUpdateService.scanDevicesIntent + ) } }) } - }} - - @Preview - @Composable - fun defaultPreview() { - composeView() + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -102,13 +107,12 @@ class MainActivity : AppCompatActivity(), Logging { // Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. - if(bluetoothAdapter != null) { + if (bluetoothAdapter != null) { bluetoothAdapter!!.takeIf { !it.isEnabled }?.apply { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) } - } - else { + } else { Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG).show() } diff --git a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt index 4c6a9d5c..fe7d84bf 100644 --- a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt @@ -36,27 +36,18 @@ class SoftwareUpdateService : JobIntentService(), Logging { 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 - lateinit var dataDesc: BluetoothGattCharacteristic - lateinit var firmwareStream: InputStream - fun startUpdate() { - totalSizeDesc = updateService.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)!! + firmwareStream = assets.open("firmware.bin") - firmwareStream = assets.open("firmware.bin") - - // 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)) - logAssert(updateGatt.readCharacteristic(totalSizeDesc)) + // 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)) } // Send the next block of our file to the device fun sendNextBlock() { - if(firmwareStream.available() > 0) { + if (firmwareStream.available() > 0) { var blockSize = 512 if (blockSize > firmwareStream.available()) @@ -66,12 +57,10 @@ class SoftwareUpdateService : JobIntentService(), Logging { // slightly expensive to keep reallocing this buffer, but whatever logAssert(firmwareStream.read(buffer) == blockSize) - dataDesc = updateService.getCharacteristic(SW_UPDATE_DATA_CHARACTER)!! // updateGatt.beginReliableWrite() dataDesc.value = buffer logAssert(updateGatt.writeCharacteristic(dataDesc)) - } - else { + } else { logAssert(false) // fixme } } @@ -99,11 +88,13 @@ class SoftwareUpdateService : JobIntentService(), Logging { // 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 -> { @@ -130,6 +121,11 @@ class SoftwareUpdateService : JobIntentService(), Logging { if (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) @@ -162,19 +158,24 @@ class SoftwareUpdateService : JobIntentService(), Logging { ) { logAssert(status == BluetoothGatt.GATT_SUCCESS) - if (characteristic == dataDesc) { + if(characteristic == totalSizeDesc) { + // Our write completed, queue up a readback + logAssert(updateGatt.readCharacteristic(totalSizeDesc)) + } else if (characteristic == dataDesc) { enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent) } } } - bluetoothGatt = result.device.connectGatt(this@SoftwareUpdateService, false, gattCallback)!! + bluetoothGatt = + result.device.connectGatt(this@SoftwareUpdateService.applicationContext, true, gattCallback)!! toast("FISH " + bluetoothGatt) - logAssert(bluetoothGatt.discoverServices()) + + // too early to do this here + // logAssert(bluetoothGatt.discoverServices()) } } - private fun scanLeDevice(enable: Boolean) { when (enable) { true -> { @@ -193,11 +194,11 @@ class SoftwareUpdateService : JobIntentService(), Logging { /* ScanSettings.CALLBACK_TYPE_FIRST_MATCH seems to trigger a bug returning an error of SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES (error #5) */ - val settings = ScanSettings.Builder(). - setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY). - // setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT). - // setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH). - build() + val settings = + ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY). + // setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT). + // setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH). + build() scanner.startScan(listOf(filter), settings, scanCallback) } else -> { @@ -216,14 +217,15 @@ class SoftwareUpdateService : JobIntentService(), Logging { } toast("Executing: $label") - when(intent.action) { + when (intent.action) { scanDevicesIntent.action -> scanLeDevice(true) startUpdateIntent.action -> startUpdate() sendNextBlockIntent.action -> sendNextBlock() else -> logAssert(false) } - info("Completed service @ " + SystemClock.elapsedRealtime() + info( + "Completed service @ " + SystemClock.elapsedRealtime() ) } @@ -269,6 +271,17 @@ class SoftwareUpdateService : JobIntentService(), Logging { 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 + // 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 + /** * Convenience method for enqueuing work in to this service. */