Merge pull request 'Kleine änderungen' (#2) from v1 into main
Some checks failed
/ build (push) Failing after 26s
Some checks failed
/ build (push) Failing after 26s
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
BIN
data/firmware.bin
Normal file
BIN
data/firmware.bin
Normal file
Binary file not shown.
@@ -471,6 +471,13 @@ body {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status.large-status.ready {
|
||||||
|
font-size: clamp(2rem, 8vw, 8rem) !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
.status.finished {
|
.status.finished {
|
||||||
background-color: rgba(73, 186, 228, 0.3);
|
background-color: rgba(73, 186, 228, 0.3);
|
||||||
border: 2px solid #49bae4;
|
border: 2px solid #49bae4;
|
||||||
@@ -479,7 +486,6 @@ body {
|
|||||||
.status.ready {
|
.status.ready {
|
||||||
background-color: rgb(0 165 3 / 54%);
|
background-color: rgb(0 165 3 / 54%);
|
||||||
border: 2px solid #06ff00;
|
border: 2px solid #06ff00;
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.armed {
|
.status.armed {
|
||||||
|
|||||||
178
data/index.html
178
data/index.html
@@ -449,57 +449,80 @@
|
|||||||
if (!lane1Connected) {
|
if (!lane1Connected) {
|
||||||
s1.className = "status standby large-status";
|
s1.className = "status standby large-status";
|
||||||
s1.textContent = "Standby: Drücke beide Buttons einmal";
|
s1.textContent = "Standby: Drücke beide Buttons einmal";
|
||||||
|
time1Element.style.display = "none";
|
||||||
// Position über time-display, aber innerhalb des Containers
|
// Position über time-display, aber innerhalb des Containers
|
||||||
if (s1.classList.contains("large-status")) {
|
if (s1.classList.contains("large-status")) {
|
||||||
const time1Rect = time1Element.getBoundingClientRect();
|
|
||||||
const lane1Rect = lane1Element.getBoundingClientRect();
|
const lane1Rect = lane1Element.getBoundingClientRect();
|
||||||
const h2Rect = h2_1.getBoundingClientRect();
|
const h2Rect = h2_1.getBoundingClientRect();
|
||||||
const time1Center = time1Rect.top - lane1Rect.top + time1Rect.height / 2;
|
|
||||||
const h2Bottom = h2Rect.bottom - lane1Rect.top;
|
const h2Bottom = h2Rect.bottom - lane1Rect.top;
|
||||||
// Stelle sicher, dass die obere Kante der Status-Box unter h2 beginnt
|
|
||||||
// Beginne unter h2 (ohne translate(-50%, -50%) beginnt die Box von oben)
|
|
||||||
const startTop = h2Bottom + 10;
|
const startTop = h2Bottom + 10;
|
||||||
// Positioniere so, dass die Box über time-display zentriert ist, aber nicht über h2 hinausragt
|
s1.style.top = startTop + "px";
|
||||||
// Berechne die benötigte Höhe, um über time-display zentriert zu sein
|
s1.style.left = "50%";
|
||||||
const statusHeight = s1.offsetHeight || 200; // Verwende tatsächliche Höhe oder Schätzwert
|
|
||||||
const targetTop = Math.max(startTop, time1Center - statusHeight / 2);
|
|
||||||
s1.style.top = targetTop + "px";
|
|
||||||
s1.style.transform = "translateX(-50%)";
|
s1.style.transform = "translateX(-50%)";
|
||||||
// Stelle sicher, dass die Box innerhalb des Containers bleibt
|
s1.style.bottom = "20px";
|
||||||
const maxHeight = lane1Rect.height - targetTop - 30;
|
s1.style.width = "calc(100% - 40px)";
|
||||||
s1.style.maxHeight = maxHeight + "px";
|
s1.style.display = "flex";
|
||||||
s1.style.overflow = "auto";
|
s1.style.alignItems = "center";
|
||||||
|
s1.style.justifyContent = "center";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s1.className = `status ${status1}`;
|
s1.className = `status ${status1}`;
|
||||||
|
|
||||||
// Add large-status class if not running and not finished
|
// Wenn status "ready" ist, verstecke Zeit und mache Status groß
|
||||||
if (status1 !== "running" && status1 !== "finished") {
|
if (status1 === "ready") {
|
||||||
s1.classList.add("large-status");
|
s1.classList.add("large-status");
|
||||||
// Position über time-display, aber innerhalb des Containers
|
time1Element.style.display = "none";
|
||||||
const time1Rect = time1Element.getBoundingClientRect();
|
|
||||||
const lane1Rect = lane1Element.getBoundingClientRect();
|
const lane1Rect = lane1Element.getBoundingClientRect();
|
||||||
const h2Rect = h2_1.getBoundingClientRect();
|
const h2Rect = h2_1.getBoundingClientRect();
|
||||||
const time1Center = time1Rect.top - lane1Rect.top + time1Rect.height / 2;
|
|
||||||
const h2Bottom = h2Rect.bottom - lane1Rect.top;
|
const h2Bottom = h2Rect.bottom - lane1Rect.top;
|
||||||
// Stelle sicher, dass die obere Kante der Status-Box unter h2 beginnt
|
|
||||||
// Beginne unter h2 (ohne translate(-50%, -50%) beginnt die Box von oben)
|
|
||||||
const startTop = h2Bottom + 10;
|
const startTop = h2Bottom + 10;
|
||||||
// Positioniere so, dass die Box über time-display zentriert ist, aber nicht über h2 hinausragt
|
s1.style.top = startTop + "px";
|
||||||
// Berechne die benötigte Höhe, um über time-display zentriert zu sein
|
s1.style.left = "50%";
|
||||||
const statusHeight = s1.offsetHeight || 200; // Verwende tatsächliche Höhe oder Schätzwert
|
|
||||||
const targetTop = Math.max(startTop, time1Center - statusHeight / 2);
|
|
||||||
s1.style.top = targetTop + "px";
|
|
||||||
s1.style.transform = "translateX(-50%)";
|
s1.style.transform = "translateX(-50%)";
|
||||||
// Stelle sicher, dass die Box innerhalb des Containers bleibt
|
s1.style.bottom = "20px";
|
||||||
const maxHeight = lane1Rect.height - targetTop - 30;
|
s1.style.width = "calc(100% - 40px)";
|
||||||
s1.style.maxHeight = maxHeight + "px";
|
s1.style.display = "flex";
|
||||||
s1.style.overflow = "auto";
|
s1.style.alignItems = "center";
|
||||||
|
s1.style.justifyContent = "center";
|
||||||
|
s1.style.fontSize = "clamp(2rem, 8vw, 8rem)";
|
||||||
} else {
|
} else {
|
||||||
s1.classList.remove("large-status");
|
// Bei anderen Status (running, finished, etc.) zeige Zeit wieder an
|
||||||
s1.style.top = "";
|
time1Element.style.display = "";
|
||||||
s1.style.transform = "";
|
if (status1 !== "running" && status1 !== "finished") {
|
||||||
s1.style.maxHeight = "";
|
s1.classList.add("large-status");
|
||||||
|
const time1Rect = time1Element.getBoundingClientRect();
|
||||||
|
const lane1Rect = lane1Element.getBoundingClientRect();
|
||||||
|
const h2Rect = h2_1.getBoundingClientRect();
|
||||||
|
const time1Center = time1Rect.top - lane1Rect.top + time1Rect.height / 2;
|
||||||
|
const h2Bottom = h2Rect.bottom - lane1Rect.top;
|
||||||
|
const startTop = h2Bottom + 10;
|
||||||
|
const statusHeight = s1.offsetHeight || 200;
|
||||||
|
const targetTop = Math.max(startTop, time1Center - statusHeight / 2);
|
||||||
|
s1.style.top = targetTop + "px";
|
||||||
|
s1.style.transform = "translateX(-50%)";
|
||||||
|
s1.style.height = "";
|
||||||
|
s1.style.width = "";
|
||||||
|
s1.style.display = "";
|
||||||
|
s1.style.alignItems = "";
|
||||||
|
s1.style.justifyContent = "";
|
||||||
|
s1.style.fontSize = "";
|
||||||
|
const maxHeight = lane1Rect.height - targetTop - 30;
|
||||||
|
s1.style.maxHeight = maxHeight + "px";
|
||||||
|
s1.style.overflow = "auto";
|
||||||
|
} else {
|
||||||
|
s1.classList.remove("large-status");
|
||||||
|
s1.style.top = "";
|
||||||
|
s1.style.transform = "";
|
||||||
|
s1.style.maxHeight = "";
|
||||||
|
s1.style.height = "";
|
||||||
|
s1.style.width = "";
|
||||||
|
s1.style.display = "";
|
||||||
|
s1.style.alignItems = "";
|
||||||
|
s1.style.justifyContent = "";
|
||||||
|
s1.style.fontSize = "";
|
||||||
|
s1.style.left = "";
|
||||||
|
s1.style.bottom = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status1) {
|
switch (status1) {
|
||||||
@@ -529,57 +552,80 @@
|
|||||||
if (!lane2Connected) {
|
if (!lane2Connected) {
|
||||||
s2.className = "status standby large-status";
|
s2.className = "status standby large-status";
|
||||||
s2.textContent = "Standby: Drücke beide Buttons einmal";
|
s2.textContent = "Standby: Drücke beide Buttons einmal";
|
||||||
|
time2Element.style.display = "none";
|
||||||
// Position über time-display, aber innerhalb des Containers
|
// Position über time-display, aber innerhalb des Containers
|
||||||
if (s2.classList.contains("large-status")) {
|
if (s2.classList.contains("large-status")) {
|
||||||
const time2Rect = time2Element.getBoundingClientRect();
|
|
||||||
const lane2Rect = lane2Element.getBoundingClientRect();
|
const lane2Rect = lane2Element.getBoundingClientRect();
|
||||||
const h2Rect = h2_2.getBoundingClientRect();
|
const h2Rect = h2_2.getBoundingClientRect();
|
||||||
const time2Center = time2Rect.top - lane2Rect.top + time2Rect.height / 2;
|
|
||||||
const h2Bottom = h2Rect.bottom - lane2Rect.top;
|
const h2Bottom = h2Rect.bottom - lane2Rect.top;
|
||||||
// Stelle sicher, dass die obere Kante der Status-Box unter h2 beginnt
|
|
||||||
// Beginne unter h2 (ohne translate(-50%, -50%) beginnt die Box von oben)
|
|
||||||
const startTop = h2Bottom + 10;
|
const startTop = h2Bottom + 10;
|
||||||
// Positioniere so, dass die Box über time-display zentriert ist, aber nicht über h2 hinausragt
|
s2.style.top = startTop + "px";
|
||||||
// Berechne die benötigte Höhe, um über time-display zentriert zu sein
|
s2.style.left = "50%";
|
||||||
const statusHeight = s2.offsetHeight || 200; // Verwende tatsächliche Höhe oder Schätzwert
|
|
||||||
const targetTop = Math.max(startTop, time2Center - statusHeight / 2);
|
|
||||||
s2.style.top = targetTop + "px";
|
|
||||||
s2.style.transform = "translateX(-50%)";
|
s2.style.transform = "translateX(-50%)";
|
||||||
// Stelle sicher, dass die Box innerhalb des Containers bleibt
|
s2.style.bottom = "20px";
|
||||||
const maxHeight = lane2Rect.height - targetTop - 30;
|
s2.style.width = "calc(100% - 40px)";
|
||||||
s2.style.maxHeight = maxHeight + "px";
|
s2.style.display = "flex";
|
||||||
s2.style.overflow = "auto";
|
s2.style.alignItems = "center";
|
||||||
|
s2.style.justifyContent = "center";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s2.className = `status ${status2}`;
|
s2.className = `status ${status2}`;
|
||||||
|
|
||||||
// Add large-status class if not running and not finished
|
// Wenn status "ready" ist, verstecke Zeit und mache Status groß
|
||||||
if (status2 !== "running" && status2 !== "finished") {
|
if (status2 === "ready") {
|
||||||
s2.classList.add("large-status");
|
s2.classList.add("large-status");
|
||||||
// Position über time-display, aber innerhalb des Containers
|
time2Element.style.display = "none";
|
||||||
const time2Rect = time2Element.getBoundingClientRect();
|
|
||||||
const lane2Rect = lane2Element.getBoundingClientRect();
|
const lane2Rect = lane2Element.getBoundingClientRect();
|
||||||
const h2Rect = h2_2.getBoundingClientRect();
|
const h2Rect = h2_2.getBoundingClientRect();
|
||||||
// Stelle sicher, dass die obere Kante der Status-Box unter h2 beginnt
|
|
||||||
const h2Bottom = h2Rect.bottom - lane2Rect.top;
|
const h2Bottom = h2Rect.bottom - lane2Rect.top;
|
||||||
const time2Center = time2Rect.top - lane2Rect.top + time2Rect.height / 2;
|
|
||||||
// Beginne unter h2 (ohne translate(-50%, -50%) beginnt die Box von oben)
|
|
||||||
const startTop = h2Bottom + 10;
|
const startTop = h2Bottom + 10;
|
||||||
// Positioniere so, dass die Box über time-display zentriert ist, aber nicht über h2 hinausragt
|
s2.style.top = startTop + "px";
|
||||||
// Berechne die benötigte Höhe, um über time-display zentriert zu sein
|
s2.style.left = "50%";
|
||||||
const statusHeight = s2.offsetHeight || 200; // Verwende tatsächliche Höhe oder Schätzwert
|
|
||||||
const targetTop = Math.max(startTop, time2Center - statusHeight / 2);
|
|
||||||
s2.style.top = targetTop + "px";
|
|
||||||
s2.style.transform = "translateX(-50%)";
|
s2.style.transform = "translateX(-50%)";
|
||||||
// Stelle sicher, dass die Box innerhalb des Containers bleibt
|
s2.style.bottom = "20px";
|
||||||
const maxHeight = lane2Rect.height - targetTop - 30;
|
s2.style.width = "calc(100% - 40px)";
|
||||||
s2.style.maxHeight = maxHeight + "px";
|
s2.style.display = "flex";
|
||||||
s2.style.overflow = "auto";
|
s2.style.alignItems = "center";
|
||||||
|
s2.style.justifyContent = "center";
|
||||||
|
s2.style.fontSize = "clamp(2rem, 8vw, 8rem)";
|
||||||
} else {
|
} else {
|
||||||
s2.classList.remove("large-status");
|
// Bei anderen Status (running, finished, etc.) zeige Zeit wieder an
|
||||||
s2.style.top = "";
|
time2Element.style.display = "";
|
||||||
s2.style.transform = "";
|
if (status2 !== "running" && status2 !== "finished") {
|
||||||
s2.style.maxHeight = "";
|
s2.classList.add("large-status");
|
||||||
|
const time2Rect = time2Element.getBoundingClientRect();
|
||||||
|
const lane2Rect = lane2Element.getBoundingClientRect();
|
||||||
|
const h2Rect = h2_2.getBoundingClientRect();
|
||||||
|
const time2Center = time2Rect.top - lane2Rect.top + time2Rect.height / 2;
|
||||||
|
const h2Bottom = h2Rect.bottom - lane2Rect.top;
|
||||||
|
const startTop = h2Bottom + 10;
|
||||||
|
const statusHeight = s2.offsetHeight || 200;
|
||||||
|
const targetTop = Math.max(startTop, time2Center - statusHeight / 2);
|
||||||
|
s2.style.top = targetTop + "px";
|
||||||
|
s2.style.transform = "translateX(-50%)";
|
||||||
|
s2.style.height = "";
|
||||||
|
s2.style.width = "";
|
||||||
|
s2.style.display = "";
|
||||||
|
s2.style.alignItems = "";
|
||||||
|
s2.style.justifyContent = "";
|
||||||
|
s2.style.fontSize = "";
|
||||||
|
const maxHeight = lane2Rect.height - targetTop - 30;
|
||||||
|
s2.style.maxHeight = maxHeight + "px";
|
||||||
|
s2.style.overflow = "auto";
|
||||||
|
} else {
|
||||||
|
s2.classList.remove("large-status");
|
||||||
|
s2.style.top = "";
|
||||||
|
s2.style.transform = "";
|
||||||
|
s2.style.maxHeight = "";
|
||||||
|
s2.style.height = "";
|
||||||
|
s2.style.width = "";
|
||||||
|
s2.style.display = "";
|
||||||
|
s2.style.alignItems = "";
|
||||||
|
s2.style.justifyContent = "";
|
||||||
|
s2.style.fontSize = "";
|
||||||
|
s2.style.left = "";
|
||||||
|
s2.style.bottom = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status2) {
|
switch (status2) {
|
||||||
|
|||||||
4
mock-server/.gitignore
vendored
4
mock-server/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
# AquaMaster Mock Server
|
|
||||||
|
|
||||||
Mock ESP32 Server und MQTT-Broker für lokales Testing ohne Hardware.
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Dieses Projekt simuliert:
|
|
||||||
- **MQTT-Broker** (Port 1883 TCP, Port 9001 WebSocket) - Lokaler MQTT-Broker für Kommunikation
|
|
||||||
- **Mock ESP32 Server** (Port 80) - Simuliert alle ESP32 API-Endpunkte und WebSocket
|
|
||||||
- **Web Debug UI** - Browser-basierte Oberfläche zum Testen von API und MQTT
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
- Node.js 16+ (LTS empfohlen)
|
|
||||||
- npm
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mock-server
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
### Option 1: Beide Server zusammen starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
# oder
|
|
||||||
node start_all.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Server einzeln starten
|
|
||||||
|
|
||||||
**Terminal 1 - MQTT Broker:**
|
|
||||||
```bash
|
|
||||||
npm run mqtt
|
|
||||||
# oder
|
|
||||||
node mqtt_broker.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 2 - Mock ESP32 Server:**
|
|
||||||
```bash
|
|
||||||
npm run server
|
|
||||||
# oder
|
|
||||||
node mock_esp32_server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web Debug UI öffnen
|
|
||||||
|
|
||||||
Nach dem Start der Server:
|
|
||||||
1. Öffne einen Browser
|
|
||||||
2. Navigiere zu: `http://localhost:80`
|
|
||||||
3. Die Debug-Oberfläche sollte sichtbar sein
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### MQTT Broker
|
|
||||||
- Läuft auf Port 1883 (TCP) und Port 9001 (WebSocket)
|
|
||||||
- Unterstützt alle relevanten Topics:
|
|
||||||
- `aquacross/button/#`
|
|
||||||
- `aquacross/button/rfid/#`
|
|
||||||
- `aquacross/battery/#`
|
|
||||||
- `heartbeat/alive/#`
|
|
||||||
- `aquacross/competition/#`
|
|
||||||
- `sync/time`
|
|
||||||
- `aquacross/lanes/#`
|
|
||||||
- Loggt alle Nachrichten für Debugging
|
|
||||||
|
|
||||||
### Mock ESP32 Server
|
|
||||||
- Simuliert alle API-Endpunkte aus der ESP32-Firmware
|
|
||||||
- WebSocket-Support für Live-Updates
|
|
||||||
- MQTT-Client, der sich mit dem Broker verbindet
|
|
||||||
- Timer-Logik (Individual/Wettkampf-Modi)
|
|
||||||
- Button-Konfigurationen und Learning-Mode
|
|
||||||
|
|
||||||
### Web Debug UI
|
|
||||||
- **API Testing Tab**: Teste alle API-Endpunkte
|
|
||||||
- **MQTT Testing Tab**: Publish/Subscribe MQTT-Nachrichten
|
|
||||||
- **Debug Endpoints Tab**: Direkte Timer-Kontrolle
|
|
||||||
|
|
||||||
## API-Endpunkte
|
|
||||||
|
|
||||||
Alle Endpunkte sind unter `http://localhost:80/api/...` verfügbar:
|
|
||||||
|
|
||||||
- `GET /api/data` - Timer-Daten abrufen
|
|
||||||
- `POST /api/reset-best` - Beste Zeiten zurücksetzen
|
|
||||||
- `POST /api/unlearn-button` - Button-Zuordnungen löschen
|
|
||||||
- `GET /api/debug/start1` - Lane 1 starten (Debug)
|
|
||||||
- `GET /api/debug/stop1` - Lane 1 stoppen (Debug)
|
|
||||||
- `GET /api/debug/start2` - Lane 2 starten (Debug)
|
|
||||||
- `GET /api/debug/stop2` - Lane 2 stoppen (Debug)
|
|
||||||
- ... und viele mehr (siehe `../API.md`)
|
|
||||||
|
|
||||||
## MQTT Topics
|
|
||||||
|
|
||||||
### Button Topics
|
|
||||||
- `aquacross/button/{MAC}` - Button-Press Nachrichten
|
|
||||||
```json
|
|
||||||
{"type": 2, "timestamp": 1234567890}
|
|
||||||
```
|
|
||||||
- `type: 2` = Start-Button
|
|
||||||
- `type: 1` = Stop-Button
|
|
||||||
|
|
||||||
### RFID Topics
|
|
||||||
- `aquacross/button/rfid/{MAC}` - RFID-Read Nachrichten
|
|
||||||
```json
|
|
||||||
{"uid": "TEST123456"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Battery Topics
|
|
||||||
- `aquacross/battery/{MAC}` - Batteriestand
|
|
||||||
```json
|
|
||||||
{"voltage": 3600}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Heartbeat Topics
|
|
||||||
- `heartbeat/alive/{MAC}` - Heartbeat-Nachrichten
|
|
||||||
```json
|
|
||||||
{"timestamp": 1234567890}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Competition Topics
|
|
||||||
- `aquacross/competition/toMaster` - Wettkampf-Start
|
|
||||||
```
|
|
||||||
"start"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Time Sync
|
|
||||||
- `sync/time` - Zeit-Synchronisation (vom Server alle 5 Sekunden)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Port bereits belegt
|
|
||||||
Falls Port 80 oder 1883 bereits belegt sind:
|
|
||||||
- Windows: Port 80 benötigt Admin-Rechte, verwende einen anderen Port
|
|
||||||
- Linux/Mac: Port 80 benötigt sudo, verwende einen anderen Port
|
|
||||||
|
|
||||||
Um Ports zu ändern, editiere:
|
|
||||||
- `mqtt_broker.js` - Zeile mit `const port = 1883;`
|
|
||||||
- `mock_esp32_server.js` - Zeile mit `const PORT = 80;`
|
|
||||||
|
|
||||||
### MQTT-Verbindung fehlgeschlagen
|
|
||||||
- Stelle sicher, dass der MQTT-Broker läuft
|
|
||||||
- Prüfe, ob Port 1883 (TCP) oder 9001 (WebSocket) erreichbar ist
|
|
||||||
- Browser benötigen WebSocket-Verbindung (Port 9001)
|
|
||||||
|
|
||||||
### WebSocket-Verbindung fehlgeschlagen
|
|
||||||
- Stelle sicher, dass der Mock ESP32 Server läuft
|
|
||||||
- Prüfe Browser-Konsole auf Fehler
|
|
||||||
- Socket.io sollte automatisch geladen werden
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
mock-server/
|
|
||||||
├── package.json # Node.js Dependencies
|
|
||||||
├── README.md # Diese Datei
|
|
||||||
├── .gitignore # Git ignore
|
|
||||||
├── mqtt_broker.js # MQTT-Broker
|
|
||||||
├── mock_esp32_server.js # Mock ESP32 Server
|
|
||||||
├── start_all.js # Startet beide Server
|
|
||||||
└── debug_server/
|
|
||||||
├── index.html # Web Debug UI
|
|
||||||
├── debug.js # JavaScript-Logik
|
|
||||||
└── debug.css # Styling
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hinweise
|
|
||||||
|
|
||||||
- Der Mock-Server speichert keinen persistenten State (alles im Speicher)
|
|
||||||
- Nach Neustart sind alle Einstellungen zurückgesetzt
|
|
||||||
- Für Produktionstests sollte der echte ESP32 verwendet werden
|
|
||||||
- Dieser Mock-Server ist nur für Entwicklung und Testing gedacht
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
MIT
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.connected {
|
|
||||||
background: #27ae60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.disconnected {
|
|
||||||
background: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button:hover {
|
|
||||||
color: #2c3e50;
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button.active {
|
|
||||||
color: #2c3e50;
|
|
||||||
border-bottom-color: #3498db;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section h2 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #2c3e50;
|
|
||||||
border-bottom: 2px solid #ecf0f1;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section h3 {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #34495e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #95a5a6;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: #7f8c8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-small {
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.response-section {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.response-section pre {
|
|
||||||
background: #2c3e50;
|
|
||||||
color: #ecf0f1;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-size: 13px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages-section {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages-container {
|
|
||||||
background: #2c3e50;
|
|
||||||
color: #ecf0f1;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item {
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 3px solid #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item .timestamp {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item .topic {
|
|
||||||
color: #3498db;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item .payload {
|
|
||||||
color: #ecf0f1;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
// Configuration
|
|
||||||
const API_BASE_URL = 'http://localhost:80';
|
|
||||||
const MQTT_BROKER_URL = 'ws://localhost:9001/mqtt'; // WebSocket port for MQTT
|
|
||||||
const WS_URL = 'http://localhost:80';
|
|
||||||
|
|
||||||
// State
|
|
||||||
let mqttClient = null;
|
|
||||||
let wsClient = null;
|
|
||||||
let subscribedTopics = new Set();
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
setupTabs();
|
|
||||||
setupAPI();
|
|
||||||
setupMQTT();
|
|
||||||
setupWebSocket();
|
|
||||||
setupDebug();
|
|
||||||
setupQuickActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab Management
|
|
||||||
function setupTabs() {
|
|
||||||
const tabButtons = document.querySelectorAll('.tab-button');
|
|
||||||
const tabContents = document.querySelectorAll('.tab-content');
|
|
||||||
|
|
||||||
tabButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const tabName = button.dataset.tab;
|
|
||||||
|
|
||||||
// Remove active class from all
|
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
tabContents.forEach(content => content.classList.remove('active'));
|
|
||||||
|
|
||||||
// Add active class to selected
|
|
||||||
button.classList.add('active');
|
|
||||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Setup
|
|
||||||
function setupAPI() {
|
|
||||||
const endpointSelect = document.getElementById('api-endpoint');
|
|
||||||
const paramsTextarea = document.getElementById('api-params');
|
|
||||||
const sendBtn = document.getElementById('api-send-btn');
|
|
||||||
const responsePre = document.getElementById('api-response');
|
|
||||||
|
|
||||||
sendBtn.addEventListener('click', async () => {
|
|
||||||
const endpoint = endpointSelect.value;
|
|
||||||
const [method, path] = endpoint.split(' ');
|
|
||||||
const params = paramsTextarea.value.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
let options = {
|
|
||||||
method: method,
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method === 'POST' && params) {
|
|
||||||
// Try to parse as JSON, otherwise use as form data
|
|
||||||
try {
|
|
||||||
const jsonData = JSON.parse(params);
|
|
||||||
options.headers['Content-Type'] = 'application/json';
|
|
||||||
options.body = JSON.stringify(jsonData);
|
|
||||||
} catch {
|
|
||||||
// Not JSON, use form data
|
|
||||||
const formData = new URLSearchParams();
|
|
||||||
const pairs = params.split('&');
|
|
||||||
pairs.forEach(pair => {
|
|
||||||
const [key, value] = pair.split('=');
|
|
||||||
if (key && value) {
|
|
||||||
formData.append(key, decodeURIComponent(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
||||||
options.body = formData.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, options);
|
|
||||||
const text = await response.text();
|
|
||||||
|
|
||||||
let formatted;
|
|
||||||
try {
|
|
||||||
formatted = JSON.stringify(JSON.parse(text), null, 2);
|
|
||||||
} catch {
|
|
||||||
formatted = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
responsePre.textContent = formatted;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
responsePre.textContent = `Error: ${error.message}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// MQTT Setup
|
|
||||||
function setupMQTT() {
|
|
||||||
const topicInput = document.getElementById('mqtt-topic');
|
|
||||||
const payloadTextarea = document.getElementById('mqtt-payload');
|
|
||||||
const publishBtn = document.getElementById('mqtt-publish-btn');
|
|
||||||
const subscribeBtn = document.getElementById('mqtt-subscribe-btn');
|
|
||||||
const unsubscribeBtn = document.getElementById('mqtt-unsubscribe-btn');
|
|
||||||
const subscribeTopicInput = document.getElementById('mqtt-subscribe-topic');
|
|
||||||
const messagesContainer = document.getElementById('mqtt-messages');
|
|
||||||
const clearMessagesBtn = document.getElementById('clear-messages-btn');
|
|
||||||
|
|
||||||
// Connect to MQTT broker
|
|
||||||
try {
|
|
||||||
mqttClient = mqtt.connect(MQTT_BROKER_URL, {
|
|
||||||
clientId: 'debug-ui-' + Math.random().toString(16).substr(2, 8)
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('connect', () => {
|
|
||||||
console.log('MQTT connected');
|
|
||||||
updateStatus('mqtt-status', 'MQTT: Connected', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('error', (error) => {
|
|
||||||
console.error('MQTT error:', error);
|
|
||||||
updateStatus('mqtt-status', 'MQTT: Error', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('close', () => {
|
|
||||||
console.log('MQTT disconnected');
|
|
||||||
updateStatus('mqtt-status', 'MQTT: Disconnected', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('message', (topic, message) => {
|
|
||||||
addMessage(topic, message.toString());
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to MQTT:', error);
|
|
||||||
updateStatus('mqtt-status', 'MQTT: Connection Failed', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
publishBtn.addEventListener('click', () => {
|
|
||||||
const topic = topicInput.value.trim();
|
|
||||||
let payload = payloadTextarea.value.trim();
|
|
||||||
|
|
||||||
if (!topic) {
|
|
||||||
alert('Please enter a topic');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse as JSON, otherwise use as-is
|
|
||||||
try {
|
|
||||||
const jsonData = JSON.parse(payload);
|
|
||||||
payload = JSON.stringify(jsonData);
|
|
||||||
} catch {
|
|
||||||
// Not JSON, use as-is
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
mqttClient.publish(topic, payload, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Publish error:', err);
|
|
||||||
alert('Failed to publish: ' + err.message);
|
|
||||||
} else {
|
|
||||||
console.log('Published to', topic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert('MQTT not connected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
subscribeBtn.addEventListener('click', () => {
|
|
||||||
const topic = subscribeTopicInput.value.trim();
|
|
||||||
if (!topic) {
|
|
||||||
alert('Please enter a topic pattern');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
mqttClient.subscribe(topic, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Subscribe error:', err);
|
|
||||||
alert('Failed to subscribe: ' + err.message);
|
|
||||||
} else {
|
|
||||||
subscribedTopics.add(topic);
|
|
||||||
console.log('Subscribed to', topic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert('MQTT not connected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
unsubscribeBtn.addEventListener('click', () => {
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
subscribedTopics.forEach(topic => {
|
|
||||||
mqttClient.unsubscribe(topic);
|
|
||||||
});
|
|
||||||
subscribedTopics.clear();
|
|
||||||
console.log('Unsubscribed from all topics');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearMessagesBtn.addEventListener('click', () => {
|
|
||||||
messagesContainer.innerHTML = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMessage(topic, payload) {
|
|
||||||
const messagesContainer = document.getElementById('mqtt-messages');
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = 'message-item';
|
|
||||||
|
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
|
||||||
let formattedPayload = payload;
|
|
||||||
try {
|
|
||||||
formattedPayload = JSON.stringify(JSON.parse(payload), null, 2);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
messageDiv.innerHTML = `
|
|
||||||
<div class="timestamp">${timestamp}</div>
|
|
||||||
<div class="topic">${topic}</div>
|
|
||||||
<div class="payload">${formattedPayload}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageDiv);
|
|
||||||
|
|
||||||
// Auto-scroll
|
|
||||||
if (document.getElementById('auto-scroll').checked) {
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket Setup
|
|
||||||
function setupWebSocket() {
|
|
||||||
if (typeof io !== 'undefined') {
|
|
||||||
try {
|
|
||||||
wsClient = io(SOCKET_IO_URL);
|
|
||||||
|
|
||||||
wsClient.on('connect', () => {
|
|
||||||
console.log('WebSocket connected');
|
|
||||||
updateStatus('ws-status', 'WebSocket: Connected', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
wsClient.on('disconnect', () => {
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
updateStatus('ws-status', 'WebSocket: Disconnected', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
wsClient.on('update', (data) => {
|
|
||||||
console.log('WebSocket update:', data);
|
|
||||||
// Could display in a separate section
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect WebSocket:', error);
|
|
||||||
updateStatus('ws-status', 'WebSocket: Error', false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('Socket.io not loaded');
|
|
||||||
updateStatus('ws-status', 'WebSocket: Library Not Loaded', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug Endpoints Setup
|
|
||||||
function setupDebug() {
|
|
||||||
const debugButtons = document.querySelectorAll('[data-debug]');
|
|
||||||
const responsePre = document.getElementById('debug-response');
|
|
||||||
|
|
||||||
debugButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', async () => {
|
|
||||||
const action = button.dataset.debug;
|
|
||||||
const endpoint = `/api/debug/${action}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`);
|
|
||||||
const text = await response.text();
|
|
||||||
responsePre.textContent = text;
|
|
||||||
} catch (error) {
|
|
||||||
responsePre.textContent = `Error: ${error.message}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick Actions Setup
|
|
||||||
function setupQuickActions() {
|
|
||||||
const quickActionButtons = document.querySelectorAll('[data-action]');
|
|
||||||
|
|
||||||
quickActionButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const action = button.dataset.action;
|
|
||||||
const topicInput = document.getElementById('mqtt-topic');
|
|
||||||
const payloadTextarea = document.getElementById('mqtt-payload');
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'button-start1':
|
|
||||||
topicInput.value = 'aquacross/button/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
type: 2,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'button-stop1':
|
|
||||||
topicInput.value = 'aquacross/button/00:00:00:00:00:03';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
type: 1,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'button-start2':
|
|
||||||
topicInput.value = 'aquacross/button/00:00:00:00:00:02';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
type: 2,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'button-stop2':
|
|
||||||
topicInput.value = 'aquacross/button/00:00:00:00:00:04';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
type: 1,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'rfid-read':
|
|
||||||
topicInput.value = 'aquacross/button/rfid/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
uid: 'TEST123456'
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'battery-update':
|
|
||||||
topicInput.value = 'aquacross/battery/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
voltage: 3600
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'heartbeat':
|
|
||||||
topicInput.value = 'heartbeat/alive/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'button-available':
|
|
||||||
topicInput.value = 'aquacross/button/status/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
available: true,
|
|
||||||
sleep: false,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
case 'button-sleep':
|
|
||||||
topicInput.value = 'aquacross/button/status/00:00:00:00:00:01';
|
|
||||||
payloadTextarea.value = JSON.stringify({
|
|
||||||
available: false,
|
|
||||||
sleep: true,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, null, 2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-publish
|
|
||||||
document.getElementById('mqtt-publish-btn').click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper Functions
|
|
||||||
function updateStatus(elementId, text, connected) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
element.textContent = text;
|
|
||||||
element.classList.remove('connected', 'disconnected');
|
|
||||||
element.classList.add(connected ? 'connected' : 'disconnected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on load
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>AquaMaster Debug Server</title>
|
|
||||||
<link rel="stylesheet" href="debug.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>AquaMaster Debug Server</h1>
|
|
||||||
<div class="status-bar">
|
|
||||||
<span id="mqtt-status" class="status-indicator">MQTT: Disconnected</span>
|
|
||||||
<span id="ws-status" class="status-indicator">WebSocket: Disconnected</span>
|
|
||||||
<span id="api-status" class="status-indicator">API: Ready</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav class="tabs">
|
|
||||||
<button class="tab-button active" data-tab="api">API Testing</button>
|
|
||||||
<button class="tab-button" data-tab="mqtt">MQTT Testing</button>
|
|
||||||
<button class="tab-button" data-tab="debug">Debug Endpoints</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- API Testing Tab -->
|
|
||||||
<div id="api-tab" class="tab-content active">
|
|
||||||
<div class="section">
|
|
||||||
<h2>API Endpoint Testing</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="api-endpoint">Endpoint:</label>
|
|
||||||
<select id="api-endpoint">
|
|
||||||
<option value="GET /api/data">GET /api/data</option>
|
|
||||||
<option value="POST /api/reset-best">POST /api/reset-best</option>
|
|
||||||
<option value="POST /api/unlearn-button">POST /api/unlearn-button</option>
|
|
||||||
<option value="POST /api/set-max-time">POST /api/set-max-time</option>
|
|
||||||
<option value="GET /api/get-settings">GET /api/get-settings</option>
|
|
||||||
<option value="POST /api/start-learning">POST /api/start-learning</option>
|
|
||||||
<option value="POST /api/stop-learning">POST /api/stop-learning</option>
|
|
||||||
<option value="GET /api/learn/status">GET /api/learn/status</option>
|
|
||||||
<option value="GET /api/buttons/status">GET /api/buttons/status</option>
|
|
||||||
<option value="GET /api/info">GET /api/info</option>
|
|
||||||
<option value="POST /api/set-wifi">POST /api/set-wifi</option>
|
|
||||||
<option value="GET /api/get-wifi">GET /api/get-wifi</option>
|
|
||||||
<option value="POST /api/set-location">POST /api/set-location</option>
|
|
||||||
<option value="GET /api/get-location">GET /api/get-location</option>
|
|
||||||
<option value="GET /api/updateButtons">GET /api/updateButtons</option>
|
|
||||||
<option value="POST /api/set-mode">POST /api/set-mode</option>
|
|
||||||
<option value="GET /api/get-mode">GET /api/get-mode</option>
|
|
||||||
<option value="POST /api/set-lane-config">POST /api/set-lane-config</option>
|
|
||||||
<option value="GET /api/get-lane-config">GET /api/get-lane-config</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="api-params">Parameters (JSON or form data):</label>
|
|
||||||
<textarea id="api-params" rows="4" placeholder='{"maxTime": 300, "maxTimeDisplay": 20}'></textarea>
|
|
||||||
</div>
|
|
||||||
<button id="api-send-btn" class="btn btn-primary">Send Request</button>
|
|
||||||
<div class="response-section">
|
|
||||||
<h3>Response:</h3>
|
|
||||||
<pre id="api-response"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MQTT Testing Tab -->
|
|
||||||
<div id="mqtt-tab" class="tab-content">
|
|
||||||
<div class="section">
|
|
||||||
<h2>MQTT Publish</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="mqtt-topic">Topic:</label>
|
|
||||||
<input type="text" id="mqtt-topic" placeholder="aquacross/button/00:00:00:00:00:01" value="aquacross/button/00:00:00:00:00:01">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="mqtt-payload">Payload (JSON or text):</label>
|
|
||||||
<textarea id="mqtt-payload" rows="4" placeholder='{"type": 2, "timestamp": 1234567890}'></textarea>
|
|
||||||
</div>
|
|
||||||
<button id="mqtt-publish-btn" class="btn btn-primary">Publish</button>
|
|
||||||
|
|
||||||
<div class="quick-actions">
|
|
||||||
<h3>Quick Actions:</h3>
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn btn-secondary" data-action="button-start1">Simulate Start1 Button</button>
|
|
||||||
<button class="btn btn-secondary" data-action="button-stop1">Simulate Stop1 Button</button>
|
|
||||||
<button class="btn btn-secondary" data-action="button-start2">Simulate Start2 Button</button>
|
|
||||||
<button class="btn btn-secondary" data-action="button-stop2">Simulate Stop2 Button</button>
|
|
||||||
<button class="btn btn-secondary" data-action="rfid-read">Simulate RFID Read</button>
|
|
||||||
<button class="btn btn-secondary" data-action="battery-update">Simulate Battery Update</button>
|
|
||||||
<button class="btn btn-secondary" data-action="heartbeat">Simulate Heartbeat</button>
|
|
||||||
<button class="btn btn-secondary" data-action="button-available">Button Available (Wake)</button>
|
|
||||||
<button class="btn btn-secondary" data-action="button-sleep">Button Sleep Mode</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>MQTT Subscribe</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="mqtt-subscribe-topic">Topic Pattern:</label>
|
|
||||||
<input type="text" id="mqtt-subscribe-topic" placeholder="# or aquacross/button/#" value="#">
|
|
||||||
</div>
|
|
||||||
<button id="mqtt-subscribe-btn" class="btn btn-primary">Subscribe</button>
|
|
||||||
<button id="mqtt-unsubscribe-btn" class="btn btn-secondary">Unsubscribe All</button>
|
|
||||||
|
|
||||||
<div class="messages-section">
|
|
||||||
<h3>Received Messages:</h3>
|
|
||||||
<div class="messages-controls">
|
|
||||||
<button id="clear-messages-btn" class="btn btn-small">Clear</button>
|
|
||||||
<label><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label>
|
|
||||||
</div>
|
|
||||||
<div id="mqtt-messages" class="messages-container"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Debug Endpoints Tab -->
|
|
||||||
<div id="debug-tab" class="tab-content">
|
|
||||||
<div class="section">
|
|
||||||
<h2>Debug Endpoints</h2>
|
|
||||||
<p>Direct access to debug endpoints for timer control:</p>
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn btn-primary" data-debug="start1">Start Lane 1</button>
|
|
||||||
<button class="btn btn-primary" data-debug="stop1">Stop Lane 1</button>
|
|
||||||
<button class="btn btn-primary" data-debug="start2">Start Lane 2</button>
|
|
||||||
<button class="btn btn-primary" data-debug="stop2">Stop Lane 2</button>
|
|
||||||
</div>
|
|
||||||
<div class="response-section">
|
|
||||||
<h3>Last Response:</h3>
|
|
||||||
<pre id="debug-response"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mqtt@5/dist/mqtt.min.js"></script>
|
|
||||||
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
|
|
||||||
<script src="debug.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,718 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const http = require('http');
|
|
||||||
const socketIo = require('socket.io');
|
|
||||||
const mqtt = require('mqtt');
|
|
||||||
const cors = require('cors');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const io = socketIo(server, {
|
|
||||||
cors: {
|
|
||||||
origin: "*",
|
|
||||||
methods: ["GET", "POST"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = 80;
|
|
||||||
const MQTT_BROKER = 'mqtt://localhost:1883';
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(express.static(path.join(__dirname, 'debug_server')));
|
|
||||||
|
|
||||||
// State - simuliert ESP32 Datenstrukturen
|
|
||||||
const state = {
|
|
||||||
timerData1: {
|
|
||||||
startTime: 0,
|
|
||||||
localStartTime: 0,
|
|
||||||
finishedSince: 0,
|
|
||||||
endTime: 0,
|
|
||||||
bestTime: 0,
|
|
||||||
isRunning: false,
|
|
||||||
isReady: true,
|
|
||||||
isArmed: false,
|
|
||||||
RFIDUID: ""
|
|
||||||
},
|
|
||||||
timerData2: {
|
|
||||||
startTime: 0,
|
|
||||||
localStartTime: 0,
|
|
||||||
finishedSince: 0,
|
|
||||||
endTime: 0,
|
|
||||||
bestTime: 0,
|
|
||||||
isRunning: false,
|
|
||||||
isReady: true,
|
|
||||||
isArmed: false,
|
|
||||||
RFIDUID: ""
|
|
||||||
},
|
|
||||||
buttonConfigs: {
|
|
||||||
start1: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false },
|
|
||||||
stop1: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false },
|
|
||||||
start2: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false },
|
|
||||||
stop2: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false }
|
|
||||||
},
|
|
||||||
learningMode: false,
|
|
||||||
learningStep: 0,
|
|
||||||
maxTimeBeforeReset: 300000,
|
|
||||||
maxTimeDisplay: 20000,
|
|
||||||
minTimeForLeaderboard: 5000,
|
|
||||||
masterlocation: "",
|
|
||||||
gamemode: 0, // 0=Individual, 1=Wettkampf
|
|
||||||
startCompetition: false,
|
|
||||||
laneConfigType: 0,
|
|
||||||
lane1DifficultyType: 0,
|
|
||||||
lane2DifficultyType: 0,
|
|
||||||
localTimes: [],
|
|
||||||
wifi: {
|
|
||||||
ssid: "",
|
|
||||||
password: ""
|
|
||||||
},
|
|
||||||
start1FoundLocally: false,
|
|
||||||
start2FoundLocally: false,
|
|
||||||
start1UID: "",
|
|
||||||
start2UID: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper: millis() - simuliert Arduino millis()
|
|
||||||
function millis() {
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: getTimerDataJSON() - simuliert getTimerDataJSON()
|
|
||||||
function getTimerDataJSON() {
|
|
||||||
const currentTime = millis();
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
// Lane 1
|
|
||||||
if (state.timerData1.isRunning) {
|
|
||||||
data.time1 = (currentTime - state.timerData1.localStartTime) / 1000.0;
|
|
||||||
data.status1 = "running";
|
|
||||||
} else if (state.timerData1.endTime > 0) {
|
|
||||||
data.time1 = (state.timerData1.endTime - state.timerData1.startTime) / 1000.0;
|
|
||||||
data.status1 = "finished";
|
|
||||||
} else if (state.timerData1.isArmed) {
|
|
||||||
data.time1 = 0;
|
|
||||||
data.status1 = "armed";
|
|
||||||
} else {
|
|
||||||
data.time1 = 0;
|
|
||||||
data.status1 = "ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lane 2
|
|
||||||
if (state.timerData2.isRunning) {
|
|
||||||
data.time2 = (currentTime - state.timerData2.localStartTime) / 1000.0;
|
|
||||||
data.status2 = "running";
|
|
||||||
} else if (state.timerData2.endTime > 0) {
|
|
||||||
data.time2 = (state.timerData2.endTime - state.timerData2.startTime) / 1000.0;
|
|
||||||
data.status2 = "finished";
|
|
||||||
} else if (state.timerData2.isArmed) {
|
|
||||||
data.time2 = 0;
|
|
||||||
data.status2 = "armed";
|
|
||||||
} else {
|
|
||||||
data.time2 = 0;
|
|
||||||
data.status2 = "ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Best times
|
|
||||||
data.best1 = state.timerData1.bestTime / 1000.0;
|
|
||||||
data.best2 = state.timerData2.bestTime / 1000.0;
|
|
||||||
|
|
||||||
// Learning mode
|
|
||||||
data.learningMode = state.learningMode;
|
|
||||||
if (state.learningMode) {
|
|
||||||
const buttons = ["Start Bahn 1", "Stop Bahn 1", "Start Bahn 2", "Stop Bahn 2"];
|
|
||||||
data.learningButton = buttons[state.learningStep];
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer-Logik: IndividualMode
|
|
||||||
function individualMode(action, press, lane, timestamp = 0) {
|
|
||||||
const ts = timestamp > 0 ? timestamp : millis();
|
|
||||||
|
|
||||||
if (action === "start" && press === 2 && lane === 1) {
|
|
||||||
if (!state.timerData1.isRunning && state.timerData1.isReady) {
|
|
||||||
state.timerData1.isReady = false;
|
|
||||||
state.timerData1.startTime = ts;
|
|
||||||
state.timerData1.localStartTime = millis();
|
|
||||||
state.timerData1.isRunning = true;
|
|
||||||
state.timerData1.endTime = 0;
|
|
||||||
state.timerData1.isArmed = false;
|
|
||||||
publishLaneStatus(1, "running");
|
|
||||||
console.log("Bahn 1 gestartet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "stop" && press === 1 && lane === 1) {
|
|
||||||
if (state.timerData1.isRunning) {
|
|
||||||
state.timerData1.endTime = ts;
|
|
||||||
state.timerData1.finishedSince = millis();
|
|
||||||
state.timerData1.isRunning = false;
|
|
||||||
const currentTime = state.timerData1.endTime - state.timerData1.startTime;
|
|
||||||
|
|
||||||
if (state.timerData1.bestTime === 0 || currentTime < state.timerData1.bestTime) {
|
|
||||||
state.timerData1.bestTime = currentTime;
|
|
||||||
}
|
|
||||||
publishLaneStatus(1, "stopped");
|
|
||||||
console.log(`Bahn 1 gestoppt - Zeit: ${(currentTime / 1000.0).toFixed(2)}s`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "start" && press === 2 && lane === 2) {
|
|
||||||
if (!state.timerData2.isRunning && state.timerData2.isReady) {
|
|
||||||
state.timerData2.isReady = false;
|
|
||||||
state.timerData2.startTime = ts;
|
|
||||||
state.timerData2.localStartTime = millis();
|
|
||||||
state.timerData2.isRunning = true;
|
|
||||||
state.timerData2.endTime = 0;
|
|
||||||
state.timerData2.isArmed = false;
|
|
||||||
publishLaneStatus(2, "running");
|
|
||||||
console.log("Bahn 2 gestartet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "stop" && press === 1 && lane === 2) {
|
|
||||||
if (state.timerData2.isRunning) {
|
|
||||||
state.timerData2.endTime = ts;
|
|
||||||
state.timerData2.finishedSince = millis();
|
|
||||||
state.timerData2.isRunning = false;
|
|
||||||
const currentTime = state.timerData2.endTime - state.timerData2.startTime;
|
|
||||||
|
|
||||||
if (state.timerData2.bestTime === 0 || currentTime < state.timerData2.bestTime) {
|
|
||||||
state.timerData2.bestTime = currentTime;
|
|
||||||
}
|
|
||||||
publishLaneStatus(2, "stopped");
|
|
||||||
console.log(`Bahn 2 gestoppt - Zeit: ${(currentTime / 1000.0).toFixed(2)}s`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: publishLaneStatus
|
|
||||||
function publishLaneStatus(lane, status) {
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
const topic = `aquacross/lanes/lane${lane}`;
|
|
||||||
const message = JSON.stringify({ lane, status });
|
|
||||||
mqttClient.publish(topic, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: pushUpdateToFrontend
|
|
||||||
function pushUpdateToFrontend(message) {
|
|
||||||
io.emit('update', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MQTT Client Setup
|
|
||||||
let mqttClient = null;
|
|
||||||
let mqttReconnectInterval = null;
|
|
||||||
|
|
||||||
function connectMQTT() {
|
|
||||||
// Don't reconnect if already connected or connecting
|
|
||||||
if (mqttClient && (mqttClient.connected || mqttClient.connecting)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear any existing reconnect interval
|
|
||||||
if (mqttReconnectInterval) {
|
|
||||||
clearInterval(mqttReconnectInterval);
|
|
||||||
mqttReconnectInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close existing client if any
|
|
||||||
if (mqttClient) {
|
|
||||||
mqttClient.end(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[MQTT] Attempting to connect to broker at', MQTT_BROKER);
|
|
||||||
mqttClient = mqtt.connect(MQTT_BROKER, {
|
|
||||||
reconnectPeriod: 5000,
|
|
||||||
connectTimeout: 10000,
|
|
||||||
clientId: 'mock-esp32-' + Math.random().toString(16).substr(2, 8)
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('connect', () => {
|
|
||||||
console.log('[MQTT] Connected to broker');
|
|
||||||
|
|
||||||
// Subscribe to all relevant topics
|
|
||||||
mqttClient.subscribe('aquacross/button/#', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to aquacross/button/#');
|
|
||||||
});
|
|
||||||
mqttClient.subscribe('aquacross/button/rfid/#', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to aquacross/button/rfid/#');
|
|
||||||
});
|
|
||||||
mqttClient.subscribe('aquacross/battery/#', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to aquacross/battery/#');
|
|
||||||
});
|
|
||||||
mqttClient.subscribe('heartbeat/alive/#', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to heartbeat/alive/#');
|
|
||||||
});
|
|
||||||
mqttClient.subscribe('aquacross/competition/toMaster', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to aquacross/competition/toMaster');
|
|
||||||
});
|
|
||||||
mqttClient.subscribe('aquacross/button/status/#', (err) => {
|
|
||||||
if (!err) console.log('[MQTT] Subscribed to aquacross/button/status/#');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('message', (topic, message) => {
|
|
||||||
const payload = message.toString();
|
|
||||||
console.log(`[MQTT] Received on ${topic}: ${payload}`);
|
|
||||||
|
|
||||||
// Handle different topic types
|
|
||||||
if (topic.startsWith('aquacross/button/rfid/')) {
|
|
||||||
handleRFIDTopic(topic, payload);
|
|
||||||
} else if (topic.startsWith('aquacross/button/status/')) {
|
|
||||||
handleButtonStatusTopic(topic, payload);
|
|
||||||
} else if (topic.startsWith('aquacross/button/')) {
|
|
||||||
handleButtonTopic(topic, payload);
|
|
||||||
} else if (topic.startsWith('aquacross/battery/')) {
|
|
||||||
handleBatteryTopic(topic, payload);
|
|
||||||
} else if (topic.startsWith('heartbeat/alive/')) {
|
|
||||||
handleHeartbeatTopic(topic, payload);
|
|
||||||
} else if (topic === 'aquacross/competition/toMaster') {
|
|
||||||
if (payload === 'start') {
|
|
||||||
state.startCompetition = true;
|
|
||||||
runCompetition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('error', (err) => {
|
|
||||||
console.error('[MQTT] Error:', err.message || err);
|
|
||||||
if (err.code === 'ECONNREFUSED') {
|
|
||||||
console.log('[MQTT] Broker not available at', MQTT_BROKER, '- will retry automatically');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('close', () => {
|
|
||||||
console.log('[MQTT] Connection closed');
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('offline', () => {
|
|
||||||
console.log('[MQTT] Client offline, will reconnect automatically...');
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttClient.on('reconnect', () => {
|
|
||||||
console.log('[MQTT] Reconnecting to broker...');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// MQTT Topic Handlers
|
|
||||||
function handleButtonTopic(topic, payload) {
|
|
||||||
try {
|
|
||||||
const buttonId = topic.replace('aquacross/button/', '');
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
const pressType = data.type || 0;
|
|
||||||
const timestamp = data.timestamp || millis();
|
|
||||||
|
|
||||||
console.log(`Button Press: ${buttonId}, Type: ${pressType}, Timestamp: ${timestamp}`);
|
|
||||||
|
|
||||||
// Simulate button assignment check (simplified)
|
|
||||||
// In real implementation, would check MAC addresses
|
|
||||||
if (state.learningMode) {
|
|
||||||
// Handle learning mode
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger action based on button (simplified - would check MAC in real implementation)
|
|
||||||
if (pressType === 2) {
|
|
||||||
// Start button
|
|
||||||
if (buttonId.includes('start1') || buttonId.includes('00:00:00:00:00:01')) {
|
|
||||||
individualMode("start", 2, 1, timestamp);
|
|
||||||
} else if (buttonId.includes('start2') || buttonId.includes('00:00:00:00:00:02')) {
|
|
||||||
individualMode("start", 2, 2, timestamp);
|
|
||||||
}
|
|
||||||
} else if (pressType === 1) {
|
|
||||||
// Stop button
|
|
||||||
if (buttonId.includes('stop1') || buttonId.includes('00:00:00:00:00:03')) {
|
|
||||||
individualMode("stop", 1, 1, timestamp);
|
|
||||||
} else if (buttonId.includes('stop2') || buttonId.includes('00:00:00:00:00:04')) {
|
|
||||||
individualMode("stop", 1, 2, timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling button topic:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRFIDTopic(topic, payload) {
|
|
||||||
try {
|
|
||||||
const buttonId = topic.replace('aquacross/button/rfid/', '');
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
const uid = data.uid || '';
|
|
||||||
|
|
||||||
console.log(`RFID Read: ${buttonId}, UID: ${uid}`);
|
|
||||||
|
|
||||||
// Send to frontend
|
|
||||||
const message = JSON.stringify({
|
|
||||||
name: uid,
|
|
||||||
lane: buttonId.includes('start1') ? 'start1' : 'start2'
|
|
||||||
});
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling RFID topic:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBatteryTopic(topic, payload) {
|
|
||||||
try {
|
|
||||||
const buttonId = topic.replace('aquacross/battery/', '');
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
const voltage = data.voltage || 0;
|
|
||||||
|
|
||||||
console.log(`Battery: ${buttonId}, Voltage: ${voltage}`);
|
|
||||||
|
|
||||||
// Update button config if known
|
|
||||||
// Send to frontend
|
|
||||||
const message = JSON.stringify({
|
|
||||||
button: buttonId,
|
|
||||||
mac: buttonId,
|
|
||||||
batteryLevel: Math.round((voltage - 3200) / 50) // Simple calculation
|
|
||||||
});
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling battery topic:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleHeartbeatTopic(topic, payload) {
|
|
||||||
try {
|
|
||||||
const buttonId = topic.replace('heartbeat/alive/', '');
|
|
||||||
console.log(`Heartbeat: ${buttonId}`);
|
|
||||||
|
|
||||||
// Update button heartbeat
|
|
||||||
// Send to frontend
|
|
||||||
const message = JSON.stringify({
|
|
||||||
button: buttonId,
|
|
||||||
mac: buttonId,
|
|
||||||
active: true
|
|
||||||
});
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling heartbeat topic:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleButtonStatusTopic(topic, payload) {
|
|
||||||
try {
|
|
||||||
const buttonId = topic.replace('aquacross/button/status/', '');
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
const available = data.available !== false;
|
|
||||||
const sleep = data.sleep === true;
|
|
||||||
|
|
||||||
console.log(`Button Status: ${buttonId}, Available: ${available}, Sleep: ${sleep}`);
|
|
||||||
|
|
||||||
// Send to frontend
|
|
||||||
const message = JSON.stringify({
|
|
||||||
button: buttonId,
|
|
||||||
mac: buttonId,
|
|
||||||
available: available,
|
|
||||||
sleep: sleep,
|
|
||||||
timestamp: data.timestamp || Date.now()
|
|
||||||
});
|
|
||||||
pushUpdateToFrontend(message);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling button status topic:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCompetition() {
|
|
||||||
if (state.timerData1.isArmed && state.timerData2.isArmed && state.startCompetition) {
|
|
||||||
const startNow = millis();
|
|
||||||
|
|
||||||
state.timerData1.isReady = false;
|
|
||||||
state.timerData1.startTime = startNow;
|
|
||||||
state.timerData1.localStartTime = millis();
|
|
||||||
state.timerData1.isRunning = true;
|
|
||||||
state.timerData1.endTime = 0;
|
|
||||||
state.timerData1.isArmed = false;
|
|
||||||
publishLaneStatus(1, "running");
|
|
||||||
|
|
||||||
state.timerData2.isReady = false;
|
|
||||||
state.timerData2.startTime = startNow;
|
|
||||||
state.timerData2.localStartTime = millis();
|
|
||||||
state.timerData2.isRunning = true;
|
|
||||||
state.timerData2.endTime = 0;
|
|
||||||
state.timerData2.isArmed = false;
|
|
||||||
publishLaneStatus(2, "running");
|
|
||||||
|
|
||||||
console.log("Competition started");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Routes
|
|
||||||
app.get('/api/data', (req, res) => {
|
|
||||||
res.json(JSON.parse(getTimerDataJSON()));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/reset-best', (req, res) => {
|
|
||||||
state.timerData1.bestTime = 0;
|
|
||||||
state.timerData2.bestTime = 0;
|
|
||||||
state.localTimes = [];
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/unlearn-button', (req, res) => {
|
|
||||||
state.buttonConfigs.start1.isAssigned = false;
|
|
||||||
state.buttonConfigs.stop1.isAssigned = false;
|
|
||||||
state.buttonConfigs.start2.isAssigned = false;
|
|
||||||
state.buttonConfigs.stop2.isAssigned = false;
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/set-max-time', (req, res) => {
|
|
||||||
if (req.body.maxTime) {
|
|
||||||
state.maxTimeBeforeReset = parseInt(req.body.maxTime) * 1000;
|
|
||||||
}
|
|
||||||
if (req.body.maxTimeDisplay) {
|
|
||||||
state.maxTimeDisplay = parseInt(req.body.maxTimeDisplay) * 1000;
|
|
||||||
}
|
|
||||||
if (req.body.minTimeForLeaderboard) {
|
|
||||||
state.minTimeForLeaderboard = parseInt(req.body.minTimeForLeaderboard) * 1000;
|
|
||||||
}
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/get-settings', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
maxTime: state.maxTimeBeforeReset / 1000,
|
|
||||||
maxTimeDisplay: state.maxTimeDisplay / 1000,
|
|
||||||
minTimeForLeaderboard: state.minTimeForLeaderboard / 1000
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/start-learning', (req, res) => {
|
|
||||||
state.learningMode = true;
|
|
||||||
state.learningStep = 0;
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/stop-learning', (req, res) => {
|
|
||||||
state.learningMode = false;
|
|
||||||
state.learningStep = 0;
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/learn/status', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
active: state.learningMode,
|
|
||||||
step: state.learningStep
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/buttons/status', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
lane1Start: state.buttonConfigs.start1.isAssigned,
|
|
||||||
lane1StartVoltage: state.buttonConfigs.start1.voltage,
|
|
||||||
lane1Stop: state.buttonConfigs.stop1.isAssigned,
|
|
||||||
lane1StopVoltage: state.buttonConfigs.stop1.voltage,
|
|
||||||
lane2Start: state.buttonConfigs.start2.isAssigned,
|
|
||||||
lane2StartVoltage: state.buttonConfigs.start2.voltage,
|
|
||||||
lane2Stop: state.buttonConfigs.stop2.isAssigned,
|
|
||||||
lane2StopVoltage: state.buttonConfigs.stop2.voltage
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/info', (req, res) => {
|
|
||||||
const connected = [
|
|
||||||
state.buttonConfigs.start1.isAssigned,
|
|
||||||
state.buttonConfigs.stop1.isAssigned,
|
|
||||||
state.buttonConfigs.start2.isAssigned,
|
|
||||||
state.buttonConfigs.stop2.isAssigned
|
|
||||||
].filter(Boolean).length;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
ip: "127.0.0.1",
|
|
||||||
ipSTA: "127.0.0.1",
|
|
||||||
channel: 1,
|
|
||||||
mac: "AA:BB:CC:DD:EE:FF",
|
|
||||||
freeMemory: 1024 * 1024,
|
|
||||||
connectedButtons: connected,
|
|
||||||
isOnline: true,
|
|
||||||
valid: "Ja",
|
|
||||||
tier: 1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/set-wifi', (req, res) => {
|
|
||||||
if (req.body.ssid) {
|
|
||||||
state.wifi.ssid = req.body.ssid;
|
|
||||||
state.wifi.password = req.body.password || "";
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
res.status(400).json({ success: false, error: "SSID fehlt" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/get-wifi', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
ssid: state.wifi.ssid,
|
|
||||||
password: state.wifi.password
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/set-location', (req, res) => {
|
|
||||||
if (req.body.name) {
|
|
||||||
state.masterlocation = req.body.name;
|
|
||||||
}
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/get-location', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
locationid: state.masterlocation
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/updateButtons', (req, res) => {
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
mqttClient.publish('aquacross/update/flag', '1');
|
|
||||||
}
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/set-mode', (req, res) => {
|
|
||||||
if (req.body.mode) {
|
|
||||||
state.gamemode = req.body.mode === "individual" ? 0 : 1;
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
res.status(400).json({ success: false, error: "Modus fehlt" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/get-mode', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
mode: state.gamemode === 0 ? "individual" : "wettkampf"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/set-lane-config', (req, res) => {
|
|
||||||
if (req.body.type) {
|
|
||||||
state.laneConfigType = req.body.type === "identical" ? 0 : 1;
|
|
||||||
if (state.laneConfigType === 1) {
|
|
||||||
if (req.body.lane1Difficulty) {
|
|
||||||
state.lane1DifficultyType = req.body.lane1Difficulty === "light" ? 0 : 1;
|
|
||||||
}
|
|
||||||
if (req.body.lane2Difficulty) {
|
|
||||||
state.lane2DifficultyType = req.body.lane2Difficulty === "light" ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
res.status(400).json({ success: false, error: "Lane type missing" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/get-lane-config', (req, res) => {
|
|
||||||
const config = {
|
|
||||||
type: state.laneConfigType === 0 ? "identical" : "different"
|
|
||||||
};
|
|
||||||
if (state.laneConfigType === 1) {
|
|
||||||
config.lane1Difficulty = state.lane1DifficultyType === 0 ? "light" : "heavy";
|
|
||||||
config.lane2Difficulty = state.lane2DifficultyType === 0 ? "light" : "heavy";
|
|
||||||
}
|
|
||||||
res.json(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug Endpoints
|
|
||||||
app.get('/api/debug/start1', (req, res) => {
|
|
||||||
individualMode("start", 2, 1, millis());
|
|
||||||
res.send("handleStart1() called");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/debug/stop1', (req, res) => {
|
|
||||||
individualMode("stop", 1, 1, millis());
|
|
||||||
res.send("handleStop1() called");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/debug/start2', (req, res) => {
|
|
||||||
individualMode("start", 2, 2, millis());
|
|
||||||
res.send("handleStart2() called");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/debug/stop2', (req, res) => {
|
|
||||||
individualMode("stop", 1, 2, millis());
|
|
||||||
res.send("handleStop2() called");
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebSocket Setup
|
|
||||||
io.on('connection', (socket) => {
|
|
||||||
console.log(`[WebSocket] Client connected: ${socket.id}`);
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
console.log(`[WebSocket] Client disconnected: ${socket.id}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Time sync - publish every 5 seconds
|
|
||||||
setInterval(() => {
|
|
||||||
if (mqttClient && mqttClient.connected) {
|
|
||||||
mqttClient.publish('sync/time', millis().toString());
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
// Auto-reset check
|
|
||||||
setInterval(() => {
|
|
||||||
const currentTime = millis();
|
|
||||||
|
|
||||||
if (state.gamemode === 0) {
|
|
||||||
// Individual mode
|
|
||||||
if (!state.timerData1.isRunning && state.timerData1.endTime > 0 &&
|
|
||||||
state.timerData1.finishedSince > 0) {
|
|
||||||
if (currentTime - state.timerData1.finishedSince > state.maxTimeDisplay) {
|
|
||||||
state.timerData1.startTime = 0;
|
|
||||||
state.timerData1.endTime = 0;
|
|
||||||
state.timerData1.finishedSince = 0;
|
|
||||||
state.timerData1.isReady = true;
|
|
||||||
publishLaneStatus(1, "ready");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.timerData2.isRunning && state.timerData2.endTime > 0 &&
|
|
||||||
state.timerData2.finishedSince > 0) {
|
|
||||||
if (currentTime - state.timerData2.finishedSince > state.maxTimeDisplay) {
|
|
||||||
state.timerData2.startTime = 0;
|
|
||||||
state.timerData2.endTime = 0;
|
|
||||||
state.timerData2.finishedSince = 0;
|
|
||||||
state.timerData2.isReady = true;
|
|
||||||
publishLaneStatus(2, "ready");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log(`[Server] Mock ESP32 Server running on port ${PORT}`);
|
|
||||||
console.log(`[Server] Web UI available at http://localhost:${PORT}`);
|
|
||||||
|
|
||||||
// Wait a moment before trying to connect to MQTT broker
|
|
||||||
// This gives the broker time to start if both are started together
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[MQTT] Attempting initial connection to broker...');
|
|
||||||
connectMQTT();
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// Also set up a periodic check (backup retry mechanism)
|
|
||||||
// Note: mqtt.js already has auto-reconnect, this is just a backup
|
|
||||||
mqttReconnectInterval = setInterval(() => {
|
|
||||||
if (!mqttClient || (!mqttClient.connected && !mqttClient.connecting)) {
|
|
||||||
console.log('[MQTT] Connection check: Not connected, attempting reconnect...');
|
|
||||||
connectMQTT();
|
|
||||||
}
|
|
||||||
}, 15000); // Check every 15 seconds if not connected
|
|
||||||
});
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('\n[Server] Shutting down...');
|
|
||||||
if (mqttClient) {
|
|
||||||
mqttClient.end();
|
|
||||||
}
|
|
||||||
server.close(() => {
|
|
||||||
console.log('[Server] Server closed');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
const aedes = require('aedes')();
|
|
||||||
const net = require('net');
|
|
||||||
const ws = require('ws');
|
|
||||||
const http = require('http');
|
|
||||||
const port = 1883;
|
|
||||||
const wsPort = 9001;
|
|
||||||
|
|
||||||
// TCP Server for MQTT
|
|
||||||
const server = net.createServer(aedes.handle);
|
|
||||||
|
|
||||||
// Logging für alle Nachrichten
|
|
||||||
aedes.on('publish', (packet, client) => {
|
|
||||||
if (client) {
|
|
||||||
console.log(`[MQTT] Client ${client.id} published to topic: ${packet.topic}`);
|
|
||||||
console.log(`[MQTT] Payload: ${packet.payload.toString()}`);
|
|
||||||
} else {
|
|
||||||
console.log(`[MQTT] Published to topic: ${packet.topic}`);
|
|
||||||
console.log(`[MQTT] Payload: ${packet.payload.toString()}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client-Verbindungen
|
|
||||||
aedes.on('client', (client) => {
|
|
||||||
console.log(`[MQTT] Client connected: ${client.id}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
aedes.on('clientDisconnect', (client) => {
|
|
||||||
console.log(`[MQTT] Client disconnected: ${client.id}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fehlerbehandlung
|
|
||||||
aedes.on('clientError', (client, err) => {
|
|
||||||
console.error(`[MQTT] Client error for ${client.id}:`, err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebSocket Server for browser connections
|
|
||||||
const httpServer = http.createServer();
|
|
||||||
const wsServer = new ws.Server({
|
|
||||||
server: httpServer,
|
|
||||||
path: '/mqtt'
|
|
||||||
});
|
|
||||||
|
|
||||||
wsServer.on('connection', (socket, req) => {
|
|
||||||
// Create a proper stream adapter for Aedes
|
|
||||||
const { Duplex } = require('stream');
|
|
||||||
|
|
||||||
const stream = new Duplex({
|
|
||||||
write(chunk, encoding, callback) {
|
|
||||||
if (socket.readyState === ws.OPEN) {
|
|
||||||
socket.send(chunk);
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
callback(new Error('WebSocket is not open'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
read() {
|
|
||||||
// No-op: we push data when we receive it
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle incoming WebSocket messages
|
|
||||||
socket.on('message', (data) => {
|
|
||||||
stream.push(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
|
||||||
console.error('[MQTT] WebSocket error:', err);
|
|
||||||
stream.destroy(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
console.log('[MQTT] WebSocket client disconnected');
|
|
||||||
stream.push(null); // End the stream
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle stream errors
|
|
||||||
stream.on('error', (err) => {
|
|
||||||
console.error('[MQTT] Stream error:', err);
|
|
||||||
if (socket.readyState === ws.OPEN) {
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pass the stream to Aedes
|
|
||||||
aedes.handle(stream);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port, () => {
|
|
||||||
console.log(`[MQTT] TCP Broker started and listening on port ${port}`);
|
|
||||||
console.log(`[MQTT] Ready to accept TCP connections`);
|
|
||||||
});
|
|
||||||
|
|
||||||
httpServer.listen(wsPort, () => {
|
|
||||||
console.log(`[MQTT] WebSocket Broker started and listening on port ${wsPort}`);
|
|
||||||
console.log(`[MQTT] Ready to accept WebSocket connections at ws://localhost:${wsPort}/mqtt`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('\n[MQTT] Shutting down broker...');
|
|
||||||
server.close(() => {
|
|
||||||
console.log('[MQTT] TCP server closed');
|
|
||||||
});
|
|
||||||
httpServer.close(() => {
|
|
||||||
console.log('[MQTT] WebSocket server closed');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
1922
mock-server/package-lock.json
generated
1922
mock-server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "aquamaster-mock-server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Mock ESP32 Server and MQTT Broker for testing AquaMaster without hardware",
|
|
||||||
"main": "start_all.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node start_all.js",
|
|
||||||
"mqtt": "node mqtt_broker.js",
|
|
||||||
"server": "node mock_esp32_server.js"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"mqtt",
|
|
||||||
"esp32",
|
|
||||||
"mock",
|
|
||||||
"testing"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"aedes": "^0.50.0",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"socket.io": "^4.6.1",
|
|
||||||
"mqtt": "^5.3.1",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"body-parser": "^1.20.2",
|
|
||||||
"ws": "^8.14.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
const { spawn } = require('child_process');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
console.log('Starting AquaMaster Mock Server...\n');
|
|
||||||
|
|
||||||
// Start MQTT Broker
|
|
||||||
console.log('[1/2] Starting MQTT Broker...');
|
|
||||||
const mqttBroker = spawn('node', [path.join(__dirname, 'mqtt_broker.js')], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: __dirname
|
|
||||||
});
|
|
||||||
|
|
||||||
mqttBroker.on('error', (err) => {
|
|
||||||
console.error('Failed to start MQTT Broker:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait a bit longer for MQTT broker to fully start
|
|
||||||
setTimeout(() => {
|
|
||||||
// Start Mock ESP32 Server
|
|
||||||
console.log('[2/2] Starting Mock ESP32 Server...');
|
|
||||||
const mockServer = spawn('node', [path.join(__dirname, 'mock_esp32_server.js')], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: __dirname
|
|
||||||
});
|
|
||||||
|
|
||||||
mockServer.on('error', (err) => {
|
|
||||||
console.error('Failed to start Mock ESP32 Server:', err);
|
|
||||||
mqttBroker.kill();
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle shutdown
|
|
||||||
const shutdown = () => {
|
|
||||||
console.log('\nShutting down servers...');
|
|
||||||
if (mqttBroker && !mqttBroker.killed) {
|
|
||||||
mqttBroker.kill();
|
|
||||||
}
|
|
||||||
if (mockServer && !mockServer.killed) {
|
|
||||||
mockServer.kill();
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('SIGINT', shutdown);
|
|
||||||
process.on('SIGTERM', shutdown);
|
|
||||||
}, 3000); // Increased wait time to 3 seconds
|
|
||||||
Reference in New Issue
Block a user