WiFi練習:熱點模式—遠端控制ESP32+AJAX即時更新

上週範例程式:

// 保護標頭檔,避免同一個 .h 標頭檔被重複引用
#ifndef WIFI_SETUP_H
#define WIFI_SETUP_H

//-------------------------------------------------------------------------------
#include <WiFi.h>                   // 引用WiFi函式庫
const char *ssid="SSID";            // 熱點名稱
const char *password="密碼";        // 熱點密碼
//-------------------------------------------------------------------------------
inline void startAP()  // 啟用熱點
{
  /*
  IPAddress local_ip(192,168,31,31);   // 設定熱點的IP位址
  IPAddress gateway(192,168,31,2);     // 設定熱點的閘道位址
  IPAddress subnet(255,255,255,0);     // 設定熱點的子網路遮罩位址
  WiFi.softAPConfig(local_ip, gateway, subnet); // 設定熱點參數(未設定時,熱點IP位址預設為192.168.4.1)
  */
  while (!WiFi.softAP(ssid, 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
//-------------------------------------------------------------------------------------------------------------------------
const int ledPin = 2;             // LED接腳
bool ledState = LOW;              // LED狀態,初始值為LOW(OFF)
bool blinkEnable = LOW;           // 閃爍控制,初始值為LOW (停止)
unsigned long previousMillis = 0; // 前一次的millis()時間
int  interval = 500;              // 預設計時的時間(ms)
//-------------------------------------------------------------------------------------------------------------------------
#include "wifi_setup.h"  // 引用自行建義的 "wifi_setup.h"
//-------------------------------------------------------------------------------------------------------------------------
#include <WebServer.h>  // 引用WebServer函式庫
WebServer server(80);   // 建立WebServer物件, port為80

void handleRoot()     // 根目錄
{
  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "<p>歡迎光臨!請點選下列連結:</p> <ul> <li><a href=\"/on\">打開 LED 燈</a></li> <li><a href=\"/off\">關閉&nbsp;LED 燈</a></li> <li><a href=\"/blink\">啟用或停止閃爍&nbsp;LED 燈</a></li> </ul>"
                "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleOn()       // LED ON
{
  blinkEnable = false;
  digitalWrite(ledPin, HIGH);
  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "<p><strong><span style=\"color:#e74c3c\">LED燈已打開!</span></strong></p> <ul> <li><a href=\"/off\">關閉&nbsp;LED 燈</a></li> <li><a href=\"/blink\">啟用或停止閃爍&nbsp;LED 燈</a></li> </ul> <p><a href=\"/\">返回首頁</a></p>"
                "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleOff()      // LED OFF
{
  blinkEnable = false;
  digitalWrite(ledPin, LOW);
  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "<p><span style=\"color:#27ae60\"><strong>LED燈已關閉!</strong></span></p> <ul> <li><a href=\"/on\">開啟&nbsp;LED 燈</a></li> <li><a href=\"/blink\">啟用或停止閃爍&nbsp;LED 燈</a></li> </ul> <p><a href=\"/\">返回首頁</a></p>"
                "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleBlink()    // LED Blink
{
  blinkEnable = !blinkEnable;
  String html;
  if (blinkEnable == true)
  {
    html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
           "<p><span style=\"color:#e74c3c\"><strong>LED燈正在閃爍!</strong></span></p> <ul> <li><a href=\"/on\">開啟&nbsp;LED 燈</a></li> <li><a href=\"/off\">關閉 LED 燈</a></li> <li><a href=\"/blink\">關閉&nbsp;LED 燈閃爍</a></li> </ul><p><a href=\"/\">返回首頁</a></p>"
           "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  }
  else
  {
    html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
           "<p><span style=\"color:#27ae60\"><strong>LED燈已停止閃爍!</strong></span></p> <ul> <li><a href=\"/on\">開啟&nbsp;LED 燈</a></li> <li><a href=\"/off\">關閉 LED 燈</a></li> <li><a href=\"/blink\">開啟&nbsp;LED 燈閃爍</a></li> </ul><p><a href=\"/\">返回首頁</a></p>"
           "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  }

  server.send(200, "text/html; charset=UTF-8", html);
}

void handleSetInterval()
{
  if (server.hasArg("time"))    // 檢查是否有收到參數
  {
    int newInterval = server.arg("time").toInt();
    if (newInterval > 0)
    {
      interval = newInterval;
    }
  }

  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "<p><strong>閃爍間隔已設定為 " + String(interval) + " 毫秒</strong></p>"
                "<p><a href=\"/\">返回首頁</a></p>"
                "<p>設定LED閃爍時間(ms):</p> <form action=\"/setInterval\" method=\"get\" name=\"setInterval\"><input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" /><input type=\"submit\" value=\"設定\" />&nbsp;</form> <p>&nbsp;</p>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleNotFound() // 找不到網頁
{
  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "找不到網頁";
  server.send(404, "text/html; charset=UTF-8", html);
}
//-------------------------------------------------------------------------------------------------------------------------

void setup() {
  //-------------------------------------------------------------------
  pinMode(ledPin, OUTPUT);  // 設定LED為輸出腳
  Serial.begin(9600);       // 啟用串列埠監看視窗
  //--------------------------- softAP設定 ----------------------------
  startAP();                // 啟用熱點
  WiFi.onEvent(WiFiEvent);  // 啟用WiFi事件處理
  listConnectedStations();  // 印出已連線裝置清單
  //---------------------------WebServer設定---------------------------
  server.on("/", handleRoot);         // 在/時,前往handleRoot()
  server.on("/on", handleOn);         // 在/on時,前往handleOn()
  server.on("/off", handleOff);       // 在/off時,前往handleOff()
  server.on("/blink", handleBlink);   // 在/blink時,前往handleBlink()
  server.on("/setInterval", handleSetInterval);  // 在/setInterval時,前往handleSetInterval()
  server.onNotFound(handleNotFound);  // 找不到網頁時,前往handleNotFound()  
  server.begin();                     // 啟動webServer
  Serial.println("Server已啟動");      // 印出提示文字
  //-------------------------------------------------------------------
}

void loop() {
  server.handleClient();    // 檢查是否有客戶端向ESP32 WebServer發送請求

  //--------------------------------------------------------------------------
  if (blinkEnable == true)
  {
    unsigned long currentMillis = millis();            // 當前的millis()時間
    if (currentMillis - previousMillis >= interval)    // 若達到預設計時的時間
    {
      previousMillis = currentMillis;                  // 更新前一次的millis()時間

      //-----------------------------
      // 每個interval所做的事
      //-----------------------------
      ledState = !ledState;                 // 改變LED狀態
      digitalWrite(ledPin, ledState);       // 輸出狀態
      //-----------------------------
    }
  }
  //--------------------------------------------------------------------------  
}

本次練習我們希望將不同動作的各個頁面進行整合,讓所有的訊息彙整呈現在首頁上,並可定期自動更新內容。

原網頁內容

目錄 /
目錄 /on
目錄 /off
目錄 /blink
目錄 /blink
目錄 /setInterval

整合網頁內容

目錄 /
目錄 /

設計思路

  1. 新增一個目錄 /status ,負責傳遞下面的 JSON 格式參數。
{
  "led": "ON",           // LED狀態
  "blink": "正在閃爍",    // 是否閃爍
  "interval": 500        // 閃爍時間間隔
}
  1. 前端網頁用 JavaScript (AJAX / fetch)
    • 每隔一段時間自動向 /status 請求資料。
    • 解析 JSON 內容,更新頁面上的狀態顯示。
  2. 所有控制連結/on, /off, /blink, /setInterval 執行後自動導向首頁,統一在首頁顯示狀態訊息。
  3. AJAX 只負責讀取狀態,並不會影響原控制功能。

實作步驟

1、在WebServer設定中新增一個處理頁面 /status,前往handleStatus()副程式。

server.on("/status", handleStatus);    // 在/status時,前往handleStatus()

2、handleStatus() 副程式責負傳遞需要更新的led、blink、interval等參數:

void handleStatus()   // Status
{
  String json = "{";
  json += "\"led\":\"" + String(ledState ? "ON" : "OFF") + "\",";                 // ledState為1時,顯示“ON”,否則顯示“OFF”
  json += "\"blink\":\"" + String(blinkEnable ? "正在閃爍" : "停止閃爍") + "\",";   // blinkEnable為1時,顯示“正在閃爍”,否則顯示“停止閃爍”
  json += "\"interval\":" + String(interval);
  json += "}";
  server.send(200, "application/json", json);
}

3、在首頁的 html 程式碼中加入AJAX自動更新的JavaScript:

void handleRoot()     // 根目錄
{
  String html = "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
                "<p>歡迎光臨!請點選下列連結:</p>"
                "<ul>"
                "<li><a href=\"/on\">打開 LED 燈</a></li>"
                "<li><a href=\"/off\">關閉 LED 燈</a></li>"
                "<li><a href=\"/blink\">啟用或停止閃爍 LED 燈</a></li>"
                "</ul>"

                // 設定閃爍時間
                "<p>設定LED閃爍時間(ms):</p>"
                "<form action=\"/setInterval\" method=\"get\" name=\"setInterval\">"
                "<input name=\"interval\" type=\"number\" value=\"" + String(interval) + "\" />"
                "<input type=\"submit\" value=\"設定\" />"
                "</form>"

                // AJAX 狀態自動更新
                "<div id=\"status\">讀取狀態...</div>"
                "<script>"
                "function updateStatus(){"
                "  fetch('/status')"                                   // 向 /status 發送請求
                "    .then(response => response.json())"               // 解析 JSON
                "    .then(data => {"
                "      let html = 'LED狀態:' + data.led + '<BR>';"
                "         html += '閃爍狀態:' + data.blink + '<BR>';"
                "         html += '閃爍間隔:' + data.interval + ' ms';"
                "      document.getElementById('status').innerHTML = html;"
                "    });"
                "}"
                "setInterval(updateStatus," + String(interval) + ");"   // 每個interval更新一次
                "updateStatus();"                                       // 載入更新頁面
                "</script>";

  server.send(200, "text/html; charset=UTF-8", html);
}

4、利用下列程式碼,可使原控制連結/on, /off, /blink, /setInterval執行後,自動導向首頁。

server.sendHeader("Location", "/");
server.send(303);
//-------------------------------------------------------------------------------
void handleOn()   // LED ON
{
  blinkEnable = false;
  digitalWrite(ledPin, HIGH);
  ledState = HIGH;
  server.sendHeader("Location", "/");
  server.send(303);
}
//-------------------------------------------------------------------------------
void handleOff()  // LED OFF
{
  blinkEnable = false;
  digitalWrite(ledPin, LOW);
  ledState = LOW;
  server.sendHeader("Location", "/");
  server.send(303);
}
//-------------------------------------------------------------------------------
void handleBlink()  // LED Blink
{
  blinkEnable = !blinkEnable;
  server.sendHeader("Location", "/");
  server.send(303);
}
//-------------------------------------------------------------------------------
void handleSetInterval()  // Setup Blink Interval
{
  if (server.hasArg("interval"))  // 檢查是否有收到參數
  {
    int newInterval = server.arg("interval").toInt();
    if (newInterval > 0)
    {
      interval = newInterval;
      blinkEnable = true;  // 設定後啟用閃爍
    }
  }
  server.sendHeader("Location", "/");
  server.send(303);
}
//-------------------------------------------------------------------------------

作業練習:請將上週作業,控制LED燈亮滅程度、閃爍時間、顯示狀態等功能整合在首頁上,並可定期自動更新內容。