From 0ba1fbaea89b5cea84519cae0ad001fcc1092163 Mon Sep 17 00:00:00 2001 From: Jannis Date: Sun, 11 Oct 2026 21:41:53 +0200 Subject: [PATCH] Website V1 --- kugelmuehle.ino | 147 ++++++++++++++++++++++++++++++++++++++++-- secrets.h | 6 ++ web_server.h | 64 ++++++++++++++++++ web_ui.h | 168 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 secrets.h create mode 100644 web_server.h create mode 100644 web_ui.h diff --git a/kugelmuehle.ino b/kugelmuehle.ino index 3d207b0..200db20 100644 --- a/kugelmuehle.ino +++ b/kugelmuehle.ino @@ -1,10 +1,12 @@ #include #include #include +#include +#include -const char* ssid = "Nordlicht"; -const char* password = "03158968696374163104"; -const char* DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1552818996582285372/ChwhTOdajIwIVbdIpweSorYauwGHJTjjzWNGCjzPw1mh31-snlynh0ZW1InSar_aV8IB"; +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"; @@ -27,6 +29,7 @@ bool holdStop = 0; bool shouldRun = true; int lastSpeed = 0; +uint32_t lastAutoHz = 0; int32_t lastControl = 0; uint32_t lastPlotTime = 0; @@ -38,6 +41,9 @@ bool send = false; bool stopMove = false; +// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr) +bool webRun = false; + struct tm timeinfo; TimeEditStep timeEditStep = EDIT_NONE; @@ -133,6 +139,86 @@ void sendDiscordNotification(const String& message) { } } +// ############################################################ +// ################ 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) webRun = (c.v[0] != 0); + 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; + + 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 ######################### //####################################################### @@ -160,6 +246,11 @@ void setup() { initWiFiAndTime(); + // Weboberfläche starten + initWebServer(); + Serial.print("[Web] Erreichbar unter http://"); + Serial.println(WiFi.localIP()); + initDisplay(); // Start Animation @@ -467,14 +558,21 @@ void applyMotor(MotorCmd cmd, int32_t speed) { stepper->stopMove(); } lastSpeed = 0; + lastAutoHz = 0; break; case MOTOR_AUTO: + uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60; if (!stepper->isRunning()) { - uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60; 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; @@ -484,6 +582,7 @@ void applyMotor(MotorCmd cmd, int32_t speed) { stepper->setAcceleration(settings.accelRate); stepper->moveByAcceleration(speed, true); lastSpeed = speed; + lastAutoHz = 0; } break; @@ -508,6 +607,9 @@ void loop() { // 1. Eingaben & Sensoren lesen getInputs(clickPlus, clickMinus, clickStop, holdPlus, holdMinus, holdStop); + + // Befehle von der Weboberfläche abarbeiten + handleWebCommands(); // 2. Zeitschaltuhr & NTP prüfen int curH, curM, curS; @@ -538,10 +640,19 @@ void loop() { 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 (bSettings) { + if (webRun) { // Web-Lauf beim Öffnen des Menüs beenden + webRun = false; + motorCmd = MOTOR_STOP; + } else { + motorCmd = MOTOR_KEEP; + } setting(); - motorCmd = MOTOR_KEEP; } // NOT-AUS BEI ÜBERHITZUNG else if (tempOverheat) { @@ -571,10 +682,18 @@ void loop() { // MANUELLER BETRIEB (Zeitschaltuhr aus) else { static bool manualStopped = false; + static bool wasWebRun = false; + isManualMode = true; motorCmd = MOTOR_MANUAL; - if (clickStop) { + if (!wasManualMode) { + // Gerade erst in den Handbetrieb gewechselt: Motor einmal sauber anhalten + motorCmd = MOTOR_STOP; + speed = 0; + motorCmd = MOTOR_STOP; + } else if (clickStop) { manualStopped = true; + webRun = false; speed = 0; motorCmd = MOTOR_STOP; } else if (manualStopped) { @@ -586,10 +705,17 @@ void loop() { speed = settings.accelRate; } else if (holdMinus) { speed = -settings.accelRate; + } 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; // 6. Motor ansteuern (einzige Stelle) applyMotor(motorCmd, speed); @@ -610,4 +736,11 @@ void loop() { } else if (!tempOverheat) { send = false; } + + // 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); + } } \ No newline at end of file diff --git a/secrets.h b/secrets.h new file mode 100644 index 0000000..755f6e0 --- /dev/null +++ b/secrets.h @@ -0,0 +1,6 @@ +#pragma once + +// Diese Datei NICHT in Git einchecken (in .gitignore eintragen)! +#define WIFI_SSID "Nordlicht" +#define WIFI_PASSWORD "03158968696374163104" +#define DISCORD_WEBHOOK_URL "https://discord.com/api/webhooks/1558839849455194162/HcKX_t714K_ZRC6bjUwt11zDIaGH5vVlrHpujGgPGlL-CIeaTgSnbmgBFFaPhvAZDgh_" diff --git a/web_server.h b/web_server.h new file mode 100644 index 0000000..8929608 --- /dev/null +++ b/web_server.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include +#include +#include "web_ui.h" + +// --------------------------------------------------------------- +// Der Async-Webserver läuft in einem eigenen Task. Damit es keine +// Race-Conditions mit loop() gibt, ändern die Callbacks NIE direkt +// settings/stepper, sondern legen nur Befehle in eine Queue. +// loop() arbeitet die Queue ab (handleWebCommands in main.cpp). +// --------------------------------------------------------------- + +enum WebCmdType : uint8_t { WC_TIMER, WC_SPEED, WC_RUN, WC_MAXTEMP, WC_ACCEL, WC_WINDOW }; + +struct WebCmd { + WebCmdType type; + int32_t v[4]; +}; + +static AsyncWebServer webServer(80); +static AsyncWebSocket webSocket("/ws"); +static QueueHandle_t webQueue = nullptr; + +static void onWsEvent(AsyncWebSocket *, AsyncWebSocketClient *, AwsEventType type, + void *arg, uint8_t *data, size_t len) { + if (type != WS_EVT_DATA) return; + + AwsFrameInfo *info = (AwsFrameInfo *)arg; + if (!(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)) return; + + JsonDocument doc; + if (deserializeJson(doc, (const char *)data, len)) return; // ungültiges JSON ignorieren + + const char *c = doc["cmd"] | ""; + WebCmd cmd = {}; + + if (!strcmp(c, "timer")) { cmd.type = WC_TIMER; cmd.v[0] = doc["v"] | 0; } + else if (!strcmp(c, "speed")) { cmd.type = WC_SPEED; cmd.v[0] = doc["v"] | 0; } + else if (!strcmp(c, "run")) { cmd.type = WC_RUN; cmd.v[0] = doc["v"] | 0; } + else if (!strcmp(c, "maxTemp")) { cmd.type = WC_MAXTEMP; cmd.v[0] = doc["v"] | 0; } + else if (!strcmp(c, "accel")) { cmd.type = WC_ACCEL; cmd.v[0] = doc["v"] | 0; } + else if (!strcmp(c, "window")) { + cmd.type = WC_WINDOW; + for (int i = 0; i < 4; i++) cmd.v[i] = doc["v"][i] | 0; + } + else return; + + xQueueSend(webQueue, &cmd, 0); +} + +inline void initWebServer() { + webQueue = xQueueCreate(8, sizeof(WebCmd)); + + webSocket.onEvent(onWsEvent); + webServer.addHandler(&webSocket); + + webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", INDEX_HTML); + }); + + webServer.begin(); + Serial.println("[Web] Server gestartet"); +} diff --git a/web_ui.h b/web_ui.h new file mode 100644 index 0000000..6b74a5a --- /dev/null +++ b/web_ui.h @@ -0,0 +1,168 @@ +#pragma once +#include + +// Komplette Weboberfläche im Flash des Programms – kein LittleFS-Upload nötig. +static const char INDEX_HTML[] PROGMEM = R"rawliteral( + + + + + +Kugelmühle + + + +
+ +
+

Kugelmühle

+
+ +
+

Status

+
+
--
Temperatur °C (Limit --)
+
--
U/min
+
--
Motor
+
--:--
Uhrzeit
+
+
+ +
+

Steuerung

+ +
+
+ +
+
Bei aktiver Zeitschaltuhr läuft der Motor nur im eingestellten Zeitfenster.
+
+ +
+

Drehzahl (Automatik / Start)

+
-- U/min
+ +
+ +
+

Zeitfenster

+
+
Start
+
Ende
+
+
+
+ +
+

Einstellungen

+
+
Max. Temperatur °C
+
Beschleunigung
+
+
+
+ +
+ + + + +)rawliteral";