ROS2&MQTT

ROS2(Cyclone DDS)๋ฅผ ๋„คํŠธ์›Œํฌ๋กœ ์—ฐ๊ฒฐํ•˜์—ฌ ์‹คํ—˜ํ•˜๊ธฐ

๋ฐ๋ถ€์žฅ 2025. 4. 4. 17:28

โœ… ๋ชฉํ‘œ: ROS2(Cyclone DDS)๋ฅผ ๋„คํŠธ์›Œํฌ๋กœ ์—ฐ๊ฒฐํ•˜์—ฌ ์‹คํ—˜ํ•˜๊ธฐ

[ํผ๋ธ”๋ฆฌ์…” ๋…ธํŠธ๋ถ1] โ”€โ”€โ”€โ”€โ”€→ [Cyclone DDS (P2P)] โ”€โ”€โ”€โ”€โ”€→ [์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ ๋…ธํŠธ๋ถ2]

โ—ROS2๋Š” MQTT์™€ ๋‹ค๋ฅด๊ฒŒ ๋ธŒ๋กœ์ปค๊ฐ€ ์—†๊ณ , Peer-to-Peer(DDS) ๊ตฌ์กฐ์ด๋‹ค.
๊ทธ๋ž˜์„œ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์กฐ๊ฑด์ด ์กฐ๊ธˆ ๋” ๊นŒ๋‹ค๋กญ์ง€๋งŒ, ์ž˜๋งŒ ์„ค์ •ํ•˜๋ฉด MQTT๋ณด๋‹ค ๋‚ฎ์€ ๋ ˆ์ดํ„ด์‹œ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค


Ubuntu์—์„œ ROS2 Humble ์„ค์น˜ (๊ณต์‹ ๋ฐฉ์‹, Ubuntu 22.04 ๊ธฐ์ค€)

 

๐Ÿ“ฆ 1๋‹จ๊ณ„: ์„ค์น˜ ์ „ ๊ธฐ๋ณธ ์„ค์ •

sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8

 

๐Ÿ”‘ 2๋‹จ๊ณ„: ROS2 ํŒจํ‚ค์ง€ ์ €์žฅ์†Œ ์„ค์ •

sudo apt install software-properties-common
sudo add-apt-repository universe
sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo tee /usr/share/keyrings/ros-archive-keyring.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
https://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | \
sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

 

๐Ÿ“ฅ 3๋‹จ๊ณ„: ROS2 Humble ์„ค์น˜

sudo apt update
sudo apt install ros-humble-desktop -y

๐Ÿ“Œ (๋””์Šคํฌ ์—ฌ์œ  ์—†์„ ๊ฒฝ์šฐ ros-humble-ros-base๋กœ ์ตœ์†Œ ์„ค์น˜๋„ ๊ฐ€๋Šฅ)

 

4๋‹จ๊ณ„: ROS2 ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc

 

์„ค์น˜ ํ™•์ธ

ros2 --version
# ์˜ˆ: ros2 0.9.8 (humble)
ros2 run demo_nodes_cpp talker
# → talker ๋…ธ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์„ฑ๊ณต!

 

colcon ๋ฐ ์˜์กด ํŒจํ‚ค์ง€ ์„ค์น˜ (๋นŒ๋“œ์šฉ)

sudo apt install python3-colcon-common-extensions -y

โœ… 1. Cyclone DDS ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์กฐ๊ฑด

๊ธฐ๋ณธ ์ „์ œ:

  • ROS2 ๋…ธ๋“œ๋Š” Cyclone DDS๋ฅผ ํ†ตํ•ด ๋ฉ€ํ‹ฐ์บ์ŠคํŠธ UDP๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์„œ๋กœ ํƒ์ƒ‰
  • ๋‹จ, ๋„คํŠธ์›Œํฌ ์„ค์ •์ด ๋‹ค๋ฅด๋ฉด ์„œ๋กœ ๋ชป ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“Œ ๊ผญ ์ง€์ผœ์•ผ ํ•  ์กฐ๊ฑด:

์กฐ๊ฑด ์„ค๋ช…
โœ… ๊ฐ™์€ ์„œ๋ธŒ๋„ท (์˜ˆ: 192.168.0.x) Multicast ํƒ์ƒ‰ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ
โœ… ๋ฐฉํ™”๋ฒฝ ํ—ˆ์šฉ UDP 7400 ~ 7500 ํฌํŠธ ๋ฒ”์œ„ ์—ด๋ ค์•ผ ํ•จ
โœ… ๋ธŒ๋ฆฌ์ง€ ๋„คํŠธ์›Œํฌ or ์œ ์„  ์—ฐ๊ฒฐ Parallels, VirtualBox ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ “๋ธŒ๋ฆฌ์ง€ ๋ชจ๋“œ” ํ•„์ˆ˜
โœ… ๋™์ผ ROS2 ๋ฒ„์ „ & RMW ๋‘˜ ๋‹ค rmw_cyclonedds_cpp ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •

โœ… 2. Cyclone DDS ํ†ต์‹  ํ™•์ธ ์‹คํ—˜

A. ํผ๋ธ”๋ฆฌ์…” ๋…ธํŠธ๋ถ์—์„œ:

ros2 run demo_nodes_cpp talker

B. ์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ ๋…ธํŠธ๋ถ์—์„œ:

ros2 run demo_nodes_cpp listener

 

 ์ •์ƒ ์—ฐ๊ฒฐ๋˜๋ฉด listener ์ชฝ์—์„œ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ๋จ

[publisher]

[subscriber]


โœ… 3. Cyclone DDS์—์„œ ํ†ต์‹ ์ด ์•ˆ ๋  ๊ฒฝ์šฐ ๋Œ€์ฒ˜๋ฒ•

๐Ÿ“ ~/.ros/ros2/์— cyclonedds.xml ์„ค์ • ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

 
<CycloneDDS>
  <Domain>
    <General>
      <NetworkInterfaceAddress>192.168.0.101</NetworkInterfaceAddress> <!-- ๋ณธ์ธ์˜ IP -->
    </General>
    <Discovery>
      <Peers>
        <Peer address="192.168.0.102"/> <!-- ์ƒ๋Œ€๋ฐฉ IP -->
      </Peers>
    </Discovery>
  </Domain>
</CycloneDDS>

๊ทธ๋ฆฌ๊ณ  ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •:

export CYCLONEDDS_URI=file:///home/devjang/.ros/ros2/cyclonedds.xml

๋˜๋Š” .bashrc์— ์ถ”๊ฐ€!


โœ… 4. ROS2 ํผํฌ๋จผ์Šค ์‹คํ—˜์„ ์œ„ํ•œ ์ฝ”๋“œ ๊ตฌ์„ฑ

 

์—ญํ•  ์˜ˆ์‹œ ํŒŒ์ผ์„ค๋ช…
ํผ๋ธ”๋ฆฌ์…” dds_pub.py rclcpp::Publisher, dummy JSON ์ „์†ก
์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ dds_sub.py ์ˆ˜์‹  ํ›„ latency ๊ณ„์‚ฐ, ์†์‹ค๋ฅ  ์ธก์ • ๊ฐ€๋Šฅ
๋นŒ๋“œ ์‹œ์Šคํ…œ colcon build + setup.py  
์‹œ๊ฐํ™” visualize_result.ipynb๋กœ ๋™์ผํ•˜๊ฒŒ ๋ถ„์„ ๊ฐ€๋Šฅ  

setup.py์™€ package.xml์€ ROS2์—์„œ ํŒจํ‚ค์ง€๋ฅผ ์ •์˜ํ•˜๊ณ  ๋นŒ๋“œ/๋ฐฐํฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ๊ตฌ์„ฑ ํŒŒ์ผ๋“ค์ด๋‹ค, ๋‘˜ ๋‹ค ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•˜์ง€๋งŒ ์šฉ๋„์™€ ํฌ๋งท์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ํ•จ๊ป˜ ์จ์•ผํ•œ๋‹ค.

[setup.py ์ฝ”๋“œ]

Python ๊ธฐ๋ฐ˜ ROS2 ํŒจํ‚ค์ง€์˜ ์„ค์น˜ ๋ฐ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ

๐Ÿ”น ์—ญํ• 

  • Python setuptools ๊ธฐ๋ฐ˜์œผ๋กœ ROS2 ๋…ธ๋“œ๋ฅผ ์ •์˜
  • ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์Šคํฌ๋ฆฝํŠธ(ros2 run ...)๋กœ ๋“ฑ๋ก
  • ์„ค์น˜ํ•  ํŒŒ์ผ, ์ด๋ฆ„, ๋ฒ„์ „, ์‹คํ–‰ entrypoint ๋“ฑ ์ง€์ •
from setuptools import setup

package_name = 'dds_test_py'

setup(
    name=package_name,
    version='0.1.0',
    packages=[package_name],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='Your Name',
    maintainer_email='you@example.com',
    description='Python ROS2 pub/sub test',
    license='Apache License 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'dds_pub = dds_pub.dds_pub:main',
            'dds_sub = dds_sub.dds_sub:main',
        ],
    },
)

๐Ÿ”บ ์ฃผ์˜: packages=[package_name] ← ์ด๊ฑด __init__.py ํฌํ•จ๋œ ํด๋” ์ด๋ฆ„๊ณผ ๊ฐ™์•„์•ผ ํ•œ๋‹ค

 


[package.xml ์ฝ”๋“œ]

ROS2๊ฐ€ ํŒจํ‚ค์ง€๋ฅผ ์ธ์‹ํ•˜๊ณ , ์˜์กด์„ฑ/๋ฉ”ํƒ€์ •๋ณด๋ฅผ ์ •์˜ํ•˜๋Š” ํŒŒ์ผ

 

๐Ÿ”น ์—ญํ• 

  • ROS2์˜ ๋นŒ๋“œ ์‹œ์Šคํ…œ(colcon)์ด ์ด ํŒจํ‚ค์ง€๋ฅผ ์ธ์‹ํ•˜๊ฒŒ ํ•ด์คŒ
  • ์–ด๋–ค ํŒจํ‚ค์ง€์ธ์ง€ ์„ค๋ช… (์ด๋ฆ„, ๋ฒ„์ „, ์œ ์ง€๊ด€๋ฆฌ์ž, ๋ผ์ด์„ ์Šค ๋“ฑ)
  • ์–ด๋–ค ROS2 ํŒจํ‚ค์ง€๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๋Š”์ง€ ์„ ์–ธ
<package format="3">
  <name>dds_test_py</name>
  <version>0.1.0</version>
  <description>Python ROS2 pub/sub test</description>
  <maintainer email="you@example.com">Your Name</maintainer>
  <license>Apache 2.0</license>
  <!-- ํ•ต์‹ฌ: Python ํŒจํ‚ค์ง€๋กœ ์„ ์–ธ -->
  <build_type>ament_python</build_type>
  <!-- ์‹คํ–‰ ์‹œ ํ•„์š”ํ•œ ์˜์กด์„ฑ -->
  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
</package>

โœ… ๋‘˜์˜ ๊ด€๊ณ„ ์š”์•ฝ

ํ•ญ๋ชฉ package.xml setup.py
์‚ฌ์šฉ ๋Œ€์ƒ ROS2 ์‹œ์Šคํ…œ Python/colcon ์„ค์น˜
๊ธฐ๋Šฅ ํŒจํ‚ค์ง€ ์ •๋ณด, ROS ์˜์กด์„ฑ Python ์„ค์น˜/์‹คํ–‰ ์„ค์ •
ํ•„์ˆ˜ ์—ฌ๋ถ€ โœ… ROS2์—์„œ ๋ฐ˜๋“œ์‹œ ํ•„์š” โœ… Python ROS2 ๋…ธ๋“œ์— ํ•„์š”
๋น„์œ  "์ฑ… ํ‘œ์ง€์™€ ๋ชฉ์ฐจ" "์ฑ… ๋‚ด์šฉ๋ฌผ์˜ ์„ค์น˜ ์„ค๋ช…์„œ"

ํ•จ๊ป˜ ์“ฐ์ผ ๋•Œ์˜ ํ๋ฆ„

  1. colcon build ๋ช…๋ น ์‹คํ–‰ ์‹œ
    • package.xml ์ฝ๊ณ  ์˜์กด์„ฑ ํ™•์ธ
    • Python ํŒจํ‚ค์ง€์ธ ๊ฒฝ์šฐ setup.py๋ฅผ ์‹คํ–‰ํ•ด ๋นŒ๋“œ/์„ค์น˜
  2. ๋นŒ๋“œ ํ›„:
    • ros2 run dds_test_py dds_pub  setup.py์—์„œ ๋“ฑ๋กํ•œ main() ํ•จ์ˆ˜ ์‹คํ–‰

์‹คํ–‰ ์˜ˆ์‹œ

๋นŒ๋“œ:

colcon build --packages-select dds_test_py

 

์†Œ์Šค:

source install/setup.bash

 

์‹คํ–‰:

ros2 run dds_test_py dds_pub
ros2 run dds_test_py dds_sub

โœ… ๋ชฉํ‘œ

๋…ธ๋“œ ๊ธฐ๋Šฅ
dds_pub.py id, timestamp, payload ํฌํ•จ ๋ฉ”์‹œ์ง€ ์ „์†ก + pub_log.csv ์ €์žฅ
dds_sub.py ์ˆ˜์‹  ํ›„ latency ๊ณ„์‚ฐ + sub_log.csv, loss_result.csv ์ €์žฅ

๐Ÿ“ ํด๋” ๊ตฌ์กฐ (๊ธฐ๋ณธ)

ros2/
โ”œโ”€โ”€ dds_pub/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ dds_pub.py
โ”œโ”€โ”€ dds_sub/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ dds_sub.py
โ”œโ”€โ”€ log/
โ”‚   โ”œโ”€โ”€ dds_pub_log.csv
โ”‚   โ”œโ”€โ”€ dds_sub_log.csv
โ”‚   โ””โ”€โ”€ loss_result.csv
โ”œโ”€โ”€ setup.py
โ”œโ”€โ”€ package.xml

[dds_pub.py (ํผ๋ธ”๋ฆฌ์…”) ์ฝ”๋“œ]

import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import json, time, csv, os

class DDSPublisher(Node):
    def __init__(self):
        super().__init__('dds_publisher')
        self.publisher_ = self.create_publisher(String, 'dds_topic', 10)
        self.timer_period = 0.1  # 10Hz
        self.timer = self.create_timer(self.timer_period, self.timer_callback)
        self.msg_id = 0
        self.log_path = 'log/dds_pub_log.csv'
        os.makedirs('log', exist_ok=True)
        self.log_file = open(self.log_path, 'w', newline='')
        self.writer = csv.writer(self.log_file)
        self.writer.writerow(['id', 'timestamp', 'payload'])
        self.get_logger().info("DDS Publisher Started")

    def timer_callback(self):
        now = time.time()
        payload = {
            "id": self.msg_id,
            "timestamp": now,
            "data": {"sensor": "dds", "value": 123}
        }
        msg_str = json.dumps(payload)
        msg = String()
        msg.data = msg_str
        self.publisher_.publish(msg)

        self.writer.writerow([self.msg_id, now, msg_str])
        self.msg_id += 1

    def destroy_node(self):
        self.log_file.close()
        super().destroy_node()

def main(args=None):
    rclpy.init(args=args)
    node = DDSPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard interrupt → stopping.')
    finally:
        node.destroy_node()
        rclpy.shutdown()

[dds_sub.py (์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ + latency + ์†์‹ค๋ฅ )]

import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import json, time, csv, os

class DDSSubscriber(Node):
    def __init__(self):
        super().__init__('dds_subscriber')
        self.subscription = self.create_subscription(
            String,
            'dds_topic',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

        self.received_ids = set()
        self.log_path = 'log/dds_sub_log.csv'
        self.loss_path = 'log/loss_result.csv'
        os.makedirs('log', exist_ok=True)
        self.log_file = open(self.log_path, 'w', newline='')
        self.writer = csv.writer(self.log_file)
        self.writer.writerow(['id', 'recv_time', 'latency_ms'])
        self.get_logger().info("DDS Subscriber Started")

    def listener_callback(self, msg):
        try:
            now = time.time()
            raw = json.loads(msg.data)
            msg_id = int(raw.get("id", -1))
            sent_time = float(raw.get("timestamp", 0))
            latency = (now - sent_time) * 1000

            self.received_ids.add(msg_id)
            self.writer.writerow([msg_id, now, round(latency, 3)])
        except Exception as e:
            self.get_logger().error(f"Error decoding message: {e}")

    def destroy_node(self):
        self.log_file.close()
        self.calculate_loss()
        super().destroy_node()

    def calculate_loss(self):
        try:
            with open('log/dds_pub_log.csv', newline='') as f:
                reader = csv.DictReader(f)
                pub_ids = set(int(row['id']) for row in reader)
        except FileNotFoundError:
            self.get_logger().warn("dds_pub_log.csv not found")
            return

        total_sent = len(pub_ids)
        total_recv = len(self.received_ids)
        loss = total_sent - total_recv
        loss_rate = (loss / total_sent) * 100 if total_sent > 0 else 0

        with open(self.loss_path, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(["total_sent", "total_received", "loss_count", "loss_rate(%)"])
            writer.writerow([total_sent, total_recv, loss, round(loss_rate, 2)])

        self.get_logger().info(f"์†์‹ค๋ฅ : {loss_rate:.2f}% ({loss}/{total_sent})")

def main(args=None):
    rclpy.init(args=args)
    node = DDSSubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        node.get_logger().info('Keyboard interrupt → stopping.')
    finally:
        node.destroy_node()
        rclpy.shutdown()

ํ˜„์žฌ ๊ฒฝ๋กœ์—๋Š” ์•„์ง ์‹คํ—˜ ๊ฒฐ๊ณผ ๋กœ๊ทธ ํŒŒ์ผ๋“ค์ด ์กด์žฌํ•˜์ง€ ์•Š์•„์„œ ์‹œ๊ฐํ™”๋ฅผ ๋ฐ”๋กœ ์ง„ํ–‰ํ•  ์ˆ˜๋Š” ์—†์œผ๋ฉฐ ์‹คํ—˜์„ ๋๋‚ด๊ณ  ๋กœ๊ทธ ํŒŒ์ผ๋“ค๋งŒ ์ƒ์„ฑํ•˜๋ฉด ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋„๋ก, visualize_result.ipynb๋ฅผ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

[visualize_result.ipynb ํ…œํ”Œ๋ฆฟ]

import pandas as pd
import matplotlib.pyplot as plt

# ํŒŒ์ผ ๊ฒฝ๋กœ
pub_log_path = "log/dds_pub_log.csv"
sub_log_path = "log/dds_sub_log.csv"
loss_result_path = "log/loss_result.csv"

# ํผ๋ธ”๋ฆฌ์…” ๋กœ๊ทธ (์„ ํƒ)
try:
    pub_df = pd.read_csv(pub_log_path)
    print("์ด ํผ๋ธ”๋ฆฌ์‹œ ์ˆ˜:", len(pub_df))
except:
    print("โš ๏ธ ํผ๋ธ”๋ฆฌ์…” ๋กœ๊ทธ ์—†์Œ")

# ์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ ๋กœ๊ทธ (latency)
sub_df = pd.read_csv(sub_log_path)

plt.figure(figsize=(10, 4))
plt.plot(sub_df["recv_time"], sub_df["latency_ms"], label="Latency (ms)")
plt.xlabel("Receive Time (s)")
plt.ylabel("Latency (ms)")
plt.title("ROS2 Latency Over Time")
plt.grid(True)
plt.legend()
plt.show()

# ์†์‹ค๋ฅ  ์ถœ๋ ฅ
loss_df = pd.read_csv(loss_result_path)
print("\n๐Ÿ“Š Message Loss Summary:")
display(loss_df)

์‹คํ—˜ ์ˆœ์„œ & ๋ช…๋ น์–ด

ํ„ฐ๋ฏธ๋„์—์„œ ROS2 ํผ๋ธ”๋ฆฌ์…” ์‹คํ–‰

ros2 run dds_test_py dds_pub

์‹คํ—˜ ์ข…๋ฃŒํ•˜๋ ค๋ฉด: Ctrl + C

 

๋‹ค๋ฅธ ํ„ฐ๋ฏธ๋„์—์„œ ROS2 ์„œ๋ธŒ์Šคํฌ๋ผ์ด๋ฒ„ ์‹คํ–‰

ros2 run dds_test_py dds_sub

 

์‹คํ—˜ ์ข…๋ฃŒํ•˜๋ ค๋ฉด: Ctrl + C

์ข…๋ฃŒ ์‹œ ์ž๋™์œผ๋กœ loss_result.csv ์ƒ์„ฑ๋จ

๋กœ๊ทธ ํŒŒ์ผ ํ™•์ธ

ls log/
# ์˜ˆ์ƒ: dds_pub_log.csv, dds_sub_log.csv, loss_result.csv

์‹œ๊ฐํ™”

jupyter notebook

 visualize_result.ipynb ์—ด๊ณ  latency plot + ์†์‹ค๋ฅ  ํ‘œ ํ™•์ธ