AIエージェントでROS2を動作させてみた〜1.SLAM ,NAV 2 ,Gazebo ,Rviz とは〜
こんにちは、竹内 です。
前回は、ロボットAIエージェントを作るための土台として ROS 2 を勉強しました。ノードに分けて、トピックやアクションで繋ぐ——という、いわば配管の作法の話です。
ただ、配管が分かっても、肝心の走らせる場所も、まわりを見る目もまだありません。「2メートル前進」を /cmd_vel に変えられたところで、その先に壁があるかどうかを知る手段が無いのです。
そこから先に進むには、ROS 2 とほぼセットで語られる 4 つの道具を知る必要がありました。Gazebo・SLAM・Nav2・RViz です。
名前は全部どこかで見たことがあったのですが、正直、最初はこの4つの区別がついていませんでした。特に Gazebo と RViz は、しばらく同じようなものだと思っていました……。
今回は、その整理をしていきます。
4つの道具は、役割がぜんぶ違う
細かい話に入る前に、まず全体像から。この4つは競合するものではなく、それぞれ別の仕事をしていて、一列に繋がっています。
ひとことで言うと、こうなります。
- Gazebo:ロボットを走らせる場所。世界と物理とセンサを作る。
- SLAM:地図を作る。ついでに「自分が地図のどこにいるか」も出す。
- Nav2:地図とゴールを受け取って、実際の速度指令まで落とす。
- RViz:上の3つが何をしているかを見る。それだけ。ロボットは動かさない。
ここを押さえてから個別に見ていくと、だいぶ迷子になりにくいです。
Gazebo — 何度でも壊せる、仮想の実験室
Gazebo はロボットのシミュレータです。3D の世界を用意して、その中にロボットを置いて、物理法則付きで動かしてくれます。

Gazebo は「絵を描いている」だけではありません。重力・摩擦・衝突を計算し、さらにセンサの値まで作って ROS 2 のトピックに流してくれます。
つまり ROS 2 側から見ると、Gazebo の中のロボットは実機とほぼ同じ顔をしています。/scan を購読して /cmd_vel を出す、という関係は全く変わりません。
シミュレータの利点は、失敗できることです。実機だと、壁に突っ込めば本当に壊れますし、1回試すのに机まで歩いて行って充電して……となります。
シミュレータなら同じ失敗を100回でも再現できます。AIエージェントのように「どう動くか事前に読み切れない」ものを試すときは、ここがかなり大きいです。
ここで最初にハマったのが Gazebo が2種類ある問題です。古い「Gazebo Classic」と、新しい Gazebo(Harmonic など)は別物で、設定ファイルも起動コマンドも連携用のパッケージ名も違います。
ネットの記事がどちらの話をしているのか分からないまま真似して、動かない理由が分からず……けっこう時間を溶かしました。検索して出てきた手順は、まずどちらの Gazebo 向けか確認するのがおすすめです。
どのくらい別物なのかというと、このくらいです。
もう1つ、シミュレータならではの注意点として時刻があります。Gazebo の中の時間は、実際の時計より速かったり遅かったりします。
そこで各ノードに use_sim_time を立てて、シミュレータが配信する時刻を使わせます。これを忘れるとセンサ値と位置の時刻がズレて、地図がぐちゃぐちゃになります。
SLAM — 地図と現在地を、同時に作る
SLAM は Simultaneous Localization and Mapping の略で、日本語だと「自己位置推定と地図作成の同時実行」です。名前のとおりです。
なぜ「同時」が問題になるのか。地図が無いと自分の位置が分からず、自分の位置が分からないと地図が描けないという、鶏と卵の関係になっているからです。
SLAM はこれを、少しずつ交互に進めることで解きます。
やっていることは間違い探しです。さっき見た景色と今見えている景色を重ねて、一番ぴったり合う位置のずれが、自分が動いた量になります。
そして SLAM が吐き出す地図は、写真ではありません。マス目に区切って、1マスずつ「通れる/壁/まだ知らない」を持つだけの地図です。占有格子地図と呼ばれます。
「未知」というマスが独立してあります。通れないから灰色なのではなく、まだ見ていないから灰色です。だからロボットは、灰色を減らすためにわざわざ探索しに行ったりします。
私が勘違いしていたのですが、SLAM は毎回動かすものではありません。一度きれいな地図が作れたら、それを保存しておいて次からは読み込むだけで済みます。
デモを何度も回すときは、この「作る」と「使う」を分けておくと、毎回同じ条件で再現できて楽でした。
保存はコマンド1本です。地図の画像(.pgm)と、縮尺などを書いた設定(.yaml)の2ファイルが出てきます。
# 走り終わったあと、その時点の地図を保存する
ros2 run nav2_map_server map_saver_cli -f my_map
Nav2 — 「あそこへ行って」を車輪の回転に変える
地図ができて、自分の位置も分かった。次は目的地まで実際に移動する役です。これを丸ごと引き受けてくれるのが Nav2(Navigation 2)です。
Nav2 は、この問題を2段構えで解きます。
先にグローバルプランナが、保存した地図の上で「だいたいこのルート」を1本引きます。カーナビが出発前に経路を出すのと同じイメージです。
ただ、地図には載っていないもの——置きっぱなしの段ボール、歩いている人——が必ずあります。そこでローカルプランナが、数秒先だけを見て実際の速度を細かく決め直します。
この2つを支えているのがコストマップです。壁のマスを実際より少し太らせておくことで、「通れるけどギリギリすぎる道」を自然に避けさせます。
それでも詰まることはあります。そのときはリカバリ行動——その場で一回転してみる、少し下がる、古いコストマップを捨てる——を順番に試して、自力で復帰しようとします。
前回の話がここで繋がります。Nav2 へのゴール指示は、トピックでもサービスでもなく アクション(NavigateToPose)です。
移動は数十秒かかり、途中経過を知りたくて、しかも途中でやめたくなる——まさにアクションを使うべき場面でした。「なぜ4種類あるのか」が、実物を見てようやく分かりました。
コマンドから直接ゴールを投げることもできます。map 座標系の (2.0, 1.0) へ行け、という指示です。
ros2 action send_goal /navigate_to_pose \
nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'},
pose: {position: {x: 2.0, y: 1.0}}}}"
RViz — ロボットの頭の中を映すモニタ
最後が RViz です。冒頭に書いたとおり、私が Gazebo と混同していたのがこれです。
どちらも 3D のロボットが表示されるので、Gazebo と RViz は似たようなものに見えます。でも役割は正反対でした。
RViz は ROS 2 のトピックを購読して、それを絵にするだけのビューアです。世界を作らないし、物理も計算しないし、ロボットを動かしもしません。
逆に言うと、RViz に映っているものは全部ロボットが実際に受け取った/計算したデータです。だから、ここが実物とズレていたら、そのズレがそのままバグということになります。
この性質のおかげで、RViz はデバッグの主戦場になります。私がよく見ているのはこのあたりです。
- LiDAR の点群が、壁の位置とちゃんと重なっているか。ズレていたら座標変換が怪しい。
- コストマップの膨らみ具合。太らせすぎると、通れるはずの通路が塞がる。
- 引かれた経路が、そもそも意図した方向を向いているか。
RViz でよくある「何も表示されない」は、たいてい壊れているわけではありません。基準にする座標系(Fixed Frame)の選び間違いが犯人であることが多かったです。
地図を見たいのに base_link(ロボット自身)を基準にしていると、ロボットは常に画面中央から動かず、地図の方がぐるぐる回ります。地図を見るときは map にします。
この4つは、ROS 2 のどこで繋がっているのか
ここまで4つを別々に見てきましたが、実際に起動すると、これらはそれぞれ独立したプロセスです。お互いを関数として呼び出したりはしません。
では何が繋いでいるのか。前回出てきたトピックとアクションです。同じ名前のトピックに publish / subscribe している、ただそれだけで繋がります。
この図であらためて気づいたのは、Gazebo だけが ROS 2 を喋らないことでした。Gazebo には Gazebo 独自の通信があります。
その隙間を埋めるのが ros_gz_bridge です。翻訳しかしないノードを1個挟むことで、ROS 2 側からは普通のトピックに見えるようになります。
実際に私が使っている起動はこんな形です。トピックごとに「型」と「向き」を書いて渡します。
ros2 run ros_gz_bridge parameter_bridge \
'/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan' \
'/cmd_vel@geometry_msgs/msg/Twist@gz.msgs.Twist'
この 記号が向きです。[ は Gazebo から ROS 2 への片方向、@ は双方向。センサは片方向、指令は双方向、という書き分けになります。
ここを間違えると、ros2 topic list には名前は出るのに、中身が永遠に来ません。エラーも出ないので、原因にたどり着くまで時間がかかりました……。
もう1本、TF という配線がある
図の中に /tf というトピックが何度か出てきました。これが TF(座標変換)で、位置の話をするときの共通語にあたります。
「地図の (2.0, 1.0)」と「ロボットの前方2m」と「LiDAR から見て右斜め前」は、全部別の座標系の言い方です。TF はこれを相互に変換します。
/tf は1本のトピックですが、複数のノードが区間を分担して流し込んでいます。誰がどこを埋めるかが決まっています。
SLAM が埋めているのは map → odom の1区間だけです。SLAM は「オドメトリのズレぶん」を出しています。
私はここで、機体側のリンク名と設定ファイルのフレーム名を食い違わせて、鎖を切ってしまいました。RViz は真っ白、Nav2 はゴールを受け取らない。それでいてエラーは出ません。
怪しいと思ったら ros2 run tf2_tools view_frames です。今そこにある鎖が PDF で出てくるので、どこで切れているか一目で分かります。
そして RViz は、この配線にぶら下がって眺めているだけです。閉じてもロボットは走り続けます。デバッグ中に気軽に開け閉めできるのは、この構造のおかげでした。
4つ繋ぐと、AIエージェントの仕事はむしろ減る
4つを繋ぐと、AIエージェント側の作りも変わります。
素朴に作ると、AIエージェントが自分で /cmd_vel を出す役を全部背負うことになります。LLM が決めるのは「前へ2メートル」「右へ90度」といった、いま居る場所からの相対的な動き方です。
Nav2 が入ると、そこは全部 Nav2 の担当になります。エージェントがやることは 言葉を「行き先の座標」に翻訳して、あとは投げるだけです。
入口はテキスト1本のまま。変わるのは、その先で誰が何を担当するかだけです。
ros2 topic pub --once /agent/instruction std_msgs/String \
"{data: '倉庫の入口まで行って'}"
役割を分ければ、壊れたときにどこが悪いかも分かりやすくなります。経路が変なら Nav2、地図が変なら SLAM、座標が変ならエージェント、という切り分けができるはずです。
まとめ
- Gazebo=走らせる場所。物理もセンサも作ってくれるので、上位のノードは実機と同じままでいい。
- SLAM=地図と現在地を同時に作る。出てくるのは白・黒・灰の3値のマス目。作ったら保存して使い回す。
- Nav2=ゴールを速度指令に変える。大まかな経路(グローバル)と目の前の回避(ローカル)の2段構え。
- RViz=シミュレータではなくビューア。ロボットの頭の中を映すので、実物とのズレがそのままバグになる。
- ハマったのは Gazebo の新旧混同・
use_sim_timeの付け忘れ・RViz の Fixed Frame。全部「エラーが出ないのに動かない」系でした。
次回は、いよいよこれを実際に動かします。環境を用意して、地図を作って、自然言語の指示だけでロボットが目的地まで自走するところまでを、手順ごと書く予定です。
ここまで名前だけ出してきたAIエージェントのノードを実際にどう組むか——ROS 2 側をどういう構成にするのかも、そこで一緒に紹介します。
たぶんまた、素直には動いてくれないと思います……。お楽しみに!
