Продолжаем работу с нашим сервером HTTP и на данном уроке мы попробуем расположить контент для ответа клиенту в пространстве файловой системы SPIFFS, с которой мы познакомились, а вернее, продолжили знакомство на прошлом уроке. Причём мы уже практически весь нужный контент (страничку и иконку) уже разместили в данной файловой системе. Схема наша также осталась прежней — отладочная плата с контроллером ESP32, подключенная к USB-порту компьютера
Проект мы будем использовать из урока 28 с именем WIFI_STA_HTTP_SERVER_IDF и присвоим ему новое имя WIFI_STA_HTTP_SERVER_SPIFFS. В каталог с проектом скопируем файл partitions.csv из проекта прошлого урока с именем SPIFFS.
Откроем проект в Espressif IDE и в конфигураторе выберем способ использования таблицы разделов — custom
В файле main.h подключим заголовочные файлы для работы с файловой системой
1 2 3 |
#include "nvs_flash.h" #include "esp_spiffs.h" #include "spiffs_config.h" |
В функции app_main файла main.c добавим код для инициализации файловой системы практически такой же как и в прошлом занятии, только без просмотра и записи файлов
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 |
ESP_LOGI(TAG, "nvs_flash_init: 0x%04x", ret); ESP_LOGI(TAG, "Initializing SPIFFS"); esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = NULL, .max_files = 5, .format_if_mount_failed = true }; 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; } size_t total = 0, used = 0; ret = esp_spiffs_info(conf.partition_label, &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); } |
В файле http.c удалим объявление и инициализацию массивов index_html и favicon_ico, так как данные файлы у нас уже есть в нашей файловой системе.
Объявим макрос для сравнения расширения файла с некоторыми известными типами, а также добавим функцию, которая будет определять тип файла для дальнейшего формирования полей заголовка ответа клиенту, а заодно его и сформирует сразу
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
static const char *TAG = "http"; //------------------------------------------------------------- #define IS_FILE_EXT(filename, ext) \ (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0) //------------------------------------------------------------- static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename) { if (IS_FILE_EXT(filename, ".pdf")) { return httpd_resp_set_type(req, "application/pdf"); } else if (IS_FILE_EXT(filename, ".html")) { return httpd_resp_set_type(req, "text/html"); } else if (IS_FILE_EXT(filename, ".jpeg")) { return httpd_resp_set_type(req, "image/jpeg"); } else if (IS_FILE_EXT(filename, ".ico")) { return httpd_resp_set_type(req, "image/x-icon"); } return httpd_resp_set_type(req, "text/plain"); } //------------------------------------------------------------- |
Ниже добавим функцию, которая будет определять путь к файлу
1 2 3 4 5 |
//------------------------------------------------------------- static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize) { } //------------------------------------------------------------- |
Вычислим длину строки пути
1 2 3 |
static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize) { const size_t base_pathlen = strlen(base_path); |
Аналогично вычислим длину строки идентификатора ресурса
1 2 |
const size_t base_pathlen = strlen(base_path); size_t pathlen = strlen(uri); |
Узнаем, есть ли параметры, если есть то сохраним позицию их начала
1 2 |
size_t pathlen = strlen(uri); const char *quest = strchr(uri, '?'); |
В файле http.h подключим файл для работы с параметрами
1 2 |
#include "esp_log.h" #include <sys/param.h> |
Вернёмся в http.c в функцию get_path_from_uri и, если параметры есть, то изменим длину пути, если позиция параметров меньше длины пути
1 2 3 4 |
const char *quest = strchr(uri, '?'); if (quest) { pathlen = MIN(pathlen, quest - uri); } |
Аналогично поступим с хештегами
1 2 3 4 5 6 |
pathlen = MIN(pathlen, quest - uri); } const char *hash = strchr(uri, '#'); if (hash) { pathlen = MIN(pathlen, hash - uri); } |
Если путь получился слишком длинным (больше чем максимальный во входящем аргументе функции), то выходим из функции
1 2 3 4 5 |
pathlen = MIN(pathlen, hash - uri); } if (base_pathlen + pathlen + 1 > destsize) { return NULL; } |
А если всё нормально, то соберём наш путь в единую строку и возвратим указатель
1 2 3 4 5 |
return NULL; } strcpy(dest, base_path); strlcpy(dest + base_pathlen, uri, pathlen + 1); return dest + base_pathlen; |
Функцию all_get_handler переименуем
static esp_err_t download_get_handler(httpd_req_t *req)
Так как в данной функции будет очень много изменений, то тело очистим полностью.
В файле http.h подключим заголовочный файл для работы с виртуальной файловой системой, объявим макрос, который определит максимальный размер пути к файлу, также объявим макрос максимального размера считываемого фрагмента файла и структуру для данных файла сервера
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <esp_http_server.h> #include "esp_vfs.h" //------------------------------------------------------------- #define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN) #define SCRATCH_BUFSIZE 8192 struct file_server_data { /* Base path of file storage */ char base_path[ESP_VFS_PATH_MAX + 1]; /* Scratch buffer for temporary storage during file transfer */ char scratch[SCRATCH_BUFSIZE]; }; //------------------------------------------------------------- |
Вернёмся в http.c и в теле функции download_get_handler объявим символьный массив для пути к файлу, указатель на дескриптор файла и также переменную типа структуры состояния файла
1 2 3 4 5 |
static esp_err_t download_get_handler(httpd_req_t *req) { char filepath[FILE_PATH_MAX]; FILE *fd = NULL; struct stat file_stat; |
Определим путь к файлу
1 2 3 4 |
struct stat file_stat; const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path, req->uri, sizeof(filepath)); |
Если NULL, то выведем в терминале соответствующее сообщение и вернём клиенту страницу ошибки
1 2 3 4 5 6 |
req->uri, sizeof(filepath)); if (!filename) { ESP_LOGE(TAG, "Filename is too long"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long"); return ESP_FAIL; } |
Если слэш, то будем считать, что клиенту надо передать файл index.html
1 2 3 4 5 |
return ESP_FAIL; } if (strcmp(filename,"/") == 0) { strcat(filepath, "index.html"); } |
Узнаем состояние файла
1 2 3 |
strcat(filepath, "index.html"); } stat(filepath, &file_stat); |
Попытаемся открыть файл на чтение
1 2 |
stat(filepath, &file_stat); fd = fopen(filepath, "r"); |
Если файл не существует, выведем соответствующее сообщение в терминал и отправим клиенту страницу с ошибкой
1 2 3 4 5 6 7 |
fd = fopen(filepath, "r"); if (!fd) { ESP_LOGE(TAG, "Failed to read existing 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; } |
Если всё нормально, то выведем в терминал информацию о файле: его имя и размер
1 2 3 |
return ESP_FAIL; } ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size); |
При помощи добавленной нами выше функции сформируем поле типа файла заголовка HTTP
1 2 |
ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size); set_content_type_from_file(req, filename); |
Найдём позицию начала контента
1 2 |
set_content_type_from_file(req, filename); char *chunk = ((struct file_server_data *)req->user_ctx)->scratch; |
Считаем контент при помощи цикла, в котором будем определять размер и разбивать контент на части, передавая клиенту каждую из них
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch; size_t chunksize; do { chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd); if (chunksize > 0) { if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) { fclose(fd); ESP_LOGE(TAG, "File sending failed!"); httpd_resp_sendstr_chunk(req, NULL); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); return ESP_FAIL; } } } while (chunksize != 0); |
Закроем файл, выведем сообщение в терминал о том, что файл передан, передадим пустой пакет, означающий окончание документа и вернём ноль.
1 2 3 4 5 |
} while (chunksize != 0); fclose(fd); ESP_LOGI(TAG, "File sending complete"); httpd_resp_send_chunk(req, NULL, 0); return ESP_OK; |
Удалим функцию favicon_get_handler, так как у нас теперь есть универсальная функция, передающая любые файлы, а также переменные all и favicon_uri.
В функции start_webserver объявим указатель на переменную типа структуры данных файла сервера
1 2 |
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); static struct file_server_data *server_data = NULL; |
Если такой указатель уже существует, то выведем соответствующее сообщение в терминал и уйдём из функции с соответствующей ошибкой
1 2 3 4 5 |
static struct file_server_data *server_data = NULL; if (server_data) { ESP_LOGE(TAG, "File server already started"); return ESP_ERR_INVALID_STATE; } |
А если всё нормально, то выделим память под данную структуру
1 2 3 |
return ESP_ERR_INVALID_STATE; } server_data = calloc(1, sizeof(struct file_server_data)); |
Если выделить память не удалось, также уйдём из функции с соответствующей ошибкой
1 2 3 4 5 |
server_data = calloc(1, sizeof(struct file_server_data)); if (!server_data) { ESP_LOGE(TAG, "Failed to allocate memory for server data"); return ESP_ERR_NO_MEM; } |
А если удалось, то скопируем путь к файлу в соответствующее поле структуры
1 2 3 4 |
return ESP_ERR_NO_MEM; } strlcpy(server_data->base_path, "/spiffs", sizeof(server_data->base_path)); |
Соответствующему полю переменной типа структуры конфигурации сервера присвоим адрес функции проверки соответствия шаблону URI
1 2 |
sizeof(server_data->base_path)); config.uri_match_fn = httpd_uri_match_wildcard; |
После следующей строки
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
удалим весь код функции.
Попытаемся запустить наш сервер и в случае неудачи уйдём из функции, выведя соответствующее сообщение в терминал
1 2 3 4 5 6 7 |
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); if (httpd_start(&server, &config) != ESP_OK) { ESP_LOGI(TAG, "Error starting server!"); return NULL; } |
Зарегистрируем URI и выйдем из функции с нулём (ESP_OK)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
return NULL; } ESP_LOGI(TAG, "Registering URI handlers"); httpd_uri_t file_download = { .uri = "/*", // Match all URIs of type /path/to/file .method = HTTP_GET, .handler = download_get_handler, .user_ctx = server_data // Pass server data as context }; httpd_register_uri_handler(server, &file_download); return server; |
Соберём код, прошьём контроллер и узнаем IP-адрес нашего HTTP-сервера в терминале
Отфильтруемся по данному адресу в WireShark и введём его в браузере
Отлично! Наш клиент всё получил. И страничку, и иконку, что подтверждается иконкой в заголовке закладки.
Также мы весь обмен видим и в WireShark
В терминале мы также видим всю передачу вместе с именами и размерами файлов
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Недорогие отладочные платы ESP32/ESP32-C3/ESP32-S3 можно купить здесь Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Добавить комментарий