ROSに依存しないROSと連携できるPythonパッケージの作り方


Pythonをぶっ壊〜〜〜す。
どうもこんにちは、「Pythonの被害から国民を守る党」党首を自称しております片岡です。
Pythonは嫌いで嫌いで正直一生書きたくないのですが、なんやかんやで書く羽目になりますよね。
ポストPythonなスクリプト言語よ早く生まれてPythonを滅ぼしておくれ・・・・・
と嘆いたところでPythonの快進撃は現状止まる気配がないのでうまいこと付き合っていかないといけません
ホントは明日にでも消えてほしいけど
まあPythonの中にも一部捨てたもんじゃない部分もあってそのおいしいとこだけはちゃんと活用していこうよということですね。
今回は普段良く使うROS1/ROS2においてPythonをなんとかうまく使っていく方法を紹介したいと思います。

ROS1におけるPython

ROS1にはPythonクライアントであるrospyがあります。
rospyの内部実装を追ってみるとSubscriberのオブジェクト作った瞬間からspinしなくても実はコールバックが回ってる、
spinは単にrospy.ok()みたいなことしてspinしてるだけとか怪しげな実装がコアライブラリに散見されます。大丈夫なんかコレ。。。

シリアライズされたrosメッセージをやり取りするので比較的すくないオーバーヘッドで他のROSノードと通信できます。
Pythonはスクリプト言語なのでビルドしないにもかかわらずビルドシステムにcatkinを使用し、しばしばrosdepによってpipに依存関係が崩壊します。
なお基本的にPython2のためrospyで実装したコードは今後技術的負債になる可能性は高いと思われます。
またROS1 -> ROS2の変換に関してはほぼリビルドになるのでそこも将来的に苦労が約束されています

ROS2におけるPython

ROS2にはPythonクライアントであるrclpyがあります。
シリアライズされたrosメッセージをやり取りするので比較的すくないオーバーヘッドで他のROSノードと通信できます。
ちなみに、現状rclpyのパッケージを作るのは結構一苦労(ros2 pkg createするとC++向けパッケージが作られるため手動でいろいろ追記しないといけない)です。
Pythonはスクリプト言語なのでビルドしないにもかかわらずビルドシステムにcolconを使用し、しばしばrosdepによってpipに依存関係が崩壊します。

なんとかならんのか・・・・

これらの手段を見て僕が思ったのは・・・・
なんだこの新手の罰ゲームは・・・・という印象ですね。
数年前の僕はこれらのことにPythonの記法から生まれる性質(長いコードを書くと可読性が低い、コンパイルがないのでテストできっちり網羅しない限り最低限のチェックすらままならない)
を鑑みてROSノードを書く時はPythonを完全排除するという考えにいたりました。
一応ある程度の成果は出て、いろいろ使いやすいROSノードが生み出せたかなと思います。
まあでもしかし、ツール系のものを整備するにはそうも行ってられません。
C++でweb APIとか叩くのほんとめんどい、となるとそういうことに強いPythonが選択肢として上がってきます。
うーんでも上記の理由でrospy等はできれば使いたくない、さらにあわよくばpipによる環境破壊を避けるため
Pythonのvenvとか依存解決ツールとかも使って効率的に開発したい・・・・・
あわよくばROS1/ROS2で入る破壊的変更でコードの書き直しが発生するのは嫌だ・・・・
となると理想形の手法はこういうことになります・・・・

「PythonとROS1/2の間で通信はできるが、パッケージ自体はROSに依存しないパッケージをつくる」

んなもんできるんか??

できるんだなコレが・・・・
ということでサンプルのパッケージを作ってみました。
依存解決ツールにはpipenvを使用しました。
pipenvはpip+venvをパッケージングしたツールでパッケージごとに仮想環境を作って依存解決をかんたんにやってくれます。
詳しくはこちらの記事を参照ください
以下のコマンドをターミナルに入力します。

mkdir roslibpy_example
cd roslibpy_example
pipenv install

すると以下のような出力が得られます。

Creating a virtualenv for this project...
Pipfile: /home/masaya/lib/roslibpy_example/Pipfile
Using /usr/bin/python (2.7.15+) to create virtualenv...
⠏ Creating virtual environment...Already using interpreter /usr/bin/python
New python executable in /home/masaya/.local/share/virtualenvs/roslibpy_example-mGuxztlN/bin/python
Installing setuptools, pip, wheel...
done.

✔ Successfully created virtual environment! 
Virtualenv location: /home/masaya/.local/share/virtualenvs/roslibpy_example-mGuxztlN
Creating a Pipfile for this project...
Pipfile.lock not found, creating...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Updated Pipfile.lock (dfae9f)!
Installing dependencies from Pipfile.lock (dfae9f)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/0 — 00:00:00
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

これでROSとの依存が切れたPythonパッケージが作れました。

ではこのパッケージの依存にROSとの通信を担当するroslibpyパッケージを追加しましょう。
以下のコマンドをターミナルで実行すると仮想環境にroslibpyがインストールされます。

pipenv install roslibpy

すると以下のような表示が現れインストールが完了します。

Installing roslibpy...
Adding roslibpy to Pipfile's [packages]...
✔ Installation Succeeded 
Pipfile.lock (134db9) out of date, updating to (dfae9f)...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
✔ Success! 
Updated Pipfile.lock (134db9)!
Installing dependencies from Pipfile.lock (134db9)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 18/18 — 00:00:01
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

roslibpyはrosbridge protocolによってROS1/ROS2と通信を行います。
rosbridge protocolはROSのメッセージをjson文字列にしてwebsockやその他の手法で投げつけるプロトコルです。
詳しくはこちらの記事を参考にしてください。

あとはこの記事のようにroslibpyをimportしたスクリプトを作って実行するだけです。
ROSライクにかけるのでとてもかんたんですね。

# -*- coding:utf8 -*-
import time
from roslibpy import Message, Ros, Topic
def main():

    # roscoreを実行しているサーバーへ接続
    ros_client = Ros('192.168.1.104', 9090)
    # Publishするtopicを指定
    publisher = Topic(ros_client, '/turtle1/cmd_vel', 'geometry_msgs/Twist')
    def start_sending():
        while True:
            if not ros_client.is_connected:
                break

            # 送信するTwistメッセージの内容
            publisher.publish(Message({
                'linear': {
                    'x': 2.0,
                    'y': 0,
                    'z': 0
                },
                'angular': {
                    'x': 0,
                    'y': 0,
                    'z': 1.8
                }
            }))
            time.sleep(0.1)
        publisher.unadvertise()
    # Publish開始
    ros_client.on_ready(start_sending, run_in_thread=True)
    ros_client.run_forever()
if __name__ == '__main__':
    main()

出典:https://symfoware.blog.fc2.com/blog-entry-2288.html

最後に、setup.py等を書いたらパッケージの完成です。
いろんな人がつかえるようにPypyとかに登録しちゃいましょう。

まとめ

以上のようなやり方を取るとROS1/2両方に対応できてROSに依存のないROSと通信できるPythonパッケージを作ることが可能です。
最後にこのやり方に対して来るであろう「大きいメッセージをこの方法でやり取りたらレイテンシがやばいでしょ」というツッコミに対して一言。
パフォーマンス気にするならおとなしくPythonをやめましょう。
Pythonをぶっ壊〜〜〜す。