Welcome to the third part of my AndroidThings tutorial. I wanted to have a way of learning more myself, and also share some of the things I've picked up along the way about AndroidThings. This series is made for anyone who wants to learn more about AndroidThings and created by the HacksterMIA community. Hopefully this becomes useful for many others!
If you missed the previous part, I recommend you check that out before continuing as this next part will build on top of what was covered in the previous one.
With that lets continue!
RecapSo in the last part we went over how to setup Android Things on a Raspberry Pi 3 and how to toggle a LED using a tactile button. We took the next steps on diving into Android Things and to make even greater progress on learning some of the basics. With this next part we will take it a step further and expand a bit on what we've made so far. More specifically, how we can extend Android Things to Android devices and have the two platforms work together.
Getting StartedSo before we start, lets just make sure we have what we need.
You will need an Android device, a physical device would be best as we will communicate with the Raspberry Pi over Bluetooth.
As with the previous part of this series, this is actually another significant jump as we are not only mixing Android devices with Android Things devices but also exploring a more advanced features, communicating via Bluetooth, between these two devices.
Hopefully you're excited to jump on in!
Update Android Things ProjectSo at this point I will assume that you have an Android device and it has been enabled for Developer mode. Now we're ready to update the code in the project.
Go ahead and open up the current project and if you would like to checkout the final version for this part go here.
You should have a separate project named 'mobile', if you grabbed from the repo on GitHub. If you don't have it and have been following along from scratch, well kudos to you! Let's make sure you add that project.
In Android Studio, File -> New -> New project... and name the project 'ButtonThings' or something you prefer. This will also likely be the name of the app when running it later on.
Click Next and be sure to have Phone and Tablet selected (this will create the mobile project).
Then Next -> Finish. The defaults should be fine for our needs. We'll create the simple UI later on.
Ask for PermissionBefore we add any code, lets go ahead and add the permissions we're going to need in order to have our devices talking.
We'll start with the mobile project, open up AndroidManifest.xml. Just outside the application tag, go ahead and add the following:
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission
android:name="android.permission.INTERNET"
android:required="true" />
The following simply requests that we can use Wifi, Bluetooth, and Internet capabilities of the mobile device. Now we may not use all these capabilities, but some permissions are dependencies, or required, in order to use request for other permissions.
Now let's go ahead and set up the permissions for the Android Things project. Open up AndroidManifest.xml in the things project. And again, just outside the application tag, go ahead and add the following:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="com.google.android.things.permission.MANAGE_INPUT_DRIVERS" />
<uses-permission android:name="com.google.android.things.permission.MANAGE_BLUETOOTH" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
android:required="true" />
Awesome! So with that done, we now have the required permissions set to run the projects once we've updated our code.
Some Android DevelopmentAlright, so let's get into some Android development. Our goal here is going to create our Android app that will communicate with the Raspberry Pi, and in order for this to happen we will have our Android device act as a server that the Raspberry Pi can connect to. Once connected the Android device will be able to send messages to the Raspberry Pi, and the Pi will be able to act on these messages. These messages will be what toggles on and off the LED.
Let's go ahead and open that MainActivity file and get right to it.
Right above the onCreate method, add the following lines of code:
private static final String TAG = MainActivity.class.getSimpleName();
private BluetoothManager mBluetoothManager;
private BluetoothGattServer mBluetoothGattServer;
private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();
Boolean toggleLight = false;
Just to go over quickly what we have added to the code so far, we declared a BluetoothManager, which we will use to determine that Bluetooth is enabled and available. Also declared a BluetoothGattServer and BluetoothLeAdvertiser which will setup our Bluetooth server and broadcasting services, this will be how the Raspberry Pi can pick up messages later on.
Now let's focus on the onCreate method. We'll be adding some code that isn't quite ready yet, so don't freak out when you see some errors.
Inside the onCreate method, match it up with the following:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothReceiver, filter);
if (!mBluetoothManager.getAdapter().isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 0);
}else {
Log.d(TAG, "Bluetooth enabled...starting services");
startAdvertising();
startServer();
}
Button toggleButton = findViewById(R.id.toggle_button);
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleLight = !toggleLight;
notifyRegisteredDevices(toggleLight);
}
});
}
There are some crucial things going on here, first we're putting that BluetoothManager to use, setting up our receiver to determine when a device connects over Bluetooth, and eventually starting up our server and services over Bluetooth. The last thing we setup here is the button to toggle the LED, and we'll come back around to the UI to setup the interface for this.
Now we're going to start resolving some of those errors you are probably getting now. Let's setup mBluetoothReceiver, after the onCreate method go ahead and drop the following:
private BroadcastReceiver mBluetoothReceiver = 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
}
}
};
So this receiver is actually what is called a BroadcastReceiver and is used within the Android framework to receive system messages, for example in our case, when the system tells us that the Bluetooth adapter has been either toggled on or off. BroadcastReceivers are not just limited to system messages, but can also be extended between apps, but for more info checkout Broadcasts here.
Next on the list, setting up the Advertising service. What this piece of code will do is broadcast the services available over Bluetooth, we will discover these services on the Raspberry Pi later on. For now go ahead and add the following to your code:
/**
* Begin advertising over Bluetooth that this device is connectable
* and supports the Remote LED Service.
*/
private void startAdvertising() {
BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
if (mBluetoothLeAdvertiser == 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(RemoteLedProfile.REMOTE_LED_SERVICE))
.build();
mBluetoothLeAdvertiser
.startAdvertising(settings, data, mAdvertiseCallback);
}
/**
* Stop Bluetooth advertisements.
*/
private void stopAdvertising() {
if (mBluetoothLeAdvertiser == null) return;
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
}
So you may or may not have noticed something interesting here, RemoteLedProfile.REMOTE_LED_SERVICE. The RemoteLedProfile class has all the necessary info for setting up the Bluetooth service.
Java classLet's take a break from the MainActivity class and let's start something new. Right click on MainActivity and hover over New -> click on Java Class and name it RemoteLedProfile. With the new class created, set it up like so:
public class RemoteLedProfile {
/* Remote LED Service UUID */
public static UUID REMOTE_LED_SERVICE = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb");
/* Remote LED Data Characteristic */
public static UUID REMOTE_LED_DATA = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb");
/**
* Return a configured {@link BluetoothGattService} instance for the
* Remote LED Service.
*/
public static BluetoothGattService createRemoteLedService() {
BluetoothGattService service = new BluetoothGattService(REMOTE_LED_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
BluetoothGattCharacteristic ledData = new BluetoothGattCharacteristic(REMOTE_LED_DATA,
//Read-only characteristic, supports notifications
BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
service.addCharacteristic(ledData);
return service;
}
}
This is the part where I make sure you're not sleeping yet, take a look at those GUIDs towards the top! REMOTE_LED_SERVICE and REMOTE_LED_DATA! These GUIDs will be making an appearance later on in the Android Things project, these are the GUIDs we'll use to make sure we hook up to the correct service and that the data we are receiving is coming from the right places.
Focus up on createRemoteLedService method, this here is part of the magic. This sets up our service and sets up the service with the appropriate characteristics, or basically we create service, and service accepts data only from this GUID.
Alright, take a breath and turn back to MainActivity class. Let's finish up this class so we can move onto the UI. Jump down to stopAdvertising and add this next snippet of code:
/**
* Initialize the GATT server instance with the services/characteristics
* from the Remote LED Profile.
*/
private void startServer() {
mBluetoothGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
if (mBluetoothGattServer == null) {
Log.w(TAG, "Unable to create GATT server");
return;
}
mBluetoothGattServer.addService(RemoteLedProfile.createRemoteLedService());
}
/**
* Shut down the GATT server.
*/
private void stopServer() {
if (mBluetoothGattServer == null) return;
mBluetoothGattServer.close();
}
This is calling that method we created in RemoteLedProfile, and setting up the server.
Important codeThe next piece of code, truly deep down is optional, but this can be an excellent sanity check when testing. Implement the mAdvertiseCallback:
/**
* Callback to receive information about the advertisement process.
*/
private AdvertiseCallback mAdvertiseCallback = 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);
}
};
Like I mentioned, optionally you can set the callback to null in the previous bit of code, but this could help you out and maybe a nice hook into enhancing this app later on.
Oh! This is my favorite part of the Android code. Quick drop this snippet:
/**
* Send a remote led service notification to any devices that are subscribed
* to the characteristic.
*/
private void notifyRegisteredDevices(Boolean toggle) {
if (mRegisteredDevices.isEmpty()) {
Log.i(TAG, "No subscribers registered");
return;
}
Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
for (BluetoothDevice device : mRegisteredDevices) {
BluetoothGattCharacteristic ledDataCharacteristic = mBluetoothGattServer
.getService(RemoteLedProfile.REMOTE_LED_SERVICE)
.getCharacteristic(RemoteLedProfile.REMOTE_LED_DATA);
ledDataCharacteristic.setValue(toggle.toString());
mBluetoothGattServer.notifyCharacteristicChanged(device, ledDataCharacteristic, false);
}
}
This is the main method, this bit is what sends our toggle message to whomever is listening (hopefully that's the Raspberry Pi). Yeah not to brag too much about this part, but it's pretty cool...you'll see.
Just a bit more to go, this is the last major chunk of code, the callback for the Bluetooth server. Add the following:
/**
* Callback to handle incoming requests to the GATT server.
* All read/write requests for characteristics and descriptors are handled here.
*/
private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
mRegisteredDevices.add(device);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
//Remove device from any active subscriptions
mRegisteredDevices.remove(device);
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
long now = System.currentTimeMillis();
if (RemoteLedProfile.REMOTE_LED_DATA.equals(characteristic.getUuid())) {
Log.i(TAG, "Read data");
mBluetoothGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_SUCCESS,
0,
null);
} else {
// Invalid characteristic
Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
mBluetoothGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattDescriptor descriptor) {
mBluetoothGattServer.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 (responseNeeded) {
mBluetoothGattServer.sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
};
The most important take away here is that this guy sets up our list of registered devices, removes and adds them as they pop in and out so we know who to send messages to.
CleanupFinally some minor cleanup (cause we're good devs like that right?):
@Override
protected void onDestroy() {
super.onDestroy();
BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
if (bluetoothAdapter.isEnabled()) {
stopServer();
stopAdvertising();
}
unregisterReceiver(mBluetoothReceiver);
}
Nope I didn't forget about UI, there's no proof I forgot. Open up the activity_main.xml (res-> layout) file and replace that whole TextView tag with the following:
<Button
android:id="@+id/toggle_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Toggle LED" />
I mean you can put whichever text you want, I just thought Toggle LED made sense.
And with that we have a nice Android app that's ready to go! Now we just need to setup the Android Things app to get them talking to each other.
If you're feeling particularly excited about the Android app, feel free to give a run. Just make sure you have mobile selected as your output in Android Studio.
If it runs successfully, then congrats! You're an Android developer as far as I'm concerned. If it failed...then I'm sorry, leave me a message or comment and I'll help you out.
Back to Android Things DevelopmentBack to the IoT? Ugh no that was bad...let's keep moving before that gets awkward.
Open up MainActivity in the things project. Somewhere towards the top, after that ButtonInputDriver variable, add the following:
/* Remote LED Service UUID */
public static UUID REMOTE_LED_SERVICE = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb");
/* Remote LED Data Characteristic */
public static UUID REMOTE_LED_DATA = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb");
private static final String ANDROID_DEVICE_NAME = "Pixel 2";
private static final String ADAPTER_FRIENDLY_NAME = "My Android Things device";
private static final int REQUEST_ENABLE_BT = 1;
// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
private BluetoothLeService mBluetoothLeService;
private boolean mConnected = false;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private String mDeviceAddress;
So let me tell you what you just did, first of all remember those GUIDs from earlier? Here they are again and they will be referenced to ensure that we connect to the right server and services. Something else to point, ANDROID_DEVICE_NAME should be the name of your Android device, Pixel 2 was mine for example, you can get this name through a few methods, easiest way would be to print out the device name back in the Android project; something like this.
Oh and don't worry about BluetoothLeService, we'll come back to that in a bit. Now in onCreate, after you set up the mButtonInputDriver variable, go ahead and drop the following code:
mHandler = new Handler();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null) {
if (mBluetoothAdapter.isEnabled()) {
Log.d(TAG, "Bluetooth Adapter is already enabled.");
initScan();
} else {
Log.d(TAG, "Bluetooth adapter not enabled. Enabling.");
mBluetoothAdapter.enable();
}
}
So this is a little similar to what we had done in the Android app, checking that Bluetooth is enabled and setup our discovery services.
Speaking of discovery, after onCreate add this:
private void initScan() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Log.e(TAG, "Bluetooth adapter not available or not enabled.");
return;
}
//setupBTProfiles();
Log.d(TAG, "Set up Bluetooth Adapter name and profile");
mBluetoothAdapter.setName(ADAPTER_FRIENDLY_NAME);
scanLeDevice();
}
private void scanLeDevice() {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
This sets up the Bluetooth profile and scans for nearby Bluetooth devices. mLeScanCallback is what we'll setup next but this will be the part we connect to the desired device.
Add the following snippet next:
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
Boolean isDeviceFound = false;
if(device != null){
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0) {
//Log.d(TAG, deviceName);
if(deviceName.equals(ANDROID_DEVICE_NAME)){
isDeviceFound = true;
mDeviceAddress = device.getAddress();
}
}
}
if(isDeviceFound && !mConnected){
Intent gattServiceIntent = new Intent(MainActivity.this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
mConnected = true;
}
}
};
So to keep things simple, and for the sake of demo purposes, we check for the Android device by name. I would advise on something a little more robust if you plan to take this concept to the next level...sorta like a Pro tip.
Here's some sanity saving specific code, which kinda means it's optional but I wouldn't really leave it out (another Pro tip?). Add this bit following mLeScanCallback:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
Log.d(TAG, "Enable discoverable returned with result " + resultCode);
// ResultCode, as described in BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE, is either
// RESULT_CANCELED or the number of milliseconds that the device will stay in
// discoverable mode. In a regular Android device, the user will see a popup requesting
// authorization, and if they cancel, RESULT_CANCELED is returned. In Android Things,
// on the other hand, the authorization for pairing is always given without user
// interference, so RESULT_CANCELED should never be returned.
if (resultCode == RESULT_CANCELED) {
Log.e(TAG, "Enable discoverable has been cancelled by the user. " +
"This should never happen in an Android Things device.");
}
}
}
This is just a pretty nice indicator that things are connecting the way we expect.
Service connectionNow we need to add the service connection, connect to the device we want so we can setup for receiving messages. Add the following:
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
if (!mBluetoothLeService.initialize()) {
Log.e(TAG, "Unable to initialize Bluetooth");
finish();
}
Boolean result = mBluetoothLeService.connect(mDeviceAddress);
Log.d(TAG, "Connect request result=" + result);
mConnected = true;
if(mScanning) {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mBluetoothLeService = null;
}
};
Alright, going to pick up the pace here a little...depending on how fast you read I guess actually. Well either way, this major chunk of code is major important! Drop this block of code:
// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read
// or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
mConnected = true;
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
mConnected = false;
} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
// Show all the supported services and characteristics on the user interface.
List<BluetoothGattService> services = mBluetoothLeService.getSupportedGattServices();
if(services != null){
for (BluetoothGattService gattService : services) {
if(gattService.getUuid().equals(REMOTE_LED_SERVICE)){
final BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(REMOTE_LED_DATA);
if (characteristic != null) {
final int charaProp = characteristic.getProperties();
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
// If there is an active notification on a characteristic, clear
// it first so it doesn't update the data field on the user interface.
if (mNotifyCharacteristic != null) {
mBluetoothLeService.setCharacteristicNotification(
mNotifyCharacteristic, false);
mNotifyCharacteristic = null;
}
mBluetoothLeService.readCharacteristic(characteristic);
}
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
mNotifyCharacteristic = characteristic;
mBluetoothLeService.setCharacteristicNotification(
characteristic, true);
}
}
}
}
}
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
String data = intent.getStringExtra(BluetoothLeService.EXTRA_DATA);
if(data.contains("false")){
setLedValue(false);
}else if(data.contains("true")){
setLedValue(true);
}
}
}
};
private static IntentFilter makeGattUpdateIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
return intentFilter;
}
It looks like a lot, but really its just the most important part of the project, um so yeah it's actually a lot. This is another BroadcastReceiver, which listens for the messages coming from the connected Bluetooth server. Yup, the Android device. When you hit that toggle button from the app, a message will get sent out and this receiver will catch that message and determine if it should turn the LED on or off.
The filters are meant to let the receiver know which events to listen for, these are all useful and important, but I suppose the most interesting is ACTION_GATT_SERVICES_DISCOVERED which will notify ACTION_DATA_AVAILABLE that there is, well data available, for our specific data characteristic that we create back in the Android app and that we are referencing here.
BluetoothLeServiceNext we're going to take care of those annoying errors about BluetoothLeService. Right click MainActivity -> New -> Java Class and name your new class BluetoothLeService.
To makes things a little easier on both of us, just go ahead and replace what you have there with the following:
public class BluetoothLeService extends Service {
private final static String TAG = BluetoothLeService.class.getSimpleName();
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
public final static String ACTION_GATT_CONNECTED =
"com.example.bluetooth.le.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED =
"com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED =
"com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE =
"com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA =
"com.example.bluetooth.le.EXTRA_DATA";
// Implements callback methods for GATT events that the app cares about. For example,
// connection change and services discovered.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
// Attempts to discover services after successful connection.
Log.i(TAG, "Attempting to start service discovery:" +
mBluetoothGatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
mConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
};
private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action);
sendBroadcast(intent);
}
private void broadcastUpdate(final String action,
final BluetoothGattCharacteristic characteristic) {
final Intent intent = new Intent(action);
// For all other profiles, writes the data formatted in HEX.
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(data.length);
for(byte byteChar : data)
stringBuilder.append(String.format("%02X ", byteChar));
intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
}
sendBroadcast(intent);
}
public class LocalBinder extends Binder {
BluetoothLeService getService() {
return BluetoothLeService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
// After using a given device, you should make sure that BluetoothGatt.close() is called
// such that resources are cleaned up properly. In this particular example, close() is
// invoked when the UI is disconnected from the Service.
close();
return super.onUnbind(intent);
}
private final IBinder mBinder = new LocalBinder();
/**
* Initializes a reference to the local Bluetooth adapter.
*
* @return Return true if the initialization is successful.
*/
public boolean initialize() {
// For API level 18 and above, get a reference to BluetoothAdapter through
// BluetoothManager.
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.e(TAG, "Unable to initialize BluetoothManager.");
return false;
}
}
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
return false;
}
return true;
}
/**
* Connects to the GATT server hosted on the Bluetooth LE device.
*
* @param address The device address of the destination device.
*
* @return Return true if the connection is initiated successfully. The connection result
* is reported asynchronously through the
* {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
* callback.
*/
public boolean connect(final String address) {
if (mBluetoothAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
}
// Previously connected device. Try to reconnect.
if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
if (mBluetoothGatt.connect()) {
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
if (device == null) {
Log.w(TAG, "Device not found. Unable to connect.");
return false;
}
// We want to directly connect to the device, so we are setting the autoConnect
// parameter to false.
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
Log.d(TAG, "Trying to create a new connection.");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
}
/**
* Disconnects an existing connection or cancel a pending connection. The disconnection result
* is reported asynchronously through the
* {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
* callback.
*/
public void disconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mBluetoothGatt.disconnect();
}
/**
* After using a given BLE device, the app must call this method to ensure resources are
* released properly.
*/
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}
/**
* Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
* asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
* callback.
*
* @param characteristic The characteristic to read from.
*/
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mBluetoothGatt.readCharacteristic(characteristic);
}
/**
* Enables or disables notification on a give characteristic.
*
* @param characteristic Characteristic to act on.
* @param enabled If true, enable notification. False otherwise.
*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
}
/**
* Retrieves a list of supported GATT services on the connected device. This should be
* invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
*
* @return A {@code List} of supported services.
*/
public List<BluetoothGattService> getSupportedGattServices() {
if (mBluetoothGatt == null) return null;
return mBluetoothGatt.getServices();
}
}
This class handles some of the lower level workings. Important take aways here are this handles connecting to the Bluetooth server and handles the actual binding and unbinding of the service, as well as taking care of notifications from discovered services. I would recommend that you read through this class, and feel free to ask any questions about it.
Now back to MainActivity! Jump down to onStart, we'll take care of some housekeeping and then finally trying it all out together!
Update your onStart method to the following:
@Override
protected void onStart() {
super.onStart();
mButtonInputDriver.register();
registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
}
Then add the onPause method:
@Override
protected void onPause(){
super.onPause();
unregisterReceiver(mGattUpdateReceiver);
}
And finally onDestroy and update to the following:
@Override
protected void onDestroy(){
super.onDestroy();
if (mButtonInputDriver != null) {
mButtonInputDriver.unregister();
try {
mButtonInputDriver.close();
} catch (IOException e) {
Log.e(TAG, "Error closing Button driver", e);
} finally{
mButtonInputDriver = null;
}
}
if (mLedGpio != null) {
try {
mLedGpio.close();
} catch (IOException e) {
Log.e(TAG, "Error closing LED GPIO", e);
} finally{
mLedGpio = null;
}
mLedGpio = null;
}
unbindService(mServiceConnection);
mBluetoothLeService = null;
}
Basically we're registering our callbacks for hooking up the server, and then unregistering and unbinding from the server and services when we no longer have use for them.
Run it!Pat yourself on the back, even if this totally blows up in your face, you've done a lot of work here and regardless should feel proud. Now for the payoff, update your app on the Raspberry Pi, same as previous parts, just make sure you have things as your selected module before running.
So because the Android device is acting as the server and the Raspberry Pi is the client, I would recommend running the Android app first and then the Android Things project. Pay attention to the logs in Android Studio and look out for the connection successful messages. I would recommend keeping logcat open for both projects for this.
Once these two devices have made the connection, you should be able to hit the toggle button on the app and watch that LED turn on and off.
Hopefully its something like this:
Level Up!This is a huge accomplishment and if you've made it this far, you definitely should be proud, and maybe give yourself a few more pats on the back...and brag..totally tell people about what you've made so far with Android Things!
This is the last part of this series, but there will definitely be more Android Things projects to come, they just might be their own series or one shots.
Stay tuned for more projects and congrats on making it to the end of this series, and most of all I hope you enjoyed it and found it useful (and maybe mildly entertaining?).
Comments