---
# System prepended metadata

title: PiCord專題筆記

---

# 樹莓派的安裝
## 下載安裝軟體
從[樹莓派官方](https://www.raspberrypi.com/software/)下載軟體，並且用一台可以插SD卡的電腦，將樹莓派的圖形介面灌進SD卡裡。
___
* 安裝時要設定使用者名稱及密碼，他相當於一台小型的電腦，只是多了引腳(GPIO)，可以接電器。
___
## 引腳GPIO
![image](https://hackmd.io/_uploads/SJLa2ExAA.png)
### 引腳控制
```js
import RPi.GPIO as GPIO //匯入RPi.GPIO模組
```

### 設定開機及運行(service)

# Arduino & ESP8266
## 安裝Ardiuno的編譯軟體
從[Arduino官方](https://www.arduino.cc/en/software)下載Arduino的編譯軟體，ESP8266相當於Arduino，只是ESP8266有連接網路的功能。
## 將程序寫入開發板(Arduino or ESP8266)
需要使用連接線將開發板與電腦連接，在編譯軟體中，選擇你的開發板型號
* *Tools > Board*

但ESP8266預設是不在編譯軟體裡的，需要安裝
* *File > Preferences*

在Additional Boards Manager URLs中填入
"http://arduino.esp8266.com/stable/package_esp8266com_index.json"

* *Tools > Board > Boards Manager中搜索ESP8266，然後點擊安裝*

在Tools > Board中選擇ESP8266，並選擇模型，在本專題中選擇使用的是"**NodeMCU LUA Lolin V3 ESP8266**"

# 無線感測器
___
利用ESP8266上傳數據，VPS接收、提供數據，樹莓派獲取數據
___
## ESP8266的代碼
### 透過HTTP POST上傳數據
```c=
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <DHT.h>

#define DHTPIN D2
#define DHTTYPE DHT22

DHT dht(DHTPIN, DHTTYPE);
const char* ssid[] = {"applepie", "CHANG"};
const char* password[] = {"000010000", "063316756"};
const int wifiNetworks = 2;

const char* serverUrl = "http://120.114.142.58:10001/upload.php";

void setup() {
  Serial.begin(115200);
  
  bool connected = false;
  
  for (int i = 0; i < wifiNetworks; i++) {
    Serial.print("try to connect wifi：");
    Serial.println(ssid[i]);

    WiFi.begin(ssid[i], password[i]);
    
    int retries = 0;
    while (WiFi.status() != WL_CONNECTED && retries < 10) {
      delay(1000);
      Serial.print(".");
      retries++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("");
      Serial.print("connect ");
      Serial.println(ssid[i]);
      connected = true;
      break;  // 成功連接後退出循環
    } else {
      Serial.println("");
      Serial.println("connect fail,try next");
    }
  }
  
  if (!connected) {
    Serial.println("can't connect any wifi！");
    return;
  }

  dht.begin();
}

void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    WiFiClient client;
    http.begin(client, serverUrl);

    http.addHeader("Content-Type", "application/x-www-form-urlencoded");

    float temp = dht.readTemperature();
    float humidity = dht.readHumidity();

    if (isnan(temp) || isnan(humidity)) {
      Serial.println("cant read DHT data");
      return;
    }

    String postData = "temperature=" + String(temp) + "&humidity=" + String(humidity);
    int httpResponseCode = http.POST(postData);

    if (httpResponseCode > 0) {
      String response = http.getString();
      Serial.println(httpResponseCode);
      Serial.println(response);
    } else {
      Serial.println("send DHT data wrong");
    }

    http.end();
  }
  
  delay(10000);  // 每10秒發送一次數據
}
```
## VPS的upload.php代碼
### 使用PHP創建一個HTTP服務器
* 服務器會接收、處理數據，然後保存，不借助Nginx之類的服務器軟件，是直接使用PHP的built-in服務器
* 檔案存放在/var/www/html/
* realtime_data記錄十秒一次的數據，並且寫入新的數據時會先清空前一筆數據，避免資料庫過於龐大。
* houly_data紀錄每個整點的數據，並確保只記錄一次，並且每天會刪除七天前的資料。
* 在此建立了兩個函數，用來讓外部設備查詢指定資料。
 ___
 __*day_param：包含日期參數的函數，將會查詢指定日期的所有數據的參數*
 *temp_param：包含溫度參數的函數，將會查詢大於、等於或小於指定溫度的數據*
 *如果不包含任何參數，則傳回最新數據*__
 ___
```php=
<?php
$servername = "1Panel-mysql-lDWX:3306";
$username = "sensordb";
$password = "2nNrGj3ftwAMb8AZ";
$dbname = "sensordb";


$conn = new mysqli($servername, $username, $password, $dbname);


if ($conn->connect_error) {
    die("連接失敗: " . $conn->connect_error);
}

// 檢查請求方法
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
    $time_param = $_GET['time'] ?? null;  //時間參數
    $day_param = $_GET['day'] ?? null;  // 日期參數
    $temp_param = $_GET['temperature'] ?? null;  //溫度參數

    //查詢資料邏輯(函數)
    if ($day_param !== null) {
    $sql = "SELECT * FROM `hourly_data` WHERE `timestamp` LIKE '%$day_param%';";

    $result = $conn->query($sql);

    // 检查是否有结果
    if ($result->num_rows > 0) {
        // 输出数据
        $json = [];
        while($row = $result->fetch_assoc()) {
            $json[] = $row;
        }
        header('Content-Type: application/json');
        echo json_encode($json);
    } else {
        echo "0 results";
    }
        
    } elseif ($temp_param !== null) {
        //查詢大於、等於或小於特定溫度的數據
        $stmt = $conn->prepare("SELECT temperature, humidity, timestamp FROM hourly_data WHERE temperature = ?");
        $stmt->bind_param("d", $temp_param);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows > 0) {
            $data = $result->fetch_all(MYSQLI_ASSOC);
            echo json_encode($data);
        } else {
            echo "未找到符合該溫度的數據";
        }
        $stmt->close();
        
    } else {
        //查詢最新的數據
        $sql = "SELECT temperature, humidity, timestamp FROM realtime_data ORDER BY timestamp DESC LIMIT 1";
        $result = $conn->query($sql);

        if ($result->num_rows > 0) {
            $row = $result->fetch_assoc();
            $data = [
                'temperature' => $row['temperature'],
                'humidity' => $row['humidity'],
                'timestamp' => $row['timestamp']
            ];
            echo json_encode($data);
        } else {
            http_response_code(404);
            echo "無可用數據";
        }
    }

} elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $temperature = $_POST['temperature'] ?? null;
    $humidity = $_POST['humidity'] ?? null;

    if ($temperature !== null && $humidity !== null) {
        //清空 realtime_data 表格
        $sql = "TRUNCATE TABLE realtime_data";
        if ($conn->query($sql) === TRUE) {
            echo "表 realtime_data 已清空<br>";
        } else {
            echo "清空表時發生錯誤: " . $conn->error;
        }

        //儲存新的即時溫濕度數據
        $stmt = $conn->prepare("INSERT INTO realtime_data (temperature, humidity) VALUES (?, ?)");
        $stmt->bind_param("dd", $temperature, $humidity);
        $stmt->execute();
        $stmt->close();
        echo "實時溫濕度已儲存<br>";

        //獲取當前時間信息
        $currentMinute = (int)date('i'); //目前分鐘
        $currentHour = (int)date('H');   //目前小時

        //檢查是否為整點，且這個小時沒有記錄過數據
        $sql = "SELECT COUNT(*) as count FROM hourly_data WHERE hour = ? AND DATE(timestamp) = CURDATE()";
        $stmt = $conn->prepare($sql);
        $stmt->bind_param("i", $currentHour);
        $stmt->execute();
        $result = $stmt->get_result();
        $row = $result->fetch_assoc();
        $alreadyRecorded = $row['count'] > 0;
        $stmt->close();

        if ($currentMinute === 0 && !$alreadyRecorded) {
            //在整點時儲存溫濕度資料（並確保每小時只記錄一次）
            $stmt = $conn->prepare("INSERT INTO hourly_data (temperature, humidity, hour) VALUES (?, ?, ?)");
            $stmt->bind_param("ddi", $temperature, $humidity, $currentHour);
            $stmt->execute();
            $stmt->close();
            echo "整點溫濕度已儲存";
        } else {
            echo "即時溫濕度已更新";
        }
    } else {
        http_response_code(400);
        echo "無效數據";
    }
}


//關閉 MySQL 連接
$conn->close();
?>
```
### 在VPS上運行PHP(目前不使用這個方法)
先到存放upload.php的資料夾內
```
cd /var/www/html/
```
啟動php服務器
```
php -S 0.0.0.0:8090
```
持續運行服務器
```
nohub php -S 0.0.0.0:8090 &
```
### 檢查端口是否可用
```
curl http://120.114.142.58:10001/upload.php
```
## 用於登入的login.php代碼
```php=
<?php
header('Content-Type: application/json');

// 資料庫連接參數
$servername = "";  
$username = "";       
$password = "";
$dbname = "";              

// 建立資料庫連接
$conn = new mysqli($servername, $username, $password, $dbname);

// 檢查連接是否成功
if ($conn->connect_error) {
    die(json_encode(['status' => 'error', 'message' => '連接失敗: ' . $conn->connect_error]));
}

// 獲取從前端傳來的資料
$data = json_decode(file_get_contents("php://input"), true);
$account = $data['account'];
$password = $data['password'];

// 驗證帳號與密碼
$sql = "SELECT * FROM fan_user WHERE account = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $account);
$stmt->execute();
$result = $stmt->get_result();

// 檢查用戶是否存在
if ($result->num_rows > 0) {
    $user = $result->fetch_assoc();
    // 檢查密碼是否正確
    if (password_verify($password, $user['password'])) {
        echo json_encode(['status' => 'success', 'message' => 'Login successful']);
    } else {
        echo json_encode(['status' => 'error', 'message' => 'Incorrect password']);
    }
} else {
    echo json_encode(['status' => 'error', 'message' => 'User not found']);
}

$stmt->close();
$conn->close();
?>
```
## 網頁的代碼
### html的代碼
```html=
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>溫度查詢與風扇控制</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
</head>
<body>
    <div class="container">
        <div class="header-bar">
            <h2>溫度查詢與風扇控制</h2><br />
            <p id="user-status"></p>
        </div>

        <div class="temperature-display">
            <h2>實時溫度</h2>
            <p id="temperature">正在獲取數據...</p>
        </div>

        <div class="dropdown-container">
            <label for="history-dropdown">歷史溫度查詢：</label>
            <select id="history-dropdown" class="history-dropdown" onchange="fetchHistoricalData()">
                <option value="0">今天</option>
                <option value="1">一天前</option>
                <option value="2">兩天前</option>
                <option value="3">三天前</option>
                <option value="4">四天前</option>
                <option value="5">五天前</option>
                <option value="6">六天前</option>
            </select>
        </div>
        
        <div class="switch-container">
            <label class="switch">
                <input type="checkbox" id="fan-switch" onclick="toggleFan()">
                <span class="slider"></span>
            </label>
            <span>風扇開關</span>
        </div>

        <div class="switch-container">
            <label class="switch">
                <input type="checkbox" id="auto-fan-switch" onclick="autoFanToggle()">
                <span class="slider"></span>
            </label>
            <span>自動風扇開關</span>
        </div>

        <!-- 登入 -->
        <button id="login-button" onclick="toggleLogin()">登入</button><br>
        <p id="user-status"></p>

        <footer class="footer">
            <p>© 2024 Picord. All Rights Reserved</p>
            <p>
                <a href="#" class="footer-link">隱私權政策</a> |
                <a href="#" class="footer-link">服務條款</a> |
                <a href="#" class="footer-link">聯絡我們</a>
            </p>
        </footer> 
    </div>

    

    <!-- 圖表覆蓋層 -->
    <div id="chart-overlay" class="overlay">
        <div class="chart-container">
            <button id="close-button" onclick="closeChartOverlay()">×</button>
            <canvas id="history-chart"></canvas>
            <button id="save-button" class="save-button" onclick="saveChart()">儲存圖片</button>
        </div>
    </div>

    <!-- 登入覆蓋層 -->
    <div id="login-modal" class="overlay" style="display: none;">
        <div class="login-container">
            <h2>登入</h2>
            <label for="login-account">帳號：</label>
            <input type="text" id="login-account" placeholder="請輸入帳號">
            <br>
            <label for="login-password">密碼：</label>
            <input type="password" id="login-password" placeholder="請輸入密碼">
            <br>
            <button id="login2-button" onclick="login()">登入</button>
            <button id="cancel-button" onclick="closeLoginModal()">取消</button>
        </div>
    </div>    
</div>
    
    <script src="script.js"></script>
</body>
</html>

```
### js的代碼
```javascript=
// 登入功能
let isLoggedIn = false;

// 在頁面加載時檢查 cookie
window.onload = function() {
    const cookies = document.cookie.split('; ');
    const usernameCookie = cookies.find(row => row.startsWith('username='));

    if (usernameCookie) {
        const userAccount = usernameCookie.split('=')[1];
        isLoggedIn = true;
        document.getElementById("login-button").textContent = "登出";
        document.getElementById("user-status").textContent = "登入身分：管理者"; // 顯示用戶名
    }
    fetchLatestTemperature();
    // 預設不顯示隱私權政策和服務條款覆蓋層
    document.getElementById("privacy-overlay").style.display = "none";
    document.getElementById("tos-overlay").style.display = "none";
    setInterval(fetchLatestTemperature, 10000); // 每10秒更新最新溫度
};

// 發送查詢最新溫度的 API 請求
function fetchLatestTemperature() {
    let url = "https://picord.vbird.tw/upload.php";

    fetch(url, { method: 'GET' })
        .then(response => {
            if (!response.ok) {
                throw new Error("Network response was not ok");
            }
            return response.json();
        })
        .then(data => {
            if (data && data.temperature) {
                document.getElementById('temperature').innerText = 
                    `溫度：${data.temperature}°C
                    濕度：${data.humidity}%
                    時間：${data.timestamp}`;
            } else {
                document.getElementById('temperature').innerText = "未能獲取到最新的溫度數據";
            }
        })
        .catch(error => {
            console.error("Error occurred:", error);
            document.getElementById('temperature').innerText = "更新中...請稍後";
        });
}

// 發送查詢指定日期歷史溫度的 API 請求
function fetchHistoricalData() {
    const dropdown = document.getElementById("history-dropdown");
    const daysAgo = dropdown.value;

    const today = new Date();
    const targetDate = new Date(today);
    targetDate.setDate(today.getDate() - daysAgo);

    const day_param = targetDate.toISOString().split('T')[0];
    console.log("Selected day:", day_param);

    let url = `https://picord.vbird.tw/upload.php?day=${day_param}`;
    console.log("Request URL:", url);

    fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error("Network response was not ok");
            }
            return response.json();
        })
        .then(data => {
            console.log("Received data:", data);
            if (data && data.length > 0) {
                displayChartOverlay(data, day_param);
            } else {
                alert("未能獲取到歷史數據");
            }
        })
        .catch(error => {
            console.error("Error occurred:", error);
            alert("無法獲取歷史數據");
        });
}

// 歷史數據圖表
function displayChartOverlay(data, day_param) {
    const timestamps = [];
    const temperatures = [];
    const humidities = [];

    data.forEach(entry => {
        if (entry.timestamp && entry.temperature && entry.humidity) {
            timestamps.push(entry.timestamp);
            temperatures.push(entry.temperature);
            humidities.push(entry.humidity);
        } else {
            console.error("數據格式錯誤：", entry);
        }
    });

    document.getElementById('chart-overlay').style.display = 'block';
    const ctx = document.getElementById('history-chart').getContext('2d');

    if (!ctx) {
        console.error("無法獲取 canvas 的上下文，請檢查元素 id 是否正確。");
        return;
    }

    if (window.myChart) {
        window.myChart.destroy();
    }

    window.myChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: timestamps,
            datasets: [
                {
                    label: '溫度',
                    data: temperatures,
                    borderColor: 'red',
                    fill: false
                },
                {
                    label: '濕度',
                    data: humidities,
                    borderColor: 'blue',
                    fill: false
                }
            ]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                title: {
                    display: true,
                    text: `${day_param}的溫度濕度變化表`,
                    font: { size: 25 }
                }
            },
            scales: {
                x: {
                    type: 'time',
                    time: {
                        unit: 'hour',
                        tooltipFormat: 'YYYY-MM-DD HH:mm',
                        displayFormats: { hour: 'HH:mm' }
                    },
                    title: {
                        display: true,
                        text: '時間 (小時)',
                        font: { size: 18 }
                    }
                },
                y: {
                    title: {
                        display: true,
                        text: '數值',
                        font: { size: 18 }
                    }
                }
            }
        }
    });
}

function closeChartOverlay() {
    const chartOverlay = document.getElementById("chart-overlay");
    chartOverlay.style.display = "none";
}

function saveChart() {
    const link = document.createElement('a');
    const canvas = document.getElementById('history-chart');
    link.href = canvas.toDataURL('image/png');
    link.download = 'temperature_plot.png';
    link.click();
}

// 開啟登入窗口
function openLoginModal() {
    document.getElementById('login-modal').style.display = 'block';
}

// 關閉登入窗口
function closeLoginModal() {
    document.getElementById('login-modal').style.display = 'none';
}

function login() {
    const account = document.getElementById('login-account').value;
    const password = document.getElementById('login-password').value;

    if (account && password) {
        console.log("開始發送登入請求...");
        fetch('https://picord.vbird.tw/login.php', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ account: account, password: password })
        })
        .then(response => {
            console.log("收到響應，狀態碼:", response.status);
            return response.json();
        })
        .then(data => {
            console.log("響應資料:", data);
            if (data.status === 'success') {
                isLoggedIn = true;

                document.cookie = `username=${account}; path=/; max-age=${60 * 60}`;
                document.getElementById("login-button").textContent = "登出";
                document.getElementById("user-status").textContent = "登入身分：管理者"; // 可以顯示用戶名
                closeLoginModal();
                alert("登入成功！");
            } else {
                alert("帳號或密碼錯誤");
            }
        })
        .catch(error => {
            console.error("Error:", error);
        });
    } else {
        alert("請輸入帳號和密碼！");
    }
}

// 登出功能
function logout() {
    if (!isLoggedIn) {
        openLoginModal();
    } else {
        isLoggedIn = false;
        document.getElementById("login-button").textContent = "登入";
        document.getElementById("user-status").textContent = "";

        document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
        alert("已登出！");
    }
}

// 切換登入和登出功能
function toggleLogin() {
    if (isLoggedIn) {
        logout();
    } else {
        openLoginModal(); 
    }
}

// 風扇開關控制
function toggleFan() {
    if (!isLoggedIn) {
        alert("請先登入，才能操作風扇開關。");
        document.getElementById("fan-switch").checked = false;
        return;
    }

    console.log("風扇開關已被觸發");
}

// 自動風扇開關控制
function autoFanToggle() {
    if (!isLoggedIn) {
        alert("請先登入，才能操作自動風扇開關。");
        document.getElementById("auto-fan-switch").checked = false;
        return;
    }

    console.log("自動風扇開關已被觸發");
}

// 顯示隱私權政策覆蓋層
function openPrivacyModal() {
    document.getElementById('privacy-overlay').style.display = 'flex';
}

// 關閉隱私權政策覆蓋層
function closePrivacyModal() {
    document.getElementById('privacy-overlay').style.display = 'none';
}

// 顯示服務條款覆蓋層
function openTosModal() {
    document.getElementById('tos-overlay').style.display = 'flex';
}

// 關閉服務條款覆蓋層
function closeTosModal() {
    document.getElementById('tos-overlay').style.display = 'none';
}
```
### css的代碼
```css=
body {
    font-family: Arial, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f4f4f4;
    margin: 0;
}

.header-bar {
    background-color: #0063b5;
    color: white;
    padding: 10px;
    border-radius: 5px;
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;     
    justify-content: center;  
    align-items: center;
    height: 70px;  
}

.header-bar h2 {
    margin: 0;
    font-size: 30px;
}

.header-bar p {
    margin: 0;
    font-size: 16px;
    color: white;  
}

.container {
    text-align: center;
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

h1 {
    color: #333;
}

.button {
    display: inline-block;
    padding: 10px 20px;
    margin: 10px;
    background-color: #007BFF;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
}

.button:hover {
    background-color: #0056b3;
}

.switch-container {
    margin: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.switch {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
}

.switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    transition: 0.4s;
    border-radius: 34px;
}

.slider:before {
    position: absolute;
    content: "";
    height: 26px;
    width: 26px;
    left: 4px;
    bottom: 4px;
    background-color: white;
    transition: 0.4s;
    border-radius: 50%;
}

input:checked + .slider {
    background-color: #007BFF;
}

input:checked + .slider:before {
    transform: translateX(26px);
}

#chart-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8);
    display: none;
    justify-content: center;
    align-items: center;
}

/*.chart-container {
    position: relative;
    background: white;
    padding: 20px;
    border-radius: 10px;
    width: 100%; 
    max-width: 800px;
    height: auto;
    margin: auto;
}*/

#history-chart {
    width: 100%;
    height: 70%;
}

#close-button {
    position: absolute;
    top: 10px;
    right: 10px;
    background-color: red;
    color: white;
    border: none;
    padding: 10px;
    border-radius: 5px;
    cursor: pointer;
}

#close-button:hover {
    background-color: darkred;
}

.overlay {
    display: none; /* 預設隱藏 */
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.8); /* 半透明背景 */
    z-index: 1000;
}

.chart-container {
    padding: 30px;
    border-radius: 10px;
    position: relative;
    background: white;
    width: 80%;
    max-width: 1000px;
    margin: auto;
    top: 50%;
    transform: translateY(-50%);
}


/*下拉選單*/
.dropdown-container {
    margin: 20px 0;
}

.history-dropdown {
    text-align: left; /* 文字左對齊 */
    padding: 10px 20px;
    border: 2px solid #007BFF; /* 邊框顏色 */
    border-radius: 5px; /* 邊框圓角 */
    background-color: #FFFFFF; /* 背景顏色 */
    color: #333333; /* 字體顏色 */
    font-size: 15px; /* 字體大小 */
    appearance: none; /* 去掉預設的下拉箭頭 */
    background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23007BFF"%3E%3Cpath d="M7 10l5 5 5-5z"/%3E%3C/svg%3E'); /* 自定義下拉箭頭 */
    background-repeat: no-repeat;
    background-position: right 0px center; /* 自定義下拉箭頭位置 */
    background-size: 18px; /* 自定義下拉箭頭大小 */
}

.history-dropdown:focus {
    outline: none; /* 去掉焦點邊框 */
    border-color: #0056b3; /* 聚焦時的邊框顏色 */
    box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* 聚焦時的陰影效果 */
}


/*儲存按鈕*/
#save-button {
    background-color: #4CAF50; /* 背景顏色 */
    color: white; /* 字體顏色 */
    border: none; /* 去掉邊框 */
    border-radius: 5px; /* 圓角 */
    padding: 10px 15px; /* 內邊距 */
    font-size: 16px; /* 字體大小 */
    cursor: pointer; /* 鼠標懸停效果 */
    position: absolute; /* 絕對定位 */
    bottom: 10px; /* 距離底部20px */
    right: 20px; /* 距離左邊20px */
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 陰影效果 */
    transition: background-color 0.3s, transform 0.2s; /* 動畫效果 */
}

#save-button:hover {
    background-color: #45a049; /* 懸停時的背景顏色 */
    transform: scale(1.05); /* 懸停時稍微放大 */
}

#save-button:focus {
    outline: none; /* 去掉焦點邊框 */
}

/*實時溫度*/
.temperature-display {
    background-color: #f0f8ff; /* 淺藍色背景 */
    border: 2px solid #b0e0e6; /* 邊框顏色 */
    border-radius: 8px; /* 圓角邊框 */
    padding: 20px; /* 內邊距 */
    margin: 20px 0; /* 上下外邊距 */
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 陰影效果 */
    text-align: center; /* 文字置中 */
}

.temperature-display h2 {
    font-size: 24px; /* 標題字體大小 */
    color: #333; /* 標題字體顏色 */
    margin-bottom: 5px; /* 標題與段落間距 */
}

.temperature-display p {
    font-size: 18px; /* 段落字體大小 */
    color: #555; /* 段落字體顏色 */
}

canvas {
    max-width: 100%;
    height: auto;
}


/*叉叉*/
#close-button {
    background-color: #f44336; /* 背景顏色 */
    color: white; /* 字體顏色 */
    border: none; /* 去掉邊框 */
    border-radius: 50%; /* 圓形 */
    width: 40px; /* 寬度 */
    height: 40px; /* 高度 */
    font-size: 24px; /* 字體大小 */
    line-height: 20px; /* 使叉叉垂直居中 */
    cursor: pointer; /* 鼠標懸停效果 */
    position: absolute; /* 使其浮動在右上角 */
    top: 10px; /* 位置調整 */
    right: 10px; /* 位置調整 */
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 陰影效果 */
    transition: background-color 0.3s, transform 0.2s; /* 動畫效果 */
}

#close-button:hover {
    background-color: #d32f2f; /* 懸停時的背景顏色 */
    transform: scale(1.1); /* 懸停時稍微放大 */
}

#close-button:focus {
    outline: none; /* 去掉焦點邊框 */
}

/*登入按鈕*/
#login-button {
    background-color: #4CAF50; /* 背景顏色 */
    color: white; /* 字體顏色 */
    border: none; /* 去掉邊框 */
    border-radius: 5px; /* 圓角 */
    padding: 10px 15px; /* 內邊距 */
    font-size: 16px; /* 字體大小 */
    cursor: pointer; /* 鼠標懸停效果 */
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 陰影效果 */
    transition: background-color 0.3s, transform 0.2s; /* 動畫效果 */
    margin-top: 10px; /* 上方間距 */
}

#login-button:hover {
    background-color: #45a049; /* 懸停時的背景顏色 */
    transform: scale(1.05); /* 懸停時稍微放大 */
}

#login-button:focus {
    outline: none; /* 去掉焦點邊框 */
    box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* 聚焦時的陰影效果 */
}


/*登入覆蓋頁*/
.login-container {
    background-color: #fff; /* 背景顏色 */
    padding: 30px; /* 內邊距 */
    border-radius: 10px; /* 圓角 */
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); /* 陰影效果 */
    width: 300px; /* 固定寬度 */
    text-align: center; /* 文字置中 */
    position: absolute; /* 使用絕對定位 */
    top: 50%; /* 垂直居中 */
    left: 50%; /* 水平居中 */
    transform: translate(-50%, -50%); /* 使容器真正居中 */
}

.login-container h2 {
    margin-bottom: 20px; /* 標題下方間距 */
    color: #333; /* 標題顏色 */
}

.login-container label {
    display: block; /* 每個標籤佔一行 */
    margin: 10px 0 5px; /* 上方和下方間距 */
    color: #666; /* 標籤顏色 */
}

.login-container input {
    width: 100%; /* 寬度100% */
    padding: 10px; /* 內邊距 */
    border: 2px solid #007BFF; /* 邊框顏色 */
    border-radius: 5px; /* 圓角 */
    margin-bottom: 15px; /* 下方間距 */
    box-sizing: border-box; /* 包括邊框和內邊距 */
}

#login2-button, #cancel-button {
    background-color: #007BFF; /* 背景顏色 */
    color: white; /* 字體顏色 */
    border: none; /* 去掉邊框 */
    border-radius: 5px; /* 圓角 */
    padding: 10px; /* 內邊距 */
    font-size: 16px; /* 字體大小 */
    cursor: pointer; /* 鼠標懸停效果 */
    margin: 5px; /* 按鈕間距 */
    transition: background-color 0.3s; /* 背景色變化效果 */
}

#login2-button:hover {
    background-color: #0056b3; /* 懸停時的顏色 */
}

#cancel-button {
    background-color: #f44336; /* 取消按鈕背景顏色 */
}

#cancel-button:hover {
    background-color: #d32f2f; /* 懸停時的顏色 */
}


/*商標*/
.footer {
    text-align: center; /* 內容置中 */
    margin-top: 0px; /* 上邊距 */
    padding: 10px; /* 內邊距 */
    background-color: #f4f4f4; /* 淡灰色背景 */
    color: #555555; /* 字體顏色 */
    border-top: 1px solid #ddd; /* 上邊框 */
    font-size: 14px; /* 字體大小 */
}

.footer p {
    margin: 5px 0; /* 段落間距 */
    font-size: 14px; /* 段落字體大小 */
}

.footer-link {
    text-decoration: none; /* 去掉下劃線 */
    color: #007BFF; /* 藍色文字 */
    transition: color 0.3s; /* 平滑過渡 */
    font-size: 14px; /* 連結字體大小 */
}

.footer-link:hover {
    color: #0056b3; /* 懸停時顏色 */
}


/*不同裝置*/
@media (max-width: 600px) {
    .chart-container {
        height: 300px; /* 小螢幕時的高度 */
    }
}

@media (max-width: 600px) {
    .temperature-display h2 {
        font-size: 20px; /* 縮小字體 */
    }

    .temperature-display p {
        font-size: 16px; /* 縮小字體 */
    }
}

```
## 樹莓派的代碼
### 利用HTTP GET的方式從VPS得到數據
```python=
import discord
from discord.ext import commands
from discord import app_commands
from discord.ui import View, Select
import requests
import time
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import asyncio
import RPi.GPIO as GPIO
import numpy as np
from io import BytesIO

class MissingRoleError(app_commands.AppCommandError):
    pass

class FanControl(commands.Cog):
    def __init__(self, bot):
        self.auto_fan_task = None
        self.bot = bot
        self.LED_PIN = 16
        self.last_on_time = 0
        self.last_off_time = 0
        self.auto_fan_timeout = 1800
        self.base_url = "http://120.114.142.58:10001/upload.php"

        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.LED_PIN, GPIO.OUT)

    def gpio_on(self):
        GPIO.output(self.LED_PIN, GPIO.HIGH)
        self.last_on_time = time.time()
        return "風扇已成功開啟。"

    def gpio_off(self):
        GPIO.output(self.LED_PIN, GPIO.LOW)
        self.last_off_time = time.time()
        return "風扇已成功關閉。"

    
    #異步
    async def fan_control_loop(self):
        """異步循環，根據溫度自動控制風扇。"""
        while True:
            response = requests.get(self.base_url)
            data = response.json()
            temperature = data.get("temperature")
            current_time = time.time()

            if temperature > 28:
                if current_time - self.last_on_time >= self.auto_fan_timeout:
                    self.gpio_on()
            else:
                if current_time - self.last_off_time >= self.auto_fan_timeout:
                    self.gpio_off()

            await asyncio.sleep(60)

    @app_commands.command(name="自動風扇", description="根據溫度自動控制風扇。")
    async def auto_fan(self, interaction: discord.Interaction):
        if self.auto_fan_task is None or self.auto_fan_task.done():
            self.auto_fan_task = asyncio.create_task(self.fan_control_loop())
            await interaction.response.send_message("自動風扇已啟動，將每分鐘檢查一次溫度。")
        else:
            await interaction.response.send_message("自動風扇已經在運行中。")

    @app_commands.command(name="停止自動風扇", description="停止自動風扇的控制循環。")
    async def stop_auto_fan(self, interaction: discord.Interaction):
        if self.auto_fan_task and not self.auto_fan_task.done():
            self.auto_fan_task.cancel()
            await interaction.response.send_message("自動風扇已停止。")
        else:
            await interaction.response.send_message("自動風扇未在運行。")

    @app_commands.command(name="風扇開啟", description="開啟風扇。")
    async def fan_on(self, interaction: discord.Interaction):
        result = self.gpio_on()
        await interaction.response.send_message(result)

    @app_commands.command(name="風扇關閉", description="關閉風扇。")
    async def fan_off(self, interaction: discord.Interaction):
        result = self.gpio_off()
        await interaction.response.send_message(result)

    @app_commands.command(name="查詢溫度", description="獲取最新的溫度和濕度。")
    async def temp(self, interaction: discord.Interaction):
        response = requests.get(self.base_url)
        data = response.json()
        temperature = data.get("temperature")
        humidity = data.get("humidity")
        await interaction.response.send_message(f"溫度: {temperature}°C, 濕度: {humidity}%")


    @app_commands.command(name="歷史溫度", description="查詢歷史溫度。")
    async def history_temp(self, interaction: discord.Interaction):

        embed = discord.Embed(title="歷史溫度查詢", description="請選擇查詢歷史溫度的某一天。")

        # 創建下拉選單
        select = Select(
            placeholder="選擇一個日期...",
            options=[
                discord.SelectOption(label="今天", description="查看今天的歷史數據", value="0"),
                discord.SelectOption(label="一天前", description="查看一天前的歷史數據", value="1"),
                discord.SelectOption(label="兩天前", description="查看兩天前的歷史數據", value="2"),
                discord.SelectOption(label="三天前", description="查看三天前的歷史數據", value="3"),
                discord.SelectOption(label="四天前", description="查看四天前的歷史數據", value="4"),
                discord.SelectOption(label="五天前", description="查看五天前的歷史數據", value="5"),
                discord.SelectOption(label="六天前", description="查看六天前的歷史數據", value="6")
            ]
        )

        async def select_callback(interaction: discord.Interaction):
            
            days_ago = int(select.values[0])
            start_time, end_time = self.get_day_timespan(days_ago)

            # 獲取圖像並發送給用戶
            plot = await self.generate_plot(start_time, end_time)
            await interaction.response.send_message(file=discord.File(plot, 'temperature_plot.png'))

        select.callback = select_callback

        view = View()
        view.add_item(select)

        await interaction.response.send_message(embed=embed, view=view)

    # 用於生成圖表的函數
    async def generate_plot(self, start_time, end_time):
        url = f"{self.base_url}?start={start_time}&end={end_time}"
        response = requests.get(url)

        if response.status_code == 200:
            try:
                data = response.json()
            except ValueError:
                raise Exception("无效的 JSON 响应")
            
            if isinstance(data, list):
                timestamps = [entry['timestamp'] for entry in data]
                temperatures = [entry['temperature'] for entry in data]
                humidities = [entry['humidity'] for entry in data]

                # 繪製圖表
                plt.figure(figsize=(10, 5))
                plt.plot(timestamps, temperatures, label='溫度', color='red')
                plt.plot(timestamps, humidities, label='濕度', color='blue')

                plt.title('溫度和濕度隨時間的變化')
                plt.xlabel('時間')
                plt.ylabel('數值')
                plt.legend()

                buf = BytesIO()
                plt.savefig(buf, format='png')
                buf.seek(0)
                return buf
            else:
                raise Exception("非有效列表")
        else:
            raise Exception(f"無法從資料庫獲取數據，狀態碼: {response.status_code}")

    # 根據選擇的天數返回該天的時間範圍
    def get_day_timespan(self, days_ago):
        
        today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
    
        start_time = today - timedelta(days=days_ago)
        end_time = start_time + timedelta(days=1) - timedelta(seconds=1)
    
        return int(start_time.timestamp()), int(end_time.timestamp())

# Bot setup
async def setup(bot):
    await bot.add_cog(FanControl(bot))

```