<aside> 🐿️ Notion 連結:AIoT HW6 - 4110056007 Youtube 影片連結:https://youtu.be/PX1zuFgwePk GitHub 程式碼連結:https://github.com/FengDian-Su/AIoT
</aside>
Raspberry Pi 4 刷機
下載 Raspberry Pi Imager 到記憶卡,然後完成初始設定

連接電源、顯示器、滑鼠、鍵盤 … 等裝置,就可以開始使用了

Arduino IDE 安裝
在 Raspberry Pi 的終端輸入 sudo api install Arduino,完成下載即可使用
設定開發版以及下載對應程式庫
將 DHT11 連接上 ESP32,將 ESP32 連接 Raspberry Pi
將 Lab 2, 3 的程式碼導入,即可使用 ( p.s. Raspberry Pi 的速度好慢 )
<aside> 💡 特別要注意的地方是,連接 WIFI 後要先確認 SSID、Password、IP
</aside>
Arduino Code
#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT11.h>
#define DHTPIN 4
#define DHTTYPE DHT11
const char* ssid = "SSID";
const char* password = "PASSWORD";
const char *serverAddress = "<http://IP:5500/post_data>";
DHT11 dht11(DHTPIN);
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
}
void sendSensorData(float temperature, float humidity) {
if ((WiFi.status() == WL_CONNECTED)) {
HTTPClient http;
http.begin(serverAddress);
http.addHeader("Content-Type", "application/json");
String jsonPayload = "{\\"temperature\\":" + String(temperature) + ",\\"humidity\\":" + String(humidity) + "}";
int httpResponseCode = http.POST(jsonPayload);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(httpResponseCode);
Serial.println(response);
} else {
Serial.print("Error code: ");
Serial.println(httpResponseCode);
}
http.end();
}
}
void loop() {
delay(2000)
int temperature = 0, humidity = 0;
int result = dht11.readTemperatureHumidity(temperature, humidity);
if (result != 0) {
Serial.println("Failed to read from DHT sensor");
return;
}
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.print(" °C\\tHumidity: ");
Serial.print(humidity);
Serial.println(" %");
sendSensorData(temperature, humidity);
}
前端視覺化顯示
由於 Lab3 中已經寫了一版 index.html,因此可以直接套上去
然而 §6.2 提出了新的要求,因此完整程式碼放在 §6.2
建立後端資料庫
db_method.py 程式碼如下:
import sqlite3
from datetime import datetime
from flask import Flask, jsonify
DB_NAME = "temp_data.sqlite"
def initdb(db_name=DB_NAME):
try:
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
cursor.execute(
"""CREATE TABLE IF NOT EXISTS sensor_data (
id INTEGER PRIMARY KEY,
temperature REAL NOT NULL,
humidity REAL NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"""
)
conn.commit()
conn.close()
print("(initdb) Database initialized successfully.")
except sqlite3.Error as e:
print("(initdb) Error occurred:", e)
initdb(db_name=DB_NAME)
def insert_sensor_data(temperature, humidity, timestamp = None,
db_name = DB_NAME):
try:
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
if timestamp is None:
timestamp = datetime.now().replace(microsecond=0)
cursor.execute("""INSERT INTO sensor_data (temperature, humidity,
timestamp) VALUES (?, ?, ?)""", (temperature, humidity, timestamp),
)
conn.commit()
conn.close()
print("(insert_sensor_data) Sensor data inserted successfully.")
except sqlite3.Error as e:
print("(insert_sensor_data) Error occurred:", e)
將溫濕度資料傳入後端並顯示在前端
由於 Lab3 中已經寫了一版 app.py,因此可以直接套上去
然而 §6.2 提出了新的要求,因此完整程式碼放在 §6.2
我們在前端新增了兩個 button,藉由 showDiv 函式可以讓使用者選擇要觀看即時數據 ( RealTime ) 或是特定區間資料 ( Interval ),實際 Demo 影片放在 §6.2.2
在即時數據中,我們設定一個 id 為 RealTime 的 div 區塊,並且定期更新圖表,以下是完整的程式碼
app.py
import sqlite3
from flask import Flask, jsonify, render_template, request
from db_method import DB_NAME, insert_sensor_data, initdb
import datetime
DB_NAME = "temp_data.sqlite"
app = Flask(__name__)
data = {"temperature": [], "humidity": []}
@app.post("/post_data")
def receive_data():
try:
content = request.get_json()
temperature = content["temperature"]
humidity = content["humidity"]
data["temperature"].append(temperature)
data["humidity"].append(humidity)
insert_sensor_data(temperature, humidity)
print(f"Received data: temperature = {temperature}, humidity = {humidity}")
return jsonify({"success": True})
except Exception as e:
print(f"Error receiving data: {str(e)}")
return jsonify({"success": False, "error": str(e)})
@app.route("/")
def index():
return render_template("index.html")
@app.route('/data')
def get_data():
start = request.args.get('start')
end = request.args.get('end')
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
if start and end:
cursor.execute("SELECT timestamp, temperature, humidity FROM sensor_data WHERE timestamp BETWEEN ? AND ?", (start, end))
else:
cursor.execute("SELECT timestamp, temperature, humidity FROM sensor_data")
data = cursor.fetchall()
conn.close()
return jsonify(data)
@app.route('/time_ranges')
def get_time_ranges():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute("SELECT MIN(timestamp), MAX(timestamp) FROM sensor_data")
min_timestamp, max_timestamp = cursor.fetchone()
conn.close()
if not min_timestamp or not max_timestamp:
return jsonify([])
min_time = datetime.datetime.strptime(min_timestamp, "%Y-%m-%d %H:%M:%S")
max_time = datetime.datetime.strptime(max_timestamp, "%Y-%m-%d %H:%M:%S")
time_ranges = []
while min_time < max_time:
end_time = min_time + datetime.timedelta(seconds=30)
time_ranges.append({
"start": min_time.strftime("%Y-%m-%d %H:%M:%S"),
"end": end_time.strftime("%Y-%m-%d %H:%M:%S")
})
min_time = end_time
return jsonify(time_ranges[-10:])
if __name__ == "__main__":
initdb(DB_NAME)
app.run(host="0.0.0.0", port = 5500, debug = True)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset = "UTF-8">
<title>IoT Project</title>
<link rel = "stylesheet" href = "/static/style.css">
<script src = "<https://code.highcharts.com/highcharts.js>"></script>
</head>
<body>
<div class = "background"></div>
<p class = "title">Temperature & Humidity Realtime Chart</p>
<div style = "display: flex; max-width: 360px; height: 40px; margin: 0 auto;">
<button onclick = "showDiv('Realtime')" class = "select-btn" style = "background-color: #b07ed2">Show Realtime</button>
<button onclick = "showDiv('Interval')" class = "select-btn" style = "background-color: #94b9de">Show Interval</button>
</div>
<div class = "chart-background">
<div id = "Realtime" class = "hide">
<div id = "realtimeChartContainer" class = "real-time"></div>
</div>
<div id = "Interval" class = "hide">
<div style = "margin-top: 20px;margin-bottom: 20px;">
<select id = "timeRangeSelector"></select>
<button id = "updateChart">Update Chart</button>
</div>
<div id = "intervalChartContainer" class = "select-interval"></div>
</div>
</div>
<script>
function showDiv(divId) {
if (divId == "Realtime") {
document.getElementById("Interval").classList.add('hide');
document.getElementById("Realtime").classList.remove('hide');
} else if (divId == "Interval") {
document.getElementById("Realtime").classList.add('hide');
document.getElementById("Interval").classList.remove('hide');
}
}
document.getElementById('Realtime').classList.remove('hide');
document.addEventListener("DOMContentLoaded", function() {
Highcharts.setOptions({
global: {
timezoneOffset: new Date().getTimezoneOffset()
}
});
const intervalChart = Highcharts.chart('intervalChartContainer', {
title: {
text: 'Sensor Data (Selected Interval)'
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
day: '%Y-%m-%d',
week: '%Y-%m-%d',
month: '%Y-%m',
year: '%Y'
}
},
series: [{
name: 'Temperature',
data: []
}, {
name: 'Humidity',
data: []
}]
});
const realtimeChart = Highcharts.chart('realtimeChartContainer', {
title: {
text: 'Sensor Data (Real-time)'
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
day: '%Y-%m-%d',
week: '%Y-%m-%d',
month: '%Y-%m',
year: '%Y'
}
},
series: [{
name: 'Temperature',
data: []
}, {
name: 'Humidity',
data: []
}]
});
let startDate, endDate;
function fetchIntervalDataAndUpdateChart() {
let url = '/data';
if (startDate && endDate) {
url += `?start=${startDate}&end=${endDate}`;
}
fetch(url)
.then(response => response.json())
.then(data => {
const seriesDataTemperature = data.map(entry => [new Date(entry[0]).getTime(), entry[1]]);
const seriesDataHumidity = data.map(entry => [new Date(entry[0]).getTime(), entry[2]]);
intervalChart.series[0].setData(seriesDataTemperature);
intervalChart.series[1].setData(seriesDataHumidity);
})
.catch(error => console.error('Error fetching data:', error));
}
function fetchRealtimeDataAndUpdateChart() {
fetch('/data')
.then(response => response.json())
.then(data => {
const latestData = data.slice(-10);
const seriesDataTemperature = latestData.map(entry => [new Date(entry[0]).getTime(), entry[1]]);
const seriesDataHumidity = latestData.map(entry => [new Date(entry[0]).getTime(), entry[2]]);
realtimeChart.series[0].setData(seriesDataTemperature);
realtimeChart.series[1].setData(seriesDataHumidity);
})
.catch(error => console.error('Error fetching data:', error));
}
fetchIntervalDataAndUpdateChart();
fetchRealtimeDataAndUpdateChart();
setInterval(fetchRealtimeDataAndUpdateChart, 3000);
function fetchTimeRangesAndUpdateSelector() {
fetch('/time_ranges')
.then(response => response.json())
.then(timeRanges => {
const selector = document.getElementById('timeRangeSelector');
selector.innerHTML = '';
timeRanges.forEach(range => {
const option = document.createElement('option');
option.value = `${range.start},${range.end}`;
option.textContent = `${range.start} - ${range.end}`;
selector.appendChild(option);
});
if (timeRanges.length > 0) {
const firstRange = timeRanges[0];
startDate = firstRange.start;
endDate = firstRange.end;
}
})
.catch(error => console.error('Error fetching time ranges:', error));
}
fetchTimeRangesAndUpdateSelector();
setInterval(fetchTimeRangesAndUpdateSelector, 3000);
document.getElementById('timeRangeSelector').addEventListener('change', function() {
const [start, end] = this.value.split(',');
startDate = start;
endDate = end;
fetchIntervalDataAndUpdateChart();
});
document.getElementById('updateChart').addEventListener('click', fetchIntervalDataAndUpdateChart);
});
</script>
</body>
</html>
在特定區間資料中,我們在 app.py 中加入了 start 和 end,並設定下拉式選單讓使用者點選想要觀看的區間,後端會將選項中的字串轉成 timestamp,然後從資料庫中取出對應溫濕度資料
當使用者想要觀看即時數據,可以再點選 TimeInterval 就可以回去了
完整程式碼附在 §6.2.1,demo 影片如下所示,因為系辦不出借螢幕,所以我們只能用投影幕顯示,造成畫面有點模糊,我們另外放電腦實際顯示的畫面