ROS2コマンドとMCAPを試してみた

# 背景・目的
前回、MCAPについて下記の記事で整理しました。

https://qiita.com/zumax/items/966a42c9b94114d62ee6

MCAPは、下記の特徴があります。
- 多様なシリアライズ形式のサポート
- 高性能な書き込みと読み取り
- メッセージスキーマやメタデータをファイル内に含む自己完結型の構造を持っています。これにより、外部の依存関係なしにデータの解釈や再生が可能
- 堅牢性とデータの回復性

今回、ros2とMCAPを操作しながら理解を進めます。

# まとめ
## ROS2の基本的概念

|用語|説明|
|:--|:--|
|ノード（node）|	処理の単位（例：カメラ、センサ）<BR>1つのプロセス（アプリケーションの単位）|
|トピック（topic）|ノード間で送受信されるメッセージのチャネル|
|パブリッシャー	|メッセージを送る側|
|サブスクライバー	|メッセージを受け取る側|


## コマンド
下記にコマンドの特徴をまとめます。

|カテゴリ|コマンド|概要|補足|
|:--|:--|:--|:--|
|トピック|ros2 topic list|現在アクティブな トピック名の一覧 を表示する|現在通信中または作成済のトピックが対象|
||ros2 topic info <トピック>|トピックの 型や Publisher/Subscriber 数 を確認する|通信の方向性や型が確認できる|
|ノード|ros2 run <パッケージ名> <ノード>|指定した ROS 2 パッケージに含まれる ノードスクリプトを起動する<BR>（例: Python ノード）|ノードの自作・動作確認に使う|
|Publisher|ros2 topic pub|指定したトピックに 手動でメッセージを Publish（送信）する|デバッグや簡易送信に利用|
||ros2 bag play|bag ファイルの内容を 再度トピックに Publish（再生）する|録画データのリプレイ用途|
|Subscriber|ros2 topic echo <トピック>| 指定したトピックのメッセージを リアルタイムで表示する|データ確認・デバッグに有効|
||ros2 bag record|指定したトピックのメッセージを ファイル（bag形式）に記録する|デバッグ・検証・再利用のために記録|
||ros2 bag info|bag ファイルの メタ情報（トピック名・件数・時間など）を表示する|記録内容の確認用|

## データフロー
上記のコマンドや概念を整理するとかきのようにあなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/89d802f9-149c-47a9-b98e-81e26122fbc1.png)



# 概要
## MCAPについて
下記にまとめましたので、よろしければご参照ください。

https://qiita.com/zumax/items/966a42c9b94114d62ee6

## ROS2
下記にまとめましたので、よろしければご参照ください。

https://qiita.com/zumax/items/5f638645fda99b75b82a

あらためて、下記にも記載します。

- ROS 2は、Robot Operating System 2の略
- ロボットアプリケーションの開発を支援するためのオープンソースのミドルウェアフレームワーク
- ROS 1の後継として開発され、リアルタイム性やセキュリティ、マルチプラットフォーム対応などの機能強化が図られている
- 主な特徴は下記の通り
    - モジュール性：ノード、トピック、サービスなどの概念により、機能を分割して開発・管理できる
    - DDS（Data Distribution Service）ベース：リアルタイム通信やQoS（Quality of Service）の設定が可能
    - マルチプラットフォーム対応：Linux、Windows、macOSなど、複数のOSで動作する
    - セキュリティ強化：通信の暗号化や認証機能が追加されている
    - ROS 2は、ロボットの制御、センサーの統合、データ処理など、ロボティクスアプリケーション全体の開発を支援するための包括的なフレームワーク



## MCAPとROS2の関連性

| 項目 | MCAP | ROS 2|
|:--|:--|:--|
|種類|ファイルフォーマット|ミドルウェアフレームワーク|
|目的|データの記録・保存|ロボットアプリケーションの開発支援|
|データの扱い|タイムスタンプ付きのメッセージの保存|ノード間の通信、センサー統合、制御など|
|対応データ形式|Protobuf、JSON、ROS 1/2メッセージなど|ROSメッセージ（ROS 1/2）|
|関連性|ROS 2のデータ記録フォーマットとして利用可能|MCAPをデータ記録フォーマットとして利用可能|

- MCAP：ロボティクス分野でのデータ記録・保存に特化したファイルフォーマット
- ROS 2：ロボットアプリケーション全体の開発を支援するミドルウェアフレームワーク

## ROS2の基本的概念

|用語|説明|
|:--|:--|
|ノード（node）|	処理の単位（例：カメラ、センサ）<BR>1つのプロセス（アプリケーションの単位）|
|トピック（topic）|ノード間で送受信されるメッセージのチャネル|
|パブリッシャー	|メッセージを送る側|
|サブスクライバー	|メッセージを受け取る側|


# 実践
## 前提
下記を使用しています。
- Mac
- UTM


## 環境の準備
下記を基に環境を作ります。

https://qiita.com/zumax/items/cbcebf55b81b9296a3d1

https://qiita.com/zumax/items/8090f59fc2b779af55be

1. UTMを起動します
1. 「ubuntu-24.04.2-live-server-amd64.iso」を基にインストールします
1. インストールできました
    ```bash
    ~$ ros2 --help
    usage: ros2 [-h] [--use-python-default-buffering] Call `ros2 <command> -h` for more detailed usage. ...
    
    ros2 is an extensible command-line tool for ROS 2.
    
    options:
      -h, --help            show this help message and exit
      --use-python-default-buffering
                            Do not force line buffering in stdout and instead use the python default buffering, which might be affected by PYTHONUNBUFFERED/-u and depends on whatever stdout is interactive or not
    
    Commands:
      action     Various action related sub-commands
      bag        Various rosbag related sub-commands
      component  Various component related sub-commands
      daemon     Various daemon related sub-commands
      doctor     Check ROS setup and other potential issues
      interface  Show information about ROS interfaces
      launch     Run a launch file
      lifecycle  Various lifecycle related sub-commands
      multicast  Various multicast related sub-commands
      node       Various node related sub-commands
      param      Various param related sub-commands
      pkg        Various package related sub-commands
      run        Run a package specific executable
      security   Various security related sub-commands
      service    Various service related sub-commands
      topic      Various topic related sub-commands
      wtf        Use `wtf` as alias to `doctor`
    
      Call `ros2 <command> -h` for more detailed usage.
    :~$ 
    ```
## トピックの確認と送受信
1. トピック一覧の確認をします。parameter_eventsとresoutがありました（デフォルトのようです）
    ```bash
    $ ros2 topic list
    /parameter_events
    /rosout
    $ 
    ```
1. ターミナルを2つ起動します（ここではターミナルAとターミナルBと呼びます）

### 送信（ターミナルA）
1. 下記のコマンドでパブリッシュします
    ```bash
    ros2 topic pub /hello_std_msgs std_msgs/String "data: 'Hello ROS 2'"

    publishing #1: std_msgs.msg.String(data='Hello ROS 2')
    
    publishing #2: std_msgs.msg.String(data='Hello ROS 2')
    
    publishing #3: std_msgs.msg.String(data='Hello ROS 2')
    
    publishing #4: std_msgs.msg.String(data='Hello ROS 2')
    
    publishing #5: std_msgs.msg.String(data='Hello ROS 2')
    
    ・・・・
    ```

※ パラメータは下記のとおりです。
- 特に引数を指定しない場合、送信し続けます
    - topic:
        - topic関連のコマンド
    - pub: 
        - publish.指定したトピックにメッセージを送る
    - /hello_std_msgs:
        - 宛先とするトピック名（任意）
    - std_msgs/Strings
        - 送信するメッセージの型
        - 標準の文字列型を指定
    - "data: 'Hello ROS 2'"
        - 送信するメッセージのbody
        - dataフィールドに'Hello ROS2'を入れている

### 受信（ターミナルB）
1. 下記のコマンドでサブスクライブします
    ```bash
    ros2 topic echo /hello_std_msgs

    data: Hello ROS 2
    ---
    data: Hello ROS 2
    ---
    data: Hello ROS 2
    ---
    data: Hello ROS 2
    ---
    data: Hello ROS 2
    ---

    ・・・

    ```

※ echoにより、リアルタイムに出力されていることが確認できます

### トピックの確認
1. ターミナルAで送信中に、ターミナルBでリストします。/hello_std_msgsトピックが表示されました
    ```bash
    $ ros2 topic list
    /hello_std_msgs
    /parameter_events
    /rosout
    $ 
    ```

1. ターミナルAからの送信を止めます。（Ctrl+Cで抜けます）

1. ターミナルBで再びリストします。先ほど見えていた **/hello_std_msgsトピック** は消えました
    ```bash
    ubuntu@ubuntu:~$ ros2 topic list
    /parameter_events
    /rosout
    ubuntu@ubuntu:~$ 
    ```

※ 一時的なトピックのため、送信を止めると消えるようです。Publisherである自分自身がノードを作っているようです。トピックを維持しているノードがいないと消えるようです。
※ `/parameter_events`や`/rosout`などは、ROS2が管理しており常に存在しているようです

## ノードの操作
### Pythonでノードを作成
#### テンプレート作成
1. srcディレクトリを作成します
    ```bash
    ~$ mkdir -p ~/ros2_ws/src
    ~$ cd ~/ros2_ws/src/
    ~/ros2_ws/src$ 
    ```
1. サンプルノードを作成します
    ```bash
    ros2 pkg create --build-type ament_python my_py_node --dependencies rclpy std_msgs

    ===
    $ ros2 pkg create --build-type ament_python my_py_node --dependencies rclpy std_msgs
    going to create a new package
    package name: my_py_node
    destination directory: /home/ubuntu/ros2_ws/src
    package format: 3
    version: 0.0.0
    description: TODO: Package description
    maintainer: ['ubuntu <ubuntu@todo.todo>']
    licenses: ['TODO: License declaration']
    build type: ament_python
    dependencies: ['rclpy', 'std_msgs']
    creating folder ./my_py_node
    creating ./my_py_node/package.xml
    creating source folder
    creating folder ./my_py_node/my_py_node
    creating ./my_py_node/setup.py
    creating ./my_py_node/setup.cfg
    creating folder ./my_py_node/resource
    creating ./my_py_node/resource/my_py_node
    creating ./my_py_node/my_py_node/__init__.py
    creating folder ./my_py_node/test
    creating ./my_py_node/test/test_copyright.py
    creating ./my_py_node/test/test_flake8.py
    creating ./my_py_node/test/test_pep257.py
    
    [WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
    It is recommended to use one of the ament license identifiers:
    Apache-2.0
    BSL-1.0
    BSD-2.0
    BSD-2-Clause
    BSD-3-Clause
    GPL-3.0-only
    LGPL-3.0-only
    MIT
    MIT-0
    $ 
    
    ```

    - `ros2 pkg create` : ROS2パッケージを新規作成
    - `--build-type ament_python`： Python を使ってビルドされることを指定
    - `my_py_node`：パッケージ名
    - `--dependencies rclpy std_msgs`：パッケージが依存しているライブラリを指定
        - `rclpy`：ROS 2 の Pythonクライアントライブラリ
        - `std_msgs`：標準のメッセージ型（String, Int32など）



1. 以下の構成ができました
    ```bash
    $ tree 
    .
    └── my_py_node
        ├── my_py_node
        │   └── __init__.py
        ├── package.xml
        ├── resource
        │   └── my_py_node
        ├── setup.cfg
        ├── setup.py
        └── test
            ├── test_copyright.py
            ├── test_flake8.py
            └── test_pep257.py
    
    5 directories, 8 files
    $ 
    ```
    
#### ソースを作成
1. my_py_node/my_py_node/talker.py を以下の内容で作成します
    ```python
    import rclpy
    from rclpy.node import Node
    from std_msgs.msg import String
    
    class TalkerNode(Node):
        def __init__(self):
            super().__init__('talker')
            self.publisher_ = self.create_publisher(String, 'hello', 10)
            self.timer = self.create_timer(0.5, self.timer_callback)
            self.count = 0
    
        def timer_callback(self):
            msg = String()
            msg.data = f'Hello ROS 2: {self.count}'
            self.publisher_.publish(msg)
            self.get_logger().info(f'Publishing: "{msg.data}"')
            self.count += 1
    
    def main(args=None):
        rclpy.init(args=args)
        node = TalkerNode()
        rclpy.spin(node)
        rclpy.shutdown()
    
    if __name__ == '__main__':
        main()
    ```

- ノード名 talker で起動
- トピック /hello に、0.5秒ごとに Hello ROS 2: 0, Hello ROS 2: 1, ... のようなメッセージが送信される
    - 0.5秒ごとに、timer_callback()を呼び出し、メッセージを出力している
    - rclpy.spin(node)により、timer_callback()が定期的に呼び出される

#### setup.pyにエントリーポイントを追記
1. `my_py_node.talker:main`を追記します
    ```py
    entry_points={
        'console_scripts': [
            'talker = my_py_node.talker:main',
        ],
    },
    ```

1. 全体像は下記のとおりです
    ```py
    $ cat my_py_node/setup.py 
    from setuptools import find_packages, setup
    
    package_name = 'my_py_node'
    
    setup(
        name=package_name,
        version='0.0.0',
        packages=find_packages(exclude=['test']),
        data_files=[
            ('share/ament_index/resource_index/packages',
                ['resource/' + package_name]),
            ('share/' + package_name, ['package.xml']),
        ],
        install_requires=['setuptools'],
        zip_safe=True,
        maintainer='ubuntu',
        maintainer_email='ubuntu@todo.todo',
        description='TODO: Package description',
        license='TODO: License declaration',
        tests_require=['pytest'],
        entry_points={
            'console_scripts': [
                'talker = my_py_node.talker:main',
            ],
        },
    )
    $ 
    ```


#### ビルド
1. ビルドします
    ```bash
    $ colcon build --symlink-install
    [0.488s] WARNING:colcon.colcon_ros.prefix_path.ament:The path '/home/ubuntu/ros2_ws/install/my_py_node' in the environment variable AMENT_PREFIX_PATH doesn't exist
    [0.488s] WARNING:colcon.colcon_ros.prefix_path.ament:The path '/home/ubuntu/ros2_ws/install' in the environment variable AMENT_PREFIX_PATH doesn't contain any 'local_setup.*' files.
    Starting >>> my_py_node
    Finished <<< my_py_node [3.55s]          
    
    Summary: 1 package finished [3.95s]
    $ 
    ```
#### publish（ターミナルA）
1. メッセージを送信します
    ```bash
    $ ros2 run my_py_node talker
    [INFO] [1746423555.971515959] [talker]: Publishing: "Hello ROS 2: 0"
    [INFO] [1746423556.434968269] [talker]: Publishing: "Hello ROS 2: 1"
    [INFO] [1746423556.936578724] [talker]: Publishing: "Hello ROS 2: 2"
    [INFO] [1746423557.437360531] [talker]: Publishing: "Hello ROS 2: 3"
    [INFO] [1746423557.936618063] [talker]: Publishing: "Hello ROS 2: 4"
    [INFO] [1746423558.436830477] [talker]: Publishing: "Hello ROS 2: 5"
    [INFO] [1746423558.938511280] [talker]: Publishing: "Hello ROS 2: 6"
    [INFO] [1746423559.437847548] [talker]: Publishing: "Hello ROS 2: 7"
    [INFO] [1746423559.935977189] [talker]: Publishing: "Hello ROS 2: 8"
    [INFO] [1746423560.437838697] [talker]: Publishing: "Hello ROS 2: 9"
    [INFO] [1746423560.938195209] [talker]: Publishing: "Hello ROS 2: 10"
    [INFO] [1746423561.437847674] [talker]: Publishing: "Hello ROS 2: 11"
    ・・・
    ```

#### subscribe（ターミナルB）
1. トピックを確認します
    ```bash
    $ ros2 topic list
    /hello
    /parameter_events
    /rosout
    $ 
    ```
1. echoで表示します。受信できました！
    ```bash
    $ ros2 topic echo /hello
    2025-05-05 05:42:53.997 [XMLPARSER Error] getcwd failed No such file or directory -> Function loadDefaultXMLFile
    data: 'Hello ROS 2: 438'
    ---
    data: 'Hello ROS 2: 439'
    ---
    data: 'Hello ROS 2: 440'
    ```

## MCAP形式で記録する
1. 下記のコマンドを実行します
    ```bash
    $ ros2 bag record -o my_bag --storage mcap /hello
    [INFO] [1746455410.390757527] [rosbag2_recorder]: Press SPACE for pausing/resuming
    [INFO] [1746455410.422411606] [rosbag2_recorder]: Listening for topics...
    [INFO] [1746455410.422631740] [rosbag2_recorder]: Event publisher thread: Starting
    [INFO] [1746455410.423877678] [rosbag2_recorder]: Recording...
    [INFO] [1746455801.472895556] [rosbag2_recorder]: Pausing recording.
    [INFO] [1746455801.480338694] [rosbag2_cpp]: Writing remaining messages from cache to the bag. It may take a while
    [INFO] [1746455801.489244086] [rosbag2_recorder]: Event publisher thread: Exiting
    [INFO] [1746455801.490517203] [rosbag2_recorder]: Recording stopped
    [INFO] [1746455801.571788548] [rclcpp]: signal_handler(signum=2)
    $ 
    ```

    - -o my_bag: 保存先フォルダ名
    - --storage mcap:MCAP形式で記録
    - /hello: 記録対象のトピック名 

1. 別ターミナルで確認します。my_bagフォルダができています
    ```bash
    $ ls -l
    total 24
    drwxrwxr-x 3 XXX XXX 4096 May  5 05:37 build
    drwxrwxr-x 2 XXX XXX 4096 May  5 14:39 hello_bag
    drwxrwxr-x 3 XXX XXX 4096 May  5 05:37 install
    drwxrwxr-x 3 XXX XXX 4096 May  5 05:37 log
    drwxrwxr-x 2 XXX XXX 4096 May  5 14:39 my_bag
    drwxrwxr-x 3 XXX XXX 4096 May  5 05:32 src
    $ 
    ```

1. 中身は、空でした
    ```bash
    $ ls -l my_bag/
    total 0
    -rw-rw-r-- 1 XXX XXX 0 May  5 14:39 my_bag_0.mcap
    $ cat my_bag/my_bag_0.mcap 
    $ 
    ```

1. 下記のコマンドでメッセージを送信します
    ```bash
    ubuntu@ubuntu:~/ros2_ws$ ros2 run my_py_node talker
    [INFO] [1746456624.855327989] [talker]: Publishing: "Hello ROS 2: 0"
    [INFO] [1746456625.321653404] [talker]: Publishing: "Hello ROS 2: 1"
    [INFO] [1746456625.817827820] [talker]: Publishing: "Hello ROS 2: 2"
    [INFO] [1746456626.317045072] [talker]: Publishing: "Hello ROS 2: 3"
    [INFO] [1746456626.818447231] [talker]: Publishing: "Hello ROS 2: 4"
    [INFO] [1746456627.319625612] [talker]: Publishing: "Hello ROS 2: 5"
    [INFO] [1746456627.819363865] [talker]: Publishing: "Hello ROS 2: 6"
    [INFO] [1746456628.317733225] [talker]: Publishing: "Hello ROS 2: 7"
    [INFO] [1746456628.813893643] [talker]: Publishing: "Hello ROS 2: 8"
    [INFO] [1746456629.312155996] [talker]: Publishing: "Hello ROS 2: 9"
    [INFO] [1746456629.811090218] [talker]: Publishing: "Hello ROS 2: 10"
    [INFO] [1746456630.314374421] [talker]: Publishing: "Hello ROS 2: 11"
    [INFO] [1746456630.819625907] [talker]: Publishing: "Hello ROS 2: 12"
    [INFO] [1746456631.314952955] [talker]: Publishing: "Hello ROS 2: 13"
    [INFO] [1746456631.810452367] [talker]: Publishing: "Hello ROS 2: 14"
    [INFO] [1746456632.312873593] [talker]: Publishing: "Hello ROS 2: 15"
    [INFO] [1746456632.810239991] [talker]: Publishing: "Hello ROS 2: 16"
    [INFO] [1746456633.320642098] [talker]: Publishing: "Hello ROS 2: 17"
    [INFO] [1746456633.819014478] [talker]: Publishing: "Hello ROS 2: 18"
    [INFO] [1746456634.311467673] [talker]: Publishing: "Hello ROS 2: 19"
    [INFO] [1746456634.818081860] [talker]: Publishing: "Hello ROS 2: 20"
    [INFO] [1746456635.313023067] [talker]: Publishing: "Hello ROS 2: 21"
    [INFO] [1746456635.811034489] [talker]: Publishing: "Hello ROS 2: 22"
    ```
1. Ctrl+Cをします
1. ファイルの内容を確認します
    ```bash
    $ ros2 bag info my_bag
    
    Files:             my_bag_0.mcap
    Bag size:          36.0 KiB
    Storage id:        mcap
    ROS Distro:        jazzy
    Duration:          214.495901362s
    Start:             May  5 2025 14:50:24.818789035 (1746456624.818789035)
    End:               May  5 2025 14:53:59.314690397 (1746456839.314690397)
    Messages:          430
    Topic information: Topic: /hello | Type: std_msgs/msg/String | Count: 430 | Serialization Format: cdr
    Service:           0
    Service information: 
    $ 
    ```
    - Files:使用されているバッグファイル
    - Bag size:データ全体のファイルサイズ
    - Storage id:記録形式(mcap)
    - ROS Distro:使用されたROS 2ディストリビューション（例: jazzy）
    - Duration:記録された時間の長さ（開始から終了までの合計時間）
    - Start / End:記録の開始時刻と終了時刻
    - Messages:430
    - Topic information	:トピックごとの詳細（トピック名、型、件数など）
1. 再生します
    ```bash
    $ ros2 bag play my_bag
    [INFO] [1746457477.260023213] [rosbag2_player]: Set rate to 1
    [INFO] [1746457477.666327399] [rosbag2_player]: Adding keyboard callbacks.
    [INFO] [1746457477.666606203] [rosbag2_player]: Press SPACE for Pause/Resume
    [INFO] [1746457477.666824385] [rosbag2_player]: Press CURSOR_RIGHT for Play Next Message
    [INFO] [1746457477.667502398] [rosbag2_player]: Press CURSOR_UP for Increase Rate 10%
    [INFO] [1746457477.667742370] [rosbag2_player]: Press CURSOR_DOWN for Decrease Rate 10%
    [INFO] [1746457477.668139344] [rosbag2_player]: Playback until timestamp: -1
    $ 
    ```

    - ros2 bag record：記録したROSメッセージを再度Publishする


# 考察
今回、ROS2とMCAPについて簡単に試してみました。
次回は、ツールを使用してMCAPファイルのかしｋを試してみます。

# 参考

https://qiita.com/zumax/items/966a42c9b94114d62ee6

https://docs.ros.org/en/humble/index.html

https://docs.ros.org/en/rolling/Releases/Release-Humble-Hawksbill.html

https://docs.ros.org/en/jazzy/Installation/Maintaining-a-Source-Checkout.html

https://qiita.com/zumax/items/cbcebf55b81b9296a3d1