2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESP32でgRPC(Protobuf)通信!Nanopbを使って爆速・軽量な自動ビルド環境を構築する(ローカル疎通)

Posted at

はじめに

IoT開発において、マイコンとサーバー間の通信にJSON(HTTP/1.1)を使用するのは一般的ですが、リソースの限られた環境では「シリアライズ負荷」「ペイロードの肥大化」「型定義の曖昧さ」が無視できない課題となります。

本連載では、Googleが開発したバイナリ形式のシリアライズ規格 Protocol Buffers (Protobuf) をマイコン向けに最適化した Nanopb を用い、ESP32-S3からローカルPCへ軽量なデータを送信・デコードするまでの工程を解説します。

理論的背景の解説はこちら
実装に入る前に、gRPCの仕組みやJSONとの比較を詳しく知りたい方は、以下の概念図解記事を併せてご覧ください。
マイコン開発の新常識?ESP32でgRPCを動かすための「理論のハブ」

1. Nanopbとは何者か?:リソース制限下での「最適解」

gRPCは本来リッチなリソースを前提としていますが、ESP32で動作させるにはフルスタックのgRPCライブラリは重すぎます。そこで、マイコン向けに特化し、静的なメモリ割り当てを基本とする Nanopb を採用します。

Nanopbは、.proto ファイルからC言語の構造体と、エンコード/デコード用のメタデータ(フィールド情報)を生成するライブラリです。

Nanopbのアイデンティティ

Nanopbは、ANSI C実装のProtobufライブラリです。Google公式のProtobufはC++がメインであり、動的メモリ割り当てを多用するため、数KBのRAMをやりくりする埋め込みシステムには贅沢すぎます。

  • 極小のフットプリント: ROM使用量はわずか2〜10KB、RAM使用量にいたっては1KB以下に抑えることが可能です。
  • 実績: 実は非常に身近なところで使われており、Android、iPhone、Garmin製品などの内部通信でも採用されています。
  • 静的アロケーション: すべてのフィールドサイズをビルド時に確定させ、ランタイムでのメモリ断片化(Heapの食いつぶし)を完全に防ぎます。

2. インターフェース定義:.proto と .options

2.1. センサーデータの定義 (sensor.proto)

まず、通信するデータの構造を言語非依存の形式で定義します。本構成では、デバイスID、温度、湿度の3項目を定義しました 。

syntax = "proto3";

message SensorData {
    string device_id = 1;
    float temperature = 2;
    int32 humidity = 3;
}

2.2. Nanopb特有の制約を制御する (sensor.options)

Nanopbにおいて、string 型の最大サイズを指定しないと、デフォルトでは「コールバック形式」で生成され、実装が非常に煩雑になります。これを防ぐために、.options ファイルでサイズを固定し、静的なchar配列 として生成させるのが「マイコン流」です 。

SensorData.device_id max_size:32

3. ESP-IDF ビルドシステムへの統合 (CMake)

idf.py build を叩いた際、自動的にPythonスクリプトが走り、最新の .proto から .c/.h を生成してプロジェクトにリンクさせる設定を main/CMakeLists.txt に記述します 。

ESP-IDFのビルドシステム(CMake)は非常に強力ですが、独自の挙動を知らないと今回のエラーのように「昨日まで動いていたのに急にビルドできなくなった」という罠に陥ります。

特に重要な 「早期展開(Early Expansion)」 の仕組みと、なぜこの条件分岐が必要なのかについて、技術的な詳細を深掘りして解説します。

🛠️ CMakeLists.txt の解説

ESP-IDFのビルドは、通常のCMakeとは異なり 「2段階」 でスクリプトを読み込みます。この特性を理解することが、nanopbをスムーズに統合する鍵です。

1. 「早期展開(Early Expansion)」という門番

ESP-IDFは、プロジェクト全体のコンパイルを始める前に、すべてのコンポーネントをスキャンして「どのコンポーネントが何に依存しているか」というグラフを作成します 。

  • 課題: このスキャン中、CMakeは「スクリプトモード」として動作します。このモードでは、ビルドターゲットを定義する add_custom_command などの命令を実行することが禁止されており、無視して記述すると 「command is not scriptable」 という致命的なエラーで停止します 。

  • 対策: そこで if(NOT CMAKE_BUILD_EARLY_EXPANSION) を使い、「今は依存関係を調べているだけ(早期展開)なら、重いビルド処理は読み飛ばしてね」と明示的に指示する必要があります 。

2. ディレクトリ作成のタイミング問題

ビルドシステムは、idf_component_registerINCLUDE_DIRS に指定されたパスが 「実在するか」 を厳格にチェックします 。

  • 落とし穴: コード生成先である generated フォルダをビルド時(if の中)に作ろうとすると、その前の「早期展開」の段階で「指定されたディレクトリが見つからない!」とエラーが出てしまいます 。

  • 解決策: したがって、file(MAKE_DIRECTORY ...) だけは条件分岐の外に出し、スキャンが始まる瞬間に「空でもいいから箱だけは作っておく」という前処理が必要になります 。

3. 生成ファイルと main.c の結合

Protobufで生成される .pb.c.pb.h は、ビルドが始まるまで存在しません。これらを安全にコンパイルに組み込むために、以下のテクニックを使っています 。

  • target_sources: コンポーネントのライブラリ(${COMPONENT_LIB})に対して、動的に生成されるソースファイルを後付けで追加します。

  • OBJECT_DEPENDS: main.c のプロパティとして設定します。これにより、「main.c をコンパイルする前に、必ず .pb.h の生成(Pythonスクリプトの実行)を完了させてね」という依存関係をCMakeに教え込み、ビルド順序の破綻を防ぎます。

💡 修正後の CMakeLists.txt 全容

これらを踏まえた、構成がこちらです。

# 1. パスの確定とディレクトリの強制作成
# idf_component_register のパスチェックをパスするために、ifの外で即座に実行する
set(GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
file(MAKE_DIRECTORY "${GEN_DIR}")

# 2. コンポーネントの登録
# 依存関係(REQUIRES)を定義し、生成先をインクルードパスに含める
idf_component_register(
    SRCS "main.c"
    INCLUDE_DIRS "." "${GEN_DIR}"
    REQUIRES nanopb nvs_flash esp_wifi esp_http_client esp_event
)

# 3. 実際のビルドロジック(実際のコンパイルフェーズのみ実行)
if(NOT CMAKE_BUILD_EARLY_EXPANSION)
    set(NANOPB_GEN "${CMAKE_SOURCE_DIR}/components/nanopb/generator/nanopb_generator.py")
    set(PROTO_DIR "${CMAKE_CURRENT_LIST_DIR}/proto")
    set(PROTO_FILE "${PROTO_DIR}/sensor.proto")
    set(OPTIONS_FILE "${PROTO_DIR}/sensor.options") 
    
    set(GEN_SRC "${GEN_DIR}/sensor.pb.c")
    set(GEN_HDR "${GEN_DIR}/sensor.pb.h")

    # Pythonによる自動コード生成コマンド
    add_custom_command(
        OUTPUT "${GEN_SRC}" "${GEN_HDR}"
        # -I でプロトファイルのディレクトリを指定し、protocのパス解決を助ける
        COMMAND python "${NANOPB_GEN}" -I "${PROTO_DIR}" -D "${GEN_DIR}" "${PROTO_FILE}"
        # .proto だけでなく .options が更新された時も再生成をトリガーする
        DEPENDS "${PROTO_FILE}" "${OPTIONS_FILE}"
        VERBATIM
    )

    # 生成されたCソースをビルド対象に追加
    target_sources(${COMPONENT_LIB} PRIVATE "${GEN_SRC}")
    
    # main.c が生成ヘッダーに依存していることを明示(コンパイル順序の制御)
    set_source_files_properties("main.c" PROPERTIES OBJECT_DEPENDS "${GEN_HDR}")
endif()

このように「ESP-IDFのビルドプロセスの多段性」を考慮した設計にすることで、開発者は .proto ファイルを更新して Build ボタンを押すだけで、常に最新のインターフェースが C言語と Python の両方に反映される、モダンな開発環境を手に入れることができます。

この CMake の挙動は、ESP-IDF で自作のコード生成ツール(画像変換やフォント生成など)を組み込む際にも応用できる、一生モノのテクニックと言えるでしょう。

4. ESP32側:実装とエンコード処理

main.c では、30秒ごとに のランダムな温度データなどを生成し、NanopbでシリアライズしてHTTP POST送信します。

シリアライズのポイント

必ず SensorData_init_default で初期化を行い、pb_ostream_from_buffer で出力ストリームを作成します。

uint8_t buffer[128];
SensorData message = SensorData_init_default; // 必須:デフォルト値で初期化

// データのセット
snprintf(message.device_id, sizeof(message.device_id), "ESP32-S3-GRPC-LAB");
message.temperature = temperature;
message.humidity = humidity;

// シリアライズ(バイナリ化)
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&stream, SensorData_fields, &message)) {
    ESP_LOGE(TAG, "Encoding failed!");
}

5. 受信側:uv + Flask によるモダンな検証環境

検証サーバーは Python の高速パッケージマネージャー uv を使用します。sensor_pb2.py を通じて受信したバイナリをデコードします。

# Python用コード生成(依存関係をその場だけ解決)
uv run --with grpcio-tools python -m grpc_tools.protoc -I=main/proto --python_out=. main/proto/sensor.proto

# サーバー起動(Flask + protobuf)
uv run --with flask --with protobuf python server.py

6. まとめ:26バイトに凝縮されたデータ

Windowsやキュリティソフトなどのファイアウォール設定(ポート5000の解放)をクリアすれば、PC側で見事にデータが復元されます。

検証結果:

--- Received Binary (Size: 26 bytes) ---
Hex: 0a 11 45 53 50 33 32 2d 53 33 2d 47 52 50 43 2d 4c 41 42 15 66 66 c6 41 18 37
Device ID: ESP32-S3-GRPC-LAB
Temp:      24.8 °C
Humidity:  55 %

image.png

JSONでは60バイトを超えていたペイロードが、型情報を維持したままわずか 26バイト に凝縮されました。この効率性と開発体験こそが、gRPCをマイコンに導入する最大のメリットです。

次回はステージをクラウドへ移し、Azure Functions でこのバイナリを受け取ります!


📦 ソースコード

本プロジェクトの全ソースコードは GitHub で公開しています。
GitHub: esp32-grpc-lab-sample


2
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?