Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Project #34

Merged
merged 9 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up Java 17
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
distribution: "temurin"
java-version: 17
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
Expand Down
106 changes: 77 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# BLESSED for Android with Coroutines - BLE made easy

[![](https://jitpack.io/v/weliem/blessed-android-coroutines.svg)](https://jitpack.io/#weliem/blessed-android-coroutines)
[![Jitpack Link](https://jitpack.io/v/weliem/blessed-android-coroutines.svg)](https://jitpack.io/#weliem/blessed-android-coroutines)
[![Downloads](https://jitpack.io/v/weliem/blessed-android-coroutines/month.svg)](https://jitpack.io/#weliem/blessed-android-coroutines)
[![Android Build](https://github.com/weliem/blessed-android-coroutines/actions/workflows/gradle.yml/badge.svg)](https://github.com/weliem/blessed-android-coroutines/actions/workflows/gradle.yml)

BLESSED is a very compact Bluetooth Low Energy (BLE) library for Android 8 and higher, that makes working with BLE on Android very easy. It is powered by Kotlin's **Coroutines** and turns asynchronous GATT methods into synchronous methods! It is based on the [Blessed](https://github.com/weliem/blessed-android) Java library and has been rewritten in Kotlin using Coroutines.

## Installation

This library is available on Jitpack. Include the following in your gradle file:
This library is available on Jitpack. Include the following in your projects's build.gradle file:

```groovy
allprojects {
Expand All @@ -17,23 +17,54 @@ allprojects {
maven { url 'https://jitpack.io' }
}
}
```

Include the following in your app's build.gradle file under `dependencies` block:

```groovy
dependencies {
...
implementation "com.github.weliem:blessed-android-coroutines:$version"
}
```
where `$version` is the latest published version in Jitpack [![](https://jitpack.io/v/weliem/blessed-android-coroutines.svg)](https://jitpack.io/#weliem/blessed-android-coroutines)

where `$version` is the latest published version in Jitpack [![Jitpack](https://jitpack.io/v/weliem/blessed-android-coroutines.svg)](https://jitpack.io/#weliem/blessed-android-coroutines)

### Adding permissions

If you plan on supporting older devices that are on Android 11 and below, then you need to add the below permissions to your AndroidManifest.xml file:

```xml
<!-- Needed to target Android 11 and lower -->
<!-- Link: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare-android11-or-lower-->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />

<!-- Link: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions -->
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
```

## Overview of classes

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 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 `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)

Expand Down Expand Up @@ -73,11 +104,10 @@ The scanning functions are not suspending functions and simply use a lambda func

**Note** Only 1 of these 4 types of scans can be active at one time! So call `stopScan()` before calling another scan.



## Connecting to devices

There are 3 ways to connect to a device:

```kotlin
suspend fun connectPeripheral(peripheral: BluetoothPeripheral): Unit
fun autoConnectPeripheral(peripheral: BluetoothPeripheral)
Expand All @@ -101,22 +131,26 @@ The method `autoConnectPeripheral` will **not suspend** and is for re-connecting
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
val peripheral = central.getPeripheral("CF:A9:BA:D9:62:9E")
```

After issuing a connect call, you can observe the connection state of peripherals:

```kotlin
central.observeConnectionState { peripheral, state ->
Timber.i("Peripheral ${peripheral.name} has $state")
}
```

To disconnect or to cancel an outstanding `connectPeripheral()` or `autoConnectPeripheral()`, you call:

```kotlin
suspend fun cancelConnection(peripheral: BluetoothPeripheral): Unit
```
The function will suspend until the peripheral is disconnected.

The function will suspend until the peripheral is disconnected.

## Service discovery

Expand Down Expand Up @@ -145,10 +179,12 @@ All methods are **suspending** and will return the result of the operation. The
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.

There are 2 ways to specify which characteristic to use in the read/write method:

- Using its `serviceUUID` and `characteristicUUID`
- Using the `BluetoothGattCharacteristic` reference directly

For example:

```kotlin
peripheral.getCharacteristic(DIS_SERVICE_UUID, MANUFACTURER_NAME_CHARACTERISTIC_UUID)?.let {
val manufacturerName = peripheral.readCharacteristic(it).asString()
Expand Down Expand Up @@ -177,22 +213,26 @@ peripheral.getCharacteristic(BLP_SERVICE_UUID, BLOOD_PRESSURE_MEASUREMENT_CHARAC
To stop observing notifications you call `peripheral.stopObserving(characteristic: BluetoothGattCharacteristic)`

## Bonding

BLESSED handles bonding for you and will make sure all bonding variants work smoothly. During the process of bonding, you will be informed of the process via a number of callbacks:

```kotlin
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:
* 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.

- 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.

It is also possible to remove a bond by calling `removeBond`. Note that this method uses a hidden Android API and may stop working in the future. When calling the `removeBond` method, the peripheral will also disappear from the settings menu on the phone.

Lastly, it is also possible to automatically issue a PIN code when pairing. Use the method `central.setPinCodeForPeripheral` to register a 6 digit PIN code. Once bonding starts, BLESSED will automatically issue the PIN code and the UI dialog to enter the PIN code will not appear anymore.

## Requesting a higher MTU to increase throughput

The default MTU is 23 bytes, which allows you to send and receive byte arrays of MTU - 3 = 20 bytes at a time. The 3 bytes overhead are used by the ATT packet. If your peripheral supports a higher MTU, you can request that by calling:

```kotlin
Expand All @@ -205,57 +245,65 @@ If you simply want the highest possible MTU, you can call `peripheral.requestMtu
Once the MTU has been set, you can always access it by calling `peripheral.currentMtu`. If you want to know the maximum length of the byte arrays that you can write, you can call the method `peripheral.getMaximumWriteValueLength()`. Note that the maximum value depends on the write type you want to use.

## Long reads and writes
The library also supports so called 'long reads/writes'. You don't need to do anything special for them. Just read a characteristic or descriptor as you normally do, and if the characteristic's value is longer than MTU - 1, then a series of reads will be done by the Android BLE stack. But you will simply receive the 'long' characteristic value in the same way as normal reads.

The library also supports so called 'long reads/writes'. You don't need to do anything special for them. Just read a characteristic or descriptor as you normally do, and if the characteristic's value is longer than MTU - 1, then a series of reads will be done by the Android BLE stack. But you will simply receive the 'long' characteristic value in the same way as normal reads.

Similarly, for long writes, you just write to a characteristic or descriptor and the Android BLE stack will take care of the rest. But keep in mind that long writes only work with `WriteType.WITH_RESPONSE` and the maximum length of your byte array should be 512 or less. Note that not all peripherals support long reads/writes so this is not guaranteed to work always.

## Status codes

When connecting or disconnecting, the callback methods will contain a parameter `HciStatus status`. This enum class will have the value `SUCCESS` if the operation succeeded and otherwise it will provide a value indicating what went wrong.

Similarly, when doing GATT operations, the callbacks methods contain a parameter `GattStatus status`. These two enum classes replace the `int status` parameter that Android normally passes.

## Bluetooth 5 support

As of Android 8, Bluetooth 5 is natively supported. One of the things that Bluetooth 5 brings, is new physical layer options, called **Phy** that either give more speed or longer range.
The options you can choose are:
* **LE_1M**, 1 mbit PHY, compatible with Bluetooth 4.0, 4.1, 4.2 and 5.0
* **LE_2M**, 2 mbit PHY for higher speeds, requires Bluetooth 5.0
* **LE_CODED**, Coded PHY for long range connections, requires Bluetooth 5.0

- **LE_1M**, 1 mbit PHY, compatible with Bluetooth 4.0, 4.1, 4.2 and 5.0
- **LE_2M**, 2 mbit PHY for higher speeds, requires Bluetooth 5.0
- **LE_CODED**, Coded PHY for long range connections, requires Bluetooth 5.0

You can set a preferred Phy by calling:

```kotlin
suspend fun setPreferredPhy(txPhy: PhyType, rxPhy: PhyType, phyOptions: PhyOptions): Phy
```

By calling `setPreferredPhy()` you indicate what you would like to have but it is not guaranteed that you get what you ask for. That depends on what the peripheral will actually support and give you.
If you are requesting `LE_CODED` you can also provide PhyOptions which has 3 possible values:
* **NO_PREFERRED**, for no preference (use this when asking for LE_1M or LE_2M)
* **S2**, for 2x long range
* **S8**, for 4x long range


- **NO_PREFERRED**, for no preference (use this when asking for LE_1M or LE_2M)
- **S2**, for 2x long range
- **S8**, for 4x long range

The result of this negotiation will be received as a `Phy` object that is returned by `setPrefferedPhy`

As you can see the Phy for sending and receiving can be different but most of the time you will see the same Phy for both.
If you don't call `setPreferredPhy()`, Android seems to pick `PHY_LE_2M` if the peripheral supports Bluetooth 5. So in practice you only need to call `setPreferredPhy` if you want to use `PHY_LE_CODED`.

You can request the current values at any point by calling:

```kotlin
suspend fun readPhy(): Phy
```

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:

* Beurer FT95 thermometer
* GRX Thermometer (TD-1241)
* Masimo MightySat
* Nonin 3230
* Indiehealth scale
* A&D 352BLE scale
* A&D 651BLE blood pressure meter
* Beurer BM57 blood pressure meter
* Soehnle Connect 300/400 blood pressure meter
* Polar H7/H10/OH1 heartrate monitors
* Contour Next One glucose meter
* Accu-Chek Instant glucose meter
- Beurer FT95 thermometer
- GRX Thermometer (TD-1241)
- Masimo MightySat
- Nonin 3230
- Indiehealth scale
- A&D 352BLE scale
- A&D 651BLE blood pressure meter
- Beurer BM57 blood pressure meter
- Soehnle Connect 300/400 blood pressure meter
- Polar H7/H10/OH1 heartrate monitors
- Contour Next One glucose meter
- Accu-Chek Instant glucose meter
17 changes: 9 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 32
namespace 'com.welie.blessedexample'

compileSdk 34

defaultConfig {
applicationId "com.welie.blessedexample"
minSdkVersion 26
targetSdkVersion 32
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand All @@ -31,13 +32,13 @@ android {

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.jakewharton.timber:timber:5.0.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 "androidx.core:core-ktx:1.12.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"


implementation project(':blessed')
Expand Down
26 changes: 23 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.welie.blessedexample">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">


<!-- Needed to target Android 11 and lower -->
<!-- Link:
https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare-android11-or-lower-->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />

<!-- Link: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions -->
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />

<application
android:allowBackup="true"
Expand All @@ -9,7 +28,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
25 changes: 14 additions & 11 deletions blessed/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'

android {
compileSdkVersion 32

namespace 'com.welie.blessed'

compileSdk 34

defaultConfig {
minSdkVersion 26
targetSdkVersion 32
minSdk 26
targetSdk 34

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -20,26 +23,26 @@ android {
}

kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

dependencies {
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 "androidx.core:core-ktx:1.12.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
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.13.8"
}

afterEvaluate {
Expand Down
Loading
Loading