diff --git a/docs/superpowers/specs/2026-08-04-deep-sleep-single-fetch-design.md b/docs/superpowers/specs/2026-08-04-deep-sleep-single-fetch-design.md new file mode 100644 index 0000000..9f39404 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-deep-sleep-single-fetch-design.md @@ -0,0 +1,113 @@ +# Deep Sleep Single-Fetch with Probabilistic Weighting + +**Date**: 2026-08-04 +**Status**: Approved + +## Overview + +Refactor the deep sleep wake path (`timerWakeCycle`) to fetch a single random photo per wake using probabilistic server-side filtering based on `cycle_mode`. Replaces the current 10-asset batch fetch with a leaner single-call approach that honors weighting settings while minimizing API calls and battery consumption. + +## Motivation + +In deep sleep slideshow mode, buttons don't respond and there's no photo queue. The device simply wakes, shows one photo, and sleeps again. Fetching 10 assets and picking one wastes HTTP round-trips, bandwidth, and battery. A single `POST /api/search/random` call with `size=1` and appropriate filters is sufficient. + +## Immich API Capabilities + +The `POST /api/search/random` endpoint (Immich v3) supports these filters relevant to our use case: + +- `size` — number of results +- `type` — media type (IMAGE) +- `albumIds` — array of album UUIDs +- `isFavorite` — boolean filter +- `takenAfter` — ISO 8601 datetime string +- `takenBefore` — ISO 8601 datetime string + +These server-side filters allow meaningful weighting without fetching large batches. + +## Design + +### Filter Selection per Cycle Mode + +| Mode | Filter applied | Probability | Fallback | +|------|---------------|-------------|----------| +| Random | No extra filter | 100% | — | +| Chronological | `takenAfter=last_shown_date` | 100% | Reset date to epoch | +| ReverseChronological | `takenBefore=last_shown_date` | 100% | Reset date to "now" | +| FavoritesWeighted | `isFavorite=true` | 66% | Drop `isFavorite` | +| FavoritesWeighted | No extra filter | 33% | — | +| WeightedChronological | `takenAfter=(now - 30 days)` | 66% | Drop date filter | +| WeightedChronological | No extra filter | 33% | — | +| WeightedReverseChronological | `takenBefore=(now - 30 days)` | 66% | Drop date filter | +| WeightedReverseChronological | No extra filter | 33% | — | + +Album IDs from settings are always included alongside any other filter. + +### Retry Logic + +``` +attempt = 0 +while (!displayed && attempt < MAX_RETRIES): + 1. Choose filter based on cycle_mode + probability roll + 2. POST /api/search/random (size=1, filter + albumIds) + 3. If empty response: + - If using a filter: retry without filter (relax) + - If already unfiltered: give up (no photos available) + 4. Download + process the asset + 5. If download/process fails: increment attempt, loop + 6. If success: display, update timer_last_date in NVS, break +``` + +MAX_RETRIES = 5 (matches current `TIMER_WAKE_RETRIES`). + +### NVS Persistence + +New Settings field: `timer_last_date` (String, ISO datetime). + +- Updated after each successful display in deep sleep mode +- Used as the cursor for Chronological and ReverseChronological modes +- Empty string means "no cursor" (start from beginning/end) + +### RTC Initialization (Firmware-Wide) + +The PaperColor has a hardware RX8130CE Real-Time Clock (I2C 0x32) that persists across deep sleep. It is not currently used in the firmware. + +**At boot (both paths), before WiFi:** +- Seed ESP32 system clock from hardware RTC via `M5.Rtc.getDateTime()` + `settimeofday()` + +**After WiFi connects (both paths):** +- Sync system clock from NTP via `configTime(0, 0, "pool.ntp.org")` +- Write corrected time back to hardware RTC via `M5.Rtc.setDateTime()` + +**Benefits:** +- Deep sleep date filters are reliable +- Normal mode metadata overlays (strftime) display correct time +- Future time-dependent features become trivial + +### Date Helpers + +New `src/time_utils.h`: +- `String isoNow()` — current time as ISO 8601 (e.g., `"2026-08-04T17:30:00.000Z"`) +- `String isoNowMinus30d()` — 30 days ago as ISO 8601 +- `bool hasValidTime()` — returns false if year < 2020 (RTC never synced) + +## Files Modified + +- `src/immich_client.h` — add `RandomFetchFilter` struct + `fetchOneRandomAssetId()` declaration +- `src/immich_client.cpp` — implement `fetchOneRandomAssetId()` +- `src/settings.h` — add `timer_last_date` field +- `src/settings.cpp` — add NVS read/write for `timer_date` +- `src/main.cpp` — refactor `timerWakeCycle()`, seed system clock from RTC at boot +- `src/wifi_manager.cpp` — add NTP sync after WiFi connect, write back to hardware RTC +- New: `src/time_utils.h` — ISO date helpers + +## Normal Mode Impact + +- **Slideshow logic**: Untouched. Normal mode still uses `PhotoQueue` with `fetchRandomAssets(50)`. +- **RTC/NTP sync**: Applied to both modes as a general firmware improvement. + +## Edge Cases + +- No photos match filter: fallback to unfiltered, then give up +- RTC never synced (first boot or battery drained): detect year < 2020, fall back to random for weighted modes +- Chronological cursor exhausted: when `takenAfter=cursor` returns empty, reset cursor to "" (wraps around) +- Album has no favorites: `isFavorite=true` + albumIds returns empty, fallback drops `isFavorite`