Timer funktionalität debug discord nachrichten zum finden von geschwindigkiets verlust. READEME
This commit is contained in:
parent
df4b67dab3
commit
da45700cc7
4 changed files with 347 additions and 15 deletions
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Bibliotheken
|
||||
|
||||
* *ESP Async WebServer* von ESP32Async
|
||||
* *Async TCP* von ESP32Async
|
||||
* *ArduinoJson* von Benoit Blanchon, Version 7.x
|
||||
* *FastAccelStepper* von Jochen Kiemes
|
||||
* *U8g2* von oliver
|
||||
|
||||
# Partitions mode
|
||||
|
||||
* No OTA 2MB APP / 2MB SPIFFS
|
||||
285
kugelmuehle.ino
285
kugelmuehle.ino
|
|
@ -44,6 +44,49 @@ bool stopMove = false;
|
|||
// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr)
|
||||
bool webRun = false;
|
||||
|
||||
// DEBUG
|
||||
// Laufzeit-Vorgabe per Web (0 = unbegrenzt).
|
||||
// Bewusst NICHT im Flash gespeichert: nach einem Neustart läuft die Mühle nicht von allein weiter.
|
||||
uint32_t webRunStartMs = 0;
|
||||
uint32_t webRunDurMs = 0;
|
||||
|
||||
// Wird gesetzt, wenn die Laufzeit-Vorgabe abgelaufen ist -> löst die Discord-Nachricht aus
|
||||
bool timedFinished = false;
|
||||
uint32_t timedFinishedSec = 0;
|
||||
|
||||
// Drehzahl-Wächter: meldet per Discord, wenn die Ist-Drehzahl im Automatik-Lauf dauerhaft
|
||||
// mehr als 10 % von der Soll-Drehzahl abweicht
|
||||
uint32_t devSinceMs = 0;
|
||||
bool devAlarmSent = false;
|
||||
bool devAlarmPending = false;
|
||||
int32_t devSoll = 0, devIst = 0;
|
||||
|
||||
// Meldung nach jedem Neustart des ESP (mit Grund), sobald das WLAN verbunden ist
|
||||
bool bootNotifyPending = true;
|
||||
|
||||
// Positions-Wächter: berechnet die Drehzahl aus den tatsächlich ausgegebenen Schritten
|
||||
uint32_t posWinStart = 0;
|
||||
int32_t posWinPos = 0;
|
||||
bool posWinValid = false;
|
||||
int32_t posWinSoll = 0;
|
||||
uint8_t posBadWindows = 0;
|
||||
bool posAlarmSent = false;
|
||||
bool posAlarmPending = false;
|
||||
int32_t posRpm = 0, posSoll = 0, posShown = 0;
|
||||
|
||||
// Meldung, wenn die Soll-Drehzahl (autoSpeed) während des Laufs geändert wurde
|
||||
int32_t lastSeenAutoSpeed = -1;
|
||||
int32_t speedChangeFrom = 0;
|
||||
uint32_t speedChangeAtMs = 0;
|
||||
bool speedChangePending = false;
|
||||
bool speedChangeWhileRunning = false;
|
||||
bool speedChangeViaWeb = false;
|
||||
|
||||
// Laufzeitmessung: Zeit seit dem letzten Start des Motors (bleibt nach dem Stopp stehen)
|
||||
uint32_t runStartMs = 0;
|
||||
uint32_t runSeconds = 0;
|
||||
bool motorWasRunning = false;
|
||||
|
||||
struct tm timeinfo;
|
||||
|
||||
TimeEditStep timeEditStep = EDIT_NONE;
|
||||
|
|
@ -77,6 +120,7 @@ void loadSettings() {
|
|||
settings.maxTemp = prefs.getShort("max_temp", 70);
|
||||
settings.coldTemp = prefs.getShort("cold_temp", 50);
|
||||
settings.accelRate = prefs.getInt("accel_rate", 20000);
|
||||
settings.autoSpeed = prefs.getInt("auto_speed", 700);
|
||||
|
||||
prefs.end();
|
||||
Serial.println("[NVS] Einstellungen erfolgreich geladen.");
|
||||
|
|
@ -92,8 +136,9 @@ void saveSettings() {
|
|||
prefs.putUChar("e_min", settings.endMin);
|
||||
prefs.putBool("t_on", settings.timerEnabled);
|
||||
prefs.putShort("max_temp", settings.maxTemp);
|
||||
prefs.putShort("cold_temp", settings.coldTemp);
|
||||
prefs.putShort("cold_temp", settings.coldTemp);
|
||||
prefs.putInt("accel_rate", settings.accelRate);
|
||||
prefs.putInt("auto_speed", settings.autoSpeed);
|
||||
|
||||
prefs.end();
|
||||
Serial.println("[NVS] Einstellungen im Flash gespeichert.");
|
||||
|
|
@ -113,6 +158,33 @@ void initWiFiAndTime() {
|
|||
}
|
||||
}
|
||||
|
||||
// Sekunden -> lesbarer Text, z. B. "2 T 3 Std 0 Min"
|
||||
String formatDuration(uint32_t s) {
|
||||
uint32_t d = s / 86400; s %= 86400;
|
||||
uint32_t h = s / 3600; s %= 3600;
|
||||
uint32_t m = s / 60;
|
||||
String out;
|
||||
if (d > 0) out += String(d) + " T ";
|
||||
out += String(h) + " Std " + String(m) + " Min";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Grund des letzten Neustarts als lesbarer Text
|
||||
const char* resetReasonText(esp_reset_reason_t r) {
|
||||
switch (r) {
|
||||
case ESP_RST_POWERON: return "Spannung eingeschaltet (Power-On)";
|
||||
case ESP_RST_EXT: return "Reset ueber Reset-Pin";
|
||||
case ESP_RST_SW: return "Software-Neustart";
|
||||
case ESP_RST_PANIC: return "ABSTURZ (Panic/Exception)";
|
||||
case ESP_RST_INT_WDT: return "ABSTURZ (Interrupt-Watchdog)";
|
||||
case ESP_RST_TASK_WDT: return "ABSTURZ (Task-Watchdog)";
|
||||
case ESP_RST_WDT: return "ABSTURZ (Watchdog)";
|
||||
case ESP_RST_DEEPSLEEP: return "Aufwachen aus Deep-Sleep";
|
||||
case ESP_RST_BROWNOUT: return "Spannungseinbruch (Brownout)";
|
||||
default: return "unbekannt";
|
||||
}
|
||||
}
|
||||
|
||||
void sendDiscordNotification(const String& message) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[Discord] Nicht verbunden mit WiFi.");
|
||||
|
|
@ -164,7 +236,15 @@ void handleWebCommands() {
|
|||
|
||||
case WC_RUN:
|
||||
// Nur ohne Zeitschaltuhr erlaubt
|
||||
if (!settings.timerEnabled) webRun = (c.v[0] != 0);
|
||||
if (!settings.timerEnabled) {
|
||||
webRun = (c.v[0] != 0);
|
||||
if (webRun) {
|
||||
// v[1] = Dauer in Sekunden (0 = unbegrenzt, max. 30 Tage)
|
||||
uint32_t sec = (uint32_t)constrain(c.v[1], 0, 2592000);
|
||||
webRunStartMs = millis();
|
||||
webRunDurMs = sec * 1000UL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WC_MAXTEMP:
|
||||
|
|
@ -206,6 +286,19 @@ void sendWebStatus(int32_t rpm, bool timeValid, int h, int m, bool overheat) {
|
|||
d["maxRpm"] = (MAX_SPEED_HZ * 60) / MIKROSTEPS;
|
||||
d["accel"] = settings.accelRate;
|
||||
d["sh"] = settings.startHour; d["sm"] = settings.startMin;
|
||||
d["eh"] = settings.endHour; d["em"] = settings.endMin;
|
||||
|
||||
// Laufzeit seit letztem Start + Restzeit der Laufzeit-Vorgabe (alles in Sekunden)
|
||||
uint32_t remainSec = 0;
|
||||
bool timed = (webRun && webRunDurMs > 0);
|
||||
if (timed) {
|
||||
uint32_t el = millis() - webRunStartMs;
|
||||
remainSec = (el >= webRunDurMs) ? 0 : (webRunDurMs - el + 999) / 1000;
|
||||
}
|
||||
d["runSec"] = runSeconds;
|
||||
d["timed"] = timed;
|
||||
d["remain"] = remainSec;
|
||||
d["sh"] = settings.startHour; d["sm"] = settings.startMin;
|
||||
d["eh"] = settings.endHour; d["em"] = settings.endMin;
|
||||
|
||||
char t[6];
|
||||
|
|
@ -228,6 +321,8 @@ void setup() {
|
|||
|
||||
// Daten aus Flash speicher laden
|
||||
loadSettings();
|
||||
lastSeenAutoSpeed = settings.autoSpeed;
|
||||
|
||||
|
||||
engine.init();
|
||||
stepper = engine.stepperConnectToPin(stepPinStepper);
|
||||
|
|
@ -523,9 +618,6 @@ bool checkOverHeat(int sensorPin){
|
|||
return overheated;
|
||||
}
|
||||
|
||||
// ################## TODO #########################
|
||||
// Betriebsmodi in funktions auslagern
|
||||
|
||||
void printStatus(FastAccelStepper *stepper, int speed, int clickPlus, int clickMinus){
|
||||
// Plotter-Ausgabe alle 50 ms (überflutet den Bus nicht)
|
||||
if (millis() - lastPlotTime >= 50) {
|
||||
|
|
@ -561,7 +653,8 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
|||
lastAutoHz = 0;
|
||||
break;
|
||||
}
|
||||
case MOTOR_AUTO: {
|
||||
|
||||
case MOTOR_AUTO:{
|
||||
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
||||
if (!stepper->isRunning()) {
|
||||
stepper->setSpeedInHz(autoHz);
|
||||
|
|
@ -576,7 +669,8 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case MOTOR_MANUAL: {
|
||||
|
||||
case MOTOR_MANUAL:{
|
||||
if (speed != lastSpeed) {
|
||||
stepper->setSpeedInHz(MAX_SPEED_HZ);
|
||||
stepper->setAcceleration(settings.accelRate);
|
||||
|
|
@ -586,11 +680,14 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case MOTOR_KEEP:{
|
||||
|
||||
}
|
||||
default:
|
||||
case MOTOR_KEEP:{
|
||||
break;
|
||||
}
|
||||
|
||||
default:{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -613,6 +710,13 @@ void loop() {
|
|||
// Befehle von der Weboberfläche abarbeiten
|
||||
handleWebCommands();
|
||||
|
||||
// Laufzeit-Vorgabe abgelaufen? -> Web-Lauf beenden (Motor wird unten sauber angehalten)
|
||||
if (webRun && webRunDurMs > 0 && (millis() - webRunStartMs) >= webRunDurMs) {
|
||||
webRun = false;
|
||||
timedFinishedSec = webRunDurMs / 1000; // Dauer sichern, bevor sie weiter unten zurückgesetzt wird
|
||||
timedFinished = true;
|
||||
}
|
||||
|
||||
// 2. Zeitschaltuhr & NTP prüfen
|
||||
int curH, curM, curS;
|
||||
bool timeIsValid = getCurrentTime(curH, curM, curS);
|
||||
|
|
@ -634,6 +738,7 @@ void loop() {
|
|||
// HotKey Timer Toggeln
|
||||
if (holdStop && clickMinus) {
|
||||
settings.timerEnabled = !settings.timerEnabled;
|
||||
webRun = false;
|
||||
}
|
||||
|
||||
// Sperr-Flag, falls der Nutzer im aktiven Timer-Fenster bewusst Stopp gedrückt hat
|
||||
|
|
@ -705,8 +810,10 @@ void loop() {
|
|||
speed = 0; // während Sperre: nichts tun
|
||||
} else if (holdPlus) {
|
||||
speed = settings.accelRate;
|
||||
webRun = false;
|
||||
} else if (holdMinus) {
|
||||
speed = -settings.accelRate;
|
||||
webRun = false;
|
||||
} else if (webRun) {
|
||||
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
|
||||
} else {
|
||||
|
|
@ -719,15 +826,101 @@ void loop() {
|
|||
}
|
||||
wasManualMode = isManualMode;
|
||||
|
||||
// Laufzeit-Vorgabe verwerfen, sobald der Web-Lauf beendet ist
|
||||
// (Stopp, Taster, Überhitzung, Menü, Ablauf ...)
|
||||
if (!webRun) webRunDurMs = 0;
|
||||
|
||||
// 6. Motor ansteuern (einzige Stelle)
|
||||
applyMotor(motorCmd, speed);
|
||||
|
||||
// Laufzeitmessung: Zeit seit dem letzten Start, bleibt nach dem Stopp stehen
|
||||
bool motorIsRunning = stepper->isRunning();
|
||||
if (motorIsRunning && !motorWasRunning) {
|
||||
runStartMs = millis(); // Motor ist gerade angelaufen
|
||||
}
|
||||
if (motorIsRunning) {
|
||||
runSeconds = (millis() - runStartMs) / 1000;
|
||||
}
|
||||
motorWasRunning = motorIsRunning;
|
||||
|
||||
// 7. Geschwindigkeitsberechnung & UI
|
||||
int32_t currentSpeed_HZ = (stepper->getCurrentSpeedInUs() == 0) ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
|
||||
int32_t currentSpeed = (currentSpeed_HZ * 60) / MIKROSTEPS;
|
||||
|
||||
// Drehzahl-Wächter: nur im Automatik-Lauf (Soll = autoSpeed).
|
||||
// Alarm, wenn die Abweichung > 10 % länger als Hochlauf-Rampe + 15 s anhält.
|
||||
if (motorCmd == MOTOR_AUTO && stepper->isRunning() && settings.autoSpeed > 0) {
|
||||
int32_t soll = settings.autoSpeed;
|
||||
bool abweichend = (abs(currentSpeed - soll) * 10 > soll);
|
||||
|
||||
if (!abweichend) {
|
||||
devSinceMs = 0;
|
||||
devAlarmSent = false;
|
||||
} else {
|
||||
if (devSinceMs == 0) devSinceMs = millis();
|
||||
|
||||
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
||||
uint32_t graceMs = (autoHz * 1000UL) / settings.accelRate + 15000UL;
|
||||
|
||||
if (!devAlarmSent && (millis() - devSinceMs) >= graceMs) {
|
||||
devAlarmSent = true; // pro Ereignis nur eine Nachricht
|
||||
devAlarmPending = true;
|
||||
devSoll = soll;
|
||||
devIst = currentSpeed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
devSinceMs = 0;
|
||||
devAlarmSent = false;
|
||||
}
|
||||
|
||||
// Positions-Wächter: berechnet die Drehzahl aus den tatsächlich ausgegebenen Schritten
|
||||
// (Positionszähler) und vergleicht sie mit dem Soll. Fängt den Fall ab, dass die Anzeige
|
||||
// stimmt, der ESP aber weniger (oder mehr) Impulse ausgibt. Nur im stabilen Automatik-Lauf.
|
||||
{
|
||||
int32_t pos = stepper->getCurrentPosition();
|
||||
bool steady = (motorCmd == MOTOR_AUTO && stepper->isRunning()
|
||||
&& settings.autoSpeed > 0 && devSinceMs == 0);
|
||||
|
||||
if (!steady || !posWinValid || settings.autoSpeed != posWinSoll) {
|
||||
// Messfenster neu starten (Hochlauf, Stopp, Drehzahländerung ...)
|
||||
posWinValid = steady;
|
||||
posWinSoll = settings.autoSpeed;
|
||||
posWinStart = millis();
|
||||
posWinPos = pos;
|
||||
if (!steady) {
|
||||
posBadWindows = 0;
|
||||
posAlarmSent = false;
|
||||
}
|
||||
} else if (millis() - posWinStart >= 10000) {
|
||||
uint32_t dt = millis() - posWinStart;
|
||||
int32_t dPos = (int32_t)((uint32_t)pos - (uint32_t)posWinPos); // überlaufsicher
|
||||
int32_t rpmPos = (int32_t)(((int64_t)abs(dPos) * 60000LL) / ((int64_t)MIKROSTEPS * dt));
|
||||
int32_t soll = settings.autoSpeed;
|
||||
|
||||
if (abs(rpmPos - soll) * 10 > soll) {
|
||||
if (posBadWindows < 255) posBadWindows++;
|
||||
} else {
|
||||
posBadWindows = 0;
|
||||
posAlarmSent = false;
|
||||
}
|
||||
|
||||
// Erst nach 2 schlechten Fenstern in Folge (ca. 20 s) melden
|
||||
if (posBadWindows >= 2 && !posAlarmSent) {
|
||||
posAlarmSent = true;
|
||||
posAlarmPending = true;
|
||||
posRpm = rpmPos;
|
||||
posSoll = soll;
|
||||
posShown = currentSpeed;
|
||||
}
|
||||
|
||||
posWinStart = millis();
|
||||
posWinPos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay(bSettings, settings.settingIdx, timeEditStep, settings, currentSpeed, curH, curM, timeIsValid, curTemp);
|
||||
//printStatus(stepper, speed, clickPlus, clickMinus);
|
||||
printStatus(stepper, speed, clickPlus, clickMinus);
|
||||
|
||||
// 8. Discord-Alarm (bewusst NACH der Motorsteuerung, da der HTTP-Call blockiert)
|
||||
// Genau eine Nachricht pro Überhitzungs-Ereignis, Reset erst wenn wieder abgekühlt
|
||||
|
|
@ -739,6 +932,76 @@ void loop() {
|
|||
send = false;
|
||||
}
|
||||
|
||||
// 8b. Discord-Nachricht, wenn die Laufzeit-Vorgabe abgelaufen ist
|
||||
// (auch NACH der Motorsteuerung, da der HTTP-Call blockiert; genau eine Nachricht pro Ablauf)
|
||||
if (timedFinished) {
|
||||
timedFinished = false;
|
||||
String message = "✅ **Kugelmuehle: Timer abgelaufen.** Laufzeit von "
|
||||
+ formatDuration(timedFinishedSec)
|
||||
+ " beendet, Motor wurde gestoppt. Aktuelle Temp: "
|
||||
+ String(curTemp) + " °C";
|
||||
sendDiscordNotification(message);
|
||||
}
|
||||
|
||||
// 8c. Discord-Alarm bei Drehzahlabweichung (einmal pro Ereignis)
|
||||
if (devAlarmPending) {
|
||||
devAlarmPending = false;
|
||||
int32_t pct = (devSoll > 0) ? ((devIst - devSoll) * 100) / devSoll : 0;
|
||||
String message = "⚠️ **Kugelmuehle: Drehzahlabweichung!** Soll " + String(devSoll)
|
||||
+ " U/min, Ist " + String(devIst) + " U/min (" + String(pct) + " %)"
|
||||
+ ", Schrittperiode " + String(stepper->getCurrentSpeedInUs()) + " us"
|
||||
+ ", Laufzeit " + formatDuration(runSeconds)
|
||||
+ ", Temp " + String(curTemp) + " °C"
|
||||
+ ", freier Heap " + String(ESP.getFreeHeap()) + " Byte";
|
||||
sendDiscordNotification(message);
|
||||
}
|
||||
|
||||
// 8c2. Discord-Alarm: aus der Position berechnete Drehzahl weicht vom Soll ab
|
||||
if (posAlarmPending) {
|
||||
posAlarmPending = false;
|
||||
String message = "⚠️ **Kugelmuehle: Schrittzaehler weicht ab!** Aus der Position berechnet "
|
||||
+ String(posRpm) + " U/min, Soll " + String(posSoll)
|
||||
+ " U/min, Anzeige " + String(posShown) + " U/min. Der ESP gibt "
|
||||
+ ((posRpm < posSoll) ? "weniger" : "mehr") + " Impulse aus als erwartet"
|
||||
+ ", Laufzeit " + formatDuration(runSeconds)
|
||||
+ ", Temp " + String(curTemp) + " °C"
|
||||
+ ", freier Heap " + String(ESP.getFreeHeap()) + " Byte";
|
||||
sendDiscordNotification(message);
|
||||
}
|
||||
|
||||
// 8c3. Discord-Info: Soll-Drehzahl wurde während des Laufs geändert (Web oder Menü).
|
||||
// Wird erst nach 5 s Ruhe gesendet, damit nicht jeder Tastendruck im Menü eine Nachricht auslöst.
|
||||
if (settings.autoSpeed != lastSeenAutoSpeed) {
|
||||
if (!speedChangePending) {
|
||||
speedChangeFrom = lastSeenAutoSpeed;
|
||||
speedChangeWhileRunning = false;
|
||||
}
|
||||
speedChangePending = true;
|
||||
speedChangeAtMs = millis();
|
||||
speedChangeWhileRunning |= stepper->isRunning();
|
||||
lastSeenAutoSpeed = settings.autoSpeed;
|
||||
}
|
||||
if (speedChangePending && (millis() - speedChangeAtMs) >= 5000) {
|
||||
speedChangePending = false;
|
||||
if (speedChangeWhileRunning && settings.autoSpeed != speedChangeFrom) {
|
||||
String message = "ℹ️ **Kugelmuehle: Drehzahl-Vorgabe geaendert** von " + String(speedChangeFrom)
|
||||
+ " auf " + String(settings.autoSpeed) + " U/min (Quelle: "
|
||||
+ (speedChangeViaWeb ? "Weboberflaeche" : "Menue am Geraet")
|
||||
+ "), waehrend die Muehle lief.";
|
||||
sendDiscordNotification(message);
|
||||
}
|
||||
speedChangeViaWeb = false;
|
||||
}
|
||||
|
||||
// 8d. Meldung nach Neustart des ESP (einmalig, sobald WLAN da ist)
|
||||
if (bootNotifyPending && WiFi.status() == WL_CONNECTED) {
|
||||
bootNotifyPending = false;
|
||||
String message = String("🔄 **Kugelmuehle gestartet.** Grund: ")
|
||||
+ resetReasonText(esp_reset_reason())
|
||||
+ ". Weboberflaeche: http://" + WiFi.localIP().toString();
|
||||
sendDiscordNotification(message);
|
||||
}
|
||||
|
||||
// 9. Status alle 500 ms an die Weboberfläche schicken
|
||||
static uint32_t lastWeb = 0;
|
||||
if (millis() - lastWeb >= 500) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ static void onWsEvent(AsyncWebSocket *, AsyncWebSocketClient *, AwsEventType typ
|
|||
|
||||
if (!strcmp(c, "timer")) { cmd.type = WC_TIMER; cmd.v[0] = doc["v"] | 0; }
|
||||
else if (!strcmp(c, "speed")) { cmd.type = WC_SPEED; cmd.v[0] = doc["v"] | 0; }
|
||||
else if (!strcmp(c, "run")) { cmd.type = WC_RUN; cmd.v[0] = doc["v"] | 0; }
|
||||
else if (!strcmp(c, "run")) { cmd.type = WC_RUN; cmd.v[0] = doc["v"] | 0; cmd.v[1] = doc["sec"] | 0; }
|
||||
else if (!strcmp(c, "maxTemp")) { cmd.type = WC_MAXTEMP; cmd.v[0] = doc["v"] | 0; }
|
||||
else if (!strcmp(c, "accel")) { cmd.type = WC_ACCEL; cmd.v[0] = doc["v"] | 0; }
|
||||
else if (!strcmp(c, "window")) {
|
||||
|
|
|
|||
62
web_ui.h
62
web_ui.h
|
|
@ -36,6 +36,8 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
|||
input[type=number], input[type=time] { width:100%; padding:10px; border-radius:8px; border:1px solid #444b57; background:#1b1d21; color:var(--txt); font-size:1rem; }
|
||||
input[type=range] { width:100%; }
|
||||
.small { font-size:.8rem; color:var(--dim); margin-top:6px; }
|
||||
select { width:100%; padding:10px; border-radius:8px; border:1px solid #444b57; background:#1b1d21; color:var(--txt); font-size:1rem; }
|
||||
.val.dim { opacity:.45; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -52,6 +54,10 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
|||
<div><div class="val" id="rpm">--</div><div class="lbl">U/min</div></div>
|
||||
<div><div class="state" id="state">--</div><div class="lbl">Motor</div></div>
|
||||
<div><div class="state" id="clock">--:--</div><div class="lbl">Uhrzeit</div></div>
|
||||
<div style="grid-column:1/-1">
|
||||
<div class="val" id="runTime">00:00:00</div>
|
||||
<div class="lbl">Laufzeit seit letztem Start<span id="remainInfo"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -65,6 +71,20 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
|||
<div class="small">Bei aktiver Zeitschaltuhr läuft der Motor nur im eingestellten Zeitfenster.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Für eine bestimmte Zeit starten</h2>
|
||||
<div class="row">
|
||||
<input type="number" id="durVal" min="0" step="any" value="2">
|
||||
<select id="durUnit">
|
||||
<option value="60">Minuten</option>
|
||||
<option value="3600" selected>Stunden</option>
|
||||
<option value="86400">Tage</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row"><button class="go" id="btnTimed" onclick="startTimed()">Jetzt starten</button></div>
|
||||
<div class="small" id="timedInfo"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Drehzahl (Automatik / Start)</h2>
|
||||
<div><b id="speedVal">--</b> U/min</div>
|
||||
|
|
@ -104,11 +124,28 @@ function connect() {
|
|||
ws.onmessage = e => render(JSON.parse(e.data));
|
||||
}
|
||||
|
||||
function send(cmd, v) {
|
||||
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v }));
|
||||
function send(cmd, v, sec) {
|
||||
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v, sec }));
|
||||
}
|
||||
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
|
||||
// Sekunden -> "HH:MM:SS" bzw. "2 T HH:MM:SS"
|
||||
function fmt(s) {
|
||||
s = Math.max(0, Math.floor(s));
|
||||
const d = Math.floor(s / 86400); s %= 86400;
|
||||
const h = Math.floor(s / 3600); s %= 3600;
|
||||
const m = Math.floor(s / 60);
|
||||
return (d > 0 ? d + ' T ' : '') + pad(h) + ':' + pad(m) + ':' + pad(s % 60);
|
||||
}
|
||||
|
||||
// Voraussichtliches Ende (Uhr des Browsers)
|
||||
function endText(remainSec) {
|
||||
const end = new Date(Date.now() + remainSec * 1000);
|
||||
const t = pad(end.getHours()) + ':' + pad(end.getMinutes()) + ' Uhr';
|
||||
if (end.toDateString() === new Date().toDateString()) return t;
|
||||
return end.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' }) + ' ' + t;
|
||||
}
|
||||
function setIfIdle(id, value) {
|
||||
const el = $(id);
|
||||
if (document.activeElement !== el) el.value = value; // nicht überschreiben, während getippt/gezogen wird
|
||||
|
|
@ -122,6 +159,17 @@ function render(d) {
|
|||
$('rpm').textContent = d.rpm;
|
||||
$('clock').textContent = d.time;
|
||||
|
||||
$('runTime').textContent = fmt(d.runSec);
|
||||
$('runTime').classList.toggle('dim', !d.running);
|
||||
$('remainInfo').textContent = d.timed
|
||||
? ' · noch ' + fmt(d.remain) + ' (bis ' + endText(d.remain) + ')'
|
||||
: '';
|
||||
|
||||
$('btnTimed').disabled = d.overheat || d.timerOn;
|
||||
$('timedInfo').textContent = d.timerOn
|
||||
? 'Gesperrt, solange die Zeitschaltuhr aktiv ist.'
|
||||
: 'Läuft ab jetzt für die gewählte Dauer und stoppt dann von selbst (max. 30 Tage). Nur per Web möglich.';
|
||||
|
||||
const st = $('state');
|
||||
st.className = 'state';
|
||||
if (d.overheat) { st.textContent = 'ÜBERHITZT'; st.classList.add('err'); }
|
||||
|
|
@ -147,6 +195,16 @@ function render(d) {
|
|||
}
|
||||
|
||||
function toggleRun() { send('run', last.webRun ? 0 : 1); }
|
||||
|
||||
function startTimed() {
|
||||
const sec = Math.round(+$('durVal').value * +$('durUnit').value);
|
||||
if (!(sec >= 60) || sec > 2592000) {
|
||||
alert('Bitte eine Dauer zwischen 1 Minute und 30 Tagen eingeben.');
|
||||
return;
|
||||
}
|
||||
if (sec > 12 * 3600 && !confirm('Die Mühle läuft dann ' + fmt(sec) + ' lang ohne Aufsicht weiter. Wirklich starten?')) return;
|
||||
send('run', 1, sec);
|
||||
}
|
||||
function toggleTimer() { send('timer', last.timerOn ? 0 : 1); }
|
||||
|
||||
function sendWindow() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue