React Native for Wearable: Bringing Bluetooth to Javascript

몬드리안에이아이(주)

With tons of stars on Github and the glowing reviews, it is hard to ignore React Native as a framework of choice to build native apps these days. The idea of unified codebase across different OS platforms seems promising, but is it also a good choice for non-trivial apps?

A wearable app is an example of such non-trivial apps. Smart watch, fitness tracker, and other wearable devices interact with the smart phone using energy-efficient wireless communication protocols, as for example Bluetooth Low Energy (BLE) or ANT+. For Bluetooth and its BLE subset, the protocol implementation is supported natively by all major mobile operating systems: iOS, Android, Windows Phone, and Blackberry. Developing a wearable app in native code is an obvious choice, but how about doing it in React Native?

Bluetooth and React Native, the Intriguing Match

We were in a collaboration with our partner to develop a mobile application for their wearable product lines (fitness tracker band and smart watch). The app would extensively communicate with the wearable device using BLE. Consequently, it's important to have the communication part handled right. Besides phone-device communication, the user experience was also of high degree of importance. The goal was to let users have consistent UI experience across different platforms, at least on iOS and Android. React Native became a strong option due to the possibility of meeting the UI goal. However, it was pretty much unclear if using JavaScript via React Native to bridge the Bluetooth communication with the underlying OS platforms was a good idea especially since we would deal with a custom BLE data protocol.

We began shopping for the existing react-native libraries to see if there was an immediate fit for our implementation plan. There were not so many Bluetooth libraries for React Native, hinting the use case for similar app was uncommon. After digging up, we concluded the search with the following libraries, which we thought might be suitable: react-native-ble-plx, react-native-ble-manager, react-native-ble, and react-native-bluetooth-serial. Unlike the rest, react-native-bluetooth-serial is a library for Bluetooth Classic instead of BLE. However, it's worth mentioning that the library along with react-native-ble-manager follow certain structure that we prefer in the BLE implementation.

After experimenting with the existing React Native Bluetooth libraries, we ended up implementing the Bluetooth bridging functionalities by ourselves. One of the reasons was the complexity of the BLE data protocol as for example having multiple phases for data transmission beyond the traditional read, write, and notify characteristics. The decision was tough one, especially when imagining the tremendous effort needed to implement the BLE native bridge for the target OS platforms.

The Gist of BLE Implementation in React Native

The common usage of BLE for wearable is for transferring data between the LE device (the wearable) with the phone. Data transfer occurs in client-server mode, where the wearable plays the role as the server (the data owner) and the phone acts as the client (the data requester). The data transfer will be possible if the phone knows the 'address' of the wearable and the specific service it is interested in. To do so, the phone will need to first discover the wearable from the network and then connect to it in order to start the data transfer flow. For the device discovery, the role of the phone is known as 'central' while the wearable as 'peripheral'. Think about a radar in an aircraft that scans the surrounding to detect if there is any object of interest nearby. After the discovery process is completed, the phone will have gathered the necessary information needed to connect to the device and start the data transfer. Upon completing the data transfer, the common practice is to close the connection to the device in order to conserve energy.

The phone may also save the information about the device it was connecting to so that in the subsequent connection, the phone does need to rescan and rediscover the device again. Saving the device information can be achieved by creating a record about the device at OS level, which is referred to as pairing in iOS / bonding in Android. When the record is stored at the application level, it is often referred to as application preference (NSUserDefaults in iOS or SharedPreferences in Android).

So, what's the strategy to handle BLE in React Native? There has been no official React Native Bluetooth / BLE module by the time this article is written. A native module should then be developed to interface with the native Bluetooth APIs provided by the OS. The basic documentation on writing native module on iOS and Android is already available on the official documentation page. The native module consists of methods that when called from Javascript will execute the native code block that it is interfacing with. These methods are also called bridge methods since they bridge the Javascript world with the native world.

In iOS, a bridge method can be defined by declaring it inside RCT_EXPORT_METHOD macro. For Android, the bridge method is defined by adding annotation @ReactMethod to a public method inside the Java native module. Let's say we have a bridge method named scanDevice that accepts one parameter of deviceName. The method will be callable from Javascript with a simple invocation:

import BleModule from '/path/to/bleModule'
…
//scan device
BleModule.scanDevice('X_SMART_WATCH');

When the bridge method is executed, it will invoke the underlying native bridge method. The implication is that the native iOS and Android must have scanDevice method declared somewhere within the native module. For iOS, the bridge method shall be declared as follows.

@implementation BleModule
…
RCT_EXPORT_METHOD(scanDevice:(NSString*)deviceName)
{
    //routine to scan LE device
}
…      
@end

The corresponding bridge method for Android will have the following structure.

public class BleModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {
    ...
    //scan device
    @ReactMethod
    public void scanDevice(String deviceName) {
        //routine to scan LE device
    }
    ...
}

The curious mind may immediately wonder, 'If I scan for devices in Javascript, how can the scan result be readable or accessible in Javascript?'. The return type of a bridge method is always void since the method is asynchronous. As a remedy, React Native provides three ways of returning value back to Javascript: callback, promise, and event. To specify the return type, we then need to add additional parameter to the native bridge method. The example scanDevice method will be modified as follows.

Callback approach:

[iOS]

@implementation BleModule
…
RCT_EXPORT_METHOD(scanDevice:(NSString*)deviceName callback: (RCTResponseSenderBlock)callback)
{
    //routine to scan LE device
    …
    NSDictionary* deviceInfo = …
    callback(@[[NSNull null], deviceInfo];
}
…
@end

[Android]

import com.facebook.react.bridge.Callback;
…
public class BleModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {

    ...
    //scan device
    @ReactMethod
    public void scanDevice(String deviceName, Callback errorCallback, Callback successCallback) {
        //routine to scan LE device
        ...
        WritableParams deviceInfo = Arguments.createMap();
        ...
        successCallback.invoke(deviceInfo);
    }
    ….
}

With the callback set up in the native code, we will now be able get the result of the bridge method invocation as follows.

import BleModule from '/path/to/bleModule'
...
//scan device
BleModule.scanDevice('X_SMART_WATCH', (msg) => {
    console.log(msg);
}, (device) => {
    console.log(device);
});

Promise approach:

[iOS]

@implementation BleModule
...
RCT_EXPORT_METHOD(scanDevice:(NSString*)deviceName resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
    //routine to scan LE device
    ...
    NSDictionary* deviceInfo = …
    resolve(deviceInfo);    
}
...
@end

[Android]

import com.facebook.react.bridge.Promise;
...
public class BleModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {
    ...
    //scan device
    @ReactMethod
    public void scanDevice(String deviceName, Promise promise) {
        //routine to scan LE device
        WritableParams deviceInfo = Arguments.createMap();
        ...
        promise.resolve(deviceInfo);
    }
    ...
}

[Javascript]

import BleModule from '/path/to/bleModule'
...
//scan device
BleModule.scanDevice('X_SMART_WATCH').then((device) => {
    console.log(device);
}).catch((err) => {
    console.log(err});
}

Event approach:

[iOS]

/*BleModule.h*/
...
@interface BleModule: RCTEventEmitter
...
@end

/*BleModule.m*/
...
- (NSArray *)supportedEvents {
    return @[@'deviceDiscovered',...
             ];
}

- (void)startObserving
{
    for (NSString *event in [self supportedEvents]) {
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bleModuleEventHandler:) name:event object:nil];
    }
}

- (void)stopObserving
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void) bleModuleEventHandler:(NSNotification*) notification
{
    [self sendEventWithName:notification.name body:notification.userInfo];
}

+ (void) notifyBleEvent:(NSString*) notificationName withData:(NSDictionary*) data
{    
    ...  
    [[NSNotificationCenter defaultCenter] postNotificationName:notificationName
                                                    object:nil
                                                  userInfo:data];
}
...
RCT_EXPORT_METHOD(scanDevice:(NSString*)deviceName)
{
    //routine to scan LE device
    NSData* deviceInfo =
    [BleModule notifyBleEvent:@'deviceDiscovered', deviceInfo];
...
}

[Android]

public class BleModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {
   ...
   //scan device
   @ReactMethod
   public void scanDevice(String deviceName) {
       //routine to scan LE device
       WritableMap deviceInfo = Arguments.createMap();
       ...
       sendEvent('deviceDiscovered',deviceInfo);
    }
    ...
    private void sendEvent(String eventName, @Nullable WritableMap params) {
        mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);                 
}

[Javascript]

/*bleModule.js*/
import { NativeModules, … } from 'react-native'
...
export default class BleModule {
    ...
    getNativeModule() {
        return NativeModules.BleModule;
    }
...
}

/*deviceScan JS UI*/
import { NativeEventEmitter, … } from 'react-native'
import BleModule from '/path/to/bleModule'
...

export default class DeviceScanComponent extends Component {
    ...
    constructor(props) {
        super(props);
        this.eventEmitter = null;
    }

    componentWillMount() {
        //register event
        this.eventEmitter = new NativeEventEmitter(BleModule.getNativeModule());
        this.eventEmitter.addListener('deviceDiscovered', (data) => {
            console.log(data);
            ...
        });
    }

    componentWillUnmount() {
        //remove event
        ...
    }

    _handleScanDevice = () => {
        //scan device
        BleModule.scanDevice('X_SMART_WATCH');
        ...
    }
}

Each approach has pros and cons. It is up to the implementer's convenience to choose the approach based on the projected use case. In our experience, the event approach fits based for BLE action that may take longer time to complete (more than 30 secs) with probability of interruption or error during the action.

기업문화 엿볼 때, 더팀스

로그인

/