Unitree Go2をteleopしてみた

この記事はNTT docomo Business Advent Calendar 2025 9日目の記事です。

Unitree Go2はROSの通信ミドルウェアとしてEclipse Cyclone DDSを利用していますが、DDSはNATを越えられないという課題があります。 この課題に対し、DDSをZenohにブリッジしてNAT越えを実現する事例がコミュニティでいくつか紹介されています(11, 22, 33)。

本記事ではこのアプローチをUnitree Go2に適用し、zenoh-plugin-ros2ddsを用いて Unitree Go2が扱うDDSメッセージをインターネット越しに送受信する方法を紹介します。

はじめに

こんにちは。イノベーションセンターの柴原です。普段はエッジコンピューティング基盤技術の検証や生成AIアプリケーションの開発に取り組んでいます。

フィジカルAIという言葉を聞いたことがあるでしょうか。生成AIの次に来るテーマとして注目されており、物理世界を理解して自律的に行動するAIを指します。 フィジカルAIの発展により、ロボットがこなせるタスクの幅は飛躍的に広がっています。 一方で、ロボットにはバッテリー容量や搭載できる計算リソース量に制約があります。 これらの制約を克服するためには、クラウドをはじめとするロボット外部の計算リソースを活用することが不可欠になると考えています。

そこで本記事では、クラウドからロボットを制御するための第一歩として、キーボード入力でロボットを操作する簡単なデモを作成したので紹介します。

環境

次のような環境で実装しました。

  • 機種: Unitree Go2 R&D Plus
  • Docking Station (Jetson Orin NX):Ubuntu 22, ROS 2 Humble
  • クラウド (Azure VM): Ubuntu 22, ROS 2 Humble

前提知識

Unitree Go2

Unitree Robotics社の小型四足歩行ロボットです。今回扱うGo2 R&D Plusは公式SDKを利用して二次開発ができるモデルです。

ROS

ROS (Robot Operating System)はロボットのソフトウェア開発においてデファクトスタンダードのプラットフォームです。 通信方法やセンサ値のデータ構造、パッケージ管理機能を提供しており、Unitree Go2もROSを利用した二次開発が可能です。

Zenoh

Unitree Go2はROSに対応していますが、その通信ミドルウェアはCyclone DDSに固定されています。 Cyclone DDSは隣のROSノードを自動発見するためにマルチキャストを使用するなどLAN向けに設計されており、NAT越えが困難です。 一方ROSの世界ではWANに対応した通信ミドルウェアとしてZenohが注目されています。現在ROSの最新バージョンであるJazzyでは公式にサポートされているようです。 Zenohの提供元であるEclipseはZenoh・Cyclone DDS間のブリッジも提供しており、これを利用してインターネット越しの通信を実現している事例がいくつかあります。

本記事ではZenohとブリッジを利用してインターネット越しにGo2のセンサ値を読み取り、キーボードからGo2を操作するところまでを実装します。

実装

TechShare社の【Unitree Go2】キーボードからGo2を操作する2次開発方法を基に、これをインターネット越しで実行します。

ビルド

.
└── workspace/
    ├── docker/
    │   ├── Dockerfile.azure
    │   ├── Dockerfile.jetson
    │   └── docker-compose.yml
    ├── src/
    │   ├── ros/
    │   │   ├── unitree_ros2/
    │   │   └── cmd_vel_control/
    │   ├── zenoh/
    │   └── zenoh-plugin-ros2dds/
    ├── zenoh-config-azure.json
    └── zenoh-config-jetson.json

Docking StationのOS・ROS環境は、unitree_ros2.devcontainer/docker-compose.yamlで定義されているdevcontainer-humbleサービスを使います。 クラウド側マシンでも同じサービスを、ベースイメージをARMのものからx64のものに変更して使います。 docker/はこれらを移動しただけです。

src/配下に利用するパッケージを配置しています。

zenoh-plugin-ros2ddsはzenohに依存しており、バージョンによってビルドできないことがあるのでcommitを指定しています。これらをクラウドとJetsonそれぞれに配置し、コンテナ内でsrc/をビルドします。

Clone

git clone https://github.com/shibahara2/ros2_ws.git
cd ros2_ws
git submodule udpate --init --recursive

コンテナに入ります。

cd docker
docker compose up unitree_ros2-<azure or jetson> -d
docker exec -it unitree_ros2-<azure or jetson> zsh

Rustをインストールします。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source /root/.cargo/env
rustup update

zenohをビルドします。

cd src/zenoh
cargo build --release

zenoh-plugin-ros2ddsをビルドします。

cd src/zenoh-plugin-ros2dds
cargo build --release

ROSパッケージをビルドします。

cd src/ros
colcon build

実行

クラウド上でzenohdを起動します。

# Cloud terminal 1
src/zenoh/target/release/zenohd -c zenoh-config-azure.json

ポート7447でクライアントを待ちます。モード (router/peer/client)やプラグインのPATHを以下のjsonで設定しています。

$ cat zenoh-config-azure.json
{
    mode: "router",
    plugins: {
        ros2dds: {
            __path__: "src/zenoh-plugin-ros2dds/target/release/libzenoh_plugin_ros2dds.so",
        }
    },
    listen: {
        endpoints: ["tcp/0.0.0.0:7447"]
    },
}

Jetson上でzenohdを起動します。

# Jetson terminal 1
src/zenoh/target/release/zenohd -c zenoh-config-jetson.json

クラウドのzenohdに接続します。設定は以下の通りです。

$ cat zenoh-config-jetson.json
{
    mode: "client",
    plugins: {
        ros2dds: {
            __path__: "src/zenoh-plugin-ros2dds/target/release/libzenoh_plugin_ros2dds.so",
        }
    },
    connect: {
        endpoints: ["tcp/<クラウドのグローバルIP>:7447"]
    }
}

zenohd同士を接続すると勝手にトピックが同期され、クラウド上でJetson上のトピックが見られるようになります。 Go2のセンサ値を確認してみます。

# Cloud terminal 2
source src/ros/install/setup.sh
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 topic echo /sportmodestate

結果(最初の一部)

---
stamp:
  sec: 1765203140
  nanosec: 497936758
error_code: 1001
imu_state:
  quaternion:
  - -0.9969053864479065
  - 0.005156185943633318
  - 0.05559273064136505
  - 0.05533893033862114

続いてクラウドからロボットを操作します。

クラウド上でノードを起動します。

# Cloud terminal 2
ros2 run teleop_twist_keyboard teleop_twist_keyboard

ノードteleop_twist_keyboardはキーボード入力を受け付け、トピック/cmd_velへpublishします。

Jetson上でノードを起動します。

# Jetson terminal 2
ros2 run unitree_ros2_example cmd_vel_control

ノードcmd_vel_to_sport_requestはトピック/cmd_velをsubscribeし、トピック/api/sport/requestへpublishします。これが低レイヤーの命令に変換されていき、最終的にモーターが駆動します。

実行の様子です。

ターミナル画面が4分割されており、左上はクラウド上でzenohd、左下はJetson上でzenohd、右上はクラウド上でROSノードteleop_twist_keyboard、右下はJetson上でROSノードcmd_vel_to_sport_requestを実行しています。 (本来かなり運動性能が高いのですが、6畳の部屋では一歩が限界でした。)

まとめと今後の取り組み

本記事ではUnitree Go2をクラウドから制御する簡単なデモを作成しました。

クラウド側の処理がシンプルだったため、Zenohにこだわる理由が伝わらなかったかもしれません。 確かにリアルタイム性を求めないアプリであれば、他に適したプロトコルがあります。 状態監視・ログ収集・UIといった処理は、MQTTやREST、WebSocketを使ってクラウド側に簡単に実装できます。

しかし私はロボットの制御ループそのものをどこまでオフロードできるかに興味があります。遅延やジッタがロボットの挙動に直結するため、ROSが提供する(予定の)Zenohを使うのが良さそうだと判断しました。

今後の取り組みとして、以下を調査したいです。

  • 他プロトコルとの比較
  • SLAMや経路計画など、ロボットの制御ループのうち遅延の制約がそこまで厳しくない処理をクラウドで実行可能か
  • フィジカルAIがロボットの制御ループに組み込まれることで、遅延の制約がどう変化するか

またNTTドコモビジネスはdocomo MECというモバイル回線の基地局の側に置かれたエッジサーバーや、5Gワイドという優先制御サービスなど、低遅延・低ジッタの基盤を提供しています。 これらのサービスを利用することで遅延・ジッタの制約が緩和され、オフロードできる範囲が広がるかもしれません。

本日はここまでです。明日の記事もお楽しみに!

参考