feat: Implement initial ESP32 school bell system with web UI, scheduling, and DFPlayer control.
This commit is contained in:
1381
data/setting.html
1381
data/setting.html
File diff suppressed because it is too large
Load Diff
156
src/main.cpp
156
src/main.cpp
@@ -18,11 +18,11 @@
|
||||
#include <RTClib.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#include <tcpip_adapter.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <limits.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <DFRobotDFPlayerMini.h>
|
||||
@@ -408,7 +408,7 @@ void initWebServer() {
|
||||
});
|
||||
|
||||
// backup: return config + schedules as JSON
|
||||
server.on("/api/backup", HTTP_GET, [](AsyncWebServerRequest *req){
|
||||
server.on("/api/backup/download", HTTP_GET, [](AsyncWebServerRequest *req){
|
||||
DynamicJsonDocument doc(4096);
|
||||
doc["schoolName"] = schoolName;
|
||||
doc["user"] = adminUser;
|
||||
@@ -483,6 +483,142 @@ void initWebServer() {
|
||||
broadcastStatus();
|
||||
});
|
||||
|
||||
// Internal: List Backups
|
||||
server.on("/api/backup/list", HTTP_GET, [](AsyncWebServerRequest *req){
|
||||
JsonDocument doc; // v7: automatic storage
|
||||
JsonArray arr = doc.to<JsonArray>();
|
||||
|
||||
Serial.println("[BACKUP] Listing files...");
|
||||
File root = LittleFS.open("/");
|
||||
if (root && root.isDirectory()) {
|
||||
File file = root.openNextFile();
|
||||
while(file){
|
||||
String fname = String(file.name());
|
||||
// Handle paths with or without leading slash
|
||||
if (fname.indexOf("backup_") >= 0 && fname.endsWith(".json")) {
|
||||
JsonObject o = arr.createNestedObject();
|
||||
if (!fname.startsWith("/")) fname = "/" + fname;
|
||||
o["name"] = fname;
|
||||
o["size"] = file.size();
|
||||
Serial.printf("[BACKUP] Found: %s (%d bytes)\n", fname.c_str(), file.size());
|
||||
}
|
||||
file = root.openNextFile();
|
||||
}
|
||||
} else {
|
||||
Serial.println("[BACKUP] Failed to open root directory");
|
||||
}
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// Internal: Save with Timestamp
|
||||
server.on("/api/backup/save", HTTP_POST, [](AsyncWebServerRequest *req){
|
||||
DateTime now = rtc.now();
|
||||
char filename[32];
|
||||
snprintf(filename, sizeof(filename), "/backup_%04d-%02d-%02d_%02d-%02d.json",
|
||||
now.year(), now.month(), now.day(), now.hour(), now.minute());
|
||||
|
||||
DynamicJsonDocument doc(4096);
|
||||
doc["schoolName"] = schoolName;
|
||||
doc["user"] = adminUser;
|
||||
doc["pass"] = adminPass;
|
||||
doc["skipSunday"] = skipSunday;
|
||||
JsonArray arr = doc.createNestedArray("schedules");
|
||||
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
||||
JsonObject o = arr.createNestedObject();
|
||||
o["jam"] = schedules[i].jam;
|
||||
o["menit"] = schedules[i].menit;
|
||||
o["track"] = schedules[i].track;
|
||||
o["desc"] = schedules[i].desc;
|
||||
o["enabled"] = schedules[i].enabled;
|
||||
}
|
||||
File f = LittleFS.open(filename, "w");
|
||||
if (f) {
|
||||
serializeJson(doc, f);
|
||||
f.close();
|
||||
DynamicJsonDocument d(256);
|
||||
d["type"] = "log";
|
||||
d["msg"] = String("💾 Backup tersimpan: ") + filename;
|
||||
sendJsonWs(d);
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
} else {
|
||||
req->send(500, "application/json", "{\"ok\":false,\"error\":\"FS Error\"}");
|
||||
}
|
||||
});
|
||||
|
||||
// Internal: Load Specific Backup
|
||||
server.on("/api/backup/load", HTTP_POST, [](AsyncWebServerRequest *req){},
|
||||
NULL, [](AsyncWebServerRequest *req, uint8_t *data, size_t len, size_t, size_t){
|
||||
DynamicJsonDocument input(256);
|
||||
if (deserializeJson(input, data, len)) { req->send(400); return; }
|
||||
String filename = input["filename"] | "";
|
||||
if (!filename.startsWith("/")) filename = "/" + filename;
|
||||
|
||||
if (!LittleFS.exists(filename)) {
|
||||
req->send(404, "application/json", "{\"ok\":false,\"error\":\"File not found\"}");
|
||||
return;
|
||||
}
|
||||
File f = LittleFS.open(filename, "r");
|
||||
if (!f) {
|
||||
req->send(500, "application/json", "{\"ok\":false,\"error\":\"Read Error\"}");
|
||||
return;
|
||||
}
|
||||
DynamicJsonDocument doc(4096);
|
||||
DeserializationError err = deserializeJson(doc, f);
|
||||
f.close();
|
||||
|
||||
if (err) {
|
||||
req->send(500, "application/json", "{\"ok\":false,\"error\":\"JSON Error\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore
|
||||
if (doc.containsKey("schoolName")) schoolName = doc["schoolName"].as<String>();
|
||||
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
||||
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
||||
if (doc.containsKey("skipSunday")) skipSunday = doc["skipSunday"];
|
||||
saveConfig();
|
||||
|
||||
if (doc.containsKey("schedules")) {
|
||||
JsonArray arr = doc["schedules"];
|
||||
uint8_t cnt = min((size_t)MAX_SCHEDULES, arr.size());
|
||||
scheduleCount = cnt;
|
||||
for (uint8_t i = 0; i < cnt; ++i) {
|
||||
schedules[i].jam = arr[i]["jam"] | 0;
|
||||
schedules[i].menit = arr[i]["menit"] | 0;
|
||||
schedules[i].track = arr[i]["track"] | 1;
|
||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||
}
|
||||
saveSchedules();
|
||||
}
|
||||
|
||||
DynamicJsonDocument d(256);
|
||||
d["type"] = "log";
|
||||
d["msg"] = "♻️ Backup dimuat: " + filename;
|
||||
sendJsonWs(d);
|
||||
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
broadcastStatus();
|
||||
});
|
||||
|
||||
// Internal: Delete Backup
|
||||
server.on("/api/backup/delete", HTTP_POST, [](AsyncWebServerRequest *req){},
|
||||
NULL, [](AsyncWebServerRequest *req, uint8_t *data, size_t len, size_t, size_t){
|
||||
DynamicJsonDocument input(256);
|
||||
if (deserializeJson(input, data, len)) { req->send(400); return; }
|
||||
String filename = input["filename"] | "";
|
||||
if (!filename.startsWith("/")) filename = "/" + filename;
|
||||
|
||||
if (LittleFS.exists(filename)) {
|
||||
LittleFS.remove(filename);
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
} else {
|
||||
req->send(404, "application/json", "{\"ok\":false,\"error\":\"File not found\"}");
|
||||
}
|
||||
});
|
||||
|
||||
// websocket handler: when new client connects, send time + status
|
||||
ws.onEvent([](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len){
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
@@ -516,10 +652,7 @@ void resetAdminToDefault() {
|
||||
// Setup & Loop
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
void setup(){
|
||||
// Initialize Task Watchdog Timer (TWDT) to prevent watchdog resets
|
||||
// Disable panic mode and set longer timeout to prevent resets
|
||||
esp_task_wdt_init(120, false); // 120 seconds timeout, don't panic on timeout
|
||||
esp_task_wdt_add(NULL); // Add current task to watchdog
|
||||
|
||||
|
||||
pinMode(STATUS_LED_PIN, OUTPUT);
|
||||
digitalWrite(STATUS_LED_PIN, LOW);
|
||||
@@ -639,8 +772,7 @@ void loop(){
|
||||
resetAdminToDefault();
|
||||
}
|
||||
|
||||
// Reset watchdog early in the loop
|
||||
esp_task_wdt_reset();
|
||||
|
||||
|
||||
// every second: send time & status
|
||||
if (millis() - lastTimeWS >= 1000) {
|
||||
@@ -706,8 +838,7 @@ void loop(){
|
||||
if (relayHoldUntil < millis() + 15000UL) relayHoldUntil = millis() + 15000UL;
|
||||
}
|
||||
|
||||
// Reset watchdog during schedule processing
|
||||
esp_task_wdt_reset();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,8 +864,7 @@ void loop(){
|
||||
digitalWrite(BUZZER_PIN, LOW);
|
||||
}
|
||||
|
||||
// Reset Task Watchdog Timer to prevent timeout
|
||||
esp_task_wdt_reset();
|
||||
|
||||
|
||||
// Use vTaskDelay instead of delay() to allow other tasks to run
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); // tiny yield
|
||||
|
||||
Reference in New Issue
Block a user