Downloading and Displaying JPG Images via WiFi on ESP32-C5 with ST7789 SPI TFT LCD

In this exercise, I demonstrate how to download a JPG image over Wi-Fi, decode it using the JPEGDEC library, and display it on a 3.2-inch 240×320 ST7789 TFT LCD (4-wire SPI). The project uses the Arduino_GFX_Library together with the Waveshare ESP32-C5-WIFI6-KIT-N16R8, running in the Arduino framework.


Connection:


	3.2" 240x320 ST7789 TFT LCD	Waveshare
	(4-Wire SPI)                    ESP32-C5-WIFI6-KIT-N16R8
	BL  --------------------------- GPIO26
	CS  --------------------------- GPIO23
	DC  --------------------------- GPIO24
	RES --------------------------- GPIO25
	SDA --------------------------- GPIO8
	SCL --------------------------- GPIO10
	VCC --------------------------- 3V3
	GND --------------------------- GND


Libraries:

Arduino_GFX_Library and JPEGDEC libraries are needed to be installed in Arduino IDE Library Manager.

Basic Test:

ESP32C5_ST7789_SPI.ino
/*
Exercise run on Waveshare ESP32-C5-WIFI6-KIT-N16R8
Work with 3.2" 240x320 ST7789 SPI

https://coxxect.blogspot.com/2026/04/downloading-and-displaying-jpg-images.html

Remark about "USB CDC On Boot" option for Serial.print()
If host PC connect to UART port, select USB CDC On Boot: "Disabled".
If connect to USB port, select USB CDC On Boot: "Enabled"
Otherwise, you cannot see the output by Serial.print().
*/
#include <Arduino.h>
#include "esp_system.h"
#include "core_version.h"
#include <Arduino_GFX_Library.h>

#define TFT_BL  26
#define TFT_CS  23
#define TFT_DC  24
#define TFT_RES 25
#define TFT_SDA 8
#define TFT_SCL	10

/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC /* DC */, TFT_CS /* CS */);

/* More display class: https://github.com/moononournation/Arduino_GFX/wiki/Display-Class */
/*
#define TFT_WIDTH  240
#define TFT_HEIGHT 320
Arduino_GFX *gfx = new Arduino_ST7789(bus, 
                                      TFT_RES,       // RST
                                      0,             // rotation 
                                      false,         // IPS
                                      TFT_WIDTH,     // width
                                      TFT_HEIGHT     // height
                                      );
*/

// Due to the metal case mis-aligned, I have to adjust TFT_HEIGHT and ROE_OFFSET to make it matched.
#define TFT_WIDTH  240
#define TFT_HEIGHT 315
#define COL_OFFSET 0
#define ROW_OFFSET 5
Arduino_GFX *gfx = new Arduino_ST7789(bus, 
                                      TFT_RES,       // RST
                                      0,             // rotation 
                                      false,         // IPS
                                      TFT_WIDTH,     // width
                                      TFT_HEIGHT,    // height
                                      COL_OFFSET,    // col offset
                                      ROW_OFFSET     // row offset 1
                                      );

void setup() {
  delay(2000);
  Serial.begin(115200);
  delay(1000);

  Serial.println("~ Start ~");
  Serial.println("--- SPI pins ---");
  Serial.printf("SS    %i\n", SS);
  Serial.printf("MOSI  %i\n", MOSI);
  Serial.printf("MISO  %i\n", MISO);
  Serial.printf("SCK   %i\n", SCK);

  Serial.println("Arduino_GFX_Library color test on 240*320 ST7789 SPI LCD");

  // Init Display
  gfx->begin();
  // gfx->invertDisplay(true);  //Invert colors
  gfx->fillScreen(RGB565_BLACK);

  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  //Turn On Backlight

  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 */);
  gfx->println();
  gfx->println("Arduino_GFX_Library");
  delay(5000); // 5 seconds

  gfx->fillScreen(RGB565_BLACK);
  gfx->setTextSize(2 /* x scale */, 2 /* y scale */, 2 /* pixel_margin */);
  gfx->setCursor(0, 20);

  // Get Chip and version
  Serial.printf("Chip model: %s\n", ESP.getChipModel());
  Serial.printf("Chip revision: %d\n", ESP.getChipRevision());
  gfx->printf("Chip model: %s\n", ESP.getChipModel());
  gfx->printf("Chip revision: %d\n", ESP.getChipRevision());

  // Get ESP-IDF Version
  // This returns the version of the SDK the core was built on (e.g., "v5.1.0")
  Serial.print("ESP-IDF Version: ");
  Serial.println(esp_get_idf_version());
  gfx->print("ESP-IDF Version: ");
  gfx->println(esp_get_idf_version());

  // Get Arduino Core Version
  // Note: Only available in Arduino Core v2.0.0 and later
  #ifdef ARDUINO_ESP32_RELEASE
    Serial.print("Arduino Core Version: ");
    Serial.println(ARDUINO_ESP32_RELEASE);
    gfx->print("Arduino Core Version: ");
    gfx->println(ARDUINO_ESP32_RELEASE);
  #endif

  delay(5000); // 5 seconds
}

#define num_of_color 5
int color[num_of_color] = {RGB565_RED, RGB565_GREEN, RGB565_BLUE, RGB565_WHITE, RGB565_BLACK};
String color_name[num_of_color] = {"RED", "GREEN", "BLUE", "WHITE", "BLACK"};
int color_index = 0;

void loop()
{
  unsigned long startTime, stopTime;
  //
  startTime = millis();
  gfx->fillScreen(color[color_index]);
  gfx->drawRect(0, 0, gfx->width()-1, gfx->height()-1, color[color_index]^0xFFFF);

  gfx->setTextColor(color[color_index]^0xFFFF);
  gfx->setCursor(10, 10);
  gfx->setTextSize(5 /* x scale */, 5 /* y scale */, 2 /* pixel_margin */);
  gfx->println(color_name[color_index]);
  stopTime = millis();

  gfx->setCursor(10, 60);
  gfx->setTextSize(2 /* x scale */, 2 /* y scale */, 2 /* pixel_margin */);
  gfx->printf("Arduino_HWSPI");

  gfx->setCursor(10, 100);
  gfx->setTextSize(3 /* x scale */, 3 /* y scale */, 3 /* pixel_margin */);
  gfx->printf("%i ms", stopTime-startTime);

  color_index++;
  if (color_index==num_of_color){
    color_index = 0;
  }

  delay(1000); // 1 second
}



Test images:

All images were generated with X Grok, then resized and converted to a suitable JPG format using FFmpeg.

Create a bat file convert_jpg.bat in the folder that the contains jpg 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 (*.jpg) do (
    echo Processing: "%%f"
    
    ffmpeg -y -i "%%f" -vf "scale=240:320:force_original_aspect_ratio=decrease,pad=240:320:(ow-iw)/2:(oh-ih)/2:black" -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

A simple way to run a web server in Python is switching to the folder containing the images and run:
python -m http.server

In my testing setup, a Win 11 PC act as both the WiFi AP and image web server, such that we can get the IP of the image web server with:
  server_ip = WiFi.gatewayIP();

Downloading and Displaying JPG Images via WiFi:

ESP32C5_ST7789_SPI_wifi_JPEGDEC.ino
/*
Exercise run on Waveshare ESP32-C5-WIFI6-KIT-N16R8 + 3.2" 240x320 ST7789 SPI
- Connect to WiFi Access Point,
- download a single jpg, 
- decode jpg using JPEGDEC,
- and display on 240x320 ST7789 SPI using Arduino_GFX_Library.

https://coxxect.blogspot.com/2026/04/downloading-and-displaying-jpg-images.html

Image server side:
A simple way to run a web server in Python is switching to the folder containing the images and run:
> python -m http.server

*/
#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;
const String image_file = "image_01_new.jpg";

// ==========================================
// 2. Display Setup (Your specific config)
// ==========================================

#define TFT_BL  26
#define TFT_CS  23
#define TFT_DC  24
#define TFT_RES 25
#define TFT_SDA 8
#define TFT_SCL	10

/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC /* DC */, TFT_CS /* CS */);

/* More display class: https://github.com/moononournation/Arduino_GFX/wiki/Display-Class */
#define TFT_WIDTH  240
#define TFT_HEIGHT 320
Arduino_GFX *gfx = new Arduino_ST7789(bus, 
                                      TFT_RES,       // RST
                                      0,             // rotation 
                                      false,         // IPS
                                      TFT_WIDTH,     // width
                                      TFT_HEIGHT     // height
                                      );

// ==========================================
// 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);

  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  //Turn On Backlight

  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 (Check connection status)");
    gfx->println("Band: Unknown (Check connection status)");
  }

  Serial.printf("Signal Strength (RSSI): %d dBm\n", WiFi.RSSI());
  gfx->printf("Signal Strength (RSSI): %d dBm\n", WiFi.RSSI());
  
  server_ip = WiFi.gatewayIP();
  String urlString = "http://" + server_ip.toString() + ":8000/" + image_file;
  const char* imageUrl = urlString.c_str();

  Serial.println(server_ip);
  gfx->println(server_ip);
  Serial.println(imageUrl);
  gfx->println(imageUrl);

  // Fetch and draw the image
  unsigned long startTime = millis();
  fetchAndDisplayImage(imageUrl);
  unsigned long endTime = millis();
  Serial.printf("fetchAndDisplayImage() running %lu (ms)\n", endTime - startTime);

  Serial.println("- Done -");

  delay(500); // 0.5 seconds
}

void loop()
{

  delay(10000); 
}

// ==========================================
// 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);

    // Allocate memory for the image.
    // An ESP32-C5 has plenty of RAM for a standard 240x320 JPEG (~10-30KB).
    uint8_t *imgBuffer = (uint8_t *)malloc(len);
    
    if (imgBuffer) {
      WiFiClient *stream = http.getStreamPtr();
      size_t bytesRead = 0;
      
      // Read the HTTP stream directly into our memory buffer
      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...");
      
      // Pass the memory buffer to the JPEG decoder
      if (jpeg.openRAM(imgBuffer, bytesRead, JPEGDraw)) {
        // Decode starting at X:0, Y:0
        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 the memory to prevent memory leaks
      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();
}


ESP32C5_ST7789_SPI_wifi_JPEGDEC_loop.ino
/*
Exercise run on Waveshare ESP32-C5-WIFI6-KIT-N16R8 + 3.2" 240x320 ST7789 SPI
- Connect to WiFi Access Point,
- download multiple jpgs in a loop, 
- decode jpg using JPEGDEC,
- and display on 240x320 ST7789 SPI using Arduino_GFX_Library.

https://coxxect.blogspot.com/2026/04/downloading-and-displaying-jpg-images.html

Image server side:
> python -m http.server

Acknowledgment: 
Special thanks to Gemini (Google AI) for the technical guidance, 
code optimization, and troubleshooting support throughout this exercise.
*/
#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;

// --- New Loop Variables ---
int currentImageIndex = 1;
const int TOTAL_IMAGES = 8; // Loops from 1 to 8

// ==========================================
// 2. Display Setup (Your specific config)
// ==========================================

#define TFT_BL  26
#define TFT_CS  23
#define TFT_DC  24
#define TFT_RES 25
#define TFT_SDA 8
#define TFT_SCL 10

/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC /* DC */, TFT_CS /* CS */);

/* More display class: https://github.com/moononournation/Arduino_GFX/wiki/Display-Class */
#define TFT_WIDTH  240
#define TFT_HEIGHT 320
Arduino_GFX *gfx = new Arduino_ST7789(bus, 
                                      TFT_RES,       // RST
                                      0,             // rotation 
                                      false,         // IPS
                                      TFT_WIDTH,     // width
                                      TFT_HEIGHT     // height
                                      );

// ==========================================
// 3. JPEG Decoder Setup
// ==========================================

JPEGDEC jpeg;

int JPEGDraw(JPEGDRAW *pDraw) {
  gfx->draw16bitRGBBitmap(pDraw->x, pDraw->y, pDraw->pPixels, pDraw->iWidth, pDraw->iHeight);
  return 1; 
}

// ==========================================
// 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);

  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  //Turn On Backlight

  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();
  
  Serial.println("- Setup Done, entering Loop -");
  delay(1000); 
}

void loop() {
  // 1. Create the dynamic filename (e.g., "image_01_new.jpg")
  // %02d ensures numbers 1-9 are padded with a leading zero
  char filename[32];
  sprintf(filename, "image_%02d_new.jpg", currentImageIndex);

  // 2. Construct the full URL
  String urlString = "http://" + server_ip.toString() + ":8000/" + String(filename);
  const char* imageUrl = urlString.c_str();

  // Optional: Print to serial to track progress
  Serial.printf("\n--- Fetching Image %d of %d ---\n", currentImageIndex, TOTAL_IMAGES);

  // 3. Fetch and draw the image
  unsigned long startTime = millis();
  fetchAndDisplayImage(imageUrl);
  unsigned long endTime = millis();
  
  Serial.printf("fetchAndDisplayImage() ran in %lu (ms)\n", endTime - startTime);

  // 4. Increment the counter and reset if we pass the max
  currentImageIndex++;
  if (currentImageIndex > TOTAL_IMAGES) {
    currentImageIndex = 1; // Loop back to the first image
  }

  // 5. Wait before loading the next image
  // Change this delay to however long you want each image to stay on screen
  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();
}


Comments

Popular posts from this blog

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

Drive 320x240 ILI9341 SPI TFT using ESP32-S3 (NodeMCU ESP-S3-12K-Kit) using TFT_eSPI library, in Arduino Framework.