🤖

ROS 2 ノードを Hy で書く

に公開

本記事では ROS 2 ノードを Hy で実装する方法について説明します。
前提知識として、Python での ROS 2 ノードの実装方法はご存知であるものとします。

以下で説明する内容についての具体的な実装例は次のリポジトリにあります。
これは ROS 2 Tutorials のいくつかの例を Hy で実装したものです。

https://github.com/aobadenshi/roshy-tutorials

環境

実装の流れ

pip で hy をインストールします。なお、上に挙げた実装例では venv を用いています。

pip install hy==1.0.0

Python で作る時と同様のやり方でパッケージを作ります。

ros2 pkg create --build-type ament_python --license Apache-2.0 example

Hy で実装するパッケージでは __init__.py を次のようにします。

exapmle/example/__init__.py
import hy

ノードの実装は Hy で Node のサブクラスと main 関数を実装するだけで、これは Python で書く場合と同様です。(なお、コードのハイライトには Clojure を指定していますので、少し変なところがあるかもしれません)

example/example/node.hy
(import rclpy)
(import rclpy.node [Node])

(defclass MinimalHyNode [Node]
  (defn __init__ [self]
    (.__init__ (super) "minimal_hy_node")))

(defn main [[args None]]
  (rclpy.init :args args)
  (let [node (MinimalHyNode)]
    (try
      (rclpy.spin node)
      (except [KeyboardInterrupt]
        (print "kbd interrputed"))
      (finally
        (node.destroy_node)
        (rclpy.try_shutdown)))))

また Python ノードは setup.py でエントリーポイントを記述しますが、こちらの書き方は Python と違いはありません。

example/setup.py
setup(
    # ... 中略 ...
    entry_points={
        'console_scripts': [
            'node = example.node:main',
        ],
    },
)

ポイント

Hy で Python クラスを実装するには defclass を用います。ドキュメントにもあるようにメソッドは defclass 内で関数定義、メンバ変数は defclass 内で setv します。以下の例では複数の変数をまとめて初期化しています。

(defclass Foo []
  (defn __init__ [self]
    (setv
      self.x 1836
      self.y 2048))
  (defn method1 [self]
    (+ self.x self.y)))

このように定義したクラスからインスタンスを作成するには、関数として呼び出します。

(setv foo (Foo))

メンバ変数へのアクセスやメソッドの呼び出し方について Hy はいくつかの方法が使えます。詳しくは Dotted identifiers をご覧ください。

(print f"x={foo.x}, y={foo.y}")
(print (.method1 foo)) ;; メソッドの呼び出し方 (1)
(print (foo.method1))  ;; メソッドの呼び出し方 (2)

ROS 2 ではタイマー、サービス、アクション、いたるところでコールバックを使用します。コールバックが用いられる状況の多くはコンテキストを要するため、コールバックはノードのメソッドとして実装されることが多いと思います。
一方 Hy では無名関数を用いて書くことができます。

(defclass MinimalPublisher [Node]
  (defn __init__ [self]
    (.__init__ (super) "minimal_publisher")
    (setv
      self.cnt_ 0
      self.publisher_ (self.create_publisher String "topic" 10)
      self.timer_ (self.create_timer
                   0.5
                   (fn []
                     (let [msg (String)]
                       (setv msg.data f"Hello World: {self.cnt_}")
                       (self.publisher_.publish msg)
                       (.info (self.get_logger) f"Publishing: {msg.data}")
                       (setv self.cnt_ (+ self.cnt_ 1))))))))

ROSのチュートリアルのようなシンプルな例ではネストが深くなっただけで便利に見えないかもしれません。
しかし例えばアクションは複数のコールバックを用いるところ、複数のノード (ActionServer) にゴールを送信して同時にアクションさせるような場合、コールバックメソッドにそれぞれのゴールに対する処理を書くよりゴール送信時にその後の処理までまとめて無名関数で書き下してしまう方が分かりやすくなることがあります。
もちろんコールバックメソッドで実装しても構いませんが、アプリケーションの性質に応じた書き方が選べることは利点といえるでしょう。

まとめ

Hy で ROS 2 ノードを実装する方法を説明しました。
Hy からは Python のライブラリを自由に使うことができますし、マクロを使うこともできます。
例えば Managed nodes の実装やライフサイクルメッセージやイベントの処理は煩雑で同じような内容を繰り返し書くことになりがちですが、Hy ではマクロ等を用いてアプリケーション固有の頻出表現をシンプルに記述できるでしょう。

参考文献

https://hylang.org/hy/doc/v1.0.0

https://docs.ros.org/en/jazzy/Tutorials.html

Discussion