Using Multiple Connected MetaWears with Android API v1.3

bluetooth_low_energy_sensor_nodes_big

INTRODUCTION

With Android API v1.3, users can now communicate with multiple connected MetaWear boards in the same app. Adding multiple MetaWear support has resulted in some API changes that will affect how you use the API. this post will cover the API changes and will show you how to modify your code to be compatible with the changes. If this is your first time using the Android API, keep on reading anyways as there will be some code snippets you may find helpful in getting started.

For compatibility purposes, the old functionality is still present, but marked as deprecated. You should not mix the deprecated functions with their replacements; either stick with using the old code or completely switch to using the new features.

Retrieving a MetaWearController

For APIs prior to v1.3, you were able obtain a MetaWearController anytime by calling getMetaWearController(). While the controller itself could not send instructions to a board unless connected, you could get a reference to a controller and simply use the same reference anywhere in the code. The Android app does this in the onServiceConnected function.

private MetaWearBleService mwService;
private MetaWearController mwController;

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mwService= ((MetaWearBleService.LocalBinder) service).getService();

    // Can use this reference anywhere in the code
    mwController= mwService.getMetaWearController();
}

In API v1.3, getMetaWearController() is now deprecated, being replaced with getMetaWearController(BluetoothDevice). This means you are only able to get a MetaWearController once they know what Bluetooth device they are connecting to, and every MetaWearController is unique for each device. To accommodate this change, you can have a callback function that is called when a Bluetooth device is selected and retrieve a MetaWearController there, If controllers are needed for later use, they can be stored in a map or collection.

private MetaWearBleService mwService;
private final HashMap<BluetoothDevice, MetaWearController> activeControllers= 
        new HashMap<>();

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mwService= ((MetaWearBleService.LocalBinder) service).getService();
}

// Assume fn is called when the Bluetooth device is selected
public void deviceSelected(BluetoothDevice mwBoard) {
    // The controller you get is specific to each unique mwBoard
    activeControllers.put(mwBoard, mwService.getMetaWearController(mwBoard));
}

Connecting and Disconnecting

Connecting to and disconnecting from the board is now handled in the MetaWearController class with the functions: close(boolean), connect, and reconnect(boolean). These functions replace the deprecated connect, close, disconnect, and reconnect variants in the MetaWearBleService class. The disconnect function is also no more; the close function will handle both disconnecting and freeing the gatt resources.

private MetaWearBleService mwService;

// Assume fn is called when the Bluetooth device is selected
public void deviceSelected(BluetoothDevice mwBoard) {
    MetaWearController mwController= mwService.getMetaWearController(mwBoard);

    // connect to the selected board
    mwController.connect();

    // do stuff

    // close connection and free gatt resources
    mwController.close(false);
}

For consistency, the new close, connect, and reconnect functions are not supported in the controller returned by the deprecated getMetaWearController() function. They will throw an UnsupportedOperationException when used. If you are retrieving MetaWearController references with the deprecated version, continue using the deprecated connect(BluetoothDevice), close(boolean), and reconnect() functions.

private MetaWearBleService mwService;

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mwService= ((MetaWearBleService.LocalBinder) service).getService();

    // Each one of these will throw UnsupportedOperationException
    // mwService.getMetaWearController().connect();
    // mwService.getMetaWearController().reconnect(false);
    // mwService.getMetaWearController().close(false);
}

// Assume fn is called when the Bluetooth device is selected
public void deviceSelected(BluetoothDevice mwBoard) {
    MetaWearController mwController= mwService.getMetaWearController(mwBoard);

    // OK to use functions with this controller reference
    // mwController.connect();
    // mwController.reconnect(false);
    // mwController.close(false);
}

Setting up Callback Functions

Setting up callback functions is the still mostly the same as in previous versions. You add and remove module and device callbacks with their respective add/remove functions in the MetaWearController class. Additionally, a clearCallbacks() function has been added to remove all registered device and module callback functions. The only differences with the new API is when you can setup the callbacks and the registered callback functions are not shared amongst the controllers.

import com.mbientlab.metawear.api.MetaWearController.DeviceCallbacks;

private MetaWearBleService mwService;
private DeviceCallbacks oddDevCallbacks= new DeviceCallbacks() {
    @Override
    public void connected() {
        Toast.makeText(getActivity(), "Odd Connected", Toast.LENGTH_SHORT).show();
    }
}, evenDevCallbacks= new DeviceCallbacks() {
    @Override
    public void connected() {
        Toast.makeText(getActivity(), "Even Connected", Toast.LENGTH_SHORT).show();
    }
};
private boolean even= false;

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mwService= ((MetaWearBleService.LocalBinder) service).getService();
}

// Assume fn is called when the bluetooth device is selected
public void deviceSelected(BluetoothDevice mwBoard) {
    mwController= mwService.getMetaWearController(mwBoard);

    // Callback functions added here are only for this specific board
    // Can use this to have different callbacks for each board
    mwController.addDeviceCallback(even ? evenDevCallbacks : oddDevCallbacks);
    even= !even;
}

State Saving

By default, the API will keep information about a board even after it disconnects. While it is useful in case a board reconnects, it can be undesirable in certain situations. For example, if the app eventually has many unique boards connected over a period of time, there will be a lot of unwanted information still held by the API. Or, if the “devicedSelected” function in the above code block is called multiple times on the same board, the same callback function can be registered several times as each instance of the anonymous class is considered to be different, even if they are functionally equivalent. The MetaWearController class provides a function for determining how to hangle the state: setRetainState(boolean). Passing in a value of false will have the API discard the internal state when the device disconnects, preventing users from running into the two pitfalls mentioned above.

private MetaWearBleService mwService;

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mwService= ((MetaWearBleService.LocalBinder) service).getService();
}

// Assumed fn is called when users intentionally disconnects the board i.e. 
// pressing the disconnect button on an app screen.  If they do, assume the 
// board is no longer going to be a part of the app and lets clear the 
// internal state
public void intentionalDisconnect(MetaWearController mwController) {
    mwController.setRetainState(false);
    mwController.close(false);
}

// Assume fn is called when the bluetooth device is selected.  
public void deviceSelected(BluetoothDevice mwBoard) {
    MetaWearController mwController= mwService.getMetaWearController(mwBoard);

    // Will not add the "same" callback fn twice as intentional disconnects will 
    // clear out the stored state
    mwController.addDeviceCallback(new MetaWearController.DeviceCallbacks() {
        @Override
        public void connected() {
            Toast.makeText(getActivity(), "Connected", Toast.LENGTH_SHORT).show();
        }
    });
}

Demo App

And with that, we have covered the major changes in the Android API that were introduced in v1.3 for supporting multiple connected MetaWear boards. To see the code in action, a sample app showcasing API v1.3 is available on our MbientLab projects GitHub page:
https://github.com/mbientlab-projects/MultiMwDemo

Addendum

Version 1.3.7 has stability and bug fixes for the v1.3 release. Release notes and download are available on the GitHub page (https://github.com/mbientlab/Metawear-AndroidAPI/releases/tag/1.3.7).