Cutting our Blueteeth on React Native

Ian C. Anderson and Blake Williams

We’ve been working on a project with Zagster that uses Bluetooth Low Energy (BLE) to connect to a BLE-powered bike lock so that we can authorize, read, write, and receive notifications from the lock, resulting in a better, faster, and easier riding experience.

Round 1: iOS and react-native-ble

For our first attempt we decided to use react-native-ble to connect and interact with the locks. We were excited to use this library largely because it would allow us to use the same application code for iOS and Android. After a lot of hard work and a few PR’s we finally got this library talking to the locks, connecting and generally doing what it is supposed to do.

Round 1.5: Android and react-native-ble

After shipping iOS it was time to start focusing on Android. Everything seemed fine except BLE wouldn’t work consistently. We tried changing our JavaScript code that interacted with the locks, we tried modifying the native code in react-native-ble, we even tried changing phones and locks in case they were problematic. Nothing we changed seemed to get BLE to work consistently on Android.

We knew that there were issues with BLE reliability on many Android phones, but the behavior we were seeing was so erratic that it became clear that our BLE stack needed an overhaul.

Round 2: Android, SweetBlue, and react-native-ble

At this point we assumed the issue was the native Java code in react-native-ble so we decided we’d replace it. Instead of re-writing using the Android included APIs for BLE we decided to use SweetBlue. SweetBlue softens a lot of the sharp edges of the native APIs and provides a much better interface in comparison to the native APIs.

We eventually got the Java portion of react-native-ble replaced with SweetBlue and things seemed more stable. Unfortunately we still had issues under non-perfect conditions and real world testing. Things seemed better but we still weren’t able to ship with these issues.

A 7 Layer BLE(ito)

Debugging the issues we were seeing was overwhelming due to the several layers of external libraries.

From lowest level to highest level, we had:

  • The Java code within react-native-ble that uses SweetBlue to interface with the Android BLE APIs
  • react-native-ble bindings, which forwards events from the Java layer to the Noble library
  • Noble, which provides a generic JavaScript API for interacting with BLE
  • Axa.js, a custom library that provides an API to interact with AXA locks through Noble, such as authorizing, locking/unlocking, reading the lock status, etc.
  • Several libraries that use Axa.js to keep track of locks, their status, and other metrics/uses.

7 layer bleito

Some of these layers were in our app, and some were scattered around GitHub as their own repositories. Adding logging was not trivial, so the feedback loop was slow and unreliable. Debugging was also painful. Finding the source of the bugs and tracking them through the many layers was difficult and not sustainable.

Round 3: Android and SweetBlue

After trying to massage SweetBlue into react-native-ble and experiencing more issues we only had two options left: inspect/debug each layer extensively, or remove react-native-ble in favor of a custom native module. We opted for the custom native module and moved all BLE code into the app so we could solve problems with more agility.

We took the largely generic SweetBlue code we wrote for react-native-ble and developed a native API specific to working with our BLE locks. This allowed us to put all the complex BLE interactions directly in the Java code removing a lot of our dependency on JavaScript. This allowed us to reduce the amount of events flowing between JS and Java, which increased reliability and performance.

After migrating to our custom Java code, connecting and reconnecting to locks became significantly more stable. We no longer saw the reconnection problems, failed locking, and other issues that we experienced when using our react-native-ble based stack.

Round 4: iOS and RZBluetooth

After shipping the Android app, we were left with a somewhat disjointed BLE stack, since the iOS app was still using react-native-ble, while Android was using our new custom native module. This meant that we had to maintain two very different paths for handling BLE, which was hard to maintain and reason about.

To consolidate our codebase and to reduce the complexity of BLE on iOS, we decided to take a similar approach for iOS as we did with Android.

After surveying the BLE library options for iOS, we came across RZBluetooth, which is a simple block-based wrapper around iOSs CoreBluetooth. This provides a more convenient interface for asynchronous operations than working with delegates and keeps our code consistent with the constructs used in our Java and JavaScript code.

Migrating was relatively simple. We used the existing Android-specific JavaScript code to guide the API of the new Objective-C native module. The end result allowed us to completely consolidate the platform-specific JavaScript code.

LOC (Lines of Code) Impact

Here’s a LOC comparison between the approaches we took:

BLE Stack LOC
react-native-ble Java 802
react-native-ble SweetBlue Java 465
react-native-ble Objective-C 650
Custom SweetBlue Java 489
Custom Objective-C 365

We removed a lot of code by removing react-native-ble and switching to custom native modules. We were able to remove several dependencies which aren’t counted in the LOC analysis above.

In addition to slimming down the amount of Java and Objective-C code we needed to maintain we were also able to remove an entire 759 LOC JavaScript library that we wrote to talk between JavaScript and react-native-ble. We did have to write more JavaScript glue code but it was significantly smaller than the axa.js library we wrote clocking in at only 240 LOC.

Conclusion

Without going through the pain we experienced, we wouldn’t have ended up with as great a solution. It’s easy to look back in hind-sight and think that we started off on the wrong path, but the knowledge we picked up along the way was essential to the success of the custom BLE modules.

That being said, if either of us started a new React Native project that depends on BLE tomorrow, we’d start with custom native modules. We’re extremely happy with the native modules we wrote and both seem to have improved performance and stability. Most React Native solutions won’t require writing native code, but it’s a valuable skill to have when you need more control over low-level behavior.