Mini Project: Real-Time Temperature Monitoring System

Microcontrollers 1 & Programming 2

Multi-Sensor Web Dashboard with Containerization

Project Overview

Objective: Build a containerized web application that displays real-time temperature data from multiple sources in an interactive dashboard.

What You'll Build:

  • Arduino-based temperature sensing system with DHT11 and MF52A sensors
  • Web application with real-time plotting capabilities
  • Integration with external weather API for comparison
  • Dockerized application with proper serial port mapping

Skills Applied: Microcontroller programming, web development, API integration, containerization, and real-time data visualization

Step 1: Hardware Setup & Arduino Programming

Sensors You'll Use:

  • DHT11: Digital temperature & humidity sensor (Pin 2)
  • MF52A: Analog thermistor (Pin A0)

Reference Materials:

Your Task: Write Arduino code that reads both sensors and sends temperature data via Serial in a structured format (e.g., JSON or CSV)

💡 Tip: Use a consistent format like: DHT:25.5,MF52:25.3 to make parsing easier in your web app

Arduino Serial Communication

Recommended Serial Output Format:

Your Arduino should continuously output temperature readings in a parseable format. Here's an example:

💡 Pro Tips:

  • Use a delimiter that's easy to split (comma, pipe, or JSON)
  • Set Serial baud rate to 9600
  • Send readings every 2-3 seconds
void loop() {
  float dht_temp = dht.readTemperature();
  float mf52_temp = readMF52Temperature();
  
  // JSON format (recommended)
  Serial.print("{\"dht\":");
  Serial.print(dht_temp);
  Serial.print(",\"mf52\":");
  Serial.print(mf52_temp);
  Serial.println("}");
  
  delay(2000);
}

Step 2: Web Application Architecture

Technology Stack Options:

  • Backend: Python (Flask/FastAPI) or Node.js (Express)
  • Frontend: HTML + JavaScript with charting library
  • Serial Communication: PySerial (Python) or SerialPort (Node.js)
  • Real-time Updates: WebSockets or Server-Sent Events (SSE)

Application Components:

  • Serial reader that continuously reads from Arduino
  • Weather API integration (See this page)
  • WebSocket/SSE server for pushing data to frontend
  • Frontend with three separate panels for visualization

Reading Serial Data (Python Example)

Using PySerial Library:

Install with: pip install pyserial

💡 Important Tips:

  • Use auto-detection to find Arduino automatically
  • Ports usually: /dev/ttyUSB0, /dev/ttyACM0 (Linux/Mac) or COM3, COM4 (Windows)
  • Always handle connection errors gracefully
import serial
import serial.tools.list_ports
import json

def find_arduino_port():
    """Auto-detect Arduino port"""
    ports = serial.tools.list_ports.comports()
    for port in ports:
        # Look for Arduino in description
        if 'Arduino' in port.description or \
           'USB' in port.description:
            return port.device
    return None

# Auto-detect or use environment variable
port = find_arduino_port() or '/dev/ttyUSB0'
ser = serial.Serial(port, 9600, timeout=1)

def read_arduino_data():
    if ser.in_waiting > 0:
        line = ser.readline().decode('utf-8').strip()
        data = json.loads(line)
        return {'dht': data['dht'], 'mf52': data['mf52']}
    return None

Step 3: Integrating Weather API

Reference Your Previous Learning:

Refer back to your weather API slides for details on API keys, endpoints, and data parsing.

Your Task:

  • Make periodic API calls to get outdoor temperature (every 5-10 minutes is sufficient)
  • Cache the results to avoid excessive API calls
  • Handle API errors (network issues, rate limits)

💡 Best Practices:

  • Don't call the API every second - weather doesn't change that fast!
  • Store your API key in environment variables (never hardcode it)
  • Implement retry logic with exponential backoff

Step 4: Creating Your Dashboard Panels

Three Panel Layout:

  • Panel 1: DHT11 Temperature (real-time line chart)
  • Panel 2: MF52A Temperature (real-time line chart)
  • Panel 3: Outdoor Temperature from API (line chart with less frequent updates)

Recommended Charting Libraries:

  • Chart.js - Easy to use, good documentation
  • Plotly.js - More interactive features
  • D3.js - Most powerful but steeper learning curve

💡 Design Tips:

  • Use a responsive grid layout (CSS Grid or Flexbox)
  • Add current temperature displays above each chart
  • Use different colors for each sensor
  • Include timestamps on the x-axis

Implementing Real-Time Updates

Option 1: WebSockets (Recommended)

Two-way communication for instant updates

💡 Implementation Tips:

  • Backend continuously reads serial data and broadcasts via WebSocket
  • Frontend receives data and updates charts
  • Keep last 50-100 data points to prevent memory issues
// Frontend WebSocket connection
const ws = new WebSocket('ws://localhost:8000');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  // Update DHT11 chart
  updateChart(dhtChart, data.dht);
  
  // Update MF52A chart
  updateChart(mf52Chart, data.mf52);
  
  // Update outdoor temp if available
  if (data.outdoor) {
    updateChart(outdoorChart, data.outdoor);
  }
};

Updating Charts Efficiently

Key Considerations:

  • Limit data points to prevent performance issues
  • Use timestamps for accurate x-axis
  • Smooth animations for better UX

💡 Performance Tip: Use a sliding window approach - remove old data as new data arrives

function updateChart(chart, newValue) {
  const now = new Date();
  
  // Add new data point
  chart.data.labels.push(now.toLocaleTimeString());
  chart.data.datasets[0].data.push(newValue);
  
  // Keep only last 50 points
  if (chart.data.labels.length > 50) {
    chart.data.labels.shift();
    chart.data.datasets[0].data.shift();
  }
  
  // Update the chart
  chart.update('none'); // 'none' disables animation
}

Recommended Project Structure

Organize your project for easy containerization:

💡 Tip: Keep frontend and backend separate but in the same project directory

temperature-monitor/
├── arduino/
│   └── sensor_reader.ino
├── backend/
│   ├── app.py (or server.js)
│   ├── serial_reader.py
│   ├── weather_api.py
│   └── requirements.txt
├── frontend/
│   └── index.html
├── Dockerfile
└── docker-compose.yml

Step 5: Containerizing Your Application

Why Containerize?

  • Ensures application runs the same on any machine
  • Easy deployment and sharing
  • Includes all dependencies in one package

Prerequisites:

  • Install Docker Desktop on your machine
  • Ensure your application runs successfully outside Docker first

💡 Development Tip: Always test your app locally before containerizing. Debug Docker issues separately from application bugs!

Creating Your Dockerfile

Basic Dockerfile Structure:

This example is for a Python Flask application

💡 Key Points:

  • Start with appropriate base image
  • Install dependencies first (better caching)
  • Copy your application code
  • Expose necessary ports
FROM python:3.10-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY backend/ ./backend/
COPY frontend/ ./frontend/

# Expose port
EXPOSE 8000

# Run the application
CMD ["python", "backend/app.py"]

🔴 Critical: Mapping Serial Ports to Docker

The Challenge: Docker containers are isolated from your host's hardware. You must explicitly give the container access to the serial port.

Step 1: Find Your Arduino Port

  • Linux/Mac: ls /dev/tty* or ls /dev/cu.*
  • Windows: Check Device Manager (usually COM3, COM4, etc.)

Step 2: Add Device Mapping to Docker Command

Use the --device flag to map the serial port:

docker run --device=/dev/ttyUSB0:/dev/ttyUSB0 your-image

💡 Important: The port path inside the container (after the colon) should match what your code expects!

Using Docker Compose (Recommended)

Docker Compose makes running containers easier:

Key Configuration:

  • devices: Map the serial port
  • privileged: May be needed for serial access (use cautiously)
  • ports: Expose web application port

Run with: docker-compose up

version: '3.8'

services:
  temp-monitor:
    build: .
    ports:
      - "8000:8000"
    devices:
      - "/dev/ttyUSB0:/dev/ttyUSB0"
    environment:
      - SERIAL_PORT=/dev/ttyUSB0
      - WEATHER_API_KEY=${WEATHER_API_KEY}
    # Optional: if serial access issues persist
    # privileged: true

Troubleshooting Serial Port Access in Docker

Common Issues & Solutions:

Problem 1: Permission Denied

  • Linux Solution: Add your user to dialout group: sudo usermod -a -G dialout $USER
  • Then logout/login or restart

Problem 2: Port Not Found

  • Verify Arduino is connected: ls -l /dev/ttyUSB*
  • Check cable connections
  • Ensure no other program is using the port (close Arduino IDE)

Problem 3: Port Name Changes

  • Use environment variables for port names
  • Create udev rules for persistent names (advanced)

💡 Debug Tip: Test serial communication outside Docker first, then troubleshoot Docker-specific issues

Building and Testing Your Container

Build Process:

💡 Testing Checklist:

  • ✓ Serial communication working
  • ✓ Weather API calls successful
  • ✓ WebSocket/SSE connection established
  • ✓ All three charts updating
  • ✓ Application accessible from browser
# Build the Docker image
docker build -t temp-monitor .

# Run with serial port mapping
docker run -p 8000:8000 \
  --device=/dev/ttyUSB0:/dev/ttyUSB0 \
  -e WEATHER_API_KEY=your_key_here \
  temp-monitor

# Or use docker-compose
docker-compose up

# Access in browser
# http://localhost:8000

Managing Configuration with Environment Variables

Never hardcode sensitive information!

Create a .env file:

💡 Security Tips:

  • Add .env to .gitignore
  • Provide .env.example for others
  • Document required variables in README
# .env file (don't commit this!)
WEATHER_API_KEY=abc123xyz789
SERIAL_PORT=/dev/ttyUSB0
BAUD_RATE=9600

# .env.example (commit this)
WEATHER_API_KEY=your_api_key_here
SERIAL_PORT=/dev/ttyUSB0
BAUD_RATE=9600

# Access in Python
import os
api_key = os.getenv('WEATHER_API_KEY')

Final Integration Checklist

Before Submission, Verify:

✓ Hardware Layer:

  • Both sensors reading correctly on Arduino
  • Serial output properly formatted and consistent

✓ Software Layer:

  • Backend successfully reads serial data
  • Weather API integration working with error handling
  • WebSocket/SSE pushing data to frontend
  • All three panels displaying and updating correctly

✓ Containerization:

  • Dockerfile builds without errors
  • Serial port correctly mapped
  • Application runs in container
  • Documentation includes how to run the container

Common Pitfalls to Avoid

🚫 Don't Do This:

  • Hardcoding paths/ports: Use environment variables or configuration files
  • Calling Weather API too frequently: Cache results for 5-10 minutes
  • Ignoring error handling: Network failures, sensor errors, and serial disconnections WILL happen
  • Storing unlimited data points: Your browser will crash. Limit to 50-100 points per chart
  • Committing API keys: Always use .env files and add them to .gitignore
  • Testing only in Docker: Debug locally first, then containerize
  • Not closing serial connections: This can prevent reconnection

✅ Do This Instead: Plan your architecture, test incrementally, handle errors gracefully, document as you go!