diff --git a/API.md b/API.md index a0702b8..8828ac7 100644 --- a/API.md +++ b/API.md @@ -7,87 +7,87 @@ All API endpoints return JSON unless otherwise noted. ## Static Files -| Route | Method | Description | Response Type | -|------------------|--------|------------------------------|--------------| -| `/` | GET | Main page | HTML | -| `/settings` | GET | Settings page | HTML | -| `/rfid` | GET | RFID page | HTML | -| `/firmware.bin` | GET | Firmware file (SPIFFS) | Binary | +| Route | Method | Description | Response Type | +| --------------- | ------ | ---------------------- | ------------- | +| `/` | GET | Main page | HTML | +| `/settings` | GET | Settings page | HTML | +| `/rfid` | GET | RFID page | HTML | +| `/firmware.bin` | GET | Firmware file (SPIFFS) | Binary | --- ## Timer & Data -| Route | Method | Description | Request Body/Params | Response Example | -|-------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/data` | GET | Get current timer and status data | – | `{...}` | -| `/api/reset-best` | POST | Reset best times | – | `{ "success": true }` | +| Route | Method | Description | Request Body/Params | Response Example | +| ----------------- | ------ | --------------------------------- | ------------------- | --------------------- | +| `/api/data` | GET | Get current timer and status data | – | `{...}` | +| `/api/reset-best` | POST | Reset best times | – | `{ "success": true }` | --- ## Button Learning -| Route | Method | Description | Request Body/Params | Response Example | -|------------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/unlearn-button` | POST | Remove all button assignments | – | `{ "success": true }` | -| `/api/start-learning` | POST | Start button learning mode | – | `{ "success": true }` | -| `/api/stop-learning` | POST | Stop button learning mode | – | `{ "success": true }` | -| `/api/learn/status` | GET | Get learning mode status | – | `{ "active": true, "step": 1 }` | -| `/api/buttons/status` | GET | Get button assignment and voltage | – | `{ "lane1Start": true, "lane1StartVoltage": 3.3, ... }` | +| Route | Method | Description | Request Body/Params | Response Example | +| --------------------- | ------ | --------------------------------- | ------------------- | ------------------------------------------------------- | +| `/api/unlearn-button` | POST | Remove all button assignments | – | `{ "success": true }` | +| `/api/start-learning` | POST | Start button learning mode | – | `{ "success": true }` | +| `/api/stop-learning` | POST | Stop button learning mode | – | `{ "success": true }` | +| `/api/learn/status` | GET | Get learning mode status | – | `{ "active": true, "step": 1 }` | +| `/api/buttons/status` | GET | Get button assignment and voltage | – | `{ "lane1Start": true, "lane1StartVoltage": 3.3, ... }` | --- ## Settings -| Route | Method | Description | Request Body/Params | Response Example | -|----------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/set-max-time` | POST | Set max timer and display time | `maxTime`, `maxTimeDisplay` (form params, seconds) | `{ "success": true }` | -| `/api/get-settings` | GET | Get current timer settings | – | `{ "maxTime": 300, "maxTimeDisplay": 20 }` | +| Route | Method | Description | Request Body/Params | Response Example | +| ------------------- | ------ | ------------------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `/api/set-max-time` | POST | Set max timer and display time | `maxTime`, `maxTimeDisplay`, `minTimeForLeaderboard` (form params, seconds) | `{ "success": true }` | +| `/api/get-settings` | GET | Get current timer settings | – | `{ "maxTime": 300, "maxTimeDisplay": 20, "minTimeForLeaderboard": 5 }` | --- ## WiFi Configuration -| Route | Method | Description | Request Body/Params | Response Example | -|-------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/set-wifi` | POST | Set WiFi SSID and password | `ssid`, `password` (form params) | `{ "success": true }` | -| `/api/get-wifi` | GET | Get current WiFi SSID and password | – | `{ "ssid": "...", "password": "..." }` | +| Route | Method | Description | Request Body/Params | Response Example | +| --------------- | ------ | ---------------------------------- | -------------------------------- | -------------------------------------- | +| `/api/set-wifi` | POST | Set WiFi SSID and password | `ssid`, `password` (form params) | `{ "success": true }` | +| `/api/get-wifi` | GET | Get current WiFi SSID and password | – | `{ "ssid": "...", "password": "..." }` | --- ## Location Configuration -| Route | Method | Description | Request Body/Params | Response Example | -|----------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/set-location` | POST | Set location name and ID | `id`, `name` (form params) | `{ "success": true }` | -| `/api/get-location` | GET | Get current location | – | `{ "locationid": "..." }` | +| Route | Method | Description | Request Body/Params | Response Example | +| ------------------- | ------ | ------------------------ | -------------------------- | ------------------------- | +| `/api/set-location` | POST | Set location name and ID | `id`, `name` (form params) | `{ "success": true }` | +| `/api/get-location` | GET | Get current location | – | `{ "locationid": "..." }` | --- ## Button Update & Mode -| Route | Method | Description | Request Body/Params | Response Example | -|----------------------|--------|-------------------------------------|--------------------|------------------| -| `/api/updateButtons` | GET | Trigger MQTT update for buttons | – | `{ "success": true }` | -| `/api/set-mode` | POST | Set operational mode | `mode` (form param: "individual" or "wettkampf") | `{ "success": true }` | -| `/api/get-mode` | GET | Get current operational mode | – | `{ "mode": "individual" }` | +| Route | Method | Description | Request Body/Params | Response Example | +| -------------------- | ------ | ------------------------------- | ------------------------------------------------ | -------------------------- | +| `/api/updateButtons` | GET | Trigger MQTT update for buttons | – | `{ "success": true }` | +| `/api/set-mode` | POST | Set operational mode | `mode` (form param: "individual" or "wettkampf") | `{ "success": true }` | +| `/api/get-mode` | GET | Get current operational mode | – | `{ "mode": "individual" }` | --- ## System Info -| Route | Method | Description | Request Body/Params | Response Example | -|---------------|--------|-------------------------------------|--------------------|------------------| -| `/api/info` | GET | Get system info (IP, MAC, memory, license, etc.) | – | `{ "ip": "...", "ipSTA": "...", "channel": 1, "mac": "...", "freeMemory": 123456, "connectedButtons": 3, "isOnline": true, "valid": "Ja", "tier": 1 }` | +| Route | Method | Description | Request Body/Params | Response Example | +| ----------- | ------ | ------------------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `/api/info` | GET | Get system info (IP, MAC, memory, license, etc.) | – | `{ "ip": "...", "ipSTA": "...", "channel": 1, "mac": "...", "freeMemory": 123456, "connectedButtons": 3, "isOnline": true, "valid": "Ja", "tier": 1 }` | --- ## WebSocket -| Route | Description | -|---------|------------------------------------| -| `/ws` | WebSocket endpoint for live updates| +| Route | Description | +| ----- | ----------------------------------- | +| `/ws` | WebSocket endpoint for live updates | --- -**All API endpoints return JSON unless otherwise noted. POST requests expect form parameters (not JSON body).** \ No newline at end of file +**All API endpoints return JSON unless otherwise noted. POST requests expect form parameters (not JSON body).** diff --git a/Bedienungsanleitung_NinjaCross_Timer.html b/Bedienungsanleitung_NinjaCross_Timer.html new file mode 100644 index 0000000..a8ed284 --- /dev/null +++ b/Bedienungsanleitung_NinjaCross_Timer.html @@ -0,0 +1,674 @@ + + + + +NinjaCross Timer - Bedienungsanleitung + + + + + +

NinjaCross Timer - Bedienungsanleitung

+ +
+

Version: 1.0

+

Hersteller: AquaMaster MQTT

+

Datum: 2024

+
+ +

1. Einleitung

+ +

Der NinjaCross Timer ist ein professionelles Zeitmessgerät für Ninjacross-Wettkämpfe. Das System ermöglicht die präzise Zeitmessung für bis zu zwei Bahnen gleichzeitig und bietet zahlreiche Features wie RFID-Erkennung, lokales Leaderboard und Internet-Konnektivität über WiFi und MQTT.

+ +

2. Systemübersicht

+ +

2.1 Komponenten

+ + + +

2.2 Anzeigen und Status

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
KomponenteBeschreibung
Heartbeat-Indikatoren4 grüne/rote Punkte zeigen die Verbindung der Buttons an (Start1, Stop1, Start2, Stop2)
Timer-AnzeigeLive-Zeit für beide Bahnen
Status-AnzeigeBereit, Läuft, Geschafft, Standby
LeaderboardTop 6 Zeiten lokal gespeichert
Batterie-WarnungBanner bei niedriger Batterie der Buttons
+ +

3. Erste Inbetriebnahme

+ +

3.1 Einschalten und Netzwerkverbindung

+ +
    +
  1. Einschalten: Master einschalten
  2. +
  3. Access Point finden: Suchen Sie nach dem WiFi-Netzwerk mit dem Namen NinjaCross-XXXXX (die letzten Zeichen sind eindeutig für Ihr Gerät)
  4. +
  5. Verbinden: Das Netzwerk ist standardmäßig ohne Passwort
  6. +
  7. IP-Adresse: Das Gerät hat die feste IP 192.168.10.1
  8. +
  9. Alternative: Sie können auch ninjacross.local im Browser verwenden (mDNS)
  10. +
+ +
+

Wichtig: Der Access Point benötigt kein Passwort.

+
+ +

3.2 Web-Interface öffnen

+ +

Öffnen Sie Ihren Webbrowser und geben Sie eine der folgenden Adressen ein:

+ + + +

4. Hauptoberfläche

+ +

4.1 Timer-Ansicht

+ +

Die Hauptseite zeigt:

+ + + +

4.2 Timer-Bedienung

+ +
    +
  1. Standby: "Drücke beide Buttons einmal" - Buttons initialisieren
  2. +
  3. Bereit: Beide Buttons sind verbunden (grüne Heartbeats)
  4. +
  5. Armiert: Startbutton gedrückt - Timer startet bei freigegebenem Button
  6. +
  7. Läuft: Timer läuft - Zeit wird live angezeigt
  8. +
  9. Geschafft: Stop-Button gedrückt - Zeit wird gespeichert
  10. +
+ +
+

Tipp: Die Anzeige blendet automatisch die Schwimmer-Namen ein, wenn sie via RFID erkannt werden.

+
+ +

5. Button-Konfiguration

+ +

5.1 Anlernmodus

+ +

Der erste Schritt ist das Anlernen Ihrer Wireless-Buttons:

+ +
    +
  1. Öffnen Sie die Einstellungen (⚙️)
  2. +
  3. Scrollen Sie zu "Button-Konfiguration"
  4. +
  5. Klicken Sie auf "🎯 Anlernmodus starten"
  6. +
  7. Folgen Sie den Anweisungen: +
      +
    1. Drücken Sie den Button für Bahn 1 Start
    2. +
    3. Drücken Sie den Button für Bahn 1 Stop
    4. +
    5. Drücken Sie den Button für Bahn 2 Start
    6. +
    7. Drücken Sie den Button für Bahn 2 Stop
    8. +
    +
  8. +
  9. Die Anzeige zeigt automatisch an, welchen Button Sie drücken müssen
  10. +
  11. Nach erfolgreicher Konfiguration erhalten Sie eine Bestätigung
  12. +
+ +
+

Erfolg: Nach dem Anlernen sollten alle 4 Heartbeat-Indikatoren grün leuchten.

+
+ +

5.2 Buttons verlernen

+ +

Um alle Button-Zuweisungen zu löschen:

+ +
    +
  1. Einstellungen öffnen
  2. +
  3. "❌ Buttons verlernen" klicken
  4. +
  5. Bestätigung erfordert
  6. +
+ +

5.3 Button-Status anzeigen

+ +

Klicken Sie auf "📊 Button-Status anzeigen" um zu sehen:

+ + + +

6. RFID-Benutzerverwaltung

+ +

6.1 RFID-Karte registrieren

+ +

Die RFID-Funktion ermöglicht die automatische Zuordnung von Zeiten zu Nutzern:

+ +
    +
  1. Öffnen Sie "RFID" (🏷️) aus dem Einstellungsmenü
  2. +
  3. Klicken Sie auf "📡 Read Chip"
  4. +
  5. Halten Sie die RFID-Karte an den Reader des Masters
  6. +
  7. Die UID wird automatisch eingefügt
  8. +
  9. Geben Sie den Namen ein
  10. +
  11. Klicken Sie auf "💾 Speichern"
  12. +
+ +
+

Funktionsweise: Beim nächsten Scannen der RFID-Karte an einem Button wird automatisch der Name angezeigt und die Zeit diesem Nutzer zugeordnet.

+
+ +

6.2 Kontinuierliches Lesen

+ +

Der "Read Chip" Button startet einen kontinuierlichen Lesemodus:

+ + + +

7. Einstellungen

+ +

7.1 Datum & Uhrzeit

+ +

Die Uhrzeit kann manuell oder automatisch gesetzt werden:

+ + + +

7.2 Modus

+ + + + + + + + + + + + + + +
ModusBeschreibung
👤 IndividualBeide Bahnen arbeiten unabhängig - ideale für Training
🏆 WettkampfBeide Bahnen starten synchron - für Wettkämpfe
+ +

7.3 Lane-Konfiguration

+ +

Die Bahnen können identisch oder unterschiedlich konfiguriert werden:

+ + + +

7.4 Grundeinstellungen

+ + + + + + + + + + + + + + + + + + + + + + +
EinstellungStandardBeschreibung
Maximale Zeit300 SekundenNach dieser Zeit wird eine Bahn automatisch zurückgesetzt
Anzeigedauer20 SekundenWie lange die letzte Zeit angezeigt bleibt
Min. Zeit Leaderboard5 SekundenZeiten unter diesem Wert werden nicht gespeichert (Missbrauchsschutz)
+ +

7.5 WLAN-Konfiguration (Lizenz Level 3 erforderlich)

+ +
+

Wichtig: Um das System mit einem bestehenden WLAN zu verbinden wird eine Lizenz Level 3 oder höher.

+
+ +

Zur Konfiguration:

+ +
    +
  1. WLAN Name (SSID) eingeben
  2. +
  3. WLAN Passwort eingeben
  4. +
  5. Aktueller STA IP-Status wird angezeigt
  6. +
  7. Nach dem Speichern startet das Gerät neu
  8. +
+ +
+

Dual-Mode: Das Gerät kann gleichzeitig Access Point (für direkte Verbindung) und WiFi Station (für Internet) betreiben.

+
+ +

7.6 Standort (Lizenz Level 3 erforderlich)

+ +

Wählen Sie Ihren Standort aus einem Dropdown-Menü:

+ + + +

7.7 OTA Update (Lizenz Level 2 erforderlich)

+ +
+

Lizenz erforderlich: OTA-Updates benötigen Lizenz Level 2 oder höher.

+
+ +
    +
  1. Klicken Sie auf "🔄 Update durchführen"
  2. +
  3. Bestätigen Sie die Abfrage
  4. +
  5. Das Gerät lädt die neueste Firmware herunter und installiert sie automatisch
  6. +
  7. Während des Updates darf der Strom nicht unterbrochen werden!
  8. +
+ +

7.8 Buttons Updaten

+ +

Sendet eine Update-Nachricht über MQTT an alle konfigurierten Buttons:

+ +
    +
  1. Klicken Sie auf "📡 Buttons Updaten"
  2. +
  3. Die Buttons erhalten die aktuelle Konfiguration
  4. +
  5. Nutzen Sie dies nach Button-Wartung oder Konfigurationsänderungen
  6. +
+ +

8. Leaderboard

+ +

8.1 Lokales Leaderboard

+ +

Die Hauptseite zeigt die Top 6 Zeiten:

+ + + +

8.2 Volle Leaderboard-Ansicht

+ +

Öffnen Sie die Leaderboard-Seite (🏆):

+ + + +

8.3 Beste Zeiten zurücksetzen

+ +

Einstellungen → "🏆 Zeiten verwalten" → "🔄 Beste Zeiten zurücksetzen"

+ +
+

Achtung: Diese Aktion kann nicht rückgängig gemacht werden!

+
+ +

9. System-Information

+ +

Die Einstellungsseite zeigt folgende Systemdaten:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
InformationBeschreibung
IP-AdresseAccess Point IP (meist 192.168.10.1)
KanalWiFi-Kanal
MAC-AdresseEindeutige Geräte-ID
InternetJa/Nein - Verbindung zum Internet
Freier SpeicherVerfügbarer RAM in Bytes
Verbundene ButtonsAnzahl konfigurierter Buttons (0-4)
Lizenz gültigStatus der Lizenz
Lizenz Level0-3 - Bestimmt verfügbare Features
+ +

10. Lizenz-System

+ +

10.1 Lizenz-Level

+ + + + + + + + + + + + + + + + + + + + + + +
LevelFeatures
0 (Basis)Standard-Timer, lokales Leaderboard, RFID
1Alle Level 0 Features
2Level 1 + OTA Updates
3Level 2 + WLAN-Station Mode, Standort-Konfiguration
+ +

10.2 Lizenz eingeben

+ +
    +
  1. Einstellungen → "🔧 Lizenz"
  2. +
  3. Lizenzschlüssel eingeben
  4. +
  5. "💾 Lizenz speichern" klicken
  6. +
  7. System-Information aktualisiert sich automatisch
  8. +
+ +

11. Batterie-Überwachung

+ +

Das System überwacht kontinuierlich die Batteriestände der Wireless-Buttons:

+ + + +
+

Tipp: Der Banner blendet automatisch aus, sobald alle Batterien wieder über 15% sind.

+
+ +

12. API & Technische Details

+ +

12.1 API-Endpunkte

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EndpointMethodFunktion
/api/dataGETTimer und Status abrufen
/api/reset-bestPOSTBeste Zeiten zurücksetzen
/api/start-learningPOSTAnlernmodus starten
/api/learn/statusGETAnlern-Status abrufen
/api/buttons/statusGETButton-Konfiguration und Batterie
/api/set-max-timePOSTTimer-Einstellungen setzen
/api/get-settingsGETEinstellungen abrufen
/api/set-wifiPOSTWiFi konfigurieren
/api/set-modePOSTModus setzen (Individual/Wettkampf)
/api/infoGETSystem-Informationen
/wsWebSocketLive-Updates für Timer
+ +

12.2 WebSocket-Daten

+ +

Der WebSocket liefert Echtzeit-Updates:

+ + + +

13. Troubleshooting

+ +

13.1 Buttons verbinden sich nicht

+ + + +

13.2 WiFi-Verbindung funktioniert nicht

+ + + +

13.3 IP-Adresse unbekannt

+ + + +

13.4 Timer startet nicht

+ + + +

13.5 RFID wird nicht erkannt

+ + + +
+

Wichtig: Bei andauernden Problemen Gerät neustarten oder Support kontaktieren.

+
+ +

14. Wartung

+ +

14.1 Regelmäßige Wartung

+ + + +

14.2 Firmware-Updates

+ +
    +
  1. Lizenz Level 2+ erforderlich
  2. +
  3. Einstellungen → OTA Update
  4. +
  5. Keine Unterbrechung während des Updates
  6. +
  7. Update dauert ca. 1-2 Minuten
  8. +
+ +

15. Support & Kontakt

+ +

Bei Fragen oder Problemen:

+ + + +
+

Hinweis: Diese Anleitung basiert auf der aktuellen Firmware-Version. Neuere Versionen könnten abweichende Features haben.

+
+ +

16. Anhang

+ +

16.1 Tastenkombinationen im Web-Interface

+ + + +

16.2 Unterstützte Browser

+ + + +

16.3 Technische Spezifikationen

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
KomponenteSpezifikation
ESP32 VersionESP32-WROOM oder kompatibel
WiFi2.4 GHz, WPA2
ProtokollMQTT für Kommunikation
RFID13.56 MHz, NFC-kompatibel
Timer-GenauigkeitMillisekunden
+ +
+ +

+Ende der Bedienungsanleitung
+NinjaCross Timer v1.0 +

+ + + diff --git a/apientpoints b/apientpoints index 9e32638..2d01232 100644 --- a/apientpoints +++ b/apientpoints @@ -9,7 +9,7 @@ POST /api/unlearn-button → Verlernt alle Button-Zuordnungen POST /api/set-max-time -→ Setzt die maximale Zeit und maxTimeDisplay +→ Setzt die maximale Zeit, maxTimeDisplay und minTimeForLeaderboard GET /api/get-settings → Gibt die aktuellen Einstellungen zurück diff --git a/data/firmware.bin b/data/firmware.bin deleted file mode 100644 index 1c22703..0000000 Binary files a/data/firmware.bin and /dev/null differ diff --git a/data/index.css b/data/index.css index bb1197d..99e32f7 100644 --- a/data/index.css +++ b/data/index.css @@ -11,8 +11,8 @@ html { } body { - font-family: "Arial", sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: "Segoe UI", Arial, sans-serif; + background: linear-gradient(0deg, #0d1733 0%, #223c83 100%); height: 100vh; width: 100vw; display: flex; @@ -38,8 +38,8 @@ body { text-decoration: none; display: block; cursor: pointer; - padding-left: 5px; - padding-right: 5px; + padding: 5px; + background:rgba(255, 255, 255, 0.6); } .logo:hover { @@ -53,6 +53,32 @@ body { border-radius: 10px; } +.leaderboard-btn { + position: fixed; + top: 20px; + right: 90px; + background: rgba(255, 255, 255, 0.2); + border: 2px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 15px; + border-radius: 50%; + text-decoration: none; + font-size: 1.5rem; + transition: all 0.3s ease; + z-index: 1000; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +.leaderboard-btn:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: scale(1.1); +} + .settings-btn { position: fixed; top: 20px; @@ -82,7 +108,7 @@ body { .heartbeat-indicators { position: fixed; top: 20px; - right: 90px; + right: 160px; display: flex; gap: 15px; z-index: 1000; @@ -93,11 +119,61 @@ body { border: 1px solid rgba(255, 255, 255, 0.2); } +@media (max-width: 768px) { + .logo { + width: 40px; + height: 40px; + top: 15px; + left: 15px; + padding: 3px; + } + + .leaderboard-btn { + top: 15px; + right: 60px; + padding: 10px; + font-size: 1.2rem; + } + + .settings-btn { + top: 15px; + right: 15px; + padding: 10px; + font-size: 1.2rem; + } + + .heartbeat-indicators { + top: 15px; + right: 90px; + gap: 8px; + padding: 8px 12px; + font-size: 0.8rem; + } + + .heartbeat-indicator { + width: 12px; + height: 12px; + } + + .heartbeat-indicator::before { + font-size: 8px; + top: -20px; + } + + .header h1 { + font-size: clamp(1.2rem, 3vw, 1.8rem); + } + + .header p { + font-size: clamp(0.7rem, 1.5vw, 0.9rem); + } +} + .heartbeat-indicator { width: 20px; height: 20px; border-radius: 50%; - background: #e74c3c; + background: #f50f0f; transition: all 0.3s ease; position: relative; } @@ -115,8 +191,8 @@ body { } .heartbeat-indicator.active { - background: #2ecc71; - box-shadow: 0 0 10px rgba(46, 204, 113, 0.5); + background: #00ff15; + box-shadow: 0 0 10px rgba(73, 186, 228, 0.5); } /* Batterie-Banner Styling */ @@ -125,7 +201,7 @@ body { top: -100px; left: 0; width: 100%; - background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); + background: linear-gradient(135deg, #f59d0f 0%, #e67e22 100%); color: white; padding: 15px 20px; text-align: center; @@ -261,6 +337,9 @@ body { font-size: clamp(1.8rem, 4vw, 2.5rem); margin-bottom: 0.5vh; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + font-weight: bold; + text-transform: uppercase; + font-family: "Segoe UI", Arial, sans-serif; } .header p { @@ -297,15 +376,20 @@ body { transition: transform 0.3s ease; display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; height: 100%; overflow: hidden; + position: relative; } .lane h2 { font-size: clamp(1.2rem, 2.5vw, 1.8rem); margin-bottom: clamp(10px, 1vh, 15px); color: #fff; + font-weight: bold; + text-transform: uppercase; + font-family: "Segoe UI", Arial, sans-serif; + flex-shrink: 0; } .swimmer-name { @@ -338,43 +422,75 @@ body { } .time-display { - font-size: clamp(3rem, 9vw, 10rem); + font-size: clamp(3rem, 13vw, 13rem); font-weight: bold; margin: clamp(10px, 1vh, 15px) 0; font-family: "Courier New", monospace; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); line-height: 1; + position: relative; + z-index: 1; + flex-shrink: 0; + order: 1; } .status { - font-size: clamp(3rem, 1.8vw, 1.2rem); + font-size: clamp(1.5rem, 4vw, 5rem); margin: clamp(8px, 1vh, 12px) 0; padding: clamp(6px, 1vh, 10px) clamp(12px, 2vw, 18px); border-radius: 20px; display: inline-block; font-weight: 600; + position: relative; + z-index: 2; +} + +.status:not(.large-status) { + position: relative; + order: 2; + margin-top: auto; +} + +.status.large-status { + font-size: clamp(1.8rem, 5vw, 5rem); + position: absolute; + left: 50%; + transform: translateX(-50%); + z-index: 10; + margin: 0 !important; + padding: clamp(8px, 1.5vh, 15px) clamp(15px, 3vw, 30px); + white-space: normal; + pointer-events: none; + text-align: center; + background-color: rgba(0, 0, 0, 0.85) !important; + backdrop-filter: blur(5px); + width: calc(100% - 40px); + max-width: calc(100% - 40px); + word-wrap: break-word; + line-height: 1.3; + overflow: visible; } .status.finished { - background-color: rgba(52, 152, 219, 0.3); - border: 2px solid #3498db; + background-color: rgba(73, 186, 228, 0.3); + border: 2px solid #49bae4; } .status.ready { - background-color: rgba(46, 204, 113, 0.3); - border: 2px solid #2ecc71; + background-color: rgb(0 165 3 / 54%); + border: 2px solid #06ff00; animation: pulse 1s infinite; } .status.armed { - background-color: rgb(197, 194, 0); - border: 2px solid #fbff00; + background-color: rgba(245, 157, 15, 0.3); + border: 2px solid #f59d0f; animation: pulse 1s infinite; } .status.running { - background-color: rgba(231, 76, 60, 0.3); - border: 2px solid #e74c3c; + background-color: rgb(255 91 0 / 65%); + border: 2px solid #f59d0f; } @keyframes pulse { @@ -390,8 +506,8 @@ body { } .status.standby { - background-color: rgba(255, 193, 7, 0.3); - border: 2px solid #ffc107; + background-color: rgba(220, 242, 250, 0.3); + border: 2px solid #DCF2FA; animation: standbyBlink 2s infinite; } @@ -422,17 +538,40 @@ body { border-radius: 15px; padding: clamp(10px, 1.5vh, 15px); margin: 1vh 0 0 0; - width: 50%; - max-width: 50%; + width: clamp(320px, 80vw, 960px); + max-width: 960px; text-align: center; border: 1px solid rgba(255, 255, 255, 0.2); flex-shrink: 0; align-self: center; + display: flex; + flex-direction: column; + align-items: stretch; + gap: clamp(12px, 2vh, 20px); + box-sizing: border-box; +} + +#leaderboard-container { + text-align: left; + display: grid; + grid-template-columns: 1fr; + gap: clamp(12px, 2vh, 20px); + width: 100%; +} + +@media (min-width: 768px) { + #leaderboard-container { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } .best-times h3 { font-size: clamp(0.9rem, 1.8vw, 1.1rem); - margin-bottom: clamp(5px, 0.5vh, 8px); + margin: 0 auto; + font-weight: bold; + text-transform: uppercase; + font-family: "Segoe UI", Arial, sans-serif; + text-align: center; } .best-time-row { @@ -446,9 +585,121 @@ body { border-radius: 8px; } +/* Leaderboard Styles */ +#leaderboard-container { + text-align: left; +} + +.leaderboard-entry { + display: flex; + justify-content: space-between; + align-items: center; + margin: clamp(8px, 1vh, 12px) 0; + font-size: clamp(1.1rem, 2.2vw, 1.4rem); + font-weight: 600; + background: rgba(255, 255, 255, 0.15); + padding: clamp(12px, 2vh, 16px) clamp(16px, 3vw, 24px); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; + min-height: 50px; + width: 100%; + box-sizing: border-box; +} + +.leaderboard-entry:hover { + background: rgba(255, 255, 255, 0.25); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.leaderboard-entry .rank { + color: #ffd700; + font-weight: bold; + min-width: 30px; + font-size: clamp(1.2rem, 2.4vw, 1.5rem); +} + +.leaderboard-entry .name { + flex: 1; + margin: 0 15px; + color: #ffffff; + font-weight: 600; +} + +.leaderboard-entry .time { + color: #00ff88; + font-weight: bold; + font-family: 'Courier New', monospace; + min-width: 80px; + text-align: right; +} + +.leaderboard-entry.gold { + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); + border-color: #ffd700; + color: #b8860b; + font-weight: bold; + box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); +} + +.leaderboard-entry.gold .rank { + color: #7a4d00; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6); +} + +.leaderboard-entry.gold .time { + color: #0f5132; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5); +} + +.leaderboard-entry.silver { + background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%); + border-color: #c0c0c0; + color: #696969; + font-weight: bold; + box-shadow: 0 4px 15px rgba(192, 192, 192, 0.3); +} + +.leaderboard-entry.silver .rank { + color: #4b5563; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6); +} + +.leaderboard-entry.silver .time { + color: #0f5132; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5); +} + +.leaderboard-entry.bronze { + background: linear-gradient(135deg, #cd7f32 0%, #e6a85c 100%); + border-color: #cd7f32; + color: #8b4513; + font-weight: bold; + box-shadow: 0 4px 15px rgba(205, 127, 50, 0.3); +} + +.leaderboard-entry.bronze .rank { + color: #7a3410; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6); +} + +.leaderboard-entry.bronze .time { + color: #0f5132; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5); +} + +.no-times { + text-align: center; + color: rgba(255, 255, 255, 0.7); + font-style: italic; + font-size: clamp(0.9rem, 1.8vw, 1.1rem); + padding: 20px; +} + .learning-mode { - background: rgba(255, 193, 7, 0.2); - border: 2px solid #ffc107; + background: rgba(245, 157, 15, 0.2); + border: 2px solid #f59d0f; border-radius: 15px; padding: clamp(15px, 2vh, 20px); margin: 2vh 0; @@ -463,9 +714,12 @@ body { } .learning-mode h3 { - color: #ffc107; + color: #f59d0f; margin-bottom: 10px; font-size: clamp(1rem, 2vw, 1.3rem); + font-weight: bold; + text-transform: uppercase; + font-family: "Segoe UI", Arial, sans-serif; } .learning-mode p { diff --git a/data/index.html b/data/index.html index c3e5607..26b031f 100644 --- a/data/index.html +++ b/data/index.html @@ -15,14 +15,16 @@
- + + 🏆 ⚙️
@@ -42,46 +44,37 @@

🏊‍♀️ NinjaCross Timer

-

Professioneller Zeitmesser für Ninjacross Wettkämpfe

+

Dein professioneller Zeitmesser für Ninjacross Wettkämpfe

🏊‍♀️ Bahn 1

-
00.00
- Standby: Bitte beide 1x betätigen + Standby: Drücke beide Buttons einmal
+
00.00

🏊‍♂️ Bahn 2

-
00.00
- Standby: Bitte beide 1x betätigen + Standby: Drücke beide Buttons einmal
+
00.00
-

🏆 Beste Zeiten des Tages

-
- Bahn 1: - --.- -
-
- Bahn 2: - --.- -
+

🏆 Lokales Leaderboard

+
diff --git a/data/leaderboard.css b/data/leaderboard.css new file mode 100644 index 0000000..b2fe2ff --- /dev/null +++ b/data/leaderboard.css @@ -0,0 +1,367 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", Arial, sans-serif; + background: linear-gradient(0deg, #0d1733 0%, #223c83 100%); + min-height: 100vh; + padding: 20px; +} + +.back-btn { + position: fixed; + top: 20px; + left: 20px; + background: rgba(255, 255, 255, 0.2); + border: 2px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 15px; + border-radius: 50%; + text-decoration: none; + font-size: 1.5rem; + transition: all 0.3s ease; + z-index: 1000; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +.back-btn:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: scale(1.1); +} + +.container { + max-width: 800px; + margin: 0 auto; + background: rgba(255, 255, 255, 0.95); + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + overflow: visible; + backdrop-filter: blur(10px); +} + +.header { + background: linear-gradient(135deg, #49bae4 0%, #223c83 100%); + color: white; + padding: 30px; + text-align: center; + position: relative; +} + +.header h1 { + font-size: 2.5em; + margin-bottom: 10px; + position: relative; + z-index: 1; + font-weight: bold; + text-transform: uppercase; + font-family: "Segoe UI", Arial, sans-serif; +} + +.content { + padding: 30px; +} + +.leaderboard-container { + background: white; + border-radius: 12px; + padding: 20px; + border: 2px solid #e9ecef; + min-height: 150px; + max-height: none; + overflow: visible; +} + +.leaderboard-row { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} + +.leaderboard-row:last-child { + margin-bottom: 0; +} + +@media (min-width: 768px) { + .leaderboard-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + align-items: start; + grid-auto-rows: min-content; + } + + .leaderboard-row { + margin-bottom: 0; + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; + } +} + +.leaderboard-entry { + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0; + font-size: 1.1em; + font-weight: 600; + background: #f8f9fa; + padding: 15px 20px; + border-radius: 10px; + border: 2px solid #e9ecef; + transition: all 0.3s ease; +} + +.leaderboard-entry:hover { + background: #e9ecef; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.leaderboard-entry.gold { + background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); + border-color: #ffd700; + color: #b8860b; + font-weight: bold; + box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); +} + +.leaderboard-entry.silver { + background: linear-gradient(135deg, #c0c0c0 0%, #e8e8e8 100%); + border-color: #c0c0c0; + color: #696969; + font-weight: bold; + box-shadow: 0 4px 15px rgba(192, 192, 192, 0.3); +} + +.leaderboard-entry.bronze { + background: linear-gradient(135deg, #cd7f32 0%, #e6a85c 100%); + border-color: #cd7f32; + color: #8b4513; + font-weight: bold; + box-shadow: 0 4px 15px rgba(205, 127, 50, 0.3); +} + +.leaderboard-entry .rank { + font-weight: bold; + min-width: 40px; + font-size: 1.2em; + text-align: center; +} + +.leaderboard-entry .name { + flex: 1; + margin: 0 20px; + font-weight: 600; +} + +.leaderboard-entry .time { + font-weight: bold; + font-family: 'Courier New', monospace; + min-width: 100px; + text-align: right; + font-size: 1.1em; +} + +.no-entries { + text-align: center; + color: #6c757d; + font-style: italic; + font-size: 1.1em; + padding: 40px; +} + +.loading { + text-align: center; + color: #49bae4; + font-size: 1.1em; + padding: 40px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +/* Modern Notification Toast */ +.notification-toast { + position: fixed; + top: 24px; + right: 24px; + min-width: 320px; + max-width: 400px; + background: rgba(255, 255, 255, 0.98); + border-radius: 16px; + box-shadow: + 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(20px); + z-index: 99999; + display: none; + align-items: flex-start; + gap: 12px; + padding: 16px; + transform: translateX(100%); + opacity: 0; + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); + pointer-events: auto; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.notification-toast.show { + transform: translateX(0); + opacity: 1; +} + +.notification-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 600; + color: white; + background: linear-gradient(135deg, #10b981, #059669); +} + +.notification-body { + flex: 1; + min-width: 0; +} + +.notification-title { + font-size: 14px; + font-weight: 600; + color: #111827; + margin-bottom: 4px; + line-height: 1.2; +} + +.notification-message { + font-size: 13px; + color: #6b7280; + line-height: 1.4; + word-wrap: break-word; +} + +.notification-close { + flex-shrink: 0; + width: 32px; + height: 32px; + border: none; + background: none; + color: #9ca3af; + cursor: pointer; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + margin-top: -4px; + margin-right: -4px; +} + +.notification-close:hover { + background: rgba(0, 0, 0, 0.05); + color: #374151; +} + +.notification-close:active { + transform: scale(0.95); +} + +/* Toast Types */ +.notification-toast.success .notification-icon { + background: linear-gradient(135deg, #10b981, #059669); +} + +.notification-toast.error .notification-icon { + background: linear-gradient(135deg, #ef4444, #dc2626); +} + +.notification-toast.info .notification-icon { + background: linear-gradient(135deg, #3b82f6, #2563eb); +} + +.notification-toast.warning .notification-icon { + background: linear-gradient(135deg, #f59e0b, #d97706); +} + +/* Mobile Responsiveness */ +@media (max-width: 768px) { + .container { + margin: 10px; + border-radius: 15px; + } + + .content { + padding: 20px; + } + + .leaderboard-entry { + flex-direction: column; + gap: 10px; + text-align: center; + } + + .leaderboard-entry .name { + margin: 0; + order: 1; + } + + .leaderboard-entry .rank { + order: 2; + } + + .leaderboard-entry .time { + order: 3; + } + + /* Mobile notification adjustments */ + .notification-toast { + top: 10px; + right: 10px; + left: 10px; + max-width: none; + font-size: 14px; + padding: 12px 16px; + } +} + +@media (max-width: 480px) { + .header h1 { + font-size: 2em; + } + + .leaderboard-entry { + padding: 12px 15px; + font-size: 1em; + } + + .leaderboard-entry .rank { + font-size: 1.1em; + } + + .leaderboard-entry .time { + font-size: 1em; + } +} \ No newline at end of file diff --git a/data/leaderboard.html b/data/leaderboard.html new file mode 100644 index 0000000..2ebd2ff --- /dev/null +++ b/data/leaderboard.html @@ -0,0 +1,227 @@ + + + + + + + + + + + Ninjacross Timer - Leaderboard + + + + + + + 🏠 + +
+ +
+

🏆 Leaderboard

+
+ +
+ +
+
Lade Leaderboard...
+
+
+
+ + + + + diff --git a/data/pictures/logo.png b/data/pictures/erlebniss.png similarity index 100% rename from data/pictures/logo.png rename to data/pictures/erlebniss.png diff --git a/data/pictures/logo.svg b/data/pictures/logo.svg new file mode 100644 index 0000000..ada9e5c --- /dev/null +++ b/data/pictures/logo.svg @@ -0,0 +1,23 @@ + + \ No newline at end of file diff --git a/data/rfid.html b/data/rfid.html index 31b65ca..75b78d6 100644 --- a/data/rfid.html +++ b/data/rfid.html @@ -62,7 +62,7 @@ type="button" id="readUidBtn" class="read-uid-btn" - onclick="readRFIDUID()" + onclick="toggleRFIDReading()" > 📡 Read Chip @@ -70,47 +70,16 @@
- +
-
- - -
- -
- -
- - -
-
-
+ +
+

🏊‍♀️ Lane-Konfiguration

+
+
+ +
+ + +
+
+ + + +
+ +
+
+
@@ -133,6 +182,18 @@ title="Zeit nach der die angezeigte Zeit zurückgesetzt wird" />
+
+ + +
+ + + + + +
+
+

API Endpoint Testing

+
+ + +
+
+ + +
+ +
+

Response:

+

+                
+
+
+ + +
+
+

MQTT Publish

+
+ + +
+
+ + +
+ + +
+

Quick Actions:

+
+ + + + + + + + + +
+
+
+ +
+

MQTT Subscribe

+
+ + +
+ + + +
+

Received Messages:

+
+ + +
+
+
+
+
+ + +
+
+

Debug Endpoints

+

Direct access to debug endpoints for timer control:

+
+ + + + +
+
+

Last Response:

+

+                
+
+
+
+ + + + + + diff --git a/mock-server/mock_esp32_server.js b/mock-server/mock_esp32_server.js new file mode 100644 index 0000000..69c8b3b --- /dev/null +++ b/mock-server/mock_esp32_server.js @@ -0,0 +1,718 @@ +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); + }); +}); diff --git a/mock-server/mqtt_broker.js b/mock-server/mqtt_broker.js new file mode 100644 index 0000000..215e109 --- /dev/null +++ b/mock-server/mqtt_broker.js @@ -0,0 +1,108 @@ +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); + }); +}); diff --git a/mock-server/package-lock.json b/mock-server/package-lock.json new file mode 100644 index 0000000..4a2e739 --- /dev/null +++ b/mock-server/package-lock.json @@ -0,0 +1,1922 @@ +{ + "name": "aquamaster-mock-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aquamaster-mock-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "aedes": "^0.50.0", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "mqtt": "^5.3.1", + "socket.io": "^4.6.1", + "ws": "^8.14.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aedes": { + "version": "0.50.1", + "resolved": "https://registry.npmjs.org/aedes/-/aedes-0.50.1.tgz", + "integrity": "sha512-S1P+COZYSDVYND8G+b7Vy+xoENix57QOJL8pDlwBYvr6GIiaJZFNvOy7GvUxPDxb0EBrsiPSfHHJc6aySK9Wfg==", + "license": "MIT", + "dependencies": { + "aedes-packet": "^3.0.0", + "aedes-persistence": "^9.1.2", + "end-of-stream": "^1.4.4", + "fastfall": "^1.5.1", + "fastparallel": "^2.4.1", + "fastseries": "^2.0.0", + "hyperid": "^3.1.1", + "mqemitter": "^5.0.0", + "mqtt-packet": "^9.0.0", + "retimer": "^3.0.0", + "reusify": "^1.0.4", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/aedes-packet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz", + "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==", + "license": "MIT", + "dependencies": { + "mqtt-packet": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/aedes-packet/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/aedes-packet/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/aedes-packet/node_modules/mqtt-packet": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz", + "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/aedes-packet/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/aedes-packet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/aedes-persistence": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz", + "integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==", + "license": "MIT", + "dependencies": { + "aedes-packet": "^3.0.0", + "qlobber": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/broker-factory": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz", + "integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.26", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz", + "integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastseries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz", + "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==", + "license": "ISC" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/hyperid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz", + "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + } + }, + "node_modules/hyperid/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqemitter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-5.0.0.tgz", + "integrity": "sha512-rqNRQhGgl0W/NV+Zrx0rpAUTZcSlAtivCVUmXBUPcFYt+AeDEpoJgy5eKlFWJP6xnatONL59WIFdV0W6niOMhw==", + "license": "ISC", + "dependencies": { + "fastparallel": "^2.3.0", + "qlobber": "^7.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mqtt": { + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.14.1.tgz", + "integrity": "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mqtt/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/number-allocator/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/number-allocator/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qlobber": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz", + "integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/retimer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", + "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/worker-factory": { + "version": "7.0.48", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz", + "integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.29", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.29.tgz", + "integrity": "sha512-9jk0MWHhWAZ2xlJPXr45oe5UF/opdpfZrY0HtyPizWuJ+ce1M3IYk/4IIdGct3kn9Ncfs+tkZt3w1tU6KW2Fsg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.15", + "worker-timers-worker": "^9.0.13" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz", + "integrity": "sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "fast-unique-numbers": "^9.0.26", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.13" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz", + "integrity": "sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "tslib": "^2.8.1", + "worker-factory": "^7.0.48" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/mock-server/package.json b/mock-server/package.json new file mode 100644 index 0000000..d6bd65a --- /dev/null +++ b/mock-server/package.json @@ -0,0 +1,28 @@ +{ + "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" + } +} diff --git a/mock-server/start_all.js b/mock-server/start_all.js new file mode 100644 index 0000000..2dde6d2 --- /dev/null +++ b/mock-server/start_all.js @@ -0,0 +1,47 @@ +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 diff --git a/platformio.ini b/platformio.ini index b4909fc..d73448a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,6 +22,9 @@ monitor_speed = 115200 build_flags = -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue + -DBATTERY_PIN=16 +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv targets = uploadfs board_build.psram = disabled lib_deps = @@ -29,8 +32,7 @@ lib_deps = esp32async/ESPAsyncWebServer@^3.7.7 esp32async/AsyncTCP@^3.4.2 mlesniew/PicoMQTT@^1.3.0 - miguelbalboa/MFRC522@^1.4.12 - adafruit/RTClib@^2.1.4 + adafruit/Adafruit PN532@^1.3.4 [env:esp32thing_OTA] board = esp32thing @@ -50,8 +52,9 @@ lib_deps = esp32async/ESPAsyncWebServer@^3.7.7 esp32async/AsyncTCP@^3.4.2 mlesniew/PicoMQTT@^1.3.0 - miguelbalboa/MFRC522@^1.4.12 - adafruit/RTClib@^2.1.4 + adafruit/Adafruit PN532@^1.3.4 + + [env:esp32thing] board = esp32thing_plus @@ -69,8 +72,7 @@ lib_deps = esp32async/ESPAsyncWebServer@^3.7.7 esp32async/AsyncTCP@^3.4.2 mlesniew/PicoMQTT@^1.3.0 - miguelbalboa/MFRC522@^1.4.12 - adafruit/RTClib@^2.1.4 + adafruit/Adafruit PN532@^1.3.4 [env:esp32thing_CI] platform = espressif32 @@ -87,21 +89,47 @@ lib_deps = esp32async/ESPAsyncWebServer@^3.7.7 esp32async/AsyncTCP@^3.4.2 mlesniew/PicoMQTT@^1.3.0 - miguelbalboa/MFRC522@^1.4.12 - adafruit/RTClib@^2.1.4 + adafruit/Adafruit PN532@^1.3.4 -[env:esp32-s3-devkitc-1] -board = esp32-s3-devkitc-1 +[env:um_feathers3] +board = um_feathers3 monitor_speed = 115200 board_upload.flash_size = 16MB board_build.partitions = default_16MB.csv +board_upload.wait_for_upload_port = false build_flags = - -DARDUINO_USB_CDC_ON_BOOT=1 - -DBATTERY_PIN=35 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D BATTERY_PIN=35 + -D ARDUINO_USB_MODE=1 + lib_deps = bblanchon/ArduinoJson@^7.4.1 esp32async/ESPAsyncWebServer@^3.7.7 esp32async/AsyncTCP@^3.4.2 mlesniew/PicoMQTT@^1.3.0 - miguelbalboa/MFRC522@^1.4.12 - adafruit/RTClib@^2.1.4 \ No newline at end of file + adafruit/Adafruit PN532@^1.3.4 + +[env:um_feathers3_debug] +board = um_feathers3 +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv +board_upload.wait_for_upload_port = false +build_flags = + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D BATTERY_PIN=35 + -D ARDUINO_USB_MODE=0 + +build_type = debug +debug_speed = 12000 +debug_tool = esp-builtin +upload_port = COM5 +monitor_speed = 115200 +monitor_port = COM7 + +lib_deps = + bblanchon/ArduinoJson@^7.4.1 + esp32async/ESPAsyncWebServer@^3.7.7 + esp32async/AsyncTCP@^3.4.2 + mlesniew/PicoMQTT@^1.3.0 + adafruit/Adafruit PN532@^1.3.4 + diff --git a/src/communication.h b/src/communication.h index 3aba2b6..9475751 100644 --- a/src/communication.h +++ b/src/communication.h @@ -2,6 +2,8 @@ #include "master.h" #include #include +#include +#include #include @@ -44,6 +46,20 @@ typedef struct { // MQTT-Server-Instanz PicoMQTT::Server mqtt; +// Tracking der Quelle für jede Lane +bool start1FoundLocally = false; +bool start2FoundLocally = false; +String start1UID = ""; +String start2UID = ""; + +// Hilfsfunktionen um die Quelle abzufragen +bool wasStart1FoundLocally() { return start1FoundLocally; } + +bool wasStart2FoundLocally() { return start2FoundLocally; } + +String getStart1UID() { return start1UID; } +String getStart2UID() { return start2UID; } + /** * Liest eine Button-JSON-Nachricht, extrahiert Typ, MAC und Timestamp, * prüft die Button-Zuordnung und ruft die entsprechende Handler-Funktion auf. @@ -249,35 +265,116 @@ void publishLaneStatus(int lane, String status) { * sendet diese ggf. als JSON an das Frontend. */ void readRFIDfromButton(const char *topic, const char *payload) { + loadLicenceFromPrefs(); + String topicStr(topic); + int lastSlash = topicStr.lastIndexOf('/'); + if (lastSlash < 0) + return; + String macStr = topicStr.substring(lastSlash + 1); // Create a JSON document to hold the button press data StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, payload); if (!error) { - const char *mac = doc["buttonmac"] | "unknown"; const char *uid = doc["uid"] | "unknown"; Serial.printf("RFID Read from Button:\n"); - Serial.printf(" Button MAC: %s\n", mac); + Serial.printf(" Button MAC: %s\n", macStr.c_str()); Serial.printf(" UID: %s\n", uid); + String debugUpperUid = String(uid); + debugUpperUid.toUpperCase(); + Serial.printf(" UID (Upper): %s\n", debugUpperUid.c_str()); // Convert buttonmac to byte array for comparison - auto macBytes = macStringToBytes(mac); + auto macBytes = macStringToBytes(macStr.c_str()); // Check if the buttonmac matches buttonConfigs.start1.mac if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { - // Fetch user data - UserData userData = checkUser(uid); - if (userData.exists) { - // Log user data - Serial.printf("User found for start1: %s %s, Alter: %d\n", - userData.firstname.c_str(), userData.lastname.c_str(), - userData.alter); + // Prüfe ob Lane 1 bereit ist + if (timerData1.isRunning || timerData1.isArmed) { + Serial.println("Lane 1 läuft - ignoriere RFID: " + String(uid)); + return; + } - // Create JSON message to send to the frontend + // Zuerst lokal suchen (UID in Großbuchstaben konvertieren) + String upperUid = String(uid); + upperUid.toUpperCase(); + UserData userData = checkUser(upperUid); + start1FoundLocally = userData.exists; // Merken ob lokal gefunden + start1UID = upperUid; // UID für später speichern + + if (!userData.exists) { + // Nicht lokal gefunden - Online-Server fragen + Serial.println("User nicht lokal gefunden, suche online..."); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(String(BACKEND_SERVER) + "/api/v1/private/users/find"); + http.addHeader("Content-Type", "application/json"); + http.addHeader("Authorization", String("Bearer ") + licence); + + Serial.println("Online-Suche mit Token: " + licence); + + StaticJsonDocument<200> requestDoc; + String upperUidForRequest = String(uid); + upperUidForRequest.toUpperCase(); + requestDoc["uid"] = + upperUidForRequest; // UID in Großbuchstaben konvertieren + String requestBody; + serializeJson(requestDoc, requestBody); + + Serial.println("Request Body: " + requestBody); + + int httpCode = http.POST(requestBody); + + if (httpCode == HTTP_CODE_OK) { + String response = http.getString(); + Serial.println("Response: " + response); + StaticJsonDocument<512> responseDoc; + DeserializationError parseError = + deserializeJson(responseDoc, response); + + if (!parseError && responseDoc["success"].as() && + responseDoc["data"]["exists"].as()) { + // Online gefundenen Benutzer verwenden (nicht lokal speichern) + String firstName = responseDoc["data"]["firstname"].as(); + String lastName = responseDoc["data"]["lastname"].as(); + String fullName = firstName + " " + lastName; + + // UserData für Frontend erstellen + userData.uid = upperUid; + userData.firstname = firstName; + userData.lastname = ""; + userData.alter = 0; + userData.exists = true; + + Serial.println("User online gefunden: " + fullName); + } else { + Serial.println("User auch online nicht gefunden für UID: " + + upperUid); + } + } else { + Serial.printf("Online-Suche fehlgeschlagen: HTTP %d\n", httpCode); + } + + http.end(); + } else { + Serial.println("Keine Internetverbindung für Online-Suche"); + } + } + + // Wenn Benutzer gefunden wurde (lokal oder online) + if (userData.exists) { + // Bestimme ob lokal oder online gefunden (bereits oben gesetzt) + String source = start1FoundLocally ? "lokal" : "online"; + + // Log user data mit Quelle + Serial.printf("User %s gefunden für start1: %s\n", source.c_str(), + userData.firstname.c_str()); + + // Create JSON message to send to the frontend (ohne source) StaticJsonDocument<128> messageDoc; - messageDoc["firstname"] = userData.firstname; - messageDoc["lastname"] = userData.lastname; - messageDoc["lane"] = "start1"; // Add lane information + messageDoc["name"] = userData.firstname; + messageDoc["lane"] = "start1"; String message; serializeJson(messageDoc, message); @@ -287,24 +384,110 @@ void readRFIDfromButton(const char *topic, const char *payload) { Serial.printf("Pushed user data for start1 to frontend: %s\n", message.c_str()); } else { - Serial.println("User not found for UID: " + String(uid)); + Serial.println("User nicht gefunden für UID: " + upperUid); + + // Sende UID an Frontend wenn kein User gefunden wurde + StaticJsonDocument<128> messageDoc; + messageDoc["name"] = upperUid; // UID als Name senden + messageDoc["lane"] = "start1"; + + String message; + serializeJson(messageDoc, message); + + // Push die UID an das Frontend + pushUpdateToFrontend(message); + Serial.printf("Sende UID an Frontend für start1: %s\n", + message.c_str()); } } // Check if the buttonmac matches buttonConfigs.start2.mac else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { - // Fetch user data - UserData userData = checkUser(uid); - if (userData.exists) { - // Log user data - Serial.printf("User found for start2: %s %s, Alter: %d\n", - userData.firstname.c_str(), userData.lastname.c_str(), - userData.alter); + // Prüfe ob Lane 2 bereit ist + if (timerData2.isRunning || timerData2.isArmed) { + Serial.println("Lane 2 nicht bereit - ignoriere RFID: " + String(uid)); + return; + } - // Create JSON message to send to the frontend + // Zuerst lokal suchen (UID in Großbuchstaben konvertieren) + String upperUid = String(uid); + upperUid.toUpperCase(); + UserData userData = checkUser(upperUid); + start2FoundLocally = userData.exists; // Merken ob lokal gefunden + start2UID = upperUid; // UID für später speichern + + if (!userData.exists) { + // Nicht lokal gefunden - Online-Server fragen + Serial.println("User nicht lokal gefunden, suche online..."); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(String(BACKEND_SERVER) + "/api/v1/private/users/find"); + http.addHeader("Content-Type", "application/json"); + http.addHeader("Authorization", String("Bearer ") + licence); + + Serial.println("Online-Suche mit Token: " + licence); + + StaticJsonDocument<200> requestDoc; + String upperUidForRequest2 = String(uid); + upperUidForRequest2.toUpperCase(); + requestDoc["uid"] = + upperUidForRequest2; // UID in Großbuchstaben konvertieren + String requestBody; + serializeJson(requestDoc, requestBody); + + Serial.println("Request Body: " + requestBody); + + int httpCode = http.POST(requestBody); + + if (httpCode == HTTP_CODE_OK) { + String response = http.getString(); + Serial.println("Response: " + response); + StaticJsonDocument<512> responseDoc; + DeserializationError parseError = + deserializeJson(responseDoc, response); + + if (!parseError && responseDoc["success"].as() && + responseDoc["data"]["exists"].as()) { + // Online gefundenen Benutzer verwenden (nicht lokal speichern) + String firstName = responseDoc["data"]["firstname"].as(); + String lastName = responseDoc["data"]["lastname"].as(); + String fullName = firstName + " " + lastName; + + // UserData für Frontend erstellen + userData.uid = upperUid; + userData.firstname = firstName; + userData.lastname = ""; + userData.alter = 0; + userData.exists = true; + + Serial.println("User online gefunden: " + fullName); + } else { + Serial.println("User auch online nicht gefunden für UID: " + + upperUid); + } + } else { + Serial.printf("Online-Suche fehlgeschlagen: HTTP %d\n", httpCode); + } + + http.end(); + } else { + Serial.println("Keine Internetverbindung für Online-Suche"); + } + } + + // Wenn Benutzer gefunden wurde (lokal oder online) + if (userData.exists) { + // Bestimme ob lokal oder online gefunden (bereits oben gesetzt) + String source = start2FoundLocally ? "lokal" : "online"; + + // Log user data mit Quelle + Serial.printf("User %s gefunden für start2: %s\n", source.c_str(), + userData.firstname.c_str()); + + // Create JSON message to send to the frontend (ohne source) StaticJsonDocument<128> messageDoc; - messageDoc["firstname"] = userData.firstname; - messageDoc["lastname"] = userData.lastname; - messageDoc["lane"] = "start2"; // Add lane information + messageDoc["name"] = userData.firstname; + messageDoc["lane"] = "start2"; String message; serializeJson(messageDoc, message); @@ -314,7 +497,20 @@ void readRFIDfromButton(const char *topic, const char *payload) { Serial.printf("Pushed user data for start2 to frontend: %s\n", message.c_str()); } else { - Serial.println("User not found for UID: " + String(uid)); + Serial.println("User nicht gefunden für UID: " + upperUid); + + // Sende UID an Frontend wenn kein User gefunden wurde + StaticJsonDocument<128> messageDoc; + messageDoc["name"] = upperUid; // UID als Name senden + messageDoc["lane"] = "start2"; + + String message; + serializeJson(messageDoc, message); + + // Push die UID an das Frontend + pushUpdateToFrontend(message); + Serial.printf("Sende UID an Frontend für start2: %s\n", + message.c_str()); } } else { Serial.println("Button MAC does not match start1.mac or start2.mac"); @@ -335,11 +531,11 @@ void setupMqttServer() { mqtt.subscribe("#", [](const char *topic, const char *payload) { // Message received callback // Serial.printf("Received message on topic '%s': %s\n", topic, payload); - if (strncmp(topic, "aquacross/button/", 17) == 0) { - readButtonJSON(topic, payload); - } else if (strncmp(topic, "aquacross/button/rfid/", 22) == 0) { + if (strncmp(topic, "aquacross/button/rfid/", 22) == 0) { readRFIDfromButton(topic, payload); // Handle RFID read messages + } else if (strncmp(topic, "aquacross/button/", 17) == 0) { + readButtonJSON(topic, payload); } else if (strncmp(topic, "aquacross/battery/", 17) == 0) { handleBatteryTopic(topic, payload); } else if (strncmp(topic, "heartbeat/alive/", 16) == 0) { diff --git a/src/databasebackend.h b/src/databasebackend.h index f9c9eec..13f8553 100644 --- a/src/databasebackend.h +++ b/src/databasebackend.h @@ -1,14 +1,25 @@ #pragma once -#include "master.h" #include +#include +#include #include +#include #include +#include const char *BACKEND_SERVER = "https://ninja.reptilfpv.de"; extern String licence; // Declare licence as an external variable defined elsewhere -String BACKEND_TOKEN = - licence; // Use the licence as the token for authentication + +// Lokale Benutzer-Struktur +struct LocalUser { + String uid; + String name; + unsigned long timestamp; // Zeitstempel der Erstellung +}; + +// Lokale Benutzer-Speicherung (geht bei Neustart verloren) +std::vector localUsers; bool backendOnline() { @@ -20,8 +31,8 @@ bool backendOnline() { } HTTPClient http; - http.begin(String(BACKEND_SERVER) + "/v1/private/health"); - http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); + http.begin(String(BACKEND_SERVER) + "/api/v1/private/health"); + http.addHeader("Authorization", String("Bearer ") + licence); int httpCode = http.GET(); bool isOnline = (httpCode == HTTP_CODE_OK); @@ -44,87 +55,38 @@ struct UserData { bool exists; }; +// Forward declarations für Leaderboard-Funktionen +void addLocalTime(String uid, String name, unsigned long timeMs); + // Prüft, ob ein Benutzer mit der angegebenen UID in der Datenbank existiert und // gibt dessen Daten zurück. UserData checkUser(const String &uid) { UserData userData = {"", "", "", 0, false}; + String upperUid = uid; + upperUid.toUpperCase(); // UID in Großbuchstaben konvertieren - if (!backendOnline()) { - Serial.println("No internet connection, cannot check user."); - return userData; - } - - HTTPClient http; - http.begin(String(BACKEND_SERVER) + "/v1/private/users/find"); - http.addHeader("Content-Type", "application/json"); - http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); - - // Create JSON payload - StaticJsonDocument<200> requestDoc; - requestDoc["uid"] = uid; - String requestBody; - serializeJson(requestDoc, requestBody); - - int httpCode = http.POST(requestBody); - - if (httpCode == HTTP_CODE_OK) { - String payload = http.getString(); - StaticJsonDocument<512> responseDoc; - DeserializationError error = deserializeJson(responseDoc, payload); - - if (!error) { - userData.uid = responseDoc["uid"].as(); - userData.firstname = responseDoc["firstname"].as(); - userData.lastname = responseDoc["lastname"].as(); - userData.alter = responseDoc["alter"] | 0; + // Lokale Benutzer durchsuchen + for (const auto &user : localUsers) { + String userUpperUid = user.uid; + userUpperUid.toUpperCase(); + if (userUpperUid == upperUid) { + userData.uid = user.uid; + userData.firstname = user.name; + userData.lastname = ""; // Nicht mehr verwendet + userData.alter = 0; // Nicht mehr verwendet userData.exists = true; + + Serial.println("Lokaler Benutzer gefunden: " + user.name); + return userData; } - } else { - Serial.printf("User check failed, HTTP code: %d\n", httpCode); } - http.end(); + Serial.println("Benutzer mit UID " + uid + + " nicht in lokaler Datenbank gefunden"); return userData; } -// Fügt einen neuen Benutzer mit den angegebenen Daten in die Datenbank ein. -bool enterUserData(const String &uid, const String &firstname, - const String &lastname, const String &geburtsdatum, - int alter) { - if (!backendOnline()) { - Serial.println("No internet connection, cannot enter user data."); - return false; - } - - HTTPClient http; - http.begin(String(BACKEND_SERVER) + "/v1/private/create-player"); - http.addHeader("Content-Type", "application/json"); - http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); - - // Create JSON payload - StaticJsonDocument<256> requestDoc; - requestDoc["rfiduid"] = uid; - requestDoc["firstname"] = firstname; - requestDoc["lastname"] = lastname; - requestDoc["birthdate"] = geburtsdatum; - - String requestBody; - serializeJson(requestDoc, requestBody); - - int httpCode = http.POST(requestBody); - - if (httpCode == HTTP_CODE_CREATED) { - Serial.println("User data entered successfully."); - http.end(); - return true; - } else { - Serial.printf("Failed to enter user data, HTTP code: %d\n", httpCode); - http.end(); - return false; - } -} - // Holt alle Standorte aus der Datenbank und gibt sie als JSON-Dokument zurück. JsonDocument getAllLocations() { JsonDocument locations; // Allocate memory for the JSON document @@ -136,7 +98,7 @@ JsonDocument getAllLocations() { HTTPClient http; http.begin(String(BACKEND_SERVER) + "/v1/private/locations"); - http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); + http.addHeader("Authorization", String("Bearer ") + licence); int httpCode = http.GET(); @@ -159,6 +121,50 @@ JsonDocument getAllLocations() { // (Kompatibilitätsfunktion). bool userExists(const String &uid) { return checkUser(uid).exists; } +// Fügt einen neuen Benutzer in die Datenbank ein +bool enterUserData(const String &uid, const String &name) { + String upperUid = uid; + upperUid.toUpperCase(); // UID in Großbuchstaben konvertieren + + // Prüfen ob Benutzer bereits existiert + for (const auto &user : localUsers) { + String userUpperUid = user.uid; + userUpperUid.toUpperCase(); + if (userUpperUid == upperUid) { + Serial.println("Benutzer mit UID " + upperUid + " existiert bereits!"); + return false; + } + } + + // Neuen Benutzer erstellen + LocalUser newUser; + newUser.uid = upperUid; // UID in Großbuchstaben speichern + newUser.name = name; + newUser.timestamp = millis(); + + // Benutzer zum lokalen Array hinzufügen + localUsers.push_back(newUser); + + Serial.println("Benutzer lokal gespeichert:"); + Serial.println("UID: " + upperUid); + Serial.println("Name: " + name); + Serial.println("Gespeicherte Benutzer: " + String(localUsers.size())); + + return true; +} + +// Gibt alle lokalen Benutzer zurück (für Debugging) +String getLocalUsersList() { + String result = "Lokale Benutzer (" + String(localUsers.size()) + "):\n"; + + for (const auto &user : localUsers) { + result += "- UID: " + user.uid + ", Name: " + user.name + + ", Erstellt: " + String(user.timestamp) + "\n"; + } + + return result; +} + // Richtet die HTTP-Routen für die Backend-API ein (z.B. Health-Check, User- und // Location-Abfragen). void setupBackendRoutes(AsyncWebServer &server) { @@ -172,14 +178,143 @@ void setupBackendRoutes(AsyncWebServer &server) { }); server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) { - if (!backendOnline()) { - request->send(503, "application/json", - "{\"error\":\"Database not connected\"}"); - return; + // Lokale Benutzer als JSON zurückgeben + DynamicJsonDocument doc(2048); + JsonArray usersArray = doc.createNestedArray("users"); + + for (const auto &user : localUsers) { + JsonObject userObj = usersArray.createNestedObject(); + userObj["uid"] = user.uid; + userObj["name"] = user.name; + userObj["timestamp"] = user.timestamp; } - // Handle user retrieval logic here + doc["count"] = localUsers.size(); + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); }); + + // Route zum Erstellen eines neuen Benutzers + server.on( + "/api/users/insert", HTTP_POST, + [](AsyncWebServerRequest *request) { + Serial.println("API: /api/users/insert aufgerufen"); + }, + NULL, + [](AsyncWebServerRequest *request, uint8_t *data, size_t len, + size_t index, size_t total) { + // Diese Funktion wird für den Body aufgerufen + static String bodyBuffer = ""; + + // Daten anhängen + for (size_t i = 0; i < len; i++) { + bodyBuffer += (char)data[i]; + } + + // Wenn alle Daten empfangen wurden + if (index + len == total) { + Serial.println("Request Body empfangen: '" + bodyBuffer + "'"); + + if (bodyBuffer.length() == 0) { + Serial.println("FEHLER: Request Body ist leer!"); + DynamicJsonDocument response(200); + response["success"] = false; + response["error"] = "Request Body ist leer"; + String jsonString; + serializeJson(response, jsonString); + request->send(400, "application/json", jsonString); + bodyBuffer = ""; + return; + } + + DynamicJsonDocument doc(512); + DeserializationError error = deserializeJson(doc, bodyBuffer); + + if (error) { + Serial.println("JSON Parse Error: " + String(error.c_str())); + DynamicJsonDocument response(200); + response["success"] = false; + response["error"] = "Invalid JSON: " + String(error.c_str()); + String jsonString; + serializeJson(response, jsonString); + request->send(400, "application/json", jsonString); + bodyBuffer = ""; + return; + } + + String uid = doc["uid"].as(); + String name = doc["name"].as(); + + Serial.println("Extrahierte UID: " + uid); + Serial.println("Extrahierter Name: " + name); + + if (uid.length() == 0 || name.length() == 0) { + DynamicJsonDocument response(200); + response["success"] = false; + response["error"] = "UID und Name sind erforderlich"; + String jsonString; + serializeJson(response, jsonString); + request->send(400, "application/json", jsonString); + bodyBuffer = ""; + return; + } + + // Prüfen ob Benutzer bereits existiert + bool userExists = false; + for (const auto &user : localUsers) { + if (user.uid == uid) { + userExists = true; + break; + } + } + + if (userExists) { + DynamicJsonDocument response(200); + response["success"] = false; + response["error"] = "Benutzer bereits vorhanden"; + String jsonString; + serializeJson(response, jsonString); + request->send(409, "application/json", jsonString); + bodyBuffer = ""; + return; + } + + // Neuen Benutzer direkt in das Array einfügen + LocalUser newUser; + newUser.uid = uid; + newUser.name = name; + newUser.timestamp = millis(); + + localUsers.push_back(newUser); + + Serial.println("Benutzer über API eingefügt:"); + Serial.println("UID: " + uid); + Serial.println("Name: " + name); + Serial.println("Gespeicherte Benutzer: " + String(localUsers.size())); + + DynamicJsonDocument response(200); + response["success"] = true; + response["message"] = "Benutzer erfolgreich erstellt"; + response["uid"] = uid; + response["name"] = name; + + String jsonString; + serializeJson(response, jsonString); + request->send(200, "application/json", jsonString); + + // Buffer zurücksetzen + bodyBuffer = ""; + } + }); + + // Debug-Route für lokale Benutzer + server.on("/api/debug/users", HTTP_GET, [](AsyncWebServerRequest *request) { + String userList = getLocalUsersList(); + request->send(200, "text/plain", userList); + }); + // Location routes /api/location/ server.on("/api/location/", HTTP_GET, [](AsyncWebServerRequest *request) { String result; @@ -218,5 +353,206 @@ void setupBackendRoutes(AsyncWebServer &server) { // Andere Logik wie in getBestLocs }); + // Lokales Leaderboard API (für Hauptseite - 6 Einträge) + server.on("/api/leaderboard", HTTP_GET, [](AsyncWebServerRequest *request) { + // Sortiere nach Zeit (beste zuerst) + std::sort(localTimes.begin(), localTimes.end(), + [](const LocalTime &a, const LocalTime &b) { + return a.timeMs < b.timeMs; + }); + + DynamicJsonDocument doc(2048); + JsonArray leaderboard = doc.createNestedArray("leaderboard"); + + // Nimm die besten 6 + int count = 0; + for (const auto &time : localTimes) { + if (count >= 6) + break; + + JsonObject entry = leaderboard.createNestedObject(); + entry["rank"] = count + 1; + entry["name"] = time.name; + entry["uid"] = time.uid; + entry["time"] = time.timeMs / 1000.0; + + // Format time inline + float seconds = time.timeMs / 1000.0; + int totalSeconds = (int)seconds; + int minutes = totalSeconds / 60; + int remainingSeconds = totalSeconds % 60; + int milliseconds = (int)((seconds - totalSeconds) * 100); + + String timeFormatted; + if (minutes > 0) { + timeFormatted = String(minutes) + ":" + + (remainingSeconds < 10 ? "0" : "") + + String(remainingSeconds) + "." + + (milliseconds < 10 ? "0" : "") + String(milliseconds); + } else { + timeFormatted = String(remainingSeconds) + "." + + (milliseconds < 10 ? "0" : "") + String(milliseconds); + } + entry["timeFormatted"] = timeFormatted; + + count++; + } + + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + + // Erweiterte Leaderboard API (für Leaderboard-Seite - 10 Einträge) + server.on( + "/api/leaderboard-full", HTTP_GET, [](AsyncWebServerRequest *request) { + // Sortiere nach Zeit (beste zuerst) + std::sort(localTimes.begin(), localTimes.end(), + [](const LocalTime &a, const LocalTime &b) { + return a.timeMs < b.timeMs; + }); + + DynamicJsonDocument doc(2048); + JsonArray leaderboard = doc.createNestedArray("leaderboard"); + + // Nimm die besten 10 + int count = 0; + for (const auto &time : localTimes) { + if (count >= 10) + break; + + JsonObject entry = leaderboard.createNestedObject(); + entry["rank"] = count + 1; + entry["name"] = time.name; + entry["uid"] = time.uid; + entry["time"] = time.timeMs / 1000.0; + + // Format time inline + float seconds = time.timeMs / 1000.0; + int totalSeconds = (int)seconds; + int minutes = totalSeconds / 60; + int remainingSeconds = totalSeconds % 60; + int milliseconds = (int)((seconds - totalSeconds) * 100); + + String timeFormatted; + if (minutes > 0) { + timeFormatted = + String(minutes) + ":" + (remainingSeconds < 10 ? "0" : "") + + String(remainingSeconds) + "." + + (milliseconds < 10 ? "0" : "") + String(milliseconds); + } else { + timeFormatted = String(remainingSeconds) + "." + + (milliseconds < 10 ? "0" : "") + + String(milliseconds); + } + + entry["timeFormatted"] = timeFormatted; + count++; + } + + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + // Add more routes as needed } + +// Hilfsfunktionen um UID und Status abzufragen (aus communication.h) +String getStart1UID(); +String getStart2UID(); +bool wasStart1FoundLocally(); +bool wasStart2FoundLocally(); + +// Funktion um Zeit an Online-API zu senden +void sendTimeToOnlineAPI(int lane, String uid, float timeInSeconds) { + // Nur senden wenn User online gefunden wurde + bool wasOnlineFound = + (lane == 1) ? !wasStart1FoundLocally() : !wasStart2FoundLocally(); + + if (!wasOnlineFound) { + Serial.println("Zeit nicht gesendet - User wurde lokal gefunden"); + return; + } + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("Keine Internetverbindung - Zeit nicht gesendet"); + return; + } + + Serial.println("Sende Zeit an Online-API für Lane " + String(lane)); + + HTTPClient http; + http.begin(String(BACKEND_SERVER) + "/api/v1/private/create-time"); + http.addHeader("Content-Type", "application/json"); + http.addHeader("Authorization", String("Bearer ") + licence); + + // Zeit in M:SS.mmm Format konvertieren (ohne führende Null bei Minuten) + int minutes = (int)(timeInSeconds / 60); + int seconds = (int)timeInSeconds % 60; + int milliseconds = (int)((timeInSeconds - (int)timeInSeconds) * 1000); + + String formattedTime = + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds) + + "." + (milliseconds < 10 ? "00" : (milliseconds < 100 ? "0" : "")) + + String(milliseconds); + + StaticJsonDocument<200> requestDoc; + requestDoc["rfiduid"] = uid; + requestDoc["location_name"] = + getLocationIdFromPrefs(); // Aus den Einstellungen + requestDoc["recorded_time"] = formattedTime; + + String requestBody; + serializeJson(requestDoc, requestBody); + + Serial.println("API Request Body: " + requestBody); + + int httpCode = http.POST(requestBody); + + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) { + String response = http.getString(); + Serial.println("Zeit erfolgreich gesendet: " + response); + } else { + Serial.printf("Fehler beim Senden der Zeit: HTTP %d\n", httpCode); + if (httpCode > 0) { + String response = http.getString(); + Serial.println("Response: " + response); + } + } + + http.end(); +} + +// Funktionen für lokales Leaderboard +void addLocalTime(String uid, String name, unsigned long timeMs) { + // Prüfe minimale Zeit für Leaderboard-Eintrag + if (timeMs < minTimeForLeaderboard) { + Serial.printf( + "Zeit zu kurz für Leaderboard: %s (%s) - %.2fs (Minimum: %.2fs)\n", + name.c_str(), uid.c_str(), timeMs / 1000.0, + minTimeForLeaderboard / 1000.0); + return; // Zeit wird nicht ins Leaderboard eingetragen + } + + LocalTime newTime; + newTime.uid = uid; + newTime.name = name; + newTime.timeMs = timeMs; + newTime.timestamp = millis(); + + localTimes.push_back(newTime); + + // Speichere das Leaderboard automatisch + saveBestTimes(); + + Serial.printf("Lokale Zeit hinzugefügt: %s (%s) - %.2fs\n", name.c_str(), + uid.c_str(), timeMs / 1000.0); +} + +// Leert das lokale Leaderboard +void clearLocalLeaderboard() { + localTimes.clear(); + saveBestTimes(); // Speichere das geleerte Leaderboard + Serial.println("Lokales Leaderboard geleert"); +} \ No newline at end of file diff --git a/src/gamemodes.h b/src/gamemodes.h index cbafae0..bcd523f 100644 --- a/src/gamemodes.h +++ b/src/gamemodes.h @@ -56,6 +56,25 @@ void IndividualMode(const char *action, int press, int lane, publishLaneStatus(1, "stopped"); Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) + "s"); + + // Speichere Zeit immer lokal + if (wasStart1FoundLocally() && getStart1UID().length() > 0) { + // Finde den Namen des lokalen Users + UserData userData = checkUser(getStart1UID()); + if (userData.exists) { + addLocalTime(getStart1UID(), userData.firstname, currentTime); + } else { + // User lokal gefunden aber keine Daten - speichere ohne Namen + addLocalTime(getStart1UID(), "Unbekannt", currentTime); + } + } else if (!wasStart1FoundLocally() && getStart1UID().length() > 0) { + // Sende Zeit an Online-API wenn User online gefunden wurde + sendTimeToOnlineAPI(1, getStart1UID(), currentTime / 1000.0); + } else { + // Kein User gefunden - speichere Zeit ohne UID und Namen + addLocalTime("", "Spieler " + String((localTimes.size() + 1)), + currentTime); + } } } if (action == "start" && press == 2 && lane == 2) { @@ -84,6 +103,25 @@ void IndividualMode(const char *action, int press, int lane, publishLaneStatus(2, "stopped"); Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) + "s"); + + // Speichere Zeit immer lokal + if (wasStart2FoundLocally() && getStart2UID().length() > 0) { + // Finde den Namen des lokalen Users + UserData userData = checkUser(getStart2UID()); + if (userData.exists) { + addLocalTime(getStart2UID(), userData.firstname, currentTime); + } else { + // User lokal gefunden aber keine Daten - speichere ohne Namen + addLocalTime(getStart2UID(), "Unbekannt", currentTime); + } + } else if (!wasStart2FoundLocally() && getStart2UID().length() > 0) { + // Sende Zeit an Online-API wenn User online gefunden wurde + sendTimeToOnlineAPI(2, getStart2UID(), currentTime / 1000.0); + } else { + // Kein User gefunden - speichere Zeit ohne UID und Namen + addLocalTime("", "Spieler " + String((localTimes.size() + 1)), + currentTime); + } } } @@ -330,4 +368,4 @@ String getTimerDataJSON() { String result; serializeJson(doc, result); return result; -} \ No newline at end of file +} diff --git a/src/master.cpp b/src/master.cpp index 4322851..4f8ff5f 100644 --- a/src/master.cpp +++ b/src/master.cpp @@ -18,18 +18,16 @@ #include #include #include +#include #include #include #include #include -#include const char *firmwareversion = "1.0.0"; // Version der Firmware // moved to preferencemanager.h - - void setup() { Serial.begin(115200); @@ -52,7 +50,6 @@ void setup() { loadWifiSettings(); loadLocationSettings(); - setupWifi(); // WiFi initialisieren setupOTA(&server); @@ -61,13 +58,24 @@ void setup() { setupLED(); setupMqttServer(); // MQTT Server initialisieren // setupBattery(); - // setupRFID(); + + setupRFID(); // RFID initialisieren (ganz einfach) } void loop() { checkAutoReset(); - loopMqttServer(); // MQTT Server in der Loop aufrufen + + // MQTT hat höchste Priorität (wird zuerst verarbeitet) + loopMqttServer(); + + // WebSocket verarbeiten loopWebSocket(); - // loopBattery(); // Batterie-Loop aufrufen - // loopRFID(); // RFID Loop aufrufen + + // RFID Loop nur wenn aktiv (spart CPU-Zyklen) + if (isRFIDReadingActive()) { + loopRFID(); + } + + // Kurze Pause um anderen Tasks Zeit zu geben + delay(1); } diff --git a/src/master.h b/src/master.h index 73fdb16..33dd487 100644 --- a/src/master.h +++ b/src/master.h @@ -4,6 +4,7 @@ #include #include #include +#include const char *ssidAP; const char *passwordAP = nullptr; @@ -21,6 +22,15 @@ struct TimerData1 { bool isRunning = false; bool isReady = true; // Status für Bahn 1 bool isArmed = false; // Status für Bahn 1 (armiert/nicht armiert) + char RFIDUID[32] = ""; +}; + +// Struktur für lokale Zeiten (Leaderboard) +struct LocalTime { + String uid; + String name; + unsigned long timeMs; + unsigned long timestamp; }; // Timer Struktur für Bahn 2 @@ -33,6 +43,7 @@ struct TimerData2 { bool isRunning = false; bool isReady = true; // Status für Bahn 2 bool isArmed = false; // Status für Bahn 2 (armiert/nicht armiert) + char RFIDUID[32] = ""; }; // Button Konfiguration @@ -61,11 +72,21 @@ bool learningMode = false; int learningStep = 0; // 0=Start1, 1=Stop1, 2=Start2, 3=Stop2 unsigned long maxTimeBeforeReset = 300000; // 5 Minuten default unsigned long maxTimeDisplay = 20000; // 20 Sekunden Standard (in ms) -bool wifimodeAP = false; // AP-Modus deaktiviert +unsigned long minTimeForLeaderboard = + 5000; // 5 Sekunden minimum für Leaderboard (in ms) +bool wifimodeAP = false; // AP-Modus deaktiviert String masterlocation; int gamemode; // 0=Individual, 1=Wettkampf bool startCompetition = false; // Flag, ob der Timer gestartet wurde +// Lane Configuration +int laneConfigType = 0; // 0=Identical, 1=Different +int lane1DifficultyType = 0; // 0=Light, 1=Heavy (difficulty) +int lane2DifficultyType = 0; // 0=Light, 1=Heavy (difficulty) + +// Lokales Leaderboard +std::vector localTimes; + // Function Declarations void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len); void handleLearningMode(const uint8_t *mac); @@ -78,6 +99,7 @@ void loadBestTimes(); void saveSettings(); void loadSettings(); void loadWifiSettings(); +void clearLocalLeaderboard(); void saveWifiSettings(); void loadLocationSettings(); void saveLocationSettings(); diff --git a/src/preferencemanager.h b/src/preferencemanager.h index f880f9a..90c2c89 100644 --- a/src/preferencemanager.h +++ b/src/preferencemanager.h @@ -2,8 +2,8 @@ #include #include -#include #include +#include // Persist and load button configuration void saveButtonConfig() { @@ -21,19 +21,60 @@ void loadButtonConfig() { preferences.end(); } -// Persist and load best times +// Persist and load local leaderboard void saveBestTimes() { - preferences.begin("times", false); - preferences.putULong("best1", timerData1.bestTime); - preferences.putULong("best2", timerData2.bestTime); + preferences.begin("leaderboard", false); + + // Speichere Anzahl der Einträge + preferences.putUInt("count", localTimes.size()); + + // Speichere jeden Eintrag (kurze Schlüssel für NVS) + for (size_t i = 0; i < localTimes.size(); i++) { + String key = "e" + String(i); // e0, e1, e2, etc. + preferences.putString((key + "u").c_str(), + localTimes[i].uid); // e0u, e1u, etc. + preferences.putString((key + "n").c_str(), + localTimes[i].name); // e0n, e1n, etc. + preferences.putULong((key + "t").c_str(), + localTimes[i].timeMs); // e0t, e1t, etc. + preferences.putULong((key + "s").c_str(), + localTimes[i].timestamp); // e0s, e1s, etc. + } + preferences.end(); + Serial.println("Lokales Leaderboard gespeichert: " + + String(localTimes.size()) + " Einträge"); } void loadBestTimes() { - preferences.begin("times", true); - timerData1.bestTime = preferences.getULong("best1", 0); - timerData2.bestTime = preferences.getULong("best2", 0); + preferences.begin("leaderboard", true); + + // Leere das aktuelle Leaderboard + localTimes.clear(); + + // Lade Anzahl der Einträge + uint32_t count = preferences.getUInt("count", 0); + + // Lade jeden Eintrag (kurze Schlüssel für NVS) + for (uint32_t i = 0; i < count; i++) { + LocalTime entry; + String key = "e" + String(i); // e0, e1, e2, etc. + + entry.uid = + preferences.getString((key + "u").c_str(), ""); // e0u, e1u, etc. + entry.name = + preferences.getString((key + "n").c_str(), ""); // e0n, e1n, etc. + entry.timeMs = + preferences.getULong((key + "t").c_str(), 0); // e0t, e1t, etc. + entry.timestamp = + preferences.getULong((key + "s").c_str(), 0); // e0s, e1s, etc. + + localTimes.push_back(entry); + } + preferences.end(); + Serial.println("Lokales Leaderboard geladen: " + String(localTimes.size()) + + " Einträge"); } // Persist and load general settings @@ -41,7 +82,11 @@ void saveSettings() { preferences.begin("settings", false); preferences.putULong("maxTime", maxTimeBeforeReset); preferences.putULong("maxTimeDisplay", maxTimeDisplay); + preferences.putULong("minTime", minTimeForLeaderboard); preferences.putUInt("gamemode", gamemode); + preferences.putUInt("laneConfigType", laneConfigType); + preferences.putUInt("lane1Diff", lane1DifficultyType); + preferences.putUInt("lane2Diff", lane2DifficultyType); preferences.end(); } @@ -49,7 +94,11 @@ void loadSettings() { preferences.begin("settings", true); maxTimeBeforeReset = preferences.getULong("maxTime", 300000); maxTimeDisplay = preferences.getULong("maxTimeDisplay", 20000); + minTimeForLeaderboard = preferences.getULong("minTime", 5000); gamemode = preferences.getUInt("gamemode", 0); + laneConfigType = preferences.getUInt("laneConfigType", 0); + lane1DifficultyType = preferences.getUInt("lane1Diff", 0); + lane2DifficultyType = preferences.getUInt("lane2Diff", 0); preferences.end(); } diff --git a/src/rfid.h b/src/rfid.h index 84c1c18..9887776 100644 --- a/src/rfid.h +++ b/src/rfid.h @@ -1,187 +1,150 @@ #pragma once +#include #include #include -#include -#include +#include -// RFID Konfiguration -#define RST_PIN 21 // Configurable, see typical pin layout above -#define SS_PIN 5 // Configurable, see typical pin layout above +// RFID Konfiguration - KORREKTE ESP32 Thing Plus Pins +#define SDA_PIN 23 // ESP32 Thing Plus SDA +#define SCL_PIN 22 // ESP32 Thing Plus SCL +#define IRQ_PIN 14 +#define RST_PIN 15 -MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance -std::map - blockedUIDs; // Map to store blocked UIDs and their timestamps -const unsigned long BLOCK_DURATION = 10 * 1000; // 10 Seconds in milliseconds +// PN532 RFID Reader (mit IRQ und Reset-Pin) +Adafruit_PN532 nfc(IRQ_PIN, RST_PIN); -// Neue Variablen für API-basiertes Lesen -bool rfidReadRequested = false; +// RFID Variablen +bool rfidInitialized = false; +bool readingMode = false; String lastReadUID = ""; -bool rfidReadSuccess = false; -unsigned long rfidReadStartTime = 0; -const unsigned long RFID_READ_TIMEOUT = - 10000; // 10 Sekunden Timeout für API Requests +unsigned long lastReadTime = 0; -// Initialisiert den RFID-Reader und das SPI-Interface. +// Hilfsfunktion um Reading-Mode zu prüfen +bool isRFIDReadingActive() { return readingMode; } + +// Initialisiert den RFID-Reader void setupRFID() { + // I2C starten mit korrekten Pins + Wire.begin(SDA_PIN, SCL_PIN, 100000); + delay(100); - // SPI und RFID initialisieren - SPI.begin(); // Init SPI bus - mfrc522.PCD_Init(); // Init MFRC522 - delay(4); // Optional delay. Some boards need more time after init to be ready - mfrc522.PCD_DumpVersionToSerial(); // Show details of PCD - MFRC522 Card - // Reader details + // PN532 initialisieren + if (!nfc.begin()) { + Serial.println("RFID: PN532 nicht gefunden!"); + return; + } + + // Firmware prüfen + uint32_t versiondata = nfc.getFirmwareVersion(); + if (!versiondata) { + Serial.println("RFID: Firmware nicht lesbar!"); + return; + } + + // SAM Config + nfc.SAMConfig(); + + rfidInitialized = true; + Serial.println("RFID: Setup erfolgreich!"); } -// Liest automatisch eine RFID-Karte ein und blockiert die UID für eine -// bestimmte Zeit. -void handleAutomaticRFID() { - if (!mfrc522.PICC_IsNewCardPresent()) { - return; +// Prüft ob RFID funktioniert +bool checkRFID() { + if (!rfidInitialized) { + return false; + } + uint32_t versiondata = nfc.getFirmwareVersion(); + return (versiondata != 0); +} + +// Liest RFID-Karte - NICHT BLOCKIEREND +String readRFIDCard() { + if (!checkRFID()) { + return ""; } - // Select one of the cards - if (!mfrc522.PICC_ReadCardSerial()) { - return; + uint8_t uid[] = {0, 0, 0, 0, 0, 0, 0}; + uint8_t uidLength; + + // Nicht-blockierender Aufruf mit sehr kurzer Timeout + uint8_t success = + nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, + 50); // 50ms Timeout statt Standard 100ms + + if (!success) { + return ""; // Keine Karte oder Timeout } - // Read the UID - String uid = ""; - for (byte i = 0; i < mfrc522.uid.size; i++) { + // UID zu String + String uidString = ""; + for (uint8_t i = 0; i < uidLength; i++) { if (i > 0) - uid += ":"; - if (mfrc522.uid.uidByte[i] < 0x10) - uid += "0"; - uid += String(mfrc522.uid.uidByte[i], HEX); + uidString += ":"; + if (uid[i] < 0x10) + uidString += "0"; + uidString += String(uid[i], HEX); + } + uidString.toUpperCase(); + + Serial.println("RFID: " + uidString); + return uidString; +} + +// RFID Loop - kontinuierliches Lesen wenn aktiviert (MQTT-optimiert) +void loopRFID() { + if (!readingMode) { + return; // Lesen nicht aktiviert } - // Check if the UID is blocked - unsigned long currentTime = millis(); - if (blockedUIDs.find(uid) != blockedUIDs.end()) { - if (currentTime - blockedUIDs[uid] < BLOCK_DURATION) { - Serial.print(F("UID blocked for 10 seconds. Remaining time: ")); - Serial.print((BLOCK_DURATION - (currentTime - blockedUIDs[uid])) / 1000); - Serial.println(F(" seconds.")); - Serial.println(uid); - return; - } else { - // Remove the UID from the blocked list if the block duration has passed - blockedUIDs.erase(uid); + static unsigned long lastCheck = 0; + + // Nur alle 300ms prüfen (weniger belastend für MQTT) + if (millis() - lastCheck < 300) { + return; + } + lastCheck = millis(); + + // Versuchen zu lesen (mit kurzer Timeout) + String uid = readRFIDCard(); + if (uid.length() > 0) { + // Nur neue UIDs oder nach 2 Sekunden Pause + if (uid != lastReadUID || millis() - lastReadTime > 2000) { + lastReadUID = uid; + lastReadTime = millis(); + Serial.println("RFID gelesen: " + uid); } } - - // Process the UID - Serial.print(F("UID: ")); - Serial.println(uid); - - // Block the UID for 10 seconds - blockedUIDs[uid] = currentTime; - // show the remaining time for the block - Serial.print(F("UID blocked for 10 seconds. Remaining time: ")); - Serial.print((BLOCK_DURATION - (currentTime - blockedUIDs[uid])) / 1000); - Serial.println(F(" seconds.")); - - // Halt the card - mfrc522.PICC_HaltA(); } -// Neue Funktion für API-basiertes RFID Lesen - -// Liest eine RFID-Karte im API-Modus ein (für Web-Requests). -void handleAPIRFIDRead() { - unsigned long currentTime = millis(); - - // Timeout prüfen - if (currentTime - rfidReadStartTime > RFID_READ_TIMEOUT) { - Serial.println("RFID API Timeout - keine Karte erkannt"); - rfidReadRequested = false; - rfidReadSuccess = false; - lastReadUID = ""; - return; - } - - // Prüfen ob neue Karte vorhanden ist - if (!mfrc522.PICC_IsNewCardPresent()) { - return; - } - - // Karte auswählen - if (!mfrc522.PICC_ReadCardSerial()) { - return; - } - - // UID für API lesen (ohne Doppelpunkt-Trenner, Großbuchstaben) - String uid = ""; - for (byte i = 0; i < mfrc522.uid.size; i++) { - if (mfrc522.uid.uidByte[i] < 0x10) { - uid += "0"; // Leading Zero für einstellige Hex-Werte - } - uid += String(mfrc522.uid.uidByte[i], HEX); - } - - // UID in Großbuchstaben konvertieren - uid.toUpperCase(); - - Serial.println("RFID API UID gelesen: " + uid); - - // Ergebnis speichern - lastReadUID = uid; - rfidReadSuccess = true; - rfidReadRequested = false; - - // Karte "halt" setzen - mfrc522.PICC_HaltA(); - mfrc522.PCD_StopCrypto1(); -} - -// API Funktion: RFID Lesevorgang starten - -// Startet einen neuen RFID-Lesevorgang über die API. -void startRFIDRead() { - Serial.println("RFID API Lesevorgang gestartet..."); - rfidReadRequested = true; - rfidReadSuccess = false; - lastReadUID = ""; - rfidReadStartTime = millis(); -} - -// API Funktion: Prüfen ob Lesevorgang abgeschlossen - -// Prüft, ob der aktuelle RFID-Lesevorgang abgeschlossen ist. -bool isRFIDReadComplete() { return !rfidReadRequested; } - -// API Funktion: Ergebnis des Lesevorgangs abrufen -// Gibt das Ergebnis des letzten RFID-Lesevorgangs zurück. -String getRFIDReadResult(bool &success) { - success = rfidReadSuccess; - return lastReadUID; -} - -// Richtet die HTTP-API-Routen für RFID-Operationen ein. +// API Routes void setupRFIDRoute(AsyncWebServer &server) { - server.on("/api/rfid/read", HTTP_GET, [](AsyncWebServerRequest *request) { - Serial.println("api/rfid/read"); - - // Start RFID-Lesevorgang - startRFIDRead(); - unsigned long startTime = millis(); - - // Warten, bis eine UID gelesen wird oder Timeout eintritt - while (!isRFIDReadComplete()) { - if (millis() - startTime > RFID_READ_TIMEOUT) { - break; - } - delay(10); // Kurze Pause, um die CPU nicht zu blockieren - } + // Toggle RFID Reading Mode + server.on("/api/rfid/toggle", HTTP_POST, [](AsyncWebServerRequest *request) { + readingMode = !readingMode; DynamicJsonDocument response(200); + response["success"] = true; + response["reading_mode"] = readingMode; + response["message"] = + readingMode ? "RFID Lesen gestartet" : "RFID Lesen gestoppt"; - if (rfidReadSuccess && lastReadUID.length() > 0) { + String jsonString; + serializeJson(response, jsonString); + request->send(200, "application/json", jsonString); + }); + + // Einzelnes Lesen (wie vorher) + server.on("/api/rfid/read", HTTP_GET, [](AsyncWebServerRequest *request) { + String uid = readRFIDCard(); + + DynamicJsonDocument response(200); + if (uid.length() > 0) { response["success"] = true; - response["uid"] = lastReadUID; - response["message"] = "UID erfolgreich gelesen"; + response["uid"] = uid; + response["message"] = "Karte gelesen"; } else { response["success"] = false; - response["error"] = "Keine RFID Karte erkannt oder Timeout"; + response["error"] = "Keine Karte gefunden"; response["uid"] = ""; } @@ -190,107 +153,32 @@ void setupRFIDRoute(AsyncWebServer &server) { request->send(200, "application/json", jsonString); }); - server.on( - "/api/users/insert", HTTP_POST, [](AsyncWebServerRequest *request) {}, - NULL, - [](AsyncWebServerRequest *request, uint8_t *data, size_t len, - size_t index, size_t total) { - Serial.println("/api/users/insert"); + // Status und letzte gelesene UID + server.on("/api/rfid/status", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument response(300); + response["success"] = true; + response["rfid_initialized"] = rfidInitialized; + response["reading_mode"] = readingMode; + response["last_uid"] = lastReadUID; + response["message"] = + readingMode ? "RFID Lesen aktiv" : "RFID Lesen inaktiv"; - // Parse the incoming JSON payload - DynamicJsonDocument doc(512); - DeserializationError error = deserializeJson(doc, data, len); + String jsonString; + serializeJson(response, jsonString); + request->send(200, "application/json", jsonString); + }); - DynamicJsonDocument response(200); + // UID zurücksetzen (nach erfolgreichem Lesen) + server.on("/api/rfid/clear", HTTP_POST, [](AsyncWebServerRequest *request) { + lastReadUID = ""; // UID zurücksetzen + lastReadTime = 0; // Zeit auch zurücksetzen - if (error) { - Serial.println("Fehler beim Parsen der JSON-Daten"); - response["success"] = false; - response["error"] = "Ungültige JSON-Daten"; - } else { - // Extract user data from the JSON payload - String uid = doc["uid"] | ""; - String vorname = doc["vorname"] | ""; - String nachname = doc["nachname"] | ""; - String geburtsdatum = doc["geburtsdatum"] | ""; - int alter = doc["alter"] | 0; + DynamicJsonDocument response(200); + response["success"] = true; + response["message"] = "UID zurückgesetzt"; - // Validate the data - if (uid.isEmpty() || vorname.isEmpty() || nachname.isEmpty() || - geburtsdatum.isEmpty() || alter <= 0) { - Serial.println("Ungültige Eingabedaten"); - response["success"] = false; - response["error"] = "Ungültige Eingabedaten"; - } else { - // Process the data using the enterUserData function - Serial.println("Benutzerdaten empfangen:"); - Serial.println("UID: " + uid); - Serial.println("Vorname: " + vorname); - Serial.println("Nachname: " + nachname); - Serial.println("Alter: " + String(alter)); - - bool dbSuccess = - enterUserData(uid, vorname, nachname, geburtsdatum, alter); - - if (dbSuccess) { - response["success"] = true; - response["message"] = "Benutzer erfolgreich gespeichert"; - } else { - response["success"] = false; - response["error"] = "Fehler beim Speichern in der Datenbank"; - } - } - } - - // Send the response back to the client - String jsonString; - serializeJson(response, jsonString); - request->send(200, "application/json", jsonString); - }); -} - -// API Funktion: RFID Reader Status prüfen - -// Prüft, ob der RFID-Reader korrekt funktioniert und gibt den Status zurück. -bool checkRFIDReaderStatus() { - byte version = mfrc522.PCD_ReadRegister(mfrc522.VersionReg); - - // Bekannte MFRC522 Versionen: 0x91, 0x92 - if (version == 0x91 || version == 0x92) { - Serial.println("RFID Reader OK (Version: 0x" + String(version, HEX) + ")"); - return true; - } else { - Serial.println("RFID Reader Fehler (Version: 0x" + String(version, HEX) + - ")"); - return false; - } -} - -// Hilfsfunktion: Blockierte UIDs aufräumen - -// Entfernt UIDs aus der Blockliste, deren Blockdauer abgelaufen ist. -void cleanupBlockedUIDs() { - unsigned long currentTime = millis(); - - // Iterator für sicheres Löschen während der Iteration - for (auto it = blockedUIDs.begin(); it != blockedUIDs.end();) { - if (currentTime - it->second >= BLOCK_DURATION) { - it = blockedUIDs.erase(it); - } else { - ++it; - } - } -} - -// Hauptschleife für das RFID-Handling (automatisch und API-basiert). -void loopRFID() { - // Originale Funktionalität für automatisches Lesen - if (!rfidReadRequested) { - handleAutomaticRFID(); - } - - // API-basiertes Lesen verarbeiten - if (rfidReadRequested) { - handleAPIRFIDRead(); - } -} + String jsonString; + serializeJson(response, jsonString); + request->send(200, "application/json", jsonString); + }); +} \ No newline at end of file diff --git a/src/timesync.h b/src/timesync.h index a6fc2b8..8656b89 100644 --- a/src/timesync.h +++ b/src/timesync.h @@ -1,15 +1,11 @@ // Zeit-bezogene Variablen und Includes #pragma once -#include "RTClib.h" #include #include #include -#include #include #include -RTC_PCF8523 rtc; - // Globale Zeitvariablen struct timeval tv; struct timezone tz; @@ -90,8 +86,6 @@ bool setSystemTime(long timestamp) { // Initialisiert die Zeit-API und richtet die HTTP-Endpunkte ein. void setupTimeAPI(AsyncWebServer &server) { - // setupRTC(); - // API-Endpunkt: Aktuelle Zeit abrufen server.on("/api/time", HTTP_GET, [](AsyncWebServerRequest *request) { String response = getCurrentTimeJSON(); diff --git a/src/webserverrouter.h b/src/webserverrouter.h index 8a584a0..485fa92 100644 --- a/src/webserverrouter.h +++ b/src/webserverrouter.h @@ -33,8 +33,8 @@ void setupRoutes() { request->send(SPIFFS, "/settings.html", "text/html"); }); - server.on("/rfid", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send(SPIFFS, "/rfid.html", "text/html"); + server.on("/leaderboard", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(SPIFFS, "/leaderboard.html", "text/html"); }); server.on("/firmware.bin", HTTP_GET, [](AsyncWebServerRequest *request) { @@ -56,6 +56,7 @@ void setupRoutes() { timerData1.bestTime = 0; timerData2.bestTime = 0; saveBestTimes(); + clearLocalLeaderboard(); // Leere auch das lokale Leaderboard DynamicJsonDocument doc(64); doc["success"] = true; String result; @@ -83,6 +84,12 @@ void setupRoutes() { request->getParam("maxTimeDisplay", true)->value().toInt() * 1000; changed = true; } + if (request->hasParam("minTimeForLeaderboard", true)) { + minTimeForLeaderboard = + request->getParam("minTimeForLeaderboard", true)->value().toInt() * + 1000; + changed = true; + } if (changed) { saveSettings(); DynamicJsonDocument doc(32); @@ -100,6 +107,7 @@ void setupRoutes() { DynamicJsonDocument doc(256); doc["maxTime"] = maxTimeBeforeReset / 1000; doc["maxTimeDisplay"] = maxTimeDisplay / 1000; + doc["minTimeForLeaderboard"] = minTimeForLeaderboard / 1000; String result; serializeJson(doc, result); request->send(200, "application/json", result); @@ -315,6 +323,71 @@ void setupRoutes() { request->send(200, "application/json", result); }); + // Lane Configuration API Routes + server.on( + "/api/set-lane-config", HTTP_POST, [](AsyncWebServerRequest *request) {}, + NULL, + [](AsyncWebServerRequest *request, uint8_t *data, size_t len, + size_t index, size_t total) { + Serial.println("/api/set-lane-config called"); + + DynamicJsonDocument doc(256); + DeserializationError error = deserializeJson(doc, data, len); + + if (error) { + Serial.println("JSON parsing error"); + request->send(400, "application/json", + "{\"success\":false,\"error\":\"Invalid JSON\"}"); + return; + } + + if (doc.containsKey("type")) { + String laneType = doc["type"]; + laneConfigType = (laneType == "identical") ? 0 : 1; + + if (laneConfigType == 1 && doc.containsKey("lane1Difficulty") && + doc.containsKey("lane2Difficulty")) { + String lane1Difficulty = doc["lane1Difficulty"]; + String lane2Difficulty = doc["lane2Difficulty"]; + lane1DifficultyType = (lane1Difficulty == "light") ? 0 : 1; + lane2DifficultyType = (lane2Difficulty == "light") ? 0 : 1; + } + + Serial.printf( + "Lane configuration set - Type: %s, Lane1: %s, Lane2: %s\n", + laneType.c_str(), + (laneConfigType == 1) + ? ((lane1DifficultyType == 0) ? "light" : "heavy") + : "identical", + (laneConfigType == 1) + ? ((lane2DifficultyType == 0) ? "light" : "heavy") + : "identical"); + + DynamicJsonDocument response(64); + response["success"] = true; + String result; + serializeJson(response, result); + request->send(200, "application/json", result); + saveSettings(); + } else { + request->send(400, "application/json", + "{\"success\":false,\"error\":\"Lane type missing\"}"); + } + }); + + server.on( + "/api/get-lane-config", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument doc(128); + doc["type"] = laneConfigType == 0 ? "identical" : "different"; + if (laneConfigType == 1) { + doc["lane1Difficulty"] = lane1DifficultyType == 0 ? "light" : "heavy"; + doc["lane2Difficulty"] = lane2DifficultyType == 0 ? "light" : "heavy"; + } + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + }); + // Statische Dateien server.serveStatic("/", SPIFFS, "/"); server.begin();