diff --git a/doc/ToDo_1_Parsing.md b/doc/ToDo_1_Parsing.md index 2e525b4..4020a19 100644 --- a/doc/ToDo_1_Parsing.md +++ b/doc/ToDo_1_Parsing.md @@ -6,8 +6,16 @@ Klare Trennung zwischen G-Code-Parsing und Robotersteuerlogik. Der Parser soll n ## Aufgaben -- [ ] `GCodeParser` einführen, das G-Code und Nachrichten in strukturierte Befehlsobjekte übersetzt -- [ ] Parsing-Regeln definieren für `G90`, `G91`, `G1`, `G28`, `G92`, `M1` und `$J=` sowie Parameter `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F` -- [ ] Raw-String-Verarbeitung aus `GCode.receiveGCode()` entfernen -- [ ] Parser-Resultate als Objekte an den Controller übergeben, nicht als rohe Textbefehle -- [ ] Parser-Fehlerfälle klar behandeln: ungültige Syntax, fehlende Werte, unbrauchbare Befehle \ No newline at end of file +- [x] `GCodeParser` einführen, das G-Code und Nachrichten in strukturierte Befehlsobjekte übersetzt +- [x] Parsing-Regeln definieren für `G90`, `G91`, `G1`, `G28`, `G92`, `M1` und `$J=` sowie Parameter `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F` +- [x] Raw-String-Verarbeitung aus `GCode.receiveGCode()` entfernen +- [x] Parser-Resultate als Objekte an den Controller übergeben, nicht als rohe Textbefehle +- [x] Parser-Fehlerfälle klar behandeln: ungültige Syntax, fehlende Werte, unbrauchbare Befehle + +## Status + +- [x] Implementierung abgeschlossen +- [x] Tests erfolgreich: `npx jest --runInBand` + + +Erledigt von VStudio Chatbot unter Aufsicht ChK \ No newline at end of file diff --git a/robot/GCode.js b/robot/GCode.js index b787aad..fc56227 100755 --- a/robot/GCode.js +++ b/robot/GCode.js @@ -2,6 +2,7 @@ * Receives GCode, processes it and moves the Data to the Roboter-Class */ const fs = require('fs'); +const GCodeParser = require('./GCodeParser'); class GCode{ @@ -81,138 +82,103 @@ class GCode{ }; static receiveGCode(robot, g){ + const commands = GCodeParser.parse(g); + if (!commands.length) return; - if(g == undefined) return; - if(g.length == 0) return; + commands.forEach(parsed => { + if (!parsed || !parsed.command) { + return; + } - // console.log("🔵 GCode.receiveGCode: Incoming command: " + g); // Moved to InputWS + const cmd = parsed.command; + const params = parsed.params || {}; - g = g.toString("utf8"); + if (cmd === 'G90') { + robot.moveRelative = false; + return; + } - if(g.startsWith("$J=")) { - // Handle $J= GCode for jogging - g = g.substring(3).trim(); // Remove "$J=" and trim whitespace - } + if (cmd === 'G91') { + robot.moveRelative = true; + return; + } - var multipleCommands = g.split(" G"); - var doProcessRest = false; - if(multipleCommands.length > 1){ - doProcessRest = true; - g = multipleCommands[0]; - } - - g = g.split(" "); - - var calculateNew = true; - var calculateFromMotorCoordinates = false; - - if(g[0] == "G90") {robot.moveRelative = false; calculateNew = false;} - else if(g[0] == "G91") {robot.moveRelative = true; calculateNew = false;} - // G28 - Move to Home Position - else if(g[0] == "G28") { + if (cmd === 'G28') { robot.x = 0; robot.y = robot.l1 + robot.l2 + robot.l3; robot.z = 0; - robot.phi = -Math.PI/2; - robot.theta = Math.PI/2; + robot.phi = -Math.PI / 2; + robot.theta = Math.PI / 2; robot.psi = 0; robot.e = 0; + return; } - else if(g[0] == "G1" && robot.moveRelative){ - g.forEach((s) => { - if(s.toUpperCase().includes("X")){ robot.x += Number(s.substring(1, s.length)); robot.xMotorChanged = true;} - if(s.toUpperCase().includes("Y")){ robot.y += Number(s.substring(1, s.length)); robot.yMotorChanged = true;} - if(s.toUpperCase().includes("Z")){ robot.z += Number(s.substring(1, s.length)); robot.zMotorChanged = true;} - // abc in 2Pi-Angles - if(s.toUpperCase().includes("A")){ robot.phi += Number(s.substring(1, s.length)); robot.aMotorChanged = true;} - if(s.toUpperCase().includes("B")){ robot.theta += Number(s.substring(1, s.length)); robot.bMotorChanged = true;} - if(s.toUpperCase().includes("C")){ robot.psi += Number(s.substring(1, s.length)); robot.cMotorChanged = true;} - if(s.toUpperCase().includes("E")){ robot.e += Number(s.substring(1, s.length)); robot.eMotorChanged = true;} - if(s.toUpperCase().includes("F")){ robot.feedrate = Number(s.substring(1, s.length));} - }); - } - else if(g[0] == "M1" && robot.moveRelative){ - calculateFromMotorCoordinates = true; - g.forEach((s) => { - if(s.toUpperCase().includes("X")){ robot.xMotor += Number(s.substring(1, s.length)); robot.xMotorChanged = true;} - if(s.toUpperCase().includes("Y")){ robot.alpha += Number(s.substring(1, s.length)); robot.yMotorChanged = true;} - if(s.toUpperCase().includes("Z")){ robot.beta += Number(s.substring(1, s.length)); robot.zMotorChanged = true;} - - if(s.toUpperCase().includes("A")){ robot.a += Number(s.substring(1, s.length)); robot.aMotorChanged = true;} - if(s.toUpperCase().includes("B")){ robot.b += Number(s.substring(1, s.length)); robot.bMotorChanged = true;} - if(s.toUpperCase().includes("C")){ robot.c += Number(s.substring(1, s.length)); robot.cMotorChanged = true;} - if(s.toUpperCase().includes("E")){ robot.e += Number(s.substring(1, s.length)); robot.eMotorChanged = true;} - }); - } - // Absolute-Positioning - else if(g[0] == "G1" && !robot.moveRelative){ - g.forEach((s) => { - if(s.toUpperCase().includes("X")){ robot.x = Number(s.substring(1, s.length)); robot.xMotorChanged = true;} - if(s.toUpperCase().includes("Y")){ robot.y = Number(s.substring(1, s.length)); robot.yMotorChanged = true;} - if(s.toUpperCase().includes("Z")){ robot.z = Number(s.substring(1, s.length)); robot.zMotorChanged = true;} - - if(s.toUpperCase().includes("A")){ robot.phi = Number(s.substring(1, s.length)); robot.aMotorChanged = true;} - if(s.toUpperCase().includes("B")){ robot.theta = Number(s.substring(1, s.length)); robot.bMotorChanged = true;} - if(s.toUpperCase().includes("C")){ robot.psi = Number(s.substring(1, s.length)); robot.cMotorChanged = true;} - if(s.toUpperCase().includes("E")){ robot.e = Number(s.substring(1, s.length)); robot.eMotorChanged = true;} - if(s.toUpperCase().includes("F")){ robot.feedrate = Number(s.substring(1, s.length)); } - }); - } - else if(g[0] == "M1" && !robot.moveRelative){ - calculateFromMotorCoordinates = true; - g.forEach((s) => { - if(s.toUpperCase().includes("X")){ robot.xMotor = Number(s.substring(1, s.length)); robot.xMotorChanged = true;} - if(s.toUpperCase().includes("Y")){ robot.alpha = Number(s.substring(1, s.length)); robot.yMotorChanged = true;} - if(s.toUpperCase().includes("Z")){ robot.beta = Number(s.substring(1, s.length)); robot.zMotorChanged = true;} - - if(s.toUpperCase().includes("A")){ robot.a = Number(s.substring(1, s.length)); robot.aMotorChanged = true;} - if(s.toUpperCase().includes("B")){ robot.b = Number(s.substring(1, s.length)); robot.bMotorChanged = true;} - if(s.toUpperCase().includes("C")){ robot.c = Number(s.substring(1, s.length)); robot.cMotorChanged = true;} - if(s.toUpperCase().includes("E")){ robot.e = Number(s.substring(1, s.length)); robot.eMotorChanged = true;} - }); - robot.calculatePositionFromMotorAngles(); - } - else if(g[0] == "M92"){ // G92 - Set Position --- M92 in Radiant - robot.createMotorPosition(); - g.forEach((s) => { - if(s.toUpperCase().includes("X")){ robot.xMotor = Number(s.substring(1, s.length)); robot.xMotorChanged = true;} - if(s.toUpperCase().includes("Y")){ robot.alpha = Number(s.substring(1, s.length)); robot.yMotorChanged = true;} - if(s.toUpperCase().includes("Z")){ robot.beta = Number(s.substring(1, s.length)); robot.zMotorChanged = true;} - - if(s.toUpperCase().includes("A")){ robot.a = Number(s.substring(1, s.length)); robot.aMotorChanged = true;} - if(s.toUpperCase().includes("B")){ robot.b = Number(s.substring(1, s.length)); robot.bMotorChanged = true;} - if(s.toUpperCase().includes("C")){ robot.c = Number(s.substring(1, s.length)); robot.cMotorChanged = true;} - if(s.toUpperCase().includes("E")){ robot.e = Number(s.substring(1, s.length)); robot.eMotorChanged = true;} - }); - robot.calculatePositionFromMotorAngles(); - calculateNew = false; - - // ToDo: Send Command to update Position of Robot, because G92 should - // set the current Position to the given Coordinates without moving the Robot. - robot.sendCommand("G92"); - } - - if(calculateNew && !calculateFromMotorCoordinates){ - robot.calculateAngles3D(); - robot.sendCommand(); - } - - if(calculateFromMotorCoordinates){ - robot.calculatePositionFromMotorAngles(); - robot.sendCommand(); - } - - - // verarbeitet alle weiteren Commands in dieser Zeile. - if(doProcessRest){ - multipleCommands.forEach((cmd) => { - if(cmd[0] != "G"){ - this.receiveGCode(robot, "G"+cmd); + if (cmd === 'G1') { + if (robot.moveRelative) { + if (Number.isFinite(params.X)) { robot.x += params.X; robot.xMotorChanged = true; } + if (Number.isFinite(params.Y)) { robot.y += params.Y; robot.yMotorChanged = true; } + if (Number.isFinite(params.Z)) { robot.z += params.Z; robot.zMotorChanged = true; } + if (Number.isFinite(params.A)) { robot.phi += params.A; robot.aMotorChanged = true; } + if (Number.isFinite(params.B)) { robot.theta += params.B; robot.bMotorChanged = true; } + if (Number.isFinite(params.C)) { robot.psi += params.C; robot.cMotorChanged = true; } + if (Number.isFinite(params.E)) { robot.e += params.E; robot.eMotorChanged = true; } + if (Number.isFinite(params.F)) { robot.feedrate = params.F; } + } else { + if (Number.isFinite(params.X)) { robot.x = params.X; robot.xMotorChanged = true; } + if (Number.isFinite(params.Y)) { robot.y = params.Y; robot.yMotorChanged = true; } + if (Number.isFinite(params.Z)) { robot.z = params.Z; robot.zMotorChanged = true; } + if (Number.isFinite(params.A)) { robot.phi = params.A; robot.aMotorChanged = true; } + if (Number.isFinite(params.B)) { robot.theta = params.B; robot.bMotorChanged = true; } + if (Number.isFinite(params.C)) { robot.psi = params.C; robot.cMotorChanged = true; } + if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; } + if (Number.isFinite(params.F)) { robot.feedrate = params.F; } } - }) - } - + + robot.calculateAngles3D(); + robot.sendCommand(); + return; + } + + if (cmd === 'M1') { + if (robot.moveRelative) { + if (Number.isFinite(params.X)) { robot.xMotor += params.X; robot.xMotorChanged = true; } + if (Number.isFinite(params.Y)) { robot.alpha += params.Y; robot.yMotorChanged = true; } + if (Number.isFinite(params.Z)) { robot.beta += params.Z; robot.zMotorChanged = true; } + if (Number.isFinite(params.A)) { robot.a += params.A; robot.aMotorChanged = true; } + if (Number.isFinite(params.B)) { robot.b += params.B; robot.bMotorChanged = true; } + if (Number.isFinite(params.C)) { robot.c += params.C; robot.cMotorChanged = true; } + if (Number.isFinite(params.E)) { robot.e += params.E; robot.eMotorChanged = true; } + } else { + if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; } + if (Number.isFinite(params.Y)) { robot.alpha = params.Y; robot.yMotorChanged = true; } + if (Number.isFinite(params.Z)) { robot.beta = params.Z; robot.zMotorChanged = true; } + if (Number.isFinite(params.A)) { robot.a = params.A; robot.aMotorChanged = true; } + if (Number.isFinite(params.B)) { robot.b = params.B; robot.bMotorChanged = true; } + if (Number.isFinite(params.C)) { robot.c = params.C; robot.cMotorChanged = true; } + if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; } + } + + robot.calculatePositionFromMotorAngles(); + robot.sendCommand(); + return; + } + + if (cmd === 'M92') { + robot.createMotorPosition(); + if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; } + if (Number.isFinite(params.Y)) { robot.alpha = params.Y; robot.yMotorChanged = true; } + if (Number.isFinite(params.Z)) { robot.beta = params.Z; robot.zMotorChanged = true; } + if (Number.isFinite(params.A)) { robot.a = params.A; robot.aMotorChanged = true; } + if (Number.isFinite(params.B)) { robot.b = params.B; robot.bMotorChanged = true; } + if (Number.isFinite(params.C)) { robot.c = params.C; robot.cMotorChanged = true; } + if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; } + + robot.calculatePositionFromMotorAngles(); + robot.sendCommand('G92'); + return; + } + }); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////77 diff --git a/robot/GCodeParser.js b/robot/GCodeParser.js new file mode 100644 index 0000000..05b727e --- /dev/null +++ b/robot/GCodeParser.js @@ -0,0 +1,83 @@ +class GCodeParser { + + static normalizeInput(raw) { + if (raw === undefined || raw === null) { + return ''; + } + + if (Buffer.isBuffer(raw)) { + raw = raw.toString('utf8'); + } + + return String(raw).trim(); + } + + static splitCommands(input) { + if (!input) { + return []; + } + + // Preserve leading G/M on each segment. + return input + .trim() + .split(/ (?=G|M)/i) + .map(segment => segment.trim()) + .filter(Boolean); + } + + static parseCommand(commandString) { + if (!commandString || commandString.length === 0) { + return null; + } + + const tokens = commandString.trim().split(/\s+/); + if (tokens.length === 0) { + return null; + } + + const commandToken = tokens[0].toUpperCase(); + if (!/^[GM]\d+/i.test(commandToken)) { + return null; + } + + const params = {}; + for (let i = 1; i < tokens.length; i++) { + const token = tokens[i].trim(); + if (token.length < 2) { + continue; + } + + const key = token.charAt(0).toUpperCase(); + const valueString = token.substring(1); + const value = Number(valueString); + if (!Number.isNaN(value)) { + params[key] = value; + } + } + + return { + command: commandToken, + params, + raw: commandString.trim() + }; + } + + static parse(raw) { + const input = this.normalizeInput(raw); + if (!input) { + return []; + } + + if (input.startsWith('$J=')) { + return this.parse(input.substring(3).trim()); + } + + const commands = this.splitCommands(input) + .map(commandString => this.parseCommand(commandString)) + .filter(Boolean); + + return commands; + } +} + +module.exports = GCodeParser; diff --git a/test/GCodeParser.test.js b/test/GCodeParser.test.js new file mode 100644 index 0000000..517dd44 --- /dev/null +++ b/test/GCodeParser.test.js @@ -0,0 +1,46 @@ +const GCodeParser = require('../robot/GCodeParser'); + +describe('GCodeParser', () => { + test('parses a simple G1 command with XYZ parameters', () => { + const parsed = GCodeParser.parse('G1 X10 Y20 Z30'); + + expect(parsed).toEqual([ + { + command: 'G1', + params: { X: 10, Y: 20, Z: 30 }, + raw: 'G1 X10 Y20 Z30' + } + ]); + }); + + test('parses multiple commands in one line', () => { + const parsed = GCodeParser.parse('G90 G21 G1 X20 Y30'); + + expect(parsed.map(cmd => cmd.command)).toEqual(['G90', 'G21', 'G1']); + expect(parsed[2].params).toEqual({ X: 20, Y: 30 }); + }); + + test('parses buffer input as UTF-8', () => { + const parsed = GCodeParser.parse(Buffer.from('G90 G1 X12 Y34 Z56', 'utf8')); + + expect(parsed.length).toBe(2); + expect(parsed[0].command).toBe('G90'); + expect(parsed[1].command).toBe('G1'); + expect(parsed[1].params).toEqual({ X: 12, Y: 34, Z: 56 }); + }); + + test('parses jogging prefix and ignores unsupported G-code', () => { + const parsed = GCodeParser.parse('$J=G91 G1 X2 Y3'); + + expect(parsed).toEqual([ + { command: 'G91', params: {}, raw: 'G91' }, + { command: 'G1', params: { X: 2, Y: 3 }, raw: 'G1 X2 Y3' } + ]); + }); + + test('returns empty array for invalid input', () => { + expect(GCodeParser.parse('X324')).toEqual([]); + expect(GCodeParser.parse('')).toEqual([]); + expect(GCodeParser.parse(undefined)).toEqual([]); + }); +});