ESP32-S3 + PlatformIOでWIFI通信
ESP32は、数あるマイコンの中でWifi+Bluetoothが一体化している唯一?のマイコンで、
アンテナ付きの本体さえあれば通信処理できるようになります。
わりと最近発売されたArduino UNO R4 WiFiなどでもESP32が使われています。
今回はこのWifi機能を使って外部サーバーと通信するシンプルな
プログラムを実装したときのメモを残しときます。
アプリケーション構成
下図のようにESP32からHTTPリクエストをAPIサーバーに送信してAPIサーバーから簡単なメッセージをレスポンスするプログラムです。
APIサーバーは、開発PC上で起動しておいて、ESP32からはあらかじめWifiのアクセスポイントや接続先サーバーのIPアドレスを仕込んでビルド&書き込みしてます。
成功すると、ESP32のシリアルモニタにレスポンス文字列を出力する簡単なものです。
ESP32のプロジェクト作成
VSCode+PlatformIO拡張機能での開発です。フレームワークはespidfを使って、FreeRTOSのタスクで処理します。
platformio.iniは以下で、特にWifi関連の設定はないです。
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = espidf
upload_speed = 2000000
monitor_speed = 115200
main.cと同じ場所に、secret.hというヘッダを追加してここに通信設定情報をマクロ定義しました。
API_SERVERは接続先のURLです。今回はルート("/")にGETメソッドでリクエストするとレスポンスが返ってくる仕様です。サーバーはポート8000で起動します。
#define WIFI_SSID "MySSID"
#define WIFI_PASSWORD "MyPassword"
#define API_SERVER "http://192.168.0.50:8000/"
まず**app_main()**について、 wifi_init でアクセスポイントに接続するまでの処理を行い、少しウェイトした後で、get_wifi_infos でMACアドレスやDHCPで割り当てられたIPアドレスを取得してます。
ウェイトしないと、IPアドレスが振られる前に通信処理開始してエラーになるので少し入れてます。
そして最後に http_get_task のタスクを開始します。
void app_main() {
wifi_init();
vTaskDelay(pdMS_TO_TICKS(3000));
get_wifi_infos();
xTaskCreate(http_get_task, "http_get_task", 4096, NULL, 5, NULL);
}
wifi_initの処理です。
nvsで始まる関数は、ESP-IDFで提供されるNon-Volatile Storage(NVS)ライブラリの初期化関数です。NVSは、フラッシュメモリ上にキー・バリュー形式で設定情報やデータを保存し、電源オフ後も保持できる仕組みです。まずは個々を初期化します。
その後、esp_netif_init 以降の処理でネットワークスタック関連を初期化して、ホスト名を設定してます。最初はMACアドレスとか不明だったので、接続できたのかルーターの管理画面で確認したくて設定しました。
そして最後にesp_wifi_set_configでWifi接続します。secret.hで設定したSSIDやパスワードを使って接続します。
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_netif.h"
#include "esp_http_client.h"
#include "secret.h"
static const char *TAG = "httpget";
void wifi_init() {
printf("WIFI_SSID = %s, WIFI_PASSWORD = %s, API_SERVER = %s\n", WIFI_SSID, WIFI_PASSWORD, API_SERVER);
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_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_wifi_set_mode(WIFI_MODE_STA);
// host name
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif != NULL) {
esp_netif_set_hostname(netif, "esp32_s3_host1");
}
// wifi
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
},
};
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_connect());
}
MACアドレスやIPアドレスを取得してダンプしてます。今回の通信処理では必須というわけではないです。
void get_wifi_infos() {
ESP_LOGI(TAG, "=========================");
uint8_t mac[6];
esp_err_t ret = esp_wifi_get_mac(ESP_IF_WIFI_STA, mac);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "WiFi MAC Address: %02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
ESP_LOGE(TAG, "** Failed to get MAC address: %s **", esp_err_to_name(ret));
}
esp_netif_ip_info_t ip_info;
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif != NULL) {
esp_netif_get_ip_info(netif, &ip_info);
ESP_LOGI(TAG, "WiFi IP Address: %d.%d.%d.%d",
esp_ip4_addr1_16(&ip_info.ip),
esp_ip4_addr2_16(&ip_info.ip),
esp_ip4_addr3_16(&ip_info.ip),
esp_ip4_addr4_16(&ip_info.ip));
} else {
ESP_LOGE(TAG, "** Failed to get IP address: Network interface not found **");
}
ESP_LOGE(TAG, "=========================");
}
通信タスクの処理が以下になります。以下の処理はesp_http_client.hで定義されるHTTP通信処理を使ってます。
実は結構ハマりました。コメントにもありますが、サンプルだとesp_http_client_performを使うコードが多いです。esp_http_client_performを使うと、通信処理がイベントとして処理されます。本来はこちらの方が効率が良いので普段は使うべきですが、今回は諸事情あってイベントじゃないシーケンシャルな処理がしたかったので、以下の処理手順になります。
見ての通りで、ファイルIOのようにオープンしてリード、終了したらクローズする処理をDelayを挟んで繰り返ししてます。
- esp_http_client_init
- esp_http_client_open
- esp_http_client_fetch_headers
- esp_http_client_get_status_code
- esp_http_client_get_content_length
- esp_http_client_read
- esp_http_client_close
- esp_http_client_cleanup
分かってしまえば単純ですが、esp_http_client_performを実行するとイベント内でレスポンスを取得されてしまい、esp_http_client_read を実行しても値が取得できない現象でずっと悩んでいました。
// HTTP GETリクエストを行い、レスポンスを出力する
// esp_http_client_perform()を使ったハンドラ処理ではなく、
// open => read => closeを行うシンプルな通信処理
void http_get_task(void *pvParameters)
{
char response_buffer[512] = {0};
// APIの設定
// 事前にHTTPサーバーを起動しておく
esp_http_client_config_t config = {
.url = API_SERVER,
.method = HTTP_METHOD_GET,
.timeout_ms = 10000,
.event_handler = NULL,
};
while(true){
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL) {
ESP_LOGE(TAG, "*** Failed to initialize HTTP connection ***");
vTaskDelete(NULL);
return;
}
esp_err_t ret = esp_http_client_open(client, 10000);
if( ret < 0 ){
ESP_LOGE(TAG, "*** HTTP CONNECTION ERROR. *** ret=%d", ret);
vTaskDelete(NULL);
return;
}
int header_status = esp_http_client_fetch_headers(client);
if (header_status < 0) {
ESP_LOGE(TAG, "*** Failed to fetch headers *** status=%d", header_status);
vTaskDelete(NULL);
return;
}
int status = esp_http_client_get_status_code(client);
int content_length = esp_http_client_get_content_length(client);
ESP_LOGI(TAG, "http status = %d, content length = %d", status, content_length);
int read_len = esp_http_client_read(client, response_buffer, sizeof(response_buffer)-1);
ESP_LOGI(TAG, "read_len=%d, received data: %s", read_len, response_buffer);
esp_http_client_close(client);
esp_http_client_cleanup(client);
// wait time
vTaskDelay(pdMS_TO_TICKS(300));
}
}
ビルドして書き込めばすぐに通信が開始します。が、サーバーが起動してないのでエラーを繰り返すはずです。次にサーバーを実装&起動します。
APIサーバーの準備
サーバー側ですが、普段使っている Python + FASTAPIです。Flaskでも構わないです。まず依存モジュールをvenv内でインストールしておきます。
> cd server
> python -m venv venv
> source venv/bin/activate
> pip install fastapi uvicorn requests
ルート"/"をエンドポイントとしたGET処理です。単純にリクエストがきたら、現在のUNIX時間の整数値を文字列化して返すだけです。
from fastapi import FastAPI
import time
app = FastAPI()
@app.get("/")
def read_root():
return "%s" % (int(time.time()))
念のためローカルでサーバーを確認するプログラムも掲載しときます。
import requests
import time
# server url
url = "http://127.0.0.1:8000/"
while True:
try:
response = requests.get(url)
print("Response:", response.json())
except Exception as e:
print("Error occurred:", e)
time.sleep(1)
サーバーを起動します。
> uvicorn app:app --host 0.0.0.0 --port 8000 --reload
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [26600] using StatReload
INFO: Started server process [5596]
INFO: Waiting for application startup.
INFO: Application startup complete.
動作確認
サーバーを起動して、ESP32も電源ONすると処理が開始されます。
サーバー側は定期的にアクセスがあるはずです。
INFO: 192.168.0.101:49743 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.0.101:49744 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.0.101:49745 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.0.101:49746 - "GET / HTTP/1.1" 200 OK
ESP32はUSBケーブルでPCに接続されていれば、そのポートを指定してシリアルモニタを表示すると、ログが流れてくるはずです。
電源ON後にMACアドレスやIPを表示した後、HTTPレスポンスがreceived dataとして表示されます。
I (3552) httpget: =========================
I (3552) httpget: WiFi MAC Address: f0:9e:9e:20:66:80
I (3552) httpget: WiFi IP Address: 192.168.0.101
I (3552) httpget: =========================
I (3562) main_task: Returned from app_main()
I (3582) httpget: http status = 200, content length = 12
I (3802) httpget: read_len=12, received data: "1740363343"
I (4132) httpget: http status = 200, content length = 12
I (4302) httpget: read_len=12, received data: "1740363343"
I (4622) httpget: http status = 200, content length = 12
I (4802) httpget: read_len=12, received data: "1740363344"
I (5112) httpget: http status = 200, content length = 12
I (5302) httpget: read_len=12, received data: "1740363344"
サーバーを停止すると、現在のプログラムだとアプリケーションが終了します。
E (163552) transport_base: poll_read select error 104, errno = Connection reset by peer, fd = 54
E (163552) httpget: *** Failed to fetch headers *** status=-1
まとめ
今回はシーケンシャルに通信処理を行う場合のサンプルでした。上記で触れてますが、イベント処理したい場合は、esp_http_client_perform を使います。その場合、esp_http_client_config_tで設定する項目が増えます。レスポンスバッファとかイベント処理する関数などをconfigに渡す必要があります。イベント処理についてはまた今度試してみたいと思います。
上記のサンプルプロジェクトは以下で公開してます。
Discussion