import fs from 'fs'; import path from 'path'; import https from 'https'; import express from 'express'; import dotenv from 'dotenv'; import { WebSocket } from 'ws'; import { EventEmitter } from 'events'; dotenv.config(); const app = express(); app.use(express.json()); const CERT_DIR = path.resolve('certs'); const KEY_PATH = path.join(CERT_DIR, 'localhost.key'); const CRT_PATH = path.join(CERT_DIR, 'localhost.crt'); function loadHttpsCredentials() { if (!fs.existsSync(KEY_PATH) || !fs.existsSync(CRT_PATH)) { console.error(`HTTPS-Zertifikate fehlen in ${CERT_DIR}. Bitte 'npm install' ausführen (Postinstall generiert Zertifikate).`); process.exit(1); } return { key: fs.readFileSync(KEY_PATH), cert: fs.readFileSync(CRT_PATH) }; } const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '2033', 10); const WSS_URL = process.env.WSS_URL || 'wss://localhost:2096'; const WSS_INSECURE_TLS = String(process.env.WSS_INSECURE_TLS || 'true').toLowerCase() === 'true'; // Nur bestimmte Kommandos erlauben (aus .env) const allowedCommands = new Set( (process.env.ALLOWED_COMMANDS || 'HOME,STOP,STATUS,RESET,PING,GCODEMOTOR') .split(',') .map(s => s.trim()) .filter(Boolean) ); // Broadcaster für Server-Sent Events const bus = new EventEmitter(); let wsDriver = null; let wsState = { connected: false, lastError: null, reconnectAttempts: 0, }; function logAndBroadcast(level, message, data) { const payload = { ts: new Date().toISOString(), level, message, data }; // Konsole const line = `[${payload.ts}] [${level}] ${message}`; //console.log(line, data ? data : ''); // SSE an Clients bus.emit('event', JSON.stringify(payload)); } function connectWss() { if (wsDriver && (wsDriver.readyState === wsDriver.OPEN || wsDriver.readyState === wsDriver.CONNECTING)) { return; } const tlsOptions = { rejectUnauthorized: !WSS_INSECURE_TLS }; logAndBroadcast('info', `Verbinde zu WSS: ${WSS_URL} (rejectUnauthorized=${tlsOptions.rejectUnauthorized})`); wsDriver = new WebSocket(WSS_URL, tlsOptions); wsDriver.on('open', () => { wsState.connected = true; wsState.lastError = null; wsState.reconnectAttempts = 0; logAndBroadcast('info', 'WSS Driver verbunden'); }); wsDriver.on('message', (data) => { let text = ''; try { text = typeof data === 'string' ? data : data.toString('utf8'); } catch { text = '[binary data]'; } logAndBroadcast('msg', 'Eingang von WSS', { text }); }); wsDriver.on('close', (code, reason) => { wsState.connected = false; logAndBroadcast('warn', `WSS getrennt (code=${code}, reason=${reason?.toString?.() || ''})`); scheduleReconnect(); }); wsDriver.on('error', (err) => { wsState.lastError = err?.message || String(err); logAndBroadcast('error', 'WSS Fehler', { error: wsState.lastError }); }); } function scheduleReconnect() { wsState.reconnectAttempts += 1; const delay = 10000; // 10s logAndBroadcast('info', `Reconnecting in ${Math.round(delay/1000)}s...`); setTimeout(connectWss, delay); } // HTTP API app.get('/api/status', (req, res) => { wsDriver.send("M114"); console.log("M114 gesendet, warte auf Antwort..."); res.json({ httpsPort: HTTPS_PORT, wssUrl: WSS_URL, connected: wsState.connected, wsDriver: wsDriver ? wsDriver.readyState : null, reconnectAttempts: wsState.reconnectAttempts, lastError: wsState.lastError, allowedCommands: Array.from(allowedCommands) }); }); app.post('/api/send', (req, res) => { const { cmd, payload } = req.body || {}; if (!cmd || !allowedCommands.has(String(cmd).trim())) { return res.status(400).json({ ok: false, error: 'Ungültiges oder nicht erlaubtes Kommando', allowed: Array.from(allowedCommands) }); } if (!wsDriver || wsDriver.readyState !== wsDriver.OPEN) { return res.status(503).json({ ok: false, error: 'WSS nicht verbunden' }); } const msg = { type: String(cmd).trim(), payload: payload ?? null }; if(msg.type==="STATUS"){ wsDriver.send("M114"); logAndBroadcast('tx', 'Sende STATUS (M114) an WSS'); return res.json({ ok: true, sent: msg }); } if(msg.type==="GCODEMOTOR"){ if(typeof msg.payload !== 'string' || !msg.payload.trim()){ return res.status(400).json({ ok: false, error: 'Ungültiger Payload für GCODEMOTOR. Erwartet: String mit G-Code Befehl.' }); } wsDriver.send(msg.payload); console.log(`G-Code gesendet: ${msg.payload}`); /* msg.payload = msg.payload.trim(); var arrayMsg = msg.payload.split(' ').filter(s => s.trim()); if(arrayMsg.length === 0 || !['G0','G1','G28', 'M0', 'M1', 'M114'].includes(arrayMsg[0].toUpperCase())){ return res.status(400).json({ ok: false, error: 'Ungültiger G-Code Befehl. Nur G0, G1 und G28 sind erlaubt.' }); } if(arrayMsg[1].toUpperCase().startsWith('X')){ wsDriver.send(`G0 ${arrayMsg[1].toUpperCase()} F1000`); // Schnelles Verfahren zu X-Position console.log(`G0 ${arrayMsg[1].toUpperCase()} F1000 gesendet`); } */ return res.json({ ok: true, sent: msg.payload}); } try { wsDriver.send(JSON.stringify(msg)); logAndBroadcast('tx', 'Sende an WSS', msg); return res.json({ ok: true, sent: msg }); } catch (err) { logAndBroadcast('error', 'Senden an WSS fehlgeschlagen', { error: err?.message || String(err) }); return res.status(500).json({ ok: false, error: 'Senden fehlgeschlagen' }); } }); // SSE-Endpoint app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders?.(); const send = (data) => { res.write(`data: ${data} `); }; const listener = (data) => send(data); bus.on('event', listener); // Initialstatus schicken send(JSON.stringify({ ts: new Date().toISOString(), level: 'info', message: 'SSE verbunden' })); req.on('close', () => { bus.off('event', listener); res.end(); }); }); //snapshot_video0_1775319258906_two_cam.csv //snapshot_video0_1775319258906_two_cam_annotated.jpg // Neuester Snapshot-Endpunkt app.get('/api/latest-snapshot', (req, res) => { const snapshotsDir = path.join(path.resolve('public'), 'snapshots'); fs.readdir(snapshotsDir, (err, files) => { if (err) { return res.status(500).json({ error: 'Fehler beim Lesen des Snapshots-Verzeichnisses' }); } const csvFiles = files.filter(file => file.endsWith('.csv')).map(file => ({ name: file, path: path.join(snapshotsDir, file), mtime: fs.statSync(path.join(snapshotsDir, file)).mtime })).sort((a, b) => b.mtime - a.mtime); if (csvFiles.length === 0) { return res.status(404).json({ error: 'Keine CSV-Dateien gefunden' }); } const latestFile = csvFiles[0]; const baseName = path.basename(latestFile.name, path.extname(latestFile.name)); const jsonFilename = `${baseName}.json`; const imageFilename = `${baseName}_annotated.jpg`; const imagePath = path.join(snapshotsDir, imageFilename); const imatePath2 = imagePath.includes('video0') ? imagePath.replace('video0', 'video1') : imagePath.replace('video1', 'video0'); fs.readFile(latestFile.path, 'utf8', (err, data) => { if (err) { return res.status(500).json({ error: 'Fehler beim Lesen der Datei' }); } const response = { filename: latestFile.name, mtime: latestFile.mtime.toISOString(), content: data }; // Lade JSON wenn vorhanden fs.readFile(jsonFilename, { encoding: 'base64' }, (jpgErr, jpgBase64) => { if (!jpgErr && jpgBase64) { response.imageFile = { filename: jsonFilename, mimeType: 'json', contentBase64: jpgBase64 }; } }); // Lade beide Bilder fs.readFile(imagePath, { encoding: 'base64' }, (jpgErr, jpgBase64) => { if (!jpgErr && jpgBase64) { response.imageFile = { filename: imageFilename, mimeType: 'image/jpeg', contentBase64: jpgBase64 }; } fs.readFile(imatePath2, { encoding: 'base64' }, (jpgErr2, jpgBase642) => { if (!jpgErr2 && jpgBase642) { response.image2 = { filename: path.basename(imatePath2), mimeType: 'image/jpeg', contentBase64: jpgBase642 }; } res.json(response); }); }); }); }); }); // Statisches Frontend app.use('/', express.static(path.resolve('public'))); // HTTPS-Server starten const creds = loadHttpsCredentials(); const server = https.createServer({ key: creds.key, cert: creds.cert, }, app); server.listen(HTTPS_PORT, () => { logAndBroadcast('info', `HTTPS Server läuft auf https://localhost:${HTTPS_PORT}`); // Nach Start WSS verbinden connectWss(); });