Neubau auf Abrufe
This commit is contained in:
391
server/server.js
391
server/server.js
@@ -1,288 +1,147 @@
|
||||
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';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { fileURLToPath } from 'url';
|
||||
import process from 'process';
|
||||
|
||||
dotenv.config();
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '20mb' }));
|
||||
|
||||
const CERT_DIR = path.resolve('certs');
|
||||
const KEY_PATH = path.join(CERT_DIR, 'localhost.key');
|
||||
const CRT_PATH = path.join(CERT_DIR, 'localhost.crt');
|
||||
const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10);
|
||||
const publicDir = path.join(__dirname, '..', 'public');
|
||||
const snapshotsDir = path.join(publicDir, 'snapshots');
|
||||
const WEBCAM_URL = process.env.WEBCAM_URL || '';
|
||||
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
|
||||
|
||||
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) };
|
||||
}
|
||||
app.use(express.static(publicDir));
|
||||
|
||||
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 delay = 10000; // 10s
|
||||
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.get('/api/health', (req, res) => {
|
||||
res.json({ ok: true, mode: 'backend-proxy', webcamUrl: WEBCAM_URL || null, bodyTrackerUrl: BODYTRACKER_URL || null });
|
||||
});
|
||||
|
||||
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 };
|
||||
async function findLatestSnapshotFile() {
|
||||
const files = await fs.readdir(snapshotsDir);
|
||||
const entries = await Promise.all(
|
||||
files
|
||||
.filter((name) => name.endsWith('.csv'))
|
||||
.map(async (name) => ({
|
||||
name,
|
||||
mtime: (await fs.stat(path.join(snapshotsDir, name))).mtime.valueOf()
|
||||
}))
|
||||
);
|
||||
if (entries.length === 0) return null;
|
||||
entries.sort((a, b) => b.mtime - a.mtime);
|
||||
return entries[0].name;
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
*/
|
||||
return res.json({ ok: true, sent: msg.payload});
|
||||
}
|
||||
|
||||
app.get('/api/latest-snapshot', async (req, res) => {
|
||||
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' });
|
||||
}
|
||||
});
|
||||
if (WEBCAM_URL) {
|
||||
const url = new URL('/api/latest-snapshot', WEBCAM_URL).toString();
|
||||
const fetchRes = await fetch(url);
|
||||
const contentType = fetchRes.headers.get('content-type') || '';
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//snapshot_video0_1775319258906_two_cam.csv
|
||||
//snapshot_video0_1775319258906_two_cam_annotated.jpg
|
||||
|
||||
// Neuester Snapshot-Endpunkt
|
||||
app.get('/api/latest-snapshot', (req, res) => {
|
||||
const snapshotsDir = path.join(path.resolve('public'), 'snapshots');
|
||||
fs.readdir(snapshotsDir, (err, files) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Fehler beim Lesen des Snapshots-Verzeichnisses' });
|
||||
}
|
||||
const csvFiles = files.filter(file => file.endsWith('.csv')).map(file => ({
|
||||
name: file,
|
||||
path: path.join(snapshotsDir, file),
|
||||
mtime: fs.statSync(path.join(snapshotsDir, file)).mtime
|
||||
})).sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (csvFiles.length === 0) {
|
||||
return res.status(404).json({ error: 'Keine CSV-Dateien gefunden' });
|
||||
}
|
||||
const latestFile = csvFiles[0];
|
||||
const baseName = path.basename(latestFile.name, path.extname(latestFile.name));
|
||||
const jsonFilename = `${baseName}.json`;
|
||||
const jsonPath = path.join(snapshotsDir, jsonFilename);
|
||||
|
||||
console.log("JSON Pfad:", jsonPath);
|
||||
console.log("Existiert JSON:", fs.existsSync(jsonPath));
|
||||
|
||||
const imageFilename = `${baseName}_annotated.jpg`;
|
||||
const imagePath = path.join(snapshotsDir, imageFilename);
|
||||
const imatePath2 = imagePath.includes('video0') ? imagePath.replace('video0', 'video1') : imagePath.replace('video1', 'video0');
|
||||
|
||||
//--
|
||||
fs.readFile(latestFile.path, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Fehler beim Lesen der Datei' });
|
||||
if (!fetchRes.ok) {
|
||||
const text = await fetchRes.text();
|
||||
return res.status(fetchRes.status).type('text/plain').send(text);
|
||||
}
|
||||
|
||||
const response = {
|
||||
filename: latestFile.name,
|
||||
mtime: latestFile.mtime.toISOString(),
|
||||
content: data
|
||||
if (contentType.includes('application/json')) {
|
||||
const body = await fetchRes.json();
|
||||
return res.json(body);
|
||||
}
|
||||
|
||||
const text = await fetchRes.text();
|
||||
return res.json({ filename: 'latest.csv', mtime: new Date().toISOString(), content: text });
|
||||
}
|
||||
|
||||
const latestFile = await findLatestSnapshotFile();
|
||||
if (!latestFile) {
|
||||
return res.status(404).json({ error: 'Keine Snapshot-CSV-Datei gefunden' });
|
||||
}
|
||||
|
||||
const baseName = path.basename(latestFile, path.extname(latestFile));
|
||||
const csvPath = path.join(snapshotsDir, latestFile);
|
||||
const jsonPath = path.join(snapshotsDir, `${baseName}.json`);
|
||||
const imagePath = path.join(snapshotsDir, `${baseName}_annotated.jpg`);
|
||||
const imagePath2 = path.join(snapshotsDir, `${baseName}_annotated2.jpg`);
|
||||
|
||||
const content = await fs.readFile(csvPath, 'utf8');
|
||||
const result = { filename: latestFile, mtime: (await fs.stat(csvPath)).mtime.toISOString(), content };
|
||||
|
||||
try {
|
||||
result.jsonFile = { filename: `${baseName}.json`, content: await fs.readFile(jsonPath, 'utf8') };
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const jpg = await fs.readFile(imagePath);
|
||||
result.imageFile = {
|
||||
filename: path.basename(imagePath),
|
||||
mimeType: 'image/jpeg',
|
||||
contentBase64: jpg.toString('base64')
|
||||
};
|
||||
} catch {}
|
||||
|
||||
const jsonPath = path.join(snapshotsDir, jsonFilename);
|
||||
try {
|
||||
const jpg2 = await fs.readFile(imagePath2);
|
||||
result.image2 = {
|
||||
filename: path.basename(imagePath2),
|
||||
mimeType: 'image/jpeg',
|
||||
contentBase64: jpg2.toString('base64')
|
||||
};
|
||||
} catch {}
|
||||
|
||||
// ✅ JSON FIRST, dann alles andere
|
||||
fs.readFile(jsonPath, 'utf8', (jsonErr, jsonData) => {
|
||||
if (!jsonErr && jsonData) {
|
||||
response.jsonFile = {
|
||||
filename: jsonFilename,
|
||||
content: jsonData
|
||||
};
|
||||
}
|
||||
|
||||
// Bild 1
|
||||
fs.readFile(imagePath, { encoding: 'base64' }, (jpgErr, jpgBase64) => {
|
||||
if (!jpgErr && jpgBase64) {
|
||||
response.imageFile = {
|
||||
filename: imageFilename,
|
||||
mimeType: 'image/jpeg',
|
||||
contentBase64: jpgBase64
|
||||
};
|
||||
}
|
||||
|
||||
// Bild 2
|
||||
fs.readFile(imatePath2, { encoding: 'base64' }, (jpgErr2, jpgBase642) => {
|
||||
if (!jpgErr2 && jpgBase642) {
|
||||
response.image2 = {
|
||||
filename: path.basename(imatePath2),
|
||||
mimeType: 'image/jpeg',
|
||||
contentBase64: jpgBase642
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ jetzt erst senden
|
||||
res.json(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//--
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
console.error('latest-snapshot error:', err);
|
||||
return res.status(500).json({ error: 'Fehler beim Laden des Snapshots', details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Statisches Frontend
|
||||
app.use('/', express.static(path.resolve('public')));
|
||||
app.post('/api/estimate', async (req, res) => {
|
||||
if (!BODYTRACKER_URL) {
|
||||
return res.status(501).json({ error: 'BODYTRACKER_URL ist nicht konfiguriert' });
|
||||
}
|
||||
|
||||
// HTTPS-Server starten
|
||||
const creds = loadHttpsCredentials();
|
||||
const server = https.createServer({
|
||||
key: creds.key,
|
||||
cert: creds.cert,
|
||||
}, app);
|
||||
try {
|
||||
const { imageFile, image2, robotIntrinsics } = req.body;
|
||||
const formData = new FormData();
|
||||
|
||||
server.listen(HTTPS_PORT, () => {
|
||||
logAndBroadcast('info', `HTTPS Server läuft auf https://localhost:${HTTPS_PORT}`);
|
||||
// Nach Start WSS verbinden
|
||||
connectWss();
|
||||
if (imageFile?.contentBase64) {
|
||||
const buffer = Buffer.from(imageFile.contentBase64, 'base64');
|
||||
formData.append('images', new Blob([buffer], { type: imageFile.mimeType || 'image/jpeg' }), imageFile.filename || 'snapshot.jpg');
|
||||
}
|
||||
|
||||
if (image2?.contentBase64) {
|
||||
const buffer2 = Buffer.from(image2.contentBase64, 'base64');
|
||||
formData.append('images', new Blob([buffer2], { type: image2.mimeType || 'image/jpeg' }), image2.filename || 'snapshot2.jpg');
|
||||
}
|
||||
|
||||
if (robotIntrinsics) {
|
||||
formData.append('intrinsics', new Blob([JSON.stringify(robotIntrinsics)], { type: 'application/json' }), 'intrinsics.json');
|
||||
}
|
||||
|
||||
const estimateUrl = new URL('/v1/estimate', BODYTRACKER_URL).toString();
|
||||
const fetchRes = await fetch(estimateUrl, { method: 'POST', body: formData });
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
const message = await fetchRes.text();
|
||||
return res.status(fetchRes.status).json({ error: 'BodyTracker-Fehler', details: message });
|
||||
}
|
||||
|
||||
const body = await fetchRes.json();
|
||||
return res.json(body);
|
||||
} catch (err) {
|
||||
console.error('estimate error:', err);
|
||||
return res.status(500).json({ error: 'Fehler beim Aufruf des BodyTracker', details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`appRobotHoming backend listening on port ${PORT}`);
|
||||
console.log(`WEBCAM_URL=${WEBCAM_URL || '<lokal>'}`);
|
||||
console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || '<nicht konfiguriert>'}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user