IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series: Lập trình Raspberry Pi – Bài 16: Web dashboard mini — hiển thị chart + history (FastAPI + HTML + Chart.js)

Series: Lập trình Raspberry Pi & Ứng dụng thực tế Phần 3 — Python “build app thật” Bài 16: Web dashboard mini — hiển thị chart + history (FastAPI + HTML + Chart.js)


1) Mục tiêu bài học

Sau bài này bạn sẽ:

  • dashboard web mini chạy trên Raspberry Pi.
  • Xem nhanh:
    • latest telemetry
    • lịch sử 50–200 điểm gần nhất
    • biểu đồ line chart (Chart.js)
  • Dùng dữ liệu từ SQLite (Bài 15) và API FastAPI (Bài 13).

2) Chuẩn bị

Trong project ~/apps/iotlabs-py-agent:

  • Đã có src/api.py (Bài 13)
  • Đã có SQLite storage (Bài 15)

Cài thêm:

cd ~/apps/iotlabs-py-agent
source .venv/bin/activate
pip install jinja2

3) Thêm query history trong SQLite

Mở src/storage_sqlite.py, thêm hàm:

def list_telemetry(device_id: str, limit: int = 200):
    with sqlite3.connect(DB_PATH) as conn:
        cur = conn.execute(
            "SELECT ts, temperature_c, humidity_pct FROM telemetry WHERE device_id=? ORDER BY ts DESC LIMIT ?",
            (device_id, limit)
        )
        rows = cur.fetchall()
        # trả về theo thứ tự thời gian tăng dần cho chart
        rows.reverse()
        return [{"ts": r[0], "temperature_c": r[1], "humidity_pct": r[2]} for r in rows]

4) Tạo template HTML dashboard

Tạo thư mục templates:

mkdir -p templates
nano templates/dashboard.html

Dán nội dung:

<!doctype html>
<html lang="vi">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>IoTLabs Pi Dashboard</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body { font-family: Arial, sans-serif; margin: 16px; }
    .row { display: flex; gap: 16px; flex-wrap: wrap; }
    .card { border: 1px solid #ddd; border-radius: 10px; padding: 12px; min-width: 260px; }
    canvas { max-width: 980px; }
    small { color: #666; }
  </style>
</head>
<body>
  <h2>IoTLabs Pi Dashboard</h2>
  <small>device_id: <b>{{ device_id }}</b> | updated: {{ now }}</small>

  <div class="row" style="margin-top:12px;">
    <div class="card">
      <div><b>Latest</b></div>
      {% if latest %}
        <div>ts: {{ latest.ts }}</div>
        <div>T: {{ latest.temperature_c }} °C</div>
        <div>H: {{ latest.humidity_pct }} %</div>
      {% else %}
        <div>Chưa có dữ liệu</div>
      {% endif %}
    </div>

    <div class="card">
      <div><b>Quick tips</b></div>
      <div>• /health, /metrics</div>
      <div>• /api/telemetry/history</div>
    </div>
  </div>

  <h3 style="margin-top:18px;">Biểu đồ nhiệt độ & độ ẩm</h3>
  <canvas id="chart" height="120"></canvas>

  <script>
    const labels = {{ labels | safe }};
    const tempData = {{ temp | safe }};
    const humData  = {{ hum | safe }};

    const ctx = document.getElementById('chart').getContext('2d');
    new Chart(ctx, {
      type: 'line',
      data: {
        labels,
        datasets: [
          { label: 'Temperature (°C)', data: tempData, tension: 0.2 },
          { label: 'Humidity (%)', data: humData, tension: 0.2 }
        ]
      },
      options: {
        responsive: true,
        interaction: { mode: 'index', intersect: false },
        plugins: { legend: { position: 'top' } },
        scales: { x: { ticks: { maxTicksLimit: 8 } } }
      }
    });
  </script>
</body>
</html>

5) Thêm endpoint API + page dashboard vào FastAPI

Mở src/api.py và bổ sung:

5.1 Imports

Thêm:

from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi import Request
from src.storage_sqlite import ensure_db, latest_telemetry, list_telemetry

5.2 Init templates

Sau app = FastAPI(…) thêm:

templates = Jinja2Templates(directory="templates")
ensure_db()

5.3 Endpoint JSON history

Thêm:

@app.get("/api/telemetry/history")
def telemetry_history(device_id: str = "pi-gw-01", limit: int = 200):
    return {"device_id": device_id, "items": list_telemetry(device_id, limit)}

5.4 Dashboard page

Thêm:

@app.get("/", response_class=HTMLResponse)
def dashboard(request: Request, device_id: str = "pi-gw-01", limit: int = 200):
    latest = latest_telemetry(device_id)
    items = list_telemetry(device_id, limit)

    labels = [i["ts"][11:] for i in items]  # HH:MM:SS
    temp = [i["temperature_c"] for i in items]
    hum  = [i["humidity_pct"] for i in items]

    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "device_id": device_id,
        "latest": latest,
        "labels": labels,
        "temp": temp,
        "hum": hum,
        "now": datetime.now().isoformat(timespec="seconds"),
    })

6) Chạy và xem dashboard

Chạy:

source .venv/bin/activate
uvicorn src.api:app --host 0.0.0.0 --port 8000

Mở trên trình duyệt:

  • http://<IP_CUA_PI>:8000/

Nếu bạn bật UFW, mở port 8000 theo Bài 13/Bài 4.

7) Bài tập nâng cấp

  1. Thêm chart pressure (nếu dùng BME280).
  2. Thêm filter from_ts/to_ts để query theo khoảng thời gian.
  3. Cache 5 giây để dashboard tải nhanh hơn.