В этом уроке мы научимся создавать веб-сервер WebSocket на ESP32, используя SPIFFS и ESP-IDF. Сервер WebSocket — это сервер, который использует протокол WebSocket для обработки соединений WebSocket. Для демонстрации мы создадим веб-страницу, которая сможет включать светодиод на плате ESP32. Эта веб-страница будет также отображать состояние светодиода.
В сети есть примеры веб-серверов на базе HTTP для ESP32, которые предоставляют HTML/CSS-файлы веб-клиентам. Недостатком использования библиотеки HTTP-сервера является то, что она не обновляет автоматически состояние светодиода на веб-странице для всех клиентов, если к веб-серверу подключено более одного клиента. Эту проблему можно решить, используя протокол связи WebSocket. Например, если к веб-серверу подключено несколько клиентов, и любой из них изменяет состояние контакта GPIO, он автоматически уведомит об этом всех подключенных клиентов.
В протоколе HTTP каждый цикл запроса и ответа между сервером и клиентом требует отправки заголовков и метаданных, что приводит к дополнительной задержке. Напротив, WebSocket обеспечивает постоянное соединение и имеет низкую задержку благодаря отсутствию необходимости повторного установления соединения для каждого запроса.
Как работает веб-сервер WebSocket на ESP32
Этот WebSocket-сервер для ESP32 предоставляет две основные функции: отображение веб-страницы с кнопкой для переключения состояния светодиода и отправку состояния светодиода в виде сообщения WebSocket всем подключенным клиентам. Когда сервер получает сообщение «toggle» от любого клиента, состояние светодиода инвертируется, и обновленное состояние затем отправляется обратно всем подключенным клиентам в виде сообщения WebSocket. Сервер также предоставляет два URI: «/» и «/ws». Первый используется для отображения веб-страницы, а второй — для обмена данными по протоколу WebSocket. Веб-страница клиента хранится в файловой системе SPI flash и отображается сервером по URI «/».

Пользователи могут получить доступ к веб-серверу, используя IP-адрес, присвоенный ESP32, по локальной сети.
Создание веб-сервера WebSocket для ESP32 с использованием ESP-IDF
В этом разделе давайте создадим проект веб-сервера ESP32 WebSocket с использованием ESP-IDF.
Создание примера проекта
Откройте VS Code и перейдите в меню View > Command Palette («Вид» > «Палитра команд»). Введите в строке поиска ESP-IDF: New Project и нажмите Enter.
Укажите название проекта и каталог, а также плату и целевое устройство ESP-IDF. Мы назвали наш проект «esp32_websocket_server». Для платы ESP-IDF мы выбрали опцию custom board ("пользовательская плата"). В качестве целевого устройства ESP-IDF мы выбрали модуль ESP32. Нажмите кнопку ‘Choose Template’ ("Выбрать шаблон"), чтобы продолжить.
В разделе «Расширение» выберите опцию ESP-IDF.
На вкладке get-started («Начало работы») мы щелкнем по пункту «sample_project». Затем нажмем ‘Create project using template sample_project’) («Создать проект, используя шаблон sample_project»).
Вы получите уведомление о создании проекта. Чтобы открыть проект в новом окне, нажмите «Да».
Это открывает наш проект esp32_websocket_server, который мы создали на вкладке EXPLORER. Внутри папки проекта находится несколько папок. Это справедливо для всех проектов, которые вы будете создавать с помощью ESP-IDF Explorer.
Создание раздела для SPIFFS
В этом разделе мы создадим раздел для SPIFFS, где будем хранить HTML/CSS для веб-сервера.
Создайте новый файл и назовите его «partitions.csv». Скопируйте в этот файл приведенные ниже данные и сохраните его. Нам нужно добавить собственную пользовательскую таблицу разделов, поэтому мы будем использовать этот файл.
|
1 2 3 4 5 6 |
# Name, Type, SubType, Offset, Size, Flags # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild nvs, data, nvs, , 0x6000, phy_init, data, phy, , 0x1000, factory, app, factory, , 1M, storage, data, spiffs, , 1M |
После создания файла partitions.csv создайте в каталоге проекта папку с именем 'data'. Внутри этой папки создайте текстовый файл с именем 'index.html'.
Файл index.htm определяет структуру и содержимое веб-страницы, которую веб-сервер ESP32 будет отправлять в ответ на HTTP-запросы от веб-клиентов.
Веб-страница веб-сервера WebSocket на ESP32
Этот HTML/CSS-код создает веб-страницу для сервера WebSocket на ESP32. На странице будет кнопка с надписью «Переключить светодиод» и текст, отображающий текущее состояние светодиода (включен или выключен).
Файл начинается с установки типа документа (doctype) на HTML и создания базовой структуры HTML-страницы с помощью тегов head и body. Внутри тега head находятся заголовок (title), несколько метатегов (meta tags) и блок CSS, определяющий макет, цвета и стили элементов страницы.
В теле HTML-кода находится верхняя панель навигации с надписью «ESP32 WebSocket Server», а внутри неё — ещё один div с классом «content», где размещена карточка с кнопкой для переключения светодиода и текстом, отображающим текущее состояние светодиода.
В конце добавляется блок скрипта, содержащий код JavaScript, в котором определяется переменная «gateway», являющаяся конечной точкой WebSocket. Имеется обработчик событий, который отслеживает событие «load» окна, и когда это событие происходит, вызывается функция «onLoad()», которая, в свою очередь, вызывает функцию «initWebSocket()» для открытия соединения WebSocket. Также имеются другие функции, такие как «onOpen()», «onClose()», «onMessage()» и т. д., которые обрабатывают различные события, которые могут происходить в WebSocket.
При нажатии кнопки «Переключить светодиод» отправляется сообщение на ESP32 через WebSocket для переключения состояния светодиода, а также обновляется текст состояния на странице, отражающий текущее состояние светодиода.
Он также содержит функцию с именем 'initButton()', которая вызывается в функции 'onLoad()'. Эта функция отвечает за инициализацию элемента кнопки на странице и прикрепление к нему обработчика событий. При нажатии кнопки она отправляет сообщение через соединение WebSocket на ESP32 для переключения состояния светодиода.
В коде также используется метод DOM document.getElementById() для получения ссылок на конкретные элементы на странице, такие как элемент span с состоянием и элемент button. Это позволяет коду JavaScript обновлять содержимое этих элементов и изменять состояние светодиода на основе сообщений, полученных от ESP32.
В коде также используется функция console.log(), которая выводит сообщения в консоль разработчика браузера. Это полезно для отладки и понимания логики выполнения кода.
В целом, этот код создает простую веб-страницу, которая подключается к WebSocket-серверу ESP32 и позволяет переключать состояние светодиода, подключенного к ESP32, нажатием кнопки на странице. Состояние светодиода также отображается на странице в режиме реального времени.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
<!DOCTYPE HTML> <html> <head> <title>ESP32 Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="data:,"> <style> html { font-family: New Times Roman; text-align: center; } h1 { font-size: 1.8rem; color: white; } h2 { font-size: 1.5rem; font-weight: bold; color: #07156d; } .card { background-color: #F8F7F9; ; box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); padding-top: 10px; padding-bottom: 20px; } .topnav { overflow: hidden; background-color: #04296d; } body { margin: 0; } .content { padding: 30px; max-width: 600px; margin: 0 auto; } .button { padding: 15px 50px; font-size: 24px; text-align: center; outline: none; color: #fff; background-color: #0ffa6d; //green border: #0ffa6d; border-radius: 5px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .button:active { background-color: #fa0f0f; transform: translateY(2px); } .state { font-size: 1.5rem; color: #120707; font-weight: bold; } </style> <title>ESP32 Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="data:,"> </head> <body> <div class="topnav"> <h1>ESP32 WebSocket Server</h1> </div> <div class="content"> <div class="card"> <h2>ONBOARD LED GPIO2</h2> <p><button id="button" class="button">Toggle LED</button></p> <p class="state">State: <span id="state">%s</span></p> </div> </div> </div> <script> var gateway = `ws://${window.location.hostname}/ws`; var websocket; window.addEventListener('load', onLoad); function initWebSocket() { console.log('Trying to open a WebSocket connection...'); websocket = new WebSocket(gateway); websocket.onopen = onOpen; websocket.onclose = onClose; websocket.onmessage = onMessage; // <-- add this line } function onOpen(event) { console.log('Connection opened'); } function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); } function onMessage(event) { var state; console.log(event.data); if (event.data == "1") { state = "ON"; } else { state = "OFF"; } document.getElementById('state').innerHTML = state; } function onLoad(event) { initWebSocket(); initButton(); } function initButton() { document.getElementById('button').addEventListener('click', toggle); } function toggle() { websocket.send('toggle'); } </script> </body> </html> |
Перейдите в папку с именем 'main' и откройте файл CMakeLists.txt. Добавьте в конец этого файла следующую строку и сохраните его.
|
1 |
spiffs_create_partition_image(storage ../data FLASH_IN_PROJECT) |
Настройка веб-сервера WebSocket на ESP32 в ESP-IDF
Теперь давайте настроим некоторые параметры для ESP32 и создадим файл Kconfig.projbuild для настройки учетных данных Wi-Fi.
Настройка пользовательского файла разделов в ESP-IDF
Сначала перейдите в меню menuconfig. Щелкните значок, показанный ниже. Откроется редактор конфигурации ESP-IDF SDK.
Прокрутите вниз и откройте конфигурацию Serial Flasher. Здесь вы можете установить режим SPI для флэш-памяти, режим выборки флэш-памяти, скорость SPI для флэш-памяти, размер флэш-памяти и т. д. Установите параметры, как показано ниже, и сохраните изменения. Обратите внимание, что мы установили размер флэш-памяти равным 4 МБ.
Далее прокрутите вниз и нажмите Partition Table («Таблица разделов»). Здесь мы выберем опцию ‘custom partition table CSV’ («Пользовательская таблица разделов CSV») в разделе «Таблица разделов», укажем имя созданного нами CSV-файла и смещение таблицы разделов.
Настройка учетных данных Wi-Fi
Создайте новый файл с именем 'Kconfig.projbuild' в основной папке. Скопируйте приведенный ниже код в этот новый файл.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
menu "Example Configuration" config ESP_WIFI_SSID string "WiFi SSID" default "myssid" help SSID (network name) for the example to connect to. config ESP_WIFI_PASSWORD string "WiFi Password" default "mypassword" help WiFi password (WPA or WPA2) for the example to use. config ESP_MAXIMUM_RETRY int "Maximum retry" default 5 help Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. endmenu |
Теперь перейдите в меню menuconfig. Щелкните значок, показанный ниже. Откроется редактор конфигурации ESP-IDF SDK.
Прокрутите вниз и откройте Example Configuration (пример конфигурации). Здесь мы можем установить параметры конфигурации для этого примера в соответствии с нашими потребностями. Это включает в себя SSID сети Wi-Fi, пароль сети Wi-Fi и максимальное количество попыток повтора. Укажите SSID и пароль сети Wi-Fi вашего маршрутизатора, к которому будет подключаться плата ESP32. По умолчанию максимальное количество попыток повтора установлено на 5. Вы можете изменить это значение по своему усмотрению. После указания параметров конфигурации нажмите кнопку «Сохранить» вверху.
Включение настроек сервера WebSocket
ESP-IDF поддерживает серверы WebSocket, и эта функция доступна в библиотеке HTTP-серверов. Она отключена по умолчанию. Её необходимо включить через menuconfig. Перейдите в setting->HTTP Server и выберите опцию поддержки серверов WebSocket.
Код веб-сервера WebSocket для ESP32
Этот код настраивает ESP32 в качестве WebSocket-сервера, который отправляет веб-страницу клиенту (веб-браузеру) и переключает состояние светодиода через эту веб-страницу.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include <esp_http_server.h> #include "nvs_flash.h" #include "esp_spiffs.h" #include "connect_wifi.h" #define LED_PIN 2 httpd_handle_t server = NULL; struct async_resp_arg { httpd_handle_t hd; int fd; }; static const char *TAG = "WebSocket Server"; // TAG for debug int led_state = 0; #define INDEX_HTML_PATH "/spiffs/index.html" char index_html[4096]; char response_data[4096]; static void initi_web_page_buffer(void) { esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = NULL, .max_files = 5, .format_if_mount_failed = true}; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); memset((void *)index_html, 0, sizeof(index_html)); struct stat st; if (stat(INDEX_HTML_PATH, &st)) { ESP_LOGE(TAG, "index.html not found"); return; } FILE *fp = fopen(INDEX_HTML_PATH, "r"); if (fread(index_html, st.st_size, 1, fp) == 0) { ESP_LOGE(TAG, "fread failed"); } fclose(fp); } esp_err_t get_req_handler(httpd_req_t *req) { int response; if(led_state) { sprintf(response_data, index_html, "ON"); } else { sprintf(response_data, index_html, "OFF"); } response = httpd_resp_send(req, response_data, HTTPD_RESP_USE_STRLEN); return response; } static void ws_async_send(void *arg) { httpd_ws_frame_t ws_pkt; struct async_resp_arg *resp_arg = arg; httpd_handle_t hd = resp_arg->hd; int fd = resp_arg->fd; led_state = !led_state; gpio_set_level(LED_PIN, led_state); char buff[4]; memset(buff, 0, sizeof(buff)); sprintf(buff, "%d",led_state); memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = (uint8_t *)buff; ws_pkt.len = strlen(buff); ws_pkt.type = HTTPD_WS_TYPE_TEXT; static size_t max_clients = CONFIG_LWIP_MAX_LISTENING_TCP; size_t fds = max_clients; int client_fds[max_clients]; esp_err_t ret = httpd_get_client_list(server, &fds, client_fds); if (ret != ESP_OK) { return; } for (int i = 0; i < fds; i++) { int client_info = httpd_ws_get_fd_info(server, client_fds[i]); if (client_info == HTTPD_WS_CLIENT_WEBSOCKET) { httpd_ws_send_frame_async(hd, client_fds[i], &ws_pkt); } } free(resp_arg); } static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) { struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); resp_arg->hd = req->handle; resp_arg->fd = httpd_req_to_sockfd(req); return httpd_queue_work(handle, ws_async_send, resp_arg); } static esp_err_t handle_ws_req(httpd_req_t *req) { if (req->method == HTTP_GET) { ESP_LOGI(TAG, "Handshake done, the new connection was opened"); return ESP_OK; } httpd_ws_frame_t ws_pkt; uint8_t *buf = NULL; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.type = HTTPD_WS_TYPE_TEXT; esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); return ret; } if (ws_pkt.len) { buf = calloc(1, ws_pkt.len + 1); if (buf == NULL) { ESP_LOGE(TAG, "Failed to calloc memory for buf"); return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); free(buf); return ret; } ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); } ESP_LOGI(TAG, "frame len is %d", ws_pkt.len); if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && strcmp((char *)ws_pkt.payload, "toggle") == 0) { free(buf); return trigger_async_send(req->handle, req); } return ESP_OK; } httpd_handle_t setup_websocket_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; httpd_uri_t ws = { .uri = "/ws", .method = HTTP_GET, .handler = handle_ws_req, .user_ctx = NULL, .is_websocket = true}; if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &ws); } return server; } void app_main() { // Initialize NVS esp_err_t ret = nvs_flash_init(); 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(); } ESP_ERROR_CHECK(ret); connect_wifi(); if (wifi_connect_status) { gpio_pad_select_gpio(LED_PIN); gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT); led_state = 0; ESP_LOGI(TAG, "ESP32 ESP-IDF WebSocket Web Server is running ... ...\n"); initi_web_page_buffer(); setup_websocket_server(); } } |
Как работает код
Сначала подключим заголовочные файлы предназначенные для библиотек, специфичных для ESP32, и предоставляющих различные функции, необходимые ESP32 для обработки задач, связанных с веб-сервером и WebSocket, таких как создание и управление сетевыми соединениями, а также хранение и чтение файлов из флэш-памяти.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include <esp_http_server.h> #include "nvs_flash.h" #include "esp_spiffs.h" #include "connect_wifi.h" |
Параметр Define LED_PIN 2 определяет номер контакта, к которому подключен светодиод.
|
1 |
#define LED_PIN 2 |
Параметр httpd_handle_t server = NULL; объявляет переменную server типа httpd_handle_t, которая является дескриптором веб-сервера WebSocker.
|
1 2 3 4 5 |
httpd_handle_t server = NULL; struct async_resp_arg { httpd_handle_t hd; int fd; }; |
static const char *TAG = “WebSocket Server”; определяет строковую константу, используемую в целях отладки.
Настройка веб-сервера WebSocket на ESP32
Функция setup_websocket_server() настраивает и запускает HTTP-сервер с конечной точкой WebSocket.
Функция начинается с определения переменной типа httpd_config_t с именем config и конфигурацией по умолчанию. Затем она определяет две переменные типа httpd_uri_t: uri_get и ws. uri_get — это конфигурация для обработки GET-запросов к корневому URL («/»), а функция обработчика для этого URI устанавливается на get_req_handler().
|
1 2 3 4 5 |
httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; |
ws — это конфигурация для обработки запросов WebSocket. URI для этой конфигурации устанавливается на «/ws», метод для этого URI — на HTTP_GET, а функция обработчика для этого URI — на handle_ws_req(). Также устанавливается значение is_websocket равным true, что сообщает серверу httpd, что этот URI будет обрабатывать трафик WebSocket.
|
1 2 3 4 5 6 |
httpd_uri_t ws = { .uri = "/ws", .method = HTTP_GET, .handler = handle_ws_req, .user_ctx = NULL, .is_websocket = true}; |
После этого вызывается функция httpd_start(), в которую передаются переменная сервера и переменная конфигурации. Функция httpd_start() настраивает HTTP-сервер с указанной конфигурацией и возвращает ESP_OK, если сервер успешно запускается.
|
1 |
if (httpd_start(&server, &config) == ESP_OK) |
Затем, после функции httpd_start(), вызывается httpd_register_uri, код вызывает httpd_register_uri_handler(server, &uri_get); и httpd_register_uri_handler(server, &ws); для регистрации переменных uri_get и ws на сервере. Это связывает соответствующую функцию обработчика с соответствующим URI, так что при получении сервером запроса для этого URI будет вызвана соответствующая функция обработчика.
Функция handle_ws_req() отвечает за обработку запросов WebSocket и занимается открытием, закрытием и обработкой данных, передаваемых по WebSocket-соединению, что объясняется в следующем разделе. Функция get_req_handler() обрабатывает HTTP GET-запрос к корневому URL-адресу и предоставляет файл index.html, хранящийся во флэш-памяти SPIFFS, в качестве ответа на первоначальное рукопожатие с клиентами.
|
1 2 3 4 5 |
if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &ws); } |
Наконец, функция setup_websocket_server() возвращает переменную server, которая является дескриптором сервера. Этот дескриптор можно использовать для доступа к серверу и выполнения таких задач, как остановка или отмена регистрации обработчиков URI.
Вкратце, эта функция настраивает и запускает HTTP-сервер с конечной точкой WebSocket, конфигурирует его для обработки GET-запросов по корневому URL и WebSocket-запросов по URI «/ws», а также регистрирует соответствующие функции обработчика для этих URI на сервере. Она также запускает HTTP-сервер и возвращает дескриптор сервера.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
httpd_handle_t setup_websocket_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; httpd_uri_t ws = { .uri = "/ws", .method = HTTP_GET, .handler = handle_ws_req, .user_ctx = NULL, .is_websocket = true}; if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &ws); } return server; } |
Функция обработки полученных кадров WebSocket
Функция handle_ws_req(httpd_req_t *req) отвечает за обработку запросов WebSocket, отправляемых на сервер от всех веб-клиентов.
|
1 |
static esp_err_t handle_ws_req(httpd_req_t *req) |
Функция начинает с проверки, является ли метод запроса HTTP_GET. Если да, она выводит сообщение о завершении рукопожатия, открытии WebSocket-соединения и возвращает ESP_OK. Это связано с тем, что веб-серверы WebSocket начинают первоначальное взаимодействие с HTTP-рукопожатия, а затем следуют протоколу WebSocket.
|
1 2 3 4 5 |
if (req->method == HTTP_GET) { ESP_LOGI(TAG, "Handshake done, the new connection was opened"); return ESP_OK; } |
Если метод запроса не HTTP_GET, это означает, что запрос отправляет кадры данных WebSocket. Затем функция объявляет переменную ws_pkt типа httpd_ws_frame_t и переменную buf типа uint8_t (указатели), которые будут использоваться для хранения кадров данных. После этого она устанавливает поля переменной ws_pkt в ноль и задает тип HTTPD_WS_TYPE_TEXT.
|
1 2 3 4 |
httpd_ws_frame_t ws_pkt; uint8_t *buf = NULL; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.type = HTTPD_WS_TYPE_TEXT; |
Затем функция вызывает httpd_ws_recv_frame(req, &ws_pkt, 0) для получения фрейма данных WebSocket и сохранения его в переменной ws_pkt. Эта функция возвращает ESP_OK, если фрейм данных получен успешно, и сохраняет длину фрейма данных в поле ws_pkt.len. Если функция возвращает ошибку, она регистрирует ошибку и возвращает её.
|
1 2 3 4 5 6 |
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); return ret; } |
Если полученный кадр данных имеет ненулевую длину, функция вызывает calloc(1, ws_pkt.len + 1) для выделения памяти для переменной buf, которая будет использоваться для хранения полезной нагрузки кадра данных. Затем она снова вызывает httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len) для получения полезной нагрузки кадра данных и сохранения её в переменной buf. Функция выводит в лог полученное сообщение и длину кадра.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if (ws_pkt.len) { buf = calloc(1, ws_pkt.len + 1); if (buf == NULL) { ESP_LOGE(TAG, "Failed to calloc memory for buf"); return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); free(buf); return ret; } ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); } ESP_LOGI(TAG, "frame len is %d", ws_pkt.len); |
Наконец, функция проверяет, является ли полученное сообщение «toggle», и если да, то вызывает функцию trigger_async_send(req->handle, req) и возвращает ESP_OK.
|
1 2 3 4 5 6 7 |
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && strcmp((char *)ws_pkt.payload, "toggle") == 0) { free(buf); return trigger_async_send(req->handle, req); } return ESP_OK |
Вкратце, эта функция обрабатывает WebSocket-запросы, обрабатывая WebSocket-кадры данных, принимая сообщение, отправленное в запросе, и обрабатывая это сообщение путем отправки асинхронного сообщения всем подключенным клиентам.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
static esp_err_t handle_ws_req(httpd_req_t *req) { if (req->method == HTTP_GET) { ESP_LOGI(TAG, "Handshake done, the new connection was opened"); return ESP_OK; } httpd_ws_frame_t ws_pkt; uint8_t *buf = NULL; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.type = HTTPD_WS_TYPE_TEXT; esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); return ret; } if (ws_pkt.len) { buf = calloc(1, ws_pkt.len + 1); if (buf == NULL) { ESP_LOGE(TAG, "Failed to calloc memory for buf"); return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); free(buf); return ret; } ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); } ESP_LOGI(TAG, "frame len is %d", ws_pkt.len); if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && strcmp((char *)ws_pkt.payload, "toggle") == 0) { free(buf); return trigger_async_send(req->handle, req); } return ESP_OK; } |
Функция отправки ответа на кадры WebSocket
Следующие две функции отвечают на запросы WebSocket и отправляют кадры всем подключенным клиентам. Функция ws_async_send отвечает за обработку отправки данных по WebSocket-соединению клиентам. Эта функция запускается при вызове функции trigger_async_send.
|
1 2 |
static void ws_async_send(void *arg) static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) |
Функция trigger_async_send отвечает за запуск функции ws_async_send путем выделения памяти для структуры async_resp_arg, заполнения ее дескриптором сервера и файловым дескриптором веб-сокетного соединения, а затем постановки функции ws_async_send в очередь с помощью функции httpd_queue_work и передачи в качестве аргументов дескриптора сервера, функции ws_async_send и указателя на структуру resp_arg.
|
1 2 3 4 5 6 7 |
static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) { struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); resp_arg->hd = req->handle; resp_arg->fd = httpd_req_to_sockfd(req); return httpd_queue_work(handle, ws_async_send, resp_arg); } |
Функция ws_async_send принимает в качестве входного параметра указатель типа void arg, который представляет собой указатель на структуру async_resp_arg, содержащую дескриптор веб-сервера и файловый дескриптор соединения. Функция выполняет следующие шаги:
- Переключает состояние переменной led_state, которая отслеживает состояние светодиода.
- Обновляет состояние светодиода в соответствии с параметром led_state.
- Форматирует строковый буфер с текущим значением led_state и формирует полезную нагрузку пакета веб-сокета.
- Отправляет пакет всем подключенным клиентам, используя функцию httpd_ws_send_frame_async, передавая дескриптор веб-сервера, дескриптор файла и веб-сокет-пакет.
- В итоге освобождается память, выделенная для структуры resp_arg.
В этом коде функция ws_async_send вызывается асинхронно для отправки сообщения WebSocket всем подключенным клиентам. Она принимает один аргумент — указатель на структуру async_resp_arg, содержащую информацию о текущем соединении.
|
1 2 3 4 |
httpd_ws_frame_t ws_pkt; struct async_resp_arg *resp_arg = arg; httpd_handle_t hd = resp_arg->hd; int fd = resp_arg->fd; |
Функция сначала инвертирует состояние светодиода, устанавливая led_state = !led_state, а также устанавливает состояние контакта светодиода в это значение с помощью функции gpio_set_level. Затем она создает буфер размером 4 и использует функцию sprintf для записи значения led_state в этот буфер в виде строки.
|
1 2 |
led_state = !led_state; gpio_set_level(LED_PIN, led_state); |
Затем создается структура httpd_ws_frame_t с именем ws_pkt, в ее полезной нагрузке указывается путь к буферу (buff), а len — длина строки, хранящейся в буфере, а также тип сообщения устанавливается на HTTPD_WS_TYPE_TEXT.
|
1 2 3 4 |
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = (uint8_t *)buff; ws_pkt.len = strlen(buff); ws_pkt.type = HTTPD_WS_TYPE_TEXT; |
Затем функция получает список подключенных клиентов и перебирает его, отправляя сообщение ws_pkt всем клиентам, тип подключения которых равен HTTPD_WS_CLIENT_WEBSOCKET.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static size_t max_clients = CONFIG_LWIP_MAX_LISTENING_TCP; size_t fds = max_clients; int client_fds[max_clients]; esp_err_t ret = httpd_get_client_list(server, &fds, client_fds); if (ret != ESP_OK) { return; } for (int i = 0; i < fds; i++) { int client_info = httpd_ws_get_fd_info(server, client_fds[i]); if (client_info == HTTPD_WS_CLIENT_WEBSOCKET) { httpd_ws_send_frame_async(hd, client_fds[i], &ws_pkt); } } |
Наконец, освобождается память структуры resp_arg с помощью функции free(resp_arg);
|
1 |
free(resp_arg); |
Обработчик начального HTTP-рукопожатия WebSocket
Как обсуждалось ранее, веб-серверы WebSocket начинают первоначальное взаимодействие с HTTP-рукопожатия, а затем следуют протоколу связи WebSocket. Поэтому нам необходимо настроить обработчик HTTP GET-запросов.
Функция «get_req_handler()» используется для отправки веб-страницы клиенту в ответ на HTTP-запрос. В качестве аргумента она принимает указатель на структуру «httpd_req_t», которая представляет собой HTTP-запрос.
Функция начинается с объявления переменной «response» типа «int». Затем она проверяет значение глобальной переменной «led_state». Если оно истинно, функция использует функцию «sprintf()» для форматирования содержимого буфера «index_html» и строки «ON» в буфер «response_data». Если «led_state» ложно, функция форматирует содержимое буфера «index_html» и строку «OFF» в буфер «response_data».
Затем функция вызывает функцию «httpd_resp_send()», передавая ей в качестве аргументов указатель на аргумент «req», указатель на буфер «response_data» и значение «HTTPD_RESP_USE_STRLEN». Это отправляет содержимое буфера «response_data» в качестве ответа на HTTP-запрос. Затем функция возвращает значение, возвращаемое функцией «httpd_resp_send()».
Функция «get_req_handler()» используется в качестве обработчика запроса для корневого URI «/». В качестве аргумента она принимает указатель на структуру «httpd_req_t», которая представляет собой HTTP-запрос. Функция просто вызывает функцию «send_web_page()» и возвращает её значение.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
esp_err_t get_req_handler(httpd_req_t *req) { int response; if(led_state) { sprintf(response_data, index_html, "ON"); } else { sprintf(response_data, index_html, "OFF"); } response = httpd_resp_send(req, response_data, HTTPD_RESP_USE_STRLEN); return response; } |
Считывание содержимого файла index.html из SPIFSS в буфер (initi_web_page_buffer)
В коде веб-сервера ESP32 WebSocket функция initi_web_page_buffer() инициализирует буфер с именем «index_html» содержимым файла «index.html», хранящегося в файловой системе SPIFFS (SPI Flash File System).
Функция начинается с определения структуры с именем «conf» типа «esp_vfs_spiffs_conf_t» и инициализации её несколькими полями. Поле «base_path» задаёт корневой каталог файловой системы SPIFFS, поле «partition_label» задаёт метку раздела SPIFFS (в данном случае она устанавливается в NULL), поле «max_files» задаёт максимальное количество файлов, которые могут быть открыты одновременно, а поле «format_if_mount_failed» указывает, следует ли форматировать файловую систему в случае неудачного монтирования.
Затем функция вызывает функцию «esp_vfs_spiffs_register()», передавая ей в качестве аргумента указатель на структуру «conf». Эта функция монтирует файловую систему SPIFFS, используя указанную конфигурацию.
Затем функция обнуляет все байты в буфере «index_html» с помощью функции «memset()». После этого она объявляет структуру с именем «st» типа «struct stat» и проверяет, существует ли файл «index.html», используя функцию «stat()». Если файл не существует, она выводит сообщение об ошибке в лог и завершает работу функции.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static void initi_web_page_buffer(void) { esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = NULL, .max_files = 5, .format_if_mount_failed = true}; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); memset((void *)index_html, 0, sizeof(index_html)); struct stat st; if (stat(INDEX_HTML_PATH, &st)) { ESP_LOGE(TAG, "index.html not found"); return; } FILE *fp = fopen(INDEX_HTML_PATH, "r"); if (fread(index_html, st.st_size, 1, fp) == 0) { ESP_LOGE(TAG, "fread failed"); } fclose(fp); } |
Демонстрация работы веб-сервера WebSocket на ESP32
В этом разделе мы рассмотрим демонстрацию работы веб-сервера WebSocket на ESP32, созданного с помощью ESP-IDF. Для начала скачайте полный проект по следующей ссылке и соберите его.
Для прошивки микросхемы введите следующую команду в последовательном терминале. Не забудьте заменить COM-порт на тот, через который подключена ваша плата.
|
1 |
idf.py -p COMX flash monitor |
После успешной прошивки кода вы можете просмотреть все информационные журналы. Сначала инициализируется Wi-Fi станции. Затем мы можем просмотреть esp_netif_handlers, который включает IP-адрес, маску и адрес шлюза. Затем мы получаем запись «получен IP-адрес», за которой следует сам IP-адрес. Мы будем использовать этот IP-адрес для доступа к веб-серверу.
Откройте новый веб-браузер и введите IP-адрес, полученный в терминале, затем нажмите Enter. Откроется веб-сервер ESP32 WebSocket, отображающий веб-страницу для переключения встроенного светодиода ESP32 (GPIO2). Изначально светодиод выключен, и его состояние также отображается на веб-сервере.
Теперь переключите кнопку, и светодиод будет включаться/выключаться соответственно. Аналогичным образом, состояние GPIO2 также будет постоянно обновляться на веб-сервере.

Теперь, если вы используете несколько клиентов с веб-сервером ESP32 и переключаете состояние светодиода с любого клиента, то состояние светодиода также будет отображаться на всех клиентах.
Заключение
В заключение, представленный код представляет собой простую реализацию WebSocket-сервера на микроконтроллере ESP32. WebSocket-сервер создан с использованием фреймворка ESP-IDF и библиотеки lwIP и основан на HTTP-сервере. Сервер предоставляет две основные функции: отображение веб-страницы с кнопкой для переключения состояния светодиода и отправку состояния светодиода в виде сообщения WebSocket всем подключенным клиентам. Когда сервер получает сообщение «toggle» от клиента, состояние светодиода инвертируется, и обновленное состояние затем отправляется обратно всем подключенным клиентам в виде сообщения WebSocket. Сервер также предоставляет два URI: «/» и «/ws», первый используется для отображения веб-страницы, а второй — для обмена данными по протоколу WebSocket. Веб-страница клиента хранится в файловой системе и отображается сервером по URI «/».
7 просмотров
















