ESP32 WiFi — различия между версиями

Материал из razgovorov.ru
Перейти к: навигация, поиск
м (added Category:ESP32 using HotCat)
Строка 46: Строка 46:
  
 
Рекомендуется провести инициализацию следующим образом:
 
Рекомендуется провести инициализацию следующим образом:
<source lang="c">
+
<source lang=c>
 
wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
 
wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
 
esp_wifi_init(&config);
 
esp_wifi_init(&config);
Строка 105: Строка 105:
 
work is performed in the event handler. When we detect a scan completion event, we
 
work is performed in the event handler. When we detect a scan completion event, we
 
retrieve the located access points and log their details.
 
retrieve the located access points and log their details.
<source lang="c">
+
<source lang=c>
 
#include "esp_wifi.h"
 
#include "esp_wifi.h"
 
#include "esp_system.h"
 
#include "esp_system.h"
Строка 177: Строка 177:
 
Using the Arduino libraries we can also make network scans. Here is an example:
 
Using the Arduino libraries we can also make network scans. Here is an example:
  
<source lang="c">
+
<source lang=c>
 
int8_t count = WiFi.scanNetworks();
 
int8_t count = WiFi.scanNetworks();
 
printf("Found %d networks\n", count);
 
printf("Found %d networks\n", count);
Строка 203: Строка 203:
 
Пример функции обратного вызова:
 
Пример функции обратного вызова:
  
<source lang="c">
+
<source lang=c>
 
esp_err_t eventHandler(void *ctx, system_event_t *event) {
 
esp_err_t eventHandler(void *ctx, system_event_t *event) {
 
// здесь код обработчика события ...
 
// здесь код обработчика события ...
Строка 217: Строка 217:
  
 
Пример регистрации функции обратного вызова:
 
Пример регистрации функции обратного вызова:
<source lang="c">
+
<source lang=c>
 
esp_event_loop_init(eventHandler, NULL);
 
esp_event_loop_init(eventHandler, NULL);
 
</source>
 
</source>
  
 
Если в последствии предполагается изменение обработчика обратного вызова можно использовать:
 
Если в последствии предполагается изменение обработчика обратного вызова можно использовать:
<source lang="c">
+
<source lang=c>
 
esp_event_loop_set_cb(eventHandler, NULL);
 
esp_event_loop_set_cb(eventHandler, NULL);
 
</source>
 
</source>
Строка 413: Строка 413:
  
 
Пример инициализации структуры wifi_config_t:
 
Пример инициализации структуры wifi_config_t:
<source lang="c">
+
<source lang=c>
 
wifi_config_t staConfig = {
 
wifi_config_t staConfig = {
 
   .sta = {
 
   .sta = {
Строка 423: Строка 423:
 
</source>
 
</source>
 
После инициализации структуры передаем её в ESP32:
 
После инициализации структуры передаем её в ESP32:
<source lang="c">
+
<source lang=c>
 
esp_wifi_set_config(WIFI_IF_STA, (wifi_config_t *)&staConfig);
 
esp_wifi_set_config(WIFI_IF_STA, (wifi_config_t *)&staConfig);
 
</source>
 
</source>
 
Предварительно установив режим WiFi с помощью метода esp_wifi_set_mode():
 
Предварительно установив режим WiFi с помощью метода esp_wifi_set_mode():
<source lang="c">
+
<source lang=c>
 
esp_wifi_set_mode(WIFI_MODE_STA)
 
esp_wifi_set_mode(WIFI_MODE_STA)
 
</source>
 
</source>
 
или
 
или
<source lang="c">
+
<source lang=c>
 
esp_wifi_set_mode(WIFI_MODE_APSTA)
 
esp_wifi_set_mode(WIFI_MODE_APSTA)
 
</source>
 
</source>
Строка 452: Строка 452:
 
Ниже приведен полный пример, иллюстрирующий все шаги, необходимые для подключения к точке доступа с информированнием, когда мы готовы к работе:
 
Ниже приведен полный пример, иллюстрирующий все шаги, необходимые для подключения к точке доступа с информированнием, когда мы готовы к работе:
  
<source lang="c">
+
<source lang=c>
 
#include "freertos/FreeRTOS.h"
 
#include "freertos/FreeRTOS.h"
 
#include "esp_wifi.h"
 
#include "esp_wifi.h"
Строка 497: Строка 497:
 
Если мы укажем IP адрес, то необходмо также указать информацию о DNS, если нам нужно подключиться к DNS-серверам.
 
Если мы укажем IP адрес, то необходмо также указать информацию о DNS, если нам нужно подключиться к DNS-серверам.
 
Пример, который выделяет нам определенный IP-адрес:
 
Пример, который выделяет нам определенный IP-адрес:
<source lang="c">
+
<source lang=c>
 
#include <lwip/sockets.h>
 
#include <lwip/sockets.h>
 
// The IP address that we want our device to have.
 
// The IP address that we want our device to have.
Строка 550: Строка 550:
 
Чтобы быть точкой доступа, нам нужно определить SSID, который позволяет другим устройствам увидеть нашу сеть. Этот SSID может быть помечен как скрытый, если мы не хотим чтобы его можно найти при сканировании. Кроме того, мы также должны указать режим аутентификации который будет использоваться, когда клиент захочет соединиться с нами.
 
Чтобы быть точкой доступа, нам нужно определить SSID, который позволяет другим устройствам увидеть нашу сеть. Этот SSID может быть помечен как скрытый, если мы не хотим чтобы его можно найти при сканировании. Кроме того, мы также должны указать режим аутентификации который будет использоваться, когда клиент захочет соединиться с нами.
 
Первой этапом для начала работы ESP32 в режиме точки доступа является установка соответствующего режима командой esp_wifi_set_mode(). Для этого необходио передать в качестве параметра код режима точки доступа или совмещенного режима точка доступа и клиента. Это будет либо:
 
Первой этапом для начала работы ESP32 в режиме точки доступа является установка соответствующего режима командой esp_wifi_set_mode(). Для этого необходио передать в качестве параметра код режима точки доступа или совмещенного режима точка доступа и клиента. Это будет либо:
<source lang="c">
+
<source lang=c>
 
esp_wifi_set_mode (WIFI_MODE_AP);
 
esp_wifi_set_mode (WIFI_MODE_AP);
 
</source>
 
</source>
 
или
 
или
<source lang="c">
+
<source lang=c>
 
esp_wifi_set_mode (WIFI_MODE_APSTA);
 
esp_wifi_set_mode (WIFI_MODE_APSTA);
 
</source>
 
</source>
Строка 573: Строка 573:
 
* beacon_interval – Unknown. 100.
 
* beacon_interval – Unknown. 100.
 
Пример структуры инициализации:
 
Пример структуры инициализации:
<source lang="c">
+
<source lang=c>
 
wifi_config_t apConfig = {
 
wifi_config_t apConfig = {
 
   .ap = {
 
   .ap = {

Версия 21:22, 28 июня 2017

WiFi Theory

When working with a WiFi oriented device, it is important that we have at least some understanding of the concepts related to WiFi. At a high level, WiFi is the ability to participate in TCP/IP connections over a wireless communication link. WiFi is specifically the set of protocols described in the IEEE 802.11 Wireless LAN architecture.

Within this story, a device called a Wireless Access Point (access point or AP) acts as the hub of all communications. Typically it is connected to (or acts as) as TCP/IP router to the rest of the TCP/IP network. For example, in your home, you are likely to have a WiFi access point connected to your modem (cable or DSL). WiFi connections are then formed to the access point (through devices called stations) and TCP/IP traffic flows through the access point to the Internet.

The devices that connect to the access points are called "stations":

An ESP32 device can play the role of an Access Point, a Station or both at the same time.

Very commonly, the access point also has a network connection to the Internet and acts as a bridge between the wireless network and the broader TCP/IP network that is the Internet.

A collection of stations that wish to communicate with each other is termed a Basic Service Set (BSS). The common configuration is what is known as an Infrastructure BSS. In this mode, all communications inbound and outbound from an individual station are routed through the access point.

A station must associate itself with an access point in order to participate in the story. A station may only be associated with a single access point at any one time. Each participant in the network has a unique identifier called the MAC address. This is a 48bit value.

When we have multiple access points within wireless range, the station needs to know with which one to connect. Each access point has a network identifier called the BSSID (or more commonly just SSID). SSID is service set identifier. It is a 32 character value that represents the target of packets of information sent over the network.

See also: • Wikipedia – Wireless access point • Wikipedia – IEEE 802.11 • Wikipedia – WiFi Protected Access • Wikipedia – IEEE 802.11i-2004

Initializing the WiFi environment

WiFi является лишь частью возможностей ESP32. Может быть много случаев, когда WiFi не требуется. Инициализация WiFi выполняется разработчиком приложения путем вызова метода esp_wifi_init().

Рекомендуется провести инициализацию следующим образом:

wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&config);

See also: • esp_wifi_init

Setting the operation mode

The ESP32 can either be a station in the network, an access point for other devices or both. Remember, when an ESP32 is being a station, it can connect to a remote access point (your WiFi hub) while when being an access point, other WiFi stations can connect to the ESP32 (think of the ESP32 as becoming a WiFi hub). This is a fundamental consideration and we will want to choose how the device behaves early on in our application design. Once we have chosen what we want, we set a global mode property which indicates which of the operational modes our device will perform (station, access point or station AND access point).

This choice is set with a call to esp_wifi_set_mode(). The parameter is an instance of wifi_mode_t which can have a value of WIFI_MODE_NULL, WIFI_MODE_STA, WIFI_MODE_AP or WIFI_MODE_APSTA. We can call esp_wifi_get_mode() to retrieve our current mode state.

Scanning for access points

If the ESP32 is going to be performing the role of a station we will need to connect to an access point. We can request a list of the available access points against which we can attempt to connect. We do this using the esp_wifi_scan_start() function.

The results of a WiFi scan are stored internally in ESP32 dynamically allocated storage.

The data is returned to us when we call esp_wifi_scan_get_ap_records() which also releases the internally allocated storage. As such, this should be considered a destructive read.

A scan record is contained in an instance of a wifi_ap_record_t structure that contains: uint8_t bssid[6] uint8_t ssid[32] uint8_t primary wifi_second_chan_t second int8_t rssi wifi_auth_mode_t authmode

The wifi_auth_mode_t is one of: • WIFI_AUTH_OPEN – No security. • WIFI_AUTH_WEP – WEP security. • WIFI_AUTH_WPA_PSK – WPA security. • WIFI_AUTH_WPA2_PSK – WPA2 security. • WIFI_AUTH_WPA_WPA2_PSK – WPA or WPA2 security.

After issuing the request to start performing a scan, we will be informed that the scan completed when a SYSTEM_EVENT_SCAN_DONE event is published. The event data contains the number of access points found but that can also be retrieved with a call to esp_wifi_scan_get_ap_num().

Should we wish to cancel the scanning before it completes on its own, we can call esp_wifi_scan_stop().

Here is a complete sample application illustrating performing a WiFi scan. Much of the work is performed in the event handler. When we detect a scan completion event, we retrieve the located access points and log their details.

#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"
esp_err_t event_handler(void *ctx, system_event_t *event)
{
if (event->event_id == SYSTEM_EVENT_SCAN_DONE) {
printf("Number of access points found: %d\n",
event->event_info.scan_done.number);
uint16_t apCount = event->event_info.scan_done.number;
if (apCount == 0) {
return ESP_OK;
}
wifi_ap_record_t *list =
(wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * apCount);
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&apCount, list));
int i;
for (i=0; i<apCount; i++) {
char *authmode;
switch(list[i].authmode) {
case WIFI_AUTH_OPEN:
authmode = "WIFI_AUTH_OPEN";
break;
case WIFI_AUTH_WEP:
authmode = "WIFI_AUTH_WEP";
break;
case WIFI_AUTH_WPA_PSK:
authmode = "WIFI_AUTH_WPA_PSK";
break;
case WIFI_AUTH_WPA2_PSK:
authmode = "WIFI_AUTH_WPA2_PSK";
break;
case WIFI_AUTH_WPA_WPA2_PSK:
authmode = "WIFI_AUTH_WPA_WPA2_PSK";
break;
default:
authmode = "Unknown";
break;
}
printf("ssid=%s, rssi=%d, authmode=%s\n",
list[i].ssid, list[i].rssi, authmode);
}
free(list);
}
return ESP_OK;
}
int app_main(void)
{
nvs_flash_init();
tcpip_adapter_init();
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// Let us test a WiFi scan ...
wifi_scan_config_t scanConf = {
.ssid = NULL,
.bssid = NULL,
.channel = 0,
.show_hidden = 1
};
ESP_ERROR_CHECK(esp_wifi_scan_start(&scanConf, 0));
return 0;
}

Using the Arduino libraries we can also make network scans. Here is an example:

int8_t count = WiFi.scanNetworks();
printf("Found %d networks\n", count);
for (uint8_t i=0; i<count; i++) {
String ssid;
uint8_t encryptionType;
int32_t RSSI;
uint8_t *BSSID;
int32_t channel;
WiFi.getNetworkInfo(i, ssid, encryptionType, RSSI, BSSID, channel);
printf("ssid=%s\n", ssid.c_str());
}

See also: • Handling WiFi events • esp_wifi_scan_start • esp_wifi_scan_stop • esp_wifi_scan_get_ap_records • esp_wifi_scan_get_ap_num

Обработчики событий WiFi

Во время работы в качестве устройства WiFi, могут возникнуть события представляющие интерес для приложения. Поскольку мы не знаем когда произойдет то или иное событие, мы не можем заблокировать приложение в их ожидании. Вместо этого мы должны определить функцию обратного вызова, которая будет вызвана если событие произойдет. Функция esp_event_loop_init() регистрирует функцию обратного вызова.

Пример функции обратного вызова:

esp_err_t eventHandler(void *ctx, system_event_t *event) {
// здесь код обработчика события ...
return ESP_OK;
}

Как правило мы должны будем включить следующие инклюды:

  • #include <esp_event.h>
  • #include <esp_event_loop.h>
  • #include <esp_wifi.h>
  • #include <esp_err.h>

Пример регистрации функции обратного вызова:

esp_event_loop_init(eventHandler, NULL);

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

esp_event_loop_set_cb(eventHandler, NULL);

В функцию обратного вызова передается параметр с деталями события. Тип данных этого параметра - «system_event_t», который содержит: System_event_id_t event_id и System_event_info_t event_info

Мы должны включить «esp_event.h», чтобы получить доступ к этим данным.

Теперь мы рассмотрим два свойства, переданные обработчику событий в system_event_t - этими свойствами являются «event_id» и «event_info». Event_id описывает, какое событие было обнаружено, а event_info содержит детали события, основанные на типе, указанном в event_id.

  • event_id – перечисление содержащее тип события, принимает следующие значения:
    • SYSTEM_EVENT_WIFI_READY – ESP32 WiFi is ready.
    • SYSTEM_EVENT_SCAN_DONE – Сканирование точек доступа завершено, список доступен.
    • SYSTEM_EVENT_STA_START – Started being a station.
    • SYSTEM_EVENT_STA_STOP – Stopped being a station.
    • SYSTEM_EVENT_STA_CONNECTED – Connected to an access point as a station. The connected data field is valid to be accessed.
    • SYSTEM_EVENT_STA_DISCONNECTED – Disconnected from access point while being a station. The disconnected data field is valid to be accessed.
    • SYSTEM_EVENT_STA_AUTHMODE_CHANGE – Authentication mode has changed. The auth_change data field is valid to be accessed.
    • SYSTEM_EVENT_STA_GOT_IP – Got an assigned IP address from the access point that we connected to while being a station. The got_ip data field is valid to be accessed.
    • SYSTEM_EVENT_AP_START – Started being an access point.
    • SYSTEM_EVENT_AP_STOP – Stopped being an access point.
    • SYSTEM_EVENT_AP_STACONNECTED – A station connected to us while we are being an access point. The sta_connected data field is valid to be accessed.
    • SYSTEM_EVENT_AP_STADISCONNECTED – A station disconnected from us while we are being an access point. The sta_disconnected data field is valid to be accessed.
    • SYSTEM_EVENT_AP_PROBEREQRECVED – Received a probe request while we are being an access point. The ap_probereqrecved data field is valid to be accessed.
  • event_info – This is a C language union of distinct data types that are keyed off the event_id. The different structures contained within are:
Structure Field Event
system_event_sta_connected_t connected SYSTEM_EVENT_STA_CONNECTED
system_event_sta_disconnected_t disconnected SYSTEM_EVENT_STA_DISCONNECTED
system_event_sta_scan_done_t scan_done SYSTEM_EVENT_SCAN_DONE
system_event_sta_authmode_change_t auth_change SYSTEM_EVENT_STA_AUTHMODE_CHANGE
system_event_sta_got_ip_t got_ip SYSTEM_EVENT_STA_GOT_IP
system_event_ap_staconnected_t sta_connected SYSTEM_EVENT_AP_STACONNECTED
system_event_ao_stadisconnected_t sta_disconnected SYSTEM_EVENT_AP_STADISCONNECTED
system_event_ap_probe_req_rx_t ap_probereqrecved SYSTEM_EVENT_AP_PROBEREQRECVED

These data structures contain information pertinent to the event type received.

system_event_sta_connected_t

This data type is associated with the SYSTEM_EVENT_STA_CONNECT event.

  • uint8_t ssid[32]
  • uint8_t ssid_len
  • uint8_t bssid[6]
  • uint8_t channel
  • wifi_auth_mode_t authmode

The ssid is the WiFi network name to which we connected. The ssid_len is the number of bytes in the ssid field that contain the name. The bssid is the MAC address of the access point. The channel is the wireless channel used for the connection. The authmode is the security authentication mode used during the connection.

system_event_sta_disconnected_t

This data type is associated with the SYSTEM_EVENT_STA_DISCONNECTED event.

  • uint8_t ssid[32]
  • uint8_t ssid_len
  • uint8_t bssid[6]
  • uint8_t reason

The reason code is an indication of why we disconnected. Symbolics are defined for each of the numeric reason codes to allow us to write more elegant and comprehensible applications should we need to consider a reason code.:

  • WIFI_REASON_UNSPECIFIED – 1
  • WIFI_REASON_AUTH_EXPIRE – 2
  • WIFI_REASON_AUTH_LEAVE – 3
  • WIFI_REASON_ASSOC_EXPIRE – 4
  • WIFI_REASON_ASSOC_TOOMANY – 5
  • WIFI_REASON_NOT_AUTHED – 6
  • WIFI_REASON_NOT_ASSOCED – 7
  • WIFI_REASON_ASSOC_LEAVE – 8
  • WIFI_REASON_ASSOC_NOT_AUTHED – 9
  • WIFI_REASON_DISASSOC_PWRCAP_BAD – 10
  • WIFI_REASON_DISASSOC_SUPCHAN_BAD – 11
  • WIFI_REASON_IE_INVALID – 13
  • WIFI_REASON_MIC_FAILURE – 14
  • WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT – 15
  • WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT – 16
  • WIFI_REASON_IE_IN_4WAY_DIFFERS – 17
  • WIFI_REASON_GROUP_CIPHER_INVALID – 18
  • WIFI_REASON_PAIRWISE_CIPHER_INVALID – 19
  • WIFI_REASON_AKMP_INVALID – 20
  • WIFI_REASON_UNSUPP_RSN_IE_VERSION – 21
  • WIFI_REASON_INVALID_RSN_IE_CAP – 22
  • WIFI_REASON_802_1X_AUTH_FAILED – 23
  • WIFI_REASON_CIPHER_SUITE_REJECTED – 24
  • WIFI_REASON_BEACON_TIMEOUT – 200
  • WIFI_REASON_NO_AP_FOUND – 201
  • WIFI_REASON_AUTH_FAIL – 202
  • WIFI_REASON_ASSOC_FAIL – 203
  • WIFI_REASON_HANDSHAKE_TIMEOUT – 204

system_event_sta_scan_done_t

This data type is associated with the SYSTEM_EVENT_SCAN_DONE event.

  • uint32_t status
  • uint8_t number
  • uint8_t scan_id

See also:

  • Scanning for access points
  • esp_wifi_scan_get_ap_records

system_event_authmode_change_t

This data type is associated with the SYSTEM_EVENT_STA_AUTHMODE_CHANGE event.

  • wifi_auth_mode_t old_mode
  • wifi_auth_mode_t new_mode

system_event_sta_got_ip_t

This data type is associated with the SYSTEM_EVENT_STA_GOT_IP event.

  • tcpip_adapter_ip_info_t ip_info

The ip_info element is an instance of a tcpip_adapter_ip_info_t which contains three fields:

  • ip -The IP address.
  • netmask – The network mask.
  • gw – The gateway for communications.

All three of these fields are of ip4_addr_t which is a 32bit representation of an IP address. During development, you might want to consider logging the IP address of the device. You can do this using: ESP_LOGD(tag, "Got an IP: " IPSTR, IP2STR(&event->event_info.got_ip.ip_info.ip));

system_event_ap_staconnected_t

This data type is associated with the SYSTEM_EVENT_AP_STACONNECTED event.

  • uint8_t mac[6]
  • uint8_t aid

system_event_ap_stadisconnected_t

This data type is associated with the SYSTEM_EVENT_AP_STADISCCONNECTED event.

  • uint8_t mac[6]
  • uint8_t aid

system_event_ap_probe_req_rx_t

This data type is associated with the SYSTEM_EVENT_AP_PROBREQRECVED event.

  • int rssi
  • uint8_t mac[6]

If we enable the correct logging levels, we can see the events arrive and their content. For example:

D (2168) event: SYSTEM_EVENT_STA_CONNECTED, ssid:RASPI3, ssid_len:6, bssid:00:00:13:80:3d:bd, channel:6, authmode:3

V (2168) event: enter default callback

V (2174) event: exit default callback

and

D (9036) event: SYSTEM_EVENT_STA_GOTIP, ip:192.168.5.62, mask:255.255.255.0, gw:192.168.5.1

V (9036) event: enter default callback

I (9037) event: ip: 192.168.5.62, mask: 255.255.255.0, gw: 192.168.5.1

V (9043) event: exit default callback

Настройки WiFi клиента

ESP32 в режиме WiFi клиента может быть подключена только к одной точке доступа. Парамерты точки доступа к которой мы хотим подключиться задаются в структуре wifi_sta_config_t. wifi_sta_config_t состоит из:

  • char ssid[32] - ssid точки доступа к которой хотим подключиться
  • char password[64] - пароль к точке доступа
  • bool bssid_set
  • uint8_t bssid[6]

Пример инициализации структуры wifi_config_t:

wifi_config_t staConfig = {
   .sta = {
      .ssid="<access point name>",
      .password="<password>",
      .bssid_set=false
   }
};

После инициализации структуры передаем её в ESP32:

esp_wifi_set_config(WIFI_IF_STA, (wifi_config_t *)&staConfig);

Предварительно установив режим WiFi с помощью метода esp_wifi_set_mode():

esp_wifi_set_mode(WIFI_MODE_STA)

или

esp_wifi_set_mode(WIFI_MODE_APSTA)

See also: • esp_wifi_set_mode • esp_wifi_set_config

Запуск модуля WiFi

Поскольку Wi-Fi утверждает, что он должен пройти, может быть задан вопрос: «Когда Wi-Fi готов к использованию?». Если мы предположим, что ESP32 загружается с холода, есть вероятность, что мы хотим сказать, что это клиент или точка доступа, и задать её параметры. Учитывая, что это последовательность шагов, мы фактически не хотим, чтобы ESP32 выполнял эти задачи до тех пор, пока мы не выполнили всю нашу настройку. Например, если мы загружаем ESP32 и запрашиваем его как точку доступа, если он сразу стал точкой доступа, он может еще не знать параметров точки доступа, которая должна быть или, что еще хуже, может временно проявляться как неправильная точка доступа. Таким образом, есть окончательная команда, которую мы должны изучить, которая является инструкцией для подсистемы WiFi, чтобы начать работать. Это команда esp_wifi_start (). Все команды которые мы делаем до неё, это настройка среды. Только при вызове esp_wifi_start () подсистема WiFi начинает выполнять какую-либо реальную работу от нашего имени. Если наш режим - это точка доступа, вызов этой функции включает точку доступа. Если наш режим является режимом клиента, начинается подключение к точке доступа. Существует соответствующая команда, называемая esp_wifi_stop (), которая останавливает подсистему WiFi.

Подключение к точке доступа в режиме клиента

После передачи параметров подключения к WiFi в режиме клиента, которая включает SSID и пароль, мы готовы выполнить подключение к точке доступа. Функция esp_wifi_connect() установит соединение. Процедура установки соединения происходит не мнгновенно. Спустя некоторое время мы фактически подключимся - произойдет два события. Первое - SYSTEM_EVENT_STA_CONNECTED, указывающий, что у нас есть подключение к точке доступа. Второе событие - SYSTEM_EVENT_STA_GOT_IP, которое Указывает, что DHCP-сервером был назначен IP-адрес. Только после этих событий мы можем передавать / принимать данные. Если мы используем статический IP-адреса для нашего устройства, то мы увидим только связанное событие. Если мы отключимся от точки доступа, мы увидим событие SYSTEM_EVENT_STA_DISCONNECTED. Для отключения от ранее подключенной точки доступа, нужно вызвать esp_wifi_disconnect().

Существует еще один вариант подключения к точкам доступа - автоматическое подключение. Существует булев флаг, который хранится во флеше, который указывает, следует ли ESP32 пытаться автоматически подключиться к последней используемой точке доступа. Если установлено значение true, то после запуска устройства и без кодирования любых вызовов API, ESP32 попытается подключиться к последней используемой точке доступа. Этот вариант я предпочитаю не использовать, так как хочу сам контролировать работу устройства. Мы можем включить или отключить функцию автоматического подключения, выполнив вызов esp_wifi_set_auto_connect (). Ниже приведен полный пример, иллюстрирующий все шаги, необходимые для подключения к точке доступа с информированнием, когда мы готовы к работе:

#include "freertos/FreeRTOS.h"
#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"
#include "tcpip_adapter.h"
esp_err_t event_handler(void *ctx, system_event_t *event)
{
   if (event->event_id == SYSTEM_EVENT_STA_GOT_IP) {
      printf("Our IP address is " IPSTR "\n",
      IP2STR(&event->event_info.got_ip.ip_info.ip));
      printf("We have now connected to a station and can do things...\n")
   }
   return ESP_OK;
}
int app_main(void)
{
   nvs_flash_init();
   tcpip_adapter_init();
   ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) );
   wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
   ESP_ERROR_CHECK(esp_wifi_init(&cfg) );
   ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
   ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA));
   wifi_config_t sta_config = {
      .sta = {
         .ssid = "RASPI3",
         .password = "password",
         .bssid_set = 0
      }
   };   
   ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
   ESP_ERROR_CHECK(esp_wifi_start());
   ESP_ERROR_CHECK(esp_wifi_connect());
   return 0;
}

Когда мы подключаемся к точке доступа, наше устройство является клиентом. Подключение к точке доступа автоматически не означает, что теперь у нас есть IP-адрес. Мы все еще должны запросить выделенный IP-адрес с сервера DHCP. Это может занять несколько секунд. В некоторых случаях мы можем запрашиватьопределенный IP-адрес адрес. Это приводит к значительно более быстрому времени соединения. Если мы укажем IP адрес, то необходмо также указать информацию о DNS, если нам нужно подключиться к DNS-серверам. Пример, который выделяет нам определенный IP-адрес:

#include <lwip/sockets.h>
// The IP address that we want our device to have.
#define DEVICE_IP "192.168.1.99"
// The Gateway address where we wish to send packets.
// This will commonly be our access point.
#define DEVICE_GW "192.168.1.1"
// The netmask specification.
#define DEVICE_NETMASK "255.255.255.0"
// The identity of the access point to which we wish to connect.
#define AP_TARGET_SSID "RASPI3"
// The password we need to supply to the access point for authorization.
#define AP_TARGET_PASSWORD "password"
esp_err_t wifiEventHandler(void *ctx, system_event_t *event)
{
   return ESP_OK;
}
// Пример кода здесь ...

nvs_flash_init();
tcpip_adapter_init();
tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); // Don't run a DHCP client
tcpip_adapter_ip_info_t ipInfo;
inet_pton(AF_INET, DEVICE_IP, &ipInfo.ip);
inet_pton(AF_INET, DEVICE_GW, &ipInfo.gw);
inet_pton(AF_INET, DEVICE_NETMASK, &ipInfo.netmask);
tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);
ESP_ERROR_CHECK(esp_event_loop_init(wifiEventHandler, NULL));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg) );
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
wifi_config_t sta_config = {
   .sta = {
      .ssid = AP_TARGET_SSID,
      .password = AP_TARGET_PASSWORD,
      .bssid_set = 0
   }
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_connect());

See also: • Handling WiFi events • esp_wifi_connect • esp_wifi_disconnect

Запуск в режиме точки доступа

До сих пор мы рассматривали ESP32 как клиента ​​WiFi для существующей точки доступа, но он также имеет возможность быть точкой доступа для других WiFi-устройств(клиентов), включая другие ESP32. Чтобы быть точкой доступа, нам нужно определить SSID, который позволяет другим устройствам увидеть нашу сеть. Этот SSID может быть помечен как скрытый, если мы не хотим чтобы его можно найти при сканировании. Кроме того, мы также должны указать режим аутентификации который будет использоваться, когда клиент захочет соединиться с нами. Первой этапом для начала работы ESP32 в режиме точки доступа является установка соответствующего режима командой esp_wifi_set_mode(). Для этого необходио передать в качестве параметра код режима точки доступа или совмещенного режима точка доступа и клиента. Это будет либо:

esp_wifi_set_mode (WIFI_MODE_AP);

или

esp_wifi_set_mode (WIFI_MODE_APSTA);

Затем нам нужно предоставить информацию о конфигурации. Мы делаем это, заполняя экземпляр wifi_ap_config_t. wifi_ap_config_t содержит:

  • ssid – The WiFi ssid name upon which we will listen for connecting stations.
  • ssid_len – The length in bytes of the ssid if not NULL terminated.
  • password – The password used for station authentication.
  • channel – The channel we will use for networking.
  • authmode – How we wish stations to authenticate (if at all). The choices are
    • open
    • wep
    • wpa
    • wpa2
    • wpa_wpa2
  • ssid_hidden – Should we broadcast our ssid.
  • max_connection – The number of concurrent stations. The default and maximum

is 4.

  • beacon_interval – Unknown. 100.

Пример структуры инициализации:

wifi_config_t apConfig = {
   .ap = {
      .ssid="<access point name>",
      .ssid_len=0,
      .password="<password>",
      .channel=0,
      .authmode=WIFI_AUTH_OPEN,
      .ssid_hidden=0,
      .max_connection=4,
      .beacon_interval=100
    }
};

With the structure populated, we call esp_wifi_set_config() … for example: esp_wifi_set_config(WIFI_IF_AP, &apConfig); Finally, we call esp_wifi_start(). Here is a snippet of code that can be used to setup and ESP32 as an access point: When we become an access point, an ESP32 WiFi event is produced of type SYSTEM_EVENT_AP_START. Note that there is no payload data associated with this event. Once the ESP32 starts listening for station connects by being an access point, we are going to want to validate that this works. You can use any device or system to scan and connect. Personally, I use a Raspberry PI 3 for testing as it provides a nice Linux environment and has a WiFi adapter build in. You can also choose to plug in a separate WiFi dongle into one of the extra USB ports. One of the first tools we want to run is called "iwlist" which will perform a scan for us: $ sudo iwlist wlan1 scan In the results, we can look for our ESP32 … for example: Cell 02 - Address: 18:FE:34:6A:94:EF ESSID:"ESP32" Protocol:IEEE 802.11bgn Mode:Master Frequency:2.412 GHz (Channel 1) Encryption key:off Bit Rates:150 Mb/s Quality=100/100 Signal level=100/100 One of the other tools available on that environment is called "wpa_cli" which provides a wealth of options for testing WiFi. The recipe I use is to connect to an access point from the command line is: $ sudo wpa_cli add_network set_network <num> ssid "<SSID>" set_network <num> key_mgmt NONE enable_network <num> status You may have to run select_network <num> reconnect or reasociate to connect to the target and you can run disconnect to disconnect from the access point. ifname – show current interface interface <name> - select current interface To perform a scan run the command "scan". When complete, run "scan_results" to see the list. When a station connects, the ESP32 will raise the SYSTEM_EVENT_AP_STACONNECTED event. When a station disconnects, we will see the SYSTEM_EVENT_AP_DISCONNECTED event. See also: • man(8) – wpa_cl i When a remote station connects to the ESP32 as an access point, we will see a debug message written to UART1 that may look similar to: station: f0:25:b7:ff:12:c5 join, AID = 1 This contains the MAC address of the new station joining the network. When the station disconnects, we will see a corresponding debug log message that may be: station: f0:25:b7:ff:12:c5 leave, AID = 1 From within the ESP32, we can determine how many stations are currently connected with a call to wifi_softap_get_station_num(). If we wish to find the details of those stations, we can call wifi_softap_get_station_info() which will return a linked list of wifi_sta_list_t. We have to explicitly release the storage allocated by this call with an invocation of wifi_softap_free_station_info(). Here is an example of a snippet of code that lists the details of the connected stations: uint8 stationCount = wifi_softap_get_station_num(); os_printf("stationCount = %d\n", stationCount); wifi_sta_list_t *stationInfo = wifi_softap_get_station_info(); if (stationInfo != NULL) { while (stationInfo != NULL) { os_printf("Station IP: %d.%d.%d.%d\n", IP2STR(&(stationInfo->ip))); stationInfo = STAILQ_NEXT(stationInfo, next); } wifi_softap_free_station_info(); } When an ESP32 acts as an access point, this allows other devices to connect to it and form a WiFi connection. However, it appears that two devices connected to the same ESP32 acting as an access point can not directly communicate between each other. For example, imagine two devices connecting to an ESP32 as an access point. They may be allocated the IP addresses 192.168.4.2 and 192.168.4.3. We might imagine that 192.168.4.2 could ping 192.168.4.3 and visa versa but that is not allowed. It appears that they only direct network connection permitted is between the newly connected stations and the access point (the ESP32) itself. This seems to limit the applicability of the ESP32 as an access point. The primary intent of the ESP32 as an access point is to allow mobile devices (eg. your phone) to connect to the ESP32 and have a conversation with an application that runs upon it. See also: • esp_wifi_set_config • esp_wifi_set_mode

Working with connected stations

When our ESP32 is being an access point, we are saying that we wish to allow stations to connect to it. This brings in the story of managing those stations. Common things we might want to do are: • Determine when a new station connects • Determine when a previously connected station leaves • List the currently connected stations • Disconnect one or more currently connected stations We can register an event handler for detecting new station connects and existing station disconnects. The event handler will receive SYSTEM_EVENT_AP_STACONNECTED when a station connects and SYSTEM_EVENT_AP_STADISCONNECTED what a station leaves. We can get the list of currently connected stations using the esp_wifi_get_station_list() function. This returns a linked list of stations. The storage for this list is allocated for us and we should indicate that we are no longer in need of it by calling esp_wifi_free_station_list() when done. If for some reason the logic in our environment wants to forcibly disconnect a currently connected station, we can use the esp_wifi_kick_station() call. See also: • Handling WiFi events • esp_wifi_free_station_list • esp_wifi_get_station_list • esp_wifi_kick_station

WiFi at boot time

ESP32 может хранить информацию о запуске WiFi во флэш-памяти. Это позволяет выполнять свои функции при запуске без необходимости запрашивать у пользователя какую либо информацию. Эта возможность контролируется функциями esp_wifi_set_auto_connect () и esp_wifi_get_auto_connect().

Значениями параметров, используемых для автоматического подключения, являются значения, сохраненные во флэш-памяти при помощи функции esp_wifi_set_config().

See also: • esp_wifi_set_auto_connect • esp_wifi_get_auto_connect • esp_wifi_set_storage

The DHCP client

When the ESP32 connects to an access point as a station, it also runs a DHCP client to connect to the DHCP server that it assumes is also available at the access point. From there, the station is supplied its IP address, gateway address and netmask. There are times however when we want to supply our own values for this data. We can do this by calling tcpip_adapter_set_ip_info() during setup. The recipe is as follows: tcpip_adapter_init(); tcpip_adapter_dhcpc_stop(); tcpip_adapter_set_ip_info(); esp_wifi_init(); esp_wifi_set_mode(); esp_wifi_set_config(); esp_wifi_start(); esp_wifi_config(); (Note that the parameters are omitted in the above). The setup for calling tcpip_adapter_set_ip_info() can be as follows: tcpip_adapter_ip_info_t ipInfo; IP4_ADDR(&ipInfo.ip, 192,168,1,99); IP4_ADDR(&ipInfo.gw, 192,168,1,1); IP4_ADDR(&ipInfo.netmask, 255,255,255,0); tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo); Alternative, using strings we have: tcpip_adapter_ip_info_t ipInfo; inet_pton(AF_INET, "192.168.1.99", &ipInfo.ip); inet_pton(AF_INET, "192.168.1.1", &ipInfo.gw); inet_pton(AF_INET, "255.255.255.0", &ipInfo.netmask); tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo); See also: • tcpip_adapter_set_ip_info • tcpip_adapter_dhcpc_start • tcpip_adapter_dhcpc_stop • tcpip_adapter_dhcpc_get_status • tcpip_adapter_dhcpc_option • inet_pton

The DHCP server

When the ESP32 is performing the role of an access point, it is likely that you will want it to also behave as a DHCP server so that connecting stations will be able to be automatically assigned IP addresses and learn their subnet masks and gateways. The DHCP server can be started and stopped within the device using the APIs called wifi_softap_dhcps_start() and wifi_softap_dhcps_stop(). The current status (started or stopped) of the DHCP server can be found with a call to wifi_softap_dhcps_status(). The default range of IP addresses offered by the DHCP server is 192.168.4.1 upwards. The first address becomes assigned to the ESP8266 itself. It is important to realize that this address range is not the same address range as your LAN where you may be working. The ESP8266 has formed its own network address space and even though they may appear with the same sorts of numbers (192.168.x.x) they are isolated and independent networks. If you start an access point on the ESP8266 and connect to it from your phone, don't be surprised when you try and ping it from your Internet connected PC and don't get a response. See also: • Error: Reference source not found

Current IP Address, netmask and gateway

Should we need it, we can query the environment for the current IP address, netmask and gateway. The values of these are commonly set for us by a DHCP server when we connect to an access point. The function called tcpip_adapter_get_ip_info() returns our current value. Since the ESP32 can have two IP interfaces (one for an access point and one for a station), we supply which interface we wish to retrieve. When we connect to an access point and have chosen to use DHCP, when we are allocated an IP address, an event is generated that can be used as an indication that we now have a valid IP address. See also: • Handling WiFi events • Error: Reference source not found • tcpip_adapter_get_ip_info

WiFi Protected Setup – WPS

The ESP8266 supports WiFi Protected Setup in station mode. This means that if the access point supports it, the ESP8266 can connect to the access point without presenting a password. Currently only the "push button mode" of connection is implemented. Using this mechanism, a physical button is pressed on the access point and, for a period of two minutes, any station in range can join the network using the WPS protocols. An example of use would be the access point WPS button being pressed and then the ESP8266 device calling wifi_wps_enable() and then wifi_wps_start(). The ESP8266 would then connect to the network. See also: • wifi_wps_enable • wifi_wps_start • wifi_set_wps_cb • Simple Questions: What is WPS (WiFi Protected Setup) • Wikipedia: WiFi Protected Setup

Алгоритм инициализации WiFi

Представьте, что мы создали проект с использованием ESP32, который хочет подключиться к сети. Чтобы это произошло, мы хотим, чтобы ESP32 подключался к существующей точке доступа. Это работает, потому что ESP32 может быть Wi-Fi-клиентом. Для того, чтобы ESP32 соединился с точкой доступа, он должен знать два важных элемента. Он должен знать, к какой сети присоединиться (SSID), и ему нужно будет знать пароль для подключения к этой сети, поскольку большинство сетей требуют аутентификации. И есть головоломка. Если ESP32 перенесен в физически новую среду, как он «узнает», к какой сети подключиться и какой пароль использовать? Мы должны предположить, что ESP32 не имеет прикрепленного к нему экрана. Если бы это было так, мы могли бы запросить у пользователя информацию. Одним из решений является то, что ESP32 первоначально «будет» точкой доступа. Если бы это была точка доступа, мы могли бы использовать наш телефон для связи с ним, спросить, какие WiFi-сети он видит, предоставить пароль для сети и разрешить ему подключаться.

Пока (не сделано) {
   если (мы знаем наш ssid и пароль) {
      попытаемся подключиться к точке доступа; 
      Если (нам удалось установить связь) {
         return; 
      }
   }
   Сами становятся точкой доступа; 
   Слушать входящие запросы браузера; 
   Дождаться ввода пары SSID / password; 
}

Нам также необходимо обработать случай, когда мы считаем, что у нас есть SSID и пароль, используемые для подключения к точке доступа, но либо те, которые были изменены, либо мы находимся в другом месте. В этом случае мы также должны вернуться к точке доступа и ждать новых инструкций. Мы можем использовать энергонезависимое хранилище для сохранения нашего SSID и пароля. Возможно, мы захотим сохранить не одну пару SSID / пароль, а, возможно, сохранить упорядоченный список. Таким образом, когда мы учим наше устройство, как подключиться к точке доступа, а затем научим его подключаться к другому, мы можем вернуться к первому. Например, представьте, что вы используете ESP32 дома с одной сетью и тем же ESP32 при работе с другой сетью. Мы также можем сохранить информацию о статическом интерфейсе, если у нас либо нет, либо нет необходимости использовать службы DHCP-сервера при запуске в качестве станции.

См. Также: • Нелетучее хранение

Working with TCP/IP

TCP/IP is the network protocol that is used on the Internet. It is the protocol that the ESP32 natively understands and uses with WiFi as the transport. Books upon books have already been written about TCP/IP and our goal is not to attempt to reproduce a detailed discussion of how it works, however, there are some concepts that we will try and capture. First, there is the IP address. This is a 32bit value and should be unique to every device connected to the Internet. A 32bit value can be thought of as four distinct 8bit values (4 x 8=32). Since we can represent an 8bit number as a decimal value between 0 and 255, we commonly represent IP addresses with the notation <number>.<number>.<number>.<number> for example 173.194.64.102. These IP addresses are not commonly entered in applications. Instead a textual name is typed such as "google.com" … but don't be misled, these names are an illusion at the TCP/IP level. All work is performed with 32bit IP addresses. There is a mapping system that takes a name (such as "google.com") and retrieves its corresponding IP address. The technology that does this is called the "Domain Name System" or DNS. When we think of TCP/IP, there are actually three distinct protocols at play here. The first is IP (Internet Protocol). This is the underlying transport layer datagram passing protocol. Above the IP layer is TCP (Transmission Control Protocol) which provides the illusion of a connection over the connectionless IP protocol. Finally there is UDP (User Datagram Protocol). This too lives above the IP protocol and provides datagram (connectionless) transmission between applications. When we say TCP/IP, we are not just talking about TCP running over IP but are in fact using this as a shorthand for the core protocols which are IP, TCP and UDP and additional related application level protocols such as DNS, HTTP, FTP, Telnet and more.

The Lightweight IP Stack – lwip

If we think of TCP/IP as a protocol then we can break up our understanding of networking into two distinct layers. One is the hardware layer that is responsible for getting a stream of 1's and 0's from one place to another. Common implementations for that include Ethernet, Token Ring and (yes … I'm dating myself now … dial-up modems). These are characterized by physical wires from your devices. WiFi is itself a transport layer. It deals with using radio waves as the communication medium of 1's and 0's between two points. The specification for WiFI is IEEE 802.11. Once we can transmit and receive data, the next level is organizing the data over that physical network and this is where TCP/IP comes into play. It provides the rules and governance of data transmission, addressing, routing, protocol negotiations and more. Typically, TCP/IP is implemented in software over the underlying physical transport mechanism. Think about this a moment. Imagine I said to you that I have a "magic box" and if you put something in that box, it will magically be transported to a different box. That is the analogy of physical transport. The software that is TCP/IP adds mechanisms above that. For example, imagine the box is only 6 inches wide. If you want to send me something through our boxes, you have to chop it up and send it in pieces. Your end of the box story handles that. My box will receive the parts and reassemble them for me. Parts may arrive in order and some parts may even get lost on route and have to be re-sent from the originals. The hardware (the boxes) have no idea how to achieve that. All they know is a piece of data in one end will hopefully arrive at the other … but not guaranteed. TCP/IP is a big protocol. It contains lots of parts. Fortunately it is well specified and has been implemented by many vendors over the last 45 years. Some of the implementations of the whole stack of TCP/IP parts have been written as open source and are distributed and maintained by the community. What this means is that if one has a new hardware layer, one can (in principle) lift an already written implementation of TCP/IP, map it to your hardware, compile it for your environment and you are good to go. This is actually much easier said than done … and fortunately for us, our friends at Espressif have done the work for us. One such open source implementation of a TCP/IP stack is called "The LightweightIPStack" which is commonly referred to as "lwIP". This can be read about in detail at its home page (see the references). As part of the distribution of the ESP-IDF, we have libraries that provide an implementation lwIP. It is lwIP that provides the ESP32 the following services: • IP • ICMP • IGMP • MLD • ND • UDP • TCP • sockets API • DNS Again, the good news is that the vast majority of lwIP is of no importance to us, ESP32 application designers and developers. It is vitally important … but important to the internal operation of ESP32 and not exposed to us as consumers. See also: • lwIP 2.0.0

TCP

A TCP connection is a bi-directional pipe through which data can flow in both directions. Before the connection is established, one side is acting as a server. It is passively listening for incoming connection requests. It will simply sit there for as long as needed until a connection request arrives. The other side of the connection is responsible for initiating the connection and it actively asks for a connection to be formed. Once the connection has been constructed, both sides can send and receive data. In order for the "client" to request a connection, it must know the address information on which the server is listening. This address is composed of two distinct parts. The first part is the IP address of the server and the second part is the "port number" for the specific listener. If we think about a PC, you may have many applications running on it, each of which can receive an incoming connection. Just knowing the IP address of your PC is not sufficient to address a connection to the correct application. The combination of IP address plus port number provides all the addressing necessary. As an analogy to this, think of your cell phone. It is passively sitting there until someone calls it. In our story your phone is the listener. The address that someone uses to form a connection is your phone number which is comprised of an area code plus the remainder. For example, a phone number of (817) 555-1234 will reach a particular phone. However the area code of 817 is for Fort Worth in Texas … calling that by itself is not sufficient to reach an individual … the full phone number is required. No we will look at how an ESP32 can set itself up as a listener for an incoming TCP/IP connection and this requires that we begin to understand the important "sockets" API.

TCP/IP Sockets

The sockets API is a programming interface for working with TCP/IP networking. It is probably the most familiar API for network programming. Sockets programming is familiar to programmers on Linux, Windows, Java and more. TCP/IP network flows come in two flavors … connection oriented over TCP and datagram oriented over UDP. The sockets API provides distinct patterns of calls for both styles. For TCP, a server is built by: 1. Creating a TCP socket 2. Associating a local port with the socket 3. Setting the socket to listen mode 4. Accepting a new connection from a client 5. Receive and send data 6. Close the client/server connection 7. Going back to step 4 For a TCP client, we build by: 1. Creating a TCP socket 2. Connecting to the TCP server 3. Sending data/receiving data 4. Close the connection Now let us break these up into code fragments that we can analyze in more depth. The header definitions for the sockets API can be found in <lwip/sockets.h>. For both the client and the server applications, the task of creating a socket is the same. It is an API call to the socket() function. int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) The return from socket() is an integer handle that is used to refer to the socket. Sockets have lots of state associated with them, however that state is internal to the TCP/IP and sockets implementation and need not be exposed to the network programmer. As such, there is no need to expose that data to the programmer. We can think of calling socket() as asking the run-time to create and initialize all the data necessary for a network communication. That data is owned by the run-time and we are passed a "reference number" or handle that acts as a proxy to the data. When ever we wish to subsequently perform work on that network connection, we pass back in that handle that was previously issued to us and we can correlate back to the connection. This isolates and insulates the programmer from the guts of the implementation of TCP/IP and leaves us with a useful abstraction. When we are creating a server side socket, we want it to listen for incoming connection requests. To do this, we need to tell the socket which TCP/IP port number it should be listening upon. Note that we don't supply the port number a directly from an int/short value. Instead we supply the value as returned by the htons() function. What this function does is convert the number into what is called "network byte order". This is the byte order that has been chosen by convention to be that used for transmitting unsigned multi byte binary data over the internet. It's actual format is "big endian" which means that if we take a number such as 9876 (decimal) then it is represented in binary as 00100110 10010100 or 0x26D4 in hex. For network byte order, we first transmit 00100110 (0x26) followed by 10010100 (0xD4). It is important to realize that the ESP32 is a little endian native architecture which means that we absolutely must transform 2 byte and 4 byte numbers into network byte order (big endian). On a given device, only one application at a time can be using any given local port number. If we want to associate a port number with an application, such as our server application in this case, we perform a task called "binding" which binds (or assigns) the port number to the socket which in turn is owned by the application. struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(portNumber); bind(sock, (struct sockaddr *)&serverAddress, sizeof(serverAddress)); With the socket now associated with a local port number, we can request that the runtime start listening for incoming connections. We do this by calling the listen() API. Before calling listen(), connections from clients would have been rejected with an indication to the client that there was nothing at the corresponding target address. Once we call listen(), the server will start accepting incoming client connections. The API looks like: listen(sock, backlog) The backlog is the number of connection requests that the run-time will listen for and accept before they are handed off to the application for processing. The way to think about this is imagine that you are the application and you can only do one thing at a time. For example, you can only be talking to one person at a time on the phone. Now imagine you have a secretary who is handling your incoming calls. When a call arrives and you are not busy, the secretary hands off the call to you. Now imagine that you are busy. At that time, the secretary answers the phone and asks the caller to wait. When you free up, she hands you the waiting call. Now let us assume that you are still busy when yet another client calls. She also tells this caller to wait. We are starting to build a queue of callers. And this is where the backlog concept comes into play. The backlog instructs the run-time how many calls can be received and asked to wait. If more calls arrive than our backlog will allow, the run-time rejects the call immediately. Not only does this prevent run-away resource consumption at the server, it also can be used as an indication to the caller that it may be better served trying elsewhere. Now from a server perspective, we are about ready to do some work. A server application can now block waiting for incoming client connections. The thinking is that a server application's purpose in life is to handle client requests and when it doesn't have an active client request, there isn't anything for it to do but wait for a request to arrive. While that is certainly one model, it isn't necessarily the only model or even the best model (in all cases). Normally we like our processors to be "utilized". Utilized means that while it has productive work it can do, then it should do it. If the only thing our program can do is service client calls, then the original model makes sense. However, there are certain programs that if they don't have a client request to immediately service, might spend time doing something else that is useful. We will come back to

that notion later on. For now, we will look at the accept() function call. When accept() is called, one of two things will happen. If there is no client connection immediately waiting for us, then we will block until such time in the future when a client connection does arrive. At that time we will wake up and be handed the connection to the newly arrived client. If on the other hand we called accept() and there was already a client connection waiting for us, we will immediately be handed that connection and we carry on. In both cases, we call accept() and are returned a connection to a client. The distinction between the cases is whether or not we have to wait for a connection to arrive. The API call looks like: struct sockaddr_in clientAddress; socklen_t clientAddressLength = sizeof(clientAddress); int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength); The return from accept() is a new socket (an integer handle) that represents the connection between the requesting client and the server. It is vital to realize that this is distinct from the server socket we created earlier which we bound to our server listening port. That socket is still alive and well and exists to continue to service further client connections. The newly returned socket is the connection for the conversation that was initiated by this single client. Like all TCP connections, the conversation is symmetric and bi-directional. This means that there is now no longer the notion of a client and server … both parties can send and receive as they would like at any time. If we wish to create a socket client, the story is similar. Again we create a socket() but this time there is no need for a bind()/listen()/accept() story. Instead we use the connect() API to connect to the target TCP/IP endpoint. For example: struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; inet_pton(AF_INET, "192.168.1.200", &serverAddress.sin_addr.s_addr); serverAddress.sin_port = htons(9999); int rc = connect(sock, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in)); See also: • Native byte order, endian and network byte order • socket • bind • listen • accept • send • recv • connect • Wikipedia – Berkeley Sockets • Beej's Guide to Network Programming

Handling errors

Most of the sockets APIs return an int return code. If this code is < 0 then an error has occurred. The nature of the error can be found using the global int called "errno". However, in a multitasking environment, working with global variables is not recommended. In the sockets area, we can ask a socket for the last error it encountered using the following code fragment: int espx_last_socket_errno(int socket) { int ret = 0; u32_t optlen = sizeof(ret); getsockopt(socket, SOL_SOCKET, SO_ERROR, &ret, &optlen); return ret; } The meanings of the errors can be compared against constants. Here is a table of constants used in the current FreeRTOS implementation: Symbol Value Description EPERM 1 Operation not permitted ENOENT 2 No such file or directory ESRCH 3 No such process EINTR 4 Interrupted system call EIO 5 I/O error ENXIO 6 No such device or address E2BIG 7 Arg list too long ENOEXEC 8 Exec format error EBADF 9 Bad file number ECHILD 10 No child processes EAGAIN 11 Try again ENOMEM 12 Out of memory EACCES 13 Permission denied EFAULT 14 Bad address ENOTBLK 15 Block device required EBUSY 16 Device or resource busy EEXIST 17 File exists EXDEV 18 Cross-device link ENODEV 19 No such device ENOTDIR 20 Not a directory EISDIR 21 Is a directory EINVAL 22 Invalid argument ENFILE 23 File table overflow EMFILE 24 Too many open files ENOTTY 25 Not a typewriter ETXTBSY 26 Text file busy EFBIG 27 File too large ENOSPC 28 No space left on device ESPIPE 29 Illegal seek EROFS 30 Read-only file system EMLINK 31 Too many links EPIPE 32 Broken pipe EDOM 33 Math argument out of domain of func ERANGE 34 Math result not representable EDEADLK 35 Resource deadlock would occur ENAMETOOLONG 36 File name too long ENOLCK 37 No record locks available ENOSYS 38 Function not implemented ENOTEMPTY 39 Directory not empty ELOOP 40 Too many symbolic links encountered EWOULDBLOCK EAGAIN 41 Operation would block ENOMSG 42 No message of desired type EIDRM 43 Identifier removed ECHRNG 44 Channel number out of range EL2NSYNC 45 Level 2 not synchronized EL3HLT 46 Level 3 halted EL3RST 47 Level 3 reset ELNRNG 48 Link number out of range EUNATCH 49 Protocol driver not attached ENOCSI 50 No CSI structure available EL2HLT 51 Level 2 halted EBADE 52 Invalid exchange EBADR 53 Invalid request descriptor EXFULL 54 Exchange full ENOANO 55 No anode EBADRQC 56 Invalid request code EBADSLT 57 Invalid slot EBFONT 59 Bad font file format ENOSTR 60 Device not a stream ENODATA 61 No data available ETIME 62 Timer expired ENOSR 63 Out of streams resources ENONET 64 Machine is not on the network ENOPKG 65 Package not installed EREMOTE 66 Object is remote ENOLINK 67 Link has been severed EADV 68 Advertise error ESRMNT 69 Srmount error ECOMM 70 Communication error on send EPROTO 71 Protocol error EMULTIHOP 72 Multihop attempted EDOTDOT 73 RFS specific error EBADMSG 74 Not a data message EOVERFLOW 75 Value too large for defined data type ENOTUNIQ 76 Name not unique on network EBADFD 77 File descriptor in bad state EREMCHG 78 Remote address changed ELIBACC 79 Can not access a needed shared library ELIBBAD 80 Accessing a corrupted shared library ELIBSCN 81 .lib section in a.out corrupted ELIBMAX 82 Attempting to link in too many shared libraries ELIBEXEC 83 Cannot exec a shared library directly EILSEQ 84 Illegal byte sequence ERESTART 85 Interrupted system call should be restarted ESTRPIPE 86 Streams pipe error EUSERS 87 Too many users ENOTSOCK 88 Socket operation on non-socket EDESTADDRREQ 89 Destination address required EMSGSIZE 90 Message too long EPROTOTYPE 91 Protocol wrong type for socket ENOPROTOOPT 92 Protocol not available EPROTONOSUPPORT 93 Protocol not supported ESOCKTNOSUPPORT 94 Socket type not supported EOPNOTSUPP 95 Operation not supported on transport endpoint EPFNOSUPPORT 96 Protocol family not supported EAFNOSUPPORT 97 Address family not supported by protocol EADDRINUSE 98 Address already in use EADDRNOTAVAIL 99 Cannot assign requested address ENETDOWN 100 Network is down ENETUNREACH 101 Network is unreachable ENETRESET 102 Network dropped connection because of reset ECONNABORTED 103 Software caused connection abort ECONNRESET 104 Connection reset by peer ENOBUFS 105 No buffer space available EISCONN 106 Transport endpoint is already connected ENOTCONN 107 Transport endpoint is not connected ESHUTDOWN 108 Cannot send after transport endpoint shutdown ETOOMANYREFS 109 Too many references: cannot splice ETIMEDOUT 110 Connection timed out ECONNREFUSED 111 Connection refused EHOSTDOWN 112 Host is down EHOSTUNREACH 113 No route to host EALREADY 114 Operation already in progress EINPROGRESS 115 Operation now in progress ESTALE 116 Stale NFS file handle EUCLEAN 117 Structure needs cleaning ENOTNAM 118 Not a XENIX named type file ENAVAIL 119 No XENIX semaphores available EISNAM 120 Is a named type file EREMOTEIO 121 Remote I/O error EDQUOT 122 Quota exceeded ENOMEDIUM 123 No medium found EMEDIUMTYPE 124 Wrong medium type

Configuration settings

Within the "menuconfig" there are some settings that relate to TCP/IP and can be found within the lwIP settings. The settings are: • Max number of open sockets – integer – CONFIG_LWIP_MAX_SOCKETS – This is the number of concurrently open sockets. The default is 4 and the maximum appears to be 16. • Enable SO_REUSEADDR – boolean – LWIP_SO_REUSE –

Using select()

Imagine that we have multiple sockets each of which may be the source of incoming data. If we try and read() data from a socket, we normally block until data is ready. If we did this, then if data becomes available on another socket, we wouldn't know. An alternative is to try and read data in a non-blocking fashion. This too would be useful but would require that we test each socket in turn in a busy or polling fashion. This too is not optimal. Ideally what we would like to do is block while watching multiple sockets simultaneously and wake up when the first one has something useful for us to do. See also: • select • The world of select()

Differences from "standard" sockets

Two header files that are commonly found in other sockets implementations are not part of the ESP-IDF definition. They are: • netinet/in.h • arpa/inet.h Despite not being present, no obvious issues have been found and it is assumed that the content normally contained within has been distributed across other headers.

UDP/IP Sockets

If we think of TCP as forming a connection between two parties similar to a telephone call, then UDP is like sending a letter through the postal system. If I were to send you a letter, I would need to know your name and address. Your address is needed so that the letter can be delivered to the correct house while your name ensure that it ends up in your hands as opposed to someone else who may live with you. In TCP/IP terms, the address is the IP address and the name is the port number. With a telephone conversation, we can exchange as much or as little information as we like. Sometimes I talk, sometimes you talk … but there is no maximum limit on how much information we can exchange in one conversation. With a letter however, there are only so many pages of paper that will fit in the envelopes I have at my disposal. The notion of the mail analogy is how we might choose to think about UDP. The acronym stands for User Datagram Protocol and it is the notion of the datagram that is akin to the letter. A datagram is an array of bytes that are transmitted from the sender to the receiver as a unit. The maximum size of a datagram using UDP is 64KBytes. No connection need be setup between the two parties before data starts to flow. However, there is a down side. The sender of the data will not be made aware of a receiver's failure to retrieve the data. With TCP, we have handshaking between the two parties that lets the sender know that the data was received and, if not, can automatically retransmit until it has been received or we decide to give up. With UDP, and just like a letter, when we send a datagram, we lose sight of whether or not it actually arrives safely at the destination. Now is a good time to come back to IP addresses and port numbers. We should start to be aware that on a PC, only one application can be listening upon any given port. For example, if my application is listening on port 12345, then no other application can also be listening on that same port … not your application nor another copy/instance of mine. When an incoming connection or datagram arrives at a machine, it has arrived because the IP address of the sent data matches the IP address of the device at which it arrived. We then route within the device based on port numbers. And here is where I want to clarify a detail. We route within the machine based on the pair of both protocol and port number. So for example, if a request arrives at a machine for port 12345 over a TCP connection, it is routed to the TCP application watching port 12345. If a request arrives at the same machine for port 12345 over UDP, it is routed to the UDP application watching port 12345. What this means is that we can have two applications listening on the same port but on different protocols. Putting this more formally, the allocation space for port numbers is a function of the protocol and it is not allowed for two applications to simultaneously reserve the same port within the same protocol allocation space. Although I used the story of a PC running multiple applications, in our ESP32 the story is similar even though we just run one application on the device. If your single application should need to listen on multiple ports, don't try and use the same port with the same protocol as the second function call will find the first one has already allocated the port. This is a detail that I am happy for you to forget as you will rarely come across it but I wanted to catch it here for completeness. To program with UDP, once again we use sockets. To set up a socket server using UDP again we call socket() to create a socket and again we call bind() to specify the port number we wish to listen upon. There is no need for a call to listen(). When the server is ready to receive an incoming request, we call recvfrom() which blocks until a datagram is received. Once one arrives, we wake up and can process the request. The request contains a return address and we can send a response using sendto() should we wish. On the client side, we create a socket() and then can invoke sendto(). The call to sendto() takes the IP address and port of the target as parameters as well as the payload data. For example: int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); sendto(socket_fd, data, size, 0, destAddr, destAddrLen); • socket • sendto • recvfrom

TLS, SSL and security

So far we have been thinking about making sockets calls that form a network connection and then sending and receiving data over that connection. However we have a security problem. The data that flows over the wire is not encrypted. This means that if one were to "sniff" or otherwise examine the network data, we would see the content of the data being transmitted. For example, if I send a password used for authentication, if we were to examine the content of the data, we would be able to determine the password I am using. It actually isn't that difficult to sense the data being sent and received. Excellent tools such as wireshark are used for debugging and can easily be used to examine the content of the network packets or stream. Obviously we are already exchanging credit card data, email and other sensitive information over the Internet so how is that done? The answer is a concept called the "Secure Socket Layer" or SSL. SSL provides the capability to encrypt the data before transmission such that only the intended recipient can decrypt it. Conversely, any responses sent by the recipient are also encrypted such that only we can decrypt the data. If someone were to capture or otherwise examine the data being sent over the wire, there is no way for them to get back to the original data content. The way this works is through the concept of private keys. Imagine I think of a very large random number (and by large I mean VERY large). We call this private number my private key. Now imagine that associated with the private key is a corresponding number (the public key) that can be used to decrypt a message that was encoded using the private key. Now imagine I want to correspond with a partner securely. I send a request (unencrypted) to the partner and ask for his public key. He sends that back and I send to him a copy of MY public key encrypted with his public key. Since only a matching pair of public/private keys can be used to decrypt data, only the desired recipient can decrypt the message at which point he will have a copy of my public key. Now in the future I can send him messages encrypted with my private key and further encrypted with his public key and he will be able to decode them with his copy of his private key and my public key while he can send me encrypted messages encoded with his private key which I can decode with my copy of his public key. By having exchanged public keys, we are now good to continue exchanging data without fear that it will be seen by anyone else. All of this encryption of data happens outside and above the knowledge of TCP/IP networking. TCP/IP provides the delivery of data but cares nothing about its content. As such, and at a high level, if we wish to exchange secure data, we must perform the encryption and decryption using algorithms and libraries that live outside of the sockets API and use sockets as the transport for transmitting and receiving the encrypted data that is fed into and received from the encryption algorithms. When using mbed TLS, we need a large stack size. I don't yet know how small we can get away with but I have been using 8000 bytes. See also: • mbed TLS • mbed TLS home page • mbed TLS tutoria l • mbed TLS API reference

mbedTLS app structure

Let us start to break down the structure of a TLS application that uses the mbedTLS APIs. First there is the notion of a network context that is initialized by a call to mbedtls_net_init(). mbedtls_net_context server_fd; mbedtls_net_init(&server_fd); There is nothing more to explain here. The data that is initialized is "opaque" to us and the invocation of this function is part of the rules. next comes the initialization of the SSL context with a call to mbedtls_ssl_init(). mbedtls_ssl_context ssl; mbedtls_ssl_init(&ssl); Again, there is nothing more to explain here. The data is again opaque and this function merely initializes it for us. Calling this function is also part of the rules. Now we call mebtls_ssl_config_init(). mbedtls_ssl_config config; mbedtls_ssl_config_init(&config); These initializations repeat for other data types including: mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_context entropy; mbedtls_entropy_init(&entropy); mbedtls_x509_crt cacert; mbedtls_x509_crt_init(&cacert); SSL utilizes good random number generators. What is a "good" random number? Since computers are deterministic devices, the generation of a random number is performed through the execution of an algorithm and since algorithms are deterministic, then a sequence of numbers generated by these functions might, in principle, be predictable. A good random number generator is one where the sequence of numbers produced is not at all easily predictable and generates values with no biases towards their values with an equal probability of any number within a range being chosen. We initialize the random number generator with a call to mbedtls_ctr_drbg_seed(). Note: We see the phrase "ctr_drbg" … that is an acronym for "Counter mode Deterministic Random Byte Generator". It is an industry standard specification/algorithm for generating random numbers. http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf With the setup under our belts, it is now time to start considering SSL based communication. Since we are considering SSL over sockets, if we were not using TLS sockets, we would perform a call to socket() to create a socket and then connect() to connect to our partner. In the world of mbedtls, we call mbedtls_net_connect(). This has the form: mbedtls_net_connect(&server_fd, <hostname>, <port>, MBEDTLS_NET_PROTO_TCP); The hostname and port define where we are connecting to. Notice the first parameter. This is the mbedtls_net_context structure that we initialized with a call to mbedtls_net_init() previously. We should always check the return code to ensure that the connection was successful. Now we get to configure our SSL defaults with a call to mbedtls_ssl_config_defaults(). For example: mbedtls_ssl_config_defaults( &conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBED_SSL_PRESET_DEFAULT) When we are communicating via SSL, we commonly wish to validate that the credentials provided by our partner indicate that they are who they claim to be. This process is called authentication. We can define what kind of authentication we wish to perform by calling mbedtls_ssl_conf_authmode(). mbedtls_ssl_conf_authmode(&ssl, MBEDTLS_SSL_VERIFY_NONE) Earlier we said that SSL is heavily dependent on a good random number generator. Now we tell the environment which random number generator we wish to use: mbedtls_ssl_conf_rng(&ssl, mbedtls_ctr_drbg_random, &ctr_drbg) Next we do some more SSL context setup by calling mbedtls_ssl_set_hostname(). mbedtls_ssl_set_hostname(&ssl, "name") Now we instruct the SSL environment which functions to use to send and receive data by calling mbedtls_ssl_set_bio(). mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL) At this point, we have formed a connection to our partner and configured the SSL environment. What remains is to actually read and write data. To write data we call mbed_ssl_write(). mbedtls_ssl_write(&ssl, buf, len) and to read data we call mbedtls_ssl_read(). mbedtls_ssl_read(&ssl, buf, len) See also: • mbedtls_net_init • mbedtls_ssl_init • mbedtls_ssl_config_init • mbedtls_net_connect • mbedtls_ssl_config_defaults • mbedtls_ssl_conf_authmode • mbedtls_ssl_conf_rng • mbedtls_ssl_set_hostname • mbedtls_ssl_set_bio • mbedtls_ssl_write • mbedtls_ssl_read

mbedTLS Example

Here is a sample function that has been tested on an ESP32 to make an HTTPS call to an HTTPS server to retrieve some results.

  1. include "mbedtls/platform.h"
  2. include "mbedtls/ctr_drbg.h"
  3. include "mbedtls/debug.h"
  4. include "mbedtls/entropy.h"
  5. include "mbedtls/error.h"
  6. include "mbedtls/net.h"
  7. include "mbedtls/ssl.h"
  8. include "esp_log.h"
  9. include "string.h"
  10. include "stdio.h"
  11. define SERVER_NAME "httpbin.org"
  12. define SERVER_PORT "443"

static char tag[] = "callhttps"; static char errortext[256]; static void my_debug(void *ctx, int level, const char *file, int line, const char *str) { ((void) level); ((void) ctx); printf("%s:%04d: %s", file, line, str); } void callhttps() { ESP_LOGD(tag, "--> callhttps\n"); mbedtls_net_context server_fd; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ssl_context ssl; mbedtls_ssl_config conf; mbedtls_x509_crt cacert; int ret; int len; char *pers = "ssl_client1"; unsigned char buf[1024]; mbedtls_net_init(&server_fd); mbedtls_ssl_init(&ssl); mbedtls_ssl_config_init(&conf); mbedtls_x509_crt_init(&cacert); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_init(&entropy); mbedtls_ssl_conf_dbg(&conf, my_debug, stdout); mbedtls_debug_set_threshold(2); // Log at error only ret = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) pers, strlen(pers)); if (ret != 0) { ESP_LOGE(tag, " failed\n ! mbedtls_ctr_drbg_seed returned %d\n", ret); return; } ret = mbedtls_net_connect(&server_fd, SERVER_NAME, SERVER_PORT, MBEDTLS_NET_PROTO_TCP); if (ret != 0) { ESP_LOGE(tag, " failed\n ! mbedtls_net_connect returned %d\n\n", ret); return; } ret = mbedtls_ssl_config_defaults( &conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { ESP_LOGE(tag, " failed\n ! mbedtls_ssl_config_defaults returned %d\n\n", ret); return; } mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_NONE); mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg); ret = mbedtls_ssl_setup(&ssl, &conf); if (ret != 0) { mbedtls_strerror(ret, errortext, sizeof(errortext)); ESP_LOGE(tag, "error from mbedtls_ssl_setup: %d - %x - %s\n", ret, ret, errortext); return; } ret = mbedtls_ssl_set_hostname(&ssl, "httpbin.org"); if (ret != 0) { mbedtls_strerror(ret, errortext, sizeof(errortext)); ESP_LOGE(tag, "error from mbedtls_ssl_set_hostname: %d - %x - %s\n", ret, ret, errortext); return; } mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); char *requestMessage = \ "GET /ip HTTP/1.1\r\n" \ "User-Agent: kolban\r\n" \ "Host: httpbin.org\r\n" \ "Accept-Language: en-us\r\n" \ "Accept-Encoding: gzip, deflate\r\n" \ "\r\n"; sprintf((char *)buf, requestMessage); len = strlen((char *)buf); ret = mbedtls_ssl_write(&ssl, buf, len); if (ret < 0) { mbedtls_strerror(ret, errortext, sizeof(errortext)); ESP_LOGE(tag, "error from write: %d -%x - %s\n", ret, ret, errortext); return; } len = sizeof(buf); ret = mbedtls_ssl_read(&ssl, buf, len); if (ret < 0) { ESP_LOGE(tag, "error from read: %d\n", len); return; } printf("Result:\n%.*s\n", len, buf); mbedtls_net_free(&server_fd); mbedtls_ssl_free(&ssl); mbedtls_ssl_config_free(&conf); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); ESP_LOGV(tag, "All done"); } Notes: When debugging MBED in Curl set MBEDTLS_DEBUG to 1 in curl_config.h

OpenSSL

OpenSSL is a popular implementation of an SSL stack. In the ESP32 environment, the selected stack for SSL/TLS is mbedTLS which is not the same as OpenSSL. As part of the ESP-IDF, a mapping layer has been provided that exposes the OpenSSL API on top of an mbedTLS implementation.

Name Service

В Интернете серверные машины можно найти по их службе имен доменов (DNS). Это сервис, который переводит читаемое человеком представление машины, например «google.com», в необходимое значение IP-адреса (например, 216.58.217.206). Чтобы это преобразование произошло, ESP32 должен знать IP-адрес одного или более DNS-серверов, которые затем будут использоваться для выполнения преобразования имени в IP-адрес. Если мы используем DHCP, тогда больше ничего не нужно делать - DHCP-сервер автоматически предоставляет адреса DNS-серверов. Однако, если мы не используем DHCP (например усли мы используем статические IP-адреса), тогда нам нужно проинструктировать ESP32 о местоположения DNS-серверов вручную. Мы можем сделать это с помощью функции dns_setserver(). Это принимает IP-адрес как входной вместе с каким из двух возможных DNS-серверов для использовать. ESP32 настроен на то, чтобы знать личность до двух внешних серверов имен. Причина для двоих заключается в том, что если попытка достичь первого не удастся, мы будем использовать второй. Мы можем получить наши текущие идентификаторы DNS-сервера, используя dns_getserver (). Google публично предоставляет два сервера имен с адресами 8.8.8.8 и 8.8.4.4. Как только мы определим серверы имен, мы можем найти адрес имени хоста используя функцию gethostbyname(). Во время разработки мы можем протестировать конкретный DNS-сервер, чтобы проверить, что он может определить по имени хоста. Отличным инструментом Linux для выполнения этой задачи является «nslookup». В нем есть многие варианты, но для наших целей мы можем предоставить им имя хоста для поиска и использовать DNS-сервер:

$ nslookup example.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: example.com
Address: 93.184.216.34
See also:
• gethostbyname
• dns_getserver
• dns_setserver
• Wikipedia: Domain Name System
• Google: Public DNS

Multicast Domain Name Systems

В локальной сети с приходом и выходом динамических устройств мы можем захотеть, чтобы одно устройство находило IP-адрес другого устройства, чтобы они могли взаимодействовать друг с другом. Проблема состоит в том, что IP-адреса могут динамически выделяться сервером DHCP, работающим на точке доступа WiFi. Это означает, что IP-адрес устройства, скорее всего, не будет статичным. Кроме того, это не очень полезная история, чтобы ссылаться на устройства по их IP-адресам. Нам нужна некоторая форма службы динамического имени для поиска устройств по имени, где их IP-адреса не настроены администратором. Именно здесь вступает в действие система доменных имен многоадресной рассылки (mDNS). На высоком уровне, когда устройство хочет найти другое устройство с заданным именем, он передает запрос всем членам сети, запрашивая ответ от устройства с таким именем. Если машина считает, что она имеет эту личность, она отвечает своей собственной трансляцией, которая включает его имя и IP-адрес. Это не только удовлетворяет первоначальному запросу, но и другие машины в сети могут видеть это взаимодействие и кэшировать ответ для себя. Это означает, что если им нужно будет разрешить один и тот же хост в будущем, у них уже есть ответ. Используя многоадресную систему доменных имен (mDNS), ESP32 может попытаться разрешить имя хоста компьютера в локальной сети на его IP-адрес. Он делает это, передавая пакет, запрашивая, чтобы машина с этим идентификатором отвечала. Демоны службы имен реализованы Bonjour и nss-mdns (Linux). Обычно хосты, расположенные с использованием этого метода, принадлежат домену, заканчивающемуся «.local». Чтобы определить, участвует ли ваш компьютер в mDNS, вы можете проверить, прослушивает ли он порт UDP 5353. Это порт, используемый для связи mDNS.

См. Также:

  • Wikipedia – Multicast DNS
  • IETF RFC 6762: Multicast DNS
  • Multicast DNS
  • New DNS Technologies in the Lan
  • Avah i – Implementation of mDNS … source project for Unix machines
  • Adafruit – Bonjour (Zeroconf) Networking for Windows and Linux
  • chrome.mdns – API description for Chrome API for mDNS
  • Android – ZeroConf Browser

mDNS API programming

The ESP-IDF provides a set of rich APIs for programming mDNS on the ESP32. Specifically, we can either advertise ourselves in mDNS or else query existing mDNS information. The attributes of an mDNS server entry appear to be: • hostname – mdns_set_hostname() • default instance – mdns_set_instance() • service – mdns_service_add() ◦ type – _http, _ftp etc etc ◦ protocol – _tcp, _udp etc etc ◦ port – port number • instance name for service – mdns_service_instance_set() • TXT data for service – mdns_service_txt_set() See also: • mdns_set_hostname • mdns_set_instance • mdns_service_add • mdns_service_instance_set • mdns_service_port_set

Installing Bonjour

Launch the Bonjour installer: If all has gone well, we will find a new Windows service running called "Bonjour Service": There is also a Bonjour browser available here … http://hobbyistsoftware.com/bonjourbrowser

Avahi

An implementation of Multicast DNS on Linux is called Avahi. Avahi runs as the systemd daemon called "avahi-daemon". We can determine whether or not it is running with: $ systemctl status avahi-daemon ● avahi-daemon.service - Avahi mDNS/DNS-SD Stack Loaded: loaded (/lib/systemd/system/avahi-daemon.service; enabled) Active: active (running) since Wed 2016-01-20 22:13:35 CST; 1 day 13h ago Main PID: 384 (avahi-daemon) Status: "avahi-daemon 0.6.31 starting up." CGroup: /system.slice/avahi-daemon.service ├─384 avahi-daemon: running [raspberrypi.local] └─426 avahi-daemon: chroot helper The avahi-daemon utilizes a configuration file found at /etc/avahi/avahi-daemon.conf. The default name that avahi advertizes itself as is the local hostname. When host-name resolution is performed, the system file called /etc/nsswitch.conf is used to determine the order of resolution. Specifically the hosts entry contains the name resolution. An example would be: hosts: files mdns4_minimal [NOTFOUND=return] dns Which says "first look in /etc/hosts, then consult mDNS and then use full DNS". What this means is that a device which advertizes itself with mDNS can be found via a lookup of "<hostname>.local". For example, if I boot up a Linux machine which gets a dynamic IP address through DHCP and the hostname of that machine is "chip1", then I can reach it with a domain name address of "chip1.local". If the IP address of the device changes, subsequent resolutions of the domain name will continue to correctly resolve. Avahi tools are not installed by default but can be installed using the "avahi-utils" package: $ sudo apt-get install avahi-utils To see the list of mDNS devices in your network, we can use the avahi-browse command. For example: $ avahi-browse -at + wlan1 IPv6 chip1 [ce:79:cf:21:db:95] Workstation local + wlan1 IPv4 chip1 [ce:79:cf:21:db:95] Workstation local + wlan0 IPv6 pizero [00:36:76:21:97:a3] Workstation local + wlan0 IPv6 raspi3 [b8:27:eb:9d:fc:60] Workstation local + wlan0 IPv6 chip1 [cc:79:cf:21:db:95] Workstation local + wlan0 IPv4 pizero [00:36:76:21:97:a3] Workstation local + wlan0 IPv4 raspi3 [b8:27:eb:9d:fc:60] Workstation local + wlan0 IPv4 chip1 [cc:79:cf:21:db:95] Workstation local + wlan0 IPv6 pizero Remote Disk Management local + wlan0 IPv6 raspi3 Remote Disk Management local + wlan0 IPv4 raspi3 Remote Disk Management local + wlan0 IPv4 pizero Remote Disk Management local + wlan0 IPv4 WDMyCloud Apple File Sharing local + wlan0 IPv4 WDMyCloud _wd-2go._tcp local + wlan0 IPv4 WDMyCloud Web Site local + wlan0 IPv4 Living Room _googlecast._tcp local + wlan0 IPv4 123456789 _teamviewer._tcp local To access an mDNS advertized server from Microsoft Windows, you will need a service similar to Apple's Bonjour installed. Bonjour is distributed as part of Apple's iTunes product. Once installed, we should be able to access the published servers at their <name>.local address. A partner windows tool called "Bonjour Browser" is available which displays an mDNS listing of servers on windows. See also: • avahi home page • man(1) – avahi-browse • man(5) – avahi-daemon.conf

Working with SNTP

SNTP is the Simple Network Time Protocol and allows a device connected to the Internet to learn the current time. In order to use this, you must know of at least one time server located on the Internet. The US National Institute for Science and Technology (NIST) maintains a number of these which can be found here: • http://tf.nist.gov/tf-cgi/servers.cg i Other time servers can be found all over the globe and I encourage you to Google search for your nearest or country specific server. Once you know the identity of a server by its host name or IP address, you can call either of the functions called sntp_setservername() or sntp_setserver() to declare that we wish to use that time server instance. The ESP32 can be configured with up to three different time servers so that if one or two are not available, we might still get a result. The ESP32 must also be told the local timezone in which it is running. This is set with a call to sntp_set_timezone() which takes the number of hours offset from UTC. For example, I am in Texas and my timezone offset becomes "-5". Although this function is present, I would suggest using the POSIX tzset() function instead. With these configured, we can start the SNTP service on the ESP32 by calling sntp_init(). This will cause the device to determine its current time by sending packets over the network to the time servers and examining their responses. It is important to note that immediately after calling sntp_init(), you will not yet know what the current time may be. This is because it may take a few seconds for the ESP32 to sends the time requests and get their responses and this will all happen asynchronously to your current commands and won't complete till sometime later. When ready, we can retrieve the current time with a call to sntp_get_current_timestamp() which will return the number of seconds since the 1st of January 1970 UTC. We can also call the function called sntp_get_real_time() which will return a string representation of the time. While these functions obviously exist, I would not recommend using them. Instead look at the POSIX alternatives which are time() and asctime(). Here is an example of using SNTP to set the time: ip_addr_t addr; sntp_setoperatingmode(SNTP_OPMODE_POLL); inet_pton(AF_INET, "129.6.15.28", &addr); sntp_setserver(0, &addr); sntp_init(); The time can be accessed from a variety of POSIX compliant functions including: • asctime – Build a string representation of time. • clock – Return processor time. • ctime – Build a string representation of time. • difftime – Calculate a time difference. • gettimeofday – Retrieve the current time of day. • gmtime – Produce a struct tm from a time_t. • localtime – Produce a struct tm from a time_t. • settimeofday – Set the current time. • strftime – Format a time_t to a string. • time – Get the current time as a time_t (seconds since epoch). See also: • SNTP API • Timers and time • asctime • ctime • gmtime • localtime • strftime • IETF RFC5905: Network Time Protocol Version 4: Protocol and Algorithms Specification

Java Sockets

The sockets API is the defacto standard API for programming against TCP/IP. My programming language of choice is Java and it has full support for sockets. What this means is that I can write a Java based application that leverages sockets to communicate with the ESP32. I can send and receive data through quite easily. In Java, there are two primary classes that represents sockets, those are java.net.Socket which represents a client application which will form a connection and the second class is java.net.ServerSocket which represents a server that is listening on a socket awaiting a client connection. Since the ESP32 can be either a client or a server, both of these Java classes will come into play. To connect to an ESP32 running as a server, we need to know the IP address of the device and the port number on which it is listening. Once we know those, we can create an instance of the Java client with: Socket clientSocket = new Socket(ipAddress, port); This will form a connection to the ESP32. Now we can ask for both an InputStream from which to receive partner data and an OutputStream to which we can write data. InputStream is = clientSocket.getInputStream(); OutputStream os = clientSocket.getOutputStream(); When we are finished with the connection, we should call close() to close the Java side of the connection: clientSocket.close(); It really is as simple as that. Here is an example application: package kolban; import java.io.OutputStream; import java.net.Socket; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; public class SocketClient { private String hostname; private int port; public static void main(String[] args) { Options options = new Options(); options.addOption("h", true, "hostname"); options.addOption("p", true, "port"); CommandLineParser parser = new DefaultParser(); try { CommandLine cmd = parser.parse(options, args); SocketClient client = new SocketClient(); client.hostname = cmd.getOptionValue("h"); client.port = Integer.parseInt(cmd.getOptionValue("p")); client.run(); } catch (Exception e) { e.printStackTrace(); } } public void run() { try { int SIZE = 65000; byte data[] = new byte[SIZE]; for (int i = 0; i < SIZE; i++) { data[i] = 'X'; } Socket s1 = new Socket(hostname, port); OutputStream os = s1.getOutputStream(); os.write(data); s1.close(); System.out.println("Data sent!"); } catch (Exception e) { e.printStackTrace(); } } } // End of class // End of file To configure a Java application as a socket server is just as easy. This time we create an instance of the SocketServer class using: SocketServer serverSocket = new SocketServer(port) The port supplied is the port number on the machine on which the JVM is running that will be the endpoint of remote client connection requests. Once we have a ServerSocket instance, we need to wait for an incoming client connection. We do this using the blocking API method called accept(). Socket partnerSocket = serverSocket.accept(); This call blocks until a client connect arrives. The returned partnerSocket is the connected socket to the partner which can used in the same fashion as we previously discussed for client connections. This means that we can request the InputStream and OutputStream objects to read and write to and from the partner. Since Java is a multithreaded language, once we wake up from accept() we can pass off the received partner socket to a new thread and repeat the accept() call for other parallel connections. Remember to close() any partner socket connections you receive when you are done with them. So far, we have been talking about TCP oriented connections where once a connection is opened it stays open until closed during which time either end can send or receive independently from the other. Now we look at datagrams that use the UDP protocol. The core class behind this is called DatagramSocket. Unlike TCP, the DatagramSocket class is used both for clients and servers. First, let us look at a client. If we wish to write a Java UDP client, we will create an instance of a DatagramSocket using: DatagramSocket clientSocket = new DatagramSocket(); Next we will "connect" to the remote UDP partner. We will need to know the IP address and port that the partner is listening upon. Although the API is called "connect", we need to realize that no connection is formed. Datagrams are connectionless so what we are actually doing is associating our client socket with the partner socket on the other end so that when we actually wish to send data, we will know where to send it to. clientSocket.connect(ipAddress, port); Now we are ready to send a datagram using the send() method: DatagramPacket data = new DatagramPacket(new byte[100], 100); clientSocket.send(data); To write a UDP listener that listens for incoming datagrams, we can use the following: DatagramSocket serverSocket = new DatagramSocket(port); The port here is the port number on the same machine as the JVM that will be used to listen for incoming UDP connections. To wait for an incoming datagram, call receive(). DatagramPacket data = new DatagramPacket(new byte[100], 100); clientSocket.receive(data); If you are going to use the Java Socket APIs, read the JavaDoc thoroughly for these classes are there are many features and options that were not listed here. See also: • Java tutorial: All About Sockets • JDK 8 JavaDoc