Bluetooth is the wireless protocol designed to be used with devices that are, roughly speaking, in the same room. So it operates at shorter range than Wifi, but longer range than NFC. But unlike a Wifi device, Bluetooth devices are usually battery powered, so that reduced range offers some power savings.
A key trait of classic bluetooth is that it is always transmitting, even when no data is being sent. In the 4.0 version of the Bluetooth spec, Bluetooth Low Energy was added as a mode that changes this dynamic so that either end of the connection only uses power to transmit when there is data to be sent.
These days, all smartphones support BLE, and the most common consumer devices that use it are wearable health devices like FitBits, heart rate monitors, and so on. But due to the simple nature of BLE, it’s straightforward to use an Android phone to connect to and communicate with BLE devices.
These are the general steps your app will need to do when establishing a connection with a BLE device.
The Bluetooth spec outlines a number of profiles (protocols for communicating) like A2DP for audio streaming, HSP/HFP for phone calls, AVRCP for controlling media, and so on. But BLE relies on GATT, the Generic Attribute Profile, which just has a few simple functions. GATT is all about reading and writing Characteristics, the Bluetooth jargon term for what are essentially key-value stores.
Understanding the hierarchy of accessing data via GATT is useful. Here is how the data accessible via GATT on a device is organized.
Below you will find the code necessary to connect your Android app to a BLE device. For greater context, you can see the full video of my recent talk at RIoT Developer Day (including me walking through the code) or review the slides from my presentation. I hope that helps!
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
fun scanForDevices(context: Context, serviceUUID: UUID) {
val adapter = BluetoothAdapter.getDefaultAdapter()
if (!adapter.isEnabled) {
//handle error
}
val uuid = ParcelUuid(serviceUUID)
val filter = ScanFilter.Builder().setServiceUuid(uuid).build()
val filters = listOf(filter)
val settings = ScanSettings
.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
context.runWithPermissions(Manifest.permission.ACCESS_COARSE_LOCATION) {
adapter.bluetoothLeScanner.startScan(filters, settings, callback)
}
}
private val callback = object : ScanCallback() {
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.forEach { result ->
deviceFound(result.device)
}
}
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let { deviceFound(result.device) }
}
override fun onScanFailed(errorCode: Int) {
handleError(errorCode)
}
}
private fun deviceFound(device: BluetoothDevice) {
device.connectGatt(context, true, gattCallback)
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
gatt?.requestMtu(256)
gatt?.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
val characteristic = gatt?.getService(expandUuid(0x180F)) // Battery service
?.getCharacteristic(expandUuid(0x2A19)) // Battery life
gatt?.readCharacteristic(characteristic)
gatt?.setCharacteristicNotification(characteristic, true)
characteristic?.value = byteArrayOf(50)
gatt?.writeCharacteristic(characteristic)
}
override fun onCharacteristicRead(
gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
) { /* ... */ }
override fun onCharacteristicWrite(
gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
) { /* ... */}
override fun onCharacteristicChanged(
gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
) {
characteristic?.let {
val batteryLife = characteristic.value[0].toInt()
Log.d(TAG, "Battery life is: $batteryLife")
}
}
}