28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ROS2Advent Calendar 2019

Day 14

Python3ではじめるROS2(自動起動編)

Last updated at Posted at 2019-10-15

#はじめに
「ROS2 Advent Calendar 2019 14日」に参加しております。
(すでに投稿済みの内容ですが、これに合わせて若干手直ししました。)

ROS2とPython3を用いて、カスタムメッセージを使った簡単なノードを作ってみます。

Python3ではじめるROS2 関連記事

回数 サブタイトル 内容
第1回 カスタムメッセージ編 ・環境の準備
・カスタムメッセージを作る
・(おまけ).bashrcのカスタマイズ
第2回 ノード編 ・Pub/Subのノードを作る
第3回(今回) 自動起動編 ・roslaunchから起動する
・systemdからroslaunchを起動する

#1.roslaunchから起動

カスタムメッセージ編・ノード編で作ってきたPub / Subノードを、launchファイルからまとめて起動します。

##(1)準備

launchファイルを作ります。

コマンドライン
~/ros2_ws/src$ cd pubsubpy
~/ros2_ws/src/pubsubpy$ touch pubsubpy.launch.py

##(2)launchファイル

pubsubpy.launch.py
# -----------------------------------------------
# ROS LAUNCH
#
# The MIT License (MIT)
# Copyright (C) 2019 myasu.
# -----------------------------------------------

"""Launch a add_two_ints_server and a (synchronous) add_two_ints_client."""

import launch
import launch_ros.actions


def generate_launch_description():

    ### ここにlaunchしたいノードを定義
    ### node_executableのところは、setup.pyのなかの
    ### entry_pointsで指定した (例)pub = node.Pub:main
    ### の左辺側の文字と合わせて下さい
    pub = launch_ros.actions.Node(
        package='pubsubpy', node_executable='pub', output='screen')
    sub = launch_ros.actions.Node(
        package='pubsubpy', node_executable='sub', output='screen')

    ### こちらにもlaunchしたいノードを記述
    ### 上記で定義した (例)pub = launch_ros.actions.Node
    ### の、左辺側の変数を列挙します。
    ### 記述しなかったら、そのノードは起動しません。
    ###
    ### target_action=sub に記述したノードが落ちたら、
    ### launchで起動したものが一式落ちます。
    return launch.LaunchDescription([
        pub,
        sub,
        # TODO(wjwwood): replace this with a `required=True|False` option on ExecuteProcess().
        # Shutdown launch when client exits.
        launch.actions.RegisterEventHandler(
            event_handler=launch.event_handlers.OnProcessExit(
                target_action=sub,
                on_exit=[launch.actions.EmitEvent(
                    event=launch.events.Shutdown())],
            )),
    ])

最終的には、下記のようなディレクトリ構成・ファイル構成になります。
image.png

##(3)ビルド

注意:launchファイルは、 --symlink-installが効かないので、編集する度に必ずビルドして下さい

コマンドライン
~/ros2_ws/src/pubsubpy$ cd ../..
~/ros2_ws$ colcon build --symlink-install
Starting >>> pubsubpy_mes
Finished <<< pubsubpy_mes [8.83s]                        
Starting >>> pubsubpy
Finished <<< pubsubpy [15.8s]            
                      
Summary: 2 packages finished [26.7s]

#環境変数の読み込み
~/ros2_ws$ source ./install/setup.bash

##(4)実行

eloquent以前
実行中は、コンソール上のメッセージが出てきません。(たまに流れてくるときもあります・・・)
[Ctrl-C]すると、バッファに貯まっていたメッセージが一気に流れてきます。

foxy以降 (21/3/18追記)
実行中は、すぐにコンソール上にメッセージが出てきます。

コマンドライン
~/ros2_ws$ ros2 launch pubsubpy pubsubpy.launch.py
[INFO] [launch]: All log files can be found below /home/ubuntu/.ros/log/2019-10-14-19-46-14-616087-raspi3u-23637
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [pub-1]: process started with pid [23708]
[INFO] [sub-2]: process started with pid [23709]
^C[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)
[sub-2] [INFO] [mysub]: mysub initializing...
[sub-2] [INFO] [mysub]: mysub do...
[sub-2] [INFO] [mysub]: Subscription > Port: 0 Value: 0
[sub-2] [INFO] [mysub]: Subscription > Port: 0 Value: 1
[sub-2] [INFO] [mysub]: mysub done.
[INFO] [pub-1]: process has finished cleanly [pid 23708]
[INFO] [sub-2]: process has finished cleanly [pid 23709]
[pub-1] [INFO] [mypub]: mypub initializing...
[pub-1] [INFO] [mypub]: mypub do...
[pub-1] [INFO] [mypub]: Publish [0]
[pub-1] [INFO] [mypub]: Publish [1]
[pub-1] [INFO] [mypub]: mypub done.
Task exception was never retrieved
future: <Task finished coro=<LaunchService._process_one_event() done, defined at /opt/ros/dashing/lib/python3.6/site-packages/launch/launch_service.py:175> exception=InvalidHandle('Asked to destroy handle, but it was already destroyed',)>
Traceback (most recent call last):
  File "/opt/ros/dashing/lib/python3.6/site-packages/launch/launch_service.py", line 177, in _process_one_event
    await self.__process_event(next_event)
  File "/opt/ros/dashing/lib/python3.6/site-packages/launch/launch_service.py", line 186, in __process_event
    entities = event_handler.handle(event, self.__context)
  File "/opt/ros/dashing/lib/python3.6/site-packages/launch/event_handlers/on_shutdown.py", line 67, in handle
    return self.__on_shutdown(cast(Shutdown, event), context)
  File "/opt/ros/dashing/lib/python3.6/site-packages/launch_ros/default_launch_description.py", line 39, in _shutdown
    self.__launch_ros_node.destroy_node()
  File "/opt/ros/dashing/lib/python3.6/site-packages/rclpy/node.py", line 1408, in destroy_node
    self.handle.destroy()
  File "/opt/ros/dashing/lib/python3.6/site-packages/rclpy/handle.py", line 92, in destroy
    raise InvalidHandle('Asked to destroy handle, but it was already destroyed')
rclpy.handle.InvalidHandle: Asked to destroy handle, but it was already destroyed
~/ros2_ws$ 
  • (12/14追記)
    • コメントに頂いたとおり、メッセージを即出力したい場合は、print関数を使って下さい。
    • get_logger().warnや、get_logger().errorなど緊急度の高いメッセージは、バッファされること無く即出力される事を確認しました

#2.systemdからroslaunchを起動

一通り試作が終わって本番に適用する際に、装置(RaspberryPi)の電源が入ってLinuxが立ち上がったら、自動的にROS2のノードも立ち上げが必要になります。
ここでは、systemdからlounchする方法を解説します。

##(1)考え方

  1. pubsubpy.shに起動コマンドros2 launch pubsubpy pubsubpy.launch.pyを記述
  2. systemd から pubsubpy.shを起動
  3. コンソールに出てくる情報は、./log/pubsubpy.logに出力
  4. ./log/pubsubpy.logは定期的にlogrotateする

##(2)ファイルの準備

コマンドライン
~/ros2_ws/$ cd src/pubsubpy
~/ros2_ws/src/pubsubpy$ touch pubsubpy.sh pubsubpy.service pubsubpy.logrotate
~/ros2_ws/src/pubsubpy$ mkdir log

###pubsubpy.sh

pubsubpy.sh
#!/bin/bash
#--------------------------------------------------------------------
#バックグラウンド実行用のスクリプト
#--------------------------------------------------------------------

#変数の設定
SCRIPTDIR=/home/ubuntu/ros2_ws/src/pubsubpy/
LOGDIR=$SCRIPTDIR/log
ENVFILE=/home/ubuntu/ros2_ws/install/setup.bash

#実行
if [ -f ${ENVFILE} ]; then
    #環境変数読み込み
    echo "Loading ROS2 Env..."
    source /home/ubuntu/ros2_ws/install/setup.bash
    if [ -d ${LOGDIR} ]; then
        echo "ROS2 Launching..."
        #スタンドアロンで起動する場合に必須の項目
        #ネットワーク上で一意にするための値
        export ROS_DOMAIN_ID=232
        #DDSの通信先ホストを限定する
        export ROS_ALLOWED_HOSTS="localhost.local:robot_1.local"
        #roslaunch実行
        exec ros2 launch pubsubpy pubsubpy.launch.py >> ${LOGDIR}/pubsubpy.log 2>&1
    else
        echo "There is no ${LOGDIR}"
    fi
else
    echo "There is no ${ENVFILE}"
fi

###pubsubpy.service

pubsubpy.service
[Unit]
Description=ROS2 launch test
After=local-fs.target
ConditionPathExists=/home/ubuntu/ros2_ws/src/pubsubpy

[Service]
ExecStart=/home/ubuntu/ros2_ws/src/pubsubpy/pubsubpy.sh
ExecStop=/bin/kill ${MAINPID}
Restart=on-failure
StartLimitInterval=60
StartLimitBurst=3
KillMode=mixed
Type=simple
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

###systemdに登録する

コマンドライン
#serviceファイルをシンボリック(cpしてもOK)
$ sudo ln -s /home/ubuntu/ros2_ws/src/pubsubpy/pubsubpy.service /etc/systemd/system
#デーモンを有効化
$ sudo systemctl enable pubsubpy.service
#起動
$ sudo systemctl start pubsubpy.service
#ステータスを確認
$ systemctl status pubsubpy.service

上記の例では、pubsubpy.serviceファイルをシンボリックリンクしてますが、コピーしても構いません。

(覚え書き)

デーモンをsudo systemctl disable pubsubpy.serviceで無効化したときに、

  • シンボリックリンク
    • pubsubpy.serviceのシンボリックリンクが消える(ので、enableする前にもう一度シンボリックリンクを作る必要がある)
  • コピー
    • pubsubpy.serviceファイルは残る

このような挙動の違いがあります。

##(3)logrotateの設定

###pubsubpy.logrotate

pubsubpy.logrotate
#---------------------------------------------------------------------
# pubsubpy.logrotate
#---------------------------------------------------------------------

/home/ubuntu/ros2_ws/src/pubsubpy/log/pubsubpy.log {
  daily
  missingok
  rotate 15
  compress
  delaycompress
  notifempty
  copytruncate
  su ubuntu ubuntu
}
コマンド 意味 他のコマンド
daily 毎日ローテート weekly/monthly/yearly
missingok ログファイルが無くてもエラーにしない nomissingok
rotate15 ログの世代数(例は15ファイル、15日分) 0にすると残さない
compress 古いログファイルをgzipで圧縮 nocompress
delaycompress 古いログの1世代目だけは圧縮しない -
notifempty 空ファイルはローテートしない ifempty
copytruncate 更新中のログファイルを別名ファイルにcpしてから、元のログファイルを空にする -
su ubuntu ubuntu 処理する時のユーザ/グループを指定 -

~/ros2_ws/src/pubsubpy/log/ディレクトリは、この時点ではubuntu:ubuntuです。

###logrotateに登録する

コマンドライン
#logrotateの設定ディレクトリに移動
$ cd /etc/logrotate.d
#logrotateファイルをコピー(シンボリックリンクは不可)
$ sudo cp ~/ros2_ws/src/pubsubpy/pubsubpy.logrotate ./pubsubpy
#パーミッションを変更
$ sudo chmod 644 pubsubpy
#logrotateに登録
$ sudo logrotate -f ./pubsubpy

これでログが自動的にローテーションします。

動作チェックは、下記のようにします。

コマンドライン
$ sudo /usr/sbin/logrotate -f /etc/logrotate.d/uecsd 
$ ls -la /home/ubuntu/ros2_ws/src/pubsubpy/log/
合計 592
drwxrwxr-x 2 pi pi   4096  1月  6 09:03 .
drwxr-xr-x 5 pi pi   4096  1月  6 08:59 ..
-rw-r--r-- 1 pi pi      0  1月  6 09:03 pubsubpy.log
-rw-r--r-- 1 pi pi   8238  1月  6 09:03 pubsubpy.log.1
-rw-r--r-- 1 pi pi 583851  1月  6 09:03 pubsubpy.log.2.gz

pubsubpy.logに番号がついて、古いものはgz圧縮されています。

###エラー例

(1)下記のエラーが出た場合は、改行コードをLFに変更してください。

$ sudo logrotate -f ./pubsubpy
error: pubsubpy:10 lines must begin with a keyword or a filename (possibly in double quotes)
error: pubsubpy:11 lines must begin with a keyword or a filename (possibly in double quotes)
error: pubsubpy:19, unexpected text after }

(2)下記のエラーが出た場合は、設定ファイルのパーミッションを644にしてください。(シンボリックリンクを使うと、ファイルのパーミッションが777になるので、このエラーの回避が出来なくなります)

$ sudo logrotate -f ./pubsubpy
error: Ignoring poka because of bad file mode - must be 0644 or 0444.

(3)下記のエラーが出た場合は、~/ros2_ws/src/pubsubpy/log/のパーミッションが合っていません。設定ファイルのsu項目で、該当ディレクトリのユーザ・グループを合わせてください。

error: skipping "/home/ubuntu/ros2_ws/src/pubsubpy/log/poka.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.

#3.その他のTips

##(1)ROS2をスタンドアロンで動かすとき

とある用途で、ネットワークの繋がらない現場で使う装置に、ROS2を使ったところ、正常に動かない現象が発生しました。

###ハマった状況

  • 環境
    • ROS2 Dashing
    • Ubuntu 18.04
    • Raspberry Pi 3+
  • 状況
    • ネットワーク(LAN / Wifi)に繋がっていないとき
      1. ros2 lainch ...で複数のノードを立ち上げた → ros2 node listros2 topic listで検索しても、一部のノードやトピックが見えない。
      • 再度ros2 lainch ...で立ち上げ直しても、やはり見えない。見えなくなってるノードの再現性も無い。(別のノードが見えたり、消えたりしている)
      • ros2 run ...でで個別に立ち上げても同様。
    • ネットワーク(LAN / Wifi)に繋がっているとき
      1. ros2 lainch ...で複数のノードを立ち上げた → ros2 node listros2 topic listで検索すると、必要なノード・トピックが全て見える

###スタンドアロンで動かすための処置

下記の環境変数を設定します。
このようにしても、正常に立ち上がらない例がありました。いまのところ、完全な回避作は見つかってません。(19/11/12追記)

#ネットワーク上で一意にするための値
export ROS_DOMAIN_ID=<任意の数字 0~232>
#DDSの通信先ホストを限定する
export ROS_ALLOWED_HOSTS="localhost.local:robot_1.local"

ROS_DOMAIN_IDは、同じ値の環境変数から起動したノード間だけで通信出来るようにします。

ROS_ALLOWED_HOSTSは、値に指定したホストとだけ通信できるようにします。

同一のLANネットワーク上でROS2の環境が存在するときに、混信を防ぐための仕組みですが、スタンドアロン環境でも設定が必須です。

ROS_ALLOWED_HOSTSに言及している資料が大変少ないので、こちらがどんな効果が有るのかはは良く解りません・・・)

###参考

##(2)ROS_DOMAIN_IDの設定範囲

同じネットワークに、複数のROS2環境が混在すると混信が発生します。
(ハンズオンなどをすると、その際中に経験します・・・)
0~65535までの任意の値を設定出来るようですが・・・本当にその範囲に割り当て出来るかを確認してみました。

###結論

  • 0~232

###範囲の確認方法

環境変数に適当な数字を入れると範囲を教えてくれます。最終的に、RTPS のほうが範囲の指定をしてきます。

コマンドライン
~$ export ROS_DOMAIN_ID=65500
~$ ros2 node list
Traceback (most recent call last):
  File "/opt/ros/dashing/bin/ros2", line 11, in <module>
・・・(省略)・・・
OverflowError: getsockaddrarg: port must be 0-65535.
~$ export ROS_DOMAIN_ID=233
~$ ros2 node list
Calculated port number is too high. Probably the domainId is over 232 or portBase is too high.
2019-11-06 12:28:17.342 [RTPS Error] Calculated port number is too high. Probably the domainId is over 232 or portBase is too high. -> Function getMulticastPort
~$ export ROS_DOMAIN_ID=232
~$ ros2 node list
~$ 

###参考

#おわりに

ROS2の勉強を19年7月から始めたのですが、やはりPython3のまとまった作例が少なかったので、覚え書きを兼ねてまとめを作ってみました。(特にlounchファイルのサンプルが少ないこととか、スタンドアロン環境でROS_DOMAIN_IDが必須なこととか・・・)
参考になりましたら幸いです。

#参考資料

28
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?