The MetaWear board allows you to easily create bluetooth enabled low power wearable devices. Unlike many solutions, as an Android developer you can write applications that program, and interact with the board using a API. We recently created an application using their new Android API to log thermistor temperature readings. The readings are read at one minute intervals and store the values on the board for retrieval from the phone.
The new API simplifies the process significantly from the previous version. So lets take a look at what is needed to get your application going with it.
Setting Things Up
To get started you need to add a reference to the Mbientlab repositories in the project level gradle file.
repositories {
jcenter()
ivy {
url "http://ivyrep.mbientlab.com"
layout "gradle"
}
}
Then you are going to need to add a dependency to it in the app level file. The compile directive for com.mbientlab.metawear is required. The one for com.mbientlab.bletoolbox is optional but makes it easier to establish bluetooth connections as you will see later on.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.mbientlab:metawear:2.2.0'
compile 'com.mbientlab.bletoolbox:scanner:0.2.0'
}
The API uses a service to communicate with the board. To use this you will need to add a reference to it in your applications manifest.xml file.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.polyglotprogramminginc.andevcon">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service android:name="com.mbientlab.metawear.MetaWearBleService" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Next you will need to bind the service to your application using an intent in the onCreate method of your Activity.
getApplicationContext().bindService(new Intent(this, MetaWearBleService.class),
this, Context.BIND_AUTO_CREATE);
Then you will need to get a reference to the binder once the service is connected by implementing ServiceConnection and over-riding the onServiceConnected method.
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
///< Get a reference to the MetaWear service from the binder
mwBinder = (MetaWearBleService.LocalBinder) service;
Log.i("Main Activity", "Service Connected");
}
Connecting To The Board
To make finding and connecting to our board easier we are using the BleToolbox scanner which you can find here. For our example we created scanner DialogFragment that has a reference to the BleToolbox in the layout file.
<fragment xmlns:tools="http://schemas.android.com/tools" android:id="@+id/metawear_blescanner_popup_fragment"
android:name="com.mbientlab.bletoolbox.scanner.BleScannerFragment"
tools:layout="@layout/blescan_device_list" android:layout_width="match_parent"
android:layout_height="match_parent" />
To scan for a board we display our fragment.
if (mwScannerFragment == null) {
mwScannerFragment = new ScannerFragment();
mwScannerFragment.show(getFragmentManager(), "metawear_scanner_fragment");
} else {
mwScannerFragment.show(getFragmentManager(), "metawear_scanner_fragment");
}
Then we implement a few callbacks to manage the scanner settings and to connect the board once we have finished scanning.
/**
* callbacks for Bluetooth device scan
*/
@Override
public void onDeviceSelected(BluetoothDevice device) {
connect(device);
Fragment metawearBlescannerPopup = getFragmentManager().findFragmentById(R.id.metawear_blescanner_popup_fragment);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.remove(metawearBlescannerPopup);
fragmentTransaction.commit();
mwScannerFragment.dismiss();
Toast.makeText(this, String.format(Locale.US, "Selected device: %s",
device.getAddress()), Toast.LENGTH_LONG).show();
}
@Override
public UUID[] getFilterServiceUuids() {
///< Only return MetaWear boards in the scan
return new UUID[]{UUID.fromString("326a9000-85cb-9195-d9dd-464cfbbae75a")};
}
@Override
public long getScanDuration() {
///< Scan for 10000ms (10 seconds)
return 10000;
}
These need to be implemented in the activity that initiated the fragment. This activity also needs to implement BleScannerFragment.ScannerCommunicationBus. The getFilterServiceUuids tells the fragment to only scan for boards with the specified UUID values. In our example we are only scanning for MetaWear boards. The getScanDuration tells it how long to scan for devices and the onDeviceSelected is called when you select a device. This hands you a BluetoothDevice object that you can use to connect to the board.
Connecting is then a matter of getting a reference to the board by passing in a reference to your BluetoothDevice, setting up a connectionStateHandler for callbacks when the board is connected/disconnected and then calling connect() on the board.
/**
* Connection callbacks
*/
private MetaWearBoard.ConnectionStateHandler connectionStateHandler = new MetaWearBoard.ConnectionStateHandler() {
@Override
public void connected() {
Log.i("Metawear Controller", "Device Connected");
runOnUiThread(new Runnable() {
@Override
public void run() {
DeviceConfirmationFragment deviceConfirmationFragment = new DeviceConfirmationFragment();
deviceConfirmationFragment.flashDeviceLight(mwBoard, getFragmentManager());
Toast.makeText(getApplicationContext(), R.string.toast_connected, Toast.LENGTH_SHORT).show();
}
}
);
}
@Override
public void disconnected() {
Log.i("Metawear Controler", "Device Disconnected");
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), R.string.toast_disconnected, Toast.LENGTH_SHORT).show();
}
});
}
};
private void connect(BluetoothDevice metaWearDevice){
mwBoard = mwBinder.getMetaWearBoard(metaWearDevice);
mwBoard.setConnectionStateHandler(connectionStateHandler);
mwBoard.connect();
}
Reading And Logging Data
The MetaWear Android API lets you access the sensors and logging with modules. First we need to get a reference to the thermistor module and set a source for it.
try {
tempModule = mwBoard.getModule(MultiChannelTemperature.class);
} catch (UnsupportedModuleException e){
Log.e("Thermistor Fragment", e.toString());
return false;
}
List tempSources= tempModule.getSources();
MultiChannelTemperature.Source tempSource = tempSources.get(MultiChannelTemperature.MetaWearRChannel.NRF_DIE);
Each module has the ability to route data via a DSL. In our example we are going to route our data from our MultiChannelTemperature module to a log with a id of “log_stream” and when the route has successfully been registered set up the rest of the process.
tempModule.routeData().fromSource(tempSource).log("log_stream")
.commit().onComplete(temperatureHandler);
In our callback we will set up a timer that reads the temperature at a set interval and start it.
private final AsyncOperation.CompletionHandler temperatureHandler = new AsyncOperation.CompletionHandler() {
@Override
public void success(RouteManager result) {
result.setLogMessageHandler("log_stream", loggingMessageHandler);
// Read temperature from the NRF soc chip
try {
AsyncOperation taskResult = mwBoard.getModule(Timer.class)
.scheduleTask(new Timer.Task() {
@Override
public void commands() {
tempModule.readTemperature(tempModule.getSources().get(MultiChannelTemperature.MetaWearRChannel.NRF_DIE));
}
}, TIME_DELAY_PERIOD, false);
taskResult.onComplete(new AsyncOperation.CompletionHandler() {
@Override
public void success(Timer.Controller result) {
// start executing the task
result.start();
}
});
}catch (UnsupportedModuleException e){
Log.e("Temperature Fragment", e.toString());
}
}
};
We are also setting a log message handler. This is simply a callback that processes each log entry as it comes from the board. Our implementation logs the entry and saves it to a SqlLite Table.
private final RouteManager.MessageHandler loggingMessageHandler = new RouteManager.MessageHandler() {
@Override
public void process(Message msg) {
Log.i("MainActivity", String.format("Ext thermistor: %.3fC",
msg.getData(Float.class)));
java.sql.Date date = new java.sql.Date(msg.getTimestamp().getTimeInMillis());
TemperatureSample sample = new TemperatureSample(date, msg.getData(Float.class).longValue(), mwBoard.getMacAddress());
sample.save();
}
};
The final part is to call downloadLog on the loggingModule when we want to retrieve the logs from the board.
loggingModule.downloadLog((float)0.1, new Logging.DownloadHandler() {
@Override
public void onProgressUpdate(int nEntriesLeft, int totalEntries) {
Log.i("Thermistor", String.format("Progress= %d / %d", nEntriesLeft,
totalEntries));
//mwController.waitToClose(false);
thermistorCallback.totalDownloadEntries(totalEntries);
thermistorCallback.downloadProgress(totalEntries - nEntriesLeft);
if(nEntriesLeft == 0) {
GraphFragment graphFragment = thermistorCallback.getGraphFragment();
graphFragment.updateGraph();
thermistorCallback.downloadFinished();
}
}
});
This method takes a callback that will give you a progress update on the data download along with a float indicating the percentage interval to give you a status update. For example if you set this value at .10 and have 100 entries to download, it will call this method each time 10 records have been transferred. BLE is a much slower than standard Bluetooth, so a large number of data points can easily take more than a minute to transfer to your Android device. In the code above we are using this to trigger a callback that updates the UI on the download progress.
Persisting Board State Between sessions
Now that we have our board logging the temperature and an app that can download it we need to make sure we can persist the references to our logging when our app pauses/stops and then restarts. We can persist the majority of the app state from the MetaWearBoard object using the serializeState method. In our application we did this in the onSaveInstanceState lifecycle method of our activity.
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if (mwBoard != null) {
mwBoard.disconnect();
state.putByteArray(mwBoard.getMacAddress(), mwBoard.serializeState());
}
}
Our logging callback will not be serialized in the serializeState method. This becomes a problem when we restore our app state because we need to have a reference to the routeManager so that we can add our logging callback when restarting the app. To fix this we will need to persist the RouteManager id in our temperature handler success callback.
bundle.putInt(mwBoard.getMacAddress() + "_log_id", result.id());
Now when we restore our app state we can deserialize the state for our board.
String boardState = bundleState.getString(bleMacAddress, null);
if (boardState != null) {
mwBoard.deserializeState(boardState.getBytes());
Log.i("connect device", "Found instance state");
}
After deserializing we will need to get the route manager that we had initially used for our logs via the RouteManager id and add the logging message handler to it.
RouteManager route = mwBoard.getRouteManager(sharedPreferences.getInt(mwBoard.getMacAddress() + "_log_id", 0));
route.setLogMessageHandler("log_stream", loggingMessageHandler);
You can see a working application that uses this here. All of the code is available on Github over here so that you can make it your own.