ROS2의 데이터 모니터링

작성자

카테고리:

펌웨어를 하던 시절, 장비 안의 값 하나를 화면에서 보려면 일이 거기서부터 시작됐다. 프레임을 정하고, 양쪽에 송수신 코드를 넣고, 포트를 열고, 수신 인터럽트를 달고, 파서를 짰다. 정작 보고 싶었던 것은 좌표 하나였는데 시간의 대부분은 통신 코드에 들어갔다.

ROS2 에서 같은 일을 하면 명령 한 줄로 끝난다. 너무 간단해서 오히려 무슨 일이 벌어지는지 보이지 않는다. 이 글은 그 한 줄 뒤에서 무엇이 돌아가는지를, turtlesim 거북이의 X · Y 좌표를 읽는 예로 정리한다.

1. 무선조종기에서 프로토콜을 설계하던 흔적

2022년에 만든 AGV 무선조종기(무선조종기 개발의 추억)가 좋은 예다. 조종기에서 블루투스로 수신기까지 오고, 수신기(STM32F429)는 이더넷으로 B&R PLC 와 TCP/IP 로 붙는 구조였다. 당시 자료를 다시 보면 프로토콜을 설계한 흔적이 그대로 남아 있다.

  • PLC 가 수신기로 Command10 을 내리면 조종기 데이터를, Command11 을 내리면 와이어 엔코더 데이터(길이 mm, 각도 degree)를 올려 보낸다.
  • 조종기는 페어링 모드에서 5 바이트를 실시간으로 보내고, 수신기는 1 바이트마다 인터럽트로 개수를 세다가 5 바이트가 차면 수신 버퍼를 리셋한다.
  • LED 표시 하나를 바꾸려고 TCP 패킷 규약을 여러 번 새로 만들었다.

핵심은 데이터 종류마다 커맨드 번호를 사람이 배정했다는 점이다. 항목이 하나 늘면 번호를 정하고 문서를 고치고 양쪽 코드를 같이 수정해야 했다. 한쪽만 고친 채로 붙이면 값이 조용히 틀어졌다.

[그때]  조종기 ──BT──→ 수신기 ──TCP/IP──→ B&R PLC
                              ↑
                    Command10 = 조종기 데이터
                    Command11 = 와이어 데이터     ← 번호를 사람이 배정
                    Command12 = ...(추가할 때마다 양쪽 수정)

[ROS2]  /joystick   ← 토픽 이름이 곧 식별자
        /wire_encoder
        /turtle1/pose                              ← 추가해도 기존 코드 무수정

<그림 1> 커맨드 번호 배정과 토픽 이름

2. open · read · write · close 를 매번 만들었다

프로토콜만 설계한 것이 아니다. 그 아래 입출력 살림도 매번 직접 만들었다. 포트를 여는 함수, 읽는 함수, 쓰는 함수, 닫는 함수를 만들고, 수신 인터럽트 핸들러에서 한 바이트씩 받아 링버퍼에 넣고, 오버런과 프레임 동기를 처리했다.

/* 펌웨어 쪽 — 프로젝트마다 다시 만들던 것 */
int  uart_open (uint32_t baud);          /* 클럭, 핀, 보레이트, 패리티 설정 */
int  uart_read (uint8_t *buf, int len);  /* 링버퍼에서 꺼내 프레임 조립 */
int  uart_write(const uint8_t *buf, int len);
void uart_close(void);

void USART1_IRQHandler(void)             /* 1 바이트 수신 인터럽트 */
{
    uint8_t b = USART1->DR;
    ring_put(&rx_ring, b);
    if (++cnt == 5) { cnt = 0; frame_ready = 1; }   /* 5 바이트 프레임 */
}

물론 맨바닥부터 만든 것은 아니다. STM32 는 HAL 이 HAL_UART_Init(), HAL_UART_Receive_IT() 같은 드라이버와 인터럽트 콜백을 제공하고, 이더넷은 lwIP 가 TCP 스택을 올려 준다. 레지스터를 직접 두드릴 일은 없다. 그래도 남는 일이 있었다.

  • 클럭·핀·DMA 설정을 맞추고, 수신 콜백에서 링버퍼로 옮기는 코드
  • 바이트 흐름을 프레임으로 자르고 동기를 복구하는 코드
  • 소켓을 열고, 접속을 기다리고, 끊기면 다시 붙고, 살아 있는지 확인하는 코드
  • 그리고 무엇보다 어떤 값을 어떤 순서와 단위로 실을 것인가 — 프로토콜 설계

즉 HAL 이 없애 준 것은 드라이버 계층이고, ROS2 가 없애 주는 것은 그 위층이다. 위 목록이 통째로 사라진다. ROS2 에서 같은 일을 하는 코드는 이것이 전부다.

// ROS2 쪽 — 구독자 한 줄과 콜백 하나
subscription_ = this->create_subscription<turtlesim::msg::Pose>(
  "pose", 10, std::bind(&PoseMonitor::onPose, this, std::placeholders::_1));

void onPose(const turtlesim::msg::Pose::SharedPtr msg)
{
  RCLCPP_INFO(this->get_logger(), "x=%.3f  y=%.3f  theta=%.3f",
              msg->x, msg->y, msg->theta);
}
펌웨어에서 만들던 것 ROS2 에서
uart_open() / socket(), connect() — 포트·보레이트·IP 설정 create_subscription() 한 줄. 전송 설정은 QoS 값 하나
uart_read() — 링버퍼에서 꺼내 프레임 조립 콜백이 완성된 메시지로 호출된다
uart_write() — 송신 버퍼, DMA 큐 publish()
uart_close() — 자원 반납, 재접속 처리 노드 소멸자. 재연결은 디스커버리가 알아서 한다
수신 인터럽트 핸들러, 바이트 카운트, 동기 복구 실행기(executor)가 콜백을 대신 불러 준다
링버퍼 크기 결정, 오버런 처리 QoS depth (예: 10), RELIABLE / BEST_EFFORT
엔디안·구조체 패킹 합의 .msg 파일 → 직렬화 코드 자동 생성
★ 주. 규율까지 없어지는 것은 아니다. 수신 인터럽트 안에서 오래 걸리는 일을 하면 안 되는 것과 똑같이, 구독 콜백 안에서 블로킹 작업을 하면 그 실행기의 다른 콜백이 전부 밀린다. 자리만 옮겨졌을 뿐 같은 규칙이 그대로 적용된다.

3. ROS2 에서는 명령 한 줄이다

turtlesim 을 띄워 두고 다른 터미널에서 다음을 실행하면 좌표가 바로 흐른다.

$ ros2 topic echo /turtle1/pose

x: 7.549399852752686
y: 3.549400091171265
theta: 0.0
linear_velocity: 0.0
angular_velocity: 0.0
---

파서도, 포트 설정도, 체크섬도 쓰지 않았다. 중요한 것은 시뮬레이터 코드 어디에도 이 값을 화면에 찍는 코드가 없다는 점이다. 시뮬레이터는 발행(publish)만 하고, 출력은 ros2 topic echo 라는 별개의 프로세스가 한다. 발행하는 쪽은 누가 받는지조차 모른다.

4. 그 한 줄이 하는 일

  ros2 topic echo /turtle1/pose
        │
        ├ (1) 파이썬 플러그인 실행     ros2topic/verb/echo.py
        ├ (2) DDS 참가자 생성          UDP 소켓 7400 / 7414 / 7415 개방
        ├ (3) 디스커버리               "누가 /turtle1/pose 를 내는가"
        │        └ turtlesim 응답      "내가. 타입은 turtlesim/msg/Pose"
        ├ (4) 구독자 연결              같은 PC 이므로 공유메모리로 전송
        └ (5) 역직렬화 후 출력          24 바이트 → YAML

<그림 2> 명령 한 줄이 거치는 다섯 단계

(3)이 옛날에 사람이 하던 일의 대부분을 대신한다. 상대 IP 와 포트를 하드코딩하고 커맨드 번호를 배정하던 자리에, 토픽 이름과 타입이 모두 맞을 때만 연결한다는 규칙이 들어갔다. 이름이나 타입이 어긋나면 값이 틀어지는 것이 아니라 아예 연결되지 않는다. 조용히 틀어지던 옛 방식보다 이쪽이 낫다.

(2)의 포트는 도메인 ID 로 계산되므로 ROS_DOMAIN_ID 가 다르면 서로 보이지 않는다. (4)에서 같은 PC 안의 통신은 네트워크를 타지 않고 공유메모리로 오간다. 다른 PC 로 나갈 때만 UDP 가 된다.

5. 바이트는 사라지지 않았다

프레임이 없어진 것이 아니라 규약으로 고정되고 코드가 자동 생성될 뿐이다. Pose 메시지를 실제로 직렬화하면 24 바이트가 나온다.

00 01 00 00  AF 94 F1 40  5F 29 63 40  00 00 00 00  00 00 C0 3F  00 00 00 00
└─ 헤더 ──┘  └─ x ─────┘  └─ y ─────┘  └─ theta ─┘  └─ lin_vel ┘  └─ ang_vel ┘
                7.5494       3.5494         0.0          1.5          0.0

<그림 3> turtlesim/msg/Pose 의 실제 직렬화 결과 (CDR, 24 바이트)

앞 4 바이트가 CDR 캡슐화 헤더이고 00 01 이 리틀엔디안을 뜻한다. 엔디안 합의를 문서가 아니라 페이로드 안에 넣어 둔 것이다. 나머지는 float32 다섯 개가 그대로 나열된다. CRC 는 없다. 하위 계층에 맡긴다. 5 바이트를 세어 동기를 잡던 그 일을, 지금은 이 형식과 시퀀스 번호가 대신한다.

6. 모니터링에 쓰는 명령

ros2 topic echo /turtle1/pose --once     # 한 번만 보고 종료
ros2 topic echo /turtle1/pose --field x  # x 값만
ros2 topic echo /turtle1/pose --csv      # 7.549,3.549,0.0,...  로그 저장용
ros2 topic hz   /turtle1/pose            # 발행 주기
ros2 topic info /turtle1/pose -v         # 타입, 발행자·구독자, QoS 전부

오실로스코프 대신 --csv 로 받아 스프레드시트에 붙이면 그대로 그래프가 된다. hz 는 주기가 흔들리는지 보는 용도로, 실기에서 센서 노드가 밀리는지 확인할 때 먼저 친다. turtlesim 의 pose 는 약 62 Hz 로 나온다.

7. 좌표의 기준을 먼저 확인한다

값이 나온다고 해서 그 값의 기준까지 알려 주지는 않는다. turtlesim 의 좌표는 화면 왼쪽 아래가 원점인 0 ~ 11.10 m 범위이고, 거북이는 한가운데인 (5.5494, 5.5494) 에서 시작한다. 시작 위치를 원점으로 보고 싶다면 그 값을 빼야 한다.

ros2 topic echo /turtle1/pose --csv | \
  awk -F, '{printf "X:%7.2f  Y:%7.2f  각도:%7.1f\n", $1-5.5494, $2-5.5494, $3*57.2958}'
★ 주. theta 는 라디안이다. 도(°)로 보려면 57.2958 을 곱한다. 실제로 시뮬레이터 화면에 좌표를 표시하는 기능을 만들다가, 내부 좌표는 y 축이 화면 아래쪽으로 증가하는데 이를 그대로 쓰는 바람에 Y 부호가 반대로 나오는 실수를 했다. 값이 나오는 것과 값이 맞는 것은 다른 문제다. 텔레포트 서비스로 아는 위치에 놓고 표시값과 대조해서 잡았다.

8. 편해진 것의 값

공짜는 아니다. 거북이 하나를 그리는 turtlesim 프로세스가 스레드 17 개, 메모리 약 114 MB 를 쓴다. 펌웨어 감각으로는 납득하기 어려운 수치다. PC 급 자원을 전제로 개발 시간을 산 구조이기 때문이다.

그래서 실제 설비에서는 층을 나눈다. 마이크로초 단위의 주기를 지켜야 하는 제어 루프는 EtherCAT 과 PLC 쪽에 두고, ROS2 는 그 위에서 명령을 내리고 상태를 모니터링하는 계층으로 쓴다. 인터럽트와 링버퍼를 다루던 실력이 쓸모없어진 것이 아니라, 그 일을 해야 하는 자리가 아래층으로 내려간 것이다.

값 하나 보려고 커맨드 번호를 새로 배정하고 양쪽 코드를 고치던 입장에서 보면, 명령 한 줄로 좌표가 흐르는 것은 분명 편하다. 다만 그 한 줄이 무엇을 대신해 주고 있는지는 알고 쓰는 편이 낫다. 문제가 생기는 지점은 언제나 그 아래층이기 때문이다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

어제 방문자 : 3 명오늘 방문자 : 1 명전체 방문자 : 584 명