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:
146
src/main.cpp
146
src/main.cpp
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user