노드의 정의
ROS2에서 노드(Node)는 시스템에서 실행되는 기본 단위로, 노드는 여러 프로세스에 걸쳐 분산될 수 있으며, 각 노드는 서로 통신을 통해 데이터를 주고받는다. 노드는 일반적으로 특정 기능을 수행하는 작은 단위의 프로그램이며, 노드 간의 통신을 통해 전체 시스템의 작업을 분배하고 협력하여 큰 작업을 수행한다.
노드의 구조
ROS2 노드는 rclcpp(C++) 또는 rclpy(Python) 라이브러리를 사용하여 구현된다. 각각의 노드는 다음과 같은 구조적 요소로 구성된다.
- 이름(Name): 각 노드는 고유한 이름을 갖는다. 노드의 이름은 네트워크 상에서 노드를 구분하는 데 사용되며, 시스템 내에서 노드 간의 충돌을 피하기 위해 고유해야 한다.
- 네임스페이스(Namespace): 여러 노드를 그룹화하여 동일한 이름의 노드가 다른 네임스페이스 내에 있을 수 있다. 네임스페이스를 통해 논리적 그룹을 만들어 관리하기 쉬워진다.
노드 이름과 네임스페이스 예시
예를 들어, 두 개의 로봇이 각각 동일한 노드를 가지고 있지만, 네임스페이스로 구분된다면 다음과 같이 관리할 수 있다.
/robot1/camera_node
/robot2/camera_node
노드 생성
ROS2에서 노드를 생성하는 방법은 프로그래밍 언어에 따라 다르며, 다음과 같은 방법으로 노드를 만들 수 있다.
C++에서의 노드 생성
C++에서 노드를 생성할 때는 rclcpp 라이브러리를 사용한다. 간단한 노드를 생성하는 코드는 아래와 같다.
#include "rclcpp/rclcpp.hpp"
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv); // ROS2 초기화
auto node = std::make_shared<rclcpp::Node>("my_node"); // 노드 생성
rclcpp::spin(node); // 노드 실행
rclcpp::shutdown(); // ROS2 종료
return 0;
}
Python에서의 노드 생성
Python에서 노드를 생성할 때는 rclpy 라이브러리를 사용한다. 아래는 Python으로 노드를 생성하는 예제이다.
import rclpy
from rclpy.node import Node
def main(args=None):
rclpy.init(args=args) # ROS2 초기화
node = Node("my_node") # 노드 생성
rclpy.spin(node) # 노드 실행
node.destroy_node() # 노드 종료
rclpy.shutdown() # ROS2 종료
if __name__ == '__main__':
main()
노드의 관리
노드 관리에는 다음과 같은 작업이 포함된다.
- 노드 실행: 노드를 생성하고 실행하는 것은 ROS2 시스템에서 기본적으로 수행하는 작업이다. C++에서는
rclcpp::spin()
, Python에서는rclpy.spin()
을 사용하여 노드를 실행한다. - 노드 종료: 노드를 종료할 때는
rclcpp::shutdown()
또는rclpy.shutdown()
함수를 호출하여 ROS2 시스템을 정상적으로 종료한다. 이는 시스템 내에서 사용하던 자원을 해제하고, 더 이상 통신하지 않음을 알린다.
노드의 상태 관리
ROS2 노드는 상태를 관리할 수 있는 생명주기(Lifecycle) 노드로 확장할 수 있다. 그러나 기본적인 노드는 상태 관리가 필요하지 않으며, 다음과 같은 간단한 상태 전환이 이루어진다.
- 생성 상태: 노드가 처음 생성되고, 필요한 초기화 작업이 완료되는 상태이다.
- 실행 상태: 노드가 동작하면서 통신을 주고받는 상태이다.
- 종료 상태: 노드가 종료되고, 메모리 자원과 통신이 해제되는 상태이다.
다음으로는 퍼블리셔와 서브스크라이버를 통해 통신을 설정하는 과정이 이어진다.
퍼블리셔와 서브스크라이버 설정
ROS2 노드 간의 통신은 기본적으로 퍼블리셔(Publisher)와 서브스크라이버(Subscriber) 개념을 통해 이루어진다. 퍼블리셔는 데이터를 송신하고, 서브스크라이버는 해당 데이터를 수신하는 방식으로 동작한다. 퍼블리셔와 서브스크라이버는 토픽(Topic)이라는 채널을 통해 데이터를 교환한다.
퍼블리셔(Publisher) 생성
퍼블리셔는 ROS2 노드가 다른 노드에게 정보를 전송할 수 있도록 설정된 객체이다. 퍼블리셔는 특정한 토픽을 통해 데이터를 송신하며, 이 토픽에 등록된 서브스크라이버들은 그 데이터를 수신한다.
C++에서 퍼블리셔 생성
C++에서 퍼블리셔를 생성하는 방법은 다음과 같다.
auto publisher = node->create_publisher<std_msgs::msg::String>("topic_name", 10);
위 코드에서 퍼블리셔는 std_msgs::msg::String
타입의 메시지를 발행하며, "topic_name"이라는 토픽으로 데이터를 전송한다. 마지막 인자는 큐 사이즈(queue size)로, 송신 대기열의 크기를 지정한다.
Python에서 퍼블리셔 생성
Python에서는 다음과 같이 퍼블리셔를 생성한다.
publisher = node.create_publisher(String, "topic_name", 10)
여기서 String
은 발행할 메시지 타입이며, "topic_name"
은 퍼블리싱할 토픽 이름이다.
서브스크라이버(Subscriber) 생성
서브스크라이버는 특정 토픽에서 데이터를 수신하기 위해 노드에 설정된 객체이다. 서브스크라이버는 퍼블리셔가 송신하는 데이터를 받아서 처리할 수 있다.
C++에서 서브스크라이버 생성
C++에서 서브스크라이버를 생성하는 방법은 다음과 같다.
auto subscription = node->create_subscription<std_msgs::msg::String>(
"topic_name", 10, [](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(node->get_logger(), "I heard: '%s'", msg->data.c_str());
});
위 코드는 "topic_name"이라는 토픽에서 데이터를 수신하는 서브스크라이버를 생성하며, 수신된 메시지를 처리하는 콜백 함수를 등록한다.
Python에서 서브스크라이버 생성
Python에서는 서브스크라이버를 다음과 같이 생성할 수 있다.
subscription = node.create_subscription(
String,
"topic_name",
lambda msg: node.get_logger().info('I heard: "%s"' % msg.data),
10)
이 코드에서는 "topic_name"
이라는 토픽에서 데이터를 받아 처리하며, 콜백 함수는 수신된 메시지를 출력하는 방식으로 동작한다.
퍼블리셔와 서브스크라이버의 통신 흐름
퍼블리셔와 서브스크라이버는 토픽(Topic)을 통해 통신하며, 토픽을 기준으로 데이터를 주고받는다. 이를 시각적으로 설명하면 다음과 같다.
위 다이어그램은 퍼블리셔가 데이터를 송신하고, 여러 서브스크라이버가 동일한 토픽에서 데이터를 수신하는 과정을 보여준다.
통신 설정과 QoS 정책
퍼블리셔와 서브스크라이버는 QoS(Quality of Service) 정책을 통해 통신 품질을 관리할 수 있다. QoS는 네트워크 상태와 응용 프로그램의 요구 사항에 따라 데이터를 어떻게 처리할지를 정의한다.
QoS 설정 예시 (C++)
rclcpp::QoS qos(rclcpp::KeepLast(10)); // 최신 10개의 메시지만 유지
auto publisher = node->create_publisher<std_msgs::msg::String>("topic_name", qos);
QoS 설정 예시 (Python)
qos = QoSProfile(depth=10) # 최신 10개의 메시지만 유지
publisher = node.create_publisher(String, "topic_name", qos)
QoS 프로파일은 다양한 통신 시나리오에 맞추어 네트워크 성능을 최적화할 수 있다.
노드의 실행 및 주기적 작업 관리
노드는 다양한 방식으로 실행될 수 있으며, 특히 주기적인 작업(예: 주기적으로 센서 데이터를 수집하거나, 주기적으로 데이터를 퍼블리싱하는 작업)을 관리하기 위해 ROS2의 타이머(Timer) 기능을 활용할 수 있다.
타이머(Timer) 생성
타이머는 노드가 일정한 시간 간격으로 작업을 수행할 수 있게 한다. 타이머는 주어진 주기마다 콜백 함수를 실행하여 노드가 특정 작업을 반복적으로 수행할 수 있게 해준다.
C++에서 타이머 생성
C++에서 타이머를 생성하는 방법은 다음과 같다.
auto timer = node->create_wall_timer(
std::chrono::milliseconds(500),
[]() {
RCLCPP_INFO(node->get_logger(), "Timer callback executed.");
});
위 코드는 500 밀리초마다 콜백 함수가 실행되는 타이머를 설정하는 예시이다. create_wall_timer()
는 시스템의 벽 시계 시간을 기준으로 타이머를 실행한다.
Python에서 타이머 생성
Python에서는 다음과 같이 타이머를 생성할 수 있다.
timer = node.create_timer(0.5, lambda: node.get_logger().info('Timer callback executed.'))
여기서 0.5
는 0.5초마다 타이머가 콜백 함수를 실행한다는 의미이다.
노드 간 통신 흐름 예시
주기적으로 데이터를 퍼블리시하는 노드와 이를 구독하는 노드 간의 상호작용은 아래와 같이 정리될 수 있다.
이 다이어그램은 퍼블리셔 노드가 정기적으로 데이터를 퍼블리시하고, 여러 서브스크라이버 노드가 이를 수신하는 통신 흐름을 나타낸다.
노드의 상태 모니터링
ROS2 시스템 내에서 노드의 상태를 모니터링하는 것은 매우 중요하다. 이를 위해 ROS2는 다양한 도구와 명령어를 제공하며, 노드의 현재 상태, 토픽, 서비스 등의 상태를 실시간으로 확인할 수 있다.
노드 상태 확인 명령어
- 노드 목록 확인: 현재 실행 중인 모든 노드를 확인하는 명령어는 다음과 같다.
bash
ros2 node list
- 노드 정보 확인: 특정 노드에 대한 정보를 확인하는 명령어는 다음과 같다.
bash
ros2 node info <node_name>
멀티 노드 시스템에서의 관리
ROS2 시스템은 분산된 노드들로 구성될 수 있으며, 멀티 노드 시스템에서의 관리가 필요하다. 각 노드는 독립적인 프로세스로 실행되며, 이를 효율적으로 관리하기 위한 다양한 기능이 제공된다.
멀티 노드 관리 예시
멀티 노드 시스템에서 중요한 요소는 노드 간의 네임스페이스를 잘 관리하는 것과, 노드가 적절한 리소스를 사용할 수 있도록 통신의 우선순위나 QoS를 잘 설정하는 것이다.
예를 들어, 하나의 로봇 시스템에서 두 개의 센서 노드가 독립적으로 데이터 수집을 수행하고, 이를 하나의 중앙 노드가 처리한다고 가정해 봅시다.
예외 처리와 에러 핸들링
ROS2 노드를 생성하고 관리하는 과정에서 발생할 수 있는 예외와 오류를 처리하는 것은 안정적인 시스템 운영을 위해 매우 중요하다. 예를 들어, 노드가 특정 리소스에 접근하지 못할 경우나, 네트워크 상의 통신 문제가 발생할 때 적절한 대처가 필요하다.
C++에서의 예외 처리
C++에서는 try-catch
구문을 사용하여 예외를 처리할 수 있다.
try {
// 노드 실행 코드
} catch (const std::exception &e) {
RCLCPP_ERROR(node->get_logger(), "Exception: %s", e.what());
}
Python에서의 예외 처리
Python에서도 try-except
구문을 통해 예외 처리를 할 수 있다.
try:
# 노드 실행 코드
except Exception as e:
node.get_logger().error(f"Exception: {str(e)}")