0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zenoh Pico上でgRPCライクなRPC開発環境構築【設計編】

0
Last updated at Posted at 2026-02-07

はじめに

以前、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(温湿度計) を引き続き使用します。

image.png
*デバッグのために、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賢いですね。さすが次世代

ソフトウェアスタック

以下のようになっています。

ディレクトリ構造

先にですが、ディレクトリ構造はこのようにしました。
環境構築時に、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という組み込み業界の時間軸では絶賛成長中の新規モノがすんなり立ち上がるとは思えません。

というわけで、次回の実装編に進みます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?