Files
immich-frame/src/main.cpp

262 lines
8.7 KiB
C++
Raw Normal View History

#include <Arduino.h>
#include <M5Unified.h>
#include <esp_heap_caps.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#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());
// Trigger first photo immediately
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
refreshRequested = true;
xSemaphoreGive(stateMutex);
}
} 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()) {
// Try up to 3 assets in case some fail to decode
static constexpr int MAX_RETRIES = 10;
bool displayed = false;
for (int attempt = 0; attempt < MAX_RETRIES && !displayed; attempt++) {
String assetId;
if (wantsRandom) {
assetId = photoQueue.random();
} else {
assetId = photoQueue.next();
}
if (assetId.length() == 0) break;
Serial.printf("[display_task] Loading asset: %s (attempt %d)\n",
assetId.c_str(), attempt + 1);
// 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) {
Serial.println("[display_task] Download failed, trying next");
continue;
}
Serial.printf("[display_task] JPEG: %u KB, PSRAM free: %u KB\n",
(unsigned)(jpegSize / 1024),
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
ProcessedImage img = imagePipeline.process(jpegBuf, jpegSize);
free(jpegBuf);
Serial.printf("[display_task] Post-process PSRAM free: %u KB\n",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
if (img.valid) {
displayManager.showImage(img);
if (s.meta_flags != 0) {
displayManager.showMetadata(info, s.meta_flags, s.meta_pos);
}
imagePipeline.freeImage(img);
displayed = true;
} else {
Serial.println("[display_task] Processing failed, trying next");
}
}
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();
cfg.clear_display = false; // Don't clear/refresh e-ink on init
M5.begin(cfg);
// Prevent any automatic e-ink refreshes until we explicitly call display()
M5.Display.setAutoDisplay(false);
Serial.begin(115200);
Serial.println("[main] Immich Frame v1.0 booting...");
Serial.printf("[main] PSRAM: %u KB free / %u KB total\n",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024),
(unsigned)(heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024));
Serial.printf("[main] Internal RAM: %u KB free\n",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
// 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);
// Single display update after all init is complete
if (wifiManager.isAPMode()) {
displayManager.showSetupScreen();
} else {
displayManager.showMessage("Immich Frame", "Loading photos...");
// Create display task on Core 1
xTaskCreatePinnedToCore(displayTask, "display", 32768, nullptr, 1,
&displayTaskHandle, 1);
// Enable light sleep only in station mode (not supported in AP mode)
powerManager.enableLightSleep();
}
Serial.println("[main] Boot complete");
}
void loop() {
M5.update();
powerManager.updateBatteryLED();
// Handle button events
ButtonEvent event = buttonHandler.poll();
switch (event) {
case ButtonEvent::NextPhoto:
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
refreshRequested = true;
xSemaphoreGive(stateMutex);
}
break;
case ButtonEvent::RandomPhoto:
if (xSemaphoreTake(stateMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
randomRequested = true;
xSemaphoreGive(stateMutex);
}
break;
case ButtonEvent::PlayPause:
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;
case ButtonEvent::None:
break;
default: {
ButtonEvent unreachable = event;
(void)unreachable;
break;
}
}
delay(10);
}