Website V1
This commit is contained in:
parent
b138a7119d
commit
0ba1fbaea8
4 changed files with 378 additions and 7 deletions
147
kugelmuehle.ino
147
kugelmuehle.ino
|
|
@ -1,10 +1,12 @@
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <types.h>
|
#include <types.h>
|
||||||
#include <display_ui.h>
|
#include <display_ui.h>
|
||||||
|
#include <web_server.h>
|
||||||
|
#include <secrets.h>
|
||||||
|
|
||||||
const char* ssid = "Nordlicht";
|
const char* ssid = WIFI_SSID;
|
||||||
const char* password = "03158968696374163104";
|
const char* password = WIFI_PASSWORD;
|
||||||
const char* DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1552818996582285372/ChwhTOdajIwIVbdIpweSorYauwGHJTjjzWNGCjzPw1mh31-snlynh0ZW1InSar_aV8IB";
|
const char* DISCORD_WEBHOOK = DISCORD_WEBHOOK_URL;
|
||||||
|
|
||||||
// NTP-Server & Zeitzone für Deutschland
|
// NTP-Server & Zeitzone für Deutschland
|
||||||
const char* ntpServer = "pool.ntp.org";
|
const char* ntpServer = "pool.ntp.org";
|
||||||
|
|
@ -27,6 +29,7 @@ bool holdStop = 0;
|
||||||
|
|
||||||
bool shouldRun = true;
|
bool shouldRun = true;
|
||||||
int lastSpeed = 0;
|
int lastSpeed = 0;
|
||||||
|
uint32_t lastAutoHz = 0;
|
||||||
|
|
||||||
int32_t lastControl = 0;
|
int32_t lastControl = 0;
|
||||||
uint32_t lastPlotTime = 0;
|
uint32_t lastPlotTime = 0;
|
||||||
|
|
@ -38,6 +41,9 @@ bool send = false;
|
||||||
|
|
||||||
bool stopMove = false;
|
bool stopMove = false;
|
||||||
|
|
||||||
|
// Start/Stopp von der Weboberfläche (nur ohne Zeitschaltuhr)
|
||||||
|
bool webRun = false;
|
||||||
|
|
||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
|
|
||||||
TimeEditStep timeEditStep = EDIT_NONE;
|
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 #########################
|
//####################### Setup #########################
|
||||||
//#######################################################
|
//#######################################################
|
||||||
|
|
@ -160,6 +246,11 @@ void setup() {
|
||||||
|
|
||||||
initWiFiAndTime();
|
initWiFiAndTime();
|
||||||
|
|
||||||
|
// Weboberfläche starten
|
||||||
|
initWebServer();
|
||||||
|
Serial.print("[Web] Erreichbar unter http://");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
initDisplay();
|
initDisplay();
|
||||||
|
|
||||||
// Start Animation
|
// Start Animation
|
||||||
|
|
@ -467,14 +558,21 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
||||||
stepper->stopMove();
|
stepper->stopMove();
|
||||||
}
|
}
|
||||||
lastSpeed = 0;
|
lastSpeed = 0;
|
||||||
|
lastAutoHz = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MOTOR_AUTO:
|
case MOTOR_AUTO:
|
||||||
|
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
||||||
if (!stepper->isRunning()) {
|
if (!stepper->isRunning()) {
|
||||||
uint32_t autoHz = ((uint32_t)settings.autoSpeed * MIKROSTEPS) / 60;
|
|
||||||
stepper->setSpeedInHz(autoHz);
|
stepper->setSpeedInHz(autoHz);
|
||||||
stepper->setAcceleration(settings.accelRate);
|
stepper->setAcceleration(settings.accelRate);
|
||||||
stepper->runForward();
|
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;
|
break;
|
||||||
|
|
||||||
|
|
@ -484,6 +582,7 @@ void applyMotor(MotorCmd cmd, int32_t speed) {
|
||||||
stepper->setAcceleration(settings.accelRate);
|
stepper->setAcceleration(settings.accelRate);
|
||||||
stepper->moveByAcceleration(speed, true);
|
stepper->moveByAcceleration(speed, true);
|
||||||
lastSpeed = speed;
|
lastSpeed = speed;
|
||||||
|
lastAutoHz = 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -508,6 +607,9 @@ void loop() {
|
||||||
|
|
||||||
// 1. Eingaben & Sensoren lesen
|
// 1. Eingaben & Sensoren lesen
|
||||||
getInputs(clickPlus, clickMinus, clickStop, holdPlus, holdMinus, holdStop);
|
getInputs(clickPlus, clickMinus, clickStop, holdPlus, holdMinus, holdStop);
|
||||||
|
|
||||||
|
// Befehle von der Weboberfläche abarbeiten
|
||||||
|
handleWebCommands();
|
||||||
|
|
||||||
// 2. Zeitschaltuhr & NTP prüfen
|
// 2. Zeitschaltuhr & NTP prüfen
|
||||||
int curH, curM, curS;
|
int curH, curM, curS;
|
||||||
|
|
@ -538,10 +640,19 @@ void loop() {
|
||||||
manualTimerStopped = false; // Reset für den nächsten regulären Zeitzyklus
|
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
|
// 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();
|
setting();
|
||||||
motorCmd = MOTOR_KEEP;
|
|
||||||
}
|
}
|
||||||
// NOT-AUS BEI ÜBERHITZUNG
|
// NOT-AUS BEI ÜBERHITZUNG
|
||||||
else if (tempOverheat) {
|
else if (tempOverheat) {
|
||||||
|
|
@ -571,10 +682,18 @@ void loop() {
|
||||||
// MANUELLER BETRIEB (Zeitschaltuhr aus)
|
// MANUELLER BETRIEB (Zeitschaltuhr aus)
|
||||||
else {
|
else {
|
||||||
static bool manualStopped = false;
|
static bool manualStopped = false;
|
||||||
|
static bool wasWebRun = false;
|
||||||
|
isManualMode = true;
|
||||||
motorCmd = MOTOR_MANUAL;
|
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;
|
manualStopped = true;
|
||||||
|
webRun = false;
|
||||||
speed = 0;
|
speed = 0;
|
||||||
motorCmd = MOTOR_STOP;
|
motorCmd = MOTOR_STOP;
|
||||||
} else if (manualStopped) {
|
} else if (manualStopped) {
|
||||||
|
|
@ -586,10 +705,17 @@ void loop() {
|
||||||
speed = settings.accelRate;
|
speed = settings.accelRate;
|
||||||
} else if (holdMinus) {
|
} else if (holdMinus) {
|
||||||
speed = -settings.accelRate;
|
speed = -settings.accelRate;
|
||||||
|
} else if (webRun) {
|
||||||
|
motorCmd = MOTOR_AUTO; // Web-Start: läuft mit autoSpeed
|
||||||
} else {
|
} else {
|
||||||
speed = 0;
|
speed = 0;
|
||||||
|
if (wasWebRun){
|
||||||
|
motorCmd = MOTOR_STOP; // Web-Stopp: einmal sauber anhalten
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
wasWebRun = webRun;
|
||||||
}
|
}
|
||||||
|
wasManualMode = isManualMode;
|
||||||
|
|
||||||
// 6. Motor ansteuern (einzige Stelle)
|
// 6. Motor ansteuern (einzige Stelle)
|
||||||
applyMotor(motorCmd, speed);
|
applyMotor(motorCmd, speed);
|
||||||
|
|
@ -610,4 +736,11 @@ void loop() {
|
||||||
} else if (!tempOverheat) {
|
} else if (!tempOverheat) {
|
||||||
send = false;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
6
secrets.h
Normal file
6
secrets.h
Normal file
|
|
@ -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_"
|
||||||
64
web_server.h
Normal file
64
web_server.h
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#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");
|
||||||
|
}
|
||||||
168
web_ui.h
Normal file
168
web_ui.h
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Komplette Weboberfläche im Flash des Programms – kein LittleFS-Upload nötig.
|
||||||
|
static const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Kugelmühle</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg:#1b1d21; --card:#272a30; --txt:#eceff4; --dim:#8b93a1; --ok:#3fb950; --warn:#f85149; --acc:#2f81f7; }
|
||||||
|
* { box-sizing:border-box; }
|
||||||
|
body { margin:0; font-family:system-ui,Arial,sans-serif; background:var(--bg); color:var(--txt); padding:16px; }
|
||||||
|
.wrap { max-width:460px; margin:0 auto; display:grid; gap:14px; }
|
||||||
|
.card { background:var(--card); border-radius:12px; padding:16px; }
|
||||||
|
h1 { font-size:1.2rem; margin:0; display:flex; justify-content:space-between; align-items:center; }
|
||||||
|
h2 { font-size:.8rem; margin:0 0 10px; color:var(--dim); text-transform:uppercase; letter-spacing:.06em; }
|
||||||
|
.dot { width:10px; height:10px; border-radius:50%; background:var(--warn); }
|
||||||
|
.dot.on { background:var(--ok); }
|
||||||
|
.grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||||
|
.val { font-size:1.8rem; font-weight:700; }
|
||||||
|
.lbl { font-size:.8rem; color:var(--dim); }
|
||||||
|
.hot { color:var(--warn); }
|
||||||
|
.state { font-weight:700; }
|
||||||
|
.state.run { color:var(--ok); }
|
||||||
|
.state.err { color:var(--warn); }
|
||||||
|
button { width:100%; padding:14px; border:0; border-radius:8px; font-size:1rem; font-weight:600; color:#fff; background:var(--acc); cursor:pointer; }
|
||||||
|
button.go { background:var(--ok); }
|
||||||
|
button.stop { background:var(--warn); }
|
||||||
|
button.off { background:#444b57; }
|
||||||
|
button:disabled { opacity:.4; cursor:not-allowed; }
|
||||||
|
.row { display:flex; gap:10px; align-items:center; margin-top:10px; }
|
||||||
|
.row > * { flex:1; }
|
||||||
|
input[type=number], input[type=time] { width:100%; padding:10px; border-radius:8px; border:1px solid #444b57; background:#1b1d21; color:var(--txt); font-size:1rem; }
|
||||||
|
input[type=range] { width:100%; }
|
||||||
|
.small { font-size:.8rem; color:var(--dim); margin-top:6px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h1>Kugelmühle <span class="dot" id="conn" title="Verbindung"></span></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Status</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div><div class="val" id="temp">--</div><div class="lbl">Temperatur °C (Limit <span id="maxT">--</span>)</div></div>
|
||||||
|
<div><div class="val" id="rpm">--</div><div class="lbl">U/min</div></div>
|
||||||
|
<div><div class="state" id="state">--</div><div class="lbl">Motor</div></div>
|
||||||
|
<div><div class="state" id="clock">--:--</div><div class="lbl">Uhrzeit</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Steuerung</h2>
|
||||||
|
<button id="btnRun" onclick="toggleRun()">Start</button>
|
||||||
|
<div class="small" id="runHint"></div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnTimer" onclick="toggleTimer()">Zeitschaltuhr</button>
|
||||||
|
</div>
|
||||||
|
<div class="small">Bei aktiver Zeitschaltuhr läuft der Motor nur im eingestellten Zeitfenster.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Drehzahl (Automatik / Start)</h2>
|
||||||
|
<div><b id="speedVal">--</b> U/min</div>
|
||||||
|
<input type="range" id="speed" min="50" max="1987" step="50"
|
||||||
|
oninput="$('speedVal').textContent=this.value"
|
||||||
|
onchange="send('speed', +this.value)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Zeitfenster</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div><div class="lbl">Start</div><input type="time" id="tStart"></div>
|
||||||
|
<div><div class="lbl">Ende</div><input type="time" id="tEnd"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row"><button onclick="sendWindow()">Übernehmen</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Einstellungen</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div><div class="lbl">Max. Temperatur °C</div><input type="number" id="maxTemp" min="-20" max="150" step="5"></div>
|
||||||
|
<div><div class="lbl">Beschleunigung</div><input type="number" id="accel" min="1000" max="30000" step="500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row"><button onclick="sendSettings()">Übernehmen</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
let ws, last = {};
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
ws = new WebSocket(`ws://${location.host}/ws`);
|
||||||
|
ws.onopen = () => $('conn').classList.add('on');
|
||||||
|
ws.onclose = () => { $('conn').classList.remove('on'); setTimeout(connect, 2000); };
|
||||||
|
ws.onmessage = e => render(JSON.parse(e.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(cmd, v) {
|
||||||
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
function setIfIdle(id, value) {
|
||||||
|
const el = $(id);
|
||||||
|
if (document.activeElement !== el) el.value = value; // nicht überschreiben, während getippt/gezogen wird
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(d) {
|
||||||
|
last = d;
|
||||||
|
$('temp').textContent = d.temp;
|
||||||
|
$('temp').classList.toggle('hot', d.overheat);
|
||||||
|
$('maxT').textContent = d.maxTemp;
|
||||||
|
$('rpm').textContent = d.rpm;
|
||||||
|
$('clock').textContent = d.time;
|
||||||
|
|
||||||
|
const st = $('state');
|
||||||
|
st.className = 'state';
|
||||||
|
if (d.overheat) { st.textContent = 'ÜBERHITZT'; st.classList.add('err'); }
|
||||||
|
else if (d.running) { st.textContent = 'Läuft'; st.classList.add('run'); }
|
||||||
|
else { st.textContent = 'Gestoppt'; }
|
||||||
|
|
||||||
|
const run = $('btnRun');
|
||||||
|
run.disabled = d.overheat || d.timerOn;
|
||||||
|
run.textContent = d.webRun ? 'Stopp' : 'Start';
|
||||||
|
run.className = d.webRun ? 'stop' : 'go';
|
||||||
|
$('runHint').textContent = d.timerOn ? 'Start/Stopp ist gesperrt, solange die Zeitschaltuhr aktiv ist.' : '';
|
||||||
|
|
||||||
|
const tm = $('btnTimer');
|
||||||
|
tm.textContent = 'Zeitschaltuhr: ' + (d.timerOn ? 'AN' : 'AUS');
|
||||||
|
tm.className = d.timerOn ? 'go' : 'off';
|
||||||
|
|
||||||
|
$('speed').max = d.maxRpm;
|
||||||
|
if (document.activeElement !== $('speed')) { $('speed').value = d.autoSpeed; $('speedVal').textContent = d.autoSpeed; }
|
||||||
|
setIfIdle('tStart', pad(d.sh) + ':' + pad(d.sm));
|
||||||
|
setIfIdle('tEnd', pad(d.eh) + ':' + pad(d.em));
|
||||||
|
setIfIdle('maxTemp', d.maxTemp);
|
||||||
|
setIfIdle('accel', d.accel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRun() { send('run', last.webRun ? 0 : 1); }
|
||||||
|
function toggleTimer() { send('timer', last.timerOn ? 0 : 1); }
|
||||||
|
|
||||||
|
function sendWindow() {
|
||||||
|
const [sh, sm] = $('tStart').value.split(':').map(Number);
|
||||||
|
const [eh, em] = $('tEnd').value.split(':').map(Number);
|
||||||
|
if ([sh, sm, eh, em].some(isNaN)) return;
|
||||||
|
send('window', [sh, sm, eh, em]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSettings() {
|
||||||
|
send('maxTemp', +$('maxTemp').value);
|
||||||
|
send('accel', +$('accel').value);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
Loading…
Reference in a new issue