217 lines
6.9 KiB
JavaScript
217 lines
6.9 KiB
JavaScript
/* 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); }
|
||
}); |