const fs = require('fs'); const https = require('https'); const { createRobotFromEnv } = require('./robot/KinematicsFactory'); const GCode = require('./robot/GCode'); const TelnetSender = require('./robot/TelnetSenderGRBL'); const RobotConfig = require('./robot/RobotConfig'); const initInputWS = require('./server/InputWS'); const createInfoServer = require('./server/InfoServer'); function loadHttpsOptions(fsModule, processEnv) { try { return { enable: true, key: fsModule.readFileSync('https/localhost.key'), cert: fsModule.readFileSync('https/localhost.pem'), passphrase: processEnv.HTTPS_PASSPHRASE ?? 'abcd' }; } catch (err) { throw new Error(`Failed to load HTTPS certificate/key: ${err.message}`); } } function getSenderConnectionStatus(senderInfo) { const { name, instance } = senderInfo; let status = 'disconnected'; let reason = 'no active socket connection'; if (instance?.getStatus) { const senderStatus = instance.getStatus(); status = senderStatus.state || status; reason = senderStatus.state === 'disconnected' ? senderStatus.error || reason : undefined; } else if (instance?.isTestMode || instance?.tSocket) { status = 'connected'; reason = undefined; } return { name, status, reason }; } function createApp(options = {}) { const { fsModule = fs, httpsModule = https, processEnv = process.env, RobotClass = null, GCodeModule = GCode, TelnetSenderClass = TelnetSender, initInputWSFn = initInputWS, createInfoServerFn = createInfoServer, setTimeoutFn = setTimeout, consoleObj = console } = options; const startupStatus = { https: { ok: false, error: null }, senders: [] }; let httpsOptions; try { httpsOptions = loadHttpsOptions(fsModule, processEnv); startupStatus.https = { ok: true }; } catch (err) { startupStatus.https = { ok: false, error: err.message }; consoleObj.error(startupStatus.https.error); return { startupStatus }; } // logs/-Verzeichnis sicherstellen (idempotent) fsModule.mkdirSync?.('logs', { recursive: true }); const httpsServer = httpsModule.createServer(httpsOptions); // robot.json lesen: Kinematik-Params, Bewegungs-Defaults, Controller-Endpunkte. // Env-Variablen überschreiben robot.json (Override-Ebene). const cfg = RobotConfig.load(fsModule, processEnv, consoleObj); const robot = RobotClass ? new RobotClass(cfg.kinematics.l1, cfg.kinematics.l2, cfg.kinematics.l3, cfg.motion) : createRobotFromEnv(processEnv, { ...cfg.kinematics, ...cfg.motion }); const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; initInputWSFn(httpsServer, robot, GCodeModule, sharedState); const senders = []; for (const [key, ctrl] of Object.entries(cfg.controllers)) { const name = key.charAt(0).toUpperCase() + key.slice(1); const instance = new TelnetSenderClass(ctrl.ip, ctrl.port, ...ctrl.axes); senders.push({ name, instance }); } startupStatus.senders = senders.map(getSenderConnectionStatus); const disconnectedSenders = startupStatus.senders.filter(s => s.status === 'disconnected'); if (disconnectedSenders.length > 0) { consoleObj.warn(`Startup warning: ${disconnectedSenders.length} sender(s) disconnected at startup.`); } // Register all senders as command receivers immediately and permanently. // Each sender's execCommand() guards internally against sending while it is // disconnected, and resumes automatically once it (re)connects — so there is // no need to wait for a socket or to re-register after a reconnect. The old // 5s one-shot registration silently dropped senders that connected later // (e.g. after EHOSTUNREACH backoff) and never registered WebSocket senders. senders.forEach(s => robot.cmdReceivers.push(s.instance)); const port = Number(processEnv.PORT) || 2095; httpsServer.listen(port); consoleObj.log(`Input HTTPS/WebSocket on https://localhost:${port}`); const infoServer = createInfoServerFn( httpsOptions, sharedState, robot, GCodeModule, senders, { apiKey: processEnv.ROBOT_API_KEY } ); const infoPort = 2098; infoServer.listen(infoPort); consoleObj.log(`Info server on https://localhost:${infoPort}`); return { httpsServer, infoServer, robot, senders: senders.map(s => s.instance), sharedState, httpsOptions, startupStatus }; } if (require.main === module) { createApp(); } module.exports = { createApp };