Phase 4까지는 팔(fk_2link_demo) 하나로 "잡기"만 다뤘다. 이번에 최종 목표를 다시 정리하면서 — 이족보행 대신 실내 가사 보조 모바일 매니퓰레이터(바퀴형)로 스코프를 확정했고, 그러면 Phase 5(이동)가 로드맵과 정확히 맞아떨어진다는 걸 확인했다. 이번 세션에서 이동(SLAM/Nav2) 세 단계 — 수동 주행(5-1), 지도 만들기(5-2), 자율 주행(5-3) — 를 한 번에 끝까지 밀어붙였다.
① 팔과 분리된 새 바퀴 로봇 (Phase 5-1)
기존 two_link_arm엔 바퀴가 없다(팔은 world_to_base fixed 조인트로 바닥에 고정된 구조). 이동 개념(바퀴 구동, 오도메트리, LiDAR)만 깨끗하게 배우려고, 팔과 완전히 분리된 새 패키지 mobile_base_demo를 만들었다.
base_link: 차체(박스, 0.30×0.22×0.10m) — 팔과 달리 world에 고정 안 함, 바퀴 구동+마찰로 실제로 움직이는 자유물체left/right_wheel_link: continuous 조인트,gazebo_ros_diff_drive플러그인이cmd_vel을 받아 두 바퀴 속도로 변환front/back_caster_link: 마찰 0(mu1=mu2=0)인 구 — 조향은 안 하고 앞뒤 지지점 역할만lidar_link: 2D LiDAR(360도, 최대 8m) — SLAM/Nav2가 쓸/scan
팔(Phase 2~4)은 gazebo_ros2_control+joint_trajectory_controller 구조였는데, 바퀴 로봇은 gazebo_ros_diff_drive 플러그인이 cmd_vel을 직접 받는 훨씬 단순한 방식을 썼다. Nav2/teleop 생태계가 기본으로 기대하는 인터페이스라 이게 표준적이다.
fk_2link_demo의 room.world(4m×4m, 남쪽 벽 출입구)에 그대로 스폰해서 teleop_twist_keyboard로 수동 주행해봤다. /odom, /scan 둘 다 정상 발행되고, RViz2에서 LaserScan이 벽 모양과 정확히 겹쳐 보였다.
(수동 주행 중 RViz — LaserScan이 room.world 벽 모양과 정확히 겹쳐 보임)

② SLAM으로 지도 그리기 (Phase 5-2)
slam_toolbox(online_async)를 붙여서 /scan+오도메트리로 room.world를 실제로 매핑했다. 설정 자체는 표준적이었는데, 이번엔 환경(WSLg) 쪽에서 계속 발목을 잡혔다.
창이 안 뜨는 WSLg
Gazebo/RViz를 띄웠는데 로그엔 에러가 하나도 없고(GLRenderSystem::_createRenderWindow 성공, 프로세스도 CPU를 정상적으로 소모하며 렌더링 중) — 그런데 창이 최소화된 채로 화면에 안 나타났다. msrdc.exe(WSLg 디스플레이 클라이언트) 재시작으로도 안 됐고, 결국 wsl --shutdown으로 WSL2 전체(WSLg 포함)를 완전히 재시작하고서야 해결됐다. 이 과정에서 VSCode의 WSL 원격 연결도 같이 끊겼는데, VSCode를 껐다 켜서 재연결했다.
RViz Map 디스플레이 빠뜨리기
Fixed Frame을 map으로 바꿨는데도 아무것도 안 그려져서 한참 헤맸다 — 알고 보니 Displays 패널에 Map 항목 자체를 안 추가한 상태였다. Add 창의 "By display type" 트리가 접혀 있어서 놓친 것 — Phase 5-1의 RobotModel 때도 똑같이 겪었던 UI 함정이라, 이제 패턴으로 확실히 기억해뒀다.
방 절반만 돌고 저장한 지도
map_saver_cli로 처음 저장한 지도를 파이썬으로 픽셀 히스토그램/컬러 시각화해서 확인해봤더니, 남쪽·서쪽 벽만 점유(빨강)로 잡히고 북쪽·동쪽은 미탐지 상태였다. 방 전체를 안 돌고 시작 위치 근처만 주행하다 저장한 거였다 — slam_toolbox 노드를 새로 띄울 필요 없이(계속 살아있었음) teleop으로 이어서 방 전체를 마저 돌고 재저장해서 해결했다.
(최종 저장된 지도 — 4면 벽이 닫힌 사각형으로, 남쪽 벽 출입구도 V자 모양으로 정확히 반영됨)

③ Nav2로 자율 주행 (Phase 5-3)
저장한 지도로 nav2_bringup의 bringup_launch.py + 직접 쓴 nav2_params.yaml을 띄워서 AMCL 위치추정 → "2D Nav Goal" 자율 주행까지 시도했다. 여기서부터가 이번 세션에서 제일 오래 걸린 구간이었다 — 파라미터 파일 하나에 실제로 겪은 버그가 다섯 종류나 나왔다.
pluginlib 이름은 추측하지 말고 직접 읽어라
Nav2 파라미터에서 플러그인은 "패키지::클래스" 또는 "패키지/클래스" 두 표기가 패키지마다 다르게 쓰인다. nav2_navfn_planner::NavfnPlanner(오답)라고 썼다가 에러 메시지가 정답(nav2_navfn_planner/NavfnPlanner)을 그대로 알려줬고, nav2_behaviors도 같은 이유로 틀렸다. 두 번 틀리고 나서는 추측을 그만두고 WSL2에 설치된 pluginlib 등록 파일(/opt/ros/humble/share/<pkg>/*.xml)을 직접 읽는 걸로 전략을 바꿨다 — name 속성이 명시된 클래스만 / 표기가 정답이고, 없으면 type(:: 표기)과 같다는 패턴을 확인했다.
예시 파일이 실제 설치 버전보다 최신이었던 경우
bt_navigator엔 BT 액션/컨디션 노드 목록(plugin_lib_names)이 필요한데, nav2_bringup에 실제로 설치된 공식 참고 파라미터 파일(nav2_multirobot_params_all.yaml)을 그대로 가져다 썼더니 그중 4개(nav2_are_error_codes_active_condition_bt_node 등)가 .so 파일 자체가 없다며 실패했다. /opt/ros/humble/lib/libnav2_*_bt_node.so 실제 목록과 대조해서 없는 것만 골라 뺐다 — 참고 yaml(share/)이 실제 컴파일된 바이너리(lib/)보다 최신 버전 기준으로 작성돼 있었던 것.
RewrittenYaml은 없는 키를 새로 만들어주지 않는다
제일 오래 걸린 문제. map_server가 parameter 'yaml_filename' is not initialized로 계속 죽었다. nav2.launch.py에서 map launch argument는 분명히 넘기고 있었는데도였다. nav2_common의 RewrittenYaml 소스 코드(rewritten_yaml.py)를 직접 읽고서야 이유를 알았다 — 이 치환 메커니즘은 yaml 파일에 이미 존재하는 leaf key만 찾아서 값을 덮어쓰고, 없는 키는 절대 새로 추가하지 않는다. 내 nav2_params.yaml엔 map_server: 섹션 자체가 없었다("런타임에 자동으로 채워지겠지"라고 생각해서 일부러 생략했었다). 섹션을 추가하고 나서도 여전히 안 됐는데, yaml_filename: ""을 placeholder로 미리 선언해두고 나서야 정상 주입됐다.
map_server:
ros__parameters:
use_sim_time: True
yaml_filename: "" # RewrittenYaml이 여기에 실제 지도 경로를 덮어씀
마지막 관문 — AMCL 위치추정 수렴
map_server가 정상화된 뒤에도 RViz의 map 프레임이 안 나타났는데, 이건 버그가 아니라 AMCL이 초기 위치를 받기 전까진 map→odom TF를 발행하지 않는 정상 동작이었다. "2D Pose Estimate"로 대략적인 위치를 지정했더니 map 프레임은 나타났는데, 초기 추정이 부정확했던 탓에 "2D Nav Goal"을 보내자 AMCL cannot publish a pose 경고와 함께 경로 계획 실패 + 복구 동작(제자리 회전)마저 "Collision Ahead"로 막히는 문제가 생겼다. 지도를 다시 그릴까 고민했지만, 재매핑 대신 teleop으로 로봇을 좀 더 움직여서 여러 각도로 스캔 매칭을 시키는 쪽을 먼저 시도했다 — LaserScan이 지도 벽에 정확히 달라붙고 경고가 멈춘 뒤 재시도하니, 이번엔 목표 지점까지 실제로 자율 주행했다.
(자율 주행 성공 영상 — Gazebo에서 목표 지점까지 경로 계획 후 실제로 이동하는 모습)
다음 편 예고
Phase 5(이동: 수동 주행 → SLAM → Nav2)까지 전부 끝났다. 다음은 Phase 6(음성 STT/LLM)이나 Phase 7(통합) 중에서 방향을 정할 차례다. 참고로 room.world에 남아있는 Phase 4의 소품(target_bottle_stand/target_bottle)은 지도엔 없지만 실시간 LaserScan엔 장애물로 잡히는데, 이건 급한 문제는 아니라 Phase 7 통합 시점으로 미뤄뒀다.
'로봇 > ROS2 공부' 카테고리의 다른 글
| [ROS2 공부 Phase 4-2] 그립이 자꾸 미끄러진다면 — 물체를 각지게 바꿔보기 (0) | 2026.09.03 |
|---|---|
| [ROS2 공부 Phase 4-1] 그리퍼 달고 MoveIt2로 물체 잡으러 가기 (버그 사냥기) (0) | 2026.09.02 |
| [ROS2 공부 Phase 3-1, 3-2] 손목 카메라 달고 YOLO로 물체 찾아보기 (0) | 2026.08.29 |
| [ROS2 공부 Phase 2-2] 방 world 만들고, RViz2+Gazebo 동시 확인하고, 계속 움직여보기 (0) | 2026.08.29 |
| [ROS2 공부 Phase 2-1] Gazebo 시뮬레이션 첫걸음 — 용어 정리 + 삽질 기록 (0) | 2026.08.25 |








