Previously in this series we’ve looked at some of the background of Bluetooth LE and set up a simple Activity / Service framework. In this article we’ll get in to the nitty gritty of Bluetooth LE and look at device discovery.
Device discovery is, quite simply, the process of looking around and finding devices within Bluetooth range. The first thing that we must do is add the necessary permissions to out Manifest otherwise we’re going to fall at the very first hurdle. The permissions that we require are android.permission.BLUETOOTH
(for general Bluetooth use) and android.permission.BLUETOOTH_ADMIN
(for additional tasks such as service discovery).
Just before we dive in, it’s worth explaining that our BleService will operate as a state machine, and perform a different task within each state. The first of these state’s that we’ll consider is SCANNING
state. It will enter SCANNING
state after receiving a MSG_START_SCAN
Message:
private static class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { BleService service = mService.get(); if (service != null) { switch (msg.what) { . . . case MSG_START_SCAN: service.startScan(); Log.d(TAG, "Start Scan"); break; default: super.handleMessage(msg); } } } }
Our startScan()
method begins the scan:
public class BleService extends Service implements BluetoothAdapter.LeScanCallback { private final MapmDevices = new HashMap (); public enum State { UNKNOWN, IDLE, SCANNING, BLUETOOTH_OFF, CONNECTING, CONNECTED, DISCONNECTING } private BluetoothAdapter mBluetooth = null; private State mState = State.UNKNOWN; . . . private void startScan() { mDevices.clear(); setState(State.SCANNING); if (mBluetooth == null) { BluetoothManager bluetoothMgr = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetooth = bluetoothMgr.getAdapter(); } if (mBluetooth == null || !mBluetooth.isEnabled()) { setState(State.BLUETOOTH_OFF); } else { mHandler.postDelayed(new Runnable() { @Override public void run() { if (mState == State.SCANNING) { mBluetooth.stopLeScan( BleService.this); setState(State.IDLE); } } }, SCAN_PERIOD); mBluetooth.startLeScan(this); } } }
The first thing this does is ensure that Bluetooth is enabled on the device, and prompt the user to turn it on if it isn’t. The process for doing this is pretty simple. First we obtain an instance of the BluetoothService, which is one of the Android system services. From the BluetoothService object we can get an instance of a BluetoothAdapter which represents the Bluetooth radio on the device. We can then perform a null check, and then call isEnabled()
to determine that Bluetooth is available and switched on. If it is, then we’re good to go, but if it isn’t we set the appropriate state. When this state changes a message will be sent to all clients of this service (in this case our Activity):
public class BleActivity extends Activity { private final int ENABLE_BT = 1; . . . private void enableBluetooth() { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, ENABLE_BT); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == ENABLE_BT) { if(resultCode == RESULT_OK) { //Bluetooth connected, we may continue startScan(); } else { //The user has elected not to turn on //Bluetooth. There's nothing we can do //without it, so let's finish(). finish(); } } else { super.onActivityResult(requestCode, resultCode, data); } } private void startScan() { mRefreshItem.setEnabled(false); mDeviceList.setDevices(this, null); mDeviceList.setScanning(true); Message msg = Message.obtain(null, BleService.MSG_START_SCAN); if (msg != null) { try { mService.send(msg); } catch (RemoteException e) { Log.w(TAG, "Lost connection to service", e); unbindService(mConnection); } } } }
In order to prompt the user to turn on Bluetooth, we can use a system service designed for this which will ensure that the user experience remains consistent across the platform. It is possible to turn on Bluetooth programatically, but prompting the user is the recommended approach. This is really quite easy, we start another Activity to prompt the user which we do by using the appropriate action that is defined within BluetoothAdapter (lines 7-9), and then handle the result once that Activity finishes (the onActivityResult()
method).
So far there’s nothing specific to BluetoothLE, these are steps that you’d have to go through with standard Bluetooth as well.
The next thing that we want to do is scan for BluetoothLE devices. This is actually really easy because the BluetoothAdapter class contains a startLeScan()
method which does precisely that! This method takes a single argument which is a BluetoothAdapter.LeScanCallback instance – which is what will receive callbacks during the scan:
public class BleService extends Service implements BluetoothAdapter.LeScanCallback private static final String DEVICE_NAME = "SensorTag"; . . . private void startScan() { mDevices.clear(); setState(State.SCANNING); if (mBluetooth == null) { BluetoothManager bluetoothMgr = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetooth = bluetoothMgr.getAdapter(); } if (mBluetooth == null || !mBluetooth.isEnabled()) { setState(State.BLUETOOTH_OFF); } else { mHandler.postDelayed(new Runnable() { @Override public void run() { if (mState == State.SCANNING) { mBluetooth.stopLeScan( BleService.this); setState(State.IDLE); } } }, SCAN_PERIOD); mBluetooth.startLeScan(this); } } @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { if (device != null && !mDevices.containsValue(device) && device.getName() != null && device.getName().equals(DEVICE_NAME)) { mDevices.put(device.getAddress(), device); Message msg = Message.obtain(null, MSG_DEVICE_FOUND); if (msg != null) { Bundle bundle = new Bundle(); String[] addresses = mDevices.keySet() .toArray(new String[mDevices.size()]); bundle.putStringArray(KEY_MAC_ADDRESSES, addresses); msg.setData(bundle); sendMessage(msg); } Log.d(TAG, "Added " + device.getName() + ": " + device.getAddress()); } } }
One important thing about onStartLeScan()
is that it does only that – starts the scan. We must also stop it. Depending on requirements, it may be correct to stop the scan as soon as a device is found, but in our case we want to scan for a fixed period, so we use postDelayed()
to schedule stopLeScan()
to be called at a fixed point in the future.
The onLeScan() method is called each time the Bluetooth adapter receives any advertising message from a BLE device whilst it is scanning. Devices will typically send out an advertising message 10 times a second while they are in advertising mode, so we must be careful to only respond to new devices that we encounter during the scan. We do this by keeping a map of the devices found (mapped by their MAC addresses, which will be useful later), and check whether we know about that device when the onLeScan()
method is called during the scan.
We also need to filter only the devices that we’re interested in. Normally you would do this based upon characteristics (more on this later in the series) but the SensorTag documentation suggests that, for the SensorTag, we should match on the device name “SensorTag”, so we do that.
It’s worth mentioning here that the host needs to scan while sensors are advertising their presence. This is the heart of security on BLE – sensors must be told by the user to advertise their presence before they can be discovered. Once they have been discovered and a trust relationship has been established between the host and the sensor, then the host may subsequently be able to connect directly to the sensor without it being put in to advertising mode although this behaviour amy vary on different sensors.
Whenever we detect a new device, we add it to the map, and also package up a String array of the MAC addresses of all discovered devices, and put this in to a MSG_DEVICE_FOUND
Message which is sent to the Activity.
It is worth mentioning that our Service is running on the UI thread, but we really don’t need to worry about blocking operations on the UI thread. The call to start the LE scan is asynchronous, and kicks off a background task which makes callbacks to our onLeScan
method. Provided we don’t do any intensive tasks in onLeScan
we don’t need to worry about doing any of this in a background thread.
The Activity also operates as a state machine, using the state of the BleService to change the UI mode. It can enable and disable the “Refresh” menu depending on whether the BleService is in SCANNING
state, and also switches between fragments to display a list of devices (when in discovery). Whenever the Activity receives a MSG_DEVICE_FOUND
Message it will update the list of devices displayed. I won’t bother to explain all of the UI code here as it is not specific to BLE, but it’s available in the published sources if you want to have a look through.
So if we run this and start a scan while there are devices adversing close by, then we’ll see them listed:
So we have basic device discovery working, then next thing that we need to do is to connect to one of the sensor(s) that we’ve discovered, and we’ll look at that in the next article.
The source code for this article is available here.
© 2014, Mark Allison. All rights reserved.
Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.