프로그래밍/ROS1

[rospy tutorial] led Tutorial 코드 해석

Se-chan Oh 2021. 7. 12. 20:59

1. 개발환경 및 서론

 

필자는 rpi 3 B+에 우분투 서버 20.04와 ROS noetic을 설치했다.

https://upgrade-sechan.tistory.com/23

 

[ROS] rpi에 ROS noetic 설치하기

1. 개발환경 개발환경은 이전에 포스팅했던 내용과 같다. 필자는 rpi 3 B+, 우분투 서버 20.04의 개발환경을 사용한다. https://upgrade-sechan.tistory.com/22 [ROS] rpi에 Ubuntu server 20.04 설치 & 와이파이..

upgrade-sechan.tistory.com

 

또한 이번 포스팅은 아래의 링크에서 다루는 Tutorial에 사용된 소스코드를 해석하는 글이다.

https://upgrade-sechan.tistory.com/24

 

[ROS noetic] led Tutorial 무작정 따라하기

1. 개발환경 개발환경은 이전에 포스팅했던 내용과 같다. 필자는 rpi 3 B+에 우분투 서버 20.04와 ROS noetic을 설치했다. catkin 설치와 gpio 권한설정 등을 이전 포스팅에서 다루었다. https://upgrade-sechan.t.

upgrade-sechan.tistory.com

 

2. 소스코드 해석

 

led Tutorial에서는 rospy를 이용하여 프로그래밍했다.

필자도 rospy를 공부하기 시작하는 단계이므로 이해가 안 되는 것은 마찬가지다.

그냥 그런가보다 하면서 읽어주기를 바란다.

 

1) ROS Service Server

 

서버부터 시작해보자. 서버의 역할은 클라이언트로부터 요청(request)를 수령하여 led를 제어하는 것이다.

우리는 제일 처음에 ~/catkin_ws/src 디렉터리 안에 rpi_ros_tutorials 패키지를 만들었다.

패키지 안에는 led_service_server.py 파일을 생성했는데, 그 내용은 다음과 같다.

#!/usr/bin/env python
import rospy
from std_srvs.srv import SetBool
import RPi.GPIO as GPIO
LED_GPIO = 20
def set_led_state_callback(req):
    GPIO.output(LED_GPIO, req.data)
    return { 'success': True,
            'message': 'Successfully changed LED state' }
if __name__ == '__main__':
    rospy.init_node('led_actuator')
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(LED_GPIO, GPIO.OUT)
    rospy.Service('set_led_state', SetBool, set_led_state_callback)
    rospy.loginfo("Service server started. Ready to get requests.")
    rospy.spin()
    GPIO.cleanup()

이제 소스코드를 분석해보자.

 

1-1) import와 gpio 설정

 

#!/usr/bin/env python

import rospy
from std_srvs.srv import SetBool
import RPi.GPIO as GPIO

LED_GPIO = 20 # BCM 기준으로 20번 핀을 led 제어에 사용한다.

 

#!/usr/bin/env python 요건 파이썬 파일의 가장 윗부분에 항상 있어야 한다.

rospy를 사용하기 위해 rospy를 import 한다.

led를 끄고 켜기 위해서 Bool 자료형을 사용한다. 따라서 SetBool을 import 한다.

GPIO를 사용하기 위해 RPi.GPIO을 import 한다.

 

if __name__ == '__main__':
    rospy.init_node('led_actuator') # 이름이 led_actuator인 노드를 초기화

    GPIO.setmode(GPIO.BCM) # 핀 번호를 BCM 기준으로 사용한다.
    GPIO.setup(LED_GPIO, GPIO.OUT) # LED_GPIO 핀을 OUTPUT으로 사용한다.

 

rospy.init_node에서 프로그램을 처음 시작할 때 노드를 초기화한다.

GPIO.setup에서 LED_GPIO핀을 OUTPUT 핀으로 사용한다.

 

1-2) 서비스 서버 초기화

 

    rospy.Service('set_led_state', SetBool, set_led_state_callback) # 서비스 서버 생성
    rospy.loginfo("Service server started. Ready to get requests.") # 터미널에 출력할 텍스트 작성

 

이제 rospy.Service를 이욯해서 서비스 서버를 생성할 수 있다. 이 함수는 3개의 인수가 필요하다.

- 서비스의 이름 : 클라이언트에서는 이 이름을 이용하여 서버에 접속한다.

- 서비스 정의 : 클라이언트와 서버 사이의 정보 교환에 사용되는 자료형을 정의한다.

- callback 함수 : 서버가 클라이언트의 요청을 수령할 때 실행할 callback 함수가 필요하다.

rospy.loginfo는 노드를 실행할 때 터미널에 출력할 텍스트를 작성한다.

 

    rospy.spin() # 프로그램 중지
    GPIO.cleanup() # 핀을 기본 설정으로 복원

 

rospy.spin은 ctrl+c로 종료할 때까지 프로그램을 중지시킨다. 무슨말인지 모르겠어서 이 라인을 주석처리하고 실행시켰더니 파이썬 파일을 실행하자마자 종료됐다.

GPIO.cleanup은 프로그램에서 사용한 핀을 기본 설정으로 복원한다. 이 프로그램에서는 20번 핀을 output으로 설정했으므로 기본 설정인 input으로 복원할 것이다. output으로 설정한 상태로 방치한다면 보드에 손상이 갈 위험이 높기 때문에 이 작업이 필요하다.

 

1-3) 서비스 서버 콜백

 

def set_led_state_callback(req):
    GPIO.output(LED_GPIO, req.data) # Bool 값을 받아서 led에 출력한다.
    return { 'success': True,
        'message': 'Successfully changed LED state' }

 

클라이언트가 서버에 요청을 보낼 때마다 이 함수가 호출된다. 여기서 클라이언트의 요청을 파라미터(여기서는 req가 파라미터이다.)를 기반으로 처리한다.

SetBool definition을 살펴보면 data 필드 밖에 존재하지 않으므로 "req.data"를 이용해서 led를 제어할 수 있다.

클라이언트는 서버의 응답을 기다리기 때문에 return문을 작성한다. dictionary 자료형을 return하는데, "success"는 boolean, "message"는 string 이렇게 2가지 필드로 처리한다.

 

2) ROS Service Client

 

클라이언트의 역할은 버튼이 눌렸는지 체크하고 서버에게 요청을 보내는 것이다.

rpi_ros_tutorials 패키지 안에 button_service_client.py 파일을 생성했는데, 그 내용은 다음과 같다.

#!/usr/bin/env python
import rospy
from std_srvs.srv import SetBool
import RPi.GPIO as GPIO
BUTTON_GPIO = 16
def button_callback(channel):
    power_on_led = not GPIO.input(BUTTON_GPIO)
    rospy.wait_for_service('set_led_state')
    try:
        set_led_state = rospy.ServiceProxy('set_led_state', SetBool)
        resp = set_led_state(power_on_led)
    except rospy.ServiceException as e:
        rospy.logwarn(e)
if __name__ == '__main__':
    rospy.init_node('button_monitor')
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_GPIO, GPIO.IN, pull_up_down = GPIO.PUD_UP)
    GPIO.add_event_detect(BUTTON_GPIO, GPIO.BOTH,
            callback=button_callback, bouncetime=50)
    rospy.spin()
    GPIO.cleanup()

이제 소스코드를 분석해보자.

 

2-1) import와 gpio 설정

 

#!/usr/bin/env python

import rospy
from std_srvs.srv import SetBool
import RPi.GPIO as GPIO

BUTTON_GPIO = 16 # BCM 기준으로 16번 핀을 버튼 input에 사용한다.

 

서버에서와 똑같이 import한다. 특히 서버와 클라이언트가 통신하기 위해 같은 자료형을 사용해야 하기 때문에 SetBool을 imoport한다.

 

if __name__ == '__main__':
    rospy.init_node('button_monitor') # 이름이 button_monitor인 노드를 초기화

    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_GPIO, GPIO.IN, pull_up_down = GPIO.PUD_UP) # 내부적으로 pull up 저항을 사용한다.

 

GPIO.setup에서 BUTTON_GPIO를 input으로 설정하고 내부 pull up 저항을 사용하도록 설정한다. 따라서 버튼을 누르면 LOW, 안 누르면 HIGH로 인식한다.

 

    GPIO.add_event_detect(BUTTON_GPIO, GPIO.BOTH,
        callback=button_callback, bouncetime=50)

 

버튼이 low에서 high로 rising할 때와 반대로 falling할 때 인터럽트를 설정한다. 따라서 버튼을 누르거나 떼면 callback 함수가 호출된다.

 

    rospy.spin()
    GPIO.cleanup()

 

서버에서와 같이 프로그램 중지 및 핀 기본설정 복원을 해준다.

 

2-2) 클라이언트에서 서버 호출하기

GPIO 인터럽트 callback 함수가 호출되면 다음과 같은 일이 일어난다.

 

def button_callback(channel):
    power_on_led = not GPIO.input(BUTTON_GPIO)

    rospy.wait_for_service('set_led_state')
    try:
        set_led_state = rospy.ServiceProxy('set_led_state', SetBool)
        resp = set_led_state(power_on_led)
    except rospy.ServiceException as e:
        rospy.logwarn(e)

 

이 코드를 조금 더 자세히 뜯어보자.

 

def button_callback(channel):
    power_on_led = not GPIO.input(BUTTON_GPIO)

 

임의로 생성한 power_on_led 변수에 버튼의 상태를 저장한다.

 

    rospy.wait_for_service('set_led_state')

 

set_led_state 노드(앞에서 우리가 작성한 서비스 서버)가 활성화 되기를 기다린다.

 

    try:
        set_led_state = rospy.ServiceProxy('set_led_state', SetBool)

 

try문에서는 rospy.ServiceProxy를 통해 클라이언트를 생성할 수 있다. 이때 2개의 인자가 필요하다.

- 서비스 이름 : 서버에서 정의한 서버 이름과 같아야 한다.

- 서비스 정의 : 서버에서 정의한 자료형과 같아야 한다.

 

        resp = set_led_state(power_on_led)

 

그러면 클라이언트는 서버에게 요청을 보내고 응답을 받을 수 있다. 요청에는 우리가 서버에서 정의했던 것처럼 Bollean 자료형만 매개 변수로 주어지면 된다.

resp 변수는 임의로 생성한 것이고, 이 변수는 set_led_state 서버가 보내는 응답을 저장한다. 이 응답은 try문 끝에 "rospy.loginfo(resp)" 를 추가하면 터미널에서 그 내용을 확인할 수 있다.

 

    except rospy.ServiceException as e:
        rospy.logwarn(e)

 

try문에서 서비스 호출이 실패할 경우 except문이 실행된다.

rospy.logwarn 요녀석은 느낌상 어딘가에 로그를 남기는 것 같은데 정확히 뭔지 모르겠다. 딱히 실행될 일이 없으므로 그냥 넘어가도록 하겠다.

 

3) Test the application

roscore

rosrun rpi_ros_tutorials led_service_server.py

rosrun rpi_ros_tutorials button_service_client.py

위 명령어를 각각 다른 터미널에서 실행하여 테스트를 진행한다.

이때 ROS가 제대로 동작하는지 확인하기 위해 아래의 작업을 수행한다.

 

3-1) ROS에서 실행 중인 노드를 확인한다.

rosnode list

위 명령어를 실행하면 아래의 결과가 나온다.

 

/button_monitor
/led_actuator
/rosout

 

3-2) 서비스 리스트를 확인한다.

rosservice list

위 명령어를 실행하면 아래의 결과가 나온다.

 

/button_monitor/get_loggers
/button_monitor/set_logger_level
/led_actuator/get_loggers
/led_actuator/set_logger_level
/rosout/get_loggers
/rosout/set_logger_level
/set_led_state

 

3-3) 서비스 디버깅 하기

뭔가 잘 실행이 되지 않는다면 아래의 절차를 통해 디버깅하기를 바란다.

- 먼저 하드웨어가 잘 연결되었는지 확인한다. 두 번 확인한다. 세 번 확인한다.

- 하드웨어가 정상이라면 클라이언트는 실행하지 않고 서버만 실행한 다음 아래의 명령어를 터미널에서 실행한다.

rosservice call /set_led_state "data: true"
success: True
message: "Successfully changed LED state"
rosservice call /set_led_state "data: false"
success: True
message: "Successfully changed LED state"

 

- led가 켜지지 않는다면 서버에 문제가 있는 것이고, led가 켜진다면 클라이언트에 문제가 있는 것이다.

- 그런데 소스코드를 복사/붙여넣기 했다면 웬만해서는 하드웨어 문제이다.