【ROS×Python】ROSタイマー(Timer)を実装しよう!

 ※ 本記事は広告・プロモーションを含みます。
Python

それでは、またサンプルコードを使ってROSタイマーの実装方法を解説していきます。

以降で紹介するサンプルコードは全てROSパッケージ化した状態でGitHubに載せています。

自分の環境で実行して挙動を確認してみたい場合は下記からクローンして使用してください。

本ページで使用するコード

ROSタイマーの解説

下記コードはROSタイマーを使って1秒周期で現在時刻を表示するコードになります。

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
import datetime


class SimpleTimer(Node):
    """
    シンプルTimerノード
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("simple_timer")

        # 1.0秒周期で実行されるROSタイマーの定義
        # (timer_callbackは1.0秒経過する度に呼び出されるコールバック関数)
        self.timer = self.create_timer(1.0, self._timer_callback)

    def _timer_callback(self) -> None:
        """
        timerコールバック
        """
        # 現在時刻を取得
        now = datetime.datetime.now()
        # 現在時刻をログ出力
        self.get_logger().info("現在時刻:{}".format(now))


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # simple_timerノードの作成
    st = SimpleTimer()
    st.get_logger().info("simple_timer start!")

    try:
        # ノードの実行開始
        rclpy.spin(st)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        st.destroy_node()

ROSタイマーの定義はcreate_timer()関数を使用します。

引数には下記を指定します。

  • 第1引数:タイムアウト時間[秒]
  • 第2引数:コールバック関数

上記コードでは20行目でROSタイマーを定義しています。

self.timer = self.create_timer(1.0, self._timer_callback)

上記実装例ですと、1秒周期でself._timer_callback関数が実行されます。

$ ros2 run ros2_example simple_timer
[simple_timer]: simple_timer start!
[simple_timer]: 現在時刻:2023-09-08 23:28:24.825684
[simple_timer]: 現在時刻:2023-09-08 23:28:25.824598
[simple_timer]: 現在時刻:2023-09-08 23:28:26.824023
[simple_timer]: 現在時刻:2023-09-08 23:28:27.824934
[simple_timer]: 現在時刻:2023-09-08 23:28:28.825264
[simple_timer]: 現在時刻:2023-09-08 23:28:29.825722
[simple_timer]: 現在時刻:2023-09-08 23:28:30.824482
[simple_timer]: 現在時刻:2023-09-08 23:28:31.824898

ちなみに、ROSタイマーは1つのノード内で複数起動することもできます。

下記のコードは1秒周期タイマーと3秒周期タイマーを同時に起動するコードになります。

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
import datetime


class DualTimer(Node):
    """
    デュアルTimerノード
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("dual_timer")

        # 1.0秒周期で実行されるROSタイマー①の定義
        # (timer1_callbackは1.0秒経過する度に呼び出されるコールバック関数)
        self.timer1 = self.create_timer(1.0, self._timer1_callback)

        # 3.0秒周期で実行されるROSタイマー②の定義
        # (timer2_callbackは3.0秒経過する度に呼び出されるコールバック関数)
        self.timer2 = self.create_timer(3.0, self._timer2_callback)

    def _timer1_callback(self) -> None:
        """
        timer①コールバック
        """
        # 現在時刻を取得
        now = datetime.datetime.now()
        # 現在時刻をログ出力
        self.get_logger().info("<タイマー1>現在時刻:{}".format(now))

    def _timer2_callback(self) -> None:
        """
        timer②コールバック
        """
        # 現在時刻を取得
        now = datetime.datetime.now()
        # 現在時刻をログ出力
        self.get_logger().info("<タイマー2>現在時刻:{}".format(now))


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # dual_timerノードの作成
    dt = DualTimer()
    dt.get_logger().info("dual_timer start!")

    try:
        # ノードの実行開始
        rclpy.spin(dt)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        dt.destroy_node()

以下、実行結果です。

$ ros2 run ros2_example dual_timer
[dual_timer]: dual_timer start!
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:23.518847
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:24.518916
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:25.519693
[dual_timer]: <タイマー2>現在時刻:2023-09-08 23:48:25.521257
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:26.519116
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:27.518999
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:28.519669
[dual_timer]: <タイマー2>現在時刻:2023-09-08 23:48:28.521348
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:29.519230
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:30.519691
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:31.519366
[dual_timer]: <タイマー2>現在時刻:2023-09-08 23:48:31.520806
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:32.519966
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:33.519313
[dual_timer]: <タイマー1>現在時刻:2023-09-08 23:48:34.519165
[dual_timer]: <タイマー2>現在時刻:2023-09-08 23:48:34.520138

ROSタイマー呼び出しが重複したらどうなる?

ここでROSタイマーについて、少し踏み込んだ検証をしてみましょう!

先ほどROSタイマーは複数起動できると説明しました。

では、ROSタイマーでコールバック関数の処理中に、別のタイマーによるコールバック関数が呼び出された場合、どのような挙動になるでしょうか?

これを検証するために、下記2つのROSタイマーを定義します。

<ROSタイマー1:書き込み処理>

  • 実行周期:5秒
  • 処理内容:counter1, 2 ,3の値を+1加算する。
         加算の順番はcounter1 → counter2 → counter3とする。
         各counter加算後に1秒間sleep処理を入れる。

<ROSタイマー2:読み出し処理>

  • 実行周期:5秒
         (タイマー2の起動タイミングはタイマー1起動後の0.5秒後とする。)
  • 処理内容:counter1, 2 ,3の値を読み出す。

上記2つのROSタイマー処理を時系列で表現すると下図のようになります。

上図のように意図的に2つのROSタイマー処理が時系列上で重複するようにします。

このとき、ROSタイマー2によるcounter1,2,3の読み出し値はいくつになるでしょうか?

下記タイミングチャート通りの数値が読み出されるのでしょうか?

それでは実際にコードを書いて検証してみましょう!

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
import datetime
import time


class TimerWrRd(Node):
    """
    Timer(Write/Read)ノード
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("timer_wrrd")

        # ロガー取得
        self.logger = self.get_logger()

        # 開始時刻を取得
        self._start_time = datetime.datetime.now()

        # 5.0秒周期でcounter1,2,3を書き込むROSタイマーの定義
        # (timer_wr_callbackは5.0秒経過する度に呼び出されるコールバック関数)
        self.timer_wr = self.create_timer(5.0, self._timer_wr_callback)

        # 読み出しタイマーを書き込みタイマーより0.5秒遅らせて実行させる
        time.sleep(0.5)  # 0.5秒停止

        # 5.0秒周期でcounter1,2,3を読み出すROSタイマーの定義
        # (timer_rd_callbackは5.0秒経過する度に呼び出されるコールバック関数)
        self.timer_rd = self.create_timer(5.0, self._timer_rd_callback)

        # counter1,2,3のインスタンス変数を定義
        self._counter1 = 0
        self._counter2 = 0
        self._counter3 = 0

    def _timer_wr_callback(self) -> None:
        """
        書き込みtimerコールバック
        """
        # 開始経過時刻を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_wr_callback:開始".format(elps))

        self._counter1 += 1  # カウンター1を+1
        self.logger.info("[{}]Wirte counter1:[{}]".format(elps, self._counter1))
        time.sleep(1)  # 1秒停止

        # 経過時刻を取得
        elps = datetime.datetime.now() - self._start_time

        self._counter2 += 1  # カウンター2を+1
        self.logger.info("[{}]Wirte counter2:[{}]".format(elps, self._counter2))
        time.sleep(1)  # 1秒停止

        # 経過時刻を取得
        elps = datetime.datetime.now() - self._start_time

        self._counter3 += 1  # カウンター3を+1
        self.logger.info("[{}]Wirte counter3:[{}]".format(elps, self._counter3))
        time.sleep(1)  # 1秒停止

        # 終了経過時間を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_wr_callback:終了".format(elps))

    def _timer_rd_callback(self) -> None:
        """
        読み出しtimerコールバック
        """
        # 開始経過時刻を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_rd_callback:開始".format(elps))

        self.logger.info("[{}]Read counter1:[{}]".format(elps, self._counter1))
        self.logger.info("[{}]Read counter2:[{}]".format(elps, self._counter2))
        self.logger.info("[{}]Read counter3:[{}]".format(elps, self._counter3))

        # 終了経過時間を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_rd_callback:終了".format(elps))


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # timer_wrrdノードの作成
    twr = TimerWrRd()
    twr.get_logger().info("timer_wrrd start!")

    try:
        # ノードの実行開始
        rclpy.spin(twr)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        twr.destroy_node()

上記コードの実行結果は下記の通りとなりました。

$ ros2 run ros2_example timer_wrrd
[timer_wrrd]: timer_wrrd start!
[timer_wrrd]: [0:00:05.001455]timer_wr_callback:開始
[timer_wrrd]: [0:00:05.001455]Wirte counter1:[1]
[timer_wrrd]: [0:00:06.006454]Wirte counter2:[1]
[timer_wrrd]: [0:00:07.008616]Wirte counter3:[1]
[timer_wrrd]: [0:00:08.010873]timer_wr_callback:終了
[timer_wrrd]: [0:00:08.013241]timer_rd_callback:開始
[timer_wrrd]: [0:00:08.013241]Read counter1:[1]
[timer_wrrd]: [0:00:08.013241]Read counter2:[1]
[timer_wrrd]: [0:00:08.013241]Read counter3:[1]
[timer_wrrd]: [0:00:08.019883]timer_rd_callback:終了
[timer_wrrd]: [0:00:10.000965]timer_wr_callback:開始
[timer_wrrd]: [0:00:10.000965]Wirte counter1:[2]
[timer_wrrd]: [0:00:11.006618]Wirte counter2:[2]
[timer_wrrd]: [0:00:12.009834]Wirte counter3:[2]
[timer_wrrd]: [0:00:13.012451]timer_wr_callback:終了
[timer_wrrd]: [0:00:13.015401]timer_rd_callback:開始
[timer_wrrd]: [0:00:13.015401]Read counter1:[2]
[timer_wrrd]: [0:00:13.015401]Read counter2:[2]
[timer_wrrd]: [0:00:13.015401]Read counter3:[2]
[timer_wrrd]: [0:00:13.023171]timer_rd_callback:終了

予想とは異なり、読み出し値は下記表の通りとなりました。

読み出し値counter1counter2counter3
1回目111
2回目222

timer_wr_callback関数処理が終了した後に、timer_rd_callback関数が呼び出されているのがわかると思います。

皆さんもうお気づきだと思いますが、コールバック関数処理が実行中に別のコールバック関数が呼び出されようとした場合、先に実行中のコールバック関数処理が終了した直後に呼び出される挙動となります。

時系列で表現すると下図のようになります。

ROSのコールバック関数はデフォルトで逐次処理となっています。

(厳密に言うと、デフォルトの逐次処理コールバックグループが割り当てられています。)

これは、ROSタイマーに限った話ではなく、ROS通信のコールバック関数も同様にデフォルトでは逐次処理となります。

ROS通信にはトピック(Topic)サービス(Service)アクション(Action)がありますが、これらは別の記事で実装例を紹介していきます。

また、コールバックグループについても別の記事で解説を出す予定です。

並列処理させることはできないのか?

答えはYes並列処理(マルチスレッド)させることも可能です。

timer_wrrd.pyのコールバック関数処理を並列処理させるように修正すると下記のようになります。

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
import datetime
import time


class TimerWrRd(Node):
    """
    Timer(Write/Read)ノード

      並列処理(Multi-Thread)
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("timer_wrrd")

        # ロガー取得
        self.logger = self.get_logger()

        # 開始時刻を取得
        self._start_time = datetime.datetime.now()

        # 書き込み用コールバックGroupを生成
        cb_grp_wr = MutuallyExclusiveCallbackGroup()
        # 読み出し用コールバックGroupを生成
        cb_grp_rd = MutuallyExclusiveCallbackGroup()

        # 5.0秒周期でcounter1,2,3を書き込むROSタイマーの定義
        # (timer_wr_callbackは5.0秒経過する度に呼び出されるコールバック関数)
        # 書き込み用コールバックGroupを設定
        self.timer_wr = self.create_timer(5.0, self._timer_wr_callback, cb_grp_wr)

        # 読み出しタイマーを書き込みタイマーより0.5秒遅らせて実行させる
        time.sleep(0.5)  # 0.5秒停止

        # 5.0秒周期でcounter1,2,3を読み出すROSタイマーの定義
        # (timer_rd_callbackは5.0秒経過する度に呼び出されるコールバック関数)
        # 読み出し用コールバックGroupを設定
        self.timer_rd = self.create_timer(5.0, self._timer_rd_callback, cb_grp_rd)

        # counter1,2,3のインスタンス変数を定義
        self._counter1 = 0
        self._counter2 = 0
        self._counter3 = 0

    def _timer_wr_callback(self) -> None:
        """
        書き込みtimerコールバック
        """
        # 開始経過時刻を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_wr_callback:開始".format(elps))

        self._counter1 += 1  # カウンター1を+1
        self.logger.info("[{}]Wirte counter1:[{}]".format(elps, self._counter1))
        time.sleep(1)  # 1秒停止

        # 経過時刻を取得
        elps = datetime.datetime.now() - self._start_time

        self._counter2 += 1  # カウンター2を+1
        self.logger.info("[{}]Wirte counter2:[{}]".format(elps, self._counter2))
        time.sleep(1)  # 1秒停止

        # 経過時刻を取得
        elps = datetime.datetime.now() - self._start_time

        self._counter3 += 1  # カウンター3を+1
        self.logger.info("[{}]Wirte counter3:[{}]".format(elps, self._counter3))
        time.sleep(1)  # 1秒停止

        # 終了経過時間を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_wr_callback:終了".format(elps))

    def _timer_rd_callback(self) -> None:
        """
        読み出しtimerコールバック
        """
        # 開始経過時刻を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_rd_callback:開始".format(elps))

        self.logger.info("[{}]Read counter1:[{}]".format(elps, self._counter1))
        self.logger.info("[{}]Read counter2:[{}]".format(elps, self._counter2))
        self.logger.info("[{}]Read counter3:[{}]".format(elps, self._counter3))

        # 終了経過時間を取得
        elps = datetime.datetime.now() - self._start_time
        self.logger.info("[{}]timer_rd_callback:終了".format(elps))


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # timer_wrrdノードの作成
    twr = TimerWrRd()
    twr.get_logger().info("timer_wrrd start!")
    # マルチスレッドExecutorを生成
    executor = MultiThreadedExecutor()
    try:
        # ノードの実行開始
        # (マルチスレッドで実行させる)
        rclpy.spin(twr, executor)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        twr.destroy_node()

上記コードを実行すると、各コールバック関数が並列処理されて、当初予測していた下記タイミングチャート通りの挙動となります。

$ ros2 run ros2_example timer_wrrd_mt 
[timer_wrrd]: timer_wrrd start!
[timer_wrrd]: [0:00:05.001719]timer_wr_callback:開始
[timer_wrrd]: [0:00:05.001719]Wirte counter1:[1]
[timer_wrrd]: [0:00:05.502680]timer_rd_callback:開始
[timer_wrrd]: [0:00:05.502680]Read counter1:[1]
[timer_wrrd]: [0:00:05.502680]Read counter2:[0]
[timer_wrrd]: [0:00:05.502680]Read counter3:[0]
[timer_wrrd]: [0:00:05.509933]timer_rd_callback:終了
[timer_wrrd]: [0:00:06.010878]Wirte counter2:[1]
[timer_wrrd]: [0:00:07.014118]Wirte counter3:[1]
[timer_wrrd]: [0:00:08.016529]timer_wr_callback:終了
[timer_wrrd]: [0:00:10.001683]timer_wr_callback:開始
[timer_wrrd]: [0:00:10.001683]Wirte counter1:[2]
[timer_wrrd]: [0:00:10.503173]timer_rd_callback:開始
[timer_wrrd]: [0:00:10.503173]Read counter1:[2]
[timer_wrrd]: [0:00:10.503173]Read counter2:[1]
[timer_wrrd]: [0:00:10.503173]Read counter3:[1]
[timer_wrrd]: [0:00:10.517371]timer_rd_callback:終了
[timer_wrrd]: [0:00:11.005545]Wirte counter2:[2]
[timer_wrrd]: [0:00:12.008421]Wirte counter3:[2]
[timer_wrrd]: [0:00:13.010717]timer_wr_callback:終了

ROSの並列処理については別の記事で解説する予定のため詳細は割愛しますが、「ROSのコールバック関数は逐次処理させることも、並列処理させることも可能である」ということを覚えておきましょう。

並列処理(マルチスレッド)実行効率を向上できる利点がある反面、共有データを更新/取得する場合は必要に応じて排他制御を実施しないとデータの整合性が破壊されてしまう可能性があり、注意が必要です。

むやみやたらに並列処理(マルチスレッド)を使っていると思わぬところでデータ整合性の破壊が起きる可能性があるため、必要以上に並列処理は使用しないことを推奨します。

まとめ

ROSタイマー(Timer)

  • create_timer()で定義
    • 第1引数:タイムアウト時間[秒]
    • 第2引数:コールバック関数
  • ROSタイマーは複数定義可能
  • コールバック関数の呼び出しが時系列上で重複した場合、デフォルトでは逐次処理となる。
スポンサーリンク

コメント

タイトルとURLをコピーしました