From 38fe6525c990f25bb8bfb0b9a4a6d1ea62c6db44 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Fri, 21 Jan 2022 20:42:55 +0100 Subject: [PATCH 01/25] Fix removeBond reflection code --- .../main/java/com/welie/blessed/BluetoothCentralManager.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index 53e83a8..8eb58fe 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -778,9 +778,8 @@ class BluetoothCentralManager(private val context: Context) { // Try to remove the bond return if (peripheralToUnBond != null) { try { - // TODO FIX THIS, automatic conversion gone wrong - val method = peripheralToUnBond.javaClass.getMethod("removeBond", null) - val result = method.invoke(peripheralToUnBond, null as Array?) as Boolean + val method = peripheralToUnBond.javaClass.getMethod("removeBond") + val result = method.invoke(peripheralToUnBond) as Boolean if (result) { Logger.i(TAG, "Succesfully removed bond for '%s'", peripheralToUnBond.name) } From eeb6bc3d1571c3b0075c1b97c5fcfceba9fc03cc Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sat, 29 Jan 2022 08:56:19 +0100 Subject: [PATCH 02/25] Refactoring and adding possibility to observe Adapter state --- .../welie/blessedexample/BluetoothHandler.kt | 9 +- .../com/welie/blessedexample/MainActivity.kt | 8 +- blessed/build.gradle | 2 - .../welie/blessed/BluetoothCentralManager.kt | 59 ++++-- .../com/welie/blessed/BluetoothPeripheral.kt | 177 ++++++++---------- .../blessed/BluetoothPeripheralManager.kt | 2 + build.gradle | 2 +- 7 files changed, 129 insertions(+), 130 deletions(-) diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index 79aeff9..0bc7e83 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -1,5 +1,6 @@ package com.welie.blessedexample +import android.bluetooth.BluetoothAdapter import android.content.Context import com.welie.blessed.* import kotlinx.coroutines.* @@ -22,7 +23,7 @@ internal class BluetoothHandler private constructor(context: Context) { val weightChannel = Channel(UNLIMITED) private fun handlePeripheral(peripheral: BluetoothPeripheral) { - scope.launch(Dispatchers.IO) { + scope.launch { try { val mtu = peripheral.requestMtu(185) Timber.i("MTU is $mtu") @@ -307,6 +308,12 @@ internal class BluetoothHandler private constructor(context: Context) { } } + central.observeAdapterState { state -> + when(state) { + BluetoothAdapter.STATE_ON -> startScanning() + } + } + startScanning() } } \ No newline at end of file diff --git a/app/src/main/java/com/welie/blessedexample/MainActivity.kt b/app/src/main/java/com/welie/blessedexample/MainActivity.kt index 666bda2..5fb5284 100644 --- a/app/src/main/java/com/welie/blessedexample/MainActivity.kt +++ b/app/src/main/java/com/welie/blessedexample/MainActivity.kt @@ -226,7 +226,7 @@ class MainActivity : AppCompatActivity() { if (missingPermissions.isNotEmpty()) { requestPermissions(missingPermissions, ACCESS_LOCATION_REQUEST) } else { - permissionsGranted() + checkIfLocationIsNeeded() } } @@ -250,10 +250,10 @@ class MainActivity : AppCompatActivity() { } else arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) } - private fun permissionsGranted() { - // Check if Location services are on because they are required to make scanning work for SDK < 31 + private fun checkIfLocationIsNeeded() { val targetSdkVersion = applicationInfo.targetSdkVersion if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && targetSdkVersion < Build.VERSION_CODES.S) { + // Check if Location services are on because they are required to make scanning work for SDK < 31 if (checkLocationServices()) { initBluetoothHandler() } @@ -313,7 +313,7 @@ class MainActivity : AppCompatActivity() { } } if (allGranted) { - permissionsGranted() + checkIfLocationIsNeeded() } else { AlertDialog.Builder(this@MainActivity) .setTitle("Location permission is required for scanning Bluetooth peripherals") diff --git a/blessed/build.gradle b/blessed/build.gradle index a36b7c4..bed313d 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -8,8 +8,6 @@ android { defaultConfig { minSdkVersion 26 targetSdkVersion 31 - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index 8eb58fe..ae76dca 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -23,6 +23,7 @@ package com.welie.blessed import android.Manifest +import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager @@ -50,6 +51,8 @@ import kotlin.coroutines.suspendCoroutine /** * Central Manager class to scan and connect with bluetooth peripherals. */ +@SuppressLint("MissingPermission") +@Suppress("unused") class BluetoothCentralManager(private val context: Context) { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val bluetoothAdapter: BluetoothAdapter @@ -74,6 +77,7 @@ class BluetoothCentralManager(private val context: Context) { private var disconnectRunnable: Runnable? = null private val pinCodes: MutableMap = ConcurrentHashMap() private var currentResultCallback : ((BluetoothPeripheral, ScanResult) -> Unit)? = null + private var adapterStateCallback: (state: Int) -> Unit = {} private val scanByNameCallback: ScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -254,7 +258,7 @@ class BluetoothCentralManager(private val context: Context) { setScanTimer() currentCallback = scanCallback currentFilters = filters - bluetoothScanner!!.startScan(filters, scanSettings, scanCallback) + bluetoothScanner?.startScan(filters, scanSettings, scanCallback) Logger.i(TAG, "scan started") } else { Logger.e(TAG, "starting scan failed") @@ -590,11 +594,11 @@ class BluetoothCentralManager(private val context: Context) { throw IllegalArgumentException(message) } return if (connectedPeripherals.containsKey(peripheralAddress)) { - Objects.requireNonNull(connectedPeripherals[peripheralAddress])!! + requireNotNull(connectedPeripherals[peripheralAddress]) } else if (unconnectedPeripherals.containsKey(peripheralAddress)) { - Objects.requireNonNull(unconnectedPeripherals[peripheralAddress])!! + requireNotNull(unconnectedPeripherals[peripheralAddress]) } else if (scannedPeripherals.containsKey(peripheralAddress)) { - Objects.requireNonNull(scannedPeripherals[peripheralAddress])!! + requireNotNull(scannedPeripherals[peripheralAddress]) } else { val peripheral = BluetoothPeripheral(context, bluetoothAdapter.getRemoteDevice(peripheralAddress), internalCallback) scannedPeripherals[peripheralAddress] = peripheral @@ -857,14 +861,17 @@ class BluetoothCentralManager(private val context: Context) { } } - @JvmField - val adapterStateReceiver: BroadcastReceiver = object : BroadcastReceiver() { + fun observeAdapterState(callback: (state: Int) -> Unit) { + this.adapterStateCallback = callback + } + + private val adapterStateReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.action ?: return if (action == BluetoothAdapter.ACTION_STATE_CHANGED) { val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) - // scope.launch { bluetoothCentralManagerCallback.onBluetoothAdapterStateChanged(state) } handleAdapterState(state) + adapterStateCallback.invoke(state) } } } @@ -873,7 +880,7 @@ class BluetoothCentralManager(private val context: Context) { when (state) { BluetoothAdapter.STATE_OFF -> { // Check if there are any connected peripherals or connections in progress - if (connectedPeripherals.size > 0 || unconnectedPeripherals.size > 0) { + if (connectedPeripherals.isNotEmpty() || unconnectedPeripherals.isNotEmpty()) { // See if they are automatically disconnect expectingBluetoothOffDisconnects = true startDisconnectionTimer() @@ -881,6 +888,24 @@ class BluetoothCentralManager(private val context: Context) { Logger.d(TAG, "bluetooth turned off") } BluetoothAdapter.STATE_TURNING_OFF -> { + // Stop all scans so that we are back in a clean state + if (isScanning) { + // Note that we can't call stopScan if the adapter is off + // On some phones like the Nokia 8, the adapter will be already off at this point + // So add a try/catch to handle any exceptions + try { + stopScan() + } catch (ignored: java.lang.Exception) { + } + } + + if (isAutoScanning) { + try { + stopAutoconnectScan() + } catch (ignored: java.lang.Exception) { + } + } + expectingBluetoothOffDisconnects = true // Stop all scans so that we are back in a clean state @@ -890,9 +915,15 @@ class BluetoothCentralManager(private val context: Context) { currentCallback = null currentFilters = null autoConnectScanner = null + bluetoothScanner = null Logger.d(TAG, "bluetooth turning off") } BluetoothAdapter.STATE_ON -> { + // On some phones like Nokia 8, this scanner may still have an older active scan from us + // This happens when bluetooth is toggled. So make sure it is gone. + bluetoothScanner = bluetoothAdapter.bluetoothLeScanner + bluetoothScanner?.stopScan(defaultScanCallback) + expectingBluetoothOffDisconnects = false Logger.d(TAG, "bluetooth turned on") } @@ -903,11 +934,11 @@ class BluetoothCentralManager(private val context: Context) { } } - fun disableLogging(): Unit { + fun disableLogging() { Logger.enabled = false } - fun enableLogging(): Unit { + fun enableLogging() { Logger.enabled = false } @@ -921,20 +952,12 @@ class BluetoothCentralManager(private val context: Context) { private const val NO_VALID_PERIPHERAL_CALLBACK_SPECIFIED = "no valid peripheral callback specified" } - /** - * Construct a new BluetoothCentralManager object - * - * @param context Android application environment. - * @param bluetoothCentralManagerCallback the callback to call for updates - * @param handler Handler to use for callbacks. - */ init { val manager = context.getSystemService(BLUETOOTH_SERVICE) as BluetoothManager bluetoothAdapter = manager.adapter autoConnectScanSettings = getScanSettings(ScanMode.LOW_POWER) scanSettings = getScanSettings(ScanMode.LOW_LATENCY) - // Register for broadcasts on BluetoothAdapter state change val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) context.registerReceiver(adapterStateReceiver, filter) } diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index f7f68d5..3c11710 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -22,6 +22,7 @@ */ package com.welie.blessed +import android.annotation.SuppressLint import android.bluetooth.* import android.bluetooth.BluetoothGattDescriptor.* import android.content.BroadcastReceiver @@ -46,6 +47,8 @@ import kotlin.coroutines.suspendCoroutine * This class is a wrapper around the [BluetoothDevice] and [BluetoothGatt] classes. * It takes care of operation queueing, some Android bugs, and provides several convenience functions. */ +@SuppressLint("MissingPermission") +@Suppress("unused") class BluetoothPeripheral internal constructor( private val context: Context, private var device: BluetoothDevice, @@ -286,10 +289,11 @@ class BluetoothPeripheral internal constructor( } } + @SuppressLint("MissingPermission") private fun discoverServices() { discoverJob = scope.launch { Logger.d(TAG, "discovering services of '%s'", name) - if (bluetoothGatt != null && bluetoothGatt!!.discoverServices()) { + if (bluetoothGatt != null && (bluetoothGatt?.discoverServices() == true)) { discoveryStarted = true } else { Logger.e(TAG, "discoverServices failed to start") @@ -405,6 +409,7 @@ class BluetoothPeripheral internal constructor( } private val pairingRequestBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + @SuppressLint("MissingPermission") override fun onReceive(context: Context, intent: Intent) { val receivedDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) ?: return if (!receivedDevice.address.equals(address, ignoreCase = true)) return @@ -437,9 +442,16 @@ class BluetoothPeripheral internal constructor( registerBondingBroadcastReceivers() state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false - bluetoothGatt = device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) - connectTimestamp = SystemClock.elapsedRealtime() - startConnectionTimer(this@BluetoothPeripheral) + bluetoothGatt = try { + device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) + } catch (e: SecurityException) { + Logger.d(TAG, "exception") + null + } + bluetoothGatt?.let { + connectTimestamp = SystemClock.elapsedRealtime() + startConnectionTimer(this@BluetoothPeripheral) + } } } else { Logger.e(TAG, "peripheral '%s' not yet disconnected, will not connect", name) @@ -459,8 +471,15 @@ class BluetoothPeripheral internal constructor( registerBondingBroadcastReceivers() state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false - bluetoothGatt = device.connectGatt(context, true, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) - connectTimestamp = SystemClock.elapsedRealtime() + bluetoothGatt = try { + device.connectGatt(context, true, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) + } catch (e: SecurityException) { + Logger.d(TAG, "exception") + null + } + bluetoothGatt?.let { + connectTimestamp = SystemClock.elapsedRealtime() + } } } else { Logger.e(TAG, "peripheral '%s' not yet disconnected, will not connect", name) @@ -482,6 +501,7 @@ class BluetoothPeripheral internal constructor( * * @return true if bonding was started/enqueued, false if not */ + fun createBond(): Boolean { // Check if we have a Gatt object if (bluetoothGatt == null) { @@ -787,7 +807,7 @@ class BluetoothPeripheral internal constructor( require(characteristic.supportsReading()) { "characteristic does not have read property" } require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback if (bluetoothGatt!!.readCharacteristic(characteristic)) { @@ -802,16 +822,11 @@ class BluetoothPeripheral internal constructor( resultCallback.onCharacteristicRead(this@BluetoothPeripheral, ByteArray(0), characteristic, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue read characteristic command") } - return result } + suspend fun writeCharacteristic(serviceUUID: UUID, characteristicUUID: UUID, value: ByteArray, writeType: WriteType): ByteArray { require(isConnected) { PERIPHERAL_NOT_CONNECTED } @@ -881,7 +896,7 @@ class BluetoothPeripheral internal constructor( // Copy the value to avoid race conditions val bytesToWrite = copyOf(value) - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback currentWriteBytes = bytesToWrite @@ -908,13 +923,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onCharacteristicWrite(this@BluetoothPeripheral, ByteArray(0), characteristic, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue write characteristic command") } - return result } private fun willCauseLongWrite(value: ByteArray, writeType: WriteType): Boolean { @@ -952,10 +961,10 @@ class BluetoothPeripheral internal constructor( private fun readDescriptor(descriptor: BluetoothGattDescriptor, resultCallback: BluetoothPeripheralCallback): Boolean { require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback - if (bluetoothGatt!!.readDescriptor(descriptor)) { + if (bluetoothGatt?.readDescriptor(descriptor) == true) { Logger.d(TAG, "reading descriptor <%s>", descriptor.uuid) nrTries++ } else { @@ -967,13 +976,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onDescriptorRead(this@BluetoothPeripheral, ByteArray(0), descriptor, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue read descriptor command") } - return result } suspend fun writeDescriptor(descriptor: BluetoothGattDescriptor, value: ByteArray): ByteArray = @@ -1015,12 +1018,12 @@ class BluetoothPeripheral internal constructor( // Copy the value to avoid race conditions val bytesToWrite = copyOf(value) - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback currentWriteBytes = bytesToWrite descriptor.value = bytesToWrite - if (bluetoothGatt!!.writeDescriptor(descriptor)) { + if (bluetoothGatt?.writeDescriptor(descriptor) == true) { Logger.d(TAG, "writing <%s> to descriptor <%s>", BluetoothBytesParser.bytes2String(bytesToWrite), descriptor.uuid) nrTries++ } else { @@ -1032,14 +1035,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onDescriptorWrite(this@BluetoothPeripheral, ByteArray(0), descriptor, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue write descriptor command") } - return result } @@ -1137,37 +1133,28 @@ class BluetoothPeripheral internal constructor( } val finalValue = if (enable) value else DISABLE_NOTIFICATION_VALUE - val result = commandQueue.add(Runnable { + return enqueue { if (notConnected()) { resultCallback.onDescriptorWrite(this@BluetoothPeripheral, ByteArray(0), descriptor, GattStatus.CONNECTION_CANCELLED) completedCommand() - return@Runnable - } - - // First try to set notification for Gatt object - currentResultCallback = resultCallback - if (!bluetoothGatt!!.setCharacteristicNotification(characteristic, enable)) { - Logger.e(TAG, "setCharacteristicNotification failed for characteristic: %s", characteristic.uuid) - completedCommand() - return@Runnable - } - - currentWriteBytes = finalValue - descriptor.value = finalValue - if (bluetoothGatt!!.writeDescriptor(descriptor)) { - nrTries++ } else { - Logger.e(TAG, "writeDescriptor failed for descriptor: %s", descriptor.uuid) - resultCallback.onDescriptorWrite(this@BluetoothPeripheral, ByteArray(0), descriptor, GattStatus.WRITE_NOT_PERMITTED) - completedCommand() + currentResultCallback = resultCallback + if (bluetoothGatt?.setCharacteristicNotification(characteristic, enable) == false) { + Logger.e(TAG, "setCharacteristicNotification failed for characteristic: %s", characteristic.uuid) + completedCommand() + } else { + currentWriteBytes = finalValue + descriptor.value = finalValue + if (bluetoothGatt?.writeDescriptor(descriptor) == true) { + nrTries++ + } else { + Logger.e(TAG, "writeDescriptor failed for descriptor: %s", descriptor.uuid) + resultCallback.onDescriptorWrite(this@BluetoothPeripheral, ByteArray(0), descriptor, GattStatus.WRITE_NOT_PERMITTED) + completedCommand() + } + } } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue setNotify command") } - return result } suspend fun readRemoteRssi(): Int = @@ -1198,10 +1185,10 @@ class BluetoothPeripheral internal constructor( private fun readRemoteRssi(resultCallback: BluetoothPeripheralCallback): Boolean { require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback - if (!bluetoothGatt!!.readRemoteRssi()) { + if (bluetoothGatt?.readRemoteRssi() == false) { Logger.e(TAG, "readRemoteRssi failed") resultCallback.onReadRemoteRssi(this@BluetoothPeripheral, 0, GattStatus.ERROR) completedCommand() @@ -1210,13 +1197,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onReadRemoteRssi(this@BluetoothPeripheral, 0, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue readRemoteRssi command") } - return result } suspend fun requestMtu(mtu: Int): Int = @@ -1260,10 +1241,10 @@ class BluetoothPeripheral internal constructor( require(mtu in DEFAULT_MTU..MAX_MTU) { "mtu must be between 23 and 517" } require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback - if (bluetoothGatt!!.requestMtu(mtu)) { + if (bluetoothGatt?.requestMtu(mtu) == true) { currentCommand = REQUEST_MTU_COMMAND Logger.d(TAG, "requesting MTU of %d", mtu) } else { @@ -1275,13 +1256,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onMtuChanged(this@BluetoothPeripheral, 0, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue requestMtu command") } - return result } @@ -1311,10 +1286,10 @@ class BluetoothPeripheral internal constructor( private fun requestConnectionPriority(priority: ConnectionPriority, resultCallback: BluetoothPeripheralCallback): Boolean { require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback - if (bluetoothGatt!!.requestConnectionPriority(priority.value)) { + if (bluetoothGatt?.requestConnectionPriority(priority.value) == true) { Logger.d(TAG, "requesting connection priority %s", priority) } else { Logger.e(TAG, "could not request connection priority") @@ -1326,14 +1301,7 @@ class BluetoothPeripheral internal constructor( currentResultCallback.onRequestedConnectionPriority(this@BluetoothPeripheral) completedCommand() } - }) - - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue request connection priority command") } - return result } /** @@ -1366,7 +1334,7 @@ class BluetoothPeripheral internal constructor( private fun setPreferredPhy(txPhy: PhyType, rxPhy: PhyType, phyOptions: PhyOptions, resultCallback: BluetoothPeripheralCallback): Boolean { require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { Logger.d(TAG, "setting preferred Phy: tx = %s, rx = %s, options = %s", txPhy, rxPhy, phyOptions) currentResultCallback = resultCallback @@ -1376,14 +1344,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onPhyUpdate(this@BluetoothPeripheral, txPhy, rxPhy, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue setPreferredPhy command") } - return result } /** @@ -1411,7 +1372,7 @@ class BluetoothPeripheral internal constructor( private fun readPhy(resultCallback: BluetoothPeripheralCallback): Boolean { require(isConnected) { PERIPHERAL_NOT_CONNECTED } - val result = commandQueue.add(Runnable { + return enqueue { if (isConnected) { currentResultCallback = resultCallback bluetoothGatt?.readPhy() @@ -1420,14 +1381,7 @@ class BluetoothPeripheral internal constructor( resultCallback.onPhyUpdate(this@BluetoothPeripheral, PhyType.UNKNOWN_PHY_TYPE, PhyType.UNKNOWN_PHY_TYPE, GattStatus.CONNECTION_CANCELLED) completedCommand() } - }) - - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue readyPhy command") } - return result } /** @@ -1439,7 +1393,7 @@ class BluetoothPeripheral internal constructor( if (bluetoothGatt == null) return false var result = false try { - val refreshMethod = bluetoothGatt!!.javaClass.getMethod("refresh") + val refreshMethod = bluetoothGatt?.javaClass?.getMethod("refresh") if (refreshMethod != null) { result = refreshMethod.invoke(bluetoothGatt) as Boolean } @@ -1450,6 +1404,21 @@ class BluetoothPeripheral internal constructor( return result } + /** + * Enqueue a command + * + * Return true if the command was enqueued, otherwise false + */ + private fun enqueue(command: Runnable) : Boolean { + val result = commandQueue.add(command) + if (result) { + nextCommand() + } else { + Logger.e(TAG, "could not enqueue command") + } + return result + } + /** * The current command has been completed, move to the next command in the queue (if any) */ diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt index 5ace389..f823c0c 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt @@ -22,6 +22,7 @@ */ package com.welie.blessed +import android.annotation.SuppressLint import android.bluetooth.* import android.bluetooth.le.AdvertiseCallback import android.bluetooth.le.AdvertiseData @@ -42,6 +43,7 @@ import java.util.concurrent.ConcurrentLinkedQueue /** * This class represent a peripheral running on the local phone */ +@SuppressLint("MissingPermission") class BluetoothPeripheralManager(private val context: Context, private val bluetoothManager: BluetoothManager, private val callback: BluetoothPeripheralManagerCallback) { // private val context: Context private val mainHandler = Handler(Looper.getMainLooper()) diff --git a/build.gradle b/build.gradle index a1674a4..3db15fd 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From dcb0243cd7d926ea7f03dc3557d6827bb352b760 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sat, 29 Jan 2022 09:05:47 +0100 Subject: [PATCH 03/25] Get rid of some !! --- .../src/main/java/com/welie/blessed/BluetoothPeripheral.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index 3c11710..a4d9570 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -576,7 +576,7 @@ class BluetoothPeripheral internal constructor( state = BluetoothProfile.STATE_DISCONNECTING scope.launch { if (state == BluetoothProfile.STATE_DISCONNECTING && bluetoothGatt != null) { - bluetoothGatt!!.disconnect() + bluetoothGatt?.disconnect() } } } else { @@ -810,7 +810,7 @@ class BluetoothPeripheral internal constructor( return enqueue { if (isConnected) { currentResultCallback = resultCallback - if (bluetoothGatt!!.readCharacteristic(characteristic)) { + if (bluetoothGatt?.readCharacteristic(characteristic) == true) { Logger.d(TAG, "reading characteristic <%s>", characteristic.uuid) nrTries++ } else { @@ -911,7 +911,7 @@ class BluetoothPeripheral internal constructor( Logger.w(TAG, "value byte array is longer than allowed by MTU, write will fail if peripheral does not support long writes") } characteristic.value = bytesToWrite - if (bluetoothGatt?.writeCharacteristic(characteristic) ?: false) { + if (bluetoothGatt?.writeCharacteristic(characteristic) == true) { Logger.d(TAG, "writing <%s> to characteristic <%s>", BluetoothBytesParser.bytes2String(bytesToWrite), characteristic.uuid) nrTries++ } else { From 22b296b817b66894be18128ab50051c735007378 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Tue, 15 Feb 2022 17:40:45 +0100 Subject: [PATCH 04/25] Make methods of BluetoothPeripheralManagerCallback open --- .../BluetoothPeripheralManagerCallback.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManagerCallback.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManagerCallback.kt index 5bba66b..b83f630 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManagerCallback.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManagerCallback.kt @@ -40,7 +40,7 @@ abstract class BluetoothPeripheralManagerCallback { * successfully. * @param service The service that has been added */ - fun onServiceAdded(status: GattStatus, service: BluetoothGattService) {} + open fun onServiceAdded(status: GattStatus, service: BluetoothGattService) {} /** * A remote central has requested to read a local characteristic. @@ -53,7 +53,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param bluetoothCentral the central that is doing the request * @param characteristic the characteristic to be read */ - fun onCharacteristicRead(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} + open fun onCharacteristicRead(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} /** * A remote central has requested to write a local characteristic. @@ -71,7 +71,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param value the value the central wants to write * @return GattStatus.SUCCESS if the value is acceptable, otherwise an appropriate status */ - fun onCharacteristicWrite(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic, value: ByteArray): GattStatus { + open fun onCharacteristicWrite(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic, value: ByteArray): GattStatus { return GattStatus.SUCCESS } @@ -86,7 +86,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param bluetoothCentral the central that is doing the request * @param descriptor the descriptor to be read */ - fun onDescriptorRead(bluetoothCentral: BluetoothCentral, descriptor: BluetoothGattDescriptor) {} + open fun onDescriptorRead(bluetoothCentral: BluetoothCentral, descriptor: BluetoothGattDescriptor) {} /** * A remote central has requested to write a local descriptor. @@ -104,7 +104,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param value the value the central wants to write * @return GattStatus.SUCCESS if the value is acceptable, otherwise an appropriate status */ - fun onDescriptorWrite(bluetoothCentral: BluetoothCentral, descriptor: BluetoothGattDescriptor, value: ByteArray): GattStatus { + open fun onDescriptorWrite(bluetoothCentral: BluetoothCentral, descriptor: BluetoothGattDescriptor, value: ByteArray): GattStatus { return GattStatus.SUCCESS } @@ -114,7 +114,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param bluetoothCentral the central * @param characteristic the characteristic */ - fun onNotifyingEnabled(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} + open fun onNotifyingEnabled(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} /** * A remote central has disabled notifications or indications for a characteristic @@ -122,7 +122,7 @@ abstract class BluetoothPeripheralManagerCallback { * @param bluetoothCentral the central * @param characteristic the characteristic */ - fun onNotifyingDisabled(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} + open fun onNotifyingDisabled(bluetoothCentral: BluetoothCentral, characteristic: BluetoothGattCharacteristic) {} /** * A notification has been sent to a central @@ -132,39 +132,39 @@ abstract class BluetoothPeripheralManagerCallback { * @param characteristic the characteristic for which the notification was sent * @param status the status of the operation */ - fun onNotificationSent(bluetoothCentral: BluetoothCentral, value: ByteArray, characteristic: BluetoothGattCharacteristic, status: GattStatus) {} + open fun onNotificationSent(bluetoothCentral: BluetoothCentral, value: ByteArray, characteristic: BluetoothGattCharacteristic, status: GattStatus) {} /** * A remote central has connected * * @param bluetoothCentral the central */ - fun onCentralConnected(bluetoothCentral: BluetoothCentral) {} + open fun onCentralConnected(bluetoothCentral: BluetoothCentral) {} /** * A remote central has disconnected * * @param bluetoothCentral the central */ - fun onCentralDisconnected(bluetoothCentral: BluetoothCentral) {} + open fun onCentralDisconnected(bluetoothCentral: BluetoothCentral) {} /** * Advertising has successfully started * * @param settingsInEffect the AdvertiseSettings that are currently active */ - fun onAdvertisingStarted(settingsInEffect: AdvertiseSettings) {} + open fun onAdvertisingStarted(settingsInEffect: AdvertiseSettings) {} /** * Advertising has failed * * @param advertiseError the error explaining why the advertising failed */ - fun onAdvertiseFailure(advertiseError: AdvertiseError) {} + open fun onAdvertiseFailure(advertiseError: AdvertiseError) {} /** * Advertising has stopped * */ - fun onAdvertisingStopped() {} + open fun onAdvertisingStopped() {} } \ No newline at end of file From 282a0961ee25cec0cf2f4520a084e30b64a5b9d3 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Tue, 15 Feb 2022 17:41:46 +0100 Subject: [PATCH 05/25] Make more methods of BluetoothPeripheralCallback open --- .../com/welie/blessed/BluetoothPeripheralCallback.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralCallback.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralCallback.kt index 0268337..914570a 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralCallback.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralCallback.kt @@ -105,28 +105,28 @@ internal abstract class BluetoothPeripheralCallback { * * @param peripheral the peripheral */ - fun onBondingStarted(peripheral: BluetoothPeripheral) {} + open fun onBondingStarted(peripheral: BluetoothPeripheral) {} /** * Callback invoked when the bonding process has succeeded * * @param peripheral the peripheral */ - fun onBondingSucceeded(peripheral: BluetoothPeripheral) {} + open fun onBondingSucceeded(peripheral: BluetoothPeripheral) {} /** * Callback invoked when the bonding process has failed * * @param peripheral the peripheral */ - fun onBondingFailed(peripheral: BluetoothPeripheral) {} + open fun onBondingFailed(peripheral: BluetoothPeripheral) {} /** * Callback invoked when a bond has been lost and the peripheral is not bonded anymore. * * @param peripheral the peripheral */ - fun onBondLost(peripheral: BluetoothPeripheral) {} + open fun onBondLost(peripheral: BluetoothPeripheral) {} /** * Callback invoked as the result of a read RSSI operation @@ -178,7 +178,7 @@ internal abstract class BluetoothPeripheralCallback { * Valid range is from 10 (0.1s) to 3200 (32s). * @param status GATT status code */ - fun onConnectionUpdated(peripheral: BluetoothPeripheral, interval: Int, latency: Int, timeout: Int, status: GattStatus) {} + open fun onConnectionUpdated(peripheral: BluetoothPeripheral, interval: Int, latency: Int, timeout: Int, status: GattStatus) {} /** * NULL class to deal with nullability From 94102f9d5db07fbe59687bfd7766efccc4b63abb Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Tue, 15 Feb 2022 17:55:40 +0100 Subject: [PATCH 06/25] Update dependencies --- app/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 43c16c9..e71c48f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,13 +31,13 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'com.jakewharton.timber:timber:5.0.1' implementation "androidx.core:core-ktx:1.7.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" testImplementation 'junit:junit:4.13.2' From d75eb809e06c537d5b75a0e87a7c8107ffcff45a Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Tue, 15 Feb 2022 17:57:48 +0100 Subject: [PATCH 07/25] Update Kotlin and Gradle versions --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3db15fd..bb56b92 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.31' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.0' + classpath 'com.android.tools.build:gradle:7.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 66d1aebcdb1f1ecf46f4e98f7b3fe8201aa982ea Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sun, 27 Feb 2022 21:03:58 +0100 Subject: [PATCH 08/25] Added some BluetoothBytesParser tests --- app/build.gradle | 1 - blessed/build.gradle | 1 + .../com/welie/blessed/BluetoothBytesParser.kt | 16 ++++----- .../welie/blessed/BluetoothBytesParserTest.kt | 35 +++++++++++++++++++ 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt diff --git a/app/build.gradle b/app/build.gradle index e71c48f..9cd3df9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" - testImplementation 'junit:junit:4.13.2' implementation project(':blessed') } \ No newline at end of file diff --git a/blessed/build.gradle b/blessed/build.gradle index bed313d..eac56b4 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation "org.robolectric:robolectric:4.5.1" testImplementation "org.mockito:mockito-core:3.8.0" testImplementation 'androidx.test:core:1.4.0' + testImplementation "io.mockk:mockk:1.12.2" } afterEvaluate { diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt index cc23258..a4e2882 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt @@ -26,6 +26,7 @@ import java.nio.ByteOrder import java.nio.ByteOrder.LITTLE_ENDIAN import java.nio.charset.StandardCharsets import java.util.* +import kotlin.math.pow class BluetoothBytesParser ( var value: ByteArray, @@ -498,6 +499,7 @@ class BluetoothBytesParser ( } else -> return false } + this.offset += getTypeLen(formatType) return true } @@ -509,7 +511,7 @@ class BluetoothBytesParser ( * @return true if the locally stored value has been set */ fun setFloatValue(value: Float, precision: Int): Boolean { - val mantissa = (value * Math.pow(10.0, precision.toDouble())).toFloat() + val mantissa = (value * 10.toDouble().pow(precision.toDouble())).toFloat() return setFloatValue(mantissa.toInt(), -precision, FORMAT_FLOAT, offset) } @@ -611,16 +613,14 @@ class BluetoothBytesParser ( * Convert signed bytes to a 16-bit short float value. */ private fun bytesToFloat(b0: Byte, b1: Byte): Float { - val mantissa = unsignedToSigned( - unsignedByteToInt(b0) - + (unsignedByteToInt(b1) and 0x0F shl 8), 12 - ) + val mantissa = unsignedToSigned(unsignedByteToInt(b0) + + (unsignedByteToInt(b1) and 0x0F shl 8), 12) val exponent = unsignedToSigned(unsignedByteToInt(b1) shr 4, 4) - return (mantissa * Math.pow(10.0, exponent.toDouble())).toFloat() + return (mantissa * 10.toDouble().pow( exponent.toDouble())).toFloat() } /** - * Convert signed bytes to a 32-bit short float value. + * Convert signed bytes to a 32-bit float value. */ private fun bytesToFloat(b0: Byte, b1: Byte, b2: Byte, b3: Byte): Float { val mantissa = unsignedToSigned( @@ -628,7 +628,7 @@ class BluetoothBytesParser ( + (unsignedByteToInt(b1) shl 8) + (unsignedByteToInt(b2) shl 16), 24 ) - return (mantissa * Math.pow(10.0, b3.toDouble())).toFloat() + return (mantissa * 10.toDouble().pow(b3.toDouble())).toFloat() } /** diff --git a/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt b/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt new file mode 100644 index 0000000..46207ec --- /dev/null +++ b/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt @@ -0,0 +1,35 @@ +package com.welie.blessed + +import android.content.Context +import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_FLOAT +import io.mockk.* +import org.junit.* +import org.junit.Assert.* +import java.nio.ByteOrder + + +class BluetoothBytesParserTest { + private val mockContext = mockk() + + @Test + fun first_test() { + val byteParser = BluetoothBytesParser(byteArrayOf(0xFF.toByte(), 0x00, 0x01, 0x6c), ByteOrder.BIG_ENDIAN) + assertEquals(byteParser.getFloatValue(FORMAT_FLOAT), 36.4f) + } + + @Test + fun second_test() { + var parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) + parser.setFloatValue(364, -1, FORMAT_FLOAT, 0) + parser.offset = 0 + assertEquals(36.4f, parser.getFloatValue(FORMAT_FLOAT)) + + parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) + parser.setFloatValue(5.3f, 1) + parser.setFloatValue(36.86f, 2) + + parser.offset = 0 + assertEquals(5.3f, parser.getFloatValue(FORMAT_FLOAT)) + assertEquals(36.86f, parser.getFloatValue(FORMAT_FLOAT)) + } +} \ No newline at end of file From 6217f830504ac205c4120459169a47857a5e0ab6 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sun, 27 Feb 2022 21:38:27 +0100 Subject: [PATCH 09/25] Removed example unit test --- .../com/welie/blessedexample/ExampleUnitTest.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/src/test/java/com/welie/blessedexample/ExampleUnitTest.kt diff --git a/app/src/test/java/com/welie/blessedexample/ExampleUnitTest.kt b/app/src/test/java/com/welie/blessedexample/ExampleUnitTest.kt deleted file mode 100644 index 6447c9b..0000000 --- a/app/src/test/java/com/welie/blessedexample/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.welie.blessedexample - -import org.junit.Assert -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see [Testing documentation](http://d.android.com/tools/testing) - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - Assert.assertEquals(4, (2 + 2).toLong()) - } -} \ No newline at end of file From 327aadb4df2bf6309a4da6b6fe5016fdd1f265f4 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Thu, 3 Mar 2022 11:15:17 +0100 Subject: [PATCH 10/25] Removed example unit test --- .../welie/blessedexample/BluetoothHandler.kt | 16 ++++--- .../welie/blessed/BluetoothCentralManager.kt | 44 +++++++++++++------ .../BluetoothCentralManagerCallback.kt | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index 0bc7e83..c2b4132 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -209,11 +209,13 @@ internal class BluetoothHandler private constructor(context: Context) { } private fun startScanning() { - central.scanForPeripheralsWithServices(supportedServices) { peripheral, scanResult -> - Timber.i("Found peripheral '${peripheral.name}' with RSSI ${scanResult.rssi}") - central.stopScan() - connectPeripheral(peripheral) - } + central.scanForPeripheralsWithServices(supportedServices, + { peripheral, scanResult -> + Timber.i("Found peripheral '${peripheral.name}' with RSSI ${scanResult.rssi}") + central.stopScan() + connectPeripheral(peripheral) + }, + { scanFailure -> Timber.e("scan failed with reason $scanFailure") }) } private fun connectPeripheral(peripheral: BluetoothPeripheral) { @@ -233,7 +235,7 @@ internal class BluetoothHandler private constructor(context: Context) { companion object { // UUIDs for the Blood Pressure service (BLP) private val BLP_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") - private val BLP_MEASUREMENT_CHARACTERISTIC_UUID : UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") + private val BLP_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") // UUIDs for the Health Thermometer service (HTS) private val HTS_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") @@ -309,7 +311,7 @@ internal class BluetoothHandler private constructor(context: Context) { } central.observeAdapterState { state -> - when(state) { + when (state) { BluetoothAdapter.STATE_ON -> startScanning() } } diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index ae76dca..f207d18 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -77,6 +77,7 @@ class BluetoothCentralManager(private val context: Context) { private var disconnectRunnable: Runnable? = null private val pinCodes: MutableMap = ConcurrentHashMap() private var currentResultCallback : ((BluetoothPeripheral, ScanResult) -> Unit)? = null + private var currentScanErrorCallback : ((ScanFailure) -> Unit)? = null private var adapterStateCallback: (state: Int) -> Unit = {} private val scanByNameCallback: ScanCallback = object : ScanCallback() { @@ -119,9 +120,10 @@ class BluetoothCentralManager(private val context: Context) { private fun sendScanFailed(scanFailure: ScanFailure) { currentCallback = null currentFilters = null + cancelTimeoutTimer() scope.launch { Logger.e(TAG, "scan failed with error code %d (%s)", scanFailure.value, scanFailure) - // bluetoothCentralManagerCallback.onScanFailed(scanFailure) + currentScanErrorCallback?.invoke(scanFailure) } } @@ -247,10 +249,6 @@ class BluetoothCentralManager(private val context: Context) { private fun startScan(filters: List, scanSettings: ScanSettings, scanCallback: ScanCallback) { if (bleNotReady()) return - if (isScanning) { - Logger.e(TAG, "other scan still active, stopping scan") - stopScan() - } if (bluetoothScanner == null) { bluetoothScanner = bluetoothAdapter.bluetoothLeScanner } @@ -271,9 +269,11 @@ class BluetoothCentralManager(private val context: Context) { * @param serviceUUIDs an array of service UUIDs * @throws IllegalArgumentException if the array of service UUIDs is empty */ - fun scanForPeripheralsWithServices(serviceUUIDs: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit ) { + fun scanForPeripheralsWithServices(serviceUUIDs: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) { require(serviceUUIDs.isNotEmpty()) { "at least one service UUID must be supplied" } + if (isScanning) stopScan() + val filters: MutableList = ArrayList() for (serviceUUID in serviceUUIDs) { val filter = ScanFilter.Builder() @@ -283,6 +283,7 @@ class BluetoothCentralManager(private val context: Context) { } currentResultCallback = resultCallback + currentScanErrorCallback = scanError startScan(filters, scanSettings, defaultScanCallback) } @@ -295,9 +296,13 @@ class BluetoothCentralManager(private val context: Context) { * @param peripheralNames array of partial peripheral names * @throws IllegalArgumentException if the array of peripheral names is empty */ - fun scanForPeripheralsWithNames(peripheralNames: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit ) { + fun scanForPeripheralsWithNames(peripheralNames: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) { require(peripheralNames.isNotEmpty()) { "at least one peripheral name must be supplied" } + + if (isScanning) stopScan() + currentResultCallback = resultCallback + currentScanErrorCallback = scanError // Start the scanner with no filter because we'll do the filtering ourselves scanPeripheralNames = peripheralNames @@ -310,9 +315,13 @@ class BluetoothCentralManager(private val context: Context) { * @param peripheralAddresses array of peripheral mac addresses to scan for * @throws IllegalArgumentException if the array of addresses is empty */ - fun scanForPeripheralsWithAddresses(peripheralAddresses: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit ) { + fun scanForPeripheralsWithAddresses(peripheralAddresses: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) { require(peripheralAddresses.isNotEmpty()) { "at least one peripheral address must be supplied" } + + if (isScanning) stopScan() + currentResultCallback = resultCallback + currentScanErrorCallback = scanError val filters: MutableList = ArrayList() for (address in peripheralAddresses) { @@ -334,17 +343,24 @@ class BluetoothCentralManager(private val context: Context) { * @param filters A list of ScanFilters * @throws IllegalArgumentException if the list of filters is empty */ - fun scanForPeripheralsUsingFilters(filters: List,resultCallback: (BluetoothPeripheral, ScanResult) -> Unit ) { + fun scanForPeripheralsUsingFilters(filters: List,resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) { require(filters.isNotEmpty()) { "at least one scan filter must be supplied" } + + if (isScanning) stopScan() + currentResultCallback = resultCallback + currentScanErrorCallback = scanError startScan(filters, scanSettings, defaultScanCallback) } /** * Scan for any peripheral that is advertising. */ - fun scanForPeripherals(resultCallback: (BluetoothPeripheral, ScanResult) -> Unit ) { + fun scanForPeripherals(resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) { + if (isScanning) stopScan() + currentResultCallback = resultCallback + currentScanErrorCallback = scanError startScan(emptyList(), scanSettings, defaultScanCallback) } @@ -397,7 +413,7 @@ class BluetoothCentralManager(private val context: Context) { Logger.i(TAG, "scan stopped") } } else { - Logger.i(TAG, "no scan to stop because no scan is running") + Logger.d(TAG, "no scan to stop because no scan is running") } currentCallback = null currentFilters = null @@ -455,7 +471,7 @@ class BluetoothCentralManager(private val context: Context) { scannedPeripherals.remove(peripheral.address) unconnectedPeripherals[peripheral.address] = peripheral - currentCentralManagerCallback = resultCentralManagerCallback +// currentCentralManagerCallback = resultCentralManagerCallback peripheral.connect() } } @@ -524,7 +540,7 @@ class BluetoothCentralManager(private val context: Context) { }) } - fun cancelConnection(peripheral: BluetoothPeripheral, resultCentralManagerCallback: BluetoothCentralManagerCallback) { + private fun cancelConnection(peripheral: BluetoothPeripheral, resultCentralManagerCallback: BluetoothCentralManagerCallback) { // First check if we are doing a reconnection scan for this peripheral val peripheralAddress = peripheral.address if (reconnectPeripheralAddresses.contains(peripheralAddress)) { @@ -543,7 +559,7 @@ class BluetoothCentralManager(private val context: Context) { // Only cancel connections if it is an known peripheral if (unconnectedPeripherals.containsKey(peripheralAddress) || connectedPeripherals.containsKey(peripheralAddress)) { - currentCentralManagerCallback = resultCentralManagerCallback +// currentCentralManagerCallback = resultCentralManagerCallback peripheral.cancelConnection() } else { Logger.e(TAG, "cannot cancel connection to unknown peripheral %s", peripheralAddress) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManagerCallback.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManagerCallback.kt index c4d2f0c..b8b8f25 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManagerCallback.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManagerCallback.kt @@ -27,7 +27,7 @@ import android.bluetooth.le.ScanResult /** * Callbacks for the BluetoothCentralManager class */ -abstract class BluetoothCentralManagerCallback { +internal abstract class BluetoothCentralManagerCallback { /** * Successfully connected with a peripheral. * From 58f32c9f974c94758ea185f5e6009377766a26e3 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Thu, 3 Mar 2022 11:18:26 +0100 Subject: [PATCH 11/25] Re-enable currentCentralManagerCallback --- .../main/java/com/welie/blessed/BluetoothCentralManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index f207d18..2c3f124 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -471,7 +471,7 @@ class BluetoothCentralManager(private val context: Context) { scannedPeripherals.remove(peripheral.address) unconnectedPeripherals[peripheral.address] = peripheral -// currentCentralManagerCallback = resultCentralManagerCallback + currentCentralManagerCallback = resultCentralManagerCallback peripheral.connect() } } @@ -559,7 +559,7 @@ class BluetoothCentralManager(private val context: Context) { // Only cancel connections if it is an known peripheral if (unconnectedPeripherals.containsKey(peripheralAddress) || connectedPeripherals.containsKey(peripheralAddress)) { -// currentCentralManagerCallback = resultCentralManagerCallback + currentCentralManagerCallback = resultCentralManagerCallback peripheral.cancelConnection() } else { Logger.e(TAG, "cannot cancel connection to unknown peripheral %s", peripheralAddress) From b382ccc4f440d6c6b5b1e4e94de4474b38841598 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Thu, 3 Mar 2022 15:30:56 +0100 Subject: [PATCH 12/25] Update README.md --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fe4c52d..51588bd 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ The `BluetoothBytesParser` class is a utility class that makes parsing byte arra The `BluetoothCentralManager` class has several differrent scanning methods: ```kotlin -fun scanForPeripherals() -fun scanForPeripheralsWithServices(serviceUUIDs: Array) -fun scanForPeripheralsWithNames(peripheralNames: Array) -fun scanForPeripheralsWithAddresses(peripheralAddresses: Array) -fun scanForPeripheralsUsingFilters(filters: List) +fun scanForPeripherals(resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit ) +fun scanForPeripheralsWithServices(serviceUUIDs: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) +fun scanForPeripheralsWithNames(peripheralNames: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) +fun scanForPeripheralsWithAddresses(peripheralAddresses: Array, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) +fun scanForPeripheralsUsingFilters(filters: List,resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) ``` They all work in the same way and take an array of either service UUIDs, peripheral names or mac addresses. When a peripheral is found your callback lambda will be called with the `BluetoothPeripheral` object and a `ScanResult` object that contains the scan details. The method `scanForPeripheralsUsingFilters` is for scanning using your own list of filters. See [Android documentation](https://developer.android.com/reference/android/bluetooth/le/ScanFilter) for more info on the use of `ScanFilter`. @@ -60,11 +60,13 @@ So in order to setup a scan for a device with the Bloodpressure service or Heart val BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") val HRS_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") -central.scanForPeripheralsWithServices(arrayOf(BLP_SERVICE_UUID, HRS_SERVICE_UUID)) { peripheral, scanResult -> - Timber.i("Found peripheral '${peripheral.name}' with RSSI ${scanResult.rssi}") - central.stopScan() - connectPeripheral(peripheral) -} +central.scanForPeripheralsWithServices(arrayOf(BLP_SERVICE_UUID, HRS_SERVICE_UUID) + { peripheral, scanResult -> + Timber.i("Found peripheral '${peripheral.name}' with RSSI ${scanResult.rssi}") + central.stopScan() + connectPeripheral(peripheral) + }, + { scanFailure -> Timber.e("scan failed with reason $scanFailure") }) ``` The scanning functions are not suspending functions and simply use a lambda function to receive the results. From 51fe74eb95076d554bee1d96c5d7d6ca6ec9c037 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Thu, 3 Mar 2022 15:31:41 +0100 Subject: [PATCH 13/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51588bd..33f7376 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ fun scanForPeripheralsWithAddresses(peripheralAddresses: Array, resultCa fun scanForPeripheralsUsingFilters(filters: List,resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) ``` -They all work in the same way and take an array of either service UUIDs, peripheral names or mac addresses. When a peripheral is found your callback lambda will be called with the `BluetoothPeripheral` object and a `ScanResult` object that contains the scan details. The method `scanForPeripheralsUsingFilters` is for scanning using your own list of filters. See [Android documentation](https://developer.android.com/reference/android/bluetooth/le/ScanFilter) for more info on the use of `ScanFilter`. +They all work in the same way and take an array of either service UUIDs, peripheral names or mac addresses. When a peripheral is found your callback lambda will be called with the `BluetoothPeripheral` object and a `ScanResult` object that contains the scan details. The method `scanForPeripheralsUsingFilters` is for scanning using your own list of filters. See [Android documentation](https://developer.android.com/reference/android/bluetooth/le/ScanFilter) for more info on the use of `ScanFilter`. A second lambda is used to deliver any scan failures. So in order to setup a scan for a device with the Bloodpressure service or HeartRate service, you do: From 9c3bcff4761051962ab9d4e404d69b515b3ac3ad Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Fri, 15 Apr 2022 09:23:26 +0200 Subject: [PATCH 14/25] Fix array out of bounds crash --- .../src/main/java/com/welie/blessed/BluetoothBytesParser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt index a4e2882..d729f4d 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt @@ -554,7 +554,7 @@ class BluetoothBytesParser ( * @return flase if the calendar object was null, otherwise true */ fun setCurrentTime(calendar: Calendar): Boolean { - value = ByteArray(10) + prepareArray(10) setDateTime(calendar) value[7] = ((calendar[Calendar.DAY_OF_WEEK] + 5) % 7 + 1).toByte() value[8] = (calendar[Calendar.MILLISECOND] * 256 / 1000).toByte() @@ -569,7 +569,7 @@ class BluetoothBytesParser ( * @return flase if the calendar object was null, otherwise true */ fun setDateTime(calendar: Calendar): Boolean { - value = ByteArray(7) + prepareArray(7) value[0] = calendar[Calendar.YEAR].toByte() value[1] = (calendar[Calendar.YEAR] shr 8).toByte() value[2] = (calendar[Calendar.MONTH] + 1).toByte() From bf12a2f5e47fa8055212c92b424c3bb012fc67d6 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sat, 16 Apr 2022 09:18:29 +0200 Subject: [PATCH 15/25] Fix Omron check to be case-insensitive --- .../main/java/com/welie/blessedexample/BluetoothHandler.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index c2b4132..97cc21d 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -49,7 +49,7 @@ internal class BluetoothHandler private constructor(context: Context) { // If it has the write property we write the current time if (it.supportsWritingWithResponse()) { // Write the current time unless it is an Omron device - if (!peripheral.name.contains("BLEsmart_")) { + if (!peripheral.name.contains("BLEsmart_", true)) { val parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) parser.setCurrentTime(Calendar.getInstance()) peripheral.writeCharacteristic(it, parser.value, WriteType.WITH_RESPONSE) @@ -76,7 +76,6 @@ internal class BluetoothHandler private constructor(context: Context) { } } - private suspend fun writeContourClock(peripheral: BluetoothPeripheral) { val calendar = Calendar.getInstance() val offsetInMinutes = calendar.timeZone.rawOffset / 60000 @@ -102,7 +101,7 @@ internal class BluetoothHandler private constructor(context: Context) { // Deal with Omron devices where we can only write currentTime under specific conditions val name = peripheral.name - if (name.contains("BLEsmart_")) { + if (name.contains("BLEsmart_", true)) { peripheral.getCharacteristic(BLP_SERVICE_UUID, BLP_MEASUREMENT_CHARACTERISTIC_UUID)?.let { val isNotifying = peripheral.isNotifying(it) if (isNotifying) currentTimeCounter++ From 2874edc1c721bd2f0062abdf46781772b24ae112 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sat, 16 Apr 2022 09:20:34 +0200 Subject: [PATCH 16/25] Fix comment --- app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index 97cc21d..6119932 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -44,7 +44,7 @@ internal class BluetoothHandler private constructor(context: Context) { val batteryLevel = peripheral.readCharacteristic(BTS_SERVICE_UUID, BATTERY_LEVEL_CHARACTERISTIC_UUID).asUInt8() Timber.i("Battery level: $batteryLevel") - // Turn on notifications for Current Time Service and write it if possible + // Write Current Time if possible peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let { // If it has the write property we write the current time if (it.supportsWritingWithResponse()) { From 42995823295dea3c36d347627e3cf33c16616f53 Mon Sep 17 00:00:00 2001 From: Scott Peterson Date: Thu, 9 Jun 2022 15:28:41 -0400 Subject: [PATCH 17/25] Fix nitpicky grammar and spelling --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 33f7376..42a0c4b 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,15 @@ where `$version` is the latest published version in Jitpack [![](https://jitpack The library consists of 5 core classes and corresponding callback abstract classes: 1. `BluetoothCentralManager`, for scanning and connecting peripherals 2. `BluetoothPeripheral`, for all peripheral related methods -3. `BluetoothPeripheralManager`, and it's companion abstract class `BluetoothPeripheralManagerCallback` +3. `BluetoothPeripheralManager`, and its companion abstract class `BluetoothPeripheralManagerCallback` 4. `BluetoothCentral` 5. `BluetoothBytesParser` The `BluetoothCentralManager` class is used to scan for devices and manage connections. The `BluetoothPeripheral` class is a replacement for the standard Android `BluetoothDevice` and `BluetoothGatt` classes. It wraps all GATT related peripheral functionality. -The `BluetoothPeripheralManager` class is used to create your own peripheral running on an Android phone. You can add service, control advertising and deal with requests from remote centrals, represented by the `BluetoothCentral` class. For more about creating your own peripherals see the separate guide: [creating your own peripheral](SERVER.md) +The `BluetoothPeripheralManager` class is used to create your own peripheral running on an Android phone. You can add service, control advertising, and deal with requests from remote centrals, represented by the `BluetoothCentral` class. For more about creating your own peripherals see the separate guide: [creating your own peripheral](SERVER.md) -The `BluetoothBytesParser` class is a utility class that makes parsing byte arrays easy. You can also use it construct your own byte arrays by adding integers, floats or strings. +The `BluetoothBytesParser` class is a utility class that makes parsing byte arrays easy. You can also use it to construct your own byte arrays by adding integers, floats, or strings. ## Scanning @@ -51,7 +51,7 @@ fun scanForPeripheralsWithAddresses(peripheralAddresses: Array, resultCa fun scanForPeripheralsUsingFilters(filters: List,resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit) ``` -They all work in the same way and take an array of either service UUIDs, peripheral names or mac addresses. When a peripheral is found your callback lambda will be called with the `BluetoothPeripheral` object and a `ScanResult` object that contains the scan details. The method `scanForPeripheralsUsingFilters` is for scanning using your own list of filters. See [Android documentation](https://developer.android.com/reference/android/bluetooth/le/ScanFilter) for more info on the use of `ScanFilter`. A second lambda is used to deliver any scan failures. +They all work in the same way and take an array of either service UUIDs, peripheral names, or mac addresses. When a peripheral is found your callback lambda will be called with the `BluetoothPeripheral` object and a `ScanResult` object that contains the scan details. The method `scanForPeripheralsUsingFilters` is for scanning using your own list of filters. See [Android documentation](https://developer.android.com/reference/android/bluetooth/le/ScanFilter) for more info on the use of `ScanFilter`. A second lambda is used to deliver any scan failures. So in order to setup a scan for a device with the Bloodpressure service or HeartRate service, you do: @@ -84,7 +84,7 @@ fun autoConnectPeripheral(peripheral: BluetoothPeripheral) fun autoConnectPeripheralsBatch(batch: Set) ``` -The method `connectPeripheral` is a **suspending function** will try to immediately connect to a device that has already been found using a scan. This method will time out after 30 seconds or less, depending on the device manufacturer, and a `ConnectionFailedException` will be thrown. Note that there can be **only 1 outstanding** `connectPeripheral`. So if it is called multiple times only 1 will succeed. +The method `connectPeripheral` is a **suspending function** that will try to immediately connect to a device that has already been found using a scan. This method will time out after 30 seconds or less, depending on the device manufacturer, and a `ConnectionFailedException` will be thrown. Note that there can be **only 1 outstanding** `connectPeripheral`. So if it is called multiple times only 1 will succeed. ```kotlin scope.launch { @@ -98,7 +98,7 @@ scope.launch { The method `autoConnectPeripheral` will **not suspend** and is for re-connecting to known devices for which you already know the device's mac address. The BLE stack will automatically connect to the device when it sees it in its internal scan. Therefore, it may take longer to connect to a device but this call will never time out! So you can issue the autoConnect command and the device will be connected whenever it is found. This call will **also work** when the device is not cached by the Android stack, as BLESSED takes care of it! In contrary to `connectPeripheral`, there can be multiple outstanding `autoConnectPeripheral` requests. -The method `autoConnectPeripheralsBatch` is for re-connecting to multiple peripherals in one go. Since the normal `autoConnectPeripheral` may involve scanning, if peripherals are uncached, it is not suitable for calling very fast after each other, since it may trigger scanner limitations of Android. So use `autoConnectPeripheralsBatch` if the want to re-connect to many known peripherals. +The method `autoConnectPeripheralsBatch` is for re-connecting to multiple peripherals in one go. Since the normal `autoConnectPeripheral` may involve scanning, if peripherals are uncached, it is not suitable for calling very fast after each other, since it may trigger scanner limitations of Android. So use `autoConnectPeripheralsBatch` if you want to re-connect to many known peripherals. If you know the mac address of your peripheral you can obtain a `BluetoothPeripheral` object using: ```kotlin @@ -116,7 +116,7 @@ To disconnect or to cancel an outstanding `connectPeripheral()` or `autoConnectP ```kotlin suspend fun cancelConnection(peripheral: BluetoothPeripheral): Unit ``` -The function will suspend untill the peripheral is disconnected. +The function will suspend until the peripheral is disconnected. ## Service discovery @@ -140,7 +140,7 @@ suspend fun readDescriptor(descriptor: BluetoothGattDescriptor): ByteArray suspend fun writeDescriptor(descriptor: BluetoothGattDescriptor, value: ByteArray): ByteArray ``` -All methods are **suspending** and will return the result of the operation. The method `readCharacteristic` will return the ByteArray that has been read. It will throw `IllegalArgumentException` if the characteristic you provide is not readable, and it will throw `GattException` if the read was not succesful. +All methods are **suspending** and will return the result of the operation. The method `readCharacteristic` will return the ByteArray that has been read. It will throw `IllegalArgumentException` if the characteristic you provide is not readable, and it will throw `GattException` if the read was not successful. If you want to write to a characteristic, you need to provide a `value` and a `writeType`. The `writeType` is usually `WITH_RESPONSE` or `WITHOUT_RESPONSE`. If the write type you specify is not supported by the characteristic it will throw `IllegalArgumentException`. The method will return the bytes that were written or an empty byte array in case something went wrong. @@ -159,7 +159,7 @@ val model = peripheral.readCharacteristic(DIS_SERVICE_UUID, MODEL_NUMBER_CHARACT Timber.i("Received: $model") ``` -Note that there are also some extension like `asString()` and `asUInt8()` to quickly turn byte arrays in Strings or UInt8s. +Note that there are also some extension methods like `asString()` and `asUInt8()` to quickly turn byte arrays in Strings or UInt8s. ## Turning notifications on/off @@ -184,7 +184,7 @@ peripheral.observeBondState { Timber.i("Bond state is $it") } ``` -In most cases, the peripheral will initiate bonding either at the time of connection, or when trying to read/write protected characteristics. However, if you want you can also initiate bonding yourself by calling `createBond` on a peripheral. There are two ways to do this: +In most cases, the peripheral will initiate bonding either at the time of connection or when trying to read/write protected characteristics. However, if you want you can also initiate bonding yourself by calling `createBond` on a peripheral. There are two ways to do this: * Calling `createBond` when not yet connected to a peripheral. In this case, a connection is made and bonding is requested. * Calling `createBond` when already connected to a peripheral. In this case, only the bond is created. @@ -245,7 +245,7 @@ It will return the current Phy ## Example application -An example application is provided in the repo. It shows how to connect to Blood Pressure meters, Heart Rate monitors, Weight scales, Glucose Meters, Pulse Oximeters and Thermometers, read the data and show it on screen. It only works with peripherals that use the Bluetooth SIG services. Working peripherals include: +An example application is provided in the repo. It shows how to connect to Blood Pressure meters, Heart Rate monitors, Weight scales, Glucose Meters, Pulse Oximeters, and Thermometers, read the data, and show it on screen. It only works with peripherals that use the Bluetooth SIG services. Working peripherals include: * Beurer FT95 thermometer * GRX Thermometer (TD-1241) From 55b8f048ebb0a13b11faea5cf6f7e37085761fb6 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Tue, 28 Jun 2022 10:40:31 +0200 Subject: [PATCH 18/25] Immediately try to disconnect devices when bluetooth is turned off --- .../welie/blessedexample/BluetoothHandler.kt | 6 ++- .../welie/blessed/BluetoothCentralManager.kt | 39 ++----------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index 6119932..c366822 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -302,7 +302,11 @@ internal class BluetoothHandler private constructor(context: Context) { ConnectionState.CONNECTED -> handlePeripheral(peripheral) ConnectionState.DISCONNECTED -> scope.launch { delay(15000) - central.autoConnectPeripheral(peripheral) + + // Check if this peripheral should still be auto connected + if (central.getPeripheral(peripheral.address).getState() == ConnectionState.DISCONNECTED) { + central.autoConnectPeripheral(peripheral) + } } else -> { } diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index 2c3f124..c52d293 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -73,7 +73,6 @@ class BluetoothCentralManager(private val context: Context) { private var scanSettings: ScanSettings private val autoConnectScanSettings: ScanSettings private val connectionRetries: MutableMap = ConcurrentHashMap() - private var expectingBluetoothOffDisconnects = false private var disconnectRunnable: Runnable? = null private val pinCodes: MutableMap = ConcurrentHashMap() private var currentResultCallback : ((BluetoothPeripheral, ScanResult) -> Unit)? = null @@ -197,10 +196,6 @@ class BluetoothCentralManager(private val context: Context) { } override fun disconnected(peripheral: BluetoothPeripheral, status: HciStatus) { - if (expectingBluetoothOffDisconnects) { - cancelDisconnectionTimer() - expectingBluetoothOffDisconnects = false - } connectedPeripherals.remove(peripheral.address) unconnectedPeripherals.remove(peripheral.address) scannedPeripherals.remove(peripheral.address) @@ -854,29 +849,6 @@ class BluetoothCentralManager(private val context: Context) { reconnectPeripheralAddresses.clear() } - /** - * Timer to determine if manual disconnection in case of bluetooth off is needed - */ - private fun startDisconnectionTimer() { - cancelDisconnectionTimer() - disconnectRunnable = Runnable { - Logger.e(TAG, "bluetooth turned off but no automatic disconnects happening, so doing it ourselves") - cancelAllConnectionsWhenBluetoothOff() - disconnectRunnable = null - } - mainHandler.postDelayed(disconnectRunnable!!, 1000) - } - - /** - * Cancel timer for bluetooth off disconnects - */ - private fun cancelDisconnectionTimer() { - if (disconnectRunnable != null) { - mainHandler.removeCallbacks(disconnectRunnable!!) - disconnectRunnable = null - } - } - fun observeAdapterState(callback: (state: Int) -> Unit) { this.adapterStateCallback = callback } @@ -897,13 +869,14 @@ class BluetoothCentralManager(private val context: Context) { BluetoothAdapter.STATE_OFF -> { // Check if there are any connected peripherals or connections in progress if (connectedPeripherals.isNotEmpty() || unconnectedPeripherals.isNotEmpty()) { - // See if they are automatically disconnect - expectingBluetoothOffDisconnects = true - startDisconnectionTimer() + cancelAllConnectionsWhenBluetoothOff() } Logger.d(TAG, "bluetooth turned off") } BluetoothAdapter.STATE_TURNING_OFF -> { + // Try to disconnect all peripherals because Android doesn't always do that + connectedPeripherals.forEach { entry -> entry.value.cancelConnection()} + // Stop all scans so that we are back in a clean state if (isScanning) { // Note that we can't call stopScan if the adapter is off @@ -922,8 +895,6 @@ class BluetoothCentralManager(private val context: Context) { } } - expectingBluetoothOffDisconnects = true - // Stop all scans so that we are back in a clean state // Note that we can't call stopScan if the adapter is off cancelTimeoutTimer() @@ -940,11 +911,9 @@ class BluetoothCentralManager(private val context: Context) { bluetoothScanner = bluetoothAdapter.bluetoothLeScanner bluetoothScanner?.stopScan(defaultScanCallback) - expectingBluetoothOffDisconnects = false Logger.d(TAG, "bluetooth turned on") } BluetoothAdapter.STATE_TURNING_ON -> { - expectingBluetoothOffDisconnects = false Logger.d(TAG, "bluetooth turning on") } } From 3ab79d144914572e021e1e4684d452bb027f15c5 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Wed, 6 Jul 2022 14:43:04 +0200 Subject: [PATCH 19/25] Cleanup and upgrade gradle --- .../welie/blessedexample/BloodPressureMeasurementStatus.kt | 4 ---- .../java/com/welie/blessedexample/SensorContactFeature.kt | 4 ---- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/welie/blessedexample/BloodPressureMeasurementStatus.kt b/app/src/main/java/com/welie/blessedexample/BloodPressureMeasurementStatus.kt index b5659b8..c7b0682 100644 --- a/app/src/main/java/com/welie/blessedexample/BloodPressureMeasurementStatus.kt +++ b/app/src/main/java/com/welie/blessedexample/BloodPressureMeasurementStatus.kt @@ -1,7 +1,3 @@ -/* - * Copyright (c) Koninklijke Philips N.V. 2020. - * All rights reserved. - */ package com.welie.blessedexample class BloodPressureMeasurementStatus internal constructor(measurementStatus: Int) { diff --git a/app/src/main/java/com/welie/blessedexample/SensorContactFeature.kt b/app/src/main/java/com/welie/blessedexample/SensorContactFeature.kt index dca09df..4afbacf 100644 --- a/app/src/main/java/com/welie/blessedexample/SensorContactFeature.kt +++ b/app/src/main/java/com/welie/blessedexample/SensorContactFeature.kt @@ -1,7 +1,3 @@ -/* - * Copyright (c) Koninklijke Philips N.V., 2017. - * All rights reserved. - */ package com.welie.blessedexample /** diff --git a/build.gradle b/build.gradle index bb56b92..1021293 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 678b4ebeb249d65382790ba3eb8f0f0aaf132459 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Thu, 18 Aug 2022 20:04:07 +0200 Subject: [PATCH 20/25] Realign (#31) * Centralize state updates and make sure connection timer gets cancelled * Enable CONNECTING and DISCONNECTING states. Some other alignments with java code * Upgrade Gradle version * Update dependencies --- app/build.gradle | 12 ++-- .../welie/blessedexample/BluetoothHandler.kt | 2 +- blessed/build.gradle | 10 +-- .../welie/blessed/BluetoothCentralManager.kt | 8 +++ .../com/welie/blessed/BluetoothPeripheral.kt | 66 +++++++++++++------ build.gradle | 4 +- 6 files changed, 69 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9cd3df9..cb73a65 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { applicationId "com.welie.blessedexample" minSdkVersion 26 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -31,13 +31,13 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.jakewharton.timber:timber:5.0.1' - implementation "androidx.core:core-ktx:1.7.0" + implementation "androidx.core:core-ktx:1.8.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" implementation project(':blessed') diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index c366822..eb6ae9d 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -297,7 +297,7 @@ internal class BluetoothHandler private constructor(context: Context) { central = BluetoothCentralManager(context) central.observeConnectionState { peripheral, state -> - Timber.i("Peripheral ${peripheral.name} has $state") + Timber.i("Peripheral '${peripheral.name}' is $state") when (state) { ConnectionState.CONNECTED -> handlePeripheral(peripheral) ConnectionState.DISCONNECTED -> scope.launch { diff --git a/blessed/build.gradle b/blessed/build.gradle index eac56b4..8f9ab13 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'maven-publish' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { minSdkVersion 26 - targetSdkVersion 31 + targetSdkVersion 32 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -30,9 +30,9 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.7.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" + implementation "androidx.core:core-ktx:1.8.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" implementation 'com.jakewharton.timber:timber:5.0.1' testImplementation 'junit:junit:4.13.2' diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index c52d293..9065d7e 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -156,6 +156,10 @@ class BluetoothCentralManager(private val context: Context) { @JvmField val internalCallback: InternalCallback = object : InternalCallback { + override fun connecting(peripheral: BluetoothPeripheral) { + scope.launch { connectionStateCallback.invoke(peripheral, ConnectionState.CONNECTING)} + } + override fun connected(peripheral: BluetoothPeripheral) { connectionRetries.remove(peripheral.address) unconnectedPeripherals.remove(peripheral.address) @@ -195,6 +199,10 @@ class BluetoothCentralManager(private val context: Context) { } } + override fun disconnecting(peripheral: BluetoothPeripheral) { + scope.launch { connectionStateCallback.invoke(peripheral, ConnectionState.DISCONNECTING)} + } + override fun disconnected(peripheral: BluetoothPeripheral, status: HciStatus) { connectedPeripherals.remove(peripheral.address) unconnectedPeripherals.remove(peripheral.address) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index a4d9570..9cb7075 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -94,7 +94,7 @@ class BluetoothPeripheral internal constructor( */ private val bluetoothGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - cancelConnectionTimer() + if (newState != BluetoothProfile.STATE_CONNECTING) cancelConnectionTimer() val previousState = state state = newState @@ -103,8 +103,14 @@ class BluetoothPeripheral internal constructor( when (newState) { BluetoothProfile.STATE_CONNECTED -> successfullyConnected() BluetoothProfile.STATE_DISCONNECTED -> successfullyDisconnected(previousState) - BluetoothProfile.STATE_DISCONNECTING -> Logger.d(TAG, "peripheral is disconnecting") - BluetoothProfile.STATE_CONNECTING -> Logger.d(TAG, "peripheral is connecting") + BluetoothProfile.STATE_DISCONNECTING -> { + Logger.d(TAG, "peripheral is disconnecting") + listener.disconnecting(this@BluetoothPeripheral) + } + BluetoothProfile.STATE_CONNECTING -> { + Logger.d(TAG, "peripheral is connecting") + listener.connecting(this@BluetoothPeripheral) + } else -> Logger.e(TAG, "unknown state received") } } else { @@ -142,9 +148,11 @@ class BluetoothPeripheral internal constructor( ) } + val value = currentWriteBytes + currentWriteBytes = ByteArray(0) + if (descriptor.uuid == CCC_DESCRIPTOR_UUID) { if (gattStatus == GattStatus.SUCCESS) { - val value = nonnullOf(descriptor.value) if (value.contentEquals(ENABLE_NOTIFICATION_VALUE) || value.contentEquals(ENABLE_INDICATION_VALUE) ) { @@ -155,7 +163,7 @@ class BluetoothPeripheral internal constructor( } callbackScope.launch { resultCallback.onNotificationStateUpdate(this@BluetoothPeripheral, parentCharacteristic, gattStatus) } } else { - callbackScope.launch { resultCallback.onDescriptorWrite(this@BluetoothPeripheral, currentWriteBytes, descriptor, gattStatus) } + callbackScope.launch { resultCallback.onDescriptorWrite(this@BluetoothPeripheral, value, descriptor, gattStatus) } } completedCommand() } @@ -440,7 +448,6 @@ class BluetoothPeripheral internal constructor( delay(DIRECT_CONNECTION_DELAY_IN_MS) Logger.d(TAG, "connect to '%s' (%s) using TRANSPORT_LE", name, address) registerBondingBroadcastReceivers() - state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false bluetoothGatt = try { device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) @@ -449,6 +456,7 @@ class BluetoothPeripheral internal constructor( null } bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_CONNECTING) connectTimestamp = SystemClock.elapsedRealtime() startConnectionTimer(this@BluetoothPeripheral) } @@ -469,15 +477,15 @@ class BluetoothPeripheral internal constructor( scope.launch { Logger.d(TAG, "autoConnect to '%s' (%s) using TRANSPORT_LE", name, address) registerBondingBroadcastReceivers() - state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false bluetoothGatt = try { device.connectGatt(context, true, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) } catch (e: SecurityException) { - Logger.d(TAG, "exception") + Logger.e(TAG, "connectGatt exception") null } bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_CONNECTING) connectTimestamp = SystemClock.elapsedRealtime() } } @@ -506,11 +514,12 @@ class BluetoothPeripheral internal constructor( // Check if we have a Gatt object if (bluetoothGatt == null) { // No gatt object so no connection issued, do create bond immediately + registerBondingBroadcastReceivers() return device.createBond() } // Enqueue the bond command because a connection has been issued or we are already connected - val result = commandQueue.add(Runnable { + return enqueue { manuallyBonding = true if (!device.createBond()) { Logger.e(TAG, "bonding failed for %s", address) @@ -519,13 +528,7 @@ class BluetoothPeripheral internal constructor( Logger.d(TAG, "manually bonding %s", address) nrTries++ } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue bonding command") } - return result } /** @@ -557,7 +560,9 @@ class BluetoothPeripheral internal constructor( // Since we will not get a callback on onConnectionStateChange for this, we issue the disconnect ourselves scope.launch { delay(50) - bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTED) + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTED) + } } } else { // Cancel active connection and onConnectionStateChange will be called by Android @@ -573,10 +578,14 @@ class BluetoothPeripheral internal constructor( */ private fun disconnect() { if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) { - state = BluetoothProfile.STATE_DISCONNECTING + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTING) + } + scope.launch { if (state == BluetoothProfile.STATE_DISCONNECTING && bluetoothGatt != null) { bluetoothGatt?.disconnect() + Logger.i(TAG, "force disconnect '%s' (%s)", name, address) } } } else { @@ -585,7 +594,6 @@ class BluetoothPeripheral internal constructor( } fun disconnectWhenBluetoothOff() { - bluetoothGatt = null completeDisconnect(true, HciStatus.SUCCESS) } @@ -600,6 +608,10 @@ class BluetoothPeripheral internal constructor( commandQueue.clear() commandQueueBusy = false notifyingCharacteristics.clear() + currentMtu = DEFAULT_MTU + currentCommand = IDLE + manuallyBonding = false + discoveryStarted = false try { context.unregisterReceiver(bondStateReceiver) context.unregisterReceiver(pairingRequestBroadcastReceiver) @@ -1482,6 +1494,13 @@ class BluetoothPeripheral internal constructor( } interface InternalCallback { + /** + * Trying to connect to [BluetoothPeripheral] + * + * @param peripheral [BluetoothPeripheral] the peripheral. + */ + fun connecting(peripheral: BluetoothPeripheral) + /** * [BluetoothPeripheral] has successfully connected. * @@ -1496,6 +1515,13 @@ class BluetoothPeripheral internal constructor( */ fun connectFailed(peripheral: BluetoothPeripheral, status: HciStatus) + /** + * Trying to disconnect to [BluetoothPeripheral] + * + * @param peripheral [BluetoothPeripheral] the peripheral. + */ + fun disconnecting(peripheral: BluetoothPeripheral) + /** * [BluetoothPeripheral] has disconnected. * @@ -1515,7 +1541,9 @@ class BluetoothPeripheral internal constructor( disconnect() scope.launch { delay(50) - bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.CONNECTION_FAILED_ESTABLISHMENT.value, BluetoothProfile.STATE_DISCONNECTED) + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.CONNECTION_FAILED_ESTABLISHMENT.value, BluetoothProfile.STATE_DISCONNECTED) + } } } } diff --git a/build.gradle b/build.gradle index 1021293..2b64d4a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 3fefdc1a606a5f271ec70c83b0b4f42d2bbdb990 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Fri, 19 Aug 2022 10:30:57 +0200 Subject: [PATCH 21/25] Update to SDK 33 and implement new APIs for Peripheral --- app/build.gradle | 4 +- blessed/build.gradle | 4 +- .../com/welie/blessed/BluetoothPeripheral.kt | 64 ++++++++++++++----- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cb73a65..9cf010b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { applicationId "com.welie.blessedexample" minSdkVersion 26 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/blessed/build.gradle b/blessed/build.gradle index 8f9ab13..33f67de 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'maven-publish' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 26 - targetSdkVersion 32 + targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index 9cb7075..a075af7 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -32,7 +32,6 @@ import android.content.IntentFilter import android.os.Build import android.os.SystemClock import kotlinx.coroutines.* -import java.lang.Runnable import java.util.* import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Executors @@ -168,23 +167,36 @@ class BluetoothPeripheral internal constructor( completedCommand() } - override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { + override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray) { val gattStatus = GattStatus.fromValue(status) if (gattStatus != GattStatus.SUCCESS) { Logger.e(TAG, "reading descriptor <%s> failed for device '%s, status '%s'", descriptor.uuid, address, gattStatus) } - val value = nonnullOf(descriptor.value) val resultCallback = currentResultCallback callbackScope.launch { resultCallback.onDescriptorRead(this@BluetoothPeripheral, value, descriptor, gattStatus) } completedCommand() } - override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - val value = nonnullOf(characteristic.value) + @Deprecated("Deprecated in Java") + override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { + if (Build.VERSION.SDK_INT < 33) { + onDescriptorRead(gatt, descriptor, status, descriptor.value) + } + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { callbackScope.launch { observeMap[characteristic]?.invoke(value) } } + @Deprecated("Deprecated in Java") + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + if (Build.VERSION.SDK_INT < 33) { + onCharacteristicChanged(gatt, characteristic, characteristic.value) + } + } + + @Deprecated("Deprecated in Java") override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { val gattStatus = GattStatus.fromValue(status) if (gattStatus != GattStatus.SUCCESS) { @@ -911,8 +923,6 @@ class BluetoothPeripheral internal constructor( return enqueue { if (isConnected) { currentResultCallback = resultCallback - currentWriteBytes = bytesToWrite - characteristic.writeType = writeType.writeType if (willCauseLongWrite(bytesToWrite, writeType)) { // Android will turn this into a Long Write because it is larger than the MTU - 3. // When doing a Long Write the byte array will be automatically split in chunks of size MTU - 3. @@ -922,8 +932,8 @@ class BluetoothPeripheral internal constructor( // See https://stackoverflow.com/questions/48216517/rxandroidble-write-only-sends-the-first-20b Logger.w(TAG, "value byte array is longer than allowed by MTU, write will fail if peripheral does not support long writes") } - characteristic.value = bytesToWrite - if (bluetoothGatt?.writeCharacteristic(characteristic) == true) { + + if (internalWriteCharacteristic(characteristic, bytesToWrite, writeType) == true) { Logger.d(TAG, "writing <%s> to characteristic <%s>", BluetoothBytesParser.bytes2String(bytesToWrite), characteristic.uuid) nrTries++ } else { @@ -938,6 +948,20 @@ class BluetoothPeripheral internal constructor( } } + private fun internalWriteCharacteristic(characteristic: BluetoothGattCharacteristic, value: ByteArray, writeType: WriteType): Boolean { + if (bluetoothGatt == null) return false + + currentWriteBytes = value + return if (Build.VERSION.SDK_INT >= 33) { + val result = bluetoothGatt?.writeCharacteristic(characteristic, currentWriteBytes, writeType.writeType) + result == BluetoothStatusCodes.SUCCESS + } else { + characteristic.writeType = writeType.writeType + characteristic.value = value + bluetoothGatt!!.writeCharacteristic(characteristic) + } + } + private fun willCauseLongWrite(value: ByteArray, writeType: WriteType): Boolean { return value.size > currentMtu - 3 && writeType == WriteType.WITH_RESPONSE } @@ -1033,9 +1057,8 @@ class BluetoothPeripheral internal constructor( return enqueue { if (isConnected) { currentResultCallback = resultCallback - currentWriteBytes = bytesToWrite - descriptor.value = bytesToWrite - if (bluetoothGatt?.writeDescriptor(descriptor) == true) { + + if (internalWriteDescriptor(descriptor, bytesToWrite)) { Logger.d(TAG, "writing <%s> to descriptor <%s>", BluetoothBytesParser.bytes2String(bytesToWrite), descriptor.uuid) nrTries++ } else { @@ -1050,6 +1073,19 @@ class BluetoothPeripheral internal constructor( } } + private fun internalWriteDescriptor(descriptor: BluetoothGattDescriptor, value: ByteArray): Boolean { + if (bluetoothGatt == null) return false + + currentWriteBytes = value + + return if (Build.VERSION.SDK_INT >= 33) { + val result = bluetoothGatt?.writeDescriptor(descriptor, value) + result == BluetoothStatusCodes.SUCCESS + } else { + descriptor.value = value + bluetoothGatt!!.writeDescriptor(descriptor) + } + } suspend fun observe(characteristic: BluetoothGattCharacteristic, callback: (value: ByteArray) -> Unit): Boolean = suspendCoroutine { @@ -1155,9 +1191,7 @@ class BluetoothPeripheral internal constructor( Logger.e(TAG, "setCharacteristicNotification failed for characteristic: %s", characteristic.uuid) completedCommand() } else { - currentWriteBytes = finalValue - descriptor.value = finalValue - if (bluetoothGatt?.writeDescriptor(descriptor) == true) { + if (internalWriteDescriptor(descriptor, finalValue)) { nrTries++ } else { Logger.e(TAG, "writeDescriptor failed for descriptor: %s", descriptor.uuid) From 5f0f9b1b7cb0552169f3f337ff895e235bbd529c Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sat, 3 Dec 2022 21:27:04 +0100 Subject: [PATCH 22/25] Create some ByteArray extensions --- blessed/build.gradle | 8 +- .../com/welie/blessed/BluetoothPeripheral.kt | 1 - .../com/welie/blessed/ByteArrayExtensions.kt | 81 +++++++++++++++++++ .../main/java/com/welie/blessed/Extensions.kt | 6 +- .../java/com/welie/blessed/ByteArrayTests.kt | 75 +++++++++++++++++ build.gradle | 2 +- 6 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt create mode 100644 blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt diff --git a/blessed/build.gradle b/blessed/build.gradle index 33f67de..90b819f 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -30,16 +30,16 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.8.0" + implementation "androidx.core:core-ktx:1.9.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'com.jakewharton.timber:timber:5.0.1' testImplementation 'junit:junit:4.13.2' testImplementation "org.robolectric:robolectric:4.5.1" testImplementation "org.mockito:mockito-core:3.8.0" - testImplementation 'androidx.test:core:1.4.0' - testImplementation "io.mockk:mockk:1.12.2" + testImplementation 'androidx.test:core:1.5.0' + testImplementation "io.mockk:mockk:1.12.5" } afterEvaluate { diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index a075af7..b0f6f41 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -850,7 +850,6 @@ class BluetoothPeripheral internal constructor( } - suspend fun writeCharacteristic(serviceUUID: UUID, characteristicUUID: UUID, value: ByteArray, writeType: WriteType): ByteArray { require(isConnected) { PERIPHERAL_NOT_CONNECTED } diff --git a/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt new file mode 100644 index 0000000..d6b2f27 --- /dev/null +++ b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt @@ -0,0 +1,81 @@ +package com.welie.blessed + +import com.welie.blessed.ByteOrder.LITTLE_ENDIAN + +enum class ByteOrder { + LITTLE_ENDIAN, + BIG_ENDIAN +} + +fun Byte.asHexString(): String { + var hexString = this.toUInt().toString(16).uppercase() + if (this.toUInt() < 16u) hexString = "0$hexString" + return hexString +} + +fun ByteArray.formatHexBytes(seperator: String?): String { + var resultString = "" + for ((index, value) in this.iterator().withIndex()) { + resultString += value.asHexString() + if (seperator != null && index < (this.size - 1)) resultString += seperator + } + return resultString +} + +fun ByteArray.asHexString() : String { + return this.formatHexBytes(null) +} + +/** + * Convert an unsigned integer value to a two's-complement encoded + * signed value. + */ +private fun unsignedToSigned(unsigned: UInt, size: UInt): Int { + if (size > 24u) throw IllegalArgumentException("size too large") + + val signBit : UInt = (1u shl ((size - 1u).toInt())) + if (unsigned and signBit != 0u) { + // Convert to a negative value + val nonsignedPart = (unsigned and (signBit - 1u)) + return -1 * (signBit - nonsignedPart).toInt() + } + return unsigned.toInt() +} + +fun ByteArray.getUInt(offset: UInt = 0u, length: UInt, order: ByteOrder) : UInt { + val start = offset.toInt() + val end = start + length.toInt() - 1 + val range : IntProgression = if (order == LITTLE_ENDIAN) IntProgression.fromClosedRange (end, start, -1) else start..end + var result : UInt = 0u + for (i in range) { + if (i != range.first) { + result = result shl 8 + } + result += this[i].toUByte().toUInt() + } + return result +} + +fun ByteArray.getUInt16(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { + return getUInt(offset = offset, length = 2u, order = order) +} + +fun ByteArray.getInt16(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { + return unsignedToSigned(getUInt(offset = offset, length = 2u, order = order), 16u) +} + +fun ByteArray.getUInt24(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { + return getUInt(offset = offset, length = 3u, order = order) +} + +fun ByteArray.getInt24(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { + return unsignedToSigned(getUInt(offset = offset, length = 3u, order = order), 24u) +} + +fun ByteArray.getUInt32(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { + return getUInt(offset = offset, length = 4u, order = order) +} + +fun ByteArray.getInt32(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { + return getUInt(offset = offset, length = 4u, order = order).toInt() +} diff --git a/blessed/src/main/java/com/welie/blessed/Extensions.kt b/blessed/src/main/java/com/welie/blessed/Extensions.kt index 5b7cbda..fd532d6 100644 --- a/blessed/src/main/java/com/welie/blessed/Extensions.kt +++ b/blessed/src/main/java/com/welie/blessed/Extensions.kt @@ -9,9 +9,9 @@ fun ByteArray.asString() : String { return parser.stringValue } -fun ByteArray.asHexString() : String { - return BluetoothBytesParser.bytes2String(this) -} +//fun ByteArray.asHexString() : String { +// return BluetoothBytesParser.bytes2String(this) +//} fun ByteArray.asUInt8() : UInt? { if (this.isEmpty()) return null diff --git a/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt new file mode 100644 index 0000000..fe410b3 --- /dev/null +++ b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt @@ -0,0 +1,75 @@ +package com.welie.blessed + +import org.junit.Assert.* +import org.junit.Test + + +class ByteArrayTests { + + @Test + fun getUInt16_pos_LE_test() { + val value = byteArrayOf(0x01,0x02) + assertEquals(513u, value.getUInt16(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getUInt16_pos_BE_test() { + val value = byteArrayOf(0x01,0x02) + assertEquals(258u, value.getUInt16(order = ByteOrder.BIG_ENDIAN)) + } + + @Test + fun getUInt16_max_LE_test() { + val value = byteArrayOf(0xFF.toByte(),0xFF.toByte()) + assertEquals(65535u, value.getUInt16(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getUInt16_max_BE_test() { + val value = byteArrayOf(0xFF.toByte(),0xFF.toByte()) + assertEquals(65535u, value.getUInt16(order = ByteOrder.BIG_ENDIAN)) + } + + @Test + fun getInt16_pos_LE_test() { + val value = byteArrayOf(0x01,0x02) + assertEquals(513, value.getInt16(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getInt16_pos_BE_test() { + val value = byteArrayOf(0x01,0x02) + assertEquals(258, value.getInt16(order = ByteOrder.BIG_ENDIAN)) + } + + @Test + fun getInt16_neg_LE_test() { + val value = byteArrayOf(3,-4) + assertEquals(-1021, value.getInt16(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getInt16_neg_BE_test() { + val value = byteArrayOf(0x01,0x02) + assertEquals(258, value.getInt16(order = ByteOrder.BIG_ENDIAN)) + } + + @Test + fun getUInt32_test() { + val value = byteArrayOf(0xFF.toByte(),0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) + assertEquals(UInt.MAX_VALUE, value.getUInt32(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getInt32_minus1_test() { + val value = byteArrayOf(0xFF.toByte(),0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) + assertEquals(-1, value.getInt32(order = ByteOrder.LITTLE_ENDIAN)) + } + + @Test + fun getInt32_minus_random_LE_test() { + val value = byteArrayOf(1,-2,3,-4) + assertEquals(-66847231, value.getInt32(order = ByteOrder.LITTLE_ENDIAN)) + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2b64d4a..58f1c0b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.7.20' repositories { google() mavenCentral() From 8030e4edcc025882a57b7f5b4b7469efdbc1a8e4 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sun, 4 Dec 2022 13:43:25 +0100 Subject: [PATCH 23/25] More ByteArray extensions --- .../com/welie/blessed/BluetoothBytesParser.kt | 65 ++++++++++ .../com/welie/blessed/BluetoothCentral.kt | 1 + .../com/welie/blessed/ByteArrayExtensions.kt | 118 +++++++++++++++--- .../welie/blessed/BluetoothBytesParserTest.kt | 5 +- .../java/com/welie/blessed/ByteArrayTests.kt | 82 +++++++++--- 5 files changed, 240 insertions(+), 31 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt index d729f4d..ec14cd1 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothBytesParser.kt @@ -50,6 +50,71 @@ class BluetoothBytesParser ( */ constructor(value: ByteArray, byteOrder: ByteOrder) : this(value, 0, byteOrder) + fun getUInt8() : UInt { + val result = value[offset].toUInt() + offset += 1 + return result + } + + fun getInt8() : Int { + val result = value[offset].toInt() + offset += 1 + return result + } + + fun getUInt16() : UInt { + val result = value.getUInt16(offset.toUInt(), byteOrder) + offset += 2 + return result + } + + fun getInt16() : Int { + val result = value.getInt16(offset.toUInt(), byteOrder) + offset += 2 + return result + } + + fun getUInt24() : UInt { + val result = value.getUInt24(offset.toUInt(), byteOrder) + offset += 3 + return result + } + + fun getInt24() : Int { + val result = value.getInt24(offset.toUInt(), byteOrder) + offset += 3 + return result + } + + fun getUInt32() : UInt { + val result = value.getUInt32(offset.toUInt(), byteOrder) + offset += 4 + return result + } + + fun getInt32() : Int { + val result = value.getInt32(offset.toUInt(), byteOrder) + offset += 4 + return result + } + + fun getFloat() : Double { + val result = value.getFloat(offset.toUInt(), byteOrder) + offset += 4 + return result + } + + fun getSFloat() : Double { + val result = value.getSFloat(offset.toUInt(), byteOrder) + offset += 2 + return result + } + + fun setUInt16(uint16: UInt) { + val uint16array = byteArrayOf(uint16, 2u, LITTLE_ENDIAN) + value += uint16array + } + /** * Return an Integer value of the specified type. This operation will automatically advance the internal offset to the next position. * diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentral.kt index 304ceb3..c1d1993 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentral.kt @@ -31,6 +31,7 @@ class BluetoothCentral internal constructor(address: String, name: String?) { val address: String private val name: String? var currentMtu = 23 + fun getName(): String { return name ?: "" } diff --git a/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt index d6b2f27..c18efff 100644 --- a/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt +++ b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt @@ -1,23 +1,22 @@ package com.welie.blessed -import com.welie.blessed.ByteOrder.LITTLE_ENDIAN -enum class ByteOrder { - LITTLE_ENDIAN, - BIG_ENDIAN -} +import java.nio.ByteOrder +import java.nio.ByteOrder.LITTLE_ENDIAN +import kotlin.math.pow + fun Byte.asHexString(): String { - var hexString = this.toUInt().toString(16).uppercase() + var hexString = this.toUByte().toString(16).uppercase() if (this.toUInt() < 16u) hexString = "0$hexString" return hexString } -fun ByteArray.formatHexBytes(seperator: String?): String { +fun ByteArray.formatHexBytes(separator: String?): String { var resultString = "" for ((index, value) in this.iterator().withIndex()) { resultString += value.asHexString() - if (seperator != null && index < (this.size - 1)) resultString += seperator + if (separator != null && index < (this.size - 1)) resultString += separator } return resultString } @@ -42,11 +41,25 @@ private fun unsignedToSigned(unsigned: UInt, size: UInt): Int { return unsigned.toInt() } -fun ByteArray.getUInt(offset: UInt = 0u, length: UInt, order: ByteOrder) : UInt { +private fun unsignedToSigned(unsigned: ULong, size: ULong): Long { + if (size > 56u) throw IllegalArgumentException("size too large") + + val signBit : ULong = (1uL shl ((size - 1uL).toInt())) + if (unsigned and signBit != 0uL) { + // Convert to a negative value + val nonsignedPart = (unsigned and (signBit - 1u)) + return -1 * (signBit - nonsignedPart).toLong() + } + return unsigned.toLong() +} + +fun ByteArray.getULong(offset: UInt = 0u, length: UInt, order: ByteOrder) : ULong { + if (length == 0u) throw IllegalArgumentException("length must not be zero") + val start = offset.toInt() val end = start + length.toInt() - 1 val range : IntProgression = if (order == LITTLE_ENDIAN) IntProgression.fromClosedRange (end, start, -1) else start..end - var result : UInt = 0u + var result : ULong = 0u for (i in range) { if (i != range.first) { result = result shl 8 @@ -57,25 +70,98 @@ fun ByteArray.getUInt(offset: UInt = 0u, length: UInt, order: ByteOrder) : UInt } fun ByteArray.getUInt16(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { - return getUInt(offset = offset, length = 2u, order = order) + return getULong(offset = offset, length = 2u, order = order).toUInt() } fun ByteArray.getInt16(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { - return unsignedToSigned(getUInt(offset = offset, length = 2u, order = order), 16u) + return unsignedToSigned(getULong(offset = offset, length = 2u, order = order).toUInt(), 16u) } fun ByteArray.getUInt24(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { - return getUInt(offset = offset, length = 3u, order = order) + return getULong(offset = offset, length = 3u, order = order).toUInt() } fun ByteArray.getInt24(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { - return unsignedToSigned(getUInt(offset = offset, length = 3u, order = order), 24u) + return unsignedToSigned(getULong(offset = offset, length = 3u, order = order).toUInt(), 24u) } fun ByteArray.getUInt32(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : UInt { - return getUInt(offset = offset, length = 4u, order = order) + return getULong(offset = offset, length = 4u, order = order).toUInt() } fun ByteArray.getInt32(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Int { - return getUInt(offset = offset, length = 4u, order = order).toInt() + return getULong(offset = offset, length = 4u, order = order).toInt() +} + +fun ByteArray.getUInt48(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : ULong { + return getULong(offset = offset, length = 6u, order = order) +} + +fun ByteArray.getInt48(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Long { + return unsignedToSigned(getULong(offset = offset, length = 6u, order = order), 48uL) +} + +fun ByteArray.geUInt64(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : ULong { + return getULong(offset = offset, length = 8u, order = order) +} + +fun ByteArray.geInt64(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Long { + return getULong(offset = offset, length = 8u, order = order).toLong() +} + +fun ByteArray.getSFloat(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Double { + val uint16 = getUInt16(offset = offset, order = order) + val mantissa = unsignedToSigned( uint16 and 0x0FFFu, 12u) + val exponent = unsignedToSigned(uint16 shr 12, 4u) + + return mantissa.toDouble() * 10.0.pow(exponent) +} + +fun ByteArray.getFloat(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : Double { + val uint32 = getUInt32(offset = offset, order = order) + val mantissa = unsignedToSigned( uint32 and 0x00FFFFFFu, 24u) + val exponent = unsignedToSigned(uint32 shr 24, 8u) + + return mantissa.toDouble() * 10.0.pow(exponent) +} + +fun byteArrayOf(value: UInt, length: UInt, order : ByteOrder) : ByteArray { + val result = ByteArray(size = length.toInt()) + val end = length.toInt() - 1 + val range : IntProgression = if (order == LITTLE_ENDIAN) 0..end else IntProgression.fromClosedRange (end, 0, -1) + for (i in range) { + if (i == range.first) { + result[i] = value.toByte() + } else { + result[i] = (value shr (i * 8)).toByte() + } + } + return result } + +fun byteArrayOf(value: Int, length: UInt, order : ByteOrder) : ByteArray { + return byteArrayOf(value.toUInt(), length, order) +} + +fun byteArrayOf(hexString: String): ByteArray { + val result = ByteArray(hexString.length / 2) + for (i in result.indices) { + val index = i * 2 + result[i] = hexString.substring(index, index + 2).toInt(16).toByte() + } + return result +} + +fun mergeArrays(vararg arrays: ByteArray): ByteArray { + var size = 0 + for (array in arrays) { + size += array.size + } + val merged = ByteArray(size) + var index = 0 + for (array in arrays) { + array.copyInto(merged, index, 0, array.size) + index += array.size + } + return merged +} \ No newline at end of file diff --git a/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt b/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt index 46207ec..aed28d3 100644 --- a/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt +++ b/blessed/src/test/java/com/welie/blessed/BluetoothBytesParserTest.kt @@ -1,6 +1,7 @@ package com.welie.blessed import android.content.Context +import android.util.Log import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_FLOAT import io.mockk.* import org.junit.* @@ -22,7 +23,9 @@ class BluetoothBytesParserTest { var parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) parser.setFloatValue(364, -1, FORMAT_FLOAT, 0) parser.offset = 0 - assertEquals(36.4f, parser.getFloatValue(FORMAT_FLOAT)) +// assertEquals(36.4f, parser.getFloatValue(FORMAT_FLOAT)) + val asstring = parser.value.asHexString() + assertEquals(36.4, parser.getFloat(), 0.01) parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) parser.setFloatValue(5.3f, 1) diff --git a/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt index fe410b3..ab8e88f 100644 --- a/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt +++ b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt @@ -2,6 +2,8 @@ package com.welie.blessed import org.junit.Assert.* import org.junit.Test +import java.nio.ByteOrder.BIG_ENDIAN +import java.nio.ByteOrder.LITTLE_ENDIAN class ByteArrayTests { @@ -9,67 +11,119 @@ class ByteArrayTests { @Test fun getUInt16_pos_LE_test() { val value = byteArrayOf(0x01,0x02) - assertEquals(513u, value.getUInt16(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(513u, value.getUInt16(order = LITTLE_ENDIAN)) } @Test fun getUInt16_pos_BE_test() { val value = byteArrayOf(0x01,0x02) - assertEquals(258u, value.getUInt16(order = ByteOrder.BIG_ENDIAN)) + assertEquals(258u, value.getUInt16(order = BIG_ENDIAN)) } @Test fun getUInt16_max_LE_test() { val value = byteArrayOf(0xFF.toByte(),0xFF.toByte()) - assertEquals(65535u, value.getUInt16(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(65535u, value.getUInt16(order = LITTLE_ENDIAN)) } @Test fun getUInt16_max_BE_test() { val value = byteArrayOf(0xFF.toByte(),0xFF.toByte()) - assertEquals(65535u, value.getUInt16(order = ByteOrder.BIG_ENDIAN)) + assertEquals(65535u, value.getUInt16(order = BIG_ENDIAN)) } @Test fun getInt16_pos_LE_test() { val value = byteArrayOf(0x01,0x02) - assertEquals(513, value.getInt16(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(513, value.getInt16(order = LITTLE_ENDIAN)) } @Test fun getInt16_pos_BE_test() { val value = byteArrayOf(0x01,0x02) - assertEquals(258, value.getInt16(order = ByteOrder.BIG_ENDIAN)) + assertEquals(258, value.getInt16(order = BIG_ENDIAN)) } @Test fun getInt16_neg_LE_test() { val value = byteArrayOf(3,-4) - assertEquals(-1021, value.getInt16(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(-1021, value.getInt16(order = LITTLE_ENDIAN)) } @Test fun getInt16_neg_BE_test() { - val value = byteArrayOf(0x01,0x02) - assertEquals(258, value.getInt16(order = ByteOrder.BIG_ENDIAN)) + val value = byteArrayOf(-4, 3) + assertEquals(-1021, value.getInt16(order = BIG_ENDIAN)) } @Test - fun getUInt32_test() { + fun getUInt32_max_LE_test() { val value = byteArrayOf(0xFF.toByte(),0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) - assertEquals(UInt.MAX_VALUE, value.getUInt32(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(UInt.MAX_VALUE, value.getUInt32(order = LITTLE_ENDIAN)) } @Test - fun getInt32_minus1_test() { + fun getInt32_minus1_LE_test() { val value = byteArrayOf(0xFF.toByte(),0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) - assertEquals(-1, value.getInt32(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(-1, value.getInt32(order = LITTLE_ENDIAN)) } @Test fun getInt32_minus_random_LE_test() { val value = byteArrayOf(1,-2,3,-4) - assertEquals(-66847231, value.getInt32(order = ByteOrder.LITTLE_ENDIAN)) + assertEquals(-66847231, value.getInt32(order = LITTLE_ENDIAN)) + } + + @Test + fun getFloat32_random_LE_test() { + val value = byteArrayOf("6C0100FF") + assertEquals(36.4, value.getFloat(order = LITTLE_ENDIAN), 0.1) + } + + @Test + fun getFloat32_random_BE_test() { + // 6C0100FF + val value = byteArrayOf("FF000170") + assertEquals(36.8, value.getFloat(order = BIG_ENDIAN), 0.1) + } + + @Test + fun getFloat16_pos_random_LE_test() { + val value = byteArrayOf("F070") + assertEquals(11.2, value.getSFloat(order = BIG_ENDIAN), 0.1) + } + + @Test + fun getFloat16_neg_random_LE_test() { + val value = byteArrayOf("70F8") + assertEquals(-193.6, value.getSFloat(order = LITTLE_ENDIAN), 0.1) } + @Test + fun getFloat16_neg_random_BE_test() { + val value = byteArrayOf("F870") + assertEquals(-193.6, value.getSFloat(order = BIG_ENDIAN), 0.1) + } + + @Test + fun byteArrayOf_UInt16_LE_test() { + val value = byteArrayOf(256u, 2u, LITTLE_ENDIAN) + assertEquals("0001", value.asHexString()) + } + + @Test + fun hexString_test() { + val value = byteArrayOf("F870") + assertEquals("F870", value.asHexString()) + } + + @Test + fun merge_test() { + val value = byteArrayOf("F870") + val value2 = byteArrayOf("A387") + val value3 = byteArrayOf("5638") + val merged = mergeArrays(value,value2,value3) + + assertEquals("F870A3875638", merged.asHexString()) + } } \ No newline at end of file From 4321e8f4e1c63ec0bafe0bd232b8d5bf4f78a0fd Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Fri, 16 Dec 2022 16:22:35 +0100 Subject: [PATCH 24/25] More ByteArray extensions --- .../com/welie/blessed/ByteArrayExtensions.kt | 55 ++++++++++++++++++- .../java/com/welie/blessed/ByteArrayTests.kt | 34 ++++++++++-- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt index c18efff..bf0315a 100644 --- a/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt +++ b/blessed/src/main/java/com/welie/blessed/ByteArrayExtensions.kt @@ -5,7 +5,6 @@ import java.nio.ByteOrder import java.nio.ByteOrder.LITTLE_ENDIAN import kotlin.math.pow - fun Byte.asHexString(): String { var hexString = this.toUByte().toString(16).uppercase() if (this.toUInt() < 16u) hexString = "0$hexString" @@ -41,6 +40,10 @@ private fun unsignedToSigned(unsigned: UInt, size: UInt): Int { return unsigned.toInt() } +/** + * Convert an unsigned long value to a two's-complement encoded + * signed value. + */ private fun unsignedToSigned(unsigned: ULong, size: ULong): Long { if (size > 56u) throw IllegalArgumentException("size too large") @@ -53,6 +56,17 @@ private fun unsignedToSigned(unsigned: ULong, size: ULong): Long { return unsigned.toLong() } +/** + * Convert an integer into the signed bits of a given length. + */ +private fun intToSignedBits(value: Int, size: Int): Int { + var i = value + if (i < 0) { + i = (1 shl size - 1) + (i and (1 shl size - 1) - 1) + } + return i +} + fun ByteArray.getULong(offset: UInt = 0u, length: UInt, order: ByteOrder) : ULong { if (length == 0u) throw IllegalArgumentException("length must not be zero") @@ -125,7 +139,7 @@ fun ByteArray.getFloat(offset : UInt = 0u, order: ByteOrder = LITTLE_ENDIAN) : D return mantissa.toDouble() * 10.0.pow(exponent) } -fun byteArrayOf(value: UInt, length: UInt, order : ByteOrder) : ByteArray { +fun byteArrayOf(value: UInt, length: UInt, order : ByteOrder = LITTLE_ENDIAN) : ByteArray { val result = ByteArray(size = length.toInt()) val end = length.toInt() - 1 val range : IntProgression = if (order == LITTLE_ENDIAN) 0..end else IntProgression.fromClosedRange (end, 0, -1) @@ -143,6 +157,43 @@ fun byteArrayOf(value: Int, length: UInt, order : ByteOrder) : ByteArray { return byteArrayOf(value.toUInt(), length, order) } +fun byteArrayOf(value: Double, length: UInt, precision: Int, order : ByteOrder = LITTLE_ENDIAN) : ByteArray { + val result = ByteArray(size = length.toInt()) + val mantissa = (value * 10.0.pow(precision)).toInt() + val exponent = -precision + + if (length == 2u) { + val localMantissa = intToSignedBits(mantissa, 12) + val localExponent = intToSignedBits(exponent, 4) + var index = 0 + if (order == LITTLE_ENDIAN) { + result[index++] = (localMantissa and 0xFF).toByte() + result[index] = (localMantissa shr 8 and 0x0F).toByte() + result[index] = (result[index] + (localExponent and 0x0F shl 4)).toByte() + } else { + result[index] = (localMantissa shr 8 and 0x0F).toByte() + result[index++] = (result[index] + (localExponent and 0x0F shl 4)).toByte() + result[index] = (localMantissa and 0xFF).toByte() + } + } else if(length == 4u) { + val localMantissa = intToSignedBits(mantissa, 24) + val localExponent = intToSignedBits(exponent, 8) + var index = 0 + if (order == LITTLE_ENDIAN) { + result[index++] = (localMantissa and 0xFF).toByte() + result[index++] = (localMantissa shr 8 and 0xFF).toByte() + result[index++] = (localMantissa shr 16 and 0xFF).toByte() + result[index] = (result[index] + (localExponent and 0xFF)).toByte() + } else { + result[index++] = (result[index] + (localExponent and 0xFF)).toByte() + result[index++] = (localMantissa shr 16 and 0xFF).toByte() + result[index++] = (localMantissa shr 8 and 0xFF).toByte() + result[index] = (localMantissa and 0xFF).toByte() + } + } + return result +} + fun byteArrayOf(hexString: String): ByteArray { val result = ByteArray(hexString.length / 2) for (i in result.indices) { diff --git a/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt index ab8e88f..584e9f8 100644 --- a/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt +++ b/blessed/src/test/java/com/welie/blessed/ByteArrayTests.kt @@ -10,13 +10,13 @@ class ByteArrayTests { @Test fun getUInt16_pos_LE_test() { - val value = byteArrayOf(0x01,0x02) + val value = byteArrayOf(0x01u,0x02u) assertEquals(513u, value.getUInt16(order = LITTLE_ENDIAN)) } @Test fun getUInt16_pos_BE_test() { - val value = byteArrayOf(0x01,0x02) + val value = byteArrayOf(0x01u,0x02u) assertEquals(258u, value.getUInt16(order = BIG_ENDIAN)) } @@ -34,13 +34,13 @@ class ByteArrayTests { @Test fun getInt16_pos_LE_test() { - val value = byteArrayOf(0x01,0x02) + val value = byteArrayOf(0x01u,0x02u) assertEquals(513, value.getInt16(order = LITTLE_ENDIAN)) } @Test fun getInt16_pos_BE_test() { - val value = byteArrayOf(0x01,0x02) + val value = byteArrayOf(0x01u,0x02u) assertEquals(258, value.getInt16(order = BIG_ENDIAN)) } @@ -88,7 +88,7 @@ class ByteArrayTests { } @Test - fun getFloat16_pos_random_LE_test() { + fun getFloat16_pos_random_BE_test() { val value = byteArrayOf("F070") assertEquals(11.2, value.getSFloat(order = BIG_ENDIAN), 0.1) } @@ -117,6 +117,30 @@ class ByteArrayTests { assertEquals("F870", value.asHexString()) } + @Test + fun byteArrayOf_SFloat_LE_test() { + val value = byteArrayOf(11.2, 2u, 1, LITTLE_ENDIAN) + assertEquals("70F0", value.asHexString()) + } + + @Test + fun byteArrayOf_SFloat_BE_test() { + val value = byteArrayOf(11.2, 2u, 1, BIG_ENDIAN) + assertEquals("F070", value.asHexString()) + } + + @Test + fun byteArrayOf_Float_LE_test() { + val value = byteArrayOf(36.4, 4u, 1, LITTLE_ENDIAN) + assertEquals("6C0100FF", value.asHexString()) + } + + @Test + fun byteArrayOf_Float_BE_test() { + val value = byteArrayOf(36.4, 4u, 1, BIG_ENDIAN) + assertEquals("FF00016C", value.asHexString()) + } + @Test fun merge_test() { val value = byteArrayOf("F870") From b1c589ed415eb48550e083f18fe7357c7a5b6235 Mon Sep 17 00:00:00 2001 From: Martijn van Welie Date: Sun, 26 Mar 2023 11:01:24 +0200 Subject: [PATCH 25/25] Fix NPE --- .../java/com/welie/blessed/BluetoothPeripheralManager.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt index f823c0c..bafc97c 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheralManager.kt @@ -82,13 +82,13 @@ class BluetoothPeripheralManager(private val context: Context, private val bluet handleDeviceDisconnected(device) } } else { - Logger.i(TAG, "Device '%s' disconnected with status %d", device.name, status) + Logger.i(TAG, "Device '%s' disconnected with status %d", device.name ?: "null", status) handleDeviceDisconnected(device) } } private fun handleDeviceConnected(device: BluetoothDevice) { - Logger.i(TAG, "Central '%s' (%s) connected", device.name, device.address) + Logger.i(TAG, "Central '%s' (%s) connected", device.name ?: "null", device.address) val bluetoothCentral = BluetoothCentral(device.address, device.name) connectedCentralsMap[bluetoothCentral.address] = bluetoothCentral mainHandler.post { callback.onCentralConnected(bluetoothCentral) } @@ -476,7 +476,7 @@ class BluetoothPeripheralManager(private val context: Context, private val bluet private fun cancelConnection(bluetoothDevice: BluetoothDevice) { Objects.requireNonNull(bluetoothDevice, DEVICE_IS_NULL) - Logger.i(TAG, "cancelConnection with '%s' (%s)", bluetoothDevice.name, bluetoothDevice.address) + Logger.i(TAG, "cancelConnection with '%s' (%s)", bluetoothDevice.name ?: "null", bluetoothDevice.address) bluetoothGattServer.cancelConnection(bluetoothDevice) }