はじめに
ROS2とPython3を用いて、カスタムメッセージを使った簡単なノードを作ってみます。
Python3ではじめるROS2 関連記事
| 回数 | サブタイトル | 内容 | 
|---|---|---|
| 第1回 | カスタムメッセージ編 | ・環境の準備 ・カスタムメッセージを作る ・(おまけ).bashrcのカスタマイズ | 
| 第2回(今回) | ノード編 | ・Pub/Subのノードを作る | 
| 第3回 | 自動起動編 | ・roslaunchから起動する ・systemdからroslaunchを起動する | 
1.Pub / Sub ノードの作成
ROS1ではあまり気にしませんでしたが、ROS2では必ず「パッケージ」を作る必要があります。
ここでは、pub/subノードののパッケージを準備します。
(1)準備
~$ cd ~/ros2_ws/src/
# ワークスペースを作成
~/ros2_ws/src/$ ros2 pkg create pubsubpy
~/ros2_ws/src/$ cd pubsubpy
# オリジナルのバックアップ、不要なファイルの改名
~/ros2_ws/src/pubsubpy$ cp -p package.xml package.xml.org
~/ros2_ws/src/pubsubpy$ mv CMakeLists.txt CMakeLists.txt.org
# ファイルの生成
~/ros2_ws/src/pubsubpy$ touch __init__.py setup.py setup.cfg
# nodeディレクトリの作成
~/ros2_ws/src/pubsubpy$ mkdir node
# あとでビルドするときに下記のエラーがでるので、予め空ファイルを作っておく
# 「error: can't copy 'resource/pubsubpy': doesn't exist or not a regular file」
~/ros2_ws/src/pubsubpy$ mkdir resource
~/ros2_ws/src/pubsubpy$ cd resource
~/ros2_ws/src/pubsubpy/resource$ touch pubsubpy
~/ros2_ws/src/pubsubpy/resource$ cd ../..
~/ros2_ws/src$
この時点で、下記のようなディレクトリ構成になっています。
~/ros2_ws
   + src
     + pubsubpy
     |  + include
     |  + node
     |  + resource
     |  + src
     |
     + pubsubpy_mes
        + msg
        + src
        + srv
VSCodeから見たディレクトリ構成・ファイル構成です。
(2)Pythonパッケージの準備
init.py
忘れずに__init__.pyを作成して下さい。中身は空で良いです。
package.xml
次に、先に実行したros2 pkg create pubsubpyで作られたファイルを編集します。
<?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>pubsubpy</name>
  <version>0.0.0</version>
  <description>pubsubpy sample</description>
  <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
  <license>TODO: License declaration</license>
  <!-- ### python使う時は不要なのでコメントアウト ### -->
  <!--<buildtool_depend>ament_cmake</buildtool_depend>
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>-->
  <!-- ### ここにmsgのパッケージ名を記述 ### -->
  <exec_depend>pubsubpy_mes</exec_depend>
  <!-- ### その他、依存関係のあるパッケージ ### -->
  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>launch_ros</exec_depend>
  <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>
   <!-- ### python使う時は不要なのでコメントアウト ### -->
    <!--<build_type>ament_cmake</build_type>-->
    <build_type>ament_python</build_type>
  </export>
</package>
setup.py
新規に作ったsetup.pyファイルを記述します。
from setuptools import find_packages
from setuptools import setup
import os
from glob import glob
### ここにパッケージ名を記述 ###
package_name = 'pubsubpy'
### 必要に応じてパッケージ情報を記述 ###
setup(
    name=package_name,
    version='0.7.1',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('*.launch.py'))
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    author='ubuntu',
    author_email='ubuntu@todo.todo',
    maintainer='ubuntu',
    maintainer_email='ubuntu@todo.todo',
    keywords=['ROS'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: MIT License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description=(
        'pubsubpy sample'
    ),
    license='MIT License',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'pub = node.Pub:main',
            'sub = node.Sub:main',
        ],
    },
)
### ↑ entry_pointsの部分に、パッケージに含まれるノード(pythonソース)を列挙 ###
上記のソースの終わりの方で、ノード(pythonソース)を記述しますが、node.*:mainとなっているのは、pythonのソースをpubsubpy/nodeディレクトリに置いているためです。
ここの書き方が、地味にハマりどころなので、注意して下さい・・・
(間違えると、runするときにpythonノードが見つからないエラーが出ます)
setup.cfg
最後に、新規に作ったsetup.cfgファイルを記述します。
[develop]
script-dir=$base/lib/pubsubpy
[install]
install-scripts=$base/lib/pubsubpy
(3)ノードを作る
~/ros2_ws/src/pubsubpy$ mkdir node 
~/ros2_ws/src/pubsubpy$ cd node
~/ros2_ws/src/pubsubpy/node$ touch Pub.py Sub.py __init__.py
# nodeディレクトリにも__init__.pyを生成します
# 作るのを忘れると ros2 run pubsubpy pub したときに
# 「ModuleNotFoundError: No module named 'Pub'」エラーが出ます
ソースファイル名の先頭文字は、大文字にしてください。
忘れずに__init__.pyを作成して下さい。中身は空で良いです。
送信側ノード
# !/usr/bin/env /usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------
# ROS2 Node 送信側
#
# The MIT License (MIT)
# Copyright (C) 2019 myasu.
# -----------------------------------------------
import rclpy
from rclpy.node import Node
# カスタムメッセージ
from pubsubpy_mes.msg import GpioMsg
class MyPublisher(Node):
    """
    送信側
    """
    # ノード名
    SELFNODE = "mypub"
    # トピック名
    SELFTOPIC = "mes_" + SELFNODE
    def __init__(self):
        """
        コンストラクタ
        Parameters
        ----------
        """
        # ノードの初期化
        super().__init__(self.SELFNODE)
        # コンソールに表示
        self.get_logger().info("%s initializing..." % (self.SELFNODE))
        # publisherインスタンスを生成
        self.pub = self.create_publisher(GpioMsg, self.SELFTOPIC, 10)
        # タイマーのインスタンスを生成(1秒ごとに発生)
        self.create_timer(1.0, self.callback)
        # カウンタをリセット
        self.count = 0
        # コンソールに表示
        self.get_logger().info("%s do..." % self.SELFNODE)
    def __del__(self):
        """
        デストラクタ
        """
        # コンソールに表示
        self.get_logger().info("%s done." % self.SELFNODE)
    def callback(self):
        """
        タイマーの実行部
        """
        self.get_logger().info("Publish [%s]" % (self.count))
        # 送信するメッセージの作成
        msg = GpioMsg()
        msg.port = 0
        msg.value = self.count
        # 送信
        self.pub.publish(msg)
        # カウンタをインクリメント
        self.count += 1
def main(args=None):
    """
    メイン関数
    Parameters
    ----------
    """
    try:
        # rclpyの初期化
        rclpy.init(args=args)
        # インスタンスを生成
        node = MyPublisher()
        # プロセス終了までアイドリング
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        # 終了処理
        node.destroy_node()
        rclpy.shutdown()
if __name__ == '__main__':
    main()
受信側ノード
# !/usr/bin/env /usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------
# ROS2 Node 受信側
#
# The MIT License (MIT)
# Copyright (C) 2019 myasu.
# -----------------------------------------------
import rclpy
from rclpy.node import Node
# カスタムメッセージ
from pubsubpy_mes.msg import GpioMsg
class MySubscription(Node):
    """
    受信側
    """
    # ノード名
    SELFNODE = "mysub"
    # トピック名
    SELFTOPIC = "mes_" + SELFNODE
    def __init__(self):
        """
        コンストラクタ
        Parameters
        ----------
        """
        # ノードの初期化
        super().__init__(self.SELFNODE)
        # コンソールに表示
        self.get_logger().info("%s initializing..." % (self.SELFNODE))
        # subscriptionインスタンスを生成
        self.sub = self.create_subscription(
            GpioMsg, "mes_mypub", self.callback, 10)
        # コンソールに表示
        self.get_logger().info("%s do..." % self.SELFNODE)
    def __del__(self):
        """
        デストラクタ
        """
        self.get_logger().info("%s done." % self.SELFNODE)
    def callback(self, message):
        """
        コールバック関数
        Parameters
        ----------
        message : gpio_mes
            メッセージ
        """
        # 受け取ったメッセージの表示
        self.get_logger().info('Subscription > Port: %d Value: %d' %
                               (message.port, message.value))
def main(args=None):
    """
    メイン関数
    Parameters
    ----------
    """
    try:
        # rclpyの初期化
        rclpy.init(args=args)
        # インスタンスを生成
        node = MySubscription()
        # プロセス終了までアイドリング
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        # 終了処理
        node.destroy_node()
        rclpy.shutdown()
if __name__ == '__main__':
    main()
(4)ビルド
# ワーキングディレクトリに戻ってからビルド
~/ros2_ws/src/node$ de ../..
~/ros2_ws$ colcon build --symlink-install
Starting >>> pubsubpy_mes
Finished <<< pubsubpy_mes [8.57s]                        
Starting >>> pubsubpy
Finished <<< pubsubpy [15.1s]            
Summary: 2 packages finished [25.8s]
# 環境変数の読み込み
~/ros2_ws$ source ./install/setup.bash
# パッケージがインストールできたか確認
~/ros2_ws$ ros2 pkg list | grep pubsubpy
pubsubpy
pubsubpy_mes
ubuntu@raspi3u:~/ros2_ws$ 
(5)実行
[Ctrl-C]で終了できます。
~/ros2_ws$ ros2 run pubsubpy pub
[INFO] [mypub]: mypub initializing...
[INFO] [mypub]: mypub do...
[INFO] [mypub]: Publish [0]
[INFO] [mypub]: Publish [1]
[INFO] [mypub]: Publish [2]
[INFO] [mypub]: Publish [3]
[INFO] [mypub]: Publish [4]
[INFO] [mypub]: Publish [5]
[INFO] [mypub]: Publish [6]
[INFO] [mypub]: Publish [7]
[INFO] [mypub]: Publish [8]
[INFO] [mypub]: Publish [9]
[INFO] [mypub]: Publish [10]
^C[INFO] [mypub]: mypub done.
~/ros2_ws$ ros2 run pubsubpy sub
[INFO] [mysub]: mysub initializing...
[INFO] [mysub]: mysub do...
[INFO] [mysub]: Subscription > Port: 0 Value: 0
[INFO] [mysub]: Subscription > Port: 0 Value: 1
[INFO] [mysub]: Subscription > Port: 0 Value: 2
[INFO] [mysub]: Subscription > Port: 0 Value: 3
[INFO] [mysub]: Subscription > Port: 0 Value: 4
[INFO] [mysub]: Subscription > Port: 0 Value: 5
[INFO] [mysub]: Subscription > Port: 0 Value: 6
[INFO] [mysub]: Subscription > Port: 0 Value: 7
[INFO] [mysub]: Subscription > Port: 0 Value: 8
[INFO] [mysub]: Subscription > Port: 0 Value: 9
[INFO] [mysub]: Subscription > Port: 0 Value: 10
^C[INFO] [mysub]: mysub done.
~/ros2_ws$ 
(6)デバッグ
(作成中)
おわりに
ROS2の勉強を19年7月から始めたのですが、やはりPython3のまとまった作例が少なかったので、覚え書きを兼ねてまとめを作ってみました。
参考になりましたら幸いです。

