以下の内容はhttps://daisuke20240310.hatenablog.com/entry/ros2_topicより取得しました。


ROS2プログラミング:トピックの実装

前回 は、ROS2 のインストールと、CUI、GUI のいくつかの動作確認を行いました。

今回は、自分でノード(ユーザアプリ)を作ってみたいと思います。これにより、既存のノードを読めるようになると思います。

それでは、やっていきます。

参考文献

環境構築については、こちらを参考にしました。Python よりも C++ の解説が多かったです。ROS2 について、しっかりと詳しく解説されていた印象です。

こちらは、逆に、Python が多く、ROS2 について、基本的なことは解説されていますが、応用的なところとして、AI と関連付けての説明に重点が置かれていた印象です。

はじめに

ROS の公式ページです。ただ、インストール手順や、その他の情報は、ROS の各ディストリビューションごとのページに書かれているので、使用するディストリビューションに応じた公式のドキュメントを参照する必要があります。このページで紹介されているディストリビューションは、Ubuntu と同じように 5年間の長期サポートをしている、最新の Jazzy Jalisco(以下、Jazzy)と、最初に長期サポートを行った Humble Hawksbill(以下、Humble)でした。現状、長期サポートしているのは、この 2つのディストリビューションのようです。

www.ros.org

今回は、Humble を使っていこうと思います。Humble が対応している Ubuntu のバージョンは、22.04 のみであり、現在、私の PC の Virtual Box に入ってる Ubuntu が 22.04 だからです。ちなみに、Jazzy の方は、Ubuntu のバージョンは、24.04 のみ対応しています。ちなみに、書籍(改訂新版 ROS 2ではじめよう 次世代ロボットプログラミング〜ロボットアプリケーション開発のための基礎から実践まで)では、Jazzy のインストール方法が解説されています。

Humble のドキュメントは以下です。

docs.ros.org

参考文献の書籍(改訂新版 ROS 2ではじめよう 次世代ロボットプログラミング〜ロボットアプリケーション開発のための基礎から実践まで)のサポートサイトです。

gihyo.jp

この書籍のサンプルコードは、以下の GitHub で公開されています。

github.com

それでは、やっていきます。

はじめてのROS2プログラミング

ROS2 では、C++、Python、Java が使えますが、まずは、Python を使います。

参考文献の書籍(改訂新版 ROS 2ではじめよう 次世代ロボットプログラミング〜ロボットアプリケーション開発のための基礎から実践まで)は、C++ の解説が多いですが、第5章に、Python のプログラミングの解説があります。

もう一つの参考文献の書籍(ROS2とPythonで作って学ぶAIロボット入門 改訂第2版 (KS理工学専門書))は、2.4 に、はじめての ROS2 プログラミングということで、Python を使った解説があります。

RO2 の公式サイトのチュートリアルでは、以下が参考になります。

ROS2 プログラミングの流れとしては、以下となります。

  1. ワークスペースの作成
  2. パッケージの作成
  3. ソースコードの作成
  4. ビルド
  5. 設定ファイルの反映
  6. ノードの実行

ちなみに、これ以降では、ROS2 の環境変数の読み込み($ source /opt/ros/humble/setup.bash)は書いていませんが必要です。私の環境では、.bashrc に書いたので、常に読み込まれるようになっています。

では、順番にやっていきます。

ワークスペースの作成

ワークスペースとは、複数のパッケージを入れることが出来るディレクトリです。よって、ここで行うことは、ディレクトリを作成することだけです。ディレクトリ名は何でもいいです。ここでは、ros2_ws という名前のディレクトリにします。

ROS2 の公式サイトのチュートリアル(Creating a workspace — ROS 2 Documentation: Humble documentation)にあるように、ワークスペースのディレクトリの中に、src という名前のディレクトリを作っておきます。この srcディレクトリの中に、パッケージを作っていくのがベストプラクティスだと言われています。

$ mkdir -p ros2_ws/src

$ cd ros2_ws/src/

パッケージの作成

パッケージとは、ROS2 のソースコードを整理するための単位とのことです。他の人とソースコードを共有する場合に、パッケージ単位で提供します。

ROS2 のパッケージ作成では、ビルドシステムとして ament(アメント)、ビルドツールとして colcon(コルコン)を使用します。

パッケージ名も任意ですが、ここでは、ex_rclpy_topic とします。

早速やってみます。

無事作られたようですが、警告が出ています。ライセンスを明示的に指定しないといけないようです。

$ ros2 pkg create --build-type ament_python ex_rclpy_topic
going to create a new package
package name: ex_rclpy_topic
destination directory: /home/daisuke/svn_/ros2/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['daisuke <daisuke@gmail.com>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: []
creating folder ./ex_rclpy_topic
creating ./ex_rclpy_topic/package.xml
creating source folder
creating folder ./ex_rclpy_topic/ex_rclpy_topic
creating ./ex_rclpy_topic/setup.py
creating ./ex_rclpy_topic/setup.cfg
creating folder ./ex_rclpy_topic/resource
creating ./ex_rclpy_topic/resource/ex_rclpy_topic
creating ./ex_rclpy_topic/ex_rclpy_topic/__init__.py
creating folder ./ex_rclpy_topic/test
creating ./ex_rclpy_topic/test/test_copyright.py
creating ./ex_rclpy_topic/test/test_flake8.py
creating ./ex_rclpy_topic/test/test_pep257.py

[WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0

次は、ライセンスを指定します。上の警告のところに、いくつかの推奨のライセンスが表示されています。ここでは、Apache-2.0 を指定します。

$ ros2 pkg create --build-type ament_python --license Apache-2.0 ex_rclpy_topic_license
going to create a new package
package name: ex_rclpy_topic_license
destination directory: /home/daisuke/svn_/ros2/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['daisuke <daisuke@gmail.com>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: []
creating folder ./ex_rclpy_topic_license
creating ./ex_rclpy_topic_license/package.xml
creating source folder
creating folder ./ex_rclpy_topic_license/ex_rclpy_topic_license
creating ./ex_rclpy_topic_license/setup.py
creating ./ex_rclpy_topic_license/setup.cfg
creating folder ./ex_rclpy_topic_license/resource
creating ./ex_rclpy_topic_license/resource/ex_rclpy_topic_license
creating ./ex_rclpy_topic_license/ex_rclpy_topic_license/__init__.py
creating folder ./ex_rclpy_topic_license/test
creating ./ex_rclpy_topic_license/test/test_copyright.py
creating ./ex_rclpy_topic_license/test/test_flake8.py
creating ./ex_rclpy_topic_license/test/test_pep257.py

ライセンスを指定した場合のディレクトリ構成を確認します。

$ tree
.
|-- LICENSE
|-- ex_rclpy_topic_license
|   `-- __init__.py
|-- package.xml
|-- resource
|   `-- ex_rclpy_topic_license
|-- setup.cfg
|-- setup.py
`-- test
    |-- test_copyright.py
    |-- test_flake8.py
    `-- test_pep257.py

3 directories, 9 files

2つのディレクトリの差分を確認しておきます(比較用に別のワークスペースを作っています)。

ライセンスファイル(LICENSE)の有無と、package.xml、setup.py に少しの差分があります。これを見る限りでは、ライセンスファイルの指定は行っておいた方が良さそうです。

$ diff -upr ros2_ws/src/ex_rclpy_topic/ ros2_ws2/src/ex_
rclpy_topic/
ros2_ws2/src/ex_rclpy_topic/ のみに存在: LICENSE
diff -u -upr ros2_ws/src/ex_rclpy_topic/package.xml ros2_ws2/src/ex_rclpy_topic/package.xml
--- ros2_ws/src/ex_rclpy_topic/package.xml      2025-06-02 23:00:07.373350497 +0900
+++ ros2_ws2/src/ex_rclpy_topic/package.xml     2025-06-02 23:08:12.246456963 +0900
@@ -5,7 +5,7 @@
   <version>0.0.0</version>
   <description>TODO: Package description</description>
   <maintainer email="daisuke@gmail.com">daisuke</maintainer>
-  <license>TODO: License declaration</license>
+  <license>Apache-2.0</license>

   <test_depend>ament_copyright</test_depend>
   <test_depend>ament_flake8</test_depend>
diff -u -upr ros2_ws/src/ex_rclpy_topic/setup.py ros2_ws2/src/ex_rclpy_topic/setup.py
--- ros2_ws/src/ex_rclpy_topic/setup.py 2025-06-02 23:00:07.394350407 +0900
+++ ros2_ws2/src/ex_rclpy_topic/setup.py        2025-06-02 23:08:12.257456923 +0900
@@ -16,7 +16,7 @@ setup(
     maintainer='daisuke',
     maintainer_email='daisuke@gmail.com',
     description='TODO: Package description',
-    license='TODO: License declaration',
+    license='Apache-2.0',
     tests_require=['pytest'],
     entry_points={
         'console_scripts': [

あまり、ディレクトリ名が長いのは好きじゃないので、全部消してから、ライセンスの指定を行ったパッケージを作ります。

$ mkdir -p ros2_ws/src

$ cd ros2_ws/src/

$ ros2 pkg create --build-type ament_python --license Apache-2.0 ex_rclpy_topic
going to create a new package
package name: ex_rclpy_topic
destination directory: /home/daisuke/svn_/ros2/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['daisuke <daisuke@gmail.com>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: []
creating folder ./ex_rclpy_topic
creating ./ex_rclpy_topic/package.xml
creating source folder
creating folder ./ex_rclpy_topic/ex_rclpy_topic
creating ./ex_rclpy_topic/setup.py
creating ./ex_rclpy_topic/setup.cfg
creating folder ./ex_rclpy_topic/resource
creating ./ex_rclpy_topic/resource/ex_rclpy_topic
creating ./ex_rclpy_topic/ex_rclpy_topic/__init__.py
creating folder ./ex_rclpy_topic/test
creating ./ex_rclpy_topic/test/test_copyright.py
creating ./ex_rclpy_topic/test/test_flake8.py
creating ./ex_rclpy_topic/test/test_pep257.py

$ tree
.
`-- ex_rclpy_topic
    |-- LICENSE
    |-- ex_rclpy_topic
    |   `-- __init__.py
    |-- package.xml
    |-- resource
    |   `-- ex_rclpy_topic
    |-- setup.cfg
    |-- setup.py
    `-- test
        |-- test_copyright.py
        |-- test_flake8.py
        `-- test_pep257.py

4 directories, 9 files

パッケージ作成時に、ノード名を指定すると、同時にノードを作成が出来るそうなので、そちらも試します。

$ mkdir -p ros2_ws2/src

$ cd ros2_ws2/src/

$ ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name hello ex_rclpy_topic
going to create a new package
package name: ex_rclpy_topic
destination directory: /home/daisuke/svn_/ros2/ros2_ws2/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['daisuke <daisuke@gmail.com>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: []
node_name: hello
creating folder ./ex_rclpy_topic
creating ./ex_rclpy_topic/package.xml
creating source folder
creating folder ./ex_rclpy_topic/ex_rclpy_topic
creating ./ex_rclpy_topic/setup.py
creating ./ex_rclpy_topic/setup.cfg
creating folder ./ex_rclpy_topic/resource
creating ./ex_rclpy_topic/resource/ex_rclpy_topic
creating ./ex_rclpy_topic/ex_rclpy_topic/__init__.py
creating folder ./ex_rclpy_topic/test
creating ./ex_rclpy_topic/test/test_copyright.py
creating ./ex_rclpy_topic/test/test_flake8.py
creating ./ex_rclpy_topic/test/test_pep257.py
creating ./ex_rclpy_topic/ex_rclpy_topic/hello.py

ノードを指定しない場合と、ノードを指定した場合を比較します。

なるほど、hello.py が作られたのと、setup.py に、hello というエントリポイントが追加されたようです。

$ diff -uprN ros2_ws ros2_ws2
diff -u -uprN ros2_ws/src/ex_rclpy_topic/ex_rclpy_topic/hello.py ros2_ws2/src/ex_rclpy_topic/ex_rclpy_topic/hello.py
--- ros2_ws/src/ex_rclpy_topic/ex_rclpy_topic/hello.py  1970-01-01 09:00:00.000000000 +0900
+++ ros2_ws2/src/ex_rclpy_topic/ex_rclpy_topic/hello.py 2025-06-03 22:44:27.741240247 +0900
@@ -0,0 +1,6 @@
+def main():
+    print('Hi from ex_rclpy_topic.')
+
+
+if __name__ == '__main__':
+    main()
diff -u -uprN ros2_ws/src/ex_rclpy_topic/setup.py ros2_ws2/src/ex_rclpy_topic/setup.py
--- ros2_ws/src/ex_rclpy_topic/setup.py 2025-06-03 22:42:05.392850422 +0900
+++ ros2_ws2/src/ex_rclpy_topic/setup.py        2025-06-03 22:44:27.693240479 +0900
@@ -20,6 +20,7 @@ setup(
     tests_require=['pytest'],
     entry_points={
         'console_scripts': [
+            'hello = ex_rclpy_topic.hello:main'
         ],
     },
 )

ビルド

ソースコードは変更してませんが、まずは、ビルドしてみます。ビルドは、ros2_wsディレクトリで実行してください。

ビルドが成功したようですが、結構な量のファイルが生成されています。

$ colcon build
Starting >>> ex_rclpy_topic
Finished <<< ex_rclpy_topic [7.87s]

Summary: 1 package finished [10.8s]

$ tree
.
|-- build
|   |-- COLCON_IGNORE
|   `-- ex_rclpy_topic
|       |-- build
|       |   `-- lib
|       |       `-- ex_rclpy_topic
|       |           `-- __init__.py
|       |-- colcon_build.rc
|       |-- colcon_command_prefix_setup_py.sh
|       |-- colcon_command_prefix_setup_py.sh.env
|       |-- ex_rclpy_topic.egg-info
|       |   |-- PKG-INFO
|       |   |-- SOURCES.txt
|       |   |-- dependency_links.txt
|       |   |-- entry_points.txt
|       |   |-- requires.txt
|       |   |-- top_level.txt
|       |   `-- zip-safe
|       |-- install.log
|       `-- prefix_override
|           |-- __pycache__
|           |   `-- sitecustomize.cpython-310.pyc
|           `-- sitecustomize.py
|-- install
|   |-- COLCON_IGNORE
|   |-- _local_setup_util_ps1.py
|   |-- _local_setup_util_sh.py
|   |-- ex_rclpy_topic
|   |   |-- lib
|   |   |   `-- python3.10
|   |   |       `-- site-packages
|   |   |           |-- ex_rclpy_topic
|   |   |           |   |-- __init__.py
|   |   |           |   `-- __pycache__
|   |   |           |       `-- __init__.cpython-310.pyc
|   |   |           `-- ex_rclpy_topic-0.0.0-py3.10.egg-info
|   |   |               |-- PKG-INFO
|   |   |               |-- SOURCES.txt
|   |   |               |-- dependency_links.txt
|   |   |               |-- entry_points.txt
|   |   |               |-- requires.txt
|   |   |               |-- top_level.txt
|   |   |               `-- zip-safe
|   |   `-- share
|   |       |-- ament_index
|   |       |   `-- resource_index
|   |       |       `-- packages
|   |       |           `-- ex_rclpy_topic
|   |       |-- colcon-core
|   |       |   `-- packages
|   |       |       `-- ex_rclpy_topic
|   |       `-- ex_rclpy_topic
|   |           |-- hook
|   |           |   |-- ament_prefix_path.dsv
|   |           |   |-- ament_prefix_path.ps1
|   |           |   |-- ament_prefix_path.sh
|   |           |   |-- pythonpath.dsv
|   |           |   |-- pythonpath.ps1
|   |           |   `-- pythonpath.sh
|   |           |-- package.bash
|   |           |-- package.dsv
|   |           |-- package.ps1
|   |           |-- package.sh
|   |           |-- package.xml
|   |           `-- package.zsh
|   |-- local_setup.bash
|   |-- local_setup.ps1
|   |-- local_setup.sh
|   |-- local_setup.zsh
|   |-- setup.bash
|   |-- setup.ps1
|   |-- setup.sh
|   `-- setup.zsh
|-- log
|   |-- COLCON_IGNORE
|   |-- build_2025-06-02_23-21-56
|   |   |-- events.log
|   |   |-- ex_rclpy_topic
|   |   |   |-- command.log
|   |   |   |-- stderr.log
|   |   |   |-- stdout.log
|   |   |   |-- stdout_stderr.log
|   |   |   `-- streams.log
|   |   `-- logger_all.log
|   |-- latest -> latest_build
|   `-- latest_build -> build_2025-06-02_23-21-56
`-- src
    `-- ex_rclpy_topic
        |-- LICENSE
        |-- ex_rclpy_topic
        |   `-- __init__.py
        |-- package.xml
        |-- resource
        |   `-- ex_rclpy_topic
        |-- setup.cfg
        |-- setup.py
        `-- test
            |-- test_copyright.py
            |-- test_flake8.py
            `-- test_pep257.py

34 directories, 66 files

書籍を見ると、--symlink-install というオプションを付けると、Python の場合、ソースコードを変更しても再ビルドが不要になるとのことです。それは付けた方が絶対いいですね。こちらも、結構な量のファイルが生成されています。

$ colcon build --symlink-install
Starting >>> ex_rclpy_topic
Finished <<< ex_rclpy_topic [10.3s]

Summary: 1 package finished [13.2s]

$ tree
.
|-- build
|   |-- COLCON_IGNORE
|   `-- ex_rclpy_topic
|       |-- colcon_build.rc
|       |-- colcon_command_prefix_setup_py.sh
|       |-- colcon_command_prefix_setup_py.sh.env
|       |-- ex_rclpy_topic -> /home/daisuke/svn_/ros2/ros2_ws2/src/ex_rclpy_topic/ex_rclpy_topic
|       |-- ex_rclpy_topic.egg-info
|       |   |-- PKG-INFO
|       |   |-- SOURCES.txt
|       |   |-- dependency_links.txt
|       |   |-- entry_points.txt
|       |   |-- requires.txt
|       |   |-- top_level.txt
|       |   `-- zip-safe
|       |-- package.xml -> /home/daisuke/svn_/ros2/ros2_ws2/src/ex_rclpy_topic/package.xml
|       |-- prefix_override
|       |   |-- __pycache__
|       |   |   `-- sitecustomize.cpython-310.pyc
|       |   `-- sitecustomize.py
|       |-- resource
|       |   `-- ex_rclpy_topic -> /home/daisuke/svn_/ros2/ros2_ws2/src/ex_rclpy_topic/resource/ex_rclpy_topic
|       |-- setup.cfg -> /home/daisuke/svn_/ros2/ros2_ws2/src/ex_rclpy_topic/setup.cfg
|       |-- setup.py -> /home/daisuke/svn_/ros2/ros2_ws2/src/ex_rclpy_topic/setup.py
|       `-- share
|           `-- ex_rclpy_topic
|               `-- hook
|                   |-- pythonpath_develop.dsv
|                   |-- pythonpath_develop.ps1
|                   `-- pythonpath_develop.sh
|-- install
|   |-- COLCON_IGNORE
|   |-- _local_setup_util_ps1.py
|   |-- _local_setup_util_sh.py
|   |-- ex_rclpy_topic
|   |   |-- lib
|   |   |   `-- python3.10
|   |   |       `-- site-packages
|   |   |           `-- ex-rclpy-topic.egg-link
|   |   `-- share
|   |       |-- ament_index
|   |       |   `-- resource_index
|   |       |       `-- packages
|   |       |           `-- ex_rclpy_topic -> /home/daisuke/svn_/ros2/ros2_ws2/build/ex_rclpy_topic/resource/ex_rclpy_topic
|   |       |-- colcon-core
|   |       |   `-- packages
|   |       |       `-- ex_rclpy_topic
|   |       `-- ex_rclpy_topic
|   |           |-- hook
|   |           |   |-- ament_prefix_path.dsv
|   |           |   |-- ament_prefix_path.ps1
|   |           |   |-- ament_prefix_path.sh
|   |           |   |-- pythonpath.dsv
|   |           |   |-- pythonpath.ps1
|   |           |   `-- pythonpath.sh
|   |           |-- package.bash
|   |           |-- package.dsv
|   |           |-- package.ps1
|   |           |-- package.sh
|   |           |-- package.xml -> /home/daisuke/svn_/ros2/ros2_ws2/build/ex_rclpy_topic/package.xml
|   |           `-- package.zsh
|   |-- local_setup.bash
|   |-- local_setup.ps1
|   |-- local_setup.sh
|   |-- local_setup.zsh
|   |-- setup.bash
|   |-- setup.ps1
|   |-- setup.sh
|   `-- setup.zsh
|-- log
|   |-- COLCON_IGNORE
|   |-- build_2025-06-02_23-27-27
|   |   |-- events.log
|   |   |-- ex_rclpy_topic
|   |   |   |-- command.log
|   |   |   |-- stderr.log
|   |   |   |-- stdout.log
|   |   |   |-- stdout_stderr.log
|   |   |   `-- streams.log
|   |   `-- logger_all.log
|   |-- latest -> latest_build
|   `-- latest_build -> build_2025-06-02_23-27-27
`-- src
    `-- ex_rclpy_topic
        |-- LICENSE
        |-- ex_rclpy_topic
        |   `-- __init__.py
        |-- package.xml
        |-- resource
        |   `-- ex_rclpy_topic
        |-- setup.cfg
        |-- setup.py
        `-- test
            |-- test_copyright.py
            |-- test_flake8.py
            `-- test_pep257.py

33 directories, 63 files

ディレクトリ構成を見比べたのですが、違いが多すぎて、あまりいいコメントが出来ません。しかし、再ビルドが不要になるというのは、とても大きいことですので、ここでは、必ず、--symlink-install を付けることにします。

設定ファイルの反映

パッケージを作成し、それを実行するには、パッケージの位置などを ROS2 に伝える必要があります。そのため、アンダーレイ設定ファイルの反映と、オーバーレイ設定ファイルの反映の 2つを実行する必要があります。

アンダーレイ設定ファイルの反映というのは、最初に行う必要があると言っていた、以下の読み込みです。

$ source /opt/ros/humble/setup.bash

オーバーレイ設定ファイルの反映というのは、今回作成したパッケージの位置を ROS2 に伝えるもので、以下の読み込みです。ビルド時に、srcディレクトリと同じ位置に、installディレクトリが生成されていて、その中の setup.bashファイルを読み込むと ROS2 が、このパッケージを認識するようになります。

$ source ros2_ws/install/setup.bash

ノードの実行

では、実行してみます。

まず、パッケージ作成時に、ノードを指定しない場合です。タブキーによる補完が効きます。

ノードを作成していないので、まぁ、このようになると思います。

$ ros2 run ex_rclpy_topic node
No executable found

次に、ノードを指定してパッケージを作成した場合です。

正しく実行できたようです。

$ ros2 run ex_rclpy_topic hello
Hi from ex_rclpy_topic.

ここまでで、とりあえず、ROS2プログラミングの一通りの流れを実行できました。

トピックの実装

公式サイトのチュートリアルでも、書籍でも、ROS2 の公式の GitHub の examples のソースコードを参照してますので、ここでも、それを参照します。

github.com

その上で、以下のソースコードを使います。これを、上で作ったパッケージの中(ros2_ws/src/ex_rclpy_topic/ex_rclpy_topic)にコピーします。

自分で、ディレクトリを辿っていく場合は、Git のブランチを humble に切り替えてから、rclpy→topics→minimal_publisher→examples_rclpy_minimal_publisher→publisher_member_function.py に進んでください。

https://github.com/ros2/examples/blob/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py

クローンする場合は、以下です。

$ git clone https://github.com/ros2/examples.git

$ cd examples/

$ git checkout humble

$ cp rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py ../ros2_ws/src/ex_rclpy_topic/ex_rclpy_topic/

このファイルは以下のようになっています。

self.create_publisher(String, 'topic', 10) は、Stringタイプのメッセージであり、topic という名前のトピックとし、キューサイズを 10 にする(QoS設定)という意味です。

タイマを作り、0.5秒ごとに、コールバック関数(timer_callback)が呼び出されるようにしています。

main関数の rclpy.spin(minimal_publisher) は、無限ループを作り、Ctrl+C が入力されるまで、Publisher が動き続けることを意味します。

あとは、だいたい予想できそうなソースコードかな、と思います。

# Copyright 2016 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalPublisher(Node):

    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: %d' % self.i
        self.publisher_.publish(msg)
        self.get_logger().info('Publishing: "%s"' % msg.data)
        self.i += 1


def main(args=None):
    rclpy.init(args=args)

    minimal_publisher = MinimalPublisher()

    rclpy.spin(minimal_publisher)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_publisher.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

ros2_ws/src/ex_rclpy_topic/package.xml をエディタで開きます。

ここでは、先ほどのソースコードが使っているライブラリの依存関係を書く必要があります。rclpy と std_msgs を使っているので、その依存関係を記述しているということになります。

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>ex_rclpy_topic</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="daisuke@gmail.com">daisuke</maintainer>
  <license>Apache-2.0</license>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

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

以下の差分のように追記します。

$ diff -upr src/ex_rclpy_topic/package.org.xml s
rc/ex_rclpy_topic/package.xml
--- src/ex_rclpy_topic/package.org.xml  2025-06-07 21:18:51.818196200 +0900
+++ src/ex_rclpy_topic/package.xml      2025-06-07 20:59:23.963658630 +0900
@@ -6,6 +6,8 @@
   <description>TODO: Package description</description>
   <maintainer email="daisuke@gmail.com">daisuke</maintainer>
   <license>Apache-2.0</license>
+  <exec_depend>rclpy</exec_depend>
+  <exec_depend>std_msgs</exec_depend>

   <test_depend>ament_copyright</test_depend>
   <test_depend>ament_flake8</test_depend>

ros2_ws/src/ex_rclpy_topic/setup.py をエディタで開きます。

ここでは、先ほどパッケージを作成したときに、ノードを指定した場合と、ノードを指定しない場合がありましたが、そこの部分に、今回追加したソースコードを実行するためのエントリポイントを追記する必要があります。

from setuptools import find_packages, setup

package_name = 'ex_rclpy_topic'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='daisuke',
    maintainer_email='daisuke@gmail.com',
    description='TODO: Package description',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
        ],
    },
)

以下の差分のように追記します。

$ diff -upr src/ex_rclpy_topic/setup.org.py src/
ex_rclpy_topic/setup.py
--- src/ex_rclpy_topic/setup.org.py     2025-06-07 21:18:58.113404537 +0900
+++ src/ex_rclpy_topic/setup.py 2025-06-07 21:06:16.049194470 +0900
@@ -20,6 +20,7 @@ setup(
     tests_require=['pytest'],
     entry_points={
         'console_scripts': [
+            'talker = ex_rclpy_topic.publisher_member_function:main',
         ],
     },
 )

ビルドします。ノードを増やした場合は、最初に一度はビルドがおそらく必要です。ros2_wsディレクトリから実行してください。

$ colcon build --symlink-install
Starting >>> ex_rclpy_topic
Finished <<< ex_rclpy_topic [7.39s]

Summary: 1 package finished [9.72s]

設定ファイルを反映します。オーバーレイ設定ファイルの反映だけでいいと思います。

$  source install/setup.bash

実行してみます。

正しく動いてそうです。

$ ros2 run ex_rclpy_topic talker
[INFO] [1749297998.439871494] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1749297998.830894738] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1749297999.331007439] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [1749297999.831704379] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [1749298000.327904624] [minimal_publisher]: Publishing: "Hello World: 4"
[INFO] [1749298000.826767945] [minimal_publisher]: Publishing: "Hello World: 5"
[INFO] [1749298001.337871749] [minimal_publisher]: Publishing: "Hello World: 6"
^CTraceback (most recent call last):
  File "/home/daisuke/svn_/ros2/ros2_ws/install/ex_rclpy_topic/lib/ex_rclpy_topic/talker", line 33, in <module>
    sys.exit(load_entry_point('ex-rclpy-topic', 'console_scripts', 'talker')())
  File "/home/daisuke/svn_/ros2/ros2_ws/build/ex_rclpy_topic/ex_rclpy_topic/publisher_member_function.py", line 43, in main
    rclpy.spin(minimal_publisher)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 226, in spin
    executor.spin_once()
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 751, in spin_once
    self._spin_once_impl(timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 740, in _spin_once_impl
    handler, entity, node = self.wait_for_ready_callbacks(timeout_sec=timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 723, in wait_for_ready_callbacks
    return next(self._cb_iter)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 620, in _wait_for_ready_callbacks
    wait_set.wait(timeout_nsec)
KeyboardInterrupt
[ros2run]: Interrupt

Publisher の発行するメッセージを受信する Subscriber の方も追加します。

ソースコードは、同じ GitHub の以下になります。

https://github.com/ros2/examples/blob/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py

私はクローンしたので、以下のようにコピーしました。

$ cp ../examples/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py src/ex_rclpy_topic/ex_rclpy_topic/

内容は以下です。

Publisher とほとんど同じ内容になっています。self.subscription = self.create_subscription(String, 'topic', self.listener_callback, 10) が、少し違いますが、対象のトピックを指定して、受信した場合のコールバック関数(self.listener_callback)を指定しているところが違います。

# Copyright 2016 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.data)


def main(args=None):
    rclpy.init(args=args)

    minimal_subscriber = MinimalSubscriber()

    rclpy.spin(minimal_subscriber)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_subscriber.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

依存関係は同じライブラリを使っているため、package.xml に追記は必要ありません。

Subscriber もノードとして実行するので、setup.py に、エントリポイントを追加する必要があります。

以下の差分のように追記します。編集前の setup.py と比較してるため、Publisher エントリポイントを追加した差分も入っています。

$ diff -upr src/ex_rclpy_topic/setup.org.py src/ex_rclpy_topic/setup.py
--- src/ex_rclpy_topic/setup.org.py     2025-06-07 21:23:49.340223302 +0900
+++ src/ex_rclpy_topic/setup.py 2025-06-07 23:02:16.976865707 +0900
@@ -20,6 +20,8 @@ setup(
     tests_require=['pytest'],
     entry_points={
         'console_scripts': [
+            'talker = ex_rclpy_topic.publisher_member_function:main',
+            'listener = ex_rclpy_topic.subscriber_member_function:main',
         ],
     },
 )

ビルドします。

$ colcon build --symlink-install
Starting >>> ex_rclpy_topic
Finished <<< ex_rclpy_topic [10.4s]

Summary: 1 package finished [14.7s]

オーバーレイ設定ファイルを反映して、実行してみます。

$ source install/setup.bash

$ ros2 run ex_rclpy_topic talker
[INFO] [1749305249.157788926] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1749305249.568920217] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1749305250.067770284] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [1749305250.565504097] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [1749305251.064488536] [minimal_publisher]: Publishing: "Hello World: 4"
[INFO] [1749305251.564976741] [minimal_publisher]: Publishing: "Hello World: 5"
[INFO] [1749305252.062874459] [minimal_publisher]: Publishing: "Hello World: 6"
[INFO] [1749305252.564983971] [minimal_publisher]: Publishing: "Hello World: 7"
[INFO] [1749305253.062627703] [minimal_publisher]: Publishing: "Hello World: 8"
[INFO] [1749305253.567195950] [minimal_publisher]: Publishing: "Hello World: 9"
[INFO] [1749305254.145280067] [minimal_publisher]: Publishing: "Hello World: 10"
[INFO] [1749305254.610332360] [minimal_publisher]: Publishing: "Hello World: 11"
[INFO] [1749305255.064946204] [minimal_publisher]: Publishing: "Hello World: 12"
[INFO] [1749305255.566667225] [minimal_publisher]: Publishing: "Hello World: 13"
[INFO] [1749305256.068202391] [minimal_publisher]: Publishing: "Hello World: 14"
[INFO] [1749305256.564076172] [minimal_publisher]: Publishing: "Hello World: 15"
[INFO] [1749305257.065215361] [minimal_publisher]: Publishing: "Hello World: 16"
[INFO] [1749305257.565358263] [minimal_publisher]: Publishing: "Hello World: 17"
^CTraceback (most recent call last):
  File "/home/daisuke/svn_/ros2/ros2_ws/install/ex_rclpy_topic/lib/ex_rclpy_topic/talker", line 33, in <module>
    sys.exit(load_entry_point('ex-rclpy-topic', 'console_scripts', 'talker')())
  File "/home/daisuke/svn_/ros2/ros2_ws/build/ex_rclpy_topic/ex_rclpy_topic/publisher_member_function.py", line 43, in main
    rclpy.spin(minimal_publisher)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 226, in spin
    executor.spin_once()
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 751, in spin_once
    self._spin_once_impl(timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 740, in _spin_once_impl
    handler, entity, node = self.wait_for_ready_callbacks(timeout_sec=timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 723, in wait_for_ready_callbacks
    return next(self._cb_iter)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 620, in _wait_for_ready_callbacks
    wait_set.wait(timeout_nsec)
KeyboardInterrupt
[ros2run]: Interrupt

別のターミナルで、Subscriber を実行します。

Publisher が発行したメッセージを Subscriber で受信できていそうです。

$ source install/setup.bash

$ ros2 run ex_rclpy_topic listener
[INFO] [1749305254.272581336] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [1749305254.612006673] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [1749305255.068237569] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [1749305255.567216857] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [1749305256.069195889] [minimal_subscriber]: I heard: "Hello World: 14"
[INFO] [1749305256.565059191] [minimal_subscriber]: I heard: "Hello World: 15"
^CTraceback (most recent call last):
  File "/home/daisuke/svn_/ros2/ros2_ws/install/ex_rclpy_topic/lib/ex_rclpy_topic/listener", line 33, in <module>
    sys.exit(load_entry_point('ex-rclpy-topic', 'console_scripts', 'listener')())
  File "/home/daisuke/svn_/ros2/ros2_ws/build/ex_rclpy_topic/ex_rclpy_topic/subscriber_member_function.py", line 41, in main
    rclpy.spin(minimal_subscriber)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/__init__.py", line 226, in spin
    executor.spin_once()
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 751, in spin_once
    self._spin_once_impl(timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 740, in _spin_once_impl
    handler, entity, node = self.wait_for_ready_callbacks(timeout_sec=timeout_sec)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 723, in wait_for_ready_callbacks
    return next(self._cb_iter)
  File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 620, in _wait_for_ready_callbacks
    wait_set.wait(timeout_nsec)
KeyboardInterrupt
[ros2run]: Interrupt

今回は以上になります。

おわりに

今回は、ROS2 のプログラミングをやってみました。トピック以外に、サービス、パラメータ、アクションがありますが、そのあたりは必要なときにやればいいとして、次回は、ROS2 の実装を見ていきたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/ros2_topicより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14