CYD WEB SERVER ESP32-32E

Cheap Yellow Display // 3.5" ST7796 // Built-in SD Card

CAPTIVE PORTAL VERSION →

KEY FEATURES

>
3.5" TFT Display ST7796 320x480 with resistive touch
>
Built-in SD Card Slot No external module required
>
RGB LED Status indication via GPIO
>
FTP Server Upload/download files wirelessly

HARDWARE SPECS

ESP32-WROOM-32E (Dual Core 240MHz)
ST7796 3.5" TFT (320x480)
XPT2046 Resistive Touch
Built-in Micro SD Slot
RGB LED (GPIO 22/16/17)
Audio DAC (GPIO 26)

DISPLAY PREVIEW

DISPLAY INFO

Resolution 320x480
Driver ST7796
Interface SPI
Scale 50%
Ready - Click RUN BOOT SEQUENCE

ARDUINO PROJECT SETUP

Create this folder structure for your project:

CYD_WEB_Server/
└── CYD_WEB_Server.ino    <-- Main sketch (copy from below)

SD Card (FAT32):
├── index.html            <-- Your website homepage
├── style.css             <-- Stylesheet (copy template below)
└── images/               <-- Optional image folder
                

Note: CYD has built-in SD card slot. Web files are served from SD card. Format as FAT32.

COMPONENTS NEEDED

  • ESP32-2432S028R (CYD) - Cheap Yellow Display board with built-in 2.8" or 3.5" ST7796 display
  • MicroSD Card - FAT32 formatted (up to 32GB recommended)
  • USB-C or Micro-USB Cable - For programming (check your board version)

All-in-One: CYD includes display, SD card slot, touch panel, RGB LED, and audio DAC on a single board. No external wiring required!

REQUIRED LIBRARIES

Install via Arduino Library Manager (Sketch → Include Library → Manage Libraries):

  • Arduino_GFX_Library - Display driver for ST7796
  • SimpleFTPServer - FTP server for wireless file upload

Note: WiFi, WebServer, SD, and SPI are built into ESP32 Arduino core.

SimpleFTPServer Config: Edit FtpServerKey.h in the library folder:

  • Set STORAGE_TYPE to STORAGE_SD
  • Set STORAGE_SD_FORCE_DISABLE_SEPORATECLASS to true

ARDUINO IDE 3.x BOARD SETTINGS

Tools menu configuration:

  • Board: ESP32 Dev Module
  • Upload Speed: 921600
  • Flash Frequency: 80MHz
  • Flash Mode: QIO
  • Flash Size: 4MB (32Mb)
  • Partition Scheme: Default 4MB with spiffs
  • PSRAM: Disabled

Note: CYD uses standard ESP32-WROOM-32E, not ESP32-S3. Select "ESP32 Dev Module".

TROUBLESHOOTING

  • Display stays white/black: Check that you're using ST7796 driver. CYD 2.8" uses different pins than 3.5" version.
  • SD Card not mounting: Must be FAT32 formatted. Try cards 32GB or smaller.
  • Upload fails: Hold BOOT button during upload. Some CYD boards need this.
  • Wrong colors: Display may need BGR mode. Check Arduino_GFX initialization.
  • Touch not working: Touch uses separate SPI pins (GPIO 33, 32, 21, 25). Check XPT2046 library.
  • FTP connection refused: Verify SimpleFTPServer is configured for SD storage.
  • Web files not loading: Check file paths on SD card. Files must be in root or match URL paths.

ARDUINO SKETCH

CYD_WEB_Server.ino
/*
  ESP32-32E Web Server + FTP + SD Card
  Generic Template - Customize for your project

  Hardware:
  - ESP32-32E Module (dual-core 240MHz)
  - ST7796 3.5" TFT Display (320x480)
  - Built-in Micro SD Card Slot
  - Resistive Touch (XPT2046)
*/

#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 (ST7796 - shared SPI bus)
#define TFT_SCK   14
#define TFT_MOSI  13
#define TFT_MISO  12
#define TFT_CS    15
#define TFT_DC     2
#define TFT_RST   -1  // Not connected (use software reset)
#define TFT_BL    27  // Backlight control pin!

// SD Card pins (built-in slot, shares SPI with display)
#define SD_CS      5  // SD card CS per lcdwiki

// Touch pins (XPT2046)
#define TOUCH_CS  33
#define TOUCH_IRQ 36

// RGB LED pins
#define LED_RED   22
#define LED_GREEN 16
#define LED_BLUE  17

// Audio pins
#define AUDIO_EN   4
#define AUDIO_DAC 26

// Battery ADC
#define BATT_ADC  34

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

// ============== DISPLAY DIMENSIONS ==============
#define SCREEN_WIDTH  320
#define SCREEN_HEIGHT 480

// ============== OBJECTS ==============
// Hardware SPI for display (HSPI bus)
Arduino_DataBus *bus = new Arduino_ESP32SPI(
  TFT_DC,    // DC
  TFT_CS,    // CS
  TFT_SCK,   // SCK  = 14
  TFT_MOSI,  // MOSI = 13
  TFT_MISO,  // MISO = 12
  HSPI,      // HSPI bus (pins 12-15)
  10000000   // 10MHz
);
Arduino_GFX *gfx = new Arduino_ST7796(bus, TFT_RST, 0, false);  // rotation=0, IPS=false
WebServer server(80);
FtpServer ftpSrv;

// ============== VARIABLES ==============
unsigned long lastDisplayUpdate = 0;
unsigned long lastTickerUpdate = 0;
unsigned long requestCount = 0;
int animFrame = 0;
bool sdCardMounted = false;
int lastDisplayedHour = -1;
int lastDisplayedMin = -1;

// ============== 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 = 12;
  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 drawHorizontalDivider(int y, uint16_t color) {
  gfx->drawFastHLine(20, y, SCREEN_WIDTH - 40, 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 = 6 + (i * 4);
    uint16_t color = (i < bars) ? CYAN : DARKGRAY;
    gfx->fillRect(x + (i * 8), y + (18 - barHeight), 5, 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 + 2;
  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 = 28, digitH = 44, thick = 5, gap = 6, colonW = 12;
  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 drawChevrons(int frame) {
  int y = 445;
  uint16_t color = (frame % 2 == 0) ? MAGENTA : DARKMAGENTA;
  uint16_t color2 = (frame % 2 == 0) ? DARKMAGENTA : MAGENTA;
  gfx->setTextSize(1);
  gfx->setTextColor(color); gfx->setCursor(80, y); gfx->print(">>");
  gfx->setTextColor(color2); gfx->setCursor(96, y); gfx->print(">>");
  gfx->setTextColor(WHITE); gfx->setCursor(120, y); gfx->print("WEBSRV 1.0T");
  gfx->setTextColor(color2); gfx->setCursor(200, y); gfx->print("<<");
  gfx->setTextColor(color); gfx->setCursor(216, y); gfx->print("<<");
}

void drawTicker(int frame) {
  const char* msg = "  /// SYSTEM NOMINAL /// WEB SERVER ONLINE /// TF CARD ACTIVE /// ESP32-32E ///  ";
  int msgLen = strlen(msg);
  int charW = 6;
  int totalW = msgLen * charW;
  int tickerY = 420;
  int scrollX = SCREEN_WIDTH - (millis() / 20) % (totalW + SCREEN_WIDTH);
  gfx->fillRect(20, tickerY, SCREEN_WIDTH - 40, 16, BLACK);
  gfx->drawFastHLine(20, tickerY, SCREEN_WIDTH - 40, DARKGRAY);
  gfx->drawFastHLine(20, tickerY + 15, SCREEN_WIDTH - 40, DARKGRAY);
  gfx->setTextColor(CYAN); gfx->setTextSize(1);
  for (int i = 0; i < msgLen; i++) {
    int charX = scrollX + (i * charW);
    if (charX >= 20 && charX < SCREEN_WIDTH - 20) {
      gfx->setCursor(charX, tickerY + 4); gfx->print(msg[i]);
    }
  }
}

// ============== MAIN DISPLAY FUNCTIONS ==============
void drawBootScreen() {
  gfx->fillScreen(BLACK);

  // Border frame
  gfx->drawRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, DARKCYAN);
  gfx->drawRect(10, 10, SCREEN_WIDTH - 20, SCREEN_HEIGHT - 20, CYAN);
  gfx->drawRect(15, 15, SCREEN_WIDTH - 30, SCREEN_HEIGHT - 30, DARKCYAN);

  // Title
  gfx->setTextColor(DARKCYAN); gfx->setTextSize(3);
  gfx->setCursor(59, 80); gfx->print("WEB SERVER");
  gfx->setTextColor(CYAN);
  gfx->setCursor(60, 81); gfx->print("WEB SERVER");

  // Decorative lines
  drawHorizontalDivider(120, DARKGRAY);
  gfx->drawFastHLine(40, 125, SCREEN_WIDTH - 80, MAGENTA);
  drawHorizontalDivider(130, DARKGRAY);

  // Subtitle
  gfx->setTextColor(MAGENTA); gfx->setTextSize(2);
  gfx->setCursor(55, 160); gfx->print("[ ESP32-32E NODE ]");

  // Status box
  drawCornerBrackets(30, 200, SCREEN_WIDTH - 60, 80, CYAN);
  gfx->setTextColor(YELLOW); gfx->setTextSize(2);
  gfx->setCursor(60, 230); gfx->print(">> INITIALIZING...");

  // Version info
  gfx->setTextColor(DARKGRAY); gfx->setTextSize(1);
  gfx->setCursor(100, 320); gfx->print("ST7796 320x480 Display");
  gfx->setCursor(115, 335); gfx->print("Firmware v1.0");
}

void drawStatusScreen() {
  gfx->fillScreen(BLACK);

  // Border frame
  gfx->drawRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, DARKCYAN);
  gfx->drawRect(10, 10, SCREEN_WIDTH - 20, SCREEN_HEIGHT - 20, DARKGRAY);

  // Title bar
  gfx->fillRect(20, 15, SCREEN_WIDTH - 40, 35, DARKMAGENTA);
  gfx->drawRect(20, 15, SCREEN_WIDTH - 40, 35, MAGENTA);
  gfx->setTextColor(WHITE); gfx->setTextSize(3);
  gfx->setCursor(60, 20); gfx->print("WEB SERVER");

  // Status indicators bar
  drawHorizontalDivider(55, CYAN);

  // Status indicators
  int indicatorY = 65;

  // Power ON indicator
  gfx->fillCircle(50, indicatorY + 6, 6, GREEN);
  gfx->drawCircle(50, indicatorY + 6, 8, GREEN);
  gfx->setTextColor(GREEN); gfx->setTextSize(1);
  gfx->setCursor(62, indicatorY + 2); gfx->print("PWR");

  // SD Card indicator
  if (sdCardMounted) {
    gfx->fillCircle(120, indicatorY + 6, 6, YELLOW);
    gfx->drawCircle(120, indicatorY + 6, 8, YELLOW);
    gfx->setTextColor(YELLOW);
  } else {
    gfx->fillCircle(120, indicatorY + 6, 6, RED);
    gfx->drawCircle(120, indicatorY + 6, 8, RED);
    gfx->setTextColor(RED);
  }
  gfx->setCursor(132, indicatorY + 2); gfx->print("SD");

  // FTP indicator
  gfx->fillCircle(180, indicatorY + 6, 6, CYAN);
  gfx->drawCircle(180, indicatorY + 6, 8, CYAN);
  gfx->setTextColor(CYAN);
  gfx->setCursor(192, indicatorY + 2); gfx->print("FTP");

  // Signal indicator
  gfx->setTextColor(WHITE);
  gfx->setCursor(240, indicatorY + 2); gfx->print("SIG");
  drawSignalBars(270, indicatorY - 2, WiFi.RSSI());

  drawHorizontalDivider(85, DARKGRAY);

  // Main content area frame
  drawCornerBrackets(15, 90, SCREEN_WIDTH - 30, 320, CYAN);

  // Static labels (drawn once)
  gfx->setTextColor(WHITE); gfx->setTextSize(1);
  gfx->setCursor(115, 97); gfx->print("NETWORK ADDRESS");

  String ip = WiFi.localIP().toString();
  gfx->setTextColor(CYAN); gfx->setTextSize(2);
  int ipWidth = ip.length() * 12;
  gfx->setCursor((SCREEN_WIDTH - ipWidth) / 2, 120);
  gfx->print(ip);

  drawHorizontalDivider(145, DARKGRAY);

  gfx->setTextColor(WHITE); gfx->setTextSize(1);
  gfx->setCursor(55, 155); gfx->print("RUNTIME");
  gfx->setCursor(210, 155); gfx->print("REQUESTS");

  drawHorizontalDivider(195, DARKGRAY);

  gfx->setTextColor(WHITE); gfx->setTextSize(1);
  gfx->setCursor(124, 210); gfx->print("CURRENT TIME");

  drawHorizontalDivider(360, DARKGRAY);
  drawHorizontalDivider(390, DARKGRAY);
}

void updateDisplay() {
  animFrame++;

  // Update signal bars
  drawSignalBars(270, 63, WiFi.RSSI());

  // Runtime and Requests section
  unsigned long uptime = millis() / 1000;
  int uptimeHours = uptime / 3600;
  int uptimeMins = (uptime % 3600) / 60;
  int uptimeSecs = uptime % 60;

  // Left side - Runtime (clear and redraw value only)
  gfx->fillRect(40, 170, 110, 18, BLACK);
  gfx->setTextColor(YELLOW); gfx->setTextSize(2);
  if (uptimeHours > 0) {
    gfx->setCursor(40, 170);
    gfx->printf("%02d:%02d hr", uptimeHours, uptimeMins);
  } else {
    gfx->setCursor(52, 170);
    gfx->printf("%02d min", uptimeMins);
  }

  // Right side - Requests (clear and redraw value only)
  gfx->fillRect(220, 170, 80, 18, BLACK);
  gfx->setTextColor(GREEN); gfx->setTextSize(2);
  gfx->setCursor(220, 170); gfx->print(requestCount);

  // Time display section
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    int hour12 = timeinfo.tm_hour % 12;
    if (hour12 == 0) hour12 = 12;

    // Large 7-segment time display (only redraw when time changes)
    if (hour12 != lastDisplayedHour || timeinfo.tm_min != lastDisplayedMin) {
      gfx->fillRect(50, 230, 220, 70, BLACK);
      draw7SegTime(55, 235, hour12, timeinfo.tm_min, CYAN, DARKGRAY);

      // AM/PM indicator
      gfx->fillRect(230, 255, 35, 20, BLACK);
      gfx->setTextSize(2); gfx->setTextColor(GREEN);
      gfx->setCursor(233, 270);
      gfx->print(timeinfo.tm_hour >= 12 ? "PM" : "AM");

      lastDisplayedHour = hour12;
      lastDisplayedMin = timeinfo.tm_min;
    }

    // Date display (clear and redraw)
    gfx->fillRect(95, 320, 130, 18, BLACK);
    char dateStr[15];
    strftime(dateStr, sizeof(dateStr), "%m/%d/%Y", &timeinfo);
    gfx->setTextColor(YELLOW); gfx->setTextSize(2);
    gfx->setCursor(95, 320); gfx->print(dateStr);
  }

  // Bottom stats (clear entire row once, then redraw)
  gfx->fillRect(25, 375, 270, 10, BLACK);

  // Battery voltage
  int battRaw = analogRead(BATT_ADC);
  float battVoltage = (battRaw / 4095.0) * 3.3 * 2;
  gfx->setTextColor(WHITE); gfx->setTextSize(1);
  gfx->setCursor(25, 375); gfx->print("BATT:");
  gfx->setTextColor(battVoltage > 3.5 ? GREEN : RED);
  gfx->printf("%.1fV", battVoltage);

  // Free heap memory
  gfx->setTextColor(WHITE);
  gfx->setCursor(115, 375); gfx->print("HEAP:");
  gfx->setTextColor(CYAN);
  gfx->printf("%dK", ESP.getFreeHeap() / 1024);

  // WiFi RSSI value
  gfx->setTextColor(WHITE);
  gfx->setCursor(205, 375); gfx->print("RSSI:");
  gfx->setTextColor(YELLOW);
  gfx->printf("%ddBm", WiFi.RSSI());

  // Ticker and chevrons
  drawTicker(animFrame);
  drawChevrons(animFrame);
}

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

  // Initialize RGB LEDs
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LED_BLUE, OUTPUT);
  digitalWrite(LED_RED, HIGH);    // Red on during boot
  digitalWrite(LED_GREEN, LOW);
  digitalWrite(LED_BLUE, LOW);

  // Set chip selects as outputs and deselect both FIRST
  pinMode(TFT_CS, OUTPUT);
  pinMode(SD_CS, OUTPUT);
  digitalWrite(TFT_CS, HIGH);
  digitalWrite(SD_CS, HIGH);

  // Turn on backlight at 40% brightness using PWM
  ledcAttach(TFT_BL, 5000, 8);  // Pin, 5kHz frequency, 8-bit resolution
  ledcWrite(TFT_BL, 102);       // 40% of 255 = 102
  Serial.println("Backlight at 40% (IO27)");

  // Initialize display FIRST (it sets up HSPI bus)
  Serial.println("Initializing display...");
  gfx->begin();
  gfx->fillScreen(BLACK);
  Serial.println("  Display initialized");

  // Initialize SD card (shares HSPI bus with display)
  Serial.println("Initializing SD card...");
  pinMode(SD_CS, OUTPUT);
  digitalWrite(SD_CS, HIGH);
  digitalWrite(TFT_CS, HIGH);  // Deselect display

  if (!SD.begin(SD_CS, SPI, 4000000)) {
    Serial.println("  SD Card mount failed!");
    sdCardMounted = false;
  } else {
    sdCardMounted = true;
    Serial.println("  SD Card OK!");
  }

  if (sdCardMounted) {
    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("  SD Card Size: %lluMB\n", cardSize);
    digitalWrite(LED_GREEN, HIGH);
  }

  // Draw boot screen
  Serial.println("Drawing boot screen...");
  drawBootScreen();

  // Show SD status on display
  if (!sdCardMounted) {
    gfx->setTextColor(RED); gfx->setTextSize(2);
    gfx->setCursor(70, 260);
    gfx->print("SD CARD ERROR!");
    digitalWrite(LED_RED, HIGH);
    delay(2000);
  }

  // Connect to WiFi
  Serial.printf("Connecting to %s", ssid);
  gfx->setTextColor(CYAN); gfx->setTextSize(1);
  gfx->setCursor(80, 370);
  gfx->print("Connecting to WiFi...");

  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    Serial.print(".");
    // Blink blue LED during connection
    digitalWrite(LED_BLUE, attempts % 2);
    attempts++;
  }

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

    digitalWrite(LED_BLUE, HIGH);  // Blue on = WiFi connected

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

    // Hold boot screen for 5 seconds
    delay(5000);

    drawStatusScreen();
    updateDisplay();
  } else {
    Serial.println(" Failed!");
    digitalWrite(LED_BLUE, LOW);
    digitalWrite(LED_RED, HIGH);  // Red = WiFi failed

    WiFi.softAP("ESP32-Server", "password123");
    Serial.print("AP IP: ");
    Serial.println(WiFi.softAPIP());

    gfx->setTextColor(ORANGE); gfx->setTextSize(2);
    gfx->setCursor(60, 380);
    gfx->print("AP Mode: 192.168.4.1");
  }

  // Setup web server

  // API endpoint for live system data
  server.on("/api/info", []() {
    String json = "{";
    json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
    json += "\"ssid\":\"" + String(ssid) + "\",";
    json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
    json += "\"port\":80,";
    json += "\"uptime\":" + String(millis() / 1000) + ",";
    json += "\"heap\":" + String(ESP.getFreeHeap()) + ",";
    json += "\"sdSize\":" + String(sdCardMounted ? SD.cardSize() / (1024 * 1024) : 0) + ",";
    json += "\"requests\":" + String(requestCount);
    json += "}";
    server.send(200, "application/json", json);
  });

  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 CYD 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 CYD SERVER</p>
        </footer>
    </div>
</body>
</html>

STYLESHEET TEMPLATE

data/style.css
/* Cyberpunk Theme - ESP32 CYD 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.
  • SD Card: Format as FAT32. Uses built-in slot on the CYD board.
  • Display: ST7796 uses HSPI bus (GPIO 12-15). Shares SPI with SD card.
  • Backlight: Controlled via PWM on GPIO 27. Adjust brightness in code (0-255).
  • Library: Install SimpleFTPServer and Arduino_GFX_Library via Arduino Library Manager.
  • Board: Select "ESP32 Dev Module" in Arduino IDE.

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 CYD board 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 CYD's IP address on port 21 using FileZilla or similar client.

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.

BUILD CHECKLIST

Follow these steps to build your CYD Web Server:

  1. Create project folder: CYD_WEB_Server/
  2. Copy Arduino sketch: Save code as CYD_WEB_Server.ino
  3. Edit WiFi credentials: Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD
  4. Format SD card: FAT32 format (32GB or smaller recommended)
  5. Copy web files to SD: Add index.html and style.css to SD card root
  6. Install libraries: Arduino_GFX_Library + SimpleFTPServer
  7. Configure SimpleFTPServer: Edit FtpServerKey.h (STORAGE_SD)
  8. Insert SD card: Place SD card in CYD's built-in slot
  9. Select board: ESP32 Dev Module (NOT ESP32-S3)
  10. Upload sketch: Compile and upload (hold BOOT if needed)
  11. Test: Open browser to ESP32's IP address shown on display