Phase 5(수동 주행 → SLAM → Nav2)까지 끝내고, 로드맵대로 Phase 6(음성 STT/LLM)에 들어갔다. 다만 마이크 입력은 WSL2에서 오디오 패스스루가 되는지부터 불확실한 리스크라, 이번엔 음성을 빼고 "텍스트 명령 → LLM 해석 → Nav2 액션"부터 만들어보기로 스코프를 좁혔다.
① 입력과 해석을 토픽으로 분리
새 패키지 voice_control_demo를 만들면서, /text_command(std_msgs/String) 토픽을 명령 해석 노드의 유일한 입력으로 뒀다. 지금은 터미널에 타이핑한 문장을 그대로 이 토픽에 발행하는 text_command_publisher 노드가 붙어있지만, 나중에 STT가 준비되면 이 노드만 통째로 갈아끼우면 되고 해석 로직(command_interpreter_node)은 손댈 필요가 없다. Phase 5-1에서 팔과 바퀴 로봇을 분리했던 것과 같은 이유 — 한 세션에서 여러 변수를 동시에 바꾸면 문제가 생겼을 때 원인을 못 좁힌다.
② Claude API로 시작했다가 로컬 LLM으로 갈아탄 이야기
처음엔 Claude API(claude-opus-5)로 짰다. navigate_to_waypoint라는 도구 하나만 정의해서, LLM이 "주방으로 가" 같은 문장을 보고 등록된 장소 이름 중 하나를 골라 호출하게 하는 구조였다.
코드를 다 쓰고 보여줬더니 "이거 API 키 돈 드는 거 아니냐"는 질문이 돌아왔다. 맞다 — Claude API는 구독과 별개로 항상 종량제 과금이다. 설명했더니 "무료 모델을 쓰고 싶다"는 답이 왔고, 그 자리에서 완전 무료·오프라인인 로컬 LLM(Ollama)으로 방향을 틀었다. 사실 이 선택지는 원래 "WSL2 리소스 제약상 속도/정확도가 떨어질 수 있다"는 이유로 뒤로 밀려있던 옵션이었는데, 이번엔 비용이 그 걱정보다 우선순위가 높다고 판단한 거다.
response = self._client.chat(
model=self._model, # qwen2.5:3b
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': text},
],
tools=[self._tool],
)
tool_calls = response.message.tool_calls or []
Ollama 파이썬 클라이언트도 OpenAI 스타일의 도구 호출(tool calling)을 지원해서, 코드 구조 자체는 Claude API 버전과 거의 그대로 유지됐다. 다만 Claude의 strict: true 같은 스키마 강제가 없어서, 로컬 소형 모델이 등록 안 된 장소 이름을 지어낼 가능성을 감안해 방어 코드를 넣어뒀다.
③ WSL2에서 실제로 부딪힌 것들
여기서부터가 이번 세션의 대부분이었다. Nav2 자체(Phase 5-3)는 이미 검증됐지만, 새로 붙인 것들에서 계속 문제가 터졌다.
RViz Map이 "No map received"
map_server는 지도를 시작할 때 딱 한 번, Transient Local(latched) 방식으로 발행한다. RViz의 Map 디스플레이가 QoS를 안 건드리면 Volatile로 구독하는데, 그러면 이미 지나간 그 한 번의 발행을 영영 못 받는다 — Remove/Add로 디스플레이를 새로 만들어도 똑같이 Volatile로 다시 구독하니 안 고쳐진다. ros2 topic echo /map --once로 서버는 멀쩡하다는 것부터 확인한 뒤, Map 디스플레이의 Durability Policy를 Transient Local로 수동 지정하고서야 해결됐다.

🖼️ (RViz Map 디스플레이 — Durability Policy가 Volatile로 되어있어 "No map received" 경고가 뜬 상태)
구석에 낀 로봇, 그리고 Gazebo Reset의 함정
2D Nav Goal을 벽에 너무 붙여서 찍었더니 로봇이 구석에서 못 빠져나오는 상황이 왔다. behavior_server가 Pose Goes Off Grid 에러를 뱉으면서 Spin 복구 동작마저 실패를 반복했다 — 좁은 구석에서는 회전 궤적을 체크할 공간조차 없었던 것.
급한 마음에 Gazebo의 Reset 버튼을 눌렀는데, 이게 훨씬 큰 문제를 만들었다. 시뮬레이션 시간이 되감기면서 AMCL/costmap이 이미 캐싱해둔 TF 기록보다 더 이른 시각의 데이터가 들어오기 시작했고, Tf has two or more unconnected trees라는 에러와 함께 map→odom, odom→base_link 두 변환이 서로 끊어져버렸다. 부분 복구는 불가능해서 Gazebo+Nav2를 전부 끄고 처음부터 재기동해야 했다.
교훈: 로봇이 갇히면 Gazebo 리셋 대신 목표부터 취소하는 게 맞다.
ros2 service call /navigate_to_pose/_action/cancel_goal action_msgs/srv/CancelGoal "{}"
그다음 teleop_twist_keyboard로 빼내면 시뮬레이션 시간을 안 건드리고 안전하게 해결된다.
그 외 자잘한 것들
- RViz2가 뜨는 셰이더 링크 에러(
indexed_8bit_image관련)는 WSLg의 소프트웨어 렌더링 한계로 나는 노이즈일 뿐, 기능엔 영향 없어서 무시해도 됐다. - 터미널에서
Ctrl+Z는 끄는 게 아니라 멈추는 것(백그라운드 job)이라, 계속 살아있는 프로세스를 그냥 방치할 뻔했다 —fg+Ctrl+C로 제대로 종료해야 한다.
④ 첫 성공, 그리고 숨어있던 로그 버그
이 모든 걸 정리하고 나서 waypoints.yaml의 세 지점(entrance/center/table)을 2D Nav Goal + /amcl_pose로 실측해 반영했다. teleop으로 손수 몰지 않고, 이미 동작하는 Nav2로 목표를 보낸 뒤 도착 후 좌표를 읽는 게 훨씬 정확하고 빨랐다.
그리고 마침내 text_command_publisher에 "입구로 가"를 입력했더니, 로컬 LLM이 정확히 navigate_to_waypoint(waypoint="entrance")를 호출했고 로봇이 실제로 그 지점까지 자율 주행했다.

🖼️ (터미널 — "입구로 가" 명령이 로컬 LLM 해석을 거쳐 실제 Nav2 이동으로 이어지고 "이동 완료"까지 찍힌 로그)
기세를 몰아 "테이블로 가"/"중앙으로 가"도 시켜봤는데, 로그엔 "이동 완료"가 떴지만 뭔가 이상했다 — 남은 거리가 계속 0에서 안 바뀌다가 바로 완료로 넘어간 것. nav2.launch.py 터미널을 확인해보니 실제로는 GridBased: failed to create plan으로 경로 계획 자체가 실패하고 있었다. 원인은 코드에 있었다: _on_result() 콜백이 목표 상태(status)를 확인 안 하고 무조건 "이동 완료"라고 로그를 찍고 있었던 것 — 실패(ABORTED)해도 성공처럼 보인 거다. action_msgs.msg.GoalStatus로 상태를 구분해서 실패 시엔 명확히 에러로 남기도록 고쳤다. (table/center 좌표 자체가 벽/장애물에 가까워서 생긴 문제로 보이는데, 이건 다음에 좌표를 다시 잡아서 확인해야 한다.)
다음 편 예고
table/center waypoint를 다시 잡아서 재검증하는 게 남았고, Phase 6-2(팔/그리퍼까지 자연어로 확장)로 갈지 음성 입력(STT)에 먼저 도전할지는 아직 정하지 않았다. 겸사겸사 실제 로봇 업계(Tesla Optimus, Figure AI 등)가 요즘 어떤 방향으로 가는지도 좀 찾아봤는데 — 이건 다음에 따로 정리해볼 생각이다.
'로봇 > ROS2 공부' 카테고리의 다른 글
| [ROS2 공부 Phase 5-1, 5-2, 5-3] 바퀴 로봇 만들고, 지도 그리고, 혼자 돌아다니게 하기 (0) | 2026.09.05 |
|---|---|
| [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 |










