# Obstacle Nav2 Continuation Handoff

## User request

On the RDKx5 robot, add a ROS 2 package under `/home/sunrise/yiliao_ws/src/navigation` that:

1. Converts `obstacle_scanner` topic `/obstacles` into a Nav2-compatible costmap.
2. Uses that costmap with Nav2 Hybrid A* planning and a Nav2 executor/controller, following the existing `gc_navigation2_real` package style.
3. Does not use `slam_toolbox`.

The user later selected **no static map initially** and asked to reserve an interface for future map support. The user also confirmed the physical chassis is Ackermann even though Nav2 outputs `geometry_msgs/Twist` on `/cmd_vel`.

## Remote environment

- Host: `sunrise@192.168.10.210`
- OS/architecture: Ubuntu 22.04 ARM64 on RDKx5
- ROS: ROS 2 Humble
- Workspace: `/home/sunrise/yiliao_ws`
- Target package path: `/home/sunrise/yiliao_ws/src/navigation/obstacle_nav2`
- Source ROS before commands: `source /opt/ros/humble/setup.bash`
- Source overlay when needed: `source /home/sunrise/yiliao_ws/install/setup.bash`

Use SSH carefully from Windows PowerShell. Read `rdkx5-access` and `windows-ssh-escaping` skills before remote operations.

## Current state

No implementation package exists yet. Do not claim that `obstacle_nav2` has been created, built, or tested.

Completed work:

- Read and analyzed the relevant remote packages.
- Wrote the design document at `docs/superpowers/specs/2026-07-20-obstacle-nav2-design.md`.
- Committed only that design document in remote commit `a5ddfeb` with message `docs: design odometry-only obstacle nav2 package`.
- Wrote the implementation plan at `docs/superpowers/plans/2026-07-20-obstacle-nav2.md`.

Implementation, unit tests, builds, launch tests, and robot motion tests are all pending.

## Important source findings

### `/obstacles` interface

Package: `/home/sunrise/yiliao_ws/src/obstacle_scanner`

`msg/Obstacle.msg`:

```text
float64 center_x
float64 center_y
float64 radius
```

`msg/ObstacleArray.msg`:

```text
std_msgs/Header header
Obstacle[] obstacles
```

The publisher is in `src/obstacle_scanner/src/obstacle_scanner_node.cpp`.

- Topic: `/obstacles`
- Type: `obstacle_scanner/msg/ObstacleArray`
- Default message frame: `laser_frame` (parameter `frame_id`)
- Message timestamp is copied from `/scan`.
- Centers are computed in the laser scan XY plane.
- When inspected, `/obstacles` was not active and the installed overlay did not expose the topic. Build `obstacle_scanner` before testing the new package.

### Existing Nav2 reference

Reference package: `/home/sunrise/yiliao_ws/src/navigation/gc_navigation2_real`

Useful files:

- `params/gc_navigation_slam_real.yaml`
- `behavior_tree/nav_to_pose_ackermann.xml`
- `launch/real_nav2_slam.launch.py`
- `launch/real_bringup.launch.py`

Relevant existing settings:

- Planner plugin: `nav2_smac_planner/SmacPlannerHybrid`
- Search model: `REEDS_SHEPP`
- Minimum turning radius: `0.40`
- Controller: `nav2_mppi_controller::MPPIController`
- MPPI motion model: `Ackermann`
- Robot footprint: `[[0.14, 0.085], [0.14, -0.085], [-0.14, -0.085], [-0.14, 0.085]]`
- Base frame: `base_footprint`
- Odometry: `/odom_combined`
- Existing BT avoids relying on in-place spin for Ackermann recovery.

Do not copy the existing hard-coded BT path `/home/guoch/test_ws/...`; use the new package share path.

### Command conversion decision

Planning and control must use Ackermann kinematics. Nav2 may still output `Twist(v, omega)`.

Existing converter:

`/home/sunrise/yiliao_ws/src/origincar_base/scripts/cmd_vel_to_ackermann_drive.py`

It computes:

```text
steering = atan(wheelbase * omega / velocity)
```

with hard-coded `wheelbase = 0.143` and publishes `/ackermann_cmd`.

There are also direct `/cmd_vel` and `/ackermann_cmd` callbacks in `origincar_base.cpp`. Exactly one conversion path must be active. Do not launch both a direct firmware conversion and the Python converter without checking the actual bottom-level interface. Default the new navigation launch to motion disabled until the command graph is verified.

At minimum turning radius `0.40 m`, the nominal steering angle is `atan(0.143 / 0.40) = 0.343 rad` (about 19.7 degrees).

## Approved design

Package name: `obstacle_nav2`

Default mode:

- No static map.
- No `map_server`.
- No AMCL.
- No `slam_toolbox`.
- Global frame: `odom_combined`.
- Both global and local costmaps are rolling windows.
- Custom plugin `ObstacleArrayLayer` subscribes to `/obstacles`.
- Each obstacle center is transformed from `msg.header.frame_id` into the costmap global frame.
- The obstacle radius is rasterized as lethal cells; Nav2 inflation adds clearance.
- Old obstacle cells are cleared on each accepted update and after an obstacle-message timeout.
- Global planner is Smac Hybrid A* using Reeds-Shepp primitives.
- Controller is MPPI using Ackermann motion model.
- No in-place Spin recovery.
- Motion is disabled by default for the first launch validation.

Reserved map interface:

- Launch argument `use_static_map`, default `false`.
- Launch argument `map_yaml`, default empty.
- Launch/config argument `global_frame`, default `odom_combined`.
- The first implementation must not start map services when `use_static_map=false`.
- Full map/AMCL behavior may be implemented later without changing `ObstacleArrayLayer`.

## Intended package files

```text
src/navigation/obstacle_nav2/
  CMakeLists.txt
  package.xml
  obstacle_nav2_plugins.xml
  include/obstacle_nav2/obstacle_array_layer.hpp
  src/obstacle_array_layer.cpp
  test/test_obstacle_array_layer.cpp
  config/nav2_params.yaml
  behavior_tree/nav_to_pose_ackermann.xml
  launch/obstacle_nav2.launch.py
  README.md
```

## Implementation order

1. Create only the package metadata, header API, and focused gtest.
2. Add tests for circle rasterization, invalid-radius fallback, grid clearing, and stale-data clearing.
3. Build/run the focused test and verify a genuine RED failure caused by missing behavior.
4. Implement the smallest costmap helper and plugin behavior needed for GREEN.
5. Add pluginlib export and confirm the shared library loads.
6. Add odometry-only Nav2 parameters.
7. Add Ackermann BT and launch file with future map arguments.
8. Build `obstacle_scanner` and `obstacle_nav2`.
9. Start with motion disabled; verify topics, TF, lifecycle nodes, plugin loading, costmap marking, and timeout clearing.
10. Check that only one node publishes the effective chassis command before enabling motion.

## Proposed plugin behavior

Derive `ObstacleArrayLayer` from `nav2_costmap_2d::CostmapLayer`.

Suggested parameters under each plugin namespace:

```yaml
enabled: true
topic: /obstacles
obstacle_timeout: 0.5
transform_tolerance: 0.2
default_obstacle_radius: 0.05
minimum_obstacle_radius: 0.02
maximum_obstacle_radius: 0.50
extra_inflation: 0.02
```

Use an internal costmap. On an accepted message:

1. Remember the previous occupied bounds.
2. Reset the internal map to `FREE_SPACE`.
3. Transform obstacle centers to the layer global frame.
4. Clamp/fallback each radius and mark all cell centers inside `radius + extra_inflation` as `LETHAL_OBSTACLE`.
5. Expand update bounds over both previous and current obstacle extents.
6. Merge the layer with the master using overwrite semantics so disappeared obstacles are actually cleared before the downstream InflationLayer runs.

On timeout, clear the layer and include previous occupied bounds in the next update. Throttle TF warnings. Ignore non-finite centers and obstacles outside the rolling costmap.

## Planned Nav2 parameters

- `global_costmap.global_frame: odom_combined`
- `global_costmap.rolling_window: true`
- Suggested global window: 10 m x 10 m at 0.05 m resolution.
- `local_costmap.global_frame: odom_combined`
- Suggested local window: 3 m x 3 m at 0.05 m resolution.
- Plugins in both: `obstacle_array_layer`, then `inflation_layer`.
- No `static_layer` in default mode.
- `track_unknown_space: false` so the empty rolling window is traversable.
- `SmacPlannerHybrid.motion_model_for_search: REEDS_SHEPP`.
- `SmacPlannerHybrid.minimum_turning_radius: 0.40`.
- `MPPIController.motion_model: Ackermann`.
- `AckermannConstraints.min_turning_r: 0.40`.
- Use `odom_topic: /odom_combined` and `robot_base_frame: base_footprint`.

## Build and verification commands

Build only scoped packages first:

```bash
cd /home/sunrise/yiliao_ws
source /opt/ros/humble/setup.bash
colcon build --symlink-install --packages-select obstacle_scanner obstacle_nav2
source install/setup.bash
```

Run tests:

```bash
colcon test --packages-select obstacle_nav2 --event-handlers console_direct+
colcon test-result --verbose
```

Inspect required interfaces before launch:

```bash
ros2 interface show obstacle_scanner/msg/ObstacleArray
ros2 run tf2_ros tf2_echo odom_combined laser_frame
ros2 topic info /cmd_vel -v
ros2 topic info /ackermann_cmd -v
```

The first runtime test must use motion disabled. Verify:

```bash
ros2 node list
ros2 topic list
ros2 topic echo /global_costmap/costmap --once
ros2 topic echo /local_costmap/costmap --once
```

Confirm the default launch does not contain `/slam_toolbox`, `/map_server`, or `/amcl` nodes.

## Worktree warning

The remote Git worktree was already heavily dirty before this task. It contains many pre-existing deletions under old navigation package paths, modifications under `origincar_description` and `planner`, and untracked `src/navigation/` content. These belong to the user.

Rules for continuation:

- Never reset, checkout, clean, or revert the remote worktree.
- Never stage all changes with `git add .` or `git add -A`.
- Restrict edits and staging to `src/navigation/obstacle_nav2` and the three `docs/superpowers/...obstacle-nav2...md` files.
- The new package path did not exist when this handoff was written.
- The dependency `obstacle_scanner` itself contains current source that may not yet be reflected in the installed overlay; do not replace it with the committed version.

## Relevant documents

- Design: `/home/sunrise/yiliao_ws/docs/superpowers/specs/2026-07-20-obstacle-nav2-design.md`
- Plan: `/home/sunrise/yiliao_ws/docs/superpowers/plans/2026-07-20-obstacle-nav2.md`
- Handoff: `/home/sunrise/yiliao_ws/docs/superpowers/handoffs/2026-07-20-obstacle-nav2-handoff.md`

## Resume instruction

Read the design, plan, and this handoff. Re-inspect the current remote Git status because the user may have changed files since this snapshot. Then continue at **Task 1** using TDD. No implementation existed at the snapshot point.
