Redesign to keepsake Memory Frame: config file based named memories with beautiful mobile web dashboard, upload wizard, and delete flow

This commit is contained in:
drjones
2026-07-04 02:18:39 -07:00
parent d23b6c245c
commit 1c5be95d4c
9 changed files with 1019 additions and 675 deletions

View File

@@ -3,240 +3,178 @@
#include "Display_PNG.h"
#include "Audio_Player.h"
#include "Web_Server.h"
#include <algorithm>
#define BOOT_KEY_PIN 0
// Playback states
enum AppState {
STATE_IDLE,
STATE_PLAYING,
STATE_AUTOPLAY,
STATE_ERROR
};
// ── App state ─────────────────────────────────────────────────────────────────
enum AppState { STATE_IDLE, STATE_AUTOPLAY, STATE_ERROR };
static AppState currentState = STATE_IDLE;
static bool hasError = false;
AppState currentState = STATE_IDLE;
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;
uint16_t currentIndex = 0;
bool lastButtonState = HIGH;
uint32_t lastDebounceTime = 0;
const uint32_t debounceDelay = 50;
// Autoplay
static uint32_t lastSlideTime = 0;
static const uint32_t SLIDE_INTERVAL = 8000; // 8 s between slides in autoplay
// Slide Show timing
uint32_t lastSlideTime = 0;
const uint32_t slideShowInterval = 5000; // 5 seconds
// ── 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;
// Pulse control for RGB LED
uint32_t lastLedPulseTime = 0;
uint8_t pulseBrightness = 0;
bool pulseDirection = true;
printf("Showing memory %d: '%s'\r\n", index, g_memories[index].name);
// Click gesture timers
uint32_t lastButtonReleaseTime = 0;
uint32_t buttonPressStartTime = 0;
uint8_t clickCount = 0;
bool isButtonPressed = false;
bool longPressTriggered = false;
void setup() {
Serial.begin(115200);
delay(1000);
printf("\r\n==========================================\r\n");
printf("Starting Sound and Picture Player...\r\n");
printf("==========================================\r\n");
// Initialize Display and Backlight first so the screen lights up immediately
LCD_Init();
Set_Backlight(90);
// Initialize other peripherals
Flash_test();
SD_Init();
// Setup button
pinMode(BOOT_KEY_PIN, INPUT_PULLUP);
// Retrieve media files from SD Card
Search_Images("/");
Search_Audio("/");
// Initialize ESP32-audioI2S player
initAudioPlayer();
printf("Initialization complete!\r\n");
printf("Found %d PNG images and %d Audio files (.mp3/.wav).\r\n", Image_Count, Audio_Count);
if (Image_Count == 0 && Audio_Count == 0) {
hasError = true;
currentState = STATE_ERROR;
printf("ERROR: No media found on microSD card!\r\n");
} else {
if (Image_Count == 0) printf("WARNING: No PNG images found — audio-only mode.\r\n");
if (Audio_Count == 0) printf("WARNING: No audio files found — silent slideshow mode.\r\n");
if (Image_Count > 0) {
printf("Displaying initial image (index 0): %s\r\n", Image_Names[0]);
Display_Image_By_Index("/", 0);
}
currentState = STATE_IDLE;
}
// Start the SoftAP and local Web Server
initWebServer();
}
void playCurrentPair() {
if (Image_Count == 0 && Audio_Count == 0) {
printf("Cannot play: missing PNG images and Audio files!\r\n");
return;
}
// Stop current playing audio if any
stopAudio();
printf("Playing item index %d:\r\n", currentIndex);
// Display the image
if (Image_Count > 0) {
uint16_t pngIndex = currentIndex % Image_Count;
Display_Image_By_Index("/", pngIndex);
}
// Play the audio bite
if (Audio_Count > 0) {
uint16_t audioIndex = currentIndex % Audio_Count;
String soundPath = String("/") + Audio_Names[audioIndex];
printf("Playing audio: %s\r\n", soundPath.c_str());
playAudio(soundPath.c_str());
}
Display_Memory_Photo(index);
playMemoryAudio(index);
Config_Save(); // persist currentIndex
}
// ── Button handlers ───────────────────────────────────────────────────────────
void handleSinglePress() {
printf("Gesture: Single Click - Next Slide\r\n");
if (!hasError) {
uint16_t total = std::max(Image_Count, Audio_Count);
if (total == 0) return;
if (currentState == STATE_AUTOPLAY) {
currentState = STATE_IDLE;
printf("Autoplay Disabled.\r\n");
}
currentIndex = (currentIndex + 1) % total;
playCurrentPair();
}
if (hasError || g_memoryCount == 0) return;
if (currentState == STATE_AUTOPLAY) currentState = STATE_IDLE;
showMemory((g_currentIndex + 1) % g_memoryCount);
}
void handleDoublePress() {
printf("Gesture: Double Click - Pause / Resume Audio\r\n");
if (!hasError) {
pauseResumeAudio();
}
if (hasError) return;
pauseResumeAudio();
}
void handleTriplePress() {
printf("Gesture: Triple Click - Previous Slide\r\n");
if (!hasError) {
uint16_t total = std::max(Image_Count, Audio_Count);
if (total == 0) return;
if (currentState == STATE_AUTOPLAY) {
currentState = STATE_IDLE;
printf("Autoplay Disabled.\r\n");
}
currentIndex = (currentIndex + total - 1) % total;
playCurrentPair();
}
if (hasError || g_memoryCount == 0) return;
if (currentState == STATE_AUTOPLAY) currentState = STATE_IDLE;
showMemory((g_currentIndex + g_memoryCount - 1) % g_memoryCount);
}
void handleQuadruplePress() {
if (!hasError) {
uint8_t nextVol = getPlayerVolume() + 4;
if (nextVol > 20) nextVol = 4; // Loop back
setPlayerVolume(nextVol);
}
uint8_t v = getPlayerVolume() + 4;
if (v > 21) v = 4;
setPlayerVolume(v);
printf("Volume: %d/21\r\n", v);
}
void handleLongPress() {
printf("Gesture: Long Press - Toggle Autoplay Slideshow\r\n");
if (!hasError) {
if (currentState == STATE_AUTOPLAY) {
currentState = STATE_IDLE;
printf("Autoplay Disabled. Manual Mode active.\r\n");
stopAudio();
} else {
currentState = STATE_AUTOPLAY;
printf("Autoplay Enabled! Cycling slides every %d ms.\r\n", slideShowInterval);
lastSlideTime = millis();
playCurrentPair();
}
if (hasError) return;
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");
}
}
void updateLEDStatus() {
// Disabled NeoPixel code to resolve hardware conflict with SPI SCLK (GPIO 38)
// ── Setup ─────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(1000);
printf("\r\n============================\r\n");
printf(" Memory Frame v2.0\r\n");
printf("============================\r\n");
// Display on first
LCD_Init();
Set_Backlight(90);
// SD card
Flash_test();
SD_Init();
// Load memories from config
Config_Load();
// Button
pinMode(BOOT_KEY_PIN, INPUT_PULLUP);
// Audio player
initAudioPlayer();
setPlayerVolume(g_volume);
// Show first memory (or error screen)
if (g_memoryCount == 0) {
printf("No memories found. Add some via the web app.\r\n");
// Screen stays dark — user will add via web
} 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 currentButtonReading = digitalRead(BOOT_KEY_PIN);
bool btn = digitalRead(BOOT_KEY_PIN);
uint32_t now = millis();
// Detect state transitions for debouncing and gesture capturing
if (currentButtonReading == LOW && lastButtonState == HIGH) {
// Button Pressed down
lastDebounceTime = now;
// ── Web display request (triggered by phone) ──────────────────────────────
if (g_webDisplayDirty) {
g_webDisplayDirty = false;
int req = g_webDisplayRequest;
g_webDisplayRequest = -1;
if (req >= 0 && req < g_memoryCount) {
showMemory(req);
}
}
// ── Button edge detection ─────────────────────────────────────────────────
if (btn == LOW && lastButtonState == HIGH) {
// Pressed down
buttonPressStartTime = now;
isButtonPressed = true;
longPressTriggered = false;
}
else if (currentButtonReading == LOW && lastButtonState == LOW && isButtonPressed) {
// Button is held down
isButtonPressed = true;
longPressTriggered = false;
}
else if (btn == LOW && lastButtonState == LOW && isButtonPressed) {
// Held down
if (!longPressTriggered && (now - buttonPressStartTime > 1500)) {
longPressTriggered = true;
handleLongPress();
}
}
else if (currentButtonReading == HIGH && lastButtonState == LOW) {
// Button Released
else if (btn == HIGH && lastButtonState == LOW) {
// Released
isButtonPressed = false;
uint32_t pressDuration = now - buttonPressStartTime;
if (!longPressTriggered && pressDuration > 50) { // Debounced release
if (!longPressTriggered && (now - buttonPressStartTime > 50)) {
clickCount++;
lastButtonReleaseTime = now;
lastButtonRelease = now;
}
}
// Process click gestures after 300ms timeout has elapsed since last release
if (clickCount > 0 && !isButtonPressed && (now - lastButtonReleaseTime > 300)) {
if (clickCount == 1) {
handleSinglePress();
} else if (clickCount == 2) {
handleDoublePress();
} else if (clickCount == 3) {
handleTriplePress();
} else if (clickCount >= 4) {
handleQuadruplePress();
}
clickCount = 0; // Reset counter
// ── 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;
}
// Handle slideshow auto-advance in Autoplay mode
if (currentState == STATE_AUTOPLAY && !hasError) {
uint16_t total = std::max(Image_Count, Audio_Count);
// Only advance when the audio stops playing and slideShowInterval (5s) has passed
if (total > 0 && !isAudioPlaying() && (now - lastSlideTime > slideShowInterval)) {
// ── Autoplay advance ──────────────────────────────────────────────────────
if (currentState == STATE_AUTOPLAY && g_memoryCount > 0) {
if (!isAudioPlaying() && (now - lastSlideTime > SLIDE_INTERVAL)) {
lastSlideTime = now;
currentIndex = (currentIndex + 1) % total;
playCurrentPair();
showMemory((g_currentIndex + 1) % g_memoryCount);
}
}
lastButtonState = currentButtonReading;
// Handle incoming web server clients
lastButtonState = btn;
// ── Web server ────────────────────────────────────────────────────────────
handleWebServer();
// Update status RGB LED
updateLEDStatus();
delay(10); // Yield to other RTOS tasks
delay(10);
}