Use Bluetooth Low Energy(BLE) to connect devices in an Android app
Introduction
Due to a project requirement, I need to make an Android app to connect external devices and read device states and write wife configures to them. I want to share some basic concepts about how to make this kind of app and issues needed to be considered.
What should we do?
The expected flow of an Android app is as following :
- Turn on BLE
- Scan nearby devices
- Connect to devices
- Read / Write device states
- Disconnect devices
Turn on BLE
Before writing codes for turning on BLE, I have to explain what is different from Bluetooth Low Energy (BLE) and Bluetooth (BT). (It takes me one day to implement a BT connection but what I actually want is a BLE connection…)
Bluetooth Vs. Bluetooth Low Energy: What’s The Difference?
In summary, Bluetooth and Bluetooth Low Energy are used for very different purposes. Bluetooth can handle a lot of data, but consumes battery life quickly and costs a lot more. BLE is used for applications that do not need to exchange large amounts of data, and can therefore run on battery power for years at a cheaper cost. It all depends on what you’re trying to accomplish.
You can read BLE documents from here for more details.
Turn on BLE
Add permissions in Android Manifest
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>--- The location permission will be explained later<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
or
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Setup BLE adapter
private val mBluetoothAdapter : BluetoothAdapter =(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapterTurn on BLE if it does not enable
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}Scan nearby devices
The BluetoothAdpater.startLeScan() is deprecated in API 21. Use BluetoothAdapter.BluetoothScanner.startScan() instead.
mBluetoothAdapter.bluetoothLeScanner.startScan(object : SanCallback() { ... })There is an another overriding function of startScan() as following.
public void startScan(List<ScanFilter> filters, ScanSettings settings, ScanCallback callback)If you want to filter a specific service UUID, do
val filters = ArrayList<ScanFilter>()
filters.add(ScanFilter.Builder().setServiceUuid(YOUR_UUID)).build())To prevent scanning nothing (It is really frequent to find nothing…), do
val scanSettings = ScanSettings.Builder()scanSettings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
*** In the startScan(…) document, you can see the location permission requirement in the comments or no devices will be discoverd.
Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
An app must hold
{@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
{@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
in order to get results.
Connect to devices
In the ScanCallback(), there are some functions you can override.
- onScanFailed(errorCode : Int)
- It will receive some error codes if the scanner encounter problems
2. onScanResult(callbackType : Int, result : ScanResult?)
- It will return scan results included device information in ScanResult.device. This function will be called multiple times if a device send a response for the scanning request. Therefore, the same device maybe trigger this function many times.
- Called in UI thread.
After getting a device information from onScanResult(), we can obtain a BluetoothGatt by involking following codes
val device = result.device //BluetoothDeviceval gatt : BluetoothGatt = device.connectGatt(context, false, object : BluetoothGattCallback() { ... })
Here, we have to implement BluetookGattCallback. There are some useful functions.
- onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int)
when (newState) {
(BluetoothProfile.STATE_CONNECTED) -> {
gatt!!.discoverServices() // To list services provided by the device
}
(BluetoothProfile.STATE_DISCONNECTED) -> {...}
(BluetoothProfile.STATE_CONNECTING) -> {...}
(BluetoothProfile.STATE_DISCONNECTING) -> {...}
else -> {...}
}- To list services of the device, call
BluetoothGatt.discoverServices()if the state is BluetoothProfile.STATE_CONNECTED. Disconnect the gatt by calling following codes if the state is not normal.
gatt?.disconnect()
gatt?.close()2. onServicesDiscovered(gatt: BluetoothGatt?, status: Int)
After calling discoverService(), the connected device will return services containing characteristics.
Characteristic — A characteristic contains a single value and 0-n descriptors that describe the characteristic’s value. A characteristic can be thought of as a type, analogous to a class.
Descriptor — Descriptors are defined attributes that describe a characteristic value. For example, a descriptor might specify a human-readable description, an acceptable range for a characteristic’s value, or a unit of measure that is specific to a characteristic’s value.
Service — A service is a collection of characteristics. For example, you could have a service called “Heart Rate Monitor” that includes characteristics such as “heart rate measurement.” You can find a list of existing GATT-based profiles and services on bluetooth.org.
from https://developer.android.com/guide/topics/connectivity/bluetooth-le
*** The most important thing is that callbacks in BluetoothGattCallback is not called in UI thread. We can use a handler which created in UI thread or given a desired thread looper to avoid race conditions.
Read / Write device characteristics
After discovering services, we can get a service and characteristics
val service : BluetoothGattService? = BluetoothGatt.getService(SERVICE_UUID)val char : BluetoothGattCharacteristic? = service.getCharacteristic(CHAR_UUID)
Then, call
BluetoothGatt.readCharacteristic(char)
// We can check the return boolean valueto read specific characteristic value in
BluetoothGattCallback() {...
override fun onCharacteristicRead(gatt: BluetoothGatt?, char: BluetoothGattCharacteristic?, status: Int) { ... }...
}
The data of characteristic is stored in BluetoothGattCharacteristic.data().
With the same concept, we firstly set a value for a characteristic and write to the device
val char : BluetoothGattCharacteristic? = {GET_CHAR_AS_ABOVE}char.setValue({YOUR_DATA})
BluetoothGatt.writeCharacteristic(char)
// We can check the return boolean value
Finally, we can check what we write in
BluetoothGattCallback() {...
override fun onCharacteristicWrite(gatt: BluetoothGatt?, char: BluetoothGattCharacteristic?, status: Int) { ... }...
}
*** If we send to multiple read or write command to the device, only the first command will be executed and call onCharacteriscticWrite(…) or onCharacteristicRead(…). To solve this problem, we can use a list to queue commands and execute at one time.
Disconnect devices
We has mentioned on above
BluetoothGatt.disconnect()
BluetoothGatt.close()Overall, we breifly go through Android BLE process. I hope this post can help you. Thank you.