はじめに
以前、Raspberry Pi Pico2w + Zephyr RTOS の環境でGoogleのマイコン向けライブラリであるPigweedを使ってRPCを試してみました。
これを拡張して、Cloudでも使えるかと考えてみたのですが、冷静に考えるとAWS IoT等を使うことになるため、MQTT/HTTPなどの高度なIPプロトコルを使うことになります。
そうなると、Pigweedが得意とする「UART等、Rawストリーム上でのパケット多重化(HDLC)」の意味がなくなるということに気づきまして、ネタ切れになりました 方向転換をしました。
普通にProtocol Buffers over MQTTを作ってみてもいいのですが、せっかくなので次世代 Robotics/IoT向けプロトコルである Zenohを使い、Protocol Buffer over ZenohなRPCに挑戦しようと思います。
Protocol BufferのスキーマからServer(マイコン)とClient(PC)のコードを自動生成してgRPCのように開発し、Zenoh上で通信するという壮大な目標です。
成果物はこちらです。
Zenohについて
ZenohとはMQTTのようなIoT向け通信プロトコルですが、
- MQTTや(ROSの既存の)DDSより速い
- リソース消費が少ない
- Broker(Router)方式だけでなく、Peer to Peer通信も可能
- TCP/IPだけでなくSerial/USB-ACMでも利用可能対応
- ROS2の通信Middleware(rmw_zenoh)としても利用可能
という夢のような機能を持った、次世代の通信プロトコルです。
Zenohの詳細については、こちらの解説記事などを参照してもらえればと思います。
今回のシステム
HW/通信構成
ハードウェアとしてはRaspi pico2 w + LED + DHT22(温湿度計) を引き続き使用します。

*デバッグのために、Raspi Debug Probeが追加されています。
通信経路は前回と同様、USB-CDC-ACM or Wi-Fiです。
ZenohはTCPだけでなく、UDP(QUIC)、Serial、Bluetoothなど様々なプロトコルに対応しています。
デバッグ・デバイス製造・立ち上げはUSB-Serialで、運用時はWi-Fiなどの柔軟な運用ができます。
システム構成図としては下記のようになります。
USB/Wi-Fiの違いはZenoh Routerが吸収してくれるのでApplicationとしてはZenoh Routerに接続するのみとなります。
また、Zenoh Routerを将来的にはサーバーに置き換えることで実運用時もシームレスな運用ができます。
通信プロトコル
Zenohには複数の通信形式があるのですが、今回は以下のように使いたいと思います。
- Pub/Subをテレメトリ & ログに使用
- QueriableをRPCに使用
-
Pub/Sub (テレメトリ & ログ):
センサーデータやシステムの稼働ログなど、一方的な通知に使用します。受信側が不在でも送信側は関知せず、軽量にデータを流し続けます -
Query / Queryable (RPC):
設定変更や再起動コマンドなど、実行結果の確認が必要な処理 に使用します。
Zenoh の Queryable 機能を使うことで、マイコン側を「リソース(URL)」として扱い、HTTPのような感覚で遠隔操作(RPC: Remote Procedure Call)を実現しています
もちろん、Pub/SubとQueryを非同期で行うことも可能です。
【コラム】Zenohで長いURL(Key Expression)でも通信が速い理由
「HTTPのようなURL(Key Expression)を使うと、パケットが長くなってマイコンの処理や帯域を圧迫するのでは?」という懸念があるかもしれません。 しかし、Zenohは非常に賢く設計されています。
一度通信が確立されると、Zenohは文字列のURLを Resource IDと呼ばれる短い数値にマッピングします。
実際の通信プロトコル上では、その長いURLを毎回送るのではなく、わずか1〜数バイトのIDとしてやり取りします。これにより、人間にとっての「読みやすさ」と、マイコンにとっての「低負荷・高効率」を完璧に両立させているのです。
Zenoh賢いですね。さすが次世代
ソフトウェアスタック
以下のようになっています。
-
nanopb: マイコン向けのProtocol Bufferライブラリ
-
Zenoh Pico: 組み込み向けのZenohライブラリ
-
Zephyr RTOS: RTOS
ディレクトリ構造
先にですが、ディレクトリ構造はこのようにしました。
環境構築時に、Zephyr関連のライブラリも入りますが、それは省略してます。
.
├── build.py # Build, flash, and monitor script
├── pyproject.toml # Python dependencies (uv)
├── manifest/
│ └── west.yml # Zephyr manifest
├── generator/ # Protobuf code generators
│ ├── gen_client_python.py # Python client code generator
│ └── gen_server_nanopb.py # C++ server code generator
├── apps/
│ └── zenoh_rpc/ # Main application
│ ├── service.proto # Service definition (Protocol Buffers)
│ ├── service.options # NanoPB options (max_size, etc.)
│ ├── main.cpp # Application entry point
│ ├── service_impl.cpp/h # RPC service implementation
│ ├── prj.conf # Zephyr project configuration
│ ├── CMakeLists.txt # CMake build script
│ ├── boards/
│ │ └── *.overlay # Device tree overlay
│ ├── wifi/
│ │ ├── wifi_manager.cpp/h # Wi-Fi connection manager
│ └── rpc/ # Generated code (auto-generated)
│ ├── service.pb.c/h # NanoPB C code
│ ├── service_server.cpp/h # RPC server stub
│ ├── zenoh_rpc_channel.cpp/h # Zenoh RPC channel
│ └── zenoh_pubsub.cpp/h # Zenoh pub/sub utilities
├── tools/ # PC-side Python tools
│ ├── start_router.py # Start Zenoh router
│ ├── configure_wifi.py # Configure Wi-Fi settings
│ ├── example_client.py # Example RPC client
│ └── rpc/ # Generated code (auto-generated)
│ ├── service_pb2.py # Python Protocol Buffers
│ ├── service_client.py # RPC client stub
│ └── zenoh_rpc_client.py # Zenoh RPC client
今回は、この状態でZephyrを立ち上げてスキーマを作り、コード生成までを行います。
通信スキーマとコード生成
スキーマ
通信の中身はProtocol Bufferとしたいので、
下記のように.protoファイルを準備しました。
syntax = "proto3";
package practice.rpc;
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
string zenoh_key = 50001; // Custom option for Zenoh telemetry key
}
message WifiSettings {
string ssid = 1;
string password = 2;
}
message LedRequest {
bool on = 1;
}
message LedResponse {}
message EchoRequest {
string msg = 1;
}
message EchoResponse {
string msg = 1;
}
message EchoRequestMalloc {
bytes msg = 1;
}
message EchoResponseMalloc {
bytes msg = 1;
}
message SensorRequest {}
message SensorTelemetry {
option (zenoh_key) = "/telemetry/sensor";
float temperature = 1;
float humidity = 2;
}
message Empty {}
service DeviceService {
rpc SetLed(LedRequest) returns (LedResponse);
rpc Echo(EchoRequest) returns (EchoResponse);
rpc EchoMalloc(EchoRequestMalloc) returns (EchoResponseMalloc);
rpc StartSensorStream(SensorRequest) returns (Empty);
rpc StopSensorStream(Empty) returns (Empty);
rpc ConfigureWifi(WifiSettings) returns (Empty);
}
RPC
service DeviceServiceをRPCで使います
Server (C++,nanopb): gRPCなどと同様に抽象クラスをコード生成して、それをユーザーが実装する形を取ります
Client (Python): 単にRPCをCallできるClassを作ります
Telemetry
SensorTelemetry をPubSubで使います
Server (C++,nanopb): C++のテンプレートを使い、任意のmessageをSerializeするClassを作れるようにするため、ここは自動生成不要です
Client (Python): XXXXTelmetryをSubscribeすることができるClassを作ります。 Telemetryを受け取った時の処理はユーザーにCallbackとして登録してもらいます
option (zenoh_key) = "/telemetry/sensor";は、Key ExpressionのPrefixです。
コード生成
コード生成はPythonスクリプトで行います。
pythonのprotobufで.protoをparseできるため、そこから自動生成できるようにします。
コード生成用のスクリプトは、ほぼAIに生成してもらいました。(人間より遥かに上手...)
生成されたコードやライブラリ部分は省略しますが、各Service, Publisherの実装は以下のようにできます。
Serviceの実装
生成されたclassを継承して中身を実装します。例えばLED ON/OFFのメソッドは以下の通りです。
zenoh_rpc::RpcStatus DeviceServiceImpl::SetLed(
const practice_rpc_LedRequest& request,
practice_rpc_LedResponse* response) {
LOG_INF("SetLed: on=%d", request.on);
if (log_pub_) {
log_pub_->log_info("LED set to %s", request.on ? "ON" : "OFF");
}
if (request.on) {
LOG_INF("Turning LED ON");
gpio_pin_set_dt(&led, 1);
} else {
LOG_INF("Turning LED OFF");
gpio_pin_set_dt(&led, 0);
}
return zenoh_rpc::RpcStatus::OK;
}
PublisherとService, Serverの登録
mainの初期化で、Zenoh接続後に以下のように登録します。
// Key Expressionのためのdefine
#define DEVICE_ID "pico2w-001"
#define PRACTICE_RPC_SENSOR_TELEMETRY_ZENOH_KEY "/telemetry/sensor"
...
//Publisherの登録
zenoh_rpc::TelemetryPublisher<practice_rpc_SensorTelemetry> sensor_pub(
session_loan, DEVICE_ID, PRACTICE_RPC_SENSOR_TELEMETRY_ZENOH_KEY,
practice_rpc_SensorTelemetry_fields);
zenoh_rpc::LogPublisher log_pub(session_loan, DEVICE_ID);
//Serviceを作成しZenoh Serverに登録
practice::rpc::DeviceServiceImpl service_impl(&sensor_pub, &log_pub);
practice::rpc::DeviceServiceServer server(channel, service_impl);
C++ではありますが、Web開発っぽくていいですね!
まとめ
今回はRaspberry Pi Pico 2w + Zephyr RTOS + ZenohとProtocol Bufferを使ってRPC/Telemetryをする準備をしました。
まるでgRPCのように、キレイな開発ができそうですが、Zephyr + Zenoh Picoという組み込み業界の時間軸では絶賛成長中の新規モノがすんなり立ち上がるとは思えません。
というわけで、次回の実装編に進みます。