const fs = require('fs'); const https = require('https'); const { createRobotFromEnv } = require('./robot/KinematicsFactory'); const GCode = require('./robot/GCode'); const TelnetSender = require('./robot/TelnetSenderGRBL'); const ShellySender = require('./robot/ShellyEmergencyStop'); 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, ShellyClass = ShellySender, 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); if (ctrl.protocol === 'shelly') { // Shelly Smart Plug: kein GCode-Empfänger, nur Emergency-Stop-Aktor const instance = new ShellyClass(ctrl.url); senders.push({ name, instance, isGCodeReceiver: false }); } else { // Telnet (FluidNC): Konstruktor erwartet 7 Achsen-Slots (x y z a b c e) vor dem // Options-Objekt. Auf genau 7 auffüllen, damit heartbeatInterval nicht als // Achsen-Arg landet. const axes7 = [...(ctrl.axes ?? [])]; while (axes7.length < 7) axes7.push(null); const instance = new TelnetSenderClass(ctrl.ip, ctrl.port, ...axes7, { heartbeatInterval: ctrl.heartbeatInterval, deadTimeout: 2 * ctrl.heartbeatInterval, }); senders.push({ name, instance, isGCodeReceiver: true }); } } 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.`); } // Nur Telnet-Sender (FluidNC) empfangen GCode — Shelly wird ausgeschlossen. // Jeder Sender reconnectet automatisch, daher sofortige Registrierung ohne Delay. senders.filter(s => s.isGCodeReceiver).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 };