204 lines
6.3 KiB
JavaScript
Executable File
204 lines
6.3 KiB
JavaScript
Executable File
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 base = 1000; // 1s
|
|
const max = 30000; // 30s
|
|
const delay = Math.min(max, base * Math.pow(2, wsState.reconnectAttempts));
|
|
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`);
|
|
}
|
|
*/
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
// 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();
|
|
});
|