プロジェクト

ROS 2のtalker・listenerと動くtf2フレーム

ROS 2 Jazzyのパッケージです。talker、listener、フレームを円運動させるtf2ブロードキャスタ、そのフレームを参照するノードで構成し、公式DockerイメージでmacOS・Linux・Windows上で実行します。

入門ROS 2更新 2026年11月14日
実行環境Dockerが使える任意のマシン(macOSではDocker DesktopまたはOrbStack)、ros:jazzy-ros-baseイメージ、10分以内
所要時間約2時間
使用ライブラリpython, ros2-python

作るもの

本プロジェクトでは、ほぼすべてのロボットプログラムが行う2つのことを示す小さなROS 2パッケージを作ります。ノード間でメッセージをやり取りすることと、座標系がどこにあるかを追跡し続けることです。対象はROS 2 Jazzy Jaliscoです。2024年5月に出た長期サポートリリースで、REP 2000では2029年5月までサポート対象とされています。すべて公式のros:jazzy-ros-base Dockerイメージ内で動くため、Mac、Windows PC、Linuxノートパソコンのどれでも同じように動作します。

パッケージには4つのノードがあります。

  • talkerはhello 0、hello 1と続く文字列を、std_msgs/Stringとしてトピック/chatterに10 Hzでパブリッシュします。
  • listenerは/chatterをサブスクライブし、各メッセージをログに出力します。
  • circle_broadcasterは、worldからorbiterというフレームへのtf2変換を50 Hzでパブリッシュします。orbiterは半径1 mの円上を、4秒で1周する速さで反時計回りに動きます。
  • frame_watcherはworldからorbiterへの変換を1秒に2回参照し、その位置、原点からの距離、向きをログに出力します。

ノードは2通りの方法で実行します。1つ目は、4つすべてを1つのPythonプロセスで3秒間動かし、短いまとめを表示して終了する方法です。Docker環境が動作するかを確かめる最も手早い方法で、当サイトのビルドパイプラインが実行するのもこの版です。2つ目は、colconでビルドしローンチファイルで起動する正式なament_pythonパッケージとして、ノードごとに1プロセスで動かす方法です。実機のロボットではこの構成にします。

Hello, ROS 2のレッスンでは、ノードとトピックを紹介しています。座標系と回転のレッスンでは、変換の背後にある数学を扱います。ROS 2 CLIチートシートには、「実行」セクションで使うコマンドをまとめています。

必要なもの

  • Docker。macOSではDocker DesktopまたはOrbStack、WindowsではDocker Desktop、LinuxではDocker Engineを使います。Docker Hubによれば、公式のrosイメージはamd64とarm64v8向けに公開されているため、Apple Silicon上でネイティブに動作します。
  • イメージ。docker pull ros:jazzy-ros-baseで取得します。筆者らのMacでは1.3 GBのディスクを使用しました。
  • ホストへのROSのインストールは不要です。すでにUbuntu 24.04にJazzyをネイティブでインストールしている場合は、Dockerのコマンドを飛ばし、同じcolconとros2のコマンドを直接実行してください。

筆者らは、OrbStackを入れたApple Silicon Mac miniで、2026年11月に取得したros:jazzy-ros-baseイメージ(パッケージros-jazzy-ros-base 0.11.0、Python 3.12.3)を使ってすべてを検証しました。

設計

talker/chatterString, 10 Hzlistenercircle_broadcaster/tfTransformStamped, 50 Hzframe_watcherBuffer, lookup 2 Hzframe tree:worldorbiterx = cos(wt), y = sin(wt), z = 0yaw = wt + 90 deg, w = 2 pi / 4 s
4つのノード、2つのトピック、フレームツリーです。talkerとlistenerはchatterトピックを共有します。ブロードキャスタはworldからorbiterへの変換をtfトピックにパブリッシュし、frame watcherはそのトピックからtf2バッファを埋めて問い合わせます。デモでは4つのノードが1つのプロセスと1つのエグゼキュータを共有します。ローンチファイルを使う場合は、各ノードが独立したプロセスになります。

トピックとQoS

talkerは、キュー深さ10のパブリッシャと0.1 sのタイマを作成します。listenerも同じ深さでサブスクリプションを作成します。どちらも、このケースでのデフォルトのQoSプロファイルを使います。配信はreliable、永続性はvolatile、履歴はkeep last 10です。ノードはDDSのディスカバリで互いを見つけます。そのため、talkerより後に起動したlistenerでも、それ以降にパブリッシュされたメッセージは受信できます。

tf2

tf2は座標系の木構造を保持します。各エッジは親座標系から子座標系への変換で、時刻のスタンプが付いています。ブロードキャスタはエッジを/tfにパブリッシュします。リスナは/tfと/tf_staticをサブスクライブし、Bufferを埋めます。Bufferはデフォルトで10 s分の履歴を保持し、スタンプの間を補間できます。

ブロードキャスタはgeometry_msgs/TransformStampedに、親world、子orbiter、現在時刻、並進、回転を設定します。座標系はREP 103に従います(右手系、x前方、y左、z上)。回転は純粋なヨー回転で、orbiterのx軸を進行方向に向けます。進行方向は、円上の角度より90度先にあります。ROSのメッセージは四元数を(x, y, z, w)の順で格納します。z軸まわりのヨー角ψは(0, 0, sin(ψ/2), cos(ψ/2))となり、watcherはatan2(2wz, 1 - 2z²)でこれを復元します。

watcherはlookup_transform("world", "orbiter", Time())を要求します。時刻ゼロは「手元にある最新の変換」を意味します。これにより、最新のスタンプが数ミリ秒古い場合のエラーを避けられます。最後のメッセージより新しい正確な時刻を指定すると、ExtrapolationExceptionが発生します。また、最初のブロードキャストが届くまではフレームが存在しないため、最初の数回の参照はLookupExceptionで失敗することがあります。コードは3種類のtf2例外をすべて捕捉し、次のティックで再試行します。

エグゼキュータ

ノードは、エグゼキュータがコールバックを実行するまで何もしません。デモでは1つのSingleThreadedExecutorが4つのノードすべてを保持し、ループは3秒が経過するまでspin_once(timeout_sec=0.05)を呼び出します。各呼び出しはタイマかメッセージを最大50 ms待ち、準備のできたコールバックを1つ実行します。ループを時間で区切ることで、プログラムは自ら終了します。その後、すべてのノードを削除・破棄し、rclpy.shutdown()を呼び出します。パッケージ版では、各エントリポイントは代わりにrclpy.spin(node)を呼び出します。これはCtrl-Cが押されるか、launchがプロセスの停止を求めるまで実行を続けます。

コード

ノード

このファイルは、パッケージのモジュールcircle_demo/nodes.pyであると同時に、単体のスクリプトでもあります。スクリプトとして実行すると、4つのノードすべてを1つのプロセスで3秒間起動します。1行目は当サイトのチェッカー向けの目印で、チェッカーはこのファイルをJazzyコンテナ内で実行します。Pythonにとってはただのコメントです。

# ros2
# circle_demo/nodes.py: talker, listener, tf2 broadcaster and tf2 listener (tested on ROS 2 Jazzy).
import math
import signal
import time

import rclpy
from geometry_msgs.msg import TransformStamped
from rclpy.executors import ExternalShutdownException, SingleThreadedExecutor
from rclpy.node import Node
from rclpy.time import Time
from std_msgs.msg import String
from tf2_ros import Buffer, TransformBroadcaster, TransformListener
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException

RADIUS = 1.0      # m, radius of the circle traced by the orbiter frame
PERIOD = 4.0      # s, time for one lap


class Talker(Node):
    def __init__(self) -> None:
        super().__init__("talker")
        self.pub = self.create_publisher(String, "chatter", 10)
        self.count = 0
        self.create_timer(0.1, self.tick)              # 10 Hz

    def tick(self) -> None:
        msg = String()
        msg.data = f"hello {self.count}"
        self.pub.publish(msg)
        self.count += 1


class Listener(Node):
    def __init__(self, verbose: bool = True) -> None:
        super().__init__("listener")
        self.received: list[str] = []
        self.verbose = verbose
        self.create_subscription(String, "chatter", self.on_msg, 10)

    def on_msg(self, msg: String) -> None:
        self.received.append(msg.data)
        if self.verbose:
            self.get_logger().info(f"heard '{msg.data}'")


class CircleBroadcaster(Node):
    """Broadcasts world -> orbiter, a frame moving anticlockwise on a circle in the x-y plane."""

    def __init__(self) -> None:
        super().__init__("circle_broadcaster")
        self.br = TransformBroadcaster(self)
        self.t0 = self.get_clock().now()
        self.create_timer(0.02, self.tick)             # 50 Hz

    def tick(self) -> None:
        now = self.get_clock().now()
        angle = 2 * math.pi * ((now - self.t0).nanoseconds * 1e-9) / PERIOD
        t = TransformStamped()
        t.header.stamp = now.to_msg()
        t.header.frame_id = "world"
        t.child_frame_id = "orbiter"
        t.transform.translation.x = RADIUS * math.cos(angle)
        t.transform.translation.y = RADIUS * math.sin(angle)
        t.transform.translation.z = 0.0
        # Yaw so the frame's x axis points along the direction of travel. ROS quaternions are (x, y, z, w).
        yaw = angle + math.pi / 2
        t.transform.rotation.z = math.sin(yaw / 2)
        t.transform.rotation.w = math.cos(yaw / 2)
        self.br.sendTransform(t)


class FrameWatcher(Node):
    """Looks up world -> orbiter twice a second and checks it lies on the circle."""

    def __init__(self) -> None:
        super().__init__("frame_watcher")
        self.buffer = Buffer()
        self.tf_listener = TransformListener(self.buffer, self)
        self.samples: list[tuple[float, float, float]] = []
        self.create_timer(0.5, self.tick)

    def tick(self) -> None:
        try:
            tf = self.buffer.lookup_transform("world", "orbiter", Time())   # Time() = latest available
        except (LookupException, ConnectivityException, ExtrapolationException) as e:
            self.get_logger().info(f"no transform yet: {type(e).__name__}")
            return
        x, y = tf.transform.translation.x, tf.transform.translation.y
        qz, qw = tf.transform.rotation.z, tf.transform.rotation.w
        yaw = math.atan2(2 * qw * qz, 1 - 2 * qz * qz)          # yaw of a pure z rotation
        self.samples.append((x, y, yaw))
        self.get_logger().info(f"orbiter at x={x:+.3f} y={y:+.3f} m, r={math.hypot(x, y):.3f} m, "
                               f"yaw={math.degrees(yaw):+.1f} deg")


def _spin_single(node_cls) -> None:
    """Entry point body used by the installed package: one node per process."""
    rclpy.init()
    node = node_cls()
    try:
        rclpy.spin(node)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass                                           # Ctrl-C or a shutdown request from launch
    signal.signal(signal.SIGINT, signal.SIG_IGN)       # a second Ctrl-C must not interrupt the clean-up
    node.destroy_node()
    rclpy.try_shutdown()


def talker_main() -> None:
    _spin_single(Talker)


def listener_main() -> None:
    _spin_single(Listener)


def broadcaster_main() -> None:
    _spin_single(CircleBroadcaster)


def watcher_main() -> None:
    _spin_single(FrameWatcher)


def demo(duration: float = 3.0) -> None:
    """All four nodes in one process for a fixed time, then a summary and a clean shutdown."""
    rclpy.init()
    nodes = [Talker(), Listener(verbose=False), CircleBroadcaster(), FrameWatcher()]
    talker, listener, _, watcher = nodes
    executor = SingleThreadedExecutor()
    for n in nodes:
        executor.add_node(n)
    end = time.monotonic() + duration
    while time.monotonic() < end:
        executor.spin_once(timeout_sec=0.05)
    print(f"talker sent {talker.count}, listener received {len(listener.received)}")
    print(f"first: {listener.received[0]!r}, last: {listener.received[-1]!r}")
    radii = [math.hypot(x, y) for x, y, _ in watcher.samples]
    print(f"tf lookups: {len(watcher.samples)}, radius min {min(radii):.4f} m, max {max(radii):.4f} m")
    for n in nodes:
        executor.remove_node(n)
        n.destroy_node()
    executor.shutdown()
    rclpy.shutdown()
    print("shut down cleanly")


if __name__ == "__main__":
    demo()

_spin_single関数は、初めて作るROS 2パッケージの多くがつまずく細部を処理しています。ターミナルでros2 launchに対してCtrl-Cを押すと、OSはフォアグラウンドのプロセスグループ内のすべてのプロセスにSIGINTを送ります。さらにlaunchが、各ノードにもう一度SIGINTを転送します。2回目のシグナルはノードの破棄中に届くことがあり、その場合はKeyboardInterruptのトレースバックが表示されます。クリーンアップの開始後にSIGINTを無視すると、ノードは正常に終了します。「テスト」セクションのlaunchの出力がそれを示しています。

パッケージ

ワークスペースは次のように配置します。resource/circle_demoは空の目印ファイルで、amentインデックスがパッケージを見つけるために使います。

ros2_ws/
  src/
    circle_demo/
      package.xml
      setup.py
      setup.cfg
      resource/
        circle_demo          (empty file)
      circle_demo/
        __init__.py          (empty file)
        nodes.py             (the file above)
      launch/
        circle_demo.launch.py

package.xmlは、パッケージと実行時に必要なものを宣言します。

<?xml version="1.0"?>
<package format="3">
  <name>circle_demo</name>
  <version>0.1.0</version>
  <description>Talker, listener and a tf2 frame moving in a circle.</description>
  <maintainer email="you@example.com">Your Name</maintainer>
  <license>Apache-2.0</license>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>geometry_msgs</exec_depend>
  <exec_depend>tf2_ros</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

setup.pyは、モジュール、目印ファイル、package.xml、ローンチファイルをインストールし、ノードごとに1つのコンソールスクリプトを作成します。

# snippet
from glob import glob

from setuptools import find_packages, setup

package_name = "circle_demo"

setup(
    name=package_name,
    version="0.1.0",
    packages=find_packages(exclude=["test"]),
    data_files=[
        ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
        ("share/" + package_name, ["package.xml"]),
        ("share/" + package_name + "/launch", glob("launch/*.launch.py")),
    ],
    install_requires=["setuptools"],
    zip_safe=True,
    maintainer="Your Name",
    maintainer_email="you@example.com",
    description="Talker, listener and a tf2 frame moving in a circle.",
    license="Apache-2.0",
    entry_points={
        "console_scripts": [
            "talker = circle_demo.nodes:talker_main",
            "listener = circle_demo.nodes:listener_main",
            "circle_broadcaster = circle_demo.nodes:broadcaster_main",
            "frame_watcher = circle_demo.nodes:watcher_main",
        ],
    },
)

setup.cfgは、ros2 runが探す場所にこれらのスクリプトを置くよう、setuptoolsに指示します。

[develop]
script_dir=$base/lib/circle_demo
[install]
install_scripts=$base/lib/circle_demo

ローンチファイルは、4つのノードを4つのプロセスとして起動します。output="screen"により、listenerとwatcherのログがターミナルに出力されます。

# snippet
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description() -> LaunchDescription:
    return LaunchDescription([
        Node(package="circle_demo", executable="talker", name="talker"),
        Node(package="circle_demo", executable="listener", name="listener", output="screen"),
        Node(package="circle_demo", executable="circle_broadcaster", name="circle_broadcaster"),
        Node(package="circle_demo", executable="frame_watcher", name="frame_watcher", output="screen"),
    ])

実行

以下のコマンドは、特に記載がない限りホスト(macOS、Windows、Linux)で実行します。まず、イメージを一度だけ取得します。

docker pull ros:jazzy-ros-base

簡易チェック(1プロセスで3秒間)

ノードのファイルをnodes.pyとして保存し、コンテナ内のPythonにパイプで渡します。

docker run --rm -i ros:jazzy-ros-base \
  bash -lc 'source /opt/ros/jazzy/setup.bash && python3 -' < nodes.py

パッケージ

上記の構成でワークスペースを作成し、ワークスペースを/wsにマウントしてコンテナを起動します。--nameを付けておくと、後で同じコンテナに2つ目のシェルを開けます。

cd ~/ros2_ws
docker run -it --rm --name jazzy -v "$PWD":/ws -w /ws ros:jazzy-ros-base bash

コンテナ内でビルドし、起動します。

source /opt/ros/jazzy/setup.bash
colcon build --symlink-install
source install/setup.bash
ros2 launch circle_demo circle_demo.launch.py

--symlink-installを使うと、Mac上でnodes.pyを編集した内容は、再ビルドなしで次回の起動時に反映されます。停止するにはCtrl-Cを押します。

実行中のシステムの中を確認するには、ホストで2つ目のターミナルを開きます。

docker exec -it jazzy bash

そのシェル内で次を実行します。

source /opt/ros/jazzy/setup.bash
source /ws/install/setup.bash
ros2 node list
ros2 topic echo --once /chatter
ros2 topic hz /tf
ros2 run tf2_ros tf2_echo world orbiter

ここではすべてのノードが1つのコンテナで動くため、DDSのディスカバリはコンテナ内で完結します。筆者らは、複数のコンテナに分けたノードや、コンテナとmacOSホストの間の通信は検証していません。それには追加のDockerネットワーク設定が必要です。ROS 2のドキュメントでは、DockerでROS 2ノードを実行するガイドで扱っています。

テスト

簡易チェックはwatcherの参照結果を表示し、続いてまとめを表示して、約3秒後に終了します。参照結果は5回か6回表示されます。6回目の0.5 sタイマのティックが打ち切り前に来るかどうかで変わります。タイムスタンプは毎回異なります。また、タイマがまったく同じ瞬間に始まるわけではないため、角度も実行ごとに1〜2度ずれます。それ以外は一致するはずです。

[INFO] [1794674607.289850288] [frame_watcher]: orbiter at x=+0.728 y=+0.686 m, r=1.000 m, yaw=+133.3 deg
[INFO] [1794674607.770268149] [frame_watcher]: orbiter at x=-0.001 y=+1.000 m, r=1.000 m, yaw=-180.0 deg
[INFO] [1794674608.271134201] [frame_watcher]: orbiter at x=-0.688 y=+0.725 m, r=1.000 m, yaw=-136.5 deg
[INFO] [1794674608.770265570] [frame_watcher]: orbiter at x=-1.000 y=+0.029 m, r=1.000 m, yaw=-91.7 deg
[INFO] [1794674609.271573417] [frame_watcher]: orbiter at x=-0.728 y=-0.686 m, r=1.000 m, yaw=-46.7 deg
[INFO] [1794674609.770458076] [frame_watcher]: orbiter at x=+0.000 y=-1.000 m, r=1.000 m, yaw=+0.0 deg
talker sent 30, listener received 30
first: 'hello 0', last: 'hello 29'
tf lookups: 6, radius min 1.0000 m, max 1.0000 m
shut down cleanly

talkerは3秒間に30個のメッセージを送信し(10 Hz)、listenerは30個すべてを受信しました。どの参照結果も、原点から1.000 mの位置にあります。連続する参照は0.5 s間隔で、これは1周の8分の1にあたります。そのため位置は円上を45度ずつ進み、向きは位置の角度より90度先行します。向きは-180〜+180度の範囲で表示されます。そのため、+180度をわずかに超えた向きは、-180度付近の値として表示されます。

ビルドとローンチファイルの出力は次のとおりです(最初の数行と、Ctrl-C後の終了処理)。

Starting >>> circle_demo
Finished <<< circle_demo [0.59s]

Summary: 1 package finished [0.65s]
[INFO] [launch]: All log files can be found below /root/.ros/log/2026-11-14-16-37-58-913561-12fd9347ea43-40
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [talker-1]: process started with pid [45]
[INFO] [listener-2]: process started with pid [46]
[INFO] [circle_broadcaster-3]: process started with pid [47]
[INFO] [frame_watcher-4]: process started with pid [48]
[listener-2] [INFO] [1794674279.294878908] [listener]: heard 'hello 0'
[listener-2] [INFO] [1794674279.387673320] [listener]: heard 'hello 1'
[listener-2] [INFO] [1794674279.490933375] [listener]: heard 'hello 2'
[listener-2] [INFO] [1794674279.592702464] [listener]: heard 'hello 3'
[listener-2] [INFO] [1794674279.688700685] [listener]: heard 'hello 4'
[frame_watcher-4] [INFO] [1794674279.705474615] [frame_watcher]: orbiter at x=+0.725 y=+0.689 m, r=1.000 m, yaw=+133.6 deg
...
[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)
[INFO] [frame_watcher-4]: sending signal 'SIGINT' to process[frame_watcher-4]
[INFO] [circle_broadcaster-3]: sending signal 'SIGINT' to process[circle_broadcaster-3]
[INFO] [listener-2]: sending signal 'SIGINT' to process[listener-2]
[INFO] [talker-1]: sending signal 'SIGINT' to process[talker-1]
[INFO] [circle_broadcaster-3]: process has finished cleanly [pid 47]
[INFO] [frame_watcher-4]: process has finished cleanly [pid 48]
[INFO] [talker-1]: process has finished cleanly [pid 45]
[INFO] [listener-2]: process has finished cleanly [pid 46]

各ノードは、トレースバックなしで「process has finished cleanly」と報告します。Ctrl-CでKeyboardInterruptのトレースバックが出る場合は、_spin_singleがノードを破棄する前にSIGINTを無視しているかを確認してください。

2つ目のシェルでは、ros2 node listが4つのノードを表示しました。ros2 topic hz /tfは平均レート49.7 Hzを報告し、50 Hzのタイマに近い値でした。tf2_echoは1秒に1回、変換を表示しました。

At time 1794673755.364677983
- Translation: [0.660, 0.751, 0.000]
- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.936, 0.353]
- Rotation: in RPY (radian) [0.000, -0.000, 2.421]
- Rotation: in RPY (degree) [0.000, -0.000, 138.692]
- Matrix:
 -0.751 -0.660  0.000  0.660
  0.660 -0.751  0.000  0.751
  0.000  0.000  1.000  0.000
  0.000  0.000  0.000  1.000

tf2_echoは、メッセージと同じく四元数を(x, y, z, w)の順で表示します。z軸まわりだけの回転では、xとyの成分はゼロになります。

実機では

実機のロボットでは同じパターンで実データを扱いますが、いくつか変わる点があります。

  • 実際のフレーム。 orbiterフレームは、ロボットの可動部分の代わりです。移動ロボットのベースは車輪のオドメトリからodomからbase_linkへの変換をパブリッシュし、自己位置推定ノードがmapからodomへの変換をパブリッシュします。これはREP 105に従った構成です。固定されたセンサの取り付け位置は、通常URDFからrobot_state_publisherを通じて、/tf_staticに一度だけパブリッシュします。
  • クロック。 tf2の参照は、異なるマシンのタイムスタンプを比較します。NTPやPTPでクロックを同期してください。同期しないと、特定時刻での参照が失敗するか、古いデータを返します。シミュレーションではuse_sim_timeを設定し、すべてのノードがシミュレータのクロックを使うようにします。
  • センサ向けのQoS。 高レートのセンサトピックは、通常best-effortのQoSを使います。reliableのサブスクライバは、best-effortのパブリッシャから受信できません。この不一致は、サブスクライバが何も受信しない原因としてよくあるものです。
  • 複数のマシン。 異なるコンピュータ上のノードは、ネットワーク上のDDSディスカバリで互いを見つけます。複数のロボットが同じネットワークを共有する場合は、ロボットごとに別のROS_DOMAIN_IDを割り当ててください。コードを疑う前に、ファイアウォールの設定を確認してください。
  • エグゼキュータ。 シングルスレッドのエグゼキュータでは、コールバック内でブロックするノードが他のすべてのコールバックを止めます。コールバックグループとMultiThreadedExecutorを使うか、時間のかかる処理をコールバックの外に移してください。

本プロジェクトのコードにはハードウェア固有の部分がありません。そのため、フレームが実機から来るようになっても、ファイル自体は何も変わりません。

試してみること

  1. 静的変換。 orbiterからsensorへの変換をパブリッシュするStaticTransformBroadcasterを追加します。sensorは、orbiterのx軸上で0.1 m前方とします。worldからsensorへの変換を参照し、半径√(1 + 0.01) m(約1.005 m)の円上にあることを確認してください。
  2. 時間をさかのぼる。 lookup_transform("world", "orbiter", t, timeout=Duration(seconds=0.1))で、now - 1 s時点の変換を参照します。位置が4分の1周分遅れていることを確認してください。
  3. ディスカバリを壊す。 一方のシェルでROS_DOMAIN_ID=5を設定してローンチファイルを起動し、もう一方のシェルではデフォルトのドメインでros2 topic echo /chatterを実行します。何も届きません。
  4. QoSの不一致。 talkerはreliableのままにして、listenerのサブスクリプションにqos_profile_sensor_data(best effort)を使わせます。次に両者を入れ替えます。一方の組み合わせは動作し、もう一方は動作しません。
  5. C++のノード。 frame watcherをrclcppとtf2_ros::Bufferで書き直します。Pythonのノードを変更せずに、並べて動かせます。

まとめ

  • ROS 2のPythonパッケージには、package.xml、setup.py、setup.cfg、resource/の目印ファイル、コンソールスクリプトのエントリポイントが必要です。ノードはローンチファイルで起動します。
  • tf2のブロードキャスタは、スタンプ付きの親から子への変換を/tfにパブリッシュします。TransformListenerを付けたBufferがそれを集め、参照に応えます。Time()は最新の変換を要求します。
  • ROSのメッセージは、四元数を(x, y, z, w)として格納します。純粋なヨー回転は(0, 0, sin(ψ/2), cos(ψ/2))です。
  • 明示的なエグゼキュータと終了条件のあるspin_onceループを使うと、自ら終了するROS 2プログラムになります。テストやCIに向いています。
  • 公式のros:jazzy-ros-baseイメージは、Apple Silicon上でネイティブに動作します。ワークスペースをマウントし、Ubuntu上と同じように作業してください。

ROS 2のドキュメントには、対応するチュートリアルがあります。シンプルなパブリッシャとサブスクライバの作成(Python)、tf2ブロードキャスタの作成(Python)、ローンチファイルの作成です。ROSとミドルウェアのトピックハブとROSのプラットフォームページでは、これらを全体の中に位置づけています。