Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
- 아키텍처: 맥 네이티브 개발(RoboStack) ↔ zenoh 브리지(TCP 7447) ↔
Lima VM Ubuntu 22.04 arm64(MoveIt·Gazebo) ↔ Foxglove(ws 8765),
양측 rmw_cyclonedds_cpp 고정 (D9)
- 함정 DB: 실측 known issues 28건 (`docs/plan/known-issues.md`)
- 함정 DB: 실측 known issues 30건 (`docs/plan/known-issues.md`)
- 진단·제보: `doctor --fix`(안전 항목 자동 수리), `report`(진단 번들 tar.gz,
`~/.rosmac` 밖 수집 금지) (Phase 5)
- **실로봇 연결 (beta)**: `robot:` config 섹션 — 맥 브리지가 로봇 쪽
Expand All @@ -35,6 +35,14 @@
- 업그레이드 경로: 버전/sha 핀을 config에 동결하지 않음(커스텀 핀만 보존) +
`up`/`init`이 브리지 바이너리 버전을 비교해 자동 갱신 — pip 업그레이드만으로
신 핀 반영 (E.7)
- **Nav2 프리셋 (`sim nav2-diffbot`)**: Gazebo diffbot + gpu_lidar + slam_toolbox
+ Nav2(풀스택, 기본 브리지), 맥에서 `/navigate_to_pose` goal 3/3 SUCCEEDED.
`rosmac sim`이 시작 시 브리지 세션을 리셋해 이전 스택 라우트 잔재로 인한 액션
디스커버리 실패를 방지(KI-17). launch 신뢰성 stagger 타이밍. `viz --layout nav2`.
- **프리셋 `mac_env_pkgs`**: 맥에서 액션 goal을 보내는 데 필요한 msg 패키지를
`rosmac sim`이 맥 conda env에 자동 설치(멱등, `deps.ensure_installed`). nav2는
nav2_msgs — 없으면 goal이 "server not available"로 조용히 실패하던 함정 제거.
(E.17/E.20)

### 실측 검증 (Phase 0/2/4 게이트)
- 브리지 대역폭 10.3 MB/s (1MB@10Hz 무손실), MoveGroup 액션 왕복 3연속 SUCCEEDED
Expand Down
19 changes: 15 additions & 4 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ rosmac은 문제와 싸우는 대신 문제를 분할한다:
설치 스크립트 이상인 이유:

- **`rosmac doctor`** — 알려진 고장 모드 16항 점검, `--fix`로 안전한 항목 자동 수리
(hung ros2 데몬, 고아 브리지, 깨진 lima 포트 규칙). **실측 함정 29개 DB** 위에서
(hung ros2 데몬, 고아 브리지, 깨진 lima 포트 규칙). **실측 함정 30개 DB** 위에서
만들어졌다.
- **`rosmac deps`** — 워크스페이스 `package.xml` 의존성을 RoboStack conda 패키지로
매핑 (conda를 아는 rosdep 대체).
Expand Down Expand Up @@ -73,10 +73,16 @@ rosmac shell -c 'ros2 topic echo /chatter --once' # 맥에서 VM 토픽 수신
```bash
rosmac sim panda-moveit # MoveIt(Panda 팔) — 맥에서 /move_action 사용 가능
rosmac sim gazebo-diffbot # Gazebo Fortress headless + 전방 카메라
rosmac sim nav2-diffbot # Nav2 이동로봇 내비게이션 — 맥에서 /navigate_to_pose
rosmac sim list / status / stop / --attach
rosmac viz --layout panda # Foxglove 연결 (+레이아웃 안내)
rosmac viz --layout nav2 # Foxglove 연결 (+레이아웃 안내)
```

`nav2-diffbot`은 벽 아레나에서 라이다 diffbot에 SLAM+Nav2를 돌린다. `/cmd_vel`로
주행해 지도를 만든 뒤 맥에서 `/navigate_to_pose` goal을 보낸다. Nav2 풀스택도
기본 브리지로 동작한다 — `rosmac sim`이 시작 시 브리지 세션을 리셋해 새 스택이
신선한 라우트를 받게 한다(KI-17).

맥 네이티브 개발 루프와 예제(pick_demo)는 [docs/workflow.ko.md](docs/workflow.ko.md) 참조.

## 내 프로젝트 가져오기
Expand Down Expand Up @@ -119,6 +125,7 @@ exit code 규약:

- 브리지 대역폭: 10.3 MB/s (1MB@10Hz 드랍 없음)
- MoveGroup 액션 왕복: 플래닝+실행 goal 3연속 SUCCEEDED
- 맥에서 Nav2 `/navigate_to_pose`: goal 3연속 SUCCEEDED (기본 브리지)
- Gazebo Fortress headless RTF: 물리만 1.00 / 카메라(320x240@15Hz) 0.99
- 카메라 스트림: VM 14.4fps → 맥 14.4fps (무손실)

Expand All @@ -130,13 +137,17 @@ exit code 규약:
|---|---|---|
| Topics | ✅ | 양방향 pub/sub; 10.3 MB/s @ 10Hz 무손실. 새 토픽의 첫 구독은 라우트 생성에 몇 초 소요 |
| Services | ✅ | 핀된 CycloneDDS RMW 필수 — Fast DDS는 디스커버리는 되는데 모든 호출이 타임아웃 (KI-16; RMW를 핀한 이유) |
| Actions | ✅ | MoveGroup 플래닝+실행 goal 3/3 SUCCEEDED |
| Actions | ✅ | MoveGroup 플래닝+실행 goal 3/3 SUCCEEDED; 맥에서 Nav2 `/navigate_to_pose` 3/3 SUCCEEDED (풀스택, 기본 브리지) |
| Parameters | ⚠️ 부분 | 원시 파라미터 서비스(`get/set_parameters` 등)는 `ros2 service call`로 동작; `ros2 param` CLI는 **불가** — 브리지가 원격 노드를 노드 그래프에 미러링하지 않아 `ros2 node list`에 VM 노드가 안 보임 |
| rosbag2 | ✅ | 맥에서 VM 토픽 녹화(무손실)·VM 내 녹화·양쪽 어디서 재생해도 반대편 도달. VM bag 회수는 `limactl cp -r rosmac:/path ~/dest` (D16) — [docs/workflow.ko.md](docs/workflow.ko.md) 참조 |
| Robot link (LAN) | 🧪 beta | `robot:` 설정 → 맥 브리지가 로봇 쪽 브리지로 TCP 엔드포인트 추가 (D15). 대리 로봇(제2 VM) 실측: 토픽/서비스 10 MB/s @ 10Hz 무손실, 서비스 RTT < 1 ms, 로봇 재시작 자동 재접속. **대리 로봇 검증** — 실기/WiFi 수치는 대기 ([E.15 R5](docs/plan/e15-real-robot.md)). 설치: [docs/robot-setup.ko.md](docs/robot-setup.ko.md). **신뢰 LAN 전용** — 평문 TCP, 인증/TLS 없음 |

구조적 한계 (버그가 아니라 설계):

- **낡은 브리지가 새 스택을 조용히 깨뜨린다 (KI-17).** 브리지를 켜둔 채 VM 시뮬
스택을 재기동하면 죽은 스택의 라우트가 남아, 새 스택의 액션 하위 서비스가 맥에서
디스커버리 안 된다(실측: 0/6 → 브리지 재시작 후 4/4). `rosmac sim`은 시작 시
브리지 세션을 리셋해 이를 방지 — Nav2 풀스택도 기본 브리지로 동작한다(스코핑 불필요).
- **맥↔VM 모든 메시지가 브리지 홉 하나를 건넌다.** 개발·teleop·시각화엔 충분;
고주파 폐루프 제어는 VM 안(또는 로봇 위)에서 완결할 것.
- **UDP ignore 규칙이 없는 *다른* lima VM이 맥 로컬 DDS 디스커버리를 조용히
Expand All @@ -149,7 +160,7 @@ exit code 규약:

결정 로그와 리스크 레지스터: [PLAN.md](PLAN.md).
막히면 [docs/plan/known-issues.md](docs/plan/known-issues.md) — 이 도구의 토대인
실측 함정 29개 DB.
실측 함정 30개 DB.

## 라이선스

Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,16 @@ rosmac shell -c 'ros2 topic echo /chatter --once' # VM topic received on the M
```bash
rosmac sim panda-moveit # MoveIt (Panda arm) — /move_action usable from the Mac
rosmac sim gazebo-diffbot # Gazebo Fortress headless + front camera
rosmac sim nav2-diffbot # Nav2 mobile navigation — /navigate_to_pose from the Mac
rosmac sim list / status / stop / --attach
rosmac viz --layout panda # Foxglove connection (+ layout import guide)
rosmac viz --layout nav2 # Foxglove connection (+ layout import guide)
```

`nav2-diffbot` runs SLAM + Nav2 on a lidar diffbot in a walled arena; drive it
with `/cmd_vel` to build the map, then send `/navigate_to_pose` goals from the Mac.
The full Nav2 stack works over the default bridge — `rosmac sim` resets the bridge
session on start so a new stack gets fresh routes (KI-17).

The native dev loop and a worked example (pick_demo) live in [docs/workflow.md](docs/workflow.md).

## Bring your own workspace
Expand Down Expand Up @@ -121,6 +127,7 @@ Errors are shown as a cause + fix panel; only unexpected errors show a traceback

- Bridge throughput: 10.3 MB/s (1 MB @ 10 Hz, no drops)
- MoveGroup action round-trip: plan+execute, 3 consecutive goals SUCCEEDED
- Nav2 `/navigate_to_pose` from the Mac: 3 consecutive goals SUCCEEDED (default bridge)
- Gazebo Fortress headless RTF: physics-only 1.00 / with camera (320×240 @ 15 Hz) 0.99
- Camera stream: VM 14.4 fps → Mac 14.4 fps (lossless)

Expand All @@ -132,13 +139,18 @@ What works across the Mac ↔ VM zenoh bridge:
|---|---|---|
| Topics | ✅ | pub/sub both directions; 10.3 MB/s @ 10 Hz no drops. First subscription to a new topic takes a few seconds (bridge route creation) |
| Services | ✅ | requires the pinned CycloneDDS RMW — with Fast DDS, discovery looks fine but every call times out (KI-16; why rosmac pins the RMW) |
| Actions | ✅ | MoveGroup plan+execute, 3/3 goals SUCCEEDED |
| Actions | ✅ | MoveGroup plan+execute, 3/3 goals SUCCEEDED; Nav2 `/navigate_to_pose` 3/3 SUCCEEDED from the Mac (full stack, default bridge) |
| Parameters | ⚠️ partial | raw parameter services (`get/set_parameters`, …) work via `ros2 service call`; the `ros2 param` CLI does **not** — the bridge doesn't mirror remote nodes into the node graph, so `ros2 node list` won't show VM nodes |
| rosbag2 | ✅ | record on Mac of VM topics (no loss), record in VM, play from either side reaches the other. Retrieve VM bags with `limactl cp -r rosmac:/path ~/dest` (D16) — see [docs/workflow.md](docs/workflow.md) |
| Robot link (LAN) | 🧪 beta | `robot:` config → Mac bridge adds a TCP endpoint to a robot-side bridge (D15). Topics/services measured against a surrogate robot (2nd VM): 10 MB/s @ 10 Hz no drops, service RTT < 1 ms, auto-reconnect on robot restart. **Surrogate-verified** — real-hardware/WiFi numbers pending ([E.15 R5](docs/plan/e15-real-robot.md)). Setup: [docs/robot-setup.md](docs/robot-setup.md). **Trusted LAN only** — plaintext TCP, no auth/TLS |

Structural limits (by design, not bugs):

- **A stale bridge silently breaks a fresh stack (KI-17).** Restarting the VM
sim stack while the bridge keeps running leaves stale routes behind, so the new
stack's action sub-services fail to discover from the Mac (measured: 0/6, then
4/4 after a bridge restart). `rosmac sim` resets the bridge session on start to
avoid this — the full Nav2 stack then works over the default bridge, no scoping.
- **Every Mac↔VM message crosses one bridge hop.** Fine for dev, teleop and
visualization; high-rate closed control loops belong inside the VM (or on
the robot).
Expand All @@ -152,7 +164,7 @@ Structural limits (by design, not bugs):

Decision log and risk register: [PLAN.md](PLAN.md).
When stuck: [docs/plan/known-issues.md](docs/plan/known-issues.md) — the field-measured
database of 29 pitfalls this tool is built on.
database of 30 pitfalls this tool is built on.

## License

Expand Down
20 changes: 20 additions & 0 deletions docs/workflow.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ feedback/result가 되돌아온다. Foxglove 3D 패널에서 팔이 움직이는
`python -m pdb $(which pick_demo)` 또는 IDE 인터프리터를
`~/micromamba/envs/ros_env/bin/python`으로 지정.

### 이동로봇 내비게이션 (`rosmac sim nav2-diffbot`)

```bash
rosmac sim nav2-diffbot # Gazebo diffbot + 라이다 + SLAM + Nav2, READY까지 대기
rosmac viz --layout nav2 # Foxglove: 지도 + 레이저 스캔 + 계획 경로

rosmac shell # 맥에서:
# /cmd_vel로 한 바퀴 돌려 SLAM이 지도를 만들게 한 뒤:
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: map}, pose: {position: {x: 1.5, y: 1.0}, orientation: {w: 1.0}}}}"
```

goal이 브리지를 넘어 VM의 `/navigate_to_pose`로 가고, 로봇이 플래닝·주행하며
`/map`·`/plan`·`/scan`이 맥으로 돌아와 Foxglove에 그려진다. Nav2 풀스택(서비스
155·액션 12)도 기본 브리지로 동작한다 — 스코핑 불필요. `rosmac sim`이 시작 시
브리지 세션을 리셋해 새 스택이 신선한 라우트를 받게 하며, 이게 없으면 이전 스택의
라우트 잔재가 맥 쪽 액션 디스커버리를 조용히 깨뜨린다(KI-17). 맥에서 goal을
보내려면 맥 env에 `ros-humble-nav2-msgs`가 필요한데, `rosmac sim`이 자동 설치한다
(프리셋의 `mac_env_pkgs`) — 수동 단계 없음.

`rosmac deps`의 한계: **package.xml에 선언된** 의존성만 본다. 코드가 선언 없이
쓰는 실행 파일(launch의 `FindExecutable` 등, KI-26 사례)은 못 잡는다 —
그런 유형은 함정표와 doctor의 영역이다.
Expand Down
21 changes: 21 additions & 0 deletions docs/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ The debugger attaches like plain Python: inside `rosmac shell`, run
`python -m pdb $(which pick_demo)`, or point your IDE interpreter at
`~/micromamba/envs/ros_env/bin/python`.

### Mobile navigation (`rosmac sim nav2-diffbot`)

```bash
rosmac sim nav2-diffbot # Gazebo diffbot + lidar + SLAM + Nav2, wait until READY
rosmac viz --layout nav2 # Foxglove: map + laser scan + planned path

rosmac shell # from the Mac:
# drive a lap with /cmd_vel to let SLAM build the map, then:
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: map}, pose: {position: {x: 1.5, y: 1.0}, orientation: {w: 1.0}}}}"
```

The goal crosses the bridge to the VM's `/navigate_to_pose`; the robot plans and
drives, and `/map` · `/plan` · `/scan` stream back to the Mac for Foxglove. The
full Nav2 stack (155 services, 12 actions) works over the default bridge — no
scoping. `rosmac sim` resets the bridge session on start so the new stack gets
fresh routes; without that, routes left over from a prior stack silently break
Mac-side action discovery (KI-17). Sending goals from the Mac needs
`ros-humble-nav2-msgs` in the Mac env — `rosmac sim` installs it automatically
(the preset's `mac_env_pkgs`), so no manual step.

Limits of `rosmac deps`: it only sees dependencies **declared in package.xml**.
Executables the code uses without declaring them (launch's `FindExecutable`
etc., the KI-26 case) slip through — that class of problem belongs to the
Expand Down
39 changes: 39 additions & 0 deletions src/rosmac/assets/layouts/nav2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"configById": {
"3D!nav2": {
"cameraState": {
"distance": 20,
"phi": 55,
"thetaOffset": 90
},
"followTf": "map",
"scene": {},
"topics": {
"/map": { "visible": true },
"/scan": { "visible": true, "colorField": "intensity", "colorMode": "colormap", "colorMap": "turbo" },
"/plan": { "visible": true },
"/global_costmap/costmap": { "visible": false }
},
"layers": {
"grid": { "layerId": "foxglove.Grid", "visible": true }
}
},
"Plot!vel": {
"paths": [
{ "value": "/odom.twist.twist.linear.x", "enabled": true, "timestampMethod": "receiveTime" },
{ "value": "/odom.twist.twist.angular.z", "enabled": true, "timestampMethod": "receiveTime" }
],
"minYValue": -1,
"maxYValue": 1
}
},
"globalVariables": {},
"userNodes": {},
"playbackConfig": { "speed": 1 },
"layout": {
"first": "3D!nav2",
"second": "Plot!vel",
"direction": "row",
"splitPercentage": 70
}
}
20 changes: 20 additions & 0 deletions src/rosmac/assets/presets/nav2-diffbot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: nav2-diffbot
description: "Nav2 이동로봇 내비게이션 (Gazebo diffbot + gpu_lidar + SLAM, 맥에서 /navigate_to_pose)"
vm_apt:
- ros-humble-navigation2
- ros-humble-nav2-bringup
# 맥에서 /navigate_to_pose goal을 보내려면 맥 conda env에도 msg 패키지가 필요
# (없으면 goal이 "server not available"로 조용히 실패 — E.20).
mac_env_pkgs:
- ros-humble-nav2-msgs
vm_env: {}
# 무스코프 — nav2 풀스택도 기본 브리지로 맥에서 정상 동작한다(S0 실측). 이전에
# 실패로 본 것은 스택 재기동 시 브리지 라우트 잔재(KI-17) 때문이며, `rosmac sim`이
# 시작 시 브리지 세션을 리셋해 해소한다(스코핑 불필요, KI-30 정정).
launch:
cmd: "ros2 launch ~/rosmac-presets/nav2-diffbot/nav2-diffbot.launch.py"
foxglove_layout: nav2.json
# health=/scan (N1: SLAM /map은 0.2m 이동 전 0×0 latch라 부적합). timeout은
# N1 ready 17s + nav2 staggered 기동(+10s) 여유 → 넉넉히 60s.
health_topics:
- { name: /scan, timeout: 60 }
101 changes: 101 additions & 0 deletions src/rosmac/assets/presets/nav2-diffbot/nav2-diffbot.launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""nav2-diffbot 프리셋 headless launch (E.17 N1/N2 실측 확정 구성).

Gazebo Fortress(diffbot + gpu_lidar, 벽 아레나) → ros_gz_bridge →
slam_toolbox(online async, mapping) → nav2_bringup(navigation, autostart).
맥에서 /navigate_to_pose 액션으로 목표 전송(브리지 스코핑 전제 — KI-30),
Foxglove로 map+scan+path 시각화.

N1 실측 확정:
- lidar frame_id=lidar_link(<ignition_frame_id>), 정적 tf base_link→lidar_link
- DiffDrive frame_id=odom child=base_link, odom 50Hz, tf는 /model/vehicle_blue/tf 브리지
- /clock 브리지 + 전 노드 use_sim_time
- nav2 기본 파라미터로 충분(base_link/odom/scan 기본값이 프레임 설계와 일치)
- 기동 타이밍: gz+bridge 먼저 → slam(+5s) → nav2(+10s) (run_spike.sh 순서)
"""

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, TimerAction
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node

PRESET = os.path.expanduser("~/rosmac-presets/nav2-diffbot")
WORLD = os.path.join(PRESET, "nav2_world.sdf")
SLAM_PARAMS = os.path.join(PRESET, "slam_params.yaml")


def generate_launch_description():
pkg_ros_gz_sim = get_package_share_directory("ros_gz_sim")
pkg_slam = get_package_share_directory("slam_toolbox")
pkg_nav2 = get_package_share_directory("nav2_bringup")

gz_sim = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={"gz_args": f"-s --headless-rendering -r {WORLD}"}.items(),
)

bridge = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
# 단방향(P2.4 실측) — cmd_vel만 ROS→GZ(]), 나머지 GZ→ROS([)
arguments=[
"/model/vehicle_blue/cmd_vel@geometry_msgs/msg/Twist]gz.msgs.Twist",
"/model/vehicle_blue/odometry@nav_msgs/msg/Odometry[gz.msgs.Odometry",
"/model/vehicle_blue/tf@tf2_msgs/msg/TFMessage[gz.msgs.Pose_V",
"/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan",
"/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock",
],
remappings=[
("/model/vehicle_blue/cmd_vel", "/cmd_vel"),
("/model/vehicle_blue/odometry", "/odom"),
("/model/vehicle_blue/tf", "/tf"),
],
parameters=[
{"qos_overrides./model/vehicle_blue.subscriber.reliability": "reliable"}
],
output="screen",
)

static_tf = Node(
package="tf2_ros",
executable="static_transform_publisher",
arguments=[
"--x", "0.648573", "--z", "0.675",
"--frame-id", "base_link", "--child-frame-id", "lidar_link",
],
parameters=[{"use_sim_time": True}],
)

slam = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_slam, "launch", "online_async_launch.py")
),
launch_arguments={
"use_sim_time": "true",
"slam_params_file": SLAM_PARAMS,
}.items(),
)

nav2 = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_nav2, "launch", "navigation_launch.py")
),
launch_arguments={"use_sim_time": "true"}.items(),
)

return LaunchDescription(
[
gz_sim,
bridge,
static_tf,
# 신뢰성 우선 stagger (S0 실측: 촘촘하면 nav2 lifecycle 활성화가 간헐 실패).
# gz가 벽 아레나 월드 로드 + /clock·/scan 발행을 마칠 시간(~10s) 뒤 SLAM,
# SLAM이 map→odom tf를 세운 뒤(~8s) Nav2 — run_spike.sh 검증 타이밍 + 여유.
TimerAction(period=10.0, actions=[slam]),
TimerAction(period=18.0, actions=[nav2]),
]
)
Loading
Loading