ESP32在IDF v5.3.1版本下建立一键WEB配置WIFI联网

一、背景

        虽然乐鑫官方提供了EspTouch软件来实现一键配置ESP32去连接目标AP实现联网,但是终究还是没有使用WEB网页后台实现一键配置WIFI联网来的方便高效,且跨平台。

二、实现思路

       ESP32在未连接 WiFi 时自动开启热点 AP →用户浏览器访问 ESP32 页面 →用户 输入 SSID/密码 → ESP32自动连接用户WEB设置的目标WiFi → 连接成功保存配置到NVS → 下次启动先检查和读取NVS里面的WIFI配置,无需配网。 

三、实现流程

1.ESP32实现开启AP热点模式

        这个部分我已经在其他博客已经详细实现,请移步查看:

ESP32 在IDF_V5.3.1版本下实现AP无线热点模式!(带WIFI事件处理)_esp32 ap模式-CSDN博客https://blog.csdn.net/qq_34885669/article/details/145736280?spm=1001.2014.3001.5502

2.ESP32搭建Web Server实现WEB配网

(1)开启esp32的menuconfig里面的http相关配置

        HTTP Request Header 和 HTTP URI 长度根据需要更改,以保证HTTP的报头发出不报错。目前我项目设置1024足够。

        还有就是在工程的CmakeList里面我们还需要添加 mdnsmbedtls 2个依赖,以确保后面使用http创建服务端的时候不会出现问题:

(2)WEB网页的实现和调用

       网页文件的内容本质上是很长的字符串,那最简单的方法就是定义一个字符串数组,将数组内容填充为网页内容。例如:

const char  index_string[] =
" \
 \
 \
wifi config \
";

        显然对于复杂一点的网页,这种方式显然就不太方便。要是能直接将用html工具设计生成的.htm格式的文件直接编译那不是更好了。这就是另外一种比较推荐的方式:使用 embed_files 将网页嵌入固件。

ESP-IDF 的 embed_files会在构建时把指定文件原样打包到固件中,生成 C 字节数组,并提供符号(链接符号)让 C 程序直接访问。

第一步:编写WEB网页的html文件

        我们需要新建一个WEB网页的html文件,用于编写存放我们WEB网页的html网页文件:例如我的命名为wifi.html。

        备注:至于网页怎么写,我也不怎么会,我也是GPT的。

我的 wifi.html 文件:



	
		
		
		ESP32 WEB CONFIG STA
		
	
	
		

参 数 配 置


注意事项:

1、WiFi名称不能长度不能超过 32 bytes。

2、WiFi密码不能长度不能超过 64 bytes。

第二步:编译链接WEB网页的html文件

        修改CMakeList.txt文件,添加EMBED_FILES,把我们刚才写好的网页文件链接进去:

 第三步:调用编译出来的网页文件。

        编译出来一般名称是默认的_binary_名称_类型_start,这个指针代编译出来文件的起始地址。_binary_名称_类型_end,代表结束地址。

        例如:我的网页文件时是wifi.html,那我的引用方式如下:

extern const unsigned char upload_script_start[] asm("_binary_wifi_html_start");
extern const unsigned char upload_script_end[] asm("_binary_wifi_html_end");
const uint32_t upload_script_size = (upload_script_end - upload_script_start);
/*
asm("_binary_wifi_html_start") 和 asm("_binary_wifi_html_end") 是使用了 GCC 的扩展,
通过 asm 关键字将变量与链接器脚本中定义的特殊符号关联起来。
通常在嵌入式系统或资源受限的环境中,使用 _binary__start 和 _binary__end 这样的符号,
这些符号是由编译器在将二进制文件嵌入到可执行文件时自动生成的,用来表示嵌入文件的起始地址和结束地址。
将一个二进制文件(如 wifi_html)嵌入到你的可执行程序中时,你可以使用这样的代码来访问该文件的内容。
    */

(3)WEB网页的http服务端实现

        http服务端主要就是启动 HTTP 服务器,实现web网页的链接调用,处理 GET/POST 请求。用户通过WEB网页配置的wifi和密码就会post到ESP32里面运行的http服务端接收到。

#include"http_server.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "sys/param.h"
#include"string.h"
/*
一个简单的示例,演示如何创建GET和POST
 */
char wifi_name[32]={0};
char wifi_password[64]={0};
extern SemaphoreHandle_t web_cfg_sta_sem;
static const char *TAG = "HTTP-SERVER";
static esp_err_t echo_post_handler(httpd_req_t *req);
static esp_err_t index_get_handler(httpd_req_t *req);
//static void connect_handler(void* arg, esp_event_base_t event_base,uint32_t event_id, void* event_data);
/*
GET请求服务处理函数
*/
static esp_err_t index_get_handler(httpd_req_t *req)
{
    /*
    asm("_binary_wifi_html_start") 和 asm("_binary_wifi_html_end") 是使用了 GCC 的扩展,
    通过 asm 关键字将变量与链接器脚本中定义的特殊符号关联起来。
    通常在嵌入式系统或资源受限的环境中,使用 _binary__start 和 _binary__end 这样的符号,
    这些符号是由编译器在将二进制文件嵌入到可执行文件时自动生成的,用来表示嵌入文件的起始地址和结束地址。
    将一个二进制文件(如 wifi_html)嵌入到你的可执行程序中时,你可以使用这样的代码来访问该文件的内容。
    */
    extern const unsigned char upload_script_start[] asm("_binary_wifi_html_start");
    extern const unsigned char upload_script_end[] asm("_binary_wifi_html_end");
    const uint32_t upload_script_size = (upload_script_end - upload_script_start);
    httpd_resp_set_type(req,HTTPD_TYPE_TEXT); //设置GET请求响应的“内容类型”字段。默认内容类型为“text/html”。
    httpd_resp_send(req, (const char *)upload_script_start, upload_script_size); //发送GET请求响应的内容
    return ESP_OK;
}
const char  index_string[] =
" \
 \
 \
wifi config \
";
httpd_uri_t index_page = {
    .uri       = "/",    //192.168.4.1
    .method    = HTTP_GET,  //定义该页面触发的方法
    .handler   = index_get_handler, //定义该页面的函数,访问了url连接,该函数就会执行。
    .user_ctx  = NULL //指向服务处理函数可用的用户上下文数据的指针。
};
/*
客户端发送POST请求时的服务处理函数
*/
static esp_err_t echo_post_handler(httpd_req_t *req)
{
    esp_err_t error;
    char buf[100];
    int ret, remaining = req->content_len;
    while (remaining > 0)
    {
        if ((ret = httpd_req_recv(req, buf,MIN(remaining, sizeof(buf)))) <= 0)  //从web客户端的POST请求里面读取数据。
        {
            if (ret == HTTPD_SOCK_ERR_TIMEOUT)
            {
                continue;
            }
            return ESP_FAIL;
        }
        /* Send back the same data */
        httpd_resp_send_chunk(req, "ESP web server received data: ", sizeof("ESP web server received data: "));
        httpd_resp_send_chunk(req, buf, ret);
        remaining -= ret;
        memset(wifi_name,0,sizeof(wifi_name));
        memset(wifi_password,0,sizeof(wifi_password));
        error = httpd_query_key_value(buf,"ssid",wifi_name,sizeof(wifi_name));
        if(ESP_OK != error)
        {
            ESP_LOGE(TAG, "query ssid error:%d",error);
            return ESP_FAIL;
        }
        error = httpd_query_key_value(buf,"password",wifi_password,sizeof(wifi_password));
        if(ESP_OK != error)
        {
            ESP_LOGE(TAG, "query password error:%d",error);
            return ESP_FAIL;
        }
        ESP_LOGI(TAG, "================ RECEIVED DATA ===============");
        ESP_LOGI(TAG, "%.*s", ret, buf);
        ESP_LOGI(TAG, "==============================================");
    }
    // End response
    httpd_resp_send_chunk(req, NULL, 0);
    if(strcmp(wifi_name ,"\0")!=0 && strcmp(wifi_password,"\0")!=0)
    {
        xSemaphoreGive(web_cfg_sta_sem);
        ESP_LOGI(TAG, "set wifi name and password successfully! goto station mode");
    }
    return ESP_OK;
}
static const httpd_uri_t echo =
{
    .uri       = "/",
    .method    = HTTP_POST, //定义该页面触发的方法
    .handler   = echo_post_handler, //客户端发送 POST 请求到该 url 时,该函数就会执行。
    .user_ctx  = NULL //指向服务处理函数可用的用户上下文数据的指针。
};
/*
    启动配置WiFi的web服务器
*/
httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    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, &connect);
        httpd_register_uri_handler(server, &echo);
        httpd_register_uri_handler(server, &index_page);
        #if CONFIG_EXAMPLE_BASIC_AUTH
        httpd_register_basic_auth(server);
        #endif
        return server;
    }
    ESP_LOGI(TAG, "Error starting server!");
    return NULL;
}
/*
static const httpd_uri_t connect =
{
    .uri       = "/",
    .method    = HTTP_CONNECT, //定义该页面触发的方法
    .handler   = connect_handler, //客户端C连接服务端时,该函数就会执行。
    .user_ctx  = NULL, //指向服务处理函数可用的用户上下文数据的指针。
};
*/
/*
    停止配置WiFi的web服务器
*/
static void stop_webserver(httpd_handle_t server)
{
    httpd_stop(server);//Stop the httpd server
}
static void disconnect_handler(void* arg, esp_event_base_t event_base,uint32_t event_id, void* event_data)
{
    httpd_handle_t* server = (httpd_handle_t*) arg;
    if (*server)
    {
        ESP_LOGI(TAG, "Stopping webserver");
        stop_webserver(*server);
        *server = NULL;
    }
}
/*
客户端连接web服务器后的服务处理函数
*/
static void connect_handler(void* arg, esp_event_base_t event_base,uint32_t event_id, void* event_data)
{
    httpd_handle_t* server = (httpd_handle_t*) arg;
    if (*server == NULL)
    {
        ESP_LOGI(TAG, "Starting webserver");
        *server = start_webserver();
    }
}

3.ESP32实现STA模式连接WEB配置的目标AP

        上一步我们已经实现了通过WEB服务端获取到了用户设置的目标AP的名称和密码,接下来我们就需要将ESP32配置为STA模式去连接目标AP。

        详细的STA模式连接目标AP的实现请查看我下面的这个博客:

ESP32 在 IDF_V5.3.1 版本下实现 STA 模式!(带WIFI事件处理)_esp32 sta模式-CSDN博客https://blog.csdn.net/qq_34885669/article/details/145747965?spm=1001.2014.3001.5502

4.ESP32实现NVS保存和读取配置

#include "bsp_nvs.h"
#include"bsp_wifi_smart_config_sta.h"
static const char *TAG_STA = "WIFI_CONFIG_STA";
esp_err_t nvs_init(void)
{
    esp_err_t ret;
    ret = nvs_flash_init(); /* 初始化 NVS 第一次调用时,它会加载存储区域。如果存储区域有问题或不兼容,会抛出 ESP_ERR_NVS_NO_FREE_PAGES 或 ESP_ERR_NVS_NEW_VERSION_FOUND 错误。此时,我们会擦除存储并重新初始化。 */
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    return ret;
}
// 存储 Wi-Fi 配置信息到 NVS
esp_err_t save_wifi_config(const char* ssid, const char* password)
{
    nvs_handle_t nvs_handle;
    esp_err_t ret = 0;
    ret = nvs_open("storage", NVS_READWRITE, &nvs_handle); // 打开 NVS
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error opening NVS: %s", esp_err_to_name(ret));
        return ret;
    }
    ret = nvs_set_str(nvs_handle, WIFI_SSID_KEY, ssid); // 写入 SSID
    if(ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error writing SSID to NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    ret = nvs_set_str(nvs_handle, WIFI_PASSWORD_KEY, password); // 写入密码
    if(ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error writing password to NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    ret = nvs_set_u8(nvs_handle, WIFI_SMART_CONFIG_KEY,1); //写入smart_config 标志
    if(ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error writing smart_config to NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    ret = nvs_commit(nvs_handle); // 提交数据
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error committing to NVS: %s", esp_err_to_name(ret));
    }
    nvs_close(nvs_handle); // 关闭 NVS
    ESP_LOGI("NVS", "save wifi_config to NVS : ssid = %s  passwd = %s  smart_config = %d ",ssid,password);
    return ESP_OK;
}
// 从 NVS 读取 Wi-Fi 配置信息
esp_err_t read_wifi_config(char* ssid, char* password, uint8_t* config_flag,uint32_t max_len)
{
    esp_err_t ret;
    nvs_handle_t nvs_handle;
    *config_flag = 0;
    ret = nvs_open("storage", NVS_READONLY, &nvs_handle);
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error opening NVS: %s", esp_err_to_name(ret));
        return ret;
    }
    ret = nvs_get_u8(nvs_handle, WIFI_SMART_CONFIG_KEY, config_flag); // 读取smart_config 标志
    if (ret == ESP_OK)
    {
        *config_flag = (*config_flag != 0);  // 转换为布尔值
        ESP_LOGI(TAG_STA, "config_flag value read from NVS: %s", *config_flag ? "true" : "false");
        if(config_flag == 0) return ESP_OK;
    }
    else if(ret == ESP_ERR_NVS_NOT_FOUND)
    {
        ESP_LOGW(TAG_STA, "config_flag not found in NVS, setting to default false.");
        *config_flag = false;  // 默认值
    }
    else
    {
        ESP_LOGE(TAG_STA, "Error reading config_flag from NVS: %s", esp_err_to_name(ret));
    }
    ret = nvs_get_str(nvs_handle, WIFI_SSID_KEY, ssid, &max_len);// 读取 SSID
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error reading SSID from NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    ret = nvs_get_str(nvs_handle, WIFI_PASSWORD_KEY, password, &max_len);// 读取密码
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error reading password from NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    nvs_close(nvs_handle);
    ESP_LOGI("NVS", "read wifi_config from NVS : ssid = %s  passwd = %s  smart_config = %d ",ssid,password,*config_flag);
    return ESP_OK;
}
esp_err_t clear_SmartConfigSTAFlag(void)
{
    nvs_handle_t nvs_handle;
    esp_err_t ret = nvs_open("storage", NVS_READWRITE, &nvs_handle); // 打开 NVS
    if (ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error opening NVS: %s", esp_err_to_name(ret));
        return ret;
    }
    ret = nvs_set_u8(nvs_handle, WIFI_SMART_CONFIG_KEY,0); //写入smart_config 标志
    if(ret != ESP_OK)
    {
        ESP_LOGE("NVS", "Error writing smart_config=0 to NVS: %s", esp_err_to_name(ret));
        nvs_close(nvs_handle);
        return ret;
    }
    else
    {
        ESP_LOGI("NVS", "set smart_config=0 to NVS");
        return ESP_OK;
    }
}

posted @ 2026-01-08 19:42  yangykaifa  阅读(31)  评论(0)    收藏  举报