はじめに

ℹ️ 前回の記事
👉 ESP32-S3からAzureへデータを飛ばす!HTTPS POSTとサーバーレス連携の極意
(Azure FunctionsへのHTTPS/JSON通信の実装方法を解説しています)

前回、Azure FunctionsへJSONデータを送信することに成功しました。しかし、そこで直面したのは「たった数個のデータを送るのに、なぜこんなに通信が重いのか?」という疑問です。

今回は、その解決策となる gRPC / Protocol Buffers (Protobuf) の概念を、わかりやすく解説します。


1. JSONは「無駄」が多い? — データ構造の比較

HTTP/JSON通信は便利ですが、リソースの限られたマイコンにとっては非常に非効率な形式です。なぜ非効率なのか、具体的なデータサイズを見ながら理解していきましょう。

JSON:説明書付きの巨大な段ボール

JSONは人間にとって読みやすいテキスト形式ですが、これが逆にデータサイズを大きくする原因になっています。

JSONは、データと一緒に「そのデータが何であるか」という説明(キー文字列)を毎回送ります。例えば、温度と湿度のセンサーデータを送る場合を考えてみましょう。

  • : {"temp": 24.8, "humi": 55, "id": "ESP32-S3"}
  • 実際のバイト数: この文字列は約45バイトになります
  • 無駄な部分: "temp""humi" というキー名、クォーテーション、コロン、カンマなどの記号類がすべてバイト数を消費します
  • 問題点: 同じセンサーから1時間に1回データを送るだけでも、1日で約1KBのデータ量になります

さらに、JSONはテキスト形式なので数値も文字列として送られます。例えば 24.8 という数値は、バイナリなら4バイトで済むところを、4文字分(4バイト)のテキストとして送信されるのです。

Protobuf:中身を知っている者同士の「真空パック」

Protobuf(Protocol Buffers)は、Googleが開発したバイナリ形式のデータフォーマットです。事前に「設計図(.proto)」を共有することで、説明書きをすべて削ぎ落とします。

  • 仕組み: 「1番目のデータは温度(float型)」「2番目は湿度(int型)」「3番目はデバイスID(string型)」というルールを双方が知っているため、実際の通信ではバイナリデータのみを並べて送ります
  • データの持ち方: JSONの {"temp": 24.8} が、Protobufでは [0x0D, 0x00, 0x00, 0xC6, 0x41] のような数バイトのバイナリになります
  • 結果: 同じ内容でもサイズは 1/3以下 に圧縮されます。先ほどの45バイトのJSONデータが、Protobufなら15〜20バイト程度で済みます

この差は、バッテリー駆動のIoTデバイスにとって非常に重要です。データ量が減れば送信時間が短くなり、その分電力消費も削減できるのです。

JSONとProtobufのデータサイズ比較:テキスト形式とバイナリ形式の差

JSONとProtobufのデータサイズ比較:テキスト形式とバイナリ形式の差


2. なぜ「1バイト」の節約が、ESP32の寿命を変えるのか

「たかだか数十バイトの差なんて…」と思うかもしれません。しかし、マイコンにおいてこの差は 「電気代(寿命)」 に直結します。特にバッテリー駆動のIoTデバイスでは、データサイズの最適化が動作時間を左右する重要な要素なのです。

電力消費の直結:送信時間=電力消費

ESP32のWi-Fi通信における電力消費を理解するには、次の3つのフェーズを知る必要があります。

  1. スリープモード: 約10μA(ほとんど電力を使わない)
  2. Wi-Fi接続中: 約80〜170mA(接続を維持するだけで電力を消費)
  3. データ送信中: 約240mA(最も電力を消費)

Wi-Fiチップが電波を飛ばす時間は、送信データ量に比例します。45バイトのJSONを送るのと、15バイトのProtobufを送るのでは、送信時間が約3倍違います。つまり、電波を飛ばしている時間(=高電力状態の時間)が3分の1になるのです。

パケットが小さければ送信時間は短くなり、それだけ早く Deep Sleep に戻れます。例えば、1日100回データを送信するセンサーの場合:

  • JSON(45バイト): 送信時間 約3秒 × 100回 = 300秒/日
  • Protobuf(15バイト): 送信時間 約1秒 × 100回 = 100秒/日

この差が積み重なると、バッテリー寿命が数ヶ月単位で変わってくるのです。

メモリの断片化防止:安定動作の鍵

ESP32のRAMは限られています(ESP32-S3でも約520KB)。この限られたメモリを効率的に使うことが、安定動作の鍵となります。

cJSON 等によるJSON処理では、文字列の動的な生成と解析が必要です。これには以下の問題があります:

  • 動的メモリ確保: JSONパース時に何度も malloc() / free() が呼ばれ、メモリが断片化します
  • 文字列操作のオーバーヘッド: "temperature" という文字列を毎回検索してパースする必要があります
  • 予測不可能なメモリ使用量: JSONの構造が複雑になると、必要なメモリ量が読めません

バイナリ処理のProtobufは、メモリ消費が予測可能で安定します:

  • 固定サイズのバッファ: 事前にメモリを確保できるため、断片化が起きません
  • 直接アクセス: 「1番目のフィールドは温度」と決まっているため、文字列検索が不要です
  • スタック上で処理可能: 小さなデータなら動的メモリ確保すら不要になります

これにより、長時間稼働してもメモリ不足でフリーズすることがなくなります。


3. RPC と gRPC の違いを整理する

「通信」といえばHTTPやREST APIが有名ですが、RPCはそれらとは少し毛色が違います。まずは、RPCという考え方の基本から理解していきましょう。

RPC(Remote Procedure Call)とは?

直訳すると「遠隔手続き呼出し」です。これは1980年代から存在する古典的な通信の考え方ですが、その本質は今でも非常に強力です。

通常、ネットワーク通信を書くときは次のような手順を踏みます:

// 従来のHTTP通信(REST API)
1. HTTPクライアントを初期化
2. URLを構築"https://example.com/api/sensor"
3. HTTPヘッダーを設定Content-Type: application/jsonなど
4. JSONデータを文字列に変換
5. POSTリクエストを送信
6. レスポンスを受信
7. JSONをパースして結果を取得

これだけのステップを毎回書くのは大変です。しかし、RPCの考え方はもっと直感的です。

「ネットワークの向こう側にある関数を、自分の手元の関数と同じ感覚で呼び出す」。これがRPCの核心です。

// RPCの理想的な形
SensorData data = {24.8, 55, "ESP32-S3"};
Response response = remote_send_sensor_data(&data);  // ←これだけ!

まるでローカルの関数を呼んでいるかのように、ネットワーク通信ができる。これがRPCの魅力です。通信の詳細(HTTPメソッド、ヘッダー、シリアライズなど)はすべてフレームワークが自動で処理してくれます。

RPCの概念:ネットワークを越えた関数呼び出し

RPCの概念:ネットワークを越えた関数呼び出し

gRPC とは?

このRPCという考え方を、Googleが最新技術で「超高速・超軽量」に作り替えたのが gRPC です。gRPCの「g」は「Google」を意味していますが、今では多くの企業で採用されています(Netflix、Uberなど)。

gRPCが従来のHTTP/JSON通信と何が違うのか、3つのポイントで比較してみましょう。

進化のポイント 従来のHTTP/JSON gRPC
運搬方法 HTTP/1.1 (1回ごとに接続を切る、遅い) HTTP/2 (太いパイプを繋ぎっぱなし、速い)
データ形式 JSON (人間が見やすいテキスト) Protobuf (機械が読みやすいバイナリ)
安全性 ルーズ (中身が違っても実行まで気づかない) 厳格 (設計図がないとビルドすらできない)

運搬方法:HTTP/2の威力

HTTP/1.1では、リクエストを送るたびに接続を確立し直す必要がありました(Keep-Aliveを使っても制限があります)。これは、家に荷物を届けるたびに、配達員が倉庫と家を往復するようなものです。

HTTP/2では、一度接続を確立したら、その「太いパイプ」を通じて複数のリクエストを同時に流せます。さらに、ヘッダー圧縮やサーバープッシュなどの機能により、通信効率が大幅に向上します。

データ形式:バイナリの優位性

JSONはテキストなので、人間がブラウザで見て理解できます。しかし、機械同士の通信では、この「人間が読める」という特性は無駄でしかありません。

Protobufはバイナリ形式なので、機械が直接読み書きできます。パース(解析)の速度も、JSONの5〜10倍高速です。

安全性:型システムによる保護

REST APIでは、「このエンドポイントはどんなデータを受け付けるのか」という仕様がドキュメント(Swagger/OpenAPIなど)に書かれているだけです。実際にコードを実行するまで、データの型が合っているかわかりません。

gRPCでは、.proto ファイルという「設計図」が必須です。この設計図と違うデータを送ろうとすると、コンパイル時(ビルド時)にエラーになります。実行する前にミスに気づけるのです。


4. なぜ「gRPC」が最強なのか — 設計図(.proto)の力

gRPCの凄さは、「共通の設計図(.proto)」 を中心とした開発スタイルにあります。この設計図が、マイコン開発における多くの問題を解決してくれるのです。

ステップ1:設計図の共有

最初に「どんなデータを送り、どんな関数を呼ぶか」を .proto ファイルに書きます。これは一種の「契約書」のようなものです。

例えば、センサーデータを送る場合の .proto ファイルはこんな感じになります:

// sensor.proto
syntax = "proto3";

message SensorData {
  float temperature = 1;  // 1番目のフィールド:温度
  int32 humidity = 2;     // 2番目のフィールド:湿度
  string device_id = 3;   // 3番目のフィールド:デバイスID
}

service SensorService {
  rpc SendData(SensorData) returns (Response);  // データを送る関数
}

この .proto ファイルをESP32側とサーバー側の両方で共有します。これにより、双方が「どんなデータをやり取りするか」を完全に理解した状態で開発が始められます。

共通の設計図(.proto)を中心とした開発:ESP32とサーバーが同じルールを共有

共通の設計図(.proto)を中心とした開発:ESP32とサーバーが同じルールを共有

重要なポイント

  • 言語非依存: .proto ファイルは言語に依存しません。C言語、Python、Go、JavaScriptなど、どの言語でも使えます
  • バージョン管理: この設計図をGitで管理すれば、データ構造の変更履歴が追跡できます
  • ドキュメントとしても機能: コメントを書いておけば、それがそのままAPIドキュメントになります

ステップ2:コードの自動生成

この設計図を専用のツール(Protocol Buffers Compiler、通称 protoc)に通すと、各言語のプログラムが自動生成されます。

# ESP32(C言語)用のコード生成
protoc --nanopb_out=. sensor.proto
→ sensor.pb.h と sensor.pb.c が生成される

# サーバー(Python)用のコード生成
protoc --python_out=. sensor.proto
→ sensor_pb2.py が生成される

生成されたコードには、以下のものが含まれています:

  • データ構造の定義: C言語なら struct、Pythonなら class として定義されます
  • シリアライズ関数: データをバイナリに変換する関数
  • デシリアライズ関数: バイナリをデータに戻す関数
  • 検証関数: データが正しいかチェックする関数
Protocによるコード自動生成:.protoファイルから各言語のコードが生成される

Protocによるコード自動生成:.protoファイルから各言語のコードが生成される

これが意味すること

開発者は、シリアライズやデシリアライズのコードを一切書く必要がありません。設計図を書けば、面倒なバイナリ処理は全部ツールがやってくれるのです。

また、設計図を変更すれば、コードも自動で更新されます。例えば「気圧センサーを追加したい」と思ったら、.proto ファイルに float pressure = 4; を追加して、再度 protoc を実行するだけ。ESP32側もサーバー側も、自動的に新しいフィールドに対応したコードが生成されます。


5. マイコン(ESP32)でやるメリットと難易度

ここまでgRPCの良さを説明してきましたが、「実際にESP32で使うとどうなのか?」という疑問にお答えします。

メリット:型(型安全)で守られる

マイコン開発で一番怖いのは「データの型崩れ」です。特にJSON通信では、次のような悲劇がよく起こります。

よくある失敗例(JSON)

// ESP32側で送信(間違ってstring型で送ってしまった)
cJSON_AddString(json, "temperature", "24.8");  // ←数値なのに文字列!
# サーバー側で受信
temp = data['temperature'] * 1.8 + 32  # ←エラー!文字列と数値の演算

このミスは、実際に実行するまで気づきません。デバッグに数時間かかることも珍しくありません。

gRPCの場合

// ESP32側(.protoでfloat型と定義されている)
SensorData data;
data.temperature = "24.8";  // ←ビルドエラー!float型にstringは入らない

gRPCでは、設計図(.proto)と違うデータを送ろうとすると、ビルドの時点でエラーになります。コンパイラが「型が合ってないよ!」と教えてくれるのです。

「動いているなら、通信形式は絶対に正しい」という安心感は絶大です。これは大規模なシステムになればなるほど、その価値が増していきます。

その他のメリット

  • ドキュメント不要: .proto ファイルが仕様書になるため、別途APIドキュメントを書く必要がありません
  • 後方互換性: フィールドを追加しても、古いバージョンのクライアントが壊れない仕組みが標準で備わっています
  • 多言語対応: サーバーをPythonからGoに変更しても、.proto ファイルが同じなら通信コードの変更は不要です

難易度:リソース制限との戦い

良いことばかりに聞こえるgRPCですが、マイコンで使うには課題もあります。

gRPCは本来、Googleのデータセンター内の高性能サーバー間通信のために作られました。そのため、フル機能のライブラリ(grpc-c++など)はESP32には重すぎます。メモリも計算リソースも足りません。

そこで、マイコン向けに最適化された軽量版の 「nanopb」 を使います。nanopbは以下の特徴があります:

  • 小さいフットプリント: フル版の1/100以下のメモリ使用量
  • 動的メモリ不要: すべてスタック上で処理可能(malloc/freeが不要)
  • C言語で実装: C++ではなくC言語なので、軽量で移植性が高い

ただし、制約もあります

  • フル機能のgRPCではない: HTTP/2やストリーミングは使えず、Protobufのシリアライズ機能のみ
  • 手動でHTTP通信を実装: gRPCの「関数を呼ぶだけ」という理想形ではなく、自分でHTTPリクエストを組み立てる必要があります

最大の山場

この「nanopb」をESP-IDFのビルドシステム(CMake)に組み込み、.proto ファイルから自動でC言語のコードを生成する環境を作るまでが、一番の難所になります。

具体的には:

  1. nanopbのライブラリをESP-IDFプロジェクトに追加
  2. .proto ファイルを配置
  3. CMakeLists.txtにカスタムビルドルールを追加
  4. ビルド時に自動で .proto.pb.c / .pb.h を生成する仕組みを構築

このセットアップには、CMakeの知識とESP-IDFのビルドシステムの理解が必要です。しかし、一度環境を作ってしまえば、あとは .proto ファイルを編集するだけで、通信コードが自動生成される快適な開発環境が手に入ります。


まとめ:贅沢な通信を、小さなマイコンに

正直に言えば、ESP32でgRPCを動かすのは、JSONに比べれば初期セットアップに手間がかかります。CMakeの設定、nanopbの組み込み、自動生成の仕組み作りなど、最初の壁は決して低くありません。

しかし、一度環境を作ってしまえば、その後の開発は驚くほど快適になります:

  • 圧倒的に軽い: データサイズは1/3以下、送信時間も短縮され、バッテリー寿命が伸びる
  • 絶対に型が壊れない: ビルド時にエラーが出るため、実行時のトラブルが激減
  • メンテナンスが楽: 新しいセンサーを追加したい?.proto に1行書いて再ビルドするだけ
  • スケールしやすい: デバイスが10台になっても100台になっても、型安全性が開発を守ってくれる

さらに、この技術を身につければ、単なる趣味の電子工作ではなく、商用レベルのIoTシステムを設計できるようになります。実際、多くの企業が本番環境でgRPCを使っています。

「できるかどうか」ではなく、「やる価値があるから、やってみる」。 この「贅沢な挑戦」の先に、IoT開発の新しい景色が待っています。