Week 14: Interface and Application Programming — Rich MQTT Dashboard

Note: My English writing skills are limited. For this documentation, I have used AI assistance for parts of the translation.

Date: April 22 - 28, 2026


What I Made This Week

I built a real-time monitoring and control web dashboard for my reptile terrarium.

In Week 11, I made a basic MQTT implementation: sensor display and one fan ON/OFF control. This week I rebuilt it into a full UI that can be used in my final project.

Dashboard URL (local): http://192.168.11.201:8002/files/week14/dashboard/index.html

Dashboard Overview The completed dashboard — sensor display, history graph, and 5 output controls


What Changed from Week 11

Item Week 11 Week 14
UI framework None (plain HTML) Bootstrap 5.3
Graph None Chart.js 4 (temperature & humidity history)
History storage None Flask + SQLite (up to 1 month)
Devices controlled 1 fan 5 devices (light, intake fan, exhaust fan, heater, humidifier)
Schedule None Time-range setting (manual override hours)
Deployment NAS nginx NAS nginx (same URL as Week 11)

System Architecture

System Architecture System architecture — ESP32C6 (device) → MQTT → NAS Docker → Browser

The system has three layers:

  1. Device layer — XIAO ESP32C6 reads two SHT31 sensors every 30 seconds and publishes data via MQTT. It also receives control commands from the browser.
  2. Server layer — A Docker container on the NAS runs Mosquitto (MQTT broker), a Flask API (history data), NGINX (static files), and SQLite (database).
  3. Browser layer — The dashboard connects to Mosquitto via WebSocket for real-time data, and calls the Flask REST API to fetch history.

Technologies Used

Frontend

Bootstrap 5.3

  • Purpose: UI components (cards, buttons, switches, grid) and responsive layout
  • Why I chose it: Faster to build consistent UI than writing CSS from scratch. Adding data-bs-theme="dark" enables dark mode with one line.
  • Key features used: row-cols-5 (5-column equal grid), form-switch (toggle switch), btn-group (ON/OFF buttons)

Chart.js 4 + chartjs-adapter-date-fns

  • Purpose: Time-series graph for temperature and humidity history
  • Why I chose it: Canvas-based and lightweight. Time axis (type: 'time') and dual Y-axis (left: °C / right: %) are built-in features.
  • Key detail: I enabled decimation: { algorithm: 'lttb' } to automatically thin out data points. This keeps rendering fast even with 1 month of data (~8,600 records).
  • Why the adapter is needed: Chart.js cannot format dates on the time axis by itself. Adding chartjs-adapter-date-fns enables formats like MM/dd HH:mm.

MQTT.js 5

  • Purpose: Connect the browser to the MQTT broker to receive real-time data and send control commands
  • Why I chose it: Browsers cannot use raw TCP MQTT — they need MQTT over WebSocket. MQTT.js is the standard library for this. It can be added with one CDN line.

Backend

Flask (Python)

  • Purpose: REST API server that returns history data
  • Why I chose it: Lightweight and simple. I only needed 1–2 endpoints, so a large framework like Django was not necessary. Python also works well with the paho-mqtt library.
  • Endpoints:
  • GET /api/history?range=1d — returns up to 1 day of data from SQLite
  • GET /api/health — health check

SQLite

  • Purpose: Store sensor history data
  • Why I chose it: No server needed — it is a single file. For about 8,640 records (1 month of data), performance is fine. It is easy to persist as a Docker volume.
  • Design: Data is saved in 5-minute buckets. Values within the same 5-minute window overwrite each other, so the database does not grow without limit.

paho-mqtt (Python library)

  • Purpose: The Flask backend subscribes to the MQTT broker to receive sensor data
  • Why I chose it: The de facto standard MQTT client for Python

Eclipse Mosquitto (Docker container)

  • Purpose: MQTT broker — the message relay server
  • Why I chose it: Lightweight and simple. An official Docker image is available. I reused the same broker from Week 11.
  • Key config: Adding listener 8083 + protocol websockets allows browser (WebSocket) connections.

Dashboard Features

Sensor Display (top row)

  • Shows real-time temperature and humidity for Bottom Sensor and Top Sensor
  • Updates immediately when an MQTT reptile/sensor message arrives
  • Temperature color changes by state: low = blue / normal = green / high = red

Temperature & Humidity History Graph (middle)

  • Switch between 1D / 1W / 1M views
  • Left Y-axis: temperature (°C), right Y-axis: humidity (%)
  • 4 datasets: Bottom °C (blue solid), Top °C (green solid), Bottom % (amber dashed), Top % (orange dashed)
  • Data fetched from the backend API

Output Controls (bottom row — 5 cards)

Each device has these controls: - Manual Override toggle: When ON, manual control is active - Time range: Set the hours during which manual override is active - Apply button (send icon): Sends the settings via MQTT

Device-specific controls: - Intake Fan / Exhaust Fan: PWM slider (0–100%) + RPM display - Lighting / Heater / Humidifier: ON / OFF buttons


MQTT Topics

Topic Direction Content
reptile/sensor ESP32C6 → Browser / Flask Temperature & humidity JSON (every 30 s)
reptile/light/cmd Browser → ESP32C6 Lighting control command
reptile/fan_in/cmd Browser → ESP32C6 Intake fan control (includes PWM)
reptile/fan_ex/cmd Browser → ESP32C6 Exhaust fan control (includes PWM)
reptile/heater/cmd Browser → ESP32C6 Heater control command
reptile/humidifier/cmd Browser → ESP32C6 Humidifier control command

Deployment

The system runs in Docker on the NAS.

# docker-compose.yml (excerpt)
services:
  reptile-api:
    image: reptile-api
    network_mode: host        # host mode to avoid port conflicts with other containers
    volumes:
      - reptile-data:/data    # persist the SQLite file
    restart: unless-stopped

I used network_mode: host to avoid port conflicts with other containers already running on the NAS.


🔗 Connection to Final Project

This dashboard is the control interface for my Smart Reptile Habitat System.

The ESP32C6 on the Reptile Monitor PCB (built in Week 8) publishes sensor data and receives commands. The dashboard runs on the NAS and is accessible from any browser on the local network.


🔧 Problems and Solutions

Problem: Browser could not connect to MQTT

What happened: The Mosquitto broker only listened on TCP port 1883. Browsers cannot use raw TCP MQTT — they require WebSocket.

Solution: Added listener 8083 and protocol websockets to the Mosquitto config. The browser now connects on port 8083 via WebSocket, while the Flask backend uses port 1883 via TCP.

Problem: Fan card time inputs overflowed the card

What happened: With the 5-card horizontal layout (each card at 20% width), the two time inputs and Apply button in a single row overflowed outside the card.

Solution: Stacked the two time inputs vertically inside a flex-fill div, with the Apply button aligned to the right. This fits within the narrow card width.


📚 References


Last updated: April 28, 2026