センサーデータを「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_initcleanup する。

これらを忘れると、数時間後にESP32が メモリ不足(Memory Leak)でリブート してしまいます。


動作確認

ESP32にプログラムを書き込み、モニターを起動しましょう。

左:FastAPIの受信ログ、右:ESP-IDFの送信ログ

左:FastAPIの受信ログ、右:ESP-IDFの送信ログ

サーバー側のコンソールに以下のように表示されれば成功です!

--- 届いたデータ ---
デバイスID: ESP32-S3-001
温度: 26.54 ℃
湿度: 60 %

📁 完全なソースコード

今回の実装コード(ESP-IDF側の完全なプロジェクトとFastAPIサーバー)は、GitHubで公開しています:

GitHubで詳細を見る

まとめと次のステップ

今回は固定値を送りましたが、ここを温湿度センサー(SHT3xなど)の値に書き換えれば、立派なIoTシステムの完成です。

さらに発展させるなら…

  • HTTPS化: 公開サーバーに送る場合は、SSL証明書を設定してセキュアに。
  • Deep Sleep: 送信が終わったらスリープさせて、バッテリー駆動を目指す。
  • データベース連携: FastAPI側で取得したデータをSQLiteやPostgreSQLに保存する。

「マイコンからWebへデータが届く」という感覚は、一度味わうと病みつきになります。ぜひ皆さんも試してみてください!