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.
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
Post a Comment