Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
38fe652
Fix removeBond reflection code
weliem Jan 21, 2022
eeb6bc3
Refactoring and adding possibility to observe Adapter state
weliem Jan 29, 2022
dcb0243
Get rid of some !!
weliem Jan 29, 2022
22b296b
Make methods of BluetoothPeripheralManagerCallback open
weliem Feb 15, 2022
282a096
Make more methods of BluetoothPeripheralCallback open
weliem Feb 15, 2022
94102f9
Update dependencies
weliem Feb 15, 2022
d75eb80
Update Kotlin and Gradle versions
weliem Feb 15, 2022
66d1aeb
Added some BluetoothBytesParser tests
weliem Feb 27, 2022
6217f83
Removed example unit test
weliem Feb 27, 2022
327aadb
Removed example unit test
weliem Mar 3, 2022
58f32c9
Re-enable currentCentralManagerCallback
weliem Mar 3, 2022
b382ccc
Update README.md
weliem Mar 3, 2022
51fe74e
Update README.md
weliem Mar 3, 2022
9c3bcff
Fix array out of bounds crash
weliem Apr 15, 2022
bf12a2f
Fix Omron check to be case-insensitive
weliem Apr 16, 2022
2874edc
Fix comment
weliem Apr 16, 2022
4299582
Fix nitpicky grammar and spelling
Jun 9, 2022
2f49d20
Merge pull request #25 from scottpeterson/main
weliem Jun 9, 2022
55b8f04
Immediately try to disconnect devices when bluetooth is turned off
weliem Jun 28, 2022
96df0d6
Merge branch 'main' of https://github.com/weliem/blessed-android-coro…
weliem Jun 28, 2022
3ab79d1
Cleanup and upgrade gradle
weliem Jul 6, 2022
678b4eb
Realign (#31)
weliem Aug 18, 2022
3fefdc1
Update to SDK 33 and implement new APIs for Peripheral
weliem Aug 19, 2022
5f0f9b1
Create some ByteArray extensions
weliem Dec 3, 2022
8030e4e
More ByteArray extensions
weliem Dec 4, 2022
4321e8f
More ByteArray extensions
weliem Dec 16, 2022
b1c589e
Fix NPE
weliem Mar 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,29 @@ 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

The `BluetoothCentralManager` class has several differrent scanning methods:

```kotlin
fun scanForPeripherals()
fun scanForPeripheralsWithServices(serviceUUIDs: Array<UUID>)
fun scanForPeripheralsWithNames(peripheralNames: Array<String>)
fun scanForPeripheralsWithAddresses(peripheralAddresses: Array<String>)
fun scanForPeripheralsUsingFilters(filters: List<ScanFilter>)
fun scanForPeripherals(resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit )
fun scanForPeripheralsWithServices(serviceUUIDs: Array<UUID>, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit)
fun scanForPeripheralsWithNames(peripheralNames: Array<String>, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit)
fun scanForPeripheralsWithAddresses(peripheralAddresses: Array<String>, resultCallback: (BluetoothPeripheral, ScanResult) -> Unit, scanError: (ScanFailure) -> Unit)
fun scanForPeripheralsUsingFilters(filters: List<ScanFilter>,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:

Expand All @@ -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.
Expand All @@ -82,7 +84,7 @@ fun autoConnectPeripheral(peripheral: BluetoothPeripheral)
fun autoConnectPeripheralsBatch(batch: Set<BluetoothPeripheral>)
```

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 {
Expand All @@ -96,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
Expand All @@ -114,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

Expand All @@ -138,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.

Expand All @@ -157,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

Expand All @@ -182,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.

Expand Down Expand Up @@ -243,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)
Expand Down
15 changes: 7 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 31
compileSdkVersion 33

defaultConfig {
applicationId "com.welie.blessedexample"
minSdkVersion 26
targetSdkVersion 31
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand All @@ -31,15 +31,14 @@ 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.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 "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"

testImplementation 'junit:junit:4.13.2'

implementation project(':blessed')
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/*
* Copyright (c) Koninklijke Philips N.V. 2020.
* All rights reserved.
*/
package com.welie.blessedexample

class BloodPressureMeasurementStatus internal constructor(measurementStatus: Int) {
Expand Down
38 changes: 25 additions & 13 deletions app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.welie.blessedexample

import android.bluetooth.BluetoothAdapter
import android.content.Context
import com.welie.blessed.*
import kotlinx.coroutines.*
Expand All @@ -22,7 +23,7 @@ internal class BluetoothHandler private constructor(context: Context) {
val weightChannel = Channel<WeightMeasurement>(UNLIMITED)

private fun handlePeripheral(peripheral: BluetoothPeripheral) {
scope.launch(Dispatchers.IO) {
scope.launch {
try {
val mtu = peripheral.requestMtu(185)
Timber.i("MTU is $mtu")
Expand All @@ -43,12 +44,12 @@ 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()) {
// 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)
Expand All @@ -75,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
Expand All @@ -101,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++
Expand Down Expand Up @@ -208,11 +208,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) {
Expand All @@ -232,7 +234,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")
Expand Down Expand Up @@ -295,18 +297,28 @@ 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 {
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 -> {
}
}
}

central.observeAdapterState { state ->
when (state) {
BluetoothAdapter.STATE_ON -> startScanning()
}
}

startScanning()
}
}
8 changes: 4 additions & 4 deletions app/src/main/java/com/welie/blessedexample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class MainActivity : AppCompatActivity() {
if (missingPermissions.isNotEmpty()) {
requestPermissions(missingPermissions, ACCESS_LOCATION_REQUEST)
} else {
permissionsGranted()
checkIfLocationIsNeeded()
}
}

Expand All @@ -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()
}
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/*
* Copyright (c) Koninklijke Philips N.V., 2017.
* All rights reserved.
*/
package com.welie.blessedexample

/**
Expand Down
16 changes: 0 additions & 16 deletions app/src/test/java/com/welie/blessedexample/ExampleUnitTest.kt

This file was deleted.

Loading