From 7a9d971310c7c7a6a9bddc4f97a59ff2325d7531 Mon Sep 17 00:00:00 2001 From: drjones Date: Thu, 2 Jul 2026 21:54:57 -0700 Subject: [PATCH] Fix 8 bugs: SD 1-bit mode GPIO conflict, PNG file handle leak, audio stack overflow, null terminator, error degradation logic, modulo div-by-zero, upload file validation, screen clear before PNG draw. Improve web UI with progress bar and toast feedback. --- sound_picture_app/Audio_Player.cpp | 14 +- sound_picture_app/Display_PNG.cpp | 14 +- sound_picture_app/SD_Card.cpp | 7 +- sound_picture_app/Web_Server.cpp | 262 ++++++++++++++++++------ sound_picture_app/sound_picture_app.ino | 24 ++- 5 files changed, 244 insertions(+), 77 deletions(-) diff --git a/sound_picture_app/Audio_Player.cpp b/sound_picture_app/Audio_Player.cpp index bf5ed5e..87cfda5 100644 --- a/sound_picture_app/Audio_Player.cpp +++ b/sound_picture_app/Audio_Player.cpp @@ -29,7 +29,7 @@ void initAudioPlayer() { xTaskCreatePinnedToCore( audioLoopTask, "audioLoopTask", - 8192, // Stack size (ESP32-audioI2S requires a larger stack size for decoding) + 16384, // Increased from 8192 — ESP32-audioI2S MP3 decode needs ~12-16KB stack NULL, 5, // Priority &audioTaskHandle, @@ -66,6 +66,14 @@ uint8_t getPlayerVolume() { return currentVolume; } +// Case-insensitive check if filename ends with a given extension +static bool hasExtension(const char* name, const char* ext) { + size_t nLen = strlen(name); + size_t eLen = strlen(ext); + if (nLen < eLen) return false; + return strcasecmp(name + nLen - eLen, ext) == 0; +} + void Search_Audio(const char* directory) { File Path = SD_MMC.open(directory); if (!Path) { @@ -79,8 +87,8 @@ void Search_Audio(const char* directory) { while (file && fileCount < 100) { if (!file.isDirectory()) { const char* name = file.name(); - // Match WAV or MP3 files - if (strstr(name, ".wav") || strstr(name, ".mp3") || strstr(name, ".WAV") || strstr(name, ".MP3")) { + // Case-insensitive extension match to handle FAT32 uppercase names + if (hasExtension(name, ".wav") || hasExtension(name, ".mp3")) { strncpy(Audio_Names[fileCount], name, sizeof(Audio_Names[fileCount]) - 1); Audio_Names[fileCount][sizeof(Audio_Names[fileCount]) - 1] = '\0'; fileCount++; diff --git a/sound_picture_app/Display_PNG.cpp b/sound_picture_app/Display_PNG.cpp index 168aedc..a1e42ca 100644 --- a/sound_picture_app/Display_PNG.cpp +++ b/sound_picture_app/Display_PNG.cpp @@ -16,8 +16,8 @@ void * pngOpen(const char *filePath, int32_t *size) { } void pngClose(void *handle) { - File Image_file = *((File*)handle); - if (Image_file) Image_file.close(); + File* fp = (File*)handle; + if (fp && *fp) fp->close(); } int32_t pngRead(PNGFILE *page, uint8_t *buffer, int32_t length) { @@ -44,6 +44,15 @@ void Search_Images(const char* directory) { Image_Count = Folder_retrieval(directory, ".png", Image_Names, 100); } +// Fill entire screen with black before drawing a new image +static void clearScreen() { + static uint16_t blackLine[MAX_IMAGE_WIDTH]; + memset(blackLine, 0, sizeof(blackLine)); + for (int y = 0; y < LCD_HEIGHT; y++) { + LCD_addWindow(0, y, LCD_WIDTH - 1, y, blackLine); + } +} + void Display_Image_By_Path(const char* filePath) { printf("Displaying image: %s\r\n", filePath); int16_t ret = png.open(filePath, pngOpen, pngClose, pngRead, pngSeek, pngDraw); @@ -52,6 +61,7 @@ void Display_Image_By_Path(const char* filePath) { if (png.getWidth() > MAX_IMAGE_WIDTH) { printf("Image is too wide for line buffer!\r\n"); } else { + clearScreen(); // Clear leftovers from previous smaller image ret = png.decode(NULL, 0); png.close(); } diff --git a/sound_picture_app/SD_Card.cpp b/sound_picture_app/SD_Card.cpp index 0f81cf9..038a72f 100644 --- a/sound_picture_app/SD_Card.cpp +++ b/sound_picture_app/SD_Card.cpp @@ -7,11 +7,11 @@ uint16_t SDCard_Size; uint16_t Flash_Size; void SD_Init() { - if(!SD_MMC.setPins(SD_CLK_PIN, SD_CMD_PIN, SD_D0_PIN, SD_D1_PIN, SD_D2_PIN, SD_D3_PIN)){ + if(!SD_MMC.setPins(SD_CLK_PIN, SD_CMD_PIN, SD_D0_PIN)){ printf("SD MMC: Pin change failed!\r\n"); return; } - if (SD_MMC.begin("/sdcard", false, true)) { + if (SD_MMC.begin("/sdcard", true, true)) { // true = 1-bit mode (avoids GPIO conflict on D3/CS) printf("SD card initialization successful (4-bit mode)!\r\n"); } else { printf("SD card initialization failed!\r\n"); @@ -81,7 +81,8 @@ uint16_t Folder_retrieval(const char* directory, const char* fileExtension, char File file = Path.openNextFile(); while (file && fileCount < maxFiles) { if (!file.isDirectory() && strstr(file.name(), fileExtension)) { - strncpy(File_Name[fileCount], file.name(), sizeof(File_Name[fileCount])); + strncpy(File_Name[fileCount], file.name(), sizeof(File_Name[fileCount]) - 1); + File_Name[fileCount][sizeof(File_Name[fileCount]) - 1] = '\0'; // Ensure null-termination if (strcmp(directory, "/") == 0) { snprintf(filePath, 100, "%s%s", directory, file.name()); } else { diff --git a/sound_picture_app/Web_Server.cpp b/sound_picture_app/Web_Server.cpp index a3a36bf..75ffc32 100644 --- a/sound_picture_app/Web_Server.cpp +++ b/sound_picture_app/Web_Server.cpp @@ -7,103 +7,224 @@ WebServer server(80); File uploadFile; +bool uploadSuccess = false; +String uploadedFilename = ""; const char* uploadPage = R"rawliteral( - + - Photo Upload Panel + + Photo Frame Upload
-

Upload Photo / Audio

+ +

Photo Frame

+

Upload media to the SD card wirelessly

+ + %TOAST% +
-
- - +
+
📁
+
Drop a file here or browse
+ +
+
No file selected
+ +
+
+
Uploading...
-
No file selected
- -
Supports PNG (172x320 max) and MP3/WAV.
+

Accepted: .png (172x320)  ·  .mp3  ·  .wav

+ )rawliteral"; +static bool isAllowedExtension(const String& filename) { + String lower = filename; + lower.toLowerCase(); + return lower.endsWith(".png") || lower.endsWith(".mp3") || lower.endsWith(".wav"); +} + void handleUpload() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { String filename = upload.filename; - if (!filename.startsWith("/")) { - filename = "/" + filename; + if (!filename.startsWith("/")) filename = "/" + filename; + + // Reject files that are not PNG, MP3, or WAV + if (!isAllowedExtension(filename)) { + printf("WebUpload: Rejected non-media file: %s\r\n", filename.c_str()); + uploadSuccess = false; + return; } + printf("WebUpload: Starting upload of %s\r\n", filename.c_str()); + uploadedFilename = filename; uploadFile = SD_MMC.open(filename, "w"); + uploadSuccess = (bool)uploadFile; } else if (upload.status == UPLOAD_FILE_WRITE) { if (uploadFile) { @@ -114,29 +235,48 @@ void handleUpload() { if (uploadFile) { uploadFile.close(); printf("WebUpload: Successfully uploaded %s (%d bytes)\r\n", upload.filename.c_str(), upload.totalSize); - // Re-scan SD card folders to index the new file + uploadSuccess = true; + // Re-scan SD card to index the new file Search_Images("/"); Search_Audio("/"); + } else { + uploadSuccess = false; } } } +static String buildPage(const String& status, const String& file) { + String toast = ""; + if (status == "ok") { + String safeName = file; + safeName.replace("<", "<"); + safeName.replace(">", ">"); + toast = "
✅ \"" + safeName + "\" uploaded successfully!
"; + } else if (status == "fail") { + toast = "
❌ Upload failed. Only .png, .mp3 and .wav files are accepted.
"; + } + String page = String(uploadPage); + page.replace("%TOAST%", toast); + return page; +} + void initWebServer() { - // Start WiFi Access Point with SSID "love" and password "love" WiFi.softAP("love", "love"); IPAddress IP = WiFi.softAPIP(); - printf("WiFi AP Started. SSID: 'love'\r\n"); - printf("Web Server IP address: %s\r\n", IP.toString().c_str()); + printf("WiFi AP Started. SSID: 'love' Password: 'love'\r\n"); + printf("Web Server IP: %s\r\n", IP.toString().c_str()); - // Serve Page server.on("/", HTTP_GET, []() { - server.send(200, "text/html", uploadPage); + String status = server.hasArg("status") ? server.arg("status") : ""; + String file = server.hasArg("file") ? server.arg("file") : ""; + server.send(200, "text/html", buildPage(status, file)); }); - // Handle file uploads server.on("/upload", HTTP_POST, []() { - // Redirect back to main upload page on success - server.sendHeader("Location", "/"); + String loc = uploadSuccess + ? "/?status=ok&file=" + uploadedFilename + : "/?status=fail"; + server.sendHeader("Location", loc); server.send(303); }, handleUpload); diff --git a/sound_picture_app/sound_picture_app.ino b/sound_picture_app/sound_picture_app.ino index e3103d3..96dca17 100644 --- a/sound_picture_app/sound_picture_app.ino +++ b/sound_picture_app/sound_picture_app.ino @@ -68,13 +68,17 @@ void setup() { 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) { + 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); + 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; } @@ -111,11 +115,13 @@ void playCurrentPair() { 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) % std::max(Image_Count, Audio_Count); + currentIndex = (currentIndex + 1) % total; playCurrentPair(); } } @@ -130,12 +136,13 @@ void handleDoublePress() { 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"); } - uint16_t totalMedia = std::max(Image_Count, Audio_Count); - currentIndex = (currentIndex + totalMedia - 1) % totalMedia; + currentIndex = (currentIndex + total - 1) % total; playCurrentPair(); } } @@ -214,10 +221,11 @@ void loop() { // 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 (!isAudioPlaying() && (now - lastSlideTime > slideShowInterval)) { + if (total > 0 && !isAudioPlaying() && (now - lastSlideTime > slideShowInterval)) { lastSlideTime = now; - currentIndex = (currentIndex + 1) % std::max(Image_Count, Audio_Count); + currentIndex = (currentIndex + 1) % total; playCurrentPair(); } }