Most IoT tutorials show you how to blink an LED over WiFi and stop there. This one builds a complete, honest project: an ESP32 with a BME280 sensor posts temperature, humidity, and pressure readings every ten minutes to a cloud backend over HTTPS, then goes to deep sleep. A 2000 mAh LiPo battery runs it for around six months. The code handles WiFi dropouts, failed HTTP requests, and power-loss recovery.
If you have done the blink-LED-over-WiFi tutorial, this is the natural next project. It covers the things the blink tutorial omitted, which are also the things that separate a demo from a product.
What we are building
I2C] --> MCU[[ESP32-S3]] Bat[LiPo Battery
2000 mAh] --> MCU end MCU -->|HTTPS POST
every 10 min| Backend[Cloud backend
webhook or MQTT] Backend --> DB[(Time-series DB
InfluxDB, Timestream)] Backend --> Dashboard[Dashboard
Grafana] Backend -->|Alert| Alerts[SMS / Email
on threshold breach]
The full system: the ESP32 is one node. The backend stores and visualises the data. Adding more nodes is a matter of deploying more copies of the device.
Bill of materials
- ESP32-S3 DevKit (or any ESP32 variant with deep sleep support) — $8
- BME280 breakout (genuine Bosch, not a BMP280 clone) — $5
- 2000 mAh single-cell LiPo battery — $8
- TP4056 charger module (for USB charging) — $2
- Breadboard, jumpers, enclosure — $5
Total around $30 per node. If you are building ten of these, drop the DevKit for a bare ESP32-C3-WROOM module ($2) on a custom PCB and the per-unit cost is under $15.
Wiring
The BME280 connects over I2C, which needs four wires:
BME280 ESP32-S3
------- --------
VCC --> 3.3V
GND --> GND
SDA --> GPIO 8 (I2C SDA, default on S3)
SCL --> GPIO 9 (I2C SCL, default on S3)If your BME280 breakout does not have onboard pull-up resistors (check the schematic or measure with a multimeter), add 4.7 kOhm resistors from SDA and SCL to 3.3V.
For the battery: connect the LiPo to the TP4056's B+ and B- pads, and the TP4056's OUT+ and OUT- to the ESP32's 5V and GND pins (the onboard regulator drops 5V to 3.3V). A LiPo at full charge is 4.2V, which is above the 3.3V logic but below any damaging level; the onboard LDO handles this fine.
Firmware architecture
The firmware is a state machine. The ESP32 wakes up, initialises the sensor, connects to WiFi, takes a reading, sends it, and goes back to sleep. Each step has a bounded retry budget so that a single failure does not kill the battery.
retry next cycle ConnectWiFi --> ReadSensor: connected ConnectWiFi --> Sleep: timeout after 20s ReadSensor --> SendData: reading OK ReadSensor --> Sleep: reading error SendData --> Sleep: 2xx response SendData --> Retry: timeout or 5xx Retry --> SendData: attempts < 3 Retry --> Sleep: attempts = 3
give up, try next cycle Sleep --> [*]: deep sleep 10 min
The firmware as a state machine. Every failure path leads back to sleep so the battery budget is bounded per cycle.
The code
This is the full working firmware, using the Arduino ESP32 core and the Adafruit BME280 library. Roughly 130 lines.
Why deep sleep matters
An ESP32 with WiFi active draws around 80 mA. A 2000 mAh battery lasts 25 hours under that load. Unacceptable for a sensor.
In deep sleep, the ESP32 draws about 5–10 µA. The same 2000 mAh battery lasts around 20,000 hours, or over two years, at that load. In practice the wake cycles add consumption back — 10 seconds every 10 minutes at 80 mA adds about 13 mA-hours per day. Over a month that is 0.4 Ah, so a 2000 mAh battery still lasts roughly 5 months.
The wake/sleep pattern is what makes battery IoT viable. Getting deep sleep right is the single biggest lever for battery life.
The backend
The firmware posts JSON to ENDPOINT. That endpoint can be:
- A managed service: Adafruit IO, Blynk, Ubidots, ThingSpeak. Free tiers exist. Easiest to get running.
- A simple webhook: Cloudflare Workers, Vercel serverless functions, AWS Lambda behind API Gateway. Write 20 lines of JavaScript or Python to receive the POST and store it in a database or log file.
- Your own server: A VPS running Node.js, Python, or Go listening on HTTPS. More work but fully owned.
- MQTT: Swap the HTTP POST for MQTT publish. Lower overhead per message, standard IoT pattern. Use a broker like Mosquitto, HiveMQ Cloud, or AWS IoT Core.
For a first project, Adafruit IO or a Cloudflare Worker with a D1 database is the fastest path to a dashboard.
Going further
Once the basic system works, the improvements you care about:
- HTTPS certificate pinning to avoid man-in-the-middle attacks on your WiFi.
- OTA (Over-The-Air) updates so you can patch firmware without physical access. ESP-IDF has robust OTA support; the Arduino core has simpler OTA libraries.
- Local buffering in NVS flash if the network is unavailable, draining the buffer on the next successful connect.
- Watchdog timer as a last-resort recovery from firmware hangs.
- Battery voltage monitoring by reading the voltage divider on the battery pin, so you know when to alert for battery replacement.
Frequently Asked Questions
Can I use this with ESP8266?
Yes, with two caveats. ESP8266 deep sleep requires connecting GPIO16 to RST, which not every dev board makes easy. And ESP8266 has only 50 KB of usable RAM, which is tight for HTTPS (TLS handshake needs >30 KB free). HTTP-only works fine; HTTPS depends on the specific library.
How often should I take readings?
Depends on your use case. Temperature in a room changes slowly — every 10 minutes is plenty. Refrigerator temperature for food safety monitoring: every 5 minutes. Greenhouse with fast light/shade changes: every 30 seconds. The trade-off is always battery life, and more frequent readings are usually less useful than people assume.
What happens if WiFi is down for a day?
With the code above, each cycle attempts to connect, fails after 20 seconds, and goes back to sleep. The reading is lost. For a better system, write readings to NVS flash when WiFi is unavailable and drain the buffer on the next successful connect. About 30 extra lines of code.How do I add more sensors?
If they are I2C (different addresses), add them to the same bus with no code changes beyond reading more values. If they are analog, use ADC1 channels only (ADC2 conflicts with WiFi on ESP32). If they are SPI, add a CS pin per device. Keep the wake time short — reading 5 sensors should still take under 500 ms total.
Is the code production-ready?
It is a solid starting point. For a product you would add: certificate validation, device provisioning (not hardcoded WiFi credentials), crash reporting, OTA updates, a watchdog, and secure boot. Each of those is a weekend of work on its own.
Get the complete project package
The article above shows the core firmware and the principles behind it. The complete project package — assembled, tested, and ready to flash — is available by email request. We send it manually, and we read every request.
- Complete Arduino sketch (.ino) with full error handling
- List of required libraries with version numbers
- Printable wiring diagram (PDF)
- Bill of materials with current part numbers
- Build guide and troubleshooting tips
- Configuration template (WiFi, MQTT, etc.)
Share your thoughts
Worked with this in production and have a story to share, or disagree with a tradeoff? Email us at support@mybytenest.com — we read everything.