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 Pico 2 W + Zephyr + Zenoh を組み合わせ、Protocol Buffers を使った 「gRPCライクなモダンIoT開発環境」 を構想しました。

今回は 【実装編】 です。 コードを書き、ビルドし、実機で動かします。

結論から言うと、なんとか動きました。

gRPCのようなRPCシステムをIoT/Robotics NativeなZenoh上で動かすことに成功し、とてもスムーズな開発環境が構築できました。

ですが・・・今回も多くの罠にハマったため、そちらも含めて共有します。

成果物はこちらです。

FWの実装

FWの実装については、Zenohの接続方法についてのみ主に記載します。

まず、Wi-Fiに接続します。SSIDやPasswordが登録されてない場合や接続に失敗した場合は諦めて、USBで接続します。

 z_owned_session_t session;
  // Initialize Wi-Fi manager
  wifi::WifiManager& wifi_mgr = wifi::get_wifi_manager();
  if (wifi_mgr.init()) {
    // Check for stored credentials and auto-connect
    if (wifi_mgr.has_stored_credentials()) {
      LOG_INF("Found stored Wi-Fi credentials, connecting...");
      if (wifi_mgr.connect_from_storage()) {
        LOG_INF("Wi-Fi connection initiated");
        // Give some time for Wi-Fi to connect
        k_sleep(K_SECONDS(5));
      } else {
        LOG_WRN("Failed to initiate Wi-Fi connection");
      }
    } else {
      LOG_INF("No stored Wi-Fi credentials");
    }
  } else {
    LOG_ERR("Failed to initialize Wi-Fi manager");
  }

次にZenohに接続します。
Wi-Fi接続してたらWi-Fiで、接続できなかったらUSB接続にします。

  while (true) {
    // Initialize Zenoh configuration
    z_owned_config_t config;
    z_config_default(&config);
    zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_MODE_KEY, "client");
    // Configure transport based on Wi-Fi or USB-ACM connection status
    if (wifi_mgr.is_connected()) {
      LOG_INF("Wi-Fi connected, using TCP connection...");
      zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_CONNECT_KEY,
                       "tcp/" WIFI_ZENOH_ROUTER_ADDR ":" ZENOH_LISTEN_PORT);
      LOG_INF("Connecting to tcp/" WIFI_ZENOH_ROUTER_ADDR
              ":" ZENOH_LISTEN_PORT);
    } else {
      LOG_INF("No Wi-Fi, using USB CDC-ACM serial...");
      // Check DTR before connecting
      if (is_dtr_set(usb_dev) == false) {
        LOG_WRN("DTR not set - waiting for host connection...");
        k_sleep(K_MSEC(1000));
        continue;
      }
      // Use serial link over USB CDC-ACM
      char connect_str[128];
      snprintf(connect_str, sizeof(connect_str), "serial/%s#baudrate=115200",
               usb_dev->name);
      zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_CONNECT_KEY,
                       connect_str);
      LOG_INF("Connecting via %s", connect_str);
    }
    z_owned_session_t session_;
    LOG_INF("Opening Zenoh session...");
    z_result_t res = z_open(&session_, z_config_move(&config), NULL);
    if (res != Z_OK) {
      gpio_pin_toggle_dt(&led);
      LOG_ERR("z_open failed: %d, retrying...", res);
      k_sleep(K_MSEC(1000));
      continue;
    }
    session = session_;
    break;
  }

ポイントは以下です。

Wi-Fi接続の時は以下の設定でz_openしてZenohに接続します。

    zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_MODE_KEY, "client");
    zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_CONNECT_KEY,
                       "tcp/" WIFI_ZENOH_ROUTER_ADDR ":" ZENOH_LISTEN_PORT);

USB接続の時は以下の設定でz_openしてZenohに接続します。

  zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_MODE_KEY, "client");
  char connect_str[128];
  snprintf(connect_str, sizeof(connect_str), "serial/%s#baudrate=115200",
           usb_dev->name);
  zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_CONNECT_KEY,
                   connect_str);

接続した後は、前回記事にもある通り、ServiceやPublisherを登録し、
その後、Telemetryのpublishをループで回しています。 簡略化したものが以下です。

  uint32_t loop_count = 0;
  while (true) {
    loop_count++;
    if (service_impl.is_streaming_enabled()) {
      LOG_INF("Loop %u: Publishing sensor data...", loop_count);
      service_impl.publish_sensor_data();
    } else {
      if (loop_count % 10 == 0) {
        LOG_INF("Loop %u: Streaming disabled", loop_count);
      }
    }
    k_sleep(K_MSEC(1000));
  }

詳細はmain.cppを参照してください。

通信切断後はRebootするようにしています。難しい事を考えるより、そちらが安定すると思ったのでそうしています。

ビルドスクリプト

今回、ビルド・デバッグには「Protocでのコード生成」「Zephyrのビルド」「書き込み」「ログ監視」と手順が複雑になります。

毎回手動でコマンドを打つのは辛いので、これらを一括管理するPythonスクリプト(build.py)を作成しました。

Protocol Bufferのコード生成

python、nanopb、カスタムServer、カスタムClientを一括で生成しています。
ここまで複雑だと、bufなどを使って管理したほうがいいのかもしれませんが、今回はprotocで無理やり行います。

    protoc_cmd = [
        "protoc",
        f"--plugin=protoc-gen-nanopb={NANOPB_GENERATOR}",
        f"--plugin=protoc-gen-custom_client={WORKSPACE_ROOT}/generator/gen_client_python.py",
        f"--plugin=protoc-gen-custom_server={WORKSPACE_ROOT}/generator/gen_server_nanopb.py",
        f"--proto_path={app_path}",
        f"--proto_path={NANOPB_PROTO_PATH}",
        f"--nanopb_opt=-I{app_path}",
        f"--nanopb_out={app_path}/rpc",
        f"--python_out={tools_dir}/rpc",
        f"--pyi_out={tools_dir}/rpc",
        f"--custom_client_out={tools_dir}/rpc",
        f"--custom_server_out={app_path}/rpc",
        str(proto_file),
    ]
    #protoc_cmdをsubprocessで実行...

FWのビルド

Zephyr RTOSのwestコマンドで行います。

west build -p -b rpi_pico2/rp2350a/m33/w apps/zenoh_rpc

FWの書き込み

今回はRaspberry Pi Debug Probeを使います。

具体的にはOpenOCDを使って、こちらもwestコマンドから書き込みできます。

west flush --runner openocd

UARTログ取得

今回は、UARTはログを確認するデバッグ目的だけなので、ひたすらpythonでreadだけ行います。

        with serial.Serial(port, baudrate, timeout=1) as ser:
            while True:
                if ser.in_waiting:
                    data = ser.read(ser.in_waiting)
                    print(data.decode("utf-8", errors="replace"), end="")
                time.sleep(0.01)

これで、Build, Run, Monitorが1コマンドでできます。

Zenoh Routerの準備

apt等からデフォルトでインストールできるZenohd(Zenoh Router)はシリアル通信に対応していません。

なので、gitからソースをcloneしてシリアル通信付きでビルド/インストールします。

git clone https://github.com/eclipse-zenoh/zenoh.git
cargo install --path ./zenohd \
    --bin zenohd \
    --features zenoh/default,zenoh/transport_serial \
    --locked

そして、zenohd を実行します。

zenohd --listen tcp/0.0.0.0:7447 --listen serial//dev/ttyACM0#baudrate=115200

ここで、/dev/ttyACM0は実際には適切なシリアルポートを選びます。
serial//dev/ttyACM0 とserialの後にスラッシュが2つ必要なので注意してください。

WindowsだとCOM3等ですが、LinuxだとDeviceの示すPATHになるのでスラッシュが重なるのです。

今回はスクリプトで、VID:PIDから正しいシリアルポートを選ぶように実装しました。

Python Clientによる動作確認

まず、Serviceを準備します。

    # open session and Create Client
    session = zenoh.open(config)
    rpc_client = ZenohRpcClient(session)
    sub_client = ZenohSubscriberClient(session)

    # Create service client and telemetry subscriber
    device_service = DeviceServiceClient(rpc_client)
    telemetry = TelemetrySubscriber(sub_client, args.device_id)
    log = LogSubscriber(sub_client, args.device_id)
    # Subscribe to telemetry and logs
    telemetry.subscribe_sensor(on_sensor_data)
    log.subscribe(on_log_message)

このように、RPCを実行することができます。

LEDが点灯させるのとStreamingを開始することができます。

    response, _ = device_service.set_led(on=True)
    response = device_service.start_sensor_stream()

センサのテレメトリや、デバイスのログが以下のように出力されます。完璧ですね!!

2026-01-30 06:07:21,363 - __main__ - INFO - Sensor: temp=23.50°C, humidity=44.40%
2026-01-30 06:07:19,534 - __main__ - INFO - Device Log: [INFO] Sensor streaming started

動作としては、こんな感じです。LチカしながらTelmetry取得できています。

ezgif-113ebb6e1c36ff79.gif

ぶっちゃけ以前と同じですが、下図のようにとてもIoT Nativeな構成なのです! だからどうした

ここまで、Repository上では、全部まとめてDevcontainer化しているので、おそらく同じ手順で環境構築できると思います。

まとめ

Raspberry Pi Pico 2 W + Zephyr + Zenoh で、Protocol Buffers をベースにした **「型安全かつ高速なRPC/Telemetryシステム」**を構築しました。

「環境構築さえ乗り越えれば」、とてもスムーズな開発体験ができました。

.proto を編集するだけで通信仕様が更新され、アプリケーションロジックに集中することができます。

まさに gRPC でマイクロサービスを書いているような感覚を、マイコン上で実現することができました。

ここまでできたら、GUIも自動生成したくなりました。
というわけで、続きはこちらです。

ーーーーーーー

さて 以降が、ある意味本番であり、現実です。

    「環境構築さえ乗り越えれば」 

の中身です。

【番外編】トラブルシューティング

今回も多くの罠がありましたが、今回 引っかかった罠のうち、大物のみを記載します。

Zenoh-Pico用のKconfigが効かない問題

Zenoh PicoにはZephyr用のKconfigがあり、

config ZENOH_PICO_LINK_SERIAL
	bool "Serial Link"
	help
	  Use serial link

上記を設定することでSerial通信ができると思いましたが、そのKconfig自体が全く効きませんでした。
*現実には、何が悪いのかわからず五里霧中状態

ビルドでもWarningが出ていたので、調査したところ、ここのconfig.hが何してもKconfigを上書きすることに気づきました・・・

#define Z_FEATURE_LINK_SERIAL 0

今回は、zenoh_generic_config.hというものを作って上書きしました。

後でこちらに修正のプルリクが出ていることに気づきました・・・

USB SerialのDTR未接続によるハング問題

上記を問題を解決したところ、FWがハングするようになりました。 (それは解決なのか?)

調査したところFW上では、USBのDTRが立っているのを確認してから(= Zenoh Routerとの通信が確立してから)、z_openしないといけないようでした。

なぜなら、今回は PC側がListenしてデバイス側がConnectしにいく、という動作だからです。普通と逆ですね。

なのでPC側がListenしてないのに、Connectしにいくと、FWがおそらく通信待ちでハングします。
TCPならハングせずエラーが起こるのですが、シリアルだとハングするものなのかなと思われます。

上記のKConfig効かない問題との合わせ技で、泥沼状態になりました。

Serialの接続文字列わからない問題

最終的に、以下のような文字列を構成して接続しにいけば成功しました。

      char connect_str[128];
      snprintf(connect_str, sizeof(connect_str), "serial/%s#baudrate=115200",
               usb_dev->name);
      zp_config_insert(z_config_loan_mut(&config), Z_CONFIG_CONNECT_KEY,
                       connect_str);
  • usb_dev->nameではない CDC_ACM_0 固定だ
  • Zenoh-Picoではserial/cdc_acm0:115200 と短縮するのだ

など、様々な誤情報が飛び交い、苦戦しました。

Publisherが1つしか設定できない問題

そんなこんなで、やっと動いたのですが、まだまだ問題は続きます。

今回、TelemetryPublisherとLogPublisherの2つをPublisherとして立ち上げているのですが、2つ目のPublisherが立ち上がらないという問題が発生しました。

ログを出力させたところ、OS側からENOMEMエラー(メモリ不足)が出ていて、Publisherが立ち上げられないとのことでした。

そこで、信頼するAI様に聞いても、

  • Heapが足りない
  • Stackが足りない
  • XXX_MAX_COUNTをKconfigとして設定が必要 → 存在しない

と言われて、空振りばかりで、、、

"そんなわけあるか!!"

と叫びながら、Zenoh-Picoのコードの奥までデバッグしたところ、Mutexの取得に失敗していることがわかり、Mutexの数(固定)を増やすことで対応できました。 

CONFIG_MAX_PTHREAD_MUTEX_COUNT=16

AI様の名誉のためにいうと、Mutexを増やすKconfigは一発で正しいものを教えてくれました。
ここまで来て、ようやくUSBシリアルでZenohが通りました。

情報の少ないシリアル通信を使おうとした私が悪かった。最初はWi-Fiにするべきでしたね:disappointed_relieved:

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?