// 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); try { GCode.receiveGCode(robot, message); } catch (err) { return sendError(ws, 'GCODE_ERROR', err.message, message); } 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;