まえがき
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 向けのリンクになっています