ESP32迷你氣象站:DHT11 + 0.96 OLED,網頁監看版

ESP32 氣象站聯網升級!手機瀏覽器即時監控,AJAX 技術讓數據秒跳不閃爍。第一次讓ESP32連上網,新手也能輕鬆打造專業 IoT 神器!

ESP32迷你氣象站:DHT11 + 0.96 OLED,網頁監看版

在上一篇文章中,我們成功做出了一個擁有專業介面、排版精美的「離線版」桌面溫濕度計。雖然它放在桌上很好看,但如果我躺在床上,或者在另一個房間,想知道現在室溫幾度怎麼辦?

這就是 IoT (物聯網) 發揮作用的時候了!

今天我們要幫 ESP32 裝上翅膀(連上 WiFi),讓它變身為一個迷你網頁伺服器 (Web Server)。你不用安裝任何 App,只要打開手機瀏覽器,就能看到即時的溫濕度數據,而且還會用到 AJAX 技術,讓網頁數據自動跳動更新,不會整頁閃爍!

有關接線和基本的DHT11和OLED的處理,這個章節就不重複說明了,建議大家先看過上一篇離線版,再接下去看哦!

ESP32迷你氣象站:DHT11 溫濕度 + 0.96 吋 OLED 顯示(離線版)
教你用 ESP32 打造「離線版」迷你氣象站。完美呈現經典雙色 OLED 介面,免設網路、隨插即用。內附超詳盡註解,新手也能輕鬆成功,快來動手 DIY 吧!

🚀 這次的升級重點

  1. OLED 介面進化:頂部原本顯示網址的地方,改為自動顯示 本機 IP 位址,方便你一眼就知道網頁要連哪裡。
  2. 手機網頁監控:打開瀏覽器輸入 IP,即時查看溫濕度。
  3. AJAX 無刷新技術:數據每 2 秒自動更新,畫面滑順不閃爍。
  4. 非阻塞程式設計:拋棄 delay(),改用 millis(),這是寫 IoT 程式最重要的觀念!

🛠️ 程式碼解析

請將以下程式碼複製到 Arduino IDE 中。

/*
 * ============================================================
 * 專題:ESP32 溫濕度 IoT 氣象站 (Web AJAX 版)
 * 作者:傑森創工 JMAKER WORKSHOP
 * ============================================================
 * 功能說明:
 * 1. [OLED] 維持離線版的排版,頂部改為顯示動態 IP 位址。
 * 2. [Web]  建立網頁伺服器,手機可直接查看數據。
 * 3. [AJAX] 網頁透過 JavaScript 背景抓取數據,畫面不閃爍。
 * * 硬體接線:
 * - DHT11 Data -> GPIO 4
 * - OLED SCL   -> GPIO 22
 * - OLED SDA   -> GPIO 21
 */

#include <WiFi.h>               // ESP32 的 WiFi 功能庫
#include <WebServer.h>          // 用來建立網頁伺服器的庫
#include <Wire.h>               // I2C 通訊庫
#include <Adafruit_GFX.h>       // 繪圖核心庫
#include <Adafruit_SSD1306.h>   // OLED 驅動庫
#include <DHT.h>                // 溫濕度感測器庫

// ========== 1. 使用者設定區 (請修改密碼) ==========
const char* ssid     = "your_ssid";         // 您的 WiFi 名稱
const char* password = "your_password";  // ★請在此填入您的 WiFi 密碼★

// ========== 2. 硬體參數設定 ==========
#define DHTPIN 4        // DHT11 接腳
#define DHTTYPE DHT11   // 感測器型號,DHT11或DHT222

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDR 0x3C // OLED 位址

// 建立物件:分別代表 OLED、DHT 感測器、以及網頁伺服器(Port 80)
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHTPIN, DHTTYPE);
WebServer server(80); 

// 全域變數:用來儲存溫濕度,讓網頁與 OLED 都能讀取同一個數據
float humidity = 0;
float temperature = 0;

// 計時器變數:這是用來取代 delay() 的關鍵
unsigned long previousMillis = 0;
const long interval = 2000; // 設定每 2000ms (2秒) 更新一次

// ==================================================
// 3. 網頁內容設計 (HTML + CSS + JavaScript)
// 使用 PROGMEM 將這串長字串存放在快閃記憶體,節省 RAM 空間
// ==================================================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <title>ESP32 氣象站</title>
  <style>
    /* 簡單的 CSS 美化,讓網頁看起來像一張卡片 */
    body { font-family: Arial; text-align: center; margin-top: 50px; background-color: #f4f4f4; }
    h1 { color: #333; }
    .card { background: white; max-width: 400px; margin: 0 auto; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
    .data { font-size: 24px; color: #007BFF; margin: 20px 0; }
    .footer { margin-top: 30px; font-size: 12px; color: #888; }
  </style>
</head>
<body>
  <div class="card">
    <h1>🏠 目前環境</h1>
    
    <div class="data">🌡️ 溫度: <b id="temp">--</b> &deg;C</div>
    <div class="data">💧 濕度: <b id="humi">--</b> %</div>
    
    <div class="footer">
      <hr>
      <p>傑森創工 JMAKER WORKSHOP</p>
    </div>
  </div>

  <script>
    // --- AJAX 核心程式碼 ---
    // 設定計時器:每 2000 毫秒執行一次 getData()
    setInterval(function() {
      getData();
    }, 2000); 

    function getData() {
      // fetch 指令:偷偷向 ESP32 的 "/data" 網址要資料
      fetch("/data")
        .then(response => response.json()) // 把拿到的資料轉成 JSON 格式
        .then(obj => {
          // 成功拿到資料後,更新網頁上的數字
          document.getElementById("temp").innerHTML = obj.temperature.toFixed(1);
          document.getElementById("humi").innerHTML = obj.humidity.toFixed(0);
        })
        .catch(error => console.log('Error:', error)); // 發生錯誤時印在 Console
    }
  </script>
</body>
</html>
)rawliteral";

// ==================================================
// 4. 伺服器處理函式 (Backend)
// ==================================================

// 當瀏覽器輸入 IP 連進來時,回傳上面的 HTML 網頁
void handleRoot() {
  server.send(200, "text/html", index_html);
}

// 當 AJAX 偷偷來要資料時,回傳純文字數據 (JSON 格式)
void handleData() {
  // 組合 JSON 字串,例如: {"temperature":25.5,"humidity":60}
  String json = "{";
  json += "\"temperature\":" + String(temperature) + ",";
  json += "\"humidity\":" + String(humidity);
  json += "}";
  server.send(200, "application/json", json);
}

// ==================================================
// setup() 初始化設定
// ==================================================
void setup() {
  Serial.begin(115200);
  dht.begin();

  // 啟動 OLED
  if (!oled.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDR)) {
    Serial.println(F("OLED Fail"));
    for (;;);
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  
  // --- 畫面 1: 連線中提示 ---
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.println("Connecting WiFi...");
  oled.display();
  
  // 開始連線 WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("."); // 在序列埠印出點點點
  }
  // 連線成功,印出 IP
  Serial.println("\nIP: " + WiFi.localIP().toString());

  // --- 設定網頁路徑 ---
  server.on("/", handleRoot);    // 首頁
  server.on("/data", handleData); // 數據接口
  server.begin();                // 啟動伺服器
  Serial.println("Server started");
}

// ==================================================
// loop() 主程式 (不使用 delay)
// ==================================================
void loop() {
  // 1. 處理網頁請求 (這行最重要,隨時監聽有沒有人連進來)
  server.handleClient();

  // 2. 檢查時間 (millis 取代 delay)
  // 只有當「目前時間 - 上次時間」超過 2000ms 時,才更新數據
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis; // 更新打卡時間

    // 讀取感測器
    float newH = dht.readHumidity();
    float newT = dht.readTemperature();
    
    // 檢查數據是否有效,有效才更新變數
    if (!isnan(newH) && !isnan(newT)) {
      humidity = newH;
      temperature = newT;
    }

    // --- 更新 OLED 畫面 (延續之前的排版) ---
    oled.clearDisplay();

    // [區域 A] 頂部 IP 欄 (白底黑字 + 自動置中)
    oled.fillRect(0, 0, 128, 16, SSD1306_WHITE);      // 畫白色背景
    oled.setTextColor(SSD1306_BLACK, SSD1306_WHITE);  // 設為反白字色
    oled.setTextSize(1);
    
    String ipStr = WiFi.localIP().toString();         // 取得 IP 字串
    int ipX = (128 - ipStr.length() * 6) / 2;         // 算出置中 X 座標
    oled.setCursor(ipX, 4);
    oled.print(ipStr);

    oled.setTextColor(SSD1306_WHITE); // 改回正常白字,準備畫下面

    // [區域 B] 左側溫度 (Y=30 下移優化版)
    oled.setTextSize(3);
    oled.setCursor(4, 30);
    oled.print((int)temperature);

    oled.setTextSize(2);
    oled.setCursor(42, 36); 
    oled.write(247); // 度數符號
    oled.print(F("C"));

    // [區域 C] 右側濕度 (X=72 分隔線, X=82 數據)
    oled.drawFastVLine(72, 18, 44, SSD1306_WHITE);

    oled.setTextSize(1);
    oled.setCursor(82, 27);
    oled.print(F("HUMID"));

    oled.setTextSize(2);
    oled.setCursor(82, 37);
    oled.print((int)humidity);
    oled.print(F("%"));

    oled.display(); // 送出畫面
  }
}

這支程式基本上就是在離線版中,加上了ESP32最重要的網路功能,讓我們可以透過瀏覽器,看到和OLED上相同的內容。程式碼中傑森已詳細備註了說明,以下把幾個重點提出來跟大家說明一下。

🧐 深入淺出:WebServer 運作原理

在這次的程式中,最核心的新功能就是 WebServer。這讓 ESP32 不再只是被動顯示資料,而是能夠與外界溝通。我們可以把它拆解成三個步驟來理解:

1. 建立通訊埠 (Create Server)

程式碼中的 WebServer server(80); 是整個網站功能的起點。

  • Port 80:這是網際網路通用的「網頁連接埠」。當你在瀏覽器輸入 IP 時,瀏覽器預設就會去敲這個 80 號大門。這行指令等於告訴 ESP32:「請打開 80 號門,準備接待看網頁的客人」。

2. 定義路徑規則 (Routing)

setup() 裡,我們設定了兩個「路徑」,這決定了瀏覽器「請求 (Request)」不同網址時,ESP32 該給出什麼「回應 (Response)」:

  • server.on("/", handleRoot);
    • 當瀏覽器連上首頁 (例如 192.168.0.10)。
    • ESP32 執行 handleRoot,回傳完整的 HTML 網頁原始碼。這包含了版面設計、CSS 樣式表,以及之後要運作的 JavaScript 程式。
  • server.on("/data", handleData);
    • 當瀏覽器請求數據頁 (例如 192.168.0.10/data)。
    • ESP32 執行 handleData,回傳純文字的 JSON 數據 (例如 {"temperature": 25, "humidity": 60})。這個畫面可以做為除錯的參考。

3. 持續監聽 (Handle Client)

這是最關鍵的一步!在 loop() 迴圈的第一行,我們必須放入:

server.handleClient();

這行指令的作用是**「檢查有沒有人敲門」**。

  • 因為 ESP32 的 loop() 是一圈又一圈不斷執行的,每次執行到這行,它就會快速看一下:「現在有沒有手機連進來?」
    • -> 趕快處理 (傳送網頁或數據)。
    • 沒有 -> 繼續往下做其他事 (讀取 DHT11、畫 OLED)。
  • 這就是為什麼我們不能用 delay()。如果你用 delay(2000),等於讓 ESP32 睡覺 2 秒,這 2 秒內它就無法執行 handleClient(),使用者的網頁就會轉圈圈連不上。

告別 delay(),擁抱 millis()

在離線版程式中,我們用了 delay(2000) 來等待兩秒。這就像讓 ESP32 去「睡覺」兩秒,這期間如果有人打開網頁,ESP32 是聽不到的,網頁就會打不開。 現在我們改用 millis()(計時器)的方法。這就像 ESP32 一邊盯著時鐘(檢查是否過了2秒),一邊同時接待客人(處理網頁請求)。這樣網頁反應速度會非常快

⚡ 關於 AJAX 技術

在傳統網頁中,要更新數據通常需要「重新整理」整個頁面,這會造成畫面閃爍。

本範例使用的 AJAX (Asynchronous JavaScript and XML) 技術,巧妙地利用了前面設定的兩個路徑:

  1. 初次載入:瀏覽器透過 / 取得 HTML 介面。
  2. 背景更新:HTML 裡的 JavaScript 程式,每隔 2 秒會自動在背景向 /data 請求最新數據。
  3. 局部替換:拿到數據後,JavaScript 只會精準地把網頁上的「數字」換掉,背景與標題完全不動。

透過這種分工,ESP32 就能輕鬆實現專業且不閃爍的即時監控儀表板!


現在大家不只可以在OLED上看到溫度及濕度,而且也可以在手機瀏覽器看到,恭喜大家走出IoT的第一步啊!


📝 加碼收錄,WebServer 指令速查表

初始化與啟動

  • WebServer server(80); - 初始化伺服器,監聽 Port 80

設定路由

  • server.on("網址", 函式); - 設定規則:規定當使用者連到某個網址時,要執行哪個函式

啟動伺服器

  • server.begin(); - 啟動伺服器:正式開始運作

處理連線

  • server.handleClient(); - 處理連線:放在 loop() 裡,負責接收與回應瀏覽器的請求

回傳資料

  • server.send(代碼, 類型, 內容); - 回傳資料:將結果傳回給瀏覽器
    • 代碼 200 代表成功
    • 類型通常是 text/htmlapplication/json