ROS2で自分で作成したpythonプログラムを動かす方法

# 初めに
前回はros2でpythonのサンプルを実行しました[^1]．
今回は自分で作成したプログラムを動かす方法とポイントをまとめます．

ターミナルを立ち上げたままだと環境情報が残っている場合があるので再度立ち上げてから行ってください．

# ワークスペースとパッケージの作成
まずはワークスペースを作ります．

```
mkdir -p ~/ros2_my_python_ws/src
cd ~/ros2_my_python_ws/src
```

自分でpython用のROS2パッケージを作成して実行していきます[^2]．

`ros2 pkg create [パッケージ名]`コマンドでros2のパッケージを作成します．
ros1でのcatkin_create_packageに当たります．

```
ros2 pkg create my_python
```

これで必要なファイルが生成されました．ディレクトリ構造は以下のようになります．

```
ros2_my_python_ws
└── src
    └── my_python
        ├── CMakeLists.txt
        ├── include
        │   └── my_python
        ├── package.xml
        └── src
```

# pythonプログラムの作成
先程作成した`my_pythonパッケージ`の中に移動してください．
pythonプログラムを配置するフォルダを作ります．
フォルダ名はなんでもいいです．

```
mkdir python_programs
```

この中にlistenerとtalkerのサンプルプログラムを入れます．

<details><summary>listener.py</summary><div>

```listener.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class Listener(Node):
    def __init__(self):
        super().__init__('listener')
        self.sub = self.create_subscription(String, 'chatter', self.chatter_callback, 10)

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

def main(args=None):
    rclpy.init(args=args)
    try:
        listener = Listener()
        rclpy.spin(listener)
    finally:
        listener.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()
```
</div></details>

<details><summary>talker.py</summary><div>

```talker.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class Talker(Node):
    def __init__(self):
        super().__init__('talker')
        self.i = 0
        self.pub = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: {0}'.format(self.i)
        self.i += 1
        self.get_logger().info('Publishing: "{0}"'.format(msg.data))
        self.pub.publish(msg)

def main(args=None):
    rclpy.init(args=args)
    try:
        talker = Talker()
        rclpy.spin(talker)
    finally:
        talker.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()
```

</div></details>

最後にポイントとして，`python_programs`フォルダに作成したpythonファイルが認識されるように`__init__.py`を追加します．

```
touch __init__.py
```

作成したROSパッケージのディレクトリ構造は以下のようになります．

```
./my_python
├── CMakeLists.txt
├── include
│   └── my_python
├── package.xml
├── python_programs
│   ├── __init__.py
│   ├── talker.py
│   └── listener.py
└── src
```

# package.xmlの編集
まずデフォルトでは以下のようになります．
<details><summary>package.xml</summary><div>

```package.xml
<?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>my_python</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="hoge@gmail.com">root</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

```

</div></details>

`<build_type>`がcpp用になっていたりするのでpython用に書き換えます．

<details><summary>package.xml</summary><div>

```package.xml
<?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>my_python</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="hoge@gmail.com">root</maintainer>
  <license>TODO: License declaration</license>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>

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

```

</div></details>

# CMakeLists.txtの削除とpython用ファイルの追加
現時点での`ros2 create pkg`コマンドはpython用ではなく，c++用のファイル(`CMakeLists.txt`)を生成します．

今回はpythonだけを動かしたいのでc++のコンパイル対象にならないように`CMakeLists.txt`は削除します．

```
rm CMakeLists.txt
```

## setup.pyの追加
pythonをros2で実行するために`setup.py`を追加します．
ros1の頃はこのようなファイルは必要なかったのですが，ros2から導入されたようです．
自分用に書き換えるのは`package_name,py_modules,entry_points`あたりでしょうか．
他はメタデータだったりするので適当で．

<details><summary>setup.py</summary><div>

```setup.py
from setuptools import setup

package_name = 'my_python'

setup(
    name=package_name,
    version='0.0.0',
    packages=[],
    py_modules=[
        'python_programs.talker',
        'python_programs.listener',
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    author='user',
    author_email="user@todo.todo",
    maintainer='user',
    maintainer_email="user@todo.todo",
    keywords=['ROS', 'ROS2'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description='TODO: Package description.',
    license='Apache License, Version 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'my_talker= python_programs.talker:main',
            'my_listener= python_programs.listener:main',
        ],
    },
)
```
</div></details>

### ポイント
いくつかポイントがあります．
`py_modules`について

```
py_modules=["foo","bar.hoge"]
```
これは
`foo.py`は`setup.py`と同じディレクトリにあるということです．
`bar.hoge`は`setup.py`と同じディレクトリに`bar`というフォルダが有り，その中に`hoge.py`があるということです．
以下のような構造になります．

```
.
├── bar
│   └── hoge.py
├── foo.py
└── setup.py
```
フォルダ構造を自分で作ると***ハマリポイント***になります．

`entry_points`について
entry_pointsは実行するファイルと関数を指定します．

```
'my_talker= python_programs.talker:main',
```

これはmy_talkerを実行すると，python_programsフォルダの中のtalker.pyの中のmain関数を呼び出すということです．

## setup.cfgの追加
次に`setup.cfg`を追加します．自分で作成したros2のパッケージ名をしっかりと指定しましょう．

```
[develop]
script-dir=$base/lib/my_python
[install]
install-scripts=$base/lib/my_python
```

ここでしっかりと指定しないと

```
$ ros2 run my_python my_talker
$ No executable found
```
などのエラーが発生します．

以上を行ったディレクトリ構造は以下のようになります．

```
./my_python
├── include
│   └── my_python
├── package.xml
├── python_programs
│   ├── __init__.py
│   ├── listener.py
│   └── talker.py
├── setup.cfg
├── setup.py
└── src
```

# コンパイルと実行
コンパイルします．

```
cd ~/ros2_my_python_ws
colcon build
```

コンパイルの出力先フォルダにパスを通します．

```
source install/setup.bash && source install/local_setup.bash
```

talkerを実行します．

```talker.
ros2 run my_python my_talker
```

ターミナルを新しく開いて同様にsourceをしてパスを通してからlistenerを起動します．

```listener.
ros2 run my_python my_listener
```

これでtalkerからpublishされたメッセージをlistenerがsubscribeします．

# おわりに
ros2で自分で作成したpythonプログラムを動かす方法をまとめました．
`ros2 pkg create`コマンドがpython用に働かないため，自分でディレクトリ構成を整えるのが大変だと思いました．
pythonのモジュールの読み込み方を知らない人などは`__init__.py`などのハマりポイントがあります．

飽きてなかったら次はpythonとC++を共生させる方法について書きます．

[^1]: https://qiita.com/inomu/items/1f8059a38b5dc73a9476
[^2]: http://www.theconstructsim.com/ja/create-python-publisher-ros2/
[^3]: git clone https://github.com/ros2/examples src/examples