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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user