159 lines
4.9 KiB
JavaScript
Executable File
159 lines
4.9 KiB
JavaScript
Executable File
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 };
|