Files
appRobotDriver/server/InfoServer.js
2026-06-12 18:47:28 +02:00

173 lines
7.4 KiB
JavaScript

// 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 }) => {
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,
state,
url: status.url || null,
isTestMode: !!status.isTestMode,
error: status.error || null,
reconnectAttempt: status.reconnectAttempt || 0,
reconnectTimer: !!status.reconnectTimer,
health,
reason
};
});
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://<host>: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;