diff --git a/mock-server/.gitignore b/mock-server/.gitignore
new file mode 100644
index 0000000..3f00ede
--- /dev/null
+++ b/mock-server/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+*.log
+.DS_Store
+.env
diff --git a/mock-server/README.md b/mock-server/README.md
new file mode 100644
index 0000000..1ad2d98
--- /dev/null
+++ b/mock-server/README.md
@@ -0,0 +1,179 @@
+# AquaMaster Mock Server
+
+Mock ESP32 Server und MQTT-Broker für lokales Testing ohne Hardware.
+
+## Übersicht
+
+Dieses Projekt simuliert:
+- **MQTT-Broker** (Port 1883 TCP, Port 9001 WebSocket) - Lokaler MQTT-Broker für Kommunikation
+- **Mock ESP32 Server** (Port 80) - Simuliert alle ESP32 API-Endpunkte und WebSocket
+- **Web Debug UI** - Browser-basierte Oberfläche zum Testen von API und MQTT
+
+## Voraussetzungen
+
+- Node.js 16+ (LTS empfohlen)
+- npm
+
+## Installation
+
+```bash
+cd mock-server
+npm install
+```
+
+## Verwendung
+
+### Option 1: Beide Server zusammen starten
+
+```bash
+npm start
+# oder
+node start_all.js
+```
+
+### Option 2: Server einzeln starten
+
+**Terminal 1 - MQTT Broker:**
+```bash
+npm run mqtt
+# oder
+node mqtt_broker.js
+```
+
+**Terminal 2 - Mock ESP32 Server:**
+```bash
+npm run server
+# oder
+node mock_esp32_server.js
+```
+
+### Web Debug UI öffnen
+
+Nach dem Start der Server:
+1. Öffne einen Browser
+2. Navigiere zu: `http://localhost:80`
+3. Die Debug-Oberfläche sollte sichtbar sein
+
+## Features
+
+### MQTT Broker
+- Läuft auf Port 1883 (TCP) und Port 9001 (WebSocket)
+- Unterstützt alle relevanten Topics:
+ - `aquacross/button/#`
+ - `aquacross/button/rfid/#`
+ - `aquacross/battery/#`
+ - `heartbeat/alive/#`
+ - `aquacross/competition/#`
+ - `sync/time`
+ - `aquacross/lanes/#`
+- Loggt alle Nachrichten für Debugging
+
+### Mock ESP32 Server
+- Simuliert alle API-Endpunkte aus der ESP32-Firmware
+- WebSocket-Support für Live-Updates
+- MQTT-Client, der sich mit dem Broker verbindet
+- Timer-Logik (Individual/Wettkampf-Modi)
+- Button-Konfigurationen und Learning-Mode
+
+### Web Debug UI
+- **API Testing Tab**: Teste alle API-Endpunkte
+- **MQTT Testing Tab**: Publish/Subscribe MQTT-Nachrichten
+- **Debug Endpoints Tab**: Direkte Timer-Kontrolle
+
+## API-Endpunkte
+
+Alle Endpunkte sind unter `http://localhost:80/api/...` verfügbar:
+
+- `GET /api/data` - Timer-Daten abrufen
+- `POST /api/reset-best` - Beste Zeiten zurücksetzen
+- `POST /api/unlearn-button` - Button-Zuordnungen löschen
+- `GET /api/debug/start1` - Lane 1 starten (Debug)
+- `GET /api/debug/stop1` - Lane 1 stoppen (Debug)
+- `GET /api/debug/start2` - Lane 2 starten (Debug)
+- `GET /api/debug/stop2` - Lane 2 stoppen (Debug)
+- ... und viele mehr (siehe `../API.md`)
+
+## MQTT Topics
+
+### Button Topics
+- `aquacross/button/{MAC}` - Button-Press Nachrichten
+ ```json
+ {"type": 2, "timestamp": 1234567890}
+ ```
+ - `type: 2` = Start-Button
+ - `type: 1` = Stop-Button
+
+### RFID Topics
+- `aquacross/button/rfid/{MAC}` - RFID-Read Nachrichten
+ ```json
+ {"uid": "TEST123456"}
+ ```
+
+### Battery Topics
+- `aquacross/battery/{MAC}` - Batteriestand
+ ```json
+ {"voltage": 3600}
+ ```
+
+### Heartbeat Topics
+- `heartbeat/alive/{MAC}` - Heartbeat-Nachrichten
+ ```json
+ {"timestamp": 1234567890}
+ ```
+
+### Competition Topics
+- `aquacross/competition/toMaster` - Wettkampf-Start
+ ```
+ "start"
+ ```
+
+### Time Sync
+- `sync/time` - Zeit-Synchronisation (vom Server alle 5 Sekunden)
+
+## Troubleshooting
+
+### Port bereits belegt
+Falls Port 80 oder 1883 bereits belegt sind:
+- Windows: Port 80 benötigt Admin-Rechte, verwende einen anderen Port
+- Linux/Mac: Port 80 benötigt sudo, verwende einen anderen Port
+
+Um Ports zu ändern, editiere:
+- `mqtt_broker.js` - Zeile mit `const port = 1883;`
+- `mock_esp32_server.js` - Zeile mit `const PORT = 80;`
+
+### MQTT-Verbindung fehlgeschlagen
+- Stelle sicher, dass der MQTT-Broker läuft
+- Prüfe, ob Port 1883 (TCP) oder 9001 (WebSocket) erreichbar ist
+- Browser benötigen WebSocket-Verbindung (Port 9001)
+
+### WebSocket-Verbindung fehlgeschlagen
+- Stelle sicher, dass der Mock ESP32 Server läuft
+- Prüfe Browser-Konsole auf Fehler
+- Socket.io sollte automatisch geladen werden
+
+## Projektstruktur
+
+```
+mock-server/
+├── package.json # Node.js Dependencies
+├── README.md # Diese Datei
+├── .gitignore # Git ignore
+├── mqtt_broker.js # MQTT-Broker
+├── mock_esp32_server.js # Mock ESP32 Server
+├── start_all.js # Startet beide Server
+└── debug_server/
+ ├── index.html # Web Debug UI
+ ├── debug.js # JavaScript-Logik
+ └── debug.css # Styling
+```
+
+## Hinweise
+
+- Der Mock-Server speichert keinen persistenten State (alles im Speicher)
+- Nach Neustart sind alle Einstellungen zurückgesetzt
+- Für Produktionstests sollte der echte ESP32 verwendet werden
+- Dieser Mock-Server ist nur für Entwicklung und Testing gedacht
+
+## Lizenz
+
+MIT
diff --git a/mock-server/debug_server/debug.css b/mock-server/debug_server/debug.css
new file mode 100644
index 0000000..4615ce5
--- /dev/null
+++ b/mock-server/debug_server/debug.css
@@ -0,0 +1,273 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background: #f5f5f5;
+ color: #333;
+ line-height: 1.6;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ background: #2c3e50;
+ color: white;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+header h1 {
+ margin-bottom: 15px;
+}
+
+.status-bar {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+.status-indicator {
+ padding: 5px 10px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+ font-size: 0.9em;
+}
+
+.status-indicator.connected {
+ background: #27ae60;
+}
+
+.status-indicator.disconnected {
+ background: #e74c3c;
+}
+
+.tabs {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ border-bottom: 2px solid #ddd;
+}
+
+.tab-button {
+ padding: 12px 24px;
+ background: transparent;
+ border: none;
+ border-bottom: 3px solid transparent;
+ cursor: pointer;
+ font-size: 16px;
+ color: #666;
+ transition: all 0.3s;
+}
+
+.tab-button:hover {
+ color: #2c3e50;
+ background: #f0f0f0;
+}
+
+.tab-button.active {
+ color: #2c3e50;
+ border-bottom-color: #3498db;
+ font-weight: bold;
+}
+
+.tab-content {
+ display: none;
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.tab-content.active {
+ display: block;
+}
+
+.section {
+ margin-bottom: 30px;
+}
+
+.section h2 {
+ margin-bottom: 15px;
+ color: #2c3e50;
+ border-bottom: 2px solid #ecf0f1;
+ padding-bottom: 10px;
+}
+
+.section h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+ color: #34495e;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: 500;
+ color: #555;
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ font-family: inherit;
+}
+
+.form-group textarea {
+ resize: vertical;
+ font-family: 'Courier New', monospace;
+}
+
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s;
+ margin-right: 10px;
+ margin-bottom: 10px;
+}
+
+.btn-primary {
+ background: #3498db;
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #2980b9;
+}
+
+.btn-secondary {
+ background: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #7f8c8d;
+}
+
+.btn-small {
+ padding: 5px 10px;
+ font-size: 12px;
+}
+
+.button-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 15px;
+}
+
+.response-section {
+ margin-top: 20px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 4px;
+}
+
+.response-section pre {
+ background: #2c3e50;
+ color: #ecf0f1;
+ padding: 15px;
+ border-radius: 4px;
+ overflow-x: auto;
+ font-size: 13px;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.quick-actions {
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #ddd;
+}
+
+.messages-section {
+ margin-top: 30px;
+}
+
+.messages-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.messages-container {
+ background: #2c3e50;
+ color: #ecf0f1;
+ padding: 15px;
+ border-radius: 4px;
+ max-height: 500px;
+ overflow-y: auto;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+}
+
+.message-item {
+ padding: 8px;
+ margin-bottom: 8px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ border-left: 3px solid #3498db;
+}
+
+.message-item .timestamp {
+ color: #95a5a6;
+ font-size: 11px;
+ margin-bottom: 4px;
+}
+
+.message-item .topic {
+ color: #3498db;
+ font-weight: bold;
+ margin-bottom: 4px;
+}
+
+.message-item .payload {
+ color: #ecf0f1;
+ word-break: break-all;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 10px;
+ }
+
+ .tabs {
+ flex-wrap: wrap;
+ }
+
+ .tab-button {
+ flex: 1;
+ min-width: 100px;
+ }
+
+ .button-group {
+ flex-direction: column;
+ }
+
+ .btn {
+ width: 100%;
+ margin-right: 0;
+ }
+}
diff --git a/mock-server/debug_server/debug.js b/mock-server/debug_server/debug.js
new file mode 100644
index 0000000..4f2e80e
--- /dev/null
+++ b/mock-server/debug_server/debug.js
@@ -0,0 +1,378 @@
+// Configuration
+const API_BASE_URL = 'http://localhost:80';
+const MQTT_BROKER_URL = 'ws://localhost:9001/mqtt'; // WebSocket port for MQTT
+const WS_URL = 'http://localhost:80';
+
+// State
+let mqttClient = null;
+let wsClient = null;
+let subscribedTopics = new Set();
+
+// Initialize
+document.addEventListener('DOMContentLoaded', init);
+
+function init() {
+ setupTabs();
+ setupAPI();
+ setupMQTT();
+ setupWebSocket();
+ setupDebug();
+ setupQuickActions();
+}
+
+// Tab Management
+function setupTabs() {
+ const tabButtons = document.querySelectorAll('.tab-button');
+ const tabContents = document.querySelectorAll('.tab-content');
+
+ tabButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const tabName = button.dataset.tab;
+
+ // Remove active class from all
+ tabButtons.forEach(btn => btn.classList.remove('active'));
+ tabContents.forEach(content => content.classList.remove('active'));
+
+ // Add active class to selected
+ button.classList.add('active');
+ document.getElementById(`${tabName}-tab`).classList.add('active');
+ });
+ });
+}
+
+// API Setup
+function setupAPI() {
+ const endpointSelect = document.getElementById('api-endpoint');
+ const paramsTextarea = document.getElementById('api-params');
+ const sendBtn = document.getElementById('api-send-btn');
+ const responsePre = document.getElementById('api-response');
+
+ sendBtn.addEventListener('click', async () => {
+ const endpoint = endpointSelect.value;
+ const [method, path] = endpoint.split(' ');
+ const params = paramsTextarea.value.trim();
+
+ try {
+ let options = {
+ method: method,
+ headers: {}
+ };
+
+ if (method === 'POST' && params) {
+ // Try to parse as JSON, otherwise use as form data
+ try {
+ const jsonData = JSON.parse(params);
+ options.headers['Content-Type'] = 'application/json';
+ options.body = JSON.stringify(jsonData);
+ } catch {
+ // Not JSON, use form data
+ const formData = new URLSearchParams();
+ const pairs = params.split('&');
+ pairs.forEach(pair => {
+ const [key, value] = pair.split('=');
+ if (key && value) {
+ formData.append(key, decodeURIComponent(value));
+ }
+ });
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ options.body = formData.toString();
+ }
+ }
+
+ const response = await fetch(`${API_BASE_URL}${path}`, options);
+ const text = await response.text();
+
+ let formatted;
+ try {
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
+ } catch {
+ formatted = text;
+ }
+
+ responsePre.textContent = formatted;
+
+ } catch (error) {
+ responsePre.textContent = `Error: ${error.message}`;
+ }
+ });
+}
+
+// MQTT Setup
+function setupMQTT() {
+ const topicInput = document.getElementById('mqtt-topic');
+ const payloadTextarea = document.getElementById('mqtt-payload');
+ const publishBtn = document.getElementById('mqtt-publish-btn');
+ const subscribeBtn = document.getElementById('mqtt-subscribe-btn');
+ const unsubscribeBtn = document.getElementById('mqtt-unsubscribe-btn');
+ const subscribeTopicInput = document.getElementById('mqtt-subscribe-topic');
+ const messagesContainer = document.getElementById('mqtt-messages');
+ const clearMessagesBtn = document.getElementById('clear-messages-btn');
+
+ // Connect to MQTT broker
+ try {
+ mqttClient = mqtt.connect(MQTT_BROKER_URL, {
+ clientId: 'debug-ui-' + Math.random().toString(16).substr(2, 8)
+ });
+
+ mqttClient.on('connect', () => {
+ console.log('MQTT connected');
+ updateStatus('mqtt-status', 'MQTT: Connected', true);
+ });
+
+ mqttClient.on('error', (error) => {
+ console.error('MQTT error:', error);
+ updateStatus('mqtt-status', 'MQTT: Error', false);
+ });
+
+ mqttClient.on('close', () => {
+ console.log('MQTT disconnected');
+ updateStatus('mqtt-status', 'MQTT: Disconnected', false);
+ });
+
+ mqttClient.on('message', (topic, message) => {
+ addMessage(topic, message.toString());
+ });
+ } catch (error) {
+ console.error('Failed to connect to MQTT:', error);
+ updateStatus('mqtt-status', 'MQTT: Connection Failed', false);
+ }
+
+ publishBtn.addEventListener('click', () => {
+ const topic = topicInput.value.trim();
+ let payload = payloadTextarea.value.trim();
+
+ if (!topic) {
+ alert('Please enter a topic');
+ return;
+ }
+
+ // Try to parse as JSON, otherwise use as-is
+ try {
+ const jsonData = JSON.parse(payload);
+ payload = JSON.stringify(jsonData);
+ } catch {
+ // Not JSON, use as-is
+ }
+
+ if (mqttClient && mqttClient.connected) {
+ mqttClient.publish(topic, payload, (err) => {
+ if (err) {
+ console.error('Publish error:', err);
+ alert('Failed to publish: ' + err.message);
+ } else {
+ console.log('Published to', topic);
+ }
+ });
+ } else {
+ alert('MQTT not connected');
+ }
+ });
+
+ subscribeBtn.addEventListener('click', () => {
+ const topic = subscribeTopicInput.value.trim();
+ if (!topic) {
+ alert('Please enter a topic pattern');
+ return;
+ }
+
+ if (mqttClient && mqttClient.connected) {
+ mqttClient.subscribe(topic, (err) => {
+ if (err) {
+ console.error('Subscribe error:', err);
+ alert('Failed to subscribe: ' + err.message);
+ } else {
+ subscribedTopics.add(topic);
+ console.log('Subscribed to', topic);
+ }
+ });
+ } else {
+ alert('MQTT not connected');
+ }
+ });
+
+ unsubscribeBtn.addEventListener('click', () => {
+ if (mqttClient && mqttClient.connected) {
+ subscribedTopics.forEach(topic => {
+ mqttClient.unsubscribe(topic);
+ });
+ subscribedTopics.clear();
+ console.log('Unsubscribed from all topics');
+ }
+ });
+
+ clearMessagesBtn.addEventListener('click', () => {
+ messagesContainer.innerHTML = '';
+ });
+}
+
+function addMessage(topic, payload) {
+ const messagesContainer = document.getElementById('mqtt-messages');
+ const messageDiv = document.createElement('div');
+ messageDiv.className = 'message-item';
+
+ const timestamp = new Date().toLocaleTimeString();
+ let formattedPayload = payload;
+ try {
+ formattedPayload = JSON.stringify(JSON.parse(payload), null, 2);
+ } catch {}
+
+ messageDiv.innerHTML = `
+
${timestamp}
+ ${topic}
+ ${formattedPayload}
+ `;
+
+ messagesContainer.appendChild(messageDiv);
+
+ // Auto-scroll
+ if (document.getElementById('auto-scroll').checked) {
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+ }
+}
+
+// WebSocket Setup
+function setupWebSocket() {
+ if (typeof io !== 'undefined') {
+ try {
+ wsClient = io(SOCKET_IO_URL);
+
+ wsClient.on('connect', () => {
+ console.log('WebSocket connected');
+ updateStatus('ws-status', 'WebSocket: Connected', true);
+ });
+
+ wsClient.on('disconnect', () => {
+ console.log('WebSocket disconnected');
+ updateStatus('ws-status', 'WebSocket: Disconnected', false);
+ });
+
+ wsClient.on('update', (data) => {
+ console.log('WebSocket update:', data);
+ // Could display in a separate section
+ });
+ } catch (error) {
+ console.error('Failed to connect WebSocket:', error);
+ updateStatus('ws-status', 'WebSocket: Error', false);
+ }
+ } else {
+ console.warn('Socket.io not loaded');
+ updateStatus('ws-status', 'WebSocket: Library Not Loaded', false);
+ }
+}
+
+// Debug Endpoints Setup
+function setupDebug() {
+ const debugButtons = document.querySelectorAll('[data-debug]');
+ const responsePre = document.getElementById('debug-response');
+
+ debugButtons.forEach(button => {
+ button.addEventListener('click', async () => {
+ const action = button.dataset.debug;
+ const endpoint = `/api/debug/${action}`;
+
+ try {
+ const response = await fetch(`${API_BASE_URL}${endpoint}`);
+ const text = await response.text();
+ responsePre.textContent = text;
+ } catch (error) {
+ responsePre.textContent = `Error: ${error.message}`;
+ }
+ });
+ });
+}
+
+// Quick Actions Setup
+function setupQuickActions() {
+ const quickActionButtons = document.querySelectorAll('[data-action]');
+
+ quickActionButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const action = button.dataset.action;
+ const topicInput = document.getElementById('mqtt-topic');
+ const payloadTextarea = document.getElementById('mqtt-payload');
+
+ switch (action) {
+ case 'button-start1':
+ topicInput.value = 'aquacross/button/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ type: 2,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'button-stop1':
+ topicInput.value = 'aquacross/button/00:00:00:00:00:03';
+ payloadTextarea.value = JSON.stringify({
+ type: 1,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'button-start2':
+ topicInput.value = 'aquacross/button/00:00:00:00:00:02';
+ payloadTextarea.value = JSON.stringify({
+ type: 2,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'button-stop2':
+ topicInput.value = 'aquacross/button/00:00:00:00:00:04';
+ payloadTextarea.value = JSON.stringify({
+ type: 1,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'rfid-read':
+ topicInput.value = 'aquacross/button/rfid/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ uid: 'TEST123456'
+ }, null, 2);
+ break;
+ case 'battery-update':
+ topicInput.value = 'aquacross/battery/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ voltage: 3600
+ }, null, 2);
+ break;
+ case 'heartbeat':
+ topicInput.value = 'heartbeat/alive/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'button-available':
+ topicInput.value = 'aquacross/button/status/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ available: true,
+ sleep: false,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ case 'button-sleep':
+ topicInput.value = 'aquacross/button/status/00:00:00:00:00:01';
+ payloadTextarea.value = JSON.stringify({
+ available: false,
+ sleep: true,
+ timestamp: Date.now()
+ }, null, 2);
+ break;
+ }
+
+ // Auto-publish
+ document.getElementById('mqtt-publish-btn').click();
+ });
+ });
+}
+
+// Helper Functions
+function updateStatus(elementId, text, connected) {
+ const element = document.getElementById(elementId);
+ element.textContent = text;
+ element.classList.remove('connected', 'disconnected');
+ element.classList.add(connected ? 'connected' : 'disconnected');
+}
+
+// Initialize on load
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
diff --git a/mock-server/debug_server/index.html b/mock-server/debug_server/index.html
new file mode 100644
index 0000000..d33444b
--- /dev/null
+++ b/mock-server/debug_server/index.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+ AquaMaster Debug Server
+
+
+
+
+
+
+
+
+
+
+
+
API Endpoint Testing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
MQTT Publish
+
+
+
+
+
+
+
+
+
+
+
+
Quick Actions:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
MQTT Subscribe
+
+
+
+
+
+
+
+
+
Received Messages:
+
+
+
+
+
+
+
+
+
+
+
+
+
Debug Endpoints
+
Direct access to debug endpoints for timer control:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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