In my last experiment I used the Android Things developer console to create a factory image for my bus stop project. I then created an OTA update to update the bus stop apk ‘in the field.’ I concluded that experiment wondering how I was going to get my network credentials onto the device (without using adb). If your device doesn’t have a touchscreen or keyboard how are you going to get it onto your end user’s network. One possible solution to this problem is to use a companion app on the user’s phone to collect configuration information. With a companion app you get all of the UI niceness provided by that platform for ‘free.’ You still need to get the configuration from the phone to your Android Things device. This is where Bluetooth Low Energy comes in.
Bluetooth Low Energy
Bluetooth Low Energy is exactly the kind of short range wireless technology that can help out here. It’s relatively easy to implement using Android APIs and there’s no need for the device to be on a network to configure it. I can start with the BLE sample for Android Things that has almost everything I’m going to need.
For my bus stop project I have two things to configure that require end user input. I need to configure which stop number I want to display bus data for. I also need to get it onto a network to get real-time bus schedule information from a web API.
I’m going to make my bus stop a BLE peripheral device. This means creating a Bluetooth GATT server and adding some characteristics and descriptors to it. If none of what I just said makes any sense take a few minutes to read this BLE tutorial by adafruit.
I’m going to use nRF connect by Nordic Semiconductor for development to get the Android Things side of the equation going. I can deffer development of my own phone app.
Profile, service, characteristic, descriptor
I need to create two services; one for configuring the bus stop, and one for configuring the network. Each service contains characteristics that enable configuration of the device.
The configuration service has a single characteristic; the bus stop number. This is the id of the bus stop as defined by Metlink. Most stops are 4 digit numbers but a few contain letters, e.g., “WELL” for the main train station. I’m going to transfer a string of text via this characteristic.
The network service has a three characteristics; SSID, password, and status. The status characteristic has a descriptor that I can use to subscribe to notifications. The arrangement of services, characteristics and descriptors make up the bus stop’s Generic ATTribute profile (GATT) shown in the image below.
Android BLE API
Using the Android BLE API to make a peripheral means that I need to create a GATT server containing the services and the handlers for the read and write requests from a client. To do this I need to:
- get a handle to the system BluetoothAdapter (a representation of the device’s Bluetooth radio)
- configure an advertising packet that will be broadcast by the device.
- create a GATT server on the device to handle client connections
- create the logic in a GATT server callback to handle interaction between the device characteristics and descriptor and the client.
Getting the Bluetooth adapter
The first step to performing any Bluetooth operations with Android is to get hold of the Bluetooth adapter. This means asking the system for a reference to the Bluetooth manager, then asking the manager for the adapter, then enabling the adapter.
private void initBt(Context context) { bluetoothManager = (BluetoothManager) context.getSystemService(BLUETOOTH_SERVICE); BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null) { Log.e(TAG, "initBt: Adapter null!"); return; } IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); context.registerReceiver(bluetoothReceiver, filter); if (!bluetoothAdapter.isEnabled()) { Log.d(TAG, "initBt: enabling Bluetooth"); bluetoothAdapter.enable(); } else { Log.d(TAG, "initBt: starting services"); startAdvertising(); startServer(); } }
In the code above I’m registering a Broadcast Receiver to respond to changes in Bluetooth state. This will tell me when the adapter is enabled and disabled. Because enabling the adapter can take a while, and I can’t block the main thread waiting for this to happen. If the adapter is disabled I enable it, deferring the adapter configuration until the Broadcast Receiver is invoked. If the adapter is already enabled I can start advertising and start the BLE server immediately.
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); switch (state) { case BluetoothAdapter.STATE_ON: startAdvertising(); startServer(); break; case BluetoothAdapter.STATE_OFF: stopServer(); stopAdvertising(); break; default: // Do nothing } } };
Start advertising
I tell the Bluetooth adapter to start advertising whenever its state changes to STATE_ON. To start advertising I get a BluetoothAdvertiser from the BluetoothAdapter and configure it with AdvertisingSettings and AdvertisingData. I also change the name of the adapter to “Bus Stop.” This is the name I’ll see in nrfConnect. The advertising data packet size is limited to 31 bytes of data, so I’m only adding the UUID for one of my two services. It doesn’t matter that only one of the services is advertised, both services will still be part of the profile.
public void startAdvertising() { BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); bluetoothAdapter.setName("Bus Stop"); bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); if (bluetoothLeAdvertiser == null) { Log.w(TAG, "Failed to create advertiser"); return; } AdvertiseSettings settings = new AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) .setConnectable(true) .setTimeout(0) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) .build(); AdvertiseData data = new AdvertiseData.Builder() .setIncludeDeviceName(true) .setIncludeTxPowerLevel(false) .addServiceUuid(new ParcelUuid(BusStopProfile.CONFIG_SERVICE)) .build(); bluetoothLeAdvertiser .startAdvertising(settings, data, advertiseCallback); }
The advertiseCallback is defined as:
private AdvertiseCallback advertiseCallback = new AdvertiseCallback() { @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { Log.i(TAG, "LE Advertise Started."); } @Override public void onStartFailure(int errorCode) { Log.w(TAG, "LE Advertise Failed: "+errorCode); } };
I’m not doing anything with the callback except logging the result. The next task required to get the system to work is to start a GATT server.
Start the server
private void startServer() { bluetoothGattServer = bluetoothManager.openGattServer(context, bluetoothGattServerCallback); if (bluetoothGattServer == null) { Log.w(TAG, "Unable to create GATT server"); return; } bluetoothGattServer.addService(BusStopProfile.createConfigService()); bluetoothGattServer.addService(BusStopProfile.createNetworkService()); }
The openGattServer method of the BluetoothManager takes a BluetoothGattServerCallback, I’ll define this a little later. Once I have an instance of the GATT server I add my two services to it. This is where the profile of the device is defined as services, characteristics and descriptors.
Create the services
To create the services I need to add information about the characteristics and descriptor to a BluetoothGattService.
public static BluetoothGattService createConfigService() { BluetoothGattService service = new BluetoothGattService(CONFIG_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY); // read / write the bus stop number BluetoothGattCharacteristic configCharacteristic = new BluetoothGattCharacteristic(STOP_NUMBER, BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); service.addCharacteristic(configCharacteristic); return service; }
Here CONFIG_SERVICE and STOP_NUMBER are UUIDs that I’ve defined for my device. I generated the UUIDs with uuidgen. The network service is similar except that I also add a descriptor to one of the status characteristic.
Implement the callback
With all of the above the system will nearly work. All I need to do now is define what will happen when a client interacts with my server. This is done by implementing the BluetoothGattServerCallback that I passed to the Bluetooth manager earlier. the callback is invoked whenever a read or write request occurs on the characteristics and descriptor.
private BluetoothGattServerCallback bluetoothGattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { Log.i(TAG, "BluetoothDevice CONNECTED: " + device); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device); //Remove device from any active subscriptions devices.remove(device); } } @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if (BusStopProfile.STOP_NUMBER.equals(characteristic.getUuid())) { stop = new String(value); Log.d(TAG, "onDescriptorWriteRequest: Stop number " + stop); if (listener != null) { listener.onStopUpdated(stop); } if (responseNeeded) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null); } } else if (BusStopProfile.WIFI_SSID.equals(characteristic.getUuid())) { Log.d(TAG, "onDescriptorWriteRequest: SSID " + new String(value)); if (responseNeeded) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null); } } else if (BusStopProfile.WIFI_PASS.equals(characteristic.getUuid())) { Log.d(TAG, "onDescriptorWriteRequest: PASS " + new String(value)); } else { Log.w(TAG, "Unknown descriptor write request"); if (responseNeeded) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null); } } } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { if (BusStopProfile.STOP_NUMBER.equals(characteristic.getUuid())) { Log.d(TAG, "Config descriptor read"); bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, stop.getBytes()); } else if (BusStopProfile.WIFI_SSID.equals(characteristic.getUuid())) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, BusStopProfile.getCurrentSsid(context).getBytes()); } else if (BusStopProfile.WIFI_STAT.equals(characteristic.getUuid())) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, BusStopProfile.isConnected(context) ? new byte[]{1} : new byte[]{0}); } else { // Invalid characteristic Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid()); bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null); } } @Override public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { if (BusStopProfile.WIFI_STAT_CONFIG.equals(descriptor.getUuid())) { Log.d(TAG, "Config descriptor read"); byte[] returnValue; if (devices.contains(device)) { returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; } else { returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; } bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, returnValue); } else { Log.w(TAG, "Unknown descriptor read request"); bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null); } } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if (BusStopProfile.WIFI_STAT_CONFIG.equals(descriptor.getUuid())) { Log.d(TAG, "onDescriptorWriteRequest: WIFI_STAT_CONFIG"); if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) { Log.d(TAG, "Subscribe device to notifications: " + device); devices.add(device); } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) { Log.d(TAG, "Unsubscribe device from notifications: " + device); devices.remove(device); } if (responseNeeded) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null); } } else { Log.w(TAG, "Unknown descriptor write request"); if (responseNeeded) { bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null); } } } };
Using nRF Connect
nRF Connect is a great little application made by Nordic Semiconductor. It’s an app that’s invaluable when you’re experimenting with BLE. I used it a lot, back when it was called Master Control Panel, during the development of the firmware for Spike. With nRF Connect you can scan for BLE devices, and read and write characteristics and descriptors. With my bus stop application running on my development board, and no stop configured the display shows the imaginative “No Stop Configured” message.

No Stop Configured (yet)
When I scan with nRF Connect, the bus stop shows up as Bus Stop.

Bus Stop Scan
Connecting to it reads the services, characteristics and descriptors. I can expand the services to see the bus stop characteristic. All services, characteristics and descriptors show up as “unknown” because they are the random UUIDs I generated. If I had used UUIDs defined by The Bluetooth SIG, they would have names in the list, but there are no characteristics defined for setting a stop number in a bus stop display. This doesn’t matter because my companion app will know that each of the characteristic UUIDs mean.

Bus Stop Services

Bus Stop Number Characteristic
I can write to the bus stop number characteristic (84ba0101-d691-4bf5-8f5a-093f328cf182″) from the stop number service (84ba0100-d691-4bf5-8f5a-093f328cf182). I’ve saved my local stop number in nRF Connect, so that I don’t need to remember it or type it in.

Writing My Stop
Once it’s written to the characteristic, the bus stop display is updated with departures from the new stop.

Showing Departures for Configured Stop
I do a similar thing for network SSID and Password.
Security
This solution, as it stands, is terribly insecure. There’s nothing stopping anybody from scanning the device, changing the configuration options, and vandalising the bus stop display. If someone knew what they were doing they could change the stop number, so that the display shows busses for another stop. To make this production ready I’d need to implement some kind of authentication mechanism to ensure that only trusted devices are changing the configuration.
Conclusion
Presented here is a way to enable the configuration of an Android Things device without it having any physical input method on the device, and without it already being connected to a network. This was done using Android APIs to turn the device into a BLE peripheral. There are a few steps involved, but nothing difficult about the process. The hardest part was to define what services, characteristics, and descriptors made up the device’s profile. That involved doing a little research to understand what those terms meant. To bring this idea to market I’m going to have to consider security to implement some sort of authentication between the device and the end user’s phone. I’ll also need to write the companion app, (maybe for the MVP I can convince people to use nrf Connect – just kidding).