CYD SERVER CAPTIVE PORTAL
Web-Configurable CYD Server - No Hardcoded Credentials
CAPTIVE PORTAL FEATURES
SETUP WORKFLOW
(hold BOOT for config)
"CYD-Server-Setup" WiFi
config page
CYD reboots
ARDUINO PROJECT SETUP
Create this folder structure for your project:
CYD_Server_Portal/
└── CYD_Server_Portal.ino <-- Main sketch (copy from below)
SD Card (FAT32):
├── index.html <-- Your website homepage
├── style.css <-- Stylesheet
└── images/ <-- Optional image folder
Note: Settings are stored in ESP32 flash (Preferences). Web files are served from SD card.
COMPONENTS NEEDED
- ESP32-2432S028R (CYD) - Cheap Yellow Display board with built-in 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. No external wiring required!
REQUIRED LIBRARIES
Install via Arduino Library Manager:
Arduino_GFX_Library- Display driver for ST7796SimpleFTPServer- FTP server for wireless file upload
Note: WiFi, WebServer, DNSServer, Preferences, SD, and SPI are built into ESP32 core.
SimpleFTPServer Config: Edit FtpServerKey.h:
- Set
STORAGE_TYPEtoSTORAGE_SD - Set
STORAGE_SD_FORCE_DISABLE_SEPORATECLASStotrue
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 ESP32-WROOM-32E, not ESP32-S3. Select "ESP32 Dev Module".
TROUBLESHOOTING
- Can't enter config mode: Hold BOOT button (GPIO 0) while powering on.
- Display stays white/black: Verify ST7796 driver and pin configuration.
- SD Card not mounting: Must be FAT32 formatted. Try cards 32GB or smaller.
- WiFi won't connect: Re-enter config mode and verify SSID/password.
- Upload fails: Hold BOOT button during upload on some CYD boards.
- FTP connection refused: Check SimpleFTPServer library config.
- Config page not loading: Try http://192.168.4.1 directly if captive portal fails.
ARDUINO SKETCH
/* * CYD Web Server - Captive Portal Version * ESP32-WROOM-32E + ST7796 3.5" TFT (320x480) + SD Card * * Features: * - Web-based configuration (no hardcoded credentials) * - WiFi network scanning * - Configurable web server port, display brightness * - Touch calibration settings * - Settings persist in flash memory * * Config Mode Triggers: * - First boot (no saved WiFi) * - Hold BOOT button during power-on */ #include <WiFi.h> #include <WebServer.h> #include <DNSServer.h> #include <Preferences.h> #include <SD.h> #include <SPI.h> #include <SimpleFTPServer.h> #include <Arduino_GFX_Library.h> #include <time.h> // ============== PIN DEFINITIONS (CYD Board) ============== // Display pins (ST7796 - HSPI 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 // SD Card (built-in slot, shares HSPI) #define SD_CS 5 // Touch pins (XPT2046) #define TOUCH_MISO 39 #define TOUCH_MOSI 32 #define TOUCH_CLK 25 #define TOUCH_CS 33 #define TOUCH_IRQ 36 // RGB LED pins #define LED_RED 4 #define LED_GREEN 16 #define LED_BLUE 17 // Boot button #define BOOT_BTN 0 // ============== DISPLAY DIMENSIONS ============== #define SCREEN_WIDTH 320 #define SCREEN_HEIGHT 480 // ============== 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 DARKYELLOW 0x8400 // ============== CONFIGURATION STRUCTURE ============== struct Config { char ssid[33]; char password[65]; char ftp_user[17]; char ftp_pass[17]; int webPort; int brightness; long gmtOffset; int dstOffset; }; Config config; Preferences prefs; // ============== OBJECTS ============== // Hardware SPI for display (HSPI bus) Arduino_DataBus *bus = new Arduino_ESP32SPI( TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI, TFT_MISO, HSPI, 10000000 ); Arduino_GFX *gfx = new Arduino_ST7796(bus, TFT_RST, 0, false); WebServer *server = nullptr; DNSServer dnsServer; FtpServer ftpSrv; // ============== STATE VARIABLES ============== bool configMode = false; bool sdCardMounted = false; unsigned long requestCount = 0; unsigned long lastDisplayUpdate = 0; int lastDisplayedHour = -1; int lastDisplayedMin = -1; // ============== DEFAULT CONFIGURATION ============== void setDefaults() { memset(&config, 0, sizeof(config)); strcpy(config.ftp_user, "admin"); strcpy(config.ftp_pass, "admin"); config.webPort = 80; config.brightness = 100; // 0-255 config.gmtOffset = -21600; // CST = UTC-6 config.dstOffset = 3600; // DST offset } // ============== LOAD/SAVE CONFIG ============== void loadConfig() { prefs.begin("cyd", 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()); strcpy(config.ftp_user, prefs.getString("ftpuser", "admin").c_str()); strcpy(config.ftp_pass, prefs.getString("ftppass", "admin").c_str()); config.webPort = prefs.getInt("port", 80); config.brightness = prefs.getInt("bright", 100); config.gmtOffset = prefs.getLong("gmt", -21600); config.dstOffset = prefs.getInt("dst", 3600); prefs.end(); } void saveConfig() { prefs.begin("cyd", false); prefs.putString("ssid", config.ssid); prefs.putString("pass", config.password); prefs.putString("ftpuser", config.ftp_user); prefs.putString("ftppass", config.ftp_pass); prefs.putInt("port", config.webPort); prefs.putInt("bright", config.brightness); prefs.putLong("gmt", config.gmtOffset); prefs.putInt("dst", config.dstOffset); prefs.end(); } // ============== CONFIG PAGE HTML ============== const char* configPage = R"rawhtml( <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>CYD Server 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:#ffe000;text-align:center;margin-bottom:20px;text-shadow:0 0 10px #ffe000} .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 #ffe000;color:#fff;border-radius:4px;margin-bottom:10px;font-family:inherit} input:focus{outline:none;box-shadow:0 0 5px #ffe000} .row{display:flex;gap:10px} .row>*{flex:1} button{width:100%;padding:12px;background:#ffe000;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 #ffe000} .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} .slider-container{display:flex;align-items:center;gap:10px;margin-bottom:10px} .slider-container input[type=range]{flex:1} .slider-val{color:#ffe000;min-width:40px;text-align:right} </style> </head> <body> <h1>[CYD SERVER 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>FTP SERVER</h2> <div class="row"> <div><label>Username</label><input type="text" name="ftpuser" value="admin" maxlength="16"></div> <div><label>Password</label><input type="text" name="ftppass" value="admin" maxlength="16"></div> </div> </div> <div class="card"> <h2>WEB SERVER</h2> <label>HTTP Port</label> <input type="number" name="port" value="80" min="1" max="65535"> </div> <div class="card"> <h2>DISPLAY SETTINGS</h2> <label>Backlight Brightness</label> <div class="slider-container"> <input type="range" name="bright" min="10" max="255" value="100" oninput="document.getElementById('brightVal').textContent=this.value"> <span class="slider-val" id="brightVal">100</span> </div> </div> <div class="card"> <h2>TIMEZONE</h2> <label>GMT Offset (seconds)</label> <select name="gmt"> <option value="-36000">UTC-10 (Hawaii)</option> <option value="-32400">UTC-9 (Alaska)</option> <option value="-28800">UTC-8 (Pacific)</option> <option value="-25200">UTC-7 (Mountain)</option> <option value="-21600" selected>UTC-6 (Central)</option> <option value="-18000">UTC-5 (Eastern)</option> <option value="0">UTC+0 (London)</option> <option value="3600">UTC+1 (Paris)</option> <option value="7200">UTC+2 (Cairo)</option> <option value="10800">UTC+3 (Moscow)</option> <option value="28800">UTC+8 (Singapore)</option> <option value="32400">UTC+9 (Tokyo)</option> <option value="36000">UTC+10 (Sydney)</option> </select> <div class="row"> <div> <label>DST Offset (seconds)</label> <input type="number" name="dst" value="3600" min="0" max="7200"> </div> </div> </div> <button type="submit">SAVE & REBOOT</button> </form> <script> function scanNetworks() { var box = document.getElementById('networks'); box.innerHTML = '<div style="color:#ffe000">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(); // Free memory } void handleSave() { strcpy(config.ssid, server->arg("ssid").c_str()); strcpy(config.password, server->arg("pass").c_str()); strcpy(config.ftp_user, server->arg("ftpuser").c_str()); strcpy(config.ftp_pass, server->arg("ftppass").c_str()); config.webPort = server->arg("port").toInt(); config.brightness = server->arg("bright").toInt(); config.gmtOffset = server->arg("gmt").toInt(); config.dstOffset = server->arg("dst").toInt(); if (config.webPort < 1) config.webPort = 80; if (config.brightness < 10) config.brightness = 100; saveConfig(); server->send(200, "text/html", "<h1 style='color:#0f0;text-align:center;font-family:monospace'>SAVED! Rebooting...</h1>"); delay(1000); ESP.restart(); } // ============== 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); } // ============== CONFIG MODE DISPLAY ============== void drawConfigScreen() { gfx->fillScreen(BLACK); // Border frame gfx->drawRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, DARKYELLOW); gfx->drawRect(10, 10, SCREEN_WIDTH - 20, SCREEN_HEIGHT - 20, YELLOW); gfx->drawRect(15, 15, SCREEN_WIDTH - 30, SCREEN_HEIGHT - 30, DARKYELLOW); // Title bar gfx->fillRect(20, 30, SCREEN_WIDTH - 40, 50, DARKYELLOW); gfx->drawRect(20, 30, SCREEN_WIDTH - 40, 50, YELLOW); gfx->setTextColor(BLACK); gfx->setTextSize(3); gfx->setCursor(45, 42); gfx->print("CONFIG MODE"); drawHorizontalDivider(100, YELLOW); // Instructions gfx->setTextColor(WHITE); gfx->setTextSize(2); gfx->setCursor(50, 130); gfx->print("Connect WiFi to:"); // AP Name box drawCornerBrackets(30, 160, SCREEN_WIDTH - 60, 60, CYAN); gfx->setTextColor(CYAN); gfx->setTextSize(2); gfx->setCursor(45, 180); gfx->print("CYD-Server-Setup"); drawHorizontalDivider(240, DARKGRAY); // Then open browser gfx->setTextColor(WHITE); gfx->setTextSize(2); gfx->setCursor(45, 270); gfx->print("Then open browser:"); // IP box drawCornerBrackets(30, 300, SCREEN_WIDTH - 60, 60, YELLOW); gfx->setTextColor(YELLOW); gfx->setTextSize(3); gfx->setCursor(55, 318); gfx->print("192.168.4.1"); drawHorizontalDivider(380, DARKGRAY); // Footer info gfx->setTextColor(DARKGRAY); gfx->setTextSize(1); gfx->setCursor(70, 400); gfx->print("Hold BOOT button during power-on"); gfx->setCursor(85, 415); gfx->print("to return to config mode"); gfx->setTextColor(YELLOW); gfx->setCursor(90, 445); gfx->print("CYD Server // Captive Portal"); } // ============== START CONFIG MODE ============== void startConfigMode() { configMode = true; Serial.println("Starting Config Mode..."); // Set LED to yellow (red+green) digitalWrite(LED_RED, HIGH); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, LOW); // Show config screen drawConfigScreen(); // Start AP + STA mode (allows WiFi scanning while AP is active) WiFi.mode(WIFI_AP_STA); WiFi.softAP("CYD-Server-Setup"); // Start DNS (captive portal) dnsServer.start(53, "*", WiFi.softAPIP()); // Create config web server on port 80 server = new WebServer(80); 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(BLACK); gfx->drawRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, YELLOW); gfx->fillRect(20, 30, SCREEN_WIDTH - 40, 50, DARKYELLOW); gfx->setTextColor(BLACK); gfx->setTextSize(3); gfx->setCursor(35, 42); gfx->print("CONNECTING..."); gfx->setTextColor(WHITE); gfx->setTextSize(2); gfx->setCursor(30, 120); gfx->print("SSID: "); gfx->setTextColor(CYAN); gfx->print(config.ssid); WiFi.mode(WIFI_STA); WiFi.begin(config.ssid, config.password); // Progress bar int barX = 40, barY = 200, barW = SCREEN_WIDTH - 80, barH = 30; gfx->drawRect(barX, barY, barW, barH, YELLOW); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); int fillW = ((barW - 4) * attempts) / 30; gfx->fillRect(barX + 2, barY + 2, fillW, barH - 4, YELLOW); digitalWrite(LED_BLUE, attempts % 2); attempts++; } if (WiFi.status() == WL_CONNECTED) { gfx->fillRect(barX + 2, barY + 2, barW - 4, barH - 4, GREEN); gfx->setTextColor(GREEN); gfx->setTextSize(2); gfx->setCursor(100, 260); gfx->print("CONNECTED!"); gfx->setTextColor(CYAN); gfx->setCursor(70, 300); gfx->print(WiFi.localIP()); delay(1500); return true; } return 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; } // ============== STATUS SCREEN ============== void drawStatusScreen() { gfx->fillScreen(BLACK); // Border frame gfx->drawRect(5, 5, SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10, DARKYELLOW); gfx->drawRect(10, 10, SCREEN_WIDTH - 20, SCREEN_HEIGHT - 20, DARKGRAY); // Title bar gfx->fillRect(20, 15, SCREEN_WIDTH - 40, 35, DARKYELLOW); gfx->drawRect(20, 15, SCREEN_WIDTH - 40, 35, YELLOW); gfx->setTextColor(BLACK); gfx->setTextSize(3); gfx->setCursor(55, 20); gfx->print("CYD SERVER"); // Status indicators drawHorizontalDivider(55, YELLOW); int indicatorY = 65; // Power ON indicator gfx->fillCircle(50, indicatorY + 6, 6, 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->setTextColor(YELLOW); } else { gfx->fillCircle(120, indicatorY + 6, 6, RED); gfx->setTextColor(RED); } gfx->setCursor(132, indicatorY + 2); gfx->print("SD"); // FTP indicator gfx->fillCircle(180, indicatorY + 6, 6, CYAN); gfx->setTextColor(CYAN); gfx->setCursor(192, indicatorY + 2); gfx->print("FTP"); // WiFi indicator gfx->fillCircle(240, indicatorY + 6, 6, CYAN); gfx->setTextColor(CYAN); gfx->setCursor(252, indicatorY + 2); gfx->print("WIFI"); drawHorizontalDivider(85, DARKGRAY); // Main content area frame drawCornerBrackets(15, 90, SCREEN_WIDTH - 30, 120, YELLOW); // Network address label gfx->setTextColor(WHITE); gfx->setTextSize(1); gfx->setCursor(115, 97); gfx->print("NETWORK ADDRESS"); // IP 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); // Port info gfx->setTextColor(YELLOW); gfx->setTextSize(1); gfx->setCursor(110, 150); gfx->print("HTTP Port: "); gfx->print(config.webPort); gfx->setCursor(110, 165); gfx->print("FTP Port: 21"); drawHorizontalDivider(220, DARKGRAY); // Runtime and requests labels gfx->setTextColor(WHITE); gfx->setTextSize(1); gfx->setCursor(55, 230); gfx->print("RUNTIME"); gfx->setCursor(210, 230); gfx->print("REQUESTS"); drawHorizontalDivider(280, DARKGRAY); // Footer gfx->setTextColor(DARKGRAY); gfx->setTextSize(1); gfx->setCursor(55, 450); gfx->print("CYD Server // Captive Portal Version"); } // ============== UPDATE DISPLAY ============== void updateDisplay() { // Runtime unsigned long uptime = millis() / 1000; int uptimeHours = uptime / 3600; int uptimeMins = (uptime % 3600) / 60; gfx->fillRect(40, 245, 110, 18, BLACK); gfx->setTextColor(YELLOW); gfx->setTextSize(2); if (uptimeHours > 0) { gfx->setCursor(40, 245); gfx->printf("%02d:%02d hr", uptimeHours, uptimeMins); } else { gfx->setCursor(52, 245); gfx->printf("%02d min", uptimeMins); } // Requests gfx->fillRect(220, 245, 80, 18, BLACK); gfx->setTextColor(GREEN); gfx->setTextSize(2); gfx->setCursor(220, 245); gfx->print(requestCount); // Time display struct tm timeinfo; if (getLocalTime(&timeinfo)) { int hour12 = timeinfo.tm_hour % 12; if (hour12 == 0) hour12 = 12; if (hour12 != lastDisplayedHour || timeinfo.tm_min != lastDisplayedMin) { gfx->fillRect(80, 300, 160, 50, BLACK); gfx->setTextColor(CYAN); gfx->setTextSize(4); gfx->setCursor(80, 310); gfx->printf("%02d:%02d", hour12, timeinfo.tm_min); gfx->setTextSize(2); gfx->setCursor(200, 325); gfx->print(timeinfo.tm_hour >= 12 ? "PM" : "AM"); lastDisplayedHour = hour12; lastDisplayedMin = timeinfo.tm_min; } // Date gfx->fillRect(95, 370, 130, 18, BLACK); char dateStr[15]; strftime(dateStr, sizeof(dateStr), "%m/%d/%Y", &timeinfo); gfx->setTextColor(YELLOW); gfx->setTextSize(2); gfx->setCursor(95, 370); gfx->print(dateStr); } // Free heap and RSSI gfx->fillRect(25, 420, 270, 12, BLACK); gfx->setTextColor(WHITE); gfx->setTextSize(1); gfx->setCursor(25, 420); gfx->print("HEAP:"); gfx->setTextColor(CYAN); gfx->printf("%dK", ESP.getFreeHeap() / 1024); gfx->setTextColor(WHITE); gfx->setCursor(130, 420); gfx->print("RSSI:"); gfx->setTextColor(YELLOW); gfx->printf("%ddBm", WiFi.RSSI()); gfx->setTextColor(WHITE); gfx->setCursor(220, 420); gfx->print("BL:"); gfx->setTextColor(YELLOW); gfx->printf("%d%%", (config.brightness * 100) / 255); } // ============== SETUP ============== void setup() { Serial.begin(115200); delay(1000); Serial.println("\n=== CYD Server - Captive Portal Version ===\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); // BOOT button pinMode(BOOT_BTN, INPUT_PULLUP); // Set chip selects as outputs pinMode(TFT_CS, OUTPUT); pinMode(SD_CS, OUTPUT); digitalWrite(TFT_CS, HIGH); digitalWrite(SD_CS, HIGH); // Load saved config loadConfig(); // Turn on backlight using saved brightness ledcAttach(TFT_BL, 5000, 8); ledcWrite(TFT_BL, config.brightness); Serial.printf("Backlight at %d%%\n", (config.brightness * 100) / 255); // Initialize display Serial.println("Initializing display..."); gfx->begin(); gfx->fillScreen(BLACK); // Initialize SD card Serial.println("Initializing SD card..."); digitalWrite(TFT_CS, HIGH); if (!SD.begin(SD_CS, SPI, 4000000)) { Serial.println(" SD Card mount failed!"); sdCardMounted = false; } else { sdCardMounted = true; Serial.println(" SD Card OK!"); digitalWrite(LED_GREEN, HIGH); } // Check if BOOT button held (force config mode) bool forceConfig = (digitalRead(BOOT_BTN) == LOW); delay(100); forceConfig = forceConfig && (digitalRead(BOOT_BTN) == LOW); // Enter config if no WiFi saved OR button held if (forceConfig || strlen(config.ssid) == 0) { startConfigMode(); return; } // Try to connect to saved WiFi if (!connectWiFi()) { startConfigMode(); return; } // WiFi connected - set LED to blue digitalWrite(LED_RED, LOW); digitalWrite(LED_BLUE, HIGH); // Configure NTP time configTime(config.gmtOffset, config.dstOffset, "pool.ntp.org"); // Start FTP server ftpSrv.begin(config.ftp_user, config.ftp_pass); Serial.printf("FTP server started (user: %s)\n", config.ftp_user); // Create web server on configured port server = new WebServer(config.webPort); // API endpoint for live system data server->on("/api/info", []() { String json = "{"; json += "\"ip\":\"" + WiFi.localIP().toString() + "\","; json += "\"ssid\":\"" + String(config.ssid) + "\","; json += "\"rssi\":" + String(WiFi.RSSI()) + ","; json += "\"port\":" + String(config.webPort) + ","; 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.printf("Web server started on port %d\n", config.webPort); // Draw status screen drawStatusScreen(); } // ============== LOOP ============== void loop() { if (configMode) { dnsServer.processNextRequest(); server->handleClient(); return; } if (WiFi.status() != WL_CONNECTED) { if (!connectWiFi()) { startConfigMode(); return; } } server->handleClient(); ftpSrv.handleFTP(); if (millis() - lastDisplayUpdate > 1000) { lastDisplayUpdate = millis(); updateDisplay(); } }
SAMPLE INDEX.HTML
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CYD Server</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <h1>CYD WEB SERVER</h1> <p class="subtitle">ESP32-2432S028R + ST7796 3.5" Display</p> <!-- Status Indicators --> <div class="indicators"> <span class="indicator green">PWR</span> <span class="indicator" id="wifi-ind">WiFi</span> <span class="indicator yellow">HTTP</span> <span class="indicator" id="sd-ind">SD</span> </div> <!-- Device Info --> <div class="status-box"> <h2>Device Info</h2> <div class="info-grid"> <div class="info-item"> <span class="label">Board</span> <span class="value">ESP32-WROOM-32E</span> </div> <div class="info-item"> <span class="label">Display</span> <span class="value">ST7796 320x480</span> </div> <div class="info-item"> <span class="label">Storage</span> <span class="value">MicroSD Card</span> </div> <div class="info-item"> <span class="label">CPU</span> <span class="value">Dual-Core 240MHz</span> </div> </div> </div> <!-- Network Status --> <div class="status-box"> <h2>Network Status</h2> <div id="network-info">Loading...</div> </div> <!-- System Stats --> <div class="status-box"> <h2>System Stats</h2> <div id="system-info">Loading...</div> </div> <p class="footer">Auto-refresh: <span id="countdown">5</span>s</p> </div> <script> function updateInfo() { fetch('/api/info') .then(r => r.json()) .then(data => { // Update WiFi indicator color based on signal var wifiInd = document.getElementById('wifi-ind'); wifiInd.className = 'indicator ' + (data.rssi > -60 ? 'green' : data.rssi > -75 ? 'yellow' : 'red'); // Update SD indicator var sdInd = document.getElementById('sd-ind'); sdInd.className = 'indicator ' + (data.sdSize > 0 ? 'green' : 'red'); // Format uptime var hrs = Math.floor(data.uptime / 3600); var mins = Math.floor((data.uptime % 3600) / 60); var secs = data.uptime % 60; var uptimeStr = hrs > 0 ? hrs + 'h ' + mins + 'm' : mins + 'm ' + secs + 's'; document.getElementById('network-info').innerHTML = ` <div class="info-grid"> <div class="info-item"> <span class="label">IP Address</span> <span class="value highlight">${data.ip}</span> </div> <div class="info-item"> <span class="label">SSID</span> <span class="value">${data.ssid || 'Connected'}</span> </div> <div class="info-item"> <span class="label">Signal Strength</span> <span class="value">${data.rssi} dBm</span> </div> <div class="info-item"> <span class="label">HTTP Port</span> <span class="value">${data.port || 80}</span> </div> </div> `; document.getElementById('system-info').innerHTML = ` <div class="info-grid"> <div class="info-item"> <span class="label">Uptime</span> <span class="value">${uptimeStr}</span> </div> <div class="info-item"> <span class="label">Free Heap</span> <span class="value">${(data.heap / 1024).toFixed(1)} KB</span> </div> <div class="info-item"> <span class="label">SD Card</span> <span class="value">${data.sdSize} MB</span> </div> <div class="info-item"> <span class="label">Requests</span> <span class="value">${data.requests || 0}</span> </div> </div> `; }) .catch(e => console.log('API error:', e)); } // Initial load updateInfo(); // Auto-refresh every 5 seconds var count = 5; setInterval(function() { count--; document.getElementById('countdown').textContent = count; if (count <= 0) { count = 5; updateInfo(); } }, 1000); </script> </body> </html>
SAMPLE STYLE.CSS
/* CYD Server - Cyberpunk Theme */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #e0e0e0; min-height: 100vh; display: flex; justify-content: center; align-items: center; } .container { max-width: 400px; padding: 20px; } h1 { color: #ffff00; text-align: center; text-shadow: 0 0 10px #ffff00; margin-bottom: 20px; letter-spacing: 3px; } .status-box { background: #12121f; border: 2px solid #ffff00; border-radius: 10px; padding: 20px; } .status-box h2 { color: #00ffff; font-size: 0.9rem; letter-spacing: 2px; margin-bottom: 15px; border-bottom: 1px solid #333; padding-bottom: 10px; } .subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.85rem; } .indicators { display: flex; justify-content: center; gap: 15px; margin-bottom: 20px; } .indicator { padding: 5px 12px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; background: #1a1a2a; border: 1px solid #333; } .indicator.green { color: #00ff00; border-color: #00ff00; box-shadow: 0 0 5px #00ff00; } .indicator.yellow { color: #ffff00; border-color: #ffff00; box-shadow: 0 0 5px #ffff00; } .indicator.red { color: #ff0000; border-color: #ff0000; box-shadow: 0 0 5px #ff0000; } .status-box + .status-box { margin-top: 15px; } .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .info-item { display: flex; flex-direction: column; } .label { font-size: 0.7rem; color: #888; text-transform: uppercase; } .value { color: #ffff00; font-size: 0.9rem; } .value.highlight { color: #00ffff; font-weight: bold; } .footer { text-align: center; margin-top: 20px; color: #555; font-size: 0.8rem; } #info strong { color: #ffff00; }
SETUP INSTRUCTIONS
- First Boot: CYD automatically enters config mode (no WiFi saved yet).
- Force Config Mode: Hold BOOT button while powering on to enter setup anytime.
- Connect: Join the "CYD-Server-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" - CYD restarts with new config.
DIFFERENCES FROM BASIC VERSION
- No hardcoded credentials - WiFi, FTP, and port 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 - Web server port, display brightness, timezone
- Dynamic web server - Port configurable (not hardcoded to 80)
CYD BOARD PINOUT
ESP32-WROOM-32E with built-in 3.5" ST7796 display:
Display (HSPI):
MOSI=13, SCK=14, CS=15, DC=2, BL=27
Touch (XPT2046):
MISO=39, MOSI=32, CLK=25, CS=33, IRQ=36
SD Card: CS=5 (shares HSPI)
RGB LED: R=4, G=16, B=17
BOOT Button: GPIO0
BUILD CHECKLIST
Follow these steps to build your CYD Web Server with Captive Portal:
- Create project folder: CYD_Server_Portal/
- Copy Arduino sketch: Save code as CYD_Server_Portal.ino
- Format SD card: FAT32 format (32GB or smaller)
- Copy web files to SD: Add index.html and style.css
- Install libraries: Arduino_GFX_Library + SimpleFTPServer
- Configure SimpleFTPServer: Edit FtpServerKey.h (STORAGE_SD)
- Insert SD card: Place SD card in CYD's built-in slot
- Select board: ESP32 Dev Module (NOT ESP32-S3)
- Upload sketch: Compile and upload (hold BOOT if needed)
- First boot: Connect to "CYD-Setup" WiFi network
- Configure: Open 192.168.4.1 in browser, enter WiFi settings
- Test: After reboot, access web server at displayed IP