kugelmuehle/kugelmuehle.ino
Jannis fb76cbb31e Websocket angepasst unterstützt jetzt https (ws und wss).
Auf der Website kann jetzt auch gestoppt werden wenn über die taster manuell gestartet wurde.

Manuell steuerung verbssert. Durch movebyacceleration(0) ist die geschwindigkeit gedriftet. Beim drücken wird jetzt konstant beschleunigt bis maximal geschwindigkeit und beim loslassen wird die geschwindigkeit fest auf die aktuelle geschwindigkeit gesetzt.
Rückwärts fahren geht jetzt nicht mehr.
2026-10-28 11:59:27 +01:00

1036 lines
No EOL
33 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <Arduino.h>
#include <types.h>
#include <display_ui.h>
#include <web_server.h>
#include <secrets.h>
const char* ssid = WIFI_SSID;
const char* password = WIFI_PASSWORD;
const char* DISCORD_WEBHOOK = DISCORD_WEBHOOK_URL;
// 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;
const int32_t COLDTEMP_C = 70;
const int32_t MIKROSTEPS = 3200;
bool clickPlus = 0;
bool clickMinus = 0;
bool clickStop = 0;
bool holdPlus = 0;
bool holdMinus = 0;
bool holdStop = 0;
bool shouldRun = true;
int lastSpeed = 0;
uint32_t lastAutoHz = 0;
int32_t lastControl = 0;
uint32_t lastPlotTime = 0;
bool autoStop = false;
bool bSettings = false;
int32_t curTemp = 0;
bool send = false;
bool stopMove = false;
// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr)
bool webRun = false;
bool webStopReq = 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;
// fest in den Speicher schreiben
Preferences prefs;
SystemSettings settings;
//#####################################
//########### Hilfsfunktion ###########
//#####################################
// Überladung des Präfix-Operators (++timeEditStep)
TimeEditStep& operator++(TimeEditStep& step) {
step = (step == EDIT_SPEED) ? EDIT_NONE : static_cast<TimeEditStep>(step + 1);
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);
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.");
}
// 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);
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.");
}
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++;
}
}
// 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.");
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();
}
}
// ############################################################
// ################ 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
if (!settings.timerEnabled) {
if (c.v[0] != 0) {
webRun = true;
uint32_t sec = (uint32_t)constrain(c.v[1], 0, 2592000);
webRunStartMs = millis();
webRunDurMs = sec * 1000UL;
} else {
webRun = false;
webStopReq = true; // Stopp-Anforderung aus dem Web vermerken
}
}
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;
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];
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();
}
//#######################################################
//####################### Setup #########################
//#######################################################
void setup() {
Serial.begin(115200);
delay(1000); // Zeit für USB-CDC-Verbindung geben
// Daten aus Flash speicher laden
loadSettings();
lastSeenAutoSpeed = settings.autoSpeed;
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();
// Weboberfläche starten
initWebServer();
Serial.print("[Web] Erreichbar unter http://");
Serial.println(WiFi.localIP());
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);
}
int32_t getFilteredTemp(int rawSensorPin) {
constexpr uint8_t FILTER_SAMPLES = 100; // Puffergröße (100 Messwerte) Mittelwert über 10s
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;
}
// 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
if (clickPlus) {
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;
case EDIT_SPEED:
if(settings.autoSpeed < ((MAX_SPEED_HZ * 60)/MIKROSTEPS) - 50){
settings.autoSpeed += 50;
}
break;
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:
settings.startMin = (settings.startMin < 5) ? 55 : settings.startMin - 5;
break;
case EDIT_END_HOUR:
settings.endHour = (settings.endHour == 0) ? 23 : settings.endHour - 1;
break;
case EDIT_END_MIN:
settings.endMin = (settings.endMin < 5) ? 55 : settings.endMin - 5;
break;
case EDIT_SPEED:
if(settings.autoSpeed >= 50){
settings.autoSpeed -= 50;
}
break;
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 #########
if (settings.editMaxTemp) {
// PLUS: Bis maximal 150
if (clickPlus) {
if (settings.maxTemp <= 145) {
settings.maxTemp += 5;
}
}
// MINUS: Bis minimal -20
if (clickMinus) {
if (settings.maxTemp >= -15) {
settings.maxTemp -= 5;
}
}
if (clickStop) {
settings.coldTemp = settings.maxTemp - 20;
settings.editMaxTemp = false;
}
return; // Wichtig: Rest der Funktion überspringen, solange editiert wird!
}
//######### Beschleunigung Edit #########
if (settings.editAcc) {
// PLUS: Erhöhen in 1000er-Schritten bis max 20.000
if (clickPlus) {
if (settings.accelRate <= 29000) {
settings.accelRate += 500;
}
}
// MINUS: Verringern in 1000er-Schritten bis min 1.000 (niemals 0!)
if (clickMinus) {
if (settings.accelRate >= 1000) {
settings.accelRate -= 500;
}
}
// STOP: Speichern / Verlassen
if (clickStop) {
settings.editAcc = false;
stepper->setAcceleration(settings.accelRate); // Direkt am Stepper aktualisieren
}
return; // Wichtig: Rest der Funktion überspringen, solange editiert wird!
}
//######### Menü #########
// Menü bewegen
if(clickPlus){
if(settings.settingIdx <= 3){
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();
}
}
}
// Prüft ob der Motor zu heiß wird (mit Hysterese: AN ab maxTemp, AUS erst bei coldTemp)
// return: true wenn zu heiß
// false wenn unter der Grenze
bool checkOverHeat(int sensorPin){
static bool overheated = false;
curTemp = getFilteredTemp(sensorPin);
if (curTemp >= settings.maxTemp) {
overheated = true;
} else if (curTemp <= settings.coldTemp) {
overheated = false;
}
return overheated;
}
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();
int32_t currentSpeed_HZ = stepper->getCurrentSpeedInUs() == 0 ? 0 : (1000000 / stepper->getCurrentSpeedInUs());
int32_t currentSpeed = (currentSpeed_HZ * 60)/1600;
// 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();
}
}
// ############################################################
// ################ EINZIGE STELLE FÜR MOTORSTEUERUNG #########
// ############################################################
void applyMotor(MotorCmd cmd, int32_t speed) {
switch(cmd){
case MOTOR_STOP:{
if (stepper->isRunning()) {
stepper->setAcceleration(10000);
stepper->applySpeedAcceleration();
stepper->stopMove();
}
lastSpeed = 0;
lastAutoHz = 0;
break;
}
case MOTOR_AUTO:{
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
if (!stepper->isRunning()) {
stepper->setSpeedInHz(autoHz);
stepper->setAcceleration(settings.accelRate);
stepper->runForward();
lastAutoHz = autoHz;
} else if (autoHz != lastAutoHz) {
// Drehzahl wurde geändert (z. B. per Web) -> live übernehmen
stepper->setSpeedInHz(autoHz);
stepper->applySpeedAcceleration();
lastAutoHz = autoHz;
}
break;
}
case MOTOR_MANUAL:{
if (speed != lastSpeed) {
if(speed == 1){
// Plus taste Beschleunigen bis max Speed
stepper->setAcceleration(settings.accelRate);
stepper->setSpeedInHz(MAX_SPEED_HZ);
stepper->runForward();
}else if(speed == -1){
if (stepper->isRunning()){
// Minus Taste bremsen bis fast stillstand
stepper->setAcceleration(settings.accelRate);
stepper->setSpeedInHz(50); // Zeiel Speed 10
stepper->runForward();
}
}else{
// Keine Taste gedrückt Geschwindigkeit halten
uint32_t currentSpeedUs = stepper->getCurrentSpeedInUs();
if(currentSpeedUs > 0){
uint32_t currentHz = 1000000/currentSpeedUs;
if(currentHz < 10){
currentHz = 10; // Absicherunng gegen 0 Hz
}
stepper->setSpeedInHz(currentHz);
stepper->applySpeedAcceleration();
}
}
lastSpeed = speed;
lastAutoHz = 0;
}
break;
}
case MOTOR_KEEP:{
break;
}
default:{
break;
}
}
}
//#############################################################
//#################### MAIN ###################################
//#############################################################
void loop() {
int32_t speed = 0;
MotorCmd motorCmd = MOTOR_KEEP;
if (!stepper) {
Serial.println("[HALT] Kein Stepper-Objekt aktiv.");
delay(1000);
return;
}
// 1. Eingaben & Sensoren lesen
getInputs(clickPlus, clickMinus, clickStop, holdPlus, holdMinus, holdStop);
// 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);
if (settings.timerEnabled && timeIsValid) {
shouldRun = isTimerActiveNow(curH, curM, settings.startHour, settings.startMin, settings.endHour, settings.endMin);
} else {
shouldRun = false;
}
// 3. Schutz-Verriegelung (Überhitzung)
bool tempOverheat = checkOverHeat(senTemp);
// 4. Menü / Hotkeys auswerten
if (holdStop && clickPlus) {
bSettings = true;
}
// 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
static bool manualTimerStopped = false;
if (!shouldRun) {
manualTimerStopped = false; // Reset für den nächsten regulären Zeitzyklus
}
// Merker, ob wir im manuellen Betrieb sind (für sauberes Anhalten beim Moduswechsel)
static bool wasManualMode = false;
bool isManualMode = false;
// 5. Betriebsmodi: hier wird nur ENTSCHIEDEN, nicht am Motor gesteuert
if (bSettings) {
if (webRun) { // Web-Lauf beim Öffnen des Menüs beenden
webRun = false;
motorCmd = MOTOR_STOP;
} else {
motorCmd = MOTOR_KEEP;
}
setting();
}
// NOT-AUS BEI ÜBERHITZUNG
else if (tempOverheat) {
motorCmd = MOTOR_STOP;
}
// AUTOMATIK-BETRIEB (Zeitschaltuhr)
else if (settings.timerEnabled) {
// Durch Drücken von Plus den Timerbetrieb fortsetzen
if (manualTimerStopped && clickPlus) {
manualTimerStopped = false;
}
if (shouldRun && !manualTimerStopped) {
motorCmd = MOTOR_AUTO;
// Manuelles Anhalten im laufenden Timer erlauben
if (clickStop) {
manualTimerStopped = true;
motorCmd = MOTOR_STOP;
}
} else {
// Außerhalb des Fensters oder manuell gestoppt
motorCmd = MOTOR_STOP;
}
}
// MANUELLER BETRIEB (Zeitschaltuhr aus)
else {
static bool manualStopped = false;
static bool wasWebRun = false;
isManualMode = true;
motorCmd = MOTOR_MANUAL;
if (!wasManualMode) {
// Gerade erst in den Handbetrieb gewechselt: Motor einmal sauber anhalten
motorCmd = MOTOR_STOP;
speed = 0;
motorCmd = MOTOR_STOP;
} else if (clickStop || webStopReq) {
manualStopped = true;
webRun = false;
webStopReq = false;
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 = 1;
webRun = false;
} else if (holdMinus) {
speed = -1;
webRun = false;
} else if (webRun) {
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
} else {
speed = 0;
if (wasWebRun){
motorCmd = MOTOR_STOP; // Web-Stopp: einmal sauber anhalten
}
}
wasWebRun = webRun;
}
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);
// 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;
}
// 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) {
lastWeb = millis();
sendWebStatus(currentSpeed, timeIsValid, curH, curM, tempOverheat);
}
}