WiFi練習:熱點模式—遠端控制ESP32內建LED燈+設定閃爍時間

在上個作業練習中,我們利用 ESP32 的熱點模式,透過網頁遠端控制ESP32內建LED燈開、關、閃爍(每0.5秒亮滅1次)。原始程式碼如下:

//-------------------------------------------------------------------------------------------------------------------------
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.h>                 // 引用WiFi函式庫
const char *ssid="SSID";          // 熱點名稱
const char *password="密碼";      // 熱點密碼
//-------------------------------------------------------------------------------------------------------------------------
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函式庫

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;
  }
}
//-------------------------------------------------------------------------------------------------------------------------
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("----------------------------------");
}
//-------------------------------------------------------------------------------------------------------------------------
#include <WebServer.h>
WebServer server(80); // 建立WebServer物件, port為80

void handleRoot()     // 根目錄
{
  String html = "<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>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleOn()       // LED ON
{
  blinkEnable = false;
  digitalWrite(ledPin, HIGH);
  String html = "<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>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleOff()      // LED OFF
{
  blinkEnable = false;
  digitalWrite(ledPin, LOW);
  String html = "<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>";
  server.send(200, "text/html; charset=UTF-8", html);
}

void handleBlink()    // LED Blink
{
  blinkEnable = !blinkEnable;
  String html;
  if (blinkEnable == true)
  {
    html ="<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>";
  }
  else
  {
    html = "<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>";
  }

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

void handleNotFound() // 找不到網頁
{
  String html = "找不到網頁";
  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.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);       // 輸出狀態
      //-----------------------------
    }
  }
  //--------------------------------------------------------------------------  
}

為了使整體程式架構更精簡、結構化,我們可以將startAP()、listConnectedStations()、WiFiEvent()等函式放在 “setup_wifi.h” 中,之後在主程式中引用。做法如下:

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

//-------------------------------------------------------------------------------
#include <WiFi.h>             // 引用WiFi函式庫
const char *ssid="SSID";      // 熱點名稱
const char *password="密碼";  // 熱點密碼
//-------------------------------------------------------------------------------
inline void startAP()         // 啟用熱點
{

}
//-------------------------------------------------------------------------------
#include <esp_wifi.h>               // 引用esp_wifi函式庫
inline void listConnectedStations() // 印出已連線裝置清單
{

}
//-------------------------------------------------------------------------------------------------------------------------
inline void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info)  // WiFi事件處理函式
{

}
//-------------------------------------------------------------------------------
#endif

主程式架構:

//-------------------------------------------------------------------------------
// 全域變數宣告
//-------------------------------------------------------------------------------
#include "wifi_setup.h"  // 引用自行建義的 "wifi_setup.h"
#include <WebServer.h>   // 引用WebServer函式庫
WebServer server(80); // 建立WebServer物件, port為80

void handleRoot()     // 根目錄
{

}

void handleOn()       // LED ON
{

}

void handleOff()      // LED OFF
{

}

void handleBlink()    // LED Blink
{

}

void handleNotFound() // 找不到網頁
{

}
//-------------------------------------------------------------------------------------------------------------------------
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.onNotFound(handleNotFound);  // 找不到網頁時,前往handleNotFound()  
  server.begin();                     // 啟動webServer
  Serial.println("Server已啟動");      // 印出提示文字
  //-------------------------------------------------------------------
}

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

當手機打開瀏覽器時有字體太小的問題,為避免每次都需要手動縮放頁面,可在所有 HTML 字串的最開頭,添加下列標簽:(註:可使用線上網頁編輯器HTML to Arduino進行編輯)

<meta name="viewport" content="width=device-width, initial-scale=1.0">

此標簽會告訴手機瀏覽器:「以設備螢幕寬度為基準,初始縮放比例為 1(即不縮放,文字大小適配螢幕)」。

HTML to Arduino程式碼

"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"

現在要在原有的網頁上,增加一個「訊息方塊」讓使用者可以輸入閃爍時間的數值,以及一個「按鈕」,送出後可以決定LED閃爍的時間間隔。

1、在原本的網頁後面加入一個「訊息方塊」與「送出按鈕」 :

在原始碼中將類型從text改成number

HTML 程式碼

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

HTML to Arduino程式碼

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

如果想在訊息方塊上顯示目前的閃爍時間間隔,可以將變數 interval 加入程式碼中:

<input name=\"time\" type=\"number\" value=\"" + String(interval) + "\" />

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

  //---------------------------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已啟動");                // 印出提示文字
  //-------------------------------------------------------------------

3、handleSetInterval() 副程式責負處理網頁表單傳遞過來的參數:

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

  String html = "<p><strong>閃爍間隔已設定為 " + String(interval) + " 毫秒</strong></p>"
                "<p><a href=\"/\">返回首頁</a></p>";
  server.send(200, "text/html; charset=UTF-8", html);
}
目錄 /
目錄 /setInterval

作業練習:請在網頁上新增一個亮度參數,可控制LED燈的亮滅程度,版面配置請自行安排與規劃。(頁面上必須有自己的班級、座號、姓名)

提示:仿照傳遞閃爍時間參數的流程即可完成。

  1. 新增廣域變數 brightness 。
  2. 新增 WebServer設定: server.on(“/setBrightness”, handleSetBrightness);
  3. 新增 handleSetBrightness() 副程式,負責處理控制亮滅程度。
  4. 新增一個表單,內含「訊息方塊」與「送出按鈕」負責傳遞亮度參數。