開発環境（ESP-IDF）によるESP32のWiFi機能の使用方法

# 記事の概要
ESP32のWiFi機能の基本的な使い方について説明します。
開発環境はESP-IDFとVScodeのPlatformIOを使用します。

# 0. ESP32のWiFi機能
ESP32のWiFi機能にはSTAモードとAPモードがあります。

STAモードは、ESP32がWiFiルータなどのアクセスポイントに接続し、ノードとして機能します。 
APモードは、ESP32自体がアクセスポイントになって、スマホやPCがWiFiネットワークに接続できます。

この2つの違いは以下の動画を見ると分かりやすいです。
前半に紹介されるのがAPモードで、後半に紹介されるのがSTAモードです。

https://www.youtube.com/watch?v=piuj4k193oI

Arduino環境を使用すると簡単にできますが、今回はESP-IDF環境を使用する場合を説明します。
ESP-IDF環境の構築方法は以下を参照してください。

https://www.mgo-tec.com/esp32-idf-howto-01

https://qiita.com/nishinohi/items/d1c1a517f26394c492c3

# 1. STAモード
以下のサンプルプログラムを例にSTAモードの使用方法を見てみます。

https://github.com/espressif/esp-idf/blob/master/examples/wifi/getting_started/station/main/station_example_main.c

動作確認したい場合は、以下のサンプルのWiFi IDとパスワード、最大再接続回数を自分で設定して、自分のEPS32に書き込んでみてください。
シリアルモニタに

```
I (2089) wifi station: connected to ap SSID:myssid password:mypassword
```

が表示されれば成功です。

## 1.1 WiFiの初期化

### 1.1.1 NVSの初期化
EPS32に内蔵されたFlashメモリのnvs (non volatile storage)領域を初期化します。
nvsはWiFiライブラリで使用します。


`nvs_flash_init`関数を実行して、返り値に問題がなければ終了し、エラーならば`nvs_flash_erase`関数でメモリを消去してから、再度`nvs_flash_init`関数を実行します。

```C:main.c
    //Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
```

### 1.1.2 WiFiイベントの作成とWiFiの初期設定
WiFiイベントを作成し、イベントに対応する処理を紐付けます。
また、WiFi接続前にしておくべき幾つかの設定を行います。

まずWiFiイベント変数を作成します。

```C:main.c
    s_wifi_event_group = xEventGroupCreate();
```

イベント検知するための初期設定関数'esp_event_loop_create_default'、およびnetifやSTAの初期化関数などを実行します。

```C:main.c
    ESP_ERROR_CHECK(esp_netif_init());

    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
```

証明書を扱う変数を作成します。

```C:main.c
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
```

そして`WIFI_EVENT`と`IP_EVENT`に対応する割り込み処理を登録します。

```C:main.c
    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));
```

### 1.1.3 WiFiの設定
WiFi接続の設定をします。

```C:main.c
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
	        .threshold.authmode = WIFI_AUTH_WPA2_PSK,

            .pmf_cfg = {
                .capable = true,
                .required = false
            },
        },
    };
```

自分のWiFiの設定を定義しておきます。
安全性のためにはIDとパスワードをコードの中に含めず、外部から入力できるようにした方がいいと思いますが、今はこのままにします。

```C
#define EXAMPLE_ESP_WIFI_SSID      "Matsui's WiFi"
#define EXAMPLE_ESP_WIFI_PASS      "abcdefg123456"
```

### 1.1.4 WiFi接続の開始
`esp_wifi_set_mode`関数でSTAモードに設定し、`esp_wifi_set_config`関数でWiFiの設定を有効化し、`esp_wifi_start`関数でWiFi接続を開始します。

```C:main.c
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );
```

### 1.1.5 WiFiイベントの待機
WiFiイベントの応答を検知して、WiFi接続の成否を判定します。

`xEventGroupWaitBits`関数により、イベントの応答を待機して、変数`bits'に結果を格納します。

```C:main.c
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);
```


変数`bits'を確認して、WiFi接続に成功したか、失敗したかで処理を分岐させます。

```C:main.c
    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
```

## 1.2 割り込み処理
WiFiイベント発生時には割り込み処理が実行されます。
この割り込み処理は、`xEventGroupWaitBits`関数を実行中に発生します。

```C:main.c
static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
```


`WIFI_EVENT_STA_START`イベント発生時には、`esp_wifi_connect`関数を実行してWiFiネットワークに接続します。

```C:main.c
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
```

`WIFI_EVENT_STA_DISCONNECTED`イベント発生時には、EXAMPLE_ESP_MAXIMUM_RETRYで設定した最大回数まで再接続を試みて、それにも失敗すれば`xEventGroupSetBits`でWiFiイベント変数のWiFi接続に関するビットに`WIFI_FAIL_BIT`を設定して、再接続を終了します。

```C:main.c
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"connect to the AP fail");
```

`WIFI_FAIL_BIT`を設定したことで、`xEventGroupWaitBits`関数を抜けて、1.1.5に先述した以下の分岐処理が実行されます。

```C
else if (bits & WIFI_FAIL_BIT)
```

WiFi接続に成功後には`IP_EVENT_STA_GOT_IP`イベントが発生するはずです。
イベントデータを取得し、`xEventGroupSetBits`でWiFiイベント変数のWiFi接続に関するビットに`WIFI_CONNECTED_BIT`を設定します。

```C:main.c
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
```

`WIFI_CONNECTED_BIT`を設定したことで、`xEventGroupWaitBits`関数を抜けて、1.1.5に先述した以下の分岐処理が実行されます。

```C
if (bits & WIFI_CONNECTED_BIT)
```

## 1.3 ページリクエスト
ESP32はWiFiルータをアクセス・ポイントにして、他のサイトのデータ転送をリクエストできます。

そのためには、あらかじめWiFi接続成功時に、FreeRTOSを利用して、ページリクエスト用のタスクを生成しておくといいでしょう。
`WIFI_CONNECTED_BIT`の分岐処理にタスク生成関数を追加します。

```C:main.c
    if (bits & WIFI_CONNECTED_BIT) {        
        xTaskCreate(request_http, "request http", 10*configMINIMAL_STACK_SIZE, NULL, 10, NULL);
    }
```

そして`request_http`関数を以下のように作成します。

configにアクセスするサイトのURLを設定してください。
なお、以下では証明書の要らないhttpサイトを対象にしています。
（httpsサイトにアクセスするには、アクセスしたいサイトのpem ファイルを入手して、以下の関数に設定を追加しないといけませんが、ここでは説明を省略します。）

```C:main.c
void request_http(void *arg)
{
    esp_http_client_config_t config = {
        .url = "http://www.chiseki.go.jp/about/point/index.html",
        .event_handler = http_handler,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    if (esp_http_client_perform(client) != ESP_OK)
        ESP_LOGE(TAG, "HTTP REQUEST ERROR");
    esp_http_client_cleanup(client);

    vTaskDelete(NULL);
}
```

HTTPイベント発生時に実行する割り込み処理も作成します。
今回は、`HTTP_EVENT_ON_DATA`イベントにより、HTMLのデータ部分をコンソール画面に表示させました。
HTML構文が表示されるはずです。
他のイベント発生時には、特にすることがないのでメッセージだけを表示させています。


```C:main.c
esp_err_t http_handler(esp_http_client_event_t *evt)
{
    switch (evt->event_id)
    {
        case HTTP_EVENT_ERROR:
            printf("HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            printf("HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            printf("HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            printf("HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            printf("%.*s\n", evt->data_len, (char *)evt->data);
            break;
        case HTTP_EVENT_ON_FINISH:
            printf("HTTP_EVENT_ON_FINISH");
            break;
        case HTTP_EVENT_DISCONNECTED:
            printf("HTTP_EVENT_DISCONNECTED");
            break;
        default:
            break;
    }
    return ESP_OK;
}
```

これを利用して様々なサイトからのデータを収集することができます。
逆にこちらから相手サイトにデータ転送することもできますが、ここでは説明しません。

なお、サンプルプログラムにはページリクエストの部分はないので、以下のサイトやGithubの複数のサンプルを参照しました。

https://tutorialmore.com/questions-1918267.htm

# 2. APモード

以下のサンプルプログラムを例にAPモードの使用方法を見てみます。

https://github.com/espressif/esp-idf/blob/master/examples/wifi/getting_started/softAP/main/softap_example_main.c

## 2.1 WiFiの初期化

### 2.1.1 NVSの初期化
STAモードの時と同様にします。

### 2.1.2 WiFiの初期設定
WiFi初期設定は、STAモードの時と同様です。
ただし、APモードの場合は`esp_netif_create_default_wifi_ap`関数を使います。

```C:main.c
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_ap();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
```

STAモードと異なりWiFiイベントは必須ではありませんが、WiFi接続時や切断時に何か処理を実行したいのならば、以下のように割り込み処理を登録しておきます。

```C:main.c
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));
```

### 2.1.3 WiFiの設定
WiFi接続の設定をします。

```C:main.c
    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
            .channel = EXAMPLE_ESP_WIFI_CHANNEL,
            .password = EXAMPLE_ESP_WIFI_PASS,
            .max_connection = EXAMPLE_MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK
        },
    };
```

今回は定数を以下のように設定しておきます。

```C
#define EXAMPLE_ESP_WIFI_SSID "esp32_ap_test"
#define EXAMPLE_ESP_WIFI_PASS "my_pass_0123"
#define EXAMPLE_ESP_WIFI_CHANNEL 11
#define EXAMPLE_MAX_STA_CONN 1
```

### 2.1.4 WiFi接続の開始
`esp_wifi_set_mode`関数でAPモードに設定し、`esp_wifi_set_config`関数でWiFiの設定を有効化し、`esp_wifi_start`関数でWiFi接続を開始します。

```C:main.c
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
```

## 2.2 割り込み処理
WiFi接続時には`WIFI_EVENT_AP_STACONNECTED`イベントが発生し、切断時には`WIFI_EVENT_AP_STADISCONNECTED`イベントが発生します。
以下では、これらのイベント発生時にメッセージを表示させています。
これらの処理がなくても、動作には問題ありません。

```C:main.c
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                    int32_t event_id, void* event_data)
{
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
                 MAC2STR(event->mac), event->aid);
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
                 MAC2STR(event->mac), event->aid);
    }
}
```

## 2.3 HTTPサーバー
以下を参考にしてHTTPサーバーを作成します。

https://github.com/espressif/esp-idf/blob/1cb31e50943bb757966ca91ed7f4852692a5b0ed/docs/en/api-reference/protocols/esp_http_server.rst

### 2.3.1 HTTPサーバ設定
HTTPメソッドのPOSTとGETがリクエストされた際の割り込み処理を登録します。

```C:main.c
httpd_uri_t uri_get = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = get_handler,
    .user_ctx = NULL};

httpd_uri_t uri_post = {
    .uri = "/",
    .method = HTTP_POST,
    .handler = post_handler,
    .user_ctx = NULL};

void start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) == ESP_OK) {
        /* Register URI handlers */
        httpd_register_uri_handler(server, &uri_get);
        httpd_register_uri_handler(server, &uri_post);
    }
}
```

### 2.3.2 POSTとGETの割り込み処理
GETがリクエストされたならば、割り込み処理`get_handler`を実行します。
`httpd_resp_send`関数により、HTMLデータを転送します。

POSTがリクエストされたならば、割り込み処理`post_handler`を実行します。
`httpd_req_recv`関数により転送されたデータを受信します。
また、応答処理が必要ならば、`httpd_resp_send`関数によりデータを転送します。


```C:main.c
static const char *html_sample 
    = "<html><form action=\"/\" method=\"post\">"
      "<label for=\"your_name\">Your Name:</label><br>"
      "<input type=\"text\" id=\"your_name\" name=\"your_name\"><br>"
      "<input type=\"submit\" value=\"Submit\">"
      "</form></html>";

esp_err_t get_handler(httpd_req_t *req)
{
    httpd_resp_send(req, html_sample , HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

esp_err_t post_handler(httpd_req_t *req)
{
    char content[100];

    size_t recv_size = MIN(req->content_len, sizeof(content));

    int ret = httpd_req_recv(req, content, recv_size);
    if (ret <= 0) {
        if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
            httpd_resp_send_408(req);
        }
        return ESP_FAIL;
    }

    const char resp[] = "Response";
    httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}
```

### 2.3.3 動作確認

まずスマホやPCのWiFi一覧から、先ほどに設定したWiFiのSSIDである"esp32_ap_test"を探して接続します。
パスワードは、先ほどに設定した"my_pass_0123"です。

次にブラウザを開いて、ホスト・アドレスに行きます。
例えば、今回私が使用しているESP32-WROOMのホスト・アドレスは、192.168.4.1です。
URLにこのアドレスを入力すれば、GETリクエストにより、html_sample で与えたHTMLが表示されます。

ブラウザのYour Nameの欄に名前を入力してSubmitボタンをクリックすれば、POSTリクエストにより、入力したデータをEPS32が受信し、"Response"と表示される応答画面に遷移します。