Port framework changes: logo, reset endpoint, hostname-change restart, 10x WiFi wipe, LittleFS logging
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -13,6 +13,7 @@ board = esp32dev
|
||||
framework = arduino
|
||||
platform = espressif32
|
||||
monitor_speed = 115200
|
||||
board_build.filesystem = littlefs
|
||||
lib_deps =
|
||||
tzapu/WiFiManager@^2.0.17
|
||||
bblanchon/ArduinoJson@^7.4.3
|
||||
|
||||
+75
-31
@@ -326,6 +326,13 @@ static bool requireAuth() {
|
||||
// Web handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void handleLogo() {
|
||||
File f = LittleFS.open("/logo.png", "r");
|
||||
if (!f) { server.send(404, "text/plain", "Not found"); return; }
|
||||
server.streamFile(f, "image/png");
|
||||
f.close();
|
||||
}
|
||||
|
||||
static void handleRoot() {
|
||||
if (!requireAuth()) { Serial.println("[HTTP] GET / -> 401"); return; }
|
||||
Serial.println("[HTTP] GET /");
|
||||
@@ -376,7 +383,8 @@ static void handleRoot() {
|
||||
html += "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}";
|
||||
html += "body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,sans-serif;background:#f2f4f8;color:#1a1a2e;display:flex;justify-content:center;align-items:flex-start;min-height:100vh;padding:24px 16px}";
|
||||
html += ".card{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.08);padding:32px;width:100%;max-width:560px}";
|
||||
html += "h1{font-size:24px;margin-bottom:4px}.sub{color:#666;font-size:14px;margin-bottom:24px}";
|
||||
html += ".hdr{display:flex;align-items:center;gap:16px;margin-bottom:24px}.hdr img{width:72px;height:72px;flex-shrink:0}";
|
||||
html += "h1{font-size:24px;margin-bottom:4px}.sub{color:#666;font-size:14px}";
|
||||
html += ".info{background:#f8f9fc;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:14px}";
|
||||
html += ".info div{display:flex;justify-content:space-between;padding:4px 0}.info div span:first-child{color:#666}";
|
||||
html += "label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}";
|
||||
@@ -388,30 +396,44 @@ static void handleRoot() {
|
||||
html += "fieldset .row label{width:70px;margin-bottom:0;flex-shrink:0}";
|
||||
html += "fieldset .row input{margin-bottom:0}";
|
||||
html += ".btn{width:100%;margin-top:8px;padding:12px;background:#4361ee;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:background .2s}";
|
||||
html += ".btn:hover{background:#3651d4}.mac{font-size:12px;color:#999;text-align:center;margin-top:20px}";
|
||||
html += ".btn:hover{background:#3651d4}.btn-danger{background:#dc2626}.btn-danger:hover{background:#b91c1c}";
|
||||
html += ".mac{font-size:12px;color:#999;text-align:center;margin-top:20px}";
|
||||
html += ".tgl-lbl{margin-bottom:0!important;line-height:18px}.switch{position:relative;display:inline-block;width:32px;height:18px;margin-left:auto;flex-shrink:0}fieldset .row label.switch{width:32px}";
|
||||
html += ".switch input{opacity:0;width:0;height:0;margin-bottom:0}.slid{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;border-radius:18px;transition:.3s}";
|
||||
html += ".slid::before{content:\"\";position:absolute;height:14px;width:14px;left:2px;top:2px;background:#fff;border-radius:50%;transition:.3s;box-shadow:0 1px 3px rgba(0,0,0,.2)}";
|
||||
html += ".switch input:checked+.slid{background:#4361ee}.switch input:checked+.slid::before{transform:translateX(14px)}";
|
||||
html += "</style></head><body><div class=card>";
|
||||
html += "<h1>Rotary Dial</h1><div class=sub>Configuration</div><div class=info>";
|
||||
html += "<div class=hdr><img src=/logo.png alt=logo><div><h1>Rotary Dial</h1><div class=sub>Configuration</div></div></div><div class=info>";
|
||||
html += "<div><span>Hostname</span><span>" + String(hostname) + "</span></div>";
|
||||
html += "<div><span>IP</span><span>" + WiFi.localIP().toString() + "</span></div></div>";
|
||||
html += "<form action=/config method=post>";
|
||||
html += "<label>Hostname</label><input name=hostname value='" + String(hostname) + "'>";
|
||||
html += "<label>Hostname</label><input name=hostname maxlength=63 value='" + String(hostname) + "'>";
|
||||
html += authSection;
|
||||
html += mqttSection;
|
||||
html += "<button class=btn type=submit>Save</button></form>";
|
||||
html += "<form action=/reset method=post style=margin-top:8px>";
|
||||
html += "<button class='btn btn-danger' type=submit>Reset Device</button></form>";
|
||||
html += "<div class=mac>" + WiFi.macAddress() + "</div></div>";
|
||||
html += "</body></html>";
|
||||
server.send(200, "text/html", html);
|
||||
Serial.printf("[HTTP] GET / -> 200 (%u bytes)\n", html.length());
|
||||
}
|
||||
|
||||
static void handleReset() {
|
||||
if (!requireAuth()) { Serial.println("[HTTP] POST /reset -> 401"); return; }
|
||||
Serial.println("[HTTP] POST /reset -> restarting");
|
||||
server.send(200, "text/plain", "Restarting...");
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
static void handleConfig() {
|
||||
if (!requireAuth()) { Serial.println("[HTTP] POST /config -> 401"); return; }
|
||||
Serial.println("[HTTP] POST /config");
|
||||
|
||||
char oldHostname[64];
|
||||
strlcpy(oldHostname, hostname, sizeof(oldHostname));
|
||||
|
||||
if (server.hasArg("hostname")) {
|
||||
String val = server.arg("hostname");
|
||||
val.trim();
|
||||
@@ -419,6 +441,8 @@ static void handleConfig() {
|
||||
val.toCharArray(hostname, sizeof(hostname));
|
||||
}
|
||||
|
||||
bool hostnameChanged = strcmp(hostname, oldHostname) != 0;
|
||||
|
||||
// Persist connection fields regardless of enable checkbox so edits made while
|
||||
// disabling MQTT aren't discarded.
|
||||
if (server.hasArg("mqtt_host"))
|
||||
@@ -454,24 +478,32 @@ static void handleConfig() {
|
||||
|
||||
server.sendHeader("Location", "/", true);
|
||||
server.send(303);
|
||||
|
||||
if (hostnameChanged) {
|
||||
Serial.printf("[HTTP] hostname changed %s -> %s, restarting\n", oldHostname, hostname);
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
static void startServer() {
|
||||
server.on("/", handleRoot);
|
||||
server.on("/logo.png", handleLogo);
|
||||
server.on("/config", HTTP_POST, handleConfig);
|
||||
server.on("/reset", HTTP_POST, handleReset);
|
||||
server.begin();
|
||||
Serial.println("HTTP server started");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory reset (rapid reset 5x within a few seconds)
|
||||
// Factory reset (5x = config only, 10x = config + WiFi credentials)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void factoryReset() {
|
||||
Serial.println("[BOOT] factory reset triggered!");
|
||||
static void factoryReset(bool clearWifi) {
|
||||
Serial.printf("[BOOT] factory reset triggered (clearWifi=%d)!\n", clearWifi);
|
||||
|
||||
strlcpy(hostname, HOSTNAME_DEFAULT, sizeof(hostname));
|
||||
hostnameParam.setValue(hostname, strlen(hostname));
|
||||
hostnameParam.setValue(hostname, sizeof(hostname) - 1);
|
||||
|
||||
authCfg.enabled = false;
|
||||
authCfg.pass[0] = '\0';
|
||||
@@ -485,31 +517,35 @@ static void factoryReset() {
|
||||
|
||||
saveConfig();
|
||||
|
||||
if (clearWifi) {
|
||||
WiFiManager wm;
|
||||
wm.resetSettings();
|
||||
Serial.println("[BOOT] WiFi credentials erased");
|
||||
}
|
||||
|
||||
Serial.println("[BOOT] factory reset complete, restarting...");
|
||||
delay(200);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
static void checkFactoryReset() {
|
||||
const int THRESHOLD = 5;
|
||||
static int incrementResetCount() {
|
||||
int count = 0;
|
||||
File f = LittleFS.open("/reset_count", "r");
|
||||
if (f) {
|
||||
count = f.parseInt();
|
||||
f.close();
|
||||
}
|
||||
if (f) { count = f.parseInt(); f.close(); }
|
||||
count++;
|
||||
if (count >= THRESHOLD) {
|
||||
LittleFS.remove("/reset_count");
|
||||
Serial.printf("[BOOT] reset count %d, triggering factory reset\n", count);
|
||||
factoryReset();
|
||||
return;
|
||||
}
|
||||
f = LittleFS.open("/reset_count", "w");
|
||||
if (f) {
|
||||
f.print(count);
|
||||
f.close();
|
||||
Serial.printf("[BOOT] reset count %d/%d\n", count, THRESHOLD);
|
||||
if (f) { f.print(count); f.close(); }
|
||||
Serial.printf("[BOOT] reset count %d\n", count);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void applyFactoryResetIfNeeded(int count) {
|
||||
const int THRESHOLD_CONFIG = 5;
|
||||
const int THRESHOLD_WIFI = 10;
|
||||
LittleFS.remove("/reset_count");
|
||||
if (count >= THRESHOLD_CONFIG) {
|
||||
Serial.printf("[BOOT] reset count %d, triggering factory reset\n", count);
|
||||
factoryReset(count >= THRESHOLD_WIFI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,18 +556,26 @@ static void checkFactoryReset() {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n\n=== Rotary Dial boot ===");
|
||||
LittleFS.begin(true);
|
||||
if (!LittleFS.begin(false)) {
|
||||
Serial.println("[FS] mount failed, formatting...");
|
||||
LittleFS.begin(true);
|
||||
} else {
|
||||
Serial.println("[FS] mounted OK");
|
||||
File root = LittleFS.open("/");
|
||||
File f = root.openNextFile();
|
||||
while (f) { Serial.printf("[FS] %s (%u bytes)\n", f.name(), f.size()); f = root.openNextFile(); }
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
hostnameParam.setValue(hostname, strlen(hostname));
|
||||
hostnameParam.setValue(hostname, sizeof(hostname) - 1);
|
||||
|
||||
checkFactoryReset();
|
||||
int resetCount = incrementResetCount();
|
||||
|
||||
// Reset-counter settle window: if the device stays powered past this point it
|
||||
// wasn't a rapid power-cycle, so clear the counter before the (possibly
|
||||
// indefinitely blocking) WiFi portal.
|
||||
// Settle window: if the device stays powered past this point it wasn't a
|
||||
// rapid power-cycle. After the delay, act on the accumulated count (5x =
|
||||
// config reset, 10x = config + WiFi reset) then clear the counter.
|
||||
delay(3000);
|
||||
LittleFS.remove("/reset_count");
|
||||
applyFactoryResetIfNeeded(resetCount);
|
||||
|
||||
pinMode(DIAL_PIN, INPUT_PULLUP);
|
||||
attachInterrupt(digitalPinToInterrupt(DIAL_PIN), onDialPulse, RISING);
|
||||
|
||||
Reference in New Issue
Block a user