Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

MiBand 3 and React Native (Part One)

DZone 's Guide to

MiBand 3 and React Native (Part One)

Teach an app to communicate with MiBand 3.

· Web Dev Zone ·
Free Resource

Recently, I decided to test an idea in practice where Xiaomi MiBand 3 can be used by a simple mobile application based on a React-Native framework. My task was simple: teach a mobile app to communicate with MiBand 3 and make it get data, such as heart rate and connection-bond level, from MiBand 3.

This article describes the dev environment setup for Android. I am not going to forget about iOS and will share my findings later in the next article. If you want to try a solution in practice, please check here.

While I was thinking about the details of implementation, the following scheme was born:

Interaction scheme between devices
Interaction scheme between devices


  1. MiBand and smartphones communicate with one another via Bluetooth protocol. Our mobile application looks for an appropriate MiBand device, tries to synchronize with it, and makes requests to get data. The final results are displayed on UI.

  2. Smartphones and a dedicated server also communicate with each other via HTTP protocol. I'll describe this a bit later in detail in the following article.

Preparations

Before I begin, some major preparations are required. I used the following devices:

  • Band: Xiaomi MiBand 3.
  • Smartphone: something with Android >v.4 on board.
  • Laptop: with Linux OS (Ubuntu, Mint, CentOs, etc…).

The software you'll need:

  • IDEs: Visual Code Studio (latest build), Android Studio (latest build).
  • JDK: v1.8 (latest inner build) or higher.
  • NodeJs: v10.x.x or above.
  • Android SDK: v21 or above.
  • Gradle: v3.3.1.
  • React-Native: v0.60.4
  • Android Debug Bridge or adb: v1.0.39.

Getting Started

We have all the components prepared and can start the project's setup. Assuming the original project folderis ~/projects/sbp, let’s initialize our project:

react-native init sbp


Once that command is run, React Native will set up a project with an appropriate set of resources to proceed with development. If all this is done correctly, you will see the next output in the terminal:

Correct output
Correct output


The project folder will look like:

sbp
android
ios
node_modules
src
__tests__
metro.config.js
index.js
package.json
app.json
babel.config.js
.buckconfig
.eslintrc.js
.flowconfig
.watchmanconfig


React Native Setup

React Native setup is almost finished. Now, we can create a src folder and put an App.js file inside it. So all JS sources will be in root src folder for comfortable navigation later. We should not forget to update the path to App.js in index.js. Currently, it looks like:

import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);


Additional changes must be done with the metro config file (metro.config.js). First of all, we have to install additional dependencies:

npm install react-native-css-transformer


This allows us to use our custom CSS files. Finally, our metro config will be:

const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve("react-native-css-transformer")
    },
    resolver: {
      sourceExts: [...sourceExts, "css", "jsx", "js"]
    }
  };
})();

Right now, we have reached a point where our application can be run. However, React Native must be aware of a target device to use for run. To make it happen, let’s execute the following commands for Android:

adb devices
react-native run-android device_id

My output shows one connected device by USB with a laptop.

Image title


The project is ready for development, and we can start designing some simple UI. There will be few components in the beginning. We just need a couple of buttons to activate the BlueTooth module and link our device with a band. Finally, a couple of fields to display results are required.

Displaying results

Displaying results


React Native has already created one App.js file where some dummy dashboard was created automatically. We do not need it obviously. That is why App.js will be overridden:

import React from 'react';
import Dashboard from './components/dashboard/index.js';

const App = () => {
  return (
    <Dashboard/>
  );
};

export default App;

Since we are concerned about UI structure, its code structure can be implemented in following way:

import React from 'react'
import {Text, View, Button, NativeModules} from 'react-native';
import styles from "./styles.css";

export default class Dashboard extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            foundDeviceName: 'None',
            deviceBondLevel: 0,
            heartBeatRate: 0
        };
    }

    searchBluetoothDevices = () => {
        NativeModules.DeviceConnector.enableBTAndDiscover( (error, deviceBondLevel)=>{
            this.setState({ deviceBondLevel: deviceBondLevel});
        })
        setInterval(this.getDeviceBondLevel, 2000)
    }

    getDeviceBondLevel = () => {
        NativeModules.DeviceConnector.getDeviceBondLevel( (error, deviceBondLevel)=>{
            this.setState({ deviceBondLevel: deviceBondLevel}, () => {
                this.getDeviceBondLevel
            });
        })
    }

    activateHeartRateCalculation = () => {
        NativeModules.HeartBeatMeasurer.startHeartRateCalculation( (error, heartBeatRate)=>{
            this.setState({ heartBeatRate: heartBeatRate});
        })
        setInterval(this.getHeartRate, 2000)
    }

    getHeartRate = () => {
        NativeModules.HeartBeatMeasurer.getHeartRate( (error, heartBeatRate)=>{
            this.setState({ heartBeatRate: heartBeatRate});
        })
    }

    render() {
        return (
            <View style={styles.container}>
                <View style={styles.package}>
                    <Text style={styles.sensorField}>Heart Beat:</Text>
                    <Text style={styles.sensorField}>{this.state.heartBeatRate + ' Bpm'}</Text>
                </View>

                <View style={styles.package}>
                    <Text style={styles.sensorField}>Device BL:</Text>
                    <Text style={styles.sensorField}>{this.state.deviceBondLevel}</Text>
                </View>

                <View style={styles.buttonContainer}>
                    <Button onPress={this.searchBluetoothDevices} title='Link With MiBand' /> 
                    <View style={styles.spacing}/>
                    <Button onPress={this.activateHeartRateCalculation} title='Get Heart Rate' /> 
                </View>
            </View>
        );
    }
}

Few comments here:

1. The NativeModules component is used for linking our native platform logic with ReactJS UI.

2. The searchBluetoothDevices function has native implementations on Android and iOS platforms. We will check them in detail quite soon, but now just keep the function's purpose in mind: find the MiBand device, link with it, and get a Bond Level value to display on the UI.

3. The activateHeartRateCalculation function also calls native code. The last one will make a request to our band for heartbeat calculation and return its value.

One more thing about styles: I decided to keep simple styling for UI and store it in styles.css:

.container {
    flex: 1;
    flex-direction: column;
    align-content: center;
    padding: 5%;
    padding-top: 50%;
}

.package {
    flex-direction: row;
    justify-content: space-between;
    margin: 10px;
}

.sensorField {
    font-size: 30;
}

.buttonContainer {
    flex-direction: column;
    justify-content: space-around;
}

.spacing{
    padding: 10px;
}

Android setup...

Since UI part is done we can refer to native programming. First step here relates to Android platform. Fortunately for us, React-Native created correspond folders for both platforms in the beginning. In general they are configured pretty well but still require small customization.

Image title

Firstly we will change gradle version to v3.3.1 build. Secondly a minimal version for Android SDK will be set to 21. I decided to increase it since 21 provides more features for Java programming including cool things come from JDK 8. In the long run our main gradle config will look like:

buildscript {
    ext {
        buildToolsVersion = "28.0.3"
        minSdkVersion = 21
        compileSdkVersion = 28
        targetSdkVersion = 28
        supportLibVersion = "28.0.0"
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
    }
}

allprojects {
    repositories {
        mavenLocal()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url("$rootDir/../node_modules/react-native/android")
        }
        maven {
            // Android JSC is installed from npm
            url("$rootDir/../node_modules/jsc-android/dist")
        }

        google()
        jcenter()
    }
}


Created in the beginning Android template has two classes. MainActivity maintains one method that will be invoked during application startup. It returns the component's name as a String value. It also has ReactActivity parent that already implements main methods (onCreate, onDestroy and so on). We do not need to do anything here and can switch on second class or MainApplication.

In my opinion, original authors decided to provide a modular system where separate native modules would be loaded to React Native infrastructure by getPackages() method. If it's true then my logic can be divided also on a couple sub modules.

So, I started from the Bluetooth part, since my application must find a MiBand device and link with it. That is how DeviceConncector was created:

package com.sbp.bluetooth;

import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.sbp.R;
import com.sbp.metric.HeartBeatMeasurer;
import com.sbp.metric.HeartBeatMeasurerPackage;

import java.util.ArrayList;
import java.util.Objects;

import static android.content.Context.BLUETOOTH_SERVICE;
import static com.sbp.common.ModuleStorage.getModuleStorage;

public class DeviceConnector  extends ReactContextBaseJavaModule {

    // Bluetooth variable section
    private BluetoothGatt bluetoothGatt;
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothDevice bluetoothDevice;
    private AppBluetoothGattCallback appBluetoothGattCallback;
    private ArrayList<BluetoothDevice> deviceArrayList = new ArrayList<>();
    private ProgressDialog searchProgressDialog;

    // Android settings section
    private SharedPreferences sharedPreferences;
    private final int DISCOVERY_TIME_DELAY_IN_MS = 120000;

    private Context applicationContext;

    DeviceConnector(ReactApplicationContext reactContext) {
        super(reactContext);

        applicationContext = getReactApplicationContext().getApplicationContext();
        HeartBeatMeasurerPackage hBMeasurerPackage = getModuleStorage().getHeartBeatMeasurerPackage();

        String sharedPreferencesAppName =
                applicationContext.getString(R.string.app_mi_band_connect_preferences);
        sharedPreferences = applicationContext
                .getSharedPreferences(sharedPreferencesAppName, Context.MODE_PRIVATE);
        HeartBeatMeasurer heartBeatMeasurer = hBMeasurerPackage.getHeartBeatMeasurer();
        appBluetoothGattCallback = new AppBluetoothGattCallback(sharedPreferences, heartBeatMeasurer);
    }

    /**
     * Enables Bluetooth module on smart phone and starts device discovering process.
     * @param successCallback - a Callback instance that will be needed in the end of discovering
     *                        process to send back a result of work.
     */
    @ReactMethod
    public void enableBTAndDiscover(Callback successCallback) {
        Context mainContext = getReactApplicationContext().getCurrentActivity();
        bluetoothAdapter = ((BluetoothManager) mainContext.
                getSystemService(BLUETOOTH_SERVICE)).
                getAdapter();

        searchProgressDialog = new ProgressDialog(mainContext);
        searchProgressDialog.setIndeterminate(true);
        searchProgressDialog.setTitle("MiBand Bluetooth Scanner");
        searchProgressDialog.setMessage("Searching...");
        searchProgressDialog.setCancelable(false);
        searchProgressDialog.show();

        if (!bluetoothAdapter.isEnabled()) {
            ((AppCompatActivity)mainContext).
                    startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),
                            1);
        }

        final ScanCallback leDeviceScanCallback =
                new DeviceScanCallback(this, successCallback);
        String sharedPreferencesDeviceMacAddress = Objects.requireNonNull(getCurrentActivity())
                .getString(R.string.app_mi_band_last_connected_device_mac_address_key);
        String lastMiBandConnectedDeviceMac =
                sharedPreferences.getString(sharedPreferencesDeviceMacAddress, null);

        if (lastMiBandConnectedDeviceMac != null) {
            bluetoothDevice = bluetoothAdapter.getRemoteDevice(lastMiBandConnectedDeviceMac);
            bluetoothGatt = bluetoothDevice.connectGatt(mainContext, true, appBluetoothGattCallback);
            getDeviceBondLevel(successCallback);
            getModuleStorage().getHeartBeatMeasurerPackage().
                    getHeartBeatMeasurer().
                    updateBluetoothConfig(bluetoothGatt);
            appBluetoothGattCallback.updateBluetoothGatt(bluetoothGatt);
            searchProgressDialog.dismiss();
        } else {
            BluetoothLeScanner bluetoothScanner = bluetoothAdapter.getBluetoothLeScanner();
            if(bluetoothScanner != null){
                bluetoothScanner.startScan(leDeviceScanCallback);
            }
        }

        new Handler().postDelayed(() -> {
            bluetoothAdapter.getBluetoothLeScanner().stopScan(leDeviceScanCallback);
            searchProgressDialog.dismiss();
        }, DISCOVERY_TIME_DELAY_IN_MS);
    }

    /**
     * Tries to connect a found miband device with tha app. In case of succeed a bound level value
     * will be send back to be displayed on UI.
     * @param successCallback - a Callback instance that will be needed in the end of discovering
     *                        process to send back a result of work.
     */
    @ReactMethod
    void connectDevice(Callback successCallback) {
        if (bluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE) {
            bluetoothDevice.createBond();
            Log.d("Bond", "Created with Device");
        }
        Context mainContext = getReactApplicationContext().getCurrentActivity();
        bluetoothGatt = bluetoothDevice.connectGatt(mainContext, true, appBluetoothGattCallback);
        getModuleStorage().
                getHeartBeatMeasurerPackage().
                getHeartBeatMeasurer().
                updateBluetoothConfig(bluetoothGatt);
        appBluetoothGattCallback.updateBluetoothGatt(bluetoothGatt);

        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(Objects.requireNonNull(getCurrentActivity())
                .getString(R.string.app_mi_band_last_connected_device_mac_address_key),
                bluetoothDevice.getAddress());
        editor.apply();

        getDeviceBondLevel(successCallback);
    }

    /**
     * Returns a bluetooth bound level of connection between miband device and android app.
     * Used by react UI part when connection has been established.
     * @param successCallback - a Callback instance that will be needed in the end of discovering
     *                        process to send back a result of work.
     */
    @ReactMethod
    private void getDeviceBondLevel(Callback successCallback){
        if(bluetoothGatt != null){
            successCallback.invoke(null, bluetoothGatt.getDevice().getBondState());
        }
    }

    @Override
    public String getName() {
        return DeviceConnector.class.getSimpleName();
    }

    Context getApplicationContext() {
        return applicationContext;
    }

    void setBluetoothDevice(BluetoothDevice bluetoothDevice) {
        this.bluetoothDevice = bluetoothDevice;
    }

    BluetoothAdapter getBluetoothAdapter() {
        return bluetoothAdapter;
    }

    ProgressDialog getSearchProgressDialog() {
        return searchProgressDialog;
    }

    ArrayList<BluetoothDevice> getDeviceArrayList() {
        return deviceArrayList;
    }

}


A few comments about code above:

  • @ReactMethod annotation must be used each time when we want to invoke some native code to be executed from UI or javascript part. That is why we can clearly observe how couple methods use this annotation. As a parameter they use a Callback object, This container will parsed later one by React-Native infrastructure for getting calculated data. Last one will be displayed on UI essentiall.

  • DeviceConncetor extends ReactContextBaseJavaModule class that comes from React-Native world. It explains why we have to keep an overridden getName() method. More likely it will be used during application startup once again.

  • Basically this module is designed to serve next goals:

    • Find the MiBand device by Bluetooth and get its Mac address. Next time, it will use a saved address to link with MiBand directly; that saves time for testing and debugging.

    • Once the device is found, we get the current bound level and return it to UI.

Next, we will focus on the heartbeat measurement. For this, I created a separate HeartBeatMeasurer module:

package com.sbp.metric;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.util.Log;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.UUID;

import javax.annotation.Nonnull;

import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
import static java.lang.Thread.sleep;

public class HeartBeatMeasurer extends ReactContextBaseJavaModule {

    /**
     * Gatt Service contains a collection of BluetoothGattCharacteristic,
     * as well as referenced services.
     */
    private BluetoothGattService variableService;

    /**
     * Public API for the Bluetooth GATT Profile.
     * This class provides Bluetooth GATT functionality to enable communication with Bluetooth
     * Smart or Smart Ready devices.
     * To connect to a remote peripheral device, create a BluetoothGattCallback and call
     * BluetoothDevice#connectGatt to get a instance of this class. GATT capable devices can be
     * discovered using the Bluetooth device discovery or BLE scan process.
     */
    private BluetoothGatt bluetoothGatt;

    /**
     * A GATT characteristic is a basic data element used to construct a GATT service,
     * BluetoothGattService. The characteristic contains a value as well as additional
     * information and optional GATT descriptors, BluetoothGattDescriptor.
     */
    private BluetoothGattCharacteristic heartRateControlPointCharacteristic;

    /**
     * keeps current heart beat value taken from miband device
     */
    private String heartRateValue = "0";

    /**
     * used to get
     */
    private final Object object = new Object();
    private final int DEVICE_PAUSE_COMMUNICATION_IN_MS = 500;

    HeartBeatMeasurer(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    /**
     * Reads recieved data from miband device with current heart beat state.
     * @param characteristic GATT characteristic is a basic data element used
     *                       to construct a GATT service
     */
    public void handleHeartRateData(final BluetoothGattCharacteristic characteristic) {

        Log.i("Heart", String.valueOf(characteristic.getValue()[1]));
        runOnUiThread(() -> {
            BluetoothGattCharacteristic heartRateMeasurementCharacteristic
                    = variableService.getCharacteristic(
                            UUID.fromString(UUIDs.HEART_RATE_MEASUREMENT_CHARACTERISTIC_STRING));

            bluetoothGatt.readCharacteristic(heartRateMeasurementCharacteristic);
            synchronized (object) {
                try {
                    object.wait(DEVICE_PAUSE_COMMUNICATION_IN_MS);
                    heartRateControlPointCharacteristic.setValue(new byte[]{0x16});
                    bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            heartRateValue = String.valueOf(characteristic.getValue()[1]);
        });
    }

    /**
     * Starts heartBeat data fetching from miband device.
     * @param successCallback - a Callback instance that contains result of native code execution
     */
    @ReactMethod
    private void startHeartRateCalculation(Callback successCallback) {
        variableService = bluetoothGatt.getService(UUIDs.HEART_RATE_SERVICE);
        UUID heartRateCharacteristicCode =
                UUID.fromString(UUIDs.HEART_RATE_MEASUREMENT_CHARACTERISTIC_STRING);

        if(variableService != null){
            BluetoothGattCharacteristic heartRateCharacteristic =
                    variableService.getCharacteristic(heartRateCharacteristicCode);
            BluetoothGattDescriptor heartRateDescriptor =
                    heartRateCharacteristic.getDescriptor(UUIDs.HEART_RATE_MEASURMENT_DESCRIPTOR);

            bluetoothGatt.setCharacteristicNotification(heartRateCharacteristic, true);
            heartRateDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            bluetoothGatt.writeDescriptor(heartRateDescriptor);

            heartRateControlPointCharacteristic = variableService
                    .getCharacteristic(UUIDs.HEART_RATE_CONTROL_POINT_CHARACTERISTIC);
            makePause();

            BluetoothGattService variableSensorService =
                    bluetoothGatt.getService(UUIDs.SENSOR_SERVICE);
            BluetoothGattCharacteristic heartCharacteristicSensor
                    = variableSensorService.getCharacteristic(UUIDs.CHARACTER_SENSOR_CHARACTERISTIC);
            makePause();

            heartRateControlPointCharacteristic.setValue(new byte[]{0x15, 0x02, 0x00});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
            makePause();

            heartRateControlPointCharacteristic.setValue(new byte[]{0x15, 0x01, 0x00});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
            makePause();

            heartCharacteristicSensor.setValue(new byte[]{0x01, 0x03, 0x19});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
            makePause();

            heartRateControlPointCharacteristic.setValue(new byte[]{0x01, 0x00});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
            makePause();

            heartRateControlPointCharacteristic.setValue(new byte[]{0x15, 0x01, 0x01});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);

            heartCharacteristicSensor.setValue(new byte[]{0x2});
            bluetoothGatt.writeCharacteristic(heartRateControlPointCharacteristic);
            getHeartRate(successCallback);
        }
    }

    /**
     * Returns current heart beat value.
     * @param successCallback - a Callback instance that contains result of native code execution
     */
    @ReactMethod
    private void getHeartRate(Callback successCallback) {
        successCallback.invoke(null, heartRateValue);
    }

    /**
     * A weird method that must be used during communication process with miband device. Unfortunately
     * last one can not process requests immediately. That is why our application must wait for some
     * time.
     */
    private void makePause(){
        try {
            sleep(DEVICE_PAUSE_COMMUNICATION_IN_MS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Re-inits BluetoothGatt instance in case bluetooth connection was interrupted somehow.
     * @param bluetoothGatt instance to be re-initialized
     */
    public void updateBluetoothConfig(BluetoothGatt bluetoothGatt){
        this.bluetoothGatt = bluetoothGatt;
    }


    @Nonnull
    @Override
    public String getName() {
        return HeartBeatMeasurer.class.getSimpleName();
    }
}


Once again, I would like to leave few comments here:

  • makePause() smells like a workaround. Still I am looking for a proper way when the module won't make pauses in communication with a device but will wait for some certain responce and perform next steps.

  • Heart-Beat calculations work in real-time. However I have not implemented yet functionality when my application can desire to stop receiving HR values from MiBand.

Final Results

Testing can be started with simple commands:

npm start
react-native run-android


Once you have UI displayed on screen just:

  • Press "LINK WITH MIBAND" button and wait until the app finds your MiBand device. Make sure your MiBand 3 has "Allow 3-rd to connect" option enabled. Otherwise, it will block any of the app's attempts to pair.

  • Press the "GET HEART RATE" button and wait until the MiBand calculates your Heart Rate and returns it to your smartphone. Once it's done, you will a similar picture like below:

Final output

Summary

If you are reading this, then more than likely, you have passed through all of the circles of hell and are ready to continue. If you have some trouble with configuration and testing, please let me know. I will be glad to help with any issues related to this article.

I am about to proceed with writing the next sections will cover these topics:

  1. Porting on iOs platform.

  2. App and Server communication process implementation.

  3. UI improvements.

I hope you have found this topic interesting. Do not hesitate to leave comments or recommendations. I wish you all the best! Take care. :)

Topics:
programming and design ,java ,android ,react-native ,web dev ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}