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.
1036 lines
No EOL
33 KiB
C++
1036 lines
No EOL
33 KiB
C++
#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);
|
||
}
|
||
} |