В этом уроке мы создадим веб-сервер на базе ESP32 с использованием ESP-IDF, который будет отображать показания датчика BME680. Датчик окружающей среды BME680 широко используется в электронных проектах для измерения температуры окружающей среды, барометрического давления, относительной влажности и содержания летучих органических соединений (VOC) или качества воздуха в помещении (IAQ). Веб-сервер будет работать как метеостанция, отображая показания температуры, влажности, содержания газов и давления на веб-странице, которая будет обновляться каждые 10 секунд. Мы будем использовать VS Code с расширением ESP-IDF для создания и сборки этого веб-сервера на базе ESP32. К нему будет легко получить доступ на различных устройствах, таких как ноутбуки, мобильные телефоны и т. д., подключенных к той же сети Wi-Fi, что и плата ESP32.
Ранее на нашем сайте мы рассматривали проекты аналогичных веб-серверов:
Также вы можете прочитать статью про подключение датчика BME680 к ESP32 с помощью ESP-IDF.
Обзор веб-сервера на ESP32 и BME680
Наш ESP32 будет размещать веб-сервер, который будет предоставлять HTML/CSS-файлы веб-клиенту, то есть веб-браузеру. Мы создадим веб-сервер с помощью библиотеки HTTP-веб-сервера, доступной в esp-idf. Когда пользователь вводит IP-адрес в веб-браузере, на ESP32 отправляется HTTP GET-запрос. ESP32 отвечает на запрос, отправляя буфер, содержащий HTML и CSS-файлы для построения веб-страницы, отображающей данные датчика BME680.
Пользователи могут получить доступ к веб-серверу через IP-адрес и любое веб-приложение, например, веб-браузер на ноутбуке или мобильном телефоне.
Датчик BME680
BME680 — это маломощный датчик «четыре в одном», объединяющий датчики температуры, давления, влажности и газа. Он работает от напряжения 1,8–3,8 В и взаимодействует с другими микроконтроллерами по протоколам I2C и SPI. Этот датчик используется в таких областях, как отслеживание качества воздуха, измерение влажности, отслеживание погодных условий, домашняя автоматизация и управление GPS-навигацией.
Схема расположения контактов
Ниже представлена схема расположения контактов датчика BME680:
| Название контакта | Описание контакта |
| VCC | Контакт для питания датчика |
| GND | Общий провод (земля) |
| SCL | Микросхема BME680 предоставляет два интерфейса для получения показаний датчика: SPI и I2C. Этот вывод отвечает за передачу тактового сигнала по последовательному интерфейсу как для I2C, так и для SPI. |
| SDA | При использовании протокола I2C этот вывод работает как SDA (Serial Data pin), а при использовании протокола SPI — как SDI (Serial Data In), также известный как MOSI («Master out Slave in»). |
| SDO | Это вывод SDO (вывод данных ведомого устройства) / MISO (вход ведущего устройства, выход ведомого устройства) для связи по протоколу SPI. Этот вывод используется только для интерфейса SPI. |
| CS | Это контакт выбора микросхемы, используемый в SPI-интерфейсе. Он служит входом для датчика. |
Как видим, распиновка у датчика достаточно продуманная.
Контакты I2C ESP32
Мы будем использовать интерфейс I2C для получения показаний датчика BME680 с помощью ESP32 и ESP-IDF. Теперь давайте обсудим контакты I2C ESP32. На приведенной ниже схеме показана распиновка ESP32, где контакты I2C по умолчанию выделены красным цветом. Это контакты I2C по умолчанию модуля ESP32 DEVKIT. Однако, если вы хотите использовать другие контакты GPIO в качестве контактов I2C, вам придется установить их в коде.
Для более подробной информации по этим вопросам можете прочитать статью про использование интерфейса I2C в ESP32 с помощью ESP-IDF.
Подключение датчика BME680 к ESP32
Для подключения датчика BME680 к плате ESP32 нам потребуются следующие компоненты:
- Плата ESP32.
- Датчик BME680.
- Соединительные провода.
- Макетная плата.
Подключение BME680 к плате ESP32 довольно простое, так как оно включает в себя лишь соединение 4 контактов (GND, VCC, SDA и SCL) с ESP32. Необходимо соединить вывод VCC с контактом 3,3 В, заземление с общим заземлением, контакт SCL датчика с контактом SCL платы ESP32, а контакт SDA датчика с контактом SDA платы ESP32.
В ESP32 по умолчанию для вывода SDA используется GPIO21, а для SCL — GPIO22.
Соединения между устройствами указаны в таблице ниже:
| Датчик BME680 | ESP32 |
| GND | GND |
| VCC | 3.3V |
| SDA | GPIO21(I2C SDA) |
| SCL | GPIO22 (I2C SCL) |
Проект веб-сервера на базе ESP32 и BME680 с помощью ESP-IDF
Мы разработаем проект в VS Code с расширением ESP-IDF для измерения температуры окружающей среды, барометрического давления, относительной влажности и содержания летучих органических соединений (ЛОС) или качества воздуха в помещении (IAQ) с помощью датчика окружающей среды BME60 и отображения этих значений на веб-странице.
Данный код создает веб-сервер на ESP32, который размещает HTML/CSS-файлы и считывает показания датчиков. Более того, при получении HTTP-запроса от веб-страницы, он будет отправлять веб-страницы вместе со значениями датчика BME680. Мы будем использовать библиотеку BME680 ESP-IDF, предоставленную UncleRus на GitHub.
Создание примера проекта
Откройте VS Code и перейдите в меню View > Command Palette («Вид» > «Палитра команд»). Введите в строке поиска ESP-IDF: New Project и нажмите Enter.
Укажите название проекта и каталог, а также плату и целевое устройство ESP-IDF. Для платы ESP-IDF мы выбрали опцию custom board («пользовательская плата»). Для целевого устройства ESP-IDF мы выбрали модуль ESP32. Нажмите кнопку ‘Choose Template’ («Выбрать шаблон»), чтобы продолжить.
В разделе «Расширение» выберите опцию ESP-IDF:
На вкладке get-started («Начало работы») мы щелкнем по пункту «sample_project». Затем нажмем ‘Create project using template sample_project’) («Создать проект, используя шаблон sample_project»).
Вы получите уведомление о создании проекта. Чтобы открыть проект в новом окне, нажмите «Да».
Это откроет проект, который мы создали на вкладке «Проводник». Внутри папки проекта находится несколько папок. Это справедливо для каждого проекта, который вы будете создавать с помощью ESP-IDF Explorer.
Для начала добавим необходимые заголовочные файлы для библиотек BME680, требуемых для этого проекта. Создайте новую папку с именем ‘components’ и добавьте в неё папки bme680, esp_idf_lib_helpers и i2cdev, скопировав их по ссылке ниже:
Теперь перейдите к файлу main.c. В папке main находится исходный код, то есть файл main.c будет находиться здесь. Перейдите в папку main > main.c и откройте его. Скопируйте текст с файла main.c.
Код веб-сервера на ESP32 и BME680
|
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 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
#include <stdio.h> #include <stdlib.h> #include <string.h> //Requires by memset #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include <esp_http_server.h> #include "esp_wifi.h" #include "esp_event.h" #include "freertos/event_groups.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "driver/gpio.h" #include <lwip/sockets.h> #include <lwip/sys.h> #include <lwip/api.h> #include <lwip/netdb.h> #include <bme680.h> #define ESP_WIFI_SSID "HUAWEI-u67E" #define ESP_WIFI_PASSWORD "4uF77R2n" #define ESP_MAXIMUM_RETRY 5 #define BME680_I2C_ADDR 0x77 #define PORT 0 #define CONFIG_EXAMPLE_I2C_MASTER_SDA 21 #define CONFIG_EXAMPLE_I2C_MASTER_SCL 22 #ifndef APP_CPU_NUM #define APP_CPU_NUM PRO_CPU_NUM #endif static const char *TAG = "esp32 webserver"; /* FreeRTOS event group to signal when we are connected*/ static EventGroupHandle_t s_wifi_event_group; /* The event group allows multiple bits for each event, but we only care about two events: * - we are connected to the AP with an IP * - we failed to connect after the maximum amount of retries */ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static int s_retry_num = 0; int wifi_connect_status = 0; bme680_t sensor; bme680_values_float_t values; uint32_t duration; char html_page[] = "<!DOCTYPE HTML><html>\n" "<head>\n" " <title>ESP-IDF BME680 Web Server</title>\n" " <meta http-equiv=\"refresh\" content=\"10\">\n" " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" " <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\" integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">\n" " <link rel=\"icon\" href=\"data:,\">\n" " <style>\n" " html {font-family: Arial; display: inline-block; text-align: center;}\n" " p { font-size: 1.2rem;}\n" " body { margin: 0;}\n" " .topnav { overflow: hidden; background-color: #4B1D3F; color: white; font-size: 1.7rem; }\n" " .content { padding: 20px; }\n" " .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }\n" " .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }\n" " .reading { font-size: 2.8rem; }\n" " .card.temperature { color: #0e7c7b; }\n" " .card.humidity { color: #17bebb; }\n" " .card.pressure { color: #3fca6b; }\n" " .card.gas { color: #d62246; }\n" " </style>\n" "</head>\n" "<body>\n" " <div class=\"topnav\">\n" " <h3>ESP-IDF BME680 WEB SERVER</h3>\n" " </div>\n" " <div class=\"content\">\n" " <div class=\"cards\">\n" " <div class=\"card temperature\">\n" " <h4><i class=\"fas fa-thermometer-half\"></i> TEMPERATURE</h4><p><span class=\"reading\">%.2f °C</span></p>\n" " </div>\n" " <div class=\"card humidity\">\n" " <h4><i class=\"fas fa-tint\"></i> HUMIDITY</h4><p><span class=\"reading\">%.2f</span> %</span></p>\n" " </div>\n" " <div class=\"card pressure\">\n" " <h4><i class=\"fas fa-angle-double-down\"></i> PRESSURE</h4><p><span class=\"reading\">%.2fhPa</span></p>\n" " </div>\n" " <div class=\"card gas\">\n" " <h4><i class=\"fas fa-wind\"></i> GAS</h4><p><span class=\"reading\">%.2f KΩ</span></p>\n" " </div>\n" " </div>\n" " </div>\n" "</body>\n" "</html>"; void init_bme680(void) { memset(&sensor, 0, sizeof(bme680_t)); ESP_ERROR_CHECK(bme680_init_desc(&sensor, BME680_I2C_ADDR, PORT, CONFIG_EXAMPLE_I2C_MASTER_SDA, CONFIG_EXAMPLE_I2C_MASTER_SCL)); // init the sensor ESP_ERROR_CHECK(bme680_init_sensor(&sensor)); // Changes the oversampling rates to 4x oversampling for temperature // and 2x oversampling for humidity. Pressure measurement is skipped. bme680_set_oversampling_rates(&sensor, BME680_OSR_4X, BME680_OSR_2X, BME680_OSR_2X); // Change the IIR filter size for temperature and pressure to 7. bme680_set_filter_size(&sensor, BME680_IIR_SIZE_7); // Change the heater profile 0 to 200 degree Celsius for 100 ms. bme680_set_heater_profile(&sensor, 0, 200, 100); bme680_use_heater_profile(&sensor, 0); // Set ambient temperature to 10 degree Celsius bme680_set_ambient_temperature(&sensor, 10); // as long as sensor configuration isn't changed, duration is constant bme680_get_measurement_duration(&sensor, &duration); } void get_bme680_readings(void) { // trigger the sensor to start one TPHG measurement cycle if (bme680_force_measurement(&sensor) == ESP_OK) { // passive waiting until measurement results are available vTaskDelay(duration); // get the results and do something with them if (bme680_get_results_float(&sensor, &values) == ESP_OK) printf("BME680 Sensor: %.2f °C, %.2f %%, %.2f hPa, %.2f KOhm\n", values.temperature, values.humidity, values.pressure, values.gas_resistance); } } 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 < 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); } wifi_connect_status = 0; 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); wifi_connect_status = 1; } } void connect_wifi(void) { 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 = ESP_WIFI_SSID, .password = ESP_WIFI_PASSWORD, /* 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_WPA2_PSK, }, }; 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", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } vEventGroupDelete(s_wifi_event_group); } esp_err_t send_web_page(httpd_req_t *req) { int response; get_bme680_readings(); char response_data[sizeof(html_page) + 4]; memset(response_data, 0, sizeof(response_data)); sprintf(response_data, html_page, values.temperature, values.humidity, values.pressure, values.gas_resistance); response = httpd_resp_send(req, response_data, HTTPD_RESP_USE_STRLEN); return response; } esp_err_t get_req_handler(httpd_req_t *req) { return send_web_page(req); } httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; httpd_handle_t setup_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_handle_t server = NULL; if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); } 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); ESP_ERROR_CHECK(i2cdev_init()); init_bme680(); ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); connect_wifi(); if (wifi_connect_status) { setup_server(); ESP_LOGI(TAG, "Web Server is up and running\n"); } else ESP_LOGI(TAG, "Failed to connected with Wi-Fi, check your network Credentials\n"); } |
Данный код можно также скачать по следующей ссылке.
Как работает код?
Для начала мы подключим необходимые библиотеки, включая bme680.h для работы с датчиком, библиотеки FreeRTOS для генерации задержек, создания задач и групп событий, esp_http_server.h для настройки веб-сервера HTTP, esp_wifi.h для включения Wi-Fi-соединения, esp_event.h для мониторинга событий Wi-Fi, esp_log.h в качестве библиотеки для логирования, библиотеку driver/gpio.h для цифровых выходных контактов ESP32 и т.д. Кроме того, используется библиотека driver/i2c.h, поскольку она позволит ESP32 взаимодействовать с другими устройствами I2C, в нашем случае это датчик BME680.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> #include <stdlib.h> #include <string.h> //Requires by memset #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include <esp_http_server.h> #include "esp_wifi.h" #include "esp_event.h" #include "freertos/event_groups.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "driver/gpio.h" #include <lwip/sockets.h> #include <lwip/sys.h> #include <lwip/api.h> #include <lwip/netdb.h> #include <bme680.h> |
Далее мы определили параметры для SSID и пароля Wi-Fi. Укажите свои собственные учетные данные сети, чтобы успешно подключить плату ESP32 к сети Wi-Fi.
|
1 2 |
#define ESP_WIFI_SSID "YOUR_SSID" #define ESP_WIFI_PASSWORD "YOUR_PASSWORD" |
Мы зададим максимальное значение количества повторных попыток для мониторинга Wi-Fi-соединения. Оно установлено равным 5.
|
1 |
#define ESP_MAXIMUM_RETRY 5 |
Далее мы объявляем переменную ‘s_wifi-event_group’ для хранения созданной группы событий FreeRTOS. Эта группа событий FreeRTOS используется для оповещения о подключении к Wi-Fi.
|
1 |
static EventGroupHandle_t s_wifi_event_group; |
Затем мы определяем два бита для важных для нас событий Wi-Fi. К ним относятся бит, обозначающий успешное подключение к точке доступа Wi-Fi, и бит, обозначающий неудачное подключение к точке доступа Wi-Fi после истечения максимального количества попыток.
|
1 2 |
#define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 |
Далее мы определили контакты SCL и SDA для связи по протоколу I2C. Используйте эти контакты GPIO для соединения двух устройств. Вы также можете изменить эти контакты.
|
1 2 |
#define SDA_PIN GPIO_NUM_21 #define SCL_PIN GPIO_NUM_22 |
Внутри кода различные теги становятся очень полезными, когда нам нужно записывать какую-либо информацию. ESP32-IDF имеет функции логирования разного уровня сложности.
Например, переменная ESP_LOGI используется для записи информации в журнал отладки. Она принимает два аргумента. Первый аргумент — это тег, а второй — отформатированная строка. Поэтому эта глобальная переменная будет полезна при вызове функций ESP_LOGI(). Здесь «esp32 webserver» — это тег, который будет использоваться при записи в журнал.
|
1 |
static const char *TAG = "esp32 webserver"; |
HTML и CSS для создания веб-сервера
Затем у нас есть массив, содержащий HTML- и CSS-скрипты для HTML-страницы. Он включает в себя заголовок веб-сервера и три карточки для отображения показаний каждого датчика, а также иконку для каждого из них.
|
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 |
char html_page[] = "<!DOCTYPE HTML><html>\n" "<head>\n" " <title>ESP-IDF BME680 Web Server</title>\n" " <meta http-equiv=\"refresh\" content=\"10\">\n" " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" " <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\" integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">\n" " <link rel=\"icon\" href=\"data:,\">\n" " <style>\n" " html {font-family: Arial; display: inline-block; text-align: center;}\n" " p { font-size: 1.2rem;}\n" " body { margin: 0;}\n" " .topnav { overflow: hidden; background-color: #4B1D3F; color: white; font-size: 1.7rem; }\n" " .content { padding: 20px; }\n" " .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }\n" " .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }\n" " .reading { font-size: 2.8rem; }\n" " .card.temperature { color: #0e7c7b; }\n" " .card.humidity { color: #17bebb; }\n" " .card.pressure { color: #3fca6b; }\n" " .card.gas { color: #d62246; }\n" " </style>\n" "</head>\n" "<body>\n" " <div class=\"topnav\">\n" " <h3>ESP-IDF BME680 WEB SERVER</h3>\n" " </div>\n" " <div class=\"content\">\n" " <div class=\"cards\">\n" " <div class=\"card temperature\">\n" " <h4><i class=\"fas fa-thermometer-half\"></i> TEMPERATURE</h4><p><span class=\"reading\">%.2f °C</span></p>\n" " </div>\n" " <div class=\"card humidity\">\n" " <h4><i class=\"fas fa-tint\"></i> HUMIDITY</h4><p><span class=\"reading\">%.2f</span> %</span></p>\n" " </div>\n" " <div class=\"card pressure\">\n" " <h4><i class=\"fas fa-angle-double-down\"></i> PRESSURE</h4><p><span class=\"reading\">%.2fhPa</span></p>\n" " </div>\n" " <div class=\"card gas\">\n" " <h4><i class=\"fas fa-wind\"></i> GAS</h4><p><span class=\"reading\">%.2f KΩ</span></p>\n" " </div>\n" " </div>\n" " </div>\n" "</body>\n" "</html>"; |
HTML-код задаётся в тегах <html></html>. Внутри тегов <style></style> задаётся стиль веб-страницы. Это включает в себя семейство шрифтов, размер шрифта, позиционирование текста, размер и цвет карточек и т. д.
Начнём с заголовка веб-страницы. Он размещается внутри тегов <title> </title>. В нашем случае мы установили заголовок веб-страницы как «ESP-IDF BME680 Web Server». Он будет отображаться в строке заголовка браузера.
|
1 |
<title>ESP-IDF BME680 Web Server</title> |
Следующий метатег позволит отображать наш веб-сервер на разных устройствах.
|
1 |
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" |
Как мы уже упоминали, мы хотим добавить значки температуры, влажности и давления внутрь карточек. Поэтому следующий тег ссылки получит значки из FontAwesome.
|
1 |
" <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\" integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">\n" |
Как вы могли заметить, весь текст набран шрифтом Arial и выровнен по центру веб-страницы. Цвет и размер шрифта верхней панели навигации также заданы. На нашей веб-странице три карточки для каждого показания датчика. Общее оформление карточек, а также цвет каждого отдельного показания также заданы. Мы задаем размер и цвет заголовка карточки, а также общее оформление карточки, включая цвет фона, тень и т. д.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<style>\n" " html {font-family: Arial; display: inline-block; text-align: center;}\n" " p { font-size: 1.2rem;}\n" " body { margin: 0;}\n" " .topnav { overflow: hidden; background-color: #4B1D3F; color: white; font-size: 1.7rem; }\n" " .content { padding: 20px; }\n" " .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }\n" " .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }\n" " .reading { font-size: 2.8rem; }\n" " .card.temperature { color: #0e7c7b; }\n" " .card.humidity { color: #17bebb; }\n" " .card.pressure { color: #3fca6b; }\n" " .card.gas { color: #d62246; }\n" " </style>\n" |
Внутри тегов <body> </body> будут размещены заголовок, абзацы и карточки.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<body>\n" " <div class=\"topnav\">\n" " <h3>ESP-IDF BME680 WEB SERVER</h3>\n" " </div>\n" " <div class=\"content\">\n" " <div class=\"cards\">\n" " <div class=\"card temperature\">\n" " <h4><i class=\"fas fa-thermometer-half\"></i> TEMPERATURE</h4><p><span class=\"reading\">%.2f °C</span></p>\n" " </div>\n" " <div class=\"card humidity\">\n" " <h4><i class=\"fas fa-tint\"></i> HUMIDITY</h4><p><span class=\"reading\">%.2f</span> %</span></p>\n" " </div>\n" " <div class=\"card pressure\">\n" " <h4><i class=\"fas fa-angle-double-down\"></i> PRESSURE</h4><p><span class=\"reading\">%.2fhPa</span></p>\n" " </div>\n" " <div class=\"card gas\">\n" " <h4><i class=\"fas fa-wind\"></i> GAS</h4><p><span class=\"reading\">%.2f KΩ</span></p>\n" " </div>\n" " </div>\n" " </div>\n" "</body>\n" |
Заголовок нашего веб-сервера — «ESP-IDF BME680 WEB SERVER», он размещен внутри тегов <h3></h3>.
|
1 |
<h3>ESP-IDF BME680 WEB SERVER</h3> |
Затем идут четыре карточки, на которых отображаются показания температуры, влажности, давления и газа соответственно. Все показания датчиков привязаны к классу «показание». Каждая карточка имеет свой значок, за которым следует заголовок и показание внизу, а также единица измерения.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<body>\n" " <div class=\"topnav\">\n" " <h3>ESP-IDF BME680 WEB SERVER</h3>\n" " </div>\n" " <div class=\"content\">\n" " <div class=\"cards\">\n" " <div class=\"card temperature\">\n" " <h4><i class=\"fas fa-thermometer-half\"></i> TEMPERATURE</h4><p><span class=\"reading\">%.2f °C</span></p>\n" " </div>\n" " <div class=\"card humidity\">\n" " <h4><i class=\"fas fa-tint\"></i> HUMIDITY</h4><p><span class=\"reading\">%.2f</span> %</span></p>\n" " </div>\n" " <div class=\"card pressure\">\n" " <h4><i class=\"fas fa-angle-double-down\"></i> PRESSURE</h4><p><span class=\"reading\">%.2fhPa</span></p>\n" " </div>\n" " <div class=\"card gas\">\n" " <h4><i class=\"fas fa-wind\"></i> GAS</h4><p><span class=\"reading\">%.2f KΩ</span></p>\n" " </div>\n" " </div>\n" " </div>\n" "</body>\n" |
Ниже приведён код строки, отображающей показания температуры, а также единицы измерения и значок термометра.
|
1 |
<h4><i class=\"fas fa-thermometer-half\"></i> TEMPERATURE</h4><p><span class=\"reading\">%.2f°C</span></p> |
Считывание показаний датчика BME680
Эти строки определяют адрес BME680, который равен 0x77, порт I2C и номера контактов I2C ESP32, которые равны 21 (SDA) и 22 (SCL).
|
1 2 3 4 |
#define BME680_I2C_ADDR 0x77 #define PORT 0 #define CONFIG_EXAMPLE_I2C_MASTER_SDA 21 #define CONFIG_EXAMPLE_I2C_MASTER_SCL 22 |
Необходимо инициализировать библиотеку i2c, причём это следует делать до вызова любой функции библиотеки bme680 esp-idf.
|
1 |
ESP_ERROR_CHECK(i2cdev_init()); |
Функция инициализации BME680
Задача init_bme680() инициализирует датчик BME680, определяет порт I2C, к которому подключен датчик, устанавливает частоту дискретизации для показаний температуры, влажности, давления и газа, устанавливает размер фильтров для температуры и давления, устанавливает продолжительность измерения и т. д.
|
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 |
void init_bme680(void) { memset(&sensor, 0, sizeof(bme680_t)); ESP_ERROR_CHECK(bme680_init_desc(&sensor, BME680_I2C_ADDR, PORT, CONFIG_EXAMPLE_I2C_MASTER_SDA, CONFIG_EXAMPLE_I2C_MASTER_SCL)); // init the sensor ESP_ERROR_CHECK(bme680_init_sensor(&sensor)); // Changes the oversampling rates to 4x oversampling for temperature // and 2x oversampling for humidity. Pressure measurement is skipped. bme680_set_oversampling_rates(&sensor, BME680_OSR_4X, BME680_OSR_2X, BME680_OSR_2X); // Change the IIR filter size for temperature and pressure to 7. bme680_set_filter_size(&sensor, BME680_IIR_SIZE_7); // Change the heater profile 0 to 200 degree Celsius for 100 ms. bme680_set_heater_profile(&sensor, 0, 200, 100); bme680_use_heater_profile(&sensor, 0); // Set ambient temperature to 10 degree Celsius bme680_set_ambient_temperature(&sensor, 10); // as long as sensor configuration isn't changed, duration is constant bme680_get_measurement_duration(&sensor, &duration); } |
Теперь давайте рассмотрим функцию каждой строки по отдельности.
Здесь мы создаём экземпляр структуры bme680_t, которая используется для настройки и инициализации датчика. После этого инициализируем все члены структуры sensor нулями с помощью команды memset.
|
1 2 |
bme680_t sensor; memset(&sensor, 0, sizeof(bme680_t)); |
|
1 2 3 4 5 6 7 8 9 10 |
typedef struct { i2c_dev_t i2c_dev; //!< I2C device descriptor bool meas_started; //!< Indicates whether measurement started uint8_t meas_status; //!< Last sensor status (for internal use only) bme680_settings_t settings; //!< Sensor settings bme680_calib_data_t calib_data; //!< Calibration data of the sensor } bme680_t; |
Функция bme680_init_desc() инициализирует дескриптор устройства BME680, передавая структуру датчика, I2C-адрес BME680 и номера контактов I2C ESP32.
|
1 |
ESP_ERROR_CHECK(bme680_init_desc(&sensor, BME680_I2C_ADDR, PORT, CONFIG_EXAMPLE_I2C_MASTER_SDA, CONFIG_EXAMPLE_I2C_MASTER_SCL)); |
Метод bme680_init_sensor() инициализирует структуру данных датчика BME680, выполняет проверку датчика, программно сбрасывает датчик и настраивает его с помощью следующих параметров по умолчанию:
- Коэффициент передискретизации для температуры, давления и влажности = osr_1x
- Размер фильтра для давления и температуры = iir_size 3
- Профиль нагревателя 0 с температурой 320 °C и длительностью 150 мс.
|
1 |
ESP_ERROR_CHECK(bme680_init_sensor(&sensor)); |
Функция bme680_set_oversampling_rates() устанавливает частоту передискретизации для измерений температуры, влажности, давления и газа. С помощью этой функции можно установить индивидуальную частоту дискретизации для температуры, давления и влажности. Функция bme680_init_desc() устанавливает частоту дискретизации по умолчанию равной 1x. Но с помощью этой функции мы можем изменить её. Возможные значения частоты передискретизации: 1x (по умолчанию), 2x, 4x, 8x, 16x.
|
1 |
bme680_set_oversampling_rates(&sensor, BME680_OSR_4X, BME680_OSR_2X, BME680_OSR_2X); |
Функция bme680_set_filter_size() определяет размер IRR-фильтров, поскольку BME680 имеет внутренний IRR-фильтр, который можно использовать для минимизации влияния кратковременных изменений выходных значений датчика, которые могут быть вызваны внешними помехами. IRR-фильтр регулирует уменьшение полосы пропускания выходного сигнала датчика.
|
1 |
bme680_set_filter_size(&sensor, BME680_IIR_SIZE_7); |
Датчик BME680 имеет встроенный разъем для измерения газа и поддерживает до 10 профилей нагревателя. Каждый профиль состоит из заданной температуры (целевой температуры) и продолжительности нагрева. Эта функция задает профиль нагревателя. Входными аргументами являются структура описания датчика, профиль нагревателя, целевая температура и продолжительность нагрева. Функция bme680_set_heater_profile() активирует измерение газа с заданным профилем нагревателя.
|
1 2 |
bme680_set_heater_profile(&sensor, 0, 200, 100); bme680_use_heater_profile(&sensor, 0); |
Функция bme680_set_ambient_temperature() задает температуру окружающей среды для расчета сопротивления нагревателя. Это связано с тем, что алгоритм расчета сопротивления нагревателя учитывает температуру окружающей среды датчика.
|
1 |
bme680_set_ambient_temperature(&sensor, 10); |
Функция bme680_get_measurement_duration() определяет продолжительность измерения, проводимого устройством BME680, в единицах тактов RTOS.
Функция считывания данных с датчика BME680
Функция get_bme680_readings() считывает показания датчика и заполняет ими структуру ‘values’ типа bme680_values_float_t.
Структура bme680_values_float_t будет заполняться значениями с датчика при вызове функции bme680_get_results_float() для получения данных о температуре, влажности, давлении и газе с датчика.
|
1 2 3 4 5 6 7 |
typedef struct { float temperature; //!< temperature in degree C (Invalid value -327.68) float pressure; //!< barometric pressure in hPascal (Invalid value 0.0) float humidity; //!< relative humidity in % (Invalid value 0.0) float gas_resistance; //!< gas resistance in Ohm (Invalid value 0.0) } bme680_values_float_t; |
|
1 |
bme680_values_float_t values; |
Внутри этой функции мы вызываем функцию bme680_get_results_float() для получения показаний датчика BME680 и сохранения их в структуру ‘values’. Эта структура определена как глобальная, и мы будем использовать ее в качестве обработчика HTTP-ответов для отправки значений датчика на веб-страницы.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void get_bme680_readings(void) { // trigger the sensor to start one TPHG measurement cycle if (bme680_force_measurement(&sensor) == ESP_OK) { // passive waiting until measurement results are available vTaskDelay(duration); // get the results and do something with them if (bme680_get_results_float(&sensor, &values) == ESP_OK) printf("BME680 Sensor: %.2f °C, %.2f %%, %.2f hPa, %.2f KOhm\n", values.temperature, values.humidity, values.pressure, values.gas_resistance); } } |
Мониторинг подключения и состояния Wi-Fi
event_handler()
Затем у нас есть функция event_handler(), которая обрабатывает события Wi-Fi. Она выступает в качестве функции обратного вызова при возникновении события Wi-Fi или события IP.
Событие IP происходит, когда плата ESP32 подключается к нашей точке доступа, и маршрутизатор назначает ей IP-адрес. События IP могут быть одного из следующих типов. Но здесь мы используем только IP_EVENT_STA_GOT_IP.
|
1 2 3 4 5 6 7 8 9 10 11 |
/** IP event declarations */ typedef enum { IP_EVENT_STA_GOT_IP, /*!< station got IP from connected AP */ IP_EVENT_STA_LOST_IP, /*!< station lost IP and the IP is reset to 0 */ IP_EVENT_AP_STAIPASSIGNED, /*!< soft-AP assign an IP to a connected station */ IP_EVENT_GOT_IP6, /*!< station or ap or ethernet interface v6IP addr is preferred */ IP_EVENT_ETH_GOT_IP, /*!< ethernet got IP from connected AP */ IP_EVENT_ETH_LOST_IP, /*!< ethernet lost IP and the IP is reset to 0 */ IP_EVENT_PPP_GOT_IP, /*!< PPP interface got IP */ IP_EVENT_PPP_LOST_IP, /*!< PPP interface lost IP */ } ip_event_t; |
Во-первых, когда происходят события Wi-Fi (WIFI_EVENT и WIFI_EVENT_STA_START), мы подключаем ESP32 к точке доступа. Для этого мы используем функцию esp_wifi_connect().
Если плата ESP32 не может подключиться к точке доступа в течение установленного максимального количества попыток, она снова пытается подключиться к точке доступа и регистрирует сообщение «повторная попытка подключения к точке доступа». Однако, если ESP32 не может установить соединение с точкой доступа после истечения максимального количества попыток, она регистрирует сообщение «сбой подключения к точке доступа» и устанавливает бит WIFI_FAIL_BIT. Теперь, если плата ESP32 успешно подключается к точке доступа, это означает, что произошли оба события IP (IP_EVENT и IP_EVENT_STA_GOT_IP). В этом случае она выводит IP-адрес в качестве результата. Кроме того, она также устанавливает бит WIFI_CONNECTED_BIT.
|
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 |
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 < 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); } wifi_connect_status = 0; 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); wifi_connect_status = 1; } } |
connect_wifi()
Функция connect_wifi() будет вызываться внутри функции app_main() для инициализации Wi-Fi в режиме станции для 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 |
void connect_wifi(void) { 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 = ESP_WIFI_SSID, .password = ESP_WIFI_PASSWORD, /* 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_WPA2_PSK, }, }; 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", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } vEventGroupDelete(s_wifi_event_group); } |
Для начала создадим группу событий FreeRTOS для мониторинга соединения. Эта функция возвращает дескриптор группы событий.
|
1 |
s_wifi_event_group = xEventGroupCreate(); |
ESP-IDF предоставляет полезный макрос ESP_ERROR_CHECK(), который используется для проверки наличия или отсутствия ошибки. Если функция, переданная в него, не возвращает ESP_OK, то мы проверяем наличие ошибки и записываем её в лог.
Здесь мы инициализируем lwIP с помощью функции esp_netif_init() и создаём задачу IwIP, также известную как задача TCP/IP. lwIP — это стек библиотек TCP/IP, предоставляемый ESP-IDF, который используется для выполнения различных протоколов, таких как TCP, UDP, DHCP и т. д.
|
1 |
ESP_ERROR_CHECK(esp_netif_init()); |
Далее мы создаём цикл обработки событий по умолчанию, который позволяет отправлять системные события в задачу обработки событий.
|
1 |
ESP_ERROR_CHECK(esp_event_loop_create_default()); |
Кроме того, следующая строка используется для инициализации Wi-Fi по умолчанию в режиме станции. Этот API используется для инициализации, а также регистрации обработчиков событий для интерфейса по умолчанию, которым в нашем случае является станция. Он создает экземпляр сетевого интерфейса, связывающий драйвер Wi-Fi и стек TCP/IP. Когда станция находится в процессе подключения к точке доступа, различные процессы автоматически обрабатываются с помощью этой функции.
|
1 |
esp_netif_create_default_wifi_sta(); |
Затем мы инициализируем ресурс выделения Wi-Fi для драйвера Wi-Fi. Он отвечает за запуск задачи Wi-Fi. Он принимает единственный параметр cfg, который является указателем на уже инициализированную структуру конфигурации Wi-Fi, установленную в значение WIFI_INIT_CONFIG_DEFAULT(), так что инициализация конфигурации происходит со значениями по умолчанию. Здесь wifi_init_config_t — это структура, обозначающая параметры конфигурации стека Wi-Fi, которые передаются в функцию esp_wifi_init().
|
1 2 |
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); |
Далее мы создаём два обработчика событий: один с именем ‘instance_any_id’ для всех событий Wi-Fi, а другой с именем ‘instance_any_ip’ — для событий, когда станция получает IP-адрес.
|
1 2 |
esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; |
Далее, у нас есть два регистра для экземпляров обработчика событий, которые мы настроили. Таким образом, задача приложения может зарегистрироваться для обратного вызова, который будет прослушивать события для Wi-Fi и TCP/IP. Первое событие — « ESP_EVENT_ANY_ID », а второе — « IP_EVENT_STA_GOT_IP ». Следовательно, она будет прослушивать все события Wi-Fi (ESP_EVENT_ANY_ID) и событие IP, когда станция получит свой IP-адрес (IP_EVENT_STA_GOT_IP). Таким образом, функция обратного вызова event_handler будет вызвана, если произойдет любое из этих двух событий.
|
1 2 3 4 5 6 7 8 9 10 |
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)); |
Другие события Wi-Fi, которые мы можем использовать в нашем коде, включают:
|
1 2 3 4 5 6 |
typedef enum { WIFI_EVENT_STA_CONNECTED, /**< ESP32 station connected to AP */ WIFI_EVENT_STA_DISCONNECTED, /**< ESP32 station disconnected from AP */ WIFI_EVENT_AP_START, /**< ESP32 soft-AP start */ WIFI_EVENT_AP_STOP, /**< ESP32 soft-AP stop */ } mdns_used_event_t; |
В этой части мы задаём SSID и пароль сети, к которой хотим подключить наш ESP32, а также назначаем параметры безопасности.
|
1 2 3 4 5 6 7 8 9 10 |
wifi_config_t wifi_config = { .sta = { .ssid = ESP_WIFI_SSID, .password = ESP_WIFI_PASSWORD, /* 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_WPA2_PSK, }, }; |
Кроме того, мы устанавливаем режим Wi-Fi как «станция». Для этого мы используем функцию esp_wifi_set_mode() и передаем в качестве параметра параметр WiFI_MODE_STA.
|
1 |
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); |
Затем мы назначаем SSID и пароль нашей сети. Таким образом, драйвер Wi-Fi настраивается с использованием учетных данных сети точки доступа и режима Wi-Fi. После этого мы запускаем Wi-Fi-соединение с заданными параметрами.
|
1 2 |
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); ESP_ERROR_CHECK(esp_wifi_start() ); |
На этом этапе в журнале информации появляется сообщение «wifi_init_sta finished». Процесс инициализации Wi-Fi в режиме станции завершен.
|
1 |
ESP_LOGI(TAG, "wifi_init_sta finished."); |
Далее приложение переходит в заблокированное состояние, ожидая установки одного из битов в группе событий. Это происходит после инициализации и запуска Wi-Fi. Приложение остается в заблокированном состоянии до тех пор, пока не будет установлено соединение Wi-Fi (контролируется битом WIFI_CONNECTED_BIT) или пока соединение Wi-Fi не будет разорвано в течение максимального количества попыток (контролируется битом WIFI_FAIL_BIT).
Если функция event_handler() устанавливает значение WIFI_CONNECTED_BIT, указывающее на успешное подключение ESP32 к точке доступа, то в информационном журнале будет указано, что соединение установлено с SSID и паролем указанной точки доступа. Если же функция event_handler() устанавливает значение WIFI_FAIL_BIT, указывающее на невозможность подключения ESP32 к точке доступа даже после достижения максимального количества попыток, то в информационном журнале будет указано, что соединение с SSID и паролем указанной точки доступа не удалось.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", ESP_WIFI_SSID, ESP_WIFI_PASSWORD); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } |
Кроме того, мы удаляем группу событий ‘s_wifi_event_group’ с помощью функции vEventGroupDelete(). Она была создана ранее с помощью вызова функции xEventGroupCreate().
|
1 |
vEventGroupDelete(s_wifi_event_group); |
Обработчики HTTP-запросов в ESP32
Функция send_web_page() отвечает за отправку полного HTTP-ответа путем вызова функции httpd_resp_send(). API httpd_resp_send() принимает три параметра. Первый параметр — это запрос, на который должен быть дан ответ. Второй параметр — это буфер, из которого необходимо получить контекст. Третий параметр — это длина буфера. В функции send_web_page() HTTP-ответ состоит из показаний температуры, влажности и давления, полученных в функции get_bme680_readings().
|
1 2 3 4 5 6 7 8 9 10 11 |
esp_err_t send_web_page(httpd_req_t *req) { int response; get_bme680_readings(); char response_data[sizeof(html_page) + 4]; memset(response_data, 0, sizeof(response_data)); sprintf(response_data, html_page, values.temperature, values.humidity, values.pressure, values.gas_resistance); response = httpd_resp_send(req, response_data, HTTPD_RESP_USE_STRLEN); return response; } |
Ниже представлена структура обработчика uri-get. Выбранный метод — HTTP_GET, URL — «/», а обработчик для этого — get_req_handler, который возвращает ответ send_web_page().
|
1 2 3 4 5 6 7 8 9 10 |
esp_err_t get_req_handler(httpd_req_t *req) { return send_web_page(req); } httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; |
Следующая функция запустит HTTP-веб-сервер и вернет сервер, который будет являться HTTP-дескриптором.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
httpd_handle_t setup_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_handle_t server = NULL; if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); } return server; } |
Процесс начинается с определения конфигурации по умолчанию и присваивает пустое значение дескриптору HTTP-сервера. Оно устанавливается как NULL.
Функция httpd_start() запускает HTTP-сервер. Она принимает два параметра: конфигурацию для нового экземпляра сервера и дескриптор этого экземпляра. Если экземпляр был успешно создан, что означает, что функция вернула ESP_OK, то мы зарегистрируем обработчики URI. Функция принимает два параметра. Первый параметр — это дескриптор экземпляра HTTP-сервера, в данном случае ‘server’. Второй параметр — это указатель на обработчик, который мы хотим зарегистрировать, — uri_get.
app_main()
Внутри функции app_main() сначала инициализируется NVS путем вызова функции nvs_flash_init(). Затем проверяется, не содержит ли раздел NVS пустых страниц или содержит ли он данные в формате, отличном от текущей версии кода. Если выполняется хотя бы одно из этих условий, то с помощью функции nvs_flash_erase() происходит очистка флэш-памяти NVS и повторная инициализация.
|
1 2 3 4 5 6 7 8 |
// 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); |
Затем мы вызовем функцию i2cdev_init() для инициализации I2C и функцию init_bme680().
|
1 2 |
ESP_ERROR_CHECK(i2cdev_init()); init_bme680(); |
Затем мы выведем в терминале “ESP_WIFI_MODE_STA” и вызовем функцию connect_wifi() для подключения к Wi-Fi в режиме станции.
|
1 2 |
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); connect_wifi(); |
Если ESP32 успешно подключается к сети Wi-Fi, вызывается функция setup_server() для запуска HTTP-веб-сервера, и терминал выводит сообщение о том, что веб-сервер BME680 запущен. В противном случае терминал ESP-IDF выводит сообщение о сбое сети.
|
1 2 3 4 5 6 7 |
if (wifi_connect_status) { setup_server(); ESP_LOGI(TAG, "Web Server is up and running\n"); } else ESP_LOGI(TAG, "Failed to connected with Wi-Fi, check your network Credentials\n"); |
Компиляция скетча
Для прошивки микросхемы введите следующую команду в последовательном терминале. Не забудьте заменить COM-порт на тот, через который подключена ваша плата.
|
1 |
idf.py -p COMX flash monitor |
После успешной прошивки кода вы можете просмотреть все информационные журналы. Сначала инициализируется Wi-Fi станции. Затем мы можем просмотреть esp_netif_handlers, который включает IP-адрес, маску и адрес шлюза. Затем мы получаем запись «получен IP-адрес», за которой следует сам IP-адрес. Мы будем использовать этот IP-адрес для доступа к веб-серверу.
Откройте новый веб-браузер и введите IP-адрес, полученный в терминале, затем нажмите Enter. Откроется веб-сервер ESP32 BME680, отображающий показания температуры, давления и влажности, которые обновляются каждые несколько секунд.
