Найти в Дзене
K12 :: О ESP32 и не только

Arduino ESP32 шаг за шагом. Телеметрия через WiFi и MQTT для чайников. Часть 2

Продолжение. Начало на Дзене здесь, полная статья на сайте kotyara12.ru здесь. Поскольку эта статья о том, как создать устройство с удаленным управлением и контролем, первым делом нам понадобиться подключить его к сети WiFi и интернету. Конечно, можно управлять ESP и без WiFi, например через BT и GPRS – но данная статья не об этом. На ESP32 поддерживаются следующие режимы работы Wi-Fi: В рамках данной статьи я буду рассматривать исключительно STA режим, то есть мы будем подключать наш микроконтроллер к существующей Wi-Fi сети (точке доступа). Для этого нам понадобятся параметры этой сети: как минимум SSID этой сети (имя сети) и кодовая фраза (пароль) для подключения. Здесь и далее с статье SSID сети и пароль доступа указаны в открытом виде как обычные константы – сделано это в основном для простоты понимания. Таким образом ESP32 может подключиться только к одной-единственной сети, для которой мы заранее указали эти параметры. Для изменения SSID сети или пароля придется внести изменения
Оглавление

Продолжение. Начало на Дзене здесь, полная статья на сайте kotyara12.ru здесь.

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

Поскольку эта статья о том, как создать устройство с удаленным управлением и контролем, первым делом нам понадобиться подключить его к сети WiFi и интернету. Конечно, можно управлять ESP и без WiFi, например через BT и GPRS – но данная статья не об этом.

На ESP32 поддерживаются следующие режимы работы Wi-Fi:

  • Station mode (STA) — режим клиента: ESP32 подключается к существующей Wi-Fi сети (точке доступа).
  • Access Point mode (AP) — режим точки доступа: ESP32 создает собственную Wi-Fi сеть, к которой могут подключаться другие устройства.
  • Station/AP coexistence mode (APSTA) — одновременная работа в режиме клиента и точки доступа: ESP32 может быть подключён к одной Wi-Fi сети и одновременно создавать свою.
  • NULL modeWi-Fi отключён: интерфейсы станции и точки доступа не инициализированы, это может использоваться для работы в режиме сниффера или для временного отключения Wi-Fi без выгрузки драйвера.

В рамках данной статьи я буду рассматривать исключительно STA режим, то есть мы будем подключать наш микроконтроллер к существующей Wi-Fi сети (точке доступа). Для этого нам понадобятся параметры этой сети: как минимум SSID этой сети (имя сети) и кодовая фраза (пароль) для подключения.

-2
Здесь и далее с статье SSID сети и пароль доступа указаны в открытом виде как обычные константы – сделано это в основном для простоты понимания. Таким образом ESP32 может подключиться только к одной-единственной сети, для которой мы заранее указали эти параметры. Для изменения SSID сети или пароля придется внести изменения в исходный код прошивки и заново перезалить прошивку в ESP32. Это может быть проблематичным в некоторых случаях.
В качестве альтернативы можно заранее настроить несколько разных “комплектов” SSID + пароль и переключаться между ними; либо использовать предусмотренный разработчиками режим SmartConfig для настройки сети после прошивки ESP32.

2.1. Простой пример подключения в режиме STA

В сети интернет и на сайте производителя вы можете найти, как правило, один и тот же пример кода, с помощью которого происходит подключение к сети WiFi:

-3

Здесь после вызова WiFi.begin() происходит ничем не ограниченное ожидание подключения в цикле, а затем выводится IP-адрес устройства, полученный от роутера. После этого могут могут выполняться другие действия.

Хотя этот пример и приведен в официальной документации, но он, скажем так, не совсем оптимален. Например я сразу вижу явные и довольно критичные проблемы:

  • Если в момент запуска доступа к заданной сети нет, то устройство не начнет выполнять основную работу в цикле loop() до момента подключения
  • В случае потери соединения и повторного переподключения мы никогда об этом не узнаем – а нужно будет вновь выполнить некоторые действия.
  • Данный код не сообщает никаких дополнительных сведений о причинах проблемы, когда что-то пошло не так.

Поэтому не будем слепо его копировать, а давайте попробуем разобраться что к чему, и попробуем его улучшить. Или переписать все “с нуля”. Но для этого нам потребуется немного погрузиться в “теорию”. Если вы уже знаете как работает класс WiFi STA или считаете, что для теория только для упоротых, то можете с чистой совестью пропустить следующую главу.

2.2. Классы WiFi и с чем их едят

Прежде чем работать с WiFi-объектами, необходимо подключить к проекту соответствующий модуль:

#include <WiFi.h>

Он подключает к вашему скетчу модуль, в котором объявлен класс WiFiClass:

class WiFiClass : public WiFiGenericClass, public WiFiSTAClass, public WiFiScanClass, public WiFiAPClass

{

...

}

WiFiClass просто объединяет в себе несколько других классов: WiFiGenericClass, WiFiSTAClass, WiFiScanClass, WiFiAPClass для большего удобства работы. В контексте данной статьи нам интересен только WiFiSTAClass. При особой необходимости, наверное, можно использовать и WiFiSTAClass напрямую.

В Arduino скетчах не требуется объявлять переменную – экземпляр класса WiFiClass, так как она уже объявлена в том же WiFi.h:

extern WiFiClass WiFi;

Когда запускается WiFi-клиент, это автоматически вызывает следующие события (кроме всего прочего):

  • запускается системный цикл событий для передачи “сигналов” управления (событий)
  • инициализируется nvs-раздел, который используется для хранения служебных данных WiFi
  • запускается специальная служебная задача “wifi”, которая будет выполнять всю фоновую работу по обслуживанию соединения

То есть вся работа с WiFi драйвером выполняется “в фоне”, помимо вашего скетча. В связи с эти хочется ещё и ещё раз упомянуть о потокобезопасности класса WiFiClass.

Из callback-ов, подключаемых к объекту WiFiClass, нельзя напрямую изменять переменные вашего скетча. Рекомендуется использовать для этого любые средства синхронизации потоков – мьютексы, очереди, группы событий и т.д.

В официальной документации нет явной информации о потокобезопасности класса WiFiClass и его компонентов в Arduino-ESP32. Поэтому, если требуется доступ к одному объекту WiFiClient из нескольких потоков, рекомендуется самостоятельно обеспечивать синхронизацию доступа (например, через мьютексы), чтобы избежать возможных проблем.

2.2.1 Подключение к сети WiFi в режиме STA

Подключение к существующей сети в Arduino-ESP32 сводится к вызову одного-единственного метода: WiFi.begin(). Именно он и только он запускает процесс подключения к сети. Всё остальное, что обычно имеется в примерах (и в примере выше) – лишнее, мусор и шелуха, от которого при желании можно легко избавиться.

В Arduino-ESP32 режим STA (Station) устанавливается автоматически при вызове этого же самого метода WiFi.begin(ssid, password); — отдельной функции для явного выбора режима STA в Arduino API не предусмотрено.

Имеется несколько вариантов этого метода:

-4

Эти функции возвращают результат типа wl_status_t, описание которого приведено ниже.

Позвольте, а где же здесь метод с двумя параметрами begin(ssid, password)?

А это и есть “вариант 2” – просто канал, bssid и признак подключения не указаны, а используются значения по умолчанию, то есть WiFi.begin(ssid, password, 0, NULL, true).

2.2.1.1 Управление автоматическим подключением и переподключением

А что произойдёт, если я укажу connect = false, то есть WiFi.begin(ssid, password, 0, NULL, false)?

Тогда автоматического подключения не произойдет и для подключения потребуется вызвать WiFi.begin() без параметров. Это можно использовать для отложенного запуска процесса подключения по каким-либо причинам.

А что будет. если соединение внезапно потеряно прервано, например роутер выключен или перезагружен? Вновь вызывать WiFi.begin()?

WiFi.reconnect(), но совсем не обязательно. Если включена опция WiFi.setAutoReconnect(true) (а она включена “по умолчанию), то драйвер сам будет пытаться переподключиться к той же сети, без вашего участия.

Но вы можете отключить эту опцию, чтобы самому управлять процессом переподключения – например для осуществления попытки подключения к другой сети, с другими параметрами. Или если хотите сами полностью “рулить” данным процессом. Само переподключение можно выполнить, в том числе, в обработчике события ARDUINO_EVENT_WIFI_STA_DISCONNECTED:

-5

А зачем ещё нужен метод WiFi.setAutoConnect() и чем он отличается от WiFi.setAutoReconnect()?

WiFi.setAutoConnect() является устаревшим и не рекомендуется к использованию.

2.2.1.2 Выбор наилучшей точки доступа

Перед подключением Вы можете дополнительно выбрать методы сканирования сети для оптимального поиска точки доступа, но делать это нужно перед вызовом любого варианта begin():

-6

2.2.1.3 Ожидание подключения

Подключение к сети WiFi занимает заметное время – от единиц до нескольких десятков секунд, а может и дольше… В примере, который приведен выше, для ожидания подключения использовался цикл. Класс WiFiSTAClass предоставляет для ожидания специальный метод, где мы можем дополнительно указать время ожидания в миллисекундах:

uint8_t waitForConnectResult(unsigned long timeoutLength = 60000);

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

2.2.1.4 Как проверить состояние подключения или если вдруг что-то пошло не так

Дабы проверить, есть ли подключение в настоящий момент, можно воспользоваться функцией:

bool isConnected();

Более расширенные сведения о состоянии подключения можно узнать с помощью метода status():

static wl_status_t status();

Она возвращает результат типа wl_status_t, в котором закодированы возможные состояния подключения:

-7

Лирическое отступление от основной темы: const или #define?

Может быть вы обратили внимание, как в примере выше объявлены строковые константы:

const char* ssid = "your-ssid"; // Замените на имя вашей WiFi сети
const char* password = "your-password"; // Замените на пароль вашей WiFi сети

Точно также можно объявить и константы – числа, и другие типы данных. В учебниках часто пишут, что именно так и нужно, ибо “правильно”.

Но, если вы заглянете в исходный код “рабочих” проектов или даже в собственно исходники Arduino ESP32 или ESP-IDF, то гораздо чаще встретите неправильные мёд объявления вида #define "бла-бла-бла" или #define 10, например примерно так:

#define CONFIG_WIFI_SSID "your-ssid" // Замените на имя вашей WiFi сети

#define CONFIG_WIFI_PSWD "your-password" // Замените на пароль вашей WiFi сети

Почему это очень часто объявлено именно так, а не через const %тип_данных%?

Дело в том, что #define – это не ключевое слово языка Си, а директива препроцессора, и это совсем не объявления констант!

Перед компиляцией специальная программка – “препроцессор” проходится по вашему коду и подготавливает его к обработке компилятором. В том числе заменяет все вхождения CONFIG_WIFI_SSID и CONFIG_WIFI_PSWD на "your-ssid" и "your-password" соответственно. Без каких-либо рассуждений и определений типов!

То есть  на “вход” компилятора поступит такая команда, буквально:

WiFi.begin("your-ssid", "your-password");

И получается, что тип константы потом определяет сам компилятор – в данном случае он сам попытается привести их к типам, которые указаны в объявлении функции или метода класса.

В некоторых случаях это может привести к маловразумительным ошибкам, с которыми очень сложно бороться. Например если вы попытаетесь объявить макрос таким образом:

#define CONFIG_INT_VALUE 022

То, наверное, ожидаете, что в исходный код для компилятора попадет именно значение 22 в десятичном выражении?

А вот и нет! В код попадет десятичное значение 18! Потому что компилятор по нулю перед числом определит это значение как восьмеричное. И вы можете долго искать причину, почему программа работает неправильно.

Так почему же многие программисты продолжают этим #define активно пользоваться? Потому то с помощью этих самых макросов можно управлять процессом сборки!

Например можно написать такой макрос:

#if defined(CONFIG_WIFI_SSID)
// Здесь поместим весь код, который относится к WiFi и все что с ним связано
#endif // CONFIG_WIFI_SSID

В этом случае, если вы закомментируете объявление макроса // #define CONFIG_WIFI_SSID "your-ssid", то препроцессор полностью удалит из подготовленного к компиляции кода всё, что связано с WiFi. С помощью const бла-бла-бла провернуть такой “фокус” не удастся – ваш код даже с условиями будет компилироваться в любом случае.

Таким образом, с помощью макросов препроцессора очень удобно управлять сборкой кода с различной функциональностью. Не ошибусь, если заявлю, что вся система конфигурирования ESP-IDF, вызываемая командой menuconfig, построена именно на этих самых макросах препроцессора.

Теперь вы можете сами обдуманно решить, какой метод использовать в вашем проекте. В данном примере я пока оставлю всё, как рекомендуют нам учебники.

2.3. Оптимизируем процесс подключения

Первым делом выкинем из setup() все, кроме инициации подключения:

-8

аким образом мы избавились от бесконечного ожидания соединения.

Но у нас появилась проблема – как определить момент подключения к сети, чтобы выполнить какие-то дополнительные действия. Это можно сделать как минимум двумя способами:

  • В каждой итерации рабочего цикла считывать состояние WiFi соединения и сравнивать его с предыдущим. Если состояние изменилось, то можно предпринять какие либо действия, например вывести сообщение
  • Написать и подключить обработчик событий WiFi, который сам будет отслеживать состояние WiFi

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

2.3.1. Отслеживаем состояние WiFi в рабочем цикле

Отслеживать состояние необходимо в рабочем цикле. Но запихивать код проверки непосредственно в loop() – плохая идея. Поэтому я написал специальную функцию – wifiCheck():

-9

Затем модифицируем основной код таким образом:

-10

Проверяем работоспособность:

-11

2.3.2. Отслеживаем состояние WiFi через callback

Другой способ, ещё более “красивый” и удобный – использовать функцию(и) обратного вызова. Данный вариант хорош хотя бы тем, что мы полностью освободили цикл loop() от чуждых ему обязанностей проверять состояние WiFi каждые nnn миллисекунд. Не говоря уже о том, что он даже выглядит более просто и понятно.

-12
-13

Но тут есть один подводный камень, про который я упоминал выше: cbWiFiEvent() вызывается не из контекста задачи-скетча, а из контекста цикла событий. Поэтому напрямую обращаться к объектам, которые объявлены и используются в цикле loop(), уже нельзя. Будем учитывать этот момент в дальнейшем.

Продолжение следует...