From 034a061293b04787e8de1f7552a0b1eae7ab499a Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 3 Aug 2026 11:00:02 -0400 Subject: [PATCH] feat: full integration with FreeRTOS tasks and slideshow state machine Co-authored-by: Cursor --- src/main.cpp | 213 ++++++++++++++++++++++++++++++++++++++++++-- src/photo_queue.cpp | 2 +- src/settings.cpp | 4 + src/settings.h | 1 + 4 files changed, 213 insertions(+), 7 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index d43eac7..4c7e5fa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,27 +1,217 @@ #include #include +#include +#include +#include + #include "config.h" #include "settings.h" #include "wifi_manager.h" #include "power_manager.h" #include "button_handler.h" +#include "immich_client.h" +#include "photo_queue.h" +#include "image_pipeline.h" +#include "display_manager.h" +#include "web_server.h" +// Global instances SettingsManager settingsManager; WiFiManager wifiManager; PowerManager powerManager; ButtonHandler buttonHandler; +ImmichClient immichClient; +PhotoQueue photoQueue; +ImagePipeline imagePipeline; +DisplayManager displayManager; +AppWebServer webServer; + +// Shared state +SemaphoreHandle_t stateMutex; +volatile bool slideshowPlaying = true; +volatile bool refreshRequested = false; +volatile bool randomRequested = false; +volatile unsigned long lastRefreshTime = 0; + +// Task handles +TaskHandle_t displayTaskHandle = nullptr; + +void displayTask(void* param) { + Serial.println("[display_task] Started on Core 1"); + + // Initial sync + if (wifiManager.isConnected()) { + Settings s = settingsManager.get(); + immichClient.begin(s.immich_url, s.immich_key); + photoQueue.begin(immichClient, settingsManager); + + if (photoQueue.sync()) { + Serial.printf("[display_task] Queue ready: %d photos\n", photoQueue.size()); + } else { + displayManager.showMessage("No Photos", "Select albums in web UI"); + } + } + + while (true) { + unsigned long now = millis(); + Settings s = settingsManager.get(); + unsigned long intervalMs = s.interval_min * 60000UL; + + bool shouldRefresh = false; + bool wantsRandom = false; + + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + if (refreshRequested) { + shouldRefresh = true; + refreshRequested = false; + } else if (randomRequested) { + shouldRefresh = true; + wantsRandom = true; + randomRequested = false; + } else if (slideshowPlaying && (now - lastRefreshTime >= intervalMs)) { + shouldRefresh = true; + } + xSemaphoreGive(stateMutex); + } + + // Periodic re-sync + if (photoQueue.needsResync() && wifiManager.isConnected()) { + photoQueue.sync(); + } + + if (shouldRefresh && photoQueue.size() > 0 && wifiManager.isConnected()) { + // Get next photo ID + String assetId; + if (wantsRandom) { + assetId = photoQueue.random(); + } else { + assetId = photoQueue.next(); + } + + if (assetId.length() > 0) { + Serial.printf("[display_task] Loading asset: %s\n", assetId.c_str()); + + // Fetch asset info for portrait detection and metadata + AssetInfo info = immichClient.fetchAssetInfo(assetId); + + // Download photo + uint8_t* jpegBuf = nullptr; + size_t jpegSize = 0; + bool downloaded = immichClient.downloadAsset( + assetId, s.img_quality, &jpegBuf, &jpegSize); + + if (downloaded && jpegBuf != nullptr) { + ProcessedImage img; + + if (info.isPortrait) { + // Try to find a portrait pair + String pairId = photoQueue.findPortraitPair( + settingsManager.get().queue_cursor); + if (pairId.length() > 0) { + AssetInfo pairInfo = immichClient.fetchAssetInfo(pairId); + if (pairInfo.isPortrait) { + uint8_t* jpeg2Buf = nullptr; + size_t jpeg2Size = 0; + if (immichClient.downloadAsset(pairId, s.img_quality, + &jpeg2Buf, &jpeg2Size)) { + img = imagePipeline.processPortraitPair( + jpegBuf, jpegSize, jpeg2Buf, jpeg2Size); + free(jpeg2Buf); + } else { + img = imagePipeline.process(jpegBuf, jpegSize); + } + } else { + img = imagePipeline.process(jpegBuf, jpegSize); + } + } else { + img = imagePipeline.process(jpegBuf, jpegSize); + } + } else { + img = imagePipeline.process(jpegBuf, jpegSize); + } + + free(jpegBuf); + + if (img.valid) { + displayManager.showImage(img); + // Show metadata overlay if enabled + if (s.meta_flags != 0) { + displayManager.showMetadata(info, s.meta_flags, s.meta_pos); + } + imagePipeline.freeImage(img); + } else { + Serial.println("[display_task] Image processing failed"); + } + } + + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + lastRefreshTime = millis(); + xSemaphoreGive(stateMutex); + } + } + } + + // Yield — check every second + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +void webActionCallback(const String& action) { + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + if (action == "next") { + refreshRequested = true; + } else if (action == "random") { + randomRequested = true; + } else if (action == "pause") { + slideshowPlaying = false; + } else if (action == "play") { + slideshowPlaying = true; + } + xSemaphoreGive(stateMutex); + } +} void setup() { auto cfg = M5.config(); M5.begin(cfg); Serial.begin(115200); - Serial.println("[main] Immich Frame booting..."); + Serial.println("[main] Immich Frame v1.0 booting..."); + // Create state mutex + stateMutex = xSemaphoreCreateMutex(); + + // Initialize subsystems powerManager.begin(); settingsManager.begin(); wifiManager.begin(settingsManager); buttonHandler.begin(powerManager, settingsManager); + displayManager.begin(powerManager); + + if (!wifiManager.isAPMode()) { + // Station mode — set up Immich and web server + Settings s = settingsManager.get(); + immichClient.begin(s.immich_url, s.immich_key); + photoQueue.begin(immichClient, settingsManager); + } + + // Start web server (works in both AP and station modes) + webServer.begin(settingsManager, immichClient, powerManager, wifiManager); + webServer.setActionCallback(webActionCallback); + + if (wifiManager.isAPMode()) { + displayManager.showMessage("Setup Required", + "Connect to PaperColor-Setup WiFi"); + } else { + displayManager.showMessage("Immich Frame", "Loading photos..."); + + // Create display task on Core 1 + xTaskCreatePinnedToCore(displayTask, "display", 32768, nullptr, 1, + &displayTaskHandle, 1); + } + + // Enable light sleep for power saving + powerManager.enableLightSleep(); Serial.println("[main] Boot complete"); } @@ -30,20 +220,31 @@ void loop() { M5.update(); powerManager.updateBatteryLED(); + // Handle button events ButtonEvent event = buttonHandler.poll(); switch (event) { case ButtonEvent::NextPhoto: - Serial.println("[main] → Next photo requested"); + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + refreshRequested = true; + xSemaphoreGive(stateMutex); + } break; case ButtonEvent::RandomPhoto: - Serial.println("[main] → Random photo requested"); + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + randomRequested = true; + xSemaphoreGive(stateMutex); + } break; case ButtonEvent::PlayPause: - Serial.println("[main] → Play/Pause toggled"); + if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + slideshowPlaying = !slideshowPlaying; + Serial.printf("[main] Slideshow: %s\n", slideshowPlaying ? "playing" : "paused"); + xSemaphoreGive(stateMutex); + } break; case ButtonEvent::DeepSleep: case ButtonEvent::FactoryReset: - break; // Handled internally + break; case ButtonEvent::None: break; default: { @@ -53,5 +254,5 @@ void loop() { } } - delay(10); // 100Hz poll rate for responsive buttons + delay(10); } diff --git a/src/photo_queue.cpp b/src/photo_queue.cpp index 90050e7..4835249 100644 --- a/src/photo_queue.cpp +++ b/src/photo_queue.cpp @@ -87,7 +87,7 @@ String PhotoQueue::next() { String id = _queue[_cursor++]; // Persist cursor - _settings->saveField("queue_cursor", static_cast(_cursor & 0xFF)); + _settings->saveField("queue_cursor", static_cast(_cursor)); return id; } diff --git a/src/settings.cpp b/src/settings.cpp index 93ad939..2a432ca 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -46,6 +46,10 @@ void SettingsManager::saveField(const char* key, uint8_t value) { writeU8(key, value); } +void SettingsManager::saveField(const char* key, uint32_t value) { + writeU32(key, value); +} + void SettingsManager::saveField(const char* key, const char* value) { writeString(key, String(value)); } diff --git a/src/settings.h b/src/settings.h index e0b54f1..34e9c7e 100644 --- a/src/settings.h +++ b/src/settings.h @@ -57,6 +57,7 @@ public: Settings get(); void save(const Settings& s); void saveField(const char* key, uint8_t value); + void saveField(const char* key, uint32_t value); void saveField(const char* key, const char* value); void factoryReset(); bool isProvisioned();