主要参考资料: 乐鑫官方ESP-IDF文档 HTTP服务器: https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-reference/protocols/esp_http_server.html# NVS入门(基于ESP-IDF): https://blog.csdn.net/qq_40773212/article/details/135595361
目录
- 一、简介
- 二、HTTP Server参考代码
- 三、用户在Web网页上输入Wi-Fi账号密码
-
- 3.1 NVS检查账号密码
- 3.2 Wi-Fi连接
- 3.3 运行HTTP Server
- 3.4 运行DNS服务器
- 3.5 处理HTTP请求的函数
一、简介
HTTP Server 组件提供了在 ESP32 上运行轻量级 Web 服务器的功能。
二、HTTP Server参考代码
/* URI 处理函数,在客户端发起 GET /uri 请求时被调用 */
esp_err_t get_handler(httpd_req_t *req)
{
/* 发送回简单的响应数据包 */
const char resp[] = "URI GET Response";
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
/* URI 处理函数,在客户端发起 POST/uri 请求时被调用 */
esp_err_t post_handler(httpd_req_t *req)
{
/* 定义 HTTP POST 请求数据的目标缓存区
* httpd_req_recv() 只接收 char* 数据,但也可以是
* 任意二进制数据(需要类型转换)
* 对于字符串数据,null 终止符会被省略,
* content_len 会给出字符串的长度 */
char content[100];
/* 如果内容长度大于缓冲区则截断 */
size_t recv_size = MIN(req->content_len, sizeof(content));
int ret = httpd_req_recv(req, content, recv_size);
if (ret <= 0) { /* 返回 0 表示连接已关闭 */
/* 检查是否超时 */
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
/* 如果是超时,可以调用 httpd_req_recv() 重试
* 简单起见,这里我们直接
* 响应 HTTP 408(请求超时)错误给客户端 */
httpd_resp_send_408(req);
}
/* 如果发生了错误,返回 ESP_FAIL 可以确保
* 底层套接字被关闭 */
return ESP_FAIL;
}
/* 发送简单的响应数据包 */
const char resp[] = "URI POST Response";
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
/* GET /uri 的 URI 处理结构 */
httpd_uri_t uri_get = {
.uri = "/uri",
.method = HTTP_GET,
.handler = get_handler,
.user_ctx = NULL
};
/* POST/uri 的 URI 处理结构 */
httpd_uri_t uri_post = {
.uri = "/uri",
.method = HTTP_POST,
.handler = post_handler,
.user_ctx = NULL
};
/* 启动 Web 服务器的函数 */
httpd_handle_t start_webserver(void)
{
/* 生成默认的配置参数 */
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
/* 置空 esp_http_server 的实例句柄 */
httpd_handle_t server = NULL;
/* 启动 httpd server */
//创建 HTTP 服务器的实例,根据具体的配置为其分配内存和资源,并返回该服务器实例的句柄。服务器使用了两个套接字,一个用来监听 HTTP 流量(TCP 类型),另一个用来处理控制信号(UDP 类型),它们在服务器的任务循环中轮流使用。通过向 httpd_start() 传递 httpd_config_t 结构体,可以在创建服务器实例时配置任务的优先级和堆栈的大小。TCP 流量被解析为 HTTP 请求,根据请求的 URI 来调用用户注册的处理程序,在处理程序中需要发送回 HTTP 响应数据包。
if (httpd_start(&server, &config) == ESP_OK) {
/* 注册 URI 处理程序 */
//通过传入 httpd_uri_t 结构体类型的对象来注册 URI 处理程序。该结构体包含如下成员:uri 名字,method 类型(比如HTTPD_GET/HTTPD_POST/HTTPD_PUT 等等), esp_err_t *handler (httpd_req_t *req) 类型的函数指针,指向用户上下文数据的 user_ctx 指针。
httpd_register_uri_handler(server, &uri_get);
httpd_register_uri_handler(server, &uri_post);
}
/* 如果服务器启动失败,返回的句柄是 NULL */
return server;
}
/* 停止 Web 服务器的函数 */
void stop_webserver(httpd_handle_t server)
{
if (server) {
/* 停止 httpd server */
httpd_stop(server);
}
}
三、用户在Web网页上输入Wi-Fi账号密码
3.1 NVS检查账号密码
//ui显示操作界面
ui_init();
//查看NVS有无存储账号
nvs_handle_t my_nvs_handle;
err = nvs_open("WI-FI", NVS_READWRITE, &my_nvs_handle);
if (err != ESP_OK) {
printf("Handle error\\n");
// Handle error
} else {
// Check if key exists
err = nvs_get_str(my_nvs_handle, "SSID", SSID_value, &SSID_size);
err = nvs_get_str(my_nvs_handle, "PWD", PWD_value, &PWD_size);
if (err == ESP_OK) {
//有账号密码,就直接连接并且显示连接界面
ESP_LOGI(TAG, "Value of 'SSID' is: %s", SSID_value);
ESP_LOGI(TAG, "Value of 'PWD' is: %s", PWD_value);
lv_disp_load_scr(ui_WiFiconnect);
} else if (err == ESP_ERR_NVS_NOT_FOUND) {
// The key does not exist
ESP_LOGI(TAG, "Key does not exist in NVS.");
lv_disp_load_scr(ui_WiFireset);
webConfigWifiLoop();
}
// Close the NVS handle when done
nvs_close(my_nvs_handle);
}
3.2 Wi-Fi连接
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && 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));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(char* SSID, char* PWD)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
// .ssid = SSID,
// .password = PWD,
/* Setting a password implies station will connect to all security modes including WEP/WPA.
* However these modes are deprecated and not advisable to be used. Incase your Access point
* doesn't support WPA2, these mode can be enabled by commenting below line */
.threshold.authmode = WIFI_AUTH_OPEN
}
};
memcpy(wifi_config.sta.ssid, SSID, strlen(SSID)); // 复制SSID,不包括 '\\0'
memcpy(wifi_config.sta.password, PWD, strlen(PWD)); // 复制PWD,不包括 '\\0'
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
/* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
* happened. */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
SSID, PWD);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
SSID, PWD);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
}
3.3 运行HTTP Server
void webConfigWifiLoop(void)
{
webConfigWifiInit();
/* Allocate memory for server data */
server_data = calloc(1, sizeof(struct file_server_data));
if (!server_data) {
printf("\\r\\nFailed to allocate memory for server data\\r\\n");
return;
}
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = 1;
config.backlog_conn = 1;
config.lru_purge_enable = true;
config.max_uri_handlers = 15;
config.stack_size = 8192;
// config.server_port = 89;
config.uri_match_fn = httpd_uri_match_wildcard;
printf("Starting Web server on port: %d\\r\\n", config.server_port);
if (httpd_start(&webConfigWifi_httpd, &config) == ESP_OK)
{
static httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = main_page_handler,
.user_ctx = NULL
};
root.user_ctx = server_data;
httpd_register_uri_handler(webConfigWifi_httpd, &root);
static httpd_uri_t wifi_data = {
.uri = "/wifi_data",
.method = HTTP_POST,
.handler = wifi_config_handler,
.user_ctx = NULL
};
wifi_data.user_ctx = server_data;
httpd_register_uri_handler(webConfigWifi_httpd, &wifi_data);
httpd_register_err_handler(webConfigWifi_httpd, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
start_dns_server();
while(1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
3.4 运行DNS服务器
在这部分,DNS服务器被设置为监听DNS查询,并针对所有类型A(A记录,用于IPv4地址解析)的查询回复软AP(Soft Access Point)的IP地址。
/*
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);
}
3.5 处理HTTP请求的函数
函数的主要任务是发送一个预压缩的HTML文件(webConfig.html)给客户端。
//配置 页面
static esp_err_t main_page_handler(httpd_req_t *req)
{
//设置HTTP响应头,允许跨域请求。这里的"*"表示允许所有域进行跨域请求。
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
//声明外部变量html_gz_start和html_gz_end,它们指向压缩后的HTML文件的开始位置和结束位置。
extern const unsigned char html_gz_start[] asm("_binary_webConfig_html_gz_start");
extern const unsigned char html_gz_end[] asm("_binary_webConfig_html_gz_end");
//计算压缩后的HTML文件的长度。
size_t html_gz_len = html_gz_end – html_gz_start;
//设置HTTP响应的内容类型为text/html。
httpd_resp_set_type(req, "text/html");
//设置HTTP响应头,告诉客户端内容是使用GZIP压缩的。
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
//这行代码被注释掉了,如果取消注释,它将设置响应头以支持GZIP和DEFLATE压缩。
// httpd_resp_set_hdr(req, "Content-Encoding", "gzip,deflate");
//发送压缩后的HTML文件内容作为HTTP响应。
return httpd_resp_send(req, (const char *)html_gz_start, html_gz_len);
}
评论前必须登录!
注册