[카테고리:] 로봇제어

  • 나에게는 어려웠던 개발 프로젝트

    2018년, 나는 8축 와이어 벤딩 머신 “프리스타일(FreeStyle)”을 처음부터 끝까지 혼자 만들었다. PLC도, HMI도, 모션도, 고객 대응도 전부 처음 해 보는 일이었다. 지금까지 올린 글 중에서 가장 오래 붙잡고 있었고, 가장 많이 막혔던 프로젝트다.

    항목내용
    프로젝트명FreeStyle — 8축 와이어 벤딩 머신
    기간2018년 개발, 2019년 초까지 수정
    제어기B&R Automation PC910 (파워링크)
    축 구성회전·이송·투피드·클램프·벤드·푸시·수평·수직 8축 서보 + 히터 4계통
    고객미국 소재 업체 — 영문 이메일·현지 출장으로 협의

    혼자 처음부터

    업무일지 첫 줄에 내가 스스로 나눠 놓은 순서가 있다. 환경설정, IO 매뉴얼 프로그램, 히터 매뉴얼 프로그램, 모션 매뉴얼 프로그램, UI 작업, 자동 프로그램, 알람 및 예외처리, 파일·레시피·로그, 그리고 야스카와 로봇 프로그램. 마지막 항목 옆에는 (0%)라고 적어 놓았다. 그때는 로봇 연동까지 갈 길이 멀다는 걸 알고 있었다는 뜻이다.

    터치스크린 HMI로 레시피를 짜서 와이어를 굽히는 기계였다. 스텝 하나에 회전·이송· 클램프·벤드 같은 축들의 목표값을 넣고, 그 스텝을 순서대로 이어 붙이면 하나의 형상이 나왔다. 이 구조를 화면 설계부터 PLC 로직까지 혼자 짰다.

    레시피가 20개에서 막혔다

    고객이 보내온 수정 요청 목록 맨 위에 이런 게 있었다. “21번째부터 40번째 스텝이 따로 작동하지 않는다. 21~40번을 고치면 1~20번도 같이 바뀐다.” 레시피 한 줄에 쓸 수 있는 스텝을 20개로 잡아 놓은 내 자료구조의 한계였다. 100개까지 늘려 달라는 요청에 나는 이렇게 답을 적어 뒀다.

    단순노동이라 쉽지만 시간이 걸리는 문제.

    쉽다고 적어 놓고도 실제로는 시간이 오래 걸렸다. 복사·붙여넣기도, 스텝 삽입도, 되돌리기(undo)도 없는 화면이었다. 고객은 반복되는 동작을 매번 손으로 새로 입력해야 했다. 그가 요청한 기능 하나하나가 지금이라면 당연히 있어야 할 것들이었다. 그때는 그 당연한 것들을 하나씩 다 만들어야 했다.

    말이 안 통해서 더 어려웠다

    기술보다 더 힘들었던 건 요구사항을 읽는 일이었다. 고객은 영어로 스크린샷과 함께 길게 적어 보냈고, 나는 그 밑에 한국어로 답을 달고 다시 영어로 옮겼다. 수정 목록에는 내가 그때 적어 둔 문장들이 그대로 남아 있다.

    나는 당신의 요구사항을 정확히 이해하지 못하겠다.
    내 생각에 다음 미국출장 때 대화하면서 이 문제 뿐 아니라 전반적으로 업데이트해야 될 것 같다.

    같은 문장이 목록 안에서 세 번 반복된다. 화면 캡처와 화살표만으로는 안 풀리는 요구가 있었고, 그런 항목은 결국 출장을 가서 기계 앞에 같이 서야 풀렸다.

    헬릭스

    고객이 만들고 싶어 한 형상 중에는 나선(helix)이 있었다. 그가 보내온 문서에는 두 가지 방법이 적혀 있었다. 하나는 파라메트릭 방정식으로 나선을 통째로 계산하는 방법, 다른 하나는 굽힘을 짧은 구간으로 잘라 이어 붙이는 방법이었다. 후자를 택하면 이송량과 굽힘각을 단순한 값 몇 개로 넣을 수 있었지만, z축은 이 계산에 들어가지 않았다. z축은 사람이 손으로 움직이거나, 나중에 로봇이 맡아야 하는 몫이었다.

    평면 위의 수학은 고객이 직접 풀어서 보내 줬다. 그 수식을 기계가 이해하는 스텝 값으로 바꾸는 일이 내 몫이었다. 로봇이 z축을 대신하는 날은 이 프로젝트 안에서 오지 않았다.

    PLC도 HMI도 모션도 혼자 배워 가며 만들었고, 말이 통하지 않는 고객과 문서를 주고받으며 요구사항 읽는 법을 배웠다. 그 몇 달이 지금 내가 새로운 프로젝트 앞에서 겁내지 않는 이유 중 하나다.

  • 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, 그리고 잃어버린 우선순위를 어디서 되찾을 것인가 하는 문제뿐이다.

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

  • 예천…겐트리의 추억

    예천…겐트리의 추억

    경북 예천의 음료 공장에 겐트리 로봇 두 대를 세웠다. 흐르는 컨베이어 위의 박스를 멈추지 않고 집어 파렛트에 쌓는 설비다. 2021년 1월에 시작해 그해 12월에 넘겼고, 5년이 지난 지금도 가끔 나를 부른다.

    흐르는 것을 집는다

    겐트리 로봇의 일 자체는 단순하다. 집어서 옮겨 놓는다. 어려운 것은 대상이 서 있지 않다는 점이다.

    컨베이어는 초당 320 mm 로 계속 흐른다. 센서가 박스를 감지한 순간부터 로봇이 그 자리에 도달할 때까지, 박스는 이미 저만치 가 있다. 그래서 로봇은 지금 위치가 아니라 집을 시점의 위치로 가야 한다. 이것이 컨베이어 트래킹이다.

    <표 1> 현장 치수

    항목값
    컨베이어 길이4,560 mm
    감지 센서 → 로봇 11,200 mm
    감지 센서 → 로봇 23,400 mm
    컨베이어 속도320 mm/s (뒤에 310 으로 낮춤)

    로봇 두 대가 한 줄을 나눠 맡는다. 홀수 번째 박스는 1번이, 짝수 번째는 2번이 집는다. 두 대가 각자 자기 몫만 정확히 세고 있어야 하며, 한 번이라도 어긋나면 그 뒤가 전부 밀린다.

    트래킹 모니터링 화면
    <그림 1> 트래킹 모니터링. 왼쪽 큐가 컨베이어 위 박스이고, 로봇별 집은 좌표와 놓은 좌표가 실시간으로 찍힌다

    세 대의 제어기

    제어가 한 곳에 모여 있지 않았다. 세 대가 물려 있었다.

    • 미쓰비시 PLC — 라인 전체의 시퀀스
    • 베이어 HMI — 작업자가 보는 터치 화면
    • Lenze 3200 — 겐트리 두 대의 모션

    박스의 품번·길이·폭·적재 패턴은 HMI 에서 입력되어 미쓰비시를 거쳐 Lenze 로 내려온다. 모션 제어기 입장에서는 남을 거쳐 오는 정보인 셈이다.

    적재 패턴 입력 화면
    <그림 2> 품번·길이·폭과 홀짝별 적재 좌표

    여기서 문제가 났다. Lenze 가 리셋되고 나면, 미쓰비시를 통해 오는 값이 갱신되지 않았다. 두 제어기가 직접 주고받는 데이터는 멀쩡한데 한 다리 건넌 값만 굳어 버렸다. 모션 쪽에서는 그것이 낡은 값인지 알 방법이 없었다. 길이와 폭이 0 이면 알람이라도 낼 수 있지만, 그럴듯한 값으로 굳어 있으면 방법이 없었다.

    도는 축에는 리밋이 걸리지 않는다

    현장 셋업에서 소프트리밋이 회전축과 그리퍼축에만 먹지 않았다. X·Y·Z 는 정상이었다. 원인은 축 타입이었다. 두 축이 모듈러로 설정되어 있었는데, 끝없이 도는 축으로 보면 상한과 하한이라는 개념 자체가 성립하지 않는다. 리미티드로 바꾸고 나서야 리밋이 걸렸다.

    회전축은 기구 원점을 180도에 두고 −10도에서 91도까지만 움직이도록 묶었다. 사람이 눈으로 확인할 수 없는 축일수록 소프트웨어가 대신 막아 줘야 한다.

    2,000 박스

    2021년 11월 22일, 양산 조건으로 2,000 박스를 흘렸다. 1,800 개가 정상 배출되고 200 개가 문제였다. 불량률 10 %.

    원인을 갈라 보니 이랬다.

    원인비중
    통신 지연80 %
    투입 물류 문제15 %
    센서 오감지 · 미파악5 %

    박스가 들어오고 2초 안에 통신이 갱신되지 않으면 몇 번 패턴인지 알 수 없었다. 공장장과 협의해 그날 생산을 멈췄다.

    결론은 소프트웨어가 아니었다. 입력 배선 11개를 새로 깔았다. 남을 거쳐 오던 신호를 모션 제어기가 직접 받도록 바꾼 것이다. 통신 프로토콜을 아무리 다듬어도 세 다리를 건너는 구조 자체는 바뀌지 않는다. 그럴 때는 선을 하나 더 까는 편이 빠르다.

    생산 중에는 건드리지 않는다

    다음 날 5,800 개를 흘렸고 7번 놓쳤다. 그중 몇 번은 내가 사이클 타임 측정 프로그램을 내려받는 중에 일어났다. 그날 수첩에 이렇게 적었다.

    생산 중에는 왠만하면 프로그램 수정 하기 없기.

    당연한 말인데, 라인이 돌고 있으면 한 줄만 고치면 될 것 같다는 생각이 계속 든다. 그 유혹이 사고를 만든다.

    텍타임

    처음엔 분당 24 박스였다. 그리퍼 제어 로직을 손보면 1회당 0.3~0.5초를 줄일 수 있다고 봤다. 투입 타이머를 박스 위치별로 분리하고, 그리퍼를 로봇이 출발하면서 함께 움직이도록 바꿨다.

    그리퍼 개폐 도면
    <그림 3> 그리퍼 개폐 스트로크. 제품 폭에 따라 벌어지는 정도가 달라진다

    분당 26 박스까지 올라갔다. 목표는 32 였다. 다만 속도를 올리자 로봇 2번에 소프트리밋 알람이 떴고, 컨베이어를 310 mm/s 로 낮췄다. 가장 빠른 설정과 가장 안정된 설정은 같지 않았다. 결국 25~26 박스 언저리에서 타협했다. 그 속도가 밤새 돌려도 알람이 없는 속도였다.

    진동과 소음은 클러치 파라미터로 잡았다. 축이 트래킹에 물리는 구간을 짧게 (ClutchDistance = 60, ClutchTime = 0) 두니 붙는 순간의 충격이 줄었다.

    5년 뒤

    2021년 12월 10일에 오퍼레이터 교육을 하고 현장에서 빠졌다. 이듬해 1월에 바이패스 모드를 넣어 주고 그걸로 끝인 줄 알았다.

    2026년 8월, X축에 팔로잉 에러가 나기 시작했다는 연락을 받았다. 축을 교체하려면 10주가 걸린다고 했다. 그때까지 라인을 세울 수는 없으니 임시 조치를 하러 다시 예천에 갔다.

    팔로잉 에러 설정
    <그림 4> 추종 오차 감시 설정

    허용 오차를 200 에서 500 으로, 판정 시간을 1,000 에서 3,000 ms 로 늘렸다. 오차 한계보다 시간을 먼저 늘리는 편이 안전하다. 순간적인 기구 마찰은 지나가게 두고, 진짜 고착이나 충돌 같은 지속성 이상은 그대로 잡아내기 때문이다. 한계값만 키우면 두 경우를 구분하지 못하게 된다.

    설비는 넘긴다고 끝나지 않는다. 기구는 닳고, 오차는 커지고, 5년 전에 임시로 박아 둔 한 줄이 어느 날 발목을 잡는다. 그때 그 코드를 쓴 사람이 나라는 것도 로그를 뒤지다 알게 된다.

    그래도 예천의 그 라인은 아직 돌고 있다. 박스는 여전히 초당 320 mm 로 흐르고, 두 대가 홀수와 짝수를 나눠 집는다.

    가끔은 그것으로 충분하다.

  • 모션제어의 추억

    모션제어의 추억

    2019년 여름, 미국 퍼듀대학교에 QTC 라는 장비가 있었다. 5축으로 움직이며 150 ℃ 히팅부에서 나오는 소재를 감아 원하는 3차원 형상을 만드는 개발 장비였다. 결국 상용화에는 이르지 못했지만, 모션 제어를 하며 겪은 것 중 가장 많이 배운 일이었다.

    장비가 하는 일

    소재는 토우(tow) 라 부르는 납작한 섬유 테이프다. 히팅부를 지나며 물러진 상태로 나오고, 이것을 헤드가 지그에 감는다. 감긴 형상이 굳으면 부품이 된다. 히팅 온도는 150 ℃ 안팎을 지켜야 했다. 낮으면 감기지 않고 높으면 소재가 상한다. 온도와 모션이 함께 맞아야 형상이 나오는 장비였다.

    공정은 네 덩어리로 이어진다 — QTC → 로딩·언로딩 로봇 → 사출 → 커팅. 장비 하나가 아니라 셀 전체가 한 호흡으로 돌아가야 했다.

    QTC 셀 전경
    <그림 1> 셀 전경. 왼쪽이 QTC 헤드, 가운데가 이송 로봇, 오른쪽이 사출기

    B&R CNC 와 G·M 코드

    제어기는 B&R 이었고, 모션은 CNC 기능으로 돌렸다. 작업자가 수동으로 티칭하면 G 코드 파일이 자동 생성되고, 그 파일을 열어 손으로 다듬는 방식이었다.

    티칭과 파일 수정까지는 금방 되었다. 문제는 그다음이었다. 자동으로 돌리면 알람이 났다. 세팅을 처음부터 다시 잡고서야 새벽에 겨우 자동 동작을 확인했다. CNC 기능 자체도 B&R 의 인증을 받아야 쓸 수 있어서, 그 절차부터 밟아야 했다.

    M 코드 매핑 화면
    <그림 2> M 코드와 IO 를 짝지은 화면. 노란색이 동기·비동기 M 코드다

    M 코드는 IO 하나하나에 대응시켰다. 그립, 회전, 푸시, 커팅, 크릴 브레이크, 로딩 로봇 호출까지 전부 번호를 받았다. 여기서 생산 시간을 줄일 여지를 찾았다. 동기 M 코드는 동작이 끝날 때까지 기다리지만, 비동기 M 코드는 걸어 놓고 다음 스텝으로 넘어간다. 기다려도 되는 것과 아닌 것을 갈라 놓자 사이클이 눈에 띄게 짧아졌다.

    다섯 축이 부딪힌 것들

    기계는 대개 계산대로 움직이지 않는다. 현장에서 나온 문제들이다.

    문제조치
    R 축이 180° 이상 돌지 않고
    연속 생산 시 좌표가 꼬임
    시작 시 헤드를 강제 회전시켜 R 축 좌표를 보정한 뒤 G 코드를 시작
    가감속 프로파일 미최적화구간별로 다시 잡음
    감을 때 토우가 밀림헤드 위치에 따라 푸시가 토우를 잡도록 타이밍 조정
    토우가 헤드 안으로 빨려 들어감크릴 브레이크 가운데가 뜨는 것이 원인. 고무 패드를 덧붙임
    커팅 후 토우가 구부러져
    다음 사이클에서 그리퍼가 놓침
    티칭 수정

    R 축 문제가 특히 오래 걸렸다. 해결책이 둘 있었는데 하나는 사용자가 G 코드를 나눠 넣어야 해서 불편했고, 다른 하나는 생산 시간이 늘었다. 어느 쪽도 공짜가 아니었다. 결국 시간을 조금 내주고 사용자 편의를 택했다.

    QTC 헤드와 지그
    <그림 3> 헤드 아래 지그. 공압 실린더가 토우를 눌러 준다

    제어만 하는 게 아니다

    히터도 우리 몫이었다. 에어 히터 네 개가 각 24 Ω 안팎, IR 램프가 0.6~0.9 Ω. 저항을 하나씩 재고 온도 센서를 1:1 로 대조했다. SCR 을 갈고, 히터에는 열전도 컴파운드를 발라 다시 조립했다.

    분해한 히터 부품
    <그림 4> 분해해 청소한 히터 부품

    제어기 OS 를 다시 깔고 파티션과 메모리를 재할당한 날도 있었다. IO 모듈 통신이 안 잡혀 버스 컨트롤러를 통째로 교체하기도 했다. 개발 장비의 현장이란 대개 이렇다. 코드만 붙들고 있을 수 있는 날은 많지 않다.

    마지막 며칠

    6월 말에는 항공기 제조사 시연회가 있었다. 그 준비를 하면서 학생들에게 QTC 운전 교육도 했다. 화면을 함께 고치고, 티칭부터 G·M 코드 수정까지 직접 해 보게 했다. 필요하다는 기능은 그 자리에서 넣었다.

    마지막 주에는 안전을 손봤다. 데드맨 스위치를 배선하고, 수동·자동 모드 릴레이가 서로 배타적으로 동작하도록 만들었다. 매뉴얼 모드에서는 CNC 가 한 스텝씩만 돌고, 조그는 데드맨 스위치가 눌린 상태에서만 먹는다. 사람이 장비 곁에 서 있는 한, 이런 것이 가장 중요하다.

    QTC 는 상용화되지 못했다. 그래도 나는 이 장비에서 배운 것으로 그 뒤의 일들을 했다. 5축을 한 번 제대로 다뤄 본 경험, G·M 코드를 손으로 고쳐 본 시간, 그리고 “안 되는 이유는 대개 제어가 아니라 다른 곳에 있다”는 감각.

    지금도 잘 풀리지 않는 축을 붙들고 있을 때면 그해 여름의 새벽이 떠오른다.

    미국 체류 중
    2019년, 미국에서

    로봇과 30대를 보냈다..

    QTC 앞에서
    퍼듀 Manufacturing Design Laboratory, QTC 와 함께
  • Lenze 9400 와인더

    Lenze 9400 와인더

    2020년 2~3월, 미국 현장에서 Lenze 9400 시리즈로 압연·슬리터·와인더 라인을 잡았던 기록이다. 두 번 건너갔고, 돌아와서도 원격으로 붙었다.

    축 구성과 동기

    라인은 Mill #1~#6, Pinch Roll, Slitter, Winder #1·#2 로 이어진다. 소재가 한 줄로 지나가므로 모든 축이 같은 선속도를 지켜야 한다. 하나만 어긋나도 소재가 늘어나거나 접힌다.

    동기는 기어비를 1:1 로 두고 GearElectricalShaft 의 스트레치 팩터 (C3001)로 비율을 맞췄다. 기어비에 직접 손대는 것보다 현장에서 다루기 쉽고, 마스터 RPM 과 슬레이브 RPM 의 관계를 계산식 그대로 검증할 수 있다.

    Lenze 9400 GearElectricalShaft 블록
    <그림 1> 전자 기어 블록. 분자·분모와 스트레치 팩터로 동기 비율을 잡는다

    모터 동정과 소음

    축마다 Motor DetermineParameter 를 돌려 고정자 저항, 누설·상호 인덕턴스, 회전자 시정수를 자동으로 뽑았다. 이 값이 맞아야 전류 제어가 안정된다.

    모터 등가회로 파라미터
    <그림 2> 자동 동정 결과
    CAN RPDO1 수신 오류
    <그림 3> CAN RPDO1 수신 오류

    Roll #3 에서 고주파 소음이 났다. C173 을 500 V, C174 를 350 V 로 낮추니 사라졌다. 나머지 축은 전류 게인으로 잡았다 — Roll #1 은 1.0, #5 는 1.2, #4 는 0.3 까지 내렸다. 툴이 자동 계산한 값이 늘 정답은 아니다.

    통신과 팔로잉 에러

    트리밍 모터가 CAN 알람을 냈다. 원인은 제어가 아니라 배선이었다. OUT 으로 들어간 선을 IN 으로 바꾸니 끝났다. 현장에서 반나절을 쓴 문제가 대개 이런 것이다.

    Lenze 키패드 Drive cannot follow 경고
    <그림 4> 현장에서 가장 많이 본 화면

    Drive cannot follow 는 지령을 따라가지 못한다는 뜻이다. 핀치롤에서 반복되길래 팔로우 허용 범위를 50 에서 200 으로 넓혔다. 근본 해결은 아니지만, 라인을 세우지 않고 원인을 좁혀 갈 수 있게 된다.

    와인더 텐션

    감는 일은 도는 일과 다르다. 지름이 계속 변하기 때문이다.

    n = v / (π · D) — 회전수

    M = F · D / 2 — 지령 토크

    v 는 라인 속도, D 는 롤의 현재 지름, F 는 장력이다. 소재가 감길수록 D 가 커지므로, 같은 선속도를 지키려면 회전수를 지름에 반비례해 줄여야 하고, 같은 장력을 지키려면 토크를 지름에 비례해 키워야 한다. 두 식이 서로 반대 방향으로 움직이는 셈이다.

    그래서 센터 와인더 제어의 핵심은 “지금 지름이 얼마인가” 하나로 모인다. 9400 은 라인 속도와 와인더 회전수로 D 를 실시간 역산한다. 이 값이 틀어지면 장력도 속도도 함께 틀어진다. 아래 증상들은 대부분 여기서 비롯됐다.

    증상조치
    초반에 우는 소리공칭 장력 3,000 N → 6,000 N
    중간에 멈춤라인 스피드 오프셋 50 % → 25 %
    지름 보상이 멎음Tension_Mode OFF + bEnableWinder ON
    미터 카운터 오차오차가 일정한 10 % 라 그만큼 시프트

    프릭션 보상은 속도 제어 모드에서만 동작한다. 65 단계로 보상치를 자동 계산해 커브를 만든다. 이 사실을 모르고 토크 모드에서 돌려 한참을 헤맸다. 튜닝은 라인이 서는 밤에 했다.

    마지막 며칠은 한국에서 원격으로 붙었다. 와인더 2번 지령 토크를 절반으로 낮춰 감아 보라 했더니 잘 감겼다고 했다. 현장을 떠난 뒤에도 일은 이어진다.

  • 651MB IGES를 Blender 디지털 트윈으로 — 1.5톤 리프트 시스템

    651MB IGES를 Blender 디지털 트윈으로 — 1.5톤 리프트 시스템

    651 MB 짜리 SolidWorks IGES 파일을 OpenCASCADE 로 읽어 삼각 메쉬로 바꾸고, Blender 씬으로 세워 전시용 렌더까지 뽑은 과정을 정리한다. 부품 1,533 개, 삼각형 346 만 개 규모다. 디지털 트윈의 첫 단계인 “형상을 정확히 옮기는 일”에서 실제로 부딪힌 함정들을 함께 적는다.

    대상 모델1.5톤 리프트 시스템 (SolidWorks 출력 IGES)
    입력 파일651 MB, 엔티티 1,606,806 개
    변환 도구Python 3.9, cadquery-ocp 7.7.2 (OpenCASCADE)
    렌더Blender 5.2 LTS, EEVEE Next, AgX
    산출물mesh_lift.npz, lift_scene.blend, 렌더 6 컷

    1. 머리말

    설비의 디지털 트윈을 만들려면 먼저 형상이 있어야 한다. 그런데 설계에서 넘어오는 파일은 대개 중립 포맷이다. STEP 이면 다행이고, IGES 면 일이 는다.

    이번 모델은 파일 이름이 ...STEP 으로 끝나는 폴더에 들어 있었으나 실제로는 IGES 였다. 확장자가 .IGS 였고 열어 보니 내용도 IGES 였다. 설계 쪽에서 “중립 파일” 이라는 뜻으로 폴더 이름을 그렇게 붙인 듯하다. 파일은 항상 이름이 아니라 내용으로 판단해야 한다.

    IGES 가 까다로운 이유는 다음과 같다.

    • 솔리드(B-rep 셸) 구조 없이 곡면이 그냥 나열되어 있는 경우가 많다.
    • 어셈블리 계층이 없어 “어디까지가 한 부품인가” 를 파일이 알려 주지 않는다.
    • 색은 곡면 엔티티에 개별로 붙어 있어, 형상과 색을 따로 읽어 짝지어야 한다.

    2. 파일을 열어 구조부터 본다

    변환 코드를 짜기 전에 파일이 어떤 모양인지부터 확인했다. OpenCASCADE 의 IGESControl_Reader 로 읽고 엔티티 종류를 세어 보면 된다.

    <표 1> 입력 IGES 의 구조

    항목값의미
    전체 엔티티1,606,806곡면, 곡선, 점, 색 정의 등 전부
    트림 곡면 (타입 144)83,480실제 형상을 이루는 면
    루트(root)1전체가 서브피겨 하나로 묶여 있음
    색 정의 (타입 314)51 종SolidWorks 외관색

    여기서 중요한 것은 루트가 1 개라는 점이다. 보통이라면 리더의 TransferRoots() 를 호출해 통째로 변환하면 된다. 그런데 이 파일은 전체가 서브피겨(308/408) 하나로 묶여 있어서, 통째로 변환하면 덩어리 하나가 나온다. 부품 1,533 개가 구분되지 않는다.

    게다가 통째로 변환하면 곡면마다 붙어 있던 색이 사라진다. 리프트의 프레임 도장색, 스테인리스 가공품, 바닥 안전 표시 노란색을 그대로 살리려면 색이 필요하다.

    그래서 트림 곡면을 하나씩 직접 변환하는 쪽을 택했다.


    3. 곡면을 하나씩 변환하기

    (1) 왜 CAF 를 쓰지 않았나

    색까지 함께 읽는 표준적인 방법은 IGESCAFControl_Reader 로 XDE 문서에 올리는 것이다. 그런데 이 PC 의 cadquery-ocp 7.7.2 에는 IGESCAFControl 모듈이 없었다. 빌드에서 빠져 있었다.

    차선책으로 model.Number(entity) 나 reader.TransferEntity() 를 써 보려 했으나, 이 바인딩에서는 Standard_Transient 를 인자로 받는 함수들이 타입 오류를 냈다. 결국 엔티티 자신의 메서드로 우회했다.

    lst = rd.GiveList("iges-type(144)")          # 트림 곡면만 골라낸다
    cs = IGESToBRep_CurveAndSurface()            # 곡면 -> BRep 저수준 변환기
    cs.SetModel(model)
    
    for k in range(1, n144 + 1):
        e = lst.Value(k)
        rgb = DEFAULT_RGB
        rc = e.RankColor()                       # >0 이면 표준색 번호
        if rc < 0:                               # <0 이면 색 엔티티(314) 참조
            r, g, b = e.Color().RGBIntensity()   # 0~100 스케일
            rgb = (r / 100.0, g / 100.0, b / 100.0)
        elif rc > 0:
            rgb = STD_COLORS.get(rc, DEFAULT_RGB)
        s = cs.TransferGeometry(e)
    

    RankColor() 의 부호가 갈림길이다. 양수면 IGES 표준 8색 중 하나를 가리키는 번호이고, 음수면 별도의 색 정의 엔티티(314)를 가리키는 포인터다. SolidWorks 외관색은 대부분 후자이며, RGBIntensity() 가 0~100 스케일로 값을 돌려주므로 100 으로 나눠야 한다. 255 로 나누면 모든 색이 거의 검게 나온다.

    (2) 결정적인 함정 — 형상 치유

    ★주 1) 직접 변환은 리더의 형상 치유를 거치지 않는다.
    TransferRoots() 로 통째로 변환하면 OpenCASCADE 가 내부적으로 ShapeProcess 를 돌려 형상을 다듬는다. 그런데 TransferGeometry() 로 곡면을 직접 변환하면 그 과정이 생략된다. 처음 이 사실을 모르고 돌렸을 때, 테셀레이션 결과에서 면의 45% 가량이 통째로 비었다. 특히 회전곡면(타입 120) 기반의 면 — 파이프, 축, 원통 — 이 거의 전부 사라졌다. 화면에는 사각 프레임만 남고 둥근 것은 하나도 없었다.

    해결은 두 줄이다.

    s = cs.TransferGeometry(e)
    BRepLib.BuildCurves3d_s(s)      # 파라미터 곡선만 있는 엣지에 3D 곡선을 만들어 준다
    fx = ShapeFix_Shape(s)          # 허용오차, 방향, 끊어진 와이어를 다듬는다
    fx.Perform()
    s = fx.Shape()
    

    BuildCurves3d_s() 가 핵심이다. IGES 의 트림 곡면은 경계 곡선이 곡면 파라미터 공간(uv)에만 정의되어 있는 경우가 흔한데, 메쉬 생성기는 3D 공간의 곡선을 요구한다. 이것이 없으면 면은 유효해 보이지만 삼각형이 하나도 생기지 않는다.

    두 줄을 넣은 뒤 변환 실패 0 건, 삼각형이 생긴 면 83,471 / 83,480 이 되었다. 빠진 9 개는 면적이 0 에 가까운 퇴화면이라 무시해도 좋다.


    4. 테셀레이션

    면을 모두 모아 컴파운드로 묶고 한 번에 테셀레이션한다. 면마다 따로 돌리는 것보다 훨씬 빠르고, 병렬 옵션을 쓸 수 있다.

    LIN_DEFLECTION = 0.8      # mm  - 곡면과 삼각형 사이의 최대 거리
    ANG_DEFLECTION = 0.35     # rad - 인접 삼각형 법선의 최대 각도차
    
    BRepMesh_IncrementalMesh(comp, LIN_DEFLECTION, False, ANG_DEFLECTION, True)
    #                                              ^ 상대오차 아님   ^ 병렬
    

    전체 치수가 9.5 m 인 설비에서 0.8 mm 는 넉넉한 값이다. 전시용 렌더가 목적이므로 이 정도면 곡면이 각져 보이지 않는다. 각도 편차 0.35 rad(약 20°)는 작은 구멍이나 필렛이 다각형으로 뭉개지는 것을 막아 준다.

    두 값을 절반으로 줄이면 삼각형이 3~4 배로 늘어난다. 메모리와 Blender 반응 속도를 생각하면 먼저 거칠게 뽑아 형상을 확인하고, 필요한 부분만 조이는 편이 낫다.


    5. 부품 단위로 묶기

    가장 머리를 쓴 부분이다. IGES 에는 “이 면들이 한 부품” 이라는 정보가 없다. 83,471 개의 면이 평평하게 나열되어 있을 뿐이다. 이대로 Blender 에 넣으면 오브젝트가 8 만 개가 되어 씬을 열 수조차 없다.

    착안점은 단순하다. 같은 부품의 면들은 꼭짓점을 공유한다. 서로 다른 부품은 닿아 있어도 꼭짓점이 정확히 일치하지 않는다. 그러니 꼭짓점을 공유하는 면들을 이어 붙이면 부품이 된다. 전형적인 서로소 집합(union-find) 문제다.

    SNAP = 0.01                                   # mm, 꼭짓점 동일 판정 격자
    
    key = np.round(allv / SNAP).astype(np.int64)  # 0.01mm 격자로 양자화
    _, inv = np.unique(key, axis=0, return_inverse=True)
    
    order = np.argsort(inv, kind="stable")        # 같은 꼭짓점끼리 모은다
    inv_s, own_s = inv[order], owner[order]
    same = inv_s[1:] == inv_s[:-1]
    a, b = own_s[:-1][same], own_s[1:][same]      # 꼭짓점을 공유하는 면 쌍
    pairs = np.unique(np.stack([a[diff], b[diff]], axis=1), axis=0)
    
    dsu = DSU(nf)
    for x, y in pairs:
        dsu.union(int(x), int(y))
    

    꼭짓점 비교를 부동소수점으로 하면 미세한 오차 때문에 같은 점이 갈린다. 그래서 0.01 mm 격자로 반올림한 정수 키를 만들어 비교한다. 이 값이 너무 크면 닿아 있는 별개 부품이 하나로 붙고, 너무 작으면 한 부품이 쪼개진다. 0.01 mm 는 기계 가공 공차보다 작고 테셀레이션 오차보다는 커서 잘 맞았다.

    비교 자체를 numpy 의 정렬로 처리한 덕에, 300 만 개 꼭짓점에서 공유 쌍 188,763 개를 찾는 데 몇 초면 끝난다. 이중 루프로 돌렸다면 하루가 걸렸을 것이다.

    결과는 부품 1,533 개였다. 실제 조립도의 부품 수와 자릿수가 맞는다.


    6. 변환 결과

    <표 2> 변환 수치 (총 241 초)

    단계결과경과
    파일 읽기엔티티 1,606,80616.7 s
    곡면 변환면 83,480 수집, 실패 0, 색 51 종188.6 s
    테셀레이션linear 0.8 mm / angular 0.35 rad212.4 s
    메쉬 추출삼각형 있는 면 83,471 / 83,480232.8 s
    부품 묶기공유 꼭짓점 쌍 188,763 → 부품 1,533236.4 s
    저장정점 3,059,633 / 삼각형 3,464,489241.1 s

    전체 치수는 9,464 × 4,980 × 6,495 mm 로 나왔다. 도면과 대조해 일치하는 것을 확인했다. 치수 검증은 반드시 해야 한다. IGES 단위 설정이 어긋나면 1,000 배 차이가 나는데, 렌더만 보고는 알아채기 어렵다.

    결과는 mesh_lift.npz 한 파일로 저장한다. 정점·삼각형 배열과 부품별 시작 인덱스, 부품별 바운딩 박스와 대표색을 함께 담았다. 651 MB 입력이 24 MB로 줄었다.


    7. Blender 씬 세우기

    (1) 방향과 바닥 찾기

    CAD 에서 온 모델은 어느 축이 위인지 알 수 없다. SolidWorks 는 Y-up 이 기본이지만 이 모델은 이미 Z-up 이었다. 확인하는 가장 빠른 방법은 양쪽으로 한 번씩 뽑아 보는 것이다. Workbench 엔진으로 부품별 랜덤색 프리뷰를 만들면 몇 초면 나온다.

    부품별 랜덤색 Workbench 프리뷰
    <그림 1> 형상·방향 확인용 프리뷰. 부품마다 다른 색을 주면 묶기가 제대로 되었는지도 함께 보인다

    바닥 높이는 베이스 레일 하단인 IGES 좌표 Z = 2,776 mm 로 잡았다. 이 값을 기준으로 그 아래로 내려가는 부품을 세면 피트(pit) 부품 99 개가 자동으로 걸러진다. 개구부 범위와 깊이 2.61 m 도 그 부품들의 바운딩 박스에서 바로 나온다. 사람이 눈으로 고르지 않아도 된다.

    (2) 재질

    SolidWorks 외관색 중 의미 있는 색 33 종은 그대로 살렸다. 문제는 설계자가 색을 지정하지 않아 기본 미색으로 남은 부품들이다. 이것들을 전부 같은 회색으로 두면 밋밋해진다.

    그래서 부품 크기를 기준으로 재질을 나눴다. 큰 것은 프레임 도장, 중간은 가공 금속, 얇고 넓은 것은 강판, 작은 것은 소물 — 네 가지다. 완벽하지는 않지만 “큰 구조물은 도장, 작은 부품은 금속” 이라는 경험칙이 실제 설비와 대체로 맞아서, 손으로 지정한 것에 가까운 결과가 나왔다.

    (3) 렌더

    EEVEE Next 로 64 샘플, 색 변환은 AgX 를 썼다. AgX 는 금속 하이라이트가 흰색으로 타버리는 것을 막아 준다. 스테인리스가 많은 설비에서는 체감 차이가 크다.

    1.5톤 리프트 시스템 전체 렌더
    <그림 2> 메인 아이소 뷰. 프레임 도장색, 스테인리스 가공품, 바닥 안전 표시 노란색이 모두 원본 외관색이다

    피트 내부는 바닥면을 숨긴 컷어웨이로 따로 뽑았다. 스크류 잭과 구동 샤프트, 기어박스의 배치가 드러난다. 이런 그림은 설비 설명 자료에서 특히 쓸모가 있다.

    피트 내부 컷어웨이 렌더
    <그림 3> 피트 컷어웨이. 승강 구동부 구조가 보인다

    8. 정리

    요점을 다시 추린다.

    1. 파일은 이름이 아니라 내용으로 판단한다. STEP 폴더 안의 IGES 였다.
    2. 루트가 하나로 묶인 IGES 는 통째로 변환하면 덩어리가 되고 색도 잃는다. 곡면을 하나씩 변환해야 색을 짝지을 수 있다.
    3. 직접 변환하면 형상 치유가 생략된다. BuildCurves3d_s() 와 ShapeFix_Shape 를 반드시 넣는다. 안 넣으면 회전곡면이 통째로 사라진다.
    4. 부품 구분이 없으면 공유 꼭짓점 + union-find 로 묶는다. 비교는 정수 격자로 양자화해 numpy 정렬로 처리한다.
    5. 치수를 도면과 대조한다. 단위 오류는 렌더만 봐서는 모른다.
    6. 바닥 높이 하나만 정하면 피트 부품, 개구부, 깊이가 자동으로 계산된다.

    여기까지가 디지털 트윈의 절반이다. 형상은 정확히 옮겨졌지만 아직 움직이지 않는다. 다음은 승강 리그를 붙이고 PLC 의 D 레지스터 값을 물려 실제 설비와 같이 움직이게 하는 일이다. PLC 쪽 통신은 Melsec PLC 데이터 엑세스하기에 정리해 두었다. 두 갈래가 만나면 트윈이 된다.

  • 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 명