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)

ChatGPT Image Jul 7, 2026, 10_36_19 PM.png

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)

ESP32-S3-Wiring-Diagram.png

This pin map is the result of painful trial and error. Two pins in particular are non-negotiable, and I'll flag them.


SensorSignalESP32-S3 GPIONotes
DHT22DATAGPIO 4Needs 10kΩ pull-up from DATA → 3.3V
MPU6050SDAGPIO 21I2C data
MPU6050SCLGPIO 47⚠️ NOT GPIO 22 — this board has no usable 22
MPU6050AD0GNDSets I2C address
MQ135AOUTGPIO 8⚠️ MUST be an ADC1 pin (see Step 7)
MQ135DOUTGPIO 13Digital threshold output
IR ×5OUTGPIO 5, 6, 7, 10, 11One per module
RFIDRXGPIO 15UART1
RFIDTXGPIO 16UART1
HX711DOUTGPIO 17Data
HX711SCKGPIO 18Clock
Onboard LEDGPIO 48Status + empty-bottle alert

Wire It Up, One Sensor at a Time

WhatsApp Image 2026-06-30 at 08.56.41 (7).jpeg
WhatsApp Image 2026-06-30 at 08.56.41 (6).jpeg
WhatsApp Image 2026-06-30 at 08.56.57 (1).jpeg

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:

  1. DHT sensor library (Adafruit)
  2. Adafruit Unified Sensor
  3. ArduinoJson (by bblanchon)
  4. Adafruit MPU6050
  5. HX711 (by Bogdan Necula — not the SparkFun one)

Then, crucially, in Tools:

  1. Board: your ESP32-S3 dev module
  2. 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:

const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* SERVER_URL = "http://YOUR_LAPTOP_IP:4000/api/readings";


The full firmware.ino is attached to this tutorial (and shown in the companion repo). Key things it does:

  1. Reads MQ135 + IR + weight every 2 seconds to Serial for live monitoring
  2. Sends all sensors to the backend every 60 seconds
  3. Maintains an RFID inventory in EEPROM that survives reboots
  4. Auto-calibrates the gyroscope at boot
  5. Verifies the network is actually reachable before sending (Step 9 — this is the important one)


Calibrate the Sensors

Screenshot 2026-03-22 174040.png

MPU6050 — automatic. Keep the box still for the first few seconds after boot while it averages 200 gyro samples.

HX711 (weight) — two steps:

  1. On boot, when prompted, place the empty bottle on the scale. It tares automatically after 5 seconds.
  2. 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:
#define CALIBRATION_FACTOR 17900.0f // ← your value here

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:

float RO_CLEAN_AIR = 16.31f; // ← specific to my sensor; recalc yours


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)

Screenshot 2026-07-02 204919.png

Install Node.js (nodejs.org) and MongoDB (with MongoDB Compass for viewing data). Then create a project folder and the server.

mkdir dht22-project && cd dht22-project
mkdir backend && cd backend
npm init -y
npm install express mongoose cors socket.io

Drop the server.js (attached) into the backend folder. It:

  1. Defines a MongoDB schema covering all six sensors (nested objects per subsystem)
  2. Exposes POST /api/readings for the ESP32
  3. Exposes GET /api/readings?hours=6, /api/latest, /api/inventory, /api/health
  4. Emits a WebSocket new_reading event on every POST for live dashboards
  5. Auto-deletes data older than 7 days via a TTL index

Start it:

node server.js

You should see:

✓ MongoDB connected: mongodb://localhost:27017/dht22
✓ Server listening on http://0.0.0.0:4000


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:

void connectWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.println("\nWiFi connected! IP: " + WiFi.localIP().toString());

// Don't trust "connected" — prove the path actually works
Serial.print("Waiting for network stability");
for (int i = 0; i < 20; i++) {
delay(500); Serial.print(".");
WiFiClient client;
if (client.connect("192.168.1.17", 4000)) { // ← your server IP/port
client.stop();
Serial.println("\n✓ Network ready!");
return;
}
}
Serial.println("\nNetwork ready (timeout).");
}

Also give the HTTP client a real timeout so a slow response never freezes the loop:

http.begin(SERVER_URL);
http.setTimeout(10000); // 10 seconds

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:

netsh advfirewall firewall add rule name="IoT Backend" dir=in action=allow protocol=TCP localport=4000 profile=any

Quick sanity checks:

  1. From your phone's browser, visit http://YOUR_LAPTOP_IP:4000/api/health — you should get a JSON {"status":"ok"}.
  2. 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:

  1. list — print the full inventory
  2. clear — wipe all inventory (use this if you get ghost/blank items from old test data)
  3. tare — re-zero the weight scale
  4. 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

Screenshot 2026-05-02 100857.png
Screenshot 2026-03-22 174151.png
Screenshot 2026-03-22 174222.png
Screenshot 2026-03-22 174319.png
Screenshot 2026-03-22 174337.png
Screenshot 2026-03-22 174413.png

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:

  1. Check the service is running:
Get-Service Grafana
net start Grafana
  1. Find the actual port (it may differ if 3000 is taken):
type "C:\Program Files\GrafanaLabs\grafana\conf\defaults.ini" | findstr "http_port"
netstat -an | findstr "3000"
  1. 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

Screenshot 2026-03-22 093938.png

The official MongoDB Grafana plugin is paid/enterprise. The trick: use the free Infinity plugin to query our Node.js REST API instead.

  1. In Grafana: Configuration → Plugins → search "Infinity" → install.
  2. Add a data source → Infinity.
  3. Create a panel. Set the Infinity query to:
  4. URL: http://YOUR_LAPTOP_IP:4000/api/readings?hours=6
  5. Parser: Backend / JSON
  6. Rows/Root: data
  7. For single-value gauges, point at /api/latest and use JSONPaths like:
  8. $.gas.aqi — air quality gauge
  9. $.bottle.fill_pct — bottle level gauge
  10. $.tilt.pitch and $.tilt.roll — orientation
  11. $.rfid.in_stock — items present


Demonstrate It

WhatsApp Image 2026-06-30 at 08.56.57 (3).jpeg
WhatsApp Image 2026-06-30 at 08.56.57 (6).jpeg
WhatsApp Image 2026-06-30 at 08.56.57 (2).jpeg

Things to show off when it's all working:

  1. Live Serial Monitor with all six sensors updating
  2. ✓ Sent OK (HTTP 201) confirmations
  3. New documents landing in MongoDB Compass in real time
  4. The Grafana gauges moving as you change conditions
  5. Scan an RFID tag → item goes IN STOCK; remove it → after 15 seconds it flips to MISSING
  6. Cover/uncover the IR array → cotton presence toggles
  7. Lift the bottle off the scale → fill drops to 0%, empty LED lights


Troubleshooting Cheat Sheet











SymptomCauseFix
Serial Monitor blankUSB CDC offEnable "USB CDC On Boot"
Gas reads negative / always CRITICALMQ135 on ADC2 / wrong ROMove AOUT to ADC1 (GPIO 8); recalc RO
HTTP -1 for minutes after bootWiFi "connected" but path not readyAdd the network stability check (Step 9)
HTTP -1 alwaysFirewall / wrong IP / server downOpen port 4000; start server first
20 blank RFID itemsCorrupted EEPROMType clear, re-scan tags
MPU6050 not foundWrong I2C pinsUse SDA=21, SCL=47
Grafana won't loadService stopped / wrong portnet start Grafana; check port
Weight always 0Not tared / bad cal factorRe-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.