LITTLE WEB SERVER TF CARD CODE

Generic Web Server + FTP + TF Card Storage

CAPTIVE PORTAL VERSION →

KEY FEATURES

>
SD Card File System Serves HTML, CSS, JS, images from TF card
>
FTP Server Upload/download files wirelessly
>
GC9A01 Display Round 240x240 status display
>
Easy Updates Edit files via FTP or swap SD card

HARDWARE COMPONENTS

ESP32-S3 Super Mini
GC9A01 Round Display
Micro SD TF Card Reader
TP4056 Charger (Optional)

PIN DEFINITIONS

GPIO 12 SPI SCK (Shared)
GPIO 11 SPI MOSI (Shared)
GPIO 8 TFT CS
GPIO 9 TFT DC
GPIO 10 TFT RST
GPIO 7 TFT Backlight
GPIO 4 SD Card SCK
GPIO 3 SD Card MISO
GPIO 2 SD Card MOSI
GPIO 1 SD Card CS

DISPLAY PREVIEW

Ready - Click RUN BOOT SEQUENCE

ARDUINO SKETCH

esp32_sdcard_server.ino
/*
  ESP32-S3 Web Server + FTP + SD Card
  Generic Template - Customize for your project

  Hardware:
  - ESP32-S3 Super Mini
  - GC9A01 Round Display (240x240)
  - Micro SD TF Card Reader
*/

#include <WiFi.h>
#include <WebServer.h>
#include <SD.h>
#include <SPI.h>
#include <SimpleFTPServer.h>
#include <Arduino_GFX_Library.h>
#include <time.h>

// ============== CONFIGURATION ==============
// WiFi credentials - CHANGE THESE
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// FTP credentials - CHANGE THESE
const char* ftp_user = "admin";
const char* ftp_pass = "admin";

// NTP Time settings
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -21600;  // CST = UTC-6 (adjust for your timezone)
const int daylightOffset_sec = 3600; // DST offset

// ============== PIN DEFINITIONS ==============
// Display pins (separate SPI bus from SD card)
#define TFT_SCK   12
#define TFT_MOSI  11
#define TFT_CS     8
#define TFT_DC     9
#define TFT_RST   10
#define TFT_BL     7

// SD Card pins (separate SPI bus - do NOT share with display)
#define SD_SCK     4
#define SD_MISO    3
#define SD_MOSI    2
#define SD_CS      1

// ============== COLORS (RGB565) ==============
#define BLACK       0x0000
#define WHITE       0xFFFF
#define CYAN        0x07FF
#define MAGENTA     0xF81F
#define YELLOW      0xFFE0
#define GREEN       0x07E0
#define RED         0xF800
#define DARKGRAY    0x4208
#define DARKCYAN    0x0345
#define DARKMAGENTA 0x4008
#define ORANGE      0xFC00

// ============== OBJECTS ==============
Arduino_DataBus *bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI, -1);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RST, 0, true);
SPIClass sdSPI = SPIClass(HSPI);
WebServer server(80);
FtpServer ftpSrv;

// ============== VARIABLES ==============
unsigned long lastDisplayUpdate = 0;
unsigned long lastTickerUpdate = 0;
unsigned long requestCount = 0;
int animFrame = 0;
bool sdCardMounted = false;

// ============== CONTENT TYPES ==============
String getContentType(String filename) {
  if (filename.endsWith(".html")) return "text/html";
  else if (filename.endsWith(".css")) return "text/css";
  else if (filename.endsWith(".js")) return "application/javascript";
  else if (filename.endsWith(".png")) return "image/png";
  else if (filename.endsWith(".jpg")) return "image/jpeg";
  else if (filename.endsWith(".gif")) return "image/gif";
  else if (filename.endsWith(".ico")) return "image/x-icon";
  else if (filename.endsWith(".json")) return "application/json";
  return "text/plain";
}

// ============== FILE HANDLER ==============
bool handleFileRead(String path) {
  Serial.println("Request: " + path);
  requestCount++;

  if (path.endsWith("/")) path += "index.html";

  if (!sdCardMounted) {
    server.send(503, "text/plain", "SD Card not mounted");
    return false;
  }

  String contentType = getContentType(path);

  if (SD.exists(path)) {
    File file = SD.open(path, FILE_READ);
    if (file) {
      server.streamFile(file, contentType);
      file.close();
      return true;
    }
  }
  return false;
}

// ============== DISPLAY HELPER FUNCTIONS ==============
void drawCornerBrackets(int x, int y, int w, int h, uint16_t color) {
  int len = 8;
  gfx->drawFastHLine(x, y, len, color);
  gfx->drawFastVLine(x, y, len, color);
  gfx->drawFastHLine(x + w - len, y, len, color);
  gfx->drawFastVLine(x + w - 1, y, len, color);
  gfx->drawFastHLine(x, y + h - 1, len, color);
  gfx->drawFastVLine(x, y + h - len, len, color);
  gfx->drawFastHLine(x + w - len, y + h - 1, len, color);
  gfx->drawFastVLine(x + w - 1, y + h - len, len, color);
}

void drawTickMarks(int cx, int cy, int r, uint16_t color) {
  for (int i = 0; i < 12; i++) {
    float angle = i * 30 * PI / 180;
    int x1 = cx + cos(angle) * (r - 5);
    int y1 = cy + sin(angle) * (r - 5);
    int x2 = cx + cos(angle) * r;
    int y2 = cy + sin(angle) * r;
    gfx->drawLine(x1, y1, x2, y2, color);
  }
}

void drawSignalBars(int x, int y, int rssi) {
  int bars = 0;
  if (rssi > -50) bars = 4;
  else if (rssi > -60) bars = 3;
  else if (rssi > -70) bars = 2;
  else if (rssi > -80) bars = 1;
  for (int i = 0; i < 4; i++) {
    int barHeight = 4 + (i * 3);
    uint16_t color = (i < bars) ? CYAN : DARKGRAY;
    gfx->fillRect(x + (i * 6), y + (12 - barHeight), 4, barHeight, color);
  }
}

// 7-Segment patterns
const uint8_t segPatterns[] = {
  0b1111110, 0b0110000, 0b1101101, 0b1111001, 0b0110011,
  0b1011011, 0b1011111, 0b1110000, 0b1111111, 0b1111011
};

void draw7Seg(int x, int y, int digit, int w, int h, int t, uint16_t color, uint16_t dimColor) {
  if (digit < 0 || digit > 9) return;
  uint8_t seg = segPatterns[digit];
  int hh = h / 2;
  gfx->fillRect(x + t, y, w - t*2, t, (seg & 0b1000000) ? color : dimColor);
  gfx->fillRect(x + w - t, y + t, t, hh - t, (seg & 0b0100000) ? color : dimColor);
  gfx->fillRect(x + w - t, y + hh, t, hh - t, (seg & 0b0010000) ? color : dimColor);
  gfx->fillRect(x + t, y + h - t, w - t*2, t, (seg & 0b0001000) ? color : dimColor);
  gfx->fillRect(x, y + hh, t, hh - t, (seg & 0b0000100) ? color : dimColor);
  gfx->fillRect(x, y + t, t, hh - t, (seg & 0b0000010) ? color : dimColor);
  gfx->fillRect(x + t, y + hh - t/2, w - t*2, t, (seg & 0b0000001) ? color : dimColor);
}

void drawColon(int x, int y, int h, int t, uint16_t color) {
  int dotSize = t + 1;
  gfx->fillRect(x, y + h/3 - dotSize/2, dotSize, dotSize, color);
  gfx->fillRect(x, y + 2*h/3 - dotSize/2, dotSize, dotSize, color);
}

void draw7SegTime(int x, int y, int hours, int mins, uint16_t color, uint16_t dimColor) {
  int digitW = 16, digitH = 24, thick = 3, gap = 3, colonW = 6;
  draw7Seg(x, y, hours / 10, digitW, digitH, thick, color, dimColor);
  draw7Seg(x + digitW + gap, y, hours % 10, digitW, digitH, thick, color, dimColor);
  int colonX = x + 2*(digitW + gap) + gap;
  drawColon(colonX, y, digitH, thick, color);
  int minX = colonX + colonW + gap;
  draw7Seg(minX, y, mins / 10, digitW, digitH, thick, color, dimColor);
  draw7Seg(minX + digitW + gap, y, mins % 10, digitW, digitH, thick, color, dimColor);
}

void drawSecondsTick(int secs) {
  int cx = 120, cy = 120, r = 112;
  float angle = (secs * 6 - 90) * PI / 180;
  gfx->drawLine(cx + cos(angle)*(r-4), cy + sin(angle)*(r-4),
             cx + cos(angle)*(r+2), cy + sin(angle)*(r+2), WHITE);
  int prevSec = (secs == 0) ? 59 : secs - 1;
  float prevAngle = (prevSec * 6 - 90) * PI / 180;
  gfx->drawLine(cx + cos(prevAngle)*(r-4), cy + sin(prevAngle)*(r-4),
             cx + cos(prevAngle)*(r+2), cy + sin(prevAngle)*(r+2), DARKGRAY);
}

void drawChevrons(int frame) {
  int y = 198;
  uint16_t color = (frame % 2 == 0) ? MAGENTA : DARKMAGENTA;
  uint16_t color2 = (frame % 2 == 0) ? DARKMAGENTA : MAGENTA;
  gfx->setTextSize(1);
  gfx->setTextColor(color); gfx->setCursor(60, y); gfx->print(">>");
  gfx->setTextColor(color2); gfx->setCursor(72, y); gfx->print(">>");
  gfx->setTextColor(DARKGRAY); gfx->setCursor(90, y); gfx->print("WEBSRV 1.0T");
  gfx->setTextColor(color2); gfx->setCursor(156, y); gfx->print("<<");
  gfx->setTextColor(color); gfx->setCursor(168, y); gfx->print("<<");
}

void drawTicker(int frame) {
  const char* msg = "  /// SYSTEM NOMINAL /// WEB SERVER ONLINE /// TF CARD ACTIVE /// ESP32-S3 ///  ";
  int msgLen = strlen(msg);
  int charW = 6;
  int totalW = msgLen * charW;
  int scrollX = 240 - (millis() / 20) % (totalW + 240);
  gfx->fillRect(35, 183, 170, 12, BLACK);
  gfx->drawFastHLine(35, 183, 170, DARKGRAY);
  gfx->drawFastHLine(35, 194, 170, DARKGRAY);
  gfx->setTextColor(CYAN); gfx->setTextSize(1);
  for (int i = 0; i < msgLen; i++) {
    int charX = scrollX + (i * charW);
    if (charX >= 35 && charX < 200) {
      gfx->setCursor(charX, 185); gfx->print(msg[i]);
    }
  }
}

// ============== MAIN DISPLAY FUNCTIONS ==============
void drawBootScreen() {
  gfx->fillScreen(BLACK);
  gfx->drawCircle(120, 120, 119, DARKCYAN);
  gfx->drawCircle(120, 120, 118, CYAN);
  gfx->drawCircle(120, 120, 117, DARKCYAN);
  gfx->drawCircle(120, 120, 110, DARKGRAY);
  drawTickMarks(120, 120, 115, MAGENTA);
  gfx->drawCircle(120, 120, 85, DARKMAGENTA);
  gfx->drawCircle(120, 120, 60, DARKCYAN);

  gfx->setTextColor(DARKCYAN); gfx->setTextSize(2);
  gfx->setCursor(59, 49); gfx->print("WEB SERVER");
  gfx->setTextColor(CYAN);
  gfx->setCursor(60, 50); gfx->print("WEB SERVER");

  gfx->drawFastHLine(40, 72, 160, DARKGRAY);
  gfx->drawFastHLine(50, 74, 140, MAGENTA);
  gfx->drawFastHLine(40, 76, 160, DARKGRAY);

  gfx->setTextColor(MAGENTA); gfx->setTextSize(1);
  gfx->setCursor(48, 85); gfx->print("[ ESP32-S3 NODE ]");
  drawCornerBrackets(35, 105, 170, 40, CYAN);
  gfx->setTextColor(YELLOW);
  gfx->setCursor(52, 120); gfx->print(">> INITIALIZING...");
}

void drawStatusScreen() {
  gfx->fillScreen(BLACK);
  gfx->drawCircle(120, 120, 119, DARKCYAN);
  gfx->drawCircle(120, 120, 118, CYAN);
  gfx->drawCircle(120, 120, 117, DARKCYAN);
  drawTickMarks(120, 120, 115, MAGENTA);
  gfx->drawCircle(120, 120, 108, DARKGRAY);

  gfx->fillRect(30, 22, 180, 22, DARKMAGENTA);
  gfx->drawRect(30, 22, 180, 22, MAGENTA);
  gfx->setTextColor(WHITE); gfx->setTextSize(2);
  gfx->setCursor(60, 26); gfx->print("WEB SERVER");
  gfx->drawFastHLine(35, 48, 170, CYAN);

  gfx->fillCircle(52, 58, 4, GREEN);
  gfx->drawCircle(52, 58, 6, GREEN);
  gfx->setTextColor(GREEN); gfx->setTextSize(1);
  gfx->setCursor(62, 54); gfx->print("ON");

  if (sdCardMounted) {
    gfx->fillCircle(90, 58, 4, YELLOW);
    gfx->setTextColor(YELLOW);
  } else {
    gfx->fillCircle(90, 58, 4, RED);
    gfx->setTextColor(RED);
  }
  gfx->setCursor(100, 54); gfx->print("SD");

  gfx->setTextColor(DARKGRAY);
  gfx->setCursor(140, 54); gfx->print("SIG");
  drawCornerBrackets(28, 68, 184, 110, CYAN);
  gfx->drawFastHLine(35, 69, 170, DARKGRAY);
  gfx->drawFastHLine(35, 177, 170, DARKGRAY);
}

void updateDisplay() {
  animFrame++;
  gfx->fillRect(30, 70, 180, 106, BLACK);
  gfx->drawFastHLine(35, 69, 170, DARKGRAY);
  gfx->drawFastHLine(35, 177, 170, DARKGRAY);

  int rssi = WiFi.RSSI();
  drawSignalBars(165, 52, rssi);

  int scanY = 70 + (animFrame % 20) * 5;
  if (scanY < 175) gfx->drawFastHLine(32, scanY, 176, DARKCYAN);

  gfx->setTextColor(DARKGRAY); gfx->setTextSize(1);
  gfx->setCursor(90, 76); gfx->print("NET.ADDR");
  String ip = WiFi.localIP().toString();
  int ipWidth = ip.length() * 6;
  gfx->setTextColor(CYAN);
  gfx->setCursor((240 - ipWidth) / 2, 88); gfx->print(ip);
  gfx->drawFastHLine(40, 100, 160, DARKGRAY);

  unsigned long uptime = millis() / 1000;
  int hours = uptime / 3600;
  int mins = (uptime % 3600) / 60;
  int secs = uptime % 60;

  gfx->setTextColor(DARKGRAY);
  gfx->setCursor(69, 106); gfx->print("RUNTIME");
  gfx->setCursor(135, 106); gfx->print("REQ");

  char uptimeStr[12];
  sprintf(uptimeStr, "%02d:%02d:%02d", hours, mins, secs);
  gfx->setTextColor(YELLOW);
  gfx->setCursor(63, 118); gfx->print(uptimeStr);
  gfx->setTextColor(GREEN);
  gfx->setCursor(135, 118); gfx->print(requestCount);
  gfx->drawFastHLine(40, 130, 160, DARKGRAY);

  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    int hour12 = timeinfo.tm_hour % 12;
    if (hour12 == 0) hour12 = 12;
    drawSecondsTick(timeinfo.tm_sec);
    draw7SegTime(78, 138, hour12, timeinfo.tm_min, CYAN, BLACK);
    gfx->setTextSize(1); gfx->setTextColor(GREEN);
    gfx->setCursor(168, 156); gfx->print(timeinfo.tm_hour >= 12 ? "PM" : "AM");
    char dateStr[12];
    strftime(dateStr, sizeof(dateStr), "%m/%d/%Y", &timeinfo);
    gfx->setTextColor(YELLOW);
    gfx->setCursor(90, 168); gfx->print(dateStr);
  } else {
    gfx->setTextColor(DARKGRAY);
    gfx->setCursor(60, 150); gfx->print("SYNCING TIME...");
  }
  drawTicker(animFrame);
  drawChevrons(animFrame);
}

// ============== SETUP ==============
void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n=== ESP32 SD Card Web Server ===\n");

  // Initialize SD card FIRST (before display)
  Serial.println("Initializing SD card...");
  pinMode(SD_CS, OUTPUT);
  digitalWrite(SD_CS, HIGH);
  sdSPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);

  if (!SD.begin(SD_CS, sdSPI, 4000000)) {  // 4MHz for compatibility
    Serial.println("SD Card mount failed!");
    sdCardMounted = false;
  } else {
    sdCardMounted = true;
    Serial.println("SD Card OK!");
    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("SD Card Size: %lluMB\n", cardSize);
  }

  // Initialize display
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);
  gfx->begin();
  gfx->setRotation(0);
  drawBootScreen();

  // Show SD status on display
  if (!sdCardMounted) {
    gfx->setTextColor(RED);
    gfx->setCursor(52, 140);
    gfx->print("SD CARD ERROR!");
    delay(2000);
  }

  // Connect to WiFi
  Serial.printf("Connecting to %s", ssid);
  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    Serial.print(".");
    attempts++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(" Connected!");
    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    // Configure NTP time
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

    // Start FTP server
    ftpSrv.begin(ftp_user, ftp_pass);
    Serial.println("FTP server started");
    Serial.printf("FTP User: %s\n", ftp_user);

    drawStatusScreen();
    updateDisplay();
  } else {
    Serial.println(" Failed!");
    WiFi.softAP("ESP32-Server", "password123");
    Serial.print("AP IP: ");
    Serial.println(WiFi.softAPIP());
  }

  // Setup web server
  server.onNotFound([]() {
    if (!handleFileRead(server.uri())) {
      server.send(404, "text/plain", "404: Not Found");
    }
  });

  server.begin();
  Serial.println("Web server started on port 80");
}

// ============== LOOP ==============
void loop() {
  server.handleClient();
  ftpSrv.handleFTP();

  if (millis() - lastDisplayUpdate > 1000) {
    lastDisplayUpdate = millis();
    if (WiFi.status() == WL_CONNECTED) {
      updateDisplay();
    }
  }

  if (millis() - lastTickerUpdate > 50) {
    lastTickerUpdate = millis();
    if (WiFi.status() == WL_CONNECTED) {
      drawTicker(0);
    }
  }
}

WEB PAGE TEMPLATE

data/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Server</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="scanlines"></div>
    <div class="container">
        <header>
            <div class="logo-frame">
                <span class="bracket">[</span>
                <h1>ESP32<span class="accent">SERVER</span></h1>
                <span class="bracket">]</span>
            </div>
            <p class="status">// SYSTEM ONLINE</p>
        </header>

        <main>
            <section class="card">
                <h2>SYSTEM STATUS</h2>
                <div class="status-grid">
                    <div class="status-item">
                        <span class="label">WiFi</span>
                        <span class="value online">CONNECTED</span>
                    </div>
                    <div class="status-item">
                        <span class="label">SD Card</span>
                        <span class="value online">MOUNTED</span>
                    </div>
                    <div class="status-item">
                        <span class="label">FTP</span>
                        <span class="value online">ACTIVE</span>
                    </div>
                    <div class="status-item">
                        <span class="label">Web Server</span>
                        <span class="value online">RUNNING</span>
                    </div>
                </div>
            </section>

            <section class="card">
                <h2>ABOUT</h2>
                <p>ESP32-S3 Web Server with SD Card storage and FTP access.</p>
                <p>Upload files via FTP or swap the SD card to update content.</p>
            </section>
        </main>

        <footer>
            <p>&copy; 2025 // ESP32 SERVER</p>
        </footer>
    </div>
</body>
</html>

STYLESHEET TEMPLATE

data/style.css
/* Cyberpunk Theme - ESP32 Server */

:root {
    --bg-dark: #0a0a0f;
    --bg-card: #12121a;
    --neon-cyan: #00ffff;
    --neon-magenta: #ff00ff;
    --neon-yellow: #ffff00;
    --text-primary: #e0e0e0;
    --text-dim: #666;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Courier New', monospace;
    background: var(--bg-dark);
    color: var(--text-primary);
    min-height: 100vh;
    overflow-x: hidden;
}

/* Scanline Effect */
.scanlines {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    background: repeating-linear-gradient(
        0deg,
        rgba(0,0,0,0.1) 0px,
        rgba(0,0,0,0.1) 1px,
        transparent 1px,
        transparent 2px
    );
    z-index: 1000;
}

.container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

/* Header */
header {
    text-align: center;
    padding: 40px 0;
    border-bottom: 1px solid #333;
}

.logo-frame {
    display: inline-flex;
    align-items: center;
    gap: 10px;
}

.bracket {
    font-size: 3rem;
    color: var(--neon-magenta);
    text-shadow: 0 0 20px var(--neon-magenta);
}

h1 {
    font-size: 2.5rem;
    color: var(--neon-cyan);
    text-shadow: 0 0 30px var(--neon-cyan);
    letter-spacing: 8px;
}

.accent {
    color: var(--neon-magenta);
    text-shadow: 0 0 30px var(--neon-magenta);
}

.status {
    color: var(--text-dim);
    margin-top: 10px;
    letter-spacing: 3px;
}

/* Cards */
.card {
    background: var(--bg-card);
    border: 1px solid #333;
    border-radius: 10px;
    padding: 25px;
    margin: 20px 0;
}

.card h2 {
    color: var(--neon-magenta);
    font-size: 1rem;
    letter-spacing: 3px;
    margin-bottom: 20px;
    border-bottom: 1px solid var(--neon-magenta);
    padding-bottom: 10px;
}

.card p {
    color: var(--text-dim);
    line-height: 1.6;
    margin-bottom: 10px;
}

/* Status Grid */
.status-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 15px;
}

.status-item {
    display: flex;
    justify-content: space-between;
    padding: 10px;
    background: rgba(0,255,255,0.05);
    border: 1px solid #333;
    border-radius: 5px;
}

.label {
    color: var(--text-dim);
}

.value.online {
    color: #00ff00;
    text-shadow: 0 0 10px #00ff00;
}

/* Footer */
footer {
    text-align: center;
    padding: 30px 0;
    border-top: 1px solid #333;
    color: var(--text-dim);
    font-size: 0.8rem;
}

/* Responsive */
@media (max-width: 600px) {
    .status-grid {
        grid-template-columns: 1fr;
    }
    h1 {
        font-size: 1.5rem;
    }
}

SETUP INSTRUCTIONS

  • WiFi: Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD with your credentials.
  • FTP: Default credentials are admin / admin. Change these for security.
  • Library: Install SimpleFTPServer and Arduino_GFX_Library via Arduino Library Manager.
  • SimpleFTPServer Config: Edit FtpServerKey.h in the library folder:
    • Set STORAGE_TYPE to STORAGE_SD
    • Set STORAGE_SD_FORCE_DISABLE_SEPORATECLASS to true
  • FTP Client: Use FileZilla or similar. Connect to the ESP32's IP on port 21.
  • Board: Select "ESP32S3 Dev Module" in Arduino IDE.
  • Serial Monitor: 115200 baud to see IP address and debug info.

SD CARD FILE STRUCTURE

Format your SD card as FAT32 and organize the website files as follows:

  1. Format SD card as FAT32 (use SD Card Formatter tool for best results)
  2. Copy index.html and style.css to the root of the SD card
  3. Create an images folder for any graphics (optional)
  4. Insert SD card into the module before powering on

SD Card folder structure:
SD Card (FAT32)/
  ├── index.html      // Main webpage
  ├── style.css       // Stylesheet
  ├── script.js       // JavaScript (optional)
  └── images/         // Image folder (optional)
        ├── logo.png
        └── background.jpg

Tip: Use FTP to upload files wirelessly after initial setup. Connect to the ESP32's IP address on port 21.

DISPLAY ROTATION

To rotate the display, add gfx->setRotation(n); in setup() after gfx->begin();

gfx->setRotation(0); // 0° - Default orientation
gfx->setRotation(1); // 90° - Rotate clockwise
gfx->setRotation(2); // 180° - Upside down
gfx->setRotation(3); // 270° - Rotate counter-clockwise

Note: This rotates the entire display output. Use this if your display is mounted upside down or at an angle.

ARDUINO PROJECT SETUP

  • Create Project Folder: Create a folder named LittleWebServer_SD in your Arduino sketches directory
  • Main Sketch: Save the code as LittleWebServer_SD.ino - filename MUST match folder name
  • Single File Project: This project uses only one .ino file
  • SD Card: Format as FAT32, copy your website files to root
LittleWebServer_SD/
└── LittleWebServer_SD.ino   // Main sketch

SD Card (FAT32):
├── index.html               // Your website
├── style.css                // Stylesheets
└── images/                  // Any assets

COMPONENTS NEEDED

ESP32-S3 Super Mini Main microcontroller AliExpress
GC9A01 Round Display 240x240 1.28" TFT Amazon
MicroSD Card Module SPI TF card reader Amazon
MicroSD Card FAT32 formatted (≤32GB) Amazon
Dupont Wires For connections Amazon

WIRING DIAGRAM

GC9A01 Display ESP32-S3
VCC 3.3V
GND GND
SCL (SCK) GPIO 12
SDA (MOSI) GPIO 11
RES (RST) GPIO 10
DC GPIO 9
CS GPIO 8
BLK GPIO 7
SD Card Module ESP32-S3
VCC 3.3V
GND GND
SCK GPIO 4
MISO GPIO 3
MOSI GPIO 2
CS GPIO 1

Note: Display and SD card use separate SPI buses.

REQUIRED LIBRARIES

Install via Arduino Library Manager:

Arduino_GFX_Library by moononournation - Display driver
SimpleFTPServer For wireless file upload (optional)

ARDUINO IDE 3.x BOARD SETTINGS

Board ESP32S3 Dev Module
USB CDC On Boot Enabled
Flash Size 4MB (32Mb)
Partition Scheme Default 4MB with spiffs
Upload Speed 921600

ESP32 Board URL:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

TROUBLESHOOTING

  • Upload fails: Hold BOOT button while clicking Upload.
  • Display is blank: Check display wiring. Verify 3.3V power.
  • SD card not detected: Format as FAT32. Try 4MHz in SD.begin().
  • Files not found: Put files in SD root, not subfolders.
  • WiFi won't connect: Check SSID/password. 2.4GHz only.
  • Web page not loading: Check Serial for IP. Clear browser cache.

BUILD CHECKLIST

  1. □ Install Arduino IDE 3.x and ESP32 boards
  2. □ Install Arduino_GFX_Library
  3. □ Create folder: LittleWebServer_SD/
  4. □ Copy sketch as LittleWebServer_SD.ino
  5. □ Edit code: Set WiFi credentials
  6. □ Format SD card as FAT32
  7. □ Copy website files to SD card
  8. □ Wire display and SD card module
  9. □ Insert SD card
  10. □ Upload sketch (hold BOOT if needed)
  11. □ Check Serial Monitor for IP
  12. □ Open IP in browser - Done!