const net = require("net"); const { resolve } = require("path"); const { TelnetSocket } = require("telnet-stream"); const SenderInterface = require("./SenderInterface"); module.exports = class TelnetSenderGRBL extends SenderInterface { /* urlGRBL: URL für den GCode Empfänger (MicroController, GRBL) * xAxisGrbl: Welche Achse soll ausgelesen werden? Welche Achse ist die GRBL-X-Achse * yAxisGrbl: ... * zAxisGrbl: ... */ constructor(urlGRBL = "grblesp.local", maxSpeedF = 5000, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z", aAxisGrbl = null, bAxisGrbl = null, cAxisGrbl = null, eAxisGrbl = null, options = {}){ super(); this.tSocket = null; this.receiver = null; this.connectPromise = null; this.connectResolver = null; this.connectRejecter = null; this.state = 'disconnected'; this.error = null; this.urlGRBLstr = urlGRBL; this.xAxisGrbl = xAxisGrbl; this.yAxisGrbl = yAxisGrbl; this.zAxisGrbl = zAxisGrbl; this.aAxisGrbl = aAxisGrbl; this.bAxisGrbl = bAxisGrbl; this.cAxisGrbl = cAxisGrbl; this.eAxisGrbl = eAxisGrbl; this.maxSpeedF = maxSpeedF; // Speichere für Backward-Kompatibilität this.netModule = options.netModule || net; this.TelnetSocketClass = options.TelnetSocketClass || TelnetSocket; this.setTimeoutFn = options.setTimeoutFn || setTimeout; this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout; this.setIntervalFn = options.setIntervalFn || setInterval; this.clearIntervalFn = options.clearIntervalFn || clearInterval; this.reconnectDelay = Number.isFinite(options.reconnectDelay) ? options.reconnectDelay : 1000; this.maxReconnectDelay = Number.isFinite(options.maxReconnectDelay) ? options.maxReconnectDelay : 30000; // Heartbeat: erkennt tote Verbindungen (z.B. nach NotAus). // Sendet alle heartbeatInterval ms '?' an den Controller. // Kommt deadTimeout ms lang keine Antwort, gilt die Verbindung als tot. this.heartbeatInterval = Number.isFinite(options.heartbeatInterval) ? options.heartbeatInterval : 10000; this.deadTimeout = Number.isFinite(options.deadTimeout) ? options.deadTimeout : 20000; this._heartbeatTimer = null; this._lastDataAt = 0; this._rawSocket = null; this.reconnectAttempt = 0; this.reconnectTimer = null; this.shouldReconnect = true; this.autoConnect = options.autoConnect !== false; this.isTestMode = false; // ── Hardware-Feedback (ToDo_9 Paket 1/3) ────────────────────────────── // Eingehende GRBL/FluidNC-Antworten werden geparst (vorher verworfen). this._rxBuffer = ''; // Zeilen-Puffer für fragmentierte data-Events this.grblState = null; // 'Idle' | 'Run' | 'Alarm' | 'Hold' | ... this.machinePosition = null; // [x, y, z, …] aus MPos (oder WPos) this.machinePositionType = null; // 'MPos' | 'WPos' this.plannerBlocksFree = null; // erste Bf-Zahl (freie Planner-Blöcke) this.rxBytesFree = null; // zweite Bf-Zahl (freie RX-Bytes) this.lastResponse = null; // letzte empfangene Zeile (roh) this.lastError = null; // letzte error:/ALARM:-Zeile this.lastOk = 0; // Zeitstempel des letzten 'ok' this.lastReportAt = 0; // Zeitstempel des letzten <…>-Reports this._statusWaiters = []; // offene requestStatusReport()-Promises (Sync, Paket 4) // Auto-Reporting (Paket 3) — opt-in, schreibt persistente FluidNC-Settings. // Default AUS: ohne Flag wird KEIN $10/$Report-Kommando an die Hardware // gesendet; der Treiber liest nur die Antworten des ohnehin laufenden // ?-Heartbeats. this.autoReport = options.autoReport !== undefined ? !!options.autoReport : (process.env.ROBOT_GRBL_AUTOREPORT === 'true'); this.reportInterval = Number.isFinite(options.reportInterval) ? options.reportInterval : (Number.isFinite(Number(process.env.ROBOT_GRBL_REPORT_INTERVAL)) ? Number(process.env.ROBOT_GRBL_REPORT_INTERVAL) : 200); if (urlGRBL === "test.test") { this.tSocket = { written: "", write(txt){ this.written = txt; } }; this.isTestMode = true; this.state = 'connected'; this.shouldReconnect = false; return; } if (this.autoConnect) { this.connect(); console.log("🤖 TelnetSenderGRBL initialized: " + urlGRBL); } } async connect() { if (this.isTestMode) { this.state = 'connected'; return Promise.resolve(this); } if (this.state === 'connected' && this.tSocket) { return Promise.resolve(this); } if (this.connectPromise) { return this.connectPromise; } if (this.reconnectTimer) { this.clearTimeoutFn(this.reconnectTimer); this.reconnectTimer = null; } this.state = 'connecting'; this.error = null; this.shouldReconnect = true; this.connectPromise = new Promise((resolve, reject) => { this.connectResolver = resolve; this.connectRejecter = reject; this.tryConnect(); }); return this.connectPromise; } tryConnect() { if (!this.shouldReconnect) { return; } this.state = 'connecting'; this.error = null; const socket = this.netModule.createConnection({ port: 23, host: this.urlGRBLstr }); socket.on('connect', () => { if (!this.shouldReconnect) { if (typeof socket.end === 'function') { socket.end(); } return; } this._rawSocket = socket; // TCP-Keepalive aktivieren: OS sendet Keepalive-Proben auf Netzwerk-Ebene. // Schützt als Fallback, falls der Heartbeat-Timer ausfällt. socket.setKeepAlive(true, 2000); this.tSocket = new this.TelnetSocketClass(socket); this.tSocket.on('close', () => { console.log("Telnet Closed " + this.urlGRBLstr); this._stopHeartbeat(); this._rejectStatusWaiters(new Error(`${this.urlGRBLstr}: Verbindung geschlossen`)); this.tSocket = null; this._rawSocket = null; if (this.shouldReconnect) { this.state = 'reconnecting'; this.scheduleReconnect(); } else { this.state = 'disconnected'; } }); // Zeitstempel jeder eingehenden Nachricht festhalten (für Heartbeat-Timeout). socket.on('data', () => { this._lastDataAt = Date.now(); }); // Eingehende GRBL/FluidNC-Antworten lesen (ToDo_9 Paket 1). // Vorher wurde dieser Kanal verworfen — jetzt werden ok/error/<…>-Reports // geparst. Frischer Zeilen-Puffer pro Verbindung (Reste der toten // Verbindung dürfen die neue nicht verfälschen). this._rxBuffer = ''; this.tSocket.on('data', (chunk) => this._handleIncomingData(chunk)); this.state = 'connected'; this.error = null; this.reconnectAttempt = 0; if (this.connectResolver) { this.connectResolver(this); this.connectResolver = null; this.connectRejecter = null; } this.connectPromise = null; // Heartbeat starten: erkennt NotAus / tote Verbindungen. this._startHeartbeat(); // Auto-Reporting konfigurieren (Paket 3) — nur bei aktivem Opt-in. this._configureAutoReport(); }); socket.on('error', (error) => { console.log("Telnet Connection Error on " + this.urlGRBLstr + ": " + error.toString()); this.tSocket = null; this.error = error.message || String(error); this.connectPromise = this.connectPromise || null; if (this.shouldReconnect) { this.state = 'reconnecting'; this.scheduleReconnect(); } else { this.state = 'disconnected'; if (this.connectRejecter) { this.connectRejecter(error); this.connectResolver = null; this.connectRejecter = null; this.connectPromise = null; } } }); } scheduleReconnect() { if (!this.shouldReconnect || this.reconnectTimer) { return; } const delay = Math.min(this.reconnectDelay * 2 ** this.reconnectAttempt, this.maxReconnectDelay); this.reconnectAttempt += 1; console.log(`Telnet reconnect attempt ${this.reconnectAttempt} in ${delay}ms for ${this.urlGRBLstr}`); this.reconnectTimer = this.setTimeoutFn(() => { this.reconnectTimer = null; this.tryConnect(); }, delay); } /** * Startet den Heartbeat-Timer. * * Sendet alle `heartbeatInterval` ms den FluidNC-Realtime-Command '?' und * prüft, ob seit `deadTimeout` ms keine Daten mehr empfangen wurden. * Bleibt die Antwort aus (z.B. nach NotAus), wird der Socket zerstört — * der bestehende 'close'-Handler leitet danach den Reconnect ein. * @private */ _startHeartbeat() { this._stopHeartbeat(); this._lastDataAt = Date.now(); this._heartbeatTimer = this.setIntervalFn(() => { if (!this.tSocket) { this._stopHeartbeat(); return; } const silent = Date.now() - this._lastDataAt; if (silent >= this.deadTimeout) { console.log( `[TelnetSenderGRBL] ${this.urlGRBLstr}: ` + `keine Daten seit ${silent}ms (deadTimeout=${this.deadTimeout}ms) — ` + `Verbindung wird als tot eingestuft und beendet` ); this._stopHeartbeat(); // rawSocket.destroy() löst 'close' auf tSocket aus → bestehende Reconnect-Logik if (this._rawSocket) this._rawSocket.destroy(); return; } // Leichtgewichtiger Probe: FluidNC antwortet mit oder try { this.tSocket.write('?'); } catch { /* Fehler kommt über 'error'-Event */ } }, this.heartbeatInterval); } /** * Stoppt den Heartbeat-Timer. * @private */ _stopHeartbeat() { if (this._heartbeatTimer) { this.clearIntervalFn(this._heartbeatTimer); this._heartbeatTimer = null; } } /** * Konfiguriert FluidNC-Auto-Reporting (ToDo_9 Paket 3) — nur bei aktivem Opt-in. * * Schreibt persistente Controller-Settings: * $10=3 → Statusreport enthält MPos und Bf * $Report/Interval=N → FluidNC pusht den Status während der Bewegung selbst * * Default AUS (ROBOT_GRBL_AUTOREPORT != 'true'): ohne Opt-in wird NICHTS gesendet, * der Treiber liest nur die Antworten des ohnehin laufenden ?-Heartbeats. * @private */ _configureAutoReport() { if (!this.autoReport || !this.tSocket) return; try { this.tSocket.write('$10=3\r\n'); this.tSocket.write(`$Report/Interval=${this.reportInterval}\r\n`); console.log( `[TelnetSenderGRBL] ${this.urlGRBLstr}: Auto-Reporting aktiviert ` + `($10=3, $Report/Interval=${this.reportInterval})` ); } catch (err) { // Fehler kommt ohnehin über das 'error'-Event; hier nur defensiv loggen. console.log( `[TelnetSenderGRBL] ${this.urlGRBLstr}: Auto-Report-Setup fehlgeschlagen: ${err.message}` ); } } /** * Verarbeitet einen eingehenden data-Chunk vom TelnetSocket (ToDo_9 Paket 1). * Puffert über Zeilengrenzen (TCP-Chunks zerteilen Nachrichten beliebig) und * gibt jede vollständige Zeile an _handleResponseLine(). * * Wirft nie — ein Parsefehler darf den data-Handler (und damit den Prozess) * nicht abreißen lassen. * @private */ _handleIncomingData(chunk) { try { this._rxBuffer += chunk.toString('utf8'); // Schutz gegen unbegrenztes Wachstum, falls Zeilenenden ausbleiben. if (this._rxBuffer.length > 8192) { this._rxBuffer = this._rxBuffer.slice(-8192); } let idx; while ((idx = this._rxBuffer.search(/\r?\n/)) !== -1) { const line = this._rxBuffer.slice(0, idx); const nlLen = this._rxBuffer[idx] === '\r' ? 2 : 1; this._rxBuffer = this._rxBuffer.slice(idx + nlLen); this._handleResponseLine(line); } } catch (err) { console.log(`[TelnetSenderGRBL] ${this.urlGRBLstr}: data-Parse-Fehler: ${err.message}`); } } /** * Klassifiziert eine einzelne Antwortzeile und aktualisiert den geparsten Zustand. * Demultiplext nach Nachrichtentyp (ToDo_9, Protokoll-Fakt 5): toleriert fremde * Zeilen (Cross-Channel-Bleed-Through), nimmt kein striktes 1:1 Request→Response an. * @private */ _handleResponseLine(line) { const trimmed = line.trim(); if (!trimmed) return; this.lastResponse = trimmed; if (trimmed[0] === '<') { this._parseStatusReport(trimmed); return; } if (trimmed === 'ok') { this.lastOk = Date.now(); return; } if (/^error:/i.test(trimmed) || /^ALARM/i.test(trimmed)) { this.lastError = trimmed; console.log(`[TelnetSenderGRBL] ${this.urlGRBLstr}: GRBL meldet ${trimmed}`); return; } // Alles andere (Start-Banner, [MSG:…], Echo, fremde Kanäle): bewusst ignorieren. } /** * Parst einen FluidNC-Statusreport: . * Speichert State, Maschinenposition (MPos bevorzugt, sonst WPos) und Bf. * Defensiv: unvollständige/zerstörte Felder werden übersprungen, nie geworfen. * @private */ _parseStatusReport(line) { const inner = line.replace(/^$/, ''); const fields = inner.split('|'); if (!fields.length) return; if (fields[0]) this.grblState = fields[0]; for (let i = 1; i < fields.length; i++) { const sep = fields[i].indexOf(':'); if (sep === -1) continue; const key = fields[i].slice(0, sep); const value = fields[i].slice(sep + 1); if (!value) continue; if (key === 'MPos' || key === 'WPos') { const nums = value.split(',').map(Number); if (nums.length && nums.every(Number.isFinite)) { this.machinePosition = nums; this.machinePositionType = key; } } else if (key === 'Bf') { const nums = value.split(',').map(Number); if (Number.isFinite(nums[0])) this.plannerBlocksFree = nums[0]; if (Number.isFinite(nums[1])) this.rxBytesFree = nums[1]; } } this.lastReportAt = Date.now(); this._resolveStatusWaiters(); } /** * Fordert einen frischen Statusreport an (ToDo_9 Paket 4): sendet das * FluidNC-Realtime-Byte '?' und wartet auf den nächsten geparsten `<…>`-Report. * * Löst mit einem Snapshot ({grblState, machinePosition, …}) auf, sobald ein * Report eintrifft, oder wirft nach `timeoutMs` ohne Antwort. Verändert den * Roboterzustand NICHT — reines Lesen. * * @param {number} timeoutMs * @returns {Promise<{grblState, machinePosition, machinePositionType, plannerBlocksFree, rxBytesFree}>} */ requestStatusReport(timeoutMs = 1000) { if (!this.tSocket || typeof this.tSocket.write !== 'function') { return Promise.reject(new Error(`${this.urlGRBLstr}: not connected`)); } return new Promise((resolve, reject) => { const waiter = { resolve, reject, timer: null }; waiter.timer = this.setTimeoutFn(() => { this._statusWaiters = this._statusWaiters.filter(w => w !== waiter); reject(new Error(`${this.urlGRBLstr}: Statusreport-Timeout nach ${timeoutMs}ms`)); }, timeoutMs); this._statusWaiters.push(waiter); try { this.tSocket.write('?'); } catch (err) { this.clearTimeoutFn(waiter.timer); this._statusWaiters = this._statusWaiters.filter(w => w !== waiter); reject(err); } }); } /** Löst alle offenen requestStatusReport()-Promises mit dem aktuellen Snapshot auf. @private */ _resolveStatusWaiters() { if (!this._statusWaiters || this._statusWaiters.length === 0) return; const snapshot = { grblState: this.grblState, machinePosition: this.machinePosition, machinePositionType: this.machinePositionType, plannerBlocksFree: this.plannerBlocksFree, rxBytesFree: this.rxBytesFree, }; const waiters = this._statusWaiters; this._statusWaiters = []; waiters.forEach(w => { this.clearTimeoutFn(w.timer); w.resolve(snapshot); }); } /** Bricht offene requestStatusReport()-Promises ab (z. B. bei Verbindungsverlust). @private */ _rejectStatusWaiters(reason) { if (!this._statusWaiters || this._statusWaiters.length === 0) return; const waiters = this._statusWaiters; this._statusWaiters = []; waiters.forEach(w => { this.clearTimeoutFn(w.timer); w.reject(reason); }); } send(command) { if (!this.tSocket || typeof this.tSocket.write !== 'function') { return false; } const payload = typeof command === 'string' ? command : String(command); if (!payload || payload.length === 0) { return false; } this.tSocket.write(payload + "\r\n"); return true; } getStatus() { return { state: this.state, url: this.urlGRBLstr, error: this.error, isTestMode: !!this.isTestMode, reconnectAttempt: this.reconnectAttempt, reconnectTimer: !!this.reconnectTimer, // Hardware-Feedback (ToDo_9 Paket 1/3) grblState: this.grblState, machinePosition: this.machinePosition, machinePositionType: this.machinePositionType, plannerBlocksFree: this.plannerBlocksFree, rxBytesFree: this.rxBytesFree, lastError: this.lastError, lastReportAt: this.lastReportAt, autoReport: !!this.autoReport }; } disconnect() { this._stopHeartbeat(); this._rejectStatusWaiters(new Error(`${this.urlGRBLstr}: disconnect`)); if (this.isTestMode) { this.tSocket = null; this._rawSocket = null; this.state = 'disconnected'; this.shouldReconnect = false; if (this.reconnectTimer) { this.clearTimeoutFn(this.reconnectTimer); this.reconnectTimer = null; } return; } this.shouldReconnect = false; if (this.reconnectTimer) { this.clearTimeoutFn(this.reconnectTimer); this.reconnectTimer = null; } if (this.connectPromise && this.connectRejecter) { this.connectRejecter(new Error('disconnect')); this.connectPromise = null; this.connectResolver = null; this.connectRejecter = null; } if (this.tSocket && typeof this.tSocket.end === 'function') { this.tSocket.end(); } if (this.tSocket && typeof this.tSocket.destroy === 'function') { this.tSocket.destroy(); } this.tSocket = null; this.state = 'disconnected'; this.connectPromise = null; this.error = null; } /** * Emergency Stop: sendet '!' (Feed Hold) an FluidNC. * Best-effort — falls nicht verbunden, wird {ok:false} zurückgegeben. * '!' ist ein FluidNC-Realtime-Byte (kein Zeilenende nötig, sofortige Wirkung). */ async emergencyStop() { if (!this.tSocket) return { ok: false, error: 'not connected' }; try { this.tSocket.write('!'); console.warn(`⚠️ [EmergencyStop] Feed Hold '!' → ${this.urlGRBLstr}`); return { ok: true }; } catch (err) { return { ok: false, error: err.message }; } } /** * Alarm-Unlock: sendet '$X\r\n' an FluidNC. * Entsperrt den Controller nach einem Power-Loss-Alarm (ALARM:1). * Nur aufrufen, nachdem sichergestellt wurde, dass der Roboter frei steht. */ async alarmUnlock() { if (!this.tSocket) return { ok: false, error: 'not connected' }; try { this.tSocket.write('$X\r\n'); return { ok: true }; } catch (err) { return { ok: false, error: err.message }; } } moveTo(mOld, mNew){ this.execCommand("G1", mOld, mNew) } /** * Liefert den Wert, der an einen GRBL-Port gesendet wird, wenn dieser auf eine * Roboter-Achse abgebildet ist — als reine Funktion der Positions-/Geschwindigkeits- * Quelle `pos` ({x,y,z,a,b,c,e}). Dieselben Formeln wie der Sende-Pfad, aber isoliert * und testbar. Wird für die koordinierte Feedrate (ToDo_6a) verwendet. * @returns {number|null} */ portValue(grblPort, robotAxis, pos) { if (!robotAxis || !pos) return null; const D = 180 / Math.PI; const factorTurnLift = 1.2; const handOpenInMM = 1.0; const { x, y, z, a, b, c, e } = pos; switch (grblPort) { case 'x': switch (robotAxis) { case 'x': return x; case 'y': return y * D; case 'z': return (z - y) * D; case 'a': return a * D; case 'b': return (b + z - y) * D; case 'c': return (-b + c) * D; case 'e': return -1.0 * (-1.0 * (b * D * factorTurnLift) + c * D) + e * handOpenInMM; } break; case 'y': switch (robotAxis) { case 'x': return x; case 'y': return y * D; case 'z': return (z - y) * D; case 'a': return a * D; case 'b': return (b + z - y) * D; case 'c': return -1.0 * (b * D * factorTurnLift) + c * D; case 'e': return e * D; } break; case 'z': switch (robotAxis) { case 'x': return x; case 'y': return y * D; case 'z': return (z - y) * D; case 'a': return a * D; case 'b': return b * D; case 'c': return (c + b + z - y) * D; case 'e': return e * D; } break; case 'a': switch (robotAxis) { case 'x': return y * D; // Quirk: a-Port auf robotAxis 'x' nutzt y (wie im Sende-Pfad) case 'y': return y * D; case 'z': return (z - y) * D; case 'a': return a * D; case 'b': return (b + z - y) * D; case 'c': return (c + b + z - y) * D; case 'e': return e * D; } break; case 'b': switch (robotAxis) { case 'x': return x; case 'y': return y * D; case 'z': return (z - y) * D; case 'a': return a * D; case 'b': return b * D; case 'c': return (c + b + z - y) * D; case 'e': return e * D; } break; } return null; } /** * Koordinierte Feedrate für diesen Sender (Korrekt-Modus): die euklidische Strecke, * die dieser Sender in diesem Schritt zurücklegt, geteilt durch die Bewegungszeit. * So beenden alle Controller den Schritt gleichzeitig. * @returns {number|null} Feedrate oder null, wenn nicht bestimmbar (→ Legacy-Fallback) */ computeCoordinatedFeedrate(mOld, mNew) { const moveTime = mNew && Number.isFinite(mNew.moveTime) ? mNew.moveTime : 0; if (!(moveTime > 0) || !mOld) return null; const ports = [ ['x', this.xAxisGrbl], ['y', this.yAxisGrbl], ['z', this.zAxisGrbl], ['a', this.aAxisGrbl], ['b', this.bAxisGrbl], ]; let sumSq = 0; let any = false; for (const [port, robotAxis] of ports) { if (!robotAxis) continue; const vNew = this.portValue(port, robotAxis, mNew); const vOld = this.portValue(port, robotAxis, mOld); if (!Number.isFinite(vNew) || !Number.isFinite(vOld)) continue; const d = vNew - vOld; sumSq += d * d; any = true; } if (!any) return null; const dist = Math.sqrt(sumSq); if (!(dist > 0)) return null; return dist / moveTime; } execCommand(strCommand = "G1", mOld, mNew ){ // The Hand-Turn is not 1:1 to the Hand-Lift ° var factorTurnLift = 1.2; // The Hand-Open is not 1:1 to the Hand-Turn var factorOpenTurn = 1.92; // Hand-Open in mm var handOpenInMM = 1.0 var data = strCommand.toString("utf-8"); if(this.xAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.x)){ data += " x" + (mNew.x).toFixed(2).toString(); } if(this.xAxisGrbl == "y" && mNew.yMotorChanged && Number.isFinite(mNew.y)){ data += " x" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.xAxisGrbl == "z" && mNew.zMotorChanged && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " x" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI) ).toFixed(2).toString(); } if(this.xAxisGrbl == "a" && mNew.aMotorChanged && Number.isFinite(mNew.a)){ // This is the case for the Ellbow, when the Motor is connected to the X-Port of the FluidNC data += " x" + (mNew.a * 180 / Math.PI).toFixed(2).toString(); } if(this.xAxisGrbl == "b" && (mNew.bMotorChanged || mNew.cMotorChanged) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " x" + (mNew.b * 180 / Math.PI+(mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.xAxisGrbl == "c" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b) && Number.isFinite(mNew.c)){ // Runs correctly, substracts the "b" axis, uses the "c" axis to send to the x-Motor wich is in this case the hand-twist data += " x" + ((-1)*mNew.b * 180 / Math.PI + (mNew.c * 180 / Math.PI) ).toFixed(2).toString(); } if(this.xAxisGrbl == "e" && mNew.eMotorChanged && Number.isFinite(mNew.b) && Number.isFinite(mNew.c) && Number.isFinite(mNew.e)){ //This is the case for the Hand, when the Open-Close Motor is connected to the X-Port of FluidNC var handUpDown = mNew.b * 180 * factorTurnLift / Math.PI ; var handTurn = mNew.c*180/Math.PI ; data += " x" + ((-1.0*(-1.0*(handUpDown) + handTurn) + mNew.e*handOpenInMM)).toFixed(2).toString(); } if(this.yAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.x)){ data += " y" + (mNew.x ).toFixed(2).toString(); } if(this.yAxisGrbl == "y" && mNew.yMotorChanged && Number.isFinite(mNew.y)){ data += " y" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.yAxisGrbl == "z" && mNew.zMotorChanged && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " y" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI) ).toFixed(2).toString(); } if(this.yAxisGrbl == "a" && mNew.aMotorChanged && Number.isFinite(mNew.a)){ data += " y" + (mNew.a * 180 / Math.PI).toFixed(2).toString(); } if(this.yAxisGrbl == "b" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " y" + (mNew.b * 180 / Math.PI+(mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.yAxisGrbl == "c" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b) && Number.isFinite(mNew.c)){ // This is the case if the hand-rotation-turner is connected to the FluidNC-Y var handUpDown = (mNew.b * 180 / Math.PI ) * factorTurnLift; var handTurn = ( mNew.c*180/Math.PI ) ; data += " y" + (-1.0*(handUpDown) + handTurn).toFixed(2).toString(); } if(this.yAxisGrbl == "e" && (mNew.eMotorChanged) && Number.isFinite(mNew.e)){ data += " y" + (mNew.e * 180 / Math.PI).toFixed(2).toString(); } if(this.zAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.x)){ data += " z" + (mNew.x).toFixed(2).toString(); } if(this.zAxisGrbl == "y" && mNew.yMotorChanged && Number.isFinite(mNew.y)){ data += " z" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.zAxisGrbl == "z" && mNew.zMotorChanged && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " z" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI) ).toFixed(2).toString(); } if(this.zAxisGrbl == "a" && mNew.aMotorChanged && Number.isFinite(mNew.a)){ data += " z" + (mNew.a * 180 / Math.PI).toFixed(2).toString(); } if(this.zAxisGrbl == "b" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b)){ // This is the case of the Hand, when the Up-Down-Motor is connected to the FluidNC-Z data += " z" + ( mNew.b * 180 / Math.PI ).toFixed(2).toString(); } if(this.zAxisGrbl == "c" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " z" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.zAxisGrbl == "e" && (mNew.eMotorChanged) && Number.isFinite(mNew.e)){ data += " z" + (mNew.e * 180 / Math.PI).toFixed(2).toString(); } if(this.aAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.x)){ data += " a" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.aAxisGrbl == "y" && mNew.yMotorChanged && Number.isFinite(mNew.y)){ data += " a" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.aAxisGrbl == "z" && mNew.zMotorChanged && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " a" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI) ).toFixed(2).toString(); } if(this.aAxisGrbl == "a" && mNew.aMotorChanged && Number.isFinite(mNew.a)){ data += " a" + (mNew.a * 180 / Math.PI).toFixed(2).toString(); } if(this.aAxisGrbl == "b" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " a" + (mNew.b * 180 / Math.PI+(mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.aAxisGrbl == "c" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " a" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.aAxisGrbl == "e" && mNew.eMotorChanged && Number.isFinite(mNew.e)){ // ToDo Mai 2024 data += " a" + (mNew.e * 180 / Math.PI).toFixed(2).toString(); } if(this.bAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.x)){ data += " b" + (mNew.x).toFixed(2).toString(); } if(this.bAxisGrbl == "y" && mNew.yMotorChanged && Number.isFinite(mNew.y)){ data += " b" + (mNew.y * 180 / Math.PI).toFixed(2).toString(); } if(this.bAxisGrbl == "z" && mNew.zMotorChanged && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " b" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI) ).toFixed(2).toString(); } if(this.bAxisGrbl == "a" && mNew.aMotorChanged && Number.isFinite(mNew.a)){ data += " b" + (mNew.a * 180 / Math.PI).toFixed(2).toString(); } if(this.bAxisGrbl == "b" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.b)){ data += " b" + (mNew.b * 180 / Math.PI).toFixed(2).toString(); } if(this.bAxisGrbl == "c" && (mNew.bMotorChanged || mNew.cMotorChanged || mNew.zMotorChanged) && Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)){ data += " b" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString(); } if(this.bAxisGrbl == "e" && mNew.eMotorChanged && Number.isFinite(mNew.e)){ data += " b" + (mNew.e * 180 / Math.PI).toFixed(2).toString(); } if(this.tSocket && data.length > 3){ if(strCommand == "G1" && mNew){ const DEFAULT_FEEDRATE = process.env.ROBOT_DEFAULT_FEEDRATE ? Number(process.env.ROBOT_DEFAULT_FEEDRATE) : this.maxSpeedF; let feedrate; if (mNew.speedMode === 'correct') { // Korrekt-Modus: koordinierte Feedrate; Fallback auf Legacy-Wert, // wenn (noch) nicht bestimmbar (z. B. erster Schritt ohne moveTime). const coordinated = this.computeCoordinatedFeedrate(mOld, mNew); feedrate = (coordinated != null && Number.isFinite(coordinated) && coordinated > 0) ? coordinated : (mNew.feedrate !== undefined ? mNew.feedrate : DEFAULT_FEEDRATE); } else { // Legacy-Pfad — unverändert: kartesische Feedrate an alle Achsen feedrate = mNew.feedrate !== undefined ? mNew.feedrate : DEFAULT_FEEDRATE; } data += " f"+(feedrate.toFixed(2).toString()) } if(data.indexOf("G90") == -1 && data.indexOf("G1 ") !== -1){ data = "G90 " + data; } console.log("Driver send to 🤖 " + this.urlGRBLstr + " the message: " + data) this.tSocket.write( data + "\r\n"); } } }