In a world of interconnected devices, the Internet of Things (IoT) has opened up endless possibilities for creating innovative and practical solutions to everyday problems. This project is a hands-on guide to building a smart water bottle and a companion mobile app using React Native and an ESP32 microcontroller. We'll explore the fascinating world of Bluetooth Low Energy (BLE) communication and learn how to create a seamless connection between hardware and software.
This tutorial will walk you through the entire process, from setting up the ESP32 and reading sensor data to building a cross-platform mobile app with React Native to track your water intake and send you reminders to stay hydrated. Whether you're a mobile developer curious about IoT or a hardware enthusiast looking to build a user-friendly interface, this project is for you.
Understanding the Problem
Forgetting to drink enough water is a common problem. While there are many apps to track water intake, they rely on manual input. A truly "smart" water bottle should automatically track your consumption and proactively remind you to drink more.
The challenge lies in creating a reliable and low-power communication channel between the water bottle and a smartphone. This is where Bluetooth Low Energy (BLE) comes in. BLE is a wireless communication protocol designed for short-range communication with low power consumption, making it ideal for battery-powered IoT devices like our smart water bottle.
Our approach will be to use an ESP32 microcontroller to read data from a water level sensor and transmit it to a React Native app via BLE. The app will then display the water intake, track progress towards a daily goal, and send push notifications as reminders.
Prerequisites
To follow this tutorial, you'll need the following:
-
Software:
- Node.js (LTS version recommended)
- React Native development environment
- Arduino IDE with the ESP32 board manager installed
- A code editor of your choice (e.g., VS Code)
-
Hardware:
- ESP32 development board
- A water level sensor (for a real-world setup) or a potentiometer (for simulation)
- Breadboard and jumper wires
- A smartphone with Bluetooth capabilities
Step 1: Setting up the ESP32 as a BLE Peripheral
First, we need to program the ESP32 to act as a BLE peripheral, which will advertise its presence and expose data to our mobile app. We'll define a BLE service with a characteristic for the water level.
What we're doing
We will write an Arduino sketch for the ESP32 that:
- Initializes a BLE server.
- Creates a BLE service and a characteristic to hold the water level data.
- Simulates reading from a water level sensor.
- Updates the characteristic with the new water level value.
Implementation
// ESP32_BLE_Water_Bottle.ino
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLECharacteristic *pCharacteristic;
float waterLevel = 100.0; // Initial water level
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE work!");
BLEDevice::init("Smart Water Bottle");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->setValue(String(waterLevel).c_str());
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("Characteristic defined! Now you can read it in your phone!");
}
void loop() {
// Simulate water level decreasing
if (waterLevel > 0) {
waterLevel -= 5.0;
} else {
waterLevel = 100.0;
}
pCharacteristic->setValue(String(waterLevel).c_str());
pCharacteristic->notify();
Serial.print("Water Level: ");
Serial.println(waterLevel);
delay(2000);
}
How it works
- We use the
BLEDevicelibrary to initialize the ESP32 as a BLE device named "Smart Water Bottle". - We create a BLE service with a unique UUID. Services are collections of characteristics.
- We create a BLE characteristic with a unique UUID. Characteristics are what hold the actual data. We give it
READandNOTIFYproperties so our app can read the value and subscribe to changes. - In the
loop()function, we simulate a decrease in the water level and update the characteristic's value. Thenotify()function sends the new value to any subscribed devices.
Step 2: Building the React Native Companion App
Now let's create the mobile app that will connect to our smart water bottle and display the water level. We'll use the react-native-ble-plx library to handle the BLE communication.
What we're doing
- Set up a new React Native project.
- Install and configure
react-native-ble-plx. - Implement BLE scanning to find our ESP32 device.
- Connect to the device and read the water level characteristic.
- Display the water level in the app's UI.
Implementation
First, create a new React Native project and install the necessary library:
npx react-native init SmartWaterBottleApp
cd SmartWaterBottleApp
npm install react-native-ble-plx
Next, let's create a custom hook to manage our BLE logic:
// src/hooks/useBLE.js
import { useState } from 'react';
import { BleManager } from 'react-native-ble-plx';
const bleManager = new BleManager();
const useBLE = () => {
const [device, setDevice] = useState(null);
const [waterLevel, setWaterLevel] = useState(0);
const requestPermissions = async () => {
// Handle Android permissions
};
const scanForDevices = () => {
bleManager.startDeviceScan(null, null, (error, scannedDevice) => {
if (error) {
console.error(error);
return;
}
if (scannedDevice && scannedDevice.name === 'Smart Water Bottle') {
bleManager.stopDeviceScan();
connectToDevice(scannedDevice);
}
});
};
const connectToDevice = async (device) => {
try {
const connectedDevice = await bleManager.connectToDevice(device.id);
setDevice(connectedDevice);
await connectedDevice.discoverAllServicesAndCharacteristics();
startStreamingData(connectedDevice);
} catch (error) {
console.error(error);
}
};
const startStreamingData = (device) => {
device.monitorCharacteristicForService(
'4fafc201-1fb5-459e-8fcc-c5c9c331914b', // Service UUID
'beb5483e-36e1-4688-b7f5-ea07361b26a8', // Characteristic UUID
(error, characteristic) => {
if (error) {
console.error(error);
return;
}
const decodedValue = atob(characteristic.value);
setWaterLevel(parseFloat(decodedValue));
}
);
};
return {
scanForDevices,
device,
waterLevel,
};
};
export default useBLE;
Now, let's use this hook in our main App.js component:
// App.js
import React, { useEffect } from 'react';
import { SafeAreaView, Text, Button, View, StyleSheet } from 'react-native';
import useBLE from './src/hooks/useBLE';
const App = () => {
const { scanForDevices, device, waterLevel } = useBLE();
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Smart Water Bottle</Text>
<Button title="Scan for Devices" onPress={scanForDevices} />
{device && (
<View style={styles.deviceContainer}>
<Text>Connected to: {device.name}</Text>
<Text style={styles.waterLevel}>Water Level: {waterLevel}%</Text>
</View>
)}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
// Add your styles here
});
export default App;
How it works
- The
useBLEhook encapsulates all the BLE logic. scanForDevicesstarts scanning for BLE devices and filters for our "Smart Water Bottle".- Once the device is found,
connectToDeviceestablishes a connection. startStreamingDatasubscribes to the water level characteristic. Whenever the ESP32 sends a notification with a new value, thewaterLevelstate in our app is updated, and the UI re-renders to show the current water level.
Putting It All Together
Now, when you run the ESP32 code and then launch the React Native app, you can press the "Scan for Devices" button. The app will find your "Smart Water Bottle", connect to it, and start displaying the simulated water level in real-time.
For a real-world application, you would replace the simulated data in the ESP32 code with readings from an actual water level sensor. There are various types of sensors you can use, such as resistive or ultrasonic sensors. The principle remains the same: read the sensor value, map it to a percentage, and update the BLE characteristic.
Performance and Security Considerations
- Power Consumption: BLE is designed for low power consumption. To further optimize, you can adjust the advertising interval on the ESP32 and the connection interval in your React Native app.
- Security: For a production application, you should implement BLE security features like pairing and bonding to prevent unauthorized devices from connecting to your smart water bottle. The
react-native-ble-plxlibrary provides APIs for handling these security measures. - Error Handling: In a real-world scenario, you need robust error handling for cases like connection drops, permission denials, and data transmission errors.
Conclusion
In this tutorial, we've successfully built a functional prototype of a smart water bottle and a companion app using React Native and an ESP32. We've learned the fundamentals of BLE communication and how to bridge the gap between hardware and software.
This project is just the beginning. You can extend it by adding features like:
- Tracking daily water intake history.
- Setting customizable hydration goals.
- Integrating with health and fitness apps.
- Designing and 3D-printing a custom enclosure for the electronics.
The world of IoT is vast and full of exciting possibilities. I encourage you to experiment with this project, explore different sensors, and build your own innovative connected devices.