2026-02-09 11:41:12 -05:00
|
|
|
#include "WebUI.h"
|
|
|
|
|
#include "../util/Version.h"
|
|
|
|
|
#include <ArduinoJson.h>
|
|
|
|
|
|
|
|
|
|
void WebUI::begin(Settings& settings,
|
|
|
|
|
WiFiManager& wifi,
|
|
|
|
|
MoistureSensor& moisture,
|
|
|
|
|
FaceRenderer& face,
|
|
|
|
|
WebhookService& webhook,
|
|
|
|
|
unsigned long bootMs) {
|
|
|
|
|
|
|
|
|
|
auto sendPortalRedirect = [&]() {
|
|
|
|
|
String target = String("http://") + wifi.apIp().toString() + "/";
|
|
|
|
|
_server.sendHeader("Location", target, true);
|
|
|
|
|
_server.send(302, "text/plain", "");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_server.on("/", HTTP_GET, [&]() {
|
|
|
|
|
String mode = (wifi.mode() == NET_AP_SETUP) ? "Setup AP" : "Station";
|
|
|
|
|
String wifiSsid = settings.wifiSsid();
|
|
|
|
|
String currentSsid = wifi.ssid();
|
2026-02-10 17:05:42 -05:00
|
|
|
String plantProfile = settings.plantProfile();
|
|
|
|
|
|
2026-02-09 11:41:12 -05:00
|
|
|
String page =
|
2026-02-10 17:05:42 -05:00
|
|
|
"<!DOCTYPE html><html><head>"
|
|
|
|
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
|
|
|
"<title>FacePlant</title>"
|
|
|
|
|
"<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css' "
|
|
|
|
|
"integrity='sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==' "
|
|
|
|
|
"crossorigin='anonymous' referrerpolicy='no-referrer' />"
|
|
|
|
|
"<style>"
|
|
|
|
|
"* { margin: 0; padding: 0; box-sizing: border-box; }"
|
|
|
|
|
"body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; "
|
|
|
|
|
"background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }"
|
|
|
|
|
".container { max-width: 600px; margin: 0 auto; }"
|
|
|
|
|
".card { background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); "
|
|
|
|
|
"border-radius: 20px; padding: 30px; margin-bottom: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }"
|
|
|
|
|
".header { text-align: center; margin-bottom: 10px; }"
|
|
|
|
|
".header h1 { font-size: 2.5em; font-weight: 700; color: #1d1d1f; margin-bottom: 5px; "
|
|
|
|
|
"background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; "
|
|
|
|
|
"-webkit-text-fill-color: transparent; }"
|
|
|
|
|
".version { color: #86868b; font-size: 0.9em; margin-bottom: 20px; }"
|
|
|
|
|
".status-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; }"
|
|
|
|
|
".status-item { background: #f5f5f7; padding: 15px; border-radius: 12px; }"
|
|
|
|
|
".status-label { font-size: 0.85em; color: #86868b; margin-bottom: 5px; }"
|
|
|
|
|
".status-value { font-size: 1em; color: #1d1d1f; font-weight: 500; word-break: break-word; }"
|
|
|
|
|
".status-value.connected { color: #34c759; }"
|
|
|
|
|
".status-value.disconnected { color: #ff3b30; }"
|
|
|
|
|
"h2 { font-size: 1.5em; font-weight: 600; color: #1d1d1f; margin: 25px 0 15px 0; }"
|
|
|
|
|
"label { display: block; font-size: 0.95em; font-weight: 500; color: #1d1d1f; margin-bottom: 8px; }"
|
|
|
|
|
"input[type='text'], input[type='password'], select { width: 100%; padding: 12px 16px; "
|
|
|
|
|
"border: 1px solid #d2d2d7; border-radius: 10px; font-size: 1em; "
|
|
|
|
|
"transition: all 0.2s; background: white; }"
|
|
|
|
|
"input[type='text']:focus, input[type='password']:focus, select:focus { "
|
|
|
|
|
"outline: none; border-color: #667eea; box-shadow: 0 0 0 4px rgba(102,126,234,0.1); }"
|
|
|
|
|
".form-hint { font-size: 0.85em; color: #86868b; margin-top: 5px; margin-bottom: 15px; }"
|
|
|
|
|
".checkbox-label { display: flex; align-items: center; margin: 15px 0; cursor: pointer; }"
|
|
|
|
|
"input[type='checkbox'] { width: 20px; height: 20px; margin-right: 10px; cursor: pointer; "
|
|
|
|
|
"accent-color: #667eea; }"
|
|
|
|
|
".btn { display: inline-block; padding: 12px 24px; border: none; border-radius: 10px; "
|
|
|
|
|
"font-size: 1em; font-weight: 500; cursor: pointer; transition: all 0.2s; "
|
|
|
|
|
"text-decoration: none; text-align: center; }"
|
|
|
|
|
".btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); "
|
|
|
|
|
"color: white; box-shadow: 0 4px 12px rgba(102,126,234,0.4); }"
|
|
|
|
|
".btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102,126,234,0.5); }"
|
|
|
|
|
".btn-secondary { background: #f5f5f7; color: #1d1d1f; }"
|
|
|
|
|
".btn-secondary:hover { background: #e8e8ed; }"
|
|
|
|
|
".btn-danger { background: #ff3b30; color: white; }"
|
|
|
|
|
".btn-danger:hover { background: #ff2d20; transform: translateY(-2px); }"
|
|
|
|
|
".btn-group { display: flex; gap: 10px; margin-top: 20px; }"
|
|
|
|
|
".btn-group .btn { flex: 1; }"
|
|
|
|
|
".links { margin-top: 20px; text-align: center; }"
|
|
|
|
|
".links a { color: #667eea; text-decoration: none; margin: 0 10px; font-weight: 500; }"
|
|
|
|
|
".links a:hover { text-decoration: underline; }"
|
|
|
|
|
"@media (max-width: 600px) { .status-grid { grid-template-columns: 1fr; } }"
|
|
|
|
|
"</style>"
|
|
|
|
|
"</head><body>"
|
|
|
|
|
"<div class='container'>"
|
|
|
|
|
"<div class='card'>"
|
|
|
|
|
"<div class='header'>"
|
|
|
|
|
"<h1><i class='fas fa-seedling' style='margin-right: 10px;'></i>FacePlant</h1>"
|
|
|
|
|
"<div class='version'>Firmware v" + String(PB_VERSION) + "</div>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<div class='status-grid'>"
|
|
|
|
|
"<div class='status-item'><div class='status-label'>Status</div>"
|
|
|
|
|
"<div class='status-value " + String(wifi.connected() ? "connected" : "disconnected") + "'>"
|
|
|
|
|
+ (wifi.connected() ? "Connected" : "Not Connected") + "</div></div>"
|
|
|
|
|
"<div class='status-item'><div class='status-label'>Mode</div>"
|
|
|
|
|
"<div class='status-value'>" + mode + "</div></div>"
|
|
|
|
|
"<div class='status-item'><div class='status-label'>Current Network</div>"
|
|
|
|
|
"<div class='status-value'>" + (currentSsid.length() ? currentSsid : "None") + "</div></div>"
|
|
|
|
|
"<div class='status-item'><div class='status-label'>Setup AP</div>"
|
|
|
|
|
"<div class='status-value'>" + String(wifi.setupSsid()) + "</div></div>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<div class='card'>"
|
|
|
|
|
"<h2><i class='fas fa-wifi' style='margin-right: 10px;'></i>Wi-Fi Settings</h2>"
|
|
|
|
|
"<form method='POST' action='/wifi' id='wifiForm'>"
|
|
|
|
|
"<label for='ssid'>Network Name (SSID)</label>"
|
|
|
|
|
"<input type='text' id='ssid' name='ssid' value='" + wifiSsid + "' required>"
|
|
|
|
|
"<div class='form-hint'>Enter your Wi-Fi network name</div>"
|
|
|
|
|
"<label for='pass'>Password</label>"
|
|
|
|
|
"<input type='password' id='pass' name='pass' placeholder='Enter password'>"
|
|
|
|
|
"<div class='form-hint'>Leave blank to keep existing password for same SSID</div>"
|
|
|
|
|
"<div class='btn-group'>"
|
|
|
|
|
"<button type='submit' name='action' value='connect' class='btn btn-primary'>Connect</button>"
|
|
|
|
|
"<button type='submit' name='action' value='forget' class='btn btn-danger' "
|
|
|
|
|
"onclick='return confirm(\"Forget Wi-Fi settings?\")'>Forget</button>"
|
|
|
|
|
"</div>"
|
2026-02-09 11:41:12 -05:00
|
|
|
"</form>"
|
2026-02-10 17:05:42 -05:00
|
|
|
"</div>"
|
|
|
|
|
"<div class='card'>"
|
|
|
|
|
"<h2><i class='fas fa-leaf' style='margin-right: 10px;'></i>Plant Settings</h2>"
|
|
|
|
|
"<form method='POST' action='/config'>"
|
|
|
|
|
"<label for='plant'>Plant Profile</label>"
|
|
|
|
|
"<select id='plant' name='plant'>"
|
|
|
|
|
"<option value='house'" + String(plantProfile == "house" ? " selected" : "") + ">Houseplant</option>"
|
|
|
|
|
"<option value='succulent'" + String(plantProfile == "succulent" ? " selected" : "") + ">Succulent</option>"
|
|
|
|
|
"<option value='herbs'" + String(plantProfile == "herbs" ? " selected" : "") + ">Herbs</option>"
|
|
|
|
|
"<option value='fern'" + String(plantProfile == "fern" ? " selected" : "") + ">Fern</option>"
|
|
|
|
|
"<option value='tropical'" + String(plantProfile == "tropical" ? " selected" : "") + ">Tropical</option>"
|
|
|
|
|
"<option value='veg'" + String(plantProfile == "veg" ? " selected" : "") + ">Vegetables</option>"
|
|
|
|
|
"</select>"
|
|
|
|
|
"<div class='form-hint'>Select your plant type for optimal watering thresholds</div>"
|
|
|
|
|
"<label class='checkbox-label'>"
|
|
|
|
|
"<input type='checkbox' name='kids' " + String(settings.kidsMode() ? "checked" : "") + "> Kids Mode"
|
|
|
|
|
"</label>"
|
|
|
|
|
"<div class='form-hint'>Simplified interface for children</div>"
|
|
|
|
|
"<label for='wh'>Webhook URL</label>"
|
|
|
|
|
"<input type='text' id='wh' name='wh' value='" + settings.webhookUrl() + "' placeholder='https://...'>"
|
|
|
|
|
"<div class='form-hint'>Optional webhook for notifications</div>"
|
|
|
|
|
"<label class='checkbox-label'>"
|
|
|
|
|
"<input type='checkbox' name='wh_en' " + String(settings.webhookEnabled() ? "checked" : "") + "> Enable Webhook"
|
|
|
|
|
"</label>"
|
|
|
|
|
"<div class='btn-group'>"
|
|
|
|
|
"<button type='submit' class='btn btn-primary'>Save Settings</button>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"</form>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<div class='card'>"
|
|
|
|
|
"<h2><i class='fas fa-sliders-h' style='margin-right: 10px;'></i>Sensor Calibration</h2>"
|
|
|
|
|
"<div style='background: #f5f5f7; padding: 15px; border-radius: 12px; margin-bottom: 20px;'>"
|
|
|
|
|
"<div style='font-size: 0.85em; color: #86868b; margin-bottom: 5px;'>Current Reading</div>"
|
|
|
|
|
"<div style='font-size: 1.5em; font-weight: 600; color: #667eea;'>" + String(moisture.raw()) + "</div>"
|
|
|
|
|
"<div style='font-size: 0.85em; color: #86868b; margin-top: 5px;'>Moisture: " + String(moisture.percent()) + "%</div>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<form method='POST' action='/calibrate'>"
|
|
|
|
|
"<p style='color: #86868b; font-size: 0.9em; margin-bottom: 20px;'>"
|
|
|
|
|
"<i class='fas fa-lightbulb' style='color: #667eea; margin-right: 5px;'></i><strong>How to calibrate:</strong><br>"
|
|
|
|
|
"1. For <strong>dry</strong> reading: Remove sensor from soil, wait 30 seconds, note the value above<br>"
|
|
|
|
|
"2. For <strong>wet</strong> reading: Place sensor in water, wait 30 seconds, note the value above<br>"
|
|
|
|
|
"3. Enter both values below and save"
|
|
|
|
|
"</p>"
|
|
|
|
|
"<label for='dry'>Dry Value (sensor in air)</label>"
|
|
|
|
|
"<input type='number' id='dry' name='dry' value='" + String(settings.dryRaw()) + "' required min='0' max='4095'>"
|
|
|
|
|
"<div class='form-hint'>Raw ADC value when sensor is completely dry (typically 3000-4000)</div>"
|
|
|
|
|
"<label for='wet'>Wet Value (sensor in water)</label>"
|
|
|
|
|
"<input type='number' id='wet' name='wet' value='" + String(settings.wetRaw()) + "' required min='0' max='4095'>"
|
|
|
|
|
"<div class='form-hint'>Raw ADC value when sensor is fully wet (typically 1000-2000)</div>"
|
|
|
|
|
"<div class='btn-group'>"
|
|
|
|
|
"<button type='submit' class='btn btn-primary'>Save Calibration</button>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"</form>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<div class='card' style='border: 2px solid #ff3b30;'>"
|
|
|
|
|
"<h2 style='color: #ff3b30;'><i class='fas fa-exclamation-triangle' style='margin-right: 10px;'></i>Danger Zone</h2>"
|
|
|
|
|
"<p style='color: #86868b; margin-bottom: 15px;'>Irreversible actions that will reset your device</p>"
|
|
|
|
|
"<form method='POST' action='/factory-reset' id='resetForm'>"
|
|
|
|
|
"<p style='margin-bottom: 15px;'><strong>Factory Reset</strong></p>"
|
|
|
|
|
"<p style='color: #86868b; font-size: 0.9em; margin-bottom: 15px;'>"
|
|
|
|
|
"This will erase all settings including Wi-Fi credentials, plant profile, calibration data, and webhooks. "
|
|
|
|
|
"The device will restart in setup mode.</p>"
|
|
|
|
|
"<button type='submit' class='btn btn-danger' style='width: 100%;'>Reset to Factory Defaults</button>"
|
|
|
|
|
"</form>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<div class='card links'>"
|
|
|
|
|
"<a href='/status'><i class='fas fa-chart-bar'></i> Status (JSON)</a>"
|
|
|
|
|
"<a href='/update'><i class='fas fa-cloud-upload-alt'></i> OTA Update</a>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"</div>"
|
|
|
|
|
"<script>"
|
|
|
|
|
"document.getElementById('wifiForm').addEventListener('submit', function(e) {"
|
|
|
|
|
" if (e.submitter.value === 'connect') {"
|
|
|
|
|
" const ssid = document.getElementById('ssid').value.trim();"
|
|
|
|
|
" const pass = document.getElementById('pass').value;"
|
|
|
|
|
" const savedSsid = '" + wifiSsid + "';"
|
|
|
|
|
" if (ssid !== savedSsid && pass.length === 0) {"
|
|
|
|
|
" e.preventDefault();"
|
|
|
|
|
" alert('Please enter a password for the new network.');"
|
|
|
|
|
" return false;"
|
|
|
|
|
" }"
|
|
|
|
|
" }"
|
|
|
|
|
"});"
|
|
|
|
|
"document.getElementById('resetForm').addEventListener('submit', function(e) {"
|
|
|
|
|
" e.preventDefault();"
|
|
|
|
|
" if (confirm('Are you sure you want to reset to factory defaults? This cannot be undone.')) {"
|
|
|
|
|
" if (confirm('Last warning: This will erase ALL settings and restart the device. Continue?')) {"
|
|
|
|
|
" this.submit();"
|
|
|
|
|
" }"
|
|
|
|
|
" }"
|
|
|
|
|
"});"
|
|
|
|
|
"</script>"
|
|
|
|
|
"</body></html>";
|
2026-02-09 11:41:12 -05:00
|
|
|
_server.send(200, "text/html", page);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_server.on("/wifi", HTTP_POST, [&]() {
|
|
|
|
|
String action = _server.hasArg("action") ? _server.arg("action") : "connect";
|
|
|
|
|
|
|
|
|
|
if (action == "forget") {
|
|
|
|
|
wifi.clearAndStartSetupAP();
|
|
|
|
|
} else {
|
|
|
|
|
String ssid = _server.hasArg("ssid") ? _server.arg("ssid") : settings.wifiSsid();
|
|
|
|
|
String pass = "";
|
|
|
|
|
|
|
|
|
|
if (_server.hasArg("pass") && _server.arg("pass").length() > 0) {
|
|
|
|
|
pass = _server.arg("pass");
|
|
|
|
|
} else if (ssid == settings.wifiSsid()) {
|
|
|
|
|
pass = settings.wifiPass();
|
2026-02-10 17:05:42 -05:00
|
|
|
} else {
|
|
|
|
|
// New SSID but no password provided - reject
|
|
|
|
|
_server.send(400, "text/plain", "Password required for new network");
|
|
|
|
|
return;
|
2026-02-09 11:41:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ssid.length() > 0) wifi.saveAndConnect(ssid, pass);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_server.sendHeader("Location", "/");
|
|
|
|
|
_server.send(303);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_server.on("/config", HTTP_POST, [&]() {
|
|
|
|
|
if (_server.hasArg("plant")) settings.setPlantProfile(_server.arg("plant"));
|
|
|
|
|
settings.setKidsMode(_server.hasArg("kids"));
|
|
|
|
|
settings.setWebhookEnabled(_server.hasArg("wh_en"));
|
|
|
|
|
if (_server.hasArg("wh")) settings.setWebhookUrl(_server.arg("wh"));
|
|
|
|
|
_server.sendHeader("Location", "/");
|
|
|
|
|
_server.send(303);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-10 17:05:42 -05:00
|
|
|
_server.on("/calibrate", HTTP_POST, [&]() {
|
|
|
|
|
if (_server.hasArg("dry") && _server.hasArg("wet")) {
|
|
|
|
|
int dryVal = _server.arg("dry").toInt();
|
|
|
|
|
int wetVal = _server.arg("wet").toInt();
|
|
|
|
|
|
|
|
|
|
Serial.println("[WebUI] Calibration update requested");
|
|
|
|
|
Serial.print("[WebUI] Dry value: ");
|
|
|
|
|
Serial.println(dryVal);
|
|
|
|
|
Serial.print("[WebUI] Wet value: ");
|
|
|
|
|
Serial.println(wetVal);
|
|
|
|
|
|
|
|
|
|
if (dryVal > wetVal && dryVal <= 4095 && wetVal >= 0) {
|
|
|
|
|
settings.setCalibration(dryVal, wetVal);
|
|
|
|
|
Serial.println("[WebUI] Calibration saved");
|
|
|
|
|
} else {
|
|
|
|
|
Serial.println("[WebUI] Invalid calibration values (dry must be > wet)");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_server.sendHeader("Location", "/");
|
|
|
|
|
_server.send(303);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_server.on("/factory-reset", HTTP_POST, [&]() {
|
|
|
|
|
Serial.println("[WebUI] Factory reset requested");
|
|
|
|
|
_server.send(200, "text/html",
|
|
|
|
|
"<!DOCTYPE html><html><head><meta http-equiv='refresh' content='10;url=http://192.168.4.1'>"
|
|
|
|
|
"<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css'>"
|
|
|
|
|
"<style>body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
|
|
|
|
|
"background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;display:flex;"
|
|
|
|
|
"align-items:center;justify-content:center;margin:0;padding:20px;}"
|
|
|
|
|
".card{background:rgba(255,255,255,0.95);border-radius:20px;padding:40px;text-align:center;"
|
|
|
|
|
"box-shadow:0 20px 60px rgba(0,0,0,0.3);max-width:400px;}"
|
|
|
|
|
"h1{font-size:2em;margin-bottom:10px;color:#1d1d1f;}"
|
|
|
|
|
"p{color:#86868b;margin:10px 0;}"
|
|
|
|
|
".spinner{font-size:3em;color:#667eea;margin:20px 0;animation:spin 2s linear infinite;}"
|
|
|
|
|
"@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style></head><body>"
|
|
|
|
|
"<div class='card'>"
|
|
|
|
|
"<div class='spinner'><i class='fas fa-sync-alt'></i></div>"
|
|
|
|
|
"<h1>Factory Reset</h1>"
|
|
|
|
|
"<p>Erasing all settings...</p>"
|
|
|
|
|
"<p>Device will restart in setup mode.</p>"
|
|
|
|
|
"<p>Reconnect to <strong>FacePlant-Setup</strong> network in 10 seconds.</p>"
|
|
|
|
|
"</div></body></html>");
|
|
|
|
|
delay(1000);
|
|
|
|
|
settings.factoryReset();
|
|
|
|
|
delay(1000);
|
|
|
|
|
Serial.println("[WebUI] Restarting device...");
|
|
|
|
|
ESP.restart();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-09 11:41:12 -05:00
|
|
|
_server.on("/status", HTTP_GET, [&]() {
|
|
|
|
|
JsonDocument doc;
|
|
|
|
|
doc["device"] = "FacePlant";
|
|
|
|
|
doc["version"] = PB_VERSION;
|
|
|
|
|
doc["net_mode"] = (wifi.mode() == NET_AP_SETUP) ? "setup_ap" : "sta";
|
|
|
|
|
doc["setup_ssid"] = wifi.setupSsid();
|
|
|
|
|
doc["ip"] = wifi.ip().toString();
|
|
|
|
|
doc["ssid"] = wifi.ssid();
|
|
|
|
|
doc["saved_ssid"] = settings.wifiSsid();
|
|
|
|
|
doc["wifi_connected"] = wifi.connected();
|
|
|
|
|
doc["moisture_pct"] = moisture.percent();
|
|
|
|
|
doc["raw"] = moisture.raw();
|
|
|
|
|
doc["kids_mode"] = settings.kidsMode();
|
|
|
|
|
doc["dead_mode"] = face.isDeadMode();
|
|
|
|
|
doc["uptime_ms"] = millis() - bootMs;
|
|
|
|
|
|
|
|
|
|
String out;
|
|
|
|
|
serializeJson(doc, out);
|
|
|
|
|
_server.send(200, "application/json", out);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Captive portal compatibility endpoints used by common OS network checkers.
|
|
|
|
|
_server.on("/generate_204", HTTP_GET, [&]() { sendPortalRedirect(); }); // Android
|
|
|
|
|
_server.on("/gen_204", HTTP_GET, [&]() { sendPortalRedirect(); }); // Android alt
|
|
|
|
|
_server.on("/hotspot-detect.html", HTTP_GET, [&]() { sendPortalRedirect(); }); // iOS/macOS
|
|
|
|
|
_server.on("/library/test/success.html", HTTP_GET, [&]() { sendPortalRedirect(); });
|
|
|
|
|
_server.on("/ncsi.txt", HTTP_GET, [&]() { sendPortalRedirect(); }); // Windows
|
|
|
|
|
_server.on("/connecttest.txt", HTTP_GET, [&]() { sendPortalRedirect(); }); // Windows alt
|
|
|
|
|
_server.on("/fwlink", HTTP_GET, [&]() { sendPortalRedirect(); }); // Windows alt
|
|
|
|
|
|
|
|
|
|
_server.onNotFound([&]() {
|
|
|
|
|
if (wifi.mode() == NET_AP_SETUP) {
|
|
|
|
|
sendPortalRedirect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_server.send(404, "text/plain", "Not found");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_server.begin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WebUI::loop() {
|
|
|
|
|
_server.handleClient();
|
|
|
|
|
}
|