feat: full integration with FreeRTOS tasks and slideshow state machine
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
213
src/main.cpp
213
src/main.cpp
@@ -1,27 +1,217 @@
|
||||
#include <Arduino.h>
|
||||
#include <M5Unified.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());
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ String PhotoQueue::next() {
|
||||
String id = _queue[_cursor++];
|
||||
|
||||
// Persist cursor
|
||||
_settings->saveField("queue_cursor", static_cast<uint8_t>(_cursor & 0xFF));
|
||||
_settings->saveField("queue_cursor", static_cast<uint32_t>(_cursor));
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user