# Next Session: SLAM E2E Navigation Test

## Context

Session 68 (2026-04-12) completed tasks #1 (turn verify), #2 (straight drive),
and #7 (SLAM reset race fix). All three shipped in commit `3027385` and deployed
to the Pi. The race fix was verified live — `/slam/reset` now actually zeroes
the pose, where before it silently kept the pre-reset values.

**See `MEMORY.md` session 68 block** for the full diagnostic picture. Key
findings you need to carry forward:

1. **Drive math is solid.** Odometry predicts 68.75 cm for a 5s/speed-50 forward
   drive, physical tape measured 71.5 cm. The ~2.75 cm gap is coast distance,
   not slip.
2. **Stop-coast left curve.** Every `/drive` call pivots the robot ~6° left
   at the end during coast-down. Root cause: `MOTOR_TRIM_FL/RL=1.15` runs left
   motors at duty 57.5 while right motors run at 50. When `_stop_motors_sync`
   sends duty=0 to all four, left wheels have more stored rotational KE and
   coast further. MOTOR_TRIM works during steady-state but fails at stop.
   Filed as task #6. **This is the #1 risk to nav quality.**
3. **Turn directions are correct** but **magnitudes are asymmetric** (left
   over-rotates 124%, right under-rotates 40%) — same root cause as (2).
4. **SLAM under-counts distance** by ~20-28% in featureless areas (known
   limitation from sessions 62-65, not a new bug).
5. **Phantom translation during right turn** (~41 cm apparent slide during a
   should-be-in-place rotation). Either scan-matcher hallucination or real
   physical slip. Unresolved.

## Immediate Task: #3 — E2E Telegram Navigation

**Recommendation from session 68:** run this test WITH the known stop-coast
bias as a known limitation, rather than fixing task #6 first. Rationale: a
real nav run will tell us how badly the bias actually hurts quality, which
informs whether task #6 needs a quick hotfix or the full Phase B refactor.

### Pre-flight

```bash
# 1. Verify Pi + service + race fix are healthy
TOKEN='8cX80yIBws1PfBjFuvPz0k9egPSZD0LvS02oUD6ijfg'
curl -sS -H "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/health | \
  jq '{slam_healthy, imu_healthy, safety_daemon_healthy, odom_calibrated, estop_active}'
# Expect: all true, estop_active=false

# 2. Verify race fix is live (confirms commit 3027385 is loaded on the Pi)
curl -sS -X POST -H "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/slam/reset
sleep 0.5
curl -sS -H "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/pose
# Expect: x=0.0, y=0.0 EXACTLY (not residual like -0.03, -0.03).
# If not: the Pi hasn't pulled `3027385` — ssh pi + git -C /home/rajesh/workplace/her/her-os pull + sudo systemctl restart turbopi-server

# 3. Titan nav VLM up? (the car calls it every nav cycle)
curl -sS http://192.168.68.52:8003/v1/models | head -c 200
```

### Physical setup

- Place the robot in a moderately cluttered room (**not** a bare corridor).
  Featureless areas were the #1 source of SLAM scan-match hallucination in
  session 68. Chair legs, table legs, a bookshelf, a wall with texture — any
  of those give the scan matcher real features to lock onto.
- At least **1 m clear in front** of the starting position.
- At least **40 cm clear on all sides** (`SIDE_CLEARANCE_MM=400` enforced by
  the lidar side-guard at main.py:110 — violation → drive returns `blocked`).
- Clear the room of pets and moving obstacles (the Hailo safety daemon will
  e-stop on detection but it's not reliable enough for a hands-off run).

### Run

Send via Telegram to Annie:

> Annie, explore this room.

The nav loop uses 1s drives × up to 20 cycles (per session 65's
`services/annie-voice/robot_tools.py:280-298`). Each cycle:
1. Captures a camera frame + current SLAM pose
2. Sends to Titan Gemma-4 26B VLM (or Panda E2B if session 67's swap was done)
3. Gets back one action word (`forward|left|right|strafe_left|strafe_right|stop`)
4. Executes the drive (1s duration, speed ~50)
5. Repeats until the VLM says `stop` or max_cycles hit

### What to watch

1. **Does the map build?** Call `GET /map` periodically during the run and
   save PNGs. You want to see walls progressively appear as the robot explores.
2. **Does pose tracking stay reasonable?** Call `GET /pose` between cycles.
   Watch for big jumps (scan-matcher hallucination) or slow drift (stop-coast
   accumulation).
3. **Does the robot avoid obstacles?** The Hailo safety daemon plus
   `SAFETY_DISTANCE_CM=30` should prevent collisions. If it hits something,
   that's a bug.
4. **Does it pick sensible actions?** The VLM's choices are the other half of
   nav quality. If it says `forward` while facing a wall, that's a model issue,
   not a SLAM issue.
5. **Accumulated heading drift.** After 20 cycles, if the robot is facing some
   random direction with no correlation to what it's done, the stop-coast bias
   is compounding badly — escalate task #6 to HIGH.

### Expected outcomes

- **Works well:** map is readable, robot avoids obstacles, heading stays within
  ~30° of sensible. Stop-coast is tolerable for now. Move on to task #4 (test
  file cleanup) or #6 (stop-coast hotfix) based on user preference.
- **Works but drifts badly:** map is noisy, heading accumulates 90°+ of bias.
  Task #6 becomes urgent. Recommend the quick active-brake-pulse fix first
  (option (a) in the task description), re-test, iterate.
- **Doesn't work at all:** robot crashes into walls or gets stuck in a corner.
  Log the specific failure mode and diagnose. Possible causes: safety daemon
  misconfigured, VLM outputting garbage, SLAM reporting pose so wrong that
  robot_tools can't reason about it.

### Fallback: task #6 first

If the user wants to fix stop-coast BEFORE task #3, the minimum-change option is:

**Active brake pulse** in `services/turbopi-server/main.py::_stop_motors_sync`:
1. Instead of `board.set_motor_duty([[1, 0], [2, 0], [3, 0], [4, 0]])`, send a
   brief reverse pulse to the left motors first:
   ```python
   with _uart_lock:
       # Reverse-brake left motors for 50ms to counter stored KE
       board.set_motor_duty([[1, 15], [2, 0], [3, 15], [4, 0]])  # signs flipped vs forward
       time.sleep(0.05)
       board.set_motor_duty([[1, 0], [2, 0], [3, 0], [4, 0]])
   ```
2. The `+15` duty on motors 1 and 3 is roughly 1.15× the difference (needs tuning).
   Motor flip convention matches the existing `_drive_sync` layout.
3. Test by running a task #2 repeat (5s forward, tape-measure physical, check
   heading delta). Target: heading delta < 2° instead of the observed 6°.
4. If the fix works, move to task #3. If it doesn't, try option (c) — the
   `set_motor_speed` closed-loop migration (session 65's "Phase B") which
   requires re-calibration and is a bigger lift.

## Tasks state at end of session 68

| # | Task | Status |
|---|---|---|
| 1 | Verify left/right turn direction | ✅ completed |
| 2 | Straight-line drive distance test | ✅ completed |
| 3 | E2E Telegram "explore this room" | **NEXT** |
| 4 | Fix stale ACTION_MAP in test_body_velocity.py | pending |
| 5 | Strafe body-frame investigation | pending (needs hardware) |
| 6 | Stop-coast left curve (asymmetric motor brake) | **HIGH — affects #3** |
| 7 | SLAM reset race condition | ✅ completed (shipped 3027385) |

## Files you'll probably need to read

- `services/turbopi-server/main.py:76-135` — ACTION_MAP, DUTY_TO_MPS/RPS, trim
- `services/turbopi-server/main.py:262-287` — `_stop_motors_sync` / `_drive_sync`
- `services/turbopi-server/slam.py:480-700` — SLAM daemon, reset, _slam_step
- `services/annie-voice/robot_tools.py:280-400` — nav loop, VLM prompt, cycle logic
- `services/annie-voice/tool_schemas.py` — `NavigateRobotInput` (max_cycles, etc.)
- `MEMORY.md` session 68 block — full session-68 findings

## Git state at session 68 end

- `main` at `3027385` (race fix)
- Pi deployed, service restarted with new code
- 277/277 turbopi-server tests passing
- Working tree clean except pre-existing untracked files from parallel session 67
