Files
appRobotDriver/server/InputWS.js
2026-06-14 10:32:31 +02:00

140 lines
4.5 KiB
JavaScript

// server/InputWS.js
const fs = require('fs');
const WebSocket = require('ws');
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;
}
/* ---------- File commands → broadcast result ----------
* Behaviour kept as-is on purpose: file/log management (and finer-grained
* targeting, e.g. FShow as a requester-only reply) is owned by ToDo 4. */
if (GCode.ContainsFilesCommand(message)) {
logCommand(sharedState, clientIP, message);
let result;
try {
result = GCode.receiveFC(robot, message);
} catch (err) {
return sendError(ws, 'FILE_ERROR', err.message, message);
}
if (result !== undefined) broadcast(wss, result);
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;