181 lines
7.9 KiB
JavaScript
181 lines
7.9 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, 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://<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;
|