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...
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:
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.
To read a characteristic, you have to follow this communication scheme:
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.
What are we going to do? The target application is an app which can:
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:
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.
![]() |
ℹ️
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.
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:
}
// 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;
// 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.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();
}
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.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// 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"));
}
// 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!
/* 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!Tools > Port
choose your Arduino boardTools > Type
choose your Arduino typeLets focus on the loop
function to add some features to our Arduino module.
elevation
characteristic 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);
// 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"));
}
// 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
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:
npx react-native start
in a terminalnpx react-native run-android
for Android devices, or npx react-native run-ios
for iOS simulator, in an other terminalFolder architecture
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 😇
![]() | ![]() | ![]() | ![]() |
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.
Our application will be available to:
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.
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 };
// 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} />
)}
);
}
// 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 };
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.
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.
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
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