[카테고리:] ROS2

  • ROS의 입문계기

    2022년 말, 우리 회사는 거동이 불편한 사람을 알아서 뒤따라가는 ‘추종형(Follow-on) 이동 휠체어’ 국책과제를 맡게 되었다. 카메라로 대상을 인식해 추종하는 핵심 기술은 H대 대학원 연구실과 협업하기로 했다. 결과보고서를 제출한 2023년 3월까지의 치열했던 몇 달은, 내가 개발자로서 ‘ROS(Robot Operating System)’를 처음 마주한 시간이기도 하다.

    항목내용
    과제거동 불편자 대상 추종(follow-on) 이동형 휠체어 개발
    사업2022년 로봇융합비즈니스지원사업 (국책과제)
    협업H대 대학원 연구실 — 보호자 추종 카메라 솔루션
    목표 양산가200~250만 원대
    기간2022년 하반기 ~ 2023년 3월

    비용과 현실의 괴리

    가장 큰 문제는 단가였다. H대 연구실의 카메라 추종 솔루션은 부품값만 대당 500만 원에 달했다. 우리가 서류에 적어낸 휠체어 완제품의 목표 양산가가 200~250만 원대였 으니, 카메라 한 대 값이 휠체어 한 대 값보다 비싼 기형적인 구조였다.

    성능의 한계도 명확했다. 카메라 시야에 사람이 둘 이상 잡히면 시스템은 곧바로 오류를 냈다. 통제된 실험실에서는 잘 굴러갔지만, 사람이 붐비는 요양원과 병원 현장에서는 실효성이 없었다. 대학원생들과 함께 만든 것은 훌륭한 ‘시제품’이었을 뿐 시장에서 팔릴 ‘제품’은 아니었다. 그들과의 협업은 과제 완수를 도왔지만, 우리를 상용화의 문턱 너머로 데려가지는 못했다.

    고립무원, 그리고 남은 것들

    프로젝트 도중 기획 담당자가 퇴사하는 일까지 겹쳤다. 졸지에 개발, 산더미 같은 페이퍼워크, 그리고 정부 부처 앞에서의 발표까지 모든 짐이 내게 쏟아졌다. 혼자서 개발자, 기획자, 발표자의 1인 3역을 해내야 했다.

    당시 회사는 서로를 터치하지 않고 각자 일에만 파묻혀 지내는 분위기였다. 주말까지 출근해 매달리는 나를 보고 동료들은 “그만 좀 나오라”고 했다. 걱정이 담긴 위로의 핀잔이었다.

    결국 그 휠체어는 시장의 빛을 보지 못했다. 하지만 아무것도 남지 않은 것은 아니었다.

    H대 대학원생들이 쓰던 ROS, 어깨너머로 처음 주워듣던 ‘토픽’과 ‘노드’라는 낯선 단어들. 카메라가 현장에서 두 사람을 가려내지 못해 쩔쩔맬 때, 그 한계를 넘어서고자 나는 직접 ROS를 파고들기 시작했다.

    프로젝트는 실패로 끝났지만 내 안에 새로운 기술은 남았다. 언젠가 그 기술이 충분히 무르익어, 합리적인 가격의 카메라가 수많은 인파 속에서도 원하는 사람을 거뜬히 가려내는 그날을 보고 싶다.

  • 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 는 그 위에서 명령을 내리고 상태를 모니터링하는 계층으로 쓴다. 인터럽트와 링버퍼를 다루던 실력이 쓸모없어진 것이 아니라, 그 일을 해야 하는 자리가 아래층으로 내려간 것이다.

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

  • CAN 하던 사람을 위한 ROS2

    CAN 을 다뤄 본 사람이 ROS2 의 토픽을 처음 보면 낯익은 느낌을 받는다. 각 장치가 자기 식별자를 달아 계속 내보내고, 필요한 쪽은 그 식별자만 알면 집어 간다 — 딱 CAN 버스다.

    그 직관은 옳다. 다만 어디까지 옳고 어디서부터 어긋나는지를 알아야 ROS2 를 제대로 쓸 수 있다. 이 글은 CAN 을 이미 아는 사람을 위해 두 방식을 나란히 놓고 비교한다.

    1. 왜 닮아 보이는가 — 데이터 중심 주소지정

    두 방식이 닮은 것은 우연이 아니다. 둘 다 데이터 중심 주소지정(data-centric addressing)이라는 같은 계열에 속한다. 「누구에게 보낼 것인가」가 아니라 「무슨 데이터인가」로 주소를 삼는다는 뜻이다.

    [연결 중심 — TCP 소켓, ROS1 의 연결 단계]
    
        노드 A ────── 직접 연결 ──────→ 노드 B
        "B 에게 보낸다"   상대를 알아야 한다
    
    
    [데이터 중심 — CAN, ROS2 토픽]
    
                          ┌──→ 노드 B   (0x1A0 을 받아 간다)
        노드 A ──0x1A0──→ ├──→ 노드 C   (0x1A0 을 받아 간다)
        "0x1A0 을 낸다"    └──✕  노드 D   (관심 없음, 버린다)
         받는 쪽을 모른다

    <그림 1> 연결 중심과 데이터 중심

    CAN 노드는 자기 프레임을 누가 받는지 모르고, 알 필요도 없다. ROS2 퍼블리셔도 마찬가지로 구독자가 몇인지 모른 채 그냥 발행한다. 송신 쪽 코드를 전혀 건드리지 않고 수신 노드를 하나 더 붙일 수 있다는 것, 이것이 두 방식이 공유하는 가장 큰 미덕이다.

    여기서 파생되는 성질도 같다. 다대다가 자연스럽고, 연결 수립 절차가 응용 코드에 보이지 않으며, 주된 쓰임새가 요청·응답이 아니라 주기적인 상태 방송이다.

    2. ROS1 은 오히려 덜 닮았다

    재미있는 것은 ROS1 보다 ROS2 가 CAN 에 훨씬 더 가깝다는 점이다.

    ROS1 에는 roscore 라는 중앙 마스터가 있었다. 퍼블리셔가 마스터에 등록하면, 구독자가 마스터에게 물어 퍼블리셔의 주소를 받아 온 뒤 그쪽으로 직접 TCP 연결을 맺는 구조였다. 이름만 데이터 중심이지 실제 데이터는 점대점으로 흘렀다. 버스라기보다 전화 교환대에 가까웠고, 마스터가 죽으면 새 연결을 맺을 수 없었다.

    ROS2 는 마스터를 없앴다. 그 자리에 DDS 의 분산 디스커버리가 들어왔다. 노드들이 멀티캐스트로 자기 존재와 관심사를 알리고, 조건이 맞는 짝끼리 알아서 연결한다. 각자 떠들고 각자 골라 듣는 구조가 된 것이다. CAN 버스의 감각이 정확히 여기서 나온다.

    ★주 1. 대신 ROS2 는 디스커버리 트래픽이라는 새 문제를 얻었다. 노드가 수백 개로 늘면 서로를 찾는 패킷만으로 망이 시끄러워진다. 이를 다루려고 도입된 것이 ROS_DOMAIN_ID 로 망을 나누는 방법, 그리고 디스커버리 서버(discovery server)를 두는 방법이다. CAN 에는 디스커버리라는 개념 자체가 없으므로 이런 고민도 없다 — 대신 배선도와 식별자 표를 사람이 관리해야 한다.

    3. 결정적인 차이 — 식별자는 주소이자 우선순위다

    여기가 두 방식이 갈라지는 지점이고, CAN 을 아는 사람이 ROS2 에서 가장 먼저 허전함을 느끼는 부분이다.

    CAN 의 식별자는 주소인 동시에 우선순위다.

    CAN 은 여러 노드가 같은 순간에 송신을 시작해도 충돌로 프레임이 깨지지 않는다. 물리 계층이 와이어드 AND 로 동작하기 때문이다. 한 노드라도 0(도미넌트)을 내보내면 버스 전체가 0 이 된다. 1(리세시브)은 아무도 0 을 내지 않을 때만 유지된다.

    송신 중인 노드는 자기가 낸 비트와 버스에서 읽히는 비트를 계속 비교한다. 자기는 1 을 냈는데 버스가 0 으로 읽히면, 나보다 우선순위 높은 노드가 말하고 있다는 뜻이므로 즉시 물러난다. 이때 이미 나간 비트들은 이긴 노드의 비트와 똑같았으므로 프레임은 전혀 손상되지 않는다. 진 노드는 버스가 비는 즉시 다시 시도한다.

    이것을 비파괴 중재(non-destructive arbitration)라 하며, 결과적으로 식별자가 작은 노드가 항상 이긴다. 아래에서 직접 돌려 볼 수 있다. 세 노드의 식별자를 바꿔 가며 「한 비트」로 한 칸씩 진행해 보라.


    <그림 2> CAN 비트 중재. 식별자가 작을수록 먼저 말한다.

    이 성질 덕분에 CAN 은 고우선순위 메시지의 최악 지연시간을 계산으로 보증할 수 있다. 식별자 배분이 곧 실시간 설계가 되는 것이다. 차량 안전계통이 수십 년째 CAN 을 쓰는 이유가 여기 있다.

    ROS2 의 토픽 이름에는 이런 성질이 전혀 없다. 이름은 그냥 이름이다. /cmd_vel 이 /camera/image_raw 보다 먼저 간다는 보장이 없고, 커다란 영상 메시지가 망을 채우는 동안 짧은 제어 명령이 뒤에서 기다릴 수 있다. DDS QoS 에 TRANSPORT_PRIORITY 항목이 있으나 일반적인 이더넷·UDP 환경에서는 CAN 같은 보증이 되지 않는다.

    정리하면 이렇다. CAN 의 식별자는 설계 결정이고, ROS2 의 토픽 이름은 작명이다.

    ★주 2. ROS2 에서 실시간성이 필요하면 토픽 이름이 아니라 다른 층에서 풀어야 한다. 실시간 커널(PREEMPT_RT), 같은 프로세스 안에서 복사를 없애는 인트라프로세스 통신, 실행기(executor) 의 콜백 그룹과 스레드 우선순위, 그리고 이더넷이라면 TSN 이 그 수단이다. 망 계층이 알아서 우선순위를 지켜 주던 CAN 과 달리, ROS2 에서는 사람이 여러 층을 직접 조립해야 한다.

    4. 나머지 차이

    항목 CAN ROS2 토픽
    페이로드 8 바이트 (CAN FD 는 64) 사실상 제한 없음
    속도 1 Mbit/s (FD 데이터 구간은 수 Mbit/s) 이더넷 그대로. 기가비트 이상
    형식 계약 DBC 파일 — 버스 밖의 약속 .msg — 디스커버리 때 형식이 맞아야 연결된다
    디스커버리 없음. 설계 시 고정 동적. 붙었다 떨어졌다 한다
    늦게 켠 노드 다음 주기까지 아무것도 모른다 TRANSIENT_LOCAL 이면 마지막 값을 즉시 받는다
    재전송 하드웨어가 자동. 에러 카운터·버스오프 RELIABLE / BEST_EFFORT 선택
    실시간성 해석 가능한 하드 실시간 일반적으로 보증 없음
    물리 계층 버스 하나에 종단 저항 120 Ω. 길이와 속도가 맞물린다 전송 방식과 무관. 보통 UDP, 같은 PC 안이면 공유 메모리

    <표 1> CAN 과 ROS2 토픽의 차이

    실무에서 의외로 크게 체감되는 것이 「늦게 켠 노드」 항목이다. CAN 에서는 나중에 전원을 넣은 장치가 모터 상태 프레임이 올 때까지 아무것도 모른 채 기다려야 한다. ROS2 는 퍼블리셔가 TRANSIENT_LOCAL 로 발행하면 구독을 시작하는 순간 마지막 값을 받는다. 지도, 로봇 기술(URDF), 설정값처럼 자주 변하지 않지만 항상 알아야 하는 데이터에 쓴다. CAN 에는 대응물이 없어서, 보통 새 노드가 SDO 로 직접 읽어 오거나 주기를 짧게 가져가는 식으로 우회한다.

    5. CANopen 으로 보면 정확히 맞아떨어진다

    날것의 CAN 보다 CANopen 과 비교하면 훨씬 깔끔하게 대응된다. CANopen 이 CAN 에 없던 것 — 형식 계약과 상태 관리 — 을 얹은 규약이기 때문이다.

    CANopen ROS2 공통점
    PDO
    Process Data Object
    토픽 주기적·무확인. 빠르고 가볍다
    SDO
    Service Data Object
    서비스 요청에 응답이 따라온다. 느리지만 확실하다
    NMT
    Pre-op → Operational
    라이프사이클 노드 상태 전이를 거쳐야 데이터가 흐른다
    하트비트 / 노드 가딩 LIVELINESS QoS 상대가 살아 있는지 주기적으로 확인
    EMCY (긴급 메시지) /diagnostics 이상 상황을 따로 알린다
    오브젝트 딕셔너리 (EDS) .msg + 파라미터 주고받을 내용의 형식 계약
    SYNC (대응물 없음) 여러 노드의 동시 샘플링·동시 구동

    <표 2> CANopen 과 ROS2 의 대응

    특히 NMT ↔ 라이프사이클 노드 대응이 볼만하다. CANopen 장치는 전원을 넣었다고 바로 PDO 를 쏟아내지 않는다. 초기화를 거쳐 Pre-operational 에 머물다가, 마스터가 Operational 로 올려 줘야 비로소 데이터를 낸다. ROS2 의 라이프사이클 노드도 똑같이 Unconfigured → Inactive → Active 를 거치며, Active 가 되기 전에는 발행하지 않는다.

    둘 다 「켜자마자 떠들기 시작하면 곤란하다」는 현장 경험에서 나온 구조다. 센서 보정이 끝나기 전 값, 모터 드라이버가 준비되기 전 지령은 없느니만 못하다. ROS2 를 쓰면서 라이프사이클 노드를 왜 쓰는지 모르겠다는 사람에게는 이 비유가 가장 잘 통한다.

    6. 토픽인가 서비스인가 — PDO 인가 SDO 인가

    ROS2 입문자가 가장 오래 헤매는 물음이 「이건 토픽으로 해야 하나 서비스로 해야 하나」다. CAN 을 아는 사람에게는 이 물음을 바꿔 물으면 답이 바로 나온다.

    이것은 PDO 인가, SDO 인가?

    • 주기적으로 계속 흘려보낼 상태인가 → PDO → 토픽
      모터 현재 속도, 엔코더 값, 센서 측정치, 조인트 상태
    • 어쩌다 한 번 물어보거나 바꾸는 설정인가 → SDO → 서비스
      가속도 한계 설정, 원점 복귀 실행, 모드 전환, 파라미터 읽기

    판별 기준도 같다. 「응답을 기다려야 하는가」와 「한 번 놓쳐도 다음 주기에 만회되는가」이다. 모터 속도는 한 번 놓쳐도 10 ms 뒤에 또 온다 — 토픽이다. 원점 복귀 명령은 놓치면 안 되고 완료 여부를 알아야 한다 — 서비스, 혹은 오래 걸린다면 액션(action)이다.

    ★주 3. ROS2 에는 CANopen 에 없는 세 번째 기구인 액션(action)이 있다. 오래 걸리고, 중간 경과를 알려 주어야 하며, 도중에 취소할 수 있어야 하는 작업에 쓴다. 로봇 팔을 목표 자세로 보내는 일이 전형적이다. SDO 로 억지로 흉내 내려면 「시작 명령 SDO + 진행률 PDO + 중단 명령 SDO」를 손으로 조립해야 하는데, 액션은 그 조합을 하나의 규약으로 묶어 둔 것이라 보면 된다.

    7. 계약이 틀렸을 때 무슨 일이 생기는가

    날마다 가장 크게 체감되는 차이는 성능도 구조도 아니고 틀렸을 때의 증상이다.

    CAN — 조용히 틀린 값이 나온다

    CAN 프레임에는 형식 정보가 없다. 8 바이트의 의미는 전적으로 DBC 파일이라는 버스 밖의 약속에 달려 있다. 스케일 인자를 잘못 넣으면 온도가 25 ℃ 대신 250 ℃ 로 나오는데, 아무도 오류를 내지 않는다. 바이트 순서를 거꾸로 읽어도, 부호 있는 값을 없는 값으로 읽어도 마찬가지다. 그럴듯하게 틀린 숫자가 태연히 흘러간다.

    그래서 CAN 디버깅은 파형과 원시 바이트를 눈으로 좇는 일이 된다. 오실로스코프, CAN 분석기, DBC 를 놓고 대조한다.

    ROS2 — 아예 연결이 안 된다

    ROS2 는 형식이 디스커버리 단계에서 대조된다. 퍼블리셔가 sensor_msgs/msg/Temperature 를 내고 구독자가 std_msgs/msg/Float32 를 기다리면 둘은 애초에 이어지지 않는다. 잘못된 값이 흘러갈 일이 없다.

    대신 ROS2 에는 CAN 에 없던 함정이 하나 생긴다. 형식이 맞는데도 QoS 가 안 맞아 연결되지 않는 경우다. 초보자를 가장 많이 잡는 대목이고, 증상이 「퍼블리시는 되는데 아무도 못 받는다」라서 원인을 짐작하기 어렵다.

    퍼블리셔가 제공 구독자가 요구 결과
    RELIABLE RELIABLE 연결됨
    RELIABLE BEST_EFFORT 연결됨
    BEST_EFFORT RELIABLE 연결 안 됨
    TRANSIENT_LOCAL VOLATILE 연결됨
    VOLATILE TRANSIENT_LOCAL 연결 안 됨

    <표 3> QoS 짝 맞추기 — 제공이 요구보다 약하면 연결되지 않는다

    규칙은 하나다. 퍼블리셔가 제공하는 품질이 구독자가 요구하는 품질보다 약하면 연결되지 않는다.

    가장 흔한 사고가 이것이다. 라이다나 카메라 드라이버는 대개 SensorDataQoS, 즉 BEST_EFFORT 로 발행한다. 그런데 직접 짠 노드에서 기본값(RELIABLE)으로 구독하면 토픽 목록에는 보이는데 콜백이 한 번도 안 불린다. ros2 topic echo 로는 잘 보여서 더 헷갈리는데, echo 가 자동으로 QoS 를 맞춰 주기 때문이다.

    확인 방법은 간단하다.

    $ ros2 topic info -v /scan
    
    Publishers: 1
      ...
      QoS profile:
        Reliability: BEST_EFFORT     ← 여기
        Durability: VOLATILE
    
    Subscribers: 1
      ...
      QoS profile:
        Reliability: RELIABLE        ← 여기. 짝이 안 맞는다
        Durability: VOLATILE

    <그림 3> QoS 불일치 확인

    CAN 으로 치면 「배선도 다 맞고 종단 저항도 맞는데, 통신 속도만 서로 다르게 잡아 놓은 상태」에 가깝다. 물리적으로는 다 연결되어 있는데 한 마디도 오가지 않는다.

    8. 도구 대응표와 실제 연결

    쓰던 도구를 기준으로 짝지으면 적응이 빠르다.

    CAN (SocketCAN) ROS2
    candump can0 ros2 topic echo /토픽
    cansniffer can0 ros2 topic hz /토픽
    candump -l ros2 bag record
    canplayer ros2 bag play
    cansend can0 123#DEADBEEF ros2 topic pub /토픽 …
    DBC 파일 .msg 파일
    버스 부하율 측정 ros2 topic bw /토픽
    노드 목록 (설계도) ros2 node list

    <표 4> 도구 대응

    그리고 둘을 실제로 이어 붙일 수도 있다. 리눅스의 SocketCAN 위에서 동작하는 다리 역할 패키지들이 있다.

    • 원시 프레임 수준 — CAN 프레임을 그대로 can_msgs/msg/Frame 토픽으로 올리고 내린다. 해석은 직접 해야 하지만 무엇이든 붙일 수 있다.
    • CANopen 수준 — EDS 파일을 읽어 오브젝트 딕셔너리를 파악하고, PDO 를 토픽으로 SDO 를 서비스로 자동 노출해 준다. 표 2 의 대응을 코드로 구현해 둔 셈이다.

    기존에 CANopen 서보 드라이브를 쓰던 장비를 ROS2 로 올릴 때는 후자가 압도적으로 편하다. 다만 3 절에서 말한 실시간성은 다리를 건너는 순간 사라진다. CAN 버스 안에서 보장되던 우선순위는 이더넷과 리눅스 스케줄러를 지나며 흩어진다. 안전 관련 정지 회로를 ROS2 토픽으로 옮기는 것은 거의 언제나 나쁜 생각이다. 그런 신호는 CAN 이나 하드와이어로 남겨 두고, ROS2 에는 상태 보고만 올리는 구성이 안전하다.

    맺으며

    정리하면 이렇다.

    • ROS2 토픽과 CAN 은 데이터 중심 주소지정이라는 같은 뿌리를 공유한다. 그 직관은 믿어도 된다.
    • 다만 CAN 의 식별자는 우선순위까지 겸하고 ROS2 의 토픽 이름은 그렇지 않다. 실시간성이 필요하면 다른 층에서 따로 풀어야 한다.
    • 대응을 정확히 보려면 CANopen 과 견주는 편이 낫다. PDO ↔ 토픽, SDO ↔ 서비스, NMT ↔ 라이프사이클이다.
    • 틀렸을 때 CAN 은 조용히 틀린 값을, ROS2 는 연결 실패를 준다. 후자의 원인은 형식 아니면 QoS 다.

    CAN 을 오래 만진 사람이 ROS2 를 배울 때 가장 큰 자산은 문법이 아니라 「버스에 데이터를 흘린다」는 사고방식이다. 그건 이미 몸에 있다. 새로 익힐 것은 형식 계약과 QoS, 그리고 잃어버린 우선순위를 어디서 되찾을 것인가 하는 문제뿐이다.

    다뤄 주었으면 하는 주제나 이상한 점이 있으면 댓글로 남겨 주기 바란다.

  • Vultr서버에 ROS2 테스트 하기

    ROS2(Robot Operating System 2)는 대부분 같은 네트워크의 PC와 로봇 사이에서 사용되며, 원격 서버에서 노드를 동작시키는 경우는 의외로 드물다. 여기서는 필자의 블로그가 동작하는 Vultr 가상 서버(1 vCPU, 메모리 1GB, Ubuntu 22.04)에 ROS2 Humble을 설치하고, 토픽으로 질의와 응답을 주고받는 실험을 정리하기로 한다.

    1. 실험의 구성

    (1) 목표

    노트북에서 /ask 토픽에 질문을 발행(publish)하면, 서버의 정보 노드가 이를 구독(subscribe)하여 /answer 토픽으로 응답하는 것이 목표이다. 구성은 <그림1>과 같다.

    노트북 ─ SSH ─▶ 서버 [ /ask ─▶ info_node ─▶ /answer ]

    <그림1> 실험의 구성

    (2) 왜 SSH를 거치는가?

    그러면, 노트북의 ROS2에서 서버로 직접 토픽을 보낼 수는 없는가? 결론부터 말하면 간단하지 않다. ROS2의 통신 계층인 DDS(Data Distribution Service)는 멀티캐스트로 상대 노드를 탐색(discovery)하는데, 멀티캐스트는 인터넷 라우터를 넘지 못하기 때문이다. 그래서 첫 실험에서는 SSH로 서버에 접속하여 서버 내부에서 토픽을 주고받는 가장 단순한 방법을 택하였다. 또한, ROS_LOCALHOST_ONLY=1로 DDS 통신을 서버 내부(127.0.0.1)로 제한하였으므로 외부에 여는 포트는 없다.

    2. 설치

    메모리를 웹 서버와 나누어 써야 하므로 GUI 도구가 포함된 desktop 구성 대신 최소 구성인 ros-core만 설치하였다. 운영 중인 서버에서는 apt upgrade로 기존 패키지를 일괄 갱신하지 않도록 유의해야 한다. PHP나 데이터베이스가 함께 갱신되어 사이트가 멈출 수 있기 때문이다.

    # ROS2 apt 저장소 등록 후
    sudo apt install -y ros-humble-ros-core
    
    # ~/.bashrc
    source /opt/ros/humble/setup.bash
    export ROS_DOMAIN_ID=7
    export ROS_LOCALHOST_ONLY=1

    3. 정보 노드의 작성

    정보 노드는 rclpy로 작성한 파이썬 파일 하나이며, 처리하는 질의는 <표1>과 같다.

    질의응답 내용
    status가동 시간, CPU 부하, 메모리 및 디스크 사용량
    time서울 현재 시각
    visitors블로그 방문자 수 (어제 · 오늘 · 전체)
    fortune로봇 관련 한마디 (무작위)
    <표1> 정보 노드가 처리하는 질의

    방문자 수는 홈페이지의 카운터 값을 읽어 오며, 사이트 부하를 줄이기 위하여 1분 동안 캐시한다. 노드의 핵심 부분은 다음과 같다.

    class InfoNode(Node):
        def __init__(self):
            super().__init__("yd_info")
            self.pub = self.create_publisher(String, "answer", 10)
            self.create_subscription(String, "ask", self.on_ask, 10)
    
        def on_ask(self, msg):
            self.pub.publish(String(data=answer(msg.data)))

    4. 실행 및 결과

    SSH 접속을 끊으면 실행 중인 프로그램도 종료되므로 노드는 tmux 안에서 실행하였다. “준비 완료” 로그가 보이면 Ctrl+B, D로 세션에서 빠져나온다(detach). 노드는 계속 동작하며 tmux attach -t ros로 다시 확인할 수 있다.

    tmux new -s ros
    python3 info_node.py
    [INFO] [yd_info]: 준비 완료 — /ask 로 질문하세요 (help | status | time | visitors | fortune)

    질의는 발행과 수신을 한 번에 처리하는 보조 스크립트를 ask라는 별칭으로 등록하여 사용하였다. 다음은 실제 서버에서 얻은 결과이다.

    $ ask status
    가동 시간 : 49일 2시간 22분
    CPU 부하  : 0.01 / 0.07 / 0.03 (1 / 5 / 15분)
    메모리    : 496 / 952 MB 사용 (스왑 71 MB)
    디스크    : 13.8 / 31.6 GB 사용
    
    $ ask visitors
    어제 1명 · 오늘 4명 · 전체 282명
    
    $ ask fortune
    로봇은 거짓말을 하지 않습니다. 엔코더가 할 뿐.

    물론 ros2 명령으로 직접 발행하여도 응답은 같다.

    ros2 topic echo /answer
    ros2 topic pub --once /ask std_msgs/msg/String "{data: time}"
    
    data: 2026-09-15 (화) 16:46:30 KST

    ★주: 노드 하나의 메모리 사용량은 약 60MB로 워드프레스와 함께 동작시켜도 부담이 거의 없으나, 스왑(swap) 영역은 미리 확인해 두는 것이 좋다.

    5. 맺음말

    이상에서 메모리 1GB의 작은 서버에서도 ROS2 노드가 무리 없이 동작함을 확인하였다. 다만 이번 실험은 엄밀히 말하면 원격 통신이라기보다 원격 접속에 가깝다. 다음에는 Zenoh 브리지나 rosbridge로 노트북과 서버의 ROS2를 직접 연결하여, 로봇의 상태를 원격으로 수신해 볼 예정이다.

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