Files
appRobotRoom/server.js
2026-06-16 10:06:05 +02:00

217 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 ESPDaten
}, 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); }
});