ESP32-WebOTA

news/2025/1/24 11:18:44/文章来源:https://www.cnblogs.com/tangwc/p/18689284

前言

在 ESP32 设备连接上 WiFi 后均获获得 WiFi 设备分配的一个 IP 地址,在同一网络的设备当中即可访问此 IP 地址,而我们既可以通过 ESP32 中的 HTML 服务访问建立在上面的网页,并且可以通过网页来实现对于 ESP32 的交互。
接下来主要介绍如何通过网页来实现 ESP32 的 OTA 升级。

网页建立

首先要制作一下静态网页,由于本人前端知识并不太熟悉,所以这里选择拿来主义,将网上两套现成的网页模板直接拿来用,参考链接:

  • ESP32-Web-Server-ESP-IDF: Build a HTTP Web server or WebSocket Web server on ESP32 using ESP-IDF. Implement some typical and interesting IoT projects throughn ESP32 Web Server. Docs reference: https://blog.csdn.net/wangyx1234/
  • ESP32 HttpServer模式下 本地OTA 例程(基于ESP-IDF类似Arduino下OTAWebUpdater例程)_esp32 fileserving-CSDN博客

其中部分网页中,为了使得网页呈现效果好看,增加了 css/js 等美化脚本,使得整体网页类似于一个工程文件夹,对于这种情况,若使用之前 web 配网的方式将 web 编译成全局变量在程序中调用显然是不合理的,况且在网页代码中调用 css/js 均为相对路径,所以这里要引入 ESP32 另一个功能:SPIFFS 文件系统。

SPIFFS 文件系统

SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统,支持磨损均衡、文件系统一致性检查等功能。
这里将网页文件夹全部放入一个文件夹下,并在同级目录下创建一个 CMakeLists 文件,主要内容如下:

idf_component_register(SRCS ${components_srcs}
                    INCLUDE_DIRS ${components_incs}
                    PRIV_REQUIRES ${components_requires})set(WEB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/web_image")
if (EXISTS ${WEB_SRC_DIR})
    spiffs_create_partition_image(spiffs ${WEB_SRC_DIR} FLASH_IN_PROJECT)
else()
    message(FATAL_ERROR "${WEB_SRC_DIR} doesn't exit.")
endif()

核心语法 spiffs_create_partition_image 使用后代码工程编译完成后,可以使用烧录命令将镜像与应用程序二进制文件、分区表等一起自动烧录至设备。
语法中 spiffs 为分区表中命令。下面主要讲解一下系统的分区表划分

分区表

因为是为了实现 OTA 功能,对于应用程序部分需要进行对半划分,所以对于 app 需要划分成两部分,加上 spiffs 需要划分一部分,创建模板卡宴参考 esp-idf 中 partition_table 中的分区表例子,这里选取一个 ota 例子后经过修改如下所示:

Name Type Sub Type Offset Size
otadata data ota 0x2000
phy_int data phy 0x1000
nvs data nvs 0x4000
ota_0 app ota_0 5M
ota_1 app ota_1 5M
spiffs data spiffs 1M
关于分区表,后面会单独开一篇进行说明讲解。

SDK 默认配置

在编译代码前需要对 ESP32 工程代码进行一些默认的配置工作,使用命令 idf.py menuconfig 将部分的参数进行配置,部分配置参考如下,介绍几个比较重要的参数。

CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048
CONFIG_HTTPD_MAX_URI_LEN=1024
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_FREERTOS_HZ=1000
CONFIG_LWIP_MAX_SOCKETS=16
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
  • CONFIG_HTTPD_MAX_REQ_HDR_LEN:默认 512,这里所代表的是 http 网页最大字符长度,过小访问网页会报错。
  • CONFIG_HTTPD_MAX_URI_LEN:同上,为网页返回数据长度,过小会出现错误。
  • CONFIG_LWIP_MAX_SOCKETS:最大开辟的网页数量,这里最大值为 16,建议写 16 比较好。
    其他的可以根据自身需求进行修改或增加。

代码编写

代码流程主要分为以下几步:

  1. WiFi 连接
  2. 读取 spiffs 中的静态网页
  3. Web 网页建立,开启 https 服务
    用户层面,主要是打开 WiFi 分配的 ip 地址,进入网页后找到 OTA 升级界面,将需要升级的固件传入 web 页面中进行升级,并观察效果。

WiFi 连接部分这里就不过多赘述了,可以参考之前的 WiFi 配网,也可以直接简单的写入 WiFi 信息直接配网。

SPIFFS 系统处理

1. 系统初始化

esp_err_t init_fs(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path = web_base_point,
        .partition_label = NULL,
        .max_files = 5,//maybe the num can be set smaller
        .format_if_mount_failed = false
    };
    esp_err_t ret = esp_vfs_spiffs_register(&conf);    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount or format filesystem");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            ESP_LOGE(TAG, "Failed to find SPIFFS partition");
        } else {
            ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
        }
        return ESP_FAIL;
    }
    
    size_t total = 0, used = 0;
    ret = esp_spiffs_info(NULL, &total, &used);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
    }
    return ESP_OK;
}

2. 提取文件并发送到 HTTP 服务器中

/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t* req, const char* filepath)
{
    const char* type = "text/plain";
    if (CHECK_FILE_EXTENSION(filepath, ".html")) {
        type = "text/html";
    } else if (CHECK_FILE_EXTENSION(filepath, ".js")) {
        type = "application/javascript";
    } else if (CHECK_FILE_EXTENSION(filepath, ".css")) {
        type = "text/css";
    } else if (CHECK_FILE_EXTENSION(filepath, ".png")) {
        type = "image/png";
    } else if (CHECK_FILE_EXTENSION(filepath, ".ico")) {
        type = "image/x-icon";
    } else if (CHECK_FILE_EXTENSION(filepath, ".svg")) {
        type = "text/xml";
    }
    return httpd_resp_set_type(req, type);
}static esp_err_t custom_send_file_chunk(httpd_req_t* req, const char *filepath)
{
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    int fd = open(filepath, O_RDONLY, 0);
    if (fd == -1) {
        ESP_LOGE(TAG, "Failed to open file : %s", filepath);
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
        return ESP_FAIL;
    }    set_content_type_from_file(req, filepath);    char* chunk = rest_context->scratch;
    ssize_t read_bytes;
    do {
        /* Read file in chunks into the scratch buffer */
        read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
        if (read_bytes == -1) {
            ESP_LOGE(TAG, "Failed to read file : %s", filepath);
        } else if (read_bytes > 0) {
            /* Send the buffer contents as HTTP response chunk */
            if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) {
                close(fd);
                ESP_LOGE(TAG, "File sending failed!");
                /* Abort sending file */
                httpd_resp_sendstr_chunk(req, NULL);
                /* Respond with 500 Internal Server Error */
                httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
                return ESP_FAIL;
            }
        }
    } while (read_bytes > 0);
    /* Close file after sending complete */
    close(fd);
    ESP_LOGI(TAG, "File sending complete");
    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

网页建立

这一步主要是将网页界面部署到 HTTP 服务器上去。分别为主界面,ota 界面,WiFi 信息界面,重启界面,界面回调建立接口大同小异,都是将 spiffs 中的静态网页拿出并部署到 web 服务器上。

  • 主界面
static esp_err_t index_html_get_handler(httpd_req_t *req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    if (req->uri[strlen(req->uri) - 1] == '/') {
        strlcat(filepath, "/index.html", sizeof(filepath));
    } else {
        strlcat(filepath, req->uri, sizeof(filepath));
    }    char* p = strrchr(filepath, '?');
    if (p != NULL) {
        *p = '\0';
    }
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }
    return ESP_OK;
}
  • OTA 界面
static esp_err_t ota_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/ota.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }    return ESP_OK;
}
  • WiFi 信息界面
static esp_err_t wifi_manage_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/wifimanager.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }    return ESP_OK;
}
  • 重启界面
static void timer_callback(TimerHandle_t timer)
{
    esp_restart();
}static void create_a_restart_timer(void)
{
    TimerHandle_t oneshot = xTimerCreate("oneshot", 5000 / portTICK_PERIOD_MS, pdFALSE,
                                         NULL, timer_callback);
    xTimerStart(oneshot, 1);
    printf("Restarting in 5 seconds...\n");
    fflush(stdout);
}static esp_err_t reboot_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/reboot.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }    create_a_restart_timer();
    return ESP_OK;
}

打开 HTTP 服务,创建所有 Web 页面。

httpd_handle_t web_server_start(const char* base_path)
{    REST_CHECK(base_path, "wrong base path", err);
    rest_server_context_t* rest_context = calloc(1, sizeof(rest_server_context_t));
    REST_CHECK(rest_context, "No memory for rest context", err);
    strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_uri_handlers = 7;
    config.max_open_sockets = 7;
    config.uri_match_fn = httpd_uri_match_wildcard;
    config.stack_size = 5912;    ESP_LOGI(TAG, "Starting HTTP server on port: '%d'", config.server_port);
    REST_CHECK(httpd_start(&server, &config) == ESP_OK, "Start server failed", err_start);    httpd_uri_t httpd_uri_array[] = {
        {"/ota", HTTP_GET, ota_html_get_handler, rest_context},
        {"/wifimanager", HTTP_GET, wifi_manage_html_get_handler, rest_context},
        {"/update", HTTP_POST, OTA_update_post_handler, rest_context},
        {"/status", HTTP_POST, OTA_update_status_handler, rest_context},
        {"/reboot", HTTP_GET, reboot_html_get_handler, rest_context},
        {"/*", HTTP_GET, index_html_get_handler, rest_context},  // 此操作是将所有spiffs文件系统目录下文件映射到根目录
    };    // Set URI handlers
    ESP_LOGI(TAG, "Registering URI handlers");
    for (int i = 0; i < sizeof(httpd_uri_array) / sizeof(httpd_uri_t); i++) {
        if (httpd_register_uri_handler(server, &httpd_uri_array[i]) != ESP_OK) {
            ESP_LOGE(TAG, "httpd register uri_array[%d] fail", i);
        }
    }    ESP_LOGI(TAG, "Success starting server!");    return server;
err_start:
    free(rest_context);
err:
    return NULL;
}

OTA 回调事件

OTA 网页含有两个接口信息,分别为接收 bin 文件升级和获取当前 bin 文件展示当前固件信息。
这里展示两个接口的写法

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>#include "esp_log.h"
#include "esp_http_server.h"
#include "esp_ota_ops.h"
#include "freertos/event_groups.h"
#include "web_ota.h"#define TAG "WEB OTA"
int8_t flash_status = 0;/* Receive .Bin file */
esp_err_t OTA_update_post_handler(httpd_req_t *req)
{
    esp_ota_handle_t ota_handle;
    char ota_buff[1024];
    int content_length = req->content_len;
    int content_received = 0;
    int recv_len;
    bool is_req_body_started = false;
    const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);    // Unsucessful Flashing
    flash_status = -1;
    do
    {
        /* Read the data for the request */
        if ((recv_len = httpd_req_recv(req, ota_buff, MIN(content_length, sizeof(ota_buff)))) < 0)
        {
            if (recv_len == HTTPD_SOCK_ERR_TIMEOUT)
            {
                ESP_LOGI(TAG, "Socket Timeout");
                /* Retry receiving if timeout occurred */
                continue;
            }
            ESP_LOGI(TAG, "OTA Other Error %d", recv_len);
            return ESP_FAIL;
        }        ESP_LOGI(TAG, "OTA RX: %d of %d\r", content_received, content_length);
        // Is this the first data we are receiving
        // If so, it will have the information in the header we need.
        if (!is_req_body_started)
        {
            is_req_body_started = true;
            // Lets find out where the actual data staers after the header info    
            char *body_start_p = strstr(ota_buff, "\r\n\r\n") + 4;  
            int body_part_len = recv_len - (body_start_p - ota_buff);
            //int body_part_sta = recv_len - body_part_len;
            //printf("OTA File Size: %d : Start Location:%d - End Location:%d\r\n", content_length, body_part_sta, body_part_len);
            ESP_LOGI(TAG, "OTA File Size: %d ", content_length);            esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &ota_handle);
            if (err != ESP_OK)
            {
                ESP_LOGI(TAG, "Error With OTA Begin, Cancelling OTA");
                return ESP_FAIL;
            }
            else
            {
                ESP_LOGI(TAG, "Writing to partition subtype 0x%x at offset 0x%lx\r\n", update_partition->subtype, update_partition->address);
            }
            // Lets write this first part of data out
            esp_ota_write(ota_handle, body_start_p, body_part_len);
        }
        else
        {
            // Write OTA data
            esp_ota_write(ota_handle, ota_buff, recv_len);
            content_received += recv_len;
        }
    } while (recv_len > 0 && content_received < content_length);    // End response
    //httpd_resp_send_chunk(req, NULL, 0);    if (esp_ota_end(ota_handle) == ESP_OK)
    {
        // Lets update the partition
        if(esp_ota_set_boot_partition(update_partition) == ESP_OK)
        {
            const esp_partition_t *boot_partition = esp_ota_get_boot_partition();            // Webpage will request status when complete
            // This is to let it know it was successful
            flash_status = 1;
            ESP_LOGI(TAG, "Next boot partition subtype 0x%x at offset 0x%lx\r\n", boot_partition->subtype, boot_partition->address);
            ESP_LOGI(TAG, "Please Restart System...");
        }
        else
        {
            ESP_LOGI(TAG, "!!! Flashed Error !!!");
        }
    }
    else
    {
        ESP_LOGI(TAG, " !!! OTA End Error !!!");
    }
    return ESP_OK;
}static void timer_callback(TimerHandle_t timer)
{
    esp_restart();
}static void create_a_restart_timer(void)
{
    TimerHandle_t oneshot = xTimerCreate("oneshot", 5000 / portTICK_PERIOD_MS, pdFALSE,
                                         NULL, timer_callback);
    xTimerStart(oneshot, 1);    ESP_LOGI(TAG, "Restarting in 5 seconds...\n");
    fflush(stdout);
}/* Status */
esp_err_t OTA_update_status_handler(httpd_req_t *req)
{
    char ledJSON[100];
    ESP_LOGI(TAG, "Status Requested");    sprintf(ledJSON, "{\"status\":%d,\"compile_time\":\"%s\",\"compile_date\":\"%s\"}", flash_status, __TIME__, __DATE__);
    httpd_resp_set_type(req, "application/json");
    httpd_resp_send(req, ledJSON, strlen(ledJSON));
    if (flash_status == 1)
    {
        // We cannot directly call reboot here because we need the
        // browser to get the ack back. Send message to another task or create a
        create_a_restart_timer();
        // xEventGroupSetBits(reboot_event_group, REBOOT_BIT);      
    }
    return ESP_OK;
}

代码参考工程见:ESP32_demo: ESP32有关的相关功能演示domo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/874664.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何修改织梦网站的颜色?

要修改织梦网站的颜色,您可以通过以下几种方式实现:使用CSS样式:在织梦网站的CSS文件中,找到控制网站颜色的部分,并进行相应的修改。通常,这些样式位于网站的主题或模板目录下的CSS文件中。您可以使用文本编辑器(如Notepad++、Sublime Text等)打开CSS文件,并查找和修改…

如何修改网站数据库密码?

修改网站数据库密码是一项重要的安全措施,可以帮助保护您的网站数据。以下是一些基本的步骤:登录到数据库管理工具:使用数据库管理工具(如phpMyAdmin、MySQL Workbench等)登录到您的网站数据库。您需要知道数据库的主机名、用户名、密码和数据库名称。 选择要修改密码的用…

学习vue05补发一下昨天学习内容

学习了vue知识,关于vue工程的运行方式和程序,并学会了组合式和分组式API,主要是在其中不断查资料的学习关于vue运行,比如钩子函数,应用实例等等

WordPress移除页面源码head中style img:is的样式代码

上月中旬 WordPress 6.7 版本正式发布,随后很快又发布了 WordPress 6.7.1 维护版本,每次 WordPress 有大版本的更新子凡我都习惯先看看官方的更新记录,然后先升级泪雪博客看看有没有问题,最后再批量的升级其他项目的 WordPress 网站,然后就是还会习惯的看看前段代码是否存…

帝国cms网站名称修改不成功怎么办

如果您在帝国cms中修改网站名称不成功,可以尝试以下步骤:检查权限:确保您有足够的权限修改网站名称。通常,只有管理员或具有相应权限的用户才能进行此类修改。 清除缓存:修改网站名称后,可能需要清除缓存才能使更改生效。您可以在帝国cms后台找到“数据更新”或“缓存管理…

网站顶部logo在哪里修改

网站顶部logo的修改位置通常取决于您使用的网站建设工具或平台。以下是一些常见的修改方法:内容管理系统(CMS):如果您使用的是CMS,如WordPress、Drupal或Joomla,通常可以在后台管理界面中找到“外观”或“模板”选项,然后在其中找到“自定义”或“主题设置”等相关选项,…

网站PHP版本如何修改

网站的PHP版本是指网站所使用的PHP解释器的版本。修改网站的PHP版本可以通过以下步骤实现:确定服务器类型:首先需要确定网站所在的服务器类型,如Apache、Nginx等。不同的服务器类型有不同的PHP版本管理方式。 找到PHP版本管理工具:根据服务器类型,找到相应的PHP版本管理工…

分布式键值存储的王者--ETCD

在分布式系统的世界里,数据的一致性、可用性和分区容错性如同三座大山,横亘在开发者面前。 而 ETCD,犹如一位技艺高超的登山者,以其卓越的性能和稳定的表现,征服了这三座高峰,成为分布式键值存储领域当之无愧的王者。 ETCD 不仅仅是一个简单的键值存储系统,它更是分布式…

如何在CMS中修改网站的安装位置

问题描述:如何在CMS中修改网站的安装位置。 解决方法:确定CMS类型:不同的CMS可能有不同的方法来修改网站的安装位置。首先需要确定您使用的是哪种CMS,例如WordPress、Drupal、Joomla等。 备份网站数据:在进行任何修改之前,务必备份网站的所有数据,包括数据库和文件。这是…

WebStorm2024如何安装?附安装包和激活方式

前言 大家好,我是小徐啊。WebStorm是我们常用的开发web应用的开发工具,其功能十分强大。今天,小徐就来介绍下如何安装和激活webstorm。文末附获取方式。 如何安装和激活WebStorm 首先,我们打开安装包,双击下,点击运行按钮。然后,我们点击下一步按钮。然后,我们选择要安…

IDEA如何将一行上移或者下移

前言 大家好,我是小徐啊。我们在使用IDEA开发Java应用的时候,都会使用到IDEA的快捷键。这些快捷键帮助我们提高了开发的效率。今天,我要介绍下,在IDEA中如何将某一行代码上移或者下移。这个技巧在我们编写代码的时候还是很有效的。 如何下移一行代码 首先,我们需要打开IDE…

NFS动态存储实战案例

NFS动态存储实战案例Kubernetes 不包含内部 NFS 驱动。你需要使用外部驱动为 NFS 创建 StorageClass。卷插件 内置配置器 配置示例AzureFile ✓ Azure FileCephFS - -FC - -FlexVolume - -iSCSI - -Local - LocalNFS - NFSPortworxVolume ✓ Portworx VolumeRBD ✓ Ceph RBDVsp…