feat: deep sleep single-fetch with probabilistic weighting

Refactor timerWakeCycle() to fetch a single random photo per wake
using cycle-mode-aware server-side filtering via Immich's
POST /api/search/random endpoint.

Changes:
- Add fetchOneRandomAssetId() to ImmichClient with support for
  isFavorite, takenAfter, takenBefore filter parameters
- Add timer_last_date NVS field for chronological cursor persistence
- Implement probabilistic filter selection per cycle mode:
  - Random: unfiltered size=1
  - Chronological/ReverseChronological: date cursor in NVS
  - FavoritesWeighted: 66% favorites, 33% unfiltered
  - WeightedChronological: 66% recent (30d), 33% unfiltered
  - WeightedReverseChronological: 66% older (30d), 33% unfiltered
- Add retry loop with filter relaxation on empty results
- Initialize hardware RTC (RX8130CE) firmware-wide:
  - Seed system clock from RTC at boot (immediate time)
  - NTP sync after WiFi connect, write back to RTC
- Add time_utils.h with isoNow(), isoNowMinus30d(), hasValidTime()

Normal mode (PhotoQueue + displayTask) is completely untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-04 20:57:59 -04:00
parent 3483b9bef8
commit b9e5044fd2
8 changed files with 285 additions and 8 deletions

View File

@@ -2,6 +2,7 @@
#include <M5Unified.h>
#include <ArduinoJson.h>
#include <esp_heap_caps.h>
#include <sys/time.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
@@ -16,6 +17,7 @@
#include "image_pipeline.h"
#include "display_manager.h"
#include "web_server.h"
#include "time_utils.h"
// Global instances
SettingsManager settingsManager;
@@ -353,6 +355,74 @@ void webActionCallback(const String& action) {
}
}
// Build a RandomFetchFilter based on the current cycle mode and probability.
// Returns a filter with appropriate date/favorite constraints.
static RandomFetchFilter buildTimerFilter(const Settings& s,
const std::vector<String>& albumIds) {
RandomFetchFilter filter;
filter.albumIds = albumIds;
// If system clock isn't valid, weighted/chrono modes can't work — use plain random
if (!hasValidTime()) {
Serial.println("[timer_wake] No valid time — using unfiltered random");
return filter;
}
// Probability roll: 0-99
int roll = random(0, 100);
switch (s.cycle_mode) {
case CycleMode::Random:
// No extra filters
break;
case CycleMode::Chronological:
if (s.timer_last_date.length() > 0) {
filter.takenAfter = s.timer_last_date;
} else {
filter.takenAfter = "1970-01-01T00:00:00.000Z";
}
break;
case CycleMode::ReverseChronological:
if (s.timer_last_date.length() > 0) {
filter.takenBefore = s.timer_last_date;
} else {
filter.takenBefore = isoNow();
}
break;
case CycleMode::FavoritesWeighted:
// 66% chance: favorites only, 33% chance: no filter
if (roll < 66) {
filter.favoriteOnly = true;
}
break;
case CycleMode::WeightedChronological:
// 66% chance: recent photos (last 30 days), 33% chance: no filter
if (roll < 66) {
filter.takenAfter = isoNowMinus30d();
}
break;
case CycleMode::WeightedReverseChronological:
// 66% chance: older photos (before 30 days ago), 33% chance: no filter
if (roll < 66) {
filter.takenBefore = isoNowMinus30d();
}
break;
default: {
CycleMode unreachable = s.cycle_mode;
(void)unreachable;
break;
}
}
return filter;
}
// Minimal boot path for RTC timer wake: fetch one photo, display, sleep again
void timerWakeCycle() {
Serial.println("[timer_wake] Starting minimal wake cycle");
@@ -373,10 +443,6 @@ void timerWakeCycle() {
imagePipeline.setBlueNoiseEnabled(s.dither_noise != 0);
displayManager.setTranslucent(s.meta_translucent != 0);
// Fetch assets — respect album selection
static constexpr int TIMER_WAKE_RETRIES = 5;
bool displayed = false;
// Parse selected album IDs from settings
std::vector<String> albumIds;
{
@@ -390,14 +456,49 @@ void timerWakeCycle() {
}
}
std::vector<String> ids = immichClient.fetchRandomAssetIds(10, albumIds);
// Single-fetch with probabilistic filtering and retry
static constexpr int TIMER_WAKE_RETRIES = 5;
bool displayed = false;
bool filterRelaxed = false;
for (int i = 0; i < (int)ids.size() && i < TIMER_WAKE_RETRIES && !displayed; i++) {
AssetInfo info = immichClient.fetchAssetInfo(ids[i]);
for (int attempt = 0; attempt < TIMER_WAKE_RETRIES && !displayed; attempt++) {
RandomFetchFilter filter;
if (filterRelaxed) {
filter.albumIds = albumIds;
} else {
filter = buildTimerFilter(s, albumIds);
}
String assetId = immichClient.fetchOneRandomAssetId(filter);
if (assetId.length() == 0) {
if (!filterRelaxed && (filter.favoriteOnly ||
filter.takenAfter.length() > 0 || filter.takenBefore.length() > 0)) {
// Filter returned no results — relax and retry
Serial.println("[timer_wake] Filtered request empty — relaxing filter");
filterRelaxed = true;
// For chronological modes, reset the cursor (wrapped around)
if (s.cycle_mode == CycleMode::Chronological ||
s.cycle_mode == CycleMode::ReverseChronological) {
settingsManager.saveField("timer_date", "");
s.timer_last_date = "";
}
continue;
}
Serial.println("[timer_wake] No assets available");
break;
}
Serial.printf("[timer_wake] Asset: %s (attempt %d)\n",
assetId.c_str(), attempt + 1);
AssetInfo info = immichClient.fetchAssetInfo(assetId);
uint8_t* jpegBuf = nullptr;
size_t jpegSize = 0;
if (!immichClient.downloadAsset(ids[i], s.img_quality, &jpegBuf, &jpegSize)) {
if (!immichClient.downloadAsset(assetId, s.img_quality, &jpegBuf, &jpegSize)) {
Serial.println("[timer_wake] Download failed, retrying");
continue;
}
@@ -418,6 +519,13 @@ void timerWakeCycle() {
displayManager.refresh();
imagePipeline.freeImage(img);
displayed = true;
// Update chronological cursor with this photo's date
if (info.dateTime.length() > 0) {
settingsManager.saveField("timer_date", info.dateTime.c_str());
}
} else {
Serial.println("[timer_wake] Processing failed, retrying");
}
}
@@ -445,6 +553,28 @@ void setup() {
Serial.printf("[main] Internal RAM: %u KB free\n",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
// Seed system clock from hardware RTC (RX8130CE) for immediate time availability
{
auto rtcDt = M5.Rtc.getDateTime();
struct tm t = {};
t.tm_year = rtcDt.date.year - 1900;
t.tm_mon = rtcDt.date.month - 1;
t.tm_mday = rtcDt.date.date;
t.tm_hour = rtcDt.time.hours;
t.tm_min = rtcDt.time.minutes;
t.tm_sec = rtcDt.time.seconds;
time_t rtcTime = mktime(&t);
if (rtcTime > 1600000000) { // Plausible (post-2020)
struct timeval tv = { .tv_sec = rtcTime, .tv_usec = 0 };
settimeofday(&tv, nullptr);
Serial.printf("[main] System clock seeded from RTC: %04d-%02d-%02d %02d:%02d:%02d\n",
rtcDt.date.year, rtcDt.date.month, rtcDt.date.date,
rtcDt.time.hours, rtcDt.time.minutes, rtcDt.time.seconds);
} else {
Serial.println("[main] RTC has no valid time — will sync from NTP after WiFi");
}
}
// Initialize core subsystems needed for both wake paths
powerManager.begin();
settingsManager.begin();