ESP32迷你氣象站:DHT11 + 0.96 OLED,網頁監看版
ESP32 氣象站聯網升級!手機瀏覽器即時監控,AJAX 技術讓數據秒跳不閃爍。第一次讓ESP32連上網,新手也能輕鬆打造專業 IoT 神器!
在上一篇文章中,我們成功做出了一個擁有專業介面、排版精美的「離線版」桌面溫濕度計。雖然它放在桌上很好看,但如果我躺在床上,或者在另一個房間,想知道現在室溫幾度怎麼辦?
這就是 IoT (物聯網) 發揮作用的時候了!
今天我們要幫 ESP32 裝上翅膀(連上 WiFi),讓它變身為一個迷你網頁伺服器 (Web Server)。你不用安裝任何 App,只要打開手機瀏覽器,就能看到即時的溫濕度數據,而且還會用到 AJAX 技術,讓網頁數據自動跳動更新,不會整頁閃爍!
有關接線和基本的DHT11和OLED的處理,這個章節就不重複說明了,建議大家先看過上一篇離線版,再接下去看哦!

🚀 這次的升級重點
- OLED 介面進化:頂部原本顯示網址的地方,改為自動顯示 本機 IP 位址,方便你一眼就知道網頁要連哪裡。
- 手機網頁監控:打開瀏覽器輸入 IP,即時查看溫濕度。
- AJAX 無刷新技術:數據每 2 秒自動更新,畫面滑順不閃爍。
- 非阻塞程式設計:拋棄
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> °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) 技術,巧妙地利用了前面設定的兩個路徑:
- 初次載入:瀏覽器透過
/取得 HTML 介面。 - 背景更新:HTML 裡的 JavaScript 程式,每隔 2 秒會自動在背景向
/data請求最新數據。 - 局部替換:拿到數據後,JavaScript 只會精準地把網頁上的「數字」換掉,背景與標題完全不動。
透過這種分工,ESP32 就能輕鬆實現專業且不閃爍的即時監控儀表板!

現在大家不只可以在OLED上看到溫度及濕度,而且也可以在手機瀏覽器看到,恭喜大家走出IoT的第一步啊!
📝 加碼收錄,WebServer 指令速查表
初始化與啟動
WebServer server(80);- 初始化伺服器,監聽 Port 80
設定路由
server.on("網址", 函式);- 設定規則:規定當使用者連到某個網址時,要執行哪個函式
啟動伺服器
server.begin();- 啟動伺服器:正式開始運作
處理連線
server.handleClient();- 處理連線:放在loop()裡,負責接收與回應瀏覽器的請求
回傳資料
server.send(代碼, 類型, 內容);- 回傳資料:將結果傳回給瀏覽器- 代碼 200 代表成功
- 類型通常是
text/html或application/json
