본문으로 바로가기

ROS2 사용자 정의 메세지 만들기 #3

category 강좌/ROS2 2026. 5. 9. 22:30
728x90
728x90

이번 글에서는 ROS 2 서비스 구조를 조금 더 확장해서, 사용자가 원하는 개수만큼 turtlesim 거북이를 생성하고 원 모양으로 배치하는 예제를 정리해 보겠습니다.

 

이전 단계에서는 사용자 정의 서비스 MultiSpawn.srv를 만들고, /multi_spawn 서비스를 호출하면 서버가 응답을 반환하는 구조까지 확인했습니다. 이번에는 그 서비스 서버 내부에서 turtlesim의 기본 서비스인 /spawn을 호출하여 실제로 여러 거북이를 생성하도록 구현합니다.

 

이번 예제의 목표는 다음과 같습니다.

1. /multi_spawn 서비스를 실행합니다.
2. 사용자가 num 값을 전달합니다.
3. 서버는 num 개수만큼 좌표를 계산합니다.
4. 계산된 좌표를 기준으로 turtlesim 거북이를 생성합니다.
5. 생성 위치는 원 둘레 형태로 배치합니다.
6. 서비스 응답으로 x, y, theta 배열을 반환합니다.
 

 

예를 들어 다음 명령을 실행하면,

 

ros2 service call /multi_spawn my_first_package_msgs/srv/MultiSpawn "{num: 9}"

 

 

turtlesim 화면에 9개의 거북이가 원형에 가깝게 배치됩니다.

 

 

 

1. 원형 배치 좌표 계산 원리

 

여러 거북이를 원형으로 배치하려면 각 거북이의 위치 좌표를 계산해야 합니다.

원 위의 점은 삼각함수로 계산할 수 있습니다.

x = r × cos(theta)
y = r × sin(theta)
 

 

여기서 의미는 다음과 같습니다.

r      → 원의 반지름
theta  → 각도
x, y   → 원 위의 좌표
 

 

예를 들어 4개의 점을 원 위에 배치한다면 각도는 90도씩 떨어집니다.

0도
90도
180도
270도
 

 

하지만 Python의 numpy.sin(), numpy.cos() 함수는 degree가 아니라 radian을 사용합니다. 따라서 각도는 다음처럼 계산합니다.

 
gap_theta = 2 * np.pi / n
 

 

여기서 2 * np.pi는 한 바퀴, 즉 360도에 해당하는 라디안 값입니다.

 

 

 

2. 좌표 계산 함수 만들기

 

서비스 서버 안에 좌표 계산 함수를 추가합니다.

 

def calc_position(self, n, r):
    gap_theta = 2 * np.pi / n
    theta = [gap_theta * n for n in range(n)]
    x = [r * np.cos(th) for th in theta]
    y = [r * np.sin(th) for th in theta]

    return x, y, theta
 
 

이 함수는 다음 값을 반환합니다.

x      → 각 거북이의 x 좌표 배열
y      → 각 거북이의 y 좌표 배열
theta  → 각 거북이의 방향 배열
 

 

예를 들어 n=4, r=3이면 중심 기준으로 원 위에 4개의 좌표가 생성됩니다.

 

 

3. turtlesim 화면 중심 보정

 

turtlesim 화면의 좌표는 일반적인 수학 좌표계처럼 (0, 0)이 화면 중앙에 있지 않습니다.

 

기본 거북이는 보통 다음 근처에 생성됩니다.

x = 5.54
y = 5.54
 

 

따라서 원형 좌표를 그대로 사용하면 일부 거북이가 화면 밖으로 나갈 수 있습니다.

 

이를 보정하기 위해 중심 좌표를 추가합니다.

 
self.center_x = 5.54
self.center_y = 5.54
 

 

 

그리고 실제 spawn 요청을 보낼 때는 계산된 좌표에 중심 좌표를 더합니다.

 

 

self.req_spawn.x = x[n] + self.center_x
self.req_spawn.y = y[n] + self.center_y

 

이렇게 하면 원의 중심이 turtlesim 화면 중앙 근처로 이동합니다.

 

 

 

4. Spawn 서비스 import

 

여러 거북이를 생성하려면 turtlesim.srv.Spawn 서비스를 사용해야 합니다.

 

 

from turtlesim.srv import Spawn

 

 

 

 

그리고 numpy도 사용하므로 함께 import 합니다.

 

import numpy as np

 

 

 

5. Spawn 서비스 클라이언트 생성

 

__init__() 내부에 /spawn 서비스 클라이언트를 생성합니다.

 

self.spawn = self.create_client(Spawn, '/spawn')

 

 

 

요청 객체도 미리 만들어 둡니다.

 

self.req_spawn = Spawn.Request()

 

 

 

기존에 사용했던 TeleportAbsolute 클라이언트도 함께 둘 수 있습니다.

 

self.teleport = self.create_client(
    TeleportAbsolute,
    '/turtle1/teleport_absolute'
)
self.req_teleport = TeleportAbsolute.Request()

 

 

 

 

 

6. callback_service에서 여러 거북이 생성하기

 

/multi_spawn 서비스가 호출되면 callback_service()가 실행됩니다.

 

여기서 사용자가 요청한 num 값을 기준으로 좌표를 계산하고, 반복문으로 /spawn 서비스를 호출합니다.

 

 

def callback_service(self, request, response):
    x, y, theta = self.calc_position(request.num, 3)

    for n in range(len(theta)):
        self.req_spawn.x = x[n] + self.center_x
        self.req_spawn.y = y[n] + self.center_y
        self.req_spawn.theta = theta[n]
        self.spawn.call_async(self.req_spawn)

    response.x = x
    response.y = y
    response.theta = theta

    return response

 

 

 

 

여기서 request.num은 사용자가 서비스 호출 시 넘긴 값입니다.

 
"{num: 9}"
 

 

즉, 위 명령을 실행하면 request.num에는 9가 들어갑니다.

 

 

 

7. 최종 my_service_server.py 코드

 

아래는 원형 배치까지 포함한 최종 코드입니다.

 

from my_first_package_msgs.srv import MultiSpawn
from turtlesim.srv import TeleportAbsolute
from turtlesim.srv import Spawn

import rclpy as rp
import numpy as np
from rclpy.node import Node


class MultiSpawning(Node):

    def __init__(self):
        super().__init__('multi_spawn')

        self.server = self.create_service(
            MultiSpawn,
            'multi_spawn',
            self.callback_service
        )

        self.teleport = self.create_client(
            TeleportAbsolute,
            '/turtle1/teleport_absolute'
        )

        self.spawn = self.create_client(
            Spawn,
            '/spawn'
        )

        self.req_teleport = TeleportAbsolute.Request()
        self.req_spawn = Spawn.Request()

        self.center_x = 5.54
        self.center_y = 5.54

    def calc_position(self, n, r):
        gap_theta = 2 * np.pi / n
        theta = [gap_theta * i for i in range(n)]
        x = [r * np.cos(th) for th in theta]
        y = [r * np.sin(th) for th in theta]

        return x, y, theta

    def callback_service(self, request, response):
        x, y, theta = self.calc_position(request.num, 3)

        for i in range(len(theta)):
            self.req_spawn.x = x[i] + self.center_x
            self.req_spawn.y = y[i] + self.center_y
            self.req_spawn.theta = theta[i]

            self.spawn.call_async(self.req_spawn)

        response.x = x
        response.y = y
        response.theta = theta

        return response


def main(args=None):
    rp.init(args=args)

    multi_spawn = MultiSpawning()
    rp.spin(multi_spawn)

    rp.shutdown()


if __name__ == '__main__':
    main()

 

 

 

8. 빌드하기

 

코드를 수정했으므로 다시 빌드합니다.

 

cd ~/ros2_study
colcon build

 

 

 

또는 수정한 Python 패키지만 빌드할 수도 있습니다. 정상적으로 실행되지 않을 경우 워크스페이스 전체를 다시 빌드해야 합니다.

 

colcon build --packages-select my_first_package

 

 

빌드 후에는 환경을 다시 적용합니다.

 

source install/local_setup.bash

또는

sl

 

 

 

 

 

9. 실행하기

 

터미널 1

 

sl
ros2 run turtlesim turtlesim_node

 

 

터미널 2

 

sl
ros2 run my_first_package my_service_server

 

 

 

터미널 3

 

sl
ros2 service call /multi_spawn my_first_package_msgs/srv/MultiSpawn "{num: 9}"

 

 

 

 

 

요청 개수를 15로 바꾸면 더 많은 거북이를 생성할 수 있습니다.

turtlesim_node를 다시 실행하고 아래의 명령을 실행합니다.

ros2 service call /multi_spawn my_first_package_msgs/srv/MultiSpawn "{num: 15}"

 

만약 15개의 거북이가 새롭게 생성되지 않고 11개 정도만 생성되고 종료하면 거북이 생성을 너무 빠르게 했기 때문입니다. 거북이를 생성할 때 100ms 지연시간  주면 이 문제는 해결됩니다.

 

아래와 같이 소스를 수정한 후에 다시 빌드를 하고 위의 실행 과정을 반복하시기 바랍니다.

 

먼저 time을 import 합니다.

 

import time

 

 

 

self.spawn.call_async(self.req_spawn)를 호출한 후에 아래와 같이 시간지연 코드를 추가합니다.

 

time.sleep(0.1)

 

 

 

다시 빌드하고 turtlesim_node를 실행하고 서버 서비스를 실행합니다. num : 15를 입력하여 15개 거북이가 정상적으로 생성되는지 여부를 확인합니다.

 

 

 

 

 

 

10. 실행 결과

 

정상적으로 실행되면 turtlesim 화면에 여러 개의 거북이가 원형에 가깝게 배치됩니다.

 

서비스 호출 터미널에는 응답 값이 출력됩니다.

response:
my_first_package_msgs.srv.MultiSpawn_Response(
  x=[...],
  y=[...],
  theta=[...]
)
 

 

이 값은 서버가 계산한 원형 배치 좌표입니다.

 

주의할 점은 response.x, response.y는 중심 보정 전의 좌표입니다.

 

실제 turtlesim에 전달되는 좌표는 다음처럼 중심 보정이 적용됩니다.

 
self.req_spawn.x = x[i] + self.center_x
self.req_spawn.y = y[i] + self.center_y

 

 

 

 

11. 왜 call_async를 사용하는가요?

 

서비스 클라이언트를 호출할 때 다음 코드를 사용했습니다.

 
self.spawn.call_async(self.req_spawn)
 

 

call_async()는 비동기 방식으로 서비스를 요청합니다.

즉, 요청을 보내고 응답을 기다리는 동안 노드 전체가 멈추지 않습니다.

 

여러 개의 거북이를 생성할 때는 반복문 안에서 서비스를 여러 번 호출해야 하므로, 비동기 호출 구조가 더 자연스럽습니다.

 

다만 실무에서는 다음 사항도 고려해야 합니다.

1. /spawn 서비스가 준비되었는지 확인
2. 요청마다 서로 다른 이름을 지정할지 결정
3. 응답 성공 여부 확인
4. 서비스 호출 간 간격 조절
5. 중복 위치 또는 화면 밖 좌표 방지
 

 

현재 예제는 학습용이므로 단순하게 작성되어 있습니다.

 

 

 

 

12. 개선 코드: 서비스 준비 상태 확인하기

 

실무 코드라면 /spawn 서비스가 준비될 때까지 기다리는 처리가 있는 것이 좋습니다.

 

while not self.spawn.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('/spawn service not available, waiting...')

 

 

이를 __init__() 안에 넣으면 turtlesim이 아직 실행되지 않았을 때도 원인을 쉽게 확인할 수 있습니다.

 

예시는 다음과 같습니다.

 

self.spawn = self.create_client(Spawn, '/spawn')

while not self.spawn.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('/spawn service not available, waiting...')

 

 

이렇게 하면 /spawn 서비스가 준비되지 않은 상태에서 무작정 호출하는 문제를 줄일 수 있습니다.

 

 

 

13. 개선 코드: 요청 값 검증하기

 

사용자가 num에 0이나 음수를 넣으면 좌표 계산에서 문제가 생깁니다.

 

gap_theta = 2 * np.pi / n
 

 

여기서 n=0이면 0으로 나누는 오류가 발생합니다.

 

따라서 서비스 콜백에서 최솟값을 확인하는 것이 좋습니다.

 

if request.num <= 0:
    response.x = []
    response.y = []
    response.theta = []
    return response

 

 

 

 

또한 너무 큰 숫자를 넣으면 거북이가 너무 많이 생성되어 화면이 복잡해질 수 있습니다.

 

if request.num > 20:
    request.num = 20

 

 

 

학습용 예제에서는 3개, 8개, 15개 정도가 적당합니다.

 

 

 

14. 개선 코드: 반지름 조절하기

 

현재 반지름은 고정값입니다.

 

x, y, theta = self.calc_position(request.num, 3)

 

하지만 거북이 개수가 많아질수록 원의 반지름을 조절하면 더 보기 좋습니다.

 

예를 들어 다음처럼 작성할 수 있습니다.

 

radius = 3.0

if request.num <= 4:
    radius = 2.0
elif request.num <= 10:
    radius = 3.0
else:
    radius = 4.0

 

 

 

다만 turtlesim 화면 범위를 벗어나지 않도록 주의하셔야 합니다.

 

 

 

 

15. Jupyter Notebook으로 좌표 먼저 검증하기

 

로봇 코드는 실행 환경, 빌드, 노드 실행 등이 얽혀 있기 때문에 알고리즘 검증은 Python에서 먼저 끝내는 것이 효율적입니다.

 

예를 들어 다음 코드를 Jupyter에서 실행해 볼 수 있습니다.

 

 

import numpy as np

n = 3
to_degree = 180 / np.pi

gap_theta = 2 * np.pi / n
gap_theta * to_degree

 

 

결과는 대략 다음과 같습니다.

120.0
 

 

즉, 3개를 원에 배치하면 각 점의 간격은 120도입니다.

 

 

 

 

16. 각도 리스트 만들기

 

theta = [gap_theta * n for n in range(n)]
[each * to_degree for each in theta]

 

 

예를 들어 n=3이면 다음과 같은 각도가 만들어집니다.

0도
120도
240도
 
 

 

Python 코드에서는 degree가 아니라 radian 값이 저장됩니다.

0
2.094...
4.188...
 

 

이 값들이 np.cos(), np.sin()에 들어갑니다.

 

 

 

17. x, y 좌표 생성하기

 

r = 3

x = [r * np.cos(th) for th in theta]
y = [r * np.sin(th) for th in theta]

 

 

3개의 점이라면 대략 다음과 같은 좌표가 나옵니다.

x = [3.0, -1.5, -1.5]
y = [0.0, 2.59, -2.59]
 

 

 

즉, 중심 기준으로 오른쪽, 왼쪽 위, 왼쪽 아래에 점이 생깁니다.

 

 

 

 

18. matplotlib으로 좌표 확인하기

 

좌표가 제대로 만들어졌는지 확인하려면 산점도로 그려보면 됩니다.

 

import matplotlib.pyplot as plt

plt.scatter(x, y)
plt.axis('equal')
plt.show()

 

 

여기서 중요한 코드는 다음입니다.

 
plt.axis('equal')
 

 

이 코드는 x축과 y축의 스케일을 같게 맞춰 줍니다.


이 설정이 없으면 원형 배치가 타원처럼 보일 수 있습니다.

 

 

 

 

 

19. 좌표 계산 함수화

 

Jupyter에서 검증한 내용을 함수로 정리하면 다음과 같습니다.

 

def calc_position(n, r):
    gap_theta = 2 * np.pi / n
    theta = [gap_theta * i for i in range(n)]
    x = [r * np.cos(th) for th in theta]
    y = [r * np.sin(th) for th in theta]

    return x, y, theta

 

 

 

시각화 함수도 따로 만들 수 있습니다.

 

 

def draw_pos(x, y):
    plt.scatter(x, y)
    plt.axis('equal')
    plt.show()

 

 

 

테스트는 다음처럼 할 수 있습니다.

 

x, y, theta = calc_position(4, 3)
draw_pos(x, y)

 

 

 

15개도 테스트할 수 있습니다.

 

x, y, theta = calc_position(15, 3)
draw_pos(x, y)

 

 

 

이렇게 하면 ROS에 사용하기 전에 알고리즘이 맞는지 빠르게 확인할 수 있습니다.

 

 

 

20. 전체 흐름 정리

 

이번 예제의 전체 구조는 다음과 같습니다.

MultiSpawn.srv
  request:
    int64 num

  response:
    float64[] x
    float64[] y
    float64[] theta
 
my_service_server.py
  /multi_spawn 서비스 서버 생성
  /spawn 서비스 클라이언트 생성
  request.num 개수만큼 좌표 계산
  /spawn 서비스 반복 호출
  계산 결과를 response로 반환
 

 

실행 흐름은 다음과 같습니다.

ros2 run turtlesim turtlesim_node
ros2 run my_first_package my_service_server
ros2 service call /multi_spawn my_first_package_msgs/srv/MultiSpawn "{num: 9}"

 

 

 

21. 자주 발생하는 문제

 

a. 거북이가 생성되지 않는 경우

 

가장 먼저 turtlesim_node가 실행 중인지 확인하셔야 합니다.

 
ros2 service list
 

 

목록에 다음 서비스가 있어야 합니다.

/spawn
/turtle1/teleport_absolute
 

없다면 turtlesim_node가 실행되지 않은 것입니다.

 

 

b. /multi_spawn 서비스가 보이지 않는 경우

 

서비스 서버가 실행 중인지 확인합니다.

 
ros2 service list -t
 

 

다음 항목이 보여야 합니다.

/multi_spawn [my_first_package_msgs/srv/MultiSpawn]
 

 

보이지 않는다면 아래를 확인하십시오.

1. my_service_server.py 파일이 있는지
2. setup.py entry_points에 등록했는지
3. colcon build를 다시 했는지
4. source install/local_setup.bash를 다시 실행했는지
 

 

 

c. numpy import 오류

 

다음 오류가 날 수 있습니다.

ModuleNotFoundError: No module named 'numpy'
 

 

이 경우 numpy를 설치합니다.

 
pip3 install numpy
 

 

또는 Ubuntu 환경에서는 다음 명령도 사용할 수 있습니다.

 
sudo apt install python3-numpy
 

 

 

 

d. num 값이 너무 큰 경우

 

turtlesim은 간단한 시뮬레이터입니다.
너무 많은 turtle을 생성하면 화면이 복잡해지고 서비스 호출이 느려질 수 있습니다.

 

실습에서는 다음 정도를 권장합니다.

3개
5개
8개
9개
15개

 

 

 

 

22. 실무 관점에서의 의미

 

이번 예제는 단순히 거북이를 여러 개 만드는 실습이지만, 구조 자체는 실제 로봇 시스템과 연결됩니다.

 

예를 들어 다음과 같은 기능으로 확장할 수 있습니다.

여러 로봇의 초기 위치 배치
드론 군집 비행의 초기 포인트 생성
AGV 여러 대의 대기 위치 지정
시뮬레이션 환경에서 장애물 또는 목표물 자동 생성
경로점 waypoint 자동 생성
 

 

특히 request.num처럼 사용자가 원하는 개수를 입력하고, 서버가 내부 알고리즘으로 좌표를 계산해서 결과를 반환하는 구조는 실무에서도 자주 사용됩니다.

 

드론 예제로 바꾸면 다음과 같이 응용할 수 있습니다.

사용자 요청: waypoint 개수 8개
서버 처리: 원형 또는 격자형 waypoint 계산
응답: waypoint 좌표 배열 반환
추가 처리: PX4 offboard setpoint로 전달
 

 

즉, 이번 turtlesim 예제는 “서비스 요청 기반 자동 배치 알고리즘”의 기초라고 보시면 됩니다.

 

 

 

23. 정리

 

이번 글에서는 ROS 2 서비스 서버 안에서 또 다른 서비스 클라이언트를 호출하여 여러 개의 turtlesim 거북이를 원형으로 배치하는 방법을 정리했습니다.

 

핵심은 다음입니다.

서비스 정의는 srv 파일로 만듭니다.
서비스 서버는 요청을 받고 응답을 반환합니다.
서비스 서버 내부에서도 다른 서비스를 호출할 수 있습니다.
여러 개의 객체를 배치할 때는 삼각함수로 좌표를 계산할 수 있습니다.
ROS 코드에 넣기 전 Jupyter에서 알고리즘을 검증하면 개발 속도가 빨라집니다.
 

 

이번 예제까지 이해하시면 ROS 2에서 토픽, 메시지, 서비스, 서비스 클라이언트의 기본 흐름을 한 번에 연결해서 볼 수 있습니다.

 

실제 로봇이나 드론 시스템에서도 이 구조는 그대로 응용할 수 있습니다.

728x90
728x90