diff --git a/README.md b/README.md index 049e681..42c839e 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,19 @@ After connecting, the device is reachable at: - `http://m1730.local` (mDNS, works on most local networks) - `http://` (shown in the serial monitor on boot) +### Factory reset (forgot password) + +If you lose the web UI password, hold the **BOOT button** (GPIO0) for 3 seconds during startup. The firmware resets all settings to factory defaults **except** meter pin assignments and calibration (maxDuty), which are preserved. On acknowledgement, all connected meters sweep 0→100→0. + +Serial output confirms the reset: + +``` +[BOOT] BOOT button held, hold 3s for factory reset... +[BOOT] factory reset triggered! +... +[BOOT] factory reset complete, restarting... +``` + --- ## Web UI @@ -223,4 +236,4 @@ Config is stored as JSON in LittleFS at `/config.json`. } ``` -The file is written by the web UI and should not need manual editing. To reset to factory defaults, delete the file or erase flash with `pio run --target erase`. +The file is written by the web UI and should not need manual editing. To reset to factory defaults (keeping pin/calibration), hold the BOOT button for 3 seconds at startup. To erase everything including flash, use `pio run --target erase`. diff --git a/src/main.cpp b/src/main.cpp index 61986fa..da3d82d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -477,7 +477,7 @@ static void handleRoot() { html += ".count-row label{margin-bottom:4px}"; 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 += ".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}"; +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}.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)}"; @@ -612,6 +612,109 @@ static void startServer() { Serial.println("HTTP server started"); } +// --------------------------------------------------------------------------- +// Factory reset (hold BOOT button 3s at startup) +// --------------------------------------------------------------------------- + +static void factoryReset() { + Serial.println("[BOOT] factory reset triggered!"); + + int count = meterCount; + int pins[MAX_METERS]; + float maxD[MAX_METERS]; + float cur[MAX_METERS]; + for (int i = 0; i < count; i++) { + pins[i] = meters[i].pin; + maxD[i] = meters[i].maxDuty; + cur[i] = meters[i].currentValue; + } + + // Reset hostname, auth, MQTT, meter metadata + strlcpy(hostname, HOSTNAME_DEFAULT, sizeof(hostname)); + hostnameParam.setValue(hostname, strlen(hostname)); + + authCfg.enabled = false; + authCfg.pass[0] = '\0'; + + mqttCfg.enabled = false; + mqttCfg.host[0] = '\0'; + mqttCfg.port = 1883; + mqttCfg.user[0] = '\0'; + mqttCfg.pass[0] = '\0'; + strlcpy(mqttCfg.prefix, "m1730", sizeof(mqttCfg.prefix)); + + for (int i = 0; i < MAX_METERS; i++) { + meters[i].pin = 0; + meters[i].maxDuty = 0; + meters[i].currentValue = 0; + meters[i].name[0] = '\0'; + meters[i].unit[0] = '\0'; + meters[i].rangeMin = 0.0; + meters[i].rangeMax = 100.0; + } + + // Restore pins, calibration, current state + for (int i = 0; i < count; i++) { + meters[i].pin = pins[i]; + meters[i].maxDuty = maxD[i]; + meters[i].currentValue = cur[i]; + } + + saveConfig(); + + // Attach meters temporarily for the sweep acknowledgement + for (int i = 0; i < count && i < 8; i++) { + if (meters[i].pin > 0) { + ledcSetup(i, PWM_FREQ, PWM_RES); + ledcAttachPin(meters[i].pin, i); + } + } + + // Sweep 0→100→0 over ~2 seconds + const int steps = 20; + for (int phase = 0; phase < 2; phase++) { + for (int s = 0; s <= steps; s++) { + float pct = (phase == 0) ? (float)s / steps * 100.0f : (float)(steps - s) / steps * 100.0f; + for (int i = 0; i < count && i < 8; i++) { + if (meters[i].pin > 0 && meters[i].maxDuty > 0) { + float duty = pct / 100.0f * meters[i].maxDuty / 100.0f; + ledcWrite(i, constrain((int)(duty * 1023), 0, 1023)); + } + } + delay(50); + } + } + + // Restore current values + for (int i = 0; i < count && i < 8; i++) { + if (meters[i].pin > 0 && meters[i].maxDuty > 0) { + float pct = cur[i] / 100.0f * meters[i].maxDuty / 100.0f; + ledcWrite(i, constrain((int)(pct * 1023), 0, 1023)); + } + } + + Serial.println("[BOOT] factory reset complete, restarting..."); + delay(200); + ESP.restart(); +} + +static void checkFactoryReset() { + pinMode(0, INPUT_PULLUP); + delay(10); + if (digitalRead(0) == LOW) { + Serial.println("[BOOT] BOOT button held, hold 3s for factory reset..."); + unsigned long start = millis(); + while (millis() - start < 3000) { + delay(50); + if (digitalRead(0) != LOW) { + Serial.println("[BOOT] factory reset aborted (button released)"); + return; + } + } + factoryReset(); + } +} + // --------------------------------------------------------------------------- // Setup & loop // --------------------------------------------------------------------------- @@ -624,6 +727,8 @@ void setup() { loadConfig(); hostnameParam.setValue(hostname, strlen(hostname)); + checkFactoryReset(); + attachMeters(); applyMeters();