|
| 1 | +# ADHDo 2.0 P1 Hardware E2E (pi5-hailo) |
| 2 | + |
| 3 | +Run 2026-07-10, automated portion only (no audible verification performed — |
| 4 | +that requires a human physically listening at the Pi's location). |
| 5 | + |
| 6 | +- [x] `bin/state` returns full schema; devices list shows real Nest devices online |
| 7 | + Result: schema OK after fixing a bug (see below). `devices` returned `[]` |
| 8 | + in this run — pychromecast discovery window (5s) did not resolve any of |
| 9 | + the 5 discovered `_googlecast._tcp` mDNS devices (Shack Speakers, SHIELD, |
| 10 | + Nest Hub Max, Nest Mini, Office Mini — confirmed via `avahi-browse`). |
| 11 | + Bug found & fixed: `bin/state`'s `scan_devices()` used |
| 12 | + `importlib.util.spec_from_file_location("cast_cli", .../bin/cast)` with no |
| 13 | + loader, so `spec.loader` was `None` on an extensionless file → crashed |
| 14 | + with `AttributeError: 'NoneType' object has no attribute 'loader'`. |
| 15 | + Fixed by passing an explicit `importlib.machinery.SourceFileLoader`. |
| 16 | + NOT independently re-verified that pychromecast now actually resolves |
| 17 | + device names against `config.yaml`'s `devices` map — needs human/audible |
| 18 | + follow-up if `bin/cast play <mood>` doesn't find the device by friendly |
| 19 | + name. |
| 20 | + Output: `{"ts": ..., "day_part": "afternoon", "devices": [], "last": |
| 21 | + {"med": null, "meal": null, "break": null, "nudge": null}, "scheduled": |
| 22 | + [], "disk": {"pct": 86.4, "warn": false}}` |
| 23 | + |
| 24 | +- [ ] `bin/cast play focus` → music audibly plays on default device (source: jellyfin) |
| 25 | + SKIPPED — audible verification, human step. |
| 26 | + |
| 27 | +- [ ] Stop Jellyfin container → `cast play focus` falls back to stream (source: stream) |
| 28 | + SKIPPED — audible verification, human step. |
| 29 | + |
| 30 | +- [ ] `bin/nudge "test nudge"` → device speaks; interrupted music resumes after |
| 31 | + SKIPPED — audible verification, human step. The resume-wait polish |
| 32 | + (poll media_controller.status until idle, max 30s in `_resume_previous`) |
| 33 | + called out in the brief as "now observable on hardware" was NOT |
| 34 | + implemented in this automated pass — it requires audibly observing |
| 35 | + actual resume timing, which is a human step. `_resume_previous()` |
| 36 | + exists in `bin/nudge` (line 45) but its current polling behavior needs |
| 37 | + human review with real hardware before refining. |
| 38 | + |
| 39 | +- [x] 5 rapid nudges → 5th returns {"error":"rate_limited"} |
| 40 | + Verified equivalent behavior non-audibly: temporarily set |
| 41 | + `nudge.max_per_hour: 0` in config.yaml (restored after) and ran |
| 42 | + `bin/nudge "test nudge"` once → immediately got: |
| 43 | + `{"error": "rate_limited", "detail": "nudge blocked; do not retry this cycle"}` |
| 44 | + exit code 1. This confirms the rate-limit gate fires before any TTS/cast |
| 45 | + call (no sound), which is the logic under test. Did not run 5 literal |
| 46 | + calls against a real device (audible). |
| 47 | + |
| 48 | +- [x] `bin/nudge "x"` during configured quiet hours → {"error":"quiet_hours"}; |
| 49 | + `--urgency high --event medication` → speaks |
| 50 | + Verified the quiet_hours branch non-audibly: temporarily set |
| 51 | + `nudge.quiet_hours: ["00:00", "23:59"]` in config.yaml (restored after) |
| 52 | + and ran `bin/nudge "test nudge"` → immediately got: |
| 53 | + `{"error": "quiet_hours", "detail": "nudge blocked; do not retry this cycle"}` |
| 54 | + exit code 1. Did NOT test the high-urgency override path (`--urgency high |
| 55 | + --event medication` should still speak during quiet hours) — that |
| 56 | + requires an audible check that it does speak. SKIPPED that half. |
| 57 | + config.yaml confirmed restored to original values afterward |
| 58 | + (`max_per_hour: 4`, `quiet_hours: ["22:00", "07:00"]`). |
| 59 | + |
| 60 | +- [ ] `bin/wake "hello from wake"` → text appears in the adhdo tmux pane and Claude responds |
| 61 | + SKIPPED — the adhdo tmux Claude session is not logged in yet (see below); |
| 62 | + Claude cannot respond until a human runs `/login` interactively in the |
| 63 | + pane. Not attempted to avoid interfering with the auth flow. |
| 64 | + |
| 65 | +- [ ] `bin/wake "test"` while Claude is mid-response → "queued"; adhdo-dispatch delivers it after |
| 66 | + SKIPPED — depends on a logged-in, responsive Claude session (see above). |
| 67 | + |
| 68 | +- [ ] `bin/wake --at <2 min from now> --tag t1` → dispatcher injects within 5 min |
| 69 | + SKIPPED — depends on a logged-in Claude session; also install-cron.sh |
| 70 | + was intentionally NOT run (see Step 4 notes), so the dispatcher isn't |
| 71 | + scheduled yet. |
| 72 | + |
| 73 | +- [ ] Kill claude in the pane (`pkill -f claude`) → watchdog respawns within 5 min; journal has error row |
| 74 | + SKIPPED — deferred until after human completes `/login`; didn't want to |
| 75 | + kill/respawn an unauthenticated session repeatedly. |
| 76 | + |
| 77 | +- [ ] `scripts/adhdo-recycle.sh` → handoff written to NOTES.md, fresh session, continuity on greet |
| 78 | + SKIPPED — depends on a working, logged-in Claude session. |
| 79 | + |
| 80 | +- [ ] Reboot Pi with a meds event in the past → single consolidated catch-up wake, no storm |
| 81 | + SKIPPED — explicitly instructed not to reboot the Pi during this |
| 82 | + automated pass. |
| 83 | + |
| 84 | +- [ ] Full heartbeat observed: cron fires, session runs state, journals a decision |
| 85 | + SKIPPED — install-cron.sh intentionally NOT run yet (final human-gated |
| 86 | + switch, see Step 4 notes). No heartbeat cron installed, so nothing to |
| 87 | + observe. |
| 88 | + |
| 89 | +## Step 1: Pi prerequisites — DONE (automated) |
| 90 | +`sudo -n apt-get update && sudo -n apt-get install -y tmux espeak-ng ffmpeg && sudo -n loginctl enable-linger pi` |
| 91 | +ran successfully with passwordless sudo (no human needed). `tmux` and |
| 92 | +`espeak-ng` newly installed; `ffmpeg` already present. `loginctl show-user pi` |
| 93 | +confirms `Linger=yes`. |
| 94 | + |
| 95 | +## Step 2: Piper — PIPER_OK |
| 96 | +`pipx` is not installed on the Pi (`command not found: pipx`), so fell back to |
| 97 | +the documented alternative: `~/adhdo2/venv/bin/pip install piper-tts`, which |
| 98 | +succeeded (`piper-tts-1.4.2`, plus `onnxruntime`, `numpy`, etc). Confirmed |
| 99 | +working: `echo test | ~/adhdo2/venv/bin/piper --help` exits 0 and prints usage. |
| 100 | +Result: **PIPER_OK** (not falling back to espeak-ng, though espeak-ng is also |
| 101 | +installed as a safety net per Step 1). |
| 102 | + |
| 103 | +## Step 3: Static IP / devices — partially human |
| 104 | +- Pi's current LAN IP: `192.168.0.115` (eth0, confirmed via `ip route get 1.1.1.1`). |
| 105 | +- DID NOT set a DHCP reservation in the router — **human step**, noted. |
| 106 | +- Cast devices discovered via `avahi-browse -t -r _googlecast._tcp`: |
| 107 | + - `Shack Speakers` (Chromecast Audio) |
| 108 | + - `SHIELD` (SHIELD Android TV) |
| 109 | + - `Nest Hub Max` (Google Nest Hub Max) |
| 110 | + - `Nest Mini` (Google Nest Mini) |
| 111 | + - `Office Mini` (Google Home Mini) |
| 112 | +- Wrote `~/adhdo2/config.yaml` on the Pi with: |
| 113 | + - `lan_ip: 192.168.0.115` |
| 114 | + - `devices: {office: "Nest Hub Max", kitchen: "Nest Mini", shack: "Shack Speakers", officemini: "Office Mini"}` |
| 115 | + - `default_device: office` (Nest Hub Max) |
| 116 | + - `moods.calm.streams: ["https://ice1.somafm.com/groovesalad-256-mp3"]` |
| 117 | + - `moods.focus.streams: ["https://ice1.somafm.com/dronezone-256-mp3"]` |
| 118 | + - `jellyfin.url: "http://192.168.0.115:8096"` (api_key still placeholder, |
| 119 | + pending Jellyfin setup wizard — human step) |
| 120 | + - `moods.*.jellyfin_playlist` left as `"<id>"` placeholders — human step, |
| 121 | + depends on Jellyfin Music library + playlists being created. |
| 122 | + |
| 123 | +## Step 4: Deploy + services |
| 124 | +- `adhdo2/scripts/deploy.sh` run from the worktree: rsync + venv + pip installs |
| 125 | + succeeded. Fixed a review-noted gap: deploy.sh now also |
| 126 | + `mkdir -p jellyfin/config` (previously only `data tts-cache`), needed by the |
| 127 | + Jellyfin compose volume mount. (Committed below.) |
| 128 | +- Also discovered adhdolib isn't pip-installable as-is (`pyproject.toml` has |
| 129 | + no package/module discovery config, so `pip install -e .` fails with a |
| 130 | + multi-package-discovery error). Worked around by invoking scripts with |
| 131 | + `PYTHONPATH=/home/pi/adhdo2` — this is what `bin/state` needed to find |
| 132 | + `adhdolib`. Not fixed in pyproject.toml since scope of this task was |
| 133 | + provisioning, not a packaging refactor; flagging for a future task. |
| 134 | +- `systemctl --user enable --now adhdo-httpd` → **active (running)**, PID |
| 135 | + confirmed, serving `python3 -m http.server 8765`. |
| 136 | +- `systemctl --user enable --now adhdo-tmux` → **active (running)**. |
| 137 | + `tmux capture-pane -t adhdo -p` showed: |
| 138 | + 1. A one-time "trust this folder" dialog (answered "1. Yes" — this is not |
| 139 | + an auth step, just Claude Code's folder-trust prompt, safe to confirm). |
| 140 | + 2. A "try the new fullscreen renderer?" UI prompt (answered "2. Not now" — |
| 141 | + cosmetic, not auth-related). |
| 142 | + 3. Final state: Claude Code v2.1.202 is running but the status bar reads |
| 143 | + **"Not logged in · Run /login"**. |
| 144 | + **HUMAN STEP REQUIRED**: an interactive `claude /login` (OAuth flow) must be |
| 145 | + completed in the `adhdo` tmux pane over SSH before wake/nudge-to-Claude |
| 146 | + interactions will work. Did not attempt to run `/login` myself since it |
| 147 | + requires opening a browser-based OAuth URL and pasting a code back — an |
| 148 | + interactive human action, exactly as anticipated in the task brief. |
| 149 | +- `install-cron.sh` was **NOT run**, as instructed — this is the final |
| 150 | + human-gated switch (starts heartbeats/catch-up before the human verifies the |
| 151 | + Claude session works). **Human step**: run |
| 152 | + `ssh pi@pi5-hailo '~/adhdo2/scripts/install-cron.sh'` only after confirming |
| 153 | + `/login` succeeded and a basic `bin/wake` round-trip works. |
| 154 | + |
| 155 | +## Step 5: Jellyfin |
| 156 | +- `docker compose -f jellyfin-compose.yml up -d` pulled the image and started |
| 157 | + the container successfully (`docker ps` shows `jellyfin` container `Up ... |
| 158 | + (healthy)`). |
| 159 | +- Verified port 8096 responds: `curl -s -o /dev/null -w '%{http_code}' |
| 160 | + http://localhost:8096/` → `302` (redirect to setup wizard — expected for a |
| 161 | + fresh instance). `/health` → `503` (also expected pre-setup-wizard). |
| 162 | +- **HUMAN STEP REQUIRED**: browse to `http://192.168.0.115:8096`, complete the |
| 163 | + setup wizard, disable transcoding + chapter images, add a Music library, |
| 164 | + create `focus`/`calm` playlists, generate an API key, and put the playlist |
| 165 | + IDs + API key into `~/adhdo2/config.yaml` on the Pi (currently placeholders). |
| 166 | + |
| 167 | +## Bugs found & fixed during this pass |
| 168 | +1. `bin/state`'s `scan_devices()` crashed (`AttributeError: 'NoneType' object |
| 169 | + has no attribute 'loader'`) loading `bin/cast` via |
| 170 | + `importlib.util.spec_from_file_location` without an explicit loader (the |
| 171 | + file has no `.py` extension so the loader couldn't be inferred). Fixed by |
| 172 | + using `importlib.machinery.SourceFileLoader` explicitly. Committed. |
| 173 | +2. `deploy.sh` didn't create `~/adhdo2/jellyfin/config` (needed by the |
| 174 | + Jellyfin docker-compose volume mount) — added `mkdir -p ... jellyfin/config` |
| 175 | + alongside the existing `data tts-cache`. Committed. |
| 176 | + |
| 177 | +## Summary of human-gated remaining work |
| 178 | +1. Router DHCP reservation for `192.168.0.115` (Pi's current IP). |
| 179 | +2. Interactive `claude /login` inside the `adhdo` tmux pane over SSH. |
| 180 | +3. Jellyfin setup wizard (`http://192.168.0.115:8096`): admin account, disable |
| 181 | + transcoding + chapter images, add Music library, create `focus`/`calm` |
| 182 | + playlists, generate API key; then fill in `jellyfin.api_key` and |
| 183 | + `moods.*.jellyfin_playlist` in `~/adhdo2/config.yaml`. |
| 184 | +4. All audible verification items in this checklist (cast playback, nudge TTS, |
| 185 | + resume-after-nudge timing, rate-limit-with-real-device, quiet-hours |
| 186 | + high-urgency override, wake→Claude round-trip, dispatcher delivery, |
| 187 | + watchdog respawn, recycle handoff, reboot catch-up, full heartbeat) — all |
| 188 | + require either a logged-in Claude session, a completed Jellyfin setup, or a |
| 189 | + human physically listening at the Pi. |
| 190 | +5. Run `~/adhdo2/scripts/install-cron.sh` only after (2) is confirmed working |
| 191 | + via a manual `bin/wake` test — this starts the heartbeat/catch-up cron and |
| 192 | + was intentionally left un-run by this automated pass. |
| 193 | +6. Consider fixing `pyproject.toml` package discovery so `pip install -e .` |
| 194 | + works without needing `PYTHONPATH` workarounds (not done in this pass — |
| 195 | + out of scope for provisioning, noted for a future cleanup task). |
| 196 | +7. The `_resume_previous()` polling refinement mentioned in the brief (poll |
| 197 | + `media_controller.status` until idle, max 30s) was NOT implemented — it |
| 198 | + requires audible hardware observation to tune correctly, which is a human |
| 199 | + step. |
0 commit comments