262 lines
9.2 KiB
C++
262 lines
9.2 KiB
C++
#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);
|
|
}
|