2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ESP-IDF で websocket を使ってみる

Last updated at Posted at 2021-01-12

まえがき

ESP32 に外からコマンドを送信してリモコンを操作する、みたいなことをやりたくて、websocket で遊んでみました。
Arduino向けの websocket ライブラリがいくつかありましたが、loop() をビジーループするような雰囲気で残念に思い、ESP-IDF の由緒正しい(?)websocket ライブラリを使ってみました。

ESP-IDF 4.1 を使っています。ESP-IDF は開発が活発なのはいいのですが、頻繁に API が変わるのが辛いですね。

コード

ESP32 は Wi-Fi Station (子機) として動作します。
今回はデモとして、同一 LAN 上の PC で websocket サーバを動かします。

Websocket サーバと ESP32 を起動してから以下のように実行すると、ESP32 にコマンド文字列が渡されます。

curl -X POST -d 'command' http://{PCのIPアドレス}:8888/api

ESP32

ちょっと長いですが、前半が wifi 接続処理、後半が websocket 関連のコードになっています。
do_something() に実際の処理を実装する想定です。

# include <freertos/FreeRTOS.h>
# include <freertos/task.h>
# include <freertos/event_groups.h>
# include <esp_system.h>
# include <esp_event.h>
# include <esp_wifi.h>
# include "esp_websocket_client.h"
# include <esp_log.h>
# include <nvs_flash.h>

# define WIFI_SSID "some ssid"
# define WIFI_PASS "Password"
# define WEBSOCKET_SERVER_URI "ws://192.168.0.25:8888/ws"

static EventGroupHandle_t wifi_event_group;
# define WIFI_CONNECTED_BIT BIT0
# define WIFI_FAIL_BIT BIT1

static const char *TAG = "WS_APP";

static int connect_retry = 0;
# define MAX_RETRY 5

static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
                               void *event_data) {
    if (event_base == WIFI_EVENT) {
        if (connect_retry > MAX_RETRY) {
            ESP_LOGI(TAG, "retry over");
            xEventGroupSetBits(wifi_event_group, WIFI_FAIL_BIT);
        }
        switch (event_id) {
        case WIFI_EVENT_STA_START:
            esp_wifi_connect();
            break;
        case WIFI_EVENT_STA_DISCONNECTED:
            esp_wifi_connect();
            connect_retry++;
            ESP_LOGI(TAG, "retry to connect");
            break;
        default:
            break;
        }
    } else if (event_base == IP_EVENT) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        connect_retry = 0;
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void init_wifi_sta(void) {
    wifi_event_group = xEventGroupCreate();

    esp_netif_create_default_wifi_sta();

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

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

    wifi_config_t wifi_config = {
        .sta =
            {
                .ssid = WIFI_SSID,
                .password = WIFI_PASS,
                .threshold.authmode = WIFI_AUTH_WPA2_PSK,
                .pmf_cfg =
                    {
                        .capable = true,
                        .required = false,
                    },
            },
    };
    wifi_country_t wifi_country = {.cc = "JP", .schan = 1, .nchan = 14};
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_set_country(&wifi_country));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "succeeded to wifi_init_sta()");

    // 接続が成功するか、リトライオーバーになるまでブロックする
    EventBits_t bits = xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE, pdFALSE, portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to SSID:%s", WIFI_SSID);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "failed to connect SSID:%s", WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}

static void do_something(const char *command, int command_len) {
    ESP_LOGI(TAG, "recieved data:%.*s", command_len, command);
    // TODO
}

static void websocket_event_handler(void *args, esp_event_base_t base, int32_t event_id,
                                    void *event_data) {
    esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
    switch (event_id) {
    case WEBSOCKET_EVENT_CONNECTED:
        ESP_LOGI(TAG, "websocket connected");
        break;
    case WEBSOCKET_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "websocket disconnected");
        break;
    case WEBSOCKET_EVENT_DATA:
        // ping/pong フレームを無視する
        if (data->op_code == 0x9 || data->op_code == 0xA) {
            break;
        }
        do_something(data->data_ptr, data->data_len);
        break;
    case WEBSOCKET_EVENT_ERROR:
        ESP_LOGW(TAG, "WEBSOCKET_EVENT_ERROR");
        break;
    default:
        break;
    }
}

static void websocket_app_start() {
    esp_websocket_client_config_t cfg = {};
    cfg.uri = WEBSOCKET_SERVER_URI;

    ESP_LOGI(TAG, "Connecting to %s...", cfg.uri);

    esp_websocket_client_handle_t client = esp_websocket_client_init(&cfg);
    esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler,
                                  (void *)client);

    esp_websocket_client_start(client);
}

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

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    init_wifi_sta();

    websocket_app_start();
}

サーバ

以下のように実行します

pip install tornado
python3 ws-server.py

色々手抜きなので、接続中のクライアント全てにコマンドが送信されます

ws-server.py
from logging import getLogger, StreamHandler, INFO

from tornado import websocket, web, ioloop

logger = getLogger(__name__)
handler = StreamHandler()
logger.setLevel(INFO)
handler.setLevel(INFO)
logger.addHandler(handler)


class WebsocketHandler(websocket.WebSocketHandler):
    clients = set()

    def open(self):
        logger.info("new connection: %s", self.request.remote_ip)
        WebsocketHandler.clients.add(self)
    
    def on_close(self):
        WebsocketHandler.clients.remove(self)

    def on_message(self, message):
        logger.info("got message: %r", message)


class ApiHandler(web.RequestHandler):
    async def post(self):
        msg = self.request.body
        for c in WebsocketHandler.clients:
            await c.write_message(msg)
        self.write("OK")


app = web.Application([
    (r'/api', ApiHandler),
    (r'/ws', WebsocketHandler),
])
app.listen(8888)
ioloop.IOLoop.instance().start()

参考

いずれも ESP-IDF v4.1 向けのリンクになっています

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?