ESP32でWi-Fi Easy Connectを試してみた

2022.07.11

こんにちは。CX事業本部Delivery部のakkyです。

今回は、PCやスマホ以外のデバイスのWi-Fi設定を簡単に行う仕組みであるWi-Fi Easy Connectを試してみましたので、ご紹介します。

Wi-Fi Easy Connect

デバイスをWi-Fiへ接続するにはSSIDとパスワードの設定が必要ですが、PCやスマホ以外などのディスプレイとキーボードを持たない機器の場合は、以下のような方法をとるのがメジャーです。

  1. WPSを利用する
  2. デバイスへAPモードを実装し、スマホのWebブラウザなどで設定できるようにする
  3. Bluetoothなど他の経路で設定する

このうち、1. WPSを利用するのがもっとも手軽な手段ですが、脆弱性が指摘されているなど問題があり、Androidではすでに非推奨となるなど、すぐになくなることはないと思われるものの、代替手段を検討したいところです。

また、2や3の方法は標準化されていないために、製品ごとに設定方法が異なることがユーザー体験上のネックになるかもしれません。

今回試したWi-Fi Easy ConnectはWPSに代わる手段で、接続したいデバイスのQRコードをスマホで読み取り、接続する方式です。

Wi-Fiアライアンスによる標準化がされており、Android11以降のスマホがあれば、既に使用することができます。詳細についてはWi-Fiアライアンスの公式ページや、解説記事などをご覧ください。

ESP32で試してみる

対応するデバイスはまだかなり少ないと思われますので、ESP32で作ってみます。ESP-IDFにライブラリとサンプルが含まれていましたので、すぐに実験することができました。

使用した機器とソフトウェアは以下の通りです。ESP32系のデバイスであれば使用できると思われます。

  • ESP32-S3-DevKitC-1
  • ESP-IDF 4.4.1
  • SHARP AQUOS sense4 (Android12)

ソースコード

examples\wifi\wifi_easy_connect\dpp-enrolleeを改変し、APへの接続後にHTTPリクエストするように変更しました。ソースコードを示します。ディレクトリをコピーし、dpp_enrollee_main.cを変更してビルドすると簡単です。

/* DPP Enrollee Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_dpp.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "qrcode.h"
#include "esp_http_client.h"

#define MAX_HTTP_OUTPUT_BUFFER 2048

#ifdef CONFIG_ESP_DPP_LISTEN_CHANNEL
#define EXAMPLE_DPP_LISTEN_CHANNEL_LIST     CONFIG_ESP_DPP_LISTEN_CHANNEL_LIST
#else
#define EXAMPLE_DPP_LISTEN_CHANNEL_LIST     "6"
#endif

#ifdef CONFIG_ESP_DPP_BOOTSTRAPPING_KEY
#define EXAMPLE_DPP_BOOTSTRAPPING_KEY   CONFIG_ESP_DPP_BOOTSTRAPPING_KEY
#else
#define EXAMPLE_DPP_BOOTSTRAPPING_KEY   0
#endif

#ifdef CONFIG_ESP_DPP_DEVICE_INFO
#define EXAMPLE_DPP_DEVICE_INFO      CONFIG_ESP_DPP_DEVICE_INFO
#else
#define EXAMPLE_DPP_DEVICE_INFO      0
#endif

static const char *TAG = "wifi dpp-enrollee";
wifi_config_t s_dpp_wifi_config;

static int s_retry_num = 0;

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_dpp_event_group;

#define DPP_CONNECTED_BIT  BIT0
#define DPP_CONNECT_FAIL_BIT     BIT1
#define DPP_AUTH_FAIL_BIT           BIT2

esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    static char *output_buffer;  // Buffer to store response of http request from event handler
    static int output_len;       // Stores number of bytes read
    switch(evt->event_id) {
        case HTTP_EVENT_ERROR:
            ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            /*
             *  Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
             *  However, event handler can also be used in case chunked encoding is used.
             */
            if (!esp_http_client_is_chunked_response(evt->client)) {
                // If user_data buffer is configured, copy the response into the buffer
                if (evt->user_data) {
                    memcpy(evt->user_data + output_len, evt->data, evt->data_len);
                } else {
                    if (output_buffer == NULL) {
                        output_buffer = (char *) malloc(esp_http_client_get_content_length(evt->client));
                        output_len = 0;
                        if (output_buffer == NULL) {
                            ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
                            return ESP_FAIL;
                        }
                    }
                    memcpy(output_buffer + output_len, evt->data, evt->data_len);
                }
                output_len += evt->data_len;
            }

            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            if (output_buffer != NULL) {
                // Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response
                // ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len);
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            if (output_buffer != NULL) {
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
    }
    return ESP_OK;
}

static void event_handler(void *arg, esp_event_base_t event_base,
                          int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        ESP_ERROR_CHECK(esp_supp_dpp_start_listen());
        ESP_LOGI(TAG, "Started listening for DPP Authentication");
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < 5) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "retry to connect to the AP"); } else { xEventGroupSetBits(s_dpp_event_group, DPP_CONNECT_FAIL_BIT); } ESP_LOGI(TAG, "connect to the AP fail"); } 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_dpp_event_group, DPP_CONNECTED_BIT);
    }
}

void dpp_enrollee_event_cb(esp_supp_dpp_event_t event, void *data)
{
    switch (event) {
    case ESP_SUPP_DPP_URI_READY:
        if (data != NULL) {
            esp_qrcode_config_t cfg = ESP_QRCODE_CONFIG_DEFAULT();

            ESP_LOGI(TAG, "Scan below QR Code to configure the enrollee:\n");
            esp_qrcode_generate(&cfg, (const char *)data);
        }
        break;
    case ESP_SUPP_DPP_CFG_RECVD:
        memcpy(&s_dpp_wifi_config, data, sizeof(s_dpp_wifi_config));
        esp_wifi_set_config(ESP_IF_WIFI_STA, &s_dpp_wifi_config);
        ESP_LOGI(TAG, "DPP Authentication successful, connecting to AP : %s",
                 s_dpp_wifi_config.sta.ssid);
        s_retry_num = 0;
        esp_wifi_connect();
        break;
    case ESP_SUPP_DPP_FAIL:
        if (s_retry_num < 5) {
            ESP_LOGI(TAG, "DPP Auth failed (Reason: %s), retry...", esp_err_to_name((int)data));
            ESP_ERROR_CHECK(esp_supp_dpp_start_listen());
            s_retry_num++;
        } else {
            xEventGroupSetBits(s_dpp_event_group, DPP_AUTH_FAIL_BIT);
        }
        break;
    default:
        break;
    }
}

void http_test_task(void *pvParameters)
{
    char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
    esp_http_client_config_t config = {
        .url = "http://httpbin.org/get",
        .user_data = local_response_buffer,
        .event_handler = _http_event_handler,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    // GET
    esp_err_t err = esp_http_client_perform(client);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d",
                esp_http_client_get_status_code(client),
                esp_http_client_get_content_length(client));
    } else {
        ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
    }
    ESP_LOGI(TAG, "%s", local_response_buffer);

    esp_http_client_cleanup(client);
    vTaskDelete(NULL);
}

void dpp_enrollee_init(void)
{
    s_dpp_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());

    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));

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

    ESP_ERROR_CHECK(esp_supp_dpp_init(dpp_enrollee_event_cb));
    /* Currently only supported method is QR Code */
    ESP_ERROR_CHECK(esp_supp_dpp_bootstrap_gen(EXAMPLE_DPP_LISTEN_CHANNEL_LIST, DPP_BOOTSTRAP_QR_CODE,
                    EXAMPLE_DPP_BOOTSTRAPPING_KEY, EXAMPLE_DPP_DEVICE_INFO));

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
    EventBits_t bits = xEventGroupWaitBits(s_dpp_event_group,
                                           DPP_CONNECTED_BIT | DPP_CONNECT_FAIL_BIT | DPP_AUTH_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,
                                           portMAX_DELAY);

    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
     * happened. */
    if (bits & DPP_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 s_dpp_wifi_config.sta.ssid, s_dpp_wifi_config.sta.password);
    } else if (bits & DPP_CONNECT_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 s_dpp_wifi_config.sta.ssid, s_dpp_wifi_config.sta.password);
    } else if (bits & DPP_AUTH_FAIL_BIT) {
        ESP_LOGI(TAG, "DPP Authentication failed after %d retries", s_retry_num);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }

    xTaskCreate(&http_test_task, "http_test_task", 8192, NULL, 5, NULL);
}

void app_main(void)
{
    //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);

    dpp_enrollee_init();
}

実験方法

ビルドして書き込むと、コンソールには以下のような出力が得られます。

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd0108,len:0x1660
load:0x403b6000,len:0xb7c
load:0x403ba000,len:0x2f74
entry 0x403b6248
I (24) boot: ESP-IDF v4.4.1-dirty 2nd stage bootloader
I (25) boot: compile time 11:14:52
I (25) boot: chip revision: 0
I (27) boot.esp32s3: Boot SPI Speed : 80MHz
I (32) boot.esp32s3: SPI Mode       : DIO
I (36) boot.esp32s3: SPI Flash Size : 2MB
I (41) boot: Enabling RNG early entropy source...
I (47) boot: Partition Table:
I (50) boot: ## Label            Usage          Type ST Offset   Length
I (57) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (65) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (72) boot:  2 factory          factory app      00 00 00010000 00100000
I (80) boot: End of partition table
I (84) esp_image: segment 0: paddr=00010020 vaddr=3c0a0020 size=1b360h (111456) map
I (112) esp_image: segment 1: paddr=0002b388 vaddr=3fc94850 size=041d8h ( 16856) load
I (116) esp_image: segment 2: paddr=0002f568 vaddr=40374000 size=00ab0h (  2736) load
I (119) esp_image: segment 3: paddr=00030020 vaddr=42000020 size=963e8h (615400) map
I (237) esp_image: segment 4: paddr=000c6410 vaddr=40374ab0 size=0fd9ch ( 64924) load
I (252) esp_image: segment 5: paddr=000d61b4 vaddr=50000000 size=00010h (    16) load
I (259) boot: Loaded app from partition at offset 0x10000
I (259) boot: Disabling RNG early entropy source...
I (272) cpu_start: Pro cpu up.
I (272) cpu_start: Starting app cpu, entry point is 0x403751d8
0x403751d8: call_start_cpu1 at C:/Espressif/frameworks/esp-idf-v4.4.1/components/esp_system/port/cpu_start.c:160

I (0) cpu_start: App cpu up.
I (286) cpu_start: Pro cpu start user code
I (286) cpu_start: cpu freq: 160000000
I (286) cpu_start: Application information:
I (289) cpu_start: Project name:     dpp-enrollee
I (294) cpu_start: App version:      v4.4.1-dirty
I (299) cpu_start: Compile time:     Jul 11 2022 11:14:25
I (305) cpu_start: ELF file SHA256:  24c09f983d8bd49a...
I (311) cpu_start: ESP-IDF:          v4.4.1-dirty
I (317) heap_init: Initializing. RAM available for dynamic allocation:
I (324) heap_init: At 3FC9C748 len 000438B8 (270 KiB): D/IRAM
I (330) heap_init: At 3FCE0000 len 0000EE34 (59 KiB): STACK/DRAM
I (337) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (343) heap_init: At 600FE000 len 00002000 (8 KiB): RTCRAM
I (350) spi_flash: detected chip: gd
I (354) spi_flash: flash io: dio
W (358) spi_flash: Detected size(8192k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (371) sleep: Configure to isolate all GPIO pins in sleep state
I (378) sleep: Enable automatic switching of GPIO sleep configuration
I (385) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (446) pp: pp rom version: e7ae62f
I (446) net80211: net80211 rom version: e7ae62f
I (456) wifi:wifi driver task: 3fce5a10, prio:23, stack:6656, core=0
I (456) system_api: Base MAC address is not set
I (456) system_api: read default base MAC address from EFUSE
I (466) wifi:wifi firmware version: 63017e0
I (466) wifi:wifi certification version: v7.0
I (476) wifi:config NVS flash: enabled
I (476) wifi:config nano formating: disabled
I (476) wifi:Init data frame dynamic rx buffer num: 32
I (476) wifi:Init management frame dynamic rx buffer num: 32
I (486) wifi:Init management short buffer num: 32
I (486) wifi:Init dynamic tx buffer num: 32
I (496) wifi:Init static tx FG buffer num: 2
I (496) wifi:Init static rx buffer size: 1600
I (506) wifi:Init static rx buffer num: 10
I (506) wifi:Init dynamic rx buffer num: 32
I (506) wifi_init: rx ba win: 6
I (516) wifi_init: tcpip mbox: 32
I (516) wifi_init: udp mbox: 6
I (526) wifi_init: tcp mbox: 6
I (526) wifi_init: tcp tx win: 5744
I (526) wifi_init: tcp rx win: 5744
I (536) wifi_init: tcp mss: 1440
I (536) wifi_init: WiFi IRAM OP enabled
I (546) wifi_init: WiFi RX IRAM OP enabled
I (706) wifi dpp-enrollee: Scan below QR Code to configure the enrollee:

I (706) QRCODE: Encoding below text with ECC LVL 0 & QR Code Version 10
I (706) QRCODE: DPP:C:81/6;M:XX:XX:XX:XX:XX:XX;K:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;;

(QRコード略)


I (1026) phy_init: phy_version 501,94697a2,Apr  7 2022,20:49:08
I (1136) wifi:mode : sta (XX:XX:XX:XX:XX:XX)
I (1136) wifi:enable tsf
I (1136) wifi dpp-enrollee: Started listening for DPP Authentication

QRコードが表示されるので、これをAndroidスマホで読み取るのですが、ここに表示されているQRコードがなぜか読み取れませんでした。 そこで、I (706) QRCODE: DPP:C:81/6;M:XX:XX:XX:XX:XX:XX;K:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;;の行からDFP:~のあとをコピーし、qrencode等でQRコードに変換する必要があります。 Linuxなどで以下のコマンドを実行し、QRコードを生成してください。

$ qrencode -t ansi "QRCODE: DPP:C:81/6;M:XX:XX:XX:XX:XX:XX;K:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;;"

(追記) examples\common_components\qrcode\esp_qrcode_main.cのesp_qrcode_print_consoleを以下のように書き換えると、コンソールから直接QRコードを読み取れるようになりました。

void esp_qrcode_print_console(esp_qrcode_handle_t qrcode)
{
    int size = qrcodegen_getSize(qrcode);

    (void)lt;

    for(int i=0;i<2;i++) { printf("\e[47m"); for(int j=0; j<size+4; j++) {printf("  ");} printf("\e[0m\n");}

    for (int y = 0; y < size; y++) {
        printf("\e[47m    ");
        for (int x = 0; x < size; x++) {
            if (qrcodegen_getModule(qrcode, x, y)) {
                printf("\e[40m  ");
            }
            else {
                printf("\e[47m  ");
            }
        }
        printf("\e[47m    \e[0m\n");
    }

    for(int i=0;i<2;i++) { printf("\e[47m"); for(int j=0; j<size+4; j++) {printf("  ");} printf("\e[0m\n");}
}

次に、Androidスマホの設定アプリを開き、ネットワークとインターネット→Wi-Fiから、アクセスポイントを選択します。

次に、「デバイスを追加」をタップします。

先ほどqrencodeで生成したQRコードを読み取ってください。

接続確認画面が出ます。「Wi-Fiの共有」をタップします。もし、このAPが2.4GHz帯でないなど、別のAPを使いたい場合は「別のネットワークを選択」をタップして、APを選択してください。

設定完了です。

ESP32のコンソールを確認すると、スマホから指定したAPへ接続し、HTTPでデータが取得できていることが確認できました。実際には、このSSIDとパスワードを保存しておくことになります。

I (36396) wifi dpp-enrollee: DPP Authentication successful, connecting to AP : accesspoint
I (36406) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1
I (36406) wifi:state: init -> auth (b0)
I (36416) wifi:state: auth -> assoc (0)
I (36436) wifi:state: assoc -> run (10)
I (36546) wifi:connected with accesspoint, aid = 7, channel 1, BW20, bssid = XX:XX:XX:XX:XX:XX
I (36546) wifi:security: WPA2-PSK, phy: bgn, rssi: -66
I (36556) wifi:pm start, type: 1

I (36556) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 0, mt_pti: 25000, mt_time: 10000
I (36576) wifi:BcnInt:102400, DTIM:1
I (37946) esp_netif_handlers: sta ip: 192.168.1.X, mask: 255.255.255.0, gw: 192.168.1.1
I (37946) wifi dpp-enrollee: got ip:192.168.1.X
I (37946) wifi dpp-enrollee: connected to ap SSID:accesspoint password:XXXXXXXXXXXXXXXX
I (38426) wifi dpp-enrollee: HTTP GET Status = 200, content_length = 268
I (38426) wifi dpp-enrollee: {
  "args": {},
  "headers": {
    "Content-Length": "0",
    "Host": "httpbin.org",
    "User-Agent": "ESP32 HTTP Client/1.0",
    "X-Amzn-Trace-Id": "Root=1-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXX"
  },
  "origin": "XXX.XXX.XXX.XXX",
  "url": "http://httpbin.org/get"
}

W (38446) wifi:I (38446) wifi dpp-enrollee: HTTP_EVENT_DISCONNECTED
<ba-add>idx:0 (ifx:0, XX:XX:XX:XX:XX:XX), tid:5, ssn:2, winSize:64
W (38626) wifi:<ba-add>idx:1 (ifx:0, XX:XX:XX:XX:XX:XX), tid:0, ssn:8, winSize:64

まとめ

Wi-Fi Easy Connectについてご紹介しました。まだ対応製品が少ないためか、スマホのUIのQRコード読み取り時にアスペクト比がおかしいなどバギーな印象を受けたのですが、動作を確認することができました。 ESP32に含まれるライブラリではQRコードを毎回新規生成していますが、これは一度生成して紙に印刷したものでよいのかなど、まだ仕様を把握しきれていませんが、大変便利に使えたので、今後の発展に期待しています。