152 lines
5.1 KiB
JavaScript
152 lines
5.1 KiB
JavaScript
// server/InputWS.js
|
|
const fs = require('fs');
|
|
const WebSocket = require('ws');
|
|
const FCodeClient = require('../robot/FCodeClient');
|
|
|
|
const LOG_DIR = './logs';
|
|
|
|
/**
|
|
* Ensures the log directory exists so the first appendFileSync() call cannot
|
|
* crash on a fresh container/checkout. Idempotent thanks to { recursive: true }.
|
|
*/
|
|
function ensureLogDir(dir = LOG_DIR) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function initInputWS(server, robot, GCode, sharedState) {
|
|
ensureLogDir();
|
|
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
wss.on('connection', (ws) => {
|
|
console.log("WebSocket Input connected");
|
|
|
|
const clientIP = ws._socket.remoteAddress || 'unknown';
|
|
sharedState.connectedClients.push(clientIP);
|
|
|
|
ws.on('close', () => {
|
|
sharedState.connectedClients =
|
|
sharedState.connectedClients.filter(ip => ip !== clientIP);
|
|
});
|
|
|
|
ws.on('message', (msg) => {
|
|
const message = msg.toString();
|
|
|
|
/* ---------- Ping (heartbeat) → targeted reply to requester ---------- */
|
|
if (message === "Ping") {
|
|
logPing(sharedState, clientIP);
|
|
reply(ws, "Ping");
|
|
return;
|
|
}
|
|
|
|
/* ---------- M114 (status query) → targeted reply to requester ---------- */
|
|
if (message === "M114") {
|
|
logCommand(sharedState, clientIP, message);
|
|
reply(ws, GCode.getM114(robot));
|
|
return;
|
|
}
|
|
|
|
/* ---------- G-code (motion command) → broadcast new state to all ----------
|
|
* A move changes the robot position, which is a status update every client
|
|
* (e.g. the simulation) should see → broadcast. Parsing/execution failures
|
|
* are reported back to the sender as a machine-readable error. */
|
|
if (GCode.containsCommand(message)) {
|
|
console.log("🔵 GCode.receiveGCode: Incoming command: " + message);
|
|
logCommand(sharedState, clientIP, message);
|
|
let result;
|
|
try {
|
|
result = GCode.receiveGCode(robot, message);
|
|
} catch (err) {
|
|
return sendError(ws, 'GCODE_ERROR', err.message, message);
|
|
}
|
|
// Asynchroner Befehl (z. B. Hardware-Sync M114 R, ToDo_9 Paket 4): erst nach
|
|
// Abschluss antworten; Fehler maschinenlesbar an den Anfrager zurückgeben.
|
|
if (result && typeof result.then === 'function') {
|
|
result
|
|
.then(() => broadcast(wss, GCode.getM114(robot)))
|
|
.catch(err => sendError(ws, 'GCODE_ERROR', err.message, message));
|
|
return;
|
|
}
|
|
broadcast(wss, GCode.getM114(robot));
|
|
return;
|
|
}
|
|
|
|
/* ---------- FCode (Datei-Befehle) → weiterleiten an appRobotFileservice ----------
|
|
* Der Driver ist Gateway: die Steuerungen kennen nur ihn.
|
|
* Stepping-Befehle (FPlus/FMinus/…) liefern eine driver-native GCode-Zeile
|
|
* (Radian) zurück, die der Driver direkt ausführt und dann broadcastet. */
|
|
if (FCodeClient.isFCode(message)) {
|
|
console.log("📁 FCode → Fileservice: " + message);
|
|
logCommand(sharedState, clientIP, message);
|
|
FCodeClient.handle(robot, message)
|
|
.then(result => {
|
|
if (result.type === 'step' && result.line) {
|
|
console.log("📁 FCode Step → execute: " + result.line);
|
|
try { GCode.receiveGCode(robot, result.line); } catch (err) {
|
|
return sendError(ws, 'GCODE_ERROR', err.message, result.line);
|
|
}
|
|
broadcast(wss, GCode.getM114(robot));
|
|
} else if (result.data) {
|
|
broadcast(wss, result.data);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error("📁 FCode FEHLER (" + message + "): " + err.message);
|
|
sendError(ws, err.code || 'FILE_ERROR', err.message, message);
|
|
});
|
|
return;
|
|
}
|
|
|
|
/* ---------- Unknown input → targeted machine-readable error ---------- */
|
|
sendError(ws, 'UNKNOWN_COMMAND', 'Unrecognized input', message);
|
|
});
|
|
});
|
|
|
|
return wss;
|
|
}
|
|
|
|
/* ---------- Helpers ---------- */
|
|
|
|
/** Targeted reply to a single client (status updates use broadcast instead). */
|
|
function reply(ws, payload) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(payload);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Machine-readable error response, sent only to the requesting client.
|
|
* Shape: { type: "error", code, message, input }
|
|
*/
|
|
function sendError(ws, code, message, input) {
|
|
reply(ws, JSON.stringify({ type: 'error', code, message, input }));
|
|
}
|
|
|
|
function broadcast(wss, payload) {
|
|
wss.clients.forEach(client => {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
client.send(payload);
|
|
}
|
|
});
|
|
}
|
|
|
|
function logCommand(state, ip, message) {
|
|
fs.appendFileSync(
|
|
`${LOG_DIR}/gcode_commands.log`,
|
|
`${new Date().toISOString()} ${ip}: ${message}\n`
|
|
);
|
|
state.lastCommands.push(`${new Date().toISOString()}: ${message}`);
|
|
if (state.lastCommands.length > 10) state.lastCommands.shift();
|
|
}
|
|
|
|
function logPing(state, ip) {
|
|
fs.appendFileSync(
|
|
`${LOG_DIR}/pings.log`,
|
|
`${new Date().toISOString()} ${ip} : Ping\n`
|
|
);
|
|
state.lastPings.push(`${new Date().toISOString()} ${ip} : Ping`);
|
|
if (state.lastPings.length > 10) state.lastPings.shift();
|
|
}
|
|
|
|
module.exports = initInputWS;
|
|
module.exports.ensureLogDir = ensureLogDir; |