#include "Display_ST7789.h" #include "SD_Card.h" #include "Display_PNG.h" #include "Audio_Player.h" #include "Web_Server.h" #define BOOT_KEY_PIN 0 // ── App state ───────────────────────────────────────────────────────────────── enum AppState { STATE_IDLE, STATE_AUTOPLAY, STATE_ERROR }; static AppState currentState = STATE_IDLE; static bool hasError = false; // ── Button gesture state ────────────────────────────────────────────────────── static bool lastButtonState = HIGH; static uint32_t buttonPressStartTime = 0; static uint32_t lastButtonRelease = 0; static uint8_t clickCount = 0; static bool isButtonPressed = false; static bool longPressTriggered = false; // Autoplay static uint32_t lastSlideTime = 0; static const uint32_t SLIDE_INTERVAL = 8000; // 8 s between slides in autoplay // Tribute Daily Alarm Run-Through State static bool g_tributeActive = false; static int g_tributeNextIndex = 0; static uint32_t lastTributeCheck = 0; // Standby Remembrance Candle State static bool g_standbyActive = false; static uint32_t lastInteractionTime = 0; static const uint32_t STANDBY_TIMEOUT = 120000; // 2 minutes standby timeout // ── Core function: show a memory ────────────────────────────────────────────── void showMemory(int index) { if (g_memoryCount == 0) return; if (index < 0 || index >= g_memoryCount) index = 0; g_currentIndex = index; printf("Showing memory %d: '%s'\r\n", index, g_memories[index].name); g_standbyActive = false; lastInteractionTime = millis(); stopAudio(); Display_Memory_Photo(index); playMemoryAudio(index); Config_Save(); // persist currentIndex } // ── Button handlers ─────────────────────────────────────────────────────────── void handleSinglePress() { if (hasError || g_memoryCount == 0) return; g_tributeActive = false; // Cancel alarm playback if active if (currentState == STATE_AUTOPLAY) currentState = STATE_IDLE; showMemory((g_currentIndex + 1) % g_memoryCount); } void handleDoublePress() { if (hasError) return; pauseResumeAudio(); } void handleTriplePress() { if (hasError || g_memoryCount == 0) return; g_tributeActive = false; // Cancel alarm playback if active if (currentState == STATE_AUTOPLAY) currentState = STATE_IDLE; showMemory((g_currentIndex + g_memoryCount - 1) % g_memoryCount); } void handleQuadruplePress() { uint8_t v = getPlayerVolume() + 4; if (v > 21) v = 4; setPlayerVolume(v); printf("Volume: %d/21\r\n", v); } void handleLongPress() { if (hasError) return; g_tributeActive = false; // Cancel alarm playback if active if (currentState == STATE_AUTOPLAY) { currentState = STATE_IDLE; stopAudio(); printf("Autoplay OFF\r\n"); } else { currentState = STATE_AUTOPLAY; lastSlideTime = millis(); showMemory(g_currentIndex); printf("Autoplay ON\r\n"); } } // ── Setup ───────────────────────────────────────────────────────────────────── void setup() { Serial.begin(115200); delay(1000); printf("\r\n============================\r\n"); printf(" Memory Frame v2.0\r\n"); printf("============================\r\n"); // SD card first (so SD_MMC driver does not overwrite LCD CS pin 21) Flash_test(); SD_Init(); // Display second LCD_Init(); Set_Backlight(90); // Load memories from config Config_Load(); // Button pinMode(BOOT_KEY_PIN, INPUT_PULLUP); // Audio player initAudioPlayer(); setPlayerVolume(g_volume); // Show favorite memory or first memory on startup if (g_memoryCount == 0) { printf("No memories found. Add some via the web app.\r\n"); g_standbyActive = true; Draw_Sunflower_Pattern(); } else { int favoriteIndex = -1; if (g_favoriteMemoryId >= 0) { for (int i = 0; i < g_memoryCount; i++) { if (g_memories[i].id == g_favoriteMemoryId) { favoriteIndex = i; break; } } } if (favoriteIndex >= 0) { showMemory(favoriteIndex); } else { showMemory(g_currentIndex); } } // Start WiFi + web server initWebServer(); printf("Ready! %d memor%s loaded.\r\n", g_memoryCount, g_memoryCount==1?"y":"ies"); } // ── Loop ────────────────────────────────────────────────────────────────────── void loop() { bool btn = digitalRead(BOOT_KEY_PIN); uint32_t now = millis(); // ── Web display request (triggered by phone) ────────────────────────────── if (g_webDisplayDirty) { g_webDisplayDirty = false; g_tributeActive = false; // Cancel alarm playback if user interacts int req = g_webDisplayRequest; g_webDisplayRequest = -1; if (req >= 0 && req < g_memoryCount) { showMemory(req); } } // ── Tribute Daily Time Alarm Trigger ────────────────────────────────────── if (g_timeSynchronized && g_tributeAlarmEnabled && g_memoryCount > 0 && !g_tributeActive) { if (now - lastTributeCheck >= 1000) { lastTributeCheck = now; time_t locTime = time(NULL) - g_timezoneOffset; struct tm* lt = gmtime(&locTime); if (lt && lt->tm_hour == g_tributeAlarmHour && lt->tm_min == g_tributeAlarmMinute && lt->tm_sec == 0) { printf("Tribute Alarm triggered! Starting keepsake memories playback.\r\n"); g_tributeActive = true; g_tributeNextIndex = 0; currentState = STATE_IDLE; lastSlideTime = now; showMemory(0); } } } // ── Button edge detection ───────────────────────────────────────────────── if (btn == LOW && lastButtonState == HIGH) { // Pressed down lastInteractionTime = now; g_standbyActive = false; buttonPressStartTime = now; isButtonPressed = true; longPressTriggered = false; } else if (btn == LOW && lastButtonState == LOW && isButtonPressed) { // Held down if (!longPressTriggered && (now - buttonPressStartTime > 1500)) { longPressTriggered = true; handleLongPress(); } } else if (btn == HIGH && lastButtonState == LOW) { // Released isButtonPressed = false; if (!longPressTriggered && (now - buttonPressStartTime > 50)) { clickCount++; lastButtonRelease = now; } } // ── Multi-click gesture resolution (300ms window) ───────────────────────── if (clickCount > 0 && !isButtonPressed && (now - lastButtonRelease > 300)) { if (clickCount == 1) handleSinglePress(); else if (clickCount == 2) handleDoublePress(); else if (clickCount == 3) handleTriplePress(); else handleQuadruplePress(); clickCount = 0; } // ── Autoplay and Tribute advance ────────────────────────────────────────── if (g_tributeActive && g_memoryCount > 0) { if (!isAudioPlaying() && (now - lastSlideTime > SLIDE_INTERVAL)) { g_tributeNextIndex++; if (g_tributeNextIndex < g_memoryCount) { lastSlideTime = now; showMemory(g_tributeNextIndex); } else { g_tributeActive = false; stopAudio(); printf("Tribute daily playback loop complete. Returning to Idle.\r\n"); } } } else if (currentState == STATE_AUTOPLAY && g_memoryCount > 0) { if (!isAudioPlaying() && (now - lastSlideTime > SLIDE_INTERVAL)) { lastSlideTime = now; showMemory((g_currentIndex + 1) % g_memoryCount); } } // ── Standby Candle Controller ───────────────────────────────────────────── if (g_memoryCount == 0) { if (!g_standbyActive) { g_standbyActive = true; Draw_Sunflower_Pattern(); } } else if (!g_standbyActive && !isAudioPlaying() && currentState == STATE_IDLE && !g_tributeActive) { if (now - lastInteractionTime > STANDBY_TIMEOUT) { g_standbyActive = true; Draw_Candle_Body(); printf("Standby: entering candle standby mode\r\n"); } } if (g_standbyActive && g_memoryCount > 0) { Draw_Candle_Flicker(); } lastButtonState = btn; // ── Web server ──────────────────────────────────────────────────────────── handleWebServer(); delay(10); }