【ROS2/Python】RyeをROS2で使う

# はじめに
本記事は、ROS2アドベントカレンダー1日目の記事です！

https://qiita.com/advent-calendar/2023/ros2

今回はPythonパッケージ管理ソフトウェアのRyeをROS2で使った例を示そうと思います。

https://rye-up.com/

:::note warn
ROS2×Pythonパッケージ管理ソフトウェアについての意見は様々あります。本記事も一例としてとらえてください。
:::

# 環境
以下の環境で動作を確認しています。

| 項目 | バージョン |
|:-: | :-:|
| Ubuntu | 22.04 |
| ROS2 | Humble |


# 手順
簡単に手順を説明すると以下です。
- Ryeをインストールする
- ROS2のパッケージを作成する (PythonのROSノードですが、ビルド方法はament_cmakeを選択)
- パッケージのディレクトリ内でRyeの設定を行う
- ソースコードを作成する
- ビルドする
- Ryeの環境に入り、ros2 run コマンドでPythonファイルを実行する

記事内では上記について説明しますが、ソースコードは以下にあるので実行したい方はREADME通りにコマンドを実行していくとRye上でROSノードの動作確認までできます。

https://github.com/koichirokato/rye_sample_pkg

## Ryeをインストールする

```bash
curl -sSf https://rye-up.com/get | bash
echo 'source "$HOME/.rye/env"' >> ~/.bashrc
```

参考：
https://rye-up.com/guide/installation/#installing-rye

## ROS2のパッケージを作成する
今回はパッケージ名を `rye_sample_pkg`とします。

```bash
ros2 pkg create rye_sample_pkg --license MIT
```

自動的に生成されるファイルは以下のようになります。
PythonのROSノードを作成しますがament_cmakeを使用するためCMakeLists.txt等があって問題ありません。

```
rye_sample_pkg/
  ├ CMakeLists.txt
  ├ LICENSE
  ├ include/
    ├ rye_sample_pkg
  ├ package.xml
  ├ src/
```

## パッケージのディレクトリ内でRyeの設定を行う

```bash
cd ~/ros2_ws/rye_sample_pkg
rye init
```

以下のように、READMEやpyproject.xmlが自動生成されます。

```
rye_sample_pkg/
  ├ CMakeLists.txt
  ├ LICENSE
  ├ README.md
  ├ include/
    ├ rye_sample_pkg
  ├ package.xml
  ├ pyproject.tom
  ├ src/
```

`rye pin`コマンドで、そのディレクトリ内で作成されるPythonのbinファイルのバージョンを指定できます。rclpy等ROS2のソフトウェアは`/opt/ros/`配下のものを使用するのでPythonのメジャーバージョンはUbuntu本体のものと合わせましょう。
その後、`rye sync`コマンドで環境が作成されます。

```bash
rye pin 3.10
rye sync
```

ディレクトリ構成は以下のようになり、requirements*ファイルが自動生成されます。

```
rye_sample_pkg/
  ├ CMakeLists.txt
  ├ LICENSE
  ├ README.md
  ├ include/
    ├ rye_sample_pkg
  ├ package.xml
  ├ pyproject.tom
  ├ src/
  ├ requirements-dev.lock
  ├ requirements.lock
```

## ソースコードを作成する
名称は問わないですが今回は `scripts`ディレクトリを作成してその中にソースコードを作成しましょう。

```bash
mkdir scripts
cd scripts
vi talker.py
```

ROS2の以下のページを参考に作成します。
https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html

```python
#/usr/bin/env python3
import rclpy
from rclpy.node import Node
import sys

from std_msgs.msg import String


class MinimalPublisher(Node):

    def __init__(self):
        super().__init__('minimal_publisher')
        self.get_logger().info(f"python path: {sys.executable}")
        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()

```

また、talker.pyに実行権限をつけましょう。CMakeの設定でPythonファイルをインストールするためです。

```
sudo chmod +x talker.py
```

ros2 のノードのために必要な `__init__.py`を作成しましょう。

```bash
mkdir rye_sample_pkg
cd rye_sample_pkg
touch __init__.py
```

CMakeLists.txtにPythonのインストールの設定を追記しましょう。

```cmake
cmake_minimum_required(VERSION 3.8)
project(rye_sample_pkg)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rclpy REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
ament_python_install_package(${PROJECT_NAME})
install(PROGRAMS scripts/talker.py DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()
```

## ビルドする

いつも通り colcon buildでビルドしましょう。
CMakeListsで設定した通り、install/rye_sample_pkg/lib/rye_sample_pkg/配下にtalker.pyがコピーされたら成功です。

```bash
colcon build --packages-select rye_sample_pkg --symlink-install
```

## ROSノードを実行する
Ryeの環境内に入った後にros2 runでROSノードを起動するとryeの環境でROSノードが立ち上がります。

```bash
cd ~/ros2_ws/src/rye_sample_pkg
rye shell
```

```bash
ros2 run rye_sample_pkg talker.py
[INFO] [1700807070.570010760] [minimal_publisher]: python path: /home/koichi/ros2_ws/src/rye_sample_pkg/.venv/bin/python3
[INFO] [1700807071.071507709] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1700807071.573184340] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1700807072.072772997] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [1700807072.572920468] [minimal_publisher]: Publishing: "Hello World: 3"
```

## Ryeの使い方
今更ですが、Ryeの使い方を簡単に紹介します。

### プロジェクトへパッケージの追加

```bash
rye add XXX
# 例
rye add numpy
rye add tensorflow
```

### パッケージの削除

```bash
rye remove XXX
# 例
rye remove numpy
```

上記のコマンドは、pyproject.tomlファイルを操作するだけで実際のインストール/アンインストールは行われません。それらを実行するには下記を実行します。

### パッケージのインストール/アンインストール

```bash
rye sync
```

### ryeの環境に入る
上記で、requirements*や、 .venvなどが生成されます。
以下のコマンドでryeの実行環境に入ることができます。

```bash
rye shell
# または
. .venv/bin/activate
```

# ROS2での使い方
Poetryなどはワークスペースごと or モノレポで管理されているプロジェクトを一括で管理する際に有用だったと思っています。
それに比べてRyeはパッケージごとにPythonの仮想環境まるごと使い分けることが可能ではあるので、そこは差別化できるのかと思いました。


# おわりに
ROS2に限らず、Pythonのパッケージ管理方法に正解はなさそうなのでこれからも慎重に向き合っていきましょう！

アドベントカレンダー、盛り上がっていきましょう！

https://qiita.com/advent-calendar/2023/ros2