AtmoNode is a full-stack IoT air-quality monitoring platform built with Django REST Framework and React. It manages sensor nodes, ingests environmental readings, supports role-based node access, and provides a live dashboard for temperature, humidity, and pollution trends.
- Backend: Django 6, Django REST Framework, Djoser (token auth), PostgreSQL, Gunicorn
- Frontend: React 19, Vite, Axios, Lucide icons
- Hardware/Firmware: ESP32 C++ (Arduino framework), DHT11, MQ135, I2C LCD, ArduinoJson
- Infra: Docker, Docker Compose, Nginx reverse proxy, optional TLS via Certbot
backend/- Django API (
/auth/*,/api/*) - PostgreSQL-backed models for users, nodes, access roles, and readings
- Gunicorn app server behind Nginx
- Django API (
frontend/- React dashboard and auth UI
- Polling-based live updates for node readings
- Hardware node (ESP32) samples sensors and sends JSON readings to backend ingestion API
- Nginx proxies requests to Django and serves static files
- Email-based user registration and login (token authentication)
- Node registration with owner/editor/viewer role model
- Owner-only access sharing per node
- Device ingestion endpoint secured by device token
- Reading filters by location
- Frontend dashboard with:
- Latest sensor values and threshold alerts
- Historical trend charts
- Sync status indicator
atmonode/
├── backend/
│ ├── atmonode/ # sensor domain app (nodes/readings/access)
│ ├── users/ # custom email-based user model
│ ├── core/ # Django settings and URL routing
│ ├── docker-compose.yml
│ ├── docker-compose.prod.yml
│ └── Dockerfile
├── frontend/
│ ├── src/
│ └── package.json
└── firmware/
└── hardware.ino # ESP32 firmware sketch (sensor read + API post)
The project includes device-side firmware in C++ at firmware/hardware.ino.
- Platform: ESP32 (Arduino framework)
- Sensors/Peripherals:
- DHT11 on GPIO 4 (temperature and humidity)
- MQ135 on ADC pin 34 (pollution approximation)
- 16x2 I2C LCD (
LiquidCrystal_I2C)
- Connectivity:
- Wi-Fi client with HTTPS POST to
/api/readings/ - Payload fields:
device_uid,temperature,humidity,pollution
- Wi-Fi client with HTTPS POST to
- Sampling/Push interval: every 10 seconds
- Firmware derives a unique
device_uidfrom ESP32 eFuse MAC. - Firmware reads DHT11 and MQ135 values and prints status to LCD + serial.
- Firmware sends JSON to backend ingestion endpoint.
- Backend validates device authentication and maps
device_uidto a registered node. - Reading is persisted and shown on dashboard.
The current sketch contains hardcoded values for:
- Wi-Fi SSID/password
- API URL
- auth token
For production, move these to secure provisioning/secrets storage and avoid committing real credentials in source control.
- Install board support for ESP32.
- Install required libraries:
DHT sensor libraryArduinoJsonLiquidCrystal_I2C
- Open
firmware/hardware.inoin Arduino IDE. - Select ESP32 board and COM port.
- Build and upload.
- Monitor serial output at
115200baud.
Create backend/.env.prod:
# Django
DJANGO_SECRET_KEY=replace-with-a-secure-secret
DEBUG=False
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8001,http://localhost:8001
DEVICE_INGEST_TOKEN=replace-with-strong-device-token
# Django DB connection
DATABASE_NAME=dockerdjango
DATABASE_USER=dbuser
DATABASE_PASSWORD=dbpassword
DATABASE_HOST=db
DATABASE_PORT=5432
# Postgres container vars
POSTGRES_DB=dockerdjango
POSTGRES_USER=dbuser
POSTGRES_PASSWORD=dbpasswordcd backend
docker compose -f docker-compose.yml up --build -dApp will be available at:
- API + auth via Nginx:
http://localhost:8001
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCreate backend/.env.local:
DJANGO_SECRET_KEY=dev-secret
DEBUG=True
DATABASE_NAME=your_db_name
DATABASE_USER=your_db_user
DATABASE_PASSWORD=your_db_password
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000
DEVICE_INGEST_TOKEN=dev-device-tokenRun migrations and server:
python manage.py migrate
python manage.py runserverBackend URL: http://127.0.0.1:8000
cd frontend
npm installCreate frontend/.env:
VITE_API_BASE_URL=http://127.0.0.1:8000Run frontend:
npm run devFrontend URL: usually http://127.0.0.1:5173
Base URL (Docker): http://localhost:8001
POST /auth/users/— registerPOST /auth/token/login/— login and receive auth tokenPOST /auth/token/logout/— logoutGET /auth/users/me/— current user profile
GET /api/users/— list users (excluding current user)GET /api/nodes/— list nodes user can accessPOST /api/nodes/— create nodePOST /api/nodes/{node_id}/grant-access/— grant/update node access roleGET /api/readings/?location=<location>— list accessible readingsPOST /api/readings/— create reading
- User-authenticated requests use header:
Authorization: Token <auth_token>
- Device ingestion (unauthenticated user) requires either:
X-Device-Token: <DEVICE_INGEST_TOKEN>- or
Authorization: Device <DEVICE_INGEST_TOKEN>
- Device ingestion payload must include
device_uidthat matches a registered node.
Example device ingestion request:
curl -X POST http://localhost:8001/api/readings/ \
-H "Content-Type: application/json" \
-H "X-Device-Token: <DEVICE_INGEST_TOKEN>" \
-d '{
"device_uid": "NODE-ABC-123",
"temperature": 24.6,
"humidity": 61.2,
"pollution": 142.0
}'cd backend
docker compose downcd backend
docker compose up --build -dcd backend
docker compose exec django-web python manage.py createsuperuser