본 블로그 포스팅은 https://www.youtube.com/@pinklab_studio/playlists
에서 현재 강의 중에 있는
ROS2 무작정 따라하기강의의 내용을 필자가 다시 복기하여 기록하는 내용에 관한 것이다.
https://docs.ros.org/en/iron/p/rclpy/
공식 doc에서는 해당 라이브러리르 이렇게 표현하고 있다.
rclpy는 ROS2와 상호작용하기 위한 표준 Python API를 제공하는 라이브러리 이다
그러면 위 사진처럼 이전 챕터에서 임의로 만들어 놓은
python
폴더로 가서 vscode
를 실행해보자
cd python/
-> 파이썬 폴더로 이동
code .
그리고 임의의 예제파일 exam.ipynb
를 생성한 후
아래의 코드만 작성하자
import rclpy as np
여기까지 하면 에러가 뜬다. 이유는 무엇일까?
이렇게
vscode
를 실행하기 전에 사전에 터미널에서 humble
를 먼저 쳐 줘서 .bashrc
에 등록한 스크립트 쉘 구동 명령어를 쳐 주고 code .
명령어를 수행해야 한다.
까먹지 말자는 차원에서 intro에 넣었다.
참고로 공식 문서에서는
위 사진처럼 가장 기본적이면서도 중요한 기능에 대해 사용방법론을 설명하고 있으니 이 문서에서 나온 대로 코드수행 및 설명을 진행하겠다.
ROS의 Topic이 제공하는 기능 중 Message 수신에 해당하는 Subcription 기능에 대한 예제 코드를 만들어 보자
우선 사전 설정으로 turtlesim_node를 구동 후 해당 노드가 제공하는 기능 중 Publish하고 있는 토픽과 해당 토픽의 메세지명(타입)을 찾는다
내가 Subscribe 기능을 만들려고 하니 상대 노드는 Publish하고 있는 토픽을 찾아야 한다.
위 사진처럼 Publish하고 있는 토픽 리스트 중
turtle1/pose
토픽 정보를 수신할 것이며, 해당 토픽의 경우 메세지를 turtelsim/msg/Pose
타입으로 방송하고 있음을 확인했다
이를 Python코드로 구현하려면 아래와 같이 진행하면 된다.
위 사진처럼 메세지명(타입)이 [A]/[B]/[C] 이런식으로 쭉 이어져 나갈 텐데 특정 항목이
msg
, srv
, action
으로 되어 있을 것이다.
그러면
from [A].msg import [C]
이렇게 코드 입력을 해주면 된다. -> 지금 예제는from turtlesim.msg import Pose
으로 되어있다.
다음으로 수행하는 코드는
rp.init()
test_node = rp.create_node('sub_test')
으로 각 코드에 대한 설명은 공식문서를 통해 설명하겠다.
ROS 커뮤니케이션 기능을 초기화 해주는 코드로, 입력할 만한 인자값(Parm)은 domain_id 정도로 나중에 정말 복잡한 분산 처리 시스템에 여러 ROS시스템이 연결되어 있다면, 조정해 볼만한 코드이다.
딱히 건드리지 않고 그냥 rp.init()
만 수행한다.
노드를 생성하는 기능으로 필수 입력 Parm(인자값)은 생성하고자 하는 노드의 이름명(Node_name) 정도가 된다.
노드 생성에 따른 반환 값이 존재하니 test_node라는 변수로 받아들이며, 좀더 정확하게는 인스턴스화 된 클래스가 반환값이 된다.
아무튼 위 두 코드를 실행하고 터미널에서 활성화 중인 노드 리스트를 출력하면 Node_name 으로 입력한 sub_test
라는 노드가 리스트에 리스트업 되었음을 확인할 수 있다.
그리고 위 사진처럼 임의의 함수
callback
을 정의하고
create_subscription메서드 함수로 해당 함수를 호출하는 코드를 작성한다.
create_subscription 이 함수에 대한 공식문서 설명은 아래와 같다.
여기서 중요한 것은
msg_type
, topic
, callback
으로
수신 받아야 할 메세지 타입 : 여기서는 Pose
해당 메세지가 담겨져 있는 토픽 : 여기서는 /turtle1/pose
메세지를 수신한 뒤에 호출할 함수 : 여기서는 callback
이렇게 특정 토픽에 담겨있는 메세지를 받았을 때마다
해당 함수를 실행해라
라는 뜻으로 풀이할 수 있다.
이제 호출할 callback 함수에 대한 설명을 하자면
callback가 호출될 때에는 수신받은 메세지안에 포함된 모든 인자값 을 활용할 수 있다. 이전에 설명했듯이 수신받은 메세지
turtlesim/msg/Pose
의 메세지 구성은 x, y, theta, linear_velocity, angular_velocity 5가지가 존재하기에
이를 data.x
, data.y
, data.theta
형태로 받아와서 이를 print하는 기능만을 수행하고 있다.
def callback(data): print("/turtle/pose : ", data) #이거는 날것의 데이터 그냥 출력 print() print(f"X: {data.x:.2f}, Y: {data.y:.2f}, theta: {data.theta:.2f}")
test_node.create_subscription(Pose, '/turtle1/pose', callback, 10)
여기까지 수행 후 rqt_graph
를 진행하면 생성한 sub_test
노드가 정상적으로 turtelsim
노드가 발행중인 topic turtle1/pose
를 수신중인 상태로 도식화가 된다.
다음으로 rp.spin_once(test_node)
와 rp.spin(test_node)
코드 작성 및 수행 결과이다
위 함수들은 생성한 노드
test_node
가 메세지를 Scribe하는 기능까지 완료했으니 이 작업을 실행하라는 함수이다.
이때 spin_once
는 한번만 작업을 실행하라
라는 뜻이고
spin
은 while 구문처럼 계속하여 실행하라는 뜻이다.
따라서 rp.spin(test_node)
구문은 작업의 설계도가 완성된 test_node의 작업 지시서에 따라 메세지를 수신할 때마다 callback함수의 print 작업을 계속 진행한다 이 뜻이 된다.
rp.spin_once(test_node)
rp.spin(test_node)
위 일련의 과정을 다 수행한 다음에는 아래의 코드를 작성해서 생성한 노드를 삭제한다.
test_node.destroy_node()
rp.shudown()
각각 생성한 노드의 소멸(destory_node()
), 초기화한 rclpy 기능의 종료(showdown()
)작업을 수행한다.
코드의 작성이 *.py
파일이 아닌 *.ipynb
로 수행되기에 코드 종료 절차를 꼭 써줘야 한다.
이번에는 Publish 기능을 수행하는 Node를 생성해 보자
turtelsim_node
를 구동한 상태에서 topic list 중 Scribe기능으로 동작하는 Topic를 선택하여 해당 Topic의 메세지명(타입)에 맞춰 코드를 작성하면 된다.
turtelsim_node
는 Scribe로 turtle1/cmd_vel
을 받을 수 있고, 해당 Topic는 geometry_msgs/msg/Twist
의 메세지명(타입)으로 구성되어 있으니 이에 맞춰
모듈을 import하는 과정을 수행한다.
import rclpy as rp
from geometry_msgs.msg import Twist
rp.init()
test_node = rp.create_node("pub_test")
다음으로 발행할 토픽의 메세지타입geometry_msgs/msg/Twist
의 구조를 알아내야 하는데
터미널에서 ros2 interface show ~~
명령을 통해서도 알아낼 수 있지만, 아래의 사진처럼 임의의 msg
라는 변수에 메세지 타입이 기록되어 있는 클래스 Twist()
를 인스턴스화 한 뒤,
msg
를 print하여 그 구조를 살펴보는 것이 더 빠르다
msg
는 향후 메세지 발행에도 사용되는 인스턴스이다.
msg = Twist() #메세지를 핸들링하는 msg를 Twist클래스로 인스턴스화
print(msg) #인스턴스화된 msg의 정보 확인
이제 인스턴스화 된
msg
라는 변수에 turtle1을 제어하기 위한 제어명령
msg.linear.x, mag.angular.z에 특정 값을 입력한다.
이렇게 제어명령에 해당하는 값을 입력하고 print하면
이전과는 다르게 값이 변동되어 msg에 저장됨을 알 수 있다.
위 사진처럼 코드를 비교한다면 msg.linear.x와 msg.angular.z에 해당하는 변수값이 각각 2.0, 1.8로 변경됨을 확인할 수 있다.
msg.linear.x = 2.0
msg.angular.z = 1.8
print(msg)
이제 msg에 변경된 값을 저장했으니
Node test_node
에 Publish 기능을 부여하는 create_publisher
메서드 함수를 붙여서
해당 함수가 필요로 하는 인자값
메세지 타입
, 메세지 명
을 붙여서 기능 선언(핸들링)을 한 뒤
pub.publish(msg)
코드를 통해
핸들러 pub
에 메서드 함수 publish, 해당함수가 필요로 하는 인자값(message -> msg)를 붙여 토픽을 발행한다.
위 두 코드에 대한 공식문서 설명은 아래와 같다.
그리고 여기까지만 수행하면 turtle이 topic를 딱 한번 받은것이기에
이렇게 반바퀴 쯤만 돌고 만다
그럼 이걸 계속 연속해서 구동하려면 어떻게 해야 할까?
https://docs.ros2.org/latest/api/rclpy/api/node.html
여기서는 Python rclpy 라이브러리 공식문서가 아닌
ROS2 rclpy 공식문서를 참조하자
위 문서에서 설명하는 Node가 지원하는 다양한 기능을 쭉 검색해보면
create_timer
라는 메서드 함수가 있다.
해당 함수의 인자값으로
timer_period_sec
callback
값을 넣어주면
선언한 노드에 timer
기능이 추가됨을 알 수 있다.
따라서 지금까지 test_node
에 Publish기능을 추가했으니
이제 timer기능을 추가하고 timer 기능이 동작 할 때마다 호출하는 callback 함수를 설계하면 된다.
이렇게 위 사진처럼
1) 콜백 함수 설계 -> 설계한 콜백함수에는 메세지 발행 기능만 넣음
2) test_node에 timer기능 추가
3) 추가된 기능에 맞춰 무한이 해당 기능을 반복하여 작업하는 spin 메서드 함수 실행
이렇게 3가지 구문을 작성해 publishing기능을 0.1초마다 반복하게 한다
def timer_callback():
pub.publish(msg)
timer_period = 0.1
timer = test_node.create_timer(timer_period, timer_callback)
rp.spin(test_node)
역시 위 과정을 모두 수행한 후 코드를 종료할 때는
test_node.destroy_node() #노드의 종료 함수
rp.shutdown()
을 꼭 수행한다.
위 Scribe, Publish 기능이 담긴 코드의 작성을 완료했다면
이를 응용하여 Service Client 코드도 설계하는 것이 가능하다.
이 과정에 대한 설명은 따로 하지 않으며, 완성된 코드만 기재하겠다.
import rclpy as rp
from turtlesim.srv import TeleportAbsolute
rp.init()
test_node = rp.create_node('srv_client_test')
service_name = '/turtle1/teleport_absolute'
client = test_node.create_client(TeleportAbsolute, service_name)
req = TeleportAbsolute.Request()
print(req)
req.x, req.y, req.theta = 2., 2., 3.14
print(req)
client.call_async(req)
rp.spin_once(test_node)
여기까지 수행한다면 터틀봇이 2, 2, 방향으로 이동 후 180도 회전된 방향을 쳐다볼 것이다.
이제 추가로
from std_srvs.srv import Empty
reset_cli = test_node.create_client(Empty, '/reset')
req_reset = Empty.Request()
print(req_reset)
reset_cli.call_async(req_reset)
rp.spin_once(test_node)
이 구문을 수행한다면 터틀봇이 제자리로 리셋 될 것이다.
위 전체 코드에서 설명할 만한 항목은 node.call_async()정도가 되겟다.
공식문서의 설명에 의하면 Request항목을 Parameter로 담아서 서비스 요청을 한 뒤 이를 '비동기'로 Response 한다 라고 이해하면 된다.
test_node.destroy_node()
rp.shutdown()
예제를 다 수행했다면 노드 종료 및 셧다운을 해주자
강의 후반부에는 위 Service Client의 실행을 기다렸다 수행하는 Node.wailt_for_service(timer) 기능에 대한 소개도 진행되었지만, 이거는 향후 Service Server / Client 때 좀 더 자세히 다뤄질 듯 하니 본 포스팅에서는 생략한다.