#include "WebUI.h" #include "../util/Version.h" #include 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(); String plantProfile = settings.plantProfile(); String timezone = settings.timezone(); String bedtime = settings.bedtime(); String wakeTime = settings.wakeTime(); String page = "" "" "FacePlant" "" "" "" "
" "
" "
" "

FacePlant

" "
Firmware v" + String(PB_VERSION) + "
" "
" "
" "
Status
" "
" + (wifi.connected() ? "Connected" : "Not Connected") + "
" "
Mode
" "
" + mode + "
" "
Current Network
" "
" + (currentSsid.length() ? currentSsid : "None") + "
" "
Setup AP
" "
" + String(wifi.setupSsid()) + "
" "
" "
" "
" "

Wi-Fi Settings

" "
" "" "" "
Enter your Wi-Fi network name
" "" "" "
Leave blank to keep existing password for same SSID
" "
" "" "" "
" "
" "
" "
" "

Plant Settings

" "
" "" "" "
Select your plant type for optimal watering thresholds
" "" "
Simplified interface for children
" "" "" "
Optional webhook for notifications
" "" "

Sleep Schedule

" "" "" "
FacePlant starts falling asleep 5 minutes before bedtime
" "" "" "
FacePlant shows wake-up animation for 5 minutes after this time
" "" "" "
Default is America/New_York
" "
" "" "
" "
" "
" "
" "

Sensor Calibration

" "
" "
Current Reading
" "
" + String(moisture.raw()) + "
" "
Moisture: " + String(moisture.percent()) + "%
" "
" "
" "

" "How to calibrate:
" "1. For dry reading: Remove sensor from soil, wait 30 seconds, note the value above
" "2. For wet reading: Place sensor in water, wait 30 seconds, note the value above
" "3. Enter both values below and save" "

" "" "" "
Raw ADC value when sensor is completely dry (typically 3000-4000)
" "" "" "
Raw ADC value when sensor is fully wet (typically 1000-2000)
" "
" "" "
" "
" "
" "
" "

Danger Zone

" "

Irreversible actions that will reset your device

" "
" "

Factory Reset

" "

" "This will erase all settings including Wi-Fi credentials, plant profile, calibration data, and webhooks. " "The device will restart in setup mode.

" "" "
" "
" "" "
" "" ""; _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(); } else { // New SSID but no password provided - reject _server.send(400, "text/plain", "Password required for new network"); return; } 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")); if (_server.hasArg("tz")) settings.setTimezone(_server.arg("tz")); if (_server.hasArg("bed")) settings.setBedtime(_server.arg("bed")); if (_server.hasArg("wake")) settings.setWakeTime(_server.arg("wake")); _server.sendHeader("Location", "/"); _server.send(303); }); _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", "" "" "" "
" "
" "

Factory Reset

" "

Erasing all settings...

" "

Device will restart in setup mode.

" "

Reconnect to FacePlant-Setup network in 10 seconds.

" "
"); delay(1000); settings.factoryReset(); delay(1000); Serial.println("[WebUI] Restarting device..."); ESP.restart(); }); _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["timezone"] = settings.timezone(); doc["bedtime"] = settings.bedtime(); doc["wake_time"] = settings.wakeTime(); 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(); }