1. 环境准备与项目创建

在开始构建ESP32 HTTP服务器之前,我们需要准备好开发环境。ESP-IDF是乐鑫官方提供的物联网开发框架,它包含了构建HTTP服务器所需的所有组件和工具链。我推荐使用VSCode加上ESP-IDF插件,这样调试和开发都会方便很多。

首先确保你已经安装了ESP-IDF环境,如果还没安装,可以去乐鑫官网下载安装器,一键安装非常方便。安装完成后,打开终端,创建一个新项目:

idf.py create-project http_server_demo
cd http_server_demo

接下来我们需要配置项目组件。在ESP-IDF中,HTTP服务器功能是由esp_http_server组件提供的,这个组件默认已经包含在框架中,我们只需要在代码中引用即可。

打开项目中的main目录,你会看到main.c文件,这就是我们编写主要代码的地方。我习惯先配置Wi-Fi连接,因为HTTP服务器需要网络才能访问。在main.c中添加Wi-Fi配置代码:

#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

void wifi_init_sta(void)
{
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "你的WiFi名称",
            .password = "你的WiFi密码",
        },
    };
    
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
}

记得把SSID和密码替换成你自己的Wi-Fi信息。Wi-Fi连接成功后,ESP32会获取到一个IP地址,这个地址就是我们后续访问Web控制台的地址。

2. HTTP服务器核心配置

配置好网络后,我们就可以开始创建HTTP服务器了。ESP-IDF的HTTP服务器组件设计得很简洁,主要涉及几个关键步骤:服务器配置、URI处理函数注册和请求处理。

首先包含必要的头文件:

#include "esp_http_server.h"

然后定义服务器句柄和配置结构体:

static httpd_handle_t server = NULL;

httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
config.ctrl_port = 32768;
config.max_open_sockets = 7;

这里我将服务器端口设置为80,这是HTTP协议的默认端口,这样在浏览器中访问时就不需要输入端口号了。max_open_sockets设置同时连接的最大数量,对于物联网控制台来说,7个连接通常足够了。

在实际项目中,我发现有几个配置参数特别重要:uri_match_fn用于设置URI匹配方式,默认是简单匹配,也支持通配符匹配;lru_purge_enable可以在连接池满时自动清理最久未使用的连接,避免服务器因为连接数过多而崩溃。

3. 实现GET请求处理函数

GET请求处理是HTTP服务器最基础的功能,当用户在浏览器中输入ESP32的IP地址时,浏览器就会发送GET请求来获取网页内容。

我们先定义一个简单的HTML页面作为控制台的界面:

static const char html_page[] = 
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta charset='UTF-8'>"
"<title>ESP32控制台</title>"
"<style>"
"body { font-family: Arial, sans-serif; margin: 40px; }"
".btn { padding: 10px 20px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }"
".on { background-color: #4CAF50; color: white; }"
".off { background-color: #f44336; color: white; }"
"</style>"
"</head>"
"<body>"
"<h1>ESP32物联网控制台</h1>"
"<div>"
"<button class='btn on' onclick='controlLED(1)'>打开LED</button>"
"<button class='btn off' onclick='controlLED(0)'>关闭LED</button>"
"</div>"
"<div id='sensorData'>传感器数据加载中...</div>"
"<script>"
"function controlLED(state) {"
"    fetch('/control?led=' + state, { method: 'POST' })"
"    .then(response => response.text())"
"    .then(data => console.log(data));"
"}"
"function updateSensorData() {"
"    fetch('/sensor')"
"    .then(response => response.json())"
"    .then(data => {"
"        document.getElementById('sensorData').innerHTML = "
"        '温度: ' + data.temperature + '°C<br>' + "
"        '湿度: ' + data.humidity + '%';"
"    });"
"}"
"setInterval(updateSensorData, 5000);"
"updateSensorData();"
"</script>"
"</body>"
"</html>";

这个HTML页面包含了基本的控制按钮和传感器数据显示区域,还有JavaScript代码来处理用户交互和定时更新传感器数据。

接下来实现GET请求处理函数:

static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    return httpd_resp_send(req, html_page, strlen(html_page));
}

这个函数很简单,就是设置响应类型为HTML,然后发送我们定义好的HTML页面内容。在实际项目中,你可能需要根据不同的URI返回不同的内容,这时候可以注册多个URI处理函数。

4. 处理POST请求与数据交互

POST请求通常用于提交数据到服务器,比如控制指令、配置信息等。在我们的物联网控制台中,POST请求用来接收LED控制指令。

先定义一个POST请求处理函数:

static esp_err_t control_post_handler(httpd_req_t *req)
{
    char buf[100];
    int ret, remaining = req->content_len;
    
    // 读取POST数据
    while (remaining > 0) {
        if ((ret = httpd_req_recv(req, buf, MIN(remaining, sizeof(buf)))) <= 0) {
            if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
                continue;
            }
            return ESP_FAIL;
        }
        remaining -= ret;
        
        // 解析数据
        char* led_param = strstr(buf, "led=");
        if (led_param != NULL) {
            int led_state = atoi(led_param + 4);
            // 控制LED
            gpio_set_level(GPIO_NUM_2, led_state);
            ESP_LOGI(TAG, "LED状态设置为: %d", led_state);
        }
    }
    
    httpd_resp_send(req, "控制指令已执行", HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

这个函数首先读取POST请求中的数据,然后解析出控制指令。我使用了strstr来查找"led="参数,然后获取其值。在实际项目中,你可能需要更健壮的数据解析方法,特别是当数据量较大或格式复杂时。

对于传感器数据请求,我们可以实现一个专门的API接口:

static esp_err_t sensor_get_handler(httpd_req_t *req)
{
    // 读取传感器数据(这里用模拟数据)
    float temperature = 25.6;
    float humidity = 60.4;
    
    char json_response[100];
    snprintf(json_response, sizeof(json_response), 
             "{\"temperature\":%.1f,\"humidity\":%.1f}", 
             temperature, humidity);
    
    httpd_resp_set_type(req, "application/json");
    return httpd_resp_send(req, json_response, strlen(json_response));
}

这个函数返回JSON格式的传感器数据,前端JavaScript代码可以方便地解析和显示这些数据。

5. 注册URI处理函数与启动服务器

有了请求处理函数后,我们需要将它们注册到HTTP服务器中。ESP-IDF使用httpd_uri_t结构体来定义URI处理映射:

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

static const httpd_uri_t control = {
    .uri       = "/control",
    .method    = HTTP_POST,
    .handler   = control_post_handler,
    .user_ctx  = NULL
};

static const httpd_uri_t sensor = {
    .uri       = "/sensor",
    .method    = HTTP_GET,
    .handler   = sensor_get_handler,
    .user_ctx  = NULL
};

然后创建一个函数来启动HTTP服务器:

void start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = 80;
    
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_register_uri_handler(server, &root);
        httpd_register_uri_handler(server, &control);
        httpd_register_uri_handler(server, &sensor);
        
        ESP_LOGI(TAG, "HTTP服务器启动成功,端口: %d", config.server_port);
    } else {
        ESP_LOGE(TAG, "HTTP服务器启动失败");
    }
}

这个函数配置服务器参数,启动服务器,然后注册我们定义的所有URI处理函数。如果一切正常,你会看到日志输出服务器启动成功的消息。

6. 硬件控制与传感器集成

HTTP服务器搭建好后,我们需要将其与实际的硬件功能集成。ESP32的GPIO控制非常简单,首先需要初始化GPIO:

void init_gpio(void)
{
    gpio_config_t io_conf = {};
    io_conf.pin_bit_mask = (1ULL << GPIO_NUM_2);
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    io_conf.intr_type = GPIO_INTR_DISABLE;
    gpio_config(&io_conf);
}

这个函数将GPIO2配置为输出模式,用于控制LED。在POST请求处理函数中,我们就是通过gpio_set_level来设置这个引脚的电平状态。

对于传感器数据读取,以DHT11温湿度传感器为例:

#include "dht11.h"

void read_sensor_data(float *temperature, float *humidity)
{
    // DHT11初始化
    dht11_init(GPIO_NUM_4);
    
    // 读取数据
    if (dht11_read() == ESP_OK) {
        *temperature = dht11_get_temperature();
        *humidity = dht11_get_humidity();
    } else {
        // 读取失败时使用默认值
        *temperature = 0.0;
        *humidity = 0.0;
    }
}

然后在sensor_get_handler中调用这个函数来获取真实的传感器数据,而不是使用模拟数据。

7. 实战调试与优化技巧

在实际开发过程中,调试是不可避免的。我总结了一些调试HTTP服务器的实用技巧:

首先,确保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) {
        esp_wifi_connect();
    } 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, "获取到IP地址:" IPSTR, IP2STR(&event->ip_info.ip));
        start_webserver();
    }
}

这个事件处理函数会在获取到IP地址后自动启动HTTP服务器。

对于HTTP服务器的调试,我强烈建议使用ESP-IDF的日志系统。在menuconfig中,可以设置HTTP服务器的日志级别:

idf.py menuconfig

进入Component config -> HTTP Server -> Default log level,设置为Verbose,这样可以看到详细的调试信息。

在代码中,可以使用ESP_LOGI、ESP_LOGD等宏输出调试信息:

ESP_LOGI(TAG, "收到来自 %s 的请求", req->aux);

如果遇到服务器无法启动的问题,首先检查端口是否被占用。ESP32的HTTP服务器默认使用80端口,如果这个端口被其他程序占用,服务器就无法启动。你可以通过修改config.server_port来更换端口。

另一个常见的问题是内存不足。HTTP服务器需要一定的内存空间来处理请求和响应,如果项目中有其他内存密集型任务,可能会导致服务器无法正常工作。可以通过减少max_uri_handlers或max_open_sockets来降低内存使用。

8. 安全增强与进阶功能

基础功能实现后,我们可以考虑一些安全增强和进阶功能。虽然我们的控制台只在局域网内访问,但基本的安全措施还是必要的。

首先可以添加简单的身份验证。在前端页面中添加登录表单:

<div id="loginForm" style="display:block;">
    <input type="password" id="password" placeholder="输入密码">
    <button onclick="login()">登录</button>
</div>
<div id="controlPanel" style="display:none;">
    <!-- 控制界面内容 -->
</div>

然后在JavaScript中处理登录:

function login() {
    var password = document.getElementById("password").value;
    fetch('/login', {
        method: 'POST',
        body: 'password=' + encodeURIComponent(password)
    }).then(response => {
        if (response.ok) {
            document.getElementById("loginForm").style.display = "none";
            document.getElementById("controlPanel").style.display = "block";
        }
    });
}

服务器端验证密码:

static esp_err_t login_post_handler(httpd_req_t *req)
{
    char buf[50];
    int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
    if (ret > 0) {
        buf[ret] = '\0';
        if (strstr(buf, "password=123456") != NULL) {
            httpd_resp_set_status(req, "200 OK");
            return httpd_resp_send(req, "登录成功", HTTPD_RESP_USE_STRLEN);
        }
    }
    httpd_resp_set_status(req, "401 Unauthorized");
    return httpd_resp_send(req, "密码错误", HTTPD_RESP_USE_STRLEN);
}

这只是一个简单的示例,实际项目中应该使用更安全的认证方式,比如使用HTTPS加密通信。

另一个有用的进阶功能是WebSocket支持,可以实现实时数据传输。ESP-IDF提供了WebSocket服务器组件,可以与HTTP服务器配合使用:

#include "esp_http_server.h"
#include "esp_http_client.h"

// WebSocket处理函数
static esp_err_t ws_handler(httpd_req_t *req)
{
    if (req->method == HTTP_GET) {
        return ESP_OK;
    }
    
    // WebSocket帧处理
    httpd_ws_frame_t ws_pkt;
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;
    
    // 接收WebSocket数据
    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) {
        return ret;
    }
    
    // 处理WebSocket消息
    // ...
    
    return ESP_OK;
}

WebSocket非常适合实时更新传感器数据,避免了HTTP轮询的开销。

最后,我们可以考虑添加OTA(空中升级)功能,通过Web界面来更新固件。这需要实现文件上传和处理:

static esp_err_t update_post_handler(httpd_req_t *req)
{
    // 处理固件上传
    esp_ota_handle_t ota_handle;
    const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
    
    esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &ota_handle);
    
    char buf[1024];
    int received;
    while ((received = httpd_req_recv(req, buf, sizeof(buf))) > 0) {
        esp_ota_write(ota_handle, buf, received);
    }
    
    esp_ota_end(ota_handle);
    esp_ota_set_boot_partition(update_partition);
    
    httpd_resp_send(req, "固件更新成功,即将重启", HTTPD_RESP_USE_STRLEN);
    esp_restart();
    
    return ESP_OK;
}

这个函数处理固件上传,写入OTA分区,然后设置新的启动分区并重启设备。

通过这些进阶功能,我们的ESP32 Web控制台就变得更加实用和强大了。在实际项目中,你可以根据具体需求选择实现哪些功能。记得在添加新功能时注意内存使用情况,ESP32的资源是有限的,需要合理分配。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐