IT Академия Samsung – WiFi # WiFi <div class="validation"> **Внимание!** <br /> Описание данного практикума **отличается** от того, что приведено на сайте [IT Академии](https://innovationcampus.ru/lms/mod/book/view.php?id=1329)! </div> ## Введение ESP8266 – это самая популярная и недорогая плата WiFi для начинающих. Появившись в 2014&nbsp;году, она взорвала рынок своей сверх-низкой ценой и ее начали встраивать абсолютно везде. Мы будем ее использовать как учебный пример модуля WiFi – это не значит, что вы будете гарантированно встраивать ее в дальнейшем в свои решения. Выбор конкретного оборудования, в том числе и модуля связи, осуществляется исходя из решаемой задачи и накладываемых ограничений. Для ESP8266 существует свой собственный SDK и ее тоже можно программировать, поскольку она представляет собой слабый микроконтроллерный модуль с небольшим количеством свободных выводов и весьма ограниченным набором периферии. Так что минимальные задачи можно выполнять и только средствами одного этого чипа. Но мы не будем погружаться в эти детали, поскольку учебная программа не предполагает изучения множества различных SDK, да это и не нужно, ведь WiFi-модуль можно использовать и как просто интерфейс связи, пользуясь его встроенными AT-командами. Сегодня мы получим необходимые начальные навыки работы с ESP8266 и посмотрим, как использовать ее в связке с внешним микроконтроллером. ## Необходимое оборудование - радиомодуль UNWR с платой-адаптером UMDK-RF: ![ud-min3](/assets/images/iot-academy/ud-min3.png) - плата-адаптер модулей связи 804-Xbee: ![804-xbee](/assets/images/iot-academy/804-xbee.png) - Модуль связи WiFi на базе ESP8266: ![xbee-wifi](/assets/images/iot-academy/xbee-wifi.png) - WiFi-точка доступа (роутер). Из первых компонентов собираем подобную конструкцию (не забываем ориентировать все платы по ключу): ![unwr_wifi](/assets/images/iot-academy/unwr_wifi.png) А точка доступа будет выполнять традиционную роль – обеспечивать подключение оборудование к сети. Соответственно, вы должны будете знать ее SSID и пароль доступа к ней. ## Коммуникация с ESP8266 через последовательный порт Представленная выше конструкция позволяет нам осуществлять взаимодействие микроконтроллера с внешним миром по следующей схеме: ![wifi](/assets/images/iot-academy/wifi.png) В целом, для подключения ESP8266 как интерфейсного модуля по UART достаточно соединить его при помощи четырех проводов: | ESP8266 | Контроллер | |:---------:|:------------:| | `Tx` | `Rx` | | `Rx` | `Tx` | | `VCC` | `VCC` | | `GND` | `GND` | За передачу данных от устройства отвечает сигнал `Tx` (transmit), а за прием – `Rx` (receive). Соответственно, передаваемый сигнал `Tx` должен поступать на вход приемника `Rx`. Обмен информацией осуществляется последовательными импульсами, амплитуда которых отчитывается относительно общего сигнала – земли (`GND`, ground). Именно по этой причине контакты `GND` всех устройств должны быть соединены вместе. Если модуль ESP8266 не имеет своего собственного питания (например, от батарейки или сетевого адаптера), то можно запитать его от платы контроллера, однако следует помнить, что номинальное рабочее напряжение – 3,3&nbsp;В. <div class="danger"> Максимально допустимое напряжение, подаваемое на входы микроконтроллера и модуля ESP8266 не должно превышать 3,3&nbsp;В! </div> В микроконтроллере STM32L151CCU6, что установлен в радиомодуле UNWR, имеется 3 UART. UART1 (`PA9`, `PA10`) используется для связи с платой-адаптером UMDK-RF и доступен для внешних подключений через контакты `2` и `3` (соответственно `UNWR_GPIO_2`, `UNWR_GPIO_3`). UART2 (`PA2`, `PA3`) доступен через контакты `25` и `26` (`UNWR_GPIO_25`, `UNWR_GPIO_26`), а UART3 (`PB10`, `PB11`) – через контакты `29` и `30` (`UNWR_GPIO_29`, `UNWR_GPIO_30`). Если бы у нас не было платы-адаптер модулей связи 804-Xbee, и нужно было бы подключать какой-либо модуль ESP8266 к нашему контроллеру, то мы бы воспользовались приведенной выше таблицей, и подключили бы ESP8266, например, к порту UART2 при помощи отдельных проводов: ![umdk-esp8266](/assets/images/iot-academy/umdk-esp8266.png) UART1 для подключения к модулю WiFi использовать не рекомендуется, так как через этот порт осуществляется связь с компьютером по USB. В нашей конструкции, плата-адаптер модулей связи 804-Xbee тоже осуществляет подключение модуля WiFi через порт UART2. Как вы помните, с WiFi модулем ESP8266 работа может осуществляться посредством AT-команд, при помощи которых этим модулем можно управлять и организовывать передачу данных. Давайте для начала пообщаемся с платой напрямую. Мы передадим ей несколько простых AT-команд и увидим, что плата действительно подключается к WiFi-точке доступа. Сделаем из нашего контроллера STM32 простой мост: все данные приходящие на UART1 будем пересылать на UART2 и наоборот. При использовании Mbed 6-й версии такая программа будет выглядеть так: ```cpp #include "mbed.h" UnbufferedSerial pc(USBTX, USBRX); UnbufferedSerial dev(PA_2, PA_3); DigitalOut led1(LED1); void dev_recv() { char c; led1 = 1; while(dev.readable()) { dev.read(&c, 1); pc.write(&c, 1); } } void pc_recv() { char c; led1 = 1; while(pc.readable()) { pc.read(&c, 1); dev.write(&c, 1); } } int main() { pc.baud(115200); dev.baud(115200); pc.attach(&pc_recv, SerialBase::RxIrq); dev.attach(&dev_recv, SerialBase::RxIrq); while(1) { led1 = 0; sleep(); } } ``` Обратите внимание, что в программе есть строчки, задающие скорость обмена данными по последовательным портам 115200&nbsp;бит/с – именно на такой скорости осуществляется управление AT-командами. Соответственно, в файл `plantormio.ini` к нашим настройкам нужно будет **добавить** следующую строку (логичнее всего дописать после указания `monitor_port`): ``` monitor_speed = 115200 ``` Скомпилируйте программу и загрузите ее в контроллер.Запустите монитор порта. Если делаете это при помощи внешнего приложения, а не PlatformIO, то укажите порт и скорость 115200. При инициализации модуля ESP8266 он выдает некоторую отладочную информацию, но использует для этого нестандартую скорость обмена, поэтому вы сначала будете наблюдать некоторый "мусор", но не обращайте на него внимания – в конце-концов должна будет появиться надпись **ready**. ![xterm_esp8266_wifi_ready](/assets/images/iot-academy/xterm_esp8266_wifi_ready.png) Давайте сначала введем команду `AT`, а в ответ мы должны получить `OK`, но при ошибках, например, когда вводится несуществующая команда, плата выдаст ответ `ERROR`. Ввод каждой команды нужно завершать символами возврат каретки (`CR`) и перенос строки (`LF`). Если вы используете встроенный монитор порта PlatformIO, то данные символы будут добавлены автоматически после нажатия клаиши **Enter**, а в такой программе, как GtkTerm, для ввода символа возврат каретки нужно нажать сочетание клавиш **CTRL**+**M**, а для символа перенос строки – **CTRL**+**J**. Это те самые ASCII-символы, которые в языке программирования C (и C++) обозначаются как `\r` и `\n`. При помощи команды `AT+GMR` можно получить информацию о радиомодуле WiFi. ![xterm_esp8266_wifi_at](/assets/images/iot-academy/xterm_esp8266_wifi_at.png) Если вы видите, что версия прошивки 1.7.4, то вы работаете с самой последней версией программного обеспечения. На текущий момент компания [Espressif Systems](https://www.espressif.com) прекратила развивать классическую версию (legacy) программного обеспечения ESP8266, но более новая, к сожалению, не подходит к нашим модулям – в ней задействованы совершенно иные выводы для подключения к UART. Так что обновлять прошивку, в отличие от рекомендаций на сайте [IT Академии](https://innovationcampus.ru/lms/mod/book/view.php?id=1329&chapterid=1138) мы не будем. ### Примеры AT-команд Чтобы немного освоиться с логикой работы WiFi-модуля, давайте сделаем следующее: через AT-команды переведем плату в режим клиента (по умолчанию она находится в режиме точки доступа), и попробуем подключиться к одной из сетей. Команда `AT+CWMODE` касается режима работы платы. Если ввести ее со знаком вопроса в конце (`AT+CWMODE?`), то выведется информация о том, в каком режиме плата находится сейчас: ![pio_monitor_at+cwmode_q](/assets/images/iot-academy/pio_monitor_at+cwmode_q.png) Режимов всего 3: <ol> <li>Режим клиента</li> <li>Режим точки доступа</li> <li>3Двойной режим: и клиент, и точка доступа</li> </ol> Мы видим, что по умолчанию WiF-модуль сейчас в режиме точки доступа. Нам в настоящий момент нужен режим клиента, поэтому выберем режим 1, командой `AT+CWMODE=1`: ![pio_monitor_at+cwmode_e1](/assets/images/iot-academy/pio_monitor_at+cwmode_e1.png) Команда `AT+CWLAP` выведет листинг имеющихся точек доступа: ![pio_monitor_at+cwlap](/assets/images/iot-academy/pio_monitor_at+cwlap.png) Эта команда не будет срабатывать, если плата находится в режиме точки доступа (что естественно). Чтобы подключиться к точке доступа, используйте команду `AT+CWJAP`, передав в нее в качестве параметров название сети и пароль от нее (всё в кавычках): ``` AT+CWJAP="my-test-wifi","1234test" ``` Если всё получится, то увидите следующее: ![pio_monitor_at+cwjap](/assets/images/iot-academy/pio_monitor_at+cwjap.png) Теперь плата запомнила эту сеть и будет подключаться к ней каждый раз при старте. Если хотите, чтобы плата забыла сеть, передайте ей такую команду `AT+CWQAP`. В ней буква **Q** означает **Q**uit (а **J** в предыдущей команде, как несложно догадаться, означает **J**oin). Сделайте это прямо сейчас, чтобы мы могли проверить, как работает поиск сети и подключение из MBed далее. Выполнив эту задачу, увидите надпись **WIFI DISCONNECT** ![pio_monitor_at+cwqap](/assets/images/iot-academy/pio_monitor_at+cwqap.png) Полный список поддерживаемых команд, формат их параметров и ответов можно узнать из официальной [документации](https://www.espressif.com/sites/default/files/documentation/4b-esp8266_at_command_examples_en.pdf) на модуль ESP8266. ## Подключение по WiFi Итак, все необходимое оборудование у нас есть, и связь между контроллером и модулем связи присутствует (это надо проверить, если у вас модуль связи подключается проводами, а не при помощи платы-адаптера). Создайте проект в PlatformIO для нашей платы, как это уже делали раньше. В `main.cpp` поместите следующий код: ```cpp #include "mbed.h" #include "TCPSocket.h" #include "ESP8266Interface.h" ESP8266Interface wifi; const char *sec2str(nsapi_security_t sec) { switch (sec) { case NSAPI_SECURITY_NONE: return "None"; case NSAPI_SECURITY_WEP: return "WEP"; case NSAPI_SECURITY_WPA: return "WPA"; case NSAPI_SECURITY_WPA2: return "WPA2"; case NSAPI_SECURITY_WPA_WPA2: return "WPA/WPA2"; case NSAPI_SECURITY_UNKNOWN: default: return "Unknown"; } } void scan_demo(WiFiInterface *wifi) { WiFiAccessPoint *ap; printf("Scan:\r\n"); int count = wifi->scan(NULL, 0); /* Limit number of network arbitrary to 15 */ count = count < 15 ? count : 15; ap = new WiFiAccessPoint[count]; count = wifi->scan(ap, count); for (int i = 0; i < count; i++) { printf("Network: %s secured: %s BSSID: %hhX:%hhX:%hhX:%hhx:%hhx:%hhx RSSI: %hhd Ch: %hhd\r\n", ap[i].get_ssid(), sec2str(ap[i].get_security()), ap[i].get_bssid()[0], ap[i].get_bssid()[1], ap[i].get_bssid()[2], ap[i].get_bssid()[3], ap[i].get_bssid()[4], ap[i].get_bssid()[5], ap[i].get_rssi(), ap[i].get_channel()); } printf("%d networks available.\r\n", count); delete[] ap; } int main() { SocketAddress sa; printf("WiFi example\r\n\r\n"); scan_demo(&wifi); printf("\r\nConnecting...\r\n"); int ret = wifi.connect(MBED_CONF_APP_WIFI_SSID, MBED_CONF_APP_WIFI_PASSWORD, NSAPI_SECURITY_WPA_WPA2); if (ret == NSAPI_ERROR_IS_CONNECTED) { printf("Already connected!\n\r"); } else if (ret != NSAPI_ERROR_OK) { printf("\r\nConnection error: %d\r\n", ret); return -1; } printf("Success\r\n\r\n"); printf("MAC: %s\r\n", wifi.get_mac_address()); wifi.get_ip_address(&sa); printf("IP: %s\r\n", sa.get_ip_address()); wifi.get_netmask(&sa); printf("Netmask: %s\r\n", sa.get_ip_address()); wifi.get_gateway(&sa); printf("Gateway: %s\r\n", sa.get_ip_address()); printf("RSSI: %d\r\n\r\n", wifi.get_rssi()); wifi.disconnect(); printf("\r\nDone\r\n"); } ``` Также нужно создать файл настроек проекта `mbed_app.json` следующего содержания: ```json { "config": { "wifi-ssid": { "help": "WiFi SSID", "value": "\"myssid\"" }, "wifi-password": { "help": "WiFi Password", "value": "\"mypassword\"" } }, "target_overrides": { "*": { "esp8266.provide-default" : true, "esp8266.tx" : "PA_2", "esp8266.rx" : "PA_3", "esp8266.built-in-dns": true, "esp8266.socket-bufsize": "1024", "esp8266.debug": false, "platform.stdio-convert-newlines": true, "platform.stdio-baud-rate": 115200, "platform.default-serial-baud-rate": 115200, "target.printf_lib" : "std" } } } ``` Обратите внимание, что в секции `config` этого файла должны быть указаны параметры сети WiFi, а именно SSID и пароль доступа. Замените, соответственно, `myssid` и `mypassword` на верные значения, при этом все имеющиеся кавычки и символы `\` должны остаться! В секцции `target_overrides` указываются параметры, специфичные для конкретного таргета. Но так как он у нас один, соответствующий нашей плате, и с другими платами контроллеров мы работать не предполагаем, то все определения сделаны в общей "для всех" таргетов части, отмеченной звездочкой. В частности, указываем параметры, обеспечивающие подключение через модуль WiFi ESP8266: ```json "esp8266.provide-default" : true ``` Строки ```json "esp8266.tx" : "PA_2", "esp8266.rx" : "PA_3", ``` очевидно отвечают за то, к каким выодам осуществлено подключение модуля ESP8266 к плате контроллера. Информацию о других конфигурационных параметрах можно посмотреть в документации или исходниках драйвера ESP8266. Чтобы продолжить работать с палатой контроллера на скорости 115200&nbsp;бит/с в конфигурационный файл добавлены соответствующие настройки: ```json "platform.stdio-baud-rate": 115200, "platform.default-serial-baud-rate": 115200, ``` В самой программе (файл `main.cpp`) создается экземпляр класса `WiFiInterface` с именем `wifi`, и вся работа осуществляется именно через него. Скомпилируйте программу. Загрузите ее в контроллер. Как несложно видеть по коду программы, практически никаких циклических действий программа не предпринимает: показывает найденные точки доступа WiFi и пытается подключиться к выбранной, после чего работа завершается. Весь вывод осуществляется через последовательный порт на скорости 115200. При успешном подключении результат работы программы будет примерно следуюшим ![xterm_wifi_demo](/assets/images/iot-academy/xterm_wifi_demo.png) В отличие от исходного примера работы с WiFi в Mbed, наш код учитывает ситуацию, при которой модуль WiFi уже подключен к точке доступа ```cpp ... if (ret == NSAPI_ERROR_IS_CONNECTED) { printf("Already connected!\n\r"); } else if (ret != NSAPI_ERROR_OK) { printf("\nConnection error: %d\n", ret); return -1; } ... ``` Относительно других возможных ошибок стоит посмотреть в [документации](https://os.mbed.com/docs/mbed-os/v6.15/apis/network-socket-interfaces-and-classes.html) (не доступно без VPN) или в [исходниках](https://github.com/ARMmbed/mbed-os/blob/master/connectivity/netsocket/include/netsocket/nsapi_types.h). ## Как устроена работа с сетями в Mbed? После того, как заработал самый простой пример с WiFi, необходимо сделать небольшое отступление. Как вообще происходит работа с сетями в Mbed? Всегда ли нужно использовать ESP8266? А если нужно подключиться по проводам, то как быть? На нижеприведенной картинке показана схема работа с сетевым стеком. Мы сейчас посмотрели работу нижнего уровня – на уровне 2 (Data Link) мы подключились к точке доступа по WiFi. Дальше мы посмотрим, как устроен транспортный уровень на примере Socket API, но это будет следующий шаг. ![mbed_osi](/assets/images/iot-academy/mbed_osi.png) Обратим внимание вот на что. Есть 4 разных протокола канального уровня – это Ethernet, WiFi, Cellular, и Mesh (или, как показано на этом изображении, IEEE 802.15.4). При этом API у всех интерфейсов одинаковый! То есть теоретически, при смене протокола программа не должна вообще поменяться. Меняется только конфигурационный файл с параметрами подключения, а сам код должен быть достаточно абстрактно устроен, чтобы его не пришлось переписывать каждый раз при смене Ethernet на WiFi, к примеру. Как достичь этого? В Mbed есть общий класс под названием `NetworkInterface`. Обращение к этому классу выглядит примерно так: ```cpp NetworkInterface *net = NetworkInterface::get_default_instance(); if (!net) { printf("Error! No network inteface found.\n"); return 0; } net->connect(); ``` Тип сетевого интерфейса и его контекстно-зависимые параметры должны быть заданы в конфигурационном файле `mbed_app.json`. По умолчанию тип интерфейса `ETHERNET`, но так как мы предполагаем использование беспроводной связи WiFi, то и укажем соответствующее значение: ```json "target.network-default-interface-type": "WIFI", "nsapi.default-wifi-security": "WPA_WPA2", "nsapi.default-wifi-ssid": "\"ssid\"", "nsapi.default-wifi-password": "\"password\"", ``` Также можно указать `CELLULAR`, `MESH` или `ETHERNET` в качестве типа сетевого интерфейса, при этом параметры для каждого из вариантов, если они вам понадобятся, можно узнать в [документации](https://mbed.com/docs/mbed-os/v6.15/apis/connectivity-options-and-config.html#selecting-the-default-network-interface) (нужен VPN). В идеале, каждая программа, использующая сетевое соединение, должна выглядеть именно так. Тот пример, который мы смотрели с WiFi – слишком конкретный, и ориентирован сугубо на использование модуля ESP8266. В дальнейшем мы используем упомянутый выше подход при обращении к другому протоколу канального уровня, а пока лишь дополним наш пример новым функционалом. ## Сокеты Для обеспечения сетевых коммуникаций используются сокеты. Сокет – это конечная точка сетевых коммуникаций. Для взаимодействия между машинами с помощью стека протоколов TCP/IP используется IP-адрес и порт. Адрес представляет собой 32-битную структуру для протокола IPv4 или 128-битную для IPv6. Номер порта – целое число в диапазоне от 0 до 65535. Именно эта комбинация (адрес и порт) определяет сокет. Каждый использующийся сокет связан с процессом (программой) и имеет тип. В TCP/IP известны TCP-сокеты и UDP-сокеты. Передача данных посредством TCP ориентирована на предварительное установление соединение, а UDP – нет. В целом, передача данных с использованием UDP проще, но при этом доставка пакетов данных не гарантируется. Для устройств интернета вещей это может быть большим минусом, так как они зачастуюя работают от батареек, а передача данных – это самая энергозатратная часть работы, и если данные будут постоянно теряться, и нужно будет их повторно передавать, то это резко сократит время службы такого устройства. Добавим следующую функцию в наш WiFi-пример: ```cpp ... void http_demo(NetworkInterface *net) { // Open a socket on the network interface, and create a TCP connection to mbed.org TCPSocket socket; socket.open(net); SocketAddress sa; net->gethostbyname("ifconfig.io", &sa); sa.set_port(80); socket.connect(sa); // Send a simple http request char sbuffer[] = "GET / HTTP/1.1\r\nHost: ifconfig.io\r\n\r\n"; int scount = socket.send(sbuffer, sizeof sbuffer); printf("sent %d [%.*s]\n", scount, strstr(sbuffer, "\r\n") - sbuffer, sbuffer); // Recieve a simple http response and print out the response line char rbuffer[64]; int rcount = socket.recv(rbuffer, sizeof rbuffer); printf("recv %d [%.*s]\n", rcount, strstr(rbuffer, "\r\n") - rbuffer, rbuffer); // Close the socket to return its memory and bring down the network interface socket.close(); } ... ``` Вызвать эту функцию нужно в `main` непосредственно перед закрытием соединения по WiFi: ```cpp ... http_demo(&wifi); wifi.disconnect(); ... ``` Вы видите, что в функции `http_demo` передается указатель на базовый класс `NetworkInterface`, то есть используется полиморфизм, о котором было сказано выше, и эта функция может работать с сетевым интерфейсом любого типа (не обязательно WiFi). После запуска примера увидим следующий результат (в программе посылается простой GET-запрос и на него приходит ответ): ![xterm_http_demo](/assets/images/iot-academy/xterm_http_demo.png) ## Приведение кода в порядок Как уже было отмечено ранее, явная привязка кода программы к используемым аппаратным интерфейсам – плохая идея. Код должен быть максимально переносимым, что позволит легко менять компоненты устройства. Удалим из нашей программы специфичекские для WiFi фрагменты кода, и в конечном итоге получим следующий текст программы: ```cpp #include "mbed.h" #include "TCPSocket.h" void http_demo(NetworkInterface *net) { // Open a socket on the network interface, and create a TCP connection to mbed.org TCPSocket socket; socket.open(net); SocketAddress sa; net->gethostbyname("ifconfig.io", &sa); sa.set_port(80); socket.connect(sa); // Send a simple http request char sbuffer[] = "GET / HTTP/1.1\r\nHost: ifconfig.io\r\n\r\n"; int scount = socket.send(sbuffer, sizeof sbuffer); printf("sent %d [%.*s]\n", scount, strstr(sbuffer, "\r\n") - sbuffer, sbuffer); // Recieve a simple http response and print out the response line char rbuffer[64]; int rcount = socket.recv(rbuffer, sizeof rbuffer); printf("recv %d [%.*s]\n", rcount, strstr(rbuffer, "\r\n") - rbuffer, rbuffer); // Close the socket to return its memory and bring down the network interface socket.close(); } int main() { printf("Network example\r\n\r\n"); SocketAddress sa; NetworkInterface *net = NetworkInterface::get_default_instance(); if (!net) { printf("Error! No network inteface found.\r\n"); return 0; } printf("\r\nConnecting...\r\n"); int ret = net->connect(); if (ret == NSAPI_ERROR_IS_CONNECTED) { printf("Already connected!\n\r"); } else if (ret != NSAPI_ERROR_OK) { printf("\r\nConnection error: %d\r\n", ret); return -1; } printf("Success\r\n\r\n"); printf("MAC: %s\r\n", net->get_mac_address()); net->get_ip_address(&sa); printf("IP: %s\r\n", sa.get_ip_address()); net->get_netmask(&sa); printf("Netmask: %s\r\n", sa.get_ip_address()); net->get_gateway(&sa); printf("Gateway: %s\r\n", sa.get_ip_address()); http_demo(net); net->disconnect(); printf("\r\nDone\r\n"); } ``` Файл конфигурации приложения тоже изменим, чтобы добиться максимальной независимости кода от аппаратного обесепечения: ```json { "target_overrides": { "*": { "target.network-default-interface-type": "WIFI", "nsapi.default-wifi-security": "WPA_WPA2", "nsapi.default-wifi-ssid": "\"ssid\"", "nsapi.default-wifi-password": "\"password\"", "esp8266.provide-default" : true, "esp8266.tx" : "PA_2", "esp8266.rx" : "PA_3", "esp8266.built-in-dns": true, "esp8266.socket-bufsize": "1024", "esp8266.debug": false, "platform.stdio-convert-newlines": true, "platform.stdio-baud-rate": 115200, "platform.default-serial-baud-rate": 115200, "target.printf_lib" : "std" } } } ``` <div class="danger"> Похоже это баг PlatformIO: Нужно полностью перекомпилировать весь проект после таких глобальных изменений! Да, это долго, но по-другому – никак. Сначала на панели задач выбираем **Clean**, а затем **Build**. </div> <div class="danger"> Не забываем указывать свои параметры подключения к WiFi-сети! </div>