Reset idleSinceMs on mode transitions to prevent stale timer carryover

This commit is contained in:
2026-03-16 22:36:33 -04:00
parent acce74e2ad
commit 63598bc453
+429 -36
View File
@@ -1,10 +1,12 @@
#include <Arduino.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include <PID_v1.h>
#include <esp_system.h>
#include <algorithm>
#include <cmath>
@@ -14,7 +16,7 @@
#include "ui_assets.h"
struct HistoryPoint {
unsigned long timestamp;
double timestamp;
float temp;
float setpoint;
float flightSetpoint;
@@ -30,7 +32,7 @@ struct Preset {
struct FlightConfig {
float takeoffSeconds = 300.0f;
float descentSeconds = 300.0f;
float descentSeconds = 600.0f;
bool turbo = false;
float descentTargetF = 120.0f;
};
@@ -61,16 +63,25 @@ struct AutotuneState {
std::vector<unsigned long> highTimes;
};
static constexpr int PIN_RELAY = D10;
static constexpr int PIN_MAX6675_SCK = D9;
static constexpr int PIN_MAX6675_CS = D8;
static constexpr int PIN_MAX6675_SO = D7;
struct EventEntry {
unsigned long id;
double ts;
String type;
String detail;
};
static constexpr int PIN_RELAY = 10; // XIAO D10 (GPIO10)
static constexpr int PIN_MAX6675_SCK = 9; // XIAO D9 (GPIO9)
static constexpr int PIN_MAX6675_CS = 8; // XIAO D8 (GPIO8)
static constexpr int PIN_MAX6675_SO = 20; // XIAO D7 (GPIO20)
static constexpr int PIN_BOOT_BUTTON = 9;
static constexpr unsigned long WIFI_RESET_HOLD_MS = 5000;
static constexpr float MAX_TEMP_F = 800.0f;
static constexpr float MIN_TEMP_F = 0.0f;
static constexpr float SPIKE_THRESHOLD_F = 50.0f;
static constexpr int IDLE_SHUTOFF_MINUTES = 30;
static constexpr float IDLE_NEAR_SETPOINT_F = 8.0f;
static constexpr float IDLE_NEAR_SETPOINT_F = 1.0f;
static constexpr bool IDLE_ONLY_IN_CRUISE = true;
static constexpr int LOOP_MIN_MS = 1500;
@@ -89,6 +100,22 @@ double ki = 3.5335;
double kd = 500.0;
PID pid(&inputF, &outputMs, &pidSetpointF, kp, ki, kd, DIRECT);
bool hasSensorBaseline = false;
int spikeRejectStreak = 0;
double lastHistoryTs = 0.0;
bool parseRequestJson(DynamicJsonDocument& req);
void setupMdns();
double nowTimestampSec();
void factoryResetWifiAndRestart() {
WiFiManager wm;
wm.resetSettings();
WiFi.disconnect(true, true);
delay(300);
ESP.restart();
}
bool enabled = false;
bool relayOn = false;
bool safetyTripped = false;
@@ -119,8 +146,147 @@ AutotuneState autotune;
std::vector<Preset> presets;
std::vector<HistoryPoint> history;
std::vector<EventEntry> events;
unsigned long nextEventId = 1;
String instanceId;
String mdnsHost = "pinail";
bool mdnsReady = false;
uint32_t bootCount = 0;
String resetReasonText = "unknown";
String lastPowerOffReason = "";
double lastPowerOffTs = 0.0;
bool onboardingApActive = false;
unsigned long onboardingApEndsMs = 0;
static constexpr unsigned long ONBOARDING_AP_GRACE_MS = 60000;
static constexpr const char* ONBOARDING_AP_SSID = "piNail-Setup";
static constexpr const char* ONBOARDING_AP_PASS = "pinailsetup";
String localIpString() {
IPAddress ip = WiFi.localIP();
if (ip == INADDR_NONE || ip.toString() == "0.0.0.0") {
return "";
}
return ip.toString();
}
void pushEvent(const String& type, const String& detail) {
EventEntry e;
e.id = nextEventId++;
e.ts = nowTimestampSec();
e.type = type;
e.detail = detail;
events.push_back(e);
if (events.size() > 250) {
events.erase(events.begin(), events.begin() + (events.size() - 250));
}
}
String resetReasonToString(esp_reset_reason_t r) {
switch (r) {
case ESP_RST_UNKNOWN: return "unknown";
case ESP_RST_POWERON: return "poweron";
case ESP_RST_EXT: return "external";
case ESP_RST_SW: return "software";
case ESP_RST_PANIC: return "panic";
case ESP_RST_INT_WDT: return "int_wdt";
case ESP_RST_TASK_WDT: return "task_wdt";
case ESP_RST_WDT: return "wdt";
case ESP_RST_DEEPSLEEP: return "deepsleep";
case ESP_RST_BROWNOUT: return "brownout";
case ESP_RST_SDIO: return "sdio";
default: return "other";
}
}
void recordPowerOffReason(const String& reason) {
lastPowerOffReason = reason;
lastPowerOffTs = nowTimestampSec();
prefs.putString("last_off_reason", reason);
prefs.putString("last_off_ts", String(lastPowerOffTs, 3));
pushEvent("power_off", reason);
}
String onboardingHtml() {
String ip = localIpString();
if (ip.isEmpty()) ip = "(waiting for DHCP)";
String body;
body.reserve(1200);
body += "<!doctype html><html><head><meta charset='utf-8'>";
body += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
body += "<title>piNail Connected</title>";
body += "<style>body{font-family:system-ui;background:#111;color:#eee;padding:16px}";
body += ".card{max-width:720px;margin:auto;background:#1b1b1b;border:1px solid #333;border-radius:12px;padding:16px}";
body += ".k{color:#aaa;font-size:13px}.v{font-size:22px;font-weight:700;margin-bottom:10px}";
body += "a{color:#ff8c3a}.small{font-size:13px;color:#bbb}</style></head><body><div class='card'>";
body += "<h2 style='margin-top:0'>Wi-Fi Connected</h2>";
body += "<div class='k'>Device IP</div><div class='v'>" + ip + "</div>";
body += "<div class='k'>Local alias</div><div class='v'>http://" + mdnsHost + ".local/</div>";
body += "<p class='small'>This setup AP stays online briefly so you can note the address, then turns off automatically.</p>";
body += "<p><a href='/wifi/reset'>Factory reset Wi-Fi credentials</a></p>";
body += "<p><a href='http://" + mdnsHost + ".local/'>Open via alias</a><br>";
if (ip != "(waiting for DHCP)") {
body += "<a href='http://" + ip + "/'>Open via IP</a>";
}
body += "</p></div></body></html>";
return body;
}
void maybeStopOnboardingAp() {
if (!onboardingApActive) return;
if (millis() < onboardingApEndsMs) return;
WiFi.softAPdisconnect(true);
onboardingApActive = false;
}
void startOnboardingGraceAp() {
WiFi.mode(WIFI_AP_STA);
if (WiFi.softAP(ONBOARDING_AP_SSID, ONBOARDING_AP_PASS)) {
onboardingApActive = true;
onboardingApEndsMs = millis() + ONBOARDING_AP_GRACE_MS;
}
}
void setupMdns() {
MDNS.end();
String mac = WiFi.macAddress();
mac.replace(":", "");
String fallback = "pinail-" + mac.substring(std::max(0, static_cast<int>(mac.length()) - 4));
if (MDNS.begin(mdnsHost.c_str())) {
mdnsReady = true;
} else if (MDNS.begin(fallback.c_str())) {
mdnsHost = fallback;
mdnsReady = true;
} else {
mdnsReady = false;
return;
}
MDNS.addService("http", "tcp", 80);
}
bool handleBootWifiResetHold() {
pinMode(PIN_BOOT_BUTTON, INPUT_PULLUP);
if (digitalRead(PIN_BOOT_BUTTON) != LOW) {
return false;
}
unsigned long start = millis();
while ((millis() - start) < WIFI_RESET_HOLD_MS) {
if (digitalRead(PIN_BOOT_BUTTON) != LOW) {
return false;
}
delay(20);
}
WiFiManager wm;
wm.resetSettings();
delay(200);
ESP.restart();
return true;
}
unsigned long nowEpochSec() {
time_t now = time(nullptr);
@@ -130,6 +296,10 @@ unsigned long nowEpochSec() {
return millis() / 1000;
}
double nowTimestampSec() {
return static_cast<double>(millis()) / 1000.0;
}
int controlTickMs() {
int tick = static_cast<int>(sleepTimeS * 1000.0f);
if (tick < 80) tick = 80;
@@ -272,6 +442,8 @@ float readMax6675F() {
}
void tripSafety(const String& reason) {
recordPowerOffReason(String("safety: ") + reason);
pushEvent("safety_trip", reason);
safetyTripped = true;
safetyReason = reason;
enabled = false;
@@ -285,6 +457,8 @@ void enterMode(const String& newMode) {
modeSinceMs = millis();
modeStartTemp = static_cast<float>(inputF);
modeStartEpochSec = nowEpochSec();
// Reset idle timer on any mode transition so prior accumulation doesn't carry over
idleSinceMs = 0;
}
void startTakeoff() {
@@ -304,14 +478,17 @@ void startDescent() {
enterMode("descent");
}
void setPower(bool on) {
void setPower(bool on, const String& offReason = "manual/api power off") {
if (!on) {
recordPowerOffReason(offReason);
pushEvent("power", "set false");
enabled = false;
relayOn = false;
digitalWrite(PIN_RELAY, LOW);
enterMode("grounded");
return;
}
pushEvent("power", "set true");
safetyTripped = false;
safetyReason = "";
if (mode == "grounded") {
@@ -344,7 +521,8 @@ void updateFlightSetpoint(unsigned long nowMs) {
float p = (nowMs - modeSinceMs) / (duration * 1000.0f);
if (p >= 1.0f) {
effectiveSetpointF = flight.descentTargetF;
setPower(false);
pushEvent("flight", "descent complete -> grounded");
setPower(false, "flight descent complete");
} else {
effectiveSetpointF = modeStartTemp + (flight.descentTargetF - modeStartTemp) * p;
}
@@ -415,6 +593,7 @@ void updateScheduler() {
if (!parseHm(t, h, m)) continue;
if (h == nowH && m == nowM) {
schedulerLastTriggerMinute = minuteKey;
pushEvent("scheduler", String("cutoff triggered at ") + t);
startDescent();
break;
}
@@ -429,6 +608,7 @@ String classifyTimingProfile(int loopMs, float sleepS) {
}
void stopAutotune(const String& message) {
pushEvent("autotune", message);
autotune.active = false;
autotune.message = message;
autotune.phase = "";
@@ -480,6 +660,7 @@ void completeAutotune() {
autotune.resKD = kd;
autotune.avgHigh = avgHigh;
autotune.avgLow = avgLow;
pushEvent("autotune", "complete");
stopAutotune("Autotune complete");
}
@@ -519,7 +700,12 @@ void updateAutotune(unsigned long nowMs) {
}
}
void pushHistory(unsigned long ts) {
void pushHistory(double ts) {
if (ts <= lastHistoryTs) {
ts = lastHistoryTs + 0.001;
}
lastHistoryTs = ts;
HistoryPoint p;
p.timestamp = ts;
p.temp = static_cast<float>(inputF);
@@ -548,10 +734,20 @@ void updateControl() {
return;
}
if (fabs(t - inputF) > SPIKE_THRESHOLD_F && inputF > 0) {
t = static_cast<float>(inputF);
if (!hasSensorBaseline) {
inputF = t;
hasSensorBaseline = true;
spikeRejectStreak = 0;
} else if (fabs(t - inputF) > SPIKE_THRESHOLD_F) {
spikeRejectStreak++;
if (spikeRejectStreak >= 3) {
inputF = t;
spikeRejectStreak = 0;
}
} else {
inputF = t;
spikeRejectStreak = 0;
}
inputF = t;
if (inputF > MAX_TEMP_F) {
tripSafety("Temperature exceeds max 800F");
@@ -562,7 +758,7 @@ void updateControl() {
relayOn = false;
outputMs = 0;
digitalWrite(PIN_RELAY, LOW);
pushHistory(nowEpochSec());
pushHistory(nowTimestampSec());
return;
}
@@ -576,7 +772,7 @@ void updateControl() {
if (idleSinceMs == 0) {
idleSinceMs = nowMs;
} else if ((nowMs - idleSinceMs) > static_cast<unsigned long>(IDLE_SHUTOFF_MINUTES) * 60000UL) {
tripSafety("Idle shutoff: within +/-8F for 30 minutes");
tripSafety("Idle shutoff: within +/-1F for 30 minutes");
return;
}
} else {
@@ -600,7 +796,7 @@ void updateControl() {
digitalWrite(PIN_RELAY, relayOn ? HIGH : LOW);
}
pushHistory(nowEpochSec());
pushHistory(nowTimestampSec());
}
void sendError(int code, const String& msg) {
@@ -608,12 +804,18 @@ void sendError(int code, const String& msg) {
doc["error"] = msg;
String out;
serializeJson(doc, out);
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send(code, "application/json", out);
}
void sendJson(const JsonDocument& doc) {
String body;
serializeJson(doc, body);
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send(200, "application/json", body);
}
@@ -633,6 +835,13 @@ void appendStatus(JsonObject obj, const String& nailName) {
obj["safety_reason"] = safetyReason;
obj["thermocouple_connected"] = thermocoupleConnected;
obj["instance_id"] = instanceId;
obj["ip"] = localIpString();
obj["mdns_host"] = mdnsHost;
obj["mdns_url"] = String("http://") + mdnsHost + ".local/";
obj["boot_count"] = bootCount;
obj["reset_reason"] = resetReasonText;
obj["last_power_off_reason"] = lastPowerOffReason;
obj["last_power_off_ts"] = lastPowerOffTs;
float modeEta = secondsToModeCompletion(nowMs);
if (modeEta >= 0) obj["mode_eta_seconds"] = modeEta;
@@ -719,6 +928,11 @@ void handleHeartbeat() {
doc["ok"] = true;
doc["instance_id"] = instanceId;
doc["ts"] = nowEpochSec();
doc["ip"] = localIpString();
doc["mdns_host"] = mdnsHost;
doc["mdns_url"] = String("http://") + mdnsHost + ".local/";
doc["boot_count"] = bootCount;
doc["reset_reason"] = resetReasonText;
JsonObject ca = doc.createNestedObject("controller_alive");
ca["nail1"] = true;
ca["nail2"] = true;
@@ -728,23 +942,166 @@ void handleHeartbeat() {
sendJson(doc);
}
void handleOnboardingStatus() {
DynamicJsonDocument out(512);
out["connected"] = WiFi.status() == WL_CONNECTED;
out["ip"] = localIpString();
out["mdns_host"] = mdnsHost;
out["mdns_url"] = String("http://") + mdnsHost + ".local/";
out["ap_active"] = onboardingApActive;
out["ap_ssid"] = ONBOARDING_AP_SSID;
out["ap_ip"] = WiFi.softAPIP().toString();
long secondsLeft = onboardingApActive ? static_cast<long>((onboardingApEndsMs - millis()) / 1000UL) : 0;
if (secondsLeft < 0) secondsLeft = 0;
out["ap_grace_seconds_remaining"] = secondsLeft;
sendJson(out);
}
void handleWifiResetPage() {
String body;
body.reserve(1200);
body += "<!doctype html><html><head><meta charset='utf-8'>";
body += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
body += "<title>Factory Reset Wi-Fi</title>";
body += "<style>body{font-family:system-ui;background:#111;color:#eee;padding:16px}";
body += ".card{max-width:640px;margin:auto;background:#1b1b1b;border:1px solid #333;border-radius:12px;padding:16px}";
body += "button{background:#8e2a2a;color:#fff;border:0;border-radius:8px;padding:10px 14px;cursor:pointer}";
body += "a{color:#ff8c3a}.small{font-size:13px;color:#bbb}</style></head><body><div class='card'>";
body += "<h2 style='margin-top:0'>Factory Reset Wi-Fi</h2>";
body += "<p>This clears saved Wi-Fi credentials and restarts the controller into onboarding mode.</p>";
body += "<p class='small'>Control settings and presets are preserved.</p>";
body += "<button onclick=\"if(confirm('Reset Wi-Fi credentials and reboot?')){fetch('/api/wifi/reset',{method:'POST'}).then(()=>{document.body.innerHTML='Rebooting... Reconnect to piNail-Setup in a few seconds.';});}\">Reset Wi-Fi Now</button>";
body += "<p style='margin-top:12px'><a href='/'>Back</a></p>";
body += "</div></body></html>";
server.send(200, "text/html", body);
}
void handleWifiResetApi() {
DynamicJsonDocument out(256);
out["ok"] = true;
out["message"] = "Resetting Wi-Fi credentials and rebooting";
sendJson(out);
pushEvent("wifi", "factory reset requested");
delay(250);
factoryResetWifiAndRestart();
}
void handleEvents() {
unsigned long sinceId = 0;
if (server.hasArg("since_id")) {
sinceId = static_cast<unsigned long>(server.arg("since_id").toInt());
}
int limit = 100;
if (server.hasArg("limit")) {
limit = server.arg("limit").toInt();
}
if (limit < 10) limit = 10;
if (limit > 250) limit = 250;
std::vector<const EventEntry*> outEvents;
outEvents.reserve(limit);
for (const auto& e : events) {
if (e.id <= sinceId) continue;
outEvents.push_back(&e);
if (static_cast<int>(outEvents.size()) > limit) {
outEvents.erase(outEvents.begin());
}
}
DynamicJsonDocument doc(128 + outEvents.size() * 120);
JsonArray arr = doc.to<JsonArray>();
for (const auto* e : outEvents) {
JsonObject o = arr.createNestedObject();
o["id"] = e->id;
o["ts"] = e->ts;
o["type"] = e->type;
o["detail"] = e->detail;
}
sendJson(doc);
}
void sendAppJsWithExtras() {
String body = FPSTR(APP_JS);
body += R"JS(
;
(function() {
function addWifiResetButton() {
if (document.getElementById('wifi-factory-reset-btn')) return;
var btn = document.createElement('button');
btn.id = 'wifi-factory-reset-btn';
btn.textContent = 'Factory Reset Wi-Fi';
btn.style.position = 'fixed';
btn.style.right = '12px';
btn.style.bottom = '12px';
btn.style.zIndex = '9999';
btn.style.background = '#8e2a2a';
btn.style.color = '#fff';
btn.style.border = '0';
btn.style.borderRadius = '8px';
btn.style.padding = '10px 12px';
btn.style.fontWeight = '700';
btn.style.cursor = 'pointer';
btn.onclick = function() {
var ok = confirm('Factory reset Wi-Fi credentials and reboot?\n\nYou will need to reconnect to piNail-Setup.');
if (!ok) return;
fetch('/api/wifi/reset', { method: 'POST' })
.then(function() {
alert('Rebooting. Reconnect to piNail-Setup in a few seconds.');
})
.catch(function() {
alert('Reset request failed. Try again from /wifi/reset');
});
};
document.body.appendChild(btn);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addWifiResetButton);
} else {
addWifiResetButton();
}
})();
)JS";
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send(200, "application/javascript", body);
}
void handleHistory() {
float since = 0;
double since = 0;
if (server.hasArg("since")) {
since = server.arg("since").toFloat();
}
DynamicJsonDocument doc(24576);
JsonArray arr = doc.to<JsonArray>();
int limit = 240;
if (server.hasArg("limit")) {
limit = server.arg("limit").toInt();
}
if (limit < 20) limit = 20;
if (limit > 500) limit = 500;
std::vector<const HistoryPoint*> points;
points.reserve(limit);
for (const auto& p : history) {
if (p.timestamp <= since) continue;
points.push_back(&p);
if (static_cast<int>(points.size()) > limit) {
points.erase(points.begin());
}
}
size_t cap = 512 + points.size() * 128;
DynamicJsonDocument doc(cap);
JsonArray arr = doc.to<JsonArray>();
for (const auto* p : points) {
JsonObject o = arr.createNestedObject();
o["timestamp"] = p.timestamp;
o["temp"] = p.temp;
o["setpoint"] = p.setpoint;
o["flight_setpoint"] = p.flightSetpoint;
o["mode"] = p.mode;
o["output"] = p.output;
o["relay"] = p.relay;
o["timestamp"] = p->timestamp;
o["temp"] = p->temp;
o["setpoint"] = p->setpoint;
o["flight_setpoint"] = p->flightSetpoint;
o["mode"] = p->mode;
o["output"] = p->output;
o["relay"] = p->relay;
}
sendJson(doc);
}
@@ -762,8 +1119,12 @@ bool parseRequestJson(DynamicJsonDocument& req) {
void handlePower() {
DynamicJsonDocument req(512);
if (!parseRequestJson(req)) return sendError(400, "invalid json");
bool target = req["enabled"].is<bool>() ? req["enabled"].as<bool>() : !enabled;
setPower(target);
if (!req["enabled"].is<bool>()) {
return sendError(400, "Missing boolean 'enabled' field");
}
bool target = req["enabled"].as<bool>();
pushEvent("api_power", String("request enabled=") + (target ? "true" : "false"));
setPower(target, "api power off");
DynamicJsonDocument out(256);
out["enabled"] = enabled;
out["ok"] = true;
@@ -929,6 +1290,7 @@ void handleFlight() {
if (req["mode"].is<const char*>()) {
String newMode = req["mode"].as<const char*>();
pushEvent("api_flight", String("mode=") + newMode);
if (newMode == "takeoff") startTakeoff();
else if (newMode == "cruise") startCruise();
else if (newMode == "descent") startDescent();
@@ -966,6 +1328,7 @@ void handleScheduler() {
scheduler.cutoffTimes = times;
}
saveSchedulerTimes();
pushEvent("scheduler", String("updated enabled=") + (scheduler.enabled ? "true" : "false"));
DynamicJsonDocument out(1024);
out["ok"] = true;
@@ -1164,11 +1527,26 @@ void handleNotFound() {
}
void setupRoutes() {
server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", INDEX_HTML); });
server.on("/", HTTP_GET, []() {
if (onboardingApActive && server.hostHeader().indexOf("192.168.4.1") >= 0) {
String page = onboardingHtml();
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send(200, "text/html", page);
return;
}
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send_P(200, "text/html", INDEX_HTML);
});
server.on("/wifi/reset", HTTP_GET, handleWifiResetPage);
server.on("/api/status", HTTP_GET, handleStatus);
server.on("/api/status/all", HTTP_GET, handleStatusAll);
server.on("/api/heartbeat", HTTP_GET, handleHeartbeat);
server.on("/api/history", HTTP_GET, handleHistory);
server.on("/api/events", HTTP_GET, handleEvents);
server.on("/api/power", HTTP_POST, handlePower);
server.on("/api/setpoint", HTTP_POST, handleSetpoint);
server.on("/api/control", HTTP_POST, handleControl);
@@ -1180,12 +1558,14 @@ void setupRoutes() {
server.on("/api/presets", HTTP_GET, handlePresetsGet);
server.on("/api/presets", HTTP_POST, handlePresetsPost);
server.on("/api/config", HTTP_GET, handleConfigGet);
server.on("/api/onboarding", HTTP_GET, handleOnboardingStatus);
server.on("/api/wifi/reset", HTTP_POST, handleWifiResetApi);
server.on("/api/autotune", HTTP_GET, handleAutotuneStatus);
server.on("/api/autotune/start", HTTP_POST, handleAutotuneStart);
server.on("/api/autotune/stop", HTTP_POST, handleAutotuneStop);
server.on("/static/style.css", HTTP_GET, []() { server.send_P(200, "text/css", STYLE_CSS); });
server.on("/static/app.js", HTTP_GET, []() { server.send_P(200, "application/javascript", APP_JS); });
server.on("/static/app.js", HTTP_GET, sendAppJsWithExtras);
server.on("/static/manifest.webmanifest", HTTP_GET, []() { server.send_P(200, "application/manifest+json", MANIFEST_JSON); });
server.on("/static/sw.js", HTTP_GET, []() { server.send_P(200, "application/javascript", SW_JS); });
server.on("/static/img/pinail_logo.svg", HTTP_GET, []() { server.send_P(200, "image/svg+xml", LOGO_SVG); });
@@ -1195,6 +1575,15 @@ void setupRoutes() {
}
void setup() {
Serial.begin(115200);
delay(250);
bootMs = millis();
instanceId = String(bootMs);
if (handleBootWifiResetHold()) {
return;
}
pinMode(PIN_RELAY, OUTPUT);
digitalWrite(PIN_RELAY, LOW);
pinMode(PIN_MAX6675_SCK, OUTPUT);
@@ -1203,12 +1592,12 @@ void setup() {
digitalWrite(PIN_MAX6675_SCK, HIGH);
digitalWrite(PIN_MAX6675_CS, HIGH);
Serial.begin(115200);
delay(250);
bootMs = millis();
instanceId = String(bootMs);
prefs.begin("pinail", false);
resetReasonText = resetReasonToString(esp_reset_reason());
bootCount = prefs.getUInt("boot_count", 0) + 1;
prefs.putUInt("boot_count", bootCount);
lastPowerOffReason = prefs.getString("last_off_reason", "");
lastPowerOffTs = prefs.getString("last_off_ts", "0").toFloat();
setpointF = prefs.getFloat("setpoint", 530.0f);
kp = prefs.getFloat("kP", 113.1768f);
ki = prefs.getFloat("kI", 3.5335f);
@@ -1219,7 +1608,7 @@ void setup() {
if (sleepTimeS < SLEEP_MIN_S || sleepTimeS > SLEEP_MAX_S) sleepTimeS = 0.2f;
flight.takeoffSeconds = prefs.getFloat("takeoff_s", 300.0f);
flight.descentSeconds = prefs.getFloat("descent_s", 300.0f);
flight.descentSeconds = prefs.getFloat("descent_s", 600.0f);
flight.descentTargetF = prefs.getFloat("descent_t", 120.0f);
flight.turbo = prefs.getBool("turbo", false);
@@ -1237,6 +1626,9 @@ void setup() {
if (!wifiOk) {
ESP.restart();
}
setupMdns();
startOnboardingGraceAp();
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
setupRoutes();
@@ -1247,6 +1639,7 @@ void setup() {
}
void loop() {
maybeStopOnboardingAp();
server.handleClient();
updateControl();
delay(2);