feat: Add restart button functionality and motion calibration

- Implemented a restart button with debounce handling and long-press detection in App.cpp.
- Added motion calibration settings to Settings.cpp and Settings.h, allowing for roll and pitch zero offsets.
- Enhanced WebUI to include motion calibration controls and a restart option.
- Updated FaceRenderer to adjust eye and mouth positions based on screen dimensions.
- Introduced deadzone handling for motion sensor readings to improve stability.
This commit is contained in:
Joshua King
2026-02-22 13:58:59 -05:00
parent a8e8268b65
commit df9bd461d1
10 changed files with 392 additions and 84 deletions

View File

@@ -5,6 +5,7 @@
void WebUI::begin(Settings& settings,
WiFiManager& wifi,
MoistureSensor& moisture,
MotionSensor& motion,
FaceRenderer& face,
WebhookService& webhook,
unsigned long bootMs) {
@@ -23,6 +24,7 @@ void WebUI::begin(Settings& settings,
String timezone = settings.timezone();
String bedtime = settings.bedtime();
String wakeTime = settings.wakeTime();
MotionCalibration mc = settings.motionCalibration();
String page =
"<!DOCTYPE html><html><head>"
@@ -157,6 +159,26 @@ void WebUI::begin(Settings& settings,
"</form>"
"</div>"
"<div class='card'>"
"<h2><i class='fas fa-compass' style='margin-right: 10px;'></i>Accelerometer 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 Tilt</div>"
"<div style='font-size: 1em; color: #1d1d1f;'>Roll: " + String(motion.rollDeg(), 1) + "&deg;</div>"
"<div style='font-size: 1em; color: #1d1d1f;'>Pitch: " + String(motion.pitchDeg(), 1) + "&deg;</div>"
"<div style='font-size: 0.85em; color: #86868b; margin-top: 8px;'>"
"Neutral offsets: roll " + String(mc.rollZeroDeg, 1) + "&deg;, pitch " + String(mc.pitchZeroDeg, 1) + "&deg;</div>"
"</div>"
"<form method='POST' action='/motion-calibrate' style='margin-bottom: 12px;'>"
"<div class='btn-group'>"
"<button type='submit' name='action' value='zero_now' class='btn btn-primary'>Set Current Position as Neutral</button>"
"</div>"
"</form>"
"<form method='POST' action='/motion-calibrate'>"
"<div class='btn-group'>"
"<button type='submit' name='action' value='reset' class='btn btn-secondary'>Reset Accelerometer Calibration</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>"
@@ -184,6 +206,12 @@ void WebUI::begin(Settings& settings,
"<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='/restart' id='restartForm' style='margin-bottom: 20px;'>"
"<p style='margin-bottom: 15px;'><strong>Restart Device</strong></p>"
"<p style='color: #86868b; font-size: 0.9em; margin-bottom: 15px;'>"
"Reboots FacePlant without erasing settings.</p>"
"<button type='submit' class='btn btn-secondary' style='width: 100%;'>Restart</button>"
"</form>"
"<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;'>"
@@ -210,6 +238,12 @@ void WebUI::begin(Settings& settings,
" }"
" }"
"});"
"document.getElementById('restartForm').addEventListener('submit', function(e) {"
" e.preventDefault();"
" if (confirm('Restart FacePlant now?')) {"
" this.submit();"
" }"
"});"
"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.')) {"
@@ -283,6 +317,28 @@ void WebUI::begin(Settings& settings,
_server.send(303);
});
_server.on("/motion-calibrate", HTTP_POST, [&]() {
String action = _server.hasArg("action") ? _server.arg("action") : "zero_now";
if (action == "reset") {
settings.clearMotionCalibration();
motion.setZeroOffsets(0.0f, 0.0f);
Serial.println("[WebUI] Accelerometer calibration reset");
} else {
if (motion.available()) {
settings.setMotionCalibration(motion.rawRollDeg(), motion.rawPitchDeg());
motion.setZeroOffsets(motion.rawRollDeg(), motion.rawPitchDeg());
Serial.print("[WebUI] Accelerometer neutral set from current position: roll=");
Serial.print(motion.rawRollDeg(), 2);
Serial.print(" pitch=");
Serial.println(motion.rawPitchDeg(), 2);
} else {
Serial.println("[WebUI] Accelerometer calibration requested but MPU6050 unavailable");
}
}
_server.sendHeader("Location", "/");
_server.send(303);
});
_server.on("/factory-reset", HTTP_POST, [&]() {
Serial.println("[WebUI] Factory reset requested");
_server.send(200, "text/html",
@@ -311,6 +367,30 @@ void WebUI::begin(Settings& settings,
ESP.restart();
});
_server.on("/restart", HTTP_POST, [&]() {
Serial.println("[WebUI] Restart requested");
_server.send(200, "text/html",
"<!DOCTYPE html><html><head><meta http-equiv='refresh' content='8;url=/'>"
"<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>Restarting</h1>"
"<p>FacePlant is rebooting...</p>"
"<p>Settings are preserved.</p>"
"</div></body></html>");
delay(500);
ESP.restart();
});
_server.on("/status", HTTP_GET, [&]() {
JsonDocument doc;
doc["device"] = "FacePlant";
@@ -327,6 +407,11 @@ void WebUI::begin(Settings& settings,
doc["timezone"] = settings.timezone();
doc["bedtime"] = settings.bedtime();
doc["wake_time"] = settings.wakeTime();
doc["motion_ok"] = motion.available();
doc["motion_roll_deg"] = motion.rollDeg();
doc["motion_pitch_deg"] = motion.pitchDeg();
doc["motion_roll_zero_deg"] = motion.rollZeroDeg();
doc["motion_pitch_zero_deg"] = motion.pitchZeroDeg();
doc["dead_mode"] = face.isDeadMode();
doc["uptime_ms"] = millis() - bootMs;