// server/InfoServer.js 'use strict'; const express = require('express'); const https = require('https'); const path = require('path'); const robotConfigService = require('./RobotConfigService'); const PUBLIC_DIR = path.join(__dirname, '..', 'public'); function createInfoServer(httpsOptions, sharedState, robot, GCode, senders, options = {}) { const app = express(); // ── Statische Dateien ──────────────────────────────────────────────────── const staticFile = (file) => (req, res) => res.sendFile(path.join(PUBLIC_DIR, file), err => { if (err) res.status(404).end('Not found'); }); app.get('/', staticFile('index.html')); app.get('/app.js', staticFile('app.js')); app.get('/style.css', staticFile('style.css')); app.get('/allApps.css', staticFile('allApps.css')); // ── API ────────────────────────────────────────────────────────────────── app.get('/api/status', (req, res) => { const sendersStatus = senders.map(({ name, instance, isGCodeReceiver }) => { const status = instance?.getStatus ? instance.getStatus() : { state: instance?.isTestMode ? 'connected' : instance?.tSocket ? 'connected' : 'disconnected', url: instance?.url || null, error: instance?.error || null, isTestMode: !!instance?.isTestMode, reconnectAttempt: instance?.reconnectAttempt || 0, reconnectTimer: !!instance?.reconnectTimer }; const state = status.state || (instance?.tSocket ? 'connected' : 'disconnected'); const health = state === 'connected' ? 'ok' : state === 'reconnecting' ? 'warning' : 'disconnected'; const reason = state === 'disconnected' ? status.error || 'no active socket connection' : undefined; return { name, isGCodeReceiver: isGCodeReceiver !== false, // false nur für Shelly state, url: status.url || null, isTestMode: !!status.isTestMode, error: status.error || null, reconnectAttempt: status.reconnectAttempt || 0, reconnectTimer: !!status.reconnectTimer, health, reason, // Hardware-Feedback (ToDo_9 Paket 1/3): GRBL-Zustand aus dem Sender. grblState: status.grblState ?? null, machinePosition: status.machinePosition ?? null, plannerBlocksFree: status.plannerBlocksFree ?? null, rxBytesFree: status.rxBytesFree ?? null, lastError: status.lastError ?? null, lastReportAt: status.lastReportAt ?? null }; }); const connectedSenders = sendersStatus.filter(s => s.health === 'ok').length; res.json({ generatedAt: new Date().toISOString(), health: { ok: sendersStatus.length > 0 && sendersStatus.every(s => s.health === 'ok'), connectedSenders, totalSenders: sendersStatus.length }, clients: sharedState.connectedClients, senders: sendersStatus, lastCommands: sharedState.lastCommands, lastPings: sharedState.lastPings }); }); app.get('/api/position', (req, res) => { res.json(JSON.parse(GCode.getM114(robot))); }); // ── Robot-Config-Service ───────────────────────────────────────────────── robotConfigService.register(app, { apiKey: options.apiKey }); // ── Power Status (Shelly) ──────────────────────────────────────────────── // // GET /api/power-status // Fragt den Shelly-Controller nach dem aktuellen Schaltzustand. // Antwort: { ok, armed: bool, voltage?, power? } // armed:true → output:true → Strom AN → Roboter bestromt ("armed") // armed:false → output:false → Strom AUS → Roboter stromlos app.get('/api/power-status', async (req, res) => { const shelly = senders.find(({ instance }) => typeof instance.getArmed === 'function'); if (!shelly) { return res.json({ ok: false, armed: false, error: 'no shelly configured' }); } const result = await shelly.instance.getArmed(); res.json(result); }); // ── Emergency Stop ─────────────────────────────────────────────────────── // // POST /api/emergency-stop // Ruft auf allen Sendern emergencyStop() auf (parallel). // FluidNC-Sender: sendet '!' (Feed Hold). // Shelly-Sender: schaltet Strom ab. // Aufruf vom Framework (Kopfzeile-Button): POST https://:2098/api/emergency-stop // // POST /api/power-on // Schaltet Strom über Shelly wieder ein (nach NotAus-Restart). // // POST /api/alarm-unlock // Sendet '$X' an alle FluidNC-Controller (entsperrt ALARM-Zustand nach Strom-Neustart). // Nur aufrufen, nachdem Roboterstellung manuell geprüft wurde. app.post('/api/emergency-stop', async (req, res) => { const settled = await Promise.allSettled( senders.map(({ name, instance }) => instance.emergencyStop().then(r => ({ name, ...r })) ) ); const results = settled.map((r, i) => r.status === 'fulfilled' ? r.value : { name: senders[i].name, ok: false, error: r.reason?.message } ); const ok = results.every(r => r.ok || r.skipped); const summary = results .map(r => `${r.name}:${r.skipped ? 'skip' : r.ok ? 'ok' : `FAIL(${r.error})`}`) .join(', '); console.warn(`⚠️ [EmergencyStop] ${new Date().toISOString()} — [${summary}]`); res.json({ ok, at: new Date().toISOString(), results }); }); app.post('/api/power-on', async (req, res) => { const shellyEntries = senders.filter(({ instance }) => typeof instance.powerOn === 'function' ); const settled = await Promise.allSettled( shellyEntries.map(({ name, instance }) => instance.powerOn().then(r => ({ name, ...r }))) ); const results = settled.map(r => r.status === 'fulfilled' ? r.value : { ok: false, error: r.reason?.message } ); const ok = results.length > 0 && results.every(r => r.ok); const summary = results.map(r => `${r.name}:${r.ok ? 'ok' : `FAIL(${r.error})`}`).join(', '); console.warn(`⚠️ [PowerOn] ${new Date().toISOString()} — [${summary || 'keine Shelly konfiguriert'}]`); res.json({ ok, at: new Date().toISOString(), results }); }); app.post('/api/alarm-unlock', async (req, res) => { const settled = await Promise.allSettled( senders.map(({ name, instance }) => instance.alarmUnlock().then(r => ({ name, ...r })) ) ); const results = settled.map((r, i) => r.status === 'fulfilled' ? r.value : { name: senders[i].name, ok: false, error: r.reason?.message } ); const ok = results.every(r => r.ok || r.skipped); const summary = results .map(r => `${r.name}:${r.skipped ? 'skip' : r.ok ? 'ok' : `FAIL(${r.error})`}`) .join(', '); console.warn(`⚠️ [AlarmUnlock] ${new Date().toISOString()} — [${summary}]`); res.json({ ok, at: new Date().toISOString(), results }); }); // ── 404 ────────────────────────────────────────────────────────────────── app.use((req, res) => res.status(404).end('Not found')); return https.createServer(httpsOptions, app); } module.exports = createInfoServer;