/* eslint-disable */ const fs = require('fs'); const http = require('http'); const path = require('path'); const express = require('express'); const cors = require('cors'); const morgan = require('morgan'); const WebSocket = require('ws'); // --- Load config --- const CONFIG_PATH = path.join(__dirname, 'room.config'); function loadConfig() { const raw = fs.readFileSync(CONFIG_PATH, 'utf8'); return JSON.parse(raw); } let cfg = loadConfig(); // --- Express HTTP server --- const app = express(); app.use(express.json({ limit: '256kb' })); app.use(cors({ origin: true })); app.use(morgan('dev')); // Serve three.js from node_modules (import-map) app.use('/vendor/three', express.static(path.join(__dirname, 'node_modules/three'))); // Serve static viewer from ./public const publicDir = path.join(__dirname, 'public'); app.use('/', express.static(publicDir, { etag: false, lastModified: false, cacheControl: false })); // Expose render-relevant config to the browser app.get('/config', (req, res) => { const { sensor, pose } = cfg; res.json({ sensor, pose }); }); // Optional: pose updates app.post('/pose', (req, res) => { const { position, rotationEulerDeg, eulerOrder } = req.body || {}; if (Array.isArray(position) && position.length === 3) cfg.pose.position = position.map(Number); if (Array.isArray(rotationEulerDeg) && rotationEulerDeg.length === 3) cfg.pose.rotationEulerDeg = rotationEulerDeg.map(Number); if (typeof eulerOrder === 'string') cfg.pose.eulerOrder = eulerOrder; broadcastState(); res.json({ ok: true, pose: cfg.pose }); }); const server = http.createServer(app); server.on('connection', (socket) => socket.setNoDelay(true)); // --- WS servers for browsers --- const wssFrames = new WebSocket.Server({ noServer: true }); const wssState = new WebSocket.Server({ noServer: true }); function upgradePath(req) { try { const u = new URL(req.url, `http://${req.headers.host}`); return u.pathname; } catch (_) { return req.url; } } server.on('upgrade', (req, socket, head) => { const pathname = upgradePath(req); console.log('[HTTP] upgrade request', pathname, 'from', req.socket.remoteAddress); if (pathname === '/ws/frames') { wssFrames.handleUpgrade(req, socket, head, (ws) => { ws._isViewer = true; ws._name = `viewer:${Math.random().toString(16).slice(2, 8)}`; ws._drops = 0; ws._socket?.setNoDelay?.(true); console.log('[WS frames] connected', ws._name); ws.on('close', (code, reason) => { console.log('[WS frames] closed', ws._name, code, reason?.toString()); }); wssFrames.emit('connection', ws, req); }); } else if (pathname === '/ws/state') { wssState.handleUpgrade(req, socket, head, (ws) => { ws._name = `state:${Math.random().toString(16).slice(2, 8)}`; ws._socket?.setNoDelay?.(true); console.log('[WS state] connected', ws._name); ws.on('close', (code, reason) => { console.log('[WS state] closed', ws._name, code, reason?.toString()); }); wssState.emit('connection', ws, req); try { ws.send(JSON.stringify({ type: 'pose', pose: cfg.pose })); } catch (_) {} }); } else { console.warn('[HTTP] upgrade unknown path:', pathname); socket.destroy(); } }); wssFrames.on('connection', (ws) => { /* frames -> viewers */ }); wssState.on('connection', (ws) => { ws.on('message', () => {/*reserved*/}); }); function broadcastState() { const msg = JSON.stringify({ type: 'pose', pose: cfg.pose }); for (const client of wssState.clients) { if (client.readyState === WebSocket.OPEN) { try { client.send(msg); } catch (_) {} } } } // --- Bridge: WS client to ESP32 --- let espWS = null; let reconnectTimer = null; // Counters let espFramesTotal = 0; let espFrames10s = 0; let sent10s = 0; let dropped10s = 0; function connectESP() { if (espWS && (espWS.readyState === WebSocket.OPEN || espWS.readyState === WebSocket.CONNECTING)) return; const url = cfg.esp.wsUrl; // e.g. "ws://10.0.0.55:81/frames" console.log(`[ESP] Connecting to ${url} ...`); espWS = new WebSocket(url, { perMessageDeflate: false, handshakeTimeout: 4000 }); espWS.on('open', () => { console.log('[ESP] connected'); try { espWS.send('csv'); } catch (_) {} }); espWS.on('message', (data) => { const frame = (typeof data === 'string') ? data : data.toString('utf8'); espFramesTotal++; espFrames10s++; broadcastFrame(frame); }); espWS.on('close', (code, reason) => { console.log(`[ESP] closed ${code} ${reason}`); scheduleReconnect(); }); espWS.on('error', (err) => { console.warn('[ESP] error', err.message); }); } function scheduleReconnect() { if (reconnectTimer) return; reconnectTimer = setTimeout(() => { reconnectTimer = null; connectESP(); }, cfg.reconnectMs || 1500); } function broadcastFrame(frame) { let sentThisFrame = 0; for (const client of wssFrames.clients) { if (client.readyState !== WebSocket.OPEN) continue; const threshold = (cfg.forwarding && cfg.forwarding.clientBufferedThreshold) || 128 * 1024; if (client.bufferedAmount > threshold) { client._drops = (client._drops || 0) + 1; continue; } try { client.send(frame, { binary: false }); sentThisFrame++; } catch (_) {} } if (sentThisFrame > 0) { sent10s += sentThisFrame; // sum over all viewers for this frame } else { dropped10s++; } } // === TESTGENERATOR via Config === if (cfg.testGenerator?.enabled) { console.log("[TESTGEN] Aktiv – künstliche Testdaten werden erzeugt!"); const W = cfg.sensor.gridW; const H = cfg.sensor.gridH; const fps = cfg.testGenerator.fps || 10; const minD = cfg.testGenerator.minDistMm || 2000; const maxD = cfg.testGenerator.maxDistMm || 2500; const noise = cfg.testGenerator.noiseMm || 20; let ts = 0; const intervalMs = 1000 / fps; setInterval(() => { const distances = []; for (let i = 0; i < W * H; i++) { let mm = minD + Math.random() * (maxD - minD); mm += (Math.random() - 0.5) * noise * 2; distances.push(Math.round(mm)); } const csv = [ts++, W, H, ...distances].join(","); broadcastFrame(csv); // <- exakt gleiche Pipeline wie echte ESP‑Daten }, intervalMs); } // ---- 10s stats ---- setInterval(() => { console.log(`[STAT 10s] fromESP:${espFrames10s} ->toViewers:${sent10s} dropped:${dropped10s} viewers:${wssFrames.clients.size}`); espFrames10s = 0; sent10s = 0; dropped10s = 0; }, 10_000); const PORT = Number(process.env.PORT || cfg.server.port || 8060); const HOST = cfg.server.host || '0.0.0.0'; server.listen(PORT, HOST, () => { console.log(`[HTTP] listening on http://${HOST}:${PORT}`); if (cfg.testGenerator?.enabled) { console.log("[TESTGEN] ESP-Verbindung deaktiviert (Testmodus aktiv)."); } else { connectESP(); // Nur verbinden, wenn Testgenerator ausgeschaltet ist } }); process.on('SIGHUP', () => { try { cfg = loadConfig(); console.log('[CFG] reloaded'); } catch (e) { console.warn('[CFG] reload failed', e.message); } });