Dynamic WiFi Image Gallery run on Waveshare ESP32-S3-Touch-AMOLED-2.16

In my previous post, I demonstrated how to download and display JPG images via WiFi on the Waveshare ESP32-C5-WIFI6-KIT-N16R8 with an 240x320 ST7789 SPI TFT LCD using hardcoded filenames. This new implementation is much more flexible: filenames are now stored in a jpg_list.txt file on the server, allowing the ESP32 to download the images dynamically. I have also successfully tested this setup on the Waveshare ESP32-S3-Touch-AMOLED-2.1 with its 480x480 AMOLED (CO5300 driver).

How it Works

    The program operates in three distinct phases:

  • WiFi connection: The ESP32-S3 connects to a local WiFi access point and establishes communication with a Python-based HTTP server. Since the WiFi access point is a Windows 11 Mobile Hotspot and the Python-based HTTP server is running on that same PC, we can assume the server IP address is identical to the Access Point (Gateway) IP.
  • Dynamic Manifest Retrieval: First downloads a jpg_list.txt file. This manifest acts as a "playlist," allowing the ESP32-S3 to know exactly which images are currently available on the server.
  • Decoding & Display: The system enters a continuous loop, downloading each JPEG image into RAM. Using the JPEGDEC and Arduino_GFX libraries, the compressed data is decoded on-the-fly and rendered onto a 480*480 CO5300 LCD.

  • Exercise Code

    S3_CO5300_wifi_JPEGDEC_list.ino
    /* 
    
    Exercise run on Waveshare ESP32-S3-Touch-AMOLED-2.16
    - Connect to WiFi Access Point,
    - download jpg_list.txt
    - download multiple jpgs accordingly in a loop, 
    - decode jpg using JPEGDEC,
    - and display on 480*480 AMOLED with CO5300 driver using Arduino_GFX_Library.
    
    https://coxxect.blogspot.com/2026/06/dynamic-wifi-image-gallery-run-on.html
    
    Remark about "USB CDC On Boot" option for Serial.print()
    For Waveshare ESP32-S3-Touch-AMOLED-2.16 with a  single USB port only, 
    select USB CDC On Boot: "Enabled".
    Otherwise, you cannot see the output by Serial.print().
    
    Image server side:
    > python -m http.server
    
    Acknowledgment: 
    Special thanks to Gemini (Google AI) for the technical guidance.
    */
    
    #include <Arduino.h>
    #include <WiFi.h>
    #include <HTTPClient.h>
    #include <JPEGDEC.h>
    #include <Arduino_GFX_Library.h>
    
    // ==========================================
    // 1. Configuration 
    // ==========================================
    
    const char* ssid = "ssid";
    const char* password = "password";
    
    IPAddress server_ip;
    
    // --- Dynamic Loop Variables ---
    String imageList[64]; // Supports up to 64 images
    int currentImageIndex = 0;
    int totalImages = 0;
    
    // ==========================================
    // 2. Display Setup (Your specific config)
    // ==========================================
    
    #define LCD_SDIO0 4
    #define LCD_SDIO1 5
    #define LCD_SDIO2 6
    #define LCD_SDIO3 7
    #define LCD_SCLK  38
    #define LCD_RESET  2
    #define LCD_CS 12
    #define LCD_WIDTH 480
    #define LCD_HEIGHT 480
    
    Arduino_DataBus *bus = new Arduino_ESP32QSPI(
      LCD_CS /* CS */, LCD_SCLK /* SCK */, LCD_SDIO0 /* SDIO0 */, LCD_SDIO1 /* SDIO1 */,
      LCD_SDIO2 /* SDIO2 */, LCD_SDIO3 /* SDIO3 */);
    
    Arduino_CO5300 *gfx = new Arduino_CO5300(
      bus, LCD_RESET /* RST */, 0 /* rotation */, LCD_WIDTH /* width */, LCD_HEIGHT /* height */, 0, 0, 0, 0);
    
    // ==========================================
    // 3. JPEG Decoder Setup
    // ==========================================
    
    JPEGDEC jpeg;
    
    // This callback function is fired by JPEGDEC every time it decodes a block of the image.
    // It hands the pixel data directly to your Arduino_GFX instance.
    int JPEGDraw(JPEGDRAW *pDraw) {
      // draw16bitRGBBitmap handles the standard Little-Endian output of JPEGDEC
      gfx->draw16bitRGBBitmap(pDraw->x, pDraw->y, pDraw->pPixels, pDraw->iWidth, pDraw->iHeight);
      return 1; // Return 1 to continue decoding
    }
    
    // ==========================================
    // 4. Main Program
    // ==========================================
    
    void setup() {
      delay(2000);
      Serial.begin(115200);
      delay(1000);
    
      Serial.println("\n\n~ Start ~");
    
      // Init Display
      gfx->begin();
      gfx->fillScreen(RGB565_BLACK);
    
      gfx->drawRect(0, 0, gfx->width()-1, gfx->height()-1, RGB565_WHITE);
    
      gfx->setCursor(10, 10);
      gfx->setTextSize(3 /* x scale */, 3 /* y scale */, 3 /* pixel_margin */);
      gfx->println("ESP32-C5-WIFI6-KIT-N16R8");
      gfx->setTextSize(2 /* x scale */, 2 /* y scale */, 2 /* pixel_margin */);
    
      delay(500); // 0.5 seconds
      gfx->println();
      gfx->println("WiFi ---");
    
      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }
      Serial.println("\nWiFi Connected!");
      gfx->println("WiFi Connected!");
    
      uint8_t connectedChannel = WiFi.channel();
      Serial.printf("Channel: %d\n", connectedChannel);
      gfx->printf("Channel: %d\n", connectedChannel);
      
      if (connectedChannel >= 1 && connectedChannel <= 14) {
        Serial.println("Band: 2.4 GHz");
        gfx->println("Band: 2.4 GHz");
      } else if (connectedChannel > 14) {
        Serial.println("Band: 5 GHz");
        gfx->println("Band: 5 GHz");
      } else {
        Serial.println("Band: Unknown");
        gfx->println("Band: Unknown");
      }
    
      Serial.printf("Signal Strength (RSSI): %d dBm\n", WiFi.RSSI());
      gfx->printf("Signal Strength (RSSI): %d dBm\n", WiFi.RSSI());
      
      // Save the gateway IP so loop() can use it
      server_ip = WiFi.gatewayIP();
    
      // Download the list of files before starting the loop
      fetchImageList();
      
      Serial.println("- Setup Done, entering Loop -");
      delay(1000); 
    }
    
    void loop() {
      if (totalImages == 0) {
        Serial.println("No images to display. Checking again in 10s...");
        delay(10000);
        return;
      }
    
      // 1. Get the filename from our dynamic list
      String filename = imageList[currentImageIndex];
      String urlString = "http://" + server_ip.toString() + ":8000/" + filename;
    
      Serial.printf("\n--- Displaying [%d/%d]: %s ---\n", 
                    currentImageIndex + 1, totalImages, filename.c_str());
    
      // 2. Fetch and draw
      fetchAndDisplayImage(urlString.c_str());
    
      // 3. Move to next image
      currentImageIndex++;
      if (currentImageIndex >= totalImages) {
        currentImageIndex = 0; // Loop back
      }
    
      delay(3000); 
    }
    
    // ==========================================
    // 5. Download and Decode Logic
    // ==========================================
    
    void fetchAndDisplayImage(const char* url) {
      HTTPClient http;
      Serial.println("Downloading image...");
      Serial.println(url);
      
      http.begin(url);
      int httpCode = http.GET();
    
      if (httpCode == HTTP_CODE_OK) {
        int len = http.getSize();
        Serial.printf("Image size: %d bytes\n", len);
    
        uint8_t *imgBuffer = (uint8_t *)malloc(len);
        
        if (imgBuffer) {
          WiFiClient *stream = http.getStreamPtr();
          size_t bytesRead = 0;
          
          while (http.connected() && (len > 0 || len == -1)) {
            size_t size = stream->available();
            if (size) {
              int c = stream->readBytes(&imgBuffer[bytesRead], size);
              bytesRead += c;
              if (len > 0) len -= c;
            }
            delay(1);
          }
    
          Serial.println("Download complete. Decoding...");
          
          if (jpeg.openRAM(imgBuffer, bytesRead, JPEGDraw)) {
            jpeg.decode(0, 0, 0); 
            jpeg.close();
            Serial.println("Image displayed successfully.");
          } else {
            Serial.println("Failed to open JPEG. Ensure it is a standard Baseline JPEG (not Progressive).");
          }
    
          free(imgBuffer); 
        } else {
          Serial.println("Error: Not enough heap memory to hold the image.");
        }
      } else {
        Serial.printf("HTTP GET failed. Error code: %d\n", httpCode);
      }
      
      http.end();
    }
    
    // ==========================================
    // Download and Parse jpg_list.txt
    // ==========================================
    void fetchImageList() {
      HTTPClient http;
      String listUrl = "http://" + server_ip.toString() + ":8000/jpg_list.txt";
      
      Serial.println("Downloading image list...");
      http.begin(listUrl);
      int httpCode = http.GET();
    
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        int startIndex = 0;
        int endIndex = payload.indexOf('\n');
    
        while (endIndex != -1 && totalImages < 64) {
          String line = payload.substring(startIndex, endIndex);
          line.trim(); // Remove \r or spaces
          
          if (line.length() > 0) {
            imageList[totalImages] = line;
            Serial.printf("Found file: %s\n", imageList[totalImages].c_str());
            totalImages++;
          }
          
          startIndex = endIndex + 1;
          endIndex = payload.indexOf('\n', startIndex);
        }
        
        // Handle the last line if it doesn't end with a newline
        if (startIndex < payload.length() && totalImages < 64) {
          String lastLine = payload.substring(startIndex);
          
          Serial.printf("%i : ", startIndex);
          Serial.println(lastLine);
    
          lastLine.trim();
          if (lastLine.length() > 0) {
            imageList[totalImages] = lastLine;
            totalImages++;
          }
        }
        Serial.printf("Total images found: %d\n", totalImages);
      } else {
        Serial.printf("Failed to get list. Error: %d\n", httpCode);
      }
      http.end();
    }
    

    Steps to Test:

    Test images:

    All images were generated png with Gemini, then resized and converted to a suitable 480*480 JPG format using FFmpeg.

    Create a bat file convert_jpg.bat in the folder that the contains png images. Re-sized jpg will be save in jpg_new sub-folder, named <original_name>_new.jpg.

    convert_jpg.bat
    @echo off
    setlocal enabledelayedexpansion
    
    :: 1. Create the output directory if it doesn't exist
    if not exist "jpg_new" (
        mkdir "jpg_new"
    )
    
    :: 2. Loop through all .jpg files
    for %%f in (*.png) do (
        echo Processing: "%%f"
        
        ffmpeg -y -i "%%f" -vf "scale=480:480:force_original_aspect_ratio=decrease" -pix_fmt yuv420p -q:v 7 -map_metadata -1 "jpg_new\%%~nf_new.jpg"
    )
    
    echo.
    echo Conversion completed! Please check the "jpg_new" folder.
    pause
    

    Generate jpg_list.txt.

    switch to the jpg_new sub-folder, run the DOS command:
    dir *.jpg /b /on > jpg_list.txt
    

    WiFi and server setup

    Enable Windows 11 Mobile Hotspot, make sure it work in 2.4 GHz band. ESP32-S3 support 2.4 GHz only band.

    Run Python-based HTTP server:
    python -m http.server
    



    Comments

    Popular posts from this blog

    480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library

    Drive ST7796 SPI TFT with XPT2046 Touch on ESP32-C3-DevKitM-1 (arduino-esp32), using TFT_eSPI.