Initial commit: appRobotHoming
This commit is contained in:
27
.gitignore
vendored
Executable file
27
.gitignore
vendored
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Zertifikate & Schlüssel (niemals committen!)
|
||||||
|
certs/
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.pfx
|
||||||
|
|
||||||
|
# Umgebungen & lokale Artefakte
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Builds & Coverage
|
||||||
|
coverage/
|
||||||
|
dist/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
57
README.md
Executable file
57
README.md
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
# appRobotHoming
|
||||||
|
|
||||||
|
Eine kleine Node.js-App mit HTTPS-Frontend (5 Buttons + Textfeld) und Backend, das sich mit einem konfigurierbaren **WSS** (WebSocket Secure) verbindet. Die Buttons senden Befehle an den WSS, und das Textfeld zeigt eingehende Nachrichten/Logs an.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **HTTPS**-Server wird automatisch mit **selbstsignierten Zertifikaten** betrieben.
|
||||||
|
- **Postinstall-Task** erstellt bei `npm install` die Zertifikate unter `./certs`.
|
||||||
|
- **WSS-Client** mit Auto-Reconnect und optionaler TLS-Validierung (in `.env` steuerbar).
|
||||||
|
- **SSE** (Server-Sent Events) für Live-Logs im Browser.
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
```bash
|
||||||
|
# 1) Abhängigkeiten installieren und Zertifikate erzeugen
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 2) (Optional) .env anlegen, basierend auf .env.sample
|
||||||
|
cp .env.sample .env
|
||||||
|
# Werte nach Bedarf anpassen
|
||||||
|
|
||||||
|
# 3) Starten
|
||||||
|
npm run dev # mit Nodemon
|
||||||
|
# oder
|
||||||
|
npm start # ohne Nodemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Öffne danach: https://localhost:8443
|
||||||
|
(Da selbstsigniert, musst du dem Zertifikat im Browser einmalig vertrauen.)
|
||||||
|
|
||||||
|
## Konfiguration (`.env`)
|
||||||
|
Siehe `.env.sample` für alle verfügbaren Variablen:
|
||||||
|
- `HTTPS_PORT` (Standard: `8443`)
|
||||||
|
- `WSS_URL` (z. B. `wss://localhost:9001`)
|
||||||
|
- `WSS_INSECURE_TLS` (`true|false`) – bei selbstsignierten Upstream-Zertifikaten oft `true`
|
||||||
|
- `HTTPS_HOST` (CN für das Zertifikat, Standard: `localhost`)
|
||||||
|
- `HTTPS_CERT_DAYS` (Gültigkeitsdauer des selbstsignierten Zertifikats in Tagen)
|
||||||
|
- `ALLOWED_COMMANDS` (kommasepariert; nur diese Kommandos akzeptiert das Backend)
|
||||||
|
|
||||||
|
## Sicherheitshinweise
|
||||||
|
- Die Inhalte des Verzeichnisses `certs/` sowie `.env` sind **absichtlich** in `.gitignore` eingetragen und werden nicht in Gitea eingecheckt.
|
||||||
|
- In Entwicklungsumgebungen kann `WSS_INSECURE_TLS=true` nötig sein. In Produktion **deaktivieren** und echte Zertifikate verwenden.
|
||||||
|
|
||||||
|
## Ordnerstruktur
|
||||||
|
```
|
||||||
|
appRobotHoming/
|
||||||
|
├─ public/ # Statisches Frontend (HTML/JS/CSS)
|
||||||
|
├─ src/ # Backend-Quellcode
|
||||||
|
├─ scripts/ # Utility-Skripte (z. B. Zertifikatserzeugung)
|
||||||
|
├─ certs/ # (auto-generiert) selbstsignierte Zertifikate
|
||||||
|
├─ .gitignore
|
||||||
|
├─ .env.sample
|
||||||
|
├─ package.json
|
||||||
|
└─ README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea-Upload
|
||||||
|
- Committe den Code **ohne** `certs/` und **ohne** `.env`.
|
||||||
|
- Nach dem Klonen auf einem anderen System einfach `npm install` ausführen – die Zertifikate werden wieder neu erzeugt.
|
||||||
1183
package-lock.json
generated
Normal file
1183
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Executable file
22
package.json
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "approbothoming",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "HTTPS Web-App mit 5 Buttons und Textfeld. Backend verbindet zu konfigurierbarem WSS, sendet Befehle und streamt Ausgaben.",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server/server.js",
|
||||||
|
"dev": "nodemon server/server.js",
|
||||||
|
"postinstall": "node scripts/generate-certs.js || true",
|
||||||
|
"create": "node scripts/generate-certs.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"selfsigned": "^2.4.1",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
66
public/client.js
Executable file
66
public/client.js
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
(function(){
|
||||||
|
const logEl = document.getElementById('log');
|
||||||
|
const connEl = document.getElementById('conn');
|
||||||
|
|
||||||
|
function append(line){
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
logEl.value += `[${now}] ${line}
|
||||||
|
`;
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStatus(){
|
||||||
|
try{
|
||||||
|
const res = await fetch('/api/status');
|
||||||
|
const st = await res.json();
|
||||||
|
if (st.connected){ connEl.textContent = 'verbunden'; connEl.className = 'badge ok'; }
|
||||||
|
else if (st.lastError){ connEl.textContent = 'fehler'; connEl.className = 'badge err'; }
|
||||||
|
else { connEl.textContent = 'getrennt'; connEl.className = 'badge warn'; }
|
||||||
|
}catch(e){ connEl.textContent = 'unbekannt'; connEl.className = 'badge'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectSSE(){
|
||||||
|
const es = new EventSource('/api/events');
|
||||||
|
es.onmessage = (ev)=>{
|
||||||
|
try{
|
||||||
|
const p = JSON.parse(ev.data);
|
||||||
|
if (p.level === 'msg') append(`WSS → ${p.data?.text ?? ''}`);
|
||||||
|
else if (p.level === 'tx') append(`TX → ${JSON.stringify(p.data)}`);
|
||||||
|
else append(`${p.level?.toUpperCase?.()}: ${p.message}`);
|
||||||
|
}catch{ append(ev.data); }
|
||||||
|
};
|
||||||
|
es.onerror = ()=>{
|
||||||
|
append('SSE Fehler/unterbrochen. Versuche neu zu verbinden…');
|
||||||
|
setTimeout(connectSSE, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindButtons(){
|
||||||
|
document.querySelectorAll('button[data-cmd]').forEach(btn =>{
|
||||||
|
btn.addEventListener('click', async () =>{
|
||||||
|
const cmd = btn.getAttribute('data-cmd');
|
||||||
|
|
||||||
|
let payload = null;
|
||||||
|
const payloadSelector = btn.getAttribute('data-payload');
|
||||||
|
if (payloadSelector) {
|
||||||
|
const field = document.querySelector(payloadSelector);
|
||||||
|
if (field) payload = field.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
const res = await fetch('/api/send', {
|
||||||
|
method:'POST', headers:{ 'Content-Type':'application/json' },
|
||||||
|
body: JSON.stringify({ cmd, payload })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if(!res.ok){ append(`FEHLER ${res.status}: ${data.error || 'Unbekannt'}`); }
|
||||||
|
else { append(`Sende: ${cmd}`); }
|
||||||
|
}catch(err){ append('FEHLER: ' + (err?.message || err)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bindButtons();
|
||||||
|
connectSSE();
|
||||||
|
refreshStatus();
|
||||||
|
})();
|
||||||
44
public/index.html
Executable file
44
public/index.html
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>appRobotHoming</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>appRobotHoming</h1>
|
||||||
|
<div id="status">Status: <span class="badge" id="conn">…</span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="controls">
|
||||||
|
<button data-cmd="HOME">HOME</button>
|
||||||
|
<button data-cmd="STOP">STOP</button>
|
||||||
|
<button data-cmd="STATUS">STATUS</button>
|
||||||
|
<button data-cmd="RESET">RESET</button>
|
||||||
|
<button data-cmd="PING">PING</button>
|
||||||
|
<input
|
||||||
|
id="gcodePayload"
|
||||||
|
type="text"
|
||||||
|
placeholder="G-Code / Motorbefehl"
|
||||||
|
style="width: 220px; padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0;"
|
||||||
|
/>
|
||||||
|
<button data-cmd="GCODEMOTOR" data-payload="#gcodePayload">GCodeMotor</button>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="log">
|
||||||
|
<label for="log">Ausgabe</label>
|
||||||
|
<textarea id="log" readonly></textarea>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<small>HTTPS + WSS Relay • ©</small>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/client.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
public/styles.css
Executable file
16
public/styles.css
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
:root{ --bg:#0f172a; --fg:#e2e8f0; --muted:#94a3b8; --accent:#38bdf8; --ok:#22c55e; --warn:#f59e0b; --err:#ef4444; }
|
||||||
|
*{ box-sizing:border-box; }
|
||||||
|
body{ margin:0; font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--fg); }
|
||||||
|
header{ display:flex; justify-content:space-between; align-items:center; padding:16px 24px; border-bottom:1px solid #1f2937; }
|
||||||
|
h1{ margin:0; font-size:20px; }
|
||||||
|
.badge{ padding:2px 8px; border-radius:999px; background:#334155; }
|
||||||
|
.badge.ok{ background: #064e3b; color:#a7f3d0; }
|
||||||
|
.badge.warn{ background:#3f1b00; color:#fdba74; }
|
||||||
|
.badge.err{ background:#3f0d0d; color:#fecaca; }
|
||||||
|
main{ display:grid; grid-template-columns:1fr; gap:16px; padding:16px; max-width:900px; margin:0 auto; }
|
||||||
|
.controls{ display:flex; gap:12px; flex-wrap:wrap; }
|
||||||
|
.controls button{ background:#1e293b; color:var(--fg); border:1px solid #334155; padding:10px 16px; border-radius:8px; cursor:pointer; }
|
||||||
|
.controls button:hover{ border-color: var(--accent); }
|
||||||
|
.log{ display:flex; flex-direction:column; gap:8px; }
|
||||||
|
#log{ width:100%; height:360px; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; }
|
||||||
|
footer{ padding:12px 24px; border-top:1px solid #1f2937; color:var(--muted); }
|
||||||
63
scripts/generate-certs.js
Executable file
63
scripts/generate-certs.js
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
// Generiert selbstsignierte Zertifikate bei npm install
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import selfsigned from 'selfsigned';
|
||||||
|
|
||||||
|
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 ensureDir(p) {
|
||||||
|
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIfMissing() {
|
||||||
|
ensureDir(CERT_DIR);
|
||||||
|
const host = process.env.HTTPS_HOST || 'localhost';
|
||||||
|
const days = parseInt(process.env.HTTPS_CERT_DAYS || '3650', 10);
|
||||||
|
|
||||||
|
const needKey = !fs.existsSync(KEY_PATH);
|
||||||
|
const needCrt = !fs.existsSync(CRT_PATH);
|
||||||
|
|
||||||
|
if (!needKey && !needCrt) {
|
||||||
|
console.log(`[certs] Zertifikate existieren bereits in ${CERT_DIR}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[certs] Erzeuge selbstsigniertes Zertifikat für CN=${host}, ${days} Tage gültig...`);
|
||||||
|
const attrs = [{ name: 'commonName', value: host }];
|
||||||
|
const pems = selfsigned.generate(attrs, {
|
||||||
|
keySize: 2048,
|
||||||
|
days,
|
||||||
|
algorithm: 'sha256',
|
||||||
|
extensions: [
|
||||||
|
{ name: 'basicConstraints', cA: true },
|
||||||
|
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true },
|
||||||
|
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
|
||||||
|
{ name: 'subjectAltName', altNames: [ { type: 2, value: host }, { type: 7, ip: '127.0.0.1' } ] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(KEY_PATH, pems.private, { mode: 0o600 });
|
||||||
|
fs.writeFileSync(CRT_PATH, pems.cert, { mode: 0o644 });
|
||||||
|
|
||||||
|
const readme = `Diese Zertifikate sind nur für lokale Entwicklung gedacht.
|
||||||
|
|
||||||
|
` +
|
||||||
|
`Dateien:
|
||||||
|
- ${KEY_PATH}
|
||||||
|
- ${CRT_PATH}
|
||||||
|
|
||||||
|
` +
|
||||||
|
`Nicht committen! Siehe .gitignore.`;
|
||||||
|
fs.writeFileSync(path.join(CERT_DIR, 'README.txt'), readme);
|
||||||
|
|
||||||
|
console.log(`[certs] Zertifikate erzeugt unter ${CERT_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
generateIfMissing();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[certs] Fehler beim Erzeugen der Zertifikate:', err?.message || err);
|
||||||
|
process.exit(0); // nicht als harter Fehler werten
|
||||||
|
}
|
||||||
203
server/server.js
Executable file
203
server/server.js
Executable file
@@ -0,0 +1,203 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user