一、简介

1.1 硬件介绍

ESP32-S3 SoC 芯片支持以下功能:

  • 2.4 GHz Wi-Fi
  • 低功耗蓝牙
  • 高性能 Xtensa® 32 位 LX7 双核处理器
  • 运行 RISC-V 或 FSM 内核的超低功耗协处理器
  • 多种外设
  • 内置安全硬件
  • USB OTG 接口
  • USB 串口/JTAG 控制

1.2 官方资料

ESP-IDF编程指南Wi-Fi库HTTP服务器

1.3 开发环境

软件:IDF 5.1.1
硬件:ESP32-S3-LCD-EV-Board-MB 开发板

1.4 Wi-Fi介绍

Wi-Fi 库支持配置及监控 ESP32-S3 Wi-Fi 连网功能。支持配置:

  • station 模式(即 STA 模式或 Wi-Fi 客户端模式),此时 ESP32-S3 连接到接入点 (AP)。
  • AP 模式(即 Soft-AP 模式或接入点模式),此时基站连接到 ESP32-S3。
  • station/AP 共存模式(ESP32-S3 既是接入点,同时又作为基站连接到另外一个接入点)。
  • 上述模式的各种安全模式(WPA、WPA2、WPA3 等)。
  • 扫描接入点(包括主动扫描及被动扫描)。
  • 使用混杂模式监控 IEEE802.11 Wi-Fi 数据包。

在ESP32中使用Wi-Fi功能非常灵活,它支持多种工作模式,包括Station模式、Access Point模式,以及同时支持这两种模式的Station + Access Point模式。ESP32作为一个高性能的Wi-Fi和蓝牙双模系统芯片(SoC),为物联网(IoT)项目提供了强大的网络连接能力。以下是在ESP32中使用Wi-Fi的基本概述:

1.4.1 Station模式(STA)

在Station模式下,ESP32作为客户端连接到一个现有的Wi-Fi网络。这允许ESP32访问互联网或局域网中的其他设备和服务。通过编程,你可以配置ESP32保存网络凭证,并在启动时自动连接到Wi-Fi网络。

1.4.2 Access Point模式(AP)

在Access Point模式下,ESP32自身创建一个Wi-Fi热点,其他Wi-Fi设备(如智能手机、电脑等)可以连接到这个热点。这种模式适合建立直接的设备到设备的通信,或在没有现成Wi-Fi网络的情况下提供网络接入点。

1.4.3 Station + Access Point模式(STA+AP)

在这种模式下,ESP32同时工作在Station模式和Access Point模式。这意味着ESP32既可以作为客户端连接到其他Wi-Fi网络,也可以创建自己的Wi-Fi热点供其他设备连接。这种灵活性使ESP32非常适合作为网络中继器或桥接设备使用。

1.5 HTTP服务器介绍

HTTP Server 组件提供了在 ESP32 上运行轻量级 Web 服务器的功能,下面介绍使用 HTTP Server 组件 API 的详细步骤:

  • httpd_start(): 创建 HTTP 服务器的实例,根据具体的配置为其分配内存和资源,并返回该服务器实例的句柄。服务器使用了两个套接字,一个用来监听 HTTP 流量(TCP 类型),另一个用来处理控制信号(UDP 类型),它们在服务器的任务循环中轮流使用。通过向 httpd_start() 传递 httpd_config_t 结构体,可以在创建服务器实例时配置任务的优先级和堆栈的大小。TCP 流量被解析为 HTTP 请求,根据请求的 URI 来调用用户注册的处理程序,在处理程序中需要发送回 HTTP 响应数据包。
  • httpd_stop(): 根据传入的句柄停止服务器,并释放相关联的内存和资源。这是一个阻塞函数,首先给服务器任务发送停止信号,然后等待其终止。期间服务器任务会关闭所有已打开的连接,删除已注册的 URI 处理程序,并将所有会话的上下文数据重置为空。
  • httpd_register_uri_handler(): 通过传入 httpd_uri_t 结构体类型的对象来注册 URI 处理程序。该结构体包含如下成员:uri 名字,method 类型(比如 HTTPD_GET/HTTPD_POST/HTTPD_PUT 等等), esp_err_t *handler (httpd_req_t *req) 类型的函数指针,指向用户上下文数据的 user_ctx 指针。

在ESP32中使用HTTP服务器是通过ESP-IDF(Espressif IoT Development Framework)提供的API来实现的。HTTP服务器可以让ESP32处理HTTP请求,比如提供网页、处理表单提交等,非常适合用于创建物联网(IoT)设备的Web接口。以下是如何在ESP32中设置和使用HTTP服务器的基本步骤:

1.5.1 初始化和启动HTTP服务器

首先,需要包含必要的头文件,并定义一个HTTP服务器实例,然后配置服务器的参数,如端口号、最大连接数等。之后,启动HTTP服务器。

1.5.2 注册URI处理程序

HTTP服务器通过URI处理程序来响应不同的HTTP请求。你可以为不同的路径(URI)注册不同的处理函数,以处理GET、POST等HTTP请求。

1.5.3 发送响应

在URI处理函数中,你可以使用httpd_resp_send函数向客户端发送响应。这个函数允许你发送文本、HTML内容或其他类型的响应。

1.5.4 处理查询字符串和POST数据

对于更复杂的应用,你可能需要处理查询字符串(URL参数)或POST请求中的数据。ESP-IDF的HTTP服务器API提供了函数来解析这些数据。

1.5.5 停止HTTP服务器

在不再需要HTTP服务器时,你可以停止它释放资源。

使用HTTP服务器,ESP32可以作为一个Web服务器来提供静态页面或动态内容,处理表单数据,甚至是RESTful API接口,非常适合用于物联网项目和设备控制。

二、开启STA+AP模式与启动http服务器

在本次实验中实现如下功能:

  1. 通过STA+AP模式,实现网络中继器,可通过连接esp32的wifi热点实现上网
  2. 通过 http服务器模式,实现路由后台功能,可通过路由器网页控制设备RGB灯的亮度与颜色

2.1 组件添加

可通过 ws2812使用教程这篇文章学习如何使用ws2812 RGB灯

2.2 添加代码

2.2.1 main.c文件修改

此时将main.c文件修改为以下内容

#include <sys/param.h>

#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"

#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "lwip/inet.h"

#include "esp_http_server.h"
#include "dns_server.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "led_strip.h"
#include "esp_task_wdt.h"
#include "lwip/opt.h"

#include "lwip/lwip_napt.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "lwip/inet.h"


#define EXAMPLE_ESP_WIFI_SSID "esp32_will"
#define EXAMPLE_ESP_WIFI_PASS "123456789"
#define EXAMPLE_MAX_STA_CONN 4

// GPIO assignment
#define LED_STRIP_BLINK_GPIO  4
// Numbers of the LED in the strip
#define LED_STRIP_LED_NUMBERS 1
// 10MHz resolution, 1 tick = 0.1us (led strip needs a high resolution)
#define LED_STRIP_RMT_RES_HZ  (10 * 1000 * 1000)

extern const char root_start[] asm("_binary_root_html_start");
extern const char root_end[] asm("_binary_root_html_end");

static const char *TAG = "HK";

static int blink_led = 0; // 控制LED闪烁的变量

int brightness = 0;

uint8_t red = 255;
uint8_t green = 255;
uint8_t blue = 255;

led_strip_handle_t led_strip = NULL;

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
        ESP_LOGI(TAG, "station " MACSTR " join, AID=%d",
                 MAC2STR(event->mac), event->aid);
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
        ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d",
                 MAC2STR(event->mac), event->aid);
    } else if(event_id == IP_EVENT_STA_GOT_IP){
    	ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
    	ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
    } else if(event_id == WIFI_EVENT_STA_START) {
    	esp_err_t status = esp_wifi_connect();
    	ESP_LOGI(TAG, "esp_wifi_connect %d", status);
    }
}


static esp_netif_t* _esp_netif_sta = NULL;
static esp_netif_t* _esp_netif_ap = NULL;

static void wifi_init_softap(void)
{
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    _esp_netif_ap  = esp_netif_create_default_wifi_ap();
    _esp_netif_sta = esp_netif_create_default_wifi_sta();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_got_ip;

    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
														IP_EVENT_STA_GOT_IP,
														&wifi_event_handler,
														NULL,
														&instance_got_ip));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
            .password = EXAMPLE_ESP_WIFI_PASS,
            .max_connection = EXAMPLE_MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK
        },
    };

    wifi_config_t wifi_sta_config = {
        .sta = {
            .ssid = "wifi名称",
            .password = "wifi密码",
            .threshold.authmode = WIFI_AUTH_OPEN,
	        .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
        },
    };

    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_sta_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    esp_netif_ip_info_t ip_info;
    esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);

    char ip_addr[16];
    inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
    ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:'%s' password:'%s'",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
}

// HTTP GET Handler
static esp_err_t root_get_handler(httpd_req_t *req)
{
    const uint32_t root_len = root_end - root_start;

    ESP_LOGI(TAG, "Serve root");
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, root_start, root_len);

    return ESP_OK;
}

static const httpd_uri_t root = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = root_get_handler
};

// 控制LED的处理函数
esp_err_t led_control_handler(httpd_req_t *req) {
    char*  buf;
    size_t buf_len;

    buf_len = httpd_req_get_url_query_len(req) + 1;
    if (buf_len > 1) {
        buf = malloc(buf_len);
        if(httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
            char param[32];
            if (httpd_query_key_value(buf, "state", param, sizeof(param)) == ESP_OK) {
                if (strcmp(param, "on") == 0) {
                    blink_led = 1;
                    ESP_LOGI(TAG, "on");
                    // gpio_set_level(LED_GPIO, 1); // 打开LED
                } else if (strcmp(param, "off") == 0) {
                    blink_led = 0;
                    ESP_LOGI(TAG, "off");
                    // gpio_set_level(LED_GPIO, 0); // 关闭LED
                }
            }
        }
        free(buf);
    }
    httpd_resp_send(req, NULL, 0); // 发送空响应
    return ESP_OK;
}

// 获取LED状态的处理函数
esp_err_t get_led_status_handler(httpd_req_t *req) {

    httpd_resp_send(req, blink_led?"LED is ON":"LED is OFF", HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

esp_err_t set_led_brightness_handler(httpd_req_t *req) {
    char buf[100];
    int ret;

    // 从请求中读取亮度值
    ret = httpd_req_recv(req, buf, sizeof(buf));
    if (ret <= 0) { // 错误或连接关闭
        if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
            httpd_resp_send_408(req);
        }
        return ESP_FAIL;
    }
    buf[ret] = '\0'; // 确保字符串终止

    // 解析亮度值
    sscanf(buf, "brightness=%d", &brightness);
    ESP_LOGI(TAG, "brightness=%d",brightness);

    // 发送响应
    httpd_resp_send(req, "Brightness updated", HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

esp_err_t set_color_handler(httpd_req_t *req) {
    char* buf;
    size_t buf_len;

    buf_len = httpd_req_get_url_query_len(req) + 1;
    if (buf_len > 1) {
        buf = malloc(buf_len);
        if(!buf){
            httpd_resp_send_500(req);
            return ESP_FAIL;
        }
        if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
            // 解析RGB值
            sscanf(buf, "r=%hhu&g=%hhu&b=%hhu", &red, &green, &blue);
            ESP_LOGI(TAG, "r=%hhu&g=%hhu&b=%hhu",red, green, blue);
        }
        free(buf);
    }
    httpd_resp_send(req, "Color set", HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

// URI结构体
httpd_uri_t led_control = {
    .uri       = "/control",
    .method    = HTTP_GET,
    .handler   = led_control_handler,
    .user_ctx  = NULL
};

httpd_uri_t uri_set_color = {
    .uri       = "/set-color",
    .method    = HTTP_GET,
    .handler   = set_color_handler,
    .user_ctx  = NULL
};

httpd_uri_t get_status = {
    .uri       = "/status",
    .method    = HTTP_GET,
    .handler   = get_led_status_handler,
    .user_ctx  = NULL
};

httpd_uri_t set_led_brightness = {
    .uri       = "/set-brightness",
    .method    = HTTP_POST,
    .handler   = set_led_brightness_handler,
    .user_ctx  = NULL
};



// HTTP Error (404) Handler - Redirects all requests to the root page
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
    // Set status
    httpd_resp_set_status(req, "302 Temporary Redirect");
    // Redirect to the "/" root directory
    httpd_resp_set_hdr(req, "Location", "/");
    // iOS requires content in the response to detect a captive portal, simply redirecting is not sufficient.
    httpd_resp_send(req, "Redirect to the captive portal", HTTPD_RESP_USE_STRLEN);

    ESP_LOGI(TAG, "Redirecting to root");
    return ESP_OK;
}

static httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_open_sockets = 13;
    config.lru_purge_enable = true;

    // Start the httpd server
    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK) {
        // Set URI handlers
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &root);
        httpd_register_uri_handler(server, &led_control);
        httpd_register_uri_handler(server, &get_status);
        httpd_register_uri_handler(server, &set_led_brightness);
        httpd_register_uri_handler(server, &uri_set_color);
        httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
    }
    return server;
}

led_strip_handle_t configure_led(void)
{
    // LED strip general initialization, according to your led board design
    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_STRIP_BLINK_GPIO,   // The GPIO that connected to the LED strip's data line
        .max_leds = LED_STRIP_LED_NUMBERS,        // The number of LEDs in the strip,
        .led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
        .led_model = LED_MODEL_WS2812,            // LED strip model
        .flags.invert_out = false,                // whether to invert the output signal
    };

    // LED strip backend configuration: RMT
    led_strip_rmt_config_t rmt_config = {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
        .rmt_channel = 0,
#else
        .clk_src = RMT_CLK_SRC_DEFAULT,        // different clock source can lead to different power consumption
        .resolution_hz = LED_STRIP_RMT_RES_HZ, // RMT counter clock frequency
        .flags.with_dma = false,               // DMA feature is available on ESP target like ESP32-S3
#endif
    };

    // LED Strip object handle
    led_strip_handle_t led_strip;
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
    ESP_LOGI(TAG, "Created LED strip object with RMT backend");
    return led_strip;
}

// LED闪烁任务
void blink_task(void *pvParameter) {
    // gpio_pad_select_gpio(LED_GPIO);
    // gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
    ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
    while(1) {
        if (blink_led) {
                    uint8_t reds = (red * brightness) / 100;
                    uint8_t greens = (green * brightness) / 100;
                    uint8_t blues = (blue * brightness) / 100;

                    ESP_ERROR_CHECK(led_strip_set_pixel(led_strip, 0, reds, greens, blues));
                    /* Refresh the strip to send data */
                    ESP_ERROR_CHECK(led_strip_refresh(led_strip));
        } else {
            
            ESP_ERROR_CHECK(led_strip_clear(led_strip));
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        esp_task_wdt_reset();
    }
    // 在任务即将结束时移除任务看门狗监控
    ESP_ERROR_CHECK(esp_task_wdt_delete(NULL));

}

void app_main(void)
{
    /*
        Turn of warnings from HTTP server as redirecting traffic will yield
        lots of invalid requests
    */
    esp_log_level_set("httpd_uri", ESP_LOG_ERROR);
    esp_log_level_set("httpd_txrx", ESP_LOG_ERROR);
    esp_log_level_set("httpd_parse", ESP_LOG_ERROR);

    led_strip = configure_led();


    // Initialize networking stack
    ESP_ERROR_CHECK(esp_netif_init());

    // Create default event loop needed by the  main app
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Initialize NVS needed by Wi-Fi
    ESP_ERROR_CHECK(nvs_flash_init());

    // Initialize Wi-Fi including netif with default config
    // esp_netif_create_default_wifi_ap();

    // esp_netif_create_default_wifi_sta();

    // Initialise ESP32 in SoftAP mode
    wifi_init_softap();

    #if IP_NAPT
        u32_t napt_netif_ip = 0xC0A80401; // Set to ip address of softAP netif (Default is 192.168.4.1)
        ip_napt_enable(htonl(napt_netif_ip), 1);
        ESP_LOGI(TAG, "------------------------>NAT is enabled");
    #endif

    // Start the server for the first time
    start_webserver();

    // // Start the DNS server that will redirect all queries to the softAP IP
    // start_dns_server();

    xTaskCreate(&blink_task, "blink_task", 2048, NULL, 5, NULL);
}

2.2.2 网页代码添加

在main文件夹下新建一个root.html文件,用于存放网页前端代码

<!DOCTYPE html>
<html>
<head>
    <title>LED Control and Status</title>
</head>
<body>
    <h2>Control LED</h2>
    <button onclick="setLed('on')">Turn On</button>
    <button onclick="setLed('off')">Turn Off</button>
    <h2>LED Brightness Control</h2>
    <input type="range" min="0" max="100" value="0" onchange="adjustBrightness(this.value)" id="brightnessSlider">
    <h2>LED Status: <span id="ledStatus">Unknown</span></h2>
    <h2>WS2812 Color Control</h2>
    <div>R: <input type="range" min="0" max="255" oninput="setColor()" id="red"></div>
    <div>G: <input type="range" min="0" max="255" oninput="setColor()" id="green"></div>
    <div>B: <input type="range" min="0" max="255" oninput="setColor()" id="blue"></div>
    <script>
    function setLed(state) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/control?state=" + state, true);
        xhr.send();
    }

    function fetchLedStatus() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4 && xhr.status == 200) {
                document.getElementById("ledStatus").innerHTML = xhr.responseText;
            }
        };
        xhr.open("GET", "/status", true);
        xhr.send();
    }

    function adjustBrightness(brightness) {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "/set-brightness", true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr.send("brightness=" + brightness);
    }

    function setColor() {
        var red = document.getElementById("red").value;
        var green = document.getElementById("green").value;
        var blue = document.getElementById("blue").value;
        
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/set-color?r=" + red + "&g=" + green + "&b=" + blue, true);
        xhr.send();
    }

    // 每秒钟查询一次LED状态
    setInterval(fetchLedStatus, 1000);
    </script>
</body>
</html>

2.2.3 dns服务文件添加

在main文件夹下新建一个dns_server.c文件,用于存放dns服务相关代码

/* Captive Portal Example

    This example code is in the Public Domain (or CC0 licensed, at your option.)

    Unless required by applicable law or agreed to in writing, this
    software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
    CONDITIONS OF ANY KIND, either express or implied.
*/

#include <sys/param.h>
#include <inttypes.h>

#include "esp_log.h"
#include "esp_system.h"
#include "esp_netif.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"

#define DNS_PORT (53)
#define DNS_MAX_LEN (256)

#define OPCODE_MASK (0x7800)
#define QR_FLAG (1 << 7)
#define QD_TYPE_A (0x0001)
#define ANS_TTL_SEC (300)

static const char *TAG = "example_dns_redirect_server";

// DNS Header Packet
typedef struct __attribute__((__packed__))
{
    uint16_t id;
    uint16_t flags;
    uint16_t qd_count;
    uint16_t an_count;
    uint16_t ns_count;
    uint16_t ar_count;
} dns_header_t;

// DNS Question Packet
typedef struct {
    uint16_t type;
    uint16_t class;
} dns_question_t;

// DNS Answer Packet
typedef struct __attribute__((__packed__))
{
    uint16_t ptr_offset;
    uint16_t type;
    uint16_t class;
    uint32_t ttl;
    uint16_t addr_len;
    uint32_t ip_addr;
} dns_answer_t;

/*
    Parse the name from the packet from the DNS name format to a regular .-seperated name
    returns the pointer to the next part of the packet
*/
static char *parse_dns_name(char *raw_name, char *parsed_name, size_t parsed_name_max_len)
{

    char *label = raw_name;
    char *name_itr = parsed_name;
    int name_len = 0;

    do {
        int sub_name_len = *label;
        // (len + 1) since we are adding  a '.'
        name_len += (sub_name_len + 1);
        if (name_len > parsed_name_max_len) {
            return NULL;
        }

        // Copy the sub name that follows the the label
        memcpy(name_itr, label + 1, sub_name_len);
        name_itr[sub_name_len] = '.';
        name_itr += (sub_name_len + 1);
        label += sub_name_len + 1;
    } while (*label != 0);

    // Terminate the final string, replacing the last '.'
    parsed_name[name_len - 1] = '\0';
    // Return pointer to first char after the name
    return label + 1;
}

// Parses the DNS request and prepares a DNS response with the IP of the softAP
static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t dns_reply_max_len)
{
    if (req_len > dns_reply_max_len) {
        return -1;
    }

    // Prepare the reply
    memset(dns_reply, 0, dns_reply_max_len);
    memcpy(dns_reply, req, req_len);

    // Endianess of NW packet different from chip
    dns_header_t *header = (dns_header_t *)dns_reply;
    ESP_LOGD(TAG, "DNS query with header id: 0x%X, flags: 0x%X, qd_count: %d",
             ntohs(header->id), ntohs(header->flags), ntohs(header->qd_count));

    // Not a standard query
    if ((header->flags & OPCODE_MASK) != 0) {
        return 0;
    }

    // Set question response flag
    header->flags |= QR_FLAG;

    uint16_t qd_count = ntohs(header->qd_count);
    header->an_count = htons(qd_count);

    int reply_len = qd_count * sizeof(dns_answer_t) + req_len;
    if (reply_len > dns_reply_max_len) {
        return -1;
    }

    // Pointer to current answer and question
    char *cur_ans_ptr = dns_reply + req_len;
    char *cur_qd_ptr = dns_reply + sizeof(dns_header_t);
    char name[128];

    // Respond to all questions with the ESP32's IP address
    for (int i = 0; i < qd_count; i++) {
        char *name_end_ptr = parse_dns_name(cur_qd_ptr, name, sizeof(name));
        if (name_end_ptr == NULL) {
            ESP_LOGE(TAG, "Failed to parse DNS question: %s", cur_qd_ptr);
            return -1;
        }

        dns_question_t *question = (dns_question_t *)(name_end_ptr);
        uint16_t qd_type = ntohs(question->type);
        uint16_t qd_class = ntohs(question->class);

        ESP_LOGD(TAG, "Received type: %d | Class: %d | Question for: %s", qd_type, qd_class, name);

        if (qd_type == QD_TYPE_A) {
            dns_answer_t *answer = (dns_answer_t *)cur_ans_ptr;

            answer->ptr_offset = htons(0xC000 | (cur_qd_ptr - dns_reply));
            answer->type = htons(qd_type);
            answer->class = htons(qd_class);
            answer->ttl = htonl(ANS_TTL_SEC);

            esp_netif_ip_info_t ip_info;
            esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
            ESP_LOGD(TAG, "Answer with PTR offset: 0x%" PRIX16 " and IP 0x%" PRIX32, ntohs(answer->ptr_offset), ip_info.ip.addr);

            answer->addr_len = htons(sizeof(ip_info.ip.addr));
            answer->ip_addr = ip_info.ip.addr;
        }
    }
    return reply_len;
}

/*
    Sets up a socket and listen for DNS queries,
    replies to all type A queries with the IP of the softAP
*/
void dns_server_task(void *pvParameters)
{
    char rx_buffer[128];
    char addr_str[128];
    int addr_family;
    int ip_protocol;

    while (1) {

        struct sockaddr_in dest_addr;
        dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        dest_addr.sin_family = AF_INET;
        dest_addr.sin_port = htons(DNS_PORT);
        addr_family = AF_INET;
        ip_protocol = IPPROTO_IP;
        inet_ntoa_r(dest_addr.sin_addr, addr_str, sizeof(addr_str) - 1);

        int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
        if (sock < 0) {
            ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket created");

        int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (err < 0) {
            ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
        }
        ESP_LOGI(TAG, "Socket bound, port %d", DNS_PORT);

        while (1) {
            ESP_LOGI(TAG, "Waiting for data");
            struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6
            socklen_t socklen = sizeof(source_addr);
            int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);

            // Error occurred during receiving
            if (len < 0) {
                ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
                close(sock);
                break;
            }
            // Data received
            else {
                // Get the sender's ip address as string
                if (source_addr.sin6_family == PF_INET) {
                    inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
                } else if (source_addr.sin6_family == PF_INET6) {
                    inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
                }

                // Null-terminate whatever we received and treat like a string...
                rx_buffer[len] = 0;

                char reply[DNS_MAX_LEN];
                int reply_len = parse_dns_request(rx_buffer, len, reply, DNS_MAX_LEN);

                ESP_LOGI(TAG, "Received %d bytes from %s | DNS reply with len: %d", len, addr_str, reply_len);
                if (reply_len <= 0) {
                    ESP_LOGE(TAG, "Failed to prepare a DNS reply");
                } else {
                    int err = sendto(sock, reply, reply_len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
                    if (err < 0) {
                        ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
                        break;
                    }
                }
            }
        }

        if (sock != -1) {
            ESP_LOGE(TAG, "Shutting down socket");
            shutdown(sock, 0);
            close(sock);
        }
    }
    vTaskDelete(NULL);
}

void start_dns_server(void)
{
    xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 5, NULL);
}

在main文件夹下新建一个include文件夹,用于存放dns服务头文件,文件名为:dns_server.h

/* Captive Portal Example

    This example code is in the Public Domain (or CC0 licensed, at your option.)

    Unless required by applicable law or agreed to in writing, this
    software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
    CONDITIONS OF ANY KIND, either express or implied.
*/

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Set ups and starts a simple DNS server that will respond to all queries
 * with the soft AP's IP address
 *
 */
void start_dns_server(void);


#ifdef __cplusplus
}
#endif

2.2.4 CMakeList.txt文件修改

将添加的文件及文件夹路径添加进去

idf_component_register(
                        SRCS 
                        "main.c" "dns_server.c"
                        INCLUDE_DIRS 
                        "include"
                        EMBED_FILES 
                        root.html
                        )

2.3 实验现象

通过连接wifi进入路由后台网页界面

ESP32 SPIFFS大小 esp32最大flash_笔记


ESP32 SPIFFS大小 esp32最大flash_笔记_02


通过在网页界面配置led灯的亮度,开关以及RGB值,从而实现让RGB灯亮起来

ESP32 SPIFFS大小 esp32最大flash_服务器_03


同时手机等智能设备可通过连接热点实现上网功能

三、代码讲解

3.1 WiFi部分

在main函数中调用了**wifi_init_softap()**函数对wifi进行初始化

esp_netif_create_default_wifi_ap() 函数用于创建默认的 Wi-Fi 接入点(AP)网络接口。
esp_netif_create_default_wifi_sta() 函数用于创建默认的Wi-Fi客户端(STA)网络接口。
esp_wifi_set_mode() 函数用于设置 ESP32 Wi-Fi 的工作模式。
esp_wifi_set_config() 函数用于设置 ESP32 Wi-Fi 接口的配置。
esp_netif_get_ip_info()函数用于获取指定网络接口的 IP 地址信息。

static void wifi_init_softap(void)
{
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    _esp_netif_ap  = esp_netif_create_default_wifi_ap();
    _esp_netif_sta = esp_netif_create_default_wifi_sta();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_got_ip;

    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
														IP_EVENT_STA_GOT_IP,
														&wifi_event_handler,
														NULL,
														&instance_got_ip));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
            .password = EXAMPLE_ESP_WIFI_PASS,
            .max_connection = EXAMPLE_MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK
        },
    };

    wifi_config_t wifi_sta_config = {
        .sta = {
            .ssid = "wifi名称",
            .password = "wifi密码",
            .threshold.authmode = WIFI_AUTH_OPEN,
	        .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
        },
    };

    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_sta_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    esp_netif_ip_info_t ip_info;
    esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);

    char ip_addr[16];
    inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
    ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:'%s' password:'%s'",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
}

3.2 web服务器部分

在main函数中调用了**start_webserver()**函数对web服务器进行初始化
httpd_start()函数用于启动一个 HTTP 服务器实例。
httpd_register_uri_handler()函数用于向 ESP32 HTTP 服务器注册一个 URI 处理程序。

static httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_open_sockets = 13;
    config.lru_purge_enable = true;

    // Start the httpd server
    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK) {
        // Set URI handlers
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &root);
        httpd_register_uri_handler(server, &led_control);
        httpd_register_uri_handler(server, &get_status);
        httpd_register_uri_handler(server, &set_led_brightness);
        httpd_register_uri_handler(server, &uri_set_color);
        httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
    }
    return server;
}

通过httpd_uri_t结构体设置相应路径的回调函数及请求方式

httpd_uri_t set_led_brightness = {
    .uri       = "/set-brightness",
    .method    = HTTP_POST,
    .handler   = set_led_brightness_handler,
    .user_ctx  = NULL
};

在回调函数中对相应数据进行处理

esp_err_t set_led_brightness_handler(httpd_req_t *req) {
    char buf[100];
    int ret;

    // 从请求中读取亮度值
    ret = httpd_req_recv(req, buf, sizeof(buf));
    if (ret <= 0) { // 错误或连接关闭
        if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
            httpd_resp_send_408(req);
        }
        return ESP_FAIL;
    }
    buf[ret] = '\0'; // 确保字符串终止

    // 解析亮度值
    sscanf(buf, "brightness=%d", &brightness);
    ESP_LOGI(TAG, "brightness=%d",brightness);

    // 发送响应
    httpd_resp_send(req, "Brightness updated", HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

3.2 网页部分

网页部分主要是前端html,js代码,在这里只做演示,未对界面进行设计,css较少提及
前端代码构成都是基础元件,相应元件作用可自行百度

<!DOCTYPE html>
<html>
<head>
    <title>LED Control and Status</title>
</head>
<body>
    <h2>Control LED</h2>
    <button onclick="setLed('on')">Turn On</button>
    <button onclick="setLed('off')">Turn Off</button>
    <h2>LED Brightness Control</h2>
    <input type="range" min="0" max="100" value="0" onchange="adjustBrightness(this.value)" id="brightnessSlider">
    <h2>LED Status: <span id="ledStatus">Unknown</span></h2>
    <h2>WS2812 Color Control</h2>
    <div>R: <input type="range" min="0" max="255" oninput="setColor()" id="red"></div>
    <div>G: <input type="range" min="0" max="255" oninput="setColor()" id="green"></div>
    <div>B: <input type="range" min="0" max="255" oninput="setColor()" id="blue"></div>
    
    <script>
    function setLed(state) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/control?state=" + state, true);
        xhr.send();
    }

    function fetchLedStatus() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4 && xhr.status == 200) {
                document.getElementById("ledStatus").innerHTML = xhr.responseText;
            }
        };
        xhr.open("GET", "/status", true);
        xhr.send();
    }

    function adjustBrightness(brightness) {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "/set-brightness", true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr.send("brightness=" + brightness);
    }

    function setColor() {
        var red = document.getElementById("red").value;
        var green = document.getElementById("green").value;
        var blue = document.getElementById("blue").value;
        
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/set-color?r=" + red + "&g=" + green + "&b=" + blue, true);
        xhr.send();
    }

    // 每秒钟查询一次LED状态
    setInterval(fetchLedStatus, 1000);
    </script>
    
</body>
</html>

3.3 RGB灯任务

在main函数中调用了**xTaskCreate()**函数创建一个blink_task任务

xTaskCreate(&blink_task, "blink_task", 2048, NULL, 5, NULL);

在任务中通过对相应的状态标识位进行判断从而实现了对RGB灯的实时控制

// LED任务
void blink_task(void *pvParameter) {
    // gpio_pad_select_gpio(LED_GPIO);
    // gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
    ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
    while(1) {
        if (blink_led) {
                    uint8_t reds = (red * brightness) / 100;
                    uint8_t greens = (green * brightness) / 100;
                    uint8_t blues = (blue * brightness) / 100;

                    ESP_ERROR_CHECK(led_strip_set_pixel(led_strip, 0, reds, greens, blues));
                    /* Refresh the strip to send data */
                    ESP_ERROR_CHECK(led_strip_refresh(led_strip));
        } else {
            
            ESP_ERROR_CHECK(led_strip_clear(led_strip));
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        esp_task_wdt_reset();
    }
    // 在任务即将结束时移除任务看门狗监控
    ESP_ERROR_CHECK(esp_task_wdt_delete(NULL));

}

四、代码地址

Github仓库:AP网页控制