2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ROS 2Advent Calendar 2024

Day 16

【ROS2/Rust】rclrs(ros2-rust)でROS2 ノードを作成してみた

Last updated at Posted at 2024-12-15

はじめに

この記事は、ROS2 Advent Calendarの16日目の記事です!

今回はROS2のRustライブラリの1つであるrclrustを使用してROS2ノードを作成してみたのでそれをまとめます。基本的にはこの記事と公式のREADMEを参考にしています。

ROS2のRustライブラリについては@MrBearing(岡本 拓海)さんが以下のようにまとめてくれているのでぜひ参考にしてください。

環境

以下の環境で動作を確認しています。

項目 バージョン
Ubuntu 22.04
ROS Humble
rustc 1.82.0

環境構築

公式のREADMEに沿って環境構築をしていきます。

sudo apt install -y git libclang-dev python3-pip python3-vcstool # libclang-dev is required by bindgen
# Install these plugins for cargo and colcon:
pip install git+https://github.com/colcon/colcon-cargo.git
pip install git+https://github.com/colcon/colcon-ros-cargo.git

mkdir -p workspace/src && cd workspace
git clone https://github.com/ros2-rust/ros2_rust.git src/ros2_rust
vcs import src < src/ros2_rust/ros2_rust_humble.repos
. /opt/ros/humble/setup.sh
colcon build

ここまでで、以下のpubliser/subscriberの動作確認ができていれば無事環境は整いました。

# In a new terminal
. ./install/setup.sh
ros2 run examples_rclrs_minimal_pub_sub minimal_publisher
# In a new terminal
. ./install/setup.sh
ros2 run examples_rclrs_minimal_pub_sub minimal_subscriber

ROSノードの作成

今回は、文字列を受け取ってその文字数をpublishするノードを作成します。

パッケージの作成

ros2 pkg createコマンドは対応していないようなので、cargo newでパッケージを作成する必要があります。

cd ~/rust_ws/src
cargo new string_length_node
cd string_length_node

依存関係の追加

package.xmlは自動生成されないので自分で作成します。

package.xml
<package format="3">
  <name>string_length_node</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="user@todo.todo">user</maintainer>
  <license>TODO: License declaration</license>

  <depend>rclrs</depend>
  <depend>std_msgs</depend>

  <export>
    <build_type>ament_cargo</build_type>
  </export>
</package>

Cargo.tomlにも以下を追加します。

[dependencies]
rclrs = "*"
std_msgs = "*"

ノードの作成

main.rsを以下のように編集します。
このノードは、std_msgs::msg::String型のデータをstring_topicトピックでsubscribeし、その文字列の長さをstd_msgs::msg::UInt8型でstring_lengthトピックへpublishします。

main.rs
// Rust imports
use std::sync::{Arc, Mutex};
// rclrs related imports
use std_msgs::msg::String as StringMsg;
use std_msgs::msg::UInt8 as UInt8Msg;

// Define the struct for the node
struct StringLengthNode {
    node: Arc<rclrs::Node>,
    _subscription: Arc<rclrs::Subscription<StringMsg>>,
    data: Arc<Mutex<Option<StringMsg>>>,
    publisher: Arc<rclrs::Publisher<UInt8Msg>>,
}

// Define the implementation of the node.
impl StringLengthNode {
// This function is called when creating the node.
    fn new(context: &rclrs::Context)-> Result<Self, rclrs::RclrsError>{
        let node = rclrs::Node::new(context, "string_length_node")?;
        let data = Arc::new(Mutex::new(None));
        let data_cb = Arc::clone(&data);
        let _subscription = node.create_subscription(
            "string_topic", rclrs::QOS_PROFILE_DEFAULT,
            move |msg: StringMsg| {
                *data_cb.lock().unwrap() = Some(msg);
            },
        )?;
        let publisher = node.create_publisher("string_length", rclrs::QOS_PROFILE_DEFAULT)?;
        // Return Ok with the constructed node
        Ok(Self{
            node,
            _subscription,
            publisher,
            data,
        })
    }
    // This function is called when publishing
    fn publish(&self) -> Result<(), rclrs::RclrsError> {
        // Get the latest data from the subscription
        if let Some(s) = &*self.data.lock().unwrap() {
            let mut length_msg = UInt8Msg { data: 0 };
            length_msg.data = s.data.len() as u8;
            self.publisher.publish(length_msg)?;
        }
        Ok(())
    }
} // impl StringLengthNode

fn main() -> Result<(), rclrs::RclrsError>{
    println!("Hello, world! - String length node.");
    // Create the rclrs context.
    let context = rclrs::Context::new(std::env::args())?;
    // Create a node and a clone. The first one will subscribe and the clone will publish
    let string_length_node = Arc::new(StringLengthNode::new(&context)?);
    let string_length_publish_node = Arc::clone(&string_length_node);
    // Thread for timer to publish
    std::thread::spawn(move || -> Result<(), rclrs::RclrsError> {
        loop {
            use std::time::Duration;
            std::thread::sleep(Duration::from_millis(1000));
            string_length_publish_node.publish()?;
        }
    });
    // Spin the subscription node
    rclrs::spin(Arc::clone(&string_length_node.node))
}

コンパイル&動作確認

いつも通りcolcon buildros2 runコマンドでできます。

cd ~/rust_ws/
colcon build
# In a new terminal
. ./install/setup.sh
ros2 run string_length_node string_length_node
# In a new terminal
. ./install/setup.sh
ros2 topic pub /string_topic std_msgs/msg/String "{data: Hello}"
# In a new terminal
. ./install/setup.sh
ros2 topic echo /string_length

ros2 topic echo /string_lengthのターミナルが以下のようになっていれば動作確認完了です!

$ ros2 topic echo /string_length
data: 5
---
data: 5
---
data: 5

また、ros2 topic pub /string_topic std_msgs/msg/String "{data: Hello}"でのpublishする文字列の文字数を変えるとstring_lengthトピックの中身も変わります。

ROSノードの簡単な解説

まず最初にノードの構造体を定義します。Rustには継承がないため、rclcppやrclpyなどのようにNodeから継承はすることができませんが、構造体のメンバとしては定義することができます。ノード、Subscription、Subscribeしたデータ、publisherをメンバにもつ構造体を定義します。

ノードの構造体
struct StringLengthNode {
    node: Arc<rclrs::Node>,
    _subscription: Arc<rclrs::Subscription<StringMsg>>,
    data: Arc<Mutex<Option<StringMsg>>>,
    publisher: Arc<rclrs::Publisher<UInt8Msg>>,
}

Subscribeしたデータを保持するために、以下の対応をする必要があります。

  1. data: Arc<Mutex<Option<StringMsg>>>で宣言したdataへの参照を作成
  2. Subscriptionのコールバック関数内でdataを扱うために、新しい参照を作成(そのままだとRustの所有権のルールに反する)
  3. コールバック関数を作成し、外部の変数(data_cb)を所有する必要があるのでmoveを使用
  4. Mutexで変数をロックしてSubscribeした値を保存
Subscription
        let data = Arc::new(Mutex::new(None));
        let data_cb = Arc::clone(&data);
        let _subscription = node.create_subscription(
            "string_topic", rclrs::QOS_PROFILE_DEFAULT,
            move |msg: StringMsg| {
                *data_cb.lock().unwrap() = Some(msg);
            },
        )?;

rclrsにはcreate_timerのようなタイマーの設定はないため自分でスレッドを作成してループさせます。

main関数内
    std::thread::spawn(move || -> Result<(), rclrs::RclrsError> {
        loop {
            use std::time::Duration;
            std::thread::sleep(Duration::from_millis(1000));
            string_length_publish_node.publish()?;
        }
    });

所感

ros2 pkg createが使えないことや、それに伴ってpackage.xmlを自分で作成しなければならないことなど、まだC++やPythonのノードのように気軽には作成できない部分もありました。今後に期待したいです!

また、公式のドキュメントにもある通り、Subscribeしたデータの取り扱いなどがRustの言語仕様によって少し見慣れないものになりました。

最後に

まだまだほかの言語と比べるとRust×Roboticsの情報や事例は少ないですが今後色々触っていきたいです。

参考情報

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?