# Next Session: Sunday Demo Implementation

## What
Implement Track 1 of the Sunday April 12 TurboPi robot car demo: Annie hosts 8 demos (Chase Ball ×3 colors, Room Explorer, Traffic Cop, Obstacle Dodge, QR Navigator, Color Spotter, What Do I See, Look Around) via Telegram inline-keyboard buttons. Guest taps a button → Pi launches a Hiwonder demo subprocess → car performs trick → guest taps Stop → clean shutdown. Track 2 (voice through WonderEcho Pro) is OUT of scope unless Track 1 is done with ≥4 hours left.

## Plan
**READ FIRST:** `~/.claude/plans/abundant-swimming-snowglobe.md`

The plan has been through full adversarial review:
- 24 architecture findings (2 CRITICAL, 9 HIGH, 9 MEDIUM, 4 LOW)
- 14 code-quality findings (3 CRITICAL, 6 HIGH, 5 MEDIUM)
- All 38 findings are addressed in the "Adversarial Review Findings Resolution" table near the end of the plan
- No deferrals, no rejections (per `feedback_no_reject_defer.md`)

## Key Design Decisions (review-driven — DO NOT revert)

1. **Subprocess pattern, NOT in-process module loading.** Arch #12 suggested importing Hiwonder demos as Python modules. Root cause blocker: `car = mecanum.MecanumChassis()` at module load creates a second `Board()` instance, which fails with EBUSY on pyserial exclusive mode. Documented in main.py:221 comment. Stick with subprocess.

2. **`/v1/tool/call` endpoint was PROPOSED AND DROPPED.** Use the existing task_results file pattern end-to-end. Telegram bot writes `car_demo_intent_<uuid>.json` → annie-voice poller → dispatch → response file → telegram-bot edits message. Zero new HTTP surfaces. Matches `order_flour` / `zomato` patterns. See plan "Decision 2: REVISED per Arch #13".

3. **Source-patching of ColorTracking.py was PROPOSED AND DROPPED.** The runner imports the demo as a module and calls `module.setTargetColor((color,))` and `module.setVehicleFollowing(True)` — using Hiwonder's own API. No regex, no injection vector, no threading race at import time. See plan "Decision 3: REVISED per A5, A6, A7, A9, A24".

4. **Triple-layer color/name validation.** Annie `CarDemoInput` uses `Literal["", "red", "blue", "green"]` for color and `Literal[...]` for demo name. Pi `/demo/start` re-validates. `_headless_runner.py` asserts. Three layers (A9, C2, C7).

5. **`_demo_lock` is assigned INSIDE `lifespan()`, not at module load.** Session 269 lesson — asyncio primitives created at module load bind to the wrong event loop. Declared at module level as `_demo_lock: asyncio.Lock | None = None`, assigned in lifespan (A17, C12).

6. **Watchdog acquires `_demo_lock` AND reads stderr BEFORE `_restart_daemons()`.** Two critical bugs from the reviews (A2 + C1 + C5). The watchdog reads the ring buffer (populated by a continuous drain task) BEFORE starting the restart, so crash logs are preserved. It then acquires the lock, checks `_demo_phase`, and no-ops if `/demo/stop` already handled the restart.

7. **`start_new_session=True`, NOT `preexec_fn=os.setsid`.** CPython's `preexec_fn` is thread-unsafe — will occasionally deadlock in a multithreaded parent (A1). `asyncio.create_subprocess_exec` supports `start_new_session=True` directly.

8. **Deadman loop has `_demo_mode` guard + `continue` (no timestamp mutation).** On `/demo/stop` completion, explicitly set `_last_command_time = 0.0`. A18 + session 47 lesson.

9. **Hard 180-second max demo duration.** Local safety escape in turbopi-server so network partition Titan↔Pi doesn't leave the car running forever. A23.

10. **Orphan subprocess cleanup on restart.** `/tmp/turbopi-demo.pid` file written atomically on spawn; lifespan reads it on startup and `killpg`s any surviving process. A21.

11. **`_demo_phase: Literal["idle","starting","running","stopping"]` is the state machine source of truth.** Separate from `_demo_mode: bool`. `/demo/status` returns `_demo_phase`. Never reads `_demo_process` as a state proxy. C10.

12. **TrafficCop grafts LineFollower's `run(img)` LAB color detection onto Avoidance sonar wander.** Documented at plan "Decision 4" — not the line range the original spec said (232-280 was wrong); the logic is in the `run()` function distributed across LineFollower.py.

13. **Task results file `pending_handler.py:253-289` needs a new `car_demo_intent`/`car_demo_menu` dispatch branch.** C11 — this is a concrete edit the plan now specifies.

## Files to Modify

### Pi side (turbopi-server)
1. `services/turbopi-server/main.py` — add `/demo/{start,stop,status}` endpoints, `_demo_mode`, `_demo_phase`, `_demo_lock` (inside lifespan!), `_demo_process`, `_demo_watchdog_task`, `_demo_duration_watchdog_task`, `_demo_stderr_ring`. Update `_deadman_loop` (line 306). Update `_safety_estop_callback` (line 356) for A11 closure break. Extract `_restart_daemons()` + `_teardown_daemons()` helpers from `lifespan()`. Add `_demo_mode` guards to `/drive`, `/look`, `/photo`, `/photo/describe`, `/buzzer`, `/oled`, `/obstacles`, `/scan`, `/distance`, `/battery`. Update `/health` to short-circuit on `_demo_mode` (A10).
2. `services/turbopi-server/frame_grabber.py` — no changes (but document: `.join()` exists via Thread parent).
3. **new:** `/home/pi/TurboPi/_headless_runner.py` — ~120 lines, import-based, allowlist-validated, cv2 monkey-patched, VideoCapture forced to index 0.
4. **new:** `/home/pi/TurboPi/Functions/TrafficCop.py` — ~200 lines, Avoidance+LineFollower graft.
5. `services/turbopi-server/test_demo_endpoints.py` — new test file, includes ONE integration test with a real Python subprocess (no Pi hardware needed).

### Annie side (annie-voice)
6. `services/annie-voice/tool_schemas.py` — add `CarDemoInput` with Literal types at line 367.
7. `services/annie-voice/robot_tools.py` — add `handle_car_demo` at line 621.
8. `services/annie-voice/bot.py` — add ToolSpec entry (group="robot", channels=("text","telegram","voice")).
9. `services/annie-voice/<task_poller>.py` — add `car_demo_intent` file handler (locate existing pattern for `order_flour` first).
10. `services/annie-voice/tests/test_robot_tools.py` — add `TestCarDemoInput`, `TestHandleCarDemo`, `TestCarDemoIntentPoller`.

### Telegram bot
11. **new:** `services/telegram-bot/car_demo_handler.py` — ~150 lines, SEC-1 auth, intent file writing, menu rendering.
12. `services/telegram-bot/bot.py` — register `CallbackQueryHandler(handle_car_demo_callback, pattern=r"^car_demo:")` in the handler block (lines 690-716).
13. `services/telegram-bot/pending_handler.py:253-289` — **ADD** `elif data.get("car_demo_intent")` AND `elif data.get("car_demo_menu")` branches in `task_result_notification_loop`. C11.
14. `services/telegram-bot/tests/test_car_demo_handler.py` — new tests.

### Vendor
15. `vendor/turbopi/` — **new directory**, `rsync -av --exclude='__pycache__' pi:/home/pi/TurboPi/ ./vendor/turbopi/` (rsync to local from Pi — not from Titan, and not TO Titan). Session 324 says no rsync TO Titan; this is rsync FROM Pi to local laptop, which is fine.

## Start Command

```bash
cat ~/.claude/plans/abundant-swimming-snowglobe.md
```

Then implement the plan. All adversarial findings are already addressed in it. Start with **Pre-Flight Prep** (6 steps), THEN Phase 1 (Pi side, 3-4h), THEN Phase 2 (Annie side, 1-1.5h), THEN Phase 3 (Telegram bot, 1-1.5h), THEN Phase 4 (E2E smoke test, 45m-1h), THEN Phase 5 (deploy, 30m).

## Verification

1. **Pre-flight (MUST pass before touching code):**
   - `ssh pi "ls /dev/ttyAMA0 /dev/video0 /dev/lidar"` — all three devices present
   - `ssh pi "python3 -c 'import serial; print(serial.VERSION)'"` — ≥3.3 (currently 3.5)
   - `ssh pi "python3 -c 'import sys; sys.path.insert(0,\"/home/pi/TurboPi\"); import Functions.ColorTracking as ct; print(getattr(ct, \"__isRunning\", None))'"` — prints `False` (attribute accessible without mangling)
   - `ssh pi "ls /home/pi/TurboPi/CameraCalibration/*.npz"` — calibration file exists
   - `ssh pi "which lsof fuser"` — both utilities present
   - Calibrate `lab_config.yaml` for red, blue, AND green in the demo room lighting (A15)

2. **Phase 1 verification (Pi side only):**
   - `curl -H "Authorization: Bearer $TOKEN" http://192.168.68.61:8080/health` → baseline healthy
   - `curl -X POST -H "..." http://192.168.68.61:8080/demo/start -d '{"name":"ColorTracking","params":{"color":"red"}}'` → `{"status":"started","pid":N}`
   - Put red ball in front of car → car tracks and drives within 3s
   - `curl /demo/status` → `{"phase":"running","name":"ColorTracking","uptime_s":N}`
   - `curl -X POST /demo/stop` → `{"status":"stopped","exit_code":-2}`
   - `curl /health` → `safety_daemon_healthy=true` within 8s

3. **Full E2E (Phase 4):**
   - Send "Annie, what can your car do?" in Telegram → menu appears with 10 buttons
   - Tap each button → verify demo runs then stops cleanly
   - After every stop, `/health` reports all daemons healthy

4. **Sunday rehearsal:** run through all 8 demos in sequence without developer intervention. If any demo fails, diagnose WITHIN 10 minutes or skip that demo on the day and ship the others.

## Known Risks for Sunday

- **Blue/green calibration may still be off** — pre-flight #3 mitigates but room lighting can change
- **Sonar false readings** (session 46 known issue) may affect Obstacle Dodge and Traffic Cop — test in demo room, drop from menu if unreliable
- **Camera reclaim can take 10-15s** (C8 corrected arithmetic) — OK for demo transitions, not for ESTOP scenarios
- **Orphan cleanup on systemd restart** — if the `_tmp/turbopi-demo.pid` file gets out of date, lifespan may kill the wrong PID (low risk but verify via pre-flight #6)
