2026-09-23 23:03:07 +02:00
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
|
#include <types.h>
|
|
|
|
|
|
#include <display_ui.h>
|
2026-10-11 21:41:53 +02:00
|
|
|
|
#include <web_server.h>
|
|
|
|
|
|
#include <secrets.h>
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
const char* ssid = WIFI_SSID;
|
|
|
|
|
|
const char* password = WIFI_PASSWORD;
|
|
|
|
|
|
const char* DISCORD_WEBHOOK = DISCORD_WEBHOOK_URL;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
// NTP-Server & Zeitzone für Deutschland
|
|
|
|
|
|
const char* ntpServer = "pool.ntp.org";
|
|
|
|
|
|
const char* tzInfo = "CET-1CEST,M3.5.0,M10.5.0/3";
|
|
|
|
|
|
|
|
|
|
|
|
FastAccelStepperEngine engine = FastAccelStepperEngine();
|
|
|
|
|
|
FastAccelStepper *stepper = NULL;
|
|
|
|
|
|
|
|
|
|
|
|
// ################ Variabeln ################
|
|
|
|
|
|
const uint32_t MAX_SPEED_HZ = 106000;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
const int32_t COLDTEMP_C = 70;
|
2026-10-10 12:04:05 +02:00
|
|
|
|
const int32_t MIKROSTEPS = 3200;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
bool clickPlus = 0;
|
|
|
|
|
|
bool clickMinus = 0;
|
|
|
|
|
|
bool clickStop = 0;
|
|
|
|
|
|
bool holdPlus = 0;
|
|
|
|
|
|
bool holdMinus = 0;
|
|
|
|
|
|
bool holdStop = 0;
|
|
|
|
|
|
|
|
|
|
|
|
bool shouldRun = true;
|
|
|
|
|
|
int lastSpeed = 0;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
uint32_t lastAutoHz = 0;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
int32_t lastControl = 0;
|
|
|
|
|
|
uint32_t lastPlotTime = 0;
|
|
|
|
|
|
|
|
|
|
|
|
bool autoStop = false;
|
|
|
|
|
|
bool bSettings = false;
|
2026-10-10 10:45:49 +02:00
|
|
|
|
int32_t curTemp = 0;
|
2026-09-25 01:19:16 +02:00
|
|
|
|
bool send = false;
|
|
|
|
|
|
|
|
|
|
|
|
bool stopMove = false;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr)
|
|
|
|
|
|
bool webRun = false;
|
|
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
struct tm timeinfo;
|
|
|
|
|
|
|
|
|
|
|
|
TimeEditStep timeEditStep = EDIT_NONE;
|
|
|
|
|
|
|
|
|
|
|
|
// fest in den Speicher schreiben
|
|
|
|
|
|
Preferences prefs;
|
|
|
|
|
|
|
|
|
|
|
|
SystemSettings settings;
|
|
|
|
|
|
|
|
|
|
|
|
//#####################################
|
|
|
|
|
|
//########### Hilfsfunktion ###########
|
|
|
|
|
|
//#####################################
|
|
|
|
|
|
|
|
|
|
|
|
// Überladung des Präfix-Operators (++timeEditStep)
|
|
|
|
|
|
TimeEditStep& operator++(TimeEditStep& step) {
|
2026-09-26 01:10:32 +02:00
|
|
|
|
step = (step == EDIT_SPEED) ? EDIT_NONE : static_cast<TimeEditStep>(step + 1);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
return step;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Werte aus dem Flash speicher laden
|
|
|
|
|
|
void loadSettings() {
|
|
|
|
|
|
// Namespace öffnen (Name max. 15 Zeichen, "true" = read-only)
|
|
|
|
|
|
prefs.begin("timer_cfg", true);
|
|
|
|
|
|
|
|
|
|
|
|
// Werte auslesen
|
|
|
|
|
|
settings.startHour = prefs.getUChar("s_hour", 0);
|
|
|
|
|
|
settings.startMin = prefs.getUChar("s_min", 0);
|
|
|
|
|
|
settings.endHour = prefs.getUChar("e_hour", 0);
|
|
|
|
|
|
settings.endMin = prefs.getUChar("e_min", 0);
|
|
|
|
|
|
settings.timerEnabled = prefs.getBool("t_on", false);
|
|
|
|
|
|
settings.maxTemp = prefs.getShort("max_temp", 70);
|
2026-10-02 22:21:15 +02:00
|
|
|
|
settings.coldTemp = prefs.getShort("cold_temp", 50);
|
2026-09-25 01:19:16 +02:00
|
|
|
|
settings.accelRate = prefs.getInt("accel_rate", 20000);
|
2026-10-13 22:47:48 +02:00
|
|
|
|
settings.autoSpeed = prefs.getInt("auto_speed", 700);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
prefs.end();
|
|
|
|
|
|
Serial.println("[NVS] Einstellungen erfolgreich geladen.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Save Settings in Flash Speichern
|
|
|
|
|
|
void saveSettings() {
|
|
|
|
|
|
prefs.begin("timer_cfg", false); // "false" = Read/Write
|
|
|
|
|
|
|
|
|
|
|
|
prefs.putUChar("s_hour", settings.startHour);
|
|
|
|
|
|
prefs.putUChar("s_min", settings.startMin);
|
|
|
|
|
|
prefs.putUChar("e_hour", settings.endHour);
|
|
|
|
|
|
prefs.putUChar("e_min", settings.endMin);
|
|
|
|
|
|
prefs.putBool("t_on", settings.timerEnabled);
|
|
|
|
|
|
prefs.putShort("max_temp", settings.maxTemp);
|
2026-10-13 22:47:48 +02:00
|
|
|
|
prefs.putShort("cold_temp", settings.coldTemp);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
prefs.putInt("accel_rate", settings.accelRate);
|
2026-10-13 22:47:48 +02:00
|
|
|
|
prefs.putInt("auto_speed", settings.autoSpeed);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
prefs.end();
|
|
|
|
|
|
Serial.println("[NVS] Einstellungen im Flash gespeichert.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void initWiFiAndTime() {
|
|
|
|
|
|
WiFi.begin(ssid, password);
|
|
|
|
|
|
|
|
|
|
|
|
// Zeitzone und NTP-Server setzen
|
|
|
|
|
|
configTzTime(tzInfo, ntpServer);
|
|
|
|
|
|
|
|
|
|
|
|
// Warten auf Verbindung (optional mit Timeout)
|
|
|
|
|
|
int retries = 0;
|
|
|
|
|
|
while (WiFi.status() != WL_CONNECTED && retries < 20) {
|
|
|
|
|
|
delay(500);
|
|
|
|
|
|
retries++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 01:19:16 +02:00
|
|
|
|
void sendDiscordNotification(const String& message) {
|
|
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
|
|
|
|
Serial.println("[Discord] Nicht verbunden mit WiFi.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
WiFiClientSecure client;
|
|
|
|
|
|
client.setInsecure(); // Überspringt die rechenintensive Zertifikatsketten-Prüfung
|
|
|
|
|
|
|
|
|
|
|
|
HTTPClient http;
|
|
|
|
|
|
if (http.begin(client, DISCORD_WEBHOOK)) {
|
|
|
|
|
|
http.addHeader("Content-Type", "application/json");
|
|
|
|
|
|
|
|
|
|
|
|
// JSON-Payload: {"content": "Deine Nachricht"}
|
|
|
|
|
|
String payload = "{\"content\": \"" + message + "\"}";
|
|
|
|
|
|
|
|
|
|
|
|
int httpResponseCode = http.POST(payload);
|
|
|
|
|
|
if (httpResponseCode == 204 || httpResponseCode == 200) {
|
|
|
|
|
|
Serial.println("[Discord] Nachricht erfolgreich gesendet.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Serial.printf("[Discord] Fehler beim Senden: HTTP %d\n", httpResponseCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
http.end();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
// ############################################################
|
|
|
|
|
|
// ################ WEBOBERFLÄCHE #############################
|
|
|
|
|
|
// ############################################################
|
|
|
|
|
|
|
|
|
|
|
|
// Arbeitet die Befehle ab, die der Webserver in die Queue gelegt hat.
|
|
|
|
|
|
// Läuft im loop() -> kein Zugriffskonflikt mit dem Webserver-Task.
|
|
|
|
|
|
void handleWebCommands() {
|
|
|
|
|
|
WebCmd c;
|
|
|
|
|
|
const int32_t maxRpm = (MAX_SPEED_HZ * 60) / MIKROSTEPS;
|
|
|
|
|
|
|
|
|
|
|
|
while (xQueueReceive(webQueue, &c, 0) == pdTRUE) {
|
|
|
|
|
|
switch (c.type) {
|
|
|
|
|
|
case WC_TIMER:
|
|
|
|
|
|
settings.timerEnabled = (c.v[0] != 0);
|
|
|
|
|
|
webRun = false; // Handbetrieb beim Umschalten beenden
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WC_SPEED:
|
|
|
|
|
|
settings.autoSpeed = constrain(c.v[0], 50, maxRpm);
|
|
|
|
|
|
saveSettings(); // wirkt live über applyMotor(MOTOR_AUTO)
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WC_RUN:
|
|
|
|
|
|
// Nur ohne Zeitschaltuhr erlaubt
|
2026-10-13 22:47:48 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-11 21:41:53 +02:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WC_MAXTEMP:
|
|
|
|
|
|
settings.maxTemp = constrain(c.v[0], -20, 150);
|
|
|
|
|
|
settings.coldTemp = settings.maxTemp - 20;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WC_ACCEL:
|
|
|
|
|
|
settings.accelRate = constrain(c.v[0], 1000, 30000);
|
|
|
|
|
|
stepper->setAcceleration(settings.accelRate);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WC_WINDOW:
|
|
|
|
|
|
settings.startHour = constrain(c.v[0], 0, 23);
|
|
|
|
|
|
settings.startMin = constrain(c.v[1], 0, 59);
|
|
|
|
|
|
settings.endHour = constrain(c.v[2], 0, 23);
|
|
|
|
|
|
settings.endMin = constrain(c.v[3], 0, 59);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Schickt den aktuellen Status als JSON an alle verbundenen Browser
|
|
|
|
|
|
void sendWebStatus(int32_t rpm, bool timeValid, int h, int m, bool overheat) {
|
|
|
|
|
|
if (webSocket.count() == 0) return; // niemand verbunden -> nichts tun
|
|
|
|
|
|
|
|
|
|
|
|
JsonDocument d;
|
|
|
|
|
|
d["temp"] = curTemp;
|
|
|
|
|
|
d["maxTemp"] = settings.maxTemp;
|
|
|
|
|
|
d["overheat"] = overheat;
|
|
|
|
|
|
d["rpm"] = rpm;
|
|
|
|
|
|
d["running"] = stepper->isRunning();
|
|
|
|
|
|
d["timerOn"] = settings.timerEnabled;
|
|
|
|
|
|
d["webRun"] = webRun;
|
|
|
|
|
|
d["autoSpeed"] = settings.autoSpeed;
|
|
|
|
|
|
d["maxRpm"] = (MAX_SPEED_HZ * 60) / MIKROSTEPS;
|
|
|
|
|
|
d["accel"] = settings.accelRate;
|
|
|
|
|
|
d["sh"] = settings.startHour; d["sm"] = settings.startMin;
|
2026-10-13 22:47:48 +02:00
|
|
|
|
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;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
d["eh"] = settings.endHour; d["em"] = settings.endMin;
|
|
|
|
|
|
|
|
|
|
|
|
char t[6];
|
|
|
|
|
|
if (timeValid) snprintf(t, sizeof(t), "%02d:%02d", h, m);
|
|
|
|
|
|
else strcpy(t, "--:--");
|
|
|
|
|
|
d["time"] = t;
|
|
|
|
|
|
|
|
|
|
|
|
String out;
|
|
|
|
|
|
serializeJson(d, out);
|
|
|
|
|
|
webSocket.textAll(out);
|
|
|
|
|
|
webSocket.cleanupClients();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
//#######################################################
|
|
|
|
|
|
//####################### Setup #########################
|
|
|
|
|
|
//#######################################################
|
|
|
|
|
|
void setup() {
|
|
|
|
|
|
Serial.begin(115200);
|
|
|
|
|
|
delay(1000); // Zeit für USB-CDC-Verbindung geben
|
|
|
|
|
|
|
|
|
|
|
|
// Daten aus Flash speicher laden
|
|
|
|
|
|
loadSettings();
|
2026-10-13 22:47:48 +02:00
|
|
|
|
lastSeenAutoSpeed = settings.autoSpeed;
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
engine.init();
|
|
|
|
|
|
stepper = engine.stepperConnectToPin(stepPinStepper);
|
|
|
|
|
|
|
|
|
|
|
|
if (stepper) {
|
|
|
|
|
|
stepper->setDirectionPin(dirPinStepper);
|
|
|
|
|
|
stepper->setEnablePin(enablePinStepper, true);
|
|
|
|
|
|
stepper->setAutoEnable(true);
|
|
|
|
|
|
stepper->setSpeedInHz(MAX_SPEED_HZ);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pin Config
|
|
|
|
|
|
pinMode(buttonPlus, INPUT_PULLUP);
|
|
|
|
|
|
pinMode(buttonMinus, INPUT_PULLUP);
|
|
|
|
|
|
pinMode(buttonStop, INPUT_PULLUP);
|
|
|
|
|
|
|
|
|
|
|
|
initWiFiAndTime();
|
|
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
// Weboberfläche starten
|
|
|
|
|
|
initWebServer();
|
|
|
|
|
|
Serial.print("[Web] Erreichbar unter http://");
|
|
|
|
|
|
Serial.println(WiFi.localIP());
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
initDisplay();
|
|
|
|
|
|
|
|
|
|
|
|
// Start Animation
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool getCurrentTime(int &hour, int &minute, int &second) {
|
|
|
|
|
|
if (!getLocalTime(&timeinfo, 50)) { // 50ms Timeout
|
|
|
|
|
|
return false; // Noch keine NTP-Zeit synchronisiert
|
|
|
|
|
|
}
|
|
|
|
|
|
hour = timeinfo.tm_hour;
|
|
|
|
|
|
minute = timeinfo.tm_min;
|
|
|
|
|
|
second = timeinfo.tm_sec;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rückgabe von Flanken (Klick) und Pegel (Halten)
|
|
|
|
|
|
void getInputs(bool &clickPlus, bool &clickMinus, bool &clickStop,
|
|
|
|
|
|
bool &holdPlus, bool &holdMinus, bool &holdStop) {
|
|
|
|
|
|
|
|
|
|
|
|
// Physische Pegel einlesen (Active-Low)
|
|
|
|
|
|
bool curPlus = (digitalRead(buttonPlus) == LOW);
|
|
|
|
|
|
bool curMinus = (digitalRead(buttonMinus) == LOW);
|
|
|
|
|
|
bool curStop = (digitalRead(buttonStop) == LOW);
|
|
|
|
|
|
|
|
|
|
|
|
// Speicher für vorherigen Durchlauf
|
|
|
|
|
|
static bool lastPlus = false;
|
|
|
|
|
|
static bool lastMinus = false;
|
|
|
|
|
|
static bool lastStop = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Nur bei Veränderung true (Sperre)
|
|
|
|
|
|
clickPlus = (curPlus && !lastPlus);
|
|
|
|
|
|
clickMinus = (curMinus && !lastMinus);
|
|
|
|
|
|
clickStop = (curStop && !lastStop);
|
|
|
|
|
|
|
|
|
|
|
|
// Dauerzustand (für Stepper-Handbetrieb)
|
|
|
|
|
|
holdPlus = curPlus;
|
|
|
|
|
|
holdMinus = curMinus;
|
|
|
|
|
|
holdStop = curStop;
|
|
|
|
|
|
|
|
|
|
|
|
// Zustand für nächsten Durchlauf merken
|
|
|
|
|
|
lastPlus = curPlus;
|
|
|
|
|
|
lastMinus = curMinus;
|
|
|
|
|
|
lastStop = curStop;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Liefert die Aktuelle Temperatur in °C
|
|
|
|
|
|
int getTempInC(uint8_t senTemp){
|
|
|
|
|
|
int milliVolt = analogReadMilliVolts(senTemp);
|
|
|
|
|
|
return ((milliVolt - 500)/10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 10:45:49 +02:00
|
|
|
|
int32_t getFilteredTemp(int rawSensorPin) {
|
2026-10-07 11:54:06 +02:00
|
|
|
|
constexpr uint8_t FILTER_SAMPLES = 100; // Puffergröße (100 Messwerte) Mittelwert über 10s
|
2026-09-26 01:10:32 +02:00
|
|
|
|
static int readings[FILTER_SAMPLES];
|
|
|
|
|
|
static uint8_t readIndex = 0;
|
|
|
|
|
|
static int32_t total = 0;
|
|
|
|
|
|
static int average = 0;
|
|
|
|
|
|
static bool initialized = false;
|
|
|
|
|
|
static uint32_t lastSampleTime = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Abtastrate: Alle 100 ms einen neuen Messwert erfassen
|
|
|
|
|
|
if (millis() - lastSampleTime >= 100 || !initialized) {
|
|
|
|
|
|
lastSampleTime = millis();
|
|
|
|
|
|
|
|
|
|
|
|
int rawVal = getTempInC(rawSensorPin); // Messwert in mV
|
|
|
|
|
|
|
|
|
|
|
|
// Kaltstart: Puffer beim ersten Aufruf vollflächig vorbefüllen
|
|
|
|
|
|
if (!initialized) {
|
|
|
|
|
|
for (uint8_t i = 0; i < FILTER_SAMPLES; i++) {
|
|
|
|
|
|
readings[i] = rawVal;
|
|
|
|
|
|
}
|
|
|
|
|
|
total = (int32_t)rawVal * FILTER_SAMPLES;
|
|
|
|
|
|
average = rawVal;
|
|
|
|
|
|
initialized = true;
|
|
|
|
|
|
return average;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ältesten Wert abziehen, neuen Wert einfügen und addieren
|
|
|
|
|
|
total -= readings[readIndex];
|
|
|
|
|
|
readings[readIndex] = rawVal;
|
|
|
|
|
|
total += rawVal;
|
|
|
|
|
|
|
|
|
|
|
|
// Index weiterschalten
|
|
|
|
|
|
readIndex = (readIndex + 1) % FILTER_SAMPLES;
|
|
|
|
|
|
|
|
|
|
|
|
// Ganzzahlige Division mit kaufmännischer Rundung (+ 5 bei 10 Werten)
|
|
|
|
|
|
if (total >= 0) {
|
|
|
|
|
|
average = (total + (FILTER_SAMPLES / 2)) / FILTER_SAMPLES;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
average = (total - (FILTER_SAMPLES / 2)) / FILTER_SAMPLES;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return average;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
// Liefert true wenn der Timer aktuell Active ist und false wenn nicht aktiv
|
|
|
|
|
|
bool isTimerActiveNow(int curH, int curM, int startH, int startM, int endH, int endM) {
|
|
|
|
|
|
int current = curH * 60 + curM;
|
|
|
|
|
|
int start = startH * 60 + startM;
|
|
|
|
|
|
int end = endH * 60 + endM;
|
|
|
|
|
|
|
|
|
|
|
|
// Fall A: Zeitfenster am selben Tag (z. B. 08:00 bis 16:00)
|
|
|
|
|
|
if (start <= end) {
|
|
|
|
|
|
return (current >= start && current < end);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fall B: Zeitfenster über Mitternacht (z. B. 22:00 bis 04:00)
|
|
|
|
|
|
else {
|
|
|
|
|
|
return (current >= start || current < end);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Die Funktion behandelt das gesammte Einstellungs menu
|
|
|
|
|
|
void setting(){
|
|
|
|
|
|
//######### Timer Edit #########
|
|
|
|
|
|
if (timeEditStep != EDIT_NONE) {
|
|
|
|
|
|
//PLUS GEDRÜCKT: Wert erhöhen
|
2026-09-25 01:19:16 +02:00
|
|
|
|
if (clickPlus) {
|
2026-09-23 23:03:07 +02:00
|
|
|
|
switch (timeEditStep) {
|
|
|
|
|
|
case EDIT_START_HOUR:
|
|
|
|
|
|
settings.startHour = (settings.startHour + 1) % 24;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_START_MIN:
|
|
|
|
|
|
settings.startMin = (settings.startMin + 5) % 60;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_END_HOUR:
|
|
|
|
|
|
settings.endHour = (settings.endHour + 1) % 24;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_END_MIN:
|
|
|
|
|
|
settings.endMin = (settings.endMin + 5) % 60;
|
|
|
|
|
|
break;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
case EDIT_SPEED:
|
|
|
|
|
|
if(settings.autoSpeed < ((MAX_SPEED_HZ * 60)/MIKROSTEPS) - 50){
|
|
|
|
|
|
settings.autoSpeed += 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MINUS GEDRÜCKT: Wert verringern
|
|
|
|
|
|
if (clickMinus) {
|
|
|
|
|
|
switch (timeEditStep) {
|
|
|
|
|
|
case EDIT_START_HOUR:
|
|
|
|
|
|
settings.startHour = (settings.startHour == 0) ? 23 : settings.startHour - 1;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_START_MIN:
|
2026-09-25 01:56:44 +02:00
|
|
|
|
settings.startMin = (settings.startMin < 5) ? 55 : settings.startMin - 5;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_END_HOUR:
|
|
|
|
|
|
settings.endHour = (settings.endHour == 0) ? 23 : settings.endHour - 1;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EDIT_END_MIN:
|
2026-09-25 01:56:44 +02:00
|
|
|
|
settings.endMin = (settings.endMin < 5) ? 55 : settings.endMin - 5;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
break;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
case EDIT_SPEED:
|
|
|
|
|
|
if(settings.autoSpeed >= 50){
|
|
|
|
|
|
settings.autoSpeed -= 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// STOP GEDRÜCKT: Zum nächsten Wert springen
|
|
|
|
|
|
if (clickStop) {
|
|
|
|
|
|
++timeEditStep;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return; // Wichtig: Rest der Funktion überspringen, solange editiert wird!
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//######### Max Temp Edit #########
|
2026-09-25 01:56:44 +02:00
|
|
|
|
if (settings.editMaxTemp) {
|
|
|
|
|
|
// PLUS: Bis maximal 150
|
|
|
|
|
|
if (clickPlus) {
|
|
|
|
|
|
if (settings.maxTemp <= 145) {
|
|
|
|
|
|
settings.maxTemp += 5;
|
|
|
|
|
|
}
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
2026-09-25 01:56:44 +02:00
|
|
|
|
|
|
|
|
|
|
// MINUS: Bis minimal -20
|
|
|
|
|
|
if (clickMinus) {
|
|
|
|
|
|
if (settings.maxTemp >= -15) {
|
|
|
|
|
|
settings.maxTemp -= 5;
|
|
|
|
|
|
}
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
2026-09-25 01:56:44 +02:00
|
|
|
|
|
|
|
|
|
|
if (clickStop) {
|
2026-09-26 01:10:32 +02:00
|
|
|
|
settings.coldTemp = settings.maxTemp - 20;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
settings.editMaxTemp = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return; // Wichtig: Rest der Funktion überspringen, solange editiert wird!
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//######### Beschleunigung Edit #########
|
2026-09-25 01:56:44 +02:00
|
|
|
|
if (settings.editAcc) {
|
|
|
|
|
|
// PLUS: Erhöhen in 1000er-Schritten bis max 20.000
|
|
|
|
|
|
if (clickPlus) {
|
2026-10-10 12:04:05 +02:00
|
|
|
|
if (settings.accelRate <= 29000) {
|
|
|
|
|
|
settings.accelRate += 500;
|
2026-09-25 01:56:44 +02:00
|
|
|
|
}
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
2026-09-25 01:56:44 +02:00
|
|
|
|
|
|
|
|
|
|
// MINUS: Verringern in 1000er-Schritten bis min 1.000 (niemals 0!)
|
|
|
|
|
|
if (clickMinus) {
|
2026-10-10 12:04:05 +02:00
|
|
|
|
if (settings.accelRate >= 1000) {
|
|
|
|
|
|
settings.accelRate -= 500;
|
2026-09-25 01:56:44 +02:00
|
|
|
|
}
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
2026-09-25 01:56:44 +02:00
|
|
|
|
|
|
|
|
|
|
// STOP: Speichern / Verlassen
|
|
|
|
|
|
if (clickStop) {
|
2026-09-23 23:03:07 +02:00
|
|
|
|
settings.editAcc = false;
|
2026-09-25 01:56:44 +02:00
|
|
|
|
stepper->setAcceleration(settings.accelRate); // Direkt am Stepper aktualisieren
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
return; // Wichtig: Rest der Funktion überspringen, solange editiert wird!
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//######### Menü #########
|
|
|
|
|
|
// Menü bewegen
|
|
|
|
|
|
if(clickPlus){
|
2026-09-25 01:19:16 +02:00
|
|
|
|
if(settings.settingIdx <= 3){
|
2026-09-23 23:03:07 +02:00
|
|
|
|
settings.settingIdx++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if(clickMinus){
|
|
|
|
|
|
if(settings.settingIdx >=1){
|
|
|
|
|
|
settings.settingIdx--;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop zum bestätigen
|
|
|
|
|
|
if (clickStop) {
|
|
|
|
|
|
if (settings.settingIdx == 0) {
|
|
|
|
|
|
// Zeitschaltuhr-Einstellung starten
|
|
|
|
|
|
timeEditStep = EDIT_START_HOUR;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (settings.settingIdx == 1) {
|
|
|
|
|
|
// Zeitschaltuhr ON/OFF
|
|
|
|
|
|
settings.timerEnabled = !settings.timerEnabled;
|
|
|
|
|
|
}else if (settings.settingIdx == 2) {
|
|
|
|
|
|
// Temperatur Grenze einstellen
|
|
|
|
|
|
settings.editMaxTemp = true;
|
|
|
|
|
|
}else if (settings.settingIdx == 3) {
|
|
|
|
|
|
// Beschleunigung
|
|
|
|
|
|
settings.editAcc = true;
|
|
|
|
|
|
}else if (settings.settingIdx == 4) {
|
|
|
|
|
|
// Menü verlassen
|
|
|
|
|
|
bSettings = false;
|
|
|
|
|
|
// Einstellungen Speichern
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// Prüft ob der Motor zu heiß wird (mit Hysterese: AN ab maxTemp, AUS erst bei coldTemp)
|
|
|
|
|
|
// return: true wenn zu heiß
|
2026-10-07 11:54:06 +02:00
|
|
|
|
// false wenn unter der Grenze
|
|
|
|
|
|
bool checkOverHeat(int sensorPin){
|
2026-10-07 12:01:38 +02:00
|
|
|
|
static bool overheated = false;
|
2026-10-07 11:54:06 +02:00
|
|
|
|
curTemp = getFilteredTemp(sensorPin);
|
|
|
|
|
|
|
|
|
|
|
|
if (curTemp >= settings.maxTemp) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
overheated = true;
|
2026-10-07 11:54:06 +02:00
|
|
|
|
} else if (curTemp <= settings.coldTemp) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
overheated = false;
|
2026-10-07 11:54:06 +02:00
|
|
|
|
}
|
2026-10-07 12:01:38 +02:00
|
|
|
|
return overheated;
|
2026-10-07 11:54:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
void printStatus(FastAccelStepper *stepper, int speed, int clickPlus, int clickMinus){
|
|
|
|
|
|
// Plotter-Ausgabe alle 50 ms (überflutet den Bus nicht)
|
|
|
|
|
|
if (millis() - lastPlotTime >= 50) {
|
|
|
|
|
|
lastPlotTime = millis();
|
|
|
|
|
|
|
2026-09-25 01:19:16 +02:00
|
|
|
|
int32_t currentSpeed_HZ = stepper->getCurrentSpeedInUs() == 0 ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
|
|
|
|
|
|
int32_t currentSpeed = (currentSpeed_HZ * 60)/1600;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
// Format kompatibel für den Arduino Serial Plotter
|
|
|
|
|
|
Serial.print("Current_Speed_U/Min:");
|
|
|
|
|
|
Serial.print((60*currentSpeed)/3200);
|
|
|
|
|
|
Serial.print(" Accel_Control:");
|
|
|
|
|
|
Serial.print(speed);
|
|
|
|
|
|
Serial.print(" Btn_Up:");
|
|
|
|
|
|
Serial.print(clickPlus ? 5000 : 0);
|
|
|
|
|
|
Serial.print(" Btn_Down:");
|
|
|
|
|
|
Serial.print(clickMinus ? -5000 : 0);
|
|
|
|
|
|
Serial.println();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// ############################################################
|
|
|
|
|
|
// ################ EINZIGE STELLE FÜR MOTORSTEUERUNG #########
|
|
|
|
|
|
// ############################################################
|
|
|
|
|
|
void applyMotor(MotorCmd cmd, int32_t speed) {
|
2026-10-10 10:45:49 +02:00
|
|
|
|
switch(cmd){
|
2026-10-12 15:17:08 +02:00
|
|
|
|
case MOTOR_STOP:{
|
2026-10-07 12:01:38 +02:00
|
|
|
|
if (stepper->isRunning()) {
|
|
|
|
|
|
stepper->setAcceleration(10000);
|
|
|
|
|
|
stepper->applySpeedAcceleration();
|
|
|
|
|
|
stepper->stopMove();
|
|
|
|
|
|
}
|
|
|
|
|
|
lastSpeed = 0;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
lastAutoHz = 0;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
break;
|
2026-10-12 15:17:08 +02:00
|
|
|
|
}
|
2026-10-13 22:47:48 +02:00
|
|
|
|
|
|
|
|
|
|
case MOTOR_AUTO:{
|
2026-10-11 21:41:53 +02:00
|
|
|
|
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
if (!stepper->isRunning()) {
|
|
|
|
|
|
stepper->setSpeedInHz(autoHz);
|
|
|
|
|
|
stepper->setAcceleration(settings.accelRate);
|
|
|
|
|
|
stepper->runForward();
|
2026-10-11 21:41:53 +02:00
|
|
|
|
lastAutoHz = autoHz;
|
|
|
|
|
|
} else if (autoHz != lastAutoHz) {
|
|
|
|
|
|
// Drehzahl wurde geändert (z. B. per Web) -> live übernehmen
|
|
|
|
|
|
stepper->setSpeedInHz(autoHz);
|
|
|
|
|
|
stepper->applySpeedAcceleration();
|
|
|
|
|
|
lastAutoHz = autoHz;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-10-12 15:17:08 +02:00
|
|
|
|
}
|
2026-10-13 22:47:48 +02:00
|
|
|
|
|
|
|
|
|
|
case MOTOR_MANUAL:{
|
2026-10-07 12:01:38 +02:00
|
|
|
|
if (speed != lastSpeed) {
|
|
|
|
|
|
stepper->setSpeedInHz(MAX_SPEED_HZ);
|
|
|
|
|
|
stepper->setAcceleration(settings.accelRate);
|
|
|
|
|
|
stepper->moveByAcceleration(speed, true);
|
|
|
|
|
|
lastSpeed = speed;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
lastAutoHz = 0;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-10-12 15:17:08 +02:00
|
|
|
|
}
|
2026-10-07 12:01:38 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
case MOTOR_KEEP:{
|
|
|
|
|
|
break;
|
2026-10-12 15:17:08 +02:00
|
|
|
|
}
|
2026-10-13 22:47:48 +02:00
|
|
|
|
|
|
|
|
|
|
default:{
|
2026-10-07 12:01:38 +02:00
|
|
|
|
break;
|
2026-10-13 22:47:48 +02:00
|
|
|
|
}
|
2026-10-07 12:01:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
//#############################################################
|
|
|
|
|
|
//#################### MAIN ###################################
|
|
|
|
|
|
//#############################################################
|
|
|
|
|
|
void loop() {
|
|
|
|
|
|
int32_t speed = 0;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
MotorCmd motorCmd = MOTOR_KEEP;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
|
|
|
|
|
if (!stepper) {
|
|
|
|
|
|
Serial.println("[HALT] Kein Stepper-Objekt aktiv.");
|
|
|
|
|
|
delay(1000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// 1. Eingaben & Sensoren lesen
|
2026-09-23 23:03:07 +02:00
|
|
|
|
getInputs(clickPlus, clickMinus, clickStop, holdPlus, holdMinus, holdStop);
|
2026-10-11 21:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
// Befehle von der Weboberfläche abarbeiten
|
|
|
|
|
|
handleWebCommands();
|
2026-09-25 01:56:44 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// 2. Zeitschaltuhr & NTP prüfen
|
2026-09-25 01:56:44 +02:00
|
|
|
|
int curH, curM, curS;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
bool timeIsValid = getCurrentTime(curH, curM, curS);
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.timerEnabled && timeIsValid) {
|
2026-09-25 01:56:44 +02:00
|
|
|
|
shouldRun = isTimerActiveNow(curH, curM, settings.startHour, settings.startMin, settings.endHour, settings.endMin);
|
2026-09-26 01:10:32 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
shouldRun = false;
|
|
|
|
|
|
}
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// 3. Schutz-Verriegelung (Überhitzung)
|
|
|
|
|
|
bool tempOverheat = checkOverHeat(senTemp);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// 4. Menü / Hotkeys auswerten
|
2026-09-26 01:10:32 +02:00
|
|
|
|
if (holdStop && clickPlus) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
bSettings = true;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
}
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// HotKey Timer Toggeln
|
|
|
|
|
|
if (holdStop && clickMinus) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
settings.timerEnabled = !settings.timerEnabled;
|
2026-10-13 22:47:48 +02:00
|
|
|
|
webRun = false;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
}
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// Sperr-Flag, falls der Nutzer im aktiven Timer-Fenster bewusst Stopp gedrückt hat
|
|
|
|
|
|
static bool manualTimerStopped = false;
|
|
|
|
|
|
if (!shouldRun) {
|
|
|
|
|
|
manualTimerStopped = false; // Reset für den nächsten regulären Zeitzyklus
|
|
|
|
|
|
}
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
// Merker, ob wir im manuellen Betrieb sind (für sauberes Anhalten beim Moduswechsel)
|
|
|
|
|
|
static bool wasManualMode = false;
|
|
|
|
|
|
bool isManualMode = false;
|
|
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// 5. Betriebsmodi: hier wird nur ENTSCHIEDEN, nicht am Motor gesteuert
|
2026-10-11 21:41:53 +02:00
|
|
|
|
if (bSettings) {
|
|
|
|
|
|
if (webRun) { // Web-Lauf beim Öffnen des Menüs beenden
|
|
|
|
|
|
webRun = false;
|
|
|
|
|
|
motorCmd = MOTOR_STOP;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
motorCmd = MOTOR_KEEP;
|
|
|
|
|
|
}
|
2026-09-26 01:10:32 +02:00
|
|
|
|
setting();
|
2026-10-07 12:01:38 +02:00
|
|
|
|
}
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// NOT-AUS BEI ÜBERHITZUNG
|
|
|
|
|
|
else if (tempOverheat) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
motorCmd = MOTOR_STOP;
|
2026-09-25 01:19:16 +02:00
|
|
|
|
}
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// AUTOMATIK-BETRIEB (Zeitschaltuhr)
|
|
|
|
|
|
else if (settings.timerEnabled) {
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// Durch Drücken von Plus den Timerbetrieb fortsetzen
|
2026-09-26 01:10:32 +02:00
|
|
|
|
if (manualTimerStopped && clickPlus) {
|
|
|
|
|
|
manualTimerStopped = false;
|
|
|
|
|
|
}
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
if (shouldRun && !manualTimerStopped) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
motorCmd = MOTOR_AUTO;
|
2026-09-25 01:19:16 +02:00
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// Manuelles Anhalten im laufenden Timer erlauben
|
|
|
|
|
|
if (clickStop) {
|
|
|
|
|
|
manualTimerStopped = true;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
motorCmd = MOTOR_STOP;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Außerhalb des Fensters oder manuell gestoppt
|
2026-10-07 12:01:38 +02:00
|
|
|
|
motorCmd = MOTOR_STOP;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
}
|
2026-10-07 12:01:38 +02:00
|
|
|
|
}
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// MANUELLER BETRIEB (Zeitschaltuhr aus)
|
|
|
|
|
|
else {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
static bool manualStopped = false;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
static bool wasWebRun = false;
|
|
|
|
|
|
isManualMode = true;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
motorCmd = MOTOR_MANUAL;
|
2026-09-26 01:10:32 +02:00
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
if (!wasManualMode) {
|
|
|
|
|
|
// Gerade erst in den Handbetrieb gewechselt: Motor einmal sauber anhalten
|
|
|
|
|
|
motorCmd = MOTOR_STOP;
|
|
|
|
|
|
speed = 0;
|
|
|
|
|
|
motorCmd = MOTOR_STOP;
|
|
|
|
|
|
} else if (clickStop) {
|
2026-10-07 12:01:38 +02:00
|
|
|
|
manualStopped = true;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
webRun = false;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
speed = 0;
|
|
|
|
|
|
motorCmd = MOTOR_STOP;
|
|
|
|
|
|
} else if (manualStopped) {
|
|
|
|
|
|
if (!holdPlus && !holdMinus) {
|
|
|
|
|
|
manualStopped = false; // erst freigeben wenn Tasten los sind
|
|
|
|
|
|
}
|
|
|
|
|
|
speed = 0; // während Sperre: nichts tun
|
|
|
|
|
|
} else if (holdPlus) {
|
|
|
|
|
|
speed = settings.accelRate;
|
2026-10-13 22:47:48 +02:00
|
|
|
|
webRun = false;
|
2026-10-07 12:01:38 +02:00
|
|
|
|
} else if (holdMinus) {
|
|
|
|
|
|
speed = -settings.accelRate;
|
2026-10-13 22:47:48 +02:00
|
|
|
|
webRun = false;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
} else if (webRun) {
|
|
|
|
|
|
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
|
2026-10-07 12:01:38 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
speed = 0;
|
2026-10-11 21:41:53 +02:00
|
|
|
|
if (wasWebRun){
|
|
|
|
|
|
motorCmd = MOTOR_STOP; // Web-Stopp: einmal sauber anhalten
|
|
|
|
|
|
}
|
2026-09-26 01:10:32 +02:00
|
|
|
|
}
|
2026-10-11 21:41:53 +02:00
|
|
|
|
wasWebRun = webRun;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|
2026-10-11 21:41:53 +02:00
|
|
|
|
wasManualMode = isManualMode;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// Laufzeit-Vorgabe verwerfen, sobald der Web-Lauf beendet ist
|
|
|
|
|
|
// (Stopp, Taster, Überhitzung, Menü, Ablauf ...)
|
|
|
|
|
|
if (!webRun) webRunDurMs = 0;
|
|
|
|
|
|
|
2026-10-07 12:01:38 +02:00
|
|
|
|
// 6. Motor ansteuern (einzige Stelle)
|
|
|
|
|
|
applyMotor(motorCmd, speed);
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-09-26 01:10:32 +02:00
|
|
|
|
// 7. Geschwindigkeitsberechnung & UI
|
|
|
|
|
|
int32_t currentSpeed_HZ = (stepper->getCurrentSpeedInUs() == 0) ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
|
|
|
|
|
|
int32_t currentSpeed = (currentSpeed_HZ * 60) / MIKROSTEPS;
|
2026-09-23 23:03:07 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 23:03:07 +02:00
|
|
|
|
updateDisplay(bSettings, settings.settingIdx, timeEditStep, settings, currentSpeed, curH, curM, timeIsValid, curTemp);
|
2026-10-13 22:47:48 +02:00
|
|
|
|
printStatus(stepper, speed, clickPlus, clickMinus);
|
2026-10-07 12:01:38 +02:00
|
|
|
|
|
|
|
|
|
|
// 8. Discord-Alarm (bewusst NACH der Motorsteuerung, da der HTTP-Call blockiert)
|
|
|
|
|
|
// Genau eine Nachricht pro Überhitzungs-Ereignis, Reset erst wenn wieder abgekühlt
|
|
|
|
|
|
if (tempOverheat && !send) {
|
|
|
|
|
|
String message = "⚠️ **Kugelmuehle ueberhitzt!** Aktuelle Temp: " + String(curTemp) + " °C";
|
|
|
|
|
|
sendDiscordNotification(message);
|
|
|
|
|
|
send = true;
|
|
|
|
|
|
} else if (!tempOverheat) {
|
|
|
|
|
|
send = false;
|
|
|
|
|
|
}
|
2026-10-11 21:41:53 +02:00
|
|
|
|
|
2026-10-13 22:47:48 +02:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 21:41:53 +02:00
|
|
|
|
// 9. Status alle 500 ms an die Weboberfläche schicken
|
|
|
|
|
|
static uint32_t lastWeb = 0;
|
|
|
|
|
|
if (millis() - lastWeb >= 500) {
|
|
|
|
|
|
lastWeb = millis();
|
|
|
|
|
|
sendWebStatus(currentSpeed, timeIsValid, curH, curM, tempOverheat);
|
|
|
|
|
|
}
|
2026-09-23 23:03:07 +02:00
|
|
|
|
}
|