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)
|
// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr)
|
||||||
bool webRun = false;
|
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;
|
struct tm timeinfo;
|
||||||
|
|
||||||
TimeEditStep timeEditStep = EDIT_NONE;
|
TimeEditStep timeEditStep = EDIT_NONE;
|
||||||
|
|
@ -77,6 +120,7 @@ void loadSettings() {
|
||||||
settings.maxTemp = prefs.getShort("max_temp", 70);
|
settings.maxTemp = prefs.getShort("max_temp", 70);
|
||||||
settings.coldTemp = prefs.getShort("cold_temp", 50);
|
settings.coldTemp = prefs.getShort("cold_temp", 50);
|
||||||
settings.accelRate = prefs.getInt("accel_rate", 20000);
|
settings.accelRate = prefs.getInt("accel_rate", 20000);
|
||||||
|
settings.autoSpeed = prefs.getInt("auto_speed", 700);
|
||||||
|
|
||||||
prefs.end();
|
prefs.end();
|
||||||
Serial.println("[NVS] Einstellungen erfolgreich geladen.");
|
Serial.println("[NVS] Einstellungen erfolgreich geladen.");
|
||||||
|
|
@ -92,8 +136,9 @@ void saveSettings() {
|
||||||
prefs.putUChar("e_min", settings.endMin);
|
prefs.putUChar("e_min", settings.endMin);
|
||||||
prefs.putBool("t_on", settings.timerEnabled);
|
prefs.putBool("t_on", settings.timerEnabled);
|
||||||
prefs.putShort("max_temp", settings.maxTemp);
|
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("accel_rate", settings.accelRate);
|
||||||
|
prefs.putInt("auto_speed", settings.autoSpeed);
|
||||||
|
|
||||||
prefs.end();
|
prefs.end();
|
||||||
Serial.println("[NVS] Einstellungen im Flash gespeichert.");
|
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) {
|
void sendDiscordNotification(const String& message) {
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
Serial.println("[Discord] Nicht verbunden mit WiFi.");
|
Serial.println("[Discord] Nicht verbunden mit WiFi.");
|
||||||
|
|
@ -164,7 +236,15 @@ void handleWebCommands() {
|
||||||
|
|
||||||
case WC_RUN:
|
case WC_RUN:
|
||||||
// Nur ohne Zeitschaltuhr erlaubt
|
// 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;
|
break;
|
||||||
|
|
||||||
case WC_MAXTEMP:
|
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["maxRpm"] = (MAX_SPEED_HZ * 60) / MIKROSTEPS;
|
||||||
d["accel"] = settings.accelRate;
|
d["accel"] = settings.accelRate;
|
||||||
d["sh"] = settings.startHour; d["sm"] = settings.startMin;
|
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;
|
d["eh"] = settings.endHour; d["em"] = settings.endMin;
|
||||||
|
|
||||||
char t[6];
|
char t[6];
|
||||||
|
|
@ -228,6 +321,8 @@ void setup() {
|
||||||
|
|
||||||
// Daten aus Flash speicher laden
|
// Daten aus Flash speicher laden
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
lastSeenAutoSpeed = settings.autoSpeed;
|
||||||
|
|
||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
stepper = engine.stepperConnectToPin(stepPinStepper);
|
stepper = engine.stepperConnectToPin(stepPinStepper);
|
||||||
|
|
@ -523,9 +618,6 @@ bool checkOverHeat(int sensorPin){
|
||||||
return overheated;
|
return overheated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ################## TODO #########################
|
|
||||||
// Betriebsmodi in funktions auslagern
|
|
||||||
|
|
||||||
void printStatus(FastAccelStepper *stepper, int speed, int clickPlus, int clickMinus){
|
void printStatus(FastAccelStepper *stepper, int speed, int clickPlus, int clickMinus){
|
||||||
// Plotter-Ausgabe alle 50 ms (überflutet den Bus nicht)
|
// Plotter-Ausgabe alle 50 ms (überflutet den Bus nicht)
|
||||||
if (millis() - lastPlotTime >= 50) {
|
if (millis() - lastPlotTime >= 50) {
|
||||||
|
|
@ -561,7 +653,8 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
||||||
lastAutoHz = 0;
|
lastAutoHz = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MOTOR_AUTO: {
|
|
||||||
|
case MOTOR_AUTO:{
|
||||||
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
||||||
if (!stepper->isRunning()) {
|
if (!stepper->isRunning()) {
|
||||||
stepper->setSpeedInHz(autoHz);
|
stepper->setSpeedInHz(autoHz);
|
||||||
|
|
@ -576,7 +669,8 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MOTOR_MANUAL: {
|
|
||||||
|
case MOTOR_MANUAL:{
|
||||||
if (speed != lastSpeed) {
|
if (speed != lastSpeed) {
|
||||||
stepper->setSpeedInHz(MAX_SPEED_HZ);
|
stepper->setSpeedInHz(MAX_SPEED_HZ);
|
||||||
stepper->setAcceleration(settings.accelRate);
|
stepper->setAcceleration(settings.accelRate);
|
||||||
|
|
@ -586,11 +680,14 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MOTOR_KEEP:{
|
|
||||||
|
|
||||||
}
|
case MOTOR_KEEP:{
|
||||||
default:
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,6 +710,13 @@ void loop() {
|
||||||
// Befehle von der Weboberfläche abarbeiten
|
// Befehle von der Weboberfläche abarbeiten
|
||||||
handleWebCommands();
|
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
|
// 2. Zeitschaltuhr & NTP prüfen
|
||||||
int curH, curM, curS;
|
int curH, curM, curS;
|
||||||
bool timeIsValid = getCurrentTime(curH, curM, curS);
|
bool timeIsValid = getCurrentTime(curH, curM, curS);
|
||||||
|
|
@ -634,6 +738,7 @@ void loop() {
|
||||||
// HotKey Timer Toggeln
|
// HotKey Timer Toggeln
|
||||||
if (holdStop && clickMinus) {
|
if (holdStop && clickMinus) {
|
||||||
settings.timerEnabled = !settings.timerEnabled;
|
settings.timerEnabled = !settings.timerEnabled;
|
||||||
|
webRun = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sperr-Flag, falls der Nutzer im aktiven Timer-Fenster bewusst Stopp gedrückt hat
|
// 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
|
speed = 0; // während Sperre: nichts tun
|
||||||
} else if (holdPlus) {
|
} else if (holdPlus) {
|
||||||
speed = settings.accelRate;
|
speed = settings.accelRate;
|
||||||
|
webRun = false;
|
||||||
} else if (holdMinus) {
|
} else if (holdMinus) {
|
||||||
speed = -settings.accelRate;
|
speed = -settings.accelRate;
|
||||||
|
webRun = false;
|
||||||
} else if (webRun) {
|
} else if (webRun) {
|
||||||
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
|
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -719,15 +826,101 @@ void loop() {
|
||||||
}
|
}
|
||||||
wasManualMode = isManualMode;
|
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)
|
// 6. Motor ansteuern (einzige Stelle)
|
||||||
applyMotor(motorCmd, speed);
|
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
|
// 7. Geschwindigkeitsberechnung & UI
|
||||||
int32_t currentSpeed_HZ = (stepper->getCurrentSpeedInUs() == 0) ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
|
int32_t currentSpeed_HZ = (stepper->getCurrentSpeedInUs() == 0) ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
|
||||||
int32_t currentSpeed = (currentSpeed_HZ * 60) / MIKROSTEPS;
|
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);
|
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)
|
// 8. Discord-Alarm (bewusst NACH der Motorsteuerung, da der HTTP-Call blockiert)
|
||||||
// Genau eine Nachricht pro Überhitzungs-Ereignis, Reset erst wenn wieder abgekühlt
|
// Genau eine Nachricht pro Überhitzungs-Ereignis, Reset erst wenn wieder abgekühlt
|
||||||
|
|
@ -739,6 +932,76 @@ void loop() {
|
||||||
send = false;
|
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
|
// 9. Status alle 500 ms an die Weboberfläche schicken
|
||||||
static uint32_t lastWeb = 0;
|
static uint32_t lastWeb = 0;
|
||||||
if (millis() - lastWeb >= 500) {
|
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; }
|
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, "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, "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, "accel")) { cmd.type = WC_ACCEL; cmd.v[0] = doc["v"] | 0; }
|
||||||
else if (!strcmp(c, "window")) {
|
else if (!strcmp(c, "window")) {
|
||||||
|
|
|
||||||
64
web_ui.h
64
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=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%; }
|
input[type=range] { width:100%; }
|
||||||
.small { font-size:.8rem; color:var(--dim); margin-top:6px; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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="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="state">--</div><div class="lbl">Motor</div></div>
|
||||||
<div><div class="state" id="clock">--:--</div><div class="lbl">Uhrzeit</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>
|
||||||
</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 class="small">Bei aktiver Zeitschaltuhr läuft der Motor nur im eingestellten Zeitfenster.</div>
|
||||||
</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">
|
<div class="card">
|
||||||
<h2>Drehzahl (Automatik / Start)</h2>
|
<h2>Drehzahl (Automatik / Start)</h2>
|
||||||
<div><b id="speedVal">--</b> U/min</div>
|
<div><b id="speedVal">--</b> U/min</div>
|
||||||
|
|
@ -104,11 +124,28 @@ function connect() {
|
||||||
ws.onmessage = e => render(JSON.parse(e.data));
|
ws.onmessage = e => render(JSON.parse(e.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(cmd, v) {
|
function send(cmd, v, sec) {
|
||||||
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v }));
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v, sec }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pad = n => String(n).padStart(2, '0');
|
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) {
|
function setIfIdle(id, value) {
|
||||||
const el = $(id);
|
const el = $(id);
|
||||||
if (document.activeElement !== el) el.value = value; // nicht überschreiben, während getippt/gezogen wird
|
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;
|
$('rpm').textContent = d.rpm;
|
||||||
$('clock').textContent = d.time;
|
$('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');
|
const st = $('state');
|
||||||
st.className = 'state';
|
st.className = 'state';
|
||||||
if (d.overheat) { st.textContent = 'ÜBERHITZT'; st.classList.add('err'); }
|
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 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 toggleTimer() { send('timer', last.timerOn ? 0 : 1); }
|
||||||
|
|
||||||
function sendWindow() {
|
function sendWindow() {
|
||||||
|
|
@ -165,4 +223,4 @@ connect();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)rawliteral";
|
)rawliteral";
|
||||||
Loading…
Reference in a new issue