Framework updated
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -8,12 +8,37 @@
|
|||||||
; Please visit documentation for the other options and examples
|
; Please visit documentation for the other options and examples
|
||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env]
|
||||||
|
|
||||||
[env:esp32-s3-devkitc-1]
|
[env:esp32-s3-devkitc-1]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32-s3-devkitc-1
|
board = esp32-s3-devkitc-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
|
board_build.filesystem = littlefs
|
||||||
lib_deps =
|
lib_deps =
|
||||||
tzapu/WiFiManager@^2.0.17
|
tzapu/WiFiManager@^2.0.17
|
||||||
bblanchon/ArduinoJson@^7.0.0
|
bblanchon/ArduinoJson@^7.0.0
|
||||||
knolleary/PubSubClient@^2.8
|
knolleary/PubSubClient@^2.8
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
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
|
||||||
|
knolleary/PubSubClient@^2.8
|
||||||
|
|
||||||
|
[env:esp32-C3-devkitm-1]
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
framework = arduino
|
||||||
|
platform = espressif32
|
||||||
|
monitor_speed = 115200
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
lib_deps =
|
||||||
|
tzapu/WiFiManager@^2.0.17
|
||||||
|
bblanchon/ArduinoJson@^7.4.3
|
||||||
|
knolleary/PubSubClient@^2.8
|
||||||
+84
-37
@@ -41,12 +41,12 @@ struct AuthConfig {
|
|||||||
char pass[32] = "";
|
char pass[32] = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
static char hostname[32] = HOSTNAME_DEFAULT;
|
static char hostname[64] = HOSTNAME_DEFAULT;
|
||||||
static int meterCount = 0;
|
static int meterCount = 0;
|
||||||
static MeterConfig meters[MAX_METERS] = {};
|
static MeterConfig meters[MAX_METERS] = {};
|
||||||
static MqttConfig mqttCfg;
|
static MqttConfig mqttCfg;
|
||||||
static AuthConfig authCfg;
|
static AuthConfig authCfg;
|
||||||
static WiFiManagerParameter hostnameParam("hostname", "Device hostname", hostname, 32);
|
static WiFiManagerParameter hostnameParam("hostname", "Device hostname", hostname, 63);
|
||||||
static WebServer server(80);
|
static WebServer server(80);
|
||||||
static WiFiClient wifiClient;
|
static WiFiClient wifiClient;
|
||||||
static PubSubClient mqttClient(wifiClient);
|
static PubSubClient mqttClient(wifiClient);
|
||||||
@@ -413,6 +413,21 @@ static bool requireAuth() {
|
|||||||
// Web handlers
|
// 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 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 handleRoot() {
|
static void handleRoot() {
|
||||||
if (!requireAuth()) { Serial.println("[HTTP] GET / -> 401"); return; }
|
if (!requireAuth()) { Serial.println("[HTTP] GET / -> 401"); return; }
|
||||||
Serial.println("[HTTP] GET /");
|
Serial.println("[HTTP] GET /");
|
||||||
@@ -515,7 +530,7 @@ static void handleRoot() {
|
|||||||
html += ".count-row{display:flex;gap:12px;align-items:flex-end;margin-bottom:20px}.count-row > div{flex:1}";
|
html += ".count-row{display:flex;gap:12px;align-items:flex-end;margin-bottom:20px}.count-row > div{flex:1}";
|
||||||
html += ".count-row label{margin-bottom:4px}";
|
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{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}.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 += ".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 += ".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 += ".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)}";
|
||||||
@@ -532,6 +547,8 @@ html += ".switch input:checked+.slid{background:#4361ee}.switch input:checked+.s
|
|||||||
html += authSection;
|
html += authSection;
|
||||||
html += mqttSection;
|
html += mqttSection;
|
||||||
html += "<button class=btn type=submit>Save</button></form>";
|
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 += "<div class=mac>" + WiFi.macAddress() + "</div></div>";
|
||||||
html += "<script>";
|
html += "<script>";
|
||||||
html += "Array.from(document.querySelectorAll('input[type=range]')).forEach(function(s){";
|
html += "Array.from(document.querySelectorAll('input[type=range]')).forEach(function(s){";
|
||||||
@@ -550,6 +567,10 @@ html += ".switch input:checked+.slid{background:#4361ee}.switch input:checked+.s
|
|||||||
static void handleConfig() {
|
static void handleConfig() {
|
||||||
if (!requireAuth()) { Serial.println("[HTTP] POST /config -> 401"); return; }
|
if (!requireAuth()) { Serial.println("[HTTP] POST /config -> 401"); return; }
|
||||||
Serial.println("[HTTP] POST /config");
|
Serial.println("[HTTP] POST /config");
|
||||||
|
|
||||||
|
char oldHostname[64];
|
||||||
|
strlcpy(oldHostname, hostname, sizeof(oldHostname));
|
||||||
|
|
||||||
if (server.hasArg("hostname")) {
|
if (server.hasArg("hostname")) {
|
||||||
String val = server.arg("hostname");
|
String val = server.arg("hostname");
|
||||||
val.trim();
|
val.trim();
|
||||||
@@ -557,6 +578,8 @@ static void handleConfig() {
|
|||||||
val.toCharArray(hostname, sizeof(hostname));
|
val.toCharArray(hostname, sizeof(hostname));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hostnameChanged = strcmp(hostname, oldHostname) != 0;
|
||||||
|
|
||||||
// Persist the connection fields regardless of the enable checkbox so edits
|
// Persist the connection fields regardless of the enable checkbox so edits
|
||||||
// made while disabling MQTT aren't discarded.
|
// made while disabling MQTT aren't discarded.
|
||||||
if (server.hasArg("mqtt_host"))
|
if (server.hasArg("mqtt_host"))
|
||||||
@@ -623,6 +646,12 @@ static void handleConfig() {
|
|||||||
|
|
||||||
server.sendHeader("Location", "/", true);
|
server.sendHeader("Location", "/", true);
|
||||||
server.send(303);
|
server.send(303);
|
||||||
|
|
||||||
|
if (hostnameChanged) {
|
||||||
|
Serial.printf("[HTTP] hostname changed %s -> %s, restarting\n", oldHostname, hostname);
|
||||||
|
delay(200);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleSet() {
|
static void handleSet() {
|
||||||
@@ -643,19 +672,23 @@ static void handleSet() {
|
|||||||
|
|
||||||
static void startServer() {
|
static void startServer() {
|
||||||
server.on("/", handleRoot);
|
server.on("/", handleRoot);
|
||||||
|
server.on("/logo.png", handleLogo);
|
||||||
server.on("/config", HTTP_POST, handleConfig);
|
server.on("/config", HTTP_POST, handleConfig);
|
||||||
|
server.on("/reset", HTTP_POST, handleReset);
|
||||||
server.on("/set", handleSet);
|
server.on("/set", handleSet);
|
||||||
server.begin();
|
server.begin();
|
||||||
Serial.println("HTTP server started");
|
Serial.println("HTTP server started");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Factory reset (rapid reset 5× within a few seconds)
|
// Factory reset (5x = config only, 10x = config + WiFi credentials)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static void factoryReset() {
|
static void factoryReset(bool clearWifi) {
|
||||||
Serial.println("[BOOT] factory reset triggered!");
|
Serial.printf("[BOOT] factory reset triggered (clearWifi=%d)!\n", clearWifi);
|
||||||
|
|
||||||
|
// Preserve pin assignments and calibration across a config-only reset so
|
||||||
|
// the physical wiring doesn't need to be re-entered after every reset.
|
||||||
int count = meterCount;
|
int count = meterCount;
|
||||||
int pins[MAX_METERS];
|
int pins[MAX_METERS];
|
||||||
float maxD[MAX_METERS];
|
float maxD[MAX_METERS];
|
||||||
@@ -664,9 +697,9 @@ static void factoryReset() {
|
|||||||
maxD[i] = meters[i].maxDuty;
|
maxD[i] = meters[i].maxDuty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset hostname, auth, MQTT, meter metadata
|
// Reset hostname, auth, MQTT, and meter metadata
|
||||||
strlcpy(hostname, HOSTNAME_DEFAULT, sizeof(hostname));
|
strlcpy(hostname, HOSTNAME_DEFAULT, sizeof(hostname));
|
||||||
hostnameParam.setValue(hostname, strlen(hostname));
|
hostnameParam.setValue(hostname, sizeof(hostname) - 1);
|
||||||
|
|
||||||
authCfg.enabled = false;
|
authCfg.enabled = false;
|
||||||
authCfg.pass[0] = '\0';
|
authCfg.pass[0] = '\0';
|
||||||
@@ -688,14 +721,23 @@ static void factoryReset() {
|
|||||||
meters[i].rangeMax = 100.0;
|
meters[i].rangeMax = 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore pins and calibration
|
// Restore pins and calibration (unless WiFi reset, where a full clean slate
|
||||||
for (int i = 0; i < count; i++) {
|
// makes more sense since the device is being re-provisioned anyway).
|
||||||
meters[i].pin = pins[i];
|
if (!clearWifi) {
|
||||||
meters[i].maxDuty = maxD[i];
|
for (int i = 0; i < count; i++) {
|
||||||
|
meters[i].pin = pins[i];
|
||||||
|
meters[i].maxDuty = maxD[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|
||||||
|
if (clearWifi) {
|
||||||
|
WiFiManager wm;
|
||||||
|
wm.resetSettings();
|
||||||
|
Serial.println("[BOOT] WiFi credentials erased");
|
||||||
|
}
|
||||||
|
|
||||||
// Attach meters temporarily for the sweep acknowledgement
|
// Attach meters temporarily for the sweep acknowledgement
|
||||||
for (int i = 0; i < count && i < 8; i++) {
|
for (int i = 0; i < count && i < 8; i++) {
|
||||||
if (meters[i].pin > 0) {
|
if (meters[i].pin > 0) {
|
||||||
@@ -704,7 +746,7 @@ static void factoryReset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sweep 0→100→0 over ~2 seconds
|
// Sweep 0→100→0 over ~2 seconds as a visual confirmation
|
||||||
const int steps = 20;
|
const int steps = 20;
|
||||||
for (int phase = 0; phase < 2; phase++) {
|
for (int phase = 0; phase < 2; phase++) {
|
||||||
for (int s = 0; s <= steps; s++) {
|
for (int s = 0; s <= steps; s++) {
|
||||||
@@ -724,26 +766,24 @@ static void factoryReset() {
|
|||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void checkFactoryReset() {
|
static int incrementResetCount() {
|
||||||
const int THRESHOLD = 5;
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
File f = LittleFS.open("/reset_count", "r");
|
File f = LittleFS.open("/reset_count", "r");
|
||||||
if (f) {
|
if (f) { count = f.parseInt(); f.close(); }
|
||||||
count = f.parseInt();
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
count++;
|
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");
|
f = LittleFS.open("/reset_count", "w");
|
||||||
if (f) {
|
if (f) { f.print(count); f.close(); }
|
||||||
f.print(count);
|
Serial.printf("[BOOT] reset count %d\n", count);
|
||||||
f.close();
|
return count;
|
||||||
Serial.printf("[BOOT] reset count %d/%d\n", count, THRESHOLD);
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,22 +794,29 @@ static void checkFactoryReset() {
|
|||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("\n\n=== M1730 boot ===");
|
Serial.println("\n\n=== M1730 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();
|
loadConfig();
|
||||||
hostnameParam.setValue(hostname, strlen(hostname));
|
hostnameParam.setValue(hostname, sizeof(hostname) - 1);
|
||||||
|
|
||||||
checkFactoryReset();
|
int resetCount = incrementResetCount();
|
||||||
|
|
||||||
attachMeters();
|
attachMeters();
|
||||||
applyMeters();
|
applyMeters();
|
||||||
|
|
||||||
// Reset-counter settle window: if the device stays powered past this point it
|
// Settle window: if the device stays powered past this point it wasn't a
|
||||||
// wasn't a rapid power-cycle, so clear the counter here — before the (possibly
|
// rapid power-cycle. After the delay, act on the accumulated count (5x =
|
||||||
// indefinitely blocking) WiFi portal — so a stuck/offline boot can't slowly
|
// config reset, 10x = config + WiFi reset) then clear the counter.
|
||||||
// accumulate into a false factory reset.
|
|
||||||
delay(3000);
|
delay(3000);
|
||||||
LittleFS.remove("/reset_count");
|
applyFactoryResetIfNeeded(resetCount);
|
||||||
|
|
||||||
Serial.println("[WIFI] starting WiFiManager");
|
Serial.println("[WIFI] starting WiFiManager");
|
||||||
WiFiManager wm;
|
WiFiManager wm;
|
||||||
|
|||||||
Reference in New Issue
Block a user