Skip to main content

Building an app with React-Native, Bluetooth Low Energy and Redux-Saga - part 1

Building an app with React-Native, Bluetooth Low Energy and Redux-Saga

Part 1 - Initial setup and scanning for devices

By Daniel Friyia, Agile Software Engineer, TribalScale

Photo Credits: Google...

What are we building?

I thought I’d start this article series by first showing what we are trying to build and then explaining how to build it. Below is an app that will allow you to scan for a heart rate monitor, connect to that heart rate monitor and get the user’s heart rate. In this article, we are going to scan for devices, but by the end of Part 2, we will have an app that works like this:

Setting up the project

Now that we know what we’re going to build, let’s start generating the React-Native project using TypeScript. The instructions in this article can be used in Vanilla JS as well. Just remove the type annotations. To generate the project, run the following command in the terminal:

npx react-native init BLEReactNativeSample --template react-native-template-typescript

Setting up Redux

For this project, we will use Redux slices since they are a lot less code than your traditional Redux setup. I won’t go into detail here since this article is more about Redux-Saga and Bluetooth than Redux. We’ll make a simple setup using slices. To start, you’ll want to run the following command in the root of your project:

npm install redux react-redux redux-logger redux-saga @types/react-redux @reduxjs/toolkit @types/redux-logger

The Redux and React-Redux libraries are obviously for running Redux. We’ll use Redux-Saga for our async functionality with Bluetooth and redux-logger for debugging events and the store. The reducer is going to start in a bare-bones way. For now, we are just going to keep track of the devices available after a scan. You should make a redux directory and use it to hold all of these redux-related files. It should look something like this:


import {createSlice, PayloadAction} from '@reduxjs/toolkit';

type BluetoothState = {
  availableDevices: Array<BluetoothPeripheral>;
};

const initialState: BluetoothState = {
  availableDevices: [],
};

const bluetoothReducer = createSlice({
  name: 'bluetooth',
  initialState: initialState,
  reducers: {
    bluetoothPeripheralsFound: (
      state: BluetoothState,
      action: PayloadAction<Array<BluetoothPeripheral>>,
    ) => {
      state.availableDevices = action.payload;
    },
  },
});

export const {
    bluetoothPeripheralsFound
} = bluetoothReducer.actions

export default bluetoothReducer

Like all things TypeScript, we are going to make use of types to bring structure to the information we get back from the Bluetooth library. I created a models folder with the BluetoothPeripheral type. You should create this file as well under <project-root>/models/BluetoothPeripheral.ts.

export type BluetoothPeripheral = {
id: string;
name: string;
serviceUUIDs: Array<string>;
}

Next, we’ll configure the store. I won’t go into too much detail because any React developer has done this a million times. I am putting this here for copy/paste convenience:


import logger from 'redux-logger';

import {configureStore, combineReducers} from '@reduxjs/toolkit';
import bluetoothReducer from '../modules/Bluetooth/bluetooth.reducer';
import {useDispatch} from 'react-redux';

const rootReducer = combineReducers({
  bluetooth: bluetoothReducer.reducer,
});

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => {
      return getDefaultMiddleware().concat(logger)
  },
  devTools: process.env.NODE_ENV === 'production'
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();

Finally, wrap your app component with a Provider like this and we can move on to setting up Bluetooth:

const App = () => {
return (
<Provider store={store}>
<Home />
</Provider>
);
};
const Home = () => {
const dispatch = useDispatch();
return (
<SafeAreaView style={styles.container}>
<Text>Hello world</Text>
<Button
title="Press Here"
onPress={() => {
dispatch(bluetoothPeripheralsFound(['AA:DD:CC:DD']));
}}
/>
</SafeAreaView>
);
};

At this point if you tap that button you should see AA:BB:CC:DD pop up in your redux logger. This should confirm that our slice setup is, in fact, working.

Setting up Bluetooth Low Energy in React Native

Here is where things get complicated and more interesting. We are going to set up Bluetooth to communicate with our React-Native app. In order to do this we’ll use the Bluetooth library react-native-ble-plx, which, at the time of writing, is the most popular in React-Native. First install the library into your project by running this command at the root of your project:

npm install --save react-native-ble-plx

Next we’ll need to follow some iOS and Android Specific instructions

iOS Specific Instructions

Because React-Native is written in Objective-C and react-native-ble-plx is written in Swift we’ll need a bridging header. This is relatively easy to create. First, create a new Swift file in XCode with any name. After that choose the option for a bridging header. It will look like this:

Next cd into the iOS folder for your React-Native project and type

npx pod install. 

This should install the native dependencies needed for ios. After that add this to your ios/info.plist file under your key/value pairs.

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Our app uses bluetooth to find, connect and transfer data between different devices</string>

Finally, you’ll want to add Bluetoooth and background permissions so that Bluetooth can run while the app is in the background. You can do it on the Signing and Capabilities Screen:

Android Instructions

The first thing we’ll need to do is make sure our minSdk in the top level build.gradle is set to at least 18

buildscript {
ext {
...
minSdkVersion = 18
...

We’ll also want to make sure gitpack is added to our repositories

allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}

Finally in the Android Manifest add the following permissions:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"...<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
....

If you get lost modifying either of these files check out my build.gradle and AndroidManifest.xml in the sample project.

Testing initial setup

In order to test our setup we are going to do the most basic thing possible and just scan for Bluetooth devices. To do this we will add some quick throwaway code to our Home component. Remember to DELETE THIS AFTER TESTING 😱!

Add this to the top of your file under the imports:

const manager = new BleManager()

Then in the Home component add this method:

const scanForPeripherals = () => {
manager.startDeviceScan(null, null, (error, scannedDevice) => {
console.log(scannedDevice)
})
}

Finally make the onPress look like this:

onPress={() => {
dispatch(bluetoothPeripheralsFound(['AA:DD:CC:DD']));
scanForPeripherals()
}}

Now when you scan in the app you should see results in your debugger. Here is an example:

Wrapping Bluetooth

Next, we are going to wrap our Bluetooth library in a singleton object. There are a couple of reasons for this. First, we want every part of the app to share the same instance of BleManager. Second, if you have to swap out this library later on for another, it’s easier if you wrap your library. Let’s get started.

In the singleton, we will construct the BleManager and add our scan for peripherals code into it. This is primarily a copy/paste job. For those curious, null, null is passed into startDeviceScan because we want to scan for all devices available with no restrictions. The end result will end up looking like this:


import {BleError, BleManager, Device} from 'react-native-ble-plx';

class BluetoothLeManager {
  bleManager: BleManager;

  constructor() {
    this.bleManager = new BleManager();
  }

  scanForPeripherals = (
    onDeviceFound: (device: Device | null) => void,
    onError: (error: BleError) => void,
  ) => {
    this.bleManager.startDeviceScan(null, null, (error, scannedDevice) => {
      if (error) {
        return onError(error);
      }
      return onDeviceFound(scannedDevice);
    });
  };

  stopScanningForPeripherals = () => {
    this.bleManager.stopDeviceScan();
  };
}

const bluetoothLeManager = new BluetoothLeManager();

export default bluetoothLeManager;

Configuring Redux-Saga

So we are all hooked up to Bluetooth, cool! Now we can move on to setting up Redux Saga to retrieve information from Bluetooth.

The first thing we’ll want to do is add Redux-Saga to our store file in the project. This is another one of those things that any React developer has done a million times, so I’ll just give a summary of the steps I used here.

First add a root saga, we’ll make the Bluetooth root saga in a minute

const sagaMiddleware = createSagaMiddleware();
const rootSaga = function* rootSaga() {
yield all([
fork(bluetoothSaga)
])
}

Then add the root saga to our middlewares:

middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(logger).concat(sagaMiddleware);
},

Finally, start the middleware

sagaMiddleware.run(rootSaga)

Your store file should now look like this minus imports and typing


// .......
const sagaMiddleware = createSagaMiddleware();

const rootSaga = function* rootSaga() {
  yield all([fork(bluetoothSaga)]);
};

const rootReducer = combineReducers({
  bluetooth: bluetoothReducer.reducer,
});

export const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware => {
    return getDefaultMiddleware().concat(logger).concat(sagaMiddleware);
  },
  devTools: process.env.NODE_ENV === 'production',
});

sagaMiddleware.run(rootSaga);
// .......

Before creating our saga, we will need to adjust our reducer so we are able to handle device found events from the Bluetooth library. First, we will add this code which shows that we are scanning in the state. It’s here to initiate scanning for peripherals:

scanForPeripherals: state => {
state.isScanning = true;
},

Next, we’re going to export the action types so that we can use them in redux-saga

export const sagaActionConstants = {
SCAN_FOR_PERIPHERALS: bluetoothReducer.actions.scanForPeripherals.type,
ON_DEVICE_DISCOVERED: bluetoothReducer.actions.bluetoothPeripheralsFound.type,
};

Now let’s create a saga file for our Bluetooth actions. We’ll start by creating a root saga that listens for all actions. We will take the types from our bluetooth slice

export function* bluetoothSaga() {
yield takeEvery(
sagaActionConstants.SCAN_FOR_PERIPHERALS,
watchForPeripherals,
);
}

After this comes the more complex part. We’ll need to make a generator function that handles Bluetooth events as they come in from the library. This can be done in one method that I will break down here.

First, we’ll make a type to handle the device information that comes in:

type TakeableDevice = {
payload: {id: string; name: string; serviceUUIDs: string};
take: (cb: (message: any | END) => void) => Device;
};

Next, we need to create an Event Channel. Event Channels are used in Redux saga to pick up on spontaneous actions that can take place at any interval.

function* watchForPeripherals(): Generator<AnyAction, void, TakeableDevice> {
const onDiscoveredPeripheral = () => eventChannel(emitter => {
return bluetoothLeManager.scanForPeripherals(emitter);
});

After this, we’ll call our event channel as a function

const channel: TakeableChannel<unknown> = yield   call(onDiscoveredPeripheral);

This next part looks a little weird because it is essentially an infinite loop that only ends when the application terminates. You may ask yourself, how does this not break the UI thread? The reason is because generator functions will pause until they get a result from yield. The JavaScript engine will then work on other things while it waits for a result to come back. This means that the infinite loop isn’t holding up the UI while waiting for events. This is the pattern recommended by Redux Saga and can be found here if you want more information.

try {
while (true) {
const response = yield take(channel);

Finally we’ll use put to send all information to the redux store

     yield put({
type: sagaActionConstants.ON_DEVICE_DISCOVERED,
payload: {
id: response.payload.id,
name: response.payload.name,
serviceUUIDs: response.payload.serviceUUIDs,
},
});
}
} catch (e) {
console.log(e);
}
}

At this point we’ll want all these devices to be captured by Redux. We’ll modify onPeripheralsFound to weed out any duplicates and add our Bluetooth Peripherals to the state.

bluetoothPeripheralsFound: (
state: BluetoothState,
action: PayloadAction<BluetoothPeripheral>,
) => {
// Ensure no duplicate devices are added
const isDuplicate = state.availableDevices.some(
device => device.id === action.payload.id,
);
if (!isDuplicate) {
state.availableDevices = state.availableDevices.concat(action.payload);
}
},

Taken together the code should look something like this:


type TakeableDevice = {
  payload: {id: string; name: string; serviceUUIDs: string};
  take: (cb: (message: any | END) => void) => Device;
};

function* watchForPeripherals(): Generator<AnyAction, void, TakeableDevice> {
  const onDiscoveredPeripheral = () =>
    eventChannel(emitter => {
      return bluetoothLeManager.scanForPeripherals(emitter);
    });

  const channel: TakeableChannel<Device> = yield call(onDiscoveredPeripheral);

  try {
    while (true) {
      const response = yield take(channel);

      yield put({
        type: sagaActionConstants.ON_DEVICE_DISCOVERED,
        payload: {
          id: response.payload.id,
          name: response.payload.name,
          serviceUUIDs: response.payload.serviceUUIDs,
        },
      });
    }
  } catch (e) {
    console.log(e);
  }
}
  
export function* bluetoothSaga() {
  yield takeEvery(
    sagaActionConstants.SCAN_FOR_PERIPHERALS,
    watchForPeripherals,
  );
}

Finally, we can finish up by displaying our peripherals to the screen to prove everything works. Simply get the peripheral information from the state and display it to the screen in string form. You can do it by adding this code to your Home component

const Home: FC = () => {
const dispatch = useDispatch();
const devices = useSelector(
(state: RootState) => state.bluetooth.availableDevices,
);

return (
<SafeAreaView style={styles.container}>
<ScrollView>
{devices.map(device => (
<>
<Text>{JSON.stringify(device)}</Text>
<View height={20} />
</>
))}
<Button
title="Press Here To Scan"
onPress={() => {
dispatch(scanForPeripherals());
}}
/>
</ScrollView>
</SafeAreaView>
);
};

And BOOM! We can get devices 🙂

This concludes part 1 of the series. Check out part 2 where we’ll connect to a device and retrieve data from it by using the infrastructure we created in this article.

Have questions about building an app with React-Native? Please mention in Comments.

Comments

Popular Posts

How I Reduced the Size of My React Native App by 85%

How and Why You Should Do It I borrowed 25$ from my friend to start a Play Store Developer account to put up my first app. I had already created the app, created the assets and published it in the store. Nobody wants to download a todo list app that costs 25mb of bandwidth and another 25 MB of storage space. So today I am going to share with you how I reduced the size of Tet from 25 MB to around 3.5 MB. Size Matters Like any beginner, I wrote my app using Expo, the awesome React Native platform that makes creating native apps a breeze. There is no native setup, you write javascript and Expo builds the binaries for you. I love everything about Expo except the size of the binaries. Each binary weighs around 25 MB regardless of your app. So the first thing I did was to migrate my existing Expo app to React Native. Migrating to React Native react-native init  a new project with the same name Copy the  source  files over from Expo project Install all de...

How to recover data of your Android KeyStore?

These methods can save you by recovering Key Alias and Key Password and KeyStore Password. This dialog becomes trouble to you? You should always keep the keystore file safe as you will not be able to update your previously uploaded APKs on PlayStore. It always need same keystore file for every version releases. But it’s even worse when you have KeyStore file and you forget any credentials shown in above box. But Good thing is you can recover them with certain tricks [Yes, there are always ways]. So let’s get straight to those ways. 1. Check your log files → For  windows  users, Go to windows file explorer C://Users/your PC name/.AndroidStudio1.4 ( your android studio version )\system\log\idea.log.1 ( or any old log number ) Open your log file in Notepad++ or Any text editor, and search for: android.injected.signing and if you are lucky enough then you will start seeing these. Pandroid.injected.signing.store.file = This is  file path where t...

React Native - Text Input

In this chapter, we will show you how to work with  TextInput  elements in React Native. The Home component will import and render inputs. App.js import React from 'react' ; import Inputs from './inputs.js' const App = () => { return ( < Inputs /> ) } export default App Inputs We will define the initial state. After defining the initial state, we will create the  handleEmail  and the  handlePassword  functions. These functions are used for updating state. The  login()  function will just alert the current value of the state. We will also add some other properties to text inputs to disable auto capitalisation, remove the bottom border on Android devices and set a placeholder. inputs.js import React , { Component } from 'react' import { View , Text , TouchableOpacity , TextInput , StyleSheet } from 'react-native' class Inputs extends Component { state = { ...