ROS 2 実践コース|#10 記録して、ロボットなしで考える(rosbag)

#07 で、Wi-Fi の具合によって /scan の届き方が変わることを確かめました。それに、ノードを少し直すたびにロボットを置き直し、電源を入れて試していては、時間がいくらあっても足りません。

そこでこの回は、ロボットのデータを一度だけ記録し、そのあとはロボットにつながずに記録を再生してノードを作ります。記録と再生には rosbag(ros2 bag コマンド)を使います。

ロボットがあるうちに記録し、あとは記録を何度でも再生してノードを作る
ロボットがあるうちに記録し、あとは記録を何度でも再生してノードを作る
  1. ロボットの周りに箱を置き、30 秒ほど記録する(実験 1)
  2. 記録に何が入っているかを読む(実験 2)
  3. ロボットから切り離し、記録を RViz で再生する(実験 3)
  4. 記録を使って、前方の障害物までの距離を表示するノードを作る(課題)

この回も、ロボットは動かしません。

どの実験も、コマンドを打つ前に「どうなるか」の予想をノートに書いてください。何を予想すればよいかは、次の表のとおりです。各実験の結果の画面と説明は、「結果を見る」をクリックすると開きます。予想を書き、自分で実行してから開いてください。

実験予想すること
2. 記録の中身30 秒ほど記録したとき、/scan はおよそ何件入っているか。#07 で測った /scan の周期から計算する
3. ロボットなしで再生ロボットから切り離しても、RViz に点が出るか。ros2 topic hz /scan は記録していたときと同じくらいになるか
課題自分のノードの「前」の値は、scan_monitor の「前」と同じくらいになるか。範囲や正面の角度を変えると、値はどちらへ変わるか

予想は外れてかまいません。点になるのは、予想と結果が違ったときに「なぜ違ったか」を考えて書くところです。分からないときも空欄にせず、「分からない」と書いて、手がかりにしたことを 1 つ書きます。実験 1 は記録をとる作業なので、予想はいりません。

授業で受ける人は、この回の記録を実習レポート(Word)の「第 8 回」のシートに書きます。スクリーンショットは、シートの中の「画面:」の欄(実験や記録の表のすぐ下)に貼ります。ひとりで学ぶ人も、予想と結果を同じ形でノートに残しておくと、あとで見返せます。

この回で撮っておく画面

動画は撮りません。そのかわり、下の画面を撮って残します。授業で受ける人は、実習レポートの「第 8 回」の、それぞれの「画面:」の欄に貼ります。ひとりで学ぶ人も、同じ画面を残しておくと、あとで値を見直せます。最初の 1 枚は、実習を始める前に端末で TZ=Asia/Tokyo date と打ち、日付と時刻が出た画面です。授業の日に実習したことの記録になります。講義用の環境の時計は世界標準時(UTC)なので、TZ=Asia/Tokyo を付けて日本時間で出します。

  • TZ=Asia/Tokyo date と、つないだ直後の robot-status
  • ros2 bag info(記録時間と件数)
  • 記録を再生しているときの ros2 topic hz /scan
  • 自分のノードの出力(ros2 run front_check front_nearest の端末)
  • scan_monitor と自分のノードを並べた画面(同じ場面の 2 つの値)
  • 再生を一時停止したときの、自分のノードの表示

この回で使うもの

もの使うところ
講義用の環境すべて
ロボットと囲い、箱 2〜3 個実験 1
設計シート(#04 で使ったもの)課題
#09 で保存した RViz の設定(あれば)実験 3

実験 1 30 秒記録する

この実験は、次の順に進めます。

  1. ロボットを囲いの中の床に置き、周りに箱を並べる(ロボットは歩かせない)
  2. ロボットにつなぎ、robot-status で確かめる
  3. 記録用のフォルダ ~/bags を作って移動する
  4. ros2 bag record で 30 秒ほど記録する。途中で箱を 1 つ動かす

ロボットを囲いの中の床に置き、周りに箱を並べます。ロボットにつないだら、robot-status でブリッジが動作中かを確かめ、その画面を回の頭の 1 枚として撮ります。続けて、記録用のフォルダを作って移動します(mkdir -p と && は #09 の実験 3 と同じです)。ros2 bag record は、コマンドを打った場所に記録のフォルダを作るので、先に移動しておきます。

robot-connect
robot-status
mkdir -p ~/bags && cd ~/bags

robot-connect は、IP を付けないと前回つないだ IP につなぎます。OLED の 2 行目の IP が前回と変わっていたら(ホットスポットを立て直したときなど)、robot-connect 192.168.137.134 のように IP を付けて打ちます。

次のコマンドで記録を始めます。30 秒ほどたったら Ctrl+C で止めます。記録している途中で、箱を 1 つだけ動かしてください。動かした時刻をメモしておきます。箱を動かすのは、あとで自分のノードを確かめるときに「場面が変わったら値も変わるか」を見るためです。動かすときは、ロボットの脚や LiDAR に手や箱を当てないようにします。

ros2 bag record -o room_scan /scan /tf /tf_static /joint_states
4 つのトピックに Subscribed したあと Recording... と出て、Ctrl+C で Recording stopped になった
4 つのトピックに Subscribed したあと Recording... と出て、Ctrl+C で Recording stopped になった
書いたもの意味
-o room_scan記録を room_scan という名前のフォルダに保存する
/scanLiDAR の距離データ
/tf と /tf_static座標系の関係(#09)。これが無いと、再生したときに点をロボットのどこに置けばよいかわからない
/joint_states脚の関節の角度

記録できるのは、ここに並べたトピックだけです。あとで「あれも記録しておけばよかった」とならないように、何に使うかを考えてから選びます。

実験 2 記録に何が入っているかを読む

ロボットから切り離してから、記録の中身を表示します。

robot-disconnect
ros2 bag info room_scan

ros2 bag info は、記録したファイルを読むだけなので、ロボットにつながっていなくても使えます。見るところは、記録した時間の Duration と、下の Topic information の各行にある Count(そのトピックのメッセージが何件入っているか)です。

結果を見る(予想を書き、自分で実行してから開く)
記録した時間(Duration)は約 32.3 秒、メッセージは全部で 650 個。/scan は 160 個
記録した時間(Duration)は約 32.3 秒、メッセージは全部で 650 個。/scan は 160 個

考える:/scan は 1 秒に何回届いていたか

Duration と、/scan の Count から、記録していた間の /scan の平均の周波数を計算してください。#07 の実験 3 で ros2 topic hz で測った値と合っているでしょうか。/tf も同じように計算してみます。

計算の例(自分で計算してから開く)

/scan:160 ÷ 32.27 秒 ≒ 4.96 Hz。#07 で確かめた、ロボット側のブリッジで 5 Hz に間引いた値とほぼ同じです。

/tf:318 ÷ 32.27 秒 ≒ 9.85 Hz。/tf はブリッジで 10 Hz までにしているので、これもほぼ上限どおりです。

記録の周波数が上限より大きく低いときは、記録していた間に Wi-Fi でデータが落ちていたことになります。記録にも、そのときの通信の具合がそのまま残ります。

実験 3 ロボットなしで再生する

この実験は、次の順に進めます。

  1. ロボットから切り離されていることを確かめる
  2. 端末 1 で記録をくり返し再生する
  3. 端末 2 で RViz を起動し、再生した点を表示する
  4. 端末 3 で ros2 topic hz /scan を測り、実験 2 で計算した周期と比べる

再生する前に、robot-disconnect でロボットから切り離しておきます。つないだままだと、ロボットから届く今の /scan と、記録から再生した /scan が同じ名前で混ざってしまいます。実験 2 で切り離していれば、そのままで大丈夫です。心配なときは robot-status で確かめます。

端末 1 で記録を再生します。--loop は、最後まで再生したら最初に戻ってくり返す指定です。

# 端末 1
cd ~/bags
ros2 bag play room_scan --loop

端末 2 で RViz を起動し、#09 の実験 4 と同じように Fixed Frame・TF・LaserScan を設定します(#09 で設定を保存した人は rviz2 -d ファイル名)。RobotModel は、記録に /robot_description を入れていないので表示できません。追加しないか、追加してあればチェックを外します。

# 端末 2
rviz2

端末 3 で、再生している /scan の周期を測ります。数行出たら Ctrl+C で止め、average rate を書きます。

# 端末 3
ros2 topic hz /scan
結果を見る(予想を書き、自分で実行してから開く)
ロボットにつないでいないのに、記録した部屋の点と TF が表示された。RobotModel は /robot_description を記録していないので表示を外している
ロボットにつないでいないのに、記録した部屋の点と TF が表示された。RobotModel は /robot_description を記録していないので表示を外している

記録の途中で箱を動かした場面では、点の並びが変わります。--loop でくり返すので、同じ場面を何度でも見られます。

端末 3 の average rate は、記録していたときとほぼ同じになります(実験 2 で計算した周期)。ros2 bag play は、記録したときの時刻の間隔をそのまま再現して送るからです。

端末 1 でスペース キーを押すと再生を一時停止でき、もう一度押すと続きから再生します。課題の検証で使います。

課題 前方でいちばん近い物体までの距離を表示するノード

記録を使って、次のノードを作ります。

ロボットの前方 ±30° の範囲で、いちばん近い物体までの距離を、1 秒に 1 回表示する。

#04 と同じく、自分で決めるのは「何を作るか」と「できたものが決めたとおりか」です。コードは AI に書かせてかまいません。次の順に進めます。

  1. /scan の 1 回分の中身を読む
  2. 設計シートを書く(分からない欄は、AI に聞き返してもらいながら自分で決める)
  3. AI にコードを書かせ、設計シートと突き合わせてからパッケージに入れる
  4. 記録を再生しながら動かし、設計シートの「検証の方法」で確かめる
  5. サンプルの scan_monitor と同じ場面で比べる

/scan の中身を確かめる

設計する前に、/scan の 1 回分の中身を見ておきます。端末 1 で記録を再生したまま、端末 3(実験 3 で ros2 topic hz を測った端末)で次を実行します。端末 2 の RViz は開いたままにしておきます。

ros2 topic echo --once /scan
$ ros2 topic echo --once /scan
(ranges と intensities の数字の列は省いた)
header:
  stamp:
    sec: 1789480703
    nanosec: 15327125
  frame_id: laser_frame
angle_min: 0.0033161682076752186
angle_max: 6.274807453155518
angle_increment: 0.013936647213995457
time_increment: 0.00022498289763461798
scan_time: 0.1012423038482666
range_min: 0.05000000074505806
range_max: 20.0
項目この LiDAR の値意味
angle_min0.0033 rad(1 回ごとに少し変わる)1 つめの点の角度
angle_max6.2748 rad最後の点の角度。0 から 2π(360°)近くまで 1 周している
angle_increment0.01394 rad(約 0.8°)となりの点との角度の差
ranges約 450 個の数それぞれの角度で測った距離 [m]
range_min・range_max0.05 m・20.0 mこの範囲の外の値は、測れていない点として扱う

i 番目の点(0 から数える)の角度は angle_min + i × angle_increment です。角度は laser_frame の x 軸を 0 として、上から見て反時計回り(左回り)に増えます。

設計シートを書く

#04 と同じ形の設計シート(#04 の「1. 設計シートを書く」の白紙)を使います。#04 の counter や even_printer とちがって、このノードには新しく決めることがいくつかあります。次の表の欄は必ず書いてください。「何を書くか」と「なぜ要るか」を読んでから、自分の値を決めます。#04 の白紙に無い「前方の判定」と「測れていない点」の 2 つは、白紙の「座標系」の下に「■ 前方の判定」「■ 測れていない点」と見出しを自分で足して書きます。

欄何を書くかなぜ要るか・手がかり
受け取るもの(入力)トピック名・型・QoS・期待する周期型は ros2 topic info -v /scan で見られます。QoS は #09 の実験 2 の「考える」で、センサーのデータをどちらで受けるとよいかを考えました。周期は、実験 2 で計算した値です。QoS を送り手と合わない組み合わせにすると、エラーも出ずに何も届きません(#09 の実験 1 の A)
座標系と「前方」の決め方どの座標系の、何度を正面とするか/scan の角度は laser_frame の x 軸が 0 です(#09 の実験 6 の frame_id)。#09 の実験 5 で、LiDAR の 0° がロボットの正面と合っていたかを確かめました。その結果をここに書きます。ずれていたら、正面を何度として扱うかを書きます
範囲に入るかの判定±30° に入る点を、角度からどう選ぶか上で読んだとおり、この LiDAR の角度は 0〜2π で 1 周します。「前方 ±30°」が角度の数字でどこに当たるかを書きます。下のヒントに、つまずきやすいところをまとめています
測れていない点の扱いinf・nan・range_min 未満・range_max を超える値をどうするか。範囲に 1 つも有効な点が無いときに何を表示するかinf は「限りなく大きい(何も返ってこなかった)」、nan は「数になっていない」という値です。これをそのまま「いちばん近い」の計算に入れると、答えがおかしくなります。上の表の range_min・range_max の意味を使って決めます
出すもの(出力)1 秒に 1 回、何をどんな形で表示するかロボットに指令は出さないので、画面に表示するだけでかまいません。白紙の「出すもの」の表は、トピック名の欄に「なし(画面に表示)」と書き、何を表示するかを型の欄の代わりに書きます。scan_monitor と比べやすいよう、m 単位で小数 2 桁くらいにしておくと見比べやすくなります
止まる条件不要速度指令を出さないので要りません(#04 と同じ)。ただし、第 5 週でこのノードを「止まる」ノードに育てるときは必ず書きます
検証の方法記録のどの場面で、どんな値になれば正しいと言えるか実験 1 で箱を動かした時刻をメモしました。その前後で値がどう変わるはずかを書きます。scan_monitor の「前」との比べ方も書いておきます
記入例:受け取るものの欄だけ(自分で書いてから開く)
■ 受け取るもの
  トピック名 | 型                          | QoS         | 期待する周期
  -----------|-----------------------------|-------------|-------------
  /scan      | sensor_msgs/msg/LaserScan   | best effort | 5 Hz

QoS を best effort にしたのは、#09 の実験 1 の C のとおり、送り手が reliable でも best effort でも受け取れるからです。周期の 5 Hz は、実験 2 で記録の件数と時間から計算した値です。ほかの欄は、自分の実験 5 の結果によって変わるので、例は載せません。

分からない欄は、AI に聞き返してもらう

#04 と同じく、空欄のまま AI に「書いて」と頼んではいけません。AI に質問役をしてもらい、1 欄ずつ自分で答えて埋めます。#04 のプロンプトを、このノード用に書きかえたものが次です。/scan について自分で調べた結果と、書けたところまでのシートを貼って渡します。

ROS 2 のノードの設計シートを書いています。分からない欄があるので、手伝ってください。
次のルールを守ってください。
・答えを先に書かないでください。空欄を 1 つずつ取り上げて、私に質問してください。
・私が「分からない」と答えたら、その欄が何を決めるものかを高校生にも分かる言葉で説明し、選ぶための手がかりを出してから、もう一度聞いてください。
・私の答えがおかしいと思ったら、どこがおかしいかを言って聞き返してください。
・全部の欄が埋まったら、シート全体を書き出してください。

作るノード:LiDAR の /scan を受け取り、ロボットの前方 ±30° の範囲でいちばん近い物体までの距離を、1 秒に 1 回表示するノード(ROS 2 Humble・Python)
/scan について私が調べたこと:
(ここに ros2 topic info -v /scan の結果と、ros2 topic echo --once /scan で見た angle_min・angle_max・angle_increment・range_min・range_max を貼る)

私が書いたところまでの設計シート:
(ここに書きかけのシートを貼る)

とくに「前方」の欄は、AI には分かりません。自分のロボットの LiDAR がどちらを向いていたかは、#09 の実験 5 で自分が見た結果だけが根拠です。AI が「0° が正面です」と言っても、そのまま書かずに自分の記録と見比べてください。

ヒント:±30° の判定でつまずきやすいところ

この LiDAR の角度は 0 から 2π まで増えます。正面を 0° とすると、前方 ±30° に入るのは「0°〜30°」と「330°〜360°」の 2 か所に分かれます。「角度の絶対値が 30° 以下」とだけ書くと、330°〜360° の点(右前)が抜け落ちます。

角度の差を −180°〜180° の範囲に直してから比べると、1 つの式で判定できます。設計シートに、どちらの方法をとるかを書いておきます。

AI に作らせて、パッケージに入れる

パッケージの作り方とビルドは #04 と同じです。パッケージ名とファイル名は、ここでは front_check と front_nearest にします(自分で決めた名前でもかまいません。そのときは、下のコマンドとプロンプトの名前も合わせて変えます)。/scan の型 LaserScan は sensor_msgs というパッケージに入っているので、--dependencies に sensor_msgs を入れます。パッケージの作成からビルド、ノードの起動までは、端末 3 で続けて行います。

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --node-name front_nearest front_check --dependencies rclpy sensor_msgs

AI には、次のプロンプトの最後に自分の設計シートを貼って渡します。

次の設計シートどおりに、ROS 2 Humble で動く Python(rclpy)のノードを書いてください。
パッケージ名は front_check、ファイルは front_check/front_nearest.py です。
設計シートにないトピックやパラメータは足さないでください。

(ここに自分の設計シートを貼る)

返ってきたコードは、パッケージに入れる前に設計シートと突き合わせます。見るところは次の 4 つです。説明できない行があったら、AI に「この行は何をしていますか」と聞き返します。

  • create_subscription:型が LaserScan、トピック名が scan(/scan と書いても同じトピックになる)、QoS がシートと同じか
  • 角度の計算:angle_min + i * angle_increment で角度を出し、シートに書いた「前方」の判定をしているか
  • inf・nan・範囲外の値を、シートどおりに取り除いているか
  • create_timer:1 秒ごとに表示しているか

突き合わせたら、#04 の「3-3」と同じようにひな形の front_nearest.py を開いて中身を置き換え、Ctrl+S で保存してからビルドします。

codium ~/ros2_ws/src/front_check/front_check/front_nearest.py
cd ~/ros2_ws && colcon build --symlink-install && source install/setup.bash

記録で検証する

  1. 端末 1 で ros2 bag play room_scan --loop を動かしたまま、ビルドして source した端末 3 で ros2 run front_check front_nearest を実行して自分のノードを起動する。
  2. 箱を動かした場面で、表示される距離が変わるかを確かめる。RViz でその場面の点と見比べる。
  3. 端末 1 でスペース キーを押して再生を一時停止し、自分のノードの表示がどうなるかを見る。止まったままの値を出し続けるのか、何も出なくなるのかを書く。
  4. わざと範囲を ±90° にしたり、正面を 180° にしたりして、値が予想どおりに変わるかも試す。コードの値を書きかえて保存し、端末 3 のノードを Ctrl+C で止めて、上の colcon build の行を打ち直してから起動し直す。試し終えたら、設計シートの値に戻す。
  5. 自分のノードを動かしたまま、新しい端末で ros2 topic info -v /scan を打つ。受ける側(Subscription)に自分のノードが出ていて、その Reliability が設計シートの QoS と同じかを見る。

4 つめ(範囲と正面を変える)の予想の手がかりです。範囲を広げると、「いちばん近い」を探す点が増えます。そのとき、いちばん近い距離は大きくなることがあるか、ないかを考えます。正面を 180° にしたときは、scan_monitor の 4 方向(前・左・後・右)のどれと同じくらいの値になるはずかを予想します。

サンプルと比べる

自分のノードができたら、同じ記録を再生しながら、サンプルの scan_monitor と比べます。scan_monitor は前・左・後・右の 4 方向それぞれで、±30° の範囲のいちばん近い距離を表示します。

ros2 run sakigake_samples scan_monitor

自分のノードの「前」の値が、scan_monitor の「前」と同じくらいになっているかを確かめます。scan_monitor は新しい端末で起動し、端末 3 の自分のノードと同時に動かして、同じ場面の値を並べて撮ります。

結果を見る(予想を書き、自分で実行してから開く)
上の端末で記録を再生し、下の端末で scan_monitor を動かしたところ
上の端末で記録を再生し、下の端末で scan_monitor を動かしたところ
$ ros2 run sakigake_samples scan_monitor
[INFO] [1789480968.161041463] [scan_monitor]: 前  0.42 m | 左  0.18 m | 後  0.50 m | 右  0.69 m | 450 点 | 5 Hz
[INFO] [1789480969.131299325] [scan_monitor]: 前  0.41 m | 左  0.19 m | 後  0.49 m | 右  0.69 m | 450 点 | 5 Hz
[INFO] [1789480970.131942426] [scan_monitor]: 前  0.41 m | 左  0.18 m | 後  0.50 m | 右  0.69 m | 450 点 | 5 Hz
(中略)
[INFO] [1789480973.134441728] [scan_monitor]: 前  0.43 m | 左  0.19 m | 後  0.49 m | 右  0.69 m | 448 点 | 5 Hz

行の右端の 450 点 は 1 回分の点の数(448 のように回ごとに少し変わる)、5 Hz はその 1 秒間に届いた /scan の数です。

#09 の実験 5 で LiDAR の 0° が正面からずれていた場合は、scan_monitor に正面の角度を渡します(例は 5° ずれていたとき)。正面に置いた箱の点が、赤い矢印より左(反時計回り)に出たら +、右に出たら − の値にします。

ros2 run sakigake_samples scan_monitor --ros-args -p front_angle_deg:=5.0
scan_monitor の判定部分(自分のノードと比べてから開く)
def normalize_angle(angle):
    """角度 [rad] を -π〜π の範囲に直す。"""
    return math.atan2(math.sin(angle), math.cos(angle))


def sector_min(ranges, angle_min, angle_increment, center, half_width, range_min, range_max):
    nearest = math.inf
    for index, distance in enumerate(ranges):
        angle = angle_min + index * angle_increment
        if abs(normalize_angle(angle - center)) > half_width:
            continue
        if is_valid_range(distance, range_min, range_max):
            nearest = min(nearest, distance)
    return nearest

normalize_angle で角度の差を −π〜π に直してから、half_width(30° をラジアンにしたもの)と比べています。is_valid_range で、inf・nan と範囲外の値を取り除いています。有効な点が 1 つも無ければ math.inf を返し、表示では --- になります。

本質の問い

授業の最後に、次の 2 つに答えてください。

  • 記録を使えば、ロボットや Wi-Fi の具合に左右されずにノードを試せる。では、記録の再生では確かめられないことは何か。1 つ挙げ、理由を書く。
  • この課題のノードを、第 5 週で「前に障害物があったら止まる」ノードに育てるとする。記録で先に確かめておくべきことを 2 つ挙げる。
考え方の例(答えを書いてから開く)

1 つめ:たとえば、自分のノードが送った /cmd_vel でロボットが動いたあとの /scan。記録は過去のデータなので、ノードが何を送っても中身は変わらない。「近づいたら止まる → 止まったので距離が変わらない」のような、ノードの出力が次の入力に返ってくる動きは、実機でしか確かめられない。

2 つめ:たとえば、①正面の角度と範囲が正しいか(箱を動かした場面で値が変わるか)、②有効な点が無いときや、/scan が途切れたときにどう表示するか。②は、記録を一時停止すれば「届かなくなった」場面を作れる。

つまずきやすい点

症状見るところ
ros2 bag record が、フォルダがすでにあると言って止まる同じ名前の記録がある。-o room_scan2 のように名前を変える
ros2 bag info でファイルが見つからないと出るcd ~/bags で記録のあるフォルダに移動したか。ls で room_scan があるか
再生しているのに RViz に点が出ないFixed Frame のつづり。記録に /tf_static を入れたか(ros2 bag info の Topic information で確かめる)
再生した値がときどき実機の値に戻るrobot-disconnect を忘れて、実機の /scan と混ざっている。robot-status で確かめる
自分のノードの「前」の値が scan_monitor と大きく違う±30° の判定(ヒントを参照)と、角度の単位(度とラジアン)

まとめ

確かめたことわかったこと
記録(実験 1)ros2 bag record で、選んだトピックだけをフォルダに保存できる。座標系の /tf・/tf_static も一緒に記録する
記録の中身(実験 2)ros2 bag info でメッセージの数と時間がわかり、記録していたときの周波数を計算できる
再生(実験 3)ロボットにつながずに、同じ場面を何度でも再生できる。再生の前には切り離す
課題「前方」は座標系と角度の決め方しだい。記録で先に確かめてから実機で試す

次の週は、今日作ったノードを出発点に、センサーの値から行動を決めるノード(scan_avoid)の構造を読み、記録と実機の両方で確かめます。

トップへ戻る