LITTLE WEB SERVER CODE

Arduino Sketch + LittleFS Web Page

CAPTIVE PORTAL VERSION →

DISPLAY PREVIEW

LIVE STATS

IP Address: 192.168.1.100
Uptime: 00:00:00
Requests: 0
Signal: -45 dBm
Ready - Click RUN BOOT SEQUENCE

ARDUINO SKETCH

websrv.ino
/*
  ESP32-S3 Web Server + Display
  ESP32-S3 SuperMini + GC9A01 Round Display
*/

#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <Arduino_GFX_Library.h>
#include <time.h>

// WiFi credentials
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

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

// Display pins
#define TFT_SCK  12
#define TFT_MOSI 11
#define TFT_CS    8
#define TFT_DC    9
#define TFT_RST  10
#define TFT_BL    7

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

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

WebServer server(80);

unsigned long lastDisplayUpdate = 0;
unsigned long lastTickerUpdate = 0;
unsigned long requestCount = 0;
int animFrame = 0;

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(".ico")) return "image/x-icon";
  return "text/plain";
}

bool handleFileRead(String path) {
  Serial.println("Request: " + path);
  requestCount++;
  if (path.endsWith("/")) path += "index.html";
  String contentType = getContentType(path);
  if (LittleFS.exists(path)) {
    File file = LittleFS.open(path, "r");
    server.streamFile(file, contentType);
    file.close();
    return true;
  }
  return false;
}

// Draw corner brackets for HUD effect
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);
}

// Draw tick marks around circle edge
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);
  }
}

// Draw signal strength bars
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 digit patterns
const uint8_t segPatterns[] = {
  0b1111110, 0b0110000, 0b1101101, 0b1111001, 0b0110011,
  0b1011011, 0b1011111, 0b1110000, 0b1111111, 0b1111011
};

// Draw a single 7-segment digit
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);
}

// Draw colon for clock
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);
}

// Draw 7-segment time display
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);
}

// Draw scrolling ticker
void drawTicker(int frame) {
  const char* msg = "  /// SYSTEM NOMINAL /// WEB SERVER ONLINE /// ESP32-S3 ACTIVE ///  ";
  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]);
    }
  }
}

// Draw pulsing chevrons
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(93, y); gfx->print("WEBSRV 1.0");
  gfx->setTextColor(color2);
  gfx->setCursor(156, y); gfx->print("<<");
  gfx->setTextColor(color);
  gfx->setCursor(168, y); gfx->print("<<");
}

// Draw seconds tick around edge
void drawSecondsTick(int secs) {
  int cx = 120, cy = 120, r = 112;
  float angle = (secs * 6 - 90) * PI / 180;
  int x1 = cx + cos(angle) * (r - 4);
  int y1 = cy + sin(angle) * (r - 4);
  int x2 = cx + cos(angle) * (r + 2);
  int y2 = cy + sin(angle) * (r + 2);
  gfx->drawLine(x1, y1, x2, y2, WHITE);
  int prevSec = (secs == 0) ? 59 : secs - 1;
  float prevAngle = (prevSec * 6 - 90) * PI / 180;
  int px1 = cx + cos(prevAngle) * (r - 4);
  int py1 = cy + sin(prevAngle) * (r - 4);
  int px2 = cx + cos(prevAngle) * (r + 2);
  int py2 = cy + sin(prevAngle) * (r + 2);
  gfx->drawLine(px1, py1, px2, py2, DARKGRAY);
}

// Draw boot screen
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("WEBSERVER");
  gfx->setTextColor(CYAN);
  gfx->setCursor(60, 50);
  gfx->print("WEBSERVER");
  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...");
}

// Draw main status screen
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("WEBSERVER");
  gfx->drawFastHLine(35, 48, 170, CYAN);
  gfx->fillCircle(60, 58, 4, GREEN);
  gfx->drawCircle(60, 58, 6, GREEN);
  gfx->setTextColor(GREEN);
  gfx->setTextSize(1);
  gfx->setCursor(70, 54);
  gfx->print("ONLINE");
  gfx->setTextColor(DARKGRAY);
  gfx->setCursor(130, 54);
  gfx->print("SIGNAL");
  drawCornerBrackets(28, 68, 184, 110, CYAN);
  gfx->drawFastHLine(35, 69, 170, DARKGRAY);
  gfx->drawFastHLine(35, 177, 170, DARKGRAY);
}

// Update display with live data
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(MAGENTA);
    gfx->setCursor(168, 148);
    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);
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n=== ESP32-S3 Web Server ===\n");

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

  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS Mount Failed!");
    return;
  }

  Serial.printf("\nConnecting 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());
    Serial.print("MAC Address: ");
    Serial.println(WiFi.macAddress());
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    drawStatusScreen();
    updateDisplay();
  } else {
    Serial.println(" Failed! Starting AP mode...");
    WiFi.softAP("ESP32-WebServer", "password123");
    Serial.print("AP IP: ");
    Serial.println(WiFi.softAPIP());
  }

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

  server.begin();
  Serial.println("Web server started!");
}

void loop() {
  server.handleClient();

  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

data/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ESP32 Web Server</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Courier New', monospace;
      background: #0a0a0f;
      color: #e0e0e0;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    /* Scanlines */
    body::before {
      content: '';
      position: fixed;
      top: 0; left: 0; width: 100%; height: 100%;
      background: repeating-linear-gradient(
        0deg, transparent, transparent 2px,
        rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px
      );
      pointer-events: none;
      z-index: 1000;
    }
    /* Grid background */
    body::after {
      content: '';
      position: fixed;
      top: 0; left: 0; width: 100%; height: 100%;
      background-image:
        linear-gradient(rgba(0,255,255,0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0,255,255,0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      z-index: -1;
    }
    .container {
      background: rgba(10,10,20,0.9);
      border: 1px solid #0ff;
      padding: 30px;
      max-width: 400px;
      box-shadow: 0 0 30px rgba(0,255,255,0.2);
    }
    h1 {
      color: #0ff;
      text-align: center;
      font-size: 1.5rem;
      letter-spacing: 4px;
      text-shadow: 0 0 20px #0ff;
      margin-bottom: 20px;
    }
    .divider {
      height: 1px;
      background: linear-gradient(90deg, transparent, #f0f, transparent);
      margin: 15px 0;
    }
    .info-row {
      display: flex;
      justify-content: space-between;
      padding: 8px 0;
      border-bottom: 1px solid #222;
    }
    .label { color: #666; font-size: 0.8rem; }
    .value { color: #0ff; }
    .status {
      text-align: center;
      margin-top: 20px;
      color: #0f0;
      font-size: 0.8rem;
    }
    .dot {
      display: inline-block;
      width: 8px; height: 8px;
      background: #0f0;
      border-radius: 50%;
      margin-right: 8px;
      box-shadow: 0 0 10px #0f0;
      animation: pulse 2s infinite;
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>[ WEB SERVER ]</h1>
    <div class="divider"></div>
    <div class="info-row">
      <span class="label">MCU</span>
      <span class="value">ESP32-S3</span>
    </div>
    <div class="info-row">
      <span class="label">DISPLAY</span>
      <span class="value">GC9A01 240x240</span>
    </div>
    <div class="info-row">
      <span class="label">FRAMEWORK</span>
      <span class="value">Arduino + LittleFS</span>
    </div>
    <div class="divider"></div>
    <div class="status">
      <span class="dot"></span>SYSTEM ONLINE
    </div>
  </div>
</body>
</html>

SETUP INSTRUCTIONS

  • WiFi: Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD with your WiFi credentials.
  • Library: Install Arduino_GFX_Library via Arduino Library Manager.
  • Board: Select "ESP32S3 Dev Module" in Arduino IDE.
  • Upload Sketch: Click Upload to flash the .ino file.
  • Upload Data: Create a data folder next to your .ino file. Put index.html inside. Use LittleFS upload tool (Ctrl+Shift+P → "Upload LittleFS").
  • Serial Monitor: Open at 115200 baud to see IP address after connection.
  • Access: Open the IP address in your browser to see your page.

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 in your Arduino sketches directory (usually Documents/Arduino/)
  • Main Sketch: Save the code as LittleWebServer.ino - filename MUST match folder name
  • Single File Project: This project uses only one .ino file - no additional .h files required
LittleWebServer/
└── LittleWebServer.ino   // Complete sketch (copy the code above)

COMPONENTS NEEDED

ESP32-S3 Super Mini Main microcontroller AliExpress
GC9A01 Round Display 240x240 1.28" TFT 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 (Backlight) GPIO 7

REQUIRED LIBRARIES

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

Arduino_GFX_Library by moononournation - Display driver

Note: WiFi and WebServer libraries are built into ESP32 Arduino core - no install needed.

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: Add this in File → Preferences → Additional Board Manager URLs:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

TROUBLESHOOTING

  • Upload fails / Guru Meditation Error: Hold BOOT button while clicking Upload, release after "Connecting..." appears.
  • Display is white/blank: Check wiring, especially CS, DC, and RST pins. Verify 3.3V power.
  • WiFi won't connect: Double-check SSID and password. ESP32 only supports 2.4GHz networks.
  • Can't access web page: Check Serial Monitor for IP address. Ensure device is on same network.
  • Display shows wrong IP: WiFi may have assigned different IP. Check router's DHCP list.
  • Serial Monitor empty: Set baud rate to 115200. Enable "USB CDC On Boot" in board settings.

BUILD CHECKLIST

  1. □ Install Arduino IDE 3.x
  2. □ Add ESP32 board package URL and install ESP32 boards
  3. □ Install required library: Arduino_GFX_Library
  4. □ Create project folder: LittleWebServer/
  5. □ Copy sketch as LittleWebServer.ino
  6. □ Edit code: Set WiFi SSID and password
  7. □ Wire GC9A01 display to ESP32-S3
  8. □ Connect ESP32-S3 via USB-C
  9. □ Select ESP32S3 Dev Module and correct COM port
  10. □ Upload sketch (hold BOOT if needed)
  11. □ Check Serial Monitor for IP address
  12. □ Open IP address in web browser - Done!