ESP32使用WiFi功能時,有3種模式:
- WIFI_STA,工作站模式(Station):當作普通設備,連接外部無線網路(如路由器)。
- WIFI_AP,熱點模式(Access Point):當作熱點供其他設備連接(如手機、電腦)。
- WIFI_AP_STA,混合模式(AP + Station):能連接外部無線網路,也能當作熱點供其他設備連接。
本次練習採用 WIFI_AP_STA 混合模式,希望達成下列功能:
- 以前次練習的「除濕機模擬系統」為基礎, ESP32 本機透過 DHT22 溫濕度感測器,可取得溫度、濕度等數據。
- WiFi 設定為混合模式。
- 透過工作站模式,連接外部無線網路,取得 NTP 網路時間,以及 OpenWeatherMap 網站的即時天氣、溫度、濕度、壓力等數據。
- 透過熱點模式,供其他設備連接,查看各項數據。
- 透過 Web Server 網頁介面,支援 AJAX 即時更新,能顯示:
(1) 本機濕度、溫度、體感溫度、運轉狀態與模式。
(2) NTP 網路時間。
(3) OpenWeatherMap 網站上的即時天氣、溫度、濕度、壓力等訊息。


WIFI混合模式範例程式
// 保護標頭檔,避免同一個 .h 標頭檔被重複引用
#ifndef WIFI_SETUP_H
#define WIFI_SETUP_H
//-------------------------------------------------------------------------------------------------------------------------
#include <WiFi.h> // 引用WiFi函式庫
const char *sta_ssid = "SSID"; // 無線基地臺名稱(工作站模式)
const char *sta_password = "密碼"; // 無線基地臺密碼(工作站模式)
const char *ap_ssid = "熱點名稱"; // 熱點名稱(熱點模式)
const char *ap_password = "熱點密碼"; // 熱點密碼(熱點模式)
//-------------------------------------------------------------------------------------------------------------------------
inline void connect_to_wifi() // 連線到WiFi(工作站模式)
{
WiFi.begin(sta_ssid, sta_password); // 啟動WiFi連線(工作站模式)
Serial.println("\n----------------------------------");
Serial.printf("Connecting to %s ", sta_ssid);
while(WiFi.status() != WL_CONNECTED) // 只要WiFi連線狀態不正常
{
delay(500); // 每0.5秒印出一個點
Serial.print(".");
}
Serial.println(" CONNECTED!");
Serial.print("SSID: ");
Serial.println(WiFi.SSID()); // 印出SSID
Serial.print("IP: ");
Serial.println(WiFi.localIP()); // 印出IP
Serial.print("Subnet Mask IP: ");
Serial.println(WiFi.subnetMask()); // 印出子網路遮罩
Serial.print("Gateway IP: ");
Serial.println(WiFi.gatewayIP()); // 印出閘道IP
Serial.print("DNS IP: ");
Serial.println(WiFi.dnsIP()); // 印出DNS IP
}
//-------------------------------------------------------------------------------------------------------------------------
inline void startAP() // 啟用熱點
{
while (!WiFi.softAP(ap_ssid, ap_password)) // 若熱點尚未啟動,則每0.5秒印出一個點
{
delay(500);
Serial.print(".");
}
Serial.println("\n----------------------------------");
Serial.println("熱點啟動成功");
Serial.print("IP Address: ");
Serial.println(WiFi.softAPIP()); // 印出熱點的IP位址
}
//-------------------------------------------------------------------------------------------------------------------------
#include <esp_wifi.h> // 引用esp_wifi函式庫
inline void listConnectedStations() // 印出已連線裝置清單
{
wifi_sta_list_t station_list; // 儲存連線裝置列表的結構
esp_wifi_ap_get_sta_list(&station_list); // 獲取當前連接到AP的所有裝置列表
Serial.println("--------- 當前連線裝置清單 ---------");
Serial.print("連線裝置數量: ");
Serial.println(station_list.num);
Serial.println("----------------------------------");
if (station_list.num > 0) {
Serial.println("編號 MAC 位址 IP 位址");
Serial.println("----------------------------------");
for (int i = 0; i < station_list.num; i++) {
wifi_sta_info_t station = station_list.sta[i];
// 取得 MAC 位址
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", station.mac[0], station.mac[1], station.mac[2], station.mac[3], station.mac[4], station.mac[5]);
// 取得 IP 位址 (需要透過 DHCP 租約列表)
IPAddress ip = WiFi.softAPIP();
ip[3] = i + 2; // 預設分配從 .2 開始
// 顯示裝置資訊
Serial.printf("%2d %s %s\n", i + 1, macStr, ip.toString().c_str());
}
} else {
Serial.println("目前沒有裝置連線");
}
Serial.println("----------------------------------");
}
//-------------------------------------------------------------------------------------------------------------------------
inline void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info) // WiFi事件處理函式
{
uint8_t *mac = nullptr; // 宣告MAC位址指標
char macStr[18] = { 0 }; // 儲存格式化MAC位址的字串緩衝區
switch (event) {
case ARDUINO_EVENT_WIFI_AP_STACONNECTED: // 裝置已連線
// 從事件資訊中提取MAC位址,並將MAC位址格式化為字串(XX:XX:XX:XX:XX:XX)
mac = info.wifi_ap_staconnected.mac;
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.println("\n[事件] 裝置已連線");
Serial.print("MAC: ");
Serial.println(macStr);
listConnectedStations(); // 更新並顯示當前連線裝置清單
break;
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: // 裝置斷開連線
// 從事件資訊中提取MAC位址,並將MAC位址格式化為字串(XX:XX:XX:XX:XX:XX)
mac = info.wifi_ap_stadisconnected.mac;
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.println("\n[事件] 裝置已斷開連線");
Serial.print("MAC: ");
Serial.println(macStr);
listConnectedStations(); // 更新並顯示當前連線裝置清單
break;
}
}
//-------------------------------------------------------------------------------------------------------------------------
#endif
//-----------------------------------------------------------------------
#include "wifi_setup.h" // 引用 "wifi_setup.h"
//-----------------------------------------------------------------------
void setup() {
//----------------------------------------------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//----------------------------------------------------------------------------------------
WiFi.mode(WIFI_AP_STA); // 設定為混合模式
//--------------------------------- 連接WiFi設定 ------------------------------------------
connect_to_wifi(); // 連線到WiFi
//--------------------------------- softAP設定 --------------------------------------------
startAP(); // 啟用熱點
WiFi.onEvent(WiFiEvent); // 啟用WiFi事件處理
listConnectedStations(); // 印出已連線裝置清單
//----------------------------------------------------------------------------------------
}
void loop() {
}
DHT 範例程式
// 保護標頭檔,避免同一個 .h 標頭檔被重複引用
#ifndef DHT_SETUP_H
#define DHT_SETUP_H
//-------------------------------------------------------------------------------------------------------------------------
#include "DHT.h" // 引用外部函式庫,名稱DHT
#define DHTPIN 23 // Digital pin connected to the DHT sensor
#define DHTTYPE DHT22 // DHT 22
DHT dht(DHTPIN, DHTTYPE); // 建立DHT物件,名為dht
float h; // 濕度資料
float t; // 攝氏溫度
float f; // 華氏溫度
float hic; // 攝氏體感溫度
float hif; // 華氏體感溫度
//-------------------------------------------------------------------------------------------------------------------------
inline void get_dht() // 取得dht資料
{
h = dht.readHumidity(); // 取得濕度資料
t = dht.readTemperature(); // 取得攝氏溫度
f = dht.readTemperature(true); // 取得華氏溫度
// 若讀不到資料,則顯示錯誤訊息,並離開
if (isnan(h) || isnan(t) || isnan(f))
{
Serial.println("Failed to read from DHT sensor!");
return;
}
hic = dht.computeHeatIndex(t, h, false); // 攝氏體感溫度
hif = dht.computeHeatIndex(f, h, true); // 華氏體感溫度
// 將溫濕度數據顯示在監看視窗
Serial.print("濕度: ");
Serial.print(h);
Serial.print("%, 溫度: ");
Serial.print(t);
Serial.print("°C ");
Serial.print(f);
Serial.print("°F, 體感溫度: ");
Serial.print(hic);
Serial.print("°C ");
Serial.print(hif);
Serial.println("°F");
}
//-------------------------------------------------------------------------------------------------------------------------
#endif
//-----------------------------------------------------------------------
#include "dht_setup.h" // 引用 "dht_setup.h"
//-----------------------------------------------------------------------
unsigned long previousMillis = 0; // 前一次的millis()時間
const long interval = 2000; // 預設計時的時間
//-----------------------------------------------------------------------
void setup() {
//----------------------------------------------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//--------------------------------- 連接 DHT22 --------------------------------------------
dht.begin(); // 啟用dht物件
//----------------------------------------------------------------------------------------
}
void loop() {
unsigned long currentMillis = millis(); // 當前的millis()時間
//---------------------------------------------------------------------
if (currentMillis - previousMillis >= interval) // 每經過一個interval的時間
{
get_dht(); // 取得dht資料
previousMillis = currentMillis; // 更新前一次的millis()時間
}
//---------------------------------------------------------------------
}
NTP 範例程式
// 保護標頭檔,避免同一個 .h 標頭檔被重複引用
#ifndef NTP_SETUP_H
#define NTP_SETUP_H
//-------------------------------------------------------------------------------------------------------------------------
#include "time.h"
#include "sntp.h"
const char* ntpServer1 = "pool.ntp.org"; // NTP伺服器網址1
const char* ntpServer2 = "time.nist.gov"; // NTP伺服器網址2
const char* ntpServer3 = "time.stdtime.gov.tw"; // NTP伺服器網址3
const long gmtOffset_sec = 28800; // GMT+8,28800秒=8小時
const int daylightOffset_sec = 0; // 日光節約時間
//-------------------------------------------------------------------------------------------------------------------------
inline void configNTP() // 設定NTP伺服器及參數
{
// configTime(GMT偏移秒數, 日光節約偏移秒數, 伺服器網址1, 伺服器網址2, 伺服器網址3)
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void printLocalTime() // 印出時間資料
{
struct tm timeinfo; // 建立一個時間結構,名稱為 timeinfo
if(!getLocalTime(&timeinfo)) // 向NTP伺服器取得時間資料,若未取得時間資料,則返回
{
Serial.println("No time available (yet)");
return;
}
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S"); // 印出時間資料(第1種資料格式)
Serial.println(&timeinfo, "%F, %r"); // 印出時間資料(第2種資料格式)
}
//-------------------------------------------------------------------------------------------------------------------------
#endif
//-----------------------------------------------------------------------
#include "wifi_setup.h" // 引用 "wifi_setup.h"
//-----------------------------------------------------------------------
#include "ntp_setup.h" // 引用 "ntp_setup.h"
//-----------------------------------------------------------------------
unsigned long previousMillis = 0; // 前一次的millis()時間
const long interval = 1000; // 預設計時的時間
//-----------------------------------------------------------------------
void setup() {
//----------------------------------------------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//----------------------------------------------------------------------------------------
WiFi.mode(WIFI_AP_STA); // 設定為混合模式
//--------------------------------- 連接WiFi設定 ------------------------------------------
connect_to_wifi(); // 連線到WiFi
//-------------------------------- NTP伺服器設定 ------------------------------------------
configNTP(); // NTP伺服器設定
//----------------------------------------------------------------------------------------
}
void loop() {
unsigned long currentMillis = millis(); // 當前的millis()時間
//---------------------------------------------------------------------
if (currentMillis - previousMillis >= interval) // 每經過一個interval的時間
{
printLocalTime(); // 印出 NTP 時間資料
previousMillis = currentMillis; // 更新前一次的millis()時間
}
//---------------------------------------------------------------------
}
OpenWeatherMap 範例程式
// 保護標頭檔,避免同一個 .h 標頭檔被重複引用
#ifndef WEATHER_SETUP_H
#define WEATHER_SETUP_H
//-------------------------------------------------------------------------------------------------------------------------
#include <HTTPClient.h> // 發送http請求,取得網站資料
HTTPClient http;
// String url = "https://api.openweathermap.org/data/2.5/weather?q=Taipei,TW&units=metric&appid=API金鑰"; // 網址
// 將網址中的城市、地區、API金鑰設定為變數,以方便後續程式操控
String city = "Taipei";
String countryCode = "TW";
String ApiKey = "API金鑰";
String url = "https://api.openweathermap.org/data/2.5/weather?q=" + city + "," + countryCode + "&units=metric&appid=" + ApiKey; // 網址
//-------------------------------------------------------------------------------------------------------------------------
#include <ArduinoJson.h> // 解析JSON資料
String weatherDescription; // 天氣概況
String temp; // 溫度
String pressure; // 大氣壓力
String humidity; // 溼度
//-------------------------------------------------------------------------------------------------------------------------
inline void get_weather_data() // 取得天氣資料
{
http.begin(url); // 開始連接網頁
int httpCode = http.GET(); // 執行GET請求,回傳碼儲存於httpCode
if (httpCode == HTTP_CODE_OK) // 如果連線正常
{
String payload = http.getString(); // 傳回的網頁內容儲存於字串變數payload(承載量)
// Serial.println(payload); // 印出傳回的網頁內容
// -------------------------------------------------------------
// OpenWeatherMap JSON格式解析
// -------------------------------------------------------------
DynamicJsonDocument WeatherJson(payload.length() * 2); // 宣告一個Json文件,名稱為WeatherJson(陣列格式)
deserializeJson(WeatherJson, payload); // 解析payload為JSON Array格式
weatherDescription = WeatherJson["weather"][0]["description"].as<String>(); // 取得天氣概況,weather是陣列,[0]是索引值
temp = WeatherJson["main"]["temp"].as<String>(); // 取得溫度
pressure = WeatherJson["main"]["pressure"].as<String>(); // 取得氣壓
humidity = WeatherJson["main"]["humidity"].as<String>(); // 取得溼度
Serial.println("----------------------------------");
Serial.print("Weather description: ");
Serial.println(weatherDescription);
Serial.print("Temp: ");
Serial.print(temp);
Serial.println(" °C");
Serial.print("Pressure: ");
Serial.print(pressure);
Serial.println(" hPa");
Serial.print("Humidity: ");
Serial.print(humidity);
Serial.println(" %");
Serial.println("----------------------------------");
}
else
{
Serial.print("HTTP GET failed, error code: "); // 印出錯誤訊息
Serial.println(httpCode);
}
http.end(); // 結束連線
}
//-------------------------------------------------------------------------------------------------------------------------
#endif
//-----------------------------------------------------------------------
#include "wifi_setup.h" // 引用 "wifi_setup.h"
//-----------------------------------------------------------------------
#include "weather_setup.h" // 引用 "weather_setup.h"
//-----------------------------------------------------------------------
unsigned long previousMillis = 0; // 前一次的millis()時間
const long interval = 5000; // 預設計時的時間
//-----------------------------------------------------------------------
void setup() {
//----------------------------------------------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//----------------------------------------------------------------------------------------
WiFi.mode(WIFI_AP_STA); // 設定為混合模式
//--------------------------------- 連接WiFi設定 ------------------------------------------
connect_to_wifi(); // 連線到WiFi
//----------------------------------------------------------------------------------------
}
void loop() {
unsigned long currentMillis = millis(); // 當前的millis()時間
//---------------------------------------------------------------------
if (currentMillis - previousMillis >= interval) // 若達到預設計時的時間
{
get_weather_data(); // 取得天氣資料
previousMillis = currentMillis; // 更新前一次的millis()時間
}
//---------------------------------------------------------------------
}
WebServer 範例程式
// 保護標頭檔,避免同一個 .h 檔被重複引用
#ifndef WEB_SETUP_H
#define WEB_SETUP_H
//-------------------------------------------------------------------------------------------------------------------------
#include <WebServer.h> // 引用WebServer函式庫
#include "dht_setup.h" // 引用dht_setup.h
//-------------------------------------------------------------------------------------------------------------------------
// 外部變數宣告,引用主程式中的變數
extern const int ledPin; // LED 模擬除濕機
extern bool ledState; // LED(除濕機)狀態
extern bool mode; // 控制模式,false = 自動(auto), true = 手動(manual)
//-------------------------------------------------------------------------------------------------------------------------
WebServer server(80); // 建立WebServer物件 (port 80)
//-------------------------------------------------------------------------------------------------------------------------
inline void handleRoot() // 首頁
{
String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<h2>ESP32 除濕機模擬系統</h2>"
"<ul>"
"<li><a href=\"/on\">手動啟動除濕機</a></li>"
"<li><a href=\"/off\">手動停止除濕機</a></li>"
"<li><a href=\"/auto\">切回自動模式</a></li>"
"</ul>"
"<div id=\"status\">AJAX 讀取中...</div>"
"<script>"
"function updateStatus(){"
" fetch('/status')"
" .then(response => response.json())"
" .then(data => {"
" let html = '';"
" html += '濕度:' + data.humidity + '%<br>';"
" html += '溫度:' + data.tempC + '°C / ' + data.tempF + '°F<br>';"
" html += '體感溫度:' + data.heatIndexC + '°C / ' + data.heatIndexF + '°F<br>';"
" html += '除濕機狀態:' + data.dehumidifier + '<br>';"
" html += '控制模式:' + data.mode;"
" document.getElementById('status').innerHTML = html;"
" });"
"}"
"setInterval(updateStatus,2000);"
"updateStatus();"
"</script>";
server.send(200, "text/html; charset=UTF-8", html);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void handleOn() // 手動啟動除濕機
{
mode = true; // 切到手動模式
ledState = true; // 開啟除濕機
server.sendHeader("Location", "/");
server.send(303);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void handleOff() // 手動停止除濕機
{
mode = true; // 切到手動模式
ledState = false; // 關閉除濕機
server.sendHeader("Location", "/");
server.send(303);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void handleAuto() // 切換自動模式
{
mode = false; // 切回自動模式
server.sendHeader("Location", "/");
server.send(303);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void handleStatus() // 更新狀態訊息
{
String json = "{";
json += "\"humidity\":" + String(h, 1) + ",";
json += "\"tempC\":" + String(t, 1) + ",";
json += "\"tempF\":" + String(f, 1) + ",";
json += "\"heatIndexC\":" + String(hic, 1) + ",";
json += "\"heatIndexF\":" + String(hif, 1) + ",";
json += "\"dehumidifier\":\"" + String(ledState ? "運轉中" : "停止") + "\",";
json += "\"mode\":\"" + String(mode ? "手動" : "自動") + "\"";
json += "}";
server.send(200, "application/json", json);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void handleNotFound() // 找不到網頁
{
String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">找不到網頁";
server.send(404, "text/html; charset=UTF-8", html);
}
//-------------------------------------------------------------------------------------------------------------------------
inline void webServerSetup() // WebServer設定
{
server.on("/", handleRoot); // 首頁
server.on("/on", handleOn); // 手動啟動除濕機
server.on("/off", handleOff); // 手動停止除濕機
server.on("/status", handleStatus); // 更新狀態訊息
server.on("/auto", handleAuto); // 切換自動模式
server.onNotFound(handleNotFound); // 找不到網頁
server.begin(); // 啟動webServer
Serial.println("Server已啟動"); // 印出提示文字
}
//-------------------------------------------------------------------------------------------------------------------------
inline void webServerHandle() // 監聽客戶端請求
{
server.handleClient(); // 檢查是否有客戶端向ESP32 WebServer發送請求
}
//-------------------------------------------------------------------------------------------------------------------------
#endif
//-----------------------------------------------------------------------
const int ledPin = 2; // LED 模擬除濕機
bool ledState = false; // LED(除濕機)狀態
bool mode = false; // 控制模式,false = 自動(auto), true = 手動(manual)
//-----------------------------------------------------------------------
#include "wifi_setup.h" // 引用 "wifi_setup.h"
//-----------------------------------------------------------------------
#include "dht_setup.h" // 引用 "dht_setup.h"
//-----------------------------------------------------------------------
#include "web_setup.h" // 引用 "web_setup.h"
//-----------------------------------------------------------------------
unsigned long previousMillis = 0; // 前一次的millis()時間
const long interval = 2000; // 預設計時的時間
//-----------------------------------------------------------------------
void setup() {
//---------------------------------------------------
pinMode(ledPin, OUTPUT); // 設定LED為輸出腳,模擬除濕機
//---------------------------------------------------
Serial.begin(9600); // 啟用串列埠監看視窗
//---------------------------------------------------
WiFi.mode(WIFI_AP_STA); // 設定為混合模式
//---------------------------------------------------
connect_to_wifi(); // 連線到WiFi
//------------------- softAP設定 --------------------
startAP(); // 啟用熱點
WiFi.onEvent(WiFiEvent); // 啟用WiFi事件處理
listConnectedStations(); // 印出已連線裝置清單
//---------------------------------------------------
dht.begin(); // 啟用dht物件,連接 DHT22
//---------------------------------------------------
webServerSetup(); // WebServer設定
//---------------------------------------------------
}
void loop() {
unsigned long currentMillis = millis(); // 當前的millis()時間
//---------------------------------------------------------------------
if (currentMillis - previousMillis >= interval) // 若達到預設計時的時間
{
//---------------------------------------------------
get_dht(); // 每經過一個interval的時間,取得dht資料
if (mode == 0) // 自動除濕控制
{
if (h > 75) ledState = true;
else if (h < 50) ledState = false;
}
digitalWrite(ledPin, ledState); // LED 模擬除濕機運作
//---------------------------------------------------
previousMillis = currentMillis; // 更新前一次的millis()時間
}
//---------------------------------------------------------------------
webServerHandle(); // 監聽客戶端請求
}