はじめに
TouchDesigner に対して ROS 2 を喋るセンサーからデータを送って映像を制御することで、いろいろな表現をしてみたいと思いました。
その下準備として、文字列を送受信 (いわゆる talker / listener) をやってみることにします。
いま熱い Zenoh を使って!
なお本記事では TouchDesigner の使い方については詳しくは述べませんので、外部の情報を参照するようにしていただければと思います。
デモ
この動画の後半で、talker から出力された文字列 Hello World: [数字]
が TouchDesigner 上にも表示される様子が見て取れるかと思います。
役割分担
- 左のターミナルはリモートマシンで ROS 2 の talker と zenohd と zenoh-bridge-dds を実行しています。
- 真ん中のコマンドプロンプトは ToouchDesigner と同じローカルマシンで zenoh-bridge-dds を実行しています。
- 左は TouchDesigner 上の Python で Zenoh のサブスクライバを実行していて、受信されたメッセージの内容を Text CHOPに表示されています。
材料
- TouchDesigner Windows版
- ここからダウンロードできます。
- 私は 2023.11220 というバージョンを使いました。
- Python 3.11.1 Windows版 embeddable パッケージ版
- eclipse-zenoh
- zenoh-python の pipパッケージです。
- embeddable 版の Python で pip を使えるようにしてから(やり方はこのあたりを参照)、zenoh-python の README に従ってインストールします。
- ROS 2 Humbleが動くマシン
- 私は Ubuntu 22.04 と Windows 11 でそれぞれ試しました。WSL や Docker でも良いと思います。
- 後ほど demo_nodes_cpp ノードを使うので Ubuntu の場合は
sudo apt install ros-humble-demo-nodes-cpp
を実行してインストールしておいてください。
- zenoh-bridge-dds
- v0.11.0-rc.3 で検証しました。
- インストール方法は下記を参考にしました。
https://github.com/eclipse-zenoh/zenoh-plugin-dds?tab=readme-ov-file#How-to-install-it
リモートマシンの設定
リモートマシンでは、zenoh-bridge-dds と、demo_nodes_cpp ノードで talker を実行します。
フォアグラウンドで動かす場合はターミナルが2つ必要です。
ターミナル1 zenoh-bridge-dds用
$ zenoh-bridge-dds -e tcp/0.0.0.0:7447
ターミナル2 talker用
$ source /opt/ros/humble/setup.bash
$ ros2 run demo_nodes_cpp talker
2024.6.6追記 以前は最初に export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
を実行していましたが、 zenoh-bridge-dds v0.11.0-rc.3 で確認したところ不要になっていたので変更しました。
ローカルマシンの設定
以下の設定はzenohdが動作しているマシンとは別のマシンとの間の通信も行いたい場合に必要。そうでなければ不要。
コマンドプロンプト zenoh-bridge-dds用
> zenoh-bridge-dds -e tcp/[リモートマシンのIPアドレス]:7447
TouchDesigner および Python embeddable
-
TouchDesigner を開き、プロジェクトファイルを適当な場所に保存します。
-
保存した場所と同じ場所に Python embeddable を python という名前のフォルダがルートになるようにして展開します。
-
Python embeddable に pip と eclipse-zenoh パッケージをインストールします。
-
Text DAT
を配置して下記の内容を貼り付けます。こうすることで外部からstart_subscription()
が呼ばれたら、zenoh のサブスクライバが動作を開始します。またend_subscription()
が呼ばれるとセッションが終了します。セッションは正しく終了させないとアプリケーションの終了処理を妨げる場合があるので注意して下さい。
ここで talker がパブリッシュしているトピック名が Zenoh の内部表現に変更されていることに注意してください。この変換規則は ROS Specific Namespace Prefix に詳しく書かれています。ちなみにプリフィックスのrt
は ROS Topics の意味だそうです。import zenoh key = "rt/chatter" value = "" session = None sub = None def listener(sample): global key global value key = sample.key_expr value = sample.payload.decode('utf-8') print(f"listener : {sample.key_expr}:{sample.payload.decode('utf-8')}") def get_received_key_value(): global key global value return (key, value) def start_subscription(): global session global sub if session == None: session = zenoh.open() # Zenoh の中では ROS 2 の talker のトピック名 chatter の頭に rt が付き rt/chatter になる sub = session.declare_subscriber("rt/chatter", listener) print("Start subscription.") def stop_subscription(): global session global sub if session != None: sub.undeclare() session.close() session = None sub = None print("Stop subscription.")
-
Text CHOP を配置します。これはサブスクライバで受信した文字列を表示するために使います。
-
Execute DAT を配置して下記の内容を貼り付けます。これで
onStart()
が呼ばれたときに、Python embeddable の site-packages フォルダが sys.path に追加されるようになります。つまり eclipse-zenoh が TouchDesigner からインポートできるようになるということです。さらに、onFrameStart(frame)
の中で受信した文字列を Text CHOP に表示するようにします。import sys import os def onStart(): mypath = os.path.abspath("./python/Lib/site-packages") if mypath not in sys.path: sys.path = [mypath] + sys.path debug(sys.path) # サブスクライバを開始 mod('サブスクライバのコードが書かれたText DATの名前').start_subscription() return def onCreate(): return def onExit(): # サブスクライバを終了 mod('サブスクライバのコードが書かれたText DATの名前').stop_subscription() return def onFrameStart(frame): # サブスクライバで受信した文字列をText CHOPに表示 key_value = mod('サブスクライバのコードが書かれたText DATの名前').get_received_key_value() op['Text CHOPの名前'].par.text = key_value[1] return def onFrameEnd(frame): return def onPlayStateChange(state): return def onDeviceChange(): return def onProjectPreSave(): return def onProjectPostSave(): return
ここでExecute DAT
の設定はこのようにしておきます。
以上の設定を行うことで冒頭の動画のような状態が実現できると思います。
ところでなぜ Zenoh を使うことにしたのか?
ROS 2が世に出始めた頃に NAT 越えでとても苦労した(結局 MQTT や ZeroMQ や WebSocket でブリッジすることで回避したのですが)経験から、ROS 2 との親和性が高く、かつ NAT越えが容易なプロトコルを探していました。
その後 ROSCon 2022 に参加したり、ZettaScale CEO/CTO直伝!Zenoh完全理解セミナー! を受講したり、実際にサンプルを動かしてみたりするうちに、Zenoh ならやりたいことが楽に出来そうなので積極的に活用していきたいと思ったためです。
もう1つの理由 (rclpy と TouchDesigner の Pythonバージョン不一致問題)
TouchDesigner は内部に Python を抱えていて、外部の様々なモジュールをインポートして使うことが出来ます。
はじめは軽い気持ちで rclpy をインポートしてやれば簡単に接続できるだろうと思ったのですが、調べてみると手元の TouchDesigner 2023.11220 の Python のバージョンは 3.11.1 で、私が使いたい ROS 2 Humble (Windows版) に必要な Python のバージョンは 3.8.3 でした。
ROS 2 関連の諸々をソースからビルドして Python 3.11.1 に対応させられれば rclpy が使えるようになるかもしれませんが面倒です。
そこで Zenoh に調べてみたところ、zenoh-python のビルド済みモジュールは Python 3.11.1 用のものが公開されており、手間を掛けずに出来そうだということが分かりました。
まとめ
Zenoh を介することで、ROS 2 の世界と TouchDesignger の世界がいま1つになりました。この構成を土台として、様々なインタラクティブな表現を実現出来そうだという雰囲気を感じで頂けたら嬉しいです。今回は書ききれませんでしたが、いずれ TouchDesignger 側をパブリッシャにするやり方についてもまとめたいと思います。最後までお読み頂きありがとうございました。
参考
Zenoh の使い方は、本家リポジトリにある各種のサンプルやドキュメントだけでなく、先日開催された ZettaScale CEO/CTO直伝!Zenoh完全理解セミナー! の資料と参考情報にリストアップされている各記事が分かりやすいです。
また、困ったら本家の Discussion で質問してみるのも良いと思います。
謝辞
この記事の内容を実現するにあたり最も苦労したのは zenoh-python のサブスクライバを TouchDesigner のPython スクリプトの実行モデルに合わせて実装する部分でした。
特に、declare_subscriber
を呼び出した後にブロッキングされる理由が良く分からず困っていたのですが、zenoh 本家の Q&A に質問を投稿したところ大変親切に教えていただいたおかげで、今回やりたかったことを実現することが出来ました。
サポートしてくれた Mallets さんと p-avital さんにはこの場を借りてお礼申し上げます。どうもありがとうございました。