Перейти к содержимому

Подключение к точке доступа WiFi из ESP32 с ESP-IDF

Метки:

Добрый день, уважаемый читатель!

В данной статье обсудим довольно непростую тему – подключение к WiFi-точке доступа в режиме STA. ESP32 не был бы так популярен, если б в нем не было встроенной поддержки WiFi подключений.

Режим станции (STA) — это такой режим, в котором контроллер не создает собственную сеть, а подключается к уже существующей сети Wi-Fi, например, к вашей локальной сети (роутеру или иному “раздающему” устройству).

Если вы программировали ESP32 из Arduino, то наверняка видели, как просто и быстро осуществляется подключение к точке доступа:

Не самый простейший вариант подключения к точке доступа в Arduino


 

Теперь давайте взглянем на пример такого же подключения, но уже из ESP-IDF:

Пример подключения к WiFi из protocol_examples_common, который используется во всех сетевых примерах ESP-IDF

Кошмар! Куча непонятных вызовов функций, которые сходу непонятно зачем нужны. И это ещё не самый сложный вариант, который не учитывает многие моменты и проблемы.

Зачем всё это надо? Может быть, проще вовсе не подключаться к WiFi?

Источник: Яндекс.Картинки

Но мы же легких путей не ищем! Давайте разбираться, что, зачем и почему.

Необходимое предупреждение. Статья получилась довольно длинной, и возможно, немного запутанной. Потому что процесс не очень простой. Я попытался описать всё максимально подробно, но возможны недочеты. Если таковые найдутся – прошу в комментарии.

 


Что можно на WiFi ESP32

ESP32 поддерживает следующие функции:

  • 3 виртуальных интерфейса Wi-Fi: STA (станция), AP (точка доступа) и Sniffer. Есть ещё четвертый режим, но он пока “в резерве”.
  • Можно скрещивать ежа с ужом режим STA c AP – поддерживается режим STA+AP.
  • Протоколы IEEE 802.11b, IEEE 802.11g, IEEE 802.11n и API для настройки режима протокола.
  • Протоколы шифрования WPA/WPA2/WPA3/WPA2-Enterprise/WPA3-Enterprise/WAPI/WPS and DPP
  • AMSDU, AMPDU, HT40, QoS и другие функции
  • Есть возможность перевода модема в режим сна для экономии энергии
  • Протокол ESP-NOW и режим Long Range, поддерживающий передачу данных на расстояние до 1 км, но только между двумя ESP32. Эти режимы разработаны Espessif и поддерживаются только на ESP.
  • Пропускная способность TCP до 20 Мбит/с и пропускная способность UDP 30 Мбит/с по радиоканалу.
  • Как быстрое сканирование, так и сканирование всех каналов.
  • Несколько типов антенн.
  • Информация о состоянии канала.

API-интерфейсы, предоставляемые ESP-IDF, являются потокобезопасными, и вам не требуется дополнительно заботиться о синхронизации доступа к интернету из различных задач и сервисов.

 


Последовательность операций для подключения к WiFi

Итак, что же необходимо сделать для того, чтобы наш контроллер подключился к WiFi сети?

  • Создать и запустить системный цикл событий. В принципе, можно использовать и пользовательский цикл событий, но никакого смысла в этом нет. Через этот цикл событий драйвер WiFi будет рассылать уведомления об изменении состояния WiFi подсистемы.
  • Так как подключение к WiFi всегда обрабатывается “по событиям”, мы должны создать и зарегистрировать обработчики для различных событий, генерируемых ESP-IDF при подключении: WIFI_EVENT_STA_CONNECTED (подключение к точке доступа установлено), IP_EVENT_STA_GOT_IP (IP-адрес получен), WIFI_EVENT_STA_DISCONNECTED (подключение к точке доступа потеряно) и других. В принципе, это почти то же самое, что и callback-функции, так что особых проблем это не доставляет.
  • Создать группу событий, в которой удобно регистрировать текущее состояние WiFi-подсистемы. Это не обязательно, в некоторых примерах этого нет. Но без этого в некоторых состояниях сложно понять – в каком месте алгоритма мы находимся и как реагировать на то или иное поступившее событие. Например, событие WIFI_EVENT_STA_DISCONNECTED будет сгенерировано в двух случаях – при разрыве существующего соединения или при неудачной попытке подключения к заданной точке доступа. И когда это событие поступит, вам нужно будет решить – как на него реагировать. Группа событий поможет вам в этом.
  • Прочитать MAC-адрес устройства из EFUSE, если это необходимо. По умолчанию WiFi не знает MAC адрес, он хранится в EFUSE микроконтроллера.
  • Инициализировать TCP/IP стек через библиотеку ESP-NETIF. Назначение библиотеки ESP-NETIF – дополнительный уровень абстракции для приложения поверх стека TCP/IP, который позволит приложениям выбирать между стеками IP. API-интерфейсы, которые он предоставляет, являются потокобезопасными, даже если базовые API-интерфейсы стека TCP/IP таковыми не являются.
  • Установить тип хранилища для служебных данных (RAM или NVS-раздел) и инициализировать NVS раздел Flash (если это ещё не было сделано). Если флэш-память NVS для WiFi включена, все конфигурации WiFi, установленные с помощью API WiFi, будут сохранены во флэш-памяти, и драйвер WiFi запустится с этими конфигурациями при следующем включении или перезагрузке. Однако приложение может отключить запись конфигурации во флэш-память NVS, если ему это не нужно.
  • Установить режим WIFI_MODE_STA. И дополнительные параметры, если требуется.
  • Запустить STA режим и дождаться выполнения нескольких последовательных стадий: STA запущен, подключение выполнено, IP-адрес получен. Да, именно в такой последовательности: вначале придет событие WIFI_EVENT_STA_CONNECTED, и только потом IP_EVENT_STA_GOT_IP. IP_EVENT_STA_GOT_IP можно считать финальным этапом подключения к сети, после чего запускать другие сетевые сервисы (SNTP, PING, MQTT, HTTP и т.д.).

Ниже представлен «общий сценарий», описывающий работу в режиме STA:

Примерная схема работы с драйвером WiFi

Примечание:

  • Main Task – ваше основное приложение
  • App Task – ваше приложение, осуществляющее работу с WiFi модулем.

Кстати, для своего варианта библиотеки WiFi я вначале беззастенчиво позаимствовал исходники модуля WiFi для ArduinoIDE. Не всё, конечно, но основной алгоритм. Постепенно, с ростом понимания “что и зачем”, отличий в моей версии от “оригинала” стало ещё больше.

 


Используемые технологии и принципы работы

Для начала я поясню, какие механизмы и технологии я использую в своих проектах. Все примеры, приведенные в данной статье, основаны на моей библиотеке, которую я использую в своих проектах для установки и поддержания подключения к WiFi в режиме STA.

GitHub – kotyara12/reWiFi: A library for connecting to a WiFi hotspot using only the ESP-IDF framework

1. Я не использую web-интерфейс в своих проектах, так как не вижу в нем вообще никакой необходимости – всё, что нужно настроить, я настраиваю в самой прошивке. Зачем же тратить на это свое время и силы, а также ресурсы микроконтроллера? Поэтому для меня нет необходимости и в использовании режима AP. Если вам позарез нужен режим AP – увы, но тут я вам помочь не смогу.

2. Для хранения состояния WiFi модуля используется специально выделенная группа событий. Там хранится следующая информация:

static const int _WIFI_TCPIP_INIT             = BIT0;
static const int _WIFI_LOWLEVEL_INIT          = BIT1;
static const int _WIFI_STA_ENABLED            = BIT2;
static const int _WIFI_STA_STARTED            = BIT3;
static const int _WIFI_STA_CONNECTED          = BIT4;
static const int _WIFI_STA_GOT_IP             = BIT5;
static const int _WIFI_STA_DISCONNECT_STOP    = BIT6; // Disconnect and stop STA mode (offline)
static const int _WIFI_STA_DISCONNECT_RESTORE = BIT7; // Disconnect and restore STA mode ("cold" reconnect)

Эти флаги позволяют в любой момент времени однозначно понять состояние WiFi подсистемы. Флаги обычно устанавливаются или сбрасываются в обработчиках событий WIFI_EVENTS.

3. Я использую “родные” / native события WIFI_EVENTS только внутри модуля reWiFi. Для оповещения других прикладных задач об подключении и отключении к сети WiFi я создал “свои” события RE_WIFI_EVENTS с немного измененной логикой, так как native события мне были не совсем удобны. Например – задаче MQTT совсем не обязательно помнить и знать, что послужило причиной события WIFI_EVENT_STA_DISCONNECTED – потеря соединения или неудачная попытка подключения. Поэтому я “транслировал” их удобным мне образом. Кроме того, для прикладного уровня я часто использую дополнительный прикладной цикл событий, дабы не перегружать системный. Поэтому новые “переведенные” события отправляются уже туда.

4. При подключении к сети WiFi при очень слабом уровне сигнала и высоком уровне помех возможны любые проблемы и промежуточные состояния. Поэтому для прерывания попытки подключения по таймауту используется программный таймер ESP-IDF, про которые я рассказывал в одной из предыдущих статей.

 


Обработчики событий для режима STA

Для начала давайте рассмотрим систему событий WiFi модуля и как с ней работать. Возможно, вначале вам будут непонятны внутренние функции библиотеки типа wifiConnectSTA() – не пугайтесь, про них я подробнее расскажу чуть позже.

Как я уже кратко упомянул выше, вся работа с драйвером WiFi основана на событиях. Поэтому прежде чем начинать подключение к WiFi, нам необходимо запустить системный цикл событий и зарегистрировать обработчики этих событий. Драйвер WiFi поддерживает следующие события для STA режима:

WIFI_EVENT_SCAN_DONE – завершено сканирование (поиск) точек доступа. На самом деле это событие не совсем относится к режиму STA, так как при прямом подключении к заранее известной точке доступа его обработка не требуется. Но если вам заранее не известно, к какой точке доступа вы хотите подключиться (например чтобы выбрать сеть с лучшим сигналом), то вы можете использовать функцию esp_wifi_scan_start() и обработать это событие. Данное событие будет сгенерировано только после завершения сканирования, например если целевая точка доступа была успешно найдена или все каналы были просканированы. Либо если сканирование было принудительно прервано esp_wifi_scan_stop(). Событие «сканирование выполнено» не будет сгенерировано при “прямом” подключении через esp_wifi_connect().

Я не использую обработчики для этого события, нет необходимости.

 

WIFI_EVENT_STA_START – режим STA был успешно запущен с помощью esp_wifi_start() и текущий режим Wi-Fi – станция (STA) или станция/точка доступа (STA+AP).

Получив это событие, внутренняя задача драйвера инициализирует сетевой интерфейс LwIP (netif). Ваш обработчик этого события в самом простом случае должен инициировать подключение к ранее настроенной точке доступа через вызов esp_wifi_connect(). Дополнительно можно переключить какие-то флаги в группе событий, если вы их используете.

Мой обработчик для данного события выглядит следующим образом:

static void wifiEventHandler_Start(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  // Set status bits
  wifiStatusSet(_WIFI_STA_ENABLED | _WIFI_STA_STARTED);
  wifiStatusClear(_WIFI_STA_CONNECTED | _WIFI_STA_GOT_IP | _WIFI_STA_DISCONNECT_STOP | _WIFI_STA_DISCONNECT_RESTORE);
  // Reset attempts count
  _wifiAttemptCount = 0;
  _wifiLastErr = 0;
  // Re-dispatch event to another loop
  eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_STARTED, nullptr, 0, portMAX_DELAY);  
  // Log
  rlog_i(logTAG, "WiFi STA started");
  // Start device restart timer
  #if defined(CONFIG_WIFI_TIMER_RESTART_DEVICE) && CONFIG_WIFI_TIMER_RESTART_DEVICE > 0
    espRestartTimerStartM(&_wdtRestartWiFi, RR_WIFI_TIMEOUT, CONFIG_WIFI_TIMER_RESTART_DEVICE, false);
  #endif // CONFIG_WIFI_TIMER_RESTART_DEVICE
  // Start connection
  if (!wifiConnectSTA()) {
    _wifiStopSTA();
  };
}

Что здесь происходит:

  1. Устанавливаем биты STA_ENABLED и STA_STARTED в группе событий, и сбрасываем биты STA_CONNECTED, STA_GOT_IP, а также служебные биты STA_DISCONNECT_STOP и STA_DISCONNECT_RESTORE.
  2. Сбрасываем счетчик попыток подключения
  3. Отправляем сообщение о запуске STA в прикладной цикл сообщений
  4. Запускаем таймер для принудительного перезапуска устройства из-за зависания попытки подключения, если “что-то пошло не так”. На самом деле сейчас этот таймер был добавлен из-за моего же собственного “косяка” в коде, который уже давно исправлен, и сейчас уже не актуален. Но пусть остается.
  5. Запускаем процесс подключения, но если эта функция вдруг вернула ошибку, останавливаем STA (а уже обработчик WIFI_EVENT_STA_STOP должен инициировать перезапуск STA с самого начала).

 

WIFI_EVENT_STA_STOP – режим STA был остановлен.

При получении этого события внутренняя задача драйвера освободит IP-адрес станции, остановит клиент DHCP, удалит соединения, связанные с TCP/UDP, и очистит netif станции LwIP и т.д. Ваш обработчик этого события приложения может заключаться в том, чтобы приостановить выполнение прикладных сетевых задач (например MQTT-клиента) и (или) перезапустить STA режим заново.

Мой обработчик для данного события выглядит следующим образом:

static void wifiEventHandler_Stop(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  // Reset status bits
  wifiStatusClear(_WIFI_STA_STARTED | _WIFI_STA_CONNECTED | _WIFI_STA_GOT_IP);
  // Log
  rlog_w(logTAG, "WiFi STA stopped");
  // Re-dispatch event to another loop
  eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_STOPPED, nullptr, 0, portMAX_DELAY);  
  // Delete timer
  wifiTimeoutDelete();
  // If WiFi is enabled, restart it
  if (wifiStatusCheck(_WIFI_STA_ENABLED, false)) {
    wifiStartWiFi();
  // ... otherwise we turn off everything
  } else {
    // Delete device restart timer
    #if defined(CONFIG_WIFI_TIMER_RESTART_DEVICE) && CONFIG_WIFI_TIMER_RESTART_DEVICE > 0
      espRestartTimerStartM(&_wdtRestartWiFi, RR_WIFI_TIMEOUT, CONFIG_WIFI_TIMER_RESTART_DEVICE, false);
    #endif // CONFIG_WIFI_TIMER_RESTART_DEVICE
    // Low-level deinit
    wifiLowLevelDeinit();
  };
}

Что здесь происходит:

  1. Сбрасываем биты STA_STARTED, STA_CONNECTED, STA_GOT_IP, если они были установлены.
  2. Отправляем сообщение об отключении STA в прикладной цикл сообщений
  3. Останавливаем и удаляем таймер подключения, если он был.
  4. Если флаг STA_ENABLED установлен (то есть это не принудительное отключение от WiFi), то начинаем процесс запуска режима STA заново. Иначе – выгружаем из памяти всё, что там было для работы STA.

 

WIFI_EVENT_STA_CONNECTED – если функция esp_wifi_connect() вернула ESP_OK и ESP32 успешно подключилась к указанной точке доступа, возникает событие подключения.

При получении этого события внутренняя задача драйвера запускает DHCP-клиент и начинает процесс DHCP для получения IP-адреса. После этого драйвер Wi-Fi готов к отправке и приему данных. Однако начинать работу с сетью еще рановато – если приложение основано на LwIP (по умолчанию оно так и есть), вам придется подождать, пока не придет событие IP_EVENT_STA_GOT_IP.

Мой обработчик для данного события выглядит следующим образом:

static void wifiEventHandler_Connect(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  // Set status bits
  wifiStatusSet(_WIFI_STA_CONNECTED);
  wifiStatusClear(_WIFI_STA_GOT_IP | _WIFI_STA_DISCONNECT_STOP | _WIFI_STA_DISCONNECT_RESTORE);
  // Save successful connection number
  #ifndef CONFIG_WIFI_SSID
    _wifiIndexNeedChange = false;
    if (_wifiIndexWasChanged) {
      nvsWrite(wifiNvsGroup, wifiNvsIndex, OPT_TYPE_U8, &_wifiCurrIndex);
      _wifiIndexWasChanged = false;
    };
  #endif // CONFIG_WIFI_SSID
  // Log
  #if CONFIG_RLOG_PROJECT_LEVEL >= RLOG_LEVEL_INFO
    if (event_data) {
      wifi_event_sta_connected_t * data = (wifi_event_sta_connected_t*)event_data;
      rlog_i(logTAG, "WiFi connection [ %s ] established, RSSI: %d dBi", (char*)data->ssid, wifiRSSI());
    };
  #endif
  // Restart timer
  wifiTimeoutStart(CONFIG_WIFI_TIMEOUT);
}

Что здесь происходит:

  1. Устанавливаем бит STA_CONNECTED, но сбрасываем биты STA_GOT_IP, STA_DISCONNECT_STOP и STA_DISCONNECT_RESTORE, если они были установлены.
  2. Если используется режим выбора из нескольких сетей WiFi, запоминаем в NVS индекс сети, к которой удалось подключиться. В следующий раз устройство начнет именно с этой сети.
  3. Выводим в лог имя сети и уровень сигнала, для отладки
  4. Перезапускаем таймер таймаута подключения – при неустойчивом сигнале получения IP-адреса можно и не дождаться, таймер при этом перезапустит STA полностью.

 

WIFI_EVENT_STA_DISCONNECTED – данное событие может быть сгенерировано в следующих случаях:

  1. Когда вызывается esp_wifi_disconnect() или esp_wifi_stop() и станция уже была подключена к точке доступа, то есть при принудительном отключении от точки доступа.
  2. При попытке подключения к точке доступа через esp_wifi_connect(), но при этом драйверу Wi-Fi не удается установить соединение по каким-либо причинам: например, при сканировании не удается найти целевую точку доступа или истекло время аутентификации. Если имеется более одной точки доступа с одним и тем же SSID, событие разъединения будет вызвано после того, как ESP32 не удастся подключиться ко всем найденным точкам доступа.
  3. Когда существующее соединение Wi-Fi прерывается по причине сбоя: например, станция теряет N маяков подряд, точка доступа отключает станцию ​​или изменяется режим аутентификации точки доступа.

После получения этого события внутренняя задача драйвера вызывает отключение LwIP станции и уведомление задачи LwIP об очистке соединений UDP/TCP, которые вызывают неправильное состояние всех сокетов. Для приложений на основе сокетов функция обратного вызова приложения может выбрать закрытие всех сокетов и их повторное создание, если это необходимо, после получения этого события.

Наиболее распространенным кодом обработки этого события в вашем приложении является вызов esp_wifi_connect() для повторного подключения к сети Wi-Fi. Однако если событие вызывается из-за принудительного вызова esp_wifi_disconnect(), приложение не должно вызывать повторное подключение. Ваше приложение обязано различать, вызвано ли данное событие принудительным отключением либо внешними причинами.

Мой обработчик для данного события самый сложный из всех и выглядит следующим образом:

static void wifiEventHandler_Disconnect(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  // Check current status
  EventBits_t prevStatusBits = wifiStatusGet();
  bool isWasConnected = (prevStatusBits & _WIFI_STA_CONNECTED) == _WIFI_STA_CONNECTED;
  bool isWasIP = (prevStatusBits & _WIFI_STA_GOT_IP) == _WIFI_STA_GOT_IP;
  // Reset status bits
  wifiStatusClear(_WIFI_STA_CONNECTED | _WIFI_STA_GOT_IP);
  // Stop timer
  wifiTimeoutStop();
  // Start device restart timer
  #if defined(CONFIG_WIFI_TIMER_RESTART_DEVICE) && CONFIG_WIFI_TIMER_RESTART_DEVICE > 0
    espRestartTimerStartM(&_wdtRestartWiFi, RR_WIFI_TIMEOUT, CONFIG_WIFI_TIMER_RESTART_DEVICE, false);
  #endif // CONFIG_WIFI_TIMER_RESTART_DEVICE
  // Check for forced (manual) WiFi disconnection
  if (wifiStatusCheck(_WIFI_STA_ENABLED, false)) {
    // Different reconnection scenarios
    if (event_id == WIFI_EVENT_STA_BEACON_TIMEOUT) {
      _wifiLastErr = WIFI_REASON_BEACON_TIMEOUT;
      if (isWasConnected && isWasIP) {
        // Re-dispatch event to another loop
        eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_DISCONNECTED, nullptr, 0, portMAX_DELAY);  
        rlog_e(logTAG, "WiFi connection [ %s ] lost: beacon timeout!", wifiGetSSID());
      } else {
        rlog_e(logTAG, "Failed to connect to WiFi network: beacon timeout!");
      };
      // Next connection attempt
      if (!wifiReconnectWiFi()) {
        _wifiRestoreSTA();
        _wifiStopSTA();
      };
    } else if (event_id == IP_EVENT_STA_LOST_IP) {
      // Re-dispatch event to another loop
      eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_DISCONNECTED, nullptr, 0, portMAX_DELAY);
      rlog_e(logTAG, "WiFi connection [ %s ] lost WiFi IP address!", wifiGetSSID());
      // Next connection attempt
      if (!wifiReconnectWiFi()) {
        _wifiRestoreSTA();
        _wifiStopSTA();
      };
    } else {
      wifi_event_sta_disconnected_t * data = (wifi_event_sta_disconnected_t*)event_data;
      if (data) {
        _wifiLastErr = data->reason;
      } else {
        _wifiLastErr = WIFI_REASON_UNSPECIFIED;
      };
      if (isWasConnected && isWasIP) {
        // Re-dispatch event to another loop
        if (data) {
          eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_DISCONNECTED, data, sizeof(wifi_event_sta_disconnected_t), portMAX_DELAY);  
        } else {
          eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_DISCONNECTED, nullptr, 0, portMAX_DELAY);
        };
        rlog_e(logTAG, "WiFi connection [ %s ] lost: #%d!", wifiGetSSID(), _wifiLastErr);
      } else {
        rlog_e(logTAG, "Failed to connect to WiFi network: #%d!", _wifiLastErr);
      };
      // Next connection attempt
      if (!wifiReconnectWiFi()) {
        _wifiRestoreSTA();
        _wifiStopSTA();
      };
    };
  } else {
    // Stop WiFi
    wifiStopWiFi();
  }
}

Что здесь происходит:

  1. Для начала определяем, чем было вызвано это событие. Если на момент события был установлен флаг STA_CONNECTED, значит это потеря подключения, если нет – неудачная попытка подключения к точке доступа. То же самое определяем и для бита STA_GOT_IP.
  2. Сбрасываем биты STA_CONNECTED и STA_GOT_IP.
  3. Останавливаем таймер подключения – это актуально, когда событие было вызвано из попытки подключения.
  4. По возможности определяем причину отключения. Затем, если это было именно потеря соединения, уведомляем прикладные задачи через прикладной цикл событий. Если это просто неудачная попытка соединения – ничего отправлять не требуется.
  5. Если это принудительное отключение командой, останавливаем WiFi режим и ждем дальнейших указаний программиста.
  6. Иначе пробуем переподключиться к точке доступа ещё раз. Если это не удалось выполнить – сбрасываем STA в исходное состояние и останавливаем его. Далее обработчик WIFI_EVENT_STA_STOP должен, в свою очередь, запустить STA заново, что называется “с нуля”.

 

IP_EVENT_STA_GOT_IP – это событие возникает, когда DHCP-клиент успешно получает адрес IPV4 от DHCP-сервера или когда адрес IPV4 изменяется.

IP_EVENT_GOT_IP6 – то же самое, но при получении IP адреса IPV6. Событие будет сгенерировано, если включена поддержка IPV6 и ваш провайдер поддерживает работу с IPV6.

Данное событие означает, что все готово и приложение может приступить к своим задачам (например, запустить MQTT клиент или HTTP-сервер).

Мой обработчик для данного события выглядит следующим образом:

static void wifiEventHandler_GotIP(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  // Set status bits
  wifiStatusSet(_WIFI_STA_GOT_IP);
  // Reset attempts count
  _wifiAttemptCount = 0;
  _wifiLastErr = 0;
  // Re-dispatch event to another loop
  if (event_data) {
    ip_event_got_ip_t * data = (ip_event_got_ip_t*)event_data;
    eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_GOT_IP, data, sizeof(ip_event_got_ip_t), portMAX_DELAY);  
    // Log
    #if CONFIG_RLOG_PROJECT_LEVEL >= RLOG_LEVEL_INFO
      uint8_t * ip = (uint8_t*)&(data->ip_info.ip.addr);
      uint8_t * mask = (uint8_t*)&(data->ip_info.netmask.addr);
      uint8_t * gw = (uint8_t*)&(data->ip_info.gw.addr);
      rlog_i(logTAG, "Got IP-address: %d.%d.%d.%d, mask: %d.%d.%d.%d, gateway: %d.%d.%d.%d",
          ip[0], ip[1], ip[2], ip[3], mask[0], mask[1], mask[2], mask[3], gw[0], gw[1], gw[2], gw[3]);
    #endif
  } else {
    eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_GOT_IP, nullptr, 0, portMAX_DELAY);  
  };
  // Delete timer
  wifiTimeoutDelete();
  // Stop device restart timer
  #if defined(CONFIG_WIFI_TIMER_RESTART_DEVICE) && CONFIG_WIFI_TIMER_RESTART_DEVICE > 0
    espRestartTimerBreak(&_wdtRestartWiFi);
  #endif // CONFIG_WIFI_TIMER_RESTART_DEVICE
}

Что здесь происходит:

  1. Устанавливаем бит STA_GOT_IP.
  2. Сбрасываем счетчик попыток подключения и последнюю ошибку.
  3. Отправляем уведомление прикладным задачам “всё отлично, можно начинать работу
  4. Удаляем таймер таймаута подключения и останавливаем таймер аварийной перезагрузки устройства.

 

IP_EVENT_STA_LOST_IP – данное событие возникает, когда адрес IPV4 становится недействительным. IP_EVENT_STA_LOST_IP не возникает сразу после отключения Wi-Fi. Вместо этого он запускает таймер потери адреса. Если адрес IPV4 будет заново получен до истечения времени таймера потери IP, IP_EVENT_STA_LOST_IP не генерируется.

Как правило, ваше приложение может игнорировать это событие, потому что это просто событие отладки, сообщающее о потере адреса IPV4. Я не использую обработчики для этого события, ввиду отсутствия необходимости.

 

Примечание: можно создать один “большой” обработчик сразу на все события (и внутри уже проверять тип события), но я предпочитаю создать несколько “мелких” отдельных обработчиков.

 

Есть ещё события для AP-режима, но я не буду обсуждать их в рамках данной статьи.

 


Подключение к сети WiFi

Обработчики событий создали и рассмотрели, можно приступать собственно к подключению. Давайте рассмотрим какие практические шаги для этого нужно предпринять.

Запуск WiFi в моем случае осуществляется с помощью вот такой нехитрой функции (открыть исходники на GitHub):

bool wifiStart()
{
  bool ret = true;
  // Initialization WiFi, if not done earlier
  if (!_wifiStatusBits) ret = wifiInit();
  // Stop the previous mode if it was activated
  if (ret) ret = wifiStop();
  // Low level init
  if (ret) ret = wifiLowLevelInit();
  // Allow reconnection
  if (ret) ret = wifiStatusSet(_WIFI_STA_ENABLED);
  // Start WiFi
  if (ret) ret = wifiStartWiFi();
  return ret;
}

 

Шаг 1. Инициализация необходимых объектов и регистрация параметров

Первым делом необходимо создать группу событий для хранения состояния системы и сторожевой программный таймер (если он разрешен через файл конфигурации проекта). Делается это с помощью функции wifiInit():

void wifiRegisterParameters();
bool wifiInit()
{
  if (!_wifiStatusBits) {
    #if NETWORK_EVENT_STATIC_ALLOCATION
      _wifiStatusBits = xEventGroupCreateStatic(&_wifiStatusBitsBuffer);
    #else
      _wifiStatusBits = xEventGroupCreate();
    #endif // NETWORK_EVENT_STATIC_ALLOCATION
    if (!_wifiStatusBits) {
      rlog_e(logTAG, "Error creating WiFi state group!");
      return false;
    };
    xEventGroupClearBits(_wifiStatusBits, 0x00FFFFFF);
  };
  wifiRegisterParameters();
  #if defined(CONFIG_WIFI_TIMER_RESTART_DEVICE) && CONFIG_WIFI_TIMER_RESTART_DEVICE > 0
    espRestartTimerInit(&_wdtRestartWiFi, RR_WIFI_TIMEOUT, "wdt_wifi");
  #endif // CONFIG_WIFI_TIMER_RESTART_DEVICE
  return true;
}

Если вы читали мои статьи о группах событий и таймерах, то вопросов возникнуть не должно. Если ещё не читали – рекомендую ознакомиться: EventGroup – группы событий, Использование таймеров на ESP32

 

Шаг 2. Остановка текущего режима, если он был запущен

Как я уже упоминал, ESP-IDF может автоматически восстановить режим работы WiFi, если он был сохранён в NVS разделе в предыдущем сеансе работы. Чтобы запустить новый сеанс с новыми параметрами, необходимо остановить текущий с помощью функции wifiStop():

bool wifiStop()
{
  wifiStatusClear(_WIFI_STA_ENABLED);
  return wifiStopWiFi();
}

Вначале снимаем флаг, разрешающий автоматическое пере-подключение к точке доступа после отключения, а затем инициируем отключение от сети:

bool wifiStopWiFi()
{
  if (wifiStatusCheck(_WIFI_STA_CONNECTED, false)) {
    return _wifiDisconnectSTA(_WIFI_STA_DISCONNECT_STOP);
  } else {
    if (wifiStatusCheck(_WIFI_STA_STARTED, false)) {
      return _wifiStopSTA();
    };
  };
  return true;
}

Если на данный момент соединения не было, то ничего и не выполнится.

 

Шаг 3. Низкоуровневая инициализация TCP/IP и netif

В первую очередь нужно прочитать MAC-адрес, запустить системный цикл событий и инициализировать библиотеку сетевого интерфейса LwIP (netif). Делается это один раз при первом запуске WiFi драйвера:

bool wifiTcpIpInit()
{
  rlog_d(logTAG, "TCP-IP initialization...");

  // MAC address initialization: deprecated since ESP-IDF 5.0.0
  // uint8_t mac[8];
  // if (esp_efuse_mac_get_default(mac) == ESP_OK) {
  //   WIFI_ERROR_CHECK_BOOL(esp_base_mac_addr_set(mac), "set MAC address");
  // };

  // Start the system events task
  esp_err_t err = esp_event_loop_create_default();
  if (!((err == ESP_OK) || (err == ESP_ERR_INVALID_STATE))) {
    rlog_e(logTAG, "Failed to create event loop: %d", err);
    return false;
  };

  // Initializing the TCP/IP stack
  WIFI_ERROR_CHECK_BOOL(esp_netif_init(), "esp netif init");

  // Set initialization bit
  return wifiStatusSet(_WIFI_TCPIP_INIT);
};

bool wifiLowLevelInit()
{
  if (!wifiStatusCheck(_WIFI_LOWLEVEL_INIT, false)) {
    rlog_d(logTAG, "WiFi low level initialization...");

    eventLoopPost(RE_WIFI_EVENTS, RE_WIFI_STA_INIT, nullptr, 0, portMAX_DELAY);  

    // Initializing TCP-IP and system task
    if (!wifiStatusCheck(_WIFI_TCPIP_INIT, false)) {
      if (!wifiTcpIpInit()) return false;
    };

    // Remove netif if it existed (e.g. when changing mode)
    if (_wifiNetif) {
      esp_netif_destroy(_wifiNetif);
      _wifiNetif = nullptr;
    };

    // Initializing netif
    _wifiNetif = esp_netif_create_default_wifi_sta();

    // WiFi initialization with default parameters
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_err_t err = esp_wifi_init(&cfg);
    // In case of error 4353, you need to erase NVS partition 
    if (err == 4353) {
      // ESP32 WiFi driver saves the configuration in NVS, and after changing esp-idf version, 
      // conflicting configuration may exist. You need to erase it and try again
      nvsInit();
      err = esp_wifi_init(&cfg);
    };
    if (err != ESP_OK) {
      rlog_e(logTAG, "Error esp_wifi_init: %d", err);
      return false;
    };

    // Set the storage type of the Wi-Fi configuration in memory
    #ifdef CONFIG_WIFI_STORAGE
      WIFI_ERROR_CHECK_BOOL(esp_wifi_set_storage(CONFIG_WIFI_STORAGE), "set WiFi configuration storage");
    #endif // CONFIG_WIFI_STORAGE

    // Register event handlers
    if (wifiRegisterEventHandlers()) {
      // Set initialization bit
      return wifiStatusSet(_WIFI_LOWLEVEL_INIT);
    };
  };
  return false;
}

Что здесь происходит:

  1. Проверяем, установлен ли бит _WIFI_LOWLEVEL_INIT – если да, то просто выходим.
  2. Выполняем инициализацию стека TCP/IP и netif, если это ещё не было выполнено.
  3. Создаем экземпляр сетевого интерфейса netif в режиме STA. Если экземпляр сетевого интерфейса netif уже был создан ранее, перед созданием уничтожаем предыдущий экземпляр.
  4. Инициализируем wifi с помощью функции esp_wifi_init(). Если данная функция вернула код ошибки 4543, это означает, что раздел NVS ещё не был инициализирован. Даже если вы не собираетесь хранить параметры сети в NVS, придется его инициализировать и повторить попытку заново. Подробнее про работу с NVS я расскажу позднее.
  5. При необходимости (если это задано макросом CONFIG_WIFI_STORAGE) указываем режим хранения служебных данных WiFi драйвера – в памяти или в NVS разделе.
  6. Регистрируем обработчики событий, которые мы рассматривали в предыдущем разделе, с помощью функции wifiRegisterEventHandlers():
static bool wifiRegisterEventHandlers()
{
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_START, &wifiEventHandler_Start, nullptr), 
    "register an event handler for WIFI_EVENT_STA_START");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &wifiEventHandler_Connect, nullptr), 
    "register an event handler for WIFI_EVENT_STA_CONNECTED");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifiEventHandler_Disconnect, nullptr), 
    "register an event handler for WIFI_EVENT_STA_DISCONNECTED");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_BEACON_TIMEOUT, &wifiEventHandler_Disconnect, nullptr), 
    "register an event handler for WIFI_EVENT_STA_BEACON_TIMEOUT");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_STOP, &wifiEventHandler_Stop, nullptr), 
    "register an event handler for WIFI_EVENT_STA_STOP");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifiEventHandler_GotIP, nullptr), 
    "register an event handler for IP_EVENT_STA_GOT_IP");
  WIFI_ERROR_CHECK_BOOL(
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_LOST_IP, &wifiEventHandler_Disconnect, nullptr), 
    "register an event handler for IP_EVENT_STA_LOST_IP");

  return true;
}

 

Шаг 4. Устанавливаем флаг _WIFI_STA_ENABLED

Этот бит разрешает автоматическое переподключение к сети после сбоя или неудачной попытки соединения.

 

Шаг 5. Настраиваем и запускаем режим STA

Почти всё готово к запуску STA. Можно начинать с помощью функции wifiStartWiFi(), которая в свою очередь вызывает другую, внутреннюю, функцию _wifiStartSTA():

bool _wifiStartSTA()
{
  rlog_i(logTAG, "Start WiFi STA mode...");
  WIFI_ERROR_CHECK_BOOL(esp_wifi_set_mode(WIFI_MODE_STA), "set the WiFi operating mode");
  #ifdef CONFIG_WIFI_BANDWIDTH
    // Theoretically the HT40 can gain better throughput because the maximum raw physicial 
    // (PHY) data rate for HT40 is 150Mbps while it’s 72Mbps for HT20. 
    // However, if the device is used in some special environment, e.g. there are too many other Wi-Fi devices around the ESP32 device, 
    // the performance of HT40 may be degraded. So if the applications need to support same or similar scenarios, 
    // it’s recommended that the bandwidth is always configured to HT20.
    // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-ht20-40
    WIFI_ERROR_CHECK_BOOL(esp_wifi_set_bandwidth(WIFI_IF_STA, CONFIG_WIFI_BANDWIDTH), "set the bandwidth");
  #endif // CONFIG_WIFI_BANDWIDTH
  #ifdef CONFIG_WIFI_LONGRANGE
    // Long Range (LR). Since LR is Espressif unique Wi-Fi mode, only ESP32 devices can transmit and receive the LR data
    // more info: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-protocol-mode
    WIFI_ERROR_CHECK_BOOL(esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR), "set protocol Long Range");
  #endif // CONFIG_WIFI_LONGRANGE
  WIFI_ERROR_CHECK_BOOL(esp_wifi_start(), "start WiFi");
  wifiTimeoutStart(CONFIG_WIFI_TIMEOUT);
  return true;
}

Здесь мы:

  1. Устанавливаем режим с помощью esp_wifi_set_mode()
  2. В случае необходимости настраиваем ширину занимаемого диапазона частот – 20 или 40 МГц. При плохом уровне сигнала имеет смысл снизить ширину канала, чтобы повысить устойчивость передачи, но это приведет к снижению скорости передачи.
  3. Запускаем STA режим с помощью esp_wifi_start().

На этом все подготовительные операции закончены, и в дело вступают события и их обработчики.

 

Дальнейшая последовательность событий

Через некоторое время драйвер WiFi будет запущен и будет сгенерировано событие WIFI_EVENT_STA_START. В конце обработчика, как я описал выше, будет вызвана функция начала подключения к точке доступа wifiConnectSTA():

bool wifiConnectSTA()
{
  // Wi-Fi Configuration Phase
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-configuration-phase

  wifi_config_t conf;
  memset(&conf, 0, sizeof(wifi_config_t));
  #ifdef CONFIG_WIFI_SSID
    // Single network mode
    strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_SSID);
    strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_PASS);
  #else
    // Multi-network mode
    if (_wifiCurrIndex == 0) {
      _wifiMaxIndex = wifiGetMaxIndex();
      _wifiIndexNeedChange = false;
      _wifiIndexWasChanged = false;
      nvsRead(wifiNvsGroup, wifiNvsIndex, OPT_TYPE_U8, &_wifiCurrIndex);
      if (_wifiCurrIndex == 0) {
        _wifiCurrIndex = 1;
        _wifiIndexNeedChange = true;
        _wifiIndexWasChanged = true;
      };
    } else {
      if (_wifiIndexNeedChange) {
        if (++_wifiCurrIndex > _wifiMaxIndex) {
          _wifiCurrIndex = 1;
        };
        rlog_d(logTAG, "Attempting to connect to another access point: %d", _wifiCurrIndex);
        _wifiIndexWasChanged = true;
      };
    };

    switch (_wifiCurrIndex) {
      case 1:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_1_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_1_PASS);
        break;
      #ifdef CONFIG_WIFI_2_SSID
      case 2:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_2_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_2_PASS);
        break;
      #endif // CONFIG_WIFI_2_SSID
      #ifdef CONFIG_WIFI_3_SSID
      case 3:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_3_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_3_PASS);
        break;
      #endif // CONFIG_WIFI_3_SSID
      #ifdef CONFIG_WIFI_4_SSID
      case 4:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_4_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_4_PASS);
        break;
      #endif // CONFIG_WIFI_4_SSID
      #ifdef CONFIG_WIFI_5_SSID
      case 5:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_5_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_5_PASS);
        break;
      #endif // CONFIG_WIFI_5_SSID
      default:
        strcpy(reinterpret_cast<char*>(conf.sta.ssid), CONFIG_WIFI_1_SSID);
        strcpy(reinterpret_cast<char*>(conf.sta.password), CONFIG_WIFI_1_PASS);
        break;
    };
  #endif // CONFIG_WIFI_SSID
  
  // Select the best access point based on signal strength (MESH systems only)
  conf.sta.rm_enabled = true;
  conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
  conf.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;

  // Support for Protected Management Frame
  conf.sta.pmf_cfg.capable = true;
  conf.sta.pmf_cfg.required = false;

  // Configure WiFi
  WIFI_ERROR_CHECK_BOOL(esp_wifi_set_config(WIFI_IF_STA, &conf), "set the configuration of the ESP32 STA");

  // Wi-Fi Connect Phase
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-connect-phase
  _wifiAttemptCount++;
  rlog_i(logTAG, "Connecting to WiFi network [ %s ], attempt %d...", reinterpret_cast<char*>(conf.sta.ssid), _wifiAttemptCount);
  wifiTimeoutStart(CONFIG_WIFI_TIMEOUT);
  WIFI_ERROR_CHECK_BOOL(esp_wifi_connect(), "сonnect the ESP32 WiFi station to the AP");

  return true;
}

Здесь происходит вся настройка подключения к заданной точке доступа с помощью sp_wifi_set_config() – именно мы указываем имя сети SSID и пароль подключения. После этого запускаем сторожевой таймер подключения и инициируем подключение с помощью системной функции esp_wifi_connect().

Если попытка соединения будет не столь успешной, то через несколько секунд мы получим событие WIFI_EVENT_STA_DISCONNECTED, которое, в свою очередь, передаст управление достаточно запутанной функции wifiReconnectWiFi(), которая по сути выполняет простую задачу повторной попытки подключения к сети, и цикл начнется заново.

Если попытка соединения будет успешной, то через ещё какое время будет сгенерировано другое событие – WIFI_EVENT_STA_CONNECTED (в обработчике которого, по сути, ничего толкового не происходит, только биты состояний переключаются), а ещё через какое-то время и ещё одно, финальное событие – IP_EVENT_STA_GOT_IP.

На это все, можно начинать работу с сетью.

 


Сценарий “дождливого дня”

Описанная выше последовательность приведена, когда все работает штатно и хорошо. Но в реальной жизни может возникнуть неопределенное количество проблем и сбоев, которые мы должны корректно обработать. На Espressif называют такой вариант развития событий сценарием пасмурного дня или сценарием дождливого дня.

Ох сколько нам глюков чудных дарует кроворуких программистов век…
(с) неизвестный автор

Уход процесса подключения в глухую несознанку

Частая проблема при большом удалении esp32 от точки доступа (на пределе “слышимости”) – зависание при попытке подключения. Когда соединение уже состоялось, драйвер контролирует соединение с помощью периодической отправки маячка. В процессе установки соединения иногда возникает ситуация, когда процесс слишком затягивается. Самое простое решение проблемы – перед установкой соединения запустить программный таймер на несколько минут, который в случае зависания прервет процесс и начнет соединение заново.

static esp_timer_handle_t _wifiTimer = nullptr;

bool _wifiStopSTA();
bool _wifiRestoreSTA();
bool wifiReconnectWiFi();

static void wifiTimeoutEnd(void* arg)
{
  rlog_e(logTAG, "WiFi operation time-out!");
  if (!wifiReconnectWiFi()) {
    _wifiRestoreSTA();
    _wifiStopSTA();
  };
}

static void wifiTimeoutCreate() 
{
  if (_wifiTimer) {
    if (esp_timer_is_active(_wifiTimer)) {
      esp_timer_stop(_wifiTimer);
    };
  } else {
    esp_timer_create_args_t timer_args;
    memset(&timer_args, 0, sizeof(esp_timer_create_args_t));
    timer_args.callback = &wifiTimeoutEnd;
    timer_args.name = "timer_wifi";
    if (esp_timer_create(&timer_args, &_wifiTimer) != ESP_OK) {
      rlog_e(logTAG, "Failed to create timeout timer");
    };
  };
  rlog_v(logTAG, "WiFi timer was created");
}

static void wifiTimeoutStart(uint32_t ms_timeout) 
{
  if (!_wifiTimer) {
    wifiTimeoutCreate();
  };
  if (_wifiTimer) {
    if (esp_timer_is_active(_wifiTimer)) {
      esp_timer_stop(_wifiTimer);
    };
    if (esp_timer_start_once(_wifiTimer, (uint64_t)ms_timeout * 1000) == ESP_OK) {
      rlog_v(logTAG, "WiFi timer was started");
    } else {  
      rlog_e(logTAG, "Failed to start timeout timer");
    };
  };
}

Это поможет избежать ситуации, когда ESP32 завис на процессе подключения к сети на неопределенное время

 

Логика при неудачной попытке подключения

Если после попытки подключения к точке доступа esp_wifi_connect() вместо ожидаемого WIFI_EVENT_STA_CONNECTED приходит унылое WIFI_EVENT_STA_DISCONNECTED, то мы имеем следующую логику:

1. Первые CONFIG_WIFI_RECONNECT_ATTEMPTS (30 по умолчанию) попыток просто пытаемся подключиться к точке доступа через тот же вызов esp_wifi_connect(). Это необходимо, чтобы иметь достаточный запас времени для перезагрузки роутера, например. В среднем на одну попытку уходит 2-3 секунды, то мы имеем 1-1,5 минут времени без смены точки доступа.

2. Когда количество попыток превысит заданные CONFIG_WIFI_RECONNECT_ATTEMPTS, библиотека начнет перебирать все доступные варианты подключений (можно настроить до пяти сетей) по очереди. Это позволяет безболезненно переключить устройство к другому роутеру.

3. Если количество неудачных попыток превысит следующий порог CONFIG_WIFI_RESTART_ATTEMPTS (100 по умолчанию), то будет отдана команда на полный перезапуск STA режима со сбросом всех параметров на начальные. Очень изредка, но увы, случается такое, что ESP32 “ни в какую” не хочет подключаться к точке доступа, хотя она успешно функционирует. “Холодный старт” STA режима позволяет решить проблему в таком случае.

 

Проблема перезапуска WiFi драйвера

Драйвер WiFi основан на событиях. Давайте подумаем, что нужно сделать, чтобы перезапустить или даже просто остановить WiFi?

  • Отправить команду на отключение, если оно имеется с помощью esp_wifi_disconnect()
  • Дождаться события WIFI_EVENT_STA_DISCONNECTED
  • Отправить команду на остановку STA, выполнив esp_wifi_stop()
  • Дождаться события WIFI_EVENT_STA_STOP
  • Предпринять какие-либо действия по восстановлению подключения…

Я не зря выделил жирным шрифтом слова “дождаться события” – в них заключается очень большая скрытая проблема. Казалось бы – в чем проблема-то? Отправили команду, ждем события через xEventGroupGetBits()

А теперь представьте, что вам необходимо сделать это из обработчика другого события. Например, вы получили событие WIFI_EVENT_STA_DISCONNECTED уже много раз, и необходимо перезапустить весь интерфейс. Вы отдаете команду esp_wifi_stop() и начинаете ожидать другого события. Ждать вы будете долго. Очень долго…. Событие WIFI_EVENT_STA_STOP будет сгенерировано, как ему и положено. Но вот задача обработки событий занята пока ещё вашим предыдущим событием и обработать новое пока не в состоянии. Возникает deadlock, тупиковая ситуация. Шах и мат.

Поэтому я в такой ситуации выставляю “служебные” биты _WIFI_STA_DISCONNECT_STOP или _WIFI_STA_DISCONNECT_RESTORE и выхожу из текущего обработчика без ожидания. А уже другой обработчик, “увидев” взведенные служебные биты, будет действовать по другому алгоритму, чем обычно.

 


Использование библиотеки reWiFi

В заключение хочу дать несколько советов по использованию или адаптации моей библиотеки reWiFi, которую можно найти на GitHub.

В конечном итоге её использование сводится к простому коду:

// Подключение к WiFi AP
if (!wifiStart()) {
  ledSysBlinkOn(1, 100, 250);
};

Как видите – все предельно просто. Все настройки – имя сети, пароль и т.д. зашиты в файле конфигурации проекта, про который я писал ранее:

// -----------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------- EN - Wifi networks -----------------------------------------------------
// ------------------------------------------------ RU - WiFi сети -------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------
/** 
 * EN: Single network mode
 * --------------------
 * Uncomment CONFIG_WIFI_SSID and CONFIG_WIFI_PASS to disable automatic switching between wifi networks
 * 
 * RU: Режим для одной сети
 * --------------------
 * Раскомментируйте CONFIG_WIFI_SSID и CONFIG_WIFI_PASS, чтобы отключить автоматическое переключение между wifi-сетями
 * */
// #define CONFIG_WIFI_SSID "wifi_network_name"
// #define CONFIG_WIFI_PASS "wifi_password"

/** 
 * EN: Multi-network mode
 * --------------------
 * You can define from one to five networks. If it is not possible to connect to one of the networks, ESP will try to connect to the next one.
 * The number of a successful connection is remembered and used later (until the next failure).
 * This allows you to move the device from one building to another without reflashing and reconfiguring it. 
 * Just define all possible networks in advance.
 * 
 * RU: Режим для нескольких сетей
 * --------------------
 * Вы можете определенить от одной до пяти сетей. При невозможности подключиться к одной из сетей, ESP попытается поключиться к следующей. 
 * Номер успешного подключения запоминается и используется в дальнейшем (до следущего сбоя). 
 * Это позволяет переносить устройство из одного здания в другое, не перепрошивая и перенастраивая его. 
 * Просто заранее определите все возможные сети.
 * */
#define CONFIG_WIFI_1_SSID "WIFI1"
#define CONFIG_WIFI_1_PASS "000000000"
#define CONFIG_WIFI_2_SSID "WIFI2"
#define CONFIG_WIFI_2_PASS "111111111"
#define CONFIG_WIFI_3_SSID "WIFI3"
#define CONFIG_WIFI_3_PASS "222222222"
#define CONFIG_WIFI_4_SSID "WIFI4"
#define CONFIG_WIFI_4_PASS "333333333"
#define CONFIG_WIFI_5_SSID "WIFI5"
#define CONFIG_WIFI_5_PASS "444444444"

// EN: WiFi connection parameters. Commenting out these lines will use the default ESP-IDF parameters
// RU: Параметры WiFi подключения. Если закомментировать эти строки, будут использованы параметры по умолчанию ESP-IDF
// #define CONFIG_WIFI_STORAGE   WIFI_STORAGE_RAM
// #define CONFIG_WIFI_BANDWIDTH WIFI_BW_HT20

// EN: Restart the device if there is no WiFi connection for more than the specified time in minutes.
//     Comment out the line if you do not need to restart the device if there is no network connection for a long time
// RU: Перезапустить устройство, если нет подключения к WiFi более указанного времени в минутах. 
//     Закомментируйте строку, если не нужно перезапускать устройство при длительном отсутствии подключения к сети
#define CONFIG_WIFI_TIMER_RESTART_DEVICE 60*24

// EN: Allow periodic check of Internet availability using ping.
//     Sometimes network access may be lost, but WiFi connection works. In this case, the device will suspend all network processes.
// RU: Разрешить периодическую проверку доступности сети интернет с помошью пинга. 
//     Иногда доступ в сеть может пропасть, но подключение к WiFi при этом работает. В этом случае устройство приостановит все сетевые процессы.
#define CONFIG_PINGER_ENABLE 1

// EN: Disable network error indication (wifi, internet, openmon, tg...) as the device is not always connected to the network
// RU: Отключить иникацию сетевых ошибок (wifi, inetnet, openmon, tg...), так как устройство не всегда подключено к сети
#define CONFIG_OFFLINE_MODE 0

Библиотека позволяет автоматически переключаться между несколькими сетями, это дает возможность переносить устройство от одного роутера к другому или подключиться к аварийной сети для обновления прошивки по воздуху. Но вот автоматического выбора сети с лучшим сигналом пока нет, я пока раздумываю над реализацией.

Если взглянуть на список прилинкованных библиотек, то можно увидеть, что кроме стандартных используется несколько моих библиотек:

#include <stdint.h>
#include <stdbool.h>
#include <cstring>
#include <time.h> 
#include "esp_wifi_types.h"
#include "esp_wifi.h"
#include "project_config.h"
#include "def_consts.h"
#include "rLog.h"
#include "rTypes.h"
#include "rStrings.h"
#include "reNvs.h"
#include "reEsp32.h"
#include "reEvents.h"
#include "reParams.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"

  • rLog – от неё, в принципе, достаточно легко избавиться, просто заменив вызовы re_logx() на ESP_LOGx().
  • rTypes – по большому счету это просто объявление “стандартных” типов данных в моей прошивке. Вы можете просто перенести используемые объявления в свой код.
  • rStrings – библиотечка для работы с текстовыми строками в динамической памяти, про неё я уже писал ранее.
  • reEsp32 – сборник разных “общесистемных” функций
  • reEvents – моя “обёртка” для циклов событий, через которую в том числе идет общение с прикладным циклом
  • reNVS – ещё одна библиотека – “обёртка” для работы с NVS разделом flash
  • reParams – библиотека для обслуживания списка параметров устройства и хранения их в NVS разделе

Вам придется либо адаптировать код без этих библиотек, либо использовать и их тоже. Про некоторые из этих библиотек я уже рассказывал, а про другие постараюсь рассказать в самое ближайшее время.

 


Полезные ссылки

  1. ESP-IDF :: Wi-Fi driver
  2. ESP-IDF :: NETIF
  3. Библиотека reWiFi

 

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

7 комментариев для “Подключение к точке доступа WiFi из ESP32 с ESP-IDF”

  1. Николай

    Добрый день.
    Я пытаюсь построить простой проект в котором хочу просто подключить ESP к WiFi.
    При попытке скомпилировть проект получаю ошибку:
    PlatformIO/libs/wifi/reWiFi/include/reWiFi.h:18:10: fatal error: cstring: No such file or directory
    18 | #include
    Пути к библиотекам указаны верно.
    При этом, если компилировать Ваш проект telemeter_dzen, то всё проходит без ошибок.
    Можите подсказать, в чём проблема и что надо “подкрутить”.
    Спасибо.

    1. А какой именно библиотеки не находит компилятор? Системной или моей?
      Если системной – то скорее всего проблема в версии, так как статья написана была еще на 4.4.3, а в 5.0.0 уже многое поменялось
      Если моей – тот тут ничего не поделаешь, у меня многие библиотеки связаны друг с другом, это по сути цельная прошивка и одна либа “тащит всех подруг в круг”.
      Или переделывайте код по своему, без этих включений, но тогда вам придется не просто скомпилировать пример, а еще и немного подумать, как заменить или исключить некоторые фрагменты.
      Или используйте все библиотеки, как в telemeter_dzen

      1. Николай

        Спасибо за Ваш ответ.
        Я нашёл проблему. Ошибка возникала в includ в вашей либе reWiFi, при попытке выполнить #include .Но это, как оказалось, больше поя вина. При создании нового проекта, VSCode создал файл main.c с расширением “С”. Очевидно компилятор основываясь на этом выполнял компиляцию с настройками именно для Си проекта. А библиотека cstring и её импорт без указания расширения .h, если я не ошибаюсь, является частью С++. Стоило переименовать main.c в main.cpp, как всё стало на свои места. 🙂
        Да, я заметил что Ваши библиотеки, во многом, связаны друг с другом. Я как раз пытаюсь эту связь немного ослабить/удалить. И для подключения к WiFi мне это удалось. Код скомпилировался, прошился и работает. Но я продолжаю изучать библиотеки. Следующий шаг будет добавление кода работы с датой и временем.
        Ещё хотел спросить. Будет ли заметка про режимы сна для ESP32? Я имею ввиду как это делается в ESP-IDF. Я сделал устройство на либах из Arduino, но мне это не очень нравится. Хотелось бы его переделать. И так как устройство питается от аккумулятора, зарядка от солнечной панели, то устройство надо укладывать спать. А при пробуждении делать замеры нужных параметров, в том числе и состояние батареи, проверять нет ли обновлений прошивки (если есть то загрузить и установить) и опять в сон. Поэтому, освещение этих вопросов в контексте ESP-IDF было бы очень интересно.
        Ну, как-то так. 🙂
        Ещё раз спасибо.

        1. С режимами сна я не разбирался, у меня все устройства с сетевым питанием и работают в цикле. Но в общих чертах знаю, что пробуждать из сна можно по таймеру или сигналу на выводе. Но там есть кое-какие ограничения, например слетает счетчик тактов процессора.

  2. Добрый день.
    Познавательно, спасибо.
    Раз уж Вы глубоко погрузились в тему, для Вас не составит труда дать совет.
    Как в среде Arduino IDE считать из NVS – SSID и WiFiPass после успешного подключения (если я правильно понял они хранятся именно там).
    Использую RainMaker, там своя процедура передачи этих параметров от клиента, заранее в скетче они не определены. Поэтому и возник такой вопрос.

  3. Анатолий

    Обязательно ли ставить таймер при получении WIFI_EVENT_STA_CONNECTED для таймаута IP_EVENT_STA_GOT_IP? Драйвер сам так и будет висеть если что-то пойдет не так? Разве он не сделает WIFI_EVENT_STA_DISCONNECTED после небольшого периода неактивности?

    1. Таймер нужен только для того, чтобы перезапустить процесс, если WIFI_EVENT_STA_CONNECTED пришло, а IP_EVENT_STA_GOT_IP мы так и не дождались.
      Что-то пойти не так может на любом этапе. У меня так бывало на устройстве, у которого был уровень сигнала на уровне -80 dBi. Подключение выполнялось, а получить IP так и не получалось, так как рвалось соединение.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *