2026年8月3日 星期一

[水井村USR] EP06|Django Dashboard 實戰:打造智慧養殖即時監控平台

智慧生活科技專業社群|IoT 入門系列|適合大一新生
前一篇已完成 ESP32 將水溫、pH、溶氧與鹽度上傳 Django REST API。EP06 要進一步把資料轉換成真正能使用的 Dashboard:以資訊卡呈現最新值、使用 Chart.js 顯示趨勢、透過 AJAX 自動更新,並支援多養殖戶、警示與歷史查詢。

請先將 EP06 主教學圖上傳 Blogger,再把 EP06_MAIN_IMAGE_URL 換成圖片網址。


圖:從 Atlas 感測器、ESP32、Django REST API、資料庫到 Dashboard 的完整流程。

一、Dashboard 為什麼重要?

感測資料存進資料庫並不代表系統已經完成。真正的價值,在於把資料轉換成使用者可以快速理解的畫面。

即時狀態快速掌握水溫、pH、DO 與鹽度。
歷史趨勢觀察數值是否逐漸上升或下降。
異常警示在溶氧過低或水溫過高時提醒。
多場域管理同時查看不同養殖戶與魚塭。
核心觀念:Dashboard 不是資料庫,而是協助使用者判斷與決策的資訊介面。

二、完整資料流程

Atlas 感測器 ESP32 Wi-Fi Django REST API 資料庫 Dashboard

ESP32 負責蒐集與傳送資料;Django 負責接收、保存、查詢與組織資料;瀏覽器則負責將資料呈現成資訊卡與圖表。

三、Dashboard 首頁應該顯示什麼?

30.1°C水溫
6.80pH
6.8 mg/L溶氧
0.44 ppt鹽度

養殖戶打開首頁後,最好在五秒內看到:

  • 目前水溫。
  • 目前 pH。
  • 目前溶氧。
  • 目前鹽度或導電度。
  • 資料更新時間。
  • 目前狀態是否異常。

四、Django 資料模型

models.py 範例
from django.db import models

class Pond(models.Model):
    farm_name = models.CharField(
        max_length=100
    )

    pond_code = models.CharField(
        max_length=30
    )

    def __str__(self):
        return f"{self.farm_name} / {self.pond_code}"


class SensorReading(models.Model):
    pond = models.ForeignKey(
        Pond,
        on_delete=models.CASCADE,
        related_name="readings"
    )

    water_temperature = models.FloatField(
        null=True,
        blank=True
    )

    salinity = models.FloatField(
        null=True,
        blank=True
    )

    ph = models.FloatField(
        null=True,
        blank=True
    )

    dissolved_oxygen = models.FloatField(
        null=True,
        blank=True
    )

    recorded_at = models.DateTimeField()

    class Meta:
        ordering = ["-recorded_at"]
欄位型態用途
pondForeignKey對應養殖戶與魚塭
water_temperatureFloatField水溫
salinityFloatField鹽度
phFloatField酸鹼值
dissolved_oxygenFloatField溶氧
recorded_atDateTimeField感測時間

五、取得最新一筆資料

views.py
from django.shortcuts import render
from .models import Pond

def dashboard_home(request):
    ponds = Pond.objects.all()

    rows = []

    for pond in ponds:
        latest = (
            pond.readings
            .order_by("-recorded_at")
            .first()
        )

        rows.append({
            "pond": pond,
            "latest": latest,
        })

    return render(
        request,
        "dashboard/home.html",
        {"rows": rows}
    )

這個 View 會逐一讀取所有魚塭,並查詢每個魚塭的最新一筆感測資料。

六、建立 Bootstrap 資訊卡

templates/dashboard/home.html
<div class="row g-3">

  <div class="col-md-3">
    <div class="card h-100 shadow-sm">
      <div class="card-body">
        <small class="text-muted">水溫</small>
        <h2 id="temperature">
          {{ latest.water_temperature|default:"--" }}
        </h2>
        <span>°C</span>
      </div>
    </div>
  </div>

  <div class="col-md-3">
    <div class="card h-100 shadow-sm">
      <div class="card-body">
        <small class="text-muted">pH</small>
        <h2 id="ph">
          {{ latest.ph|default:"--" }}
        </h2>
      </div>
    </div>
  </div>

</div>
Bootstrap 的 Grid 系統可以讓四張資訊卡在電腦上並排,在手機上自動換行,形成響應式版面。

七、建立最新資料 API

若 Dashboard 要自動更新,瀏覽器需要一個可查詢最新資料的 API。

views.py:latest_reading
from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(["GET"])
def latest_reading(request, pond_id):
    pond = Pond.objects.get(id=pond_id)

    latest = (
        pond.readings
        .order_by("-recorded_at")
        .first()
    )

    if latest is None:
        return Response(
            {"message": "No data"},
            status=404
        )

    return Response({
        "water_temperature":
            latest.water_temperature,

        "salinity":
            latest.salinity,

        "ph":
            latest.ph,

        "dissolved_oxygen":
            latest.dissolved_oxygen,

        "recorded_at":
            latest.recorded_at,
    })
urls.py
path(
    "api/pond/<int:pond_id>/latest/",
    views.latest_reading,
    name="latest-reading"
)

八、AJAX 自動更新資訊卡

JavaScript 自動刷新
async function loadLatest() {
  const response = await fetch(
    "/api/pond/1/latest/"
  );

  if (!response.ok) {
    console.log("讀取失敗");
    return;
  }

  const data = await response.json();

  document.getElementById(
    "temperature"
  ).textContent =
    data.water_temperature ?? "--";

  document.getElementById(
    "ph"
  ).textContent =
    data.ph ?? "--";

  document.getElementById(
    "dissolvedOxygen"
  ).textContent =
    data.dissolved_oxygen ?? "--";

  document.getElementById(
    "salinity"
  ).textContent =
    data.salinity ?? "--";
}

loadLatest();

setInterval(
  loadLatest,
  5000
);
瀏覽器 每 5 秒呼叫 API 取得最新 JSON 更新資訊卡

九、建立歷史趨勢 API

views.py:trend_data
from django.utils import timezone
from datetime import timedelta

@api_view(["GET"])
def trend_data(request, pond_id):
    start_time = (
        timezone.now()
        - timedelta(hours=2)
    )

    readings = (
        SensorReading.objects
        .filter(
            pond_id=pond_id,
            recorded_at__gte=start_time
        )
        .order_by("recorded_at")
    )

    data = []

    for item in readings:
        data.append({
            "time":
                item.recorded_at.strftime(
                    "%H:%M"
                ),

            "temperature":
                item.water_temperature,

            "ph":
                item.ph,

            "dissolved_oxygen":
                item.dissolved_oxygen,

            "salinity":
                item.salinity,
        })

    return Response(data)

十、使用 Chart.js 顯示趨勢

HTML 畫布
<canvas id="trendChart"></canvas>
Chart.js 初始化
const chartContext = document
  .getElementById("trendChart")
  .getContext("2d");

const trendChart = new Chart(
  chartContext,
  {
    type: "line",

    data: {
      labels: [],

      datasets: [
        {
          label: "水溫",
          data: []
        },
        {
          label: "pH",
          data: []
        },
        {
          label: "溶氧",
          data: []
        },
        {
          label: "鹽度",
          data: []
        }
      ]
    },

    options: {
      responsive: true,
      maintainAspectRatio: false
    }
  }
);

十一、將 API 資料放入圖表

更新 Chart.js
async function loadTrend() {
  const response = await fetch(
    "/api/pond/1/trend/"
  );

  const rows = await response.json();

  trendChart.data.labels =
    rows.map(row => row.time);

  trendChart.data.datasets[0].data =
    rows.map(row => row.temperature);

  trendChart.data.datasets[1].data =
    rows.map(row => row.ph);

  trendChart.data.datasets[2].data =
    rows.map(row =>
      row.dissolved_oxygen
    );

  trendChart.data.datasets[3].data =
    rows.map(row => row.salinity);

  trendChart.update();
}

loadTrend();

setInterval(
  loadTrend,
  30000
);
水溫、pH、溶氧與鹽度的數值範圍不同。正式 Dashboard 可使用雙 Y 軸,或將不同單位拆成多張圖,避免曲線互相壓縮。

十二、多養殖戶監控

養殖戶魚塭水溫鹽度pH溶氧
博論水質系統001/北港溪29.4°C0.0 ppt8.010.0 mg/L
湖虎戰隊1/文蛤池29.3°C0.5 ppt6.86.8 mg/L
LiaoZike41243166/水井蛤蜊池30.0°C0.4 ppt7.16.5 mg/L

多場域 Dashboard 的重點是每筆感測資料都必須關聯到正確的 Pond,才能依養殖戶、場域或魚塭進行分類查詢。

十三、狀態判斷與警示

Python 狀態判斷範例
def evaluate_status(reading):
    alerts = []

    if (
        reading.dissolved_oxygen
        is not None
        and reading.dissolved_oxygen < 3
    ):
        alerts.append("溶氧過低")

    if (
        reading.water_temperature
        is not None
        and reading.water_temperature > 35
    ):
        alerts.append("水溫過高")

    if (
        reading.ph is not None
        and (
            reading.ph < 5
            or reading.ph > 9
        )
    ):
        alerts.append("pH 異常")

    return alerts
提醒:上述門檻僅為程式示例。正式養殖警示值應依養殖物種、成長階段、鹽度、水溫與場域管理需求設定。

十四、在 Template 顯示警示

Bootstrap Alert
{% if alerts %}
  <div class="alert alert-danger">
    <strong>目前異常:</strong>

    <ul class="mb-0">
      {% for alert in alerts %}
        <li>{{ alert }}</li>
      {% endfor %}
    </ul>
  </div>
{% else %}
  <div class="alert alert-success">
    目前監測狀態正常
  </div>
{% endif %}

十五、資料更新時間與離線判斷

數值看起來正常,不代表設備一定在線。若最後資料時間已經超過設定範圍,Dashboard 應標示「資料逾時」或「裝置離線」。

離線判斷範例
from django.utils import timezone
from datetime import timedelta

offline = (
    latest is None
    or latest.recorded_at
       < timezone.now()
         - timedelta(minutes=5)
)
狀態建議顯示
5 分鐘內有資料綠色:正常監測
5~15 分鐘沒有資料橘色:資料延遲
超過 15 分鐘沒有資料紅色:裝置離線
從未上傳資料灰色:尚無資料

十六、Dashboard 頁面結構

建議目錄
project/
├─ dashboard/
│  ├─ models.py
│  ├─ views.py
│  ├─ urls.py
│  ├─ serializers.py
│  └─ templates/
│     └─ dashboard/
│        ├─ base.html
│        ├─ home.html
│        ├─ pond_detail.html
│        └─ history.html
│
├─ static/
│  ├─ css/
│  │  └─ dashboard.css
│  └─ js/
│     ├─ latest.js
│     └─ trend.js
│
└─ manage.py

十七、RWD 響應式設計

智慧養殖 Dashboard 可能在辦公室電腦、平板或養殖戶手機上使用,因此版面必須能自動調整。

Bootstrap Grid 建議
<div class="col-12 col-sm-6 col-xl-3">
  ...
</div>
這個設定代表:手機一列一張、平板一列兩張、大螢幕一列四張資訊卡。

十八、Dashboard 實際連線

開啟水井村 USR 智慧養殖 Dashboard

https://shuijingusr.pythonanywhere.com/dashboard/

水井村 USR 智慧養殖 Dashboard
請將實際 Dashboard 截圖上傳 Blogger,再替換 EP06_DASHBOARD_IMAGE_URL
圖:多養殖戶即時水質狀態與場域分布。

十九、常見問題與排除

問題可能原因解決方法
Dashboard 沒有資料資料庫尚無資料或 View 查詢錯誤先到 Django Admin 確認資料
資訊卡顯示 None感測欄位允許 null使用 default 或 JavaScript 的 ??
Chart.js 沒有曲線API 回傳空陣列或欄位名稱不一致先在瀏覽器開啟 API 檢查 JSON
自動更新失敗API URL、權限或 JavaScript 錯誤查看瀏覽器 Console 與 Network
時間差 8 小時Django 時區設定錯誤確認 TIME_ZONE 與 USE_TZ
顯示舊資料排序方向錯誤使用 -recorded_at 取得最新值
頁面載入過慢一次查詢過多歷史資料限制時間範圍並建立索引

二十、效能與資料量

若 ESP32 每 15 秒上傳一次,一天約產生 5,760 筆資料。多台裝置長期運作後,資料量會快速增加。

  • Dashboard 只查詢必要時間範圍。
  • 在 pond 與 recorded_at 建立資料庫索引。
  • 歷史報表可做每分鐘、每小時或每日彙整。
  • 最新值可使用快取,減少重複查詢。
  • 定期備份與清理測試資料。

二十一、本篇重點

  1. Dashboard 的目的,是將資料轉換成可快速判斷的資訊。
  2. Bootstrap Card 適合呈現最新水質值。
  3. Django API 提供最新值與歷史趨勢資料。
  4. AJAX 與 setInterval 可定時更新畫面。
  5. Chart.js 適合呈現時間序列。
  6. 多養殖戶管理必須建立 Pond 與 SensorReading 關聯。
  7. 警示值、離線判斷與更新時間同樣重要。

結語

完成 EP06 後,智慧養殖系統已從「收集資料」進一步提升為「呈現資訊與支援決策」。Django 負責整合資料庫與 API,Bootstrap 建立清楚的資訊卡,Chart.js 呈現歷史變化,AJAX 則讓畫面自動更新。當 Dashboard 能同時呈現即時值、趨勢、警示、時間與多場域狀態時,IoT 系統才真正開始對養殖管理產生價值。

AI 協作聲明:本文由作者主導 Dashboard 架構設計、Django 資料模型與 API 規劃、Bootstrap 介面、Chart.js 趨勢圖、自動更新與智慧養殖場域需求整理,並使用生成式 AI 協助文字整理、程式碼說明、版面配置與教學資訊圖生成;所有系統流程、介面概念與程式內容均依本專案實際開發經驗整理,並由作者審核修訂。

沒有留言:

張貼留言