はじめに
前回の記事で、Waveshare JetBot AI Kit(Jetson Nano)の Docker 環境を ROS 2 Humble に移行しました。今回はその環境を NVIDIA Isaac Sim 5.1.0 とつなぎ、1つの /cmd_vel で実機の JetBot と Sim 上の JetBot を同時に動かすところまで進めます。
JetBot にはホイールエンコーダがないため、実機の位置を Sim に反映させることはできません。そこで、同じ速度指令を実機と Sim に流して並走させる方式にしました。あわせて、通信が途切れたときに実機が暴走しないよう、watchdog ノードも追加します。
環境
| 項目 | 内容 |
|---|---|
| PC | Ubuntu 24.04、RTX GPU、Isaac Sim 5.1.0(standalone) |
| PC の ROS 環境 | Isaac Sim 内部の Humble ライブラリ + osrf/ros:humble-desktop Docker |
| ロボット | Waveshare JetBot AI Kit(Jetson Nano、JetPack 4.6.x / L4T R32.7.1) |
| ロボットの ROS 環境 |
dustynv/ros:humble-pytorch-l4t-r32.7.1 ベースの jetbot_ros コンテナ |
| 通信 | Wi-Fi、Fast DDS(UDP のみ) |
構成
flowchart LR
subgraph PC["RTX PC(Ubuntu 24.04)"]
T\["操作端末\<br/\>Humble Docker\<br/\>teleop / topic pub"\]
S\["Isaac Sim 5.1.0\<br/\>内部 Humble\<br/\>OmniGraph"\]
end
subgraph J["Jetson Nano(Docker, Humble)"]
W\["cmd\_vel\_watchdog\<br/\>0.5 秒受信なし → 停止"\]
M\["motors\_waveshare"\]
end
T -- "/cmd_vel" --> S
T -- "/cmd_vel(Wi-Fi)" --> W
W -- "/jetbot/cmd_vel" --> M
ポイントは次の3つです。
- Isaac Sim は Ubuntu 24.04 だと既定で内部 Jazzy を読み込むので、Jetson に合わせて内部 Humble を明示的に選ぶ
- PC・Isaac Sim・Jetson の3か所で
ROS_DOMAIN_IDと Fast DDS の設定ファイルを揃える - 実機側は watchdog を挟んで、指令が途切れたら止まるようにする
1. 共通設定(PC・Jetson 共通)
Isaac Sim、PC の Docker、Jetson のコンテナの3か所すべてで、同じ値を使います。
export ROS_DOMAIN_ID=30
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export FASTRTPS_DEFAULT_PROFILES_FILE=$HOME/.ros/fastdds.xml
Docker をまたぐと Fast DDS の共有メモリ転送が失敗しやすいため、UDP のみを使うプロファイルにします。
<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<transport_descriptors>
\<transport\_descriptor\>
\<transport\_id\>UdpTransport\</transport\_id\>
\<type\>UDPv4\</type\>
\</transport\_descriptor\>
</transport_descriptors>
<participant profile_name="udp_transport_profile" is_default_profile="true">
\<rtps\>
\<userTransports\>
\<transport\_id\>UdpTransport\</transport\_id\>
\</userTransports\>
\<useBuiltinTransports\>false\</useBuiltinTransports\>
\</rtps\>
</participant>
</profiles>
PC で作成したら Jetson にもコピーします。Jetson 側に ~/.ros がないと scp が失敗するので、先にフォルダを作っておきます。
ssh <user>@<JETSON_IP> 'mkdir -p ~/.ros'
scp ~/.ros/fastdds.xml <user>@<JETSON_IP>:~/.ros/fastdds.xml
:::note warn ~/.ros/fastdds.xml が存在しない状態で docker run -v でマウントすると、Docker が同名の空ディレクトリを作ってしまいます。必ずファイルを作ってからコンテナを起動してください。間違えて作られた場合は rmdir ~/.ros/fastdds.xml で削除します。 :::
2. Isaac Sim 5.1.0 を内部 Humble で起動する
Isaac Sim 5.1 の内部 ROS ライブラリは Python 3.11 でビルドされています。そのため、Isaac Sim を起動するターミナルでは、システムの ROS(/opt/ros/*/setup.bash)を source しないでください。
App Selector で起動する(GUI)
export ROS_DOMAIN_ID=30
export FASTRTPS_DEFAULT_PROFILES_FILE=$HOME/.ros/fastdds.xml
cd ~/isaac-sim-standalone-5.1.0-linux-x86_64
./isaac-sim.selector.sh
App Selector の ROS Options で次のように設定し、Isaac Sim Full を START します。
- ROS Bridge Extension:
isaacsim.ros2.bridge - Use Internal ROS 2 Libraries:
humble
起動したら、Script Editor で次を実行して humble と表示されることを確認します。
import os; print(os.environ.get("ROS_DISTRO"))
:::note info App Selector は 5.1 で非推奨になっています。スクリプトで起動する場合は、ROS_DISTRO=humble を設定し、LD_LIBRARY_PATH に <isaac-sim>/exts/isaacsim.ros2.bridge/humble/lib を追加してから isaac-sim.sh を実行します(LD_LIBRARY_PATH への追加は1ターミナルにつき1回だけにします)。 :::
シーンを作る
Stage に次の4つを用意します。
| Prim | 作り方 | 注意 |
|---|---|---|
/World/jetbot |
Isaac Sim Assets の Robots/NVIDIA/Jetbot をドラッグ |
パスは小文字の jetbot になる。Translate Z = 0.05 |
/World/GroundPlane |
Create → Physics → Ground Plane | 衝突判定付きの床 |
/World/PhysicsScene |
Create → Physics → Physics Scene | これがないと Play で JetBot が落下する |
/World/JetbotROS |
次の OmniGraph スクリプトで作成 |
OmniGraph で /cmd_vel を差動駆動につなぐ
Window → Script Editor に貼り付けて実行します。
import omni.graph.core as og
import omni.usd
# 作り直し用:既存のグラフを削除
stage = omni.usd.get_context().get_stage()
if stage.GetPrimAtPath("/World/JetbotROS"):
stage.RemovePrim("/World/JetbotROS")
og.Controller.edit(
{"graph\_path": "/World/JetbotROS", "evaluator\_name": "execution"},
{
og.Controller.Keys.CREATE\_NODES: \[
("Tick", "omni.graph.action.OnPlaybackTick"),
("Ctx", "isaacsim.ros2.bridge.ROS2Context"),
("SubTwist", "isaacsim.ros2.bridge.ROS2SubscribeTwist"),
("BreakLin", "omni.graph.nodes.BreakVector3"),
("BreakAng", "omni.graph.nodes.BreakVector3"),
("Diff", "isaacsim.robot.wheeled\_robots.DifferentialController"),
("Art", "isaacsim.core.nodes.IsaacArticulationController"),
\],
og.Controller.Keys.SET\_VALUES: \[
("SubTwist.inputs:topicName", "/cmd\_vel"),
("Diff.inputs:wheelRadius", 0.0325),
("Diff.inputs:wheelDistance", 0.1125),
("Art.inputs:targetPrim", "/World/jetbot"),
("Art.inputs:jointNames", \["left\_wheel\_joint", "right\_wheel\_joint"\]),
\],
og.Controller.Keys.CONNECT: \[
("Tick.outputs:tick", "SubTwist.inputs:execIn"),
("Ctx.outputs:context", "SubTwist.inputs:context"),
("SubTwist.outputs:execOut", "Diff.inputs:execIn"),
("SubTwist.outputs:linearVelocity", "BreakLin.inputs:tuple"),
("SubTwist.outputs:angularVelocity", "BreakAng.inputs:tuple"),
("BreakLin.outputs:x", "Diff.inputs:linearVelocity"),
("BreakAng.outputs:z", "Diff.inputs:angularVelocity"),
("Tick.outputs:tick", "Art.inputs:execIn"),
("Diff.outputs:velocityCommand", "Art.inputs:velocityCommand"),
\],
},
)
print("OK")
車輪半径 0.0325 m、車輪間隔 0.1125 m は暫定値です。実機に合わせる場合は実測してください。
ノードの型名はバージョンによって変わることがあります。unrecognized type エラーが出たら、次のコマンドで正式な型名を確認できます。
import omni.graph.core as og
names = og.get_registered_nodes()
for key in ["Break", "Twist", "Differential", "ArticulationController"]:
print(key, \[n for n in names if key in n\])
設定が終わったら、File → Save As でシーンを .usd として保存しておくと、次回からはシーンを開くだけで使えます。
3. PC 側の操作端末(Humble Docker)
Ubuntu 24.04 には Humble を apt でインストールできないので、操作用のツールは Docker で動かします。
xhost +local:
docker run -it --rm --net=host --ipc=host \
-e DISPLAY -e ROS_DOMAIN_ID=30 \
-e FASTRTPS_DEFAULT_PROFILES_FILE=/fastdds.xml \
-v $HOME/.ros/fastdds.xml:/fastdds.xml \
--name ros_humble osrf/ros:humble-desktop bash
コンテナ内で次を実行します。
source /opt/ros/humble/setup.bash
apt update && apt install -y ros-humble-teleop-twist-keyboard
Isaac Sim で Play ▶ した状態で ros2 topic list を実行し、/cmd_vel が見えれば Isaac Sim とつながっています。
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}}"
これで Sim の JetBot が前進します。
4. Jetson 側(motors + watchdog)
watchdog ノード
jetbot_ros の motors ノードは、最後に受け取った速度で回り続けます。Wi-Fi が切れると実機が止まらなくなるため、/cmd_vel を中継しつつ、0.5 秒受信がなければ停止指令を送るノードを間に挟みます。jetbot_ros 本体のコードは変更しません。
コンテナは --rm で起動するので、スクリプトは Jetson 本体に置いてマウントします。
import time
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class CmdVelWatchdog(Node):
def \_\_init\_\_(self):
super().\_\_init\_\_('cmd\_vel\_watchdog')
self.timeout \= self.declare\_parameter('timeout', 0.5).value
self.pub \= self.create\_publisher(Twist, '/jetbot/cmd\_vel', 10\)
self.create\_subscription(Twist, '/cmd\_vel', self.on\_cmd, 10\)
self.last \= 0.0
self.stopped \= True
self.create\_timer(0.1, self.check)
self.get\_logger().info(f'watchdog started (timeout={self.timeout}s)')
def on\_cmd(self, msg):
self.last \= time.monotonic()
self.stopped \= False
self.pub.publish(msg)
def check(self):
if not self.stopped and time.monotonic() \- self.last \> self.timeout:
self.pub.publish(Twist())
self.stopped \= True
self.get\_logger().warn('cmd\_vel timeout \-\> STOP')
def stop(self):
self.pub.publish(Twist())
def main():
rclpy.init()
node \= CmdVelWatchdog()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.stop()
node.destroy\_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
コンテナ起動
sudo docker run -it --rm \
--runtime nvidia \
--privileged \
--net=host --ipc=host \
-v /tmp/argus_socket:/tmp/argus_socket \
-e ROS_DOMAIN_ID=30 \
-e FASTRTPS_DEFAULT_PROFILES_FILE=/fastdds.xml \
-v $HOME/.ros/fastdds.xml:/fastdds.xml \
-v $HOME/jetbot_tools:/jetbot_tools \
jetbot_ros:humble-r32.7.1-stable bash
| オプション | 役割 |
|---|---|
--runtime nvidia |
GPU・JetPack のライブラリをコンテナ内で使う |
--privileged |
モーターと OLED の I2C にアクセスする |
-v /tmp/argus_socket:... |
CSI カメラ(nvarguscamerasrc)を使う |
--net=host --ipc=host |
PC と DDS で通信する |
-v $HOME/jetbot_tools:... |
watchdog スクリプトを読み込む |
ノード起動
ros2 run jetbot_ros motors_waveshare &
python3 /jetbot_tools/cmd_vel_watchdog.py &
motors ノードは jetbot 名前空間で起動するため、購読するトピックは /cmd_vel ではなく /jetbot/cmd_vel です。watchdog がこの差を吸収します。
$ ros2 node info /jetbot/motors
/jetbot/motors
Subscribers:
/jetbot/cmd\_vel: geometry\_msgs/msg/Twist
:::note info watchdog を使わない場合は、ros2 run jetbot_ros motors_waveshare --ros-args -r /jetbot/cmd_vel:=/cmd_vel でリマップすれば、/cmd_vel を直接受けられます。 :::
5. 動作確認
最初は車輪を浮かせた状態で試してください。
- Isaac Sim で Play ▶ し、JetBot が床の上で静止することを確認する
- Jetson のコンテナで
ros2 topic listを実行し、/cmd_velが見えることを確認する(Wi-Fi 越しなので数秒かかることがあります) - PC の Docker から前進指令を送る
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}}"
実機の車輪と Sim の JetBot が同時に動けば成功です。Ctrl+C で送信を止めると、0.5 秒後に watchdog が cmd_vel timeout -> STOP を出して実機が止まります。
キーボードで操作する場合は次のコマンドを使います。
ros2 run teleop_twist_keyboard teleop_twist_keyboard
teleop_twist_keyboard はキーを押したときだけ指令を送るので、0.5 秒のタイムアウトだと、キーを押し続けていないと止まります。余裕がほしい場合は、python3 /jetbot_tools/cmd_vel_watchdog.py --ros-args -p timeout:=1.0 で起動してください。
ハマりどころ
| 症状 | 原因 | 対処 |
|---|---|---|
unrecognized type 'omni.graph.nodes.Break3Vector' |
ノード型名の誤り | 正しくは omni.graph.nodes.BreakVector3
|
| Play すると JetBot が落下する | PhysicsScene がない | Create → Physics → Physics Scene を追加 |
/cmd_vel を送っても Sim の JetBot が動かない |
Stage 上のパスが小文字の /World/jetbot
|
targetPrim をパスに合わせる |
コンテナ内の /fastdds.xml がディレクトリになっている |
ホスト側にファイルがない状態でマウントした | 空ディレクトリを削除し、ファイルを作ってから起動し直す |
実機が /cmd_vel に反応しない |
motors が /jetbot/cmd_vel を購読している |
watchdog で中継する(またはリマップする) |
| Sim は止まるのに実機が止まらない |
ros2 topic pub --once の停止指令が、Wi-Fi 越しの discovery が終わる前に送られて失われた |
-r 10 -t 20 のように複数回送る。watchdog で根本対策 |
ros2 topic pub -r 10 -t 20 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}}"
Jetson から PC のトピックが見えない場合は、まず Isaac Sim が Play 中かを確認し、次に sudo ufw status でファイアウォールが UDP を止めていないかを確認してください。Wi-Fi ルータがマルチキャストを通さない環境では、fastdds.xml の <builtin><initialPeersList> に相手の IP を書くと見つけられるようになります。
おわりに
Isaac Sim 5.1 を内部 Humble で起動し、DDS の設定を3か所で揃えるだけで、Jetson Nano(JetPack 4.6 / Humble)の JetBot と Sim を同じ /cmd_vel で動かせました。
次は、実機の CSI カメラ映像を圧縮して PC に送り、Isaac Sim のカメラ映像と並べて表示するところに挑戦する予定です。