PinkLAB - 민형기 ROS2 기초 실습 4일차 - Python 코드작성

안상훈·2024년 5월 15일
0

ROS2 실습

목록 보기
4/13
post-thumbnail

본 블로그 포스팅은 https://www.youtube.com/@pinklab_studio/playlists
에서 현재 강의 중에 있는

ROS2 무작정 따라하기강의의 내용을 필자가 다시 복기하여 기록하는 내용에 관한 것이다.


1. rclpy

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에 넣었다.

참고로 공식 문서에서는
위 사진처럼 가장 기본적이면서도 중요한 기능에 대해 사용방법론을 설명하고 있으니 이 문서에서 나온 대로 코드수행 및 설명을 진행하겠다.


2. Subsciption 기능만들기

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로 수행되기에 코드 종료 절차를 꼭 써줘야 한다.


3. Publish 기능만들기

이번에는 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()

을 꼭 수행한다.


3. 서비스 클라이언트 설계하기

위 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 때 좀 더 자세히 다뤄질 듯 하니 본 포스팅에서는 생략한다.

profile
자율차 공부중

0개의 댓글

관련 채용 정보