ALIVE LCD CAPTIVE PORTAL

Web-Configurable Server Monitor - Round GC9A01 Display

← BASIC VERSION (Hardcoded Config)

CAPTIVE PORTAL FEATURES

>
Web Configuration Configure WiFi, servers, and settings via browser
>
WiFi Network Scan Automatically discovers available networks
>
Persistent Storage Settings saved to flash - survives reboot
>
Auto Config Mode Enters setup on first boot or BOOT button hold

SETUP WORKFLOW

1
Power on ESP32
(hold BOOT for config)
→
2
Connect to
"ALIVE-LCD-Setup" WiFi
→
3
Browser opens
config page
→
4
Save settings
ESP32 reboots

ARDUINO SKETCH

AliveLCD_Portal.ino
/*
 * Server Status Dashboard - "ALIVE" (LCD + Captive Portal)
 * ESP32-S3 Super Mini + GC9A01 1.28" Round LCD + Status LEDs
 *
 * Features:
 * - Web-based configuration (no hardcoded credentials)
 * - WiFi network scanning
 * - Configurable servers (up to 5), check interval, display settings
 * - Settings persist in flash memory
 *
 * Config Mode Triggers:
 * - First boot (no saved WiFi)
 * - Hold BOOT button during power-on
 *
 * Hardware:
 * - GC9A01 240x240 Round LCD (SPI): SCK=12, MOSI=11, CS=8, DC=9, RST=10, BL=7
 * - Status LEDs: Blue=GPIO 5, Red=GPIO 10
 */

#include <WiFi.h>
#include <ESP32Ping.h>
#include <Arduino_GFX_Library.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>

// ============== PIN DEFINITIONS ==============
// GC9A01 Display (SPI)
#define TFT_SCK   12
#define TFT_MOSI  11
#define TFT_CS    8
#define TFT_DC    9
#define TFT_RST   10
#define TFT_BL    7

// Status LEDs
#define LED_BLUE  5
#define LED_RED   10
#define BOOT_BTN  0    // GPIO0 is BOOT button

// ============== DISPLAY SETTINGS ==============
#define SCREEN_WIDTH  240
#define SCREEN_HEIGHT 240
#define CX (SCREEN_WIDTH / 2)
#define CY (SCREEN_HEIGHT / 2)

// Colors (RGB565)
#define COLOR_BG       0x0821  // Dark blue-black
#define COLOR_TEXT     0xB5B6  // Light gray
#define COLOR_CYAN     0x07FF  // Neon Cyan
#define COLOR_MAGENTA  0xF81F  // Neon Magenta
#define COLOR_YELLOW   0xFFE0  // Neon Yellow
#define COLOR_GREEN    0x07E0  // Neon Green
#define COLOR_RED      0xF800  // Red

// ============== CONFIGURATION STRUCTURE ==============
struct Config {
  char ssid[33];
  char password[65];
  char servers[5][64];
  char serverNames[5][16];
  int numServers;
  unsigned long checkInterval;
  uint8_t brightness;
};

Config config;
Preferences prefs;

// Display setup using Arduino_GFX
Arduino_DataBus *bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI, GFX_NOT_DEFINED, HSPI);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RST, 0, true);

WebServer server(80);
DNSServer dnsServer;

// ============== STATE VARIABLES ==============
bool configMode = false;
bool serverStatus[5];
bool allServersUp = true;
unsigned long lastCheck = 0;
unsigned long lastBlink = 0;
unsigned long lastFlashUpdate = 0;
bool redLedState = false;
bool flashState = true;

// ============== DEFAULT CONFIGURATION ==============
void setDefaults() {
  memset(&config, 0, sizeof(config));
  config.numServers = 3;
  config.checkInterval = 30000;
  config.brightness = 255;
  strcpy(config.servers[0], "8.8.8.8");
  strcpy(config.serverNames[0], "Google");
  strcpy(config.servers[1], "1.1.1.1");
  strcpy(config.serverNames[1], "Cloudflare");
  strcpy(config.servers[2], "192.168.1.1");
  strcpy(config.serverNames[2], "Router");
}

// ============== LOAD/SAVE CONFIG ==============
void loadConfig() {
  prefs.begin("alivelcd", true);

  if (!prefs.isKey("ssid")) {
    prefs.end();
    setDefaults();
    return;
  }

  strcpy(config.ssid, prefs.getString("ssid", "").c_str());
  strcpy(config.password, prefs.getString("pass", "").c_str());
  config.numServers = prefs.getInt("numSrv", 3);
  config.checkInterval = prefs.getULong("interval", 30000);
  config.brightness = prefs.getUChar("bright", 255);

  for (int i = 0; i < 5; i++) {
    String srvKey = "srv" + String(i);
    String nameKey = "name" + String(i);
    strcpy(config.servers[i], prefs.getString(srvKey.c_str(), "").c_str());
    strcpy(config.serverNames[i], prefs.getString(nameKey.c_str(), "").c_str());
  }

  prefs.end();
}

void saveConfig() {
  prefs.begin("alivelcd", false);
  prefs.putString("ssid", config.ssid);
  prefs.putString("pass", config.password);
  prefs.putInt("numSrv", config.numServers);
  prefs.putULong("interval", config.checkInterval);
  prefs.putUChar("bright", config.brightness);

  for (int i = 0; i < 5; i++) {
    String srvKey = "srv" + String(i);
    String nameKey = "name" + String(i);
    prefs.putString(srvKey.c_str(), config.servers[i]);
    prefs.putString(nameKey.c_str(), config.serverNames[i]);
  }

  prefs.end();
}

// ============== DRAW HELPERS ==============
void drawGlowRing(int cx, int cy, int r, uint16_t color) {
  gfx->drawCircle(cx, cy, r, color);
  gfx->drawCircle(cx, cy, r - 1, color);
}

void drawCenteredText(const char* text, int y, uint16_t color, uint8_t size) {
  gfx->setTextSize(size);
  gfx->setTextColor(color);
  int16_t x1, y1;
  uint16_t w, h;
  gfx->getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
  gfx->setCursor(CX - w/2, y);
  gfx->print(text);
}

// ============== CONFIG PAGE HTML ==============
const char* configPage = R"rawhtml(
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>ALIVE LCD Setup</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:'Courier New',monospace;background:#0a0a1a;color:#b0b0b0;padding:20px}
    h1{color:#ff00ff;text-align:center;margin-bottom:20px;text-shadow:0 0 10px #ff00ff}
    .card{background:#12121f;border:1px solid #333;border-radius:8px;padding:15px;margin-bottom:15px}
    .card h2{color:#00ffff;font-size:0.9rem;margin-bottom:10px;letter-spacing:2px}
    label{display:block;color:#888;font-size:0.75rem;margin-bottom:3px}
    input,select{width:100%;padding:8px;background:#1a1a2e;border:1px solid #00ffff;color:#fff;border-radius:4px;margin-bottom:10px;font-family:inherit}
    input:focus{outline:none;box-shadow:0 0 5px #00ffff}
    .row{display:flex;gap:10px}
    .row>*{flex:1}
    button{width:100%;padding:12px;background:#ff00ff;border:none;color:#000;font-weight:bold;cursor:pointer;border-radius:4px;font-family:inherit;letter-spacing:2px;margin-top:10px}
    button:hover{box-shadow:0 0 15px #ff00ff}
    .scan-btn{background:transparent;border:1px solid #00ffff;color:#00ffff;padding:8px;margin-bottom:10px}
    .scan-btn:hover{background:#00ffff;color:#000}
    #networks{max-height:150px;overflow-y:auto;border:1px solid #333;border-radius:4px;margin-bottom:10px}
    .net{padding:8px;cursor:pointer;border-bottom:1px solid #222}
    .net:hover{background:#1a1a2e}
    .net .rssi{float:right;color:#666}
  </style>
</head>
<body>
  <h1>[ALIVE LCD SETUP]</h1>
  <form action="/save" method="POST">
    <div class="card">
      <h2>WiFi NETWORK</h2>
      <button type="button" class="scan-btn" onclick="scanNetworks()">SCAN NETWORKS</button>
      <div id="networks"></div>
      <label>SSID</label>
      <input type="text" name="ssid" id="ssid" maxlength="32" required>
      <label>PASSWORD</label>
      <input type="password" name="pass" maxlength="64">
    </div>

    <div class="card">
      <h2>SERVERS TO MONITOR</h2>
      <div class="row">
        <div><label>Server 1 IP/Host</label><input type="text" name="srv0" maxlength="63"></div>
        <div><label>Name</label><input type="text" name="name0" maxlength="15"></div>
      </div>
      <div class="row">
        <div><label>Server 2 IP/Host</label><input type="text" name="srv1" maxlength="63"></div>
        <div><label>Name</label><input type="text" name="name1" maxlength="15"></div>
      </div>
      <div class="row">
        <div><label>Server 3 IP/Host</label><input type="text" name="srv2" maxlength="63"></div>
        <div><label>Name</label><input type="text" name="name2" maxlength="15"></div>
      </div>
      <div class="row">
        <div><label>Server 4 IP/Host</label><input type="text" name="srv3" maxlength="63"></div>
        <div><label>Name</label><input type="text" name="name3" maxlength="15"></div>
      </div>
      <div class="row">
        <div><label>Server 5 IP/Host</label><input type="text" name="srv4" maxlength="63"></div>
        <div><label>Name</label><input type="text" name="name4" maxlength="15"></div>
      </div>
    </div>

    <div class="card">
      <h2>DISPLAY SETTINGS</h2>
      <div class="row">
        <div>
          <label>Check Interval (sec)</label>
          <input type="number" name="interval" value="30" min="10" max="300">
        </div>
        <div>
          <label>Brightness (0-255)</label>
          <input type="number" name="bright" value="255" min="0" max="255">
        </div>
      </div>
    </div>

    <button type="submit">SAVE & REBOOT</button>
  </form>

  <script>
    function scanNetworks() {
      var box = document.getElementById('networks');
      box.innerHTML = '<div style="color:#00ff00">Scanning...</div>';
      var xhr = new XMLHttpRequest();
      xhr.open('GET', '/scan', true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            try {
              var nets = JSON.parse(xhr.responseText);
              if (!nets || nets.length === 0) {
                box.innerHTML = '<div style="color:#888">No networks found</div>';
                return;
              }
              var html = '';
              for (var i = 0; i < nets.length; i++) {
                var n = nets[i];
                html += '<div class="net" onclick="document.getElementById(\'ssid\').value=\'' + n.ssid.replace(/'/g, '') + '\'">' + n.ssid + '<span class="rssi">' + n.rssi + ' dBm</span></div>';
              }
              box.innerHTML = html;
            } catch (e) {
              box.innerHTML = '<div style="color:#f00">Parse error: ' + e.message + '</div>';
            }
          } else {
            box.innerHTML = '<div style="color:#f00">HTTP error: ' + xhr.status + '</div>';
          }
        }
      };
      xhr.onerror = function() {
        box.innerHTML = '<div style="color:#f00">Network error</div>';
      };
      xhr.send();
    }
  </script>
</body>
</html>
)rawhtml";

// ============== WEB SERVER HANDLERS ==============
void handleRoot() {
  server.send(200, "text/html", configPage);
}

String escapeJson(String s) {
  s.replace("\\", "\\\\");
  s.replace("\"", "\\\"");
  return s;
}

void handleScan() {
  Serial.println("Scanning WiFi networks...");
  int n = WiFi.scanNetworks();
  Serial.printf("Found %d networks\n", n);

  if (n < 0) {
    server.send(500, "application/json", "[]");
    return;
  }

  String json = "[";
  for (int i = 0; i < n; i++) {
    if (i > 0) json += ",";
    json += "{\"ssid\":\"" + escapeJson(WiFi.SSID(i)) + "\",\"rssi\":" + WiFi.RSSI(i) + "}";
  }
  json += "]";
  server.send(200, "application/json", json);
  WiFi.scanDelete();
}

void handleSave() {
  strcpy(config.ssid, server.arg("ssid").c_str());
  strcpy(config.password, server.arg("pass").c_str());

  config.numServers = 0;
  for (int i = 0; i < 5; i++) {
    String srv = server.arg("srv" + String(i));
    String name = server.arg("name" + String(i));
    if (srv.length() > 0) {
      strcpy(config.servers[i], srv.c_str());
      strcpy(config.serverNames[i], name.length() > 0 ? name.c_str() : srv.c_str());
      config.numServers++;
    }
  }

  config.checkInterval = server.arg("interval").toInt() * 1000;
  config.brightness = server.arg("bright").toInt();

  if (config.numServers == 0) config.numServers = 1;
  if (config.checkInterval < 10000) config.checkInterval = 30000;

  saveConfig();

  server.send(200, "text/html", "<h1 style='color:#0f0;text-align:center;font-family:monospace'>SAVED! Rebooting...</h1>");
  delay(1000);
  ESP.restart();
}

// ============== CONFIG MODE DISPLAY ==============
void drawConfigScreen() {
  gfx->fillScreen(COLOR_BG);
  drawGlowRing(CX, CY, 115, COLOR_MAGENTA);

  // Header
  drawCenteredText("CONFIG", 30, COLOR_MAGENTA, 2);
  drawCenteredText("MODE", 55, COLOR_MAGENTA, 2);

  // Instructions
  gfx->setTextSize(1);
  gfx->setTextColor(COLOR_TEXT);
  drawCenteredText("Connect WiFi to:", 100, COLOR_TEXT, 1);
  drawCenteredText("ALIVE-LCD-Setup", 125, COLOR_CYAN, 1);

  drawCenteredText("Then open:", 160, COLOR_TEXT, 1);
  drawCenteredText("192.168.4.1", 185, COLOR_YELLOW, 1);
}

// ============== START CONFIG MODE ==============
void startConfigMode() {
  configMode = true;
  Serial.println("Starting Config Mode...");

  drawConfigScreen();

  // Start AP
  WiFi.mode(WIFI_AP);
  WiFi.softAP("ALIVE-LCD-Setup");

  // Start DNS (captive portal)
  dnsServer.start(53, "*", WiFi.softAPIP());

  // Start web server
  server.on("/", handleRoot);
  server.on("/scan", handleScan);
  server.on("/save", HTTP_POST, handleSave);
  server.onNotFound(handleRoot);  // Redirect all to config
  server.begin();

  Serial.println("AP IP: 192.168.4.1");
}

// ============== CONNECT WiFi ==============
bool connectWiFi() {
  if (strlen(config.ssid) == 0) return false;

  gfx->fillScreen(COLOR_BG);
  drawGlowRing(CX, CY, 115, COLOR_YELLOW);
  drawCenteredText("WIFI", CY - 40, COLOR_YELLOW, 2);
  drawCenteredText("Connecting...", CY - 10, COLOR_TEXT, 1);
  drawCenteredText(config.ssid, CY + 10, COLOR_CYAN, 1);

  // Progress bar
  int barX = 40, barY = CY + 35, barW = 160, barH = 12;
  gfx->drawRect(barX, barY, barW, barH, COLOR_CYAN);

  WiFi.mode(WIFI_STA);
  WiFi.begin(config.ssid, config.password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    attempts++;
    int progress = (attempts * 100) / 30;
    int fillW = (barW - 4) * progress / 100;
    gfx->fillRect(barX + 2, barY + 2, fillW, barH - 4, COLOR_CYAN);
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("WiFi connected: " + WiFi.localIP().toString());

    gfx->fillScreen(COLOR_BG);
    drawGlowRing(CX, CY, 115, COLOR_CYAN);
    drawCenteredText("CONNECTED", CY - 20, COLOR_CYAN, 2);
    drawCenteredText(WiFi.localIP().toString().c_str(), CY + 15, COLOR_GREEN, 1);
    delay(1000);
    return true;
  }

  Serial.println("WiFi connection failed!");
  return false;
}

// ============== STARTUP SCREEN ==============
void drawStartupScreen() {
  gfx->fillScreen(COLOR_BG);
  drawGlowRing(CX, CY, 115, COLOR_CYAN);
  drawCenteredText("ALIVE", 35, COLOR_CYAN, 3);
  drawCenteredText("SERVER MONITOR", 195, COLOR_TEXT, 1);

  // Triangle animation
  int triR = 45;
  for (int frame = 0; frame < 20; frame++) {
    float angle = frame * 0.3;
    for (int p = 0; p < 3; p++) {
      float a1 = angle + (p * 2.094);
      float a2 = angle + ((p + 1) * 2.094);
      int x1 = CX + cos(a1) * triR;
      int y1 = CY + sin(a1) * triR;
      int x2 = CX + cos(a2) * triR;
      int y2 = CY + sin(a2) * triR;
      gfx->drawLine(x1, y1, x2, y2, COLOR_CYAN);
    }
    delay(40);
    for (int p = 0; p < 3; p++) {
      float a1 = angle + (p * 2.094);
      float a2 = angle + ((p + 1) * 2.094);
      int x1 = CX + cos(a1) * triR;
      int y1 = CY + sin(a1) * triR;
      int x2 = CX + cos(a2) * triR;
      int y2 = CY + sin(a2) * triR;
      gfx->drawLine(x1, y1, x2, y2, COLOR_BG);
    }
  }
}

// ============== SETUP ==============
void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.println("\n=== ALIVE LCD - Captive Portal Version ===");

  // Init LEDs
  pinMode(LED_BLUE, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(BOOT_BTN, INPUT_PULLUP);
  digitalWrite(LED_BLUE, HIGH);  // Blue on during boot

  // Init backlight
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  // Init display
  gfx->begin();
  gfx->setRotation(0);
  gfx->fillScreen(COLOR_BG);

  // Load saved config
  loadConfig();

  // Apply brightness
  analogWrite(TFT_BL, config.brightness);

  // Check if BOOT button held (force config mode)
  bool forceConfig = (digitalRead(BOOT_BTN) == LOW);
  delay(100);  // Debounce
  forceConfig = forceConfig && (digitalRead(BOOT_BTN) == LOW);

  // Enter config if no WiFi saved OR button held
  if (forceConfig || strlen(config.ssid) == 0) {
    startConfigMode();
    return;
  }

  // Show startup animation
  drawStartupScreen();

  // Try to connect to saved WiFi
  if (!connectWiFi()) {
    startConfigMode();
    return;
  }

  // Initial server check
  checkAllServers();
  updateDisplay();
  lastCheck = millis();
  lastFlashUpdate = millis();
}

// ============== CHECK ALL SERVERS ==============
void checkAllServers() {
  Serial.println("\n--- Checking servers ---");
  allServersUp = true;

  for (int i = 0; i < config.numServers; i++) {
    Serial.print("Pinging ");
    Serial.print(config.servers[i]);
    Serial.print("... ");

    bool success = Ping.ping(config.servers[i], 2);
    serverStatus[i] = success;

    if (success) {
      Serial.println("OK");
    } else {
      Serial.println("FAILED");
      allServersUp = false;
    }
  }
}

// ============== UPDATE DISPLAY ==============
void updateDisplay() {
  gfx->fillScreen(COLOR_BG);

  // Draw outer status ring
  uint16_t ringColor = allServersUp ? COLOR_CYAN : COLOR_MAGENTA;
  drawGlowRing(CX, CY, 115, ringColor);

  // Draw header
  drawCenteredText("ALIVE", 28, COLOR_CYAN, 2);

  // Draw server status list
  int startY = 50;
  int lineHeight = 26;
  int maxVisible = min(config.numServers, 6);

  gfx->setTextSize(1);

  for (int i = 0; i < maxVisible; i++) {
    int yPos = startY + (i * lineHeight);

    // Status indicator circle
    if (serverStatus[i]) {
      gfx->fillCircle(45, yPos + 6, 6, COLOR_CYAN);
    } else {
      gfx->fillCircle(45, yPos + 6, 6, COLOR_MAGENTA);
      gfx->drawLine(42, yPos + 3, 48, yPos + 9, COLOR_BG);
      gfx->drawLine(48, yPos + 3, 42, yPos + 9, COLOR_BG);
    }

    // Server name
    gfx->setTextColor(COLOR_TEXT);
    gfx->setCursor(58, yPos + 3);
    String name = String(config.serverNames[i]);
    if (name.length() > 12) {
      name = name.substring(0, 11) + ".";
    }
    gfx->print(name);

    // Status text
    gfx->setCursor(170, yPos + 3);
    if (serverStatus[i]) {
      gfx->setTextColor(COLOR_GREEN);
      gfx->print("OK");
    } else {
      gfx->setTextColor(COLOR_RED);
      gfx->print("DOWN");
    }
  }
}

// ============== FLASH DOWN TEXT ==============
void flashDownText(bool showText) {
  int startY = 50;
  int lineHeight = 26;
  int maxVisible = min(config.numServers, 6);

  gfx->setTextSize(1);

  for (int i = 0; i < maxVisible; i++) {
    if (!serverStatus[i]) {
      int yPos = startY + (i * lineHeight);
      gfx->fillRect(170, yPos + 1, 30, 10, COLOR_BG);
      if (showText) {
        gfx->setTextColor(COLOR_RED);
        gfx->setCursor(170, yPos + 3);
        gfx->print("DOWN");
      }
    }
  }
}

// ============== UPDATE LEDs ==============
void updateLEDs() {
  digitalWrite(LED_BLUE, HIGH);  // Blue always on

  if (allServersUp) {
    digitalWrite(LED_RED, LOW);
    redLedState = false;
  } else {
    if (millis() - lastBlink >= 500) {
      redLedState = !redLedState;
      digitalWrite(LED_RED, redLedState ? HIGH : LOW);
      lastBlink = millis();
    }
  }
}

// ============== MAIN LOOP ==============
void loop() {
  if (configMode) {
    dnsServer.processNextRequest();
    server.handleClient();
    return;
  }

  // Maintain WiFi connection
  if (WiFi.status() != WL_CONNECTED) {
    if (!connectWiFi()) {
      startConfigMode();
      return;
    }
  }

  // Periodic server check
  if (millis() - lastCheck >= config.checkInterval) {
    checkAllServers();
    updateDisplay();
    lastCheck = millis();
    lastFlashUpdate = millis();
    flashState = true;
  }

  // Flash DOWN text
  if (!allServersUp && millis() - lastFlashUpdate >= 300) {
    flashState = !flashState;
    flashDownText(flashState);
    lastFlashUpdate = millis();
  }

  // Update LEDs
  updateLEDs();

  delay(50);
}

SETUP INSTRUCTIONS

  • First Boot: ESP32 automatically enters config mode (no WiFi saved yet).
  • Force Config Mode: Hold BOOT button while powering on to enter setup anytime.
  • Connect: Join the "ALIVE-LCD-Setup" WiFi network from your phone/laptop.
  • Configure: Browser should auto-open config page, or go to 192.168.4.1
  • Scan Networks: Click "SCAN NETWORKS" to find available WiFi networks.
  • Save: Fill in settings and click "SAVE & REBOOT" - ESP32 restarts with new config.

REQUIRED LIBRARIES

  • Arduino_GFX_Library by moononournation - Display driver
  • ESP32Ping by Matteo Dainese - ICMP ping

Note: WebServer, DNSServer, and Preferences are built into ESP32 Arduino core - no install needed.

HARDWARE PINOUT

GC9A01 240x240 Round LCD (SPI):

  • SCK: GPIO 12
  • MOSI: GPIO 11
  • CS: GPIO 8
  • DC: GPIO 9
  • RST: GPIO 10
  • BL: GPIO 7

Status LEDs:

  • Blue LED: GPIO 5 (Always ON - power indicator)
  • Red LED: GPIO 10 (Flashing when server down)

DIFFERENCES FROM BASIC VERSION

  • No hardcoded credentials - WiFi and servers configured via web interface
  • Persistent storage - Settings saved to ESP32 flash memory (NVS)
  • Captive portal - Auto-redirects to config page when in AP mode
  • Network scanning - Discovers available WiFi networks
  • Configurable settings - Check interval, brightness, up to 5 servers
  • Arduino_GFX Library - Uses Arduino_GFX instead of Adafruit libraries

ARDUINO PROJECT SETUP

  • Create Project Folder: Create a folder named ALIVE_Portal in your Arduino sketches directory (usually Documents/Arduino/)
  • Main Sketch: Save the code as ALIVE_Portal.ino - filename MUST match folder name
  • Single File Project: This project uses only one .ino file - no additional .h files required
ALIVE_Portal/
└── ALIVE_Portal.ino   // Complete sketch (copy the code above)

COMPONENTS NEEDED

ESP32-S3 Super Mini Main microcontroller AliExpress
GC9A01 Round Display 240x240 1.28" TFT Amazon
Blue LED (5mm) Status indicator - always ON Amazon
Red LED (5mm) Alert indicator - flashes on server down Amazon
330Ω Resistors (x2) Current limiting for LEDs 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
Status LEDs ESP32-S3
Blue LED (+) → 330Ω GPIO 5
Red LED (+) → 330Ω GPIO 10
Both LED (-) GND

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.
  • Can't find "ALIVE-LCD-Setup" network: Wait 10 seconds after power on. Hold BOOT to force config mode.
  • Captive portal doesn't auto-open: Manually navigate to 192.168.4.1 in your browser.
  • WiFi won't connect after config: Double-check SSID and password. ESP32 only supports 2.4GHz networks.
  • Settings not saved: Make sure you click "SAVE & REBOOT" button. Check Serial Monitor for errors.
  • All servers show DOWN: Verify server IPs are correct. Check if firewall blocks ICMP ping.
  • LEDs not working: Check LED polarity (long leg = positive). Verify 330Ω resistor is connected.
  • 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 libraries: Arduino_GFX_Library, ESP32Ping
  4. □ Create project folder: ALIVE_Portal/
  5. □ Copy sketch as ALIVE_Portal.ino
  6. □ Wire GC9A01 display to ESP32-S3
  7. □ Wire status LEDs with 330Ω resistors
  8. □ Connect ESP32-S3 via USB-C
  9. □ Select ESP32S3 Dev Module and correct COM port
  10. □ Upload sketch (hold BOOT if needed)
  11. □ Connect to "ALIVE-LCD-Setup" WiFi network
  12. □ Open 192.168.4.1 in browser
  13. □ Scan and select your WiFi network
  14. □ Add server IPs and names
  15. □ Click "SAVE & REBOOT" - Done!