From 9d707c523386aa917651af0a5ef9392b5b814b8d Mon Sep 17 00:00:00 2001 From: HelgeKeck Date: Fri, 3 May 2024 03:11:18 +0200 Subject: [PATCH] host post processor --- klippy/ratos_post_processor.py | 361 ++++++++++++++++++++++++++++ macros/idex/overrides.cfg | 12 + printers/caramba-idex/caramba.cfg | 4 +- printers/caramba-idex/macros.cfg | 1 + printers/v-core-3-idex/macros.cfg | 1 + printers/v-core-3-idex/v-core-3.cfg | 4 +- scripts/ratos-common.sh | 9 + scripts/ratos-install.sh | 1 + scripts/ratos-update.sh | 1 + 9 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 klippy/ratos_post_processor.py diff --git a/klippy/ratos_post_processor.py b/klippy/ratos_post_processor.py new file mode 100644 index 000000000..0ab04ce0e --- /dev/null +++ b/klippy/ratos_post_processor.py @@ -0,0 +1,361 @@ +# RatOS IDEX and RMMU Gcode Post Processor +# +# Copyright (C) 2024 Helge Magnus Keck +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from math import fabs +from shutil import ReadError, copy2 +from os import path, remove, getenv +import os, logging, io + +##### +# RMMU Hub +##### +class RatOS_Post_Processor: + + ##### + # Initialize + ##### + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name() + self.gcode = self.printer.lookup_object('gcode') + self.reactor = self.printer.get_reactor() + + self.register_commands() + self.register_handler() + + def get_status(self, eventtime): + return {'name': self.name} + + ##### + # Handler + ##### + def register_handler(self): + self.printer.register_event_handler("klippy:connect", self._connect) + + def _connect(self): + self.v_sd = self.printer.lookup_object('virtual_sdcard', None) + self.sdcard_dirname = self.v_sd.sdcard_dirname + self.dual_carriage = self.printer.lookup_object("dual_carriage", None) + self.rmmu_hub = self.printer.lookup_object("rmmu_hub", None) + + ##### + # Gcode commands + ##### + def register_commands(self): + self.gcode.register_command('RATOS_POST_PROCESSOR', self.cmd_RATOS_POST_PROCESSOR, desc=(self.desc_RATOS_POST_PROCESSOR)) + + desc_RATOS_POST_PROCESSOR = "" + def cmd_RATOS_POST_PROCESSOR(self, gcmd): + if self.dual_carriage == None and self.rmmu_hub == None: + self.v_sd.cmd_SDCARD_PRINT_FILE(gcmd) + else: + filename = gcmd.get('FILENAME', "") + if filename[0] == '/': + filename = filename[1:] + if self.process_file(filename): + self.v_sd.cmd_SDCARD_PRINT_FILE(gcmd) + else: + raise self.printer.command_error("Could not process gcode file") + + ##### + # Post Processor + ##### + def process_file(self, filename): + path = self.get_file_path(filename) + lines = self.get_file_lines(path) + + if self.already_processed(path): + return True + + self.ratos_echo("processing...") + + slicer = self.get_slicer(lines[0].rstrip()) + + if slicer["Name"] != "PrusaSlicer" and slicer["Name"] != "SuperSlicer": + raise self.printer.command_error("Unsupported Slicer") + + min_x = 1000 + max_x = 0 + first_x = -1 + first_y = -1 + toolshift_count = 0 + tower_line = -1 + start_print_line = 0 + file_has_changed = False + wipe_accel = 0 + used_tools = [] + pause_counter = 0 + for line in range(len(lines)): + # give the cpu some time + pause_counter += 1 + if pause_counter == 1000: + pause_counter = 0 + self.reactor.pause(.001) + + # get slicer profile settings + if slicer["Name"] == "PrusaSlicer": + if wipe_accel == 0: + if lines[line].rstrip().startswith("; wipe_tower_acceleration = "): + wipe_accel = int(lines[line].rstrip().replace("; wipe_tower_acceleration = ", "")) + + # get the start_print line number + if start_print_line == 0: + if lines[line].rstrip().startswith("START_PRINT") or lines[line].rstrip().startswith("RMMU_START_PRINT"): + lines[line] = lines[line].replace("#", "") # fix color variable format + start_print_line = line + + # count toolshifts + if start_print_line > 0: + if lines[line].rstrip().startswith("T") and lines[line].rstrip()[1:].isdigit(): + if toolshift_count == 0: + lines[line] = '' # remove first toolchange + toolshift_count += 1 + + # get first tools usage in order + if start_print_line > 0: + if len(used_tools) == 0: + index = lines[start_print_line].rstrip().find("INITIAL_TOOL=") + if index != -1: + used_tools.append(lines[start_print_line].rstrip()[index + len("INITIAL_TOOL="):].split()[0]) + if lines[line].rstrip().startswith("T") and lines[line].rstrip()[1:].isdigit(): + # add tool to the list if not already added + t = lines[line].rstrip()[1:] + if t not in used_tools: + used_tools.append(t) + + # get first XY coordinates + if start_print_line > 0 and first_x < 0 and first_y < 0: + if lines[line].rstrip().startswith("G1") or lines[line].rstrip().startswith("G0"): + split = lines[line].rstrip().replace(" ", " ").split(" ") + for s in range(len(split)): + if split[s].lower().startswith("x"): + try: + x = float(split[s].lower().replace("x", "")) + if x > first_x: + first_x = x + except Exception as exc: + self.ratos_echo("Can not get first x coordinate. " + str(exc)) + return False + if split[s].lower().startswith("y"): + try: + y = float(split[s].lower().replace("y", "")) + if y > first_y: + first_y = y + except Exception as exc: + self.ratos_echo("Can not get first y coordinate. " + str(exc)) + return False + + # get x boundaries + if start_print_line > 0: + if lines[line].rstrip().startswith("G1") or lines[line].rstrip().startswith("G0"): + split = lines[line].rstrip().replace(" ", " ").split(" ") + for s in range(len(split)): + if split[s].lower().startswith("x"): + try: + x = float(split[s].lower().replace("x", "")) + if x < min_x: + min_x = x + if x > max_x: + max_x = x + except Exception as exc: + self.ratos_echo("Can not get x boundaries . " + str(exc)) + return False + + # toolshift processing + if start_print_line > 0: + if lines[line].rstrip().startswith("T") and lines[line].rstrip()[1:].isdigit(): + + # purge tower + if tower_line == -1: + tower_line = 0 + for i2 in range(20): + if lines[line-i2].rstrip().startswith("; CP TOOLCHANGE START"): + tower_line = line-i2 + break + + # z-hop before toolchange + zhop = 0 + zhop_line = 0 + if tower_line == 0: + for i2 in range(20): + if lines[line-i2].rstrip().startswith("; custom gcode: end_filament_gcode"): + if lines[line-i2-1].rstrip().startswith("G1 Z"): + split = lines[line-i2-1].rstrip().split(" ") + if split[1].startswith("Z"): + zhop = float(split[1].replace("Z", "")) + if zhop > 0.0: + zhop_line = line-i2-1 + + # toolchange line + toolchange_line = 0 + for i2 in range(20): + if lines[line + i2].rstrip().startswith("T") and lines[line].rstrip()[1:].isdigit(): + toolchange_line = line + i2 + break + + # retraction after toolchange + retraction_line = 0 + if tower_line == 0 and toolchange_line > 0: + for i2 in range(20): + if lines[toolchange_line + i2].rstrip().startswith("G1 E-"): + retraction_line = toolchange_line + i2 + break + + # move after toolchange + move_x = '' + move_y = '' + move_line = 0 + if toolchange_line > 0: + for i2 in range(20): + if lines[toolchange_line + i2].rstrip().replace(" ", " ").startswith("G1 X"): + splittedstring = lines[toolchange_line + i2].rstrip().replace(" ", " ").split(" ") + if splittedstring[1].startswith("X"): + if splittedstring[2].startswith("Y"): + move_x = splittedstring[1].rstrip() + move_y = splittedstring[2].rstrip() + move_line = toolchange_line + i2 + break + + # z-drop after toolchange + move_z = '' + zdrop_line = 0 + if tower_line == 0: + if lines[move_line + 1].rstrip().startswith("G1 Z"): + zdrop_line = move_line + 1 + elif lines[move_line + 2].rstrip().startswith("G1 Z"): + zdrop_line = move_line + 2 + if zdrop_line > 0: + split = lines[zdrop_line].rstrip().split(" ") + if split[1].startswith("Z"): + move_z = split[1].rstrip() + + # extrusion after move + extrusion_line = 0 + if tower_line == 0 and move_line > 0: + for i2 in range(5): + if lines[move_line + i2].rstrip().startswith("G1 E"): + extrusion_line = move_line + i2 + break + + # make toolshift changes + if toolshift_count > 0 and toolchange_line > 0 and move_line > 0: + file_has_changed = True + + if zhop_line > 0: + lines[zhop_line] = '; Z-Hop removed by RatOS IDEX Postprocessor: ' + lines[zhop_line].rstrip() + '\n' + + if zdrop_line > 0: + lines[zdrop_line] = '; Z-Drop removed by RatOS IDEX Postprocessor: ' + lines[zdrop_line].rstrip() + '\n' + + new_toolchange_gcode = ('TOOL T=' + lines[toolchange_line].rstrip().replace("T", "") + ' ' + move_x.replace("X", "X=") + ' ' + move_y.replace("Y", "Y=") + ' ' + move_z.replace("Z", "Z=")).rstrip() + lines[toolchange_line] = new_toolchange_gcode + '\n' + lines[move_line] = '; Horizontal move removed by RatOS IDEX Postprocessor: ' + lines[move_line].rstrip().replace(" ", " ") + '\n' + + if retraction_line > 0 and extrusion_line > 0: + lines[retraction_line] = '; Retraction removed by RatOS IDEX Postprocessor: ' + lines[retraction_line].rstrip() + '\n' + lines[extrusion_line] = '; Deretraction removed by RatOS IDEX Postprocessor: ' + lines[extrusion_line].rstrip() + '\n' + + # add START_PRINT parameters + if start_print_line > 0: + if toolshift_count > 0: + file_has_changed = True + lines[start_print_line] = lines[start_print_line].rstrip() + ' TOTAL_TOOLSHIFTS=' + str(toolshift_count - 1) + '\n' + if first_x >= 0 and first_y >= 0: + file_has_changed = True + lines[start_print_line] = lines[start_print_line].rstrip() + ' FIRST_X=' + str(first_x) + ' FIRST_Y=' + str(first_y) + '\n' + if min_x < 1000: + file_has_changed = True + lines[start_print_line] = lines[start_print_line].rstrip() + ' MIN_X=' + str(min_x) + ' MAX_X=' + str(max_x) + '\n' + if len(used_tools) > 0: + file_has_changed = True + lines[start_print_line] = lines[start_print_line].rstrip() + ' USED_TOOLS=' + ','.join(used_tools) + '\n' + lines[start_print_line] = lines[start_print_line].rstrip() + ' WIPE_ACCEL=' + str(wipe_accel) + '\n' + + # console output + self.ratos_echo("USED TOOLS: " + ','.join(used_tools)) + self.ratos_echo("TOOLSHIFTS: " + str(0 if toolshift_count == 0 else toolshift_count - 1)) + self.ratos_echo("SLICER: " + slicer["Name"] + " " + slicer["Version"]) + + # save file if it has changed + if file_has_changed: + lines[1] = lines[1].rstrip()+ "; processed by RatOS\n" + self.save_file(path, lines) + + self.ratos_echo("Done!") + return True + + def already_processed(self, path): + readfile = None + try: + i = 0 + with open(path, 'r', encoding='utf-8') as readfile: + for line in readfile: + i += 1 + if i == 2: + return line.rstrip().lower().startswith("; processed by ratos") + except: + raise self.printer.command_error("Can not get processed state") + finally: + readfile.close() + + def get_slicer(self, line): + try: + line = line.rstrip().replace("; generated by ", "") + split = line.split(" on ")[0] + return {"Name": split.split(" ")[0], "Version": split.split(" ")[1]} + except: + raise self.printer.command_error("Can not get slicer version") + + def get_file_path(self, filename): + files = self.v_sd.get_file_list(True) + flist = [f[0] for f in files] + files_by_lower = { filepath.lower(): filepath for filepath, fsize in files } + filepath = filename + try: + if filepath not in flist: + filepath = files_by_lower[filepath.lower()] + return os.path.join(self.sdcard_dirname, filepath) + except: + raise self.printer.command_error("Can not get path for file " + filename) + + def get_file_lines(self, filepath): + try: + with open(filepath, "r", encoding='UTF-8') as readfile: + return readfile.readlines() + except: + raise self.printer.command_error("Unable to open file") + finally: + readfile.close() + + def save_file(self, path, lines): + writefile = None + try: + pause_counter = 0 + with open(path, "w", newline='\n', encoding='UTF-8') as writefile: + for i, strline in enumerate(lines): + pause_counter += 1 + if pause_counter == 1000: + pause_counter = 0 + self.reactor.pause(.001) + writefile.write(strline) + except Exception as exc: + raise self.printer.command_error("FileWriteError: " + str(exc)) + finally: + writefile.close() + + ##### + # Helper + ##### + def ratos_echo(self, msg): + self.gcode.run_script_from_command("RATOS_ECHO PREFIX='POST_PROCESSOR' MSG='" + str(msg) + "'") + + def ratos_debug_echo(self, msg): + self.gcode.run_script_from_command("DEBUG_ECHO PREFIX='POST_PROCESSOR' MSG='" + str(msg) + "'") + +##### +# Loader +##### +def load_config(config): + return RatOS_Post_Processor(config) diff --git a/macros/idex/overrides.cfg b/macros/idex/overrides.cfg index ceac5a3bd..3b9611236 100644 --- a/macros/idex/overrides.cfg +++ b/macros/idex/overrides.cfg @@ -56,6 +56,7 @@ gcode: {% endif %} + [gcode_macro M106] # Only rename_existing if you have a sacrificial [fan] section rename_existing: M106.1 @@ -114,8 +115,19 @@ gcode: # Update core Klipper's fan speed M106.1 S{s} + [gcode_macro M107] rename_existing: M107.1 gcode: {% set p = params.P|default(-1)|int %} M106 S0 P{p} + + +[gcode_macro SDCARD_PRINT_FILE] +rename_existing: SDCARD_PRINT_FILE_BASE +gcode: + {% if printer["ratos_post_processor"] is defined %} + RATOS_POST_PROCESSOR { rawparams } + {% else %} + SDCARD_PRINT_FILE_BASE { rawparams } + {% endif %} diff --git a/printers/caramba-idex/caramba.cfg b/printers/caramba-idex/caramba.cfg index 4bbf6eaff..bc0c11094 100644 --- a/printers/caramba-idex/caramba.cfg +++ b/printers/caramba-idex/caramba.cfg @@ -38,4 +38,6 @@ inverted: true [dual_carriage] axis: x -safe_distance: 70 \ No newline at end of file +safe_distance: 70 + +[ratos_post_processor] diff --git a/printers/caramba-idex/macros.cfg b/printers/caramba-idex/macros.cfg index fc4fb27e9..0b1e79209 100644 --- a/printers/caramba-idex/macros.cfg +++ b/printers/caramba-idex/macros.cfg @@ -14,6 +14,7 @@ variable_safe_z: 20 # safe z height for VAOC start and en [delayed_gcode _IDEX_INIT] initial_duration: 0.1 gcode: + ENABLE_DEBUG # ensure IDEX homing order SET_GCODE_VARIABLE MACRO=RatOS VARIABLE=home_y_first VALUE=True # ensure inverted hybrid corexy configuration diff --git a/printers/v-core-3-idex/macros.cfg b/printers/v-core-3-idex/macros.cfg index dcc8fefd3..f1b8a511d 100644 --- a/printers/v-core-3-idex/macros.cfg +++ b/printers/v-core-3-idex/macros.cfg @@ -13,6 +13,7 @@ variable_is_fixed: False # true = VAOC camera is fixed on th [delayed_gcode _IDEX_INIT] initial_duration: 0.1 gcode: + ENABLE_DEBUG # ensure IDEX homing order SET_GCODE_VARIABLE MACRO=RatOS VARIABLE=home_y_first VALUE=True # ensure inverted hybrid corexy configuration diff --git a/printers/v-core-3-idex/v-core-3.cfg b/printers/v-core-3-idex/v-core-3.cfg index 4bbf6eaff..bc0c11094 100644 --- a/printers/v-core-3-idex/v-core-3.cfg +++ b/printers/v-core-3-idex/v-core-3.cfg @@ -38,4 +38,6 @@ inverted: true [dual_carriage] axis: x -safe_distance: 70 \ No newline at end of file +safe_distance: 70 + +[ratos_post_processor] diff --git a/scripts/ratos-common.sh b/scripts/ratos-common.sh index f3459b4ec..6f16468dd 100755 --- a/scripts/ratos-common.sh +++ b/scripts/ratos-common.sh @@ -112,6 +112,15 @@ register_z_offset_probe() _register_klippy_extension $EXT_NAME "$EXT_PATH" $EXT_FILE "false" } +register_ratos_post_processor() +{ + EXT_NAME="ratos_post_processor_extension" + EXT_PATH=$(realpath "$SCRIPT_DIR"/../klippy) + EXT_FILE="ratos_post_processor.py" + # Don't error if extension is already registered + _register_klippy_extension $EXT_NAME "$EXT_PATH" $EXT_FILE "false" +} + install_hooks() { report_status "Installing git hooks" diff --git a/scripts/ratos-install.sh b/scripts/ratos-install.sh index 06dc79bdf..7a9102813 100755 --- a/scripts/ratos-install.sh +++ b/scripts/ratos-install.sh @@ -49,4 +49,5 @@ ensure_sudo_command_whitelisting sudo register_gcode_shell_command register_ratos_homing register_z_offset_probe +register_ratos_post_processor register_resonance_generator \ No newline at end of file diff --git a/scripts/ratos-update.sh b/scripts/ratos-update.sh index e5e5ad73d..d502d5ce8 100755 --- a/scripts/ratos-update.sh +++ b/scripts/ratos-update.sh @@ -57,6 +57,7 @@ install_beacon install_hooks ensure_node_18 register_z_offset_probe +register_ratos_post_processor register_resonance_generator symlink_extensions restart_configurator \ No newline at end of file