282 lines
7.4 KiB
C++
282 lines
7.4 KiB
C++
#include "Display_ST7789.h"
|
|
#include "SD_Card.h"
|
|
#include "Display_PNG.h"
|
|
#include "Audio_Player.h"
|
|
#include "Web_Server.h"
|
|
#include <algorithm>
|
|
|
|
#define BOOT_KEY_PIN 9
|
|
#define PIN_NEOPIXEL 38
|
|
|
|
// Playback states
|
|
enum AppState {
|
|
STATE_IDLE,
|
|
STATE_PLAYING,
|
|
STATE_AUTOPLAY,
|
|
STATE_ERROR
|
|
};
|
|
|
|
AppState currentState = STATE_IDLE;
|
|
bool hasError = false;
|
|
|
|
uint16_t currentIndex = 0;
|
|
bool lastButtonState = HIGH;
|
|
uint32_t lastDebounceTime = 0;
|
|
const uint32_t debounceDelay = 50;
|
|
|
|
// Slide Show timing
|
|
uint32_t lastSlideTime = 0;
|
|
const uint32_t slideShowInterval = 5000; // 5 seconds
|
|
|
|
// Pulse control for RGB LED
|
|
uint32_t lastLedPulseTime = 0;
|
|
uint8_t pulseBrightness = 0;
|
|
bool pulseDirection = true;
|
|
|
|
// 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");
|
|
|
|
// Setup NeoPixel
|
|
pinMode(PIN_NEOPIXEL, OUTPUT);
|
|
neopixelWrite(PIN_NEOPIXEL, 0, 0, 32); // Turn on solid blue to show booting
|
|
|
|
// 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 {
|
|
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());
|
|
}
|
|
}
|
|
|
|
void handleSinglePress() {
|
|
printf("Gesture: Single Click - Next Slide\r\n");
|
|
if (!hasError) {
|
|
if (currentState == STATE_AUTOPLAY) {
|
|
currentState = STATE_IDLE;
|
|
printf("Autoplay Disabled.\r\n");
|
|
}
|
|
currentIndex = (currentIndex + 1) % std::max(Image_Count, Audio_Count);
|
|
playCurrentPair();
|
|
}
|
|
}
|
|
|
|
void handleDoublePress() {
|
|
printf("Gesture: Double Click - Pause / Resume Audio\r\n");
|
|
if (!hasError) {
|
|
pauseResumeAudio();
|
|
}
|
|
}
|
|
|
|
void handleTriplePress() {
|
|
printf("Gesture: Triple Click - Previous Slide\r\n");
|
|
if (!hasError) {
|
|
if (currentState == STATE_AUTOPLAY) {
|
|
currentState = STATE_IDLE;
|
|
printf("Autoplay Disabled.\r\n");
|
|
}
|
|
uint16_t totalMedia = std::max(Image_Count, Audio_Count);
|
|
currentIndex = (currentIndex + totalMedia - 1) % totalMedia;
|
|
playCurrentPair();
|
|
}
|
|
}
|
|
|
|
void handleQuadruplePress() {
|
|
if (!hasError) {
|
|
uint8_t nextVol = getPlayerVolume() + 4;
|
|
if (nextVol > 20) nextVol = 4; // Loop back
|
|
setPlayerVolume(nextVol);
|
|
|
|
// Quick flash NeoPixel white to indicate volume change
|
|
for(int i = 0; i < 3; i++) {
|
|
neopixelWrite(PIN_NEOPIXEL, 64, 64, 64);
|
|
delay(50);
|
|
neopixelWrite(PIN_NEOPIXEL, 0, 0, 0);
|
|
delay(50);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateLEDStatus() {
|
|
uint32_t now = millis();
|
|
|
|
// Pulse animation frequency
|
|
if (now - lastLedPulseTime > 15) {
|
|
lastLedPulseTime = now;
|
|
if (pulseDirection) {
|
|
pulseBrightness += 2;
|
|
if (pulseBrightness >= 120) pulseDirection = false;
|
|
} else {
|
|
pulseBrightness -= 2;
|
|
if (pulseBrightness <= 10) pulseDirection = true;
|
|
}
|
|
}
|
|
|
|
// Update LED color based on state
|
|
if (hasError) {
|
|
// Flashing red
|
|
if ((now / 250) % 2 == 0) {
|
|
neopixelWrite(PIN_NEOPIXEL, 64, 0, 0); // Dim Red
|
|
} else {
|
|
neopixelWrite(PIN_NEOPIXEL, 0, 0, 0);
|
|
}
|
|
}
|
|
else if (isAudioPlaying()) {
|
|
// Pulsing Green
|
|
neopixelWrite(PIN_NEOPIXEL, 0, pulseBrightness, 0);
|
|
}
|
|
else if (currentState == STATE_AUTOPLAY) {
|
|
// Pulsing Purple (Red + Blue)
|
|
neopixelWrite(PIN_NEOPIXEL, pulseBrightness / 2, 0, pulseBrightness);
|
|
}
|
|
else {
|
|
// Solid Blue
|
|
neopixelWrite(PIN_NEOPIXEL, 0, 0, 32);
|
|
}
|
|
}
|
|
|
|
void loop() {
|
|
bool currentButtonReading = 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;
|
|
buttonPressStartTime = now;
|
|
isButtonPressed = true;
|
|
longPressTriggered = false;
|
|
}
|
|
else if (currentButtonReading == LOW && lastButtonState == LOW && isButtonPressed) {
|
|
// Button is held down
|
|
if (!longPressTriggered && (now - buttonPressStartTime > 1500)) {
|
|
longPressTriggered = true;
|
|
handleLongPress();
|
|
}
|
|
}
|
|
else if (currentButtonReading == HIGH && lastButtonState == LOW) {
|
|
// Button Released
|
|
isButtonPressed = false;
|
|
uint32_t pressDuration = now - buttonPressStartTime;
|
|
|
|
if (!longPressTriggered && pressDuration > 50) { // Debounced release
|
|
clickCount++;
|
|
lastButtonReleaseTime = 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
|
|
}
|
|
|
|
// Handle slideshow auto-advance in Autoplay mode
|
|
if (currentState == STATE_AUTOPLAY && !hasError) {
|
|
// Only advance when the audio stops playing and slideShowInterval (5s) has passed
|
|
if (!isAudioPlaying() && (now - lastSlideTime > slideShowInterval)) {
|
|
lastSlideTime = now;
|
|
currentIndex = (currentIndex + 1) % std::max(Image_Count, Audio_Count);
|
|
playCurrentPair();
|
|
}
|
|
}
|
|
|
|
lastButtonState = currentButtonReading;
|
|
|
|
// Handle incoming web server clients
|
|
handleWebServer();
|
|
|
|
// Update status RGB LED
|
|
updateLEDStatus();
|
|
|
|
delay(10); // Yield to other RTOS tasks
|
|
}
|