ROS 2 のトピックを Zenoh でブリッジして TouchDesigner でサブスクライブする

## はじめに

TouchDesigner に対して ROS 2 を喋るセンサーからデータを送って映像を制御することで、いろいろな表現をしてみたいと思いました。
その下準備として、文字列を送受信 (いわゆる talker / listener) をやってみることにします。
いま熱い Zenoh を使って！

なお本記事では TouchDesigner の使い方については詳しくは述べませんので、外部の情報を参照するようにしていただければと思います。

## デモ

<iframe width="560" height="315" src="https://www.youtube.com/embed/kfleHOhMWbI?si=LsGOeSqD-4PfcDK7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

この動画の後半で、talker から出力された文字列 `Hello World: [数字]` が TouchDesigner 上にも表示される様子が見て取れるかと思います。

### 役割分担
* 左のターミナルはリモートマシンで ROS 2 の talker と zenohd と zenoh-bridge-dds を実行しています。
* 真ん中のコマンドプロンプトは ToouchDesigner と同じローカルマシンで zenoh-bridge-dds を実行しています。
* 左は TouchDesigner 上の Python で Zenoh のサブスクライバを実行していて、受信されたメッセージの内容を Text CHOPに表示されています。

## 材料

* TouchDesigner Windows版
  * [ここ](https://derivative.ca/download)からダウンロードできます。 
  * 私は 2023.11220 というバージョンを使いました。
* Python 3.11.1 Windows版 embeddable パッケージ版
  * [ここ](https://www.python.org/downloads/windows/)からダウンロードできます。 
  * バージョンは TouchDesigner の Pythonに合わせてください。バージョンは Texport を開くと表示されます。
  * 詳しい使い方は[このあたり](https://sho1i4da.com/touchdesigenr-python-embeddable/)を見てください。
    * embeddable 版を使う理由は zenoh-python を TouchDesigner 本体の Pythonではなくembeddable 版の方に pip インストールすることで、モジュールの可搬性を高めたりコンタミネーションを防いだりするためです。
* eclipse-zenoh
  * zenoh-python の pipパッケージです。
  * embeddable 版の Python で pip を使えるようにしてから(やり方は[このあたり](https://zenn.dev/syache_fh/articles/af125d416ff93c)を参照)、[zenoh-python](https://github.com/eclipse-zenoh/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用
```bash
$ zenoh-bridge-dds -e tcp/0.0.0.0:7447
```

### ターミナル2 talker用
```bash
$ 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用

```cmd
> zenoh-bridge-dds -e tcp/[リモートマシンのIPアドレス]:7447
```

### TouchDesigner および Python embeddable

1. TouchDesigner を開き、プロジェクトファイルを適当な場所に保存します。
2. 保存した場所と同じ場所に Python embeddable を python という名前のフォルダがルートになるようにして展開します。
3. Python embeddable に pip と eclipse-zenoh パッケージをインストールします。
4. `Text DAT` を配置して下記の内容を貼り付けます。こうすることで外部から `start_subscription()` が呼ばれたら、zenoh のサブスクライバが動作を開始します。また`end_subscription()` が呼ばれるとセッションが終了します。セッションは正しく終了させないとアプリケーションの終了処理を妨げる場合があるので注意して下さい。
ここで talker がパブリッシュしているトピック名が Zenoh の内部表現に変更されていることに注意してください。この変換規則は [ROS Specific Namespace Prefix](https://design.ros2.org/articles/topic_and_service_names.html#ros-specific-namespace-prefix) に詳しく書かれています。ちなみにプリフィックスの `rt` は ROS Topics の意味だそうです。
    ```python
    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.")
    ``` 

5. Text CHOP を配置します。これはサブスクライバで受信した文字列を表示するために使います。
6. Execute DAT を配置して下記の内容を貼り付けます。これで`onStart()`が呼ばれたときに、Python embeddable の site-packages フォルダが sys.path に追加されるようになります。つまり eclipse-zenoh が TouchDesigner からインポートできるようになるということです。さらに、`onFrameStart(frame)`の中で受信した文字列を Text CHOP に表示するようにします。
    ```python
    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`の設定はこのようにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39561/b91309d7-e234-f5e0-8894-d7c2c2384e39.png)

以上の設定を行うことで冒頭の動画のような状態が実現できると思います。

## ところでなぜ Zenoh を使うことにしたのか?

ROS 2が世に出始めた頃に NAT 越えでとても苦労した(結局 MQTT や ZeroMQ や WebSocket でブリッジすることで回避したのですが)経験から、ROS 2 との親和性が高く、かつ NAT越えが容易なプロトコルを探していました。
その後 [ROSCon 2022](https://roscon.ros.org/2022/) に参加したり、[ZettaScale CEO/CTO直伝！Zenoh完全理解セミナー！](https://connpass.com/event/304464/) を受講したり、実際にサンプルを動かしてみたりするうちに、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](https://github.com/eclipse-zenoh/zenoh-python) のビルド済みモジュールは Python 3.11.1 用のものが公開されており、手間を掛けずに出来そうだということが分かりました。

## まとめ

Zenoh を介することで、ROS 2 の世界と TouchDesignger の世界がいま1つになりました。この構成を土台として、様々なインタラクティブな表現を実現出来そうだという雰囲気を感じで頂けたら嬉しいです。今回は書ききれませんでしたが、いずれ TouchDesignger 側をパブリッシャにするやり方についてもまとめたいと思います。最後までお読み頂きありがとうございました。

## 参考

Zenoh の使い方は、本家リポジトリにある各種のサンプルやドキュメントだけでなく、先日開催された [ZettaScale CEO/CTO直伝！Zenoh完全理解セミナー！](https://connpass.com/event/304464/) の資料と参考情報にリストアップされている各記事が分かりやすいです。
また、困ったら本家の [Discussion](https://github.com/eclipse-zenoh/roadmap/discussions) で質問してみるのも良いと思います。

## 謝辞

この記事の内容を実現するにあたり最も苦労したのは zenoh-python のサブスクライバを TouchDesigner のPython　スクリプトの実行モデルに合わせて実装する部分でした。
特に、`declare_subscriber` を呼び出した後にブロッキングされる理由が良く分からず困っていたのですが、zenoh 本家の Q&A に[質問](https://github.com/eclipse-zenoh/roadmap/discussions/109)を投稿したところ大変親切に教えていただいたおかげで、今回やりたかったことを実現することが出来ました。
サポートしてくれた Mallets さんと p-avital さんにはこの場を借りてお礼申し上げます。どうもありがとうございました。