diff --git a/files/cartographer/alter_config.py b/files/cartographer/alter_config.py new file mode 100644 index 0000000..258e1c0 --- /dev/null +++ b/files/cartographer/alter_config.py @@ -0,0 +1,95 @@ +import logging +import os +import re + +def remove_section_from_ini(input_file: str, section_to_remove: str, backup_dir: str = "backup") -> tuple[bool, str]: + """ + Remove a section from an INI file and save the removed section separately, preserving comments. + + Args: + input_file: Path to the input INI file + section_to_remove: Name of the section to remove + backup_dir: Directory to store the removed section (default: "backup") + + Returns: + tuple: (success: bool, message: str) + """ + try: + input_file = os.path.expanduser(input_file) + backup_dir = os.path.expanduser(backup_dir) + + logging.info(f"Backup directory: {backup_dir}") + # Create backup directory if it doesn't exist + os.makedirs(backup_dir, exist_ok=True) + + + # Read the original file content + with open(input_file, 'r') as f: + lines = f.readlines() + logging.info("Read original file content") + + # Initialize variables + current_section = None + removed_lines = [] + kept_lines = [] + in_target_section = False + found_section = False + + # Process the file line by line + for line in lines: + # Check for section header + section_match = re.match(r'^\s*\[(.*?)\]\s*$', line) + if section_match: + current_section = section_match.group(1) + in_target_section = (current_section == section_to_remove) + if in_target_section: + found_section = True + + # Add line to appropriate list + if in_target_section: + removed_lines.append(line) + else: + kept_lines.append(line) + + if not found_section: + return False, f"Section '{section_to_remove}' not found in {input_file}" + + backup_file = os.path.join( + backup_dir, + f"{section_to_remove}.cfg" + ) + + # Save the removed section + with open(backup_file, 'w') as f: + f.writelines(removed_lines) + + # Save the modified file + with open(input_file, 'w') as f: + f.writelines(kept_lines) + + return True, f"Section removed successfully. Backup saved to {backup_file}" + + except Exception as e: + return False, f"Error: {str(e)}" + +def main(): + """ + Example usage of the remove_section_from_ini function. + """ + # Example usage + printer_data_dir = os.environ.get("PRINTER_DATA_DIR") + if not printer_data_dir: + if os.path.isdir("/mnt/UDISK/printer_data"): + printer_data_dir = "/mnt/UDISK/printer_data" + else: + printer_data_dir = os.path.expanduser("~/printer_data") + + input_file = os.path.join(printer_data_dir, "config/printer.cfg") + section_to_remove = "prtouch_v3" + backup_dir = os.path.join(printer_data_dir, "config/custom") + + success, message = remove_section_from_ini(input_file, section_to_remove, backup_dir) + print(message) + +if __name__ == "__main__": + main() diff --git a/files/cartographer/bed_mesh.py b/files/cartographer/bed_mesh.py new file mode 100644 index 0000000..bcfd7c7 --- /dev/null +++ b/files/cartographer/bed_mesh.py @@ -0,0 +1,1763 @@ +# Mesh Bed Leveling +# +# Copyright (C) 2018-2019 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging, math, json, collections +from . import probe + +PROFILE_VERSION = 1 +PROFILE_OPTIONS = { + 'min_x': float, 'max_x': float, 'min_y': float, 'max_y': float, + 'x_count': int, 'y_count': int, 'mesh_x_pps': int, 'mesh_y_pps': int, + 'algo': str, 'tension': float +} + +class BedMeshError(Exception): + pass + +# PEP 485 isclose() +def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): + return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + +# return true if a coordinate is within the region +# specified by min_c and max_c +def within(coord, min_c, max_c, tol=0.0): + return (max_c[0] + tol) >= coord[0] >= (min_c[0] - tol) and \ + (max_c[1] + tol) >= coord[1] >= (min_c[1] - tol) + +# Constrain value between min and max +def constrain(val, min_val, max_val): + return min(max_val, max(min_val, val)) + +# Linear interpolation between two values +def lerp(t, v0, v1): + return (1. - t) * v0 + t * v1 + +# retreive commma separated pair from config +def parse_config_pair(config, option, default, minval=None, maxval=None): + pair = config.getintlist(option, (default, default)) + if len(pair) != 2: + if len(pair) != 1: + raise config.error("bed_mesh: malformed '%s' value: %s" + % (option, config.get(option))) + pair = (pair[0], pair[0]) + if minval is not None: + if pair[0] < minval or pair[1] < minval: + raise config.error( + "Option '%s' in section bed_mesh must have a minimum of %s" + % (option, str(minval))) + if maxval is not None: + if pair[0] > maxval or pair[1] > maxval: + raise config.error( + "Option '%s' in section bed_mesh must have a maximum of %s" + % (option, str(maxval))) + return pair + +# retreive commma separated pair from a g-code command +def parse_gcmd_pair(gcmd, name, minval=None, maxval=None): + try: + pair = [int(v.strip()) for v in gcmd.get(name).split(',')] + except: + raise gcmd.error("Unable to parse parameter '%s'" % (name,)) + if len(pair) != 2: + if len(pair) != 1: + raise gcmd.error("Unable to parse parameter '%s'" % (name,)) + pair = (pair[0], pair[0]) + if minval is not None: + if pair[0] < minval or pair[1] < minval: + raise gcmd.error("Parameter '%s' must have a minimum of %d" + % (name, minval)) + if maxval is not None: + if pair[0] > maxval or pair[1] > maxval: + raise gcmd.error("Parameter '%s' must have a maximum of %d" + % (name, maxval)) + return pair + +# retreive commma separated coordinate from a g-code command +def parse_gcmd_coord(gcmd, name): + try: + v1, v2 = [float(v.strip()) for v in gcmd.get(name).split(',')] + except: + raise gcmd.error("Unable to parse parameter '%s'" % (name,)) + return v1, v2 + + +class BedMesh: + FADE_DISABLE = 0x7FFFFFFF + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + self.last_position = [0., 0., 0., 0.] + self.bmc = BedMeshCalibrate(config, self) + self.z_mesh = None + self.toolhead = None + self.horizontal_move_z = config.getfloat('horizontal_move_z', 5.) + self.fade_start = config.getfloat('fade_start', 1.) + self.fade_end = config.getfloat('fade_end', 0.) + self.fade_dist = self.fade_end - self.fade_start + if self.fade_dist <= 0.: + self.fade_start = self.fade_end = self.FADE_DISABLE + self.log_fade_complete = False + self.base_fade_target = config.getfloat('fade_target', None) + self.fade_target = 0. + self.tool_offset = 0. + self.gcode = self.printer.lookup_object('gcode') + self.splitter = MoveSplitter(config, self.gcode) + # setup persistent storage + self.pmgr = ProfileManager(config, self) + self.save_profile = self.pmgr.save_profile + # register gcodes + self.gcode.register_command( + 'BED_MESH_OUTPUT', self.cmd_BED_MESH_OUTPUT, + desc=self.cmd_BED_MESH_OUTPUT_help) + self.gcode.register_command( + 'BED_MESH_MAP', self.cmd_BED_MESH_MAP, + desc=self.cmd_BED_MESH_MAP_help) + self.gcode.register_command( + 'BED_MESH_CLEAR', self.cmd_BED_MESH_CLEAR, + desc=self.cmd_BED_MESH_CLEAR_help) + self.gcode.register_command( + 'BED_MESH_OFFSET', self.cmd_BED_MESH_OFFSET, + desc=self.cmd_BED_MESH_OFFSET_help) + # Register dump webhooks + webhooks = self.printer.lookup_object('webhooks') + webhooks.register_endpoint( + "bed_mesh/dump_mesh", self._handle_dump_request + ) + # Register transform + gcode_move = self.printer.load_object(config, 'gcode_move') + gcode_move.set_move_transform(self) + # initialize status dict + self.update_status() + def handle_connect(self): + self.toolhead = self.printer.lookup_object('toolhead') + self.bmc.print_generated_points(logging.info) + def set_mesh(self, mesh): + if mesh is not None and self.fade_end != self.FADE_DISABLE: + self.log_fade_complete = True + if self.base_fade_target is None: + self.fade_target = mesh.get_z_average() + else: + self.fade_target = self.base_fade_target + min_z, max_z = mesh.get_z_range() + if (not min_z <= self.fade_target <= max_z and + self.fade_target != 0.): + # fade target is non-zero, out of mesh range + err_target = self.fade_target + self.z_mesh = None + self.fade_target = 0. + raise self.gcode.error( + "bed_mesh: ERROR, fade_target lies outside of mesh z " + "range\nmin: %.4f, max: %.4f, fade_target: %.4f" + % (min_z, max_z, err_target)) + min_z, max_z = mesh.get_z_range() + if self.fade_dist <= max(abs(min_z), abs(max_z)): + self.z_mesh = None + self.fade_target = 0. + raise self.gcode.error( + "bed_mesh: Mesh extends outside of the fade range, " + "please see the fade_start and fade_end options in" + "example-extras.cfg. fade distance: %.2f mesh min: %.4f" + "mesh max: %.4f" % (self.fade_dist, min_z, max_z)) + else: + self.fade_target = 0. + self.tool_offset = 0. + self.z_mesh = mesh + self.splitter.initialize(mesh, self.fade_target) + # cache the current position before a transform takes place + gcode_move = self.printer.lookup_object('gcode_move') + gcode_move.reset_last_position() + self.update_status() + def get_z_factor(self, z_pos): + z_pos += self.tool_offset + if z_pos >= self.fade_end: + return 0. + elif z_pos >= self.fade_start: + return (self.fade_end - z_pos) / self.fade_dist + else: + return 1. + def get_position(self): + # Return last, non-transformed position + if self.z_mesh is None: + # No mesh calibrated, so send toolhead position + self.last_position[:] = self.toolhead.get_position() + self.last_position[2] -= self.fade_target + else: + # return current position minus the current z-adjustment + x, y, z, e = self.toolhead.get_position() + max_adj = self.z_mesh.calc_z(x, y) + factor = 1. + z_adj = max_adj - self.fade_target + fade_z_pos = z + self.tool_offset + if min(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_end: + # Fade out is complete, no factor + factor = 0. + elif max(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_start: + # Likely in the process of fading out adjustment. + # Because we don't yet know the gcode z position, use + # algebra to calculate the factor from the toolhead pos + factor = ((self.fade_end + self.fade_target - fade_z_pos) / + (self.fade_dist - z_adj)) + factor = constrain(factor, 0., 1.) + final_z_adj = factor * z_adj + self.fade_target + self.last_position[:] = [x, y, z - final_z_adj, e] + return list(self.last_position) + def move(self, newpos, speed): + factor = self.get_z_factor(newpos[2]) + if self.z_mesh is None or not factor: + # No mesh calibrated, or mesh leveling phased out. + x, y, z, e = newpos + if self.log_fade_complete: + self.log_fade_complete = False + logging.info( + "bed_mesh fade complete: Current Z: %.4f fade_target: %.4f " + % (z, self.fade_target)) + self.toolhead.move([x, y, z + self.fade_target, e], speed) + else: + self.splitter.build_move(self.last_position, newpos, factor) + while not self.splitter.traverse_complete: + split_move = self.splitter.split() + if split_move: + self.toolhead.move(split_move, speed) + else: + raise self.gcode.error( + "Mesh Leveling: Error splitting move ") + self.last_position[:] = newpos + def get_status(self, eventtime=None): + return self.status + def update_status(self): + self.status = { + "profile_name": "", + "mesh_min": (0., 0.), + "mesh_max": (0., 0.), + "probed_matrix": [[]], + "mesh_matrix": [[]], + "profiles": self.pmgr.get_profiles() + } + if self.z_mesh is not None: + params = self.z_mesh.get_mesh_params() + mesh_min = (params['min_x'], params['min_y']) + mesh_max = (params['max_x'], params['max_y']) + probed_matrix = self.z_mesh.get_probed_matrix() + mesh_matrix = self.z_mesh.get_mesh_matrix() + self.status['profile_name'] = self.z_mesh.get_profile_name() + self.status['mesh_min'] = mesh_min + self.status['mesh_max'] = mesh_max + self.status['probed_matrix'] = probed_matrix + self.status['mesh_matrix'] = mesh_matrix + def get_mesh(self): + return self.z_mesh + cmd_BED_MESH_OUTPUT_help = "Retrieve interpolated grid of probed z-points" + def cmd_BED_MESH_OUTPUT(self, gcmd): + if gcmd.get_int('PGP', 0): + # Print Generated Points instead of mesh + self.bmc.print_generated_points(gcmd.respond_info) + elif self.z_mesh is None: + gcmd.respond_info("Bed has not been probed") + else: + self.z_mesh.print_probed_matrix(gcmd.respond_info) + self.z_mesh.print_mesh(gcmd.respond_raw, self.horizontal_move_z) + cmd_BED_MESH_MAP_help = "Serialize mesh and output to terminal" + def cmd_BED_MESH_MAP(self, gcmd): + if self.z_mesh is not None: + params = self.z_mesh.get_mesh_params() + outdict = { + 'mesh_min': (params['min_x'], params['min_y']), + 'mesh_max': (params['max_x'], params['max_y']), + 'z_positions': self.z_mesh.get_probed_matrix()} + gcmd.respond_raw("mesh_map_output " + json.dumps(outdict)) + else: + gcmd.respond_info("Bed has not been probed") + cmd_BED_MESH_CLEAR_help = "Clear the Mesh so no z-adjustment is made" + def cmd_BED_MESH_CLEAR(self, gcmd): + self.set_mesh(None) + cmd_BED_MESH_OFFSET_help = "Add X/Y offsets to the mesh lookup" + def cmd_BED_MESH_OFFSET(self, gcmd): + if self.z_mesh is not None: + offsets = [None, None] + for i, axis in enumerate(['X', 'Y']): + offsets[i] = gcmd.get_float(axis, None) + self.z_mesh.set_mesh_offsets(offsets) + tool_offset = gcmd.get_float("ZFADE", None) + if tool_offset is not None: + self.tool_offset = tool_offset + gcode_move = self.printer.lookup_object('gcode_move') + gcode_move.reset_last_position() + else: + gcmd.respond_info("No mesh loaded to offset") + def _handle_dump_request(self, web_request): + eventtime = self.printer.get_reactor().monotonic() + prb = self.printer.lookup_object("probe", None) + th_sts = self.printer.lookup_object("toolhead").get_status(eventtime) + result = {"current_mesh": {}, "profiles": self.pmgr.get_profiles()} + if self.z_mesh is not None: + result["current_mesh"] = { + "name": self.z_mesh.get_profile_name(), + "probed_matrix": self.z_mesh.get_probed_matrix(), + "mesh_matrix": self.z_mesh.get_mesh_matrix(), + "mesh_params": self.z_mesh.get_mesh_params() + } + mesh_args = web_request.get_dict("mesh_args", {}) + gcmd = None + if mesh_args: + gcmd = self.gcode.create_gcode_command("", "", mesh_args) + with self.gcode.get_mutex(): + result["calibration"] = self.bmc.dump_calibration(gcmd) + else: + result["calibration"] = self.bmc.dump_calibration() + offsets = [0, 0, 0] if prb is None else prb.get_offsets() + result["probe_offsets"] = offsets + result["axis_minimum"] = th_sts["axis_minimum"] + result["axis_maximum"] = th_sts["axis_maximum"] + web_request.send(result) + + +class ZrefMode: + DISABLED = 0 # Zero reference disabled + IN_MESH = 1 # Zero reference position within mesh + PROBE = 2 # Zero refrennce position outside of mesh, probe needed + + +class BedMeshCalibrate: + ALGOS = ['lagrange', 'bicubic'] + def __init__(self, config, bedmesh): + self.printer = config.get_printer() + self.orig_config = {'radius': None, 'origin': None} + self.radius = self.origin = None + self.mesh_min = self.mesh_max = (0., 0.) + self.adaptive_margin = config.getfloat('adaptive_margin', 0.0) + self.bedmesh = bedmesh + self.mesh_config = collections.OrderedDict() + self._init_mesh_config(config) + self.probe_mgr = ProbeManager( + config, self.orig_config, self.probe_finalize + ) + try: + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin + ) + except BedMeshError as e: + raise config.error(str(e)) + self._profile_name = "default" + self.gcode = self.printer.lookup_object('gcode') + self.gcode.register_command( + 'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE, + desc=self.cmd_BED_MESH_CALIBRATE_help) + def print_generated_points(self, print_func): + x_offset = y_offset = 0. + probe = self.printer.lookup_object('probe', None) + if probe is not None: + x_offset, y_offset = probe.get_offsets()[:2] + print_func("bed_mesh: generated points\nIndex" + " | Tool Adjusted | Probe") + points = self.probe_mgr.get_base_points() + for i, (x, y) in enumerate(points): + adj_pt = "(%.1f, %.1f)" % (x - x_offset, y - y_offset) + mesh_pt = "(%.1f, %.1f)" % (x, y) + print_func( + " %-4d| %-16s| %s" % (i, adj_pt, mesh_pt)) + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + if zero_ref_pos is not None: + print_func( + "bed_mesh: zero_reference_position is (%.2f, %.2f)" + % (zero_ref_pos[0], zero_ref_pos[1]) + ) + substitutes = self.probe_mgr.get_substitutes() + if substitutes: + print_func("bed_mesh: faulty region points") + for i, v in substitutes.items(): + pt = points[i] + print_func("%d (%.2f, %.2f), substituted points: %s" + % (i, pt[0], pt[1], repr(v))) + def _init_mesh_config(self, config): + mesh_cfg = self.mesh_config + orig_cfg = self.orig_config + self.radius = config.getfloat('mesh_radius', None, above=0.) + if self.radius is not None: + self.origin = config.getfloatlist('mesh_origin', (0., 0.), count=2) + x_cnt = y_cnt = config.getint('round_probe_count', 5, minval=3) + # round beds must have an odd number of points along each axis + if not x_cnt & 1: + raise config.error( + "bed_mesh: probe_count must be odd for round beds") + # radius may have precision to .1mm + self.radius = math.floor(self.radius * 10) / 10 + orig_cfg['radius'] = self.radius + orig_cfg['origin'] = self.origin + min_x = min_y = -self.radius + max_x = max_y = self.radius + else: + # rectangular + x_cnt, y_cnt = parse_config_pair(config, 'probe_count', 3, minval=3) + min_x, min_y = config.getfloatlist('mesh_min', count=2) + max_x, max_y = config.getfloatlist('mesh_max', count=2) + if max_x <= min_x or max_y <= min_y: + raise config.error('bed_mesh: invalid min/max points') + orig_cfg['x_count'] = mesh_cfg['x_count'] = x_cnt + orig_cfg['y_count'] = mesh_cfg['y_count'] = y_cnt + orig_cfg['mesh_min'] = self.mesh_min = (min_x, min_y) + orig_cfg['mesh_max'] = self.mesh_max = (max_x, max_y) + + pps = parse_config_pair(config, 'mesh_pps', 2, minval=0) + orig_cfg['mesh_x_pps'] = mesh_cfg['mesh_x_pps'] = pps[0] + orig_cfg['mesh_y_pps'] = mesh_cfg['mesh_y_pps'] = pps[1] + orig_cfg['algo'] = mesh_cfg['algo'] = \ + config.get('algorithm', 'lagrange').strip().lower() + orig_cfg['tension'] = mesh_cfg['tension'] = config.getfloat( + 'bicubic_tension', .2, minval=0., maxval=2.) + self._verify_algorithm(config.error) + def _verify_algorithm(self, error): + params = self.mesh_config + x_pps = params['mesh_x_pps'] + y_pps = params['mesh_y_pps'] + if params['algo'] not in self.ALGOS: + raise error( + "bed_mesh: Unknown algorithm <%s>" + % (self.mesh_config['algo'])) + # Check the algorithm against the current configuration + max_probe_cnt = max(params['x_count'], params['y_count']) + min_probe_cnt = min(params['x_count'], params['y_count']) + if max(x_pps, y_pps) == 0: + # Interpolation disabled + self.mesh_config['algo'] = 'direct' + elif params['algo'] == 'lagrange' and max_probe_cnt > 6: + # Lagrange interpolation tends to oscillate when using more + # than 6 samples + raise error( + "bed_mesh: cannot exceed a probe_count of 6 when using " + "lagrange interpolation. Configured Probe Count: %d, %d" % + (self.mesh_config['x_count'], self.mesh_config['y_count'])) + elif params['algo'] == 'bicubic' and min_probe_cnt < 4: + if max_probe_cnt > 6: + raise error( + "bed_mesh: invalid probe_count option when using bicubic " + "interpolation. Combination of 3 points on one axis with " + "more than 6 on another is not permitted. " + "Configured Probe Count: %d, %d" % + (self.mesh_config['x_count'], self.mesh_config['y_count'])) + else: + logging.info( + "bed_mesh: bicubic interpolation with a probe_count of " + "less than 4 points detected. Forcing lagrange " + "interpolation. Configured Probe Count: %d, %d" % + (self.mesh_config['x_count'], self.mesh_config['y_count'])) + params['algo'] = 'lagrange' + def set_adaptive_mesh(self, gcmd): + if not gcmd.get_int('ADAPTIVE', 0): + return False + exclude_objects = self.printer.lookup_object("exclude_object", None) + if exclude_objects is None: + gcmd.respond_info("Exclude objects not enabled. Using full mesh...") + return False + objects = exclude_objects.get_status().get("objects", []) + if not objects: + return False + margin = gcmd.get_float('ADAPTIVE_MARGIN', self.adaptive_margin) + + # List all exclude_object points by axis and iterate over + # all polygon points, and pick the min and max or each axis + list_of_xs = [] + list_of_ys = [] + gcmd.respond_info("Found %s objects" % (len(objects))) + for obj in objects: + for point in obj["polygon"]: + list_of_xs.append(point[0]) + list_of_ys.append(point[1]) + + # Define bounds of adaptive mesh area + mesh_min = [min(list_of_xs), min(list_of_ys)] + mesh_max = [max(list_of_xs), max(list_of_ys)] + adjusted_mesh_min = [x - margin for x in mesh_min] + adjusted_mesh_max = [x + margin for x in mesh_max] + + # Force margin to respect original mesh bounds + adjusted_mesh_min[0] = max(adjusted_mesh_min[0], + self.orig_config["mesh_min"][0]) + adjusted_mesh_min[1] = max(adjusted_mesh_min[1], + self.orig_config["mesh_min"][1]) + adjusted_mesh_max[0] = min(adjusted_mesh_max[0], + self.orig_config["mesh_max"][0]) + adjusted_mesh_max[1] = min(adjusted_mesh_max[1], + self.orig_config["mesh_max"][1]) + + adjusted_mesh_size = (adjusted_mesh_max[0] - adjusted_mesh_min[0], + adjusted_mesh_max[1] - adjusted_mesh_min[1]) + + # Compute a ratio between the adapted and original sizes + ratio = (adjusted_mesh_size[0] / + (self.orig_config["mesh_max"][0] - + self.orig_config["mesh_min"][0]), + adjusted_mesh_size[1] / + (self.orig_config["mesh_max"][1] - + self.orig_config["mesh_min"][1])) + + gcmd.respond_info("Original mesh bounds: (%s,%s)" % + (self.orig_config["mesh_min"], + self.orig_config["mesh_max"])) + gcmd.respond_info("Original probe count: (%s,%s)" % + (self.mesh_config["x_count"], + self.mesh_config["y_count"])) + gcmd.respond_info("Adapted mesh bounds: (%s,%s)" % + (adjusted_mesh_min, adjusted_mesh_max)) + gcmd.respond_info("Ratio: (%s, %s)" % ratio) + + new_x_probe_count = int( + math.ceil(self.mesh_config["x_count"] * ratio[0])) + new_y_probe_count = int( + math.ceil(self.mesh_config["y_count"] * ratio[1])) + + # There is one case, where we may have to adjust the probe counts: + # axis0 < 4 and axis1 > 6 (see _verify_algorithm). + min_num_of_probes = 3 + if max(new_x_probe_count, new_y_probe_count) > 6 and \ + min(new_x_probe_count, new_y_probe_count) < 4: + min_num_of_probes = 4 + + new_x_probe_count = max(min_num_of_probes, new_x_probe_count) + new_y_probe_count = max(min_num_of_probes, new_y_probe_count) + + gcmd.respond_info("Adapted probe count: (%s,%s)" % + (new_x_probe_count, new_y_probe_count)) + + # If the adapted mesh size is too small, adjust it to something + # useful. + adjusted_mesh_size = (max(adjusted_mesh_size[0], new_x_probe_count), + max(adjusted_mesh_size[1], new_y_probe_count)) + + if self.radius is not None: + adapted_radius = math.sqrt((adjusted_mesh_size[0] ** 2) + + (adjusted_mesh_size[1] ** 2)) / 2 + adapted_origin = (adjusted_mesh_min[0] + + (adjusted_mesh_size[0] / 2), + adjusted_mesh_min[1] + + (adjusted_mesh_size[1] / 2)) + to_adapted_origin = math.sqrt(adapted_origin[0]**2 + + adapted_origin[1]**2) + # If the adapted mesh size is smaller than the default/full + # mesh, adjust the parameters. Otherwise, just do the full mesh. + if adapted_radius + to_adapted_origin < self.radius: + self.radius = adapted_radius + self.origin = adapted_origin + self.mesh_min = (-self.radius, -self.radius) + self.mesh_max = (self.radius, self.radius) + new_probe_count = max(new_x_probe_count, new_y_probe_count) + # Adaptive meshes require odd number of points + new_probe_count += 1 - (new_probe_count % 2) + self.mesh_config["x_count"] = self.mesh_config["y_count"] = \ + new_probe_count + else: + self.mesh_min = adjusted_mesh_min + self.mesh_max = adjusted_mesh_max + self.mesh_config["x_count"] = new_x_probe_count + self.mesh_config["y_count"] = new_y_probe_count + self._profile_name = None + return True + def update_config(self, gcmd): + # reset default configuration + self.radius = self.orig_config['radius'] + self.origin = self.orig_config['origin'] + self.mesh_min = self.orig_config['mesh_min'] + self.mesh_max = self.orig_config['mesh_max'] + for key in list(self.mesh_config.keys()): + self.mesh_config[key] = self.orig_config[key] + + params = gcmd.get_command_parameters() + need_cfg_update = False + if self.radius is not None: + if "MESH_RADIUS" in params: + self.radius = gcmd.get_float("MESH_RADIUS") + self.radius = math.floor(self.radius * 10) / 10 + self.mesh_min = (-self.radius, -self.radius) + self.mesh_max = (self.radius, self.radius) + need_cfg_update = True + if "MESH_ORIGIN" in params: + self.origin = parse_gcmd_coord(gcmd, 'MESH_ORIGIN') + need_cfg_update = True + if "ROUND_PROBE_COUNT" in params: + cnt = gcmd.get_int('ROUND_PROBE_COUNT', minval=3) + self.mesh_config['x_count'] = cnt + self.mesh_config['y_count'] = cnt + need_cfg_update = True + else: + if "MESH_MIN" in params: + self.mesh_min = parse_gcmd_coord(gcmd, 'MESH_MIN') + need_cfg_update = True + if "MESH_MAX" in params: + self.mesh_max = parse_gcmd_coord(gcmd, 'MESH_MAX') + need_cfg_update = True + if "PROBE_COUNT" in params: + x_cnt, y_cnt = parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=3) + self.mesh_config['x_count'] = x_cnt + self.mesh_config['y_count'] = y_cnt + need_cfg_update = True + + if "MESH_PPS" in params: + xpps, ypps = parse_gcmd_pair(gcmd, 'MESH_PPS', minval=0) + self.mesh_config['mesh_x_pps'] = xpps + self.mesh_config['mesh_y_pps'] = ypps + need_cfg_update = True + + if "ALGORITHM" in params: + self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower() + need_cfg_update = True + + need_cfg_update |= self.set_adaptive_mesh(gcmd) + probe_method = gcmd.get("METHOD", "automatic") + + if need_cfg_update: + self._verify_algorithm(gcmd.error) + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin, probe_method + ) + gcmd.respond_info("Generating new points...") + self.print_generated_points(gcmd.respond_info) + msg = "\n".join(["%s: %s" % (k, v) + for k, v in self.mesh_config.items()]) + logging.info("Updated Mesh Configuration:\n" + msg) + else: + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin, probe_method + ) + def dump_calibration(self, gcmd=None): + if gcmd is not None and gcmd.get_command_parameters(): + self.update_config(gcmd) + cfg = dict(self.mesh_config) + cfg["mesh_min"] = self.mesh_min + cfg["mesh_max"] = self.mesh_max + cfg["origin"] = self.origin + cfg["radius"] = self.radius + return { + "points": self.probe_mgr.get_base_points(), + "config": cfg, + "probe_path": self.probe_mgr.get_std_path(), + "rapid_path": list(self.probe_mgr.iter_rapid_path()) + } + cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling" + def cmd_BED_MESH_CALIBRATE(self, gcmd): + self._profile_name = gcmd.get('PROFILE', "default") + if not self._profile_name.strip(): + raise gcmd.error("Value for parameter 'PROFILE' must be specified") + self.bedmesh.set_mesh(None) + try: + self.update_config(gcmd) + except BedMeshError as e: + raise gcmd.error(str(e)) + self.probe_mgr.start_probe(gcmd) + def probe_finalize(self, offsets, positions): + z_offset = offsets[2] + positions = [[round(p[0], 2), round(p[1], 2), p[2]] + for p in positions] + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE: + ref_pos = positions.pop() + logging.info( + "bed_mesh: z-offset replaced with probed z value at " + "position (%.2f, %.2f, %.6f)" + % (ref_pos[0], ref_pos[1], ref_pos[2]) + ) + z_offset = ref_pos[2] + base_points = self.probe_mgr.get_base_points() + params = dict(self.mesh_config) + params['min_x'] = min(base_points, key=lambda p: p[0])[0] + params['max_x'] = max(base_points, key=lambda p: p[0])[0] + params['min_y'] = min(base_points, key=lambda p: p[1])[1] + params['max_y'] = max(base_points, key=lambda p: p[1])[1] + x_cnt = params['x_count'] + y_cnt = params['y_count'] + + substitutes = self.probe_mgr.get_substitutes() + probed_pts = positions + if substitutes: + # Replace substituted points with the original generated + # point. Its Z Value is the average probed Z of the + # substituted points. + corrected_pts = [] + idx_offset = 0 + start_idx = 0 + for i, pts in substitutes.items(): + fpt = [p - o for p, o in zip(base_points[i], offsets[:2])] + # offset the index to account for additional samples + idx = i + idx_offset + # Add "normal" points + corrected_pts.extend(positions[start_idx:idx]) + avg_z = sum([p[2] for p in positions[idx:idx+len(pts)]]) \ + / len(pts) + idx_offset += len(pts) - 1 + start_idx = idx + len(pts) + fpt.append(avg_z) + logging.info( + "bed_mesh: Replacing value at faulty index %d" + " (%.4f, %.4f): avg value = %.6f, avg w/ z_offset = %.6f" + % (i, fpt[0], fpt[1], avg_z, avg_z - z_offset)) + corrected_pts.append(fpt) + corrected_pts.extend(positions[start_idx:]) + positions = corrected_pts + + # validate length of result + if len(base_points) != len(positions): + self._dump_points(probed_pts, positions, offsets) + raise self.gcode.error( + "bed_mesh: invalid position list size, " + "generated count: %d, probed count: %d" + % (len(base_points), len(positions)) + ) + + probed_matrix = [] + row = [] + prev_pos = base_points[0] + for pos, result in zip(base_points, positions): + offset_pos = [p - o for p, o in zip(pos, offsets[:2])] + if ( + not isclose(offset_pos[0], result[0], abs_tol=.5) or + not isclose(offset_pos[1], result[1], abs_tol=.5) + ): + logging.info( + "bed_mesh: point deviation > .5mm: orig pt = (%.2f, %.2f)" + ", probed pt = (%.2f, %.2f)" + % (offset_pos[0], offset_pos[1], result[0], result[1]) + ) + z_pos = result[2] - z_offset + if not isclose(pos[1], prev_pos[1], abs_tol=.1): + # y has changed, append row and start new + probed_matrix.append(row) + row = [] + if pos[0] > prev_pos[0]: + # probed in the positive direction + row.append(z_pos) + else: + # probed in the negative direction + row.insert(0, z_pos) + prev_pos = pos + # append last row + probed_matrix.append(row) + + # make sure the y-axis is the correct length + if len(probed_matrix) != y_cnt: + raise self.gcode.error( + ("bed_mesh: Invalid y-axis table length\n" + "Probed table length: %d Probed Table:\n%s") % + (len(probed_matrix), str(probed_matrix))) + + if self.radius is not None: + # round bed, extrapolate probed values to create a square mesh + for row in probed_matrix: + row_size = len(row) + if not row_size & 1: + # an even number of points in a row shouldn't be possible + msg = "bed_mesh: incorrect number of points sampled on X\n" + msg += "Probed Table:\n" + msg += str(probed_matrix) + raise self.gcode.error(msg) + buf_cnt = (x_cnt - row_size) // 2 + if buf_cnt == 0: + continue + left_buffer = [row[0]] * buf_cnt + right_buffer = [row[row_size-1]] * buf_cnt + row[0:0] = left_buffer + row.extend(right_buffer) + + # make sure that the x-axis is the correct length + for row in probed_matrix: + if len(row) != x_cnt: + raise self.gcode.error( + ("bed_mesh: invalid x-axis table length\n" + "Probed table length: %d Probed Table:\n%s") % + (len(probed_matrix), str(probed_matrix))) + + z_mesh = ZMesh(params, self._profile_name) + try: + z_mesh.build_mesh(probed_matrix) + except BedMeshError as e: + raise self.gcode.error(str(e)) + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.IN_MESH: + # The reference can be anywhere in the mesh, therefore + # it is necessary to set the reference after the initial mesh + # is generated to lookup the correct z value. + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + z_mesh.set_zero_reference(*zero_ref_pos) + self.bedmesh.set_mesh(z_mesh) + self.gcode.respond_info("Mesh Bed Leveling Complete") + if self._profile_name is not None: + self.bedmesh.save_profile(self._profile_name) + def _dump_points(self, probed_pts, corrected_pts, offsets): + # logs generated points with offset applied, points received + # from the finalize callback, and the list of corrected points + points = self.probe_mgr.get_base_points() + max_len = max([len(points), len(probed_pts), len(corrected_pts)]) + logging.info( + "bed_mesh: calibration point dump\nIndex | %-17s| %-25s|" + " Corrected Point" % ("Generated Point", "Probed Point")) + for i in list(range(max_len)): + gen_pt = probed_pt = corr_pt = "" + if i < len(points): + off_pt = [p - o for p, o in zip(points[i], offsets[:2])] + gen_pt = "(%.2f, %.2f)" % tuple(off_pt) + if i < len(probed_pts): + probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) + if i < len(corrected_pts): + corr_pt = "(%.2f, %.2f, %.4f)" % tuple(corrected_pts[i]) + logging.info( + " %-4d| %-17s| %-25s| %s" % (i, gen_pt, probed_pt, corr_pt)) + +class ProbeManager: + def __init__(self, config, orig_config, finalize_cb): + self.printer = config.get_printer() + self.cfg_overshoot = config.getfloat("scan_overshoot", 0, minval=1.) + self.orig_config = orig_config + self.faulty_regions = [] + self.overshoot = self.cfg_overshoot + self.zero_ref_pos = config.getfloatlist( + "zero_reference_position", None, count=2 + ) + self.zref_mode = ZrefMode.DISABLED + self.base_points = [] + self.substitutes = collections.OrderedDict() + self.is_round = orig_config["radius"] is not None + self.probe_helper = probe.ProbePointsHelper(config, finalize_cb, []) + self.probe_helper.use_xy_offsets(True) + self.rapid_scan_helper = RapidScanHelper(config, self, finalize_cb) + self._init_faulty_regions(config) + + def _init_faulty_regions(self, config): + for i in list(range(1, 100, 1)): + start = config.getfloatlist("faulty_region_%d_min" % (i,), None, + count=2) + if start is None: + break + end = config.getfloatlist("faulty_region_%d_max" % (i,), count=2) + # Validate the corners. If necessary reorganize them. + # c1 = min point, c3 = max point + # c4 ---- c3 + # | | + # c1 ---- c2 + c1 = [min([s, e]) for s, e in zip(start, end)] + c3 = [max([s, e]) for s, e in zip(start, end)] + c2 = [c1[0], c3[1]] + c4 = [c3[0], c1[1]] + # Check for overlapping regions + for j, (prev_c1, prev_c3) in enumerate(self.faulty_regions): + prev_c2 = [prev_c1[0], prev_c3[1]] + prev_c4 = [prev_c3[0], prev_c1[1]] + # Validate that no existing corner is within the new region + for coord in [prev_c1, prev_c2, prev_c3, prev_c4]: + if within(coord, c1, c3): + raise config.error( + "bed_mesh: Existing faulty_region_%d %s overlaps " + "added faulty_region_%d %s" + % (j+1, repr([prev_c1, prev_c3]), + i, repr([c1, c3]))) + # Validate that no new corner is within an existing region + for coord in [c1, c2, c3, c4]: + if within(coord, prev_c1, prev_c3): + raise config.error( + "bed_mesh: Added faulty_region_%d %s overlaps " + "existing faulty_region_%d %s" + % (i, repr([c1, c3]), + j+1, repr([prev_c1, prev_c3]))) + self.faulty_regions.append((c1, c3)) + + def start_probe(self, gcmd): + method = gcmd.get("METHOD", "automatic").lower() + can_scan = False + pprobe = self.printer.lookup_object("probe", None) + if pprobe is not None: + probe_name = pprobe.get_status(None).get("name", "") + can_scan = probe_name.startswith("probe_eddy_current") + if method == "rapid_scan" and can_scan: + self.rapid_scan_helper.perform_rapid_scan(gcmd) + else: + self.probe_helper.start_probe(gcmd) + + def get_zero_ref_pos(self): + return self.zero_ref_pos + + def get_zero_ref_mode(self): + return self.zref_mode + + def get_substitutes(self): + return self.substitutes + + def generate_points( + self, mesh_config, mesh_min, mesh_max, radius, origin, + probe_method="automatic" + ): + x_cnt = mesh_config['x_count'] + y_cnt = mesh_config['y_count'] + min_x, min_y = mesh_min + max_x, max_y = mesh_max + x_dist = (max_x - min_x) / (x_cnt - 1) + y_dist = (max_y - min_y) / (y_cnt - 1) + # floor distances down to next hundredth + x_dist = math.floor(x_dist * 100) / 100 + y_dist = math.floor(y_dist * 100) / 100 + if x_dist < 1. or y_dist < 1.: + raise BedMeshError("bed_mesh: min/max points too close together") + + if radius is not None: + # round bed, min/max needs to be recalculated + y_dist = x_dist + new_r = (x_cnt // 2) * x_dist + min_x = min_y = -new_r + max_x = max_y = new_r + else: + # rectangular bed, only re-calc max_x + max_x = min_x + x_dist * (x_cnt - 1) + pos_y = min_y + points = [] + for i in range(y_cnt): + for j in range(x_cnt): + if not i % 2: + # move in positive directon + pos_x = min_x + j * x_dist + else: + # move in negative direction + pos_x = max_x - j * x_dist + if radius is None: + # rectangular bed, append + points.append((pos_x, pos_y)) + else: + # round bed, check distance from origin + dist_from_origin = math.sqrt(pos_x*pos_x + pos_y*pos_y) + if dist_from_origin <= radius: + points.append( + (origin[0] + pos_x, origin[1] + pos_y)) + pos_y += y_dist + if self.zero_ref_pos is None or probe_method == "manual": + # Zero Reference Disabled + self.zref_mode = ZrefMode.DISABLED + elif within(self.zero_ref_pos, mesh_min, mesh_max): + # Zero Reference position within mesh + self.zref_mode = ZrefMode.IN_MESH + else: + # Zero Reference position outside of mesh + self.zref_mode = ZrefMode.PROBE + self.base_points = points + self.substitutes.clear() + # adjust overshoot + og_min_x = self.orig_config["mesh_min"][0] + og_max_x = self.orig_config["mesh_max"][0] + add_ovs = min(max(0, min_x - og_min_x), max(0, og_max_x - max_x)) + self.overshoot = self.cfg_overshoot + math.floor(add_ovs) + min_pt, max_pt = (min_x, min_y), (max_x, max_y) + self._process_faulty_regions(min_pt, max_pt, radius) + self.probe_helper.update_probe_points(self.get_std_path(), 3) + + def _process_faulty_regions(self, min_pt, max_pt, radius): + if not self.faulty_regions: + return + # Cannot probe a reference within a faulty region + if self.zref_mode == ZrefMode.PROBE: + for min_c, max_c in self.faulty_regions: + if within(self.zero_ref_pos, min_c, max_c): + opt = "zero_reference_position" + raise BedMeshError( + "bed_mesh: Cannot probe zero reference position at " + "(%.2f, %.2f) as it is located within a faulty region." + " Check the value for option '%s'" + % (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,) + ) + # Check to see if any points fall within faulty regions + last_y = self.base_points[0][1] + is_reversed = False + for i, coord in enumerate(self.base_points): + if not isclose(coord[1], last_y): + is_reversed = not is_reversed + last_y = coord[1] + adj_coords = [] + for min_c, max_c in self.faulty_regions: + if within(coord, min_c, max_c, tol=.00001): + # Point lies within a faulty region + adj_coords = [ + (min_c[0], coord[1]), (coord[0], min_c[1]), + (coord[0], max_c[1]), (max_c[0], coord[1])] + if is_reversed: + # Swap first and last points for zig-zag pattern + first = adj_coords[0] + adj_coords[0] = adj_coords[-1] + adj_coords[-1] = first + break + if not adj_coords: + # coord is not located within a faulty region + continue + valid_coords = [] + for ac in adj_coords: + # make sure that coordinates are within the mesh boundary + if radius is None: + if within(ac, min_pt, max_pt, .000001): + valid_coords.append(ac) + else: + dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1]) + if dist_from_origin <= radius: + valid_coords.append(ac) + if not valid_coords: + raise BedMeshError( + "bed_mesh: Unable to generate coordinates" + " for faulty region at index: %d" % (i) + ) + self.substitutes[i] = valid_coords + + def get_base_points(self): + return self.base_points + + def get_std_path(self): + path = [] + for idx, pt in enumerate(self.base_points): + if idx in self.substitutes: + for sub_pt in self.substitutes[idx]: + path.append(sub_pt) + else: + path.append(pt) + if self.zref_mode == ZrefMode.PROBE: + path.append(self.zero_ref_pos) + return path + + def iter_rapid_path(self): + ascnd_x = True + last_base_pt = last_mv_pt = self.base_points[0] + # Generate initial move point + if self.overshoot: + overshoot = min(8, self.overshoot) + last_mv_pt = (last_base_pt[0] - overshoot, last_base_pt[1]) + yield last_mv_pt, False + for idx, pt in enumerate(self.base_points): + # increasing Y indicates direction change + dir_change = not isclose(pt[1], last_base_pt[1], abs_tol=1e-6) + if idx in self.substitutes: + fp_gen = self._gen_faulty_path( + last_mv_pt, idx, ascnd_x, dir_change + ) + for sub_pt, is_smp in fp_gen: + yield sub_pt, is_smp + last_mv_pt = sub_pt + else: + if dir_change: + for dpt in self._gen_dir_change(last_mv_pt, pt, ascnd_x): + yield dpt, False + yield pt, True + last_mv_pt = pt + last_base_pt = pt + ascnd_x ^= dir_change + if self.zref_mode == ZrefMode.PROBE: + if self.overshoot: + ovs = min(4, self.overshoot) + ovs = ovs if ascnd_x else -ovs + yield (last_mv_pt[0] + ovs, last_mv_pt[1]), False + yield self.zero_ref_pos, True + + def _gen_faulty_path(self, last_pt, idx, ascnd_x, dir_change): + subs = self.substitutes[idx] + sub_cnt = len(subs) + if dir_change: + for dpt in self._gen_dir_change(last_pt, subs[0], ascnd_x): + yield dpt, False + if self.is_round: + # No faulty region path handling for round beds + for pt in subs: + yield pt, True + return + # Check to see if this is the first corner + first_corner = False + sorted_sub_idx = sorted(self.substitutes.keys()) + if sub_cnt == 2 and idx < len(sorted_sub_idx): + first_corner = sorted_sub_idx[idx] == idx + yield subs[0], True + if sub_cnt == 1: + return + last_pt, next_pt = subs[:2] + if sub_cnt == 2: + if first_corner or dir_change: + # horizontal move first + yield (next_pt[0], last_pt[1]), False + else: + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + elif sub_cnt >= 3: + if dir_change: + # first move should be a vertical switch up. If overshoot + # is available, simulate another direction change. Otherwise + # move inward 2 mm, then up through the faulty region. + if self.overshoot: + for dpt in self._gen_dir_change(last_pt, next_pt, ascnd_x): + yield dpt, False + else: + shift = -2 if ascnd_x else 2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (last_pt[0] + shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + else: + # vertical move + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + if sub_cnt == 4: + # Vertical switch up within faulty region + shift = 2 if ascnd_x else -2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (next_pt[0] - shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[2:4] + # horizontal move before final point + yield (next_pt[0], last_pt[1]), False + yield next_pt, True + + def _gen_dir_change(self, last_pt, next_pt, ascnd_x): + if not self.overshoot: + return + # overshoot X beyond the outer point + xdir = 1 if ascnd_x else -1 + overshoot = 2. if self.overshoot >= 3. else self.overshoot + ovr_pt = (last_pt[0] + overshoot * xdir, last_pt[1]) + yield ovr_pt + if self.overshoot < 3.: + # No room to generate an arc, move up to next y + yield (next_pt[0] + overshoot * xdir, next_pt[1]) + else: + # generate arc + STEP_ANGLE = 3 + START_ANGLE = 270 + ydiff = abs(next_pt[1] - last_pt[1]) + xdiff = abs(next_pt[0] - last_pt[0]) + max_radius = min(self.overshoot - 2, 8) + radius = min(ydiff / 2, max_radius) + origin = [ovr_pt[0], last_pt[1] + radius] + next_origin_y = next_pt[1] - radius + # determine angle + if xdiff < .01: + # Move is aligned on the x-axis + angle = 90 + if next_origin_y - origin[1] < .05: + # The move can be completed in a single arc + angle = 180 + else: + angle = int(math.degrees(math.atan(ydiff / xdiff))) + if ( + (ascnd_x and next_pt[0] < last_pt[0]) or + (not ascnd_x and next_pt[0] > last_pt[0]) + ): + angle = 180 - angle + count = int(angle // STEP_ANGLE) + # Gen first arc + step = STEP_ANGLE * xdir + start = START_ANGLE + step + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + if angle == 180: + # arc complete + return + # generate next arc + origin = [next_pt[0] + overshoot * xdir, next_origin_y] + # start at the angle where the last arc finished + start = START_ANGLE + count * step + # recalculate the count to make sure we generate a full 180 + # degrees. Add a step for the repeated connecting angle + count = 61 - count + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + + def _gen_arc(self, origin, radius, start, step, count): + end = start + step * count + # create a segent for every 3 degress of travel + for angle in range(start, end, step): + rad = math.radians(angle % 360) + opp = math.sin(rad) * radius + adj = math.cos(rad) * radius + yield (origin[0] + adj, origin[1] + opp) + + +MAX_HIT_DIST = 2. +MM_WIN_SPEED = 125 + +class RapidScanHelper: + def __init__(self, config, probe_mgr, finalize_cb): + self.printer = config.get_printer() + self.probe_manager = probe_mgr + self.speed = config.getfloat("speed", 50., above=0.) + self.scan_height = config.getfloat("horizontal_move_z", 5.) + self.finalize_callback = finalize_cb + + def perform_rapid_scan(self, gcmd): + speed = gcmd.get_float("SCAN_SPEED", self.speed) + scan_height = gcmd.get_float("HORIZONTAL_MOVE_Z", self.scan_height) + gcmd.respond_info( + "Beginning rapid surface scan at height %.2f..." % (scan_height) + ) + pprobe = self.printer.lookup_object("probe") + toolhead = self.printer.lookup_object("toolhead") + # Calculate time window around which a sample is valid. Current + # assumption is anything within 2mm is usable, so: + # window = 2 / max_speed + # + # TODO: validate maximum speed allowed based on sample rate of probe + # Scale the hit distance window for speeds lower than 125mm/s. The + # lower the speed the less the window shrinks. + scale = max(0, 1 - speed / MM_WIN_SPEED) + 1 + hit_dist = min(MAX_HIT_DIST, scale * speed / MM_WIN_SPEED) + half_window = hit_dist / speed + gcmd.respond_info( + "Sample hit distance +/- %.4fmm, time window +/- ms %.4f" + % (hit_dist, half_window * 1000) + ) + gcmd_params = gcmd.get_command_parameters() + gcmd_params["SAMPLE_TIME"] = half_window * 2 + self._raise_tool(gcmd, scan_height) + probe_session = pprobe.start_probe_session(gcmd) + offsets = pprobe.get_offsets() + initial_move = True + for pos, is_probe_pt in self.probe_manager.iter_rapid_path(): + pos = self._apply_offsets(pos[:2], offsets) + toolhead.manual_move(pos, speed) + if initial_move: + initial_move = False + self._move_to_scan_height(gcmd, scan_height) + if is_probe_pt: + probe_session.run_probe(gcmd) + results = probe_session.pull_probed_results() + toolhead.get_last_move_time() + self.finalize_callback(offsets, results) + probe_session.end_probe_session() + + def _raise_tool(self, gcmd, scan_height): + # If the nozzle is below scan height raise the tool + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + if cur_pos[2] >= scan_height: + return + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + cur_pos[2] = self.scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + + def _move_to_scan_height(self, gcmd, scan_height): + time_window = gcmd.get_float("SAMPLE_TIME") + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + probe_speed = pparams["probe_speed"] + cur_pos[2] = scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[2] = scan_height + toolhead.manual_move(cur_pos, probe_speed) + toolhead.dwell(time_window / 2 + .01) + + def _apply_offsets(self, point, offsets): + return [(pos - ofs) for pos, ofs in zip(point, offsets)] + + +class MoveSplitter: + def __init__(self, config, gcode): + self.split_delta_z = config.getfloat( + 'split_delta_z', .025, minval=0.01) + self.move_check_distance = config.getfloat( + 'move_check_distance', 5., minval=3.) + self.z_mesh = None + self.fade_offset = 0. + self.gcode = gcode + def initialize(self, mesh, fade_offset): + self.z_mesh = mesh + self.fade_offset = fade_offset + def build_move(self, prev_pos, next_pos, factor): + self.prev_pos = tuple(prev_pos) + self.next_pos = tuple(next_pos) + self.current_pos = list(prev_pos) + self.z_factor = factor + self.z_offset = self._calc_z_offset(prev_pos) + self.traverse_complete = False + self.distance_checked = 0. + axes_d = [self.next_pos[i] - self.prev_pos[i] for i in range(4)] + self.total_move_length = math.sqrt(sum([d*d for d in axes_d[:3]])) + self.axis_move = [not isclose(d, 0., abs_tol=1e-10) for d in axes_d] + def _calc_z_offset(self, pos): + z = self.z_mesh.calc_z(pos[0], pos[1]) + offset = self.fade_offset + return self.z_factor * (z - offset) + offset + def _set_next_move(self, distance_from_prev): + t = distance_from_prev / self.total_move_length + if t > 1. or t < 0.: + raise self.gcode.error( + "bed_mesh: Slice distance is negative " + "or greater than entire move length") + for i in range(4): + if self.axis_move[i]: + self.current_pos[i] = lerp( + t, self.prev_pos[i], self.next_pos[i]) + def split(self): + if not self.traverse_complete: + if self.axis_move[0] or self.axis_move[1]: + # X and/or Y axis move, traverse if necessary + while self.distance_checked + self.move_check_distance \ + < self.total_move_length: + self.distance_checked += self.move_check_distance + self._set_next_move(self.distance_checked) + next_z = self._calc_z_offset(self.current_pos) + if abs(next_z - self.z_offset) >= self.split_delta_z: + self.z_offset = next_z + return self.current_pos[0], self.current_pos[1], \ + self.current_pos[2] + self.z_offset, \ + self.current_pos[3] + # end of move reached + self.current_pos[:] = self.next_pos + self.z_offset = self._calc_z_offset(self.current_pos) + # Its okay to add Z-Offset to the final move, since it will not be + # used again. + self.current_pos[2] += self.z_offset + self.traverse_complete = True + return self.current_pos + else: + # Traverse complete + return None + + +class ZMesh: + def __init__(self, params, name): + self.profile_name = name or "adaptive-%X" % (id(self),) + self.probed_matrix = self.mesh_matrix = None + self.mesh_params = params + self.mesh_offsets = [0., 0.] + logging.debug('bed_mesh: probe/mesh parameters:') + for key, value in self.mesh_params.items(): + logging.debug("%s : %s" % (key, value)) + self.mesh_x_min = params['min_x'] + self.mesh_x_max = params['max_x'] + self.mesh_y_min = params['min_y'] + self.mesh_y_max = params['max_y'] + logging.debug( + "bed_mesh: Mesh Min: (%.2f,%.2f) Mesh Max: (%.2f,%.2f)" + % (self.mesh_x_min, self.mesh_y_min, + self.mesh_x_max, self.mesh_y_max)) + # Set the interpolation algorithm + interpolation_algos = { + 'lagrange': self._sample_lagrange, + 'bicubic': self._sample_bicubic, + 'direct': self._sample_direct + } + self._sample = interpolation_algos.get(params['algo']) + # Number of points to interpolate per segment + mesh_x_pps = params['mesh_x_pps'] + mesh_y_pps = params['mesh_y_pps'] + px_cnt = params['x_count'] + py_cnt = params['y_count'] + self.mesh_x_count = (px_cnt - 1) * mesh_x_pps + px_cnt + self.mesh_y_count = (py_cnt - 1) * mesh_y_pps + py_cnt + self.x_mult = mesh_x_pps + 1 + self.y_mult = mesh_y_pps + 1 + logging.debug("bed_mesh: Mesh grid size - X:%d, Y:%d" + % (self.mesh_x_count, self.mesh_y_count)) + self.mesh_x_dist = (self.mesh_x_max - self.mesh_x_min) / \ + (self.mesh_x_count - 1) + self.mesh_y_dist = (self.mesh_y_max - self.mesh_y_min) / \ + (self.mesh_y_count - 1) + def get_mesh_matrix(self): + if self.mesh_matrix is not None: + return [[round(z, 6) for z in line] + for line in self.mesh_matrix] + return [[]] + def get_probed_matrix(self): + if self.probed_matrix is not None: + return [[round(z, 6) for z in line] + for line in self.probed_matrix] + return [[]] + def get_mesh_params(self): + return self.mesh_params + def get_profile_name(self): + return self.profile_name + def print_probed_matrix(self, print_func): + if self.probed_matrix is not None: + msg = "Mesh Leveling Probed Z positions:\n" + for line in self.probed_matrix: + for x in line: + msg += " %f" % x + msg += "\n" + print_func(msg) + else: + print_func("bed_mesh: bed has not been probed") + def print_mesh(self, print_func, move_z=None): + matrix = self.get_mesh_matrix() + if matrix is not None: + msg = "Mesh X,Y: %d,%d\n" % (self.mesh_x_count, self.mesh_y_count) + if move_z is not None: + msg += "Search Height: %d\n" % (move_z) + msg += "Mesh Offsets: X=%.4f, Y=%.4f\n" % ( + self.mesh_offsets[0], self.mesh_offsets[1]) + msg += "Mesh Average: %.2f\n" % (self.get_z_average()) + rng = self.get_z_range() + msg += "Mesh Range: min=%.4f max=%.4f\n" % (rng[0], rng[1]) + msg += "Interpolation Algorithm: %s\n" \ + % (self.mesh_params['algo']) + msg += "Measured points:\n" + for y_line in range(self.mesh_y_count - 1, -1, -1): + for z in matrix[y_line]: + msg += " %f" % (z) + msg += "\n" + print_func(msg) + else: + print_func("bed_mesh: Z Mesh not generated") + def build_mesh(self, z_matrix): + self.probed_matrix = z_matrix + self._sample(z_matrix) + self.print_mesh(logging.debug) + def set_zero_reference(self, xpos, ypos): + offset = self.calc_z(xpos, ypos) + logging.info( + "bed_mesh: setting zero reference at (%.2f, %.2f, %.6f)" + % (xpos, ypos, offset) + ) + for matrix in [self.probed_matrix, self.mesh_matrix]: + for yidx in range(len(matrix)): + for xidx in range(len(matrix[yidx])): + matrix[yidx][xidx] -= offset + def set_mesh_offsets(self, offsets): + for i, o in enumerate(offsets): + if o is not None: + self.mesh_offsets[i] = o + def get_x_coordinate(self, index): + return self.mesh_x_min + self.mesh_x_dist * index + def get_y_coordinate(self, index): + return self.mesh_y_min + self.mesh_y_dist * index + def calc_z(self, x, y): + if self.mesh_matrix is not None: + tbl = self.mesh_matrix + tx, xidx = self._get_linear_index(x + self.mesh_offsets[0], 0) + ty, yidx = self._get_linear_index(y + self.mesh_offsets[1], 1) + z0 = lerp(tx, tbl[yidx][xidx], tbl[yidx][xidx+1]) + z1 = lerp(tx, tbl[yidx+1][xidx], tbl[yidx+1][xidx+1]) + return lerp(ty, z0, z1) + else: + # No mesh table generated, no z-adjustment + return 0. + def get_z_range(self): + if self.mesh_matrix is not None: + mesh_min = min([min(x) for x in self.mesh_matrix]) + mesh_max = max([max(x) for x in self.mesh_matrix]) + return mesh_min, mesh_max + else: + return 0., 0. + def get_z_average(self): + if self.mesh_matrix is not None: + avg_z = (sum([sum(x) for x in self.mesh_matrix]) / + sum([len(x) for x in self.mesh_matrix])) + # Round average to the nearest 100th. This + # should produce an offset that is divisible by common + # z step distances + return round(avg_z, 2) + else: + return 0. + def _get_linear_index(self, coord, axis): + if axis == 0: + # X-axis + mesh_min = self.mesh_x_min + mesh_cnt = self.mesh_x_count + mesh_dist = self.mesh_x_dist + cfunc = self.get_x_coordinate + else: + # Y-axis + mesh_min = self.mesh_y_min + mesh_cnt = self.mesh_y_count + mesh_dist = self.mesh_y_dist + cfunc = self.get_y_coordinate + t = 0. + idx = int(math.floor((coord - mesh_min) / mesh_dist)) + idx = constrain(idx, 0, mesh_cnt - 2) + t = (coord - cfunc(idx)) / mesh_dist + return constrain(t, 0., 1.), idx + def _sample_direct(self, z_matrix): + self.mesh_matrix = z_matrix + def _sample_lagrange(self, z_matrix): + x_mult = self.x_mult + y_mult = self.y_mult + self.mesh_matrix = \ + [[0. if ((i % x_mult) or (j % y_mult)) + else z_matrix[j//y_mult][i//x_mult] + for i in range(self.mesh_x_count)] + for j in range(self.mesh_y_count)] + xpts, ypts = self._get_lagrange_coords() + # Interpolate X coordinates + for i in range(self.mesh_y_count): + # only interpolate X-rows that have probed coordinates + if i % y_mult != 0: + continue + for j in range(self.mesh_x_count): + if j % x_mult == 0: + continue + x = self.get_x_coordinate(j) + self.mesh_matrix[i][j] = self._calc_lagrange(xpts, x, i, 0) + # Interpolate Y coordinates + for i in range(self.mesh_x_count): + for j in range(self.mesh_y_count): + if j % y_mult == 0: + continue + y = self.get_y_coordinate(j) + self.mesh_matrix[j][i] = self._calc_lagrange(ypts, y, i, 1) + def _get_lagrange_coords(self): + xpts = [] + ypts = [] + for i in range(self.mesh_params['x_count']): + xpts.append(self.get_x_coordinate(i * self.x_mult)) + for j in range(self.mesh_params['y_count']): + ypts.append(self.get_y_coordinate(j * self.y_mult)) + return xpts, ypts + def _calc_lagrange(self, lpts, c, vec, axis=0): + pt_cnt = len(lpts) + total = 0. + for i in range(pt_cnt): + n = 1. + d = 1. + for j in range(pt_cnt): + if j == i: + continue + n *= (c - lpts[j]) + d *= (lpts[i] - lpts[j]) + if axis == 0: + # Calc X-Axis + z = self.mesh_matrix[vec][i*self.x_mult] + else: + # Calc Y-Axis + z = self.mesh_matrix[i*self.y_mult][vec] + total += z * n / d + return total + def _sample_bicubic(self, z_matrix): + # should work for any number of probe points above 3x3 + x_mult = self.x_mult + y_mult = self.y_mult + c = self.mesh_params['tension'] + self.mesh_matrix = \ + [[0. if ((i % x_mult) or (j % y_mult)) + else z_matrix[j//y_mult][i//x_mult] + for i in range(self.mesh_x_count)] + for j in range(self.mesh_y_count)] + # Interpolate X values + for y in range(self.mesh_y_count): + if y % y_mult != 0: + continue + for x in range(self.mesh_x_count): + if x % x_mult == 0: + continue + pts = self._get_x_ctl_pts(x, y) + self.mesh_matrix[y][x] = self._cardinal_spline(pts, c) + # Interpolate Y values + for x in range(self.mesh_x_count): + for y in range(self.mesh_y_count): + if y % y_mult == 0: + continue + pts = self._get_y_ctl_pts(x, y) + self.mesh_matrix[y][x] = self._cardinal_spline(pts, c) + def _get_x_ctl_pts(self, x, y): + # Fetch control points and t for a X value in the mesh + x_mult = self.x_mult + x_row = self.mesh_matrix[y] + last_pt = self.mesh_x_count - 1 - x_mult + if x < x_mult: + p0 = p1 = x_row[0] + p2 = x_row[x_mult] + p3 = x_row[2*x_mult] + t = x / float(x_mult) + elif x > last_pt: + p0 = x_row[last_pt - x_mult] + p1 = x_row[last_pt] + p2 = p3 = x_row[last_pt + x_mult] + t = (x - last_pt) / float(x_mult) + else: + found = False + for i in range(x_mult, last_pt, x_mult): + if x > i and x < (i + x_mult): + p0 = x_row[i - x_mult] + p1 = x_row[i] + p2 = x_row[i + x_mult] + p3 = x_row[i + 2*x_mult] + t = (x - i) / float(x_mult) + found = True + break + if not found: + raise BedMeshError( + "bed_mesh: Error finding x control points") + return p0, p1, p2, p3, t + def _get_y_ctl_pts(self, x, y): + # Fetch control points and t for a Y value in the mesh + y_mult = self.y_mult + last_pt = self.mesh_y_count - 1 - y_mult + y_col = self.mesh_matrix + if y < y_mult: + p0 = p1 = y_col[0][x] + p2 = y_col[y_mult][x] + p3 = y_col[2*y_mult][x] + t = y / float(y_mult) + elif y > last_pt: + p0 = y_col[last_pt - y_mult][x] + p1 = y_col[last_pt][x] + p2 = p3 = y_col[last_pt + y_mult][x] + t = (y - last_pt) / float(y_mult) + else: + found = False + for i in range(y_mult, last_pt, y_mult): + if y > i and y < (i + y_mult): + p0 = y_col[i - y_mult][x] + p1 = y_col[i][x] + p2 = y_col[i + y_mult][x] + p3 = y_col[i + 2*y_mult][x] + t = (y - i) / float(y_mult) + found = True + break + if not found: + raise BedMeshError( + "bed_mesh: Error finding y control points") + return p0, p1, p2, p3, t + def _cardinal_spline(self, p, tension): + t = p[4] + t2 = t*t + t3 = t2*t + m1 = tension * (p[2] - p[0]) + m2 = tension * (p[3] - p[1]) + a = p[1] * (2*t3 - 3*t2 + 1) + b = p[2] * (-2*t3 + 3*t2) + c = m1 * (t3 - 2*t2 + t) + d = m2 * (t3 - t2) + return a + b + c + d + + +class ProfileManager: + def __init__(self, config, bedmesh): + self.name = config.get_name() + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') + self.bedmesh = bedmesh + self.profiles = {} + self.incompatible_profiles = [] + # Fetch stored profiles from Config + stored_profs = config.get_prefix_sections(self.name) + stored_profs = [s for s in stored_profs + if s.get_name() != self.name] + for profile in stored_profs: + name = profile.get_name().split(' ', 1)[1] + version = profile.getint('version', 0) + if version != PROFILE_VERSION: + logging.info( + "bed_mesh: Profile [%s] not compatible with this version\n" + "of bed_mesh. Profile Version: %d Current Version: %d " + % (name, version, PROFILE_VERSION)) + self.incompatible_profiles.append(name) + continue + self.profiles[name] = {} + zvals = profile.getlists('points', seps=(',', '\n'), parser=float) + self.profiles[name]['points'] = zvals + self.profiles[name]['mesh_params'] = params = \ + collections.OrderedDict() + for key, t in PROFILE_OPTIONS.items(): + if t is int: + params[key] = profile.getint(key) + elif t is float: + params[key] = profile.getfloat(key) + elif t is str: + params[key] = profile.get(key) + # Register GCode + self.gcode.register_command( + 'BED_MESH_PROFILE', self.cmd_BED_MESH_PROFILE, + desc=self.cmd_BED_MESH_PROFILE_help) + def get_profiles(self): + return self.profiles + def _check_incompatible_profiles(self): + if self.incompatible_profiles: + configfile = self.printer.lookup_object('configfile') + for profile in self.incompatible_profiles: + configfile.remove_section('bed_mesh ' + profile) + self.gcode.respond_info( + "The following incompatible profiles have been detected\n" + "and are scheduled for removal:\n%s\n" + "The SAVE_CONFIG command will update the printer config\n" + "file and restart the printer" % + (('\n').join(self.incompatible_profiles))) + def save_profile(self, prof_name): + z_mesh = self.bedmesh.get_mesh() + if z_mesh is None: + self.gcode.respond_info( + "Unable to save to profile [%s], the bed has not been probed" + % (prof_name)) + return + probed_matrix = z_mesh.get_probed_matrix() + mesh_params = z_mesh.get_mesh_params() + configfile = self.printer.lookup_object('configfile') + cfg_name = self.name + " " + prof_name + # set params + z_values = "" + for line in probed_matrix: + z_values += "\n " + for p in line: + z_values += "%.6f, " % p + z_values = z_values[:-2] + configfile.set(cfg_name, 'version', PROFILE_VERSION) + configfile.set(cfg_name, 'points', z_values) + for key, value in mesh_params.items(): + configfile.set(cfg_name, key, value) + # save copy in local storage + # ensure any self.profiles returned as status remains immutable + profiles = dict(self.profiles) + profiles[prof_name] = profile = {} + profile['points'] = probed_matrix + profile['mesh_params'] = collections.OrderedDict(mesh_params) + self.profiles = profiles + self.bedmesh.update_status() + self.gcode.respond_info( + "Bed Mesh state has been saved to profile [%s]\n" + "for the current session. The SAVE_CONFIG command will\n" + "update the printer config file and restart the printer." + % (prof_name)) + def load_profile(self, prof_name): + profile = self.profiles.get(prof_name, None) + if profile is None: + raise self.gcode.error( + "bed_mesh: Unknown profile [%s]" % prof_name) + probed_matrix = profile['points'] + mesh_params = profile['mesh_params'] + z_mesh = ZMesh(mesh_params, prof_name) + try: + z_mesh.build_mesh(probed_matrix) + except BedMeshError as e: + raise self.gcode.error(str(e)) + self.bedmesh.set_mesh(z_mesh) + def remove_profile(self, prof_name): + if prof_name in self.profiles: + configfile = self.printer.lookup_object('configfile') + configfile.remove_section('bed_mesh ' + prof_name) + profiles = dict(self.profiles) + del profiles[prof_name] + self.profiles = profiles + self.bedmesh.update_status() + self.gcode.respond_info( + "Profile [%s] removed from storage for this session.\n" + "The SAVE_CONFIG command will update the printer\n" + "configuration and restart the printer" % (prof_name)) + else: + self.gcode.respond_info( + "No profile named [%s] to remove" % (prof_name)) + cmd_BED_MESH_PROFILE_help = "Bed Mesh Persistent Storage management" + def cmd_BED_MESH_PROFILE(self, gcmd): + options = collections.OrderedDict({ + 'LOAD': self.load_profile, + 'SAVE': self.save_profile, + 'REMOVE': self.remove_profile + }) + for key in options: + name = gcmd.get(key, None) + if name is not None: + if not name.strip(): + raise gcmd.error( + "Value for parameter '%s' must be specified" % (key) + ) + if name == "default" and key == 'SAVE': + gcmd.respond_info( + "Profile 'default' is reserved, please choose" + " another profile name.") + else: + options[key](name) + return + gcmd.respond_info("Invalid syntax '%s'" % (gcmd.get_commandline(),)) + + +def load_config(config): + return BedMesh(config) diff --git a/files/cartographer/cartographer.cfg b/files/cartographer/cartographer.cfg new file mode 100644 index 0000000..04c29d2 --- /dev/null +++ b/files/cartographer/cartographer.cfg @@ -0,0 +1,39 @@ +[mcu scanner] +serial: /dev/cartographer + +[scanner] +mcu: scanner +x_offset: 0 +y_offset: -15 +sensor: cartographer +sensor_alt: carto +mesh_runs: 2 + +[temperature_sensor Cartographer_MCU] +sensor_type: temperature_mcu +sensor_mcu: scanner +min_temp: 0 +max_temp: 105 + +[bed_mesh] +zero_reference_position: 175, 175 +speed: 200 +mesh_min: 10,5 +mesh_max: 340,330 +probe_count:31,31 +mesh_pps: 2, 2 +fade_start: 5.0 +fade_end: 50.0 +bicubic_tension: 0.2 +algorithm: bicubic +horizontal_move_z:5 +split_delta_z: 0.01 +move_check_distance: 3 + +[stepper_y] +position_endstop: -0.4 +position_min: -0.4 + +[stepper_z] +endstop_pin: probe:z_virtual_endstop # uses cartographer as virtual endstop +homing_retract_dist: 0 # cartographer needs this to be set to 0 diff --git a/files/cartographer/cartographer.init b/files/cartographer/cartographer.init new file mode 100755 index 0000000..0997e19 --- /dev/null +++ b/files/cartographer/cartographer.init @@ -0,0 +1,24 @@ +#!/bin/ash /etc/rc.common + +START=50 +USE_PROCD=1 +DEPEND=fstab +PROG=/mnt/UDISK/bin/usb_bridge + +start_service() { + procd_open_instance + procd_set_param command $PROG + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param respawn 3600 5 5 + procd_set_param pidfile /var/run/cartographer.pid + procd_close_instance +} + +reload_service() { + stop + sleep 2 + start + # restarting the bridge will upset klipper, force restart + /etc/init.d/klipper restart +} diff --git a/files/cartographer/cartographer.sh b/files/cartographer/cartographer.sh new file mode 100755 index 0000000..5c4f130 --- /dev/null +++ b/files/cartographer/cartographer.sh @@ -0,0 +1,66 @@ +#!/bin/ash +set -e + +SCRIPT_DIR=$(readlink -f $(dirname ${0})) +ACTION=${1} +PATCH_LEGACY="${SCRIPT_DIR}/homing.patch" +PATCH_SCANNER="${SCRIPT_DIR}/homing.scanner.patch" +RESTORE_PATH="${SCRIPT_DIR}/restore-path.sh" +HOMING_FILE="${HOME}/klipper/klippy/extras/homing.py" + +usage() { + echo "" + echo "${0} ACTION" + echo "" + echo "ACTION:" + echo " enable -- enables the cartographer probe, disabling the prtouch" + echo " disable -- disables the cartogrpher probe, enabling the prtouch" + echo " restart -- restarts the cartographer serial bridge" + echo "" +} + +case ${ACTION} in + enable) + ln -sf ~/cartographer-klipper/scanner.py ~/klipper/klippy/extras + ln -sf ~/cartographer-klipper/cartographer.py ~/klipper/klippy/extras + ln -sf ~/cartographer-klipper/idm.py ~/klipper/klippy/extras + cd ~/klipper/klippy/extras + if grep -q "lookup_object('scanner')" "${HOMING_FILE}"; then + echo "I: homing.py already patched for scanner" + elif grep -q "self.prtouch_v3 = self.printer.lookup_object('prtouch_v3') if self.printer.objects.get('prtouch_v3') else None" "${HOMING_FILE}"; then + patch < "${PATCH_SCANNER}" + else + patch < "${PATCH_LEGACY}" + fi + rm -f homing.pyc + rm -f bed_mesh.py* + ln -sf "${SCRIPT_DIR}/bed_mesh.py" ./bed_mesh.py + sed -E \ + -i \ + -e 's/(.*prtouch.*)/#\1/' \ + -e 's/#(.*carto.*)/\1/' \ + ~/printer_data/config/custom/main.cfg + /etc/init.d/klipper restart + ;; + disable) + rm -f ~/klipper/klippy/extras/scanner.py* + rm -f ~/klipper/klippy/extras/cartographer.py* + rm -f ~/klipper/klippy/extras/idm.py* + sed -E \ + -i \ + -e 's/#(.*prtouch.*)/\1/' \ + -e 's/(.*carto.*)/#\1/' \ + ~/printer_data/config/custom/main.cfg + sh "${RESTORE_PATH}" ~/klipper/klippy/extras/homing.py + sh "${RESTORE_PATH}" ~/klipper/klippy/extras/homing.pyc + sh "${RESTORE_PATH}" ~/klipper/klippy/extras/bed_mesh.py + sh "${RESTORE_PATH}" ~/klipper/klippy/extras/bed_mesh.pyc + /etc/init.d/klipper restart + ;; + restart) + /etc/init.d/cartographer restart + ;; + *) + usage + ;; +esac diff --git a/files/cartographer/ensure_included.py b/files/cartographer/ensure_included.py new file mode 100755 index 0000000..15c4d87 --- /dev/null +++ b/files/cartographer/ensure_included.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import os +import sys + +def add_include(config_path, include_path, commented=False): + """ + Add an include statement to a configuration file if it doesn't exist. + + Args: + config_path (str): Full path to the configuration file + include_path (str): Path to be included + commented (bool): Whether to comment out the include (default: False) + """ + target = f"[include {include_path}]" + if commented: + target = f"#{target}" + + # Create the directory path if it doesn't exist + os.makedirs(os.path.dirname(config_path), exist_ok=True) + + # If file doesn't exist, create it with the include + if not os.path.exists(config_path): + with open(config_path, 'w') as handle: + handle.write(target + '\n') + return + + update_needed = True + insert_before = False + + with open(config_path, 'r') as handle: + contents = handle.readlines() + + for line in contents: + if line.strip() == target: + update_needed = False + break + if line.startswith('#*#'): + insert_before = True + break + if line.startswith('[include overrides.cfg]'): + insert_before = True + break + + if update_needed: + if insert_before: + contents.insert(contents.index(line), target + '\n') + else: + contents.append(target + '\n') + with open(config_path, 'w') as handle: + handle.writelines(contents) + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: script.py [commented]") + sys.exit(1) + + config_path = os.path.expanduser(sys.argv[1]) + include_path = os.path.expanduser(sys.argv[2]) + commented = bool(sys.argv[3]) if len(sys.argv) > 3 else False + + add_include(config_path, include_path, commented) diff --git a/files/cartographer/fix_venv.py b/files/cartographer/fix_venv.py new file mode 100755 index 0000000..7e04a60 --- /dev/null +++ b/files/cartographer/fix_venv.py @@ -0,0 +1,55 @@ +import os +import sys +import sysconfig +import shutil +from pathlib import Path + +def update_so_files(venv_path): + """ + Walk through a virtualenv and update all SO files to match system expectations. + + Args: + venv_path: Path to the virtualenv directory + """ + # Get the expected suffix for this Python installation + expected_suffix = sysconfig.get_config_var('EXT_SUFFIX') + if not expected_suffix: + print("Error: Could not determine Python extension suffix", file=sys.stderr) + return 1 + + print(f"Expected suffix: {expected_suffix}") + + # Walk through all directories in the venv + for root, _, files in os.walk(venv_path): + for filename in files: + if filename.endswith('.so'): + filepath = Path(root) / filename + + # Skip if it already has the correct suffix + if filename.endswith(expected_suffix): + continue + + # Get the base module name (strip off .cpython-*-*.so) + base_name = filename.split('.cpython-')[0] + new_name = base_name + expected_suffix + new_path = filepath.parent / new_name + + print(f"Renaming: {filepath} -> {new_path}") + try: + shutil.move(str(filepath), str(new_path)) + except OSError as e: + print(f"Error renaming {filepath}: {e}", file=sys.stderr) + + return 0 + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage: script.py ", file=sys.stderr) + sys.exit(1) + + venv_path = sys.argv[1] + if not os.path.isdir(venv_path): + print(f"Error: {venv_path} is not a directory", file=sys.stderr) + sys.exit(1) + + sys.exit(update_so_files(venv_path)) diff --git a/files/cartographer/homing.patch b/files/cartographer/homing.patch new file mode 100644 index 0000000..df75691 --- /dev/null +++ b/files/cartographer/homing.patch @@ -0,0 +1,17 @@ +--- homing.py.orig ++++ homing.py +@@ -43,7 +43,12 @@ + if toolhead is None: + toolhead = printer.lookup_object('toolhead') + +- self.prtouch_v3 = printer.lookup_object('prtouch_v3') +- self.prtouch_v3.z_full_movement_flag = False ++ self.prtouch_v3 = None ++ if self.printer.objects.get('scanner'): ++ self.prtouch_v3 = self.printer.lookup_object('scanner') ++ elif self.printer.objects.get('prtouch_v3'): ++ self.prtouch_v3 = self.printer.lookup_object('prtouch_v3') ++ if self.prtouch_v3 is not None: ++ self.prtouch_v3.z_full_movement_flag = False + self.toolhead = toolhead + self.stepper_positions = [] diff --git a/files/cartographer/homing.scanner.patch b/files/cartographer/homing.scanner.patch new file mode 100644 index 0000000..96e106a --- /dev/null +++ b/files/cartographer/homing.scanner.patch @@ -0,0 +1,17 @@ +--- homing.py.orig ++++ homing.py +@@ -43,7 +43,12 @@ + if toolhead is None: + toolhead = printer.lookup_object('toolhead') + +- self.prtouch_v3 = self.printer.lookup_object('prtouch_v3') if self.printer.objects.get('prtouch_v3') else None +- self.prtouch_v3.z_full_movement_flag = False ++ self.prtouch_v3 = None ++ if self.printer.objects.get('scanner'): ++ self.prtouch_v3 = self.printer.lookup_object('scanner') ++ elif self.printer.objects.get('prtouch_v3'): ++ self.prtouch_v3 = self.printer.lookup_object('prtouch_v3') ++ if self.prtouch_v3 is not None: ++ self.prtouch_v3.z_full_movement_flag = False + self.toolhead = toolhead + self.stepper_positions = [] diff --git a/files/cartographer/install.sh b/files/cartographer/install.sh new file mode 100644 index 0000000..f7802a0 --- /dev/null +++ b/files/cartographer/install.sh @@ -0,0 +1,290 @@ +#!/bin/ash +set -e + +SCRIPT_DIR=$(readlink -f $(dirname ${0})) + +cd ${HOME} + +export TMPDIR=/mnt/UDISK/tmp + +PRINTER_DATA_DIR="/mnt/UDISK/printer_data" +if [ ! -d "$PRINTER_DATA_DIR" ]; then + PRINTER_DATA_DIR="${HOME}/printer_data" +fi + +export PRINTER_DATA_DIR + +HOMING_DIR=$(readlink -f ~/klipper/klippy/extras) +HOMING_FILE="${HOMING_DIR}/homing.py" +BED_MESH_FILE="${HOMING_DIR}/bed_mesh.py" +PRINTER_CFG="${PRINTER_DATA_DIR}/config/printer.cfg" +CUSTOM_DIR="${PRINTER_DATA_DIR}/config/custom" +CUSTOM_MAIN="${CUSTOM_DIR}/main.cfg" +CARTOGRAPHER_CFG="${CUSTOM_DIR}/cartographer.cfg" +PRTOUCH_CFG="${CUSTOM_DIR}/prtouch_v3.cfg" +PATCH_MODE="" + +precheck() { + if [ ! -f "$HOMING_FILE" ]; then + echo "E: homing.py not found at $HOMING_FILE" + exit 1 + fi + if [ ! -f "$BED_MESH_FILE" ]; then + echo "E: bed_mesh.py not found at $BED_MESH_FILE" + exit 1 + fi + if [ ! -d "$PRINTER_DATA_DIR" ]; then + echo "E: printer_data not found at $PRINTER_DATA_DIR" + exit 1 + fi + if ! command -v git >/dev/null 2>&1; then + echo "E: git not found" + exit 1 + fi + if [ ! -d "$CUSTOM_DIR" ]; then + mkdir -p "$CUSTOM_DIR" + fi + + if grep -q "lookup_object('scanner')" "$HOMING_FILE"; then + PATCH_MODE="skip" + elif grep -q "prtouch_v3" "$HOMING_FILE" && grep -q "z_full_movement_flag" "$HOMING_FILE"; then + PATCH_MODE="apply" + else + echo "E: unsupported homing.py format, aborting" + exit 1 + fi +} + +apply_homing_patch() { + local py_bin="python3" + if ! command -v "$py_bin" >/dev/null 2>&1; then + py_bin="python" + fi + if ! command -v "$py_bin" >/dev/null 2>&1; then + echo "E: python not found for homing patch" + exit 1 + fi + "$py_bin" - "$HOMING_FILE" <<'PY_HOMING' +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +text = path.read_text() +if "lookup_object('scanner')" in text or 'lookup_object("scanner")' in text: + print("I: homing.py already patched for scanner") + sys.exit(0) + +pattern = re.compile( + r"^(?P\s*)self\.prtouch_v3\s*=.*lookup_object\([\"\']prtouch_v3[\"\']\).*\n" + r"(?P=indent)self\.prtouch_v3\.z_full_movement_flag\s*=\s*False\s*$", + re.M, +) +match = pattern.search(text) +if not match: + sys.stderr.write("E: unsupported homing.py format for patch\n") + sys.exit(1) + +indent = match.group("indent") +replacement = ( + f"{indent}self.prtouch_v3 = None\n" + f"{indent}if self.printer.objects.get('scanner'):\n" + f"{indent} self.prtouch_v3 = self.printer.lookup_object('scanner')\n" + f"{indent}elif self.printer.objects.get('prtouch_v3'):\n" + f"{indent} self.prtouch_v3 = self.printer.lookup_object('prtouch_v3')\n" + f"{indent}if self.prtouch_v3 is not None:\n" + f"{indent} self.prtouch_v3.z_full_movement_flag = False\n" +) + +text = pattern.sub(replacement, text, count=1) +path.write_text(text) +print("I: homing.py patched for scanner") +PY_HOMING +} + + +disable_prtouch_includes() { + local cfg_file="$1" + [ -f "$cfg_file" ] || return 0 + awk '{ + line=$0 + if (line ~ /^\[include/ && line ~ /prtouch_v3\.cfg/ && line !~ /^#/) { + print "#" line + } else { + print line + } + }' "$cfg_file" > "${cfg_file}.tmp" && mv "${cfg_file}.tmp" "$cfg_file" +} + + +BACKUP_DIR="/tmp/cartographer-backup-$(date +%s)" + +backup_file() { + local src="$1" + local name="$2" + if [ -f "$src" ]; then + cp -f "$src" "${BACKUP_DIR}/${name}" + else + touch "${BACKUP_DIR}/${name}.missing" + fi +} + +restore_file() { + local src="$1" + local name="$2" + if [ -f "${BACKUP_DIR}/${name}" ]; then + cp -f "${BACKUP_DIR}/${name}" "$src" + elif [ -f "${BACKUP_DIR}/${name}.missing" ]; then + rm -f "$src" + fi +} + +rollback() { + echo "E: install failed, rolling back changes" + if [ -d "$BACKUP_DIR" ]; then + restore_file "$HOMING_FILE" homing.py + restore_file "$BED_MESH_FILE" bed_mesh.py + restore_file "$PRINTER_CFG" printer.cfg + restore_file "$CUSTOM_MAIN" main.cfg + restore_file "$CARTOGRAPHER_CFG" cartographer.cfg + restore_file "$PRTOUCH_CFG" prtouch_v3.cfg + fi +} + +SUCCESS=0 +on_exit() { + if [ "$SUCCESS" -eq 0 ]; then + rollback + fi +} +trap 'on_exit' EXIT + +precheck + +mkdir -p "$BACKUP_DIR" +backup_file "$HOMING_FILE" homing.py +backup_file "$BED_MESH_FILE" bed_mesh.py +backup_file "$PRINTER_CFG" printer.cfg +backup_file "$CUSTOM_MAIN" main.cfg +backup_file "$CARTOGRAPHER_CFG" cartographer.cfg +backup_file "$PRTOUCH_CFG" prtouch_v3.cfg + +if [ ! -d cartographer-klipper/.git ]; then + if [ -d cartographer-klipper ]; then + rm -rf cartographer-klipper + fi + git clone https://github.com/jamincollins/cartographer-klipper.git + git -C cartographer-klipper checkout k2 +fi + +if [ -L klippy-env ]; then + echo "I: moving klippy-env to /mnt/UDISK/root" + # move lippy-env to /mnt/UDISK + rm -f klippy-env + rsync -SHa /usr/share/klippy-env/ klippy-env/ +fi + +ensure_pip() { + if ~/klippy-env/bin/pip --version >/dev/null 2>&1; then + echo "I: pip already available, skipping upgrade" + return 0 + fi + echo "I: upgrading klippy-env pip version" + local pip_url="https://bootstrap.pypa.io/get-pip.py" + local pip_file="${PWD}/get-pip.py" + rm -f "$pip_file" + if command -v curl >/dev/null 2>&1; then + curl -fsSL --retry 3 --retry-delay 2 -o "$pip_file" "$pip_url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$pip_file" "$pip_url" + else + echo "E: curl or wget not found" + return 1 + fi + ~/klippy-env/bin/python3 "$pip_file" + rm -f "$pip_file" +} +ensure_pip + +# ensure we are pulling wheels from piwheels +if ! grep -q 'extra-index-url=https://www.piwheels.org/simple' /etc/pip.conf; then + echo 'extra-index-url=https://www.piwheels.org/simple' >> /etc/pip.conf +fi + +# install requirements +REQ_FILE="cartographer-klipper/requirements.txt" +REQ_HASH_FILE="${HOME}/.cartographer_requirements_hash" +REQ_HASH="" +if command -v sha1sum >/dev/null 2>&1; then + REQ_HASH=$(sha1sum "$REQ_FILE" | awk '{print $1}') +elif command -v md5sum >/dev/null 2>&1; then + REQ_HASH=$(md5sum "$REQ_FILE" | awk '{print $1}') +fi + +if [ -n "$REQ_HASH" ] && [ -f "$REQ_HASH_FILE" ] && [ "$(cat "$REQ_HASH_FILE")" = "$REQ_HASH" ]; then + echo "I: cartographer requirements already satisfied, skipping" +else + echo "I: installing cartographer requirements" + ~/klippy-env/bin/pip install --upgrade --requirement "$REQ_FILE" + if [ -n "$REQ_HASH" ]; then + echo "$REQ_HASH" > "$REQ_HASH_FILE" + fi +fi + +# fix the klippy-env libraries +python3 ${SCRIPT_DIR}/fix_venv.py ~/klippy-env + +# drop missing libraries in place +echo "I: installing cartographer libraries" +cp ${SCRIPT_DIR}/*.so* /usr/lib/ + +# install cartographer +echo "I: installing cartographer" +~/cartographer-klipper/install.sh + +# install usb-serial bridge +mkdir -p /mnt/UDISK/bin +ln -sf ${SCRIPT_DIR}/usb_bridge /mnt/UDISK/bin/usb_bridge +chmod +x /mnt/UDISK/bin/usb_bridge +rm -f /mnt/UDISK/bin/cartographer.sh +ln -sf ${SCRIPT_DIR}/cartographer.sh /mnt/UDISK/bin/cartographer.sh +ln -sf ${SCRIPT_DIR}/cartographer.init /etc/init.d/cartographer +ln -sf ${SCRIPT_DIR}/cartographer.init /opt/etc/init.d/S50cartographer +/etc/init.d/cartographer start + +# install cartographer convenience scripts +ln -sf ${SCRIPT_DIR}/cartographer.sh /mnt/UDISK/bin +chmod +x /mnt/UDISK/bin/cartographer.sh + +# remove the prtouch_v3 section from printer.cfg +python ${SCRIPT_DIR}/alter_config.py +disable_prtouch_includes "$PRINTER_CFG" +disable_prtouch_includes "$CUSTOM_MAIN" +# add a commented include to custom/main.cfg +python ${SCRIPT_DIR}/ensure_included.py \ + "${PRINTER_DATA_DIR}/config/custom/main.cfg" prtouch_v3.cfg True +# add the main.cfg to printer.cfg +python ${SCRIPT_DIR}/ensure_included.py \ + "${PRINTER_DATA_DIR}/config/printer.cfg" custom/main.cfg +# I believe I still want this as a true copy +# add the cartographer.cfg to main.cfg +cp ${SCRIPT_DIR}/cartographer.cfg "${PRINTER_DATA_DIR}/config/custom" +python ${SCRIPT_DIR}/ensure_included.py "${PRINTER_DATA_DIR}/config/custom/main.cfg" cartographer.cfg + +# patch homing.py for scanner support +if [ "$PATCH_MODE" = "skip" ]; then + echo "I: homing.py already patched for scanner" +else + echo "I: applying homing scanner patch" + apply_homing_patch +fi +rm -f "${HOMING_FILE}c" + +# replace the bed mesh +rm -f "${BED_MESH_FILE}" "${BED_MESH_FILE}c" +ln -sf "${SCRIPT_DIR}/bed_mesh.py" "${BED_MESH_FILE}" + +# restart klipper +/etc/init.d/klipper restart + +SUCCESS=1 diff --git a/files/cartographer/libgfortran.so.4 b/files/cartographer/libgfortran.so.4 new file mode 100644 index 0000000..e59d012 Binary files /dev/null and b/files/cartographer/libgfortran.so.4 differ diff --git a/files/cartographer/libopenblas.so.0 b/files/cartographer/libopenblas.so.0 new file mode 100644 index 0000000..56d14d9 Binary files /dev/null and b/files/cartographer/libopenblas.so.0 differ diff --git a/files/cartographer/restore-path.sh b/files/cartographer/restore-path.sh new file mode 100755 index 0000000..ad9b123 --- /dev/null +++ b/files/cartographer/restore-path.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +FULLPATH=$(readlink -f ${1}) +rm -fr ${FULLPATH} +rm -fr /overlay/upper${FULLPATH} +mount -o remount / diff --git a/files/cartographer/update-klippy-venv.sh b/files/cartographer/update-klippy-venv.sh new file mode 100644 index 0000000..ee1c572 --- /dev/null +++ b/files/cartographer/update-klippy-venv.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +source klippy-venv/bin/activate + +wget https://bootstrap.pypa.io/get-pip.py +python ./get-pip.py diff --git a/files/cartographer/usb_bridge b/files/cartographer/usb_bridge new file mode 100755 index 0000000..f704ee9 Binary files /dev/null and b/files/cartographer/usb_bridge differ diff --git a/files/entware/generic.sh b/files/entware/generic.sh index 85e4393..7988e22 100755 --- a/files/entware/generic.sh +++ b/files/entware/generic.sh @@ -23,14 +23,22 @@ do done echo -e "Info: Downloading opkg package manager from Entware repo..." -chmod 755 /usr/data/helper-script/files/fixes/curl +if command -v curl >/dev/null 2>&1; then + CURL_BIN="$(command -v curl)" +elif [ -x /usr/data/helper-script/files/fixes/curl ]; then + CURL_BIN="/usr/data/helper-script/files/fixes/curl" + chmod 755 "$CURL_BIN" +else + echo "Error: curl not found. Install curl or run bootstrap again." + exit 1 +fi primary_URL="https://bin.entware.net/mipselsf-k3.4/installer" secondary_URL="http://www.openk1.org/static/entware/mipselsf-k3.4/installer" download_files() { local url="$1" local output_file="$2" - /usr/data/helper-script/files/fixes/curl -L "$url" -o "$output_file" + "$CURL_BIN" -L "$url" -o "$output_file" return $? } diff --git a/files/fixes/curl b/files/fixes/curl index 00cb09f..421b7ad 100755 Binary files a/files/fixes/curl and b/files/fixes/curl differ diff --git a/files/fixes/curl.bin b/files/fixes/curl.bin new file mode 100755 index 0000000..00cb09f Binary files /dev/null and b/files/fixes/curl.bin differ diff --git a/files/guppy-screen/guppy-update.sh b/files/guppy-screen/guppy-update.sh index ee9eb42..5505988 100644 --- a/files/guppy-screen/guppy-update.sh +++ b/files/guppy-screen/guppy-update.sh @@ -1,7 +1,15 @@ #!/bin/sh GUPPY_DIR="/usr/data/guppyscreen" -CURL="/usr/data/helper-script/files/fixes/curl" +if command -v curl >/dev/null 2>&1; then + CURL_BIN="$(command -v curl)" +elif [ -x /usr/data/helper-script/files/fixes/curl ]; then + CURL_BIN="/usr/data/helper-script/files/fixes/curl" + chmod 755 "$CURL_BIN" +else + echo "Error: curl not found. Install curl or run bootstrap again." + exit 1 +fi VERSION_FILE="$GUPPY_DIR/.version" CUSTOM_UPGRADE_SCRIPT="$GUPPY_DIR/custom_upgrade.sh" @@ -11,7 +19,7 @@ if [ -f "$VERSION_FILE" ]; then ASSET_NAME=$(jq '.asset_name' "$VERSION_FILE") fi -"$CURL" -s https://api.github.com/repos/ballaswag/guppyscreen/releases -o /tmp/guppy-releases.json +"$CURL_BIN" -s https://api.github.com/repos/ballaswag/guppyscreen/releases -o /tmp/guppy-releases.json latest_version=$(jq -r '.[0].tag_name' /tmp/guppy-releases.json) if [ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n1)" = "$latest_version" ]; then echo "Guppy Screen $CURRENT_VERSION is already up to date!" @@ -20,7 +28,7 @@ if [ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n1) else asset_url=$(jq -r ".[0].assets[] | select(.name == $ASSET_NAME).browser_download_url" /tmp/guppy-releases.json) echo "Downloading latest version $latest_version from $asset_url" - "$CURL" -L "$asset_url" -o /usr/data/guppyscreen.tar.gz + "$CURL_BIN" -L "$asset_url" -o /usr/data/guppyscreen.tar.gz fi tar -xvf /usr/data/guppyscreen.tar.gz -C "$GUPPY_DIR/.." diff --git a/files/macros/bed_mesh.cfg b/files/macros/bed_mesh.cfg new file mode 100644 index 0000000..325f74a --- /dev/null +++ b/files/macros/bed_mesh.cfg @@ -0,0 +1,70 @@ +### WARNING: Do not edit this file, changes will be overwritten! ### + +# to make changes, copy this macro to the overrides.cfg in the custom/ directory + +[respond] + +# if you find you need to alter these macros, please file a feature request +# at https://github.com/jamincollins/k2-improvements + +[gcode_macro _CREATE_MESH] +gcode: + {% set BED_TEMP = params.BED_TEMP|default(60)|float %} + {% set CHAMBER_TEMP = params.CHAMBER_TEMP|default(0)|float %} + {% set PROFILE_NAME = BED_TEMP|string + 'c_' + CHAMBER_TEMP|string + 'c' %} + + ## make these configurable + {% set SOAK_TIME = params.SOAK_TIME|default(5)|float %} + {% set EXTRUDER_WAITTEMP = (140.0|float)|int %} + + {% if not 'xyz' in printer.toolhead.homed_axes %} + G28 + {% endif %} + + M104 S{EXTRUDER_WAITTEMP} + M190 S{BED_TEMP} + M109 S{EXTRUDER_WAITTEMP} + + {% if CHAMBER_TEMP > 0 %} + M191 S{CHAMBER_TEMP} + {% endif %} + + RESPOND MSG="Soaking for {SOAK_TIME} minutes ..." + M117 Heat soaking + G4 P{60000 * SOAK_TIME} # x minute heat soak + + # ensure bed is level + # some users have reported that the bed does not raise uniformly from the bottom + Z_TILT_ADJUST + # rehome Z after tilt adjust + G28 Z + + BOX_NOZZLE_CLEAN + + M117 Bed meshing + BED_MESH_CALIBRATE PROFILE={PROFILE_NAME} + + +[gcode_macro MESH_IF_NEEDED] +gcode: + {% set BED_TEMP = params.BED_TEMP|default(60)|float %} + {% set CHAMBER_TEMP = params.CHAMBER_TEMP|default(0)|float %} + {% set PROFILE_NAME = BED_TEMP|string + 'c_' + CHAMBER_TEMP|string + 'c' %} + + ## make these configurable + {% set SOAK_TIME = params.SOAK_TIME|default(5)|float %} + + {% if printer.scanner %} # cartographer + _CREATE_MESH BED_TEMP={BED_TEMP} CHAMBER_TEMP={CHAMBER_TEMP} SOAK_TIME={SOAK_TIME} + {% else %} + RESPOND MSG="Looking for {PROFILE_NAME} ..." + + {% if PROFILE_NAME in printer.bed_mesh.profiles %} + RESPOND MSG="{PROFILE_NAME} exists ..." + {% else %} + M117 Mesh {PROFILE_NAME} missing + RESPOND MSG="{PROFILE_NAME} does not exist, creating ..." + + _CREATE_MESH BED_TEMP={BED_TEMP} CHAMBER_TEMP={CHAMBER_TEMP} SOAK_TIME={SOAK_TIME} + {% endif %} + {% endif %} diff --git a/files/macros/m191.cfg b/files/macros/m191.cfg new file mode 100644 index 0000000..173686f --- /dev/null +++ b/files/macros/m191.cfg @@ -0,0 +1,43 @@ +### WARNING: Do not edit this file, changes will be overwritten! ### + +# to make changes, copy this macro to the overrides.cfg in the custom/ directory + +[gcode_macro M191] +#TODO: should this leverage the existing M141? +description: Set and wait for chamber temperature, with intelligent bed assist +gcode: + SAVE_GCODE_STATE NAME=M191 + # Parameters + {% set S = params.S|default(0)|float %} + + {% if S == 0 %} + TURN_OFF_HEATERS + M107 + {% else %} + M117 Heating chamber + {% if S > 35.0 %} + RESPOND MSG="The chamber heater alone can not reach {S}c, using bed assist ..." + # bed needs to raise to near the nozzle + {% if "xyz" not in printer.toolhead.homed_axes %} + G28 + {% endif %} + G1 Z5 F600 + {% if printer.heater_bed.target > 99.0 %} + RESPOND MSG="Bed target temp already high enough, not changing ..." + {% else %} + RESPOND MSG="Bed target temp not high enough, setting to 105c ..." + SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=105 + {% endif %} + SET_PIN PIN=fan2 VALUE=153 + {% endif %} + # turn on the fan to help + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=chamber_fan TARGET={S} + # Set chamber temperature + SET_HEATER_TEMPERATURE HEATER=chamber_heater TARGET={S} + + # Wait indefinitely for temperature + TEMPERATURE_WAIT SENSOR="temperature_sensor chamber_temp" MINIMUM={S} MAXIMUM={S+5} + + RESPOND MSG="Chamber temperature {S}c reached ..." + {% endif %} + RESTORE_GCODE_STATE NAME=M191 diff --git a/files/macros/overrides.cfg b/files/macros/overrides.cfg new file mode 100644 index 0000000..2f9d4f3 --- /dev/null +++ b/files/macros/overrides.cfg @@ -0,0 +1,7 @@ +# including some common overrides, feel free to change these to your liking + +[virtual_sdcard] +forced_leveling: false + +[bed_mesh] +probe_count:19,19 diff --git a/files/macros/start_print.cfg b/files/macros/start_print.cfg new file mode 100644 index 0000000..10ead90 --- /dev/null +++ b/files/macros/start_print.cfg @@ -0,0 +1,158 @@ +### WARNING: Do not edit this file, changes will be overwritten! ### + +# to make changes, copy this macro to the overrides.cfg in the custom/ directory + +[gcode_macro _START_PRINT_VARS] +###################################################################### +# !!! copy this to overrides.cfg and set your material offset here !!! +###################################################################### +variable_offset_PLA: 0 +variable_offset_PETG: 0 +variable_offset_ABS: 0 +variable_offset_ASA: 0 +variable_offset_DEFAULT: 0 +variable_offset_PROBE: 0 +variable_heat_soak: 5 # minutes +gcode: + +# if you find you need to alter this macro, please file a feature request +# at https://github.com/jamincollins/k2-improvements + +[gcode_macro START_PRINT] +# be sure to update your slicer to pass in both chamber temp and material type, for Creality Print: +# START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] CHAMBER_TEMP=[overall_chamber_temperature] MATERIAL={filament_type[initial_tool]} +variable_prepare: 0 +gcode: + M117 START_PRINT + BOX_START_PRINT # what exactly does this do? + G90 + {% set BED_TEMP = params.BED_TEMP|default(60)|float %} + {% set CHAMBER_TEMP = params.CHAMBER_TEMP|default(0)|float %} + {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(220)|float %} + {% set EXTRUDER_WAITTEMP = (140.0|float)|int %} + {% set MATERIAL = params.MATERIAL|default('')|string %} + + {% if printer['gcode_macro _START_PRINT_VARS'].offset_probe == 1 %} + RESPOND MSG="Not adjusting Z offset relying on probe ..." + {% else %} + {% if MATERIAL == 'PLA' %} + {% set OFFSET = printer['gcode_macro _START_PRINT_VARS'].offset_pla %} + {% elif MATERIAL == 'PETG' %} + {% set OFFSET = printer['gcode_macro _START_PRINT_VARS'].offset_petg %} + {% elif MATERIAL == 'ASA' %} + {% set HEAT_BUMP = 1 %} + {% set OFFSET = printer['gcode_macro _START_PRINT_VARS'].offset_asa %} + {% elif MATERIAL == 'ABS' %} + {% set HEAT_BUMP = 1 %} + {% set OFFSET = printer['gcode_macro _START_PRINT_VARS'].offset_abs %} + {% else %} + # default value + {% set OFFSET = printer['gcode_macro _START_PRINT_VARS'].offset_default %} + {% endif %} + + RESPOND MSG="Setting Z offset for {MATERIAL} of {OFFSET} ..." + SET_GCODE_OFFSET Z={OFFSET} + {% endif %} + + {% if 'scanner' not in printer %} + # presumably prtouch, so try to ensure the nozzle is clear + BOX_NOZZLE_CLEAN + {% endif %} + + # better safe than sorry, re-homing is a small price to pay + G28 + # ensure bed is level + # some users have reported that the bed does not raise uniformly from the bottom + Z_TILT_ADJUST + # rehome Z after tilt adjust + G28 Z + + {% if CHAMBER_TEMP > 0 %} + M141 S{CHAMBER_TEMP} + {% endif %} + + # when is a print prepared? + {% if printer['gcode_macro START_PRINT'].prepare|int == 0 %} + {action_respond_info("print prepared 111")} + M106 S0 # No need to turn off the model fan + M140 S{BED_TEMP} + M104 S{EXTRUDER_WAITTEMP} + SET_VELOCITY_LIMIT ACCEL=5000 ACCEL_TO_DECEL=5000 + M104 S{EXTRUDER_WAITTEMP} + M117 Heating bed ... + M190 S{BED_TEMP} + M109 S{EXTRUDER_WAITTEMP} + BOX_NOZZLE_CLEAN#M1501 + # Return to zero + NEXT_HOMEZ_NACCU + G28 Z + # BED_MESH_CALIBRATE + # CXSAVE_CONFIG + {% else %} + PRINT_PREPARE_CLEAR + {% endif %} + + # don't want to accidently turn off chamber heating if a temp wasn't passed in + {% if CHAMBER_TEMP > 0 %} + M191 S{CHAMBER_TEMP} + {% endif %} + + M109 S{EXTRUDER_WAITTEMP} + + # Ensure bed is at the desired temp + # works around some firmware bugs that sometimes turn off the bed + M190 S{BED_TEMP} + + + {% if CHAMBER_TEMP > 0 %} + {% set SOAK_TIME = printer['gcode_macro _START_PRINT_VARS'].heat_soak|float %} + {% if SOAK_TIME > 0 %} + M117 Heat soaking ... + RESPOND MSG="Heat soaking for {SOAK_TIME} minutes" + G4 P{60000 * SOAK_TIME} + {% endif %} + {% endif %} + + # ensure a nozzle wipe happens before touching the bed + BOX_NOZZLE_CLEAN + + {% if printer.scanner %} # cartographer + BED_MESH_CALIBRATE PROFILE=adaptive ADAPTIVE=1 + CARTOGRAPHER_TOUCH + {% else %} # everyone else + # !!! this MUST come after all G28s as they reset the mesh to "default" + # load the mesh for the current bed and chamber temp + MESH_IF_NEEDED BED_TEMP={BED_TEMP} CHAMBER_TEMP={CHAMBER_TEMP} + RESPOND MSG="Loading bed mesh: {BED_TEMP}c_{CHAMBER_TEMP}c ..." + BED_MESH_PROFILE LOAD={BED_TEMP}c_{CHAMBER_TEMP}c + M117 MESH: {BED_TEMP}c_{CHAMBER_TEMP}c + {% endif %} + + BOX_GO_TO_EXTRUDE_POS#M1500 + M109 S{EXTRUDER_TEMP} ;wait nozzle heating + + # the stock chamber heater configuration is watermark + # which means at best it will reach the target temp, but rarely exceed it + # for materials like ASA the chamber temp should be a _minimum_ + {% if HEAT_BUMP == 1%} + {% if CHAMBER_TEMP > 0 %} + {% if CHAMBER_TEMP + 5 <= 60 %} + M141 S{CHAMBER_TEMP + 5} + {% else %} + M141 S60 + {% endif %} + {% endif %} + {% endif %} + + M220 S100 ;Reset Feedrate + # M221 S100 ;Reset Flowrate + G21 + SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=10 + M204 S5000 + SET_VELOCITY_LIMIT ACCEL_TO_DECEL=5000 + + G92 E0 ; Reset Extruder + SET_PIN PIN=extruder_fan VALUE=1 + M117 Printing ... + +[respond] diff --git a/files/screws-tilt-adjust/screws-tilt-adjust-k2plus.cfg b/files/screws-tilt-adjust/screws-tilt-adjust-k2plus.cfg new file mode 100644 index 0000000..c411bd0 --- /dev/null +++ b/files/screws-tilt-adjust/screws-tilt-adjust-k2plus.cfg @@ -0,0 +1,22 @@ +######################################## +# Screws Tilt Adjust for K2PLUS +######################################## + +[gcode_macro SCREWS_TILT_CALCULATE] +rename_existing: _OG_SCREWS_TILT_CALCULATE +gcode: + Z_TILT_ADJUST + _OG_SCREWS_TILT_CALCULATE + +[screws_tilt_adjust] +screw1: 45,28 +screw1_name: front left screw +screw2: 307,28 +screw2_name: front right screw +screw3: 307,314 +screw3_name: rear right screw +screw4: 45,314 +screw4_name: rear left screw +speed: 150 +horizontal_move_z: 10 +screw_thread: CW-M4 diff --git a/files/services/S50nginx b/files/services/S50nginx index 2bd89f9..3f56035 100755 --- a/files/services/S50nginx +++ b/files/services/S50nginx @@ -7,26 +7,62 @@ NGINX="/usr/data/nginx/sbin/nginx" PIDFILE="/var/run/nginx.pid" NGINX_ARGS="-c /usr/data/nginx/nginx/nginx.conf" +pick_nginx() { + if [ -x "$NGINX" ]; then + "$NGINX" -v >/dev/null 2>&1 && return 0 + fi + + if command -v nginx >/dev/null 2>&1; then + NGINX="$(command -v nginx)" + if [ -f "/usr/data/nginx/nginx/nginx.conf" ]; then + NGINX_ARGS="-c /usr/data/nginx/nginx/nginx.conf" + elif [ -f "/etc/nginx/nginx.conf" ]; then + NGINX_ARGS="-c /etc/nginx/nginx.conf" + else + NGINX_ARGS="" + fi + return 0 + fi + + return 1 +} + case "$1" in start) + if ! pick_nginx; then + echo "Nginx binary not found." + exit 1 + fi echo "Starting nginx..." - mkdir -p /var/log/nginx /var/tmp/nginx - start-stop-daemon -S -p "$PIDFILE" --exec "$NGINX" -- $NGINX_ARGS - ;; + mkdir -p /var/log/nginx /var/tmp/nginx + if [ -n "$NGINX_ARGS" ]; then + start-stop-daemon -S -p "$PIDFILE" --exec "$NGINX" -- $NGINX_ARGS + else + start-stop-daemon -S -p "$PIDFILE" --exec "$NGINX" + fi + ;; stop) + if ! pick_nginx; then + echo "Nginx binary not found; none killed" + exit 0 + fi echo "Stopping nginx..." - start-stop-daemon -K -x "$NGINX" -p "$PIDFILE" -o - ;; + start-stop-daemon -K -x "$NGINX" -p "$PIDFILE" -o + ;; reload|force-reload) + if ! pick_nginx; then + echo "Nginx binary not found." + exit 1 + fi echo "Reloading nginx..." - "$NGINX" -s reload - ;; + "$NGINX" -s reload + ;; restart) - "$0" stop - sleep 1 - "$0" start - ;; + "$0" stop + sleep 1 + "$0" start + ;; *) - echo "Usage: $0 {start|stop|restart|reload|force-reload}" - exit 1 + echo "Usage: $0 {start|stop|restart|reload|force-reload}" + exit 1 esac diff --git a/helper.sh b/helper.sh index ad4f8d5..d334357 100755 --- a/helper.sh +++ b/helper.sh @@ -7,6 +7,7 @@ HELPER_SCRIPT_FOLDER="$(dirname "$(readlink -f "$0")")" for script in "${HELPER_SCRIPT_FOLDER}/scripts/"*.sh; do . "${script}"; done for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/"*.sh; do . "${script}"; done for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/K1/"*.sh; do . "${script}"; done +for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/K2PLUS/"*.sh; do . "${script}"; done for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/3V3/"*.sh; do . "${script}"; done for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/3KE/"*.sh; do . "${script}"; done for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/10SE/"*.sh; do . "${script}"; done diff --git a/scripts/cartographer_k2plus.sh b/scripts/cartographer_k2plus.sh new file mode 100755 index 0000000..422627a --- /dev/null +++ b/scripts/cartographer_k2plus.sh @@ -0,0 +1,315 @@ +#!/bin/sh + +set -e + +function _cartographer_require_k2plus() { + if [ "$model" = "K2PLUS" ]; then + return 0 + fi + if [ -x /usr/bin/get_sn_mac.sh ]; then + local model_raw + model_raw=$(/usr/bin/get_sn_mac.sh model 2>/dev/null || true) + case "$model_raw" in + *F008*|*K2PLUS*|*k2plus*) + return 0 + ;; + esac + fi + echo "Error: Cartographer flashing is supported only on K2PLUS (F008)." + return 1 +} + +function _cartographer_python_bin() { + local python_bin="/mnt/UDISK/root/klippy-env/bin/python" + if [ ! -x "$python_bin" ]; then + python_bin="$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)" + fi + if [ -z "$python_bin" ]; then + echo "Error: Python not found." + return 1 + fi + echo "$python_bin" +} + +function _cartographer_check_libusb() { + if [ ! -f /usr/lib/libusb-1.0.so.0 ]; then + echo "Error: libusb not found at /usr/lib/libusb-1.0.so.0." + return 1 + fi +} + +function _cartographer_install_pyusb() { + local python_bin="$1" + "$python_bin" - <<'PY' +import importlib.util +import subprocess +import sys + +if importlib.util.find_spec("usb") is None: + subprocess.check_call([sys.executable, "-m", "pip", "install", "pyusb"]) +else: + print("pyusb already installed") +PY +} + +function _cartographer_fetch_tools() { + local python_bin="$1" + "$python_bin" - <<'PY' +import os +import urllib.request + +def fetch(url, path): + if os.path.exists(path) and os.path.getsize(path) > 0: + print(f"Using existing {path}") + return + print(f"Downloading {url}") + urllib.request.urlretrieve(url, path) + +fetch( + "https://raw.githubusercontent.com/Klipper3d/klipper/master/lib/canboot/flash_can.py", + "/tmp/flash_can.py", +) +fetch( + "https://raw.githubusercontent.com/Cartographer3D/cartographer-klipper/master/firmware/v2-v3/survey/5.1.0/Survey_Cartographer_K1_USB_8kib_offset.bin", + "/tmp/Survey_Cartographer_K1_USB_8kib_offset.bin", +) +PY +} + +function install_cartographer_k2plus_prereqs() { + _cartographer_require_k2plus + local python_bin + python_bin="$(_cartographer_python_bin)" + _cartographer_check_libusb + + echo "Installing Python prerequisites..." + _cartographer_install_pyusb "$python_bin" + + echo "Downloading flash tools and firmware..." + _cartographer_fetch_tools "$python_bin" + + echo "Cartographer flash prerequisites installed." +} + +function flash_cartographer_k2plus() { + _cartographer_require_k2plus + + if ! command -v lsusb >/dev/null 2>&1; then + echo "Error: lsusb not available." + return 1 + fi + + if ! lsusb | grep -q "1d50:614e"; then + echo "Error: Cartographer not detected (VID:PID 1d50:614e)." + return 1 + fi + + local python_bin + python_bin="$(_cartographer_python_bin)" + _cartographer_check_libusb + + echo "Checking prerequisites..." + _cartographer_install_pyusb "$python_bin" + _cartographer_fetch_tools "$python_bin" + + echo "Entering bootloader..." + "$python_bin" - <<'PY' +import struct +import usb.core +import usb.util +import usb.backend.libusb1 + +backend = usb.backend.libusb1.get_backend( + find_library=lambda x: "/usr/lib/libusb-1.0.so.0" +) +if backend is None: + raise SystemExit("No libusb backend found") + +dev = usb.core.find(idVendor=0x1D50, idProduct=0x614E, backend=backend) +if dev is None: + raise SystemExit("Cartographer device not found") + +try: + dev.set_configuration() +except usb.core.USBError: + pass + +cfg = dev.get_active_configuration() +intf = cfg[(0, 0)] +iface = intf.bInterfaceNumber +try: + if dev.is_kernel_driver_active(iface): + dev.detach_kernel_driver(iface) +except Exception: + pass +usb.util.claim_interface(dev, iface) +line_coding = struct.pack(" /tmp/katapult_bridge.py <<'PY' +#!/usr/bin/env python3 +import os +import pty +import time +import threading +import usb.core +import usb.util +import usb.backend.libusb1 +import fcntl + +VID = 0x1D50 +PID = 0x6177 +LINK_PATH = "/tmp/katapult_tty" +LIBUSB_PATH = "/usr/lib/libusb-1.0.so.0" + +backend = usb.backend.libusb1.get_backend(find_library=lambda x: LIBUSB_PATH) +if backend is None: + raise SystemExit("No libusb backend found") + +dev = usb.core.find(idVendor=VID, idProduct=PID, backend=backend) +if dev is None: + raise SystemExit("Katapult device not found") + +try: + dev.set_configuration() +except usb.core.USBError: + pass + +cfg = dev.get_active_configuration() +intf = None +ep_in = None +ep_out = None +for i in cfg: + for ep in i: + if usb.util.endpoint_type(ep.bmAttributes) != usb.util.ENDPOINT_TYPE_BULK: + continue + if usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN: + ep_in = ep + else: + ep_out = ep + if ep_in is not None and ep_out is not None: + intf = i + break + +if intf is None or ep_in is None or ep_out is None: + raise SystemExit("Could not find bulk IN/OUT endpoints") + +iface = intf.bInterfaceNumber +try: + if dev.is_kernel_driver_active(iface): + dev.detach_kernel_driver(iface) +except Exception: + pass +usb.util.claim_interface(dev, iface) + +master, slave = pty.openpty() +slave_name = os.ttyname(slave) +if os.path.exists(LINK_PATH) or os.path.islink(LINK_PATH): + os.unlink(LINK_PATH) +os.symlink(slave_name, LINK_PATH) + +flags = fcntl.fcntl(master, fcntl.F_GETFL) +fcntl.fcntl(master, fcntl.F_SETFL, flags | os.O_NONBLOCK) + +print("BRIDGE_READY", LINK_PATH, "->", slave_name, flush=True) + +running = True + +def usb_to_pty(): + while running: + try: + data = dev.read(ep_in.bEndpointAddress, ep_in.wMaxPacketSize, timeout=100) + if data: + os.write(master, data.tobytes()) + except usb.core.USBError as e: + if getattr(e, "errno", None) == 110: + continue + break + except OSError: + break + +def pty_to_usb(): + while running: + try: + data = os.read(master, 4096) + if data: + dev.write(ep_out.bEndpointAddress, data, timeout=100) + except BlockingIOError: + time.sleep(0.01) + except usb.core.USBError as e: + if getattr(e, "errno", None) == 110: + continue + break + except OSError: + break + +t1 = threading.Thread(target=usb_to_pty, daemon=True) +t2 = threading.Thread(target=pty_to_usb, daemon=True) +t1.start() +t2.start() + +try: + while True: + time.sleep(1) +except KeyboardInterrupt: + running = False +PY + + "$python_bin" /tmp/katapult_bridge.py >/tmp/katapult_bridge.log 2>&1 & + local bridge_pid=$! + + i=1 + while [ $i -le 10 ]; do + if [ -e /tmp/katapult_tty ]; then + break + fi + sleep 1 + i=$((i + 1)) + done + if [ ! -e /tmp/katapult_tty ]; then + echo "Error: Katapult bridge did not start." + cat /tmp/katapult_bridge.log || true + kill "$bridge_pid" || true + return 1 + fi + + echo "Flashing firmware..." + "$python_bin" /tmp/flash_can.py -d /tmp/katapult_tty -f /tmp/Survey_Cartographer_K1_USB_8kib_offset.bin + + kill "$bridge_pid" || true + rm -f /tmp/katapult_bridge.py /tmp/katapult_bridge.log /tmp/katapult_tty + + echo "Cartographer firmware flash completed." +} + +function install_cartographer_k2plus() { + _cartographer_require_k2plus + export PATH="/opt/bin:/opt/sbin:$PATH" + + local install_script="${HELPER_SCRIPT_FOLDER}/files/cartographer/install.sh" + + if [ ! -f "$install_script" ]; then + echo "Cartographer install script not found: $install_script" + return 1 + fi + + sh "$install_script" +} diff --git a/scripts/menu/K2PLUS/customize_menu_K2PLUS.sh b/scripts/menu/K2PLUS/customize_menu_K2PLUS.sh new file mode 100755 index 0000000..4048053 --- /dev/null +++ b/scripts/menu/K2PLUS/customize_menu_K2PLUS.sh @@ -0,0 +1,110 @@ +#!/bin/sh + +set -e + +function customize_menu_ui_k2plus() { + top_line + title '[ CUSTOMIZE MENU ]' "${yellow}" + inner_line + hr + menu_option '1' 'Install' 'Custom Boot Display' + menu_option '2' 'Remove' 'Custom Boot Display' + hr + menu_option '3' 'Remove' 'Creality Web Interface' + menu_option '4' 'Restore' 'Creality Web Interface' + hr + menu_option '5' 'Install' 'Guppy Screen' + menu_option '6' 'Remove' 'Guppy Screen' + hr + menu_option '7' 'Install' 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function customize_menu_k2plus() { + clear + customize_menu_ui_k2plus + local customize_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" customize_menu_opt + case "${customize_menu_opt}" in + 1) + if [ -f "$BOOT_DISPLAY_FILE" ]; then + error_msg "Custom Boot Display is already installed!" + elif [ ! -d "$BOOT_DISPLAY_FOLDER" ]; then + error_msg "Please use latest firmware to install Custom Boot Display!" + else + run "install_custom_boot_display" "customize_menu_ui_k2plus" + fi;; + 2) + if [ ! -f "$BOOT_DISPLAY_FILE" ]; then + error_msg "Custom Boot Display is not installed!" + elif [ ! -d "$BOOT_DISPLAY_FOLDER" ]; then + error_msg "Please use latest firmware to restore Stock Boot Display!" + else + run "remove_custom_boot_display" "customize_menu_ui_k2plus" + fi;; + 3) + if [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to change the default Web Interface.${white}" + echo + else + run "remove_creality_web_interface" "customize_menu_ui_k2plus" + fi;; + 4) + if [ -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already present!" + elif [ ! -f "$INITD_FOLDER"/S99start_app ]; then + error_msg "Guppy Screen need to be removed first to restore Creality Web Interface!" + else + run "restore_creality_web_interface" "customize_menu_ui_k2plus" + fi;; + 5) + if [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ "$(curl -s localhost:7125/server/info | jq .result.klippy_connected)" != "true" ]; then + error_msg "Moonraker and Klipper do not seem to be functional. Please check this!" + elif [ ! -f /lib/ld-2.29.so ]; then + error_msg "Make sure you're running latest firmware version!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Please remove Improved Shapers Calibrations first, Guppy Screen already use it!" + else + run "install_guppy_screen" "customize_menu_ui_k2plus" + fi;; + 6) + if [ ! -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is not installed!" + else + run "remove_guppy_screen" "customize_menu_ui_k2plus" + fi;; + 7) + if [ -f "$FLUIDD_LOGO_FILE" ]; then + error_msg "Creality Dynamic Logos for Fluidd are already installed!" + elif [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is needed, please install it first!" + else + run "install_creality_dynamic_logos" "customize_menu_ui_k2plus" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + customize_menu_k2plus +} diff --git a/scripts/menu/K2PLUS/info_menu_K2PLUS.sh b/scripts/menu/K2PLUS/info_menu_K2PLUS.sh new file mode 100755 index 0000000..e287395 --- /dev/null +++ b/scripts/menu/K2PLUS/info_menu_K2PLUS.sh @@ -0,0 +1,103 @@ +#!/bin/sh + +set -e + +function check_folder_k2plus() { + local folder_path="$1" + if [ -d "$folder_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function check_file_k2plus() { + local file_path="$1" + if [ -f "$file_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function check_simplyprint_k2plus() { + if [ ! -f "$MOONRAKER_CFG" ]; then + echo -e "${red}✗" + elif grep -q "\[simplyprint\]" "$MOONRAKER_CFG"; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function info_menu_ui_k2plus() { + top_line + title '[ INFORMATION MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + info_line "$(check_folder_k2plus "$MOONRAKER_FOLDER")" 'Moonraker & Nginx' + info_line "$(check_folder_k2plus "$FLUIDD_FOLDER")" 'Fluidd' + info_line "$(check_folder_k2plus "$MAINSAIL_FOLDER")" 'Mainsail' + hr + subtitle '•UTILITIES:' + info_line "$(check_file_k2plus "$ENTWARE_FILE")" 'Entware' + info_line "$(check_file_k2plus "$KLIPPER_SHELL_FILE")" 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + info_line "$(check_folder_k2plus "$KAMP_FOLDER")" 'Klipper Adaptive Meshing & Purging' + info_line "$(check_file_k2plus "$BUZZER_FILE")" 'Buzzer Support' + info_line "$(check_folder_k2plus "$NOZZLE_CLEANING_FOLDER")" 'Nozzle Cleaning Fan Control' + info_line "$(check_file_k2plus "$FAN_CONTROLS_FILE")" 'Fans Control Macros' + info_line "$(check_folder_k2plus "$IMP_SHAPERS_FOLDER")" 'Improved Shapers Calibrations' + info_line "$(check_file_k2plus "$USEFUL_MACROS_FILE")" 'Useful Macros' + info_line "$(check_file_k2plus "$SAVE_ZOFFSET_FILE")" 'Save Z-Offset Macros' + info_line "$(check_file_k2plus "$SCREWS_ADJUST_FILE")" 'Screws Tilt Adjust Support' + info_line "$(check_file_k2plus "$M600_SUPPORT_FILE")" 'M600 Support' + info_line "$(check_file_k2plus "$GIT_BACKUP_FILE")" 'Git Backup' + hr + subtitle '•CAMERA:' + info_line "$(check_file_k2plus "$TIMELAPSE_FILE")" 'Moonraker Timelapse' + info_line "$(check_file_k2plus "$CAMERA_SETTINGS_FILE")" 'Camera Settings Control' + info_line "$(check_file_k2plus "$USB_CAMERA_FILE")" 'USB Camera Support' + hr + subtitle '•REMOTE ACCESS:' + info_line "$(check_folder_k2plus "$OCTOEVERYWHERE_FOLDER")" 'OctoEverywhere' + info_line "$(check_folder_k2plus "$MOONRAKER_OBICO_FOLDER")" 'Obico' + info_line "$(check_folder_k2plus "$GUPPYFLO_FOLDER")" 'GuppyFLO' + info_line "$(check_folder_k2plus "$MOBILERAKER_COMPANION_FOLDER")" 'Mobileraker Companion' + info_line "$(check_folder_k2plus "$OCTOAPP_COMPANION_FOLDER")" 'OctoApp Companion' + info_line "$(check_simplyprint_k2plus)" 'SimplyPrint' + hr + subtitle '•CUSTOMIZATION:' + info_line "$(check_file_k2plus "$BOOT_DISPLAY_FILE")" 'Custom Boot Display' + info_line "$(check_file_k2plus "$CREALITY_WEB_FILE")" 'Creality Web Interface' + info_line "$(check_folder_k2plus "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' + info_line "$(check_file_k2plus "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function info_menu_k2plus() { + clear + info_menu_ui_k2plus + local info_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" info_menu_opt + case "${info_menu_opt}" in + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + info_menu_k2plus +} diff --git a/scripts/menu/K2PLUS/install_menu_K2PLUS.sh b/scripts/menu/K2PLUS/install_menu_K2PLUS.sh new file mode 100755 index 0000000..29caabb --- /dev/null +++ b/scripts/menu/K2PLUS/install_menu_K2PLUS.sh @@ -0,0 +1,261 @@ +#!/bin/sh + +set -e + +function install_menu_ui_k2plus() { + top_line + title '[ INSTALL MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Install' 'Moonraker and Nginx' + menu_option ' 2' 'Install' 'Fluidd (port 4408)' + menu_option ' 3' 'Install' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Install' 'Entware' + menu_option ' 5' 'Install' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Install' 'Klipper Adaptive Meshing & Purging' + menu_option ' 7' 'Install' 'Buzzer Support' + menu_option ' 8' 'Install' 'Nozzle Cleaning Fan Control' + menu_option ' 9' 'Install' 'Fans Control Macros' + menu_option '10' 'Install' 'Improved Shapers Calibrations' + menu_option '11' 'Install' 'Useful Macros' + menu_option '12' 'Install' 'Save Z-Offset Macros' + menu_option '13' 'Install' 'Screws Tilt Adjust Support' + menu_option '14' 'Install' 'M600 Support' + menu_option '15' 'Install' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '16' 'Install' 'Moonraker Timelapse' + menu_option '17' 'Install' 'Camera Settings Control' + menu_option '18' 'Install' 'USB Camera Support' + hr + subtitle '•REMOTE ACCESS:' + menu_option '19' 'Install' 'OctoEverywhere' + menu_option '20' 'Install' 'Moonraker Obico' + menu_option '21' 'Install' 'GuppyFLO' + menu_option '22' 'Install' 'Mobileraker Companion' + menu_option '23' 'Install' 'OctoApp Companion' + menu_option '24' 'Install' 'SimplyPrint' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function install_menu_k2plus() { + clear + install_menu_ui_k2plus + local install_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" install_menu_opt + case "${install_menu_opt}" in + 1) + if [ -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are already installed!" + else + run "install_moonraker_nginx" "install_menu_ui_k2plus" + fi;; + 2) + if [ -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_fluidd" "install_menu_ui_k2plus" + fi;; + 3) + if [ -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_mainsail" "install_menu_ui_k2plus" + fi;; + 4) + if [ -f "$ENTWARE_FILE" ]; then + error_msg "Entware is already installed!" + else + run "install_entware" "install_menu_ui_k2plus" + fi;; + 5) + if [ -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is already installed!" + else + run "install_gcode_shell_command" "install_menu_ui_k2plus" + fi;; + 6) + if [ -d "$KAMP_FOLDER" ]; then + error_msg "Klipper Adaptive Meshing & Purging is already installed!" + else + run "install_kamp" "install_menu_ui_k2plus" + fi;; + 7) + if [ -f "$BUZZER_FILE" ]; then + error_msg "Buzzer Support is already installed!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_buzzer_support" "install_menu_ui_k2plus" + fi;; + 8) + if [ -d "$NOZZLE_CLEANING_FOLDER" ]; then + error_msg "Nozzle Cleaning Fan Control is already installed!" + else + run "install_nozzle_cleaning_fan_control" "install_menu_ui_k2plus" + fi;; + 9) + if [ -f "$FAN_CONTROLS_FILE" ]; then + error_msg "Fans Control Macros are already installed!" + else + run "install_fans_control_macros" "install_menu_ui_k2plus" + fi;; + 10) + if [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are already installed!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen already has these features!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_improved_shapers" "install_menu_ui_k2plus" + fi;; + 11) + if [ -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Useful Macros are already installed!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_useful_macros" "install_menu_ui_k2plus" + fi;; + 12) + if [ -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are already installed!" + else + run "install_save_zoffset_macros" "install_menu_ui_k2plus" + fi;; + 13) + if [ -f "$SCREWS_ADJUST_FILE" ]; then + error_msg "Screws Tilt Adjust Support is already installed!" + else + run "install_screws_tilt_adjust" "install_menu_ui_k2plus" + fi;; + 14) + if [ -f "$M600_SUPPORT_FILE" ]; then + error_msg "M600 Support is already installed!" + else + run "install_m600_support" "install_menu_ui_k2plus" + fi;; + 15) + if [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_git_backup" "install_menu_ui_k2plus" + fi;; + 16) + if [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_timelapse" "install_menu_ui_k2plus" + fi;; + 17) + if [ -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Camera Settings Control is already installed!" + elif v4l2-ctl --list-devices | grep -q 'CCX2F3299' && [ ! -f "$INITD_FOLDER"/S50usb_camera ]; then + error_msg "This is not compatible with the new hardware version of the camera!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_camera_settings_control" "install_menu_ui_k2plus" + fi;; + 18) + if [ -f "$USB_CAMERA_FILE" ]; then + error_msg "Camera USB Support is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_usb_camera" "install_menu_ui_k2plus" + fi;; + 19) + if [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_octoeverywhere" "install_menu_ui_k2plus" + fi;; + 20) + if [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_obico" "install_menu_ui_k2plus" + fi;; + 21) + if [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_guppyflo" "install_menu_ui_k2plus" + fi;; + 22) + if [ -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_mobileraker_companion" "install_menu_ui_k2plus" + fi;; + 23) + if [ -d "$OCTOAPP_COMPANION_FOLDER" ]; then + error_msg "OctoApp Companion is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_octoapp_companion" "install_menu_ui_k2plus" + fi;; + 24) + if grep -q "\[simplyprint\]" "$MOONRAKER_CFG"; then + error_msg "SimplyPrint is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install one of them first!" + else + run "install_simplyprint" "install_menu_ui_k2plus" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + install_menu_k2plus +} diff --git a/scripts/menu/K2PLUS/remove_menu_K2PLUS.sh b/scripts/menu/K2PLUS/remove_menu_K2PLUS.sh new file mode 100755 index 0000000..2f0b2c8 --- /dev/null +++ b/scripts/menu/K2PLUS/remove_menu_K2PLUS.sh @@ -0,0 +1,245 @@ +#!/bin/sh + +set -e + +function remove_menu_ui_k2plus() { + top_line + title '[ REMOVE MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Remove' 'Moonraker and Nginx' + menu_option ' 2' 'Remove' 'Fluidd (port 4408)' + menu_option ' 3' 'Remove' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Remove' 'Entware' + menu_option ' 5' 'Remove' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Remove' 'Klipper Adaptive Meshing & Purging' + menu_option ' 7' 'Remove' 'Buzzer Support' + menu_option ' 8' 'Remove' 'Nozzle Cleaning Fan Control' + menu_option ' 9' 'Remove' 'Fans Control Macros' + menu_option '10' 'Remove' 'Improved Shapers Calibrations' + menu_option '11' 'Remove' 'Useful Macros' + menu_option '12' 'Remove' 'Save Z-Offset Macros' + menu_option '13' 'Remove' 'Screws Tilt Adjust Support' + menu_option '14' 'Remove' 'M600 Support' + menu_option '15' 'Remove' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '16' 'Remove' 'Moonraker Timelapse' + menu_option '17' 'Remove' 'Camera Settings Control' + menu_option '18' 'Remove' 'USB Camera Support' + hr + subtitle '•REMOTE ACCESS:' + menu_option '19' 'Remove' 'OctoEverywhere' + menu_option '20' 'Remove' 'Moonraker Obico' + menu_option '21' 'Remove' 'GuppyFLO' + menu_option '22' 'Remove' 'Mobileraker Companion' + menu_option '23' 'Remove' 'OctoApp Companion' + menu_option '24' 'Remove' 'SimplyPrint' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function remove_menu_k2plus() { + clear + remove_menu_ui_k2plus + local remove_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" remove_menu_opt + case "${remove_menu_opt}" in + 1) + if [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are not installed!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Moonraker is needed to use Guppy Screen, please uninstall it first!" + else + run "remove_moonraker_nginx" "remove_menu_ui_k2plus" + fi;; + 2) + if [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Fluidd.${white}" + echo + else + run "remove_fluidd" "remove_menu_ui_k2plus" + fi;; + 3) + if [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ] && [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Mainsail.${white}" + echo + else + run "remove_mainsail" "remove_menu_ui_k2plus" + fi;; + 4) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + elif [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Entware is needed to use Moonraker Timelapse, please uninstall it first!" + elif [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Entware is needed to use Git Backup, please uninstall it first!" + elif [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "Entware is needed to use OctoEverywhere, please uninstall it first!" + elif [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Entware is needed to use Moonraker Obico, please uninstall it first!" + elif [ -f "$USB_CAMERA_FILE" ]; then + error_msg "Entware is needed to use USB Camera Support, please uninstall it first!" + else + run "remove_entware" "remove_menu_ui_k2plus" + fi;; + 5) + if [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is not installed!" + elif [ -f "$BUZZER_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Buzzer Support, please uninstall it first!" + elif [ -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Camera Settings Control, please uninstall it first!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Guppy Screen, please uninstall it first!" + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Improved Shapers Calibrations, please uninstall it first!" + elif [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Git Backup, please uninstall it first!" + elif [ -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Useful Macros, please uninstall it first!" + else + run "remove_gcode_shell_command" "remove_menu_ui_k2plus" + fi;; + 6) + if [ ! -d "$KAMP_FOLDER" ]; then + error_msg "Klipper Adaptive Meshing & Purging is not installed!" + else + run "remove_kamp" "remove_menu_ui_k2plus" + fi;; + 7) + if [ ! -f "$BUZZER_FILE" ]; then + error_msg "Buzzer Support is not installed!" + else + run "remove_buzzer_support" "remove_menu_ui_k2plus" + fi;; + 8) + if [ ! -d "$NOZZLE_CLEANING_FOLDER" ]; then + error_msg "Nozzle Cleaning Fan Control is not installed!" + else + run "remove_nozzle_cleaning_fan_control" "remove_menu_ui_k2plus" + fi;; + 9) + if [ ! -f "$FAN_CONTROLS_FILE" ]; then + error_msg "Fans Control Macros are not installed!" + else + run "remove_fans_control_macros" "remove_menu_ui_k2plus" + fi;; + 10) + if [ ! -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are not installed!" + else + run "remove_improved_shapers" "remove_menu_ui_k2plus" + fi;; + 11) + if [ ! -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Useful Macros are not installed!" + else + run "remove_useful_macros" "remove_menu_ui_k2plus" + fi;; + 12) + if [ ! -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are not installed!" + else + run "remove_save_zoffset_macros" "remove_menu_ui_k2plus" + fi;; + 13) + if [ ! -f "$SCREWS_ADJUST_FILE" ]; then + error_msg "Screws Tilt Adjust Support is not installed!" + else + run "remove_screws_tilt_adjust" "remove_menu_ui_k2plus" + fi;; + 14) + if [ ! -f "$M600_SUPPORT_FILE" ]; then + error_msg "M600 Support is not installed!" + else + run "remove_m600_support" "remove_menu_ui_k2plus" + fi;; + 15) + if [ ! -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is not installed!" + else + run "remove_git_backup" "remove_menu_ui_k2plus" + fi;; + 16) + if [ ! -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is not installed!" + else + run "remove_moonraker_timelapse" "remove_menu_ui_k2plus" + fi;; + 17) + if [ ! -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Camera Settings Control is not installed!" + else + run "remove_camera_settings_control" "remove_menu_ui_k2plus" + fi;; + 18) + if [ ! -f "$USB_CAMERA_FILE" ]; then + error_msg "USB Camera Support is not installed!" + else + run "remove_usb_camera" "remove_menu_ui_k2plus" + fi;; + 19) + if [ ! -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "OctoEverywhere is not installed!" + else + run "remove_octoeverywhere" "remove_menu_ui_k2plus" + fi;; + 20) + if [ ! -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Moonraker Obico is not installed!" + else + run "remove_moonraker_obico" "remove_menu_ui_k2plus" + fi;; + 21) + if [ ! -d "$GUPPYFLO_FOLDER" ]; then + error_msg "GuppyFLO is not installed!" + else + run "remove_guppyflo" "remove_menu_ui_k2plus" + fi;; + 22) + if [ ! -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is not installed!" + else + run "remove_mobileraker_companion" "remove_menu_ui_k2plus" + fi;; + 23) + if [ ! -d "$OCTOAPP_COMPANION_FOLDER" ]; then + error_msg "OctoApp Companion is not installed!" + else + run "remove_octoapp_companion" "remove_menu_ui_k2plus" + fi;; + 24) + if ! grep -q "\[simplyprint\]" "$MOONRAKER_CFG"; then + error_msg "SimplyPrint is not installed!" + else + run "remove_simplyprint" "remove_menu_ui_k2plus" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + remove_menu_k2plus +} diff --git a/scripts/menu/K2PLUS/tools_menu_K2PLUS.sh b/scripts/menu/K2PLUS/tools_menu_K2PLUS.sh new file mode 100755 index 0000000..3b1e243 --- /dev/null +++ b/scripts/menu/K2PLUS/tools_menu_K2PLUS.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +set -e + +function tools_menu_ui_k2plus() { + top_line + title '[ TOOLS MENU ]' "${yellow}" + inner_line + hr + menu_option ' 1' 'Prevent updating' 'Klipper configuration files' + menu_option ' 2' 'Allow updating' 'Klipper configuration files' + menu_option ' 3' 'Fix' 'printing Gcode files from folder' + hr + menu_option ' 4' 'Enable' 'camera settings in Moonraker' + menu_option ' 5' 'Disable' 'camera settings in Moonraker' + hr + menu_option ' 6' 'Restart' 'Nginx service' + menu_option ' 7' 'Restart' 'Moonraker service' + menu_option ' 8' 'Restart' 'Klipper service' + hr + menu_option ' 9' 'Update' 'Entware packages' + hr + menu_option '10' 'Clear' 'cache' + menu_option '11' 'Clear' 'logs files' + hr + menu_option '12' 'Restore' 'a previous firmware' + hr + menu_option '13' 'Reset' 'factory settings' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function tools_menu_k2plus() { + clear + tools_menu_ui_k2plus + local tools_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" tools_menu_opt + case "${tools_menu_opt}" in + 1) + if [ -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already prevented!" + else + run "prevent_updating_klipper_files" "tools_menu_ui_k2plus" + fi;; + 2) + if [ ! -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already allowed!" + else + run "allow_updating_klipper_files" "tools_menu_ui_k2plus" + fi;; + 3) + if [ -f "$KLIPPER_KLIPPY_FOLDER"/gcode.py ]; then + run "printing_gcode_from_folder" "tools_menu_ui_k2plus" + fi;; + 4) + if grep -q "^\[webcam Camera\]$" "$MOONRAKER_CFG"; then + error_msg "Camera settings are alredy enabled in Moonraker!" + else + run "enable_camera_settings" "tools_menu_ui_k2plus" + fi;; + 5) + if grep -q "^#\[webcam Camera\]" "$MOONRAKER_CFG"; then + error_msg "Camera settings are alredy disabled in Moonraker!" + else + run "disable_camera_settings" "tools_menu_ui_k2plus" + fi;; + 6) + if [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Nginx is not installed!" + else + run "restart_nginx_action" "tools_menu_ui_k2plus" + fi;; + 7) + if [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker is not installed!" + else + run "restart_moonraker_action" "tools_menu_ui_k2plus" + fi;; + 8) + if [ ! -f "$INITD_FOLDER"/S55klipper_service ]; then + error_msg "Klipper service is not present!" + else + run "restart_klipper_action" "tools_menu_ui_k2plus" + fi;; + 9) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + else + run "update_entware_packages" "tools_menu_ui_k2plus" + fi;; + 10) + run "clear_cache" "tools_menu_ui_k2plus";; + 11) + run "clear_logs" "tools_menu_ui_k2plus";; + 12) + run "restore_previous_firmware" "tools_menu_ui_k2plus";; + 13) + run "reset_factory_settings" "tools_menu_ui_k2plus";; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + tools_menu_k2plus +} diff --git a/scripts/menu/main_menu.sh b/scripts/menu/main_menu.sh index f240e3e..1e28ae5 100755 --- a/scripts/menu/main_menu.sh +++ b/scripts/menu/main_menu.sh @@ -10,6 +10,10 @@ fi get_model=$( /usr/bin/get_sn_mac.sh model 2>&1 ) if echo "$get_model" | grep -iq "K1"; then model="K1" +elif echo "$get_model" | grep -iq "K2"; then + model="K2PLUS" +elif echo "$get_model" | grep -iq "F008"; then + model="K2PLUS" elif echo "$get_model" | grep -iq "F001"; then model="3V3" elif echo "$get_model" | grep -iq "F002"; then @@ -41,6 +45,8 @@ function script_title() { local title if [ "$model" = "K1" ]; then title="K1 SERIES" + elif [ "$model" = "K2PLUS" ]; then + title="K2PLUS" elif [ "$model" = "3V3" ]; then title="ENDER-3 V3 SERIES" elif [ "$model" = "3KE" ]; then @@ -89,6 +95,8 @@ function main_menu() { 1) clear if [ "$model" = "K1" ]; then install_menu_k1 + elif [ "$model" = "K2PLUS" ]; then + install_menu_k2plus elif [ "$model" = "3V3" ]; then install_menu_3v3 elif [ "$model" = "3KE" ]; then @@ -102,6 +110,8 @@ function main_menu() { 2) clear if [ "$model" = "K1" ]; then remove_menu_k1 + elif [ "$model" = "K2PLUS" ]; then + remove_menu_k2plus elif [ "$model" = "3V3" ]; then remove_menu_3v3 elif [ "$model" = "3KE" ]; then @@ -115,6 +125,8 @@ function main_menu() { 3) clear if [ "$model" = "K1" ]; then customize_menu_k1 + elif [ "$model" = "K2PLUS" ]; then + customize_menu_k2plus elif [ "$model" = "3V3" ]; then customize_menu_3v3 elif [ "$model" = "3KE" ]; then @@ -131,6 +143,8 @@ function main_menu() { 5) clear if [ "$model" = "K1" ]; then tools_menu_k1 + elif [ "$model" = "K2PLUS" ]; then + tools_menu_k2plus elif [ "$model" = "3V3" ]; then tools_menu_3v3 elif [ "$model" = "3KE" ]; then @@ -144,6 +158,8 @@ function main_menu() { 6) clear if [ "$model" = "K1" ]; then info_menu_k1 + elif [ "$model" = "K2PLUS" ]; then + info_menu_k2plus elif [ "$model" = "3V3" ]; then info_menu_3v3 elif [ "$model" = "3KE" ]; then diff --git a/scripts/moonraker_nginx.sh b/scripts/moonraker_nginx.sh index 9dfbecd..9aa88fd 100755 --- a/scripts/moonraker_nginx.sh +++ b/scripts/moonraker_nginx.sh @@ -42,10 +42,8 @@ function install_moonraker_nginx(){ echo -e "Info: Extracting Nginx files..." tar -xvf "$NGINX_URL" -C "$USR_DATA" echo -e "Info: Copying services files..." - if [ ! -f "$INITD_FOLDER"/S50nginx ]; then - cp "$NGINX_SERVICE_URL" "$INITD_FOLDER"/S50nginx - chmod +x "$INITD_FOLDER"/S50nginx - fi + cp "$NGINX_SERVICE_URL" "$INITD_FOLDER"/S50nginx + chmod +x "$INITD_FOLDER"/S50nginx if [ ! -f "$INITD_FOLDER"/S56moonraker_service ]; then cp "$MOONRAKER_SERVICE_URL" "$INITD_FOLDER"/S56moonraker_service chmod +x "$INITD_FOLDER"/S56moonraker_service @@ -61,7 +59,11 @@ function install_moonraker_nginx(){ cp "$MOONRAKER_URL3" "$PRINTER_DATA_FOLDER"/moonraker.asvc echo -e "Info: Applying changes from official repo..." cd "$MOONRAKER_FOLDER"/moonraker - git stash; git checkout master; git pull + git config --global --add safe.directory "$MOONRAKER_FOLDER"/moonraker + git fetch origin master + git checkout master + git reset --hard FETCH_HEAD + git clean -fd echo -e "Info: Installing Supervisor Lite..." chmod 755 "$SUPERVISOR_URL" ln -sf "$SUPERVISOR_URL" "$SUPERVISOR_FILE" @@ -161,7 +163,11 @@ function install_moonraker_3v3(){ cp "$MOONRAKER_URL3" "$PRINTER_DATA_FOLDER"/moonraker.asvc echo -e "Info: Applying changes from official repo..." cd "$MOONRAKER_FOLDER"/moonraker - git stash; git checkout master; git pull + git config --global --add safe.directory "$MOONRAKER_FOLDER"/moonraker + git fetch origin master + git checkout master + git reset --hard FETCH_HEAD + git clean -fd echo -e "Info: Installing Supervisor Lite..." chmod 755 "$SUPERVISOR_URL" ln -sf "$SUPERVISOR_URL" "$SUPERVISOR_FILE" diff --git a/scripts/paths.sh b/scripts/paths.sh index ddb9468..88e7a7b 100755 --- a/scripts/paths.sh +++ b/scripts/paths.sh @@ -14,11 +14,24 @@ function set_paths() { red=`echo -en "\033[01;31m"` # System # - CURL="${HELPER_SCRIPT_FOLDER}/files/fixes/curl" + if command -v curl >/dev/null 2>&1; then + CURL="$(command -v curl)" + elif [ -x "${HELPER_SCRIPT_FOLDER}/files/fixes/curl" ]; then + CURL="${HELPER_SCRIPT_FOLDER}/files/fixes/curl" + else + CURL="curl" + fi INITD_FOLDER="/etc/init.d" USR_DATA="/usr/data" USR_SHARE="/usr/share" - PRINTER_DATA_FOLDER="$USR_DATA/printer_data" + if [ -d "/mnt/UDISK/printer_data" ]; then + PRINTER_DATA_FOLDER="/mnt/UDISK/printer_data" + else + PRINTER_DATA_FOLDER="$USR_DATA/printer_data" + fi + if [ "$PRINTER_DATA_FOLDER" = "/mnt/UDISK/printer_data" ] && [ ! -e "$USR_DATA/printer_data" ]; then + ln -sf "$PRINTER_DATA_FOLDER" "$USR_DATA/printer_data" + fi # Helper Script # HS_FILES="${HELPER_SCRIPT_FOLDER}/files" @@ -105,6 +118,16 @@ function set_paths() { USEFUL_MACROS_FILE="${HS_CONFIG_FOLDER}/useful-macros.cfg" USEFUL_MACROS_URL="${HS_FILES}/macros/useful-macros.cfg" USEFUL_MACROS_3V3_URL="${HS_FILES}/macros/useful-macros-3v3.cfg" + + # K2PLUS Macros # + M191_MACRO_FILE="${HS_CONFIG_FOLDER}/m191.cfg" + M191_MACRO_URL="${HS_FILES}/macros/m191.cfg" + START_PRINT_MACRO_FILE="${HS_CONFIG_FOLDER}/start_print.cfg" + START_PRINT_MACRO_URL="${HS_FILES}/macros/start_print.cfg" + BED_MESH_MACRO_FILE="${HS_CONFIG_FOLDER}/bed_mesh.cfg" + BED_MESH_MACRO_URL="${HS_FILES}/macros/bed_mesh.cfg" + OVERRIDES_MACRO_FILE="${HS_CONFIG_FOLDER}/overrides.cfg" + OVERRIDES_MACRO_URL="${HS_FILES}/macros/overrides.cfg" # Save Z-Offset Macros # SAVE_ZOFFSET_FILE="${HS_CONFIG_FOLDER}/save-zoffset.cfg" @@ -115,6 +138,7 @@ function set_paths() { SCREWS_ADJUST_URL="${HS_FILES}/screws-tilt-adjust/screws_tilt_adjust.py" SCREWS_ADJUST_K1_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-k1.cfg" SCREWS_ADJUST_K1M_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-k1max.cfg" + SCREWS_ADJUST_K2PLUS_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-k2plus.cfg" SCREWS_ADJUST_3KE_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-3ke.cfg" SCREWS_ADJUST_E5M_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-e5m.cfg" @@ -191,6 +215,8 @@ function set_paths() { function set_permissions() { - chmod +x "$CURL" >/dev/null 2>&1 & + if [ "$CURL" = "${HELPER_SCRIPT_FOLDER}/files/fixes/curl" ]; then + chmod +x "$CURL" >/dev/null 2>&1 & + fi } \ No newline at end of file diff --git a/scripts/screws_tilt_adjust.sh b/scripts/screws_tilt_adjust.sh index 1cb2b75..c155eaf 100755 --- a/scripts/screws_tilt_adjust.sh +++ b/scripts/screws_tilt_adjust.sh @@ -55,6 +55,9 @@ function install_screws_tilt_adjust(){ error_msg "Please select a correct choice!";; esac done + elif [ "$model" = "K2PLUS" ]; then + echo -e "Info: Linking files..." + ln -sf "$SCREWS_ADJUST_K2PLUS_URL" "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg elif [ "$model" = "E5M" ]; then echo -e "Info: Linking files..." ln -sf "$SCREWS_ADJUST_E5M_URL" "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg diff --git a/scripts/useful_macros.sh b/scripts/useful_macros.sh index 4665eba..5bd4fb9 100755 --- a/scripts/useful_macros.sh +++ b/scripts/useful_macros.sh @@ -40,6 +40,42 @@ function install_useful_macros(){ echo -e "Info: Adding Useful Macros configurations in printer.cfg file..." sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/useful-macros\.cfg\]' "$PRINTER_CFG" fi + if [ "$model" = "K2PLUS" ]; then + echo -e "Info: Linking K2PLUS macros..." + ln -sf "$M191_MACRO_URL" "$HS_CONFIG_FOLDER"/m191.cfg + ln -sf "$START_PRINT_MACRO_URL" "$HS_CONFIG_FOLDER"/start_print.cfg + ln -sf "$BED_MESH_MACRO_URL" "$HS_CONFIG_FOLDER"/bed_mesh.cfg + if [ -f "$HS_CONFIG_FOLDER"/overrides.cfg ]; then + echo -e "Info: overrides.cfg already exists, keeping it..." + else + echo -e "Info: Copying overrides.cfg..." + cp "$OVERRIDES_MACRO_URL" "$HS_CONFIG_FOLDER"/overrides.cfg + fi + if grep -q "include Helper-Script/m191" "$PRINTER_CFG" ; then + echo -e "Info: M191 configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding M191 configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/m191\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/start_print" "$PRINTER_CFG" ; then + echo -e "Info: Start Print configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Start Print configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/start_print\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/bed_mesh" "$PRINTER_CFG" ; then + echo -e "Info: Bed Mesh configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Bed Mesh configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/bed_mesh\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/overrides" "$PRINTER_CFG" ; then + echo -e "Info: Overrides configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Overrides configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/overrides\.cfg\]' "$PRINTER_CFG" + fi + fi echo -e "Info: Restarting Klipper service..." restart_klipper ok_msg "Useful Macros have been installed successfully!" @@ -69,6 +105,26 @@ function remove_useful_macros(){ else echo -e "Info: Useful Macros configurations are already removed in printer.cfg file..." fi + rm -f "$HS_CONFIG_FOLDER"/m191.cfg + rm -f "$HS_CONFIG_FOLDER"/start_print.cfg + rm -f "$HS_CONFIG_FOLDER"/bed_mesh.cfg + rm -f "$HS_CONFIG_FOLDER"/overrides.cfg + if grep -q "include Helper-Script/m191" "$PRINTER_CFG" ; then + echo -e "Info: Removing M191 configurations in printer.cfg file..." + sed -i '/include Helper-Script\/m191\.cfg/d' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/start_print" "$PRINTER_CFG" ; then + echo -e "Info: Removing Start Print configurations in printer.cfg file..." + sed -i '/include Helper-Script\/start_print\.cfg/d' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/bed_mesh" "$PRINTER_CFG" ; then + echo -e "Info: Removing Bed Mesh configurations in printer.cfg file..." + sed -i '/include Helper-Script\/bed_mesh\.cfg/d' "$PRINTER_CFG" + fi + if grep -q "include Helper-Script/overrides" "$PRINTER_CFG" ; then + echo -e "Info: Removing Overrides configurations in printer.cfg file..." + sed -i '/include Helper-Script\/overrides\.cfg/d' "$PRINTER_CFG" + fi if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then rm -rf "$HS_CONFIG_FOLDER" fi