ALIVE OLED CAPTIVE PORTAL
Web-Configurable Server Monitor - No Hardcoded Credentials
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)
(hold BOOT for config)
2
Connect to
"ALIVE-Setup" WiFi
"ALIVE-Setup" WiFi
3
Browser opens
config page
config page
4
Save settings
ESP32 reboots
ESP32 reboots
ARDUINO SKETCH
AliveOLED_Portal.ino
/* * Server Status Dashboard - "ALIVE" (OLED + Captive Portal) * ESP32-S3 Super Mini + SSD1306 0.96" OLED + Status LEDs * * Features: * - Web-based configuration (no hardcoded credentials) * - WiFi network scanning * - Configurable servers, check interval, display settings * - Settings persist in flash memory * * Config Mode Triggers: * - First boot (no saved WiFi) * - Hold BOOT button during power-on */ #include <WiFi.h> #include <ESP32Ping.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <WebServer.h> #include <DNSServer.h> #include <Preferences.h> // ============== PIN DEFINITIONS ============== #define OLED_SDA 8 #define OLED_SCL 9 #define LED_BLUE 5 #define LED_RED 10 #define BOOT_BTN 0 // GPIO0 is BOOT button // ============== DISPLAY SETTINGS ============== #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 // ============== CONFIGURATION STRUCTURE ============== struct Config { char ssid[33]; char password[65]; char servers[5][64]; char serverNames[5][16]; int numServers; unsigned long checkInterval; int scrollSpeed; uint8_t oledAddr; }; Config config; Preferences prefs; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); WebServer server(80); DNSServer dnsServer; // ============== STATE VARIABLES ============== bool configMode = false; bool serverStatus[5]; bool allServersUp = true; unsigned long lastCheck = 0; unsigned long lastBlink = 0; bool redLedState = false; // Non-blocking scan int scanIndex = -1; bool scanInProgress = false; // Scrolling display int scrollX = 128; int currentScrollServer = 0; unsigned long lastScroll = 0; // ============== DEFAULT CONFIGURATION ============== void setDefaults() { memset(&config, 0, sizeof(config)); config.numServers = 3; config.checkInterval = 30000; config.scrollSpeed = 3; config.oledAddr = 0x3C; 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("alive", 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.scrollSpeed = prefs.getInt("scroll", 3); config.oledAddr = prefs.getUChar("oled", 0x3C); 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("alive", false); prefs.putString("ssid", config.ssid); prefs.putString("pass", config.password); prefs.putInt("numSrv", config.numServers); prefs.putULong("interval", config.checkInterval); prefs.putInt("scroll", config.scrollSpeed); prefs.putUChar("oled", config.oledAddr); 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(); } // ============== CONFIG PAGE HTML ============== const char* configPage = R"rawhtml( <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>ALIVE 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 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>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>Scroll Speed (1-10)</label> <input type="number" name="scroll" value="3" min="1" max="10"> </div> </div> <label>OLED I2C Address</label> <select name="oled"> <option value="60">0x3C (Default)</option> <option value="61">0x3D</option> </select> </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.scrollSpeed = server.arg("scroll").toInt(); config.oledAddr = server.arg("oled").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(); } // ============== START CONFIG MODE ============== void startConfigMode() { configMode = true; Serial.println("Starting Config Mode..."); // Show on OLED display.clearDisplay(); display.fillRect(0, 0, 128, 15, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setTextSize(1); display.setCursor(20, 4); display.print("CONFIG MODE"); display.setTextColor(SSD1306_WHITE); display.setCursor(10, 22); display.print("Connect WiFi to:"); display.setTextSize(1); display.setCursor(10, 35); display.print("ALIVE-Setup"); display.setCursor(10, 50); display.print("Open: 192.168.4.1"); display.display(); // Start AP WiFi.mode(WIFI_AP); WiFi.softAP("ALIVE-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; display.clearDisplay(); display.fillRect(0, 0, 128, 15, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setCursor(25, 4); display.print("NET_CONNECT"); display.setTextColor(SSD1306_WHITE); display.setCursor(10, 24); display.print("SSID: "); display.print(config.ssid); display.display(); WiFi.mode(WIFI_STA); WiFi.begin(config.ssid, config.password); int attempts = 0; display.setCursor(10, 42); display.print("["); while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); display.print("="); display.display(); attempts++; } if (WiFi.status() == WL_CONNECTED) { display.clearDisplay(); display.fillRect(0, 0, 128, 15, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setCursor(25, 4); display.print("LINK_ACTIVE"); display.setTextColor(SSD1306_WHITE); display.setCursor(25, 36); display.print(WiFi.localIP()); display.display(); delay(750); return true; } return false; } // ============== SETUP ============== void setup() { Serial.begin(115200); delay(100); Serial.println("\n=== ALIVE - 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 I2C and display Wire.begin(OLED_SDA, OLED_SCL); // Load saved config loadConfig(); if (!display.begin(SSD1306_SWITCHCAPVCC, config.oledAddr)) { Serial.println("OLED failed!"); while(1); } display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // 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; } // Try to connect to saved WiFi if (!connectWiFi()) { startConfigMode(); return; } // Initial server check checkAllServers(); lastCheck = millis(); } // ============== NON-BLOCKING SERVER CHECK ============== void startScan() { scanInProgress = true; scanIndex = 0; allServersUp = true; } void checkNextServer() { if (scanIndex >= config.numServers) { scanInProgress = false; lastCheck = millis(); return; } bool success = Ping.ping(config.servers[scanIndex], 1); serverStatus[scanIndex] = success; if (!success) allServersUp = false; scanIndex++; } void checkAllServers() { allServersUp = true; for (int i = 0; i < config.numServers; i++) { serverStatus[i] = Ping.ping(config.servers[i], 2); if (!serverStatus[i]) allServersUp = false; } } // ============== UPDATE DISPLAY ============== void updateDisplay() { display.clearDisplay(); if (allServersUp) { display.fillRect(0, 0, 120, 14, SSD1306_WHITE); display.fillTriangle(120, 0, 120, 14, 127, 0, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setTextSize(1); display.setCursor(4, 4); display.print("ALIVE"); display.setCursor(50, 4); if (scanInProgress) { display.print("[SCAN "); display.print(scanIndex + 1); display.print("/"); display.print(config.numServers); display.print("]"); } else { display.print("[NOMINAL]"); } display.setTextColor(SSD1306_WHITE); String scrollText = String(config.serverNames[currentScrollServer]) + " ALIVE"; int textWidth = scrollText.length() * 12; display.setTextSize(2); display.setTextWrap(false); display.setCursor(scrollX, 32); display.print(scrollText); if (millis() - lastScroll > (50 - config.scrollSpeed * 4)) { scrollX -= config.scrollSpeed; lastScroll = millis(); if (scrollX < -textWidth) { scrollX = 128; currentScrollServer = (currentScrollServer + 1) % config.numServers; } } display.setTextSize(1); display.setCursor(25, 56); display.print("System Active"); } else { int downIndex = -1; for (int i = 0; i < config.numServers; i++) { if (!serverStatus[i]) { downIndex = i; break; } } bool blinkOn = (millis() / 300) % 2 == 0; if (blinkOn) { display.fillRect(0, 0, 128, 15, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setCursor(40, 4); display.print("WARNING"); display.setTextColor(SSD1306_WHITE); } if (blinkOn && downIndex >= 0) { display.setTextSize(1); display.setCursor(45, 24); display.print("[DEAD]"); display.setTextSize(2); display.setCursor(10, 36); display.print(config.serverNames[downIndex]); } } display.display(); } // ============== UPDATE LEDs ============== void updateLEDs() { digitalWrite(LED_BLUE, HIGH); if (allServersUp) { digitalWrite(LED_RED, LOW); } else { if (millis() - lastBlink >= 500) { redLedState = !redLedState; digitalWrite(LED_RED, redLedState); lastBlink = millis(); } } } // ============== MAIN LOOP ============== void loop() { if (configMode) { dnsServer.processNextRequest(); server.handleClient(); return; } if (WiFi.status() != WL_CONNECTED) { if (!connectWiFi()) { startConfigMode(); return; } } if (!scanInProgress && millis() - lastCheck >= config.checkInterval) { startScan(); } if (scanInProgress) { checkNextServer(); } updateDisplay(); 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-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
Adafruit GFX Library- Graphics primitivesAdafruit SSD1306- OLED driverESP32Pingby Matteo Dainese - ICMP ping
Note: WebServer, DNSServer, and Preferences are built into ESP32 Arduino core - no install needed.
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, scroll speed, OLED address
- Larger code size - ~450 lines vs ~300 lines for basic version
ARDUINO PROJECT SETUP
- Create Project Folder: Create a folder named
AliveOLED_Portalin your Arduino sketches directory (usuallyDocuments/Arduino/) - Main Sketch: Save the code as
AliveOLED_Portal.ino- filename MUST match folder name - Single File Project: This project uses only one .ino file - no additional .h files required
AliveOLED_Portal/
└── AliveOLED_Portal.ino // Complete sketch (copy the code above)
COMPONENTS NEEDED
| ESP32-S3 Super Mini | Main microcontroller | AliExpress |
| SSD1306 OLED Display | 128x64 0.96" I2C (dual-color yellow/blue recommended) | Amazon |
| Blue LED (5mm) | Status indicator - always ON when running | Amazon |
| Red LED (5mm) | Alert indicator - flashes when server down | Amazon |
| 330Ω Resistors (x2) | Current limiting for LEDs | Amazon |
| Dupont Wires | For connections (F-F for OLED) | Amazon |
WIRING DIAGRAM
| SSD1306 OLED | ESP32-S3 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | GPIO 8 |
| SCL | GPIO 9 |
| Status LEDs | ESP32-S3 |
|---|---|
| Blue LED (+) → 330Ω | GPIO 5 |
| Red LED (+) → 330Ω | GPIO 10 |
| Both LED (-) | GND |
Note: Only 4 wires needed for OLED (I2C is simpler than SPI displays).
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.
- OLED shows nothing: Check I2C address in config page. Common addresses: 0x3C or 0x3D.
- Can't find "ALIVE-OLED-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.
- Display flickers: Add 4.7K pull-up resistors on SDA and SCL (some modules have built-in).
- 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 resistors connected.
- Serial Monitor empty: Set baud rate to 115200. Enable "USB CDC On Boot" in board settings.
BUILD CHECKLIST
- □ Install Arduino IDE 3.x
- □ Add ESP32 board package URL and install ESP32 boards
- □ Install required libraries: Adafruit GFX, Adafruit SSD1306, ESP32Ping
- □ Create project folder:
AliveOLED_Portal/ - □ Copy sketch as
AliveOLED_Portal.ino - □ Wire SSD1306 OLED to ESP32-S3 (4 wires: VCC, GND, SDA, SCL)
- □ Wire status LEDs with 330Ω resistors
- □ Connect ESP32-S3 via USB-C
- □ Select ESP32S3 Dev Module and correct COM port
- □ Upload sketch (hold BOOT if needed)
- □ Connect to "ALIVE-OLED-Setup" WiFi network
- □ Open 192.168.4.1 in browser
- □ Set OLED I2C address (0x3C or 0x3D)
- □ Scan and select your WiFi network
- □ Add server IPs and names (keep names short: 4-8 chars)
- □ Click "SAVE & REBOOT" - Done!