From 6d03b3bad9a45d16411f603b959bf31c1dac1723 Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Wed, 7 Apr 2021 15:42:17 +0100 Subject: [PATCH 1/7] update absolute to relative extrusion conversion --- src/gcode_parser/__init__.py | 6 ++ src/gcode_parser/commands.py | 8 ++ src/gcode_parser/gcode_parser.py | 122 +++++++++++++++++++++++++++++++ src/utils.py | 44 +++++------ 4 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 src/gcode_parser/__init__.py create mode 100644 src/gcode_parser/commands.py create mode 100644 src/gcode_parser/gcode_parser.py diff --git a/src/gcode_parser/__init__.py b/src/gcode_parser/__init__.py new file mode 100644 index 0000000..6a2c11c --- /dev/null +++ b/src/gcode_parser/__init__.py @@ -0,0 +1,6 @@ +# Original code from https://github.com/AndyEveritt/GcodeParser + +__version__ = "0.0.6" + +from .gcode_parser import GcodeParser, get_lines +from .commands import Commands diff --git a/src/gcode_parser/commands.py b/src/gcode_parser/commands.py new file mode 100644 index 0000000..2c32492 --- /dev/null +++ b/src/gcode_parser/commands.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class Commands(Enum): + COMMENT = 0 + MOVE = 1 + OTHER = 2 + TOOLCHANGE = 3 diff --git a/src/gcode_parser/gcode_parser.py b/src/gcode_parser/gcode_parser.py new file mode 100644 index 0000000..49e1035 --- /dev/null +++ b/src/gcode_parser/gcode_parser.py @@ -0,0 +1,122 @@ +from typing import List, Dict, Tuple, Union +from dataclasses import dataclass +import re +from .commands import Commands + + +@dataclass +class GcodeLine: + command: Tuple[str, int] + params: Dict[str, float] + comment: str + + def __post_init__(self): + if self.command[0] == 'G' and self.command[1] in (0, 1, 2, 3): + self.type = Commands.MOVE + elif self.command[0] == ';': + self.type = Commands.COMMENT + elif self.command[0] == 'T': + self.type = Commands.TOOLCHANGE + else: + self.type = Commands.OTHER + + @property + def command_str(self): + return f"{self.command[0]}{self.command[1] if self.command[1] is not None else ''}" + + def get_param(self, param: str, return_type=None, default=None): + """ + Returns the value of the param if it exists, otherwise it will the default value. + If `return_type` is set, the return value will be type cast. + """ + try: + if return_type is None: + return self.params[param] + else: + return return_type(self.params[param]) + except KeyError: + return default + + def update_param(self, param: str, value: Union[int, float]): + if self.get_param(param) is None: + return + if type(value) not in (int, float): + raise TypeError(f"Type {type(value)} is not a valid parameter type") + self.params[param] = value + return self.get_param(param) + + def delete_param(self, param: str): + if self.get_param(param) is None: + return + self.params.pop(param) + + @property + def to_gcode(self): + command = self.command_str + params = " ".join(f"{param}{self.get_param(param)}" for param in self.params.keys()) + comment = f"; {self.comment}" if self.comment != '' else "" + if command == ';': + return comment + return f"{command} {params} {comment}".strip() + + +class GcodeParser: + def __init__(self, gcode: str, include_comments=False): + self.gcode = gcode + self.lines: List[GcodeLine] = get_lines(self.gcode, include_comments) + self.include_comments = include_comments + + +def get_lines(gcode, include_comments=False): + regex = r'(?!; *.+)(G|M|T|g|m|t)(\d+)(([ \t]*(?!G|M|g|m)\w(".*"|([-\d\.]*)))*)[ \t]*(;[ \t]*(.*))?|;[ \t]*(.+)' + regex_lines = re.findall(regex, gcode) + lines = [] + for line in regex_lines: + if line[0]: + command = (line[0].upper(), int(line[1])) + comment = line[-2] + params = split_params(line[2]) + + elif include_comments: + command = (';', None) + comment = line[-1] + params = {} + + else: + continue + + lines.append( + GcodeLine( + command=command, + params=params, + comment=comment.strip(), + )) + + return lines + + +def element_type(element: str): + if re.search(r'"', element): + return str + if re.search(r'\..*\.', element): + return str + if re.search(r'[+-]?\d+\.\d+', element): + return float + return int + + +def split_params(line): + regex = r'((?!\d)\w+?)(".*"|(\d+\.?)+|-?\d+\.?\d*)' + elements = re.findall(regex, line) + params = {} + for element in elements: + params[element[0].upper()] = element_type(element[1])(element[1]) + + return params + + +if __name__ == '__main__': + with open('3DBenchy.gcode', 'r') as f: + gcode = f.read() + parsed_gcode = GcodeParser(gcode) + pass diff --git a/src/utils.py b/src/utils.py index 30d7a73..9e07bff 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,8 +1,11 @@ import sys +import re import os import subprocess import time +from .gcode_parser import GcodeParser, Commands + def convert_relative(gcode_abs): """ Converts absolute extrusion gcode into relative extrusion gcode """ @@ -10,46 +13,43 @@ def convert_relative(gcode_abs): last_tool = None last_e = {} # {'tool': last extrusion value} - lines = gcode_abs.split('\n') + lines = GcodeParser(gcode_abs, include_comments=True).lines gcode_rel = '' for line in lines: - if line == '': + if line.command[0] == ';': + gcode_rel += line.to_gcode + '\n' continue - line_start = line.split(' ')[0] # Check for absolute extrusion - if line_start == ('M82'): + if line.command_str == 'M82': absolute_mode = True - line = 'M83' + line.command = ('M', 83) # Check for relative extrusion - elif line_start == ('M83'): + elif line.command_str == 'M83': absolute_mode = False # Check for tool change - elif line_start[0] == 'T': - last_tool = line_start + elif line.command[0] == 'T': + last_tool = line.command_str # Check for extrusion reset - elif line_start == 'G92': - extrusion_reset = line.split('E')[1] + elif line.command_str == 'G92': + extrusion_reset = line.get_param('E') last_e[last_tool] = extrusion_reset # Convert Extrusion coordinates to relative if in absolute mode - if absolute_mode: - if line_start == 'G0' or line_start == 'G1': - try: - line_split = line.split('E') - current_extrusion = line_split[1] - extrusion_diff = float(current_extrusion) - float(last_e[last_tool]) - extrusion_diff = round(extrusion_diff, 3) - last_e[last_tool] = current_extrusion - line = line_split[0] + 'E' + str(extrusion_diff) - except IndexError: - pass - gcode_rel += line + '\n' + if absolute_mode and line.type == Commands.MOVE: + current_extrusion = line.get_param('E') + if current_extrusion is not None: + extrusion_diff = float(current_extrusion) - float(last_e[last_tool]) + extrusion_diff = round(extrusion_diff, 5) + last_e[last_tool] = current_extrusion + line.update_param('E', extrusion_diff) + + gcode_rel += line.to_gcode + '\n' return gcode_rel From b914f9b65b89e27adb12d048e2f02c321fee4a3c Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Wed, 7 Apr 2021 16:29:28 +0100 Subject: [PATCH 2/7] update offset gcode function --- src/utils.py | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/utils.py b/src/utils.py index 9e07bff..e6e6c05 100644 --- a/src/utils.py +++ b/src/utils.py @@ -18,7 +18,7 @@ def convert_relative(gcode_abs): gcode_rel = '' for line in lines: - if line.command[0] == ';': + if line.type == Commands.COMMENT: gcode_rel += line.to_gcode + '\n' continue @@ -55,27 +55,24 @@ def convert_relative(gcode_abs): def offset_gcode(gcode, offset): - gcode_segments = gcode.split(' ') - offset_gcode = '' - for gcode_segment in gcode_segments: - if gcode_segment[0] == 'X': - x_pos = float(gcode_segment[1:]) - x_pos += offset[0] - gcode_segment = gcode_segment[0] + str(x_pos) - - elif gcode_segment[0] == 'Y': - y_pos = float(gcode_segment[1:]) - y_pos += offset[1] - gcode_segment = gcode_segment[0] + str(y_pos) - - elif gcode_segment[0] == 'Z': - z_pos = float(gcode_segment[1:]) - z_pos += offset[2] - gcode_segment = gcode_segment[0] + str(z_pos) - - offset_gcode += ' ' + gcode_segment - - return offset_gcode[1:] + parsed_gcode = GcodeParser(gcode).lines[0] + + if parsed_gcode.get_param('X') is not None: + x_pos = parsed_gcode.get_param('X') + x_pos += offset[0] + parsed_gcode.update_param('X', x_pos) + + if parsed_gcode.get_param('Y') is not None: + y_pos = parsed_gcode.get_param('Y') + y_pos += offset[1] + parsed_gcode.update_param('Y', y_pos) + + if parsed_gcode.get_param('Z') is not None: + z_pos = parsed_gcode.get_param('Z') + z_pos += offset[2] + parsed_gcode.update_param('Z', z_pos) + + return parsed_gcode.to_gcode def find_maxima(numbers): From eed37e5b2411cc45b1b9f7c65d3049058babe3db Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Wed, 7 Apr 2021 16:58:37 +0100 Subject: [PATCH 3/7] update additive layer change detection string --- src/ASMBL_parser.py | 5 ++--- src/additive_gcode.py | 21 +++++++-------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/ASMBL_parser.py b/src/ASMBL_parser.py index a226792..21ac4c5 100644 --- a/src/ASMBL_parser.py +++ b/src/ASMBL_parser.py @@ -90,7 +90,7 @@ def open_files(self, config): def split_additive_layers(self, gcode_add): """ Takes Simplify3D gcode and splits in by layer """ - tmp_list = re.split('(; layer)', gcode_add) + tmp_list = re.split('(; LAYER_CHANGE)', gcode_add) gcode_add_layers = [] initialise_layer = AdditiveGcodeLayer( @@ -105,14 +105,13 @@ def split_additive_layers(self, gcode_add): for i in range(ceil(len(tmp_list)/2)): layer = tmp_list[2*i] + tmp_list[2*i+1] - name = layer.split(',')[0][2:] if 2*i + 1 == len(tmp_list) - 1: gcode_add_layers.append(AdditiveGcodeLayer( layer, 'end', inf)) continue - gcode_add_layers.append(AdditiveGcodeLayer(layer, name)) + gcode_add_layers.append(AdditiveGcodeLayer(layer, None)) return gcode_add_layers diff --git a/src/additive_gcode.py b/src/additive_gcode.py index 6ea1a50..de4568b 100644 --- a/src/additive_gcode.py +++ b/src/additive_gcode.py @@ -1,5 +1,7 @@ from math import inf +from .gcode_parser import GcodeParser, Commands + class AdditiveGcodeLayer: """ Stores a complete layer of gcode produced in Simplify3d """ @@ -11,33 +13,24 @@ def __init__(self, gcode, name=None, layer_height=None): self.remove_park_gcode() - if name is None: - self.name = self.get_name(self.gcode) - if layer_height is None: self.layer_height = self.get_layer_height(self.gcode) - def get_name(self, gcode): - return gcode.split(',')[0][2:] - def get_layer_height(self, gcode): height = None - lines = gcode.split('\n') + lines = GcodeParser(gcode, include_comments=True).lines # Check for Simplify3D end of file code - if lines[0] == '; layer end': + if lines[0].to_gcode == '; layer end': height = inf else: line_heights = [] for line in lines: - if line == '': - continue - if line[0] == ';': + if line.type == Commands.COMMENT: continue - line_segments = line.split('Z') - if len(line_segments) > 1: - line_height = float(line_segments[1].split(' ')[0]) + line_height = line.get_param('Z') + if line_height is not None: line_heights.append(line_height) height = min(line_heights) From fd75365476c491ddfd2da4ce9b85d95ee7a7c81a Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Thu, 22 Jul 2021 18:22:20 +0100 Subject: [PATCH 4/7] Stop detecting layer change comment in initial settings gcode Fixes #50 --- src/ASMBL_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ASMBL_parser.py b/src/ASMBL_parser.py index 21ac4c5..f83b527 100644 --- a/src/ASMBL_parser.py +++ b/src/ASMBL_parser.py @@ -90,7 +90,7 @@ def open_files(self, config): def split_additive_layers(self, gcode_add): """ Takes Simplify3D gcode and splits in by layer """ - tmp_list = re.split('(; LAYER_CHANGE)', gcode_add) + tmp_list = re.split('(\n; LAYER_CHANGE)', gcode_add) gcode_add_layers = [] initialise_layer = AdditiveGcodeLayer( @@ -111,7 +111,7 @@ def split_additive_layers(self, gcode_add): layer, 'end', inf)) continue - gcode_add_layers.append(AdditiveGcodeLayer(layer, None)) + gcode_add_layers.append(AdditiveGcodeLayer(layer, f"FFF layer {i}")) return gcode_add_layers From cb1f5a57dc64f24be38b864c61b2caa81333a896 Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Thu, 22 Jul 2021 18:40:40 +0100 Subject: [PATCH 5/7] change FFF layer change flag Fixes SuperSlicer not impressed by layerchange gcode #51 --- src/ASMBL_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ASMBL_parser.py b/src/ASMBL_parser.py index f83b527..ea99733 100644 --- a/src/ASMBL_parser.py +++ b/src/ASMBL_parser.py @@ -90,7 +90,7 @@ def open_files(self, config): def split_additive_layers(self, gcode_add): """ Takes Simplify3D gcode and splits in by layer """ - tmp_list = re.split('(\n; LAYER_CHANGE)', gcode_add) + tmp_list = re.split('(\n; ASMBL_LAYER_CHANGE_FLAG)', gcode_add) gcode_add_layers = [] initialise_layer = AdditiveGcodeLayer( From a3f22e782d5097c0cb2be357ef05edf69772a89c Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Tue, 27 Jul 2021 16:58:35 +0100 Subject: [PATCH 6/7] add fff layer flag to end gcode --- post_processors/asmbl_fff.cps | 2 ++ 1 file changed, 2 insertions(+) diff --git a/post_processors/asmbl_fff.cps b/post_processors/asmbl_fff.cps index 97accd9..9abc33c 100644 --- a/post_processors/asmbl_fff.cps +++ b/post_processors/asmbl_fff.cps @@ -134,6 +134,7 @@ function getPrinterGeometry() { function onClose() { writeComment("layer end,") + writeComment("ASMBL_LAYER_CHANGE_FLAG") writeComment("END OF GCODE"); // turn off the ESC @@ -224,6 +225,7 @@ function onExtrusionReset(length) { function onLayer(num) { writeComment("layer " + integerFormat.format(num) + " of " + integerFormat.format(layerCount) + ","); // comment format to match Simplify3D + writeComment("ASMBL_LAYER_CHANGE_FLAG"); // layer change flag for ASMBL } // Temp controller not needed for ASMBL From 7b926fd21b78976eee81de9d2782bc3cbba32751 Mon Sep 17 00:00:00 2001 From: AndyEveritt Date: Tue, 27 Jul 2021 17:00:11 +0100 Subject: [PATCH 7/7] fix tool change gcode being added as comment bug --- src/ASMBL_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ASMBL_parser.py b/src/ASMBL_parser.py index ea99733..a48e189 100644 --- a/src/ASMBL_parser.py +++ b/src/ASMBL_parser.py @@ -315,9 +315,9 @@ def tool_change(self, layer, prev_layer): return # no need to add a tool change first_gcode = layer.gcode.split('\n')[1] if first_gcode[0] != 'T': - self.merged_gcode_script += self.last_additive_tool + '\n' + self.merged_gcode_script += f"\n{self.last_additive_tool}\n" elif type(layer) == CamGcodeLayer: - self.merged_gcode_script += layer.tool + '\n' + self.merged_gcode_script += f"\n{layer.tool}\n" def create_output_file(self, gcode, folder_path="output/", relative_path=True): """ Saves the file to the output folder """