Smart Hydration Tracker Bottle Using ESP32 & VL53L0X Sensor

Components Required
- ESP32-C3 Super Mini Development Board
- VL53L0X Laser Distance Sensor
- Li-ion Battery (3.7V)
- USB Cable
- Connecting wires
- Water Bottle with Screw Cap
- Android Smartphone
About the Components
ESP32-C3 Super Mini Development Board
The ESP32-C3 is a low-power RISC-V microcontroller with integrated Wi-Fi and Bluetooth 5 LE support. It is widely used for IoT prototyping, wearables, and wireless sensor nodes. The compact Super Mini form factor makes it ideal for embedding inside a bottle cap with minimal space requirements.

- RISC-V 32-bit single-core processor at 160 MHz
- Integrated Bluetooth 5 LE and Wi-Fi 802.11 b/g/n
- 22 programmable GPIO pins
- Ultra-low deep-sleep current (5 µA)
- 3.3V operating voltage
- Supports SPI, I2C, UART, and ADC interfaces
- Compatible with Arduino IDE and ESP-IDF
In this project, the ESP32-C3 acts as the central controller. It reads raw distance measurements from the VL53L0X over I2C and broadcasts the values to the Android smartphone via BLE notifications every 200ms.
VL53L0X Laser Distance Sensor
The VL53L0X is a Time-of-Flight laser ranging sensor from STMicroelectronics capable of measuring distances up to 2 meters with millimeter-level accuracy. It communicates over I2C and operates at 3.3V, making it directly compatible with the ESP32-C3 without any level shifting.

- Time-of-Flight (ToF) laser ranging technology
- Measurement range: 50 mm to 2000 mm
- 940 nm invisible laser emitter (Class 1, eye-safe)
- I2C interface (default address 0x29)
- Operating voltage: 2.6V to 3.5V
- Up to 50 readings per second
- Compact 2.4 x 4.4 mm sensor footprint
- Compatible with 3.3V and 5V logic
In this project, the VL53L0X sensor is mounted inside the bottle cap pointing downward. It continuously measures the distance from the cap to the water surface and sends the raw millimeter value directly to the Android app over BLE.
Circuit Diagram
The VL53L0X sensor communicates with the ESP32-C3 using the I2C interface. Only four connections are required: VCC, GND, SDA, and SCL. The sensor operates directly at 3.3V, making it fully compatible with the ESP32-C3 without any level shifter.


Fig. ESP32-C3 and VL53L0X Circuit Connections
| VL53L0X Pin | ESP32-C3 Super Mini Pin | Description |
|---|---|---|
| VIN | 3.3V | Main Power Supply |
| GND | GND | Common Ground |
| SDA | GPIO8 | I2C Data Line |
| SCL | GPIO9 | I2C Clock Line |
The ESP32-C3 reads distance measurements from the VL53L0X sensor over I2C and transmits the values wirelessly to the Android application using Bluetooth Low Energy notifications.
Assembly
Drill a small hole in the center of the bottle cap and mount the VL53L0X sensor flush with the underside, pointing directly down. Secure the ESP32-C3 and Li-ion battery on top of the cap using epoxy or hot glue. Connect the sensor to the ESP32-C3 using four short jumper wires for VCC, GND, SDA, and SCL according to the circuit connections table. Define the SDA and SCL pins explicitly in the firmware code using Wire.begin(8, 9) to ensure proper hardware I2C mapping on the compact Super Mini board. No external breadboard or additional wiring is needed.

Soldering and final assembly of ESP32-C3, VL53L0X sensor, and battery on the bottle cap
Downloading and Running the Android Application

The complete Android application source code is available in the GitHub repository. Open Android Studio and select File → New → Project from Version Control. Paste the repository URL and clone the project.
After Gradle sync completes, connect your Android phone or start an emulator. Press the Run button to build and install the application.
Grant Bluetooth and notification permissions when prompted. The app will automatically scan for the Smart Bottle device and connect over BLE.
Code Explanation (ESP32 Firmware)
Libraries, UUIDs, and Global Variables
The firmware includes Wire.h and Adafruit_VL53L0X.h for the I2C sensor, plus the four BLE libraries for the ESP32 BLE stack. The service UUID and characteristic UUID are defined here and must exactly match the values used in the Android app. A global deviceConnected flag tracks whether the phone is currently connected.
BLE Server Callbacks
The MyServerCallbacks class handles connection events. When the Android app connects, deviceConnected is set to true. When it disconnects, advertising restarts automatically so the app can reconnect without needing to reboot the hardware module.
Setup – Sensor and BLE Initialization
The setup() function first initializes the VL53L0X sensor over I2C using pins 8 and 9. If the sensor is not detected, the firmware halts. It then creates the BLE server, service, and characteristic. The BLE2902 descriptor is added to the characteristic — this client characteristic configuration descriptor (CCCD) is required for Android BLE notifications to work correctly. Finally, the device starts advertising under the name SmartBot.
Main Loop – Sensor Reading and BLE Notify
The main loop calls lox.rangingTest() to get a fresh distance measurement each iteration. A RangeStatus check of 4 indicates an out-of-range error and is skipped. Valid readings are converted to a String and pushed to the connected Android app using pCharacteristic->notify(). A 200ms delay provides smooth UI updates on the phone while keeping power consumption low.
Android App Architecture & Implementation
The companion Android app uses a split architecture to process, compute, and display water level data:
-
Jetpack Compose UI: Reactive dark-mode theme with a custom
Canvascomponent to animate the fluid level. Navigates between a first-run Calibration Wizard and the main tracking dashboard based onisCalibrationComplete. - Foreground Service Thread: Manages the BLE lifecycle in the background, keeping data streaming alive when the app is minimized.
-
StateFlow Tracking: Couples UI rendering with GATT updates via
MutableStateFlow<BottleState>.
Android application showing the first-time calibration wizard and the main hydration tracking dashboard.
Calibration Wizard
On first launch, the app guides the user through a 3-step setup flow to measure the bottle's actual geometry. At each step the live sensor reading is shown, and tapping Save & Continue persists the distance value to SharedPreferences:
| Step | Bottle State | Key Saved | Default Fallback (mm) |
|---|---|---|---|
| 1 | Empty (0%) | cal_0 |
280 |
| 2 | Half Full (50%) | cal_50 |
180 |
| 3 | Full (100%) | cal_100 |
70 |
Once cal_100 is saved, isCalibrationComplete is set to true and the app transitions to the main dashboard permanently.
Key Implementation Snippets
Calibration Wizard UI — Displays the live rawDistanceMm reading and saves each step on tap:
Dynamic calibration in calculatePercentage() — Reads user-saved points from SharedPreferences, falling back to sensible defaults if not yet calibrated:
15-point rolling average — Smooths raw sensor spikes caused by water sloshing. Also stores rawDistanceMm in state for the calibration wizard to display live:
CCCD descriptor write — Tells the ESP32-C3 to push notifications automatically instead of waiting for manual poll requests:
The complete Android source — AndroidManifest.xml, MainActivity.kt, SmartBottleService.kt, BottleState.kt, and BootReceiver.kt - is available in the GitHub repository.
Software Architecture Highlights

Result
The ESP32-C3 successfully runs a complete wireless water level monitoring system using the VL53L0X laser sensor and BLE communication. The device advertises as SmartBot, reconnects automatically after disconnection, and streams live distance readings to the Android app every 200ms with smooth real-time UI updates.
The out-of-range status check prevents invalid sensor readings from being transmitted, while the BLE2902 descriptor ensures Android notification subscriptions work reliably without any additional configuration on the phone side.

Checkout the full tutorial :
Complete Code
The following ESP32 Arduino sketch implements the complete smart bottle firmware using the VL53L0X laser sensor and BLE stack. The code includes sensor initialisation, BLE service and characteristic setup with the required BLE2902 descriptor, connection callbacks with auto re-advertising, and the main 200ms polling loop.