Make Your First IoT React Native Application with Bluetooth Low Energy

React Native

iot

objet connecté

Bluetooth Low Energy

arduino

ble

What is Bluetooth Low Energy ?

You probably know Bluetooth? So first, remember that Bluetooth Low Energy (BLE) is not the same at all as Bluetooth Classic! What are the differences? Mainly the consumption! Indeed, the Bluetooth LE consumes from 2 to 4 times less than Bluetooth Classic. So it is the perfect candidate for the Internet of Thing (IoT) devices which could be autonomous up to 5 years while broadcasting data!

What you should remember is that both technologies use the 2.4 GHz frequency band. The Bluetooth LE can support data rate from 125 kb/s to 2 Mb/s and is particularly appropriate for IoT applications. It is used into smartwatches, hearth rate sensors, sensors for the industry, connected lightbulbs, etc...

How does it work?

The Bluetooth SIG (the organization who writes the standard) described how to communicate with a Bluetooth LE module. To do so they implemented the GATT profile (Generic Attribute Profile) to maximize the benefit of the BLE system. This profile describe how to communicate between a central (e.g. you phone) and a peripheral (e.g. the sensor).

You can read the differences between the serial port UART and the GATT profile here.

With the GATT communication profile, you can see a Bluetooth LE (BLE) chip as a group of services (like an API with routes), which have characteristics for each of them. Finally each characteristics have descriptors.

Each service provides some features to the BLE module (battery level, elevation, hearth rate, etc...). Each service has characteristics which are properties of it. You can read/write/subscribe on these characteristics (the value of the battery level, the name of the battery manufacturer, etc...). To describe what are these characteristics, there are the descriptors for each of them. These descriptors are characteristic metadata. They give more information about them (measure unit, standard, etc..).

I will illustrate this with a simple use case. I have a flower at home, and I would try to keep it alive longer than the previous one (I forgot to water it... 🍂). I bought a humidity sensor to detect if the flower need to be watered or not. This sensor has a BLE module with the following architecture:

Untitled

The first service is the service allocated to the battery information. The second one is relative to the humidity measure of my flower's earth. Let's focus on the second characteristic we are interested in. I can read the second service's characteristic to get the humidity value measured by the sensor. The sensor can write on the characteristic the value it measures, then I can read the value with my phone to get this value.

What is the process to read a characteristic?

To read a characteristic, you have to follow this communication scheme:

Capture d’écran 2021-03-29 à 19.11.18

All these steps take quite a long time. Indeed, it is a trade-off of BLE: all communication are asynchronous, and we can receive just a few information each time.

You can read more about GATT profile on this article.

Connect your first object to a React Native application

What are we going to do? The target application is an app which can:

  • scan BLE devices around
  • connect to a BLE device
  • discover its services and characteristic
  • read and write on a characteristic

Let's build a BLE object with an Arduino board! What is an Arduino? According to the official website:

Arduino is an open-source electronics platform based on easy-to-use hardware and software. Arduino boards are able to read inputs - light on a sensor, a finger on a button, or a Twitter message - and turn it into an output - activating a motor, turning on an LED, publishing something online.

For this tutorial I'll use the following hardware:

  • A BLE module that is Arduino compatible. I have chosen the Adafruit Bluefruit LE UART Friend (17,50€), but you can choose yours while you take care of the documentation of your chip.
  • Wires, resistors, LEDs, and a breadboard you can find here for example (35€ for the whole kit)

Arduino circuit

Connect the BLE module to the Arduino Uno. Then connect a RGB led which will be used to inform the user about the BLE module status.

Untitled 2 IMG_7279

 

 
ℹ️
The black wire linked to the RGB led is connected to the anode, the longer leg of my led. But be careful! The circuit could change if your RGB led is not the same than mine. Some RGB led must be connected to the 5V pin by the cathode leg. Look at this article for further information.

Arduino Code

Open the Arduino IDE then create a new sketch and save it.

You should have the following code base:

void setup() {
  // put your setup code here, to run once:
}

void loop() {
  // put your main code here, to run repeatedly:
}
  • Follow the software configuration for the BLE module
  • Initialize variables in the head of the file (before the setup function)
    // Include libraries needed
    #include <Arduino.h>
    #include "Adafruit_BLE.h"
    #include "Adafruit_BluefruitLE_UART.h"
    #include "BluefruitConfig.h"
    
    #if SOFTWARE_SERIAL_AVAILABLE
      #include <SoftwareSerial.h>
    #endif
    
        #define FACTORYRESET_ENABLE         1
        #define MINIMUM_FIRMWARE_VERSION    "0.6.6"
        #define MODE_LED_BEHAVIOUR          "MODE"
    
    
    // Initialize the Software serial link to read data from the module
    SoftwareSerial bluefruitSS = SoftwareSerial(BLUEFRUIT_SWUART_TXD_PIN, BLUEFRUIT_SWUART_RXD_PIN);
    
    // Create bluetooth LE class to control our BLE module 
    Adafruit_BluefruitLE_UART ble(bluefruitSS, BLUEFRUIT_UART_MODE_PIN,
                          BLUEFRUIT_UART_CTS_PIN, BLUEFRUIT_UART_RTS_PIN);
    
    // Define the pin where the RGB led is wired
    int pinR = 3;
    int pinG = 5;
    int pinB = 6;
    
    // Create variables to check characteristics have been created successful
    int counterChannel;
    int elevationChannel;
    
    // Initialize the counter to 0
    int counter = 0;
  • Add an error handler to help us to detect errors
    // A small helper
    void error(const __FlashStringHelper*err) {
      Serial.println(err);
      digitalWrite(pinR, 255);
      while (1);
    }

    If an error occurred we will call the function, and the red led will blink to alert us about it.

  • Set up your Arduino, pins, serial output, and BLE module
    void setup(void)
    {
      pinMode(pinR, OUTPUT);
      pinMode(pinG, OUTPUT);
      pinMode(pinB, OUTPUT);
    
      // At the beginning all the led are turned down
      analogWrite(pinR, 0);
      analogWrite(pinG, 0);
      analogWrite(pinB, 0);
    
      // Verify if the serial port is available, and initialize it to display some information
      while(!Serial) {
        delay(500);
      }
    
      Serial.begin(115200);
    
      /* Initialize the module */
      Serial.print(F("Initializing the Bluefruit LE module: "));
    
      if ( !ble.begin(VERBOSE_MODE) )
      {
        error(F("Couldn't find Bluefruit, make sure it's in CoMmanD mode & check wiring?"));
      }
    
      if ( FACTORYRESET_ENABLE )
      {
        /* Perform a factory reset to make sure everything is in a known state */
        Serial.println(F("Performing a factory reset: "));
        if ( !ble.factoryReset() ){
          error(F("Couldn't factory reset"));
        }
      }
    
      /* Disable command echo from Bluefruit */
      ble.echo(false);
    
      Serial.println("Requesting Bluefruit info:");
      /* Print Bluefruit information */
      ble.info();
    
      ble.reset();
    }

     

  • Following the above code, in the setup function, create a new BLE service with the AT command
    ble.println(F("AT+GATTADDSERVICE=UUID=0x180F"));
       if(!ble.waitForOK()){
        error(F("Error adding service"));
       }

    Here we send command to the BLE module with the method println. We format the string with F() to tell the BLE module that the command is a string. It avoids lots of errors due to both the language C++ and its types and the serial Arduino link. Indeed, an Arduino board is a physical device with which you have to converse. To do it, we use the serial port (the USB wire linked to your computer). This serial port sends data with a complex protocol I won't detail here, but in short terms it groups all the bytes you want to send by packages (group of bytes). And if you want to send long strings (just a few bytes sometimes), your string is cut, or send partially.

    Here we speak the AT Command language with the BLE module serial port AT+. We tell it to add a new service with the GATT protocol GATTADDSERVICE , and this service will have the 16-bit UUID (universally unique identifier) 0x180F, as described in the GATT Bluefruit documentation

    If an error occurred, we send the message "Error adding service" to our error helper function, then the red led will blink.

  • Let's add two characteristics to this new service
    // properties 0x10 means that this characteristic could be used to notify any change in value
    counterChannel = ble.println(F("AT+GATTADDCHAR=UUID=0x2A19,PROPERTIES=0x10,MIN_LEN=1,DESCRIPTION=Counter,VALUE=100"));
    if(counterChannel == 0){
     error(F("Error adding characteristic"));
    }
    
    elevationChannel = ble.println(F("AT+GATTADDCHAR=UUID=0x2A6C,PROPERTIES=0x08,MIN_LEN=1,DESCRIPTION=Elevation,VALUE=0"));
    if(elevationChannel == 0){
     error(F("Error adding characteristic"));
    }
  • We can reset the BLE module now to take in count the previous modifications. It is needed to work perfectly.
    // Reset the BLE module to take in count the previous modifications
    ble.reset();
    Serial.println();
    
    ble.verbose(false);  // Debug info is a little annoying after this point!
  • We have finished the setup process. Now we can wait for a device to connect to the Adafruit BLE module. When a device will be connected, the blue led blinks 2 times to alert the user, then lights in green to show that the bluetooth LE link is ready for usage.
    /* Wait for connection */
      while (!ble.isConnected()) {
          digitalWrite(pinB, 255);
          delay(500);
          digitalWrite(pinB, 0);
          delay(500);
      }
      digitalWrite(pinG, 255);

    Here the Arduino board is waiting for a device to be connected before to continue. Once a device is connected, the board enter in the loop function. Let's test it now!

Test the code

  • Connect the Arduino board to your computer
  • Open the Arduino IDE
  • In the menu under Tools > Port choose your Arduino board
  • In the menu under Tools > Type choose your Arduino type
    Untitled 3
  • Click on the upload button to upload and execute your code in the Arduino board
    Untitled 4
    ℹ️
    If the console shows you an error message, try to fix the error, or ask the big Arduino community to help you 😇
  • If everything goes well, you should have your LED lights up in blue waiting for a connection
    ℹ️
    Sometimes the Arduino link with your computer is capricious, and you have to upload several times your code before finishing it with success.

Read and write characteristic values

Lets focus on the loop function to add some features to our Arduino module.

  • Read a characteristic value

    In the loop function you can add the following lines to read the elevationcharacteristic value and print it in the serial monitor (you can open with ctrl + M on windows or cmd+M on macOS

    // Send the AT command to read the second characteristic we added
    	int elevation = ble.println(F("AT+GATTCHAR=2"));
    
    // If an error occured ("OK" is not received), then display an error message
      if(!ble.waitForOK()) {
        Serial.println(F("Error when reading elevation"));
       }
    
    // Print the elevation value in the serial monitor
      Serial.print(F("[Elevation] ")); Serial.println(elevation);

     

  • Write in a characteristic
    // Increase the counter value of 1;
      counter++;
    
    // Send the AT command to write the counter value in the first characteristic we added
      ble.print(F("AT+GATTCHAR=1,"));
      ble.println(counter);
    
    // Handle the error if "OK" is not received
      if(!ble.waitForOK()) {
        Serial.println(F("Error when sending counter"));
       }
  • Set a visual callback for the user to know if the counter has increased
    // The green LED blinks to inform the user the counter inscreased
       digitalWrite(pinG, 0);
       delay(500);
       digitalWrite(pinG, 255);
       delay(500);
    
    // End of the loop function

React Native application code

Let's code the React Native application to read and write on the BLE module!

First you need the React Native environment to develop a React Native application. You can follow this article or the official documentation.

Then you can initialize your app by creating a folder for your code and execute:

npx react-native init BleProject --template react-native-template-typescript

where BleProject is the name of your project

Check that your application can be launched correctly:

  • Execute npx react-native start in a terminal
  • Execute npx react-native run-android for Android devices, or npx react-native run-ios for iOS simulator, in an other terminal

The Architecture of the app

Folder architecture

Untitled 5

Screen architecture

Here are screenshots of the app at the end of this article. I apologize for the UX/UI, but this is not the subject 😇

Screenshot_20210304-171455_BLEProject Screenshot_20210304-171810_BLEProject Screenshot_20210304-171759_BLEProject Screenshot_20210304-171802_BLEProject

You see that there are 2 screens the Home screen and the Device screen. Let's begin by adding the react-navigation package to have a navigator and our 2 screens.

You can follow the react-navigation documentation, or see my Github repo with the code. I will focus here on Bluetooth Low Energy features.

Features

Our application will be available to:

  • scan all BLE devices around us
  • connect to one device (our Adafruit device)
  • listen for connection or disconnection of the device
  • read information about the device, its services, and its characteristics
  • write on a characteristic

The react-native-ble-plx package

Thanks to Polidea which have develop the awesome react-native-ble-plx package we can use the Ble module of our devices with a React Native app without effort!

Install the package on your project by following the Configure & Install section.

This package is a react-native package which uses native Android and iOS module to connect to the hardware and communicate with the phone's BLE module.

ℹ️
Because the package uses native modules, you can't use it with an Expo project. You must eject your project if you want to continue.

Connect your phone with a BLE device

  • On the Home screen create a new BleManager class:
    import { BleManager, Device } from 'react-native-ble-plx';
    
    const manager = new BleManager();
    
    const HomeScreen = () => {
    	return (
        <SafeAreaView>
    	    <View style={styles.body}>
    	      <View style={styles.sectionContainer}>
    	        <Text style={styles.sectionTitle}>Step One</Text>
    	      </View>
    	    </View>
        </SafeAreaView>
      );
    }
    
    const styles = StyleSheet.create({
      body: {
        backgroundColor: Colors.red,
      },
      sectionContainer: {
        marginTop: 32,
        paddingHorizontal: 24,
      },
      sectionTitle: {
        fontSize: 24,
        fontWeight: '600',
        color: Colors.black,
      },
      
    });
    
    export { HomeScreen };
  • Add a scan button in the home screen
    // Reducer to add only the devices which have not been added yet
    // Indeed, when the bleManager searches for devices, each time it detects a ble device, it returns the ble device even if this one has already been returned
    const reducer = (
      state: Device[],
      action: { type: 'ADD_DEVICE'; payload: Device } | { type: 'CLEAR' },
    ): Device[] => {
      switch (action.type) {
        case 'ADD_DEVICE':
          const { payload: device } = action;
    
          // check if the detected device is not already added to the list
          if (device && !state.find((dev) => dev.id === device.id)) {
            return [...state, device];
          }
          return state;
        case 'CLEAR':
          return [];
        default:
          return state;
      }
    };
    
    const HomeScreen = () => {
    	// reducer to store and display detected ble devices
      const [scannedDevices, dispatch] = useReducer(reducer, []);
    
      // state to give the user a feedback about the manager scanning devices
      const [isLoading, setIsLoading] = useState(false);
    
      const scanDevices = () => {
        // display the Activityindicator
        setIsLoading(true);
    
        // scan devices
        manager.startDeviceScan(null, null, (error, scannedDevice) => {
          if (error) {
            console.warn(error);
          }
    
          // if a device is detected add the device to the list by dispatching the action into the reducer
          if (scannedDevice) {
            dispatch({ type: 'ADD_DEVICE', payload: scannedDevice });
          }
        });
    
        // stop scanning devices after 5 seconds
        setTimeout(() => {
          manager.stopDeviceScan();
          setIsLoading(false);
        }, 5000);
      };
    	return (
    		{/* ...Header with title */}
    
    		{/* Clear the device list to reset it */}
    		<Button
          title="Clear devices"
          onPress={() => dispatch({ type: 'CLEAR' })}
        />
    		{isLoading ? (
    	      <ActivityIndicator color={'teal'} size={25} />
    	  ) : (
    	    <Button title="Scan devices" onPress={scanDevices} />
    	  )}
    	);
    }

     

  • Displays the list of devices
    // screens/HomeScreen.tsx
    return (
        <SafeAreaView style={styles.body}>
    			<View style={styles.body}>
    	      <View style={styles.sectionContainer}>
    	        <Text style={styles.sectionTitle}>Step One</Text>
    	      </View>
    	    </View>
          <FlatList
            keyExtractor={(item) => item.id}
            data={scannedDevices}
            renderItem={({ item }) => <DeviceCard device={item} />}
            contentContainerStyle={styles.content}
          />
        </SafeAreaView>
      );
    // components/DeviceCard.tsx
    
    type DeviceCardProps = {
      device: Device;
    };
    
    const DeviceCard = ({ device }: DeviceCardProps) => {
      const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
    
      const [isConnected, setIsConnected] = useState(false);
    
      useEffect(() => {
    		// is the device connected?
        device.isConnected().then(setIsConnected);
      }, [device]);
    
      return (
        <TouchableOpacity
          style={styles.container}
    			// navigate to the Device Screen
          onPress={() => navigation.navigate('Device', { device })}>
          <Text>{`Id : ${device.id}`}</Text>
          <Text>{`Name : ${device.name}`}</Text>
          <Text>{`Is connected : ${isConnected}`}</Text>
          <Text>{`RSSI : ${device.rssi}`}</Text>
    			{/* Decode the ble device manufacturer which is encoded with the base64 algorithm */}
          <Text>{`Manufacturer : ${Base64.decode(
            device.manufacturerData?.replace(/[=]/g, ''),
          )}`}</Text>
          <Text>{`ServiceData : ${device.serviceData}`}</Text>
          <Text>{`UUIDS : ${device.serviceUUIDs}`}</Text>
        </TouchableOpacity>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        backgroundColor: 'white',
        marginBottom: 12,
        borderRadius: 16,
        shadowColor: 'rgba(60,64,67,0.3)',
        shadowOpacity: 0.4,
        shadowRadius: 10,
        elevation: 4,
        padding: 12,
      },
    });
    
    export { DeviceCard };

     

    At this step you should launch the app, scan for devices, press on the device (check that your Arduino is launched and the LED is blinking before), then you should be redirected to the device screen.

    Well done! You have done your first step in the BLE world! 👏

Discover services and characteristics

On the device screen we receive the connected device object. With it we can discover more information about it and display it to the user:

const DeviceScreen = ({
  route,
  navigation,
}: StackScreenProps<RootStackParamList, 'Device'>) => {
  // get the device object which was given through navigation params
  const { device } = route.params;

  const [isConnected, setIsConnected] = useState(false);
  const [services, setServices] = useState<Service[]>([]);

  // handle the device disconnection
  const disconnectDevice = useCallback(async () => {
    navigation.goBack();
    const isDeviceConnected = await device.isConnected();
    if (isDeviceConnected) {
      await device.cancelConnection();
    }
  }, [device]);

  useEffect(() => {
    const getDeviceInformations = async () => {
      // connect to the device
      const connectedDevice = await device.connect();
      setIsConnected(true);

      // discover all device services and characteristics
      const allServicesAndCharacteristics = await connectedDevice.discoverAllServicesAndCharacteristics();
      // get the services only
      const discoveredServices = await allServicesAndCharacteristics.services();
      setServices(discoveredServices);
    };

    getDeviceInformations();

    device.onDisconnected(() => {
      navigation.navigate('Home');
    });

    // give a callback to the useEffect to disconnect the device when we will leave the device screen
    return () => {
      disconnectDevice();
    };
  }, [device, disconnectDevice, navigation]);

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Button title="disconnect" onPress={disconnectDevice} />
      <View>
        <View style={styles.header}>
          <Text>{`Id : ${device.id}`}</Text>
          <Text>{`Name : ${device.name}`}</Text>
          <Text>{`Is connected : ${isConnected}`}</Text>
          <Text>{`RSSI : ${device.rssi}`}</Text>
          <Text>{`Manufacturer : ${device.manufacturerData}`}</Text>
          <Text>{`ServiceData : ${device.serviceData}`}</Text>
          <Text>{`UUIDS : ${device.serviceUUIDs}`}</Text>
        </View>
        {/* Displays a list of all services */}
        {services &&
          services.map((service) => <ServiceCard service={service} />)}
      </View>
    </ScrollView>
  );
};

What we can observe is that all BLE class methods are asynchronous methods. It is explained by the physical communication between the phone and the BLE module. It is not instantaneous! So each time you ask for an information you have to wait that the question is asked to the BLE module, then the BLE module has to send you back the answer to your phone.

Read a characteristic

Finally, we are getting to the end of this long process to read a characteristic 😃

Here is the code of the ServiceCard used in the device screen:

const ServiceCard = ({ service }: ServiceCardProps) => {
  const [descriptors, setDescriptors] = useState<Descriptor[]>([]);
  const [characteristics, setCharacteristics] = useState<Characteristic[]>([]);
  const [areCharacteristicsVisible, setAreCharacteristicsVisible] = useState(
    false,
  );

  useEffect(() => {
    const getCharacteristics = async () => {
      const newCharacteristics = await service.characteristics();
      setCharacteristics(newCharacteristics);
      newCharacteristics.forEach(async (characteristic) => {
        const newDescriptors = await characteristic.descriptors();
        setDescriptors((prev) => [...new Set([...prev, ...newDescriptors])]);
      });
    };

    getCharacteristics();
  }, [service]);

  return (
    <View style={styles.container}>
      <TouchableOpacity
        onPress={() => {
          setAreCharacteristicsVisible((prev) => !prev);
        }}>
        <Text>{`UUID : ${service.uuid}`}</Text>
      </TouchableOpacity>

      {areCharacteristicsVisible &&
        characteristics &&
        characteristics.map((char) => (
          <CharacteristicCard key={char.id} char={char} />
        ))}
      {descriptors &&
        descriptors.map((descriptor) => (
          <DescriptorCard key={descriptor.id} descriptor={descriptor} />
        ))}
    </View>
  );
};

And once you have read the service, you are able to read its characteristic and display them in the CharacteristicCard:

const CharacteristicCard = ({ char }: CharacteristicCardProps) => {
  const [measure, setMeasure] = useState('');
  const [descriptor, setDescriptor] = useState<string | null>('');

  useEffect(() => {
    // discover characteristic descriptors
    char.descriptors().then((desc) => {
      desc[0]?.read().then((val) => {
        if (val) {
          setDescriptor(Base64.decode(val.value));
        }
      });
    });

    // read on the characteristic 👏
    char.monitor((err, cha) => {
      if (err) {
        console.warn('ERROR');
        return;
      }
      // each received value has to be decoded with a Base64 algorithm you can find on the Internet (or in my repository 😉)
      setMeasure(decodeBleString(cha?.value));
    });
  }, [char]);

  // write on a charactestic the number 6 (e.g.)
  const writeCharacteristic = () => {
    // encode the string with the Base64 algorithm
    char
      .writeWithResponse(Base64.encode('6'))
      .then(() => {
        console.warn('Success');
      })
      .catch((e) => console.log('Error', e));
  };

  return (
    <TouchableOpacity
      key={char.uuid}
      style={styles.container}
      onPress={writeCharacteristic}>
      <Text style={styles.measure}>{measure}</Text>
      <Text style={styles.descriptor}>{descriptor}</Text>
      <Text>{`isIndicatable : ${char.isIndicatable}`}</Text>
      <Text>{`isNotifiable : ${char.isNotifiable}`}</Text>
      <Text>{`isNotifying : ${char.isNotifying}`}</Text>
      <Text>{`isReadable : ${char.isReadable}`}</Text>
      <TouchableOpacity>
        <Text>{`isWritableWithResponse : ${char.isWritableWithResponse}`}</Text>
      </TouchableOpacity>
      <Text>{`isWritableWithoutResponse : ${char.isWritableWithoutResponse}`}</Text>
    </TouchableOpacity>
  );
};

 

Youhou! We can read in real time the characteristic value written on our BLE characteristic by our Arduino code 🎉 This is a great achievement we won not without pain. The particular point is to understand that when you read a characteristic, react-native-ble-plx returns its value in the Base64 format. You have to decode it to be able to read it in a human understandable response.

Moreover here I have displayed the characteristic properties: is it notifiable? readable? writable? The one we have created in our Arduino code has the property 0x10 which means that the characteristic can notify each modification, according to the Adafruit documentation. Here I attached a listener on the characteristic with .monitor method to get the characteristic value each time it changes.

IMG_7283_2

Bonus : control a LED from your phone

To go further you can try to control a LED from your phone.

But be aware! You can write only on a writable characteristic! Ensure you have set the right permission to your characteristic when you create it in the Arduino code (0x04 or 0x08 property)

Clue: you'll find the code to write on a characteristic in the code above

Conclusion

We learned how to create a BLE device, how to get notified of each value change. Now you are ready to create your own device with a real life application (not just a blinking led 😉).

The Bluetooth Low Energy is invading our smart objects in our life. Now you know how it works, and how to make your own device! 🚀

Note: You will find all the code above in my github repository