# Next Session: Fix All Sunday Demos

## What
Make all 8 TurboPi Sunday demos work reliably end-to-end via the Telegram
inline keyboard before guests arrive on **Sunday April 12**. The demo
infrastructure is deployed and the state machine works — what remains is
empirical tuning of individual demos (calibration, motion signs, timing).

**Rajesh says the car is able to move.** Ignore the "waiting for jumper
caps" blocker from session 50 — proceed with motor testing.

## Already Done (session 50, commits `1c8d2f5` + `47fb0fe`)

All code is **committed, pushed, deployed, and live on all three machines**.

### Pi (`services/turbopi-server`)
- `demo_runner.DemoRunner` state machine (`idle`/`starting`/`running`/`stopping`)
  with lock-guarded transitions, single-reader stderr ring buffer, watchdog
  that reads stderr BEFORE restart, 180s max-duration hard cap, early-death
  detection, orphan PID cleanup on startup.
- `POST /demo/start`, `POST /demo/stop`, `GET /demo/status` endpoints.
- `_demo_mode` guards on every hardware endpoint (`/drive` `/look` `/photo`
  `/buzzer` `/oled` `/obstacles` `/scan` `/distance` `/battery`) → return
  **423 Locked** while a demo subprocess owns the hardware.
- `/health` short-circuits during demo mode (A10) — returns a reduced
  payload without touching Board.
- `pi-files/_headless_runner.py` — imports the demo as a Python module,
  reuses `mecanum.board` (single Board per process, avoids pyserial 3.5
  EBUSY), sets `setTargetColor`+`setVehicleFollowing` via Hiwonder's own
  API, drives the frame loop. NO source patching, NO runpy.
- `pi-files/Functions/TrafficCop.py` — new Avoidance-sonar-wander +
  LineFollower-LAB-color-detect graft.
- `test_demo_runner.py` — 25 tests passing (including the C14 real-subprocess
  integration test).
- **Self-cancellation bug fixed (session 50):** `DemoRunner.stop()` now
  skips cancelling the current task to avoid killing itself when
  `_max_duration_kill` calls `self.stop()`. Regression test added.
  Commit `47fb0fe`.

### Annie (`services/annie-voice`)
- `tool_schemas.CarDemoInput` with triple-layer Literal types.
- `robot_tools.handle_car_demo` — menu file write / Pi `/demo/start` /
  Pi `/demo/stop` / passthroughs to `handle_navigate_robot` /
  `handle_robot_photo` / `handle_robot_look`.
- `text_llm.TOOL_SPECS` entry `car_demo`, `group="robotics"`,
  `channels=("text","telegram","voice")`, `gated=True`.
- `tool_adapters.ADAPTERS["car_demo"]` registered.
- 30 tests added to `test_robot_tools.py`; full annie-voice suite green.

### Telegram bot (`services/telegram-bot`)
- `car_demo_handler.py` — 11-button inline keyboard, SEC-1 auth, callback
  parser with allowlist (defense layer 2), auto-stop running demo before
  start (A22), direct Pi HTTP (no LLM round-trip).
- `bot.py` registers `CallbackQueryHandler(handle_car_demo_callback, pattern=r"^car_demo:")`.
- `pending_handler.py` has `car_demo_menu` dispatch branch (C11).
- `test_car_demo_handler.py` — 42 tests passing.
- **`ROBOT_API_TOKEN` + `ROBOT_BASE_URL` added to
  `services/telegram-bot/.env` on Titan** (session 50 — first button tap
  surfaced it missing).

### What works live (verified session 50)
- `/demo/start ColorTracking red` → subprocess alive → `/demo/stop` → clean
  daemon restart within 8 seconds.
- Validation: 422 on invalid script name / color, 423 Locked on
  `/drive` during demo, 409 on double-start.
- Menu render flow: writing `{"car_demo_menu": true}` to
  `~/.her-os/annie/task_results/` on Titan → telegram-bot sends the
  11-button keyboard to your chat within 5 seconds.
- Button tap → Pi HTTP → subprocess start (once `ROBOT_API_TOKEN` was
  added to telegram-bot's `.env`).

## What's Broken — Your Job This Session

### 1. LAB calibration for ALL THREE BALLS (CRITICAL)

**Rajesh's explicit instruction: calibrate RED, BLUE, AND GREEN.** Do
not assume the pre-existing `red` range is correct — the camera has a
strong red tint across the whole scene, so even the valid-looking red
range may match too much of the room. Recalibrate all three against
the actual physical balls in the actual demo-room lighting.

Pre-existing `/home/pi/TurboPi/lab_config.yaml`:

```yaml
red:
  max: [255, 255, 255]   # looks valid but UNTESTED against real ball — RECALIBRATE
  min: [0, 173, 130]
blue:
  max: [130, 185, 120]   # a.max=185 matches strong red/magenta — DEFINITELY BROKEN
  min: [0, 126, 0]
green:
  max: [252, 109, 207]   # b.max=207 is yellow-green — DEFINITELY BROKEN
  min: [0, 48, 94]
```

In session 50 Rajesh showed a blue ball and ColorTracking locked onto
something-else-not-blue that fit the broken "blue" LAB range. Car drove
away from the ball because it was chasing a different target. Red has
never been verified against a physical red ball under current lighting.

**Fix procedure** — do ALL THREE colors, in order `red → blue → green`
(red first because it's the "known control" — if red fails to detect a
real ball, the tool itself is broken). Use `/tmp/lab_calibrate_pi.py`
already on Pi:

```bash
TOKEN=<from turbopi-server systemd override>

# ── RED ────────────────────────────────────────────────────────
# 1. Have Rajesh position the red ball inside the center-frame green box
#    (~30cm in front of car, centered in camera view).
ssh pi "sudo systemctl stop turbopi-server && sleep 2 && \
        python3 /tmp/lab_calibrate_pi.py red --wait 5 --dry-run"
scp pi:/tmp/lab_calibrate/red_frame.jpg /tmp/red_cal.jpg
# Read /tmp/red_cal.jpg — confirm the red ball is INSIDE the green box.
# If yes, drop --dry-run and commit:
ssh pi "python3 /tmp/lab_calibrate_pi.py red --wait 3 && \
        sudo systemctl start turbopi-server"

# ── BLUE (same pattern) ────────────────────────────────────────
ssh pi "sudo systemctl stop turbopi-server && sleep 2 && \
        python3 /tmp/lab_calibrate_pi.py blue --wait 5 --dry-run"
scp pi:/tmp/lab_calibrate/blue_frame.jpg /tmp/blue_cal.jpg
ssh pi "python3 /tmp/lab_calibrate_pi.py blue --wait 3 && \
        sudo systemctl start turbopi-server"

# ── GREEN (same pattern) ───────────────────────────────────────
ssh pi "sudo systemctl stop turbopi-server && sleep 2 && \
        python3 /tmp/lab_calibrate_pi.py green --wait 5 --dry-run"
scp pi:/tmp/lab_calibrate/green_frame.jpg /tmp/green_cal.jpg
ssh pi "python3 /tmp/lab_calibrate_pi.py green --wait 3 && \
        sudo systemctl start turbopi-server"

# After all three, verify the yaml looks sensible:
ssh pi "cat /home/pi/TurboPi/lab_config.yaml"
```

**After calibration, verify live:** run `ChaseBall red`, then blue, then
green. Each should lock onto the physical ball and drive toward it.
If a color detects but drives WRONG direction → PID sign issue (see §1b).

### 1b. PID sign inversion fallback

If post-calibration, a ball IS detected but car drives away from it,
the servo horn remount in session 38 may have inverted the servo_x
sign in ColorTracking's PID. Edit on Pi:

```bash
ssh pi "sudo sed -i 's/servo_x += int(servo_x_pid.output)/servo_x -= int(servo_x_pid.output)/' /home/pi/TurboPi/Functions/ColorTracking.py"
```

Test. If worse, revert and try servo_y instead. If both still wrong,
the car velocity direction itself is inverted — check `car.set_velocity`
call inside the move() thread. **Do NOT blindly flip signs** — test one
change at a time and reset on failure.

The script opens `/dev/video0` with YUYV+saturation=40 (same as Hiwonder
Camera.py — verified in session 50) so the calibrated LAB range matches
what ColorTracking sees at runtime.

**If the calibration still fails** (ball detected but car drives wrong
direction), it may be a PID sign inversion from the servo horn remount
(session 38). In that case, edit `Functions/ColorTracking.py` on the Pi:
find `servo_x += int(servo_x_pid.output)` and try flipping to `-=`.
Same for servo_y.

### 2. Camera red tint (POSSIBLE ROOT CAUSE)

The camera delivers frames with a strong red tint across the entire
scene. Session 50 found this affects both `FrameGrabber` and Hiwonder's
`Camera.py`. `CAP_PROP_SATURATION=40` does NOT fix it — this is a
sensor-level issue.

**Try this BEFORE recalibrating:**

```bash
ssh pi "python3 -c '
import cv2, time
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(\"Y\",\"U\",\"Y\",\"V\"))
cap.set(cv2.CAP_PROP_AUTO_WB, 0)
cap.set(cv2.CAP_PROP_WB_TEMPERATURE, 5500)
cap.set(cv2.CAP_PROP_SATURATION, 40)
for _ in range(5):
    cap.read(); time.sleep(0.1)
ret, f = cap.read()
print(\"mean BGR:\", f.mean(axis=(0,1)))
cv2.imwrite(\"/tmp/wb_test.jpg\", f)
cap.release()
'"
scp pi:/tmp/wb_test.jpg /tmp/wb_test.jpg  # Read this file
```

If the image looks neutral (not red-tinted), ALSO patch
`services/turbopi-server/frame_grabber.py` to apply the same WB settings
in `FrameGrabber.run()` — that would improve vision for navigate_robot,
robot_photo, and all future demos.

If still tinted after WB tweaks → the camera is missing its IR cut
filter. Recalibrate with the red tint as-is (LAB is relative) and note
for post-Sunday hardware fix.

### 3. Per-demo empirical testing (the long tail)

For each demo, smoke-test via the Telegram inline keyboard. Before every
motor test, verify baseline with `curl http://192.168.68.61:8080/health`
and confirm `demo_mode=false, phase=idle, all daemons healthy`.

**Start with the SAFE (no-motor) passthroughs to validate the stack:**

| Demo | Type | Expected | Verify |
|------|------|----------|--------|
| 🎯 Look Around | passthrough `/look` | Camera pans left→right→center | `/health pan+tilt` values change, no errors |
| 📸 What Do I See? | passthrough `/photo/describe` | Titan VLM description in Telegram | Description mentions actual room contents |

**Then the motor demos in order of safety:**

| Demo | Script | Risk | What to check |
|------|--------|------|---------------|
| 🔴 Chase Red Ball | ColorTracking (red) | LOW — red calib is known-good | Car tracks ball smoothly, stops when ball hidden |
| 🛑 Stop Demo | `/demo/stop` | LOW — halts subprocess | `/health` returns `demo_mode=false` within 8s, all daemons healthy |
| 🔵 Chase Blue Ball | ColorTracking (blue) | **needs calibration fix** | After step 1 above |
| 🟢 Chase Green Ball | ColorTracking (green) | **needs calibration fix** | After step 1 above |
| 🚧 Obstacle Dodge | Avoidance | MEDIUM — sonar false readings (session 46) | Wanders forward, turns left <30cm. Test in open space first |
| 🚦 Traffic Cop | **new TrafficCop.py** | HIGH — untested, new code | Wanders like Avoidance, halts on red card, resumes on green card |
| 📱 QR Navigator | QuickMark | UNKNOWN — never tested | Show it QR codes "1"/"2"/"3"/"4", see what it does |
| 🔍 Color Spotter | ColorDetect | UNKNOWN — never tested | Hold up colored objects, verify detection |

**If a demo fails, diagnostics in order of cost:**
1. `curl /health` — is the car back to idle? If not, watchdog bug.
2. `ssh pi "sudo journalctl -u turbopi-server --since '2 minutes ago'"` —
   look for `DEMO CRASHED`, Python tracebacks, lidar/sonar errors.
3. `ssh pi "ls /tmp/turbopi-demo.pid"` — if present, orphan cleanup missed.
4. If a demo consistently misbehaves but the state machine is clean,
   it's a Hiwonder-code issue (PID signs, calibration, threshold tuning).

**Time budget:** 3-4 hours. If a demo is consistently broken and takes
>20 min to diagnose, **drop it from the Sunday menu** rather than rabbit-hole.
The goal is 6/8 demos reliably working, not 8/8 shaky.

### 4. Known issues from session 50 (do NOT re-discover)

| Issue | Where | Status |
|-------|-------|--------|
| `DemoRunner.stop()` self-cancellation when `_max_duration_kill` calls `self.stop()` | `demo_runner.py:442` | **FIXED** commit `47fb0fe`. Regression test exists. |
| `ROBOT_API_TOKEN` missing from `telegram-bot/.env` | Titan | **FIXED** session 50 |
| Pyserial 3.5 TIOCEXCL means `rrc.Board()` can only be called once per process | all Hiwonder code | **KNOWN** — `_headless_runner.py` reuses `mecanum.board`, never calls `rrc.Board()` directly |
| `mecanum.py` eagerly opens `/dev/ttyAMA0` at module load | `HiwonderSDK/mecanum.py` | **KNOWN** — do NOT import mecanum in turbopi-server main.py |
| Module-level `__isRunning` in Hiwonder demos is NOT name-mangled at module scope (only inside classes) | all Hiwonder `Functions/*.py` | **KNOWN** — runner uses `getattr(mod, "__isRunning", False)` |
| Camera red tint affects both `/photo` and Hiwonder Camera | `/dev/video0` sensor profile | **WORKAROUND** — LAB calibration is relative. Try WB fix (§2 above) |
| Pre-existing `blue` and `green` LAB ranges are broken | `/home/pi/TurboPi/lab_config.yaml` | **PENDING** — step 1 above |

## Tools Available

### Calibration (run on Pi)
- `/tmp/lab_calibrate_pi.py` (repo: `scripts/lab_calibrate_pi.py`)
- Usage: `python3 /tmp/lab_calibrate_pi.py {red|blue|green} [--crop 60] [--margin 15] [--wait 3] [--dry-run]`

### Direct Pi control (works while demo is idle)
```bash
TOKEN=$(ssh pi 'sudo cat /etc/systemd/system/turbopi-server.service.d/override.conf' | grep ROBOT_API_TOKEN | cut -d= -f2)

# Menu
curl -sH "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/demo/status
curl -sX POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  http://192.168.68.61:8080/demo/start -d '{"name":"ColorTracking","color":"red"}'
curl -sX POST -H "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/demo/stop
curl -s http://192.168.68.61:8080/health  # no auth
```

### Drop menu file on Titan to resend keyboard
```bash
ssh titan "UUID=\$(python3 -c 'import uuid; print(uuid.uuid4().hex[:8])'); \
  echo '{\"car_demo_menu\": true}' > ~/.her-os/annie/task_results/car-demo-menu-\$UUID.json"
# Keyboard appears in Telegram within 5s
```

### Deploy Pi-side changes
```bash
rsync -av services/turbopi-server/ pi:/home/rajesh/workplace/her/her-os/services/turbopi-server/
ssh pi "sudo systemctl restart turbopi-server && sleep 4 && curl -s http://localhost:8080/health"
```

### Deploy Annie/Telegram changes (Titan)
```bash
git commit -am "..."
git push origin main
ssh titan "cd ~/workplace/her/her-os && git pull --ff-only && find services/{annie-voice,telegram-bot} -name __pycache__ -exec rm -rf {} +"
./stop.sh annie && ./start.sh annie
./stop.sh telegram && ./start.sh telegram
```

## Verification (end of session)

Demo is Sunday-ready when:
1. `/health` consistently reports all daemons healthy when idle
2. **All 3 colors (red, blue, green) are calibrated** against the actual
   physical balls in demo-room lighting. Verify by running ChaseBall for
   each color — ball must be detected and car must drive TOWARD it.
3. At least **6 of 8 demos** run cleanly: start → observable behavior
   → stop → clean daemon restart
4. Every demo, when stopped, returns the state to `idle` within 8 seconds
5. Calibration yaml has narrow LAB ranges (spread ≤ ~50 units per
   channel) centered on actual ball pixel values
6. No stuck state machine bugs — state recovers from every failure mode

## Out of Scope

- **Track 2** (voice via WonderEcho Pro): still out of scope. Only
  tackle if all Track 1 demos work with ≥4 hours remaining.
- **Camera IR filter** hardware fix: post-Sunday.
- **FrameGrabber WB fix**: optional, only if step 2 above proves that
  WB_TEMPERATURE fixes the red tint.
- **Hiwonder demo source code changes**: avoid unless absolutely
  necessary. The SDK files are at `/home/pi/TurboPi/Functions/*.py`
  and are NOT in the her-os repo (except TrafficCop.py which IS ours
  at `services/turbopi-server/pi-files/Functions/TrafficCop.py`).

## Start Command

```bash
cat docs/NEXT-SESSION-SUNDAY-DEMO-FIX.md
```

Read this file. Then read `docs/NEXT-SESSION-SUNDAY-DEMO-IMPL.md` and
`~/.claude/plans/abundant-swimming-snowglobe.md` for deeper context if
needed. Then start with **§1 (LAB calibration)**.
