はじめに
前回【設計編】では、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取得できています。
ぶっちゃけ以前と同じですが、下図のようにとても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にするべきでしたね![]()
