センサーデータを「Web」へ解き放つ
IoT開発において、マイコンで取得したデータをサーバーへ送る処理は避けて通れません。 今回は ESP32-S3 と公式開発環境の ESP-IDF を使い、データを JSON形式 でサーバーに飛ばす、最も基本的かつ強力な方法を紹介します。
ℹ️ ESP-IDF環境の準備
ESP-IDFの環境構築がまだの方は、先にこちらの記事をご覧ください:
👉 【ESP32で電子工作】VS Codeで始めるESP-IDF (v5.5.2) 環境構築ガイド
(VS Codeの設定から最小プロジェクトのビルドまで、ステップバイステップで解説しています)
受信側のサーバーには、爆速で構築できる FastAPI を採用。開発環境管理ツール uv を使って、一瞬でテスト環境を整えていきましょう。
システム構成
今回の検証環境は以下の通りです。
- デバイス: ESP32-S3 (ESP-IDF v5.5)
- サーバー: Python 3.10 (FastAPI)
- パッケージ管理:
uv(高速なPythonパッケージマネージャー)
ネットワーク図
[ESP32-S3] --- (Wi-Fi) ---> [ローカルPC (FastAPI)]
192.168.x.y 192.168.x.z:8000
技術の基礎知識
実装に入る前に、今回使用する技術について簡単に解説します。
JSON(JavaScript Object Notation)とは?
JSON は、データを構造化して表現するための軽量なテキスト形式です。人間にも読みやすく、機械でも処理しやすいため、Web APIのデータ交換フォーマットとして標準的に使われています。
JSONの例:
{
"temperature": 26.54,
"humidity": 60,
"device_id": "ESP32-S3-001"
}
キーと値のペアで構成され、数値、文字列、配列、ネストしたオブジェクトなど、様々なデータ型を表現できます。
cJSONライブラリとは?
cJSON は、C言語でJSON形式のデータを生成・解析するための軽量ライブラリです。ESP-IDFには標準で組み込まれています。
主な機能:
- JSONの生成: C言語のデータ構造からJSON文字列を作成
- JSONの解析: JSON文字列をパースして値を取り出す
- メモリ管理: 動的にメモリを確保・解放
cJSON_CreateObject() でオブジェクトを作り、cJSON_AddNumberToObject() や cJSON_AddStringToObject() で値を追加していくだけで、複雑なJSON構造を簡単に構築できます。
esp_http_client(HTTP Client)とは?
esp_http_client は、ESP-IDFが提供するHTTP通信ライブラリです。
主な機能:
- HTTP/HTTPSリクエスト: GET、POST、PUT、DELETEなどのメソッドに対応
- ヘッダー設定: Content-Type、Authorizationなどのカスタムヘッダーを追加可能
- SSL/TLS対応: HTTPS通信にも対応(証明書の検証も可能)
- チャンク転送: 大容量データの送受信にも対応
今回は、このライブラリを使ってJSON形式のデータをPOSTメソッドでサーバーに送信します。
1. 受信サーバーの準備 (Python)
まずは受け皿を作ります。Pythonの新しいツール uv を使うと、ライブラリのインストールに悩まされることがありません。
サーバーコード (main.py)
FastAPIとPydanticを使い、受信データの型を定義します。
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 受信データの型定義
class SensorData(BaseModel):
temperature: float
humidity: int
device_id: str
@app.post("/api/data")
async def receive_data(data: SensorData):
print("--- 届いたデータ ---")
print(f"デバイスID: {data.device_id}")
print(f"温度: {data.temperature} ℃")
print(f"湿度: {data.humidity} %")
return {"status": "success", "message": "Data received"}
if __name__ == "__main__":
import uvicorn
# PCのIPアドレス(0.0.0.0)で待ち受け
uvicorn.run(app, host="0.0.0.0", port=8000)
サーバーの起動
uv run main.py
これで、http://[PCのIPアドレス]:8000/api/data でJSONを待ち受ける状態になりました。
2. ESP32-S3側の実装 (C言語)
ESP-IDFでHTTP通信を行うには、いくつかのコンポーネントを有効にする必要があります。
CMakeLists.txt の設定
main ディレクトリの CMakeLists.txt で、必要なライブラリ(REQUIRES)を明示的に指定します。
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES json esp_http_client esp_wifi nvs_flash esp_event)
ネットワークとPOST処理 (main.c)
技術的なポイントは、「Wi-Fi接続の完了を待ってから送信タスクを動かす」 という同期処理です。
#include <stdio.h>
#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_log.h"
#include "nvs_flash.h"
#include "esp_http_client.h"
#include "cJSON.h"
// --- 設定項目(環境に合わせて書き換え) ---
#define WIFI_SSID "あなたのSSID"
#define WIFI_PASS "あなたのパスワード"
#define WEB_URL "http://[PCのIPアドレス]:8000/api/data"
static const char *TAG = "HTTP_POST";
// Wi-Fi接続完了の通知用
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
// Wi-Fiイベントハンドラ
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_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGI(TAG, "再接続を試みます...");
esp_wifi_connect();
} 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, "IPアドレス取得: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
// Wi-Fi初期化
void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
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_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));
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
},
};
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());
ESP_LOGI(TAG, "Wi-Fi初期化完了");
}
// HTTP POST送信タスク
static void http_post_json_task(void *pvParameters) {
while (1) {
// 1. JSONオブジェクトの作成
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "temperature", 26.54);
cJSON_AddNumberToObject(root, "humidity", 60);
cJSON_AddStringToObject(root, "device_id", "ESP32-S3-001");
// 2. 文字列に変換(シリアライズ)
char *post_data = cJSON_PrintUnformatted(root);
ESP_LOGI(TAG, "送信するJSON: %s", post_data);
// 3. HTTP Clientの設定
esp_http_client_config_t config = {
.url = WEB_URL,
.method = HTTP_METHOD_POST,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// ヘッダーをJSONに設定
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, post_data, strlen(post_data));
// 4. 送信実行
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %lld",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
} else {
ESP_LOGE(TAG, "HTTP POST 失敗: %s", esp_err_to_name(err));
}
// 5. 後片付け(重要!)
esp_http_client_cleanup(client);
cJSON_Delete(root);
free(post_data);
vTaskDelay(pdMS_TO_TICKS(10000)); // 10秒待機
}
}
void app_main(void)
{
// 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);
ESP_LOGI(TAG, "Wi-Fi接続を開始します");
wifi_init_sta();
// Wi-Fi接続完了を待機
xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
ESP_LOGI(TAG, "Wi-Fi接続完了、HTTP POSTタスクを開始");
xTaskCreate(&http_post_json_task, "http_post_task", 8192, NULL, 5, NULL);
}
🛠 実装のキモ:なぜ動くのか?
1. 「イベントグループ」による同期
Wi-Fiは接続を開始しても、すぐにIPアドレスが割り当てられるわけではありません。xEventGroupWaitBits を使うことで、「IPアドレスを完全に取得してから送信タスクを開始する」 という安全な設計にしています。
2. cJSONによるシリアライズ
C言語で文字列を手動で繋げる(sprintfなど)のは、エスケープ処理やバッファオーバーフローの危険があり、非常に面倒です。cJSON ライブラリを使うことで、安全かつ直感的にJSONを組み立てることができます。
3. メモリ管理の鉄則
ここが最も重要です。
cJSON_CreateObjectで確保したメモリはcJSON_Deleteで消す。cJSON_PrintUnformattedで生成した文字列はfreeで解放する。esp_http_client_initはcleanupする。
これらを忘れると、数時間後にESP32が メモリ不足(Memory Leak)でリブート してしまいます。
動作確認
ESP32にプログラムを書き込み、モニターを起動しましょう。
左:FastAPIの受信ログ、右:ESP-IDFの送信ログ
サーバー側のコンソールに以下のように表示されれば成功です!
--- 届いたデータ ---
デバイスID: ESP32-S3-001
温度: 26.54 ℃
湿度: 60 %
📁 完全なソースコード
今回の実装コード(ESP-IDF側の完全なプロジェクトとFastAPIサーバー)は、GitHubで公開しています:
まとめと次のステップ
今回は固定値を送りましたが、ここを温湿度センサー(SHT3xなど)の値に書き換えれば、立派なIoTシステムの完成です。
さらに発展させるなら…
- HTTPS化: 公開サーバーに送る場合は、SSL証明書を設定してセキュアに。
- Deep Sleep: 送信が終わったらスリープさせて、バッテリー駆動を目指す。
- データベース連携: FastAPI側で取得したデータをSQLiteやPostgreSQLに保存する。
「マイコンからWebへデータが届く」という感覚は、一度味わうと病みつきになります。ぜひ皆さんも試してみてください!