一、Dashboard 為什麼重要?
感測資料存進資料庫並不代表系統已經完成。真正的價值,在於把資料轉換成使用者可以快速理解的畫面。
二、完整資料流程
ESP32 負責蒐集與傳送資料;Django 負責接收、保存、查詢與組織資料;瀏覽器則負責將資料呈現成資訊卡與圖表。
三、Dashboard 首頁應該顯示什麼?
養殖戶打開首頁後,最好在五秒內看到:
- 目前水溫。
- 目前 pH。
- 目前溶氧。
- 目前鹽度或導電度。
- 資料更新時間。
- 目前狀態是否異常。
四、Django 資料模型
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"]
| 欄位 | 型態 | 用途 |
|---|---|---|
| pond | ForeignKey | 對應養殖戶與魚塭 |
| water_temperature | FloatField | 水溫 |
| salinity | FloatField | 鹽度 |
| ph | FloatField | 酸鹼值 |
| dissolved_oxygen | FloatField | 溶氧 |
| recorded_at | DateTimeField | 感測時間 |
五、取得最新一筆資料
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 資訊卡
<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>
七、建立最新資料 API
若 Dashboard 要自動更新,瀏覽器需要一個可查詢最新資料的 API。
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,
})
path(
"api/pond/<int:pond_id>/latest/",
views.latest_reading,
name="latest-reading"
)
八、AJAX 自動更新資訊卡
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
);
九、建立歷史趨勢 API
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 顯示趨勢
<canvas id="trendChart"></canvas>
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 資料放入圖表
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 | 溶氧 |
|---|---|---|---|---|---|
| 博論水質系統 | 001/北港溪 | 29.4°C | 0.0 ppt | 8.0 | 10.0 mg/L |
| 湖虎戰隊 | 1/文蛤池 | 29.3°C | 0.5 ppt | 6.8 | 6.8 mg/L |
| LiaoZike | 41243166/水井蛤蜊池 | 30.0°C | 0.4 ppt | 7.1 | 6.5 mg/L |
多場域 Dashboard 的重點是每筆感測資料都必須關聯到正確的 Pond,才能依養殖戶、場域或魚塭進行分類查詢。
十三、狀態判斷與警示
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 顯示警示
{% 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 可能在辦公室電腦、平板或養殖戶手機上使用,因此版面必須能自動調整。
<div class="col-12 col-sm-6 col-xl-3">
...
</div>
十八、Dashboard 實際連線
https://shuijingusr.pythonanywhere.com/dashboard/
十九、常見問題與排除
| 問題 | 可能原因 | 解決方法 |
|---|---|---|
| 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 建立資料庫索引。
- 歷史報表可做每分鐘、每小時或每日彙整。
- 最新值可使用快取,減少重複查詢。
- 定期備份與清理測試資料。
二十一、本篇重點
- Dashboard 的目的,是將資料轉換成可快速判斷的資訊。
- Bootstrap Card 適合呈現最新水質值。
- Django API 提供最新值與歷史趨勢資料。
- AJAX 與 setInterval 可定時更新畫面。
- Chart.js 適合呈現時間序列。
- 多養殖戶管理必須建立 Pond 與 SensorReading 關聯。
- 警示值、離線判斷與更新時間同樣重要。
結語
完成 EP06 後,智慧養殖系統已從「收集資料」進一步提升為「呈現資訊與支援決策」。Django 負責整合資料庫與 API,Bootstrap 建立清楚的資訊卡,Chart.js 呈現歷史變化,AJAX 則讓畫面自動更新。當 Dashboard 能同時呈現即時值、趨勢、警示、時間與多場域狀態時,IoT 系統才真正開始對養殖管理產生價值。

沒有留言:
張貼留言