ALIVE OLED CAPTIVE PORTAL

Web-Configurable Server Monitor - No Hardcoded Credentials

← 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-Setup" WiFi
→
3
Browser opens
config page
→
4
Save settings
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 primitives
  • Adafruit SSD1306 - OLED driver
  • ESP32Ping by 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_Portal in your Arduino sketches directory (usually Documents/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

  1. □ Install Arduino IDE 3.x
  2. □ Add ESP32 board package URL and install ESP32 boards
  3. □ Install required libraries: Adafruit GFX, Adafruit SSD1306, ESP32Ping
  4. □ Create project folder: AliveOLED_Portal/
  5. □ Copy sketch as AliveOLED_Portal.ino
  6. □ Wire SSD1306 OLED to ESP32-S3 (4 wires: VCC, GND, SDA, SCL)
  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-OLED-Setup" WiFi network
  12. □ Open 192.168.4.1 in browser
  13. □ Set OLED I2C address (0x3C or 0x3D)
  14. □ Scan and select your WiFi network
  15. □ Add server IPs and names (keep names short: 4-8 chars)
  16. □ Click "SAVE & REBOOT" - Done!