Smart First Aid Box: a 6-Sensor IoT Build With ESP32-S3, MongoDB & Grafana (Full Tutorial)
by thilina lakshan in Circuits > Arduino
61 Views, 0 Favorites, 0 Comments
Smart First Aid Box: a 6-Sensor IoT Build With ESP32-S3, MongoDB & Grafana (Full Tutorial)
Build a self-monitoring first aid box that tracks its own inventory, supply levels, air quality, and motion — then streams it all to a live dashboard.
What You'll Build
By the end of this tutorial you'll have an ESP32-S3 reading six different sensors, packaging the data into JSON, and POSTing it every 60 seconds to a Node.js + MongoDB backend, with a Grafana dashboard showing it all live. We'll go from bare wires to a working dashboard — including all the calibration values and the bug fixes that cost me days so they cost you minutes.
Difficulty: Intermediate Time: A full weekend (the gas sensor calibration and WiFi debugging take patience) Cost: Roughly the price of an ESP32 dev kit plus a handful of cheap sensors
Supplies
This pin map is the result of painful trial and error. Two pins in particular are non-negotiable, and I'll flag them.
Understand the Pin Map (Read This Twice)
This pin map is the result of painful trial and error. Two pins in particular are non-negotiable, and I'll flag them.
| Sensor | Signal | ESP32-S3 GPIO | Notes |
|---|---|---|---|
| DHT22 | DATA | GPIO 4 | Needs 10kΩ pull-up from DATA → 3.3V |
| MPU6050 | SDA | GPIO 21 | I2C data |
| MPU6050 | SCL | GPIO 47 | ⚠️ NOT GPIO 22 — this board has no usable 22 |
| MPU6050 | AD0 | GND | Sets I2C address |
| MQ135 | AOUT | GPIO 8 | ⚠️ MUST be an ADC1 pin (see Step 7) |
| MQ135 | DOUT | GPIO 13 | Digital threshold output |
| IR ×5 | OUT | GPIO 5, 6, 7, 10, 11 | One per module |
| RFID | RX | GPIO 15 | UART1 |
| RFID | TX | GPIO 16 | UART1 |
| HX711 | DOUT | GPIO 17 | Data |
| HX711 | SCK | GPIO 18 | Clock |
| Onboard LED | — | GPIO 48 | Status + empty-bottle alert |
Wire It Up, One Sensor at a Time
Don't wire all six at once and hope. Wire one, flash a tiny test sketch, confirm it reads, then move to the next. This is the single best way to stay sane.
Suggested order: DHT22 → MPU6050 → MQ135 → IR array → RFID → HX711. Each adds a new "class" of complexity (analog, I2C, UART, persistent storage), so you learn progressively.
Set Up the Arduino IDE
Install these libraries via Tools → Manage Libraries:
- DHT sensor library (Adafruit)
- Adafruit Unified Sensor
- ArduinoJson (by bblanchon)
- Adafruit MPU6050
- HX711 (by Bogdan Necula — not the SparkFun one)
Then, crucially, in Tools:
- Board: your ESP32-S3 dev module
- USB CDC On Boot: Enabled ← without this, your Serial Monitor stays blank and you'll lose an hour wondering why
Flash the Firmware
Here's the complete firmware. It reads all six sensors, packages JSON, and sends every 60 seconds. Paste it into the Arduino IDE, set your WiFi credentials and server IP near the top, and upload.
Set these three lines for your network:
The full firmware.ino is attached to this tutorial (and shown in the companion repo). Key things it does:
- Reads MQ135 + IR + weight every 2 seconds to Serial for live monitoring
- Sends all sensors to the backend every 60 seconds
- Maintains an RFID inventory in EEPROM that survives reboots
- Auto-calibrates the gyroscope at boot
- Verifies the network is actually reachable before sending (Step 9 — this is the important one)
Calibrate the Sensors
MPU6050 — automatic. Keep the box still for the first few seconds after boot while it averages 200 gyro samples.
HX711 (weight) — two steps:
- On boot, when prompted, place the empty bottle on the scale. It tares automatically after 5 seconds.
- To find your calibration factor: send R in the Serial Monitor to get a raw value, place a known weight, then compute CALIBRATION_FACTOR = raw / known_grams. Update this line and re-flash:
MQ135 (gas) — the fiddly one. The sensor needs a clean-air reference resistance (RO). After a warm-up, the firmware samples clean air to set RO. For best accuracy, let the sensor burn in for 24–48 hours before trusting absolute ppm numbers. The current calibrated value in my build:
The MQ135 / WiFi Pin Trap (Don't Skip)
My MQ135 first reported negative resistance ratios — implying the air was toxic in a normal room. The cause: I'd put AOUT on an ADC2 pin, and ADC2 is disabled the moment WiFi turns on, returning junk.
Fix: put MQ135 AOUT on an ADC1 pin. GPIO 8 works. After moving it, readings became sane immediately (RS/RO ≈ 1.3 in clean air instead of near zero).
If your gas readings look impossible, this is almost certainly why.
Set Up the Backend (Node.js + MongoDB)
Install Node.js (nodejs.org) and MongoDB (with MongoDB Compass for viewing data). Then create a project folder and the server.
Drop the server.js (attached) into the backend folder. It:
- Defines a MongoDB schema covering all six sensors (nested objects per subsystem)
- Exposes POST /api/readings for the ESP32
- Exposes GET /api/readings?hours=6, /api/latest, /api/inventory, /api/health
- Emits a WebSocket new_reading event on every POST for live dashboards
- Auto-deletes data older than 7 days via a TTL index
Start it:
You should see:
Fix the 8-Minute WiFi Delay (The Big One)
Here's the bug that almost killed the project, and the fix that saved it.
Symptom: After every boot, the ESP32 connects to WiFi, shows a valid IP, then fails every send with HTTP -1 for about 8 minutes — then suddenly works forever after.
Cause: WiFi.status() == WL_CONNECTED becomes true the instant the ESP32 associates with the access point. But the actual usable network path — DHCP fully settled, routing live, the server genuinely reachable — can lag behind by minutes on some routers. During that window, TCP connections silently fail even though everything "looks" connected.
The fix — a network stability check. Before trusting WiFi, make the device prove it can reach the server with a real TCP connection, in a retry loop:
Also give the HTTP client a real timeout so a slow response never freezes the loop:
After this, the device waits until it has genuinely reached the server once, then sends successfully from the very first try. No more dead zone.
Tip: Always start the Node.js server first, then reset the ESP32. That way the stability check finds the server immediately instead of timing out.
Open the Windows Firewall for Port 4000
If sends still fail after the stability fix, Windows Firewall may be blocking the port. Open Command Prompt as Administrator and run:
Quick sanity checks:
- From your phone's browser, visit http://YOUR_LAPTOP_IP:4000/api/health — you should get a JSON {"status":"ok"}.
- Confirm the server listens on all interfaces: netstat -an | findstr "4000" should show 0.0.0.0:4000 LISTENING.
Manage the RFID Inventory
The RFID system registers each new tag and remembers it in EEPROM across reboots.
To register an item: scan a new (unknown) tag. The Serial Monitor prompts you to type a name and press Enter.
Useful Serial commands:
- list — print the full inventory
- clear — wipe all inventory (use this if you get ghost/blank items from old test data)
- tare — re-zero the weight scale
- weight — print current bottle weight
If you see 20 blank "missing" items: that's corrupted EEPROM from earlier testing. Type clear, then re-scan your real tags. Build this reset habit early.
View Your Data in MongoDB Compass
Open MongoDB Compass, connect to mongodb://localhost:27017, and browse to the dht22 database → readings collection. Each document is one full snapshot of all six sensors. Watch new ones appear every 60 seconds.
Set Up Grafana
Install Grafana for Windows (MSI installer). It runs as a Windows service.
If the dashboard won't load at localhost:3000:
- Check the service is running:
- Find the actual port (it may differ if 3000 is taken):
- Try http://localhost:3000, :3001, :3002 until one responds.
Port conflict note: our Node backend uses 4000 and Grafana defaults to 3000, so they don't clash — but if you moved anything, double-check.
Build the Dashboard With the Infinity Plugin
The official MongoDB Grafana plugin is paid/enterprise. The trick: use the free Infinity plugin to query our Node.js REST API instead.
- In Grafana: Configuration → Plugins → search "Infinity" → install.
- Add a data source → Infinity.
- Create a panel. Set the Infinity query to:
- URL: http://YOUR_LAPTOP_IP:4000/api/readings?hours=6
- Parser: Backend / JSON
- Rows/Root: data
- For single-value gauges, point at /api/latest and use JSONPaths like:
- $.gas.aqi — air quality gauge
- $.bottle.fill_pct — bottle level gauge
- $.tilt.pitch and $.tilt.roll — orientation
- $.rfid.in_stock — items present
Demonstrate It
Things to show off when it's all working:
- Live Serial Monitor with all six sensors updating
- ✓ Sent OK (HTTP 201) confirmations
- New documents landing in MongoDB Compass in real time
- The Grafana gauges moving as you change conditions
- Scan an RFID tag → item goes IN STOCK; remove it → after 15 seconds it flips to MISSING
- Cover/uncover the IR array → cotton presence toggles
- Lift the bottle off the scale → fill drops to 0%, empty LED lights
Troubleshooting Cheat Sheet
| Symptom | Cause | Fix |
|---|---|---|
| Serial Monitor blank | USB CDC off | Enable "USB CDC On Boot" |
| Gas reads negative / always CRITICAL | MQ135 on ADC2 / wrong RO | Move AOUT to ADC1 (GPIO 8); recalc RO |
HTTP -1 for minutes after boot | WiFi "connected" but path not ready | Add the network stability check (Step 9) |
HTTP -1 always | Firewall / wrong IP / server down | Open port 4000; start server first |
| 20 blank RFID items | Corrupted EEPROM | Type clear, re-scan tags |
| MPU6050 not found | Wrong I2C pins | Use SDA=21, SCL=47 |
| Grafana won't load | Service stopped / wrong port | net start Grafana; check port |
| Weight always 0 | Not tared / bad cal factor | Re-tare; recompute CALIBRATION_FACTOR |
Wrap-Up
You now have a first aid box that knows its own contents, supply levels, air safety, and orientation — and streams it all to a dashboard. More importantly, you've dodged the exact traps (ADC2/WiFi, the phantom 8-minute delay, EEPROM ghosts) that cost me real time.
If you build this, I'd love to see your version. Tag your build and share your Serial Monitor's first glorious ✓ Sent OK (HTTP 201).
Happy building.