From 8dcb75d216e916b8e3e62a464664809c5218157c Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 23 Jan 2025 19:31:00 +0100 Subject: [PATCH 001/139] Mesh/Beacon: better handling of compensation meshes and reference pos --- configuration/klippy/beacon_mesh.py | 321 +++++++++++++++++++--------- configuration/z-probe/beacon.cfg | 14 +- 2 files changed, 216 insertions(+), 119 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 0134e8b15..7402b29aa 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,6 +1,20 @@ import logging, collections from . import bed_mesh as BedMesh +### +# Mesh constants +### +RATOS_TEMP_SCAN_MESH_NAME = "__BEACON_TEMP_SCAN_MESH__" +RATOS_TEMP_CONTACT_MESH_NAME = "__BEACON_TEMP_CONTACT_MESH__" +RATOS_DEFAULT_COMPENSATION_MESH_NAME = "Beacon Compensation Mesh" +RATOS_MESH_VERSION = 1 +RATOS_MESH_PROFILE_OPTIONS = { + "ratos_mesh_version": int, + "beacon_model_temp": float, + "beacon_model_name": str +} + + ##### # Beacon Mesh ##### @@ -16,10 +30,14 @@ def __init__(self, config): self.name = config.get_name() self.gcode = self.printer.lookup_object('gcode') self.reactor = self.printer.get_reactor() + + # These are loaded on klippy:connect. + self.beacon = None + self.ratos = None + self.bed_mesh = None self.offset_mesh = None self.offset_mesh_points = [[]] - self.pmgr = BedMeshProfileManager(self.config, self) self.register_commands() self.register_handler() @@ -35,140 +53,231 @@ def _connect(self): self.ratos = self.printer.lookup_object('ratos') if self.config.has_section("bed_mesh"): self.bed_mesh = self.printer.lookup_object('bed_mesh') + # Load additional RatOS mesh params + self.load_extra_mesh_params() + # run klippers inompatible profile check which is never called by bed_mesh + self.bed_mesh.pmgr._check_incompatible_profiles() + if self.config.has_section("beacon"): + self.beacon = self.printer.lookup_object('beacon') ##### # Gcode commands ##### def register_commands(self): - self.gcode.register_command('BEACON_APPLY_SCAN_COMPENSATION', self.cmd_BEACON_APPLY_SCAN_COMPENSATION, desc=(self.desc_BEACON_APPLY_SCAN_COMPENSATION)) - self.gcode.register_command('CREATE_BEACON_COMPENSATION_MESH', self.cmd_CREATE_BEACON_COMPENSATION_MESH, desc=(self.desc_CREATE_BEACON_COMPENSATION_MESH)) + if self.config.has_section("beacon"): + self.gcode.register_command('BEACON_APPLY_SCAN_COMPENSATION', + self.cmd_BEACON_APPLY_SCAN_COMPENSATION, + desc=(self.desc_BEACON_APPLY_SCAN_COMPENSATION)) + self.gcode.register_command('CREATE_BEACON_COMPENSATION_MESH', + self.cmd_CREATE_BEACON_COMPENSATION_MESH, + desc=(self.desc_CREATE_BEACON_COMPENSATION_MESH)) + self.gcode.register_command('SET_ZERO_REFERENCE_POSITION', + self.cmd_SET_ZERO_REFERENCE_POSITION, + desc=(self.desc_SET_ZERO_REFERENCE_POSITION)) - desc_BEACON_APPLY_SCAN_COMPENSATION = "Compensates a beacon scan mesh with the beacon compensation mesh." + desc_BEACON_APPLY_SCAN_COMPENSATION = "Compensates a beacon scan mesh with a beacon compensation mesh." def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): - profile = gcmd.get('PROFILE', "Offset") + profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") - if profile not in self.pmgr.get_profiles(): + if profile not in self.bed_mesh.pmgr.get_profiles(): raise self.printer.command_error("Profile " + str(profile) + " not found for Beacon scan compensation") - self.offset_mesh = self.pmgr.load_profile(profile) + self.offset_mesh = self.bed_mesh.pmgr.load_profile(profile) if not self.offset_mesh: raise self.printer.command_error("Could not load profile " + str(profile) + " for Beacon scan compensation") - self.compensate_beacon_scan(profile) + self.apply_scan_compensation(profile) - desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by joining a contact and a scan mesh." + desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): - profile = gcmd.get('PROFILE', "Offset") + profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) + probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=3) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") - self.create_compensation_mesh(profile) + if not probe_count: + raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") + self.create_compensation_mesh(profile, probe_count) + + desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." + def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): + if (self.bed_mesh.z_mesh is None): + self.ratos.console_echo("Set zero reference position error", "error", + "No bed mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") + return + + x_pos = gcmd.get('X', 0.0) + y_pos = gcmd.get('Y', 0.0) + save_profile = gcmd.get('SAVE_PROFILE', False) + + + org_mesh = self.bed_mesh.get_mesh() + new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) + new_mesh.build_mesh(org_mesh.get_mesh_matrix()) + new_mesh.set_zero_reference(x_pos, y_pos) + self.bed_mesh.set_mesh(new_mesh) + + if save_profile: + self.bed_mesh.pmgr.save_profile(new_mesh.get_profile_name()) + self.ratos.console_echo("Set zero reference position", "info", + "Zero reference position saved for profile %s" % (str(new_mesh.get_profile_name()))) + else: + self.ratos.console_echo("Set zero reference position", "info", + "Zero reference position set for profile %s" % (str(new_mesh.get_profile_name()))) ##### # Beacon Scan Compensation ##### - def compensate_beacon_scan(self, profile): - systime = self.reactor.monotonic() + def apply_scan_compensation(self, profile): + if not self.bed_mesh.z_mesh: + self.ratos.console_echo("Apply scan compensation error", "error", + "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") + return + + if not self.offset_mesh: + self.ratos.console_echo("Apply scan compensation error", "error", + "No scan compensation mesh loaded.") + return + + offset_mesh_params = self.offset_mesh.get_mesh_params() + + if offset_mesh_params["ratos_mesh_version"] != RATOS_MESH_VERSION: + self.ratos.console_echo("Apply scan compensation error", "error", + "Compensation mesh is not compatible with this version of RatOS.") + return + + if offset_mesh_params["beacon_model_name"] != self.beacon.model.name: + self.ratos.console_echo("Apply scan compensation error", "warning", + "Compensation mesh is calibrated for a different beacon model than the one currently loaded._N_" + "This may result in inaccurate compensation.") + + if offset_mesh_params["beacon_model_temp"] > self.beacon.model.temp + 2.5 or offset_mesh_params["beacon_model_temp"] < self.beacon.model.temp - 2.5: + self.ratos.console_echo("Apply scan compensation error", "warning", + "Compensation mesh is calibrated for a temperature that is %0.2fC different than the one currently loaded._N_" + "This may result in inaccurate compensation." % (abs(offset_mesh_params["beacon_model_temp"] - self.beacon.model.temp))) + try: - if self.bed_mesh.z_mesh: - profile_name = self.bed_mesh.z_mesh.get_profile_name() - if profile_name != profile: - points = self.bed_mesh.get_status(systime)["profiles"][profile_name]["points"] - params = self.bed_mesh.z_mesh.get_mesh_params() - x_step = ((params["max_x"] - params["min_x"]) / (len(points[0]) - 1)) - y_step = ((params["max_y"] - params["min_y"]) / (len(points) - 1)) - new_points = [] - for y in range(len(points)): - new_points.append([]) - for x in range(len(points[0])): - x_pos = params["min_x"] + x * x_step - y_pos = params["min_y"] + y * y_step - scan_z = points[y][x] - offset_z = self.offset_mesh.calc_z(x_pos, y_pos) - new_z = scan_z + offset_z - self.ratos.debug_echo("Beacon scan compensation", "scan: %0.4f offset: %0.4f new: %0.4f" % (scan_z, offset_z, new_z)) - new_points[y].append(new_z) - self.bed_mesh.z_mesh.build_mesh(new_points) - self.bed_mesh.save_profile(profile_name) - self.bed_mesh.set_mesh(self.bed_mesh.z_mesh) - self.ratos.console_echo("Beacon scan compensation", "debug", "Mesh scan profile %s compensated with contact profile %s" % (str(profile_name), str(profile))) + profile_name = self.bed_mesh.z_mesh.get_profile_name() + if profile_name == profile: + self.ratos.console_echo("Beacon scan compensation error", "error", + "Compensation profile name %s is the same as the scan profile name %s" % (str(profile), str(profile_name))) + return + + points = self.bed_mesh.pmgr.get_profiles()[profile_name]["points"] + params = self.bed_mesh.z_mesh.get_mesh_params() + x_step = ((params["max_x"] - params["min_x"]) / (len(points[0]) - 1)) + y_step = ((params["max_y"] - params["min_y"]) / (len(points) - 1)) + new_points = [] + + self.ratos.debug_echo("Beacon scan compensation", "scan mesh: %s" % (str(profile_name))) + self.ratos.debug_echo("Beacon scan compensation", "offset mesh: %s" % (str(profile))) + + for y in range(len(points)): + new_points.append([]) + for x in range(len(points[0])): + x_pos = params["min_x"] + x * x_step + y_pos = params["min_y"] + y * y_step + scan_z = points[y][x] + offset_z = self.offset_mesh.calc_z(x_pos, y_pos) + new_z = scan_z + offset_z + self.ratos.debug_echo("Beacon scan compensation", "scan: %0.4f offset: %0.4f new: %0.4f" % (scan_z, offset_z, new_z)) + new_points[y].append(new_z) + + self.bed_mesh.z_mesh.build_mesh(new_points) + self.bed_mesh.save_profile(profile_name) + self.bed_mesh.set_mesh(self.bed_mesh.z_mesh) + + self.ratos.console_echo("Beacon scan compensation", "debug", + "Mesh scan profile %s compensated with contact profile %s" % (str(profile_name), str(profile))) + except BedMesh.BedMeshError as e: self.ratos.console_echo("Beacon scan compensation error", "error", str(e)) - def create_compensation_mesh(self, profile): - systime = self.reactor.monotonic() - if self.bed_mesh.z_mesh: - self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='RatOSTempOffsetScan'") - scan_mesh_points = self.bed_mesh.get_status(systime)["profiles"]["RatOSTempOffsetScan"]["points"] - self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % profile) - try: - points = self.bed_mesh.get_status(systime)["profiles"][profile]["points"] - new_points = [] - for y in range(len(points)): - new_points.append([]) - for x in range(len(points[0])): - contact_z = points[y][x] - scan_z = scan_mesh_points[y][x] - offset_z = contact_z - scan_z - self.ratos.debug_echo("Create compensation mesh", "scan: %0.4f contact: %0.4f offset: %0.4f" % (scan_z, contact_z, offset_z)) - new_points[y].append(offset_z) - self.bed_mesh.z_mesh.build_mesh(new_points) - self.bed_mesh.save_profile(profile) - self.bed_mesh.set_mesh(self.bed_mesh.z_mesh) - self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) - except BedMesh.BedMeshError as e: - self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + def create_compensation_mesh(self, profile, probe_count): + if not self.bed_mesh.z_mesh: + self.ratos.console_echo("Create compensation mesh error", "error", + "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") + return + if not self.beacon: + self.ratos.console_echo("Create compensation mesh error", "error", + "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") + return -##### -# Bed Mesh Profile Manager -##### -PROFILE_VERSION = 1 -class BedMeshProfileManager: - def __init__(self, config, bedmesh): - self.name = "bed_mesh" - 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 != BedMesh.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, BedMesh.PROFILE_VERSION)) - self.incompatible_profiles.append(name) + # Calibrate a fresh model + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") + + # create contact mesh + self.gcode.run_script_from_command( + "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=2 SAMPLES_DROP=1 SAMPLES_TOLERANCE_RETRIES=10 " + "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], RATOS_TEMP_CONTACT_MESH_NAME)) + + # create temp scan mesh + self.gcode.run_script_from_command( + "BED_MESH_CALIBRATE METHOD=automatic USE_CONTACT_AREA=1 " + "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], RATOS_TEMP_SCAN_MESH_NAME)) + + self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % RATOS_TEMP_SCAN_MESH_NAME) + scan_mesh_points = self.bed_mesh.pmgr.get_profiles()[RATOS_TEMP_SCAN_MESH_NAME]["points"] + self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % RATOS_TEMP_CONTACT_MESH_NAME) + contact_mesh_points = self.bed_mesh.pmgr.get_profiles()[RATOS_TEMP_CONTACT_MESH_NAME]["points"] + compensation_mesh_points = [] + + try: + + for y in range(len(contact_mesh_points)): + compensation_mesh_points.append([]) + for x in range(len(contact_mesh_points[0])): + contact_z = contact_mesh_points[y][x] + scan_z = scan_mesh_points[y][x] + offset_z = contact_z - scan_z + self.ratos.debug_echo("Create compensation mesh", + "scan: %0.4f contact: %0.4f offset: %0.4f" % (scan_z, contact_z, offset_z)) + compensation_mesh_points[y].append(offset_z) + + # Get the beacon model + beacon_model = self.beacon.model + + # Create new mesh + params = self.bed_mesh.z_mesh.get_mesh_params() + params["ratos_mesh_version"] = RATOS_MESH_VERSION + params["beacon_model_temp"] = beacon_model.temp + params["beacon_model_name"] = beacon_model.name + new_mesh = BedMesh.ZMesh(self.bed_mesh.z_mesh.get_mesh_params(), profile) + new_mesh.build_mesh(compensation_mesh_points) + self.bed_mesh.set_mesh(new_mesh) + self.bed_mesh.save_profile(profile) + + # Remove temp meshes + self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % RATOS_TEMP_CONTACT_MESH_NAME) + self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % RATOS_TEMP_SCAN_MESH_NAME) + + self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) + except BedMesh.BedMeshError as e: + self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + + def load_extra_mesh_params(self): + profiles = self.bed_mesh.pmgr.get_profiles() + for profile_name in profiles.keys(): + profile = profiles[profile_name] + config = self.config.getsection(self.bed_mesh.pmgr.name + " " + profile_name) + profile_params = profile["mesh_params"] + if config is None: 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 BedMesh.PROFILE_OPTIONS.items(): + for key, t in RATOS_MESH_PROFILE_OPTIONS.items(): if t is int: - params[key] = profile.getint(key) + profile_params[key] = config.getint(key) elif t is float: - params[key] = profile.getfloat(key) + profile_params[key] = config.getfloat(key) elif t is str: - params[key] = profile.get(key) - def get_profiles(self): - return self.profiles - def load_profile(self, prof_name): - profile = self.profiles.get(prof_name, None) - if profile is None: - return None - probed_matrix = profile['points'] - mesh_params = profile['mesh_params'] - z_mesh = BedMesh.ZMesh(mesh_params, prof_name) - try: - z_mesh.build_mesh(probed_matrix) - except BedMesh.BedMeshError as e: - raise self.gcode.error(str(e)) - return z_mesh + profile_params[key] = config.get(key) + + ratos_mesh_version = profile.getint('ratos_mesh_version', None) + if ratos_mesh_version is not None and ratos_mesh_version != RATOS_MESH_VERSION: + logging.info( + "beacon_mesh: Profile [%s] is not compatible with this version of RatOS.\n" + "Profile Version: %d\nCurrent Version: %d " + % (profile_name, ratos_mesh_version, RATOS_MESH_VERSION)) + self.incompatible_profiles.append(profile_name) + continue + return None ##### # Loader diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 76512c6a3..08ec43eaa 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -901,20 +901,8 @@ gcode: # Go to safe home _MOVE_TO_SAFE_Z_HOME Z_HOP=True - # Zero Z with contact to make sure the mesh is created at true zero - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 - - # create contact mesh - BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=2 SAMPLES_DROP=1 SAMPLES_TOLERANCE_RETRIES=10 PROBE_COUNT={probe_count_x},{probe_count_y} PROFILE={profile} - - # create temp scan mesh - BED_MESH_CALIBRATE METHOD=automatic USE_CONTACT_AREA=1 PROBE_COUNT={probe_count_x},{probe_count_y} PROFILE="RatOSTempOffsetScan" - # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE={profile} - - # remove temp scan mesh - BED_MESH_PROFILE REMOVE="RatOSTempOffsetScan" + CREATE_BEACON_COMPENSATION_MESH PROFILE={profile} PROBE_COUNT={probe_count_x},{probe_count_y} # turn bed and extruder heaters off {% if not automated %} From 5f236ffe9f15ea2d90b1df07ed3b0c232f69a0a1 Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 27 Feb 2025 03:32:47 +0100 Subject: [PATCH 002/139] Mesh/Beacon: minor improvements to mesh error handling and logging --- configuration/klippy/beacon_mesh.py | 60 ++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 7402b29aa..1b620d1c4 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -101,7 +101,7 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): if (self.bed_mesh.z_mesh is None): self.ratos.console_echo("Set zero reference position error", "error", - "No bed mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") + "No bed mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") return x_pos = gcmd.get('X', 0.0) @@ -129,7 +129,7 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): def apply_scan_compensation(self, profile): if not self.bed_mesh.z_mesh: self.ratos.console_echo("Apply scan compensation error", "error", - "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") + "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") return if not self.offset_mesh: @@ -255,29 +255,51 @@ def create_compensation_mesh(self, profile, probe_count): def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() + modified_profiles = {} + for profile_name in profiles.keys(): profile = profiles[profile_name] - config = self.config.getsection(self.bed_mesh.pmgr.name + " " + profile_name) profile_params = profile["mesh_params"] - if config is None: + + # Try to find the config section for this profile + # Handle profile names with spaces correctly + try: + config_section_name = self.bed_mesh.pmgr.name + " " + profile_name + config = self.config.getsection(config_section_name) + except Exception: + # Skip if no config section exists for this profile continue + + # Load the extra parameters from config for key, t in RATOS_MESH_PROFILE_OPTIONS.items(): - if t is int: - profile_params[key] = config.getint(key) - elif t is float: - profile_params[key] = config.getfloat(key) - elif t is str: - profile_params[key] = config.get(key) + try: + if t is int: + profile_params[key] = config.getint(key) + elif t is float: + profile_params[key] = config.getfloat(key) + elif t is str: + profile_params[key] = config.get(key) + except Exception: + # Skip parameters that don't exist in the config + continue - ratos_mesh_version = profile.getint('ratos_mesh_version', None) - if ratos_mesh_version is not None and ratos_mesh_version != RATOS_MESH_VERSION: - logging.info( - "beacon_mesh: Profile [%s] is not compatible with this version of RatOS.\n" - "Profile Version: %d\nCurrent Version: %d " - % (profile_name, ratos_mesh_version, RATOS_MESH_VERSION)) - self.incompatible_profiles.append(profile_name) - continue - return None + # Check for version compatibility + try: + ratos_mesh_version = profile_params.get('ratos_mesh_version') + if ratos_mesh_version is not None and ratos_mesh_version != RATOS_MESH_VERSION: + logging.info( + "beacon_mesh: Profile [%s] is not compatible with this version of RatOS.\n" + "Profile Version: %d\nCurrent Version: %d " + % (profile_name, ratos_mesh_version, RATOS_MESH_VERSION)) + self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) + continue + except Exception: + # If we can't determine version compatibility, continue anyway + pass + + modified_profiles[profile_name] = profile + + return modified_profiles ##### # Loader From 112349eee1300cb8ae655f70ecbadefd81818f7d Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 27 Feb 2025 03:44:13 +0100 Subject: [PATCH 003/139] Mesh/Beacon: mark meshes invalid if parameters are missing --- configuration/klippy/beacon_mesh.py | 1 + 1 file changed, 1 insertion(+) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 1b620d1c4..8f9e3b1ad 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -281,6 +281,7 @@ def load_extra_mesh_params(self): profile_params[key] = config.get(key) except Exception: # Skip parameters that don't exist in the config + self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) continue # Check for version compatibility From 2717930e45fbc7d2d48de4256524db2afc43186d Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 27 Feb 2025 03:51:05 +0100 Subject: [PATCH 004/139] Mesh/Beacon: fix profile loading when applying compensation --- configuration/klippy/beacon_mesh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 8f9e3b1ad..69109531f 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -80,9 +80,10 @@ def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") - if profile not in self.bed_mesh.pmgr.get_profiles(): + profiles = self.bed_mesh.pmgr.get_profiles() + if profile not in profiles: raise self.printer.command_error("Profile " + str(profile) + " not found for Beacon scan compensation") - self.offset_mesh = self.bed_mesh.pmgr.load_profile(profile) + self.offset_mesh = profiles[profile] if not self.offset_mesh: raise self.printer.command_error("Could not load profile " + str(profile) + " for Beacon scan compensation") self.apply_scan_compensation(profile) From c9e7b5410f999129fd538eb6f99f4a6614d64bfd Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 27 Feb 2025 03:56:55 +0100 Subject: [PATCH 005/139] Mesh/Beacon: instantiate offset mesh when applying scan comp --- configuration/klippy/beacon_mesh.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 69109531f..3640ac7ba 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -80,12 +80,21 @@ def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") + profiles = self.bed_mesh.pmgr.get_profiles() if profile not in profiles: raise self.printer.command_error("Profile " + str(profile) + " not found for Beacon scan compensation") - self.offset_mesh = profiles[profile] + + self.offset_mesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile) + + try: + self.offset_mesh.build_mesh(profiles[profile]["points"]) + except BedMesh.BedMeshError as e: + raise self.gcode.error(str(e)) + if not self.offset_mesh: raise self.printer.command_error("Could not load profile " + str(profile) + " for Beacon scan compensation") + self.apply_scan_compensation(profile) desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." From caff3a237814134062b35a7975e80f9ee8486f8d Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Thu, 27 Feb 2025 04:02:51 +0100 Subject: [PATCH 006/139] Mesh/Beacon: properly check for mesh params when applying scan comp --- configuration/klippy/beacon_mesh.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 3640ac7ba..ffc1547e6 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -149,17 +149,30 @@ def apply_scan_compensation(self, profile): offset_mesh_params = self.offset_mesh.get_mesh_params() + if "ratos_mesh_version" not in offset_mesh_params: + self.ratos.console_echo("Apply scan compensation error", "error", + "Compensation mesh is missing version information.") + return + if offset_mesh_params["ratos_mesh_version"] != RATOS_MESH_VERSION: self.ratos.console_echo("Apply scan compensation error", "error", "Compensation mesh is not compatible with this version of RatOS.") return - if offset_mesh_params["beacon_model_name"] != self.beacon.model.name: + if "beacon_model_name" not in offset_mesh_params: + self.ratos.console_echo("Apply scan compensation error", "warning", + "Compensation mesh is missing beacon model information._N_" + "This may result in inaccurate compensation.") + elif offset_mesh_params["beacon_model_name"] != self.beacon.model.name: self.ratos.console_echo("Apply scan compensation error", "warning", "Compensation mesh is calibrated for a different beacon model than the one currently loaded._N_" "This may result in inaccurate compensation.") - if offset_mesh_params["beacon_model_temp"] > self.beacon.model.temp + 2.5 or offset_mesh_params["beacon_model_temp"] < self.beacon.model.temp - 2.5: + if "beacon_model_temp" not in offset_mesh_params: + self.ratos.console_echo("Apply scan compensation error", "warning", + "Compensation mesh is missing temperature calibration information._N_" + "This may result in inaccurate compensation.") + elif offset_mesh_params["beacon_model_temp"] > self.beacon.model.temp + 2.5 or offset_mesh_params["beacon_model_temp"] < self.beacon.model.temp - 2.5: self.ratos.console_echo("Apply scan compensation error", "warning", "Compensation mesh is calibrated for a temperature that is %0.2fC different than the one currently loaded._N_" "This may result in inaccurate compensation." % (abs(offset_mesh_params["beacon_model_temp"] - self.beacon.model.temp))) From 365ac5ee1cd5b8e8713cf02fa415154d918939fc Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Fri, 14 Mar 2025 16:02:34 +0100 Subject: [PATCH 007/139] Macros/Mesh: set zero reference position on loaded mesh in start_print --- configuration/klippy/beacon_mesh.py | 8 ++++---- configuration/macros/mesh.cfg | 30 +++++++++++++++++++++++++---- configuration/z-probe/beacon.cfg | 4 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index ffc1547e6..eb4d5cb4f 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -114,10 +114,10 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): "No bed mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") return - x_pos = gcmd.get('X', 0.0) - y_pos = gcmd.get('Y', 0.0) - save_profile = gcmd.get('SAVE_PROFILE', False) - + x_pos = gcmd.get_float('X') + y_pos = gcmd.get_float('Y') + save_profile = gcmd.get('SAVE_PROFILE', "false") + save_profile = save_profile.lower() in ("true", "1", "yes") org_mesh = self.bed_mesh.get_mesh() new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 1cd217d58..f3e447449 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -47,6 +47,23 @@ gcode: # config {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} + {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} + + {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} + {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} + {% set safe_home_x = printable_x_max / 2 %} + {% endif %} + {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} + {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} + {% set safe_home_y = printable_y_max / 2 %} + {% endif %} + + {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} + {% if zero_ref_pos is not defined %} + {% set zero_ref_pos = [safe_home_x, safe_home_y] %} + {% endif %} + + {% if idex_mode == "copy" or idex_mode == "mirror" %} # in copy and mirror mode we mesh the whole x bed length of the print area # this is later needed for the live toohlead z-offset compensation @@ -81,15 +98,20 @@ gcode: BED_MESH_CALIBRATE PROFILE={default_profile} {% endif %} {% endif %} - _BEACON_APPLY_SCAN_COMPENSATION + _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} {% endif %} {% endif %} BED_MESH_PROFILE LOAD={default_profile} + + SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} + {% elif printer["gcode_macro RatOS"].bed_mesh_profile is defined %} BED_MESH_CLEAR BED_MESH_PROFILE LOAD={printer["gcode_macro RatOS"].bed_mesh_profile} + + SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} {% endif %} # Handle toolhead settings @@ -133,7 +155,7 @@ gcode: {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} {% endif %} - _BEACON_APPLY_SCAN_COMPENSATION + _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% endif %} {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} @@ -166,7 +188,7 @@ gcode: {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} {% endif %} - _BEACON_APPLY_SCAN_COMPENSATION + _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% endif %} {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} @@ -256,7 +278,7 @@ gcode: BED_MESH_CALIBRATE PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% endif %} {% endif %} - _BEACON_APPLY_SCAN_COMPENSATION + _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% else %} BED_MESH_CALIBRATE PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% endif %} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 08ec43eaa..cad1e444f 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -938,13 +938,13 @@ gcode: {% endif %} -[gcode_macro _BEACON_APPLY_SCAN_COMPENSATION] +[gcode_macro _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED] gcode: # beacon config {% set beacon_scan_compensation_profile = printer["gcode_macro RatOS"].beacon_scan_compensation_profile %} {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} - DEBUG_ECHO PREFIX="_BEACON_APPLY_SCAN_COMPENSATION" MSG="beacon_scan_compensation_profile {beacon_scan_compensation_profile}, beacon_scan_compensation_enable {beacon_scan_compensation_enable}" + DEBUG_ECHO PREFIX="_BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED" MSG="beacon_scan_compensation_profile {beacon_scan_compensation_profile}, beacon_scan_compensation_enable {beacon_scan_compensation_enable}" {% if beacon_scan_compensation_enable %} BEACON_APPLY_SCAN_COMPENSATION PROFILE={beacon_scan_compensation_profile} From bd0a74e12f6b3f82ac713ce3b6603c41410768bf Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 7 Apr 2025 21:24:31 +0100 Subject: [PATCH 008/139] Macros/Mesh: when setting zero reference position, copy the probed points, not the interpolated points. --- configuration/klippy/beacon_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index eb4d5cb4f..c0ebc9959 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -121,7 +121,7 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): org_mesh = self.bed_mesh.get_mesh() new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) - new_mesh.build_mesh(org_mesh.get_mesh_matrix()) + new_mesh.build_mesh(org_mesh.get_probed_matrix()) new_mesh.set_zero_reference(x_pos, y_pos) self.bed_mesh.set_mesh(new_mesh) From b835a6c03545de9ceefb0548dc8afa1c9d15a584 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 14 Apr 2025 19:49:02 +0100 Subject: [PATCH 009/139] Macros/Mesh: Move to safe Z home in CREATE_BEACON_COMPENSATION_MESH --- configuration/klippy/beacon_mesh.py | 3 +++ configuration/z-probe/beacon.cfg | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index c0ebc9959..4cb4d8bf3 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -224,6 +224,9 @@ def create_compensation_mesh(self, profile, probe_count): "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") return + # Go to safe home + self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") + # Calibrate a fresh model self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index cad1e444f..b98530a1f 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -898,9 +898,6 @@ gcode: RATOS_ECHO MSG="Rehoming Z after quad gantry leveling..." {% endif %} - # Go to safe home - _MOVE_TO_SAFE_Z_HOME Z_HOP=True - # create compensation mesh CREATE_BEACON_COMPENSATION_MESH PROFILE={profile} PROBE_COUNT={probe_count_x},{probe_count_y} From 5f93c7aaae48b69ae80836d3373ebba1e5293a6d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 14 Apr 2025 19:50:07 +0100 Subject: [PATCH 010/139] Macros/Mesh: don't require a mesh to be pre-loaded when calling CREATE_BEACON_COMPENSATION_MESH --- configuration/klippy/beacon_mesh.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 4cb4d8bf3..6941af3eb 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -215,10 +215,6 @@ def apply_scan_compensation(self, profile): self.ratos.console_echo("Beacon scan compensation error", "error", str(e)) def create_compensation_mesh(self, profile, probe_count): - if not self.bed_mesh.z_mesh: - self.ratos.console_echo("Create compensation mesh error", "error", - "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=[profile_name]") - return if not self.beacon: self.ratos.console_echo("Create compensation mesh error", "error", "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") From eaf021fe2f63dbc13be477df7f954f067cdfa6dc Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:03:06 +0100 Subject: [PATCH 011/139] Macros/Beacon: Add early checks in START_PRINT for beacon model and scan compensation mesh issues rather than failing late eg after heat soaking. --- configuration/klippy/ratos.py | 15 ++++++++++++++ configuration/macros.cfg | 6 ++++++ configuration/macros/mesh.cfg | 7 +++++++ configuration/z-probe/beacon.cfg | 35 +++++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index e5e10b314..4a2ca09fa 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -25,6 +25,7 @@ def __init__(self, config): 'TEST_RESONANCES': None, 'SHAPER_CALIBRATE': None, } + self.bed_mesh = None self.old_is_graph_files = [] self.load_settings() @@ -48,6 +49,8 @@ def _connect(self): self.rmmu_hub = None if self.config.has_section("rmmu_hub"): self.rmmu_hub = self.printer.lookup_object("rmmu_hub", None) + if self.config.has_section("bed_mesh"): + self.bed_mesh = self.printer.lookup_object('bed_mesh') # Register overrides. self.register_command_overrides() @@ -75,6 +78,7 @@ def register_commands(self): self.gcode.register_command('ALLOW_UNKNOWN_GCODE_GENERATOR', self.cmd_ALLOW_UNKNOWN_GCODE_GENERATOR, desc=(self.desc_ALLOW_UNKNOWN_GCODE_GENERATOR)) self.gcode.register_command('BYPASS_GCODE_PROCESSING', self.cmd_BYPASS_GCODE_PROCESSING, desc=(self.desc_BYPASS_GCODE_PROCESSING)) self.gcode.register_command('_SYNC_GCODE_POSITION', self.cmd_SYNC_GCODE_POSITION, desc=(self.desc_SYNC_GCODE_POSITION)) + self.gcode.register_command('_CHECK_BED_MESH_PROFILE_EXISTS', self.cmd_CHECK_BED_MESH_PROFILE_EXISTS, desc=(self.desc_CHECK_BED_MESH_PROFILE_EXISTS)) def register_command_overrides(self): self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) @@ -200,6 +204,17 @@ def cmd_RATOS_LOG(self, gcmd): msg = gcmd.get('MSG') logging.info(prefix + ": " + msg) + desc_CHECK_BED_MESH_PROFILE_EXISTS = "Raises an error if [bed_mesh] is configured and the specified profile does not exist" + def cmd_CHECK_BED_MESH_PROFILE_EXISTS(self, gcmd): + if self.bed_mesh: + profile = gcmd.get('PROFILE', '') + if not profile.strip(): + raise gcmd.error("Value for parameter 'PROFILE' must be specified") + msg = gcmd.get('MSG', "Bed mesh profile '%s' not found") + profiles = self.bed_mesh.pmgr.get_profiles() + if profile not in profiles: + raise self.printer.command_error(msg % (profile)) + desc_PROCESS_GCODE_FILE = "G-code post-processor for IDEX and RMMU" def cmd_PROCESS_GCODE_FILE(self, gcmd): filename = gcmd.get('FILENAME', "") diff --git a/configuration/macros.cfg b/configuration/macros.cfg index b723cacd8..cdccc1e08 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -344,6 +344,12 @@ gcode: {% set total_layer_count = params.TOTAL_LAYER_COUNT|default(0)|int %} {% set extruder_first_layer_temp = (params.EXTRUDER_TEMP|default("")).split(",") %} + {% if printer.configfile.settings.beacon is defined %} + _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL BEACON_CONTACT_START_PRINT_TRUE_ZERO={beacon_contact_start_print_true_zero} BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT={beacon_contact_calibrate_model_on_print} + {% endif %} + + _START_PRINT_PREFLIGHT_CHECK_BED_MESH + # echo first print coordinates RATOS_ECHO MSG="First print coordinates X:{first_x} Y:{first_y}" diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index f3e447449..f02f694a2 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -32,6 +32,13 @@ gcode: {% endif %} {% endif %} +[gcode_macro _START_PRINT_PREFLIGHT_CHECK_BED_MESH] +gcode: + # Error early if the scan compensation mesh is required but does not exist + {% if printer.configfile.settings.beacon is defined %} + _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION + {% endif %} + [gcode_macro _START_PRINT_BED_MESH] gcode: # Handle toolhead settings diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index b98530a1f..5cbcb3b2a 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -944,7 +944,40 @@ gcode: DEBUG_ECHO PREFIX="_BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED" MSG="beacon_scan_compensation_profile {beacon_scan_compensation_profile}, beacon_scan_compensation_enable {beacon_scan_compensation_enable}" {% if beacon_scan_compensation_enable %} - BEACON_APPLY_SCAN_COMPENSATION PROFILE={beacon_scan_compensation_profile} + BEACON_APPLY_SCAN_COMPENSATION PROFILE="{beacon_scan_compensation_profile}" + {% endif %} + +[gcode_macro _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL] +gcode: + # Passed as params to avoid duplication of defaulting + {% set beacon_contact_start_print_true_zero = params.BEACON_CONTACT_START_PRINT_TRUE_ZERO|lower == 'true' %} + {% set beacon_contact_calibrate_model_on_print = params.BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT|lower == 'true' %} + + # Error early with a clear message if a Beacon model is required but not active. + {% if printer.configfile.settings.beacon is defined and not printer.beacon.model %} + {% set beacon_model_required_for_true_zero = beacon_contact_start_print_true_zero and not beacon_contact_calibrate_model_on_print %} + {% set beacon_model_required_for_homing = + printer.configfile.settings.stepper_z.endstop_pin == 'probe:z_virtual_endstop' + and (( printer.configfile.settings.beacon.home_method|default('proximity')|lower == 'proximity' + and printer["gcode_macro RatOS"].beacon_contact_z_homing|default(false)|lower != 'true') + or ( printer.configfile.settings.beacon.default_probe_method|default('proximity')|lower == 'proximity' + and printer["gcode_macro RatOS"].beacon_contact_z_tilt_adjust|default(false)|lower != 'true' )) %} + {% if beacon_model_required_for_homing or beacon_model_required_for_true_zero %} + _LED_START_PRINTING_ERROR + { action_raise_error("An active Beacon model is required. Have you performed initial Beacon calibration?") } + {% endif %} + {% endif %} + + +[gcode_macro _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION] +gcode: + {% set beacon_scan_compensation_profile = printer["gcode_macro RatOS"].beacon_scan_compensation_profile %} + {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} + + DEBUG_ECHO PREFIX="_START_PRINT_PREFLIGHT_CHECK_BEACON" MSG="beacon_scan_compensation_profile {beacon_scan_compensation_profile}, beacon_scan_compensation_enable {beacon_scan_compensation_enable}" + + {% if beacon_scan_compensation_enable %} + _CHECK_BED_MESH_PROFILE_EXISTS PROFILE="{beacon_scan_compensation_profile}" MSG="Beacon scan compensation bed mesh profile '%s' not found" {% endif %} From 4c456b6b2ee62b45b86612ef659b77b0e6323a11 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:07:52 +0100 Subject: [PATCH 012/139] Macros/Beacon: Before BED_MESH_CALIBRATE, check beacon model temp is reasonably close to the current beacon coil temp. Currently allowing up to 20C difference as the acceptable range. --- configuration/klippy/beacon_mesh.py | 19 +++++++++++++++++++ configuration/z-probe/beacon.cfg | 14 +++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 6941af3eb..29579e8bf 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -74,6 +74,25 @@ def register_commands(self): self.gcode.register_command('SET_ZERO_REFERENCE_POSITION', self.cmd_SET_ZERO_REFERENCE_POSITION, desc=(self.desc_SET_ZERO_REFERENCE_POSITION)) + self.gcode.register_command('_CHECK_ACTIVE_BEACON_MODEL_TEMP', + self.cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP, + desc=(self.desc_CHECK_ACTIVE_BEACON_MODEL_TEMP)) + + desc_CHECK_ACTIVE_BEACON_MODEL_TEMP = "Warns if the active Beacon model temperature is far from the current Beacon coil temperature." + def cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP(self, gcmd): + margin = gcmd.get_int('MARGIN', 20, minval=1) + title = gcmd.get('TITLE', 'Active Beacon model temperature warning') + self.check_active_beacon_model_temp(margin, title) + + def check_active_beacon_model_temp(self, margin=20, title='Active Beacon model temperature warning'): + if self.ratos and self.beacon and self.beacon.model: + coil_temp = self.beacon.last_temp + model_temp = self.beacon.model.temp + + if coil_temp < model_temp - margin or coil_temp > model_temp + margin: + self.ratos.console_echo(title, "warning", + "The active Beacon model ('%s') is calibrated for a temperature that is %0.2fC different than the current Beacon coil temperature._N_" + "This may result in inaccurate compensation." % (self.beacon.model.name, abs(coil_temp - model_temp))) desc_BEACON_APPLY_SCAN_COMPENSATION = "Compensates a beacon scan mesh with a beacon compensation mesh." def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 5cbcb3b2a..8561e658a 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1122,4 +1122,16 @@ gcode: # probe BEACON_OFFSET_COMPARE SAMPLES_DROP=1 SAMPLES=3 - BEACON_QUERY \ No newline at end of file + BEACON_QUERY + +[gcode_macro BED_MESH_CALIBRATE] +rename_existing: _BED_MESH_CALIBRATE_ORIG_RAT_BEACON +gcode: + {% if printer.configfile.settings.beacon is defined %} + {% set beacon_default_probe_method = printer.configfile.settings.beacon.default_probe_method|default('proximity') %} + {% set probe_method = params.PROBE_METHOD|default(beacon_default_probe_method)|lower %} + {% if probe_method == 'proximity' %} + _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" + {% endif %} + {% endif %} + _BED_MESH_CALIBRATE_ORIG_RAT_BEACON {rawparams} \ No newline at end of file From feea5bf671bfc823dbbe902a5dc98157fa4e8cc9 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:09:11 +0100 Subject: [PATCH 013/139] Macros/Beacon: Consistently use "Beacon Scan Compensation" as the default name for the beacon scan compensation mesh --- configuration/klippy/beacon_mesh.py | 2 +- configuration/z-probe/beacon.cfg | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 29579e8bf..11130f318 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -6,7 +6,7 @@ ### RATOS_TEMP_SCAN_MESH_NAME = "__BEACON_TEMP_SCAN_MESH__" RATOS_TEMP_CONTACT_MESH_NAME = "__BEACON_TEMP_CONTACT_MESH__" -RATOS_DEFAULT_COMPENSATION_MESH_NAME = "Beacon Compensation Mesh" +RATOS_DEFAULT_COMPENSATION_MESH_NAME = "Beacon Scan Compensation" RATOS_MESH_VERSION = 1 RATOS_MESH_PROFILE_OPTIONS = { "ratos_mesh_version": int, diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 8561e658a..dfa153a0f 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -55,9 +55,10 @@ variable_beacon_contact_bed_mesh_samples: 2 # probe samples for cont variable_beacon_contact_z_tilt_adjust: False # z-tilt adjust with contact method variable_beacon_contact_z_tilt_adjust_samples: 2 # probe samples for contact z-tilt adjust -variable_beacon_scan_compensation_enable: False # Enables the beacon scan compensation -variable_beacon_scan_compensation_profile: "Offset" # The beacon offset profile name for the scan compensation -variable_beacon_scan_compensation_resolution: 40 # The mesh resolution in mm for the scan compensation +variable_beacon_scan_compensation_enable: False # Enables beacon scan compensation +variable_beacon_scan_compensation_profile: "Beacon Scan Compensation" + # The bed mesh profile name for the scan compensation mesh +variable_beacon_scan_compensation_resolution: 40 # The mesh resolution in mm for scan compensation variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the contact poke test variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option @@ -811,7 +812,7 @@ gcode: # parameters {% set bed_temp = params.BED_TEMP|default(85)|int %} {% set chamber_temp = params.CHAMBER_TEMP|default(0)|int %} - {% set profile = params.PROFILE|default("Offset")|string %} + {% set profile = params.PROFILE|default("Beacon Scan Compensation")|string %} {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} # config From bd5ae1d834b3d4100b7cda988718c3354e4051f8 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:11:31 +0100 Subject: [PATCH 014/139] Mesh/Beacon: In create_compensation_mesh, use existing or fresh beacon model according to beacon_contact_calibrate_model_on_print --- configuration/klippy/beacon_mesh.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 11130f318..67b131176 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -34,6 +34,7 @@ def __init__(self, config): # These are loaded on klippy:connect. self.beacon = None self.ratos = None + self.gm_ratos = None self.bed_mesh = None self.offset_mesh = None @@ -51,6 +52,7 @@ def register_handler(self): def _connect(self): if self.config.has_section("ratos"): self.ratos = self.printer.lookup_object('ratos') + self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') if self.config.has_section("bed_mesh"): self.bed_mesh = self.printer.lookup_object('bed_mesh') # Load additional RatOS mesh params @@ -239,11 +241,23 @@ def create_compensation_mesh(self, profile, probe_count): "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") return + beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' + # Go to safe home self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") - # Calibrate a fresh model - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") + if beacon_contact_calibrate_model_on_print: + # Calibrate a fresh model + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") + else: + if self.beacon.model is None: + self.ratos.console_echo("Create compensation mesh error", "error", + "No active Beacon model is selected._N_Make sure you've performed initial Beacon calibration.") + return + + self.check_active_beacon_model_temp(title="Create compensation mesh warning") + + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1") # create contact mesh self.gcode.run_script_from_command( From 959a0ddbd706f944536b12161477688f963e471a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:14:43 +0100 Subject: [PATCH 015/139] Macros: Add missing _LED_START_PRINTING_ERROR in START_PRINT --- configuration/macros.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index cdccc1e08..653f92651 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -281,12 +281,15 @@ variable_extruder_other_layer_temp: "" # internal use only. Do not touch! gcode: # mandatory parameter sanity check {% if params.EXTRUDER_TEMP is not defined %} + _LED_START_PRINTING_ERROR { action_raise_error("Missing START_PRINT parameter. EXTRUDER_TEMP parameter not found.")} {% endif %} {% if params.EXTRUDER_OTHER_LAYER_TEMP is not defined %} + _LED_START_PRINTING_ERROR { action_raise_error("Missing START_PRINT parameter. EXTRUDER_OTHER_LAYER_TEMP parameter not found.")} {% endif %} {% if params.BED_TEMP is not defined %} + _LED_START_PRINTING_ERROR { action_raise_error("Missing START_PRINT parameter. BED_TEMP parameter not found.")} {% endif %} From 96ddb5b177f49384494ef50eb1ebae8e2b99cb93 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:16:21 +0100 Subject: [PATCH 016/139] Macros/Mesh: Quote bed mesh profile name gcode arguments so that spaces are handled --- configuration/macros/mesh.cfg | 38 ++++++++++++++++---------------- configuration/z-probe/beacon.cfg | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index f02f694a2..2db047819 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -93,30 +93,30 @@ gcode: {% if printer["gcode_macro RatOS"].calibrate_bed_mesh|lower == 'true' %} BED_MESH_CLEAR {% if printer["gcode_macro RatOS"].adaptive_mesh|lower == 'true' %} - CALIBRATE_ADAPTIVE_MESH PROFILE={default_profile} X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} + CALIBRATE_ADAPTIVE_MESH PROFILE="{default_profile}" X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} {% else %} {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE={default_profile} + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} - BED_MESH_CALIBRATE METHOD=automatic PROFILE={default_profile} + BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} {% endif %} _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} {% endif %} - BED_MESH_PROFILE LOAD={default_profile} + BED_MESH_PROFILE LOAD="{default_profile}" SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} {% elif printer["gcode_macro RatOS"].bed_mesh_profile is defined %} BED_MESH_CLEAR - BED_MESH_PROFILE LOAD={printer["gcode_macro RatOS"].bed_mesh_profile} + BED_MESH_PROFILE LOAD="{printer["gcode_macro RatOS"].bed_mesh_profile}" SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} {% endif %} @@ -155,17 +155,17 @@ gcode: RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Invalid coordinates received. Please check your slicer settings. Falling back to full bed mesh." {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE={default_profile} + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} - BED_MESH_CALIBRATE METHOD=automatic PROFILE={default_profile} + BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% endif %} {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} {% else %} # get bed mesh config object @@ -188,17 +188,17 @@ gcode: RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Print is using the full bed, falling back to full bed mesh." {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE={default_profile} + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} - BED_MESH_CALIBRATE METHOD=automatic PROFILE={default_profile} + BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% endif %} {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} + BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} {% else %} {% if printer["gcode_macro RatOS"].z_probe|lower == 'stowable' %} @@ -277,17 +277,17 @@ gcode: RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0} Y0={mesh_y0} X1={mesh_x1} Y1={mesh_y1}" {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 {% else %} {% if beacon_scan_method_automatic %} - BED_MESH_CALIBRATE METHOD=automatic PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} + BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} + BED_MESH_CALIBRATE PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% endif %} {% endif %} _BEACON_APPLY_SCAN_COMPENSATION_IF_ENABLED {% else %} - BED_MESH_CALIBRATE PROFILE={default_profile} ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} + BED_MESH_CALIBRATE PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% endif %} # probe for priming diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index dfa153a0f..fb25403cc 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -900,7 +900,7 @@ gcode: {% endif %} # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE={profile} PROBE_COUNT={probe_count_x},{probe_count_y} + CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} # turn bed and extruder heaters off {% if not automated %} From 3862fe163d2faebaab1ebc5963e9f0fa9c567e90 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 19:17:20 +0100 Subject: [PATCH 017/139] Beacon/Mesh: Improve logging/debug in SET_ZERO_REFERENCE_POSITION --- configuration/klippy/beacon_mesh.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 67b131176..41145e289 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -140,6 +140,8 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): save_profile = gcmd.get('SAVE_PROFILE', "false") save_profile = save_profile.lower() in ("true", "1", "yes") + self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f} save:{save_profile}") + org_mesh = self.bed_mesh.get_mesh() new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) new_mesh.build_mesh(org_mesh.get_probed_matrix()) @@ -149,10 +151,10 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): if save_profile: self.bed_mesh.pmgr.save_profile(new_mesh.get_profile_name()) self.ratos.console_echo("Set zero reference position", "info", - "Zero reference position saved for profile %s" % (str(new_mesh.get_profile_name()))) + f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") else: self.ratos.console_echo("Set zero reference position", "info", - "Zero reference position set for profile %s" % (str(new_mesh.get_profile_name()))) + f"Zero reference position set temporarily for profile '{new_mesh.get_profile_name()}'._N_Note: the zeroed state will be lost if a different profile is selected.") ##### # Beacon Scan Compensation From c049d29962a856143dc8b525379765381e4ad59b Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Apr 2025 21:24:52 +0100 Subject: [PATCH 018/139] Mesh/Beacon: Remove SAVE_PROFILE option from SET_ZERO_REFERENCE_POSITION, now always saves. From discussion with MS there was no known use case for *not* saving, and not saving creates a profile in a confusing ephemeral state which is lost when the profile is reloaded. --- configuration/klippy/beacon_mesh.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 41145e289..6d0c6bc61 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -137,10 +137,8 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): x_pos = gcmd.get_float('X') y_pos = gcmd.get_float('Y') - save_profile = gcmd.get('SAVE_PROFILE', "false") - save_profile = save_profile.lower() in ("true", "1", "yes") - self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f} save:{save_profile}") + self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f}") org_mesh = self.bed_mesh.get_mesh() new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) @@ -148,13 +146,9 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): new_mesh.set_zero_reference(x_pos, y_pos) self.bed_mesh.set_mesh(new_mesh) - if save_profile: - self.bed_mesh.pmgr.save_profile(new_mesh.get_profile_name()) - self.ratos.console_echo("Set zero reference position", "info", - f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") - else: - self.ratos.console_echo("Set zero reference position", "info", - f"Zero reference position set temporarily for profile '{new_mesh.get_profile_name()}'._N_Note: the zeroed state will be lost if a different profile is selected.") + self.bed_mesh.pmgr.save_profile(new_mesh.get_profile_name()) + self.ratos.console_echo("Set zero reference position", "info", + f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") ##### # Beacon Scan Compensation From b8558b18b4c0f07ea92cc349e9e3ded2e9e27df7 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 22 Apr 2025 15:18:53 +0100 Subject: [PATCH 019/139] Macros: error handling utility improvements - Implement _RAISE_ERROR in python instead of jinja for cleaner error messages - Implement _TRY, the try/except/finally pattern for macros --- configuration/klippy/ratos.py | 45 ++++++++++++++++++++++++++++++++++- configuration/macros/util.cfg | 5 ---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 4a2ca09fa..d5606722a 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -79,6 +79,8 @@ def register_commands(self): self.gcode.register_command('BYPASS_GCODE_PROCESSING', self.cmd_BYPASS_GCODE_PROCESSING, desc=(self.desc_BYPASS_GCODE_PROCESSING)) self.gcode.register_command('_SYNC_GCODE_POSITION', self.cmd_SYNC_GCODE_POSITION, desc=(self.desc_SYNC_GCODE_POSITION)) self.gcode.register_command('_CHECK_BED_MESH_PROFILE_EXISTS', self.cmd_CHECK_BED_MESH_PROFILE_EXISTS, desc=(self.desc_CHECK_BED_MESH_PROFILE_EXISTS)) + self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=(self.desc_RAISE_ERROR)) + self.gcode.register_command('_TRY', self.cmd_TRY, desc=(self.desc_TRY)) def register_command_overrides(self): self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) @@ -204,7 +206,48 @@ def cmd_RATOS_LOG(self, gcmd): msg = gcmd.get('MSG') logging.info(prefix + ": " + msg) - desc_CHECK_BED_MESH_PROFILE_EXISTS = "Raises an error if [bed_mesh] is configured and the specified profile does not exist" + desc_RAISE_ERROR = "Raises an error when the macro is executed, unlike {action_raise_error()} which is executed when the macro is evaluated (rendered)" + def cmd_RAISE_ERROR(self, gcmd): + # This is implemented in python to avoid the unhelpful prefixing of the current macro name to the error message + # when {action_raise_error()} is used in a [gcode_macro] template. + msg = gcmd.get('MSG') + raise self.printer.command_error(msg) + + desc_TRY = "Implements the try/except/finally pattern" + def cmd_TRY(self, gcmd): + command = gcmd.get("__COMMAND").strip() + if not command: + raise gcmd.error("Value for parameter '__COMMAND' must be specified") + + _except = gcmd.get("__EXCEPT", "").strip() + _finally = gcmd.get("__FINALLY", "").strip() + + to_run = f'{command} {gcmd.get_raw_command_parameters()}' + + self.debug_echo("TRY", f"Command: {command}") + self.debug_echo("TRY", f"Run: {to_run}") + if _except: + self.debug_echo("TRY", f"Except: {_except}") + if _finally: + self.debug_echo("TRY", f"Finally: {_finally}") + + try: + self.gcode.run_script_from_command(to_run) + except: + if _except: + try: + self.gcode.run_script_from_command(_except) + except Exception as ex: + self.debug_echo("TRY", f"Except command failed: {str(ex)}") + raise + finally: + if _finally: + try: + self.gcode.run_script_from_command(_finally) + except Exception as ex: + self.debug_echo("TRY", f"Finally command failed: {str(ex)}") + + desc_CHECK_BED_MESH_PROFILE_EXISTS = "Sets status last_check_bed_mesh_profile_exists_result to True if [bed_mesh] is configured and the specified profile exists, otherwise False." def cmd_CHECK_BED_MESH_PROFILE_EXISTS(self, gcmd): if self.bed_mesh: profile = gcmd.get('PROFILE', '') diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index 045a006e6..303fc26e0 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -402,11 +402,6 @@ gcode: [gcode_macro M601] gcode: PAUSE - -[gcode_macro _RAISE_ERROR] -gcode: - { action_raise_error(params.MSG) } - [gcode_macro _STOP_AND_RAISE_ERROR] gcode: From aba1ecab2ffdf3b09e83876c39b852649a39e31f Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 22 Apr 2025 15:22:40 +0100 Subject: [PATCH 020/139] Macros: _CHECK_BED_MESH_PROFILE_EXISTS now sets a last check result status instead of raising an error. --- configuration/klippy/ratos.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index d5606722a..88bf78698 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -14,7 +14,6 @@ def __init__(self, config): self.config = config self.printer = config.get_printer() self.name = config.get_name() - self.last_processed_file_result = None self.bypass_post_processing = False self.enable_gcode_transform = False self.allow_unsupported_slicer_versions = False @@ -27,6 +26,10 @@ def __init__(self, config): } self.bed_mesh = None + # Status fields + self.last_processed_file_result = None + self.last_check_bed_mesh_profile_exists_result = None + self.old_is_graph_files = [] self.load_settings() self.register_commands() @@ -249,14 +252,14 @@ def cmd_TRY(self, gcmd): desc_CHECK_BED_MESH_PROFILE_EXISTS = "Sets status last_check_bed_mesh_profile_exists_result to True if [bed_mesh] is configured and the specified profile exists, otherwise False." def cmd_CHECK_BED_MESH_PROFILE_EXISTS(self, gcmd): + self.last_check_bed_mesh_profile_exists_result = False if self.bed_mesh: profile = gcmd.get('PROFILE', '') if not profile.strip(): - raise gcmd.error("Value for parameter 'PROFILE' must be specified") - msg = gcmd.get('MSG', "Bed mesh profile '%s' not found") + raise gcmd.error("Value for parameter 'PROFILE' must be specified") profiles = self.bed_mesh.pmgr.get_profiles() - if profile not in profiles: - raise self.printer.command_error(msg % (profile)) + if profile in profiles: + self.last_check_bed_mesh_profile_exists_result = True desc_PROCESS_GCODE_FILE = "G-code post-processor for IDEX and RMMU" def cmd_PROCESS_GCODE_FILE(self, gcmd): @@ -510,10 +513,10 @@ def get_gcode_file_info(self, filename): # Helper ##### def ratos_echo(self, prefix, msg): - self.gcode.run_script_from_command("RATOS_ECHO PREFIX='" + str(prefix) + "' MSG='" + str(msg) + "'") + self.gcode.run_script_from_command("RATOS_ECHO PREFIX='" + str(prefix) + "' MSG='" + str(msg).replace("'", "`").replace("\n", "_N_") + "'") def debug_echo(self, prefix, msg): - self.gcode.run_script_from_command("DEBUG_ECHO PREFIX='" + str(prefix) + "' MSG='" + str(msg) + "'") + self.gcode.run_script_from_command("DEBUG_ECHO PREFIX='" + str(prefix) + "' MSG='" + str(msg).replace("'", "`").replace("\n", "_N_") + "'") def console_echo(self, title, type, msg=''): color = "white" @@ -573,7 +576,10 @@ def get_ratos_version(self): return version def get_status(self, eventtime): - return {'name': self.name, 'last_processed_file_result': self.last_processed_file_result} + return { + 'name': self.name, + 'last_processed_file_result': self.last_processed_file_result, + 'last_check_bed_mesh_profile_exists_result': self.last_check_bed_mesh_profile_exists_result } ##### # Loader From 5e76248b963cfc30f27fc1ab339fc1faa13fd0e1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 22 Apr 2025 15:33:41 +0100 Subject: [PATCH 021/139] Beacon/Mesh: Substantial rework and improve extended metadata and related functionality - Change and enhance the set of metadata stored: - Remove beacon_model_temp as this is not relevant. - Add bed temperature, mesh kind and probe method. - Add warnings in create_compensation_mesh() for unapplied z-tilt or QGL - Add various new gcode commands - Defer profile deserialization using delayed gcode to ensure that warnings are seen in the console - Replace/improve checks and warnings/errors - Enhance preflight checks called from START_PRINT - Apply extended metadata to all profiles/meshes created via BED_MESH_CALIBRATE --- configuration/klippy/beacon_mesh.py | 492 +++++++++++++++++++++------- configuration/macros.cfg | 2 +- configuration/macros/mesh.cfg | 2 +- configuration/z-probe/beacon.cfg | 23 +- 4 files changed, 384 insertions(+), 135 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 6d0c6bc61..f69de8644 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,4 +1,4 @@ -import logging, collections +import collections from . import bed_mesh as BedMesh ### @@ -8,19 +8,55 @@ RATOS_TEMP_CONTACT_MESH_NAME = "__BEACON_TEMP_CONTACT_MESH__" RATOS_DEFAULT_COMPENSATION_MESH_NAME = "Beacon Scan Compensation" RATOS_MESH_VERSION = 1 -RATOS_MESH_PROFILE_OPTIONS = { - "ratos_mesh_version": int, - "beacon_model_temp": float, - "beacon_model_name": str -} +RATOS_MESH_KIND_MEASURED = "measured" +# - a regular, uncorrected bed mesh +RATOS_MESH_KIND_COMPENSATION = "compensation" +# - can be used to compensate a proximity mesh to account for the proximity/contact difference. +RATOS_MESH_KIND_COMPENSATED = "compensated" +# - a compensated mesh. A measured proximity mesh that was compensated with a compensation mesh. +RATOS_MESH_KIND_CHOICES = (RATOS_MESH_KIND_MEASURED, RATOS_MESH_KIND_COMPENSATION, RATOS_MESH_KIND_COMPENSATED) + +RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY = "proximity" +# - rapid scan +RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC = "proximity_automatic" +# - stop and sample (with diving if needed) +RATOS_MESH_BEACON_PROBE_METHOD_CONTACT = "contact" +RATOS_MESH_BEACON_PROBE_METHOD_CHOICES = (RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC, RATOS_MESH_BEACON_PROBE_METHOD_CONTACT) + +RATOS_MESH_VERSION_PARAMETER = "ratos_mesh_version" +# - versioning of the extra metadata attached to meshes by ratos +RATOS_MESH_BED_TEMP_PARAMETER = "ratos_bed_temp" +# - the prevailing target bed temp when the mesh was created. For a compensated mesh, it's the +# target bed temp of the source measured mesh. +RATOS_MESH_KIND_PARAMETER = "ratos_mesh_kind" +RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER = "ratos_beacon_probe_method" +# - for measured meshes, it's the probe method of measurement +# - for compensation meshes, it's the probe method of the proximity mesh used to make the compensation mesh +# - for compensated meshes, it's the probe method of the measured mesh that was then compensated + +RATOS_MESH_PARAMETERS = ( + RATOS_MESH_VERSION_PARAMETER, + RATOS_MESH_BED_TEMP_PARAMETER, + RATOS_MESH_KIND_PARAMETER, + RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER) ##### # Beacon Mesh ##### class BeaconMesh: - + bed_temp_warning_margin = 15 + + @staticmethod + def format_pretty_list(items, conjunction="or"): + if len(items) == 0: + return "" + elif len(items) == 1: + return items[0] + else: + return ", ".join(items[:-1]) + f" {conjunction} " + items[-1] + ##### # Initialize ##### @@ -36,6 +72,10 @@ def __init__(self, config): self.ratos = None self.gm_ratos = None self.bed_mesh = None + self.heater_bed = None + self.heaters = None + self.z_tilt = None + self.qgl = None self.offset_mesh = None self.offset_mesh_points = [[]] @@ -55,18 +95,25 @@ def _connect(self): self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') if self.config.has_section("bed_mesh"): self.bed_mesh = self.printer.lookup_object('bed_mesh') - # Load additional RatOS mesh params - self.load_extra_mesh_params() - # run klippers inompatible profile check which is never called by bed_mesh - self.bed_mesh.pmgr._check_incompatible_profiles() if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') + if self.config.has_section("heater_bed"): + self.heater_bed = self.printer.lookup_object('heater_bed') + if self.config.has_section("z_tilt"): + self.z_tilt = self.printer.lookup_object('z_tilt') + if self.config.has_section("quad_gantry_level"): + self.qgl = self.printer.lookup_object('quad_gantry_level') + + self.heaters = self.printer.lookup_object('heaters', None) ##### # Gcode commands ##### def register_commands(self): if self.config.has_section("beacon"): + self.gcode.register_command('_BEACON_MESH_INIT', + self.cmd_BEACON_MESH_INIT, + desc=(self.desc_BEACON_MESH_INIT)) self.gcode.register_command('BEACON_APPLY_SCAN_COMPENSATION', self.cmd_BEACON_APPLY_SCAN_COMPENSATION, desc=(self.desc_BEACON_APPLY_SCAN_COMPENSATION)) @@ -79,8 +126,90 @@ def register_commands(self): self.gcode.register_command('_CHECK_ACTIVE_BEACON_MODEL_TEMP', self.cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP, desc=(self.desc_CHECK_ACTIVE_BEACON_MODEL_TEMP)) + self.gcode.register_command('_VALIDATE_COMPENSATION_MESH_PROFILE', + self.cmd_VALIDATE_COMPENSATION_MESH_PROFILE, + desc=(self.desc_VALIDATE_COMPENSATION_MESH_PROFILE)) + self.gcode.register_command('_APPLY_RATOS_BED_MESH_PARAMETERS', + self.cmd_APPLY_RATOS_BED_MESH_PARAMETERS, + desc=(self.desc_APPLY_RATOS_BED_MESH_PARAMETERS)) + self.gcode.register_command('GET_RATOS_EXTENDED_BED_MESH_PARAMETERS', + self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, + desc=(self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS)) + + desc_BEACON_MESH_INIT = "Performs Beacon mesh initialization tasks" + def cmd_BEACON_MESH_INIT(self, gcmd): + # Note: we don't do these things in _connect as console logging would not be visible + if self.bed_mesh: + # Load additional RatOS mesh params + self.load_extra_mesh_params() + # run klippers inompatible profile check which is never called by bed_mesh + self.bed_mesh.pmgr._check_incompatible_profiles() + + desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS = "Writes the extended RatOS bed mesh parameters to console for the active bed mesh" + def cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS(self, gcmd): + if self.bed_mesh is None: + gcmd.respond_info("The [bed_mesh] component is not active") + return + + mesh = self.bed_mesh.get_mesh() + if mesh is None: + gcmd.respond_info("There is no active bed mesh") + return - desc_CHECK_ACTIVE_BEACON_MODEL_TEMP = "Warns if the active Beacon model temperature is far from the current Beacon coil temperature." + params = collections.OrderedDict({k: v for k,v in mesh.get_mesh_params().items() if str(k).startswith("ratos_")}) + if len(params) == 0: + gcmd.respond_info('No extended RatOS bed mesh parameters found') + else: + gcmd.respond_info('\n'.join(f"{key}: {value}" for key, value in params.items())) + + desc_APPLY_RATOS_BED_MESH_PARAMETERS = "Applies RatOS extended Beacon bed mesh parameters immediately following BED_MESH_CALIBRATE" + def cmd_APPLY_RATOS_BED_MESH_PARAMETERS(self, gcmd): + # This should only be called by our override of BED_MESH_CALIBRATE immediately after the call to the original + # macro, and with the same rawargs as passed to BED_MESH_CALIBRATE. + + mesh = self.bed_mesh.get_mesh() + if mesh is None: + raise gcmd.error("Expected an active bed mesh, but there is none") + + # replicate beacon defaults exactly as per start of beacon.py cmd_BED_MESH_CALIBRATE: + method = gcmd.get("METHOD", "beacon").lower() + probe_method = gcmd.get( "PROBE_METHOD", self.beacon.default_probe_method ).lower() + if probe_method != "proximity": + method = "automatic" + # end of beacon defaults + + if probe_method == "proximity": + ratos_probe_method = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC if method == "automatic" else RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY + else: + ratos_probe_method = RATOS_MESH_BEACON_PROBE_METHOD_CONTACT + + bed_temp = self._get_nominal_bed_temp() + + params = mesh.get_mesh_params() + params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + params[RATOS_MESH_BED_TEMP_PARAMETER] = bed_temp + params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_MEASURED + params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = ratos_probe_method + + msg = ( + f"Setting parameters for active bed mesh '{mesh.get_profile_name()}':_N_" + f"{RATOS_MESH_BED_TEMP_PARAMETER}: {params[RATOS_MESH_BED_TEMP_PARAMETER]}_N_" + f"{RATOS_MESH_KIND_PARAMETER}: {params[RATOS_MESH_KIND_PARAMETER]}_N_" + f"{RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER}: {params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]}") + + self.ratos.debug_echo("_APPLY_RATOS_BED_MESH_PARAMETERS_FOR_MEASURED", msg) + + self.bed_mesh.pmgr.save_profile( mesh.get_profile_name() ) + + def _get_nominal_bed_temp(self): + target_temp = self.heater_bed.heater.target_temp if self.heater_bed else 0. + actual_temp = self.heater_bed.heater.smoothed_temp if self.heater_bed else 0. + + self.ratos.debug_echo("BeaconMesh._get_nominal_bed_temp", f"target_temp={target_temp:.2f}, actual_temp={actual_temp:.2f}") + + return round(target_temp if target_temp > 0. else actual_temp, 1) + + desc_CHECK_ACTIVE_BEACON_MODEL_TEMP = "Warns if the active Beacon model temperature is far from the current Beacon coil temperature" def cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP(self, gcmd): margin = gcmd.get_int('MARGIN', 20, minval=1) title = gcmd.get('TITLE', 'Active Beacon model temperature warning') @@ -96,28 +225,41 @@ def check_active_beacon_model_temp(self, margin=20, title='Active Beacon model t "The active Beacon model ('%s') is calibrated for a temperature that is %0.2fC different than the current Beacon coil temperature._N_" "This may result in inaccurate compensation." % (self.beacon.model.name, abs(coil_temp - model_temp))) + desc_VALIDATE_COMPENSATION_MESH_PROFILE = "Raises an error if the speficied profile is not a valid compensation mesh, and warns if there is a significant temperature difference" + def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): + + profile = gcmd.get("PROFILE").strip() + if not profile: + raise gcmd.error("Value for parameter 'PROFILE' must be specified") + + title = gcmd.get("TITLE", "Validate compensation mesh profile") + subject = gcmd.get("SUBJECT", f"Profile '{profile}'") + bed_temp = gcmd.get_float("COMPARE_BED_TEMP", None) + bed_temp_is_error = gcmd.get("COMPARE_BED_TEMP_IS_ERROR", "false").strip().lower() in ("1", "true") + + # eg, caller can use BED_TEMP=-1 when bed temp should not be checked + if bed_temp < 0: + bed_temp = None + + if not self._validate_extended_parameters( + self._get_compensation_zmesh(profile, subject).get_mesh_params(), + title, + subject, + compare_bed_temp=bed_temp, + compare_bed_temp_is_error=bed_temp_is_error, + allowed_kinds=(RATOS_MESH_KIND_COMPENSATION,)): + + raise self.printer.command_error(f"{subject} is not a valid compensation mesh profile") + desc_BEACON_APPLY_SCAN_COMPENSATION = "Compensates a beacon scan mesh with a beacon compensation mesh." def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") - profiles = self.bed_mesh.pmgr.get_profiles() - if profile not in profiles: - raise self.printer.command_error("Profile " + str(profile) + " not found for Beacon scan compensation") - - self.offset_mesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile) - - try: - self.offset_mesh.build_mesh(profiles[profile]["points"]) - except BedMesh.BedMeshError as e: - raise self.gcode.error(str(e)) - - if not self.offset_mesh: - raise self.printer.command_error("Could not load profile " + str(profile) + " for Beacon scan compensation") + if not self.apply_scan_compensation(self._get_compensation_zmesh(profile)): + raise self.printer.command_error("Could not apply scan compensation") - self.apply_scan_compensation(profile) - desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) @@ -136,7 +278,7 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): return x_pos = gcmd.get_float('X') - y_pos = gcmd.get_float('Y') + y_pos = gcmd.get_float('Y') self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f}") @@ -150,86 +292,181 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): self.ratos.console_echo("Set zero reference position", "info", f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") - ##### - # Beacon Scan Compensation - ##### - def apply_scan_compensation(self, profile): - if not self.bed_mesh.z_mesh: - self.ratos.console_echo("Apply scan compensation error", "error", - "No scan mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") - return + def _get_compensation_zmesh(self, profile, subject=None): + if not profile: + raise TypeError("Argument profile cannot be None") - if not self.offset_mesh: - self.ratos.console_echo("Apply scan compensation error", "error", - "No scan compensation mesh loaded.") - return + if subject is None: + subject = f"Profile '{profile}'" - offset_mesh_params = self.offset_mesh.get_mesh_params() + profiles = self.bed_mesh.pmgr.get_profiles() + if profile not in profiles: + raise self.printer.command_error(f"{subject} not found for Beacon scan compensation") - if "ratos_mesh_version" not in offset_mesh_params: - self.ratos.console_echo("Apply scan compensation error", "error", - "Compensation mesh is missing version information.") - return + try: + compensation_zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile) + compensation_zmesh.build_mesh(profiles[profile]["points"]) + return compensation_zmesh + except Exception as e: + raise self.printer.command_error(f"Could not load {subject[0].lower()}{subject[1:]} for Beacon scan compensation: {str(e)}") from e + + # Logs to console for any problems with extended mesh parameters. Returns True if the extended parameters are present + # and valid, otherwise False. Version must be the current version. + def _validate_extended_parameters(self, + params, + title, + subject="Mesh", + compare_bed_temp=None, + compare_bed_temp_is_error=False, + allowed_kinds=RATOS_MESH_KIND_CHOICES, + allowed_probe_methods=RATOS_MESH_BEACON_PROBE_METHOD_CHOICES ) -> bool: + + if not params: + raise TypeError("Argument params cannot be None") - if offset_mesh_params["ratos_mesh_version"] != RATOS_MESH_VERSION: - self.ratos.console_echo("Apply scan compensation error", "error", - "Compensation mesh is not compatible with this version of RatOS.") - return - - if "beacon_model_name" not in offset_mesh_params: - self.ratos.console_echo("Apply scan compensation error", "warning", - "Compensation mesh is missing beacon model information._N_" - "This may result in inaccurate compensation.") - elif offset_mesh_params["beacon_model_name"] != self.beacon.model.name: - self.ratos.console_echo("Apply scan compensation error", "warning", - "Compensation mesh is calibrated for a different beacon model than the one currently loaded._N_" - "This may result in inaccurate compensation.") + # - Earlier versions stored in config will have been migrated where possible by load_extra_mesh_params() + # - load_extra_mesh_params() will only deserialize and apply a valid config, never a partial or unmigratable config. + # - the only scenario where we should encounter a partial or invalid set of params is when they have been + # set weirdly by python code at runtime. This would either be a bug here, or some other bad actor code. + + error_title = title + " error" + warning_title = title + " warning" + + if not all(p in params for p in RATOS_MESH_PARAMETERS): + missing = [p for p in RATOS_MESH_PARAMETERS if p not in params] + self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"missing parameters: {', '.join(missing)}") + self.ratos.console_echo(error_title, "error", + f"{subject} has incomplete extended metadata.") + return False + + if params[RATOS_MESH_VERSION_PARAMETER] != RATOS_MESH_VERSION: + self.ratos.console_echo(error_title, "error", + f"{subject} is not compatible with this version of RatOS.") + return False + + if params[RATOS_MESH_KIND_PARAMETER] not in RATOS_MESH_KIND_CHOICES: + self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_KIND_PARAMETER} value '{params[RATOS_MESH_KIND_PARAMETER]}'") + self.ratos.console_echo(error_title, "error", + f"{subject} has invalid extended metadata.") + return False + + if params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] not in RATOS_MESH_BEACON_PROBE_METHOD_CHOICES: + self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER} value '{params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]}'") + self.ratos.console_echo(error_title, "error", + f"{subject} has invalid extended metadata.") + return False + + bed_temp = params[RATOS_MESH_BED_TEMP_PARAMETER] + if not isinstance(bed_temp, float): + self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BED_TEMP_PARAMETER} value type {type(params[RATOS_MESH_BED_TEMP_PARAMETER])}") + self.ratos.console_echo(error_title, "error", + f"{subject} has invalid extended metadata.") + return False + + if bed_temp < 0: + self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BED_TEMP_PARAMETER} value {bed_temp}") + self.ratos.console_echo(error_title, "error", + f"{subject} has invalid extended metadata.") + return False + + if params[RATOS_MESH_KIND_PARAMETER] not in allowed_kinds: + self.ratos.console_echo(error_title, "error", + f"{subject} must be a {self.format_pretty_list(allowed_kinds)} mesh. A {params[RATOS_MESH_KIND_PARAMETER]} mesh cannot be used.") + return False + + if params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] not in allowed_probe_methods: + self.ratos.console_echo(error_title, "error", + f"{subject} must be a {self.format_pretty_list(allowed_probe_methods)} probe method mesh. A {params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]} probe method mesh cannot be used.") + return False + + if compare_bed_temp is not None and (compare_bed_temp < bed_temp - self.bed_temp_warning_margin or compare_bed_temp > bed_temp + self.bed_temp_warning_margin): + self.ratos.console_echo( + error_title if compare_bed_temp_is_error else warning_title, + "error" if compare_bed_temp_is_error else "warning", + f"{subject} was created with a bed temperature that differs by {abs(bed_temp - compare_bed_temp)}._N_" + "This may result in innaccurate compensation.") + if compare_bed_temp_is_error: + return False + + return True - if "beacon_model_temp" not in offset_mesh_params: - self.ratos.console_echo("Apply scan compensation error", "warning", - "Compensation mesh is missing temperature calibration information._N_" - "This may result in inaccurate compensation.") - elif offset_mesh_params["beacon_model_temp"] > self.beacon.model.temp + 2.5 or offset_mesh_params["beacon_model_temp"] < self.beacon.model.temp - 2.5: - self.ratos.console_echo("Apply scan compensation error", "warning", - "Compensation mesh is calibrated for a temperature that is %0.2fC different than the one currently loaded._N_" - "This may result in inaccurate compensation." % (abs(offset_mesh_params["beacon_model_temp"] - self.beacon.model.temp))) + ##### + # Beacon Scan Compensation + ##### + def apply_scan_compensation(self, compensation_zmesh) -> bool: + if not compensation_zmesh: + raise TypeError("Argument compensation_zmesh cannot be None") + error_title = "Apply scan compensation error" try: - profile_name = self.bed_mesh.z_mesh.get_profile_name() - if profile_name == profile: - self.ratos.console_echo("Beacon scan compensation error", "error", - "Compensation profile name %s is the same as the scan profile name %s" % (str(profile), str(profile_name))) - return - - points = self.bed_mesh.pmgr.get_profiles()[profile_name]["points"] - params = self.bed_mesh.z_mesh.get_mesh_params() - x_step = ((params["max_x"] - params["min_x"]) / (len(points[0]) - 1)) - y_step = ((params["max_y"] - params["min_y"]) / (len(points) - 1)) + measured_zmesh = self.bed_mesh.z_mesh + + if not measured_zmesh: + self.ratos.console_echo(error_title, "error", + "No mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") + return False + + measured_mesh_params = measured_zmesh.get_mesh_params() + measured_mesh_name = measured_zmesh.get_profile_name() + + if not self._validate_extended_parameters( + measured_mesh_params, + "Apply scan compensation", + f"Loaded mesh '{measured_mesh_name}'", + allowed_kinds=(RATOS_MESH_KIND_MEASURED,), + allowed_probe_methods=(RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC)): + return False + + compensation_mesh_params = compensation_zmesh.get_mesh_params() + compensation_mesh_name = compensation_zmesh.get_profile_name() + + if not self._validate_extended_parameters( + compensation_mesh_params, + "Apply scan compensation", + f"Specified compensation mesh '{compensation_mesh_name}'", + compare_bed_temp=measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER], + allowed_kinds=(RATOS_MESH_KIND_COMPENSATION,)): + return False + + if measured_mesh_name == compensation_mesh_name: + self.ratos.console_echo(error_title, "error", + f"Compensation profile name '{compensation_mesh_name}' is the same as the scan profile name '{measured_mesh_name}'") + return False + + measured_points = self.bed_mesh.pmgr.get_profiles()[measured_mesh_name]["points"] + + x_step = ((measured_mesh_params["max_x"] - measured_mesh_params["min_x"]) / (len(measured_points[0]) - 1)) + y_step = ((measured_mesh_params["max_y"] - measured_mesh_params["min_y"]) / (len(measured_points) - 1)) new_points = [] - self.ratos.debug_echo("Beacon scan compensation", "scan mesh: %s" % (str(profile_name))) - self.ratos.debug_echo("Beacon scan compensation", "offset mesh: %s" % (str(profile))) + self.ratos.debug_echo("Beacon scan compensation", f"measured mesh: '{measured_mesh_name}'") + self.ratos.debug_echo("Beacon scan compensation", f"compensation mesh: '{compensation_mesh_name}'") - for y in range(len(points)): + for y in range(len(measured_points)): new_points.append([]) - for x in range(len(points[0])): - x_pos = params["min_x"] + x * x_step - y_pos = params["min_y"] + y * y_step - scan_z = points[y][x] - offset_z = self.offset_mesh.calc_z(x_pos, y_pos) - new_z = scan_z + offset_z - self.ratos.debug_echo("Beacon scan compensation", "scan: %0.4f offset: %0.4f new: %0.4f" % (scan_z, offset_z, new_z)) + for x in range(len(measured_points[0])): + x_pos = measured_mesh_params["min_x"] + x * x_step + y_pos = measured_mesh_params["min_y"] + y * y_step + measured_z = measured_points[y][x] + compensation_z = compensation_zmesh.calc_z(x_pos, y_pos) + new_z = measured_z + compensation_z + self.ratos.debug_echo("Beacon scan compensation", "measured: %0.4f compensation: %0.4f new: %0.4f" % (measured_z, compensation_z, new_z)) new_points[y].append(new_z) - self.bed_mesh.z_mesh.build_mesh(new_points) - self.bed_mesh.save_profile(profile_name) - self.bed_mesh.set_mesh(self.bed_mesh.z_mesh) + measured_zmesh.build_mesh(new_points) + # NB: build_mesh does not replace or mutate its params, so no need to reassign measured_mesh_params. + measured_mesh_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + self.bed_mesh.save_profile(measured_mesh_name) + self.bed_mesh.set_mesh(measured_zmesh) self.ratos.console_echo("Beacon scan compensation", "debug", - "Mesh scan profile %s compensated with contact profile %s" % (str(profile_name), str(profile))) + f"Measured mesh '{measured_mesh_name}' compensated with compensation mesh '{compensation_mesh_name}'") + + return True except BedMesh.BedMeshError as e: - self.ratos.console_echo("Beacon scan compensation error", "error", str(e)) + self.ratos.console_echo(error_title, "error", str(e)) + return False def create_compensation_mesh(self, profile, probe_count): if not self.beacon: @@ -237,6 +474,16 @@ def create_compensation_mesh(self, profile, probe_count): "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") return + if self.z_tilt and not self.z_tilt.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Z-tilt levelling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + + if self.qgl and not self.qgl.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Quad gantry levelling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' # Go to safe home @@ -283,15 +530,13 @@ def create_compensation_mesh(self, profile, probe_count): "scan: %0.4f contact: %0.4f offset: %0.4f" % (scan_z, contact_z, offset_z)) compensation_mesh_points[y].append(offset_z) - # Get the beacon model - beacon_model = self.beacon.model - # Create new mesh params = self.bed_mesh.z_mesh.get_mesh_params() - params["ratos_mesh_version"] = RATOS_MESH_VERSION - params["beacon_model_temp"] = beacon_model.temp - params["beacon_model_name"] = beacon_model.name - new_mesh = BedMesh.ZMesh(self.bed_mesh.z_mesh.get_mesh_params(), profile) + params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() + params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC + new_mesh = BedMesh.ZMesh(params, profile) new_mesh.build_mesh(compensation_mesh_points) self.bed_mesh.set_mesh(new_mesh) self.bed_mesh.save_profile(profile) @@ -306,7 +551,6 @@ def create_compensation_mesh(self, profile, probe_count): def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() - modified_profiles = {} for profile_name in profiles.keys(): profile = profiles[profile_name] @@ -320,38 +564,32 @@ def load_extra_mesh_params(self): except Exception: # Skip if no config section exists for this profile continue - - # Load the extra parameters from config - for key, t in RATOS_MESH_PROFILE_OPTIONS.items(): - try: - if t is int: - profile_params[key] = config.getint(key) - elif t is float: - profile_params[key] = config.getfloat(key) - elif t is str: - profile_params[key] = config.get(key) - except Exception: - # Skip parameters that don't exist in the config - self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) - continue + + version = config.getint(RATOS_MESH_VERSION_PARAMETER, None) - # Check for version compatibility - try: - ratos_mesh_version = profile_params.get('ratos_mesh_version') - if ratos_mesh_version is not None and ratos_mesh_version != RATOS_MESH_VERSION: - logging.info( - "beacon_mesh: Profile [%s] is not compatible with this version of RatOS.\n" - "Profile Version: %d\nCurrent Version: %d " - % (profile_name, ratos_mesh_version, RATOS_MESH_VERSION)) + if version == 1: + try: + mesh_kind = config.getchoice(RATOS_MESH_KIND_PARAMETER, list(RATOS_MESH_KIND_CHOICES)) + mesh_probe_method = config.getchoice(RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER, list(RATOS_MESH_BEACON_PROBE_METHOD_CHOICES)) + mesh_bed_temp = config.getfloat(RATOS_MESH_BED_TEMP_PARAMETER) + except config.error as ex: + self.ratos.console_echo("RatOS Beacon bed mesh management", "error", + f"Bed mesh profile '{profile_name}' configuration is invalid: {str(ex)}") self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) continue - except Exception: - # If we can't determine version compatibility, continue anyway - pass - modified_profiles[profile_name] = profile - - return modified_profiles + profile_params[RATOS_MESH_VERSION_PARAMETER] = version + profile_params[RATOS_MESH_KIND_PARAMETER] = mesh_kind + profile_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = mesh_probe_method + profile_params[RATOS_MESH_BED_TEMP_PARAMETER] = mesh_bed_temp + else: + self.ratos.console_echo("RatOS Beacon bed mesh management", "warning", + f"Bed mesh profile '{profile_name}' was created without extended RatOS Beacon bed mesh support." + if version is None else + f"Bed mesh profile '{profile_name}' has version {version} which is not compatible with this version of RatOS.") + self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) + continue + ##### # Loader diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 653f92651..b768b8315 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -351,7 +351,7 @@ gcode: _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL BEACON_CONTACT_START_PRINT_TRUE_ZERO={beacon_contact_start_print_true_zero} BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT={beacon_contact_calibrate_model_on_print} {% endif %} - _START_PRINT_PREFLIGHT_CHECK_BED_MESH + _START_PRINT_PREFLIGHT_CHECK_BED_MESH BED_TEMP={bed_temp} # echo first print coordinates RATOS_ECHO MSG="First print coordinates X:{first_x} Y:{first_y}" diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 2db047819..281e52729 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -36,7 +36,7 @@ gcode: gcode: # Error early if the scan compensation mesh is required but does not exist {% if printer.configfile.settings.beacon is defined %} - _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION + _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION {rawparams} {% endif %} [gcode_macro _START_PRINT_BED_MESH] diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index fb25403cc..d4e68c6b0 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -59,17 +59,22 @@ variable_beacon_scan_compensation_enable: False # Enables beacon scan co variable_beacon_scan_compensation_profile: "Beacon Scan Compensation" # The bed mesh profile name for the scan compensation mesh variable_beacon_scan_compensation_resolution: 40 # The mesh resolution in mm for scan compensation +variable_beacon_scan_compensation_bed_temp_mismatch_is_error: False + # If True, attempting to use a compensation mesh calibrated for a significantly + # different bed temperature will raise an error. Otherwise, a warning is reported. variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the contact poke test variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option - ##### # BEACON COMMON ##### [delayed_gcode _BEACON_INIT] initial_duration: 1 gcode: + # Initialize mesh management + _BEACON_MESH_INIT + # reset nozzle thermal expansion offset _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True @@ -974,14 +979,19 @@ gcode: gcode: {% set beacon_scan_compensation_profile = printer["gcode_macro RatOS"].beacon_scan_compensation_profile %} {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} + {% set bed_temp_mismatch_is_error = true if printer["gcode_macro RatOS"].beacon_scan_compensation_bed_temp_mismatch_is_error|default(false)|lower == 'true' else false %} - DEBUG_ECHO PREFIX="_START_PRINT_PREFLIGHT_CHECK_BEACON" MSG="beacon_scan_compensation_profile {beacon_scan_compensation_profile}, beacon_scan_compensation_enable {beacon_scan_compensation_enable}" + {% set bed_temp = params.BED_TEMP|default(-1, true)|float %} + {% if bed_temp == 0 %} + {% set bed_temp = printer.heater_bed.temperature %} + {% endif %} + + DEBUG_ECHO PREFIX="_START_PRINT_PREFLIGHT_CHECK_BEACON" MSG="beacon_scan_compensation_profile={beacon_scan_compensation_profile}, beacon_scan_compensation_enable={beacon_scan_compensation_enable}, bed_temp={bed_temp}" {% if beacon_scan_compensation_enable %} - _CHECK_BED_MESH_PROFILE_EXISTS PROFILE="{beacon_scan_compensation_profile}" MSG="Beacon scan compensation bed mesh profile '%s' not found" + _VALIDATE_COMPENSATION_MESH_PROFILE PROFILE="{beacon_scan_compensation_profile}" TITLE="Check Beacon scan compensation profile" SUBJECT="Configured compensation profile '{beacon_scan_compensation_profile}'" COMPARE_BED_TEMP={bed_temp} COMPARE_BED_TEMP_IS_ERROR={bed_temp_mismatch_is_error} {% endif %} - [gcode_macro _BEACON_MAYBE_SCAN_COMPENSATION] gcode: # parameters @@ -1126,7 +1136,7 @@ gcode: BEACON_QUERY [gcode_macro BED_MESH_CALIBRATE] -rename_existing: _BED_MESH_CALIBRATE_ORIG_RAT_BEACON +rename_existing: _BED_MESH_CALIBRATE_BASE gcode: {% if printer.configfile.settings.beacon is defined %} {% set beacon_default_probe_method = printer.configfile.settings.beacon.default_probe_method|default('proximity') %} @@ -1135,4 +1145,5 @@ gcode: _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" {% endif %} {% endif %} - _BED_MESH_CALIBRATE_ORIG_RAT_BEACON {rawparams} \ No newline at end of file + _BED_MESH_CALIBRATE_BASE {rawparams} + _APPLY_RATOS_BED_MESH_PARAMETERS {rawparams} \ No newline at end of file From c9e808e41a99853336aef1b11371a40434b99c1c Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Fri, 14 Mar 2025 13:51:21 +0100 Subject: [PATCH 022/139] Mesh/Beacon: remove runtime multiplier, add runtime offset --- configuration/macros/overrides.cfg | 9 --- configuration/macros/util.cfg | 10 ---- configuration/z-probe/beacon.cfg | 90 ++++++------------------------ 3 files changed, 18 insertions(+), 91 deletions(-) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index aa5388822..1380eb4f3 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -217,15 +217,6 @@ gcode: RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for toolhead T{t} reached." -[gcode_macro SET_GCODE_OFFSET] -rename_existing: SET_GCODE_OFFSET_ORG -gcode: - SET_GCODE_OFFSET_ORG { rawparams } - {% if printer.configfile.settings.beacon is defined and (params.Z_ADJUST is defined or params.Z is defined) %} - _BEACON_APPLY_RUNTIME_MULTIPLIER - {% endif %} - - [gcode_macro SDCARD_PRINT_FILE] rename_existing: SDCARD_PRINT_FILE_BASE gcode: diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index 303fc26e0..6f20d55a8 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -375,16 +375,6 @@ gcode: gcode: M118 Click SAVE_CONFIG to save the settings to your printer.cfg. - -[gcode_macro SAVE_Z_OFFSET] -gcode: - {% if printer.configfile.settings.beacon is defined %} - _BEACON_SAVE_MULTIPLIER - {% else %} - Z_OFFSET_APPLY_PROBE - {% endif %} - - [gcode_macro _LOAD_RATOS_SKEW_PROFILE] gcode: {% set ratos_skew_profile = printer["gcode_macro RatOS"].skew_profile|default("") %} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index d4e68c6b0..6913ac609 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -41,8 +41,8 @@ variable_beacon_contact_wipe_before_true_zero: True # enables a nozzle wipe variable_beacon_contact_true_zero_temp: 150 # nozzle temperature for true zeroing # WARNING: if you're using a smooth PEI sheet, be careful with the temperature -variable_beacon_contact_calibrate_model_on_print: False # Calibrate a new beacon model every print, it's recommended to enable this - # if you're often swapping build plates with different surface types. +variable_beacon_contact_calibrate_model_on_print: True # Calibrate a new beacon model every print, it's recommended to enable this + # especially if you're often swapping build plates with different surface types. # NOTE: this effectively disables z_offset on the beacon model, since a new one # will be calibrated every print, however True Zero replaces the model offset anyway. @@ -1048,82 +1048,28 @@ gcode: BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 {% endif %} -[gcode_macro _BEACON_SAVE_MULTIPLIER] +[gcode_macro _BEACON_SET_RUNTIME_OFFSET] gcode: - # parameters - {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% set beacon_contact_expansion_compensation = true if printer["gcode_macro RatOS"].beacon_contact_expansion_compensation|default(false)|lower == 'true' else false %} - {% set multiplier = printer["gcode_macro _BEACON_APPLY_RUNTIME_MULTIPLIER"].runtime_multiplier|default(-1.0)|float %} - - DEBUG_ECHO PREFIX="_BEACON_SAVE_MULTIPLIER" MSG="multiplier: {multiplier}, beacon_contact_start_print_true_zero: {beacon_contact_start_print_true_zero}, beacon_contact_expansion_compensation: {beacon_contact_expansion_compensation}" + # Get the current gcode_offset, call it new_runtime_offset + {% set new_runtime_offset = printer.gcode_move.homing_origin.z %} + # Get applied expansion offset + {% set applied_expansion_offset = printer.save_variables.variables.nozzle_expansion_applied_offset|default(0)|float %} + # Subtract the applied expansion compensation offset, this needs to be calculated from the + {% set new_runtime_offset = new_runtime_offset - applied_expansion_offset %} - {% if multiplier > 0 and beacon_contact_start_print_true_zero and beacon_contact_expansion_compensation %} - SAVE_VARIABLE VARIABLE=nozzle_expansion_coefficient_multiplier VALUE={multiplier} - SET_GCODE_VARIABLE MACRO=_BEACON_APPLY_RUNTIME_MULTIPLIER VARIABLE=runtime_multiplier VALUE=-1.0 - CONSOLE_ECHO TITLE="Hotend thermal expansion compensation" TYPE="success" MSG={'"New value is: %.6f_N_The new multiplier value has been saved to the configuration."' % multiplier} - {% else %} - Z_OFFSET_APPLY_PROBE - {% endif %} + # Save the result as beacon_saved_runtime_offset via SAVE_VARIABLE + SAVE_VARIABLE VARIABLE=beacon_saved_runtime_offset VALUE={new_runtime_offset} + DEBUG_ECHO PREFIX="_BEACON_SET_RUNTIME_OFFSET" MSG="Saved new runtime_offset: {new_runtime_offset}, current z_offset: {printer.gcode_move.homing_origin.z}, applied expansion offset: {applied_expansion_offset}" -[gcode_macro _BEACON_APPLY_RUNTIME_MULTIPLIER] -variable_runtime_multiplier: -1.0 +[gcode_macro _BEACON_RESTORE_RUNTIME_OFFSET] gcode: - # config - {% set toolhead = 0 %} - {% if printer["dual_carriage"] is defined %} - {% set idex_mode = printer["dual_carriage"].carriage_1|lower %} - {% set toolhead = 1 if idex_mode == 'primary' else 0 %} - {% endif %} - - # beacon config - {% set beacon_contact_true_zero_temp = printer["gcode_macro RatOS"].beacon_contact_true_zero_temp|default(150)|int %} - {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% set beacon_contact_expansion_compensation = true if printer["gcode_macro RatOS"].beacon_contact_expansion_compensation|default(false)|lower == 'true' else false %} - - # get current layer number - {% set layer_number = printer["gcode_macro _ON_LAYER_CHANGE"].layer_number|default(0)|int %} + # Get the current GCODE_OFFSET, call it NEW_RUNTIME_OFFSET + {% set runtime_offset = printer.save_variables.variables.beacon_saved_runtime_offset|default(0) %} + {% set offset_before = printer.gcode_move.homing_origin.z %} + SET_GCODE_OFFSET Z_ADJUST={runtime_offset} - # get is printing gcode state - {% set is_printing_gcode = true if printer["gcode_macro START_PRINT"].is_printing_gcode|default(true)|lower == 'true' else false %} - - {% if layer_number == 0 and is_printing_gcode %} - {% set link_url = "https://os.ratrig.com/docs/slicers" %} - {% set link_text = "RatOS Slicer Documentation" %} - {% set line_1 = '"Your slicer is not correctly reporting layer information. See the layer change custom g-code in the %s".' % (link_url, link_text) %} - CONSOLE_ECHO TITLE="Missing layer information" TYPE="warning" MSG={line_1} - {% endif %} - - DEBUG_ECHO PREFIX="_BEACON_APPLY_RUNTIME_MULTIPLIER" MSG="layer_number: {layer_number}, is_printing_gcode: {is_printing_gcode}, beacon_contact_start_print_true_zero: {beacon_contact_start_print_true_zero}, beacon_contact_expansion_compensation: {beacon_contact_expansion_compensation}" - - {% if layer_number == 1 and is_printing_gcode and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero and beacon_contact_expansion_compensation %} - - # ratos variables file - {% set svv = printer.save_variables.variables %} - - # get coefficient - {% set nozzle_expansion_coefficient = svv.nozzle_expansion_coefficient_t0|default(0)|float %} - {% if toolhead == 1 %} - {% set nozzle_expansion_coefficient = svv.nozzle_expansion_coefficient_t1|default(0)|float %} - {% endif %} - - # calculate new multiplier - {% set beacon_contact_expansion_multiplier = svv.nozzle_expansion_coefficient_multiplier|default(1.0)|float %} - {% set print_temp = printer["gcode_macro _BEACON_SET_NOZZLE_TEMP_OFFSET"].runtime_temp|default(0)|int %} - - {% if print_temp > 0 %} - {% set z_offset = printer.gcode_move.homing_origin.z|float %} - {% set temp_delta = print_temp - beacon_contact_true_zero_temp %} - {% set coefficient_per_degree = nozzle_expansion_coefficient / 100 %} - {% set z_offset_per_degree = z_offset / temp_delta %} - {% set new_multiplier = z_offset_per_degree / coefficient_per_degree %} - - DEBUG_ECHO PREFIX="_BEACON_APPLY_RUNTIME_MULTIPLIER" MSG="print_temp: {print_temp}, z_offset: {z_offset}, temp_delta: {temp_delta}, nozzle_expansion_coefficient: {nozzle_expansion_coefficient}, coefficient_per_degree: {coefficient_per_degree}, z_offset_per_degree: {z_offset_per_degree}, old_multiplier: {beacon_contact_expansion_multiplier}, new_multiplier: {new_multiplier}" - SET_GCODE_VARIABLE MACRO=_BEACON_APPLY_RUNTIME_MULTIPLIER VARIABLE=runtime_multiplier VALUE={new_multiplier} - - {% endif %} - - {% endif %} + DEBUG_ECHO PREFIX="_BEACON_RESTORE_RUNTIME_OFFSET" MSG="runtime_offset restored: {runtime_offset}, z offset before: {offset_before}, z offset after: {printer.gcode_move.homing_origin.z}" [gcode_macro _BEACON_OFFSET_COMPARE] From 25ef320ac76b99e0ae51da3975e63032e7a1045c Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Fri, 14 Mar 2025 16:12:47 +0100 Subject: [PATCH 023/139] Beacon: remove obsolete comment --- configuration/z-probe/beacon.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 6913ac609..d2234dad0 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1064,7 +1064,6 @@ gcode: [gcode_macro _BEACON_RESTORE_RUNTIME_OFFSET] gcode: - # Get the current GCODE_OFFSET, call it NEW_RUNTIME_OFFSET {% set runtime_offset = printer.save_variables.variables.beacon_saved_runtime_offset|default(0) %} {% set offset_before = printer.gcode_move.homing_origin.z %} SET_GCODE_OFFSET Z_ADJUST={runtime_offset} From 4f0df534160d4420f582ac8e096b255d302a51df Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Fri, 14 Mar 2025 16:36:29 +0100 Subject: [PATCH 024/139] Macros/Beacon: properly apply/subtract runtime/expansion offsets --- configuration/macros.cfg | 6 ++++-- configuration/z-probe/beacon.cfg | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index b768b8315..977bb9596 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -770,12 +770,14 @@ gcode: {% endif %} {% endif %} - # set nozzle thermal expansion offset + # set nozzle thermal expansion offset and restore runtime offset {% if printer.configfile.settings.beacon is defined %} # the previously called restore gcode state removed the temp offset # we need first to reset the applied offset value in the variables file _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={initial_tool} + # Restore the beacon runtime offset. + _BEACON_RESTORE_RUNTIME_OFFSET {% endif %} # Set extrusion mode based on user configuration @@ -1261,7 +1263,7 @@ gcode: {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_expansion_compensation = true if printer["gcode_macro RatOS"].beacon_contact_expansion_compensation|default(false)|lower == 'true' else false %} {% if beacon_contact_start_print_true_zero and beacon_contact_expansion_compensation %} - SET_GCODE_OFFSET Z=0 MOVE=0 + _BEACON_SUBTRACT_CONTACT_EXPANSION_OFFSET {% endif %} {% endif %} _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index d2234dad0..94e181d5f 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1048,6 +1048,11 @@ gcode: BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 {% endif %} +[gcode_macro _BEACON_SUBTRACT_CONTACT_EXPANSION_OFFSET] +gcode: + {% set applied_expansion_offset = printer.save_variables.variables.nozzle_expansion_applied_offset|default(0)|float %} + SET_GCODE_OFFSET Z_ADJUST={applied_expansion_offset * -1} + [gcode_macro _BEACON_SET_RUNTIME_OFFSET] gcode: # Get the current gcode_offset, call it new_runtime_offset From 4d6623ecb1c46f9517107888cd2e608a195a352c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Apr 2025 18:43:54 +0100 Subject: [PATCH 025/139] Macros/Beacon: fix saving of runtime z offset (babystepping): - Support saving via the standard Z_OFFSET_APPLY_PROBE macro - Exclude active IDEX toolhead offset from the saved value --- configuration/macros.cfg | 6 ++-- configuration/z-probe/beacon.cfg | 47 +++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 977bb9596..b0810a95a 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -776,8 +776,10 @@ gcode: # we need first to reset the applied offset value in the variables file _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={initial_tool} - # Restore the beacon runtime offset. - _BEACON_RESTORE_RUNTIME_OFFSET + # If we're using beacon contact true zero, restore the runtime z offset. + {% if beacon_contact_start_print_true_zero %} + _BEACON_RESTORE_RUNTIME_OFFSET + {% endif %} {% endif %} # Set extrusion mode based on user configuration diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 94e181d5f..6e68daf21 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1055,17 +1055,47 @@ gcode: [gcode_macro _BEACON_SET_RUNTIME_OFFSET] gcode: - # Get the current gcode_offset, call it new_runtime_offset - {% set new_runtime_offset = printer.gcode_move.homing_origin.z %} + {% set svv = printer.save_variables.variables %} + # Get the current gcode_offset + {% set current_gcode_offset = printer.gcode_move.homing_origin.z %} # Get applied expansion offset - {% set applied_expansion_offset = printer.save_variables.variables.nozzle_expansion_applied_offset|default(0)|float %} - # Subtract the applied expansion compensation offset, this needs to be calculated from the - {% set new_runtime_offset = new_runtime_offset - applied_expansion_offset %} + {% set applied_expansion_offset = svv.nozzle_expansion_applied_offset|default(0)|float %} + # Subtract the applied expansion compensation offset from the current gcode offset. + {% set new_runtime_offset = current_gcode_offset - applied_expansion_offset %} + {% set active_toolhead_z_offset = 'n/a' %} + # Subtract idex tool z offset if applicable + {% if printer["dual_carriage"] is defined %} + {% set active_toolhead_z_offset = 0 %} + {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|int %} + {% set active_toolhead_index = svv.idex_applied_offset|int %} + {% if active_toolhead_index != default_toolhead %} + {% set active_toolhead_z_offset = svv.idex_zoffset|float %} + # _SET_TOOLHEAD_OFFSET applies negative svv.idex_zoffset when the non-default toolhead is selected, + # so we *add* it to exclude its effect from new_runtime_offset: + {% set new_runtime_offset = new_runtime_offset + active_toolhead_z_offset %} + {% endif %} + {% endif %} + # What remains is the user's explicit z babystepping (at least accounting for the other factors we know about...) + # Note: any z offset set in the printing gcode file will also be included - we can't tell the difference between + # babystepping and gcode file offset. Users should not use gcode file offsets at the same time as adjusting + # and saving babystepping. + # Save the result as beacon_saved_runtime_offset via SAVE_VARIABLE SAVE_VARIABLE VARIABLE=beacon_saved_runtime_offset VALUE={new_runtime_offset} - DEBUG_ECHO PREFIX="_BEACON_SET_RUNTIME_OFFSET" MSG="Saved new runtime_offset: {new_runtime_offset}, current z_offset: {printer.gcode_move.homing_origin.z}, applied expansion offset: {applied_expansion_offset}" + DEBUG_ECHO PREFIX="_BEACON_SET_RUNTIME_OFFSET" MSG="Saved new runtime_offset: {new_runtime_offset}, current z_offset: {printer.gcode_move.homing_origin.z}, applied expansion offset: {applied_expansion_offset}, active IDEX toolhead offset: {active_toolhead_z_offset}" + +[gcode_macro Z_OFFSET_APPLY_PROBE] +rename_existing: _Z_OFFSET_APPLY_PROBE_BASE +gcode: + {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} + # If we're using contact true zero, use RatOS offset management; otherwise, delegate to base + {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + _BEACON_SET_RUNTIME_OFFSET {rawparams} + {% else %} + _Z_OFFSET_APPLY_PROBE_BASE {rawparams} + {% endif %} [gcode_macro _BEACON_RESTORE_RUNTIME_OFFSET] gcode: @@ -1073,8 +1103,11 @@ gcode: {% set offset_before = printer.gcode_move.homing_origin.z %} SET_GCODE_OFFSET Z_ADJUST={runtime_offset} - DEBUG_ECHO PREFIX="_BEACON_RESTORE_RUNTIME_OFFSET" MSG="runtime_offset restored: {runtime_offset}, z offset before: {offset_before}, z offset after: {printer.gcode_move.homing_origin.z}" + _BEACON_RESTORE_RUNTIME_OFFSET_LOG_BEFORE_AFTER RUNTIME_OFFSET={runtime_offset} OFFSET_BEFORE={offset_before} +[gcode_macro _BEACON_RESTORE_RUNTIME_OFFSET_LOG_BEFORE_AFTER] +gcode: + DEBUG_ECHO PREFIX="_BEACON_RESTORE_RUNTIME_OFFSET" MSG="runtime_offset restored: {params.RUNTIME_OFFSET}, z offset before: {params.OFFSET_BEFORE}, z offset after: {printer.gcode_move.homing_origin.z}" [gcode_macro _BEACON_OFFSET_COMPARE] gcode: From 0465a29ad39dab78289ef3e179667e94c16b78c9 Mon Sep 17 00:00:00 2001 From: Mikkel Schmidt Date: Sun, 16 Mar 2025 21:48:13 +0100 Subject: [PATCH 026/139] Macros/Beacon: update hotend expansion offset when print temperature changes --- configuration/macros/overrides.cfg | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index 1380eb4f3..d6b8fdd63 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -100,6 +100,12 @@ gcode: {% endif %} {% endif %} + # Update the nozzle thermal expansion offset + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} + {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} + {% endif %} + # do not call M109 from within RatOS macros # use TEMPERATURE_WAIT instead @@ -139,6 +145,11 @@ gcode: # call klipper base function {% if not is_in_standby %} M109.1 S{s} T{t} + # Update the nozzle thermal expansion offset + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} + {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} + {% endif %} {% endif %} @@ -169,6 +180,17 @@ gcode: # call klipper base function SET_HEATER_TEMPERATURE_BASE HEATER="{heater}" TARGET={target} + # Update the nozzle thermal expansion offset + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} + {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if heater|lower == "extruder" or heater|lower == "extruder1" %} + # get physical toolhead + {% set t = 0 if heater|lower == "extruder" else 1 %} + # Update the nozzle thermal expansion offset + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} + {% endif %} + {% endif %} + [gcode_macro TEMPERATURE_WAIT] rename_existing: TEMPERATURE_WAIT_BASE From 77c72aa556495f8a214f512d79204bf619fad925 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 25 Apr 2025 12:52:03 +0100 Subject: [PATCH 027/139] Beacon: minor code cleanup --- configuration/z-probe/beacon.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 6e68daf21..df008d519 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -90,7 +90,7 @@ gcode: SAVE_VARIABLE VARIABLE=nozzle_expansion_coefficient_multiplier VALUE={nozzle_expansion_coefficient_multiplier} {% endif %} {% if printer["gcode_macro RatOS"].beacon_contact_expansion_multiplier is defined %} - CONSOLE_ECHO TITLE="Deprecated gcode variable" TYPE="warning" MSG={'"Please remove the variable beacon_contact_expansion_multiplier from your config file."'} + CONSOLE_ECHO TITLE="Deprecated gcode variable" TYPE="warning" MSG="Please remove the variable beacon_contact_expansion_multiplier from your config file." {% endif %} From 29209638ac50e691e17e6c400750034ece1f6528 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 25 Apr 2025 13:21:39 +0100 Subject: [PATCH 028/139] Macros: ensure TEMPERATURE_WAIT override does not swallow error conditions - TEMPERATURE_WAIT now calls TEMPERATURE_WAIT_BASE even when both MINIMUM and MAXIMUM are not specified, allowing the base impl to raise an error --- configuration/macros/overrides.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index d6b8fdd63..c333742db 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -235,6 +235,12 @@ gcode: RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}, MAXIMUM: {maximum}" RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" MAXIMUM={maximum} + {% else %} + # Neither minimum or maximum specified: TEMPERATURE_WAIT_BASE will raise an error, but we need to let that happen + # to ensure consistent behaviour. + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}" + RATOS_ECHO MSG="please wait..." + TEMPERATURE_WAIT_BASE SENSOR="{sensor}" {% endif %} RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for toolhead T{t} reached." From 1abb257ca1cdca76ffa61a6e2b20477497c1d68c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 25 Apr 2025 13:25:40 +0100 Subject: [PATCH 029/139] Macros/Beacon: update hotend expansion offset in all relevant scenarios --- configuration/macros/overrides.cfg | 40 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index c333742db..d10faebfa 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -92,21 +92,22 @@ gcode: # call klipper base function {% if not is_in_standby %} + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} {% if idex_mode == "copy" or idex_mode == "mirror" %} M104.1 S{s0} T0 M104.1 S{s1} T1 + {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=0 + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=1 + {% endif %} {% else %} M104.1 S{s} T{t} + {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} + {% endif %} {% endif %} {% endif %} - # Update the nozzle thermal expansion offset - {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} - _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} - {% endif %} - - # do not call M109 from within RatOS macros # use TEMPERATURE_WAIT instead [gcode_macro M109] @@ -162,6 +163,7 @@ gcode: DEBUG_ECHO PREFIX="SET_HEATER_TEMPERATURE" MSG="heater: {heater}, target: {target}" + {% set t = -1 %} {% if heater|lower == "extruder" or heater|lower == "extruder1" %} # get physical toolhead {% set t = 0 if heater|lower == "extruder" else 1 %} @@ -180,18 +182,14 @@ gcode: # call klipper base function SET_HEATER_TEMPERATURE_BASE HEATER="{heater}" TARGET={target} - # Update the nozzle thermal expansion offset - {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} - {% if heater|lower == "extruder" or heater|lower == "extruder1" %} - # get physical toolhead - {% set t = 0 if heater|lower == "extruder" else 1 %} - # Update the nozzle thermal expansion offset + {% if t != -1 and printer.configfile.settings.beacon is defined %} + # Update the nozzle thermal expansion offset + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} + {% if is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} - [gcode_macro TEMPERATURE_WAIT] rename_existing: TEMPERATURE_WAIT_BASE gcode: @@ -202,6 +200,7 @@ gcode: DEBUG_ECHO PREFIX="TEMPERATURE_WAIT" MSG="sensor: {sensor}, minimum: {minimum}, maximum: {maximum}" + {% set t = -1 %} {% if sensor|lower == "extruder" or sensor|lower == "extruder1" %} # get physical toolhead {% set t = 0 if sensor|lower == "extruder" else 1 %} @@ -219,7 +218,6 @@ gcode: RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature offset of {temperature_offset}°C added to toolhead T{t}." {% endif %} {% endif %} - {% endif %} # call klipper base function @@ -242,8 +240,16 @@ gcode: RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" {% endif %} - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for toolhead T{t} reached." + {% if t != -1 and printer.configfile.settings.beacon is defined %} + # Update the nozzle thermal expansion offset + {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} + {% if is_printing_gcode %} + _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} + {% endif %} + {% endif %} + + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for toolhead T{t} reached." [gcode_macro SDCARD_PRINT_FILE] rename_existing: SDCARD_PRINT_FILE_BASE From dbe11da2467fdac33265dd07541564e1e7584e2d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 26 Apr 2025 18:28:37 +0100 Subject: [PATCH 030/139] Macros/Beacon: improve variable naming and debug logging --- configuration/z-probe/beacon.cfg | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index df008d519..c4bf702aa 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -600,6 +600,8 @@ gcode: {% else %} {% if beacon_contact_start_print_true_zero and beacon_contact_expansion_compensation %} + + {% set offset_before = printer.gcode_move.homing_origin.z %} # get coefficient {% set nozzle_expansion_coefficient_t0 = svv.nozzle_expansion_coefficient_t0|default(0)|float %} @@ -611,29 +613,33 @@ gcode: {% set nozzle_expansion_coefficient_multiplier = svv.nozzle_expansion_coefficient_multiplier|default(1.0)|float %} # get applied offset - {% set applied_offset = svv.nozzle_expansion_applied_offset|default(0)|float %} + {% set existing_expansion_offset = svv.nozzle_expansion_applied_offset|default(0)|float %} # get extruder target temperature {% set temp = printer['extruder' if toolhead == 0 else 'extruder1'].target|float %} # calculate new offset - {% set temp_offset = temp - beacon_contact_true_zero_temp %} + {% set temp_delta = temp - beacon_contact_true_zero_temp %} {% set expansion_coefficient = nozzle_expansion_coefficient_t0 if toolhead == 0 else nozzle_expansion_coefficient_t1 %} - {% set expansion_offset = nozzle_expansion_coefficient_multiplier * (temp_offset * (expansion_coefficient / 100)) %} + {% set required_expansion_offset = nozzle_expansion_coefficient_multiplier * (temp_delta * (expansion_coefficient / 100)) %} # set new offset - {% set new_offset = ((-applied_offset) + expansion_offset) %} - SET_GCODE_OFFSET Z_ADJUST={new_offset} MOVE=1 SPEED={z_speed} - SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE={expansion_offset} + {% set required_offset_adjustment = required_expansion_offset - existing_expansion_offset %} + SET_GCODE_OFFSET Z_ADJUST={required_offset_adjustment} MOVE=1 SPEED={z_speed} + SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE={required_expansion_offset} SET_GCODE_VARIABLE MACRO=_BEACON_SET_NOZZLE_TEMP_OFFSET VARIABLE=runtime_temp VALUE={temp} # echo - RATOS_ECHO PREFIX="BEACON" MSG={'"Nozzle expansion offset of %.6fmm applied to T%s"' % (expansion_offset, toolhead)} - DEBUG_ECHO PREFIX="_BEACON_SET_NOZZLE_TEMP_OFFSET" MSG="multiplier: {nozzle_expansion_coefficient_multiplier}, coefficient: {expansion_coefficient}, temp_offset: {temp_offset}, expansion_offset: {expansion_offset}, applied_offset: {applied_offset}, new_offset: {new_offset}" + RATOS_ECHO PREFIX="BEACON" MSG={'"Nozzle expansion offset of %.6fmm applied to T%s"' % (required_expansion_offset, toolhead)} + _BEACON_SET_NOZZLE_TEMP_OFFSET_LOG_DEBUG MSG="toolhead: {toolhead}, multiplier: {nozzle_expansion_coefficient_multiplier|round(4)}, coefficient: {expansion_coefficient|round(6)}, temp_delta: {temp_delta|round(1)}, existing_expansion_offset: {existing_expansion_offset|round(6)}, required_expansion_offset: {required_expansion_offset|round(6)}, required_offset_adjustment: {required_offset_adjustment|round(6)}, offset_before: {offset_before|round(6)}" {% endif %} {% endif %} +[gcode_macro _BEACON_SET_NOZZLE_TEMP_OFFSET_LOG_DEBUG] +gcode: + {% set offset_after = printer.gcode_move.homing_origin.z %} + DEBUG_ECHO PREFIX="_BEACON_SET_NOZZLE_TEMP_OFFSET" MSG="{params.MSG}, offset_after: {offset_after|round(6)}" ##### # BEACON MEASURE BEACON OFFSET From 14e51e0dac23eae61361d990ffb1e0538cb93b40 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 26 Apr 2025 18:29:20 +0100 Subject: [PATCH 031/139] Macros/Toolheads: improve debug logging --- configuration/macros/idex/toolheads.cfg | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/configuration/macros/idex/toolheads.cfg b/configuration/macros/idex/toolheads.cfg index bdfc969b5..e87a18e4b 100644 --- a/configuration/macros/idex/toolheads.cfg +++ b/configuration/macros/idex/toolheads.cfg @@ -788,7 +788,8 @@ gcode: gcode: # parameters {% set t = params.T|int %} - {% set move = params.MOVE|default(0)|int %} + {% set move = params.MOVE|default(0)|int %} + {% set offset_before = printer.gcode_move.homing_origin %} # echo DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="T: {t}, MOVE: {move}" @@ -807,16 +808,17 @@ gcode: {% endif %} {% if svv.idex_applied_offset != t %} {% if t != printer["gcode_macro RatOS"].default_toolhead|int %} - DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET X_ADJUST: {(-svv.idex_xoffset)} Y_ADJUST: {(-svv.idex_yoffset)} MOVE: {move} SPEED: {speed}" + DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET X_ADJUST: {(-svv.idex_xoffset)|round(6)} Y_ADJUST: {(-svv.idex_yoffset)|round(6)} MOVE: {move} SPEED: {speed}" SET_GCODE_OFFSET X_ADJUST={(-svv.idex_xoffset)} Y_ADJUST={(-svv.idex_yoffset)} MOVE={move} SPEED={speed} - DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET Z_ADJUST: {(-svv.idex_zoffset)} MOVE: {move} SPEED: {z_speed}" + DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET Z_ADJUST: {(-svv.idex_zoffset)|round(6)} MOVE: {move} SPEED: {z_speed}" SET_GCODE_OFFSET Z_ADJUST={(-svv.idex_zoffset)} MOVE={move} SPEED={z_speed} {% else %} - DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_ X_ADJUST: {svv.idex_xoffset} Y_ADJUST: {svv.idex_yoffset} MOVE: {move} SPEED: {speed}" + DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_ X_ADJUST: {svv.idex_xoffset|round(6)} Y_ADJUST: {svv.idex_yoffset|round(6)} MOVE: {move} SPEED: {speed}" SET_GCODE_OFFSET X_ADJUST={svv.idex_xoffset} Y_ADJUST={svv.idex_yoffset} MOVE={move} SPEED={speed} - DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET Z_ADJUST: {svv.idex_zoffset} MOVE: {move} SPEED: {z_speed}" + DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="SET_GCODE_OFFSET Z_ADJUST: {svv.idex_zoffset|round(6)} MOVE: {move} SPEED: {z_speed}" SET_GCODE_OFFSET Z_ADJUST={svv.idex_zoffset} MOVE={move} SPEED={z_speed} {% endif %} + _SET_TOOLHEAD_OFFSET_LOG_DEBUG MSG="offset_before: ({offset_before.x|round(6)}, {offset_before.y|round(6)}, {offset_before.z|round(6)})" RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T{t}" SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE={t} # set nozzle thermal expansion offset @@ -825,6 +827,11 @@ gcode: {% endif %} {% endif %} +[gcode_macro _SET_TOOLHEAD_OFFSET_LOG_DEBUG] +gcode: + {% set offset_after = printer.gcode_move.homing_origin %} + DEBUG_ECHO PREFIX="_SET_TOOLHEAD_OFFSET" MSG="{params.MSG}, offset_after: ({offset_after.x|round(6)}, {offset_after.y|round(6)}, {offset_after.z|round(6)})" + [gcode_macro TOOLSHIFT_CONFIG] gcode: SET_GCODE_VARIABLE MACRO=RatOS VARIABLE=toolchange_travel_speed VALUE={params.SPEED|default(300)|int} From 5317358babf24e70bfd88b3e99b2438e6308cdbd Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 26 Apr 2025 18:34:25 +0100 Subject: [PATCH 032/139] Extras: add _DEBUG_ECHO_STACK_TRACE --- configuration/klippy/ratos.py | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 88bf78698..644756415 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,4 +1,4 @@ -import os, logging, glob +import os, logging, glob, traceback, inspect, re import json, subprocess, pathlib ##### @@ -84,6 +84,7 @@ def register_commands(self): self.gcode.register_command('_CHECK_BED_MESH_PROFILE_EXISTS', self.cmd_CHECK_BED_MESH_PROFILE_EXISTS, desc=(self.desc_CHECK_BED_MESH_PROFILE_EXISTS)) self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=(self.desc_RAISE_ERROR)) self.gcode.register_command('_TRY', self.cmd_TRY, desc=(self.desc_TRY)) + self.gcode.register_command('_DEBUG_ECHO_STACK_TRACE', self.cmd_DEBUG_ECHO_STACK_TRACE, desc=(self.desc_DEBUG_ECHO_STACK_TRACE)) def register_command_overrides(self): self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) @@ -581,6 +582,102 @@ def get_status(self, eventtime): 'last_processed_file_result': self.last_processed_file_result, 'last_check_bed_mesh_profile_exists_result': self.last_check_bed_mesh_profile_exists_result } + ##### + # Stack trace + ##### + + _rx_stack_crawl_ = re.compile(r";\$(\S+)") + desc_DEBUG_ECHO_STACK_TRACE = "Logs a gcode command stack trace when debug is enabled. Add comments to template macros formatted exactly {';$some-short-text-without-whitespace'} to enhance callsite identification." + def cmd_DEBUG_ECHO_STACK_TRACE(self, gcmd): + macro = self.printer.lookup_object('gcode_macro DEBUG_ECHO') + if macro.variables['enabled']: + def callback(frame_info): + locals = frame_info.frame.f_locals + self_obj = locals.get("self", None) + if self_obj: + if isinstance(self_obj, type(macro)): + f_gcmd = locals.get('gcmd',None) + if f_gcmd: + return (False,f" {f_gcmd.get_commandline()}") + return (False,f" {self_obj.alias}") + if type(self_obj).__name__ == 'GCodeDispatch': + f_commands = locals.get('commands', None) + f_origline = locals.get('origline', None) + if f_commands and f_origline: + def format_with_preceding_crawlmark(index): + for index2, line2 in enumerate(f_commands[index::-1]): + match = self._rx_stack_crawl_.search(line2) + if match: + return f"{match.group(1)}+{index2}" if index2 > 0 else match.group(1) + return str(index) + matches = [] + for index, line in enumerate(f_commands): + if f_origline is line: + matches = [format_with_preceding_crawlmark(index)] + break + if f_origline == line.strip(): + matches.append(format_with_preceding_crawlmark(index)) + if matches: + return (False,f" from line {' or '.join(matches)} of:") + gcmd_args = self.get_function_arguments_of_type(frame_info, 'GCodeCommand') + if len(gcmd_args) == 1: + return (True,f" {gcmd_args[0][1].get_commandline()}") + return (False, None) + msg = self.get_formatted_extended_stack_trace(callback, 0) + self.console_echo("RATOS_STACK_TRACE", "debug", msg) + logging.info("RATOS_STACK_TRACE" + "\n" + msg) + + # Helper for get_formatted_extended_stack_trace callbacks. + @staticmethod + def get_function_arguments_of_type(frame_info, type_name): + function_name = frame_info.function # Get the function name + if function_name: + locals = frame_info.frame.f_locals + function_object = frame_info.frame.f_globals.get(function_name, None) # Retrieve the function object + if function_object: + signature = inspect.signature(function_object) # Get the function signature + return [(name, locals.get(name,None)) for name in signature.parameters.keys() if type(locals.get(name, None)).__name__ == type_name] + return [] + + @staticmethod + def get_formatted_extended_stack_trace(callback=None, skip=0): + """ + Capture the current stack, format it like traceback.format_list, + and for each frame allow a callback (if provided) to add extra lines. + + Parameters: + callback (function): A function that takes an inspect.FrameInfo object + and returns a string containing extra info (or '' if none). + skip (int): Number of frames to skip from the bottom of the stack. + For example, skip=1 will omit the current frame. + + Returns: + str: The formatted multi-line string of the stack trace plus any extra info. + """ + # Get the current stack. Using inspect.stack() returns a list where each + # element is an inspect.FrameInfo object. + # We skip the first few frames (including this function itself) using skip. + stack = inspect.stack()[skip+1:] + lines = [] + + for frame_info in stack: + # Convert each inspect.FrameInfo to a FrameSummary, which is what + # traceback.format_list expects. This lets us format it the usual way. + code_line = frame_info.code_context[0].strip() if frame_info.code_context else None + frame_summary = traceback.FrameSummary(frame_info.filename, frame_info.lineno, frame_info.function, line=code_line) + + # If a callback is provided, get extra information from it. + should_emit, extra_lines = callback(frame_info) if callback is not None else (True, None) + if should_emit: + # Format the frame like traceback.format_list + lines.extend(traceback.format_list([frame_summary])) + + if extra_lines: + # Append the extra info as extra lines + lines.append(extra_lines + "\n") + + return "".join(lines) + ##### # Loader ##### From df9da6ff622be9b2d61de859298c4945ae5a5f25 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 27 Apr 2025 20:01:08 +0100 Subject: [PATCH 033/139] Macros: Don't call _BEACON_SET_NOZZLE_TEMP_OFFSET from TEMPERATURE_WAIT - because TEMPERATURE_WAIT does not actually set nozzle temp - also improve logging consistency --- configuration/macros/overrides.cfg | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index d10faebfa..0ff65c95f 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -220,36 +220,31 @@ gcode: {% endif %} {% endif %} + {% set id = "sensor " ~ sensor if t == -1 else "toolhead T" ~ t %} + # call klipper base function {% if minimum > -1 and maximum > -1 %} - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}, MINIMUM: {minimum}, MAXIMUM: {maximum}" + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for {id}, MINIMUM: {minimum}, MAXIMUM: {maximum}" RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" MINIMUM={minimum} MAXIMUM={maximum} {% elif minimum > -1 and maximum == -1 %} - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}, MINIMUM: {minimum}" + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for {id}, MINIMUM: {minimum}" RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" MINIMUM={minimum} {% elif minimum == -1 and maximum > -1 %} - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}, MAXIMUM: {maximum}" + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for {id}, MAXIMUM: {maximum}" RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" MAXIMUM={maximum} {% else %} # Neither minimum or maximum specified: TEMPERATURE_WAIT_BASE will raise an error, but we need to let that happen # to ensure consistent behaviour. - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for sensor: {sensor}" + DEBUG_ECHO PREFIX="TEMPERATURE_WAIT" MSG="neither MINIMUM or MAXIMUM args were specified, call TEMPERATURE_WAIT_BASE is expected to fail" + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Waiting for {id}" RATOS_ECHO MSG="please wait..." TEMPERATURE_WAIT_BASE SENSOR="{sensor}" {% endif %} - {% if t != -1 and printer.configfile.settings.beacon is defined %} - # Update the nozzle thermal expansion offset - {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} - {% if is_printing_gcode %} - _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} - {% endif %} - {% endif %} - - RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for toolhead T{t} reached." + RATOS_ECHO PREFIX="TEMPERATURE_WAIT" MSG="Temperature for {id} reached." [gcode_macro SDCARD_PRINT_FILE] rename_existing: SDCARD_PRINT_FILE_BASE From 16a981b23fb36c0fc7f3f98501d9b209decdbe82 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 28 Apr 2025 12:28:55 +0100 Subject: [PATCH 034/139] Mesh/Beacon: adjust default config to avoid aliasing during rapid scans --- configuration/z-probe/beacon.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index c4bf702aa..7349818ff 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -16,7 +16,14 @@ serial: /dev/beacon x_offset: 0 y_offset: 22.5 mesh_main_direction: x +# Using more than one mesh run is not currently reccomended because the Beacon extension determines the +# value to use from the combined run samples using median. If there is some drift between runs, median +# median instability can lead to an irregular spiky-looking mesh. mesh_runs: 1 +# When mesh_cluster_size is zero, all samples are clustered to their nearest mesh point. This approach +# avoids the aliasing seen when using small cluster sizes, which leads to the sample stream being effectively +# downsampled using unfiltered decimation. +mesh_cluster_size: 0 speed: 15. lift_speed: 80. contact_max_hotend_temperature: 275 From e79c74c9d5e89ab7357dd5fa7412799ff5c8f5e1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 28 Apr 2025 14:10:30 +0100 Subject: [PATCH 035/139] Mesh/Beacon: code for testing purposes --- configuration/klippy/beacon_mesh.py | 153 ++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 7 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index f69de8644..d0ca90969 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,6 +1,10 @@ import collections from . import bed_mesh as BedMesh +## TESTING rapid-contact-rapid comp mesh generation +RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" +RATOS_TEMP_SCAN_MESH_ATFER_NAME = "__BEACON_TEMP_SCAN_MESH_AFTER__" + ### # Mesh constants ### @@ -233,7 +237,7 @@ def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): raise gcmd.error("Value for parameter 'PROFILE' must be specified") title = gcmd.get("TITLE", "Validate compensation mesh profile") - subject = gcmd.get("SUBJECT", f"Profile '{profile}'") + subject = gcmd.get("SUBJECT", None) bed_temp = gcmd.get_float("COMPARE_BED_TEMP", None) bed_temp_is_error = gcmd.get("COMPARE_BED_TEMP_IS_ERROR", "false").strip().lower() in ("1", "true") @@ -242,7 +246,7 @@ def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): bed_temp = None if not self._validate_extended_parameters( - self._get_compensation_zmesh(profile, subject).get_mesh_params(), + self._create_zmesh_from_profile(profile, subject, "Beacon compensation mesh validation").get_mesh_params(), title, subject, compare_bed_temp=bed_temp, @@ -257,7 +261,7 @@ def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") - if not self.apply_scan_compensation(self._get_compensation_zmesh(profile)): + if not self.apply_scan_compensation(self._create_zmesh_from_profile(profile, purpose="Beacon scan compensation")): raise self.printer.command_error("Could not apply scan compensation") desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." @@ -268,7 +272,16 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): raise gcmd.error("Value for parameter 'PROFILE' must be specified") if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - self.create_compensation_mesh(profile, probe_count) + + keep_temp_meshs = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') + + # TODO: Remove TESTING stuff before release + method = gcmd.get('TESTING_GENERATION_METHOD', self.gm_ratos.variables.get('testing_default_compensation_mesh_generation_method')) + if method and method.strip().lower() == 'temporal_blend': + gcmd.respond_info("TESTING: using rapid-contact-rapid temporal blend") + self.create_compensation_mesh_TESTING_rapid_contact_rapid(profile, probe_count, keep_temp_meshs) + else: + self.create_compensation_mesh(profile, probe_count) desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): @@ -292,23 +305,26 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): self.ratos.console_echo("Set zero reference position", "info", f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") - def _get_compensation_zmesh(self, profile, subject=None): + def _create_zmesh_from_profile(self, profile, subject=None, purpose=None): if not profile: raise TypeError("Argument profile cannot be None") if subject is None: subject = f"Profile '{profile}'" + + if purpose: + purpose = f" for {purpose}" profiles = self.bed_mesh.pmgr.get_profiles() if profile not in profiles: - raise self.printer.command_error(f"{subject} not found for Beacon scan compensation") + raise self.printer.command_error(f"{subject} not found{purpose}") try: compensation_zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile) compensation_zmesh.build_mesh(profiles[profile]["points"]) return compensation_zmesh except Exception as e: - raise self.printer.command_error(f"Could not load {subject[0].lower()}{subject[1:]} for Beacon scan compensation: {str(e)}") from e + raise self.printer.command_error(f"Could not load {subject[0].lower()}{subject[1:]}{purpose}: {str(e)}") from e # Logs to console for any problems with extended mesh parameters. Returns True if the extended parameters are present # and valid, otherwise False. Version must be the current version. @@ -548,7 +564,130 @@ def create_compensation_mesh(self, profile, probe_count): self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + + def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_count, keep_temp_meshes): + if not self.beacon: + self.ratos.console_echo("Create compensation mesh error", "error", + "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") + return + + if self.z_tilt and not self.z_tilt.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Z-tilt levelling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + + if self.qgl and not self.qgl.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Quad gantry levelling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + + beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' + + # Go to safe home + self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") + + if beacon_contact_calibrate_model_on_print: + # Calibrate a fresh model + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") + else: + if self.beacon.model is None: + self.ratos.console_echo("Create compensation mesh error", "error", + "No active Beacon model is selected._N_Make sure you've performed initial Beacon calibration.") + return + + self.check_active_beacon_model_temp(title="Create compensation mesh warning") + + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1") + + mesh_before_name = RATOS_TEMP_SCAN_MESH_BEFORE_NAME if not keep_temp_meshes else RATOS_TEMP_SCAN_MESH_BEFORE_NAME + profile + mesh_after_name = RATOS_TEMP_SCAN_MESH_ATFER_NAME if not keep_temp_meshes else RATOS_TEMP_SCAN_MESH_ATFER_NAME + profile + contact_mesh_name = RATOS_TEMP_CONTACT_MESH_NAME if not keep_temp_meshes else RATOS_TEMP_CONTACT_MESH_NAME + profile + + # create 'before' temp scan mesh + self.gcode.run_script_from_command( + "BED_MESH_CALIBRATE " + "PROFILE='%s'" % (mesh_before_name)) + + # create contact mesh + self.gcode.run_script_from_command( + "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=2 SAMPLES_DROP=1 SAMPLES_TOLERANCE_RETRIES=10 " + "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], contact_mesh_name)) + + # create 'after' temp scan mesh + self.gcode.run_script_from_command( + "BED_MESH_CALIBRATE " + "PROFILE='%s'" % (mesh_after_name)) + scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) + scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) + + self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % contact_mesh_name) + + contact_mesh_points = self.bed_mesh.pmgr.get_profiles()[contact_mesh_name]["points"][:] + contact_params = self.bed_mesh.z_mesh.get_mesh_params() + contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) + contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) + + compensation_mesh_points = [] + + try: + if not self.beacon.mesh_helper.dir in ("x", "y"): + raise ValueError(f"Expected 'x' or 'y' for self.beacon.mesh_helper.dir, but got '{self.beacon.mesh_helper.dir}'") + + dir = self.beacon.mesh_helper.dir + y_count = len(contact_mesh_points) + x_count = len(contact_mesh_points[0]) + contact_mesh_point_count = len(contact_mesh_points) * len(contact_mesh_points[0]) + + debug_lines = [] + + for y in range(y_count): + compensation_mesh_points.append([]) + for x in range(x_count): + contact_mesh_index = \ + ((x if y % 2 == 0 else x_count - x - 1) + y * x_count) \ + if dir == "x" else \ + ((y if x % 2 == 0 else y_count - y - 1) + x * y_count) + + blend_factor = contact_mesh_index / (contact_mesh_point_count - 1) + + contact_x_pos = contact_params["min_x"] + x * contact_x_step + contact_y_pos = contact_params["min_y"] + y * contact_y_step + + scan_before_z = scan_before_zmesh.calc_z(contact_x_pos, contact_y_pos) + scan_after_z = scan_after_zmesh.calc_z(contact_x_pos, contact_y_pos) + scan_temporal_crossfade_z = ((1 - blend_factor) * scan_before_z) + (blend_factor * scan_after_z) + + contact_z = contact_mesh_points[y][x] + offset_z = contact_z - scan_temporal_crossfade_z + + compensation_mesh_points[y].append(offset_z) + + debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + + self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + + # Create new mesh + params = self.bed_mesh.z_mesh.get_mesh_params() + params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() + params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC + new_mesh = BedMesh.ZMesh(params, profile) + new_mesh.build_mesh(compensation_mesh_points) + self.bed_mesh.set_mesh(new_mesh) + self.bed_mesh.save_profile(profile) + + if not keep_temp_meshes: + # Remove temp meshes + self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % contact_mesh_name) + self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % mesh_before_name) + self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % mesh_after_name) + + self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) + except BedMesh.BedMeshError as e: + self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() From 4a48590f6a0a014cd0a1b459d1692b0df226c797 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 30 Apr 2025 13:27:45 +0100 Subject: [PATCH 036/139] Mesh/Beacon: add optional 'notes' extended mesh parameter --- configuration/klippy/beacon_mesh.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index d0ca90969..bf25dc66b 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -38,8 +38,10 @@ # - for measured meshes, it's the probe method of measurement # - for compensation meshes, it's the probe method of the proximity mesh used to make the compensation mesh # - for compensated meshes, it's the probe method of the measured mesh that was then compensated +RATOS_MESH_NOTES_PARAMETER = "ratos_notes" +# - abitrary notes, optional -RATOS_MESH_PARAMETERS = ( +RATOS_REQUIRED_MESH_PARAMETERS = ( RATOS_MESH_VERSION_PARAMETER, RATOS_MESH_BED_TEMP_PARAMETER, RATOS_MESH_KIND_PARAMETER, @@ -194,6 +196,7 @@ def cmd_APPLY_RATOS_BED_MESH_PARAMETERS(self, gcmd): params[RATOS_MESH_BED_TEMP_PARAMETER] = bed_temp params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_MEASURED params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = ratos_probe_method + params.pop(RATOS_MESH_NOTES_PARAMETER, None) msg = ( f"Setting parameters for active bed mesh '{mesh.get_profile_name()}':_N_" @@ -348,8 +351,8 @@ def _validate_extended_parameters(self, error_title = title + " error" warning_title = title + " warning" - if not all(p in params for p in RATOS_MESH_PARAMETERS): - missing = [p for p in RATOS_MESH_PARAMETERS if p not in params] + if not all(p in params for p in RATOS_REQUIRED_MESH_PARAMETERS): + missing = [p for p in RATOS_REQUIRED_MESH_PARAMETERS if p not in params] self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"missing parameters: {', '.join(missing)}") self.ratos.console_echo(error_title, "error", f"{subject} has incomplete extended metadata.") @@ -711,6 +714,7 @@ def load_extra_mesh_params(self): mesh_kind = config.getchoice(RATOS_MESH_KIND_PARAMETER, list(RATOS_MESH_KIND_CHOICES)) mesh_probe_method = config.getchoice(RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER, list(RATOS_MESH_BEACON_PROBE_METHOD_CHOICES)) mesh_bed_temp = config.getfloat(RATOS_MESH_BED_TEMP_PARAMETER) + notes = config.get(RATOS_MESH_NOTES_PARAMETER, None) except config.error as ex: self.ratos.console_echo("RatOS Beacon bed mesh management", "error", f"Bed mesh profile '{profile_name}' configuration is invalid: {str(ex)}") @@ -721,6 +725,10 @@ def load_extra_mesh_params(self): profile_params[RATOS_MESH_KIND_PARAMETER] = mesh_kind profile_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = mesh_probe_method profile_params[RATOS_MESH_BED_TEMP_PARAMETER] = mesh_bed_temp + if notes: + profile_params[RATOS_MESH_NOTES_PARAMETER] = notes + else: + profile_params.pop(RATOS_MESH_NOTES_PARAMETER, None) else: self.ratos.console_echo("RatOS Beacon bed mesh management", "warning", f"Bed mesh profile '{profile_name}' was created without extended RatOS Beacon bed mesh support." From 5a2da79235e05565a86bcc2b280db3261b7df820 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 30 Apr 2025 16:34:20 +0100 Subject: [PATCH 037/139] Mesh/Beacon: code for testing purposes --- configuration/klippy/beacon_mesh.py | 137 +++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 11 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index bf25dc66b..f8d632654 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,5 +1,7 @@ import collections from . import bed_mesh as BedMesh +import numpy as np +from scipy.ndimage import gaussian_filter ## TESTING rapid-contact-rapid comp mesh generation RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" @@ -141,6 +143,9 @@ def register_commands(self): self.gcode.register_command('GET_RATOS_EXTENDED_BED_MESH_PARAMETERS', self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, desc=(self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS)) + self.gcode.register_command('REMAKE_BEACON_COMPENSATION_MESH', + self.cmd_REMAKE_BEACON_COMPENSATION_MESH, + desc=(self.desc_REMAKE_BEACON_COMPENSATION_MESH)) desc_BEACON_MESH_INIT = "Performs Beacon mesh initialization tasks" def cmd_BEACON_MESH_INIT(self, gcmd): @@ -276,16 +281,22 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - keep_temp_meshs = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') - # TODO: Remove TESTING stuff before release method = gcmd.get('TESTING_GENERATION_METHOD', self.gm_ratos.variables.get('testing_default_compensation_mesh_generation_method')) + if method and method.strip().lower() == 'temporal_blend': gcmd.respond_info("TESTING: using rapid-contact-rapid temporal blend") - self.create_compensation_mesh_TESTING_rapid_contact_rapid(profile, probe_count, keep_temp_meshs) + self.create_compensation_mesh_TESTING_rapid_contact_rapid(gcmd, profile, probe_count) else: self.create_compensation_mesh(profile, probe_count) + desc_REMAKE_BEACON_COMPENSATION_MESH = "TESTING! PROFILE='exising comp mesh' NEW_PROFILE='new name' [GAUSSIAN_SIGMA=x]" + def cmd_REMAKE_BEACON_COMPENSATION_MESH(self, gcmd): + profile = gcmd.get('PROFILE') + new_profile = gcmd.get('NEW_PROFILE') + gaussian_sigma = gcmd.get_float('GAUSSIAN_SIGMA', self.gm_ratos.variables.get('testing_default_compensation_mesh_gaussian_sigma')) + self.TESTING_remake_compensation_mesh(profile, new_profile, gaussian_sigma) + desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): if (self.bed_mesh.z_mesh is None): @@ -568,7 +579,7 @@ def create_compensation_mesh(self, profile, probe_count): except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) - def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_count, keep_temp_meshes): + def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, probe_count): if not self.beacon: self.ratos.console_echo("Create compensation mesh error", "error", "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") @@ -584,6 +595,13 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co "Quad gantry levelling is configured but has not been applied._N_" "This may result in inaccurate compensation.") + gaussian_sigma = gcmd.get_float('GAUSSIAN_SIGMA', self.gm_ratos.variables.get('testing_default_compensation_mesh_gaussian_sigma')) + keep_temp_meshes = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') + samples = gcmd.get_int('SAMPLES', 2) + samples_drop = gcmd.get_int('SAMPLES_DROP', 1) + + gcmd.respond_info(f"keep_temp_meshes: {keep_temp_meshes}, gaussian_sigma: {gaussian_sigma}, samples: {samples} samples_drop: {samples_drop}") + beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' # Go to safe home @@ -602,9 +620,9 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1") - mesh_before_name = RATOS_TEMP_SCAN_MESH_BEFORE_NAME if not keep_temp_meshes else RATOS_TEMP_SCAN_MESH_BEFORE_NAME + profile - mesh_after_name = RATOS_TEMP_SCAN_MESH_ATFER_NAME if not keep_temp_meshes else RATOS_TEMP_SCAN_MESH_ATFER_NAME + profile - contact_mesh_name = RATOS_TEMP_CONTACT_MESH_NAME if not keep_temp_meshes else RATOS_TEMP_CONTACT_MESH_NAME + profile + mesh_before_name = RATOS_TEMP_SCAN_MESH_BEFORE_NAME if not keep_temp_meshes else profile + "_SCAN_BEFORE" + mesh_after_name = RATOS_TEMP_SCAN_MESH_ATFER_NAME if not keep_temp_meshes else profile + "_SCAN_AFTER" + contact_mesh_name = RATOS_TEMP_CONTACT_MESH_NAME if not keep_temp_meshes else profile + "_CONTACT" # create 'before' temp scan mesh self.gcode.run_script_from_command( @@ -613,8 +631,8 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co # create contact mesh self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=2 SAMPLES_DROP=1 SAMPLES_TOLERANCE_RETRIES=10 " - "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], contact_mesh_name)) + "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=%d SAMPLES_DROP=%d SAMPLES_TOLERANCE_RETRIES=10 " + "PROBE_COUNT=%d,%d PROFILE='%s'" % (samples, samples_drop, probe_count[0], probe_count[1], contact_mesh_name)) # create 'after' temp scan mesh self.gcode.run_script_from_command( @@ -631,6 +649,12 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) + if gaussian_sigma is not None and gaussian_sigma > 0: + self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") + filtered_np = gaussian_filter(contact_mesh_points, sigma=gaussian_sigma, mode='nearest') + contact_mesh_points = filtered_np.tolist() + contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" + compensation_mesh_points = [] try: @@ -670,12 +694,20 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + if keep_temp_meshes and gaussian_sigma is not None and gaussian_sigma > 0: + params = contact_params.copy() + filtered_profile = contact_mesh_name + "_filtered" + new_mesh = BedMesh.ZMesh(params, filtered_profile) + new_mesh.build_mesh(contact_mesh_points) + self.bed_mesh.set_mesh(new_mesh) + self.bed_mesh.save_profile(filtered_profile) + # Create new mesh - params = self.bed_mesh.z_mesh.get_mesh_params() + params = contact_params.copy() params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION - params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC + params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY new_mesh = BedMesh.ZMesh(params, profile) new_mesh.build_mesh(compensation_mesh_points) self.bed_mesh.set_mesh(new_mesh) @@ -691,6 +723,89 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, profile, probe_co except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma=None): + + mesh_before_name = profile + "_SCAN_BEFORE" + mesh_after_name = profile + "_SCAN_AFTER" + contact_mesh_name = profile + "_CONTACT" + + scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) + scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) + contact_zmesh = self._create_zmesh_from_profile(contact_mesh_name) + + contact_mesh_points = contact_zmesh.probed_matrix + contact_params = contact_zmesh.get_mesh_params().copy() + contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) + contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) + + if gaussian_sigma is not None and gaussian_sigma > 0: + self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") + filtered_np = gaussian_filter(contact_mesh_points, sigma=gaussian_sigma, mode='nearest') + contact_mesh_points = filtered_np.tolist() + contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" + + compensation_mesh_points = [] + + try: + if not self.beacon.mesh_helper.dir in ("x", "y"): + raise ValueError(f"Expected 'x' or 'y' for self.beacon.mesh_helper.dir, but got '{self.beacon.mesh_helper.dir}'") + + dir = self.beacon.mesh_helper.dir + y_count = len(contact_mesh_points) + x_count = len(contact_mesh_points[0]) + contact_mesh_point_count = len(contact_mesh_points) * len(contact_mesh_points[0]) + + debug_lines = [] + + for y in range(y_count): + compensation_mesh_points.append([]) + for x in range(x_count): + contact_mesh_index = \ + ((x if y % 2 == 0 else x_count - x - 1) + y * x_count) \ + if dir == "x" else \ + ((y if x % 2 == 0 else y_count - y - 1) + x * y_count) + + blend_factor = contact_mesh_index / (contact_mesh_point_count - 1) + + contact_x_pos = contact_params["min_x"] + x * contact_x_step + contact_y_pos = contact_params["min_y"] + y * contact_y_step + + scan_before_z = scan_before_zmesh.calc_z(contact_x_pos, contact_y_pos) + scan_after_z = scan_after_zmesh.calc_z(contact_x_pos, contact_y_pos) + scan_temporal_crossfade_z = ((1 - blend_factor) * scan_before_z) + (blend_factor * scan_after_z) + + contact_z = contact_mesh_points[y][x] + offset_z = contact_z - scan_temporal_crossfade_z + + compensation_mesh_points[y].append(offset_z) + + debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + + self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + + if gaussian_sigma is not None: + params = contact_params.copy() + filtered_profile = new_profile + RATOS_TEMP_CONTACT_MESH_NAME + "_filtered" + new_mesh = BedMesh.ZMesh(params, filtered_profile) + new_mesh.build_mesh(contact_mesh_points) + self.bed_mesh.set_mesh(new_mesh) + self.bed_mesh.save_profile(filtered_profile) + + # Create new mesh + params = contact_params.copy() + params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() + params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY + new_mesh = BedMesh.ZMesh(params, new_profile) + new_mesh.build_mesh(compensation_mesh_points) + self.bed_mesh.set_mesh(new_mesh) + self.bed_mesh.save_profile(new_profile) + + self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(new_profile))) + except BedMesh.BedMeshError as e: + self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() From 53666b39845c5c74c90940f96f4c324c4ac083be Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 2 May 2025 15:25:48 +0100 Subject: [PATCH 038/139] Mesh/Beacon: move scipy calculation to external process and elsewhere make reactor-friendly --- configuration/klippy/beacon_mesh.py | 43 +++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index f8d632654..70de2113b 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,4 +1,4 @@ -import collections +import collections, multiprocessing, traceback from . import bed_mesh as BedMesh import numpy as np from scipy.ndimage import gaussian_filter @@ -579,6 +579,36 @@ def create_compensation_mesh(self, profile, probe_count): except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + def _apply_filter(self, data, sigma): + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_apply_filter(data, sigma)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise Exception("Error applying filter: %s" % (result,)) + else: + return result + + def _do_apply_filter(self, data, sigma): + return gaussian_filter(data, sigma=sigma, mode='nearest').tolist() + def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, probe_count): if not self.beacon: self.ratos.console_echo("Create compensation mesh error", "error", @@ -651,11 +681,12 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr if gaussian_sigma is not None and gaussian_sigma > 0: self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - filtered_np = gaussian_filter(contact_mesh_points, sigma=gaussian_sigma, mode='nearest') - contact_mesh_points = filtered_np.tolist() + contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma) contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" compensation_mesh_points = [] + + eventtime = self.reactor.monotonic() try: if not self.beacon.mesh_helper.dir in ("x", "y"): @@ -692,6 +723,9 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + eventtime = self.reactor.pause(eventtime + 0.05) + + self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) if keep_temp_meshes and gaussian_sigma is not None and gaussian_sigma > 0: @@ -740,8 +774,7 @@ def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma= if gaussian_sigma is not None and gaussian_sigma > 0: self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - filtered_np = gaussian_filter(contact_mesh_points, sigma=gaussian_sigma, mode='nearest') - contact_mesh_points = filtered_np.tolist() + contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma) contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" compensation_mesh_points = [] From 810d67d19bcccaf27656d9087760bc064d686fdf Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 8 May 2025 15:47:24 +0100 Subject: [PATCH 039/139] Mesh/Beacon: testing edge-enhanced filtering --- configuration/klippy/beacon_mesh.py | 80 ++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 70de2113b..6f06d0c84 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,7 +1,8 @@ -import collections, multiprocessing, traceback +import collections, multiprocessing, traceback, logging from . import bed_mesh as BedMesh import numpy as np from scipy.ndimage import gaussian_filter +from scipy.interpolate import RectBivariateSpline ## TESTING rapid-contact-rapid comp mesh generation RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" @@ -579,13 +580,13 @@ def create_compensation_mesh(self, profile, probe_count): except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) - def _apply_filter(self, data, sigma): + def _apply_filter(self, data, extrapolate_sigma=0.75, sigma=0.75, pad=3): parent_conn, child_conn = multiprocessing.Pipe() def do(): try: child_conn.send( - (False, self._do_apply_filter(data, sigma)) + (False, self._do_apply_filter(np.array(data), extrapolate_sigma, sigma, pad)) ) except Exception: child_conn.send((True, traceback.format_exc())) @@ -606,8 +607,58 @@ def do(): else: return result - def _do_apply_filter(self, data, sigma): - return gaussian_filter(data, sigma=sigma, mode='nearest').tolist() + @staticmethod + def _do_apply_filter(data, extrapolate_sigma, sigma, pad): + # This enhanced filter routine seeks to reduce edge distortion that occurs when + # a filter kernel consumes values beyond the edge of the defined data (ie, as + # per the 'mode' argument). + + # Pre-filter the data to compute a smoothed, noise-reduced model, using a mode + # that minimizes boundary artifacts on the interior. + filtered_interior = gaussian_filter(data, sigma=extrapolate_sigma, mode='nearest') + + # Extrapolate the filtered surface. + # Determine a pad size that roughly covers the effective kernel support. + # For sigma=0.75, a pad of about 3 pixels is a reasonable starting point. + + # Coordinates for original grid: + x = np.arange(data.shape[0]) + y = np.arange(data.shape[1]) + + # Define an extended grid that covers the original plus the padding + x_ext = np.arange(x[0] - pad, x[-1] + pad + 1) + y_ext = np.arange(y[0] - pad, y[-1] + pad + 1) + + # Trim the edges of the pre-filtered data as these will include distorted edge gradients + # from the filter which used 'nearest' mode. + trim = int(np.ceil(sigma*2)) + + trimmed_filtered_interior = filtered_interior[trim:-trim, trim:-trim] + + x_trimmed = x[trim:-trim] + y_trimmed = y[trim:-trim] + + # Fit a 2D cubic spline to the trimmed filtered data. Use degree 1 to avoid wild values at + # extrapolated corners. + spline = RectBivariateSpline(x_trimmed, y_trimmed, trimmed_filtered_interior, kx=1, ky=1, + bbox= [x_ext[0], x_ext[-1], y_ext[0], y_ext[-1]]) + + # Create the extrapolated surface + surface_extended = spline(x_ext, y_ext) + + # Replace the interior with the original data + surface_extended[x[0] + pad:x[-1] + pad + 1, y[0] + pad:y[-1] + pad + 1] = data + + # Apply the Gaussian filter to the extended data. When consuming values beyond the edges + # of the original data, the kernel will use the extrapolated values we created + # above, which are a better approximation than any of the standard modes. + filtered_extended = gaussian_filter(surface_extended, sigma=sigma, mode='nearest') + + # Crop the filtered array back to the original shape. + result = filtered_extended[pad:-pad, pad:-pad] + + # 'result' now should display edges that more faithfully follow the filtered curvature. + return result def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, probe_count): if not self.beacon: @@ -668,10 +719,10 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr self.gcode.run_script_from_command( "BED_MESH_CALIBRATE " "PROFILE='%s'" % (mesh_after_name)) - + scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) - + self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % contact_mesh_name) contact_mesh_points = self.bed_mesh.pmgr.get_profiles()[contact_mesh_name]["points"][:] @@ -681,7 +732,7 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr if gaussian_sigma is not None and gaussian_sigma > 0: self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma) + contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma * 2., gaussian_sigma, int(np.ceil(gaussian_sigma * 4.))) contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" compensation_mesh_points = [] @@ -721,12 +772,12 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr compensation_mesh_points[y].append(offset_z) - debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") eventtime = self.reactor.pause(eventtime + 0.05) - - self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + # For a large mesh (eg, 60x60) this can take 2+ minutes + #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) if keep_temp_meshes and gaussian_sigma is not None and gaussian_sigma > 0: params = contact_params.copy() @@ -774,7 +825,7 @@ def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma= if gaussian_sigma is not None and gaussian_sigma > 0: self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma) + contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma * 2., gaussian_sigma, int(np.ceil(gaussian_sigma * 4.))) contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" compensation_mesh_points = [] @@ -812,9 +863,10 @@ def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma= compensation_mesh_points[y].append(offset_z) - debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") - self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + # For a large mesh (eg, 60x60) this can take 2+ minutes + #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) if gaussian_sigma is not None: params = contact_params.copy() From 85f65109d7b7697a98f5015c5323e495fd547a91 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 9 May 2025 18:00:19 +0100 Subject: [PATCH 040/139] Extras: testing - add partial MULTI_POINT_PROBE implementation for data gathering --- configuration/klippy/ratos.py | 142 +++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 644756415..d847be0f6 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,5 +1,7 @@ -import os, logging, glob, traceback, inspect, re -import json, subprocess, pathlib +import os, logging, glob, traceback, inspect, re, math +import json, subprocess, pathlib, time +import numpy as np +from . import probe ##### # RatOS @@ -24,7 +26,15 @@ def __init__(self, config): 'TEST_RESONANCES': None, 'SHAPER_CALIBRATE': None, } + + # Fields initialized in _connect + self.v_sd = None + self.sdcard_dirname = None + self.dual_carriage = None + self.rmmu_hub = None self.bed_mesh = None + self.gm_ratos = None + self.toolhead = None # Status fields self.last_processed_file_result = None @@ -46,7 +56,9 @@ def register_handler(self): def _connect(self): self.v_sd = self.printer.lookup_object('virtual_sdcard', None) self.sdcard_dirname = self.v_sd.sdcard_dirname - self.dual_carriage = None + self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') + self.toolhead = self.printer.lookup_object("toolhead") + if self.config.has_section("dual_carriage"): self.dual_carriage = self.printer.lookup_object("dual_carriage", None) self.rmmu_hub = None @@ -85,6 +97,7 @@ def register_commands(self): self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=(self.desc_RAISE_ERROR)) self.gcode.register_command('_TRY', self.cmd_TRY, desc=(self.desc_TRY)) self.gcode.register_command('_DEBUG_ECHO_STACK_TRACE', self.cmd_DEBUG_ECHO_STACK_TRACE, desc=(self.desc_DEBUG_ECHO_STACK_TRACE)) + self.gcode.register_command('MULTI_POINT_PROBE', self.cmd_MULTI_POINT_PROBE, desc=(self.desc_MULTI_POINT_PROBE)) def register_command_overrides(self): self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) @@ -678,6 +691,129 @@ def get_formatted_extended_stack_trace(callback=None, skip=0): return "".join(lines) + ##### + # Multi-point Probe + ##### + + def _generate_points(self, n, x_lim, y_lim, min_dist, max_iter=10000): + """ + Generate n random points within given x and y limits such that + any two points are at least min_dist apart. + + Parameters: + - n: number of points to generate + - x_lim: tuple (min_x, max_x) + - y_lim: tuple (min_y, max_y) + - min_dist: minimum required Euclidean distance between any two points + - max_iter: maximum number of iterations to try (to avoid infinite loops) + + Returns: + - A NumPy array of shape (m, 2) of the generated points, where m <= n. + """ + points = [] + iterations = 0 + + while len(points) < n and iterations < max_iter: + # Generate a candidate point uniformly within the given x and y limits. + candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), + np.random.uniform(y_lim[0], y_lim[1])]) + + # Check that candidate is at least min_dist away from every existing point. + if all(np.linalg.norm(candidate - p) >= min_dist for p in points): + points.append(candidate.tolist()) # don't leak numpy types + + iterations += 1 + + if len(points) < n: + raise self.gcode.error( + "Could not generate all required probe points within the specified iteration limit. " + "The conditions are too strict.") + + return points + + def _check_homed(self, msg = 'Must home first'): + status = self.toolhead.get_status(self.reactor.monotonic()) + homed_axes = status["homed_axes"] + if any(axis not in homed_axes for axis in "xyz"): + raise self.gcode.error( msg ) + + desc_MULTI_POINT_PROBE = "TO DO" + def cmd_MULTI_POINT_PROBE(self, gcmd): + + self._check_homed() + + # - assumes already at desired centre location + # cmd COUNT=5 MIN_SPAN=10 [SAMPLES=1 SAMPLES_DROP=0 PROBE_METHOD=contact] + count = gcmd.get_int('COUNT', 5) + min_span = gcmd.get_float('MIN_SPAN', 10.) + + extruder_name = 'extruder' + + if self.dual_carriage and self.dual_carriage.dc[1].mode.lower() == 'primary': + extruder_name = 'extruder1' + + extruder = self.printer.lookup_object(extruder_name) + nozzle_diameter = extruder.nozzle_diameter + + # Assume typical 0.5mm rim around nozzle + nozzle_tip_dia = nozzle_diameter + 1. + + # Calculate the nozzle-based min range as the length of the side of a + # square with area four times the footprint of COUNT nozzle tips. + nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * count * 4.) + span = max(min_span, nozzle_based_min_span) + half_span = span / 2. + + gcmd.respond_info(f"count: {count} min_span: {min_span} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_range: {nozzle_based_min_span:.2f} use_range: {span:.2f}") + + pos = self.toolhead.get_position() + + range_x = (pos[0] - half_span, pos[0] + half_span) + range_y = (pos[1] - half_span, pos[1] + half_span) + + printable_x = ( self.gm_ratos.variables.get('printable_x_min'), self.gm_ratos.variables.get('printable_x_max') ) + printable_y = ( self.gm_ratos.variables.get('printable_y_min'), self.gm_ratos.variables.get('printable_y_max') ) + + def includes( r, value ): + return r[0] <= value <= r[1] + + if not ( + includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and + includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): + self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') + raise gcmd.error('The required span would probe outside the printable area') + + points = self._generate_points(count, range_x, range_y, nozzle_tip_dia) + + #gcmd.respond_info( "\n".join([f"{p[0]:.2f}, {p[1]:.2f}" for p in points])) + + # TODO: ProbePointsHelper will consider name, horizontal_move_z and speed from config. It's weird to conflate those + # values with [ratos] config. It would seem cleaner to move MULTI_POINT_PROBE into its own file. + probe_helper = probe.ProbePointsHelper(self.config, self.probe_finalize, []) + probe_helper.update_probe_points(points, len(points)) + probe_helper.start_probe(gcmd) + + def probe_finalize(self, offsets, positions): + def percentile_filter(data, margin=5.): + lower_bound = np.percentile(data, margin) + upper_bound = np.percentile(data, 100. - margin) + filtered_data = data[np.logical_and(data >= lower_bound, data <= upper_bound)] # Safe comparison + return filtered_data + + #self.gcode.respond_info(f"offsets:\n{offsets}") + #self.gcode.respond_info(f"positions:\n{positions}") + z = np.array([p[2] for p in positions]) + self.gcode.respond_info(f"mean: {np.mean(z):.5f} median: {np.median(z):.5f} min: {np.min(z):.5f} max: {np.max(z):.5f} spread: {np.max(z)-np.min(z):.5f} sd: {np.std(z):.5f}") + #z5 = percentile_filter(z, 5.) + #self.gcode.respond_info(f"mean5: {np.mean(z5):.5f} median5: {np.median(z5):.5f}") + + fn = "/tmp/multi-point-probe-" + time.strftime("%Y%m%d") + ".csv" + with open(fn, "a") as f: + f.write(",".join([str(v) for v in z])) + f.write("\n") + + return 'done' + ##### # Loader ##### From 23c52684f08095ca1c799c9f31d29195fd40f2bb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 12 May 2025 20:10:46 +0100 Subject: [PATCH 041/139] Extras/Ratos: remove TEST_PROCESS_GCODE_FILE - it has been replaced by CLI ratos postprocess --- configuration/klippy/ratos.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index d847be0f6..ff2195938 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -61,7 +61,6 @@ def _connect(self): if self.config.has_section("dual_carriage"): self.dual_carriage = self.printer.lookup_object("dual_carriage", None) - self.rmmu_hub = None if self.config.has_section("rmmu_hub"): self.rmmu_hub = self.printer.lookup_object("rmmu_hub", None) if self.config.has_section("bed_mesh"): @@ -89,7 +88,6 @@ def register_commands(self): self.gcode.register_command('CONSOLE_ECHO', self.cmd_CONSOLE_ECHO, desc=(self.desc_CONSOLE_ECHO)) self.gcode.register_command('RATOS_LOG', self.cmd_RATOS_LOG, desc=(self.desc_RATOS_LOG)) self.gcode.register_command('PROCESS_GCODE_FILE', self.cmd_PROCESS_GCODE_FILE, desc=(self.desc_PROCESS_GCODE_FILE)) - self.gcode.register_command('TEST_PROCESS_GCODE_FILE', self.cmd_TEST_PROCESS_GCODE_FILE, desc=(self.desc_TEST_PROCESS_GCODE_FILE)) self.gcode.register_command('ALLOW_UNKNOWN_GCODE_GENERATOR', self.cmd_ALLOW_UNKNOWN_GCODE_GENERATOR, desc=(self.desc_ALLOW_UNKNOWN_GCODE_GENERATOR)) self.gcode.register_command('BYPASS_GCODE_PROCESSING', self.cmd_BYPASS_GCODE_PROCESSING, desc=(self.desc_BYPASS_GCODE_PROCESSING)) self.gcode.register_command('_SYNC_GCODE_POSITION', self.cmd_SYNC_GCODE_POSITION, desc=(self.desc_SYNC_GCODE_POSITION)) @@ -159,17 +157,6 @@ def cmd_BYPASS_GCODE_PROCESSING(self, gcmd): 'bypass_post_processing: True_N_' ])) - desc_TEST_PROCESS_GCODE_FILE = "Test the G-code post-processor for IDEX and RMMU, only for debugging purposes" - def cmd_TEST_PROCESS_GCODE_FILE(self, gcmd): - dual_carriage = self.dual_carriage - self.dual_carriage = gcmd.get('IDEX', dual_carriage != None).lower() == "true" - filename = gcmd.get('FILENAME', "") - if filename[0] == '/': - filename = filename[1:] - self.process_gcode_file(filename, True) - self.dual_carriage = dual_carriage - self.console_echo('Post processing test results', 'debug', 'Output: %s' % (self.last_processed_file_result)) - desc_HELLO_RATOS = "RatOS mainsail welcome message" def cmd_HELLO_RATOS(self, gcmd): url = "https://os.ratrig.com/" @@ -278,7 +265,7 @@ def cmd_CHECK_BED_MESH_PROFILE_EXISTS(self, gcmd): desc_PROCESS_GCODE_FILE = "G-code post-processor for IDEX and RMMU" def cmd_PROCESS_GCODE_FILE(self, gcmd): filename = gcmd.get('FILENAME', "") - isIdex = self.config.has_section("dual_carriage") + isIdex = self.dual_carriage is not None if filename[0] == '/': filename = filename[1:] self.gcode.run_script_from_command("SET_GCODE_VARIABLE MACRO=START_PRINT VARIABLE=first_x VALUE=-1") From ee02eb8b2dd36148f24ad3f0edab15ef55490873 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 13 May 2025 12:07:42 +0100 Subject: [PATCH 042/139] Extras/z-offset: Add ratos_z_offset extension --- configuration/klippy/ratos_z_offset.py | 120 +++++++++++++++++++++++++ configuration/scripts/ratos-common.sh | 1 + 2 files changed, 121 insertions(+) create mode 100644 configuration/klippy/ratos_z_offset.py diff --git a/configuration/klippy/ratos_z_offset.py b/configuration/klippy/ratos_z_offset.py new file mode 100644 index 000000000..746a61931 --- /dev/null +++ b/configuration/klippy/ratos_z_offset.py @@ -0,0 +1,120 @@ +# Additional Z-Offset Support +# +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +COMBINED_OFFSET_KEY = 'combined_offset' +OFFSET_NAMES = ('toolhead', 'true_zero_correction', 'hotend_thermal_expansion') + +class RatOSZOffset: + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name() + self.printer.register_event_handler("klippy:connect", + self._handle_connect) + self.next_transform = None + self.offsets = {} + self.status = None + self.combined_offset = 0. + + self.gcode = self.printer.lookup_object('gcode') + + self.gcode.register_command('GET_RATOS_Z_OFFSET', self.cmd_GET_RATOS_Z_OFFSET, + desc=self.desc_GET_RATOS_Z_OFFSET) + self.gcode.register_command('SET_RATOS_Z_OFFSET', self.cmd_SET_RATOS_Z_OFFSET, + desc=self.desc_SET_RATOS_Z_OFFSET) + self.gcode.register_command('CLEAR_RATOS_Z_OFFSET', self.cmd_CLEAR_RATOS_Z_OFFSET, + desc=self.desc_CLEAR_RATOS_Z_OFFSET) + + def _handle_connect(self): + gcode_move = self.printer.lookup_object('gcode_move') + self.next_transform = gcode_move.set_move_transform(self, force=True) + + ###### + # commands + ###### + desc_GET_RATOS_Z_OFFSET = "Report current RatOS Z offsets" + def cmd_GET_RATOS_Z_OFFSET(self, gcmd): + msg = "\n".join( f"{k}: {v:.5f}" for k, v in self.offsets.items()) + if msg: + msg += f"\n{COMBINED_OFFSET_KEY}: {self.combined_offset:.5f}" + else: + msg = "no offsets defined" + gcmd.respond_info(msg) + + desc_SET_RATOS_Z_OFFSET = "Set a RatOS Z offset" + def cmd_SET_RATOS_Z_OFFSET(self, gcmd): + name = gcmd.get('NAME').lower().strip() + if name not in OFFSET_NAMES: + raise gcmd.error(f"Offset name '{name}' is not recognized.") + offset = gcmd.get_float('OFFSET') + if offset == 0.: + self.offsets.pop(name, None) + else: + self.offsets[name] = offset + self._offset_changed() + + desc_CLEAR_RATOS_Z_OFFSET = "Clear a RatOS Z offset. This is equivalent to setting the offset to zero." + def cmd_CLEAR_RATOS_Z_OFFSET(self, gcmd): + names = gcmd.get('NAME').lower().strip() + if names == 'all': + self.offsets = {} + else: + names = [n.lower().strip() for n in names.split(',')] + if any(n not in OFFSET_NAMES for n in names): + msg = f"One or more offset names are not recognized: {', '.join(n for n in names if n not in OFFSET_NAMES)}" + raise gcmd.error(msg) + for n in names: + self.offsets.pop(n, None) + self._offset_changed() + + def _offset_changed(self): + self.combined_offset = sum(self.offsets.values(), 0.) + gcode_move = self.printer.lookup_object('gcode_move') + gcode_move.reset_last_position() + self._update_status() + + # For use by other extensions + def set_offset(self, name:str, offset:float): + if name: + name = name.strip().lower() + if name not in OFFSET_NAMES: + raise self.gcode.error(f"Offset name '{name}' is not recognized.") + if offset == 0.: + self.offsets.pop(name, None) + else: + self.offsets[name] = float(offset) + self._offset_changed() + + ###### + # gcode_move transform compliance + ###### + def get_position(self): + # Remove correction + offset = self.combined_offset + pos = self.next_transform.get_position()[:] + pos[2] -= offset + return pos + + def move(self, newpos, speed): + # Apply correction + offset = self.combined_offset + pos = newpos[:] + pos[2] += offset + self.next_transform.move(pos, speed) + + ###### + # status + ###### + def _update_status(self): + self.status = dict(self.offsets) + self.status[COMBINED_OFFSET_KEY] = self.combined_offset + + def get_status(self, eventtime=None): + if self.status is None: + self._update_status() + return self.status + +def load_config(config): + return RatOSZOffset(config) \ No newline at end of file diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index 496482d70..f538ce245 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -191,6 +191,7 @@ verify_registered_extensions() ["resonance_generator_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/resonance_generator.py") ["ratos_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos.py") ["beacon_mesh_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_mesh.py") + ["ratos_z_offset_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos_z_offset.py") ) declare -A kinematics_extensions=( From 5034dffd1ffb22832521c9601dbdfcd98195419e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 14 May 2025 23:34:04 +0100 Subject: [PATCH 043/139] Extras/Ratos: code for testing purposes --- configuration/klippy/ratos.py | 242 ++++++++++++++++++++++++++++++---- 1 file changed, 214 insertions(+), 28 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index ff2195938..734ee1322 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -2,6 +2,7 @@ import json, subprocess, pathlib, time import numpy as np from . import probe +import multiprocessing ##### # RatOS @@ -682,6 +683,80 @@ def get_formatted_extended_stack_trace(callback=None, skip=0): # Multi-point Probe ##### + @staticmethod + def pack_circles_concentric(radius, x_offset = 0., y_offset = 0., rings = 3, include_centre = True): + """ + Pack circles (radius r) using a concentric rings approach. + + Parameters: + - radius: radius of circles + - rings: number of rings, including the single central circle as the first ring. + With the centre circle included, 2 rings produces 7 circles, 3 rings 19, + 4 rings 37, 5 rings 61. + - include_centre: include the central circule in the result. Does not change + the meaning of the `rings` argument. + + Returns: + centres: a list of (x, y) coordinates for the centres of the packed circles. + """ + centres = [] + + # Place the center circle if it fits + if include_centre and rings > 0: + centres.append((x_offset, y_offset)) + + ring = 1 + # For each ring, compute the ring radius as d = ring * 2r. + # (This is a simple choice; more refined methods can use non-uniform ring spacing) + while ring < rings: + d = ring * 2 * radius # distance from center for current ring + + # Maximum circles that fit in this ring (angle between centers at least 2r/d) + n_circles = int(np.floor(2 * np.pi * d / (2 * radius))) + + # Place circles evenly around the ring + for i in range(n_circles): + theta = 2 * np.pi * i / n_circles + x = d * np.cos(theta) + y = d * np.sin(theta) + centres.append((float(x + x_offset), float(y + y_offset))) + ring += 1 + + return centres + + @staticmethod + def random_point_in_circle(radius, center_x, center_y): + # Generate a random angle between 0 and 2π + theta = np.random.uniform(0, 2 * np.pi) + + # Generate a random distance, ensuring uniform distribution within the circle + r = radius * np.sqrt(np.random.uniform(0, 1)) + + # Convert polar coordinates to Cartesian coordinates + x = center_x + r * np.cos(theta) + y = center_y + r * np.sin(theta) + + return x, y + + @staticmethod + def random_point_on_circle(radius, center_x, center_y): + # Generate a random angle in radians + theta = np.random.uniform(0, 2 * np.pi) + + # Compute the x and y coordinates + x = center_x + radius * np.cos(theta) + y = center_y + radius * np.sin(theta) + + return float(x), float(y) + + @staticmethod + def circle_points(n, radius, center_x, center_y): + """Generate 'n' evenly spaced points on a circle of given radius centered at (center_x, center_y).""" + angles = np.linspace(0, 2 * np.pi, n, endpoint=False) + x_points = center_x + radius * np.cos(angles) + y_points = center_y + radius * np.sin(angles) + return np.column_stack((x_points, y_points)).tolist() + def _generate_points(self, n, x_lim, y_lim, min_dist, max_iter=10000): """ Generate n random points within given x and y limits such that @@ -731,8 +806,9 @@ def cmd_MULTI_POINT_PROBE(self, gcmd): # - assumes already at desired centre location # cmd COUNT=5 MIN_SPAN=10 [SAMPLES=1 SAMPLES_DROP=0 PROBE_METHOD=contact] - count = gcmd.get_int('COUNT', 5) - min_span = gcmd.get_float('MIN_SPAN', 10.) + pattern = gcmd.get('PATTERN', 'random').strip().lower() + if pattern not in ('random', 'concentric', 'circle'): + raise gcmd.error('If specified, PATTERN must be random, concentric or circle') extruder_name = 'extruder' @@ -742,38 +818,99 @@ def cmd_MULTI_POINT_PROBE(self, gcmd): extruder = self.printer.lookup_object(extruder_name) nozzle_diameter = extruder.nozzle_diameter - # Assume typical 0.5mm rim around nozzle - nozzle_tip_dia = nozzle_diameter + 1. + # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), + # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. + # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier + # of 1.56, or an increase of 1.35. Here we make some effort at a reasonable approximation. + if nozzle_diameter < 0.25: + nozzle_tip_dia = 1.5 * nozzle_diameter + elif nozzle_diameter <= 0.8: + nozzle_tip_dia = 2.5 * nozzle_diameter + else: + nozzle_tip_dia = nozzle_diameter + 1.35 - # Calculate the nozzle-based min range as the length of the side of a - # square with area four times the footprint of COUNT nozzle tips. - nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * count * 4.) - span = max(min_span, nozzle_based_min_span) - half_span = span / 2. - - gcmd.respond_info(f"count: {count} min_span: {min_span} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_range: {nozzle_based_min_span:.2f} use_range: {span:.2f}") - pos = self.toolhead.get_position() - range_x = (pos[0] - half_span, pos[0] + half_span) - range_y = (pos[1] - half_span, pos[1] + half_span) - printable_x = ( self.gm_ratos.variables.get('printable_x_min'), self.gm_ratos.variables.get('printable_x_max') ) printable_y = ( self.gm_ratos.variables.get('printable_y_min'), self.gm_ratos.variables.get('printable_y_max') ) def includes( r, value ): return r[0] <= value <= r[1] - - if not ( - includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and - includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): - self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') - raise gcmd.error('The required span would probe outside the printable area') - - points = self._generate_points(count, range_x, range_y, nozzle_tip_dia) + + if pattern == 'random': + count = gcmd.get_int('COUNT', 5) + min_span = gcmd.get_float('MIN_SPAN', 10.) + + # Calculate the nozzle-based min range as the length of the side of a + # square with area four times the footprint of COUNT nozzle tips. + nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * count * 4.) + span = max(min_span, nozzle_based_min_span) + half_span = span / 2. + + gcmd.respond_info(f"count: {count} min_span: {min_span} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_range: {nozzle_based_min_span:.2f} use_range: {span:.2f}") + self.mpp_save_meta = dict(pattern=0,count=count, min_span=min_span, nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia, nozzle_based_min_span=nozzle_based_min_span, span=span) + self.mpp_filename_suffix = f"-random{count}" + + range_x = (pos[0] - half_span, pos[0] + half_span) + range_y = (pos[1] - half_span, pos[1] + half_span) + + if not ( + includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and + includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): + self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') + raise gcmd.error('The required span would probe outside the printable area') + + points = self._generate_points(count, range_x, range_y, nozzle_tip_dia) + elif pattern == 'concentric': + rings = gcmd.get_int('RINGS', 3, minval=2, maxval=4) + include_centre = gcmd.get_int('INCLUDE_CENTRE', 0) == 1 + jitter_tip_dia_factor = gcmd.get_float('JITTER', 3., minval=0., maxval=50.) + + span = ((((rings * 2) - 1 ) * nozzle_tip_dia)/2) + ( jitter_tip_dia_factor * nozzle_tip_dia ) + + cx, cy = self.random_point_in_circle(jitter_tip_dia_factor * nozzle_tip_dia / 2, pos[0], pos[1]) + + gcmd.respond_info(f"rings: {rings} include_centre: {include_centre} jitter: {jitter_tip_dia_factor:.1f} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} span: {span:.2f} c: {cx:.2f}, {cy:.2f}") + self.mpp_filename_suffix = f"-concentric-r{rings}-ic{'1' if include_centre else '0'}-j{jitter_tip_dia_factor:.1f}" + self.mpp_save_meta = dict(pattern=1,rings=rings,include_centre=include_centre,jitter=jitter_tip_dia_factor,nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia,span=span,centre=(cx,cy)) + + range_x = (pos[0] - span, pos[0] + span) + range_y = (pos[1] - span, pos[1] + span) + + if not ( + includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and + includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): + self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') + raise gcmd.error('The required span would probe outside the printable area') + + points = self.pack_circles_concentric(nozzle_tip_dia/2, cx, cy, rings, include_centre) + elif pattern == 'circle': + dia = gcmd.get_float('DIA', 10.0) + count = gcmd.get_int('COUNT', 60) + + span = dia + nozzle_diameter + cx = pos[0] + cy = pos[1] + + gcmd.respond_info(f"dia: {dia} count: {count} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} span: {span:.2f} c: {cx:.2f}, {cy:.2f}") + self.mpp_filename_suffix = f"-circle-{dia:.1f}d{count}" + self.mpp_save_meta = dict(pattern=2,count=count,dia=dia,nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia,span=span,centre=(cx,cy)) + + range_x = (pos[0] - span, pos[0] + span) + range_y = (pos[1] - span, pos[1] + span) + + if not ( + includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and + includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): + self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') + raise gcmd.error('The required span would probe outside the printable area') + + points = self.circle_points(count, dia/2, cx, cy) + else: + raise gcmd.error(f"Pattern '{pattern}' not implemented.") #gcmd.respond_info( "\n".join([f"{p[0]:.2f}, {p[1]:.2f}" for p in points])) - + # TODO: ProbePointsHelper will consider name, horizontal_move_z and speed from config. It's weird to conflate those # values with [ratos] config. It would seem cleaner to move MULTI_POINT_PROBE into its own file. probe_helper = probe.ProbePointsHelper(self.config, self.probe_finalize, []) @@ -794,12 +931,61 @@ def percentile_filter(data, margin=5.): #z5 = percentile_filter(z, 5.) #self.gcode.respond_info(f"mean5: {np.mean(z5):.5f} median5: {np.median(z5):.5f}") - fn = "/tmp/multi-point-probe-" + time.strftime("%Y%m%d") + ".csv" - with open(fn, "a") as f: - f.write(",".join([str(v) for v in z])) - f.write("\n") + #fn = f"/tmp/multi-point-probe{self.mpp_filename_suffix}.csv" + #with open(fn, "a") as f: + # f.write(",".join([str(v) for v in z])) + # f.write("\n") + self.append_to_mpp_file(positions, offsets) return 'done' + + def append_to_mpp_file(self, positions, offsets): + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_append_to_mpp_file(positions, offsets, self.mpp_save_meta, self.mpp_filename_suffix)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise Exception("Error appending data to npz file: %s" % (result,)) + else: + is_inner_err, inner_result = result + if is_inner_err: + raise self.gcode.error(inner_result) + else: + return inner_result + + @staticmethod + def _do_append_to_mpp_file(positions, offsets, meta, filename_suffix): + def get_save_map(i): + return { + f'positions_{i}': positions, + f'offsets_{i}': offsets + } | {f'{k}_{i}': np.asanyarray(v) for k,v in meta.items()} + + fn = f"/tmp/multi-point-probe{filename_suffix}.npz" + if os.path.exists(fn): + with np.load(fn) as npz: + count = int(npz['count']) + np.savez_compressed( fn, count=np.array(count+1), **{k:v for k,v in npz.items() if k != 'count'}, **get_save_map(count) ) + else: + np.savez_compressed( fn, count=np.array(1), **get_save_map(0) ) + return (False, None) ##### # Loader From 300c3b03d0e1ec2b9ddea2b471884ea257fd8462 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 21 May 2025 12:08:50 +0100 Subject: [PATCH 044/139] Extras/Beacon: added beacon_true_zero_correction module --- .../klippy/beacon_true_zero_correction.py | 319 ++++++++++++++++++ configuration/scripts/ratos-common.sh | 1 + 2 files changed, 320 insertions(+) create mode 100644 configuration/klippy/beacon_true_zero_correction.py diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py new file mode 100644 index 000000000..5dad7a424 --- /dev/null +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -0,0 +1,319 @@ +# Improve Beacon true zero consistency +# +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import math, logging +import numpy as np +from . import probe + + +# NOTE: Not tested with multi-beacon setup. The design seeks to pass through the SENSOR argument, so multi-beacon +# *might* work, but this has not yet been tested. + +BEACON_AUTO_CALIBRATE = 'BEACON_AUTO_CALIBRATE' +RATOS_TITLE = 'BEACON_AUTO_CALIBRATE Multi-point Probing' + +class BeaconTrueZeroCorrection: + def __init__(self, config): + self.config = config + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.gcode = self.printer.lookup_object('gcode') + self.name = config.get_name() + + self.status = None + self.ratos = None + self.gm_ratos = None + self.ratos_z_offset = None + self.toolhead = None + self.dual_carriage = None + self.orig_cmd = None + + ####### + # Config + ####### + + # z values greater than z_rejection_threshold are rejected. These typically correspond to early triggering + # of beacon contact before the nozzle has touched the bed. From test data, these are rare. Only 0.028% of samples + # exceeded 75um (from over 32,000 samples across multiple machines and print surfaces). + self.z_rejection_threshold = config.getfloat('z_rejection_threshold', 0.075, minval=0.03) + + # The number of times to probe an additional point if any z values are rejected. + self.max_retries = config.getint('max_retries', 10, minval=0, maxval=15) + + # If true, each of the multiple probe locations will itself be probed several times using + # the standard beacon error detection logic. From extensive testing, this mode offers no benefit + # and should not be used. It is included only as an option for diagnostic purposes. + self.use_error_corrected_probing = config.getboolean('use_error_corrected_probing', False) + + if config.has_section('beacon'): + self.printer.register_event_handler("klippy:connect", + self._handle_connect) + self.printer.register_event_handler("homing:home_rails_end", + self._handle_homing_move_end) + self.printer.register_event_handler("stepper_enable:motor_off", + self._handle_motor_off) + + else: + logging.info(f"{self.name}: beacon is not configured, beacon true zero correction disabled.") + + def _handle_connect(self): + self.ratos = self.printer.lookup_object('ratos') + self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') + self.ratos_z_offset = self.printer.lookup_object('ratos_z_offset') + self.toolhead = self.printer.lookup_object("toolhead") + + if self.config.has_section("dual_carriage"): + self.dual_carriage = self.printer.lookup_object("dual_carriage", None) + + self.orig_cmd = self.gcode.register_command(BEACON_AUTO_CALIBRATE, None) + if self.orig_cmd == None: + raise self.printer.config_error(f"{BEACON_AUTO_CALIBRATE} command is not registered, {self.name} cannot be enabled. Ensure that [beacon] occurs before [{self.name}] in the configuration.") + + self.gcode.register_command( + BEACON_AUTO_CALIBRATE, + self.cmd_BEACON_AUTO_CALIBRATE, + desc=self.desc_BEACON_AUTO_CALIBRATE) + + def _handle_homing_move_end(self, homing_state, rails): + # Clear the true zero correction offset if the Z axis is homed. + # Any existing true zero correction is invalidated when z is re-homed. + if 2 in homing_state.get_axes(): + self.ratos_z_offset.set_offset('true_zero_correction', 0) + + def _handle_motor_off(self, print_time): + # Clear the true zero correction offset if motors are disabled. + # Any existing true zero correction is invalidated when z is disabled. + self.ratos_z_offset.set_offset('true_zero_correction', 0) + + ###### + # commands + ###### + def _check_homed(self, msg = 'Must home all axes first'): + status = self.toolhead.get_status(self.reactor.monotonic()) + homed_axes = status["homed_axes"] + if any(axis not in homed_axes for axis in "xyz"): + raise self.gcode.error( msg ) + + desc_BEACON_AUTO_CALIBRATE = "Automatically calibrates the Beacon probe. Extended with RatOS multi-point probing for improved true zero consistency. Use SKIP_MULTIPOINT_PROBING=1 to bypass." + def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): + # Clear existing offset + self.ratos_z_offset.set_offset('true_zero_correction', 0) + + skip = gcmd.get('SKIP_MULTIPOINT_PROBING', '').lower() in ('1', 'true', 'yes') + if skip: + return self.orig_cmd(gcmd) + + zero_xy = self.toolhead.get_position()[:2] + retval = self.orig_cmd(gcmd) + self._check_homed() + ps = ProbingSession(self, gcmd, zero_xy, self.max_retries) + ps.run() + + return retval + +class ProbingSession: + + def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position, max_retries = 10): + self.gcmd = gcmd + self.tzc = tzc + self.zero_xy_position = zero_xy_position + self.max_retries = max_retries + self.retries = 0 + self.probe_helper = probe.ProbePointsHelper(self.tzc.config, self._probe_finalize, []) + self._finalize_result = None + self._has_run = False + self._points = None + self._next_points_index = 0 + + # NOTE: The following values are hard-coded for now, but could be made configurable in the future. + + # The take-7-drop-4-max approach was determined from extensive testing, with a wide range of + # print surfaces and printers. Statistical analysis of the data shows that this approach + # provides a significantly-enhanced level of confidence that the true zero correction is + # accurate and has significantly increased immunity to local location-dependent variation + # in probe results. + + # Number of samples to take, including the implied zero sample from BEACON_AUTO_CALIBRATE + self._take = 7 + # Number of maximal-valued samples to discard + self._drop_top = 4 + # The zero-value initial sample is implied from BEACON_AUTO_CALIBRATE, which is expected to have + # been invoked immediatley prior to this command, at the same location. + self._samples = [0.] + + def run(self): + if self._has_run: + raise Exception("ProbingSession has already been run, and cannot be run more than once.") + self._has_run = True + + num_points_to_generate = self._take - len(self._samples) + self.max_retries + min_span = 9. + + nozzle_tip_dia = self._get_nozzle_tip_diameter() + + # Calculate the nozzle-based min span as the length of the side of a + # square with area four times the footprint of COUNT nozzle tips. + nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * num_points_to_generate * 4.) + span = max(min_span, nozzle_based_min_span) + half_span = span / 2. + + logging.info(f"{self.tzc.name}: count: {num_points_to_generate} min_span: {min_span} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_span: {nozzle_based_min_span:.2f} use_span: {span:.2f}") + + # Calculate probing region + range_x = (self.zero_xy_position[0] - half_span, self.zero_xy_position[0] + half_span) + range_y = (self.zero_xy_position[1] - half_span, self.zero_xy_position[1] + half_span) + + self._validate_probing_region(range_x, range_y, span) + + probe_gcmd = self._prepare_probe_command() + + self._points = self._generate_points(num_points_to_generate, range_x, range_y, nozzle_tip_dia) + self._next_points_index = self._take - len(self._samples) + self.probe_helper.update_probe_points(self._points[:self._next_points_index], 1) + self.probe_helper.start_probe(probe_gcmd) + + self._finalize() + + def _finalize(self): + if self._finalize_result == 'retry': + self.tzc.ratos.console_echo( + RATOS_TITLE, + 'error', + 'One or more z values were out of range, maximum retries exceeded.') + raise self.gcmd.error('One or more z values were out of range, maximum retries exceeded.') + elif isinstance(self._finalize_result, float): + if self._finalize_result < -0.2: + # Sanity check to reduce the risk of bed damage + self.tzc.ratos.console_echo( + RATOS_TITLE, + 'error', + f'The measured true zero correction {self._finalize_result:.6f} is below the safety limit of -0.2mm._N_This is not expected behaviour.') + raise self.gcmd.error(f'Measured correction is below safety limit') + logging.info(f'{self.tzc.name}: applying correction {self._finalize_result:.6f}') + self.gcmd.respond_info(f'Applying true zero correction of {self._finalize_result*1000.:.1f} µm') + self.tzc.ratos_z_offset.set_offset('true_zero_correction', self._finalize_result) + else: + raise ValueError('Internal error: unexpected value for _finalize_result') + + def _validate_probing_region(self, range_x, range_y, span): + printable_x = (self.tzc.gm_ratos.variables.get('printable_x_min'), self.tzc.gm_ratos.variables.get('printable_x_max')) + printable_y = (self.tzc.gm_ratos.variables.get('printable_y_min'), self.tzc.gm_ratos.variables.get('printable_y_max')) + + def in_range(r, value): + return r[0] <= value <= r[1] + + if not ( + in_range(printable_x, range_x[0]) and in_range(printable_x, range_x[1]) and + in_range(printable_y, range_y[0]) and in_range(printable_y, range_y[1])): + + self.tzc.console_echo(RATOS_TITLE, 'error', f'The required probing region ({span:.1f}x{span:.1f}) would probe outside the printable area.') + raise self.gcmd.error('The required probing region would probe outside the printable area') + + def _prepare_probe_command(self): + probe_args = dict( + PROBE_METHOD='contact', + SAMPLES='1', + SAMPLES_DROP='0' + ) if not self.tzc.use_error_corrected_probing else dict( + PROBE_METHOD='contact', + SAMPLES='3', + SAMPLES_DROP='1', + SAMPLES_TOLERANCE_RETRIES='10' + ) + + sensor = self.gcmd.get('SENSOR', None) + if sensor: + probe_args['SENSOR'] = sensor + + return self.tzc.gcode.create_gcode_command( + self.gcmd.get_command(), + self.gcmd.get_command() + + "".join(" " + k + "=" + v for k, v in probe_args.items()), + probe_args + ) + + def _probe_finalize(self, _, positions): + zvals = [p[2] for p in positions] + logging.info(f'{self.tzc.name}: probed z-values: {", ".join(f"{z:.6f}" for z in zvals)}') + good = [z for z in zvals if z < self.tzc.z_rejection_threshold] + self._samples.extend(good) + if len(self._samples) == self._take: + # Gathered enough good samples + self._samples.sort() + use_samples = self._samples[:-self._drop_top] + logging.info(f'{self.tzc.name}: samples: {", ".join(f"{z:.6f}" for z in self._samples)} using: {", ".join(f"{z:.6f}" for z in use_samples)}') + self._finalize_result = float(np.mean(use_samples)) + return 'done' + + rejects = [z for z in zvals if z >= self.tzc.z_rejection_threshold] + logging.info(f'{self.tzc.name}: rejected z-values: {", ".join(f"{z:.6f}" for z in rejects)}') + + if self._next_points_index + len(rejects) <= len(self._points): + self.retries += 1 + self.gcmd.respond_info(f'{len(rejects)} z value(s) were out of range, probing additional point(s)') + logging.info(f'{self.tzc.name}: will probe additional {len(rejects)}') + self.probe_helper.update_probe_points(self._points[self._next_points_index:self._next_points_index + len(rejects)], 1) + self._next_points_index += len(rejects) + return 'retry' + + self.gcmd.respond_info(f'{len(rejects)} z value(s) were out of range, exceeding the number of available retry points.') + self._finalize_result = 'retry' + return 'done' + + def _generate_points(self, n, x_lim, y_lim, min_dist, avoid_centre=True, max_iter=1000): + points = [] + centre = [np.mean(x_lim), np.mean(y_lim)] + iterations = 0 + + while len(points) < n and iterations < max_iter: + # Generate a candidate point uniformly within the given x and y limits. + candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), + np.random.uniform(y_lim[0], y_lim[1])]) + + # Check that candidate is at least min_dist away from every existing point. + if ((not avoid_centre) or np.linalg.norm(candidate - centre) >= min_dist) \ + and all(np.linalg.norm(candidate - p) >= min_dist for p in points): + points.append(candidate.tolist()) # don't leak numpy types + + iterations += 1 + + if len(points) < n: + raise self.gcode.error( + "Could not generate all required probe points within the specified iteration limit. " + "The conditions are too strict.") + + return points + + def _get_nozzle_diameter(self): + extruder_name = 'extruder' + + if self.tzc.dual_carriage and self.tzc.dual_carriage.dc[1].mode.lower() == 'primary': + extruder_name = 'extruder1' + + extruder = self.tzc.printer.lookup_object(extruder_name) + nozzle_diameter = extruder.nozzle_diameter + return nozzle_diameter + + def _get_nozzle_tip_diameter(self, nozzle_diameter=None): + if nozzle_diameter is None: + nozzle_diameter = self._get_nozzle_diameter() + + # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), + # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. + # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier + # of 1.56, or an increase of 1.35. Here we make some effort at a reasonable approximation. + if nozzle_diameter < 0.25: + nozzle_tip_dia = 1.5 * nozzle_diameter + elif nozzle_diameter <= 0.8: + nozzle_tip_dia = 2.5 * nozzle_diameter + else: + nozzle_tip_dia = nozzle_diameter + 1.35 + + return nozzle_tip_dia + +# Register the configuration +def load_config(config): + return BeaconTrueZeroCorrection(config) \ No newline at end of file diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index f538ce245..b8bc45c45 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -192,6 +192,7 @@ verify_registered_extensions() ["ratos_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos.py") ["beacon_mesh_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_mesh.py") ["ratos_z_offset_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos_z_offset.py") + ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") ) declare -A kinematics_extensions=( From d88f000e7973b7c81add6a1d0ba3db2479c07205 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 24 May 2025 14:58:06 +0100 Subject: [PATCH 045/139] Mesh/Beacon: use local low filter for compensation mesh, and general code cleanup --- configuration/klippy/beacon_mesh.py | 236 ++++++++-------------------- 1 file changed, 69 insertions(+), 167 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 6f06d0c84..485cdfaca 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -2,9 +2,8 @@ from . import bed_mesh as BedMesh import numpy as np from scipy.ndimage import gaussian_filter -from scipy.interpolate import RectBivariateSpline -## TESTING rapid-contact-rapid comp mesh generation +# Temporary mesh names RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" RATOS_TEMP_SCAN_MESH_ATFER_NAME = "__BEACON_TEMP_SCAN_MESH_AFTER__" @@ -276,27 +275,20 @@ def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) - probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=3) + # Using minval=4 to avoid BedMesh defaulting to using Lagrangian interpolation which appears to be broken + probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=4) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - # TODO: Remove TESTING stuff before release - method = gcmd.get('TESTING_GENERATION_METHOD', self.gm_ratos.variables.get('testing_default_compensation_mesh_generation_method')) - - if method and method.strip().lower() == 'temporal_blend': - gcmd.respond_info("TESTING: using rapid-contact-rapid temporal blend") - self.create_compensation_mesh_TESTING_rapid_contact_rapid(gcmd, profile, probe_count) - else: - self.create_compensation_mesh(profile, probe_count) + self.create_compensation_mesh(profile, probe_count) desc_REMAKE_BEACON_COMPENSATION_MESH = "TESTING! PROFILE='exising comp mesh' NEW_PROFILE='new name' [GAUSSIAN_SIGMA=x]" def cmd_REMAKE_BEACON_COMPENSATION_MESH(self, gcmd): profile = gcmd.get('PROFILE') new_profile = gcmd.get('NEW_PROFILE') - gaussian_sigma = gcmd.get_float('GAUSSIAN_SIGMA', self.gm_ratos.variables.get('testing_default_compensation_mesh_gaussian_sigma')) - self.TESTING_remake_compensation_mesh(profile, new_profile, gaussian_sigma) + self.TESTING_remake_compensation_mesh(profile, new_profile) desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): @@ -486,7 +478,7 @@ def apply_scan_compensation(self, compensation_zmesh) -> bool: measured_zmesh.build_mesh(new_points) # NB: build_mesh does not replace or mutate its params, so no need to reassign measured_mesh_params. - measured_mesh_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + measured_mesh_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATED self.bed_mesh.save_profile(measured_mesh_name) self.bed_mesh.set_mesh(measured_zmesh) @@ -499,94 +491,13 @@ def apply_scan_compensation(self, compensation_zmesh) -> bool: self.ratos.console_echo(error_title, "error", str(e)) return False - def create_compensation_mesh(self, profile, probe_count): - if not self.beacon: - self.ratos.console_echo("Create compensation mesh error", "error", - "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") - return - - if self.z_tilt and not self.z_tilt.z_status.applied: - self.ratos.console_echo("Create compensation mesh warning", "warning", - "Z-tilt levelling is configured but has not been applied._N_" - "This may result in inaccurate compensation.") - - if self.qgl and not self.qgl.z_status.applied: - self.ratos.console_echo("Create compensation mesh warning", "warning", - "Quad gantry levelling is configured but has not been applied._N_" - "This may result in inaccurate compensation.") - - beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' - - # Go to safe home - self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") - - if beacon_contact_calibrate_model_on_print: - # Calibrate a fresh model - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") - else: - if self.beacon.model is None: - self.ratos.console_echo("Create compensation mesh error", "error", - "No active Beacon model is selected._N_Make sure you've performed initial Beacon calibration.") - return - - self.check_active_beacon_model_temp(title="Create compensation mesh warning") - - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1") - - # create contact mesh - self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=2 SAMPLES_DROP=1 SAMPLES_TOLERANCE_RETRIES=10 " - "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], RATOS_TEMP_CONTACT_MESH_NAME)) - - # create temp scan mesh - self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE METHOD=automatic USE_CONTACT_AREA=1 " - "PROBE_COUNT=%d,%d PROFILE='%s'" % (probe_count[0], probe_count[1], RATOS_TEMP_SCAN_MESH_NAME)) - - self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % RATOS_TEMP_SCAN_MESH_NAME) - scan_mesh_points = self.bed_mesh.pmgr.get_profiles()[RATOS_TEMP_SCAN_MESH_NAME]["points"] - self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % RATOS_TEMP_CONTACT_MESH_NAME) - contact_mesh_points = self.bed_mesh.pmgr.get_profiles()[RATOS_TEMP_CONTACT_MESH_NAME]["points"] - compensation_mesh_points = [] - - try: - - for y in range(len(contact_mesh_points)): - compensation_mesh_points.append([]) - for x in range(len(contact_mesh_points[0])): - contact_z = contact_mesh_points[y][x] - scan_z = scan_mesh_points[y][x] - offset_z = contact_z - scan_z - self.ratos.debug_echo("Create compensation mesh", - "scan: %0.4f contact: %0.4f offset: %0.4f" % (scan_z, contact_z, offset_z)) - compensation_mesh_points[y].append(offset_z) - - # Create new mesh - params = self.bed_mesh.z_mesh.get_mesh_params() - params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION - params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() - params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION - params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC - new_mesh = BedMesh.ZMesh(params, profile) - new_mesh.build_mesh(compensation_mesh_points) - self.bed_mesh.set_mesh(new_mesh) - self.bed_mesh.save_profile(profile) - - # Remove temp meshes - self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % RATOS_TEMP_CONTACT_MESH_NAME) - self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % RATOS_TEMP_SCAN_MESH_NAME) - - self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) - except BedMesh.BedMeshError as e: - self.ratos.console_echo("Create compensation mesh error", "error", str(e)) - - def _apply_filter(self, data, extrapolate_sigma=0.75, sigma=0.75, pad=3): + def _apply_filter(self, data): parent_conn, child_conn = multiprocessing.Pipe() def do(): try: child_conn.send( - (False, self._do_apply_filter(np.array(data), extrapolate_sigma, sigma, pad)) + (False, self._do_local_low_filter(np.array(data))) ) except Exception: child_conn.send((True, traceback.format_exc())) @@ -606,61 +517,54 @@ def do(): raise Exception("Error applying filter: %s" % (result,)) else: return result - - @staticmethod - def _do_apply_filter(data, extrapolate_sigma, sigma, pad): - # This enhanced filter routine seeks to reduce edge distortion that occurs when - # a filter kernel consumes values beyond the edge of the defined data (ie, as - # per the 'mode' argument). - - # Pre-filter the data to compute a smoothed, noise-reduced model, using a mode - # that minimizes boundary artifacts on the interior. - filtered_interior = gaussian_filter(data, sigma=extrapolate_sigma, mode='nearest') - - # Extrapolate the filtered surface. - # Determine a pad size that roughly covers the effective kernel support. - # For sigma=0.75, a pad of about 3 pixels is a reasonable starting point. - - # Coordinates for original grid: - x = np.arange(data.shape[0]) - y = np.arange(data.shape[1]) - - # Define an extended grid that covers the original plus the padding - x_ext = np.arange(x[0] - pad, x[-1] + pad + 1) - y_ext = np.arange(y[0] - pad, y[-1] + pad + 1) - - # Trim the edges of the pre-filtered data as these will include distorted edge gradients - # from the filter which used 'nearest' mode. - trim = int(np.ceil(sigma*2)) - trimmed_filtered_interior = filtered_interior[trim:-trim, trim:-trim] - - x_trimmed = x[trim:-trim] - y_trimmed = y[trim:-trim] - - # Fit a 2D cubic spline to the trimmed filtered data. Use degree 1 to avoid wild values at - # extrapolated corners. - spline = RectBivariateSpline(x_trimmed, y_trimmed, trimmed_filtered_interior, kx=1, ky=1, - bbox= [x_ext[0], x_ext[-1], y_ext[0], y_ext[-1]]) - - # Create the extrapolated surface - surface_extended = spline(x_ext, y_ext) - - # Replace the interior with the original data - surface_extended[x[0] + pad:x[-1] + pad + 1, y[0] + pad:y[-1] + pad + 1] = data - - # Apply the Gaussian filter to the extended data. When consuming values beyond the edges - # of the original data, the kernel will use the extrapolated values we created - # above, which are a better approximation than any of the standard modes. - filtered_extended = gaussian_filter(surface_extended, sigma=sigma, mode='nearest') - - # Crop the filtered array back to the original shape. - result = filtered_extended[pad:-pad, pad:-pad] - - # 'result' now should display edges that more faithfully follow the filtered curvature. - return result - - def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, probe_count): + @staticmethod + def _do_local_low_filter(data, lowpass_sigma=1., num_keep=4, num_keep_edge=3, num_keep_corner=2): + # 1. Low-pass filter to obtain general shape + lowpass = gaussian_filter(data, sigma=lowpass_sigma, mode='nearest') + + # 2. Subtract the low-pass filtered version from the original + # to get the high-frequency details + high_freq_details = data - lowpass + + # 3. Prepare a new array of the same shape as the original + filtered_data = np.zeros_like(data) + + # 4. For each point in the original array: + rows, cols = data.shape + for i in range(rows): + for j in range(cols): + # Get the 3x3 neighborhood around the current point within the high-frequency details + neighbours = [] + neighbour_coords = [] + neighbour_distances = [] + for di in [-1, 0, 1]: + for dj in [-1, 0, 1]: + ni, nj = i + di, j + dj + if 0 <= ni < rows and 0 <= nj < cols: + neighbours.append(high_freq_details[ni, nj]) + neighbour_coords.append((ni, nj)) + neighbour_distances.append((di**2 + dj**2)**0.5) + + # Identify the indices of the N lowest values from the neighborhood + lowest_indices = np.argsort(neighbours)[:num_keep if len(neighbours) > 6 else num_keep_edge if len(neighbours) > 4 else num_keep_corner] + + # Select the corresponding values from the original array + lowest_values = [data[neighbour_coords[idx]] for idx in lowest_indices] + + # Select the corresponding distances + lowest_values_distances = [neighbour_distances[idx] for idx in lowest_indices] + + # Calculate weights for the lowest values based on their distances + lowest_values_weights = [1.0 / (d + 1) for d in lowest_values_distances] + + # Set the current point in the new array to the weighted average of these lowest values + filtered_data[i, j] = np.average(lowest_values, weights=lowest_values_weights) + + # 5. Return the new array. Don't leak numpy types to the caller. + return filtered_data.tolist() + + def create_compensation_mesh(self, gcmd, profile, probe_count): if not self.beacon: self.ratos.console_echo("Create compensation mesh error", "error", "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") @@ -676,12 +580,11 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr "Quad gantry levelling is configured but has not been applied._N_" "This may result in inaccurate compensation.") - gaussian_sigma = gcmd.get_float('GAUSSIAN_SIGMA', self.gm_ratos.variables.get('testing_default_compensation_mesh_gaussian_sigma')) keep_temp_meshes = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') - samples = gcmd.get_int('SAMPLES', 2) - samples_drop = gcmd.get_int('SAMPLES_DROP', 1) + samples = gcmd.get_int('SAMPLES', 1) + samples_drop = gcmd.get_int('SAMPLES_DROP', 0) - gcmd.respond_info(f"keep_temp_meshes: {keep_temp_meshes}, gaussian_sigma: {gaussian_sigma}, samples: {samples} samples_drop: {samples_drop}") + gcmd.respond_info(f"keep_temp_meshes: {keep_temp_meshes}, samples: {samples} samples_drop: {samples_drop}") beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' @@ -690,7 +593,7 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr if beacon_contact_calibrate_model_on_print: # Calibrate a fresh model - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE") + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1") else: if self.beacon.model is None: self.ratos.console_echo("Create compensation mesh error", "error", @@ -699,7 +602,7 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr self.check_active_beacon_model_temp(title="Create compensation mesh warning") - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1") + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 SKIP_MODEL_CREATION=1") mesh_before_name = RATOS_TEMP_SCAN_MESH_BEFORE_NAME if not keep_temp_meshes else profile + "_SCAN_BEFORE" mesh_after_name = RATOS_TEMP_SCAN_MESH_ATFER_NAME if not keep_temp_meshes else profile + "_SCAN_AFTER" @@ -730,10 +633,9 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) - if gaussian_sigma is not None and gaussian_sigma > 0: - self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma * 2., gaussian_sigma, int(np.ceil(gaussian_sigma * 4.))) - contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" + self.ratos.debug_echo("Create compensation mesh", "Filtering contact mesh") + contact_mesh_points = self._apply_filter(contact_mesh_points) + contact_params[RATOS_MESH_NOTES_PARAMETER] = "contact mesh filtered using local low filter" compensation_mesh_points = [] @@ -774,12 +676,12 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") - eventtime = self.reactor.pause(eventtime + 0.05) + self.reactor.pause(self.reactor.NOW) # For a large mesh (eg, 60x60) this can take 2+ minutes #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) - if keep_temp_meshes and gaussian_sigma is not None and gaussian_sigma > 0: + if keep_temp_meshes: params = contact_params.copy() filtered_profile = contact_mesh_name + "_filtered" new_mesh = BedMesh.ZMesh(params, filtered_profile) @@ -808,6 +710,7 @@ def create_compensation_mesh_TESTING_rapid_contact_rapid(self, gcmd, profile, pr except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + # TODO: Remove testing stuff before release def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma=None): mesh_before_name = profile + "_SCAN_BEFORE" @@ -823,10 +726,9 @@ def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma= contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) - if gaussian_sigma is not None and gaussian_sigma > 0: - self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh with sigma={gaussian_sigma:.4f}") - contact_mesh_points = self._apply_filter(contact_mesh_points, gaussian_sigma * 2., gaussian_sigma, int(np.ceil(gaussian_sigma * 4.))) - contact_params[RATOS_MESH_NOTES_PARAMETER] = f"contact mesh gaussian filtered with sigma={gaussian_sigma:.4f}" + self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh") + contact_mesh_points = self._apply_filter(contact_mesh_points) + contact_params[RATOS_MESH_NOTES_PARAMETER] = "contact mesh filtered using local low filter" compensation_mesh_points = [] From 1605c594070b1fb3d830b19fbcd05a853674a1bf Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 24 May 2025 15:04:25 +0100 Subject: [PATCH 046/139] Mesh/Beacon: change beacon_scan_compensation_resolution to 8 to suit new compensation mesh methodology --- configuration/z-probe/beacon.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 7349818ff..960f24f4b 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -65,7 +65,7 @@ variable_beacon_contact_z_tilt_adjust_samples: 2 # probe samples for cont variable_beacon_scan_compensation_enable: False # Enables beacon scan compensation variable_beacon_scan_compensation_profile: "Beacon Scan Compensation" # The bed mesh profile name for the scan compensation mesh -variable_beacon_scan_compensation_resolution: 40 # The mesh resolution in mm for scan compensation +variable_beacon_scan_compensation_resolution: 8 # The mesh resolution in mm for scan compensation variable_beacon_scan_compensation_bed_temp_mismatch_is_error: False # If True, attempting to use a compensation mesh calibrated for a significantly # different bed temperature will raise an error. Otherwise, a warning is reported. From c15ada3a2506ae59e7b18de189d1ad26a7dd9e3f Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 24 May 2025 15:46:32 +0100 Subject: [PATCH 047/139] Mesh/Beacon: add warnings to sanity check - Beacon contact can show location-dependent noise of 10's of microns. Use of contact without noise mitigation is not recommended (z-tilt, START_PRINT contact mesh) - The 'automatic' (pause-and-measure) proximity probing method produces data that's not like-for-like compatible with our new-style contact compensation meshes. --- configuration/macros/mesh.cfg | 48 ++++++++++++++++++++++++----------- configuration/macros/util.cfg | 5 ++-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 281e52729..9bc682985 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -12,24 +12,42 @@ variable_adaptive_mesh: True # True|False = enable adaptive [gcode_macro _BED_MESH_SANITY_CHECK] gcode: - {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} + {% if printer.configfile.settings.bed_mesh.zero_reference_position is defined %} + {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} + {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} + {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} + {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} + {% set safe_home_x = printable_x_max / 2 %} + {% endif %} + {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} + {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} + {% set safe_home_y = printable_y_max / 2 %} + {% endif %} + {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} + + {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} + {% if zero_ref_pos is not defined or zero_ref_pos[0]|float|round != safe_home_x|float|round or zero_ref_pos[1]|float|round != safe_home_y|float|round %} + CONSOLE_ECHO TYPE="error" TITLE="Zero reference position does not match safe home position" MSG="Please update your bed mesh zero reference position in printer.cfg, like so:_N__N_[bed_mesh]_N_zero_reference_position: {safe_home_x|float|round},{safe_home_y|float|round}_N_" + _STOP_AND_RAISE_ERROR MSG="Zero reference position does not match safe home position" + {% endif %} + {% endif %} {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} + + {% set beacon_contact_bed_mesh = true if printer["gcode_macro RatOS"].beacon_contact_bed_mesh|default(false)|lower == 'true' else false %} + {% set beacon_scan_method_automatic = true if printer["gcode_macro RatOS"].beacon_scan_method_automatic|default(false)|lower == 'true' else false %} + {% set beacon_contact_z_tilt_adjust = true if printer["gcode_macro RatOS"].beacon_contact_z_tilt_adjust|default(false)|lower == 'true' else false %} + + {% if beacon_contact_bed_mesh %} + CONSOLE_ECHO TYPE="warning" TITLE="Beacon contact bed mesh is enabled" MSG="Beacon contact bed mesh is enabled. This is not recommended for normal use and may result in inaccurate bed mesh calibration._N_Please disable it in your printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_bed_mesh: False" {% endif %} - {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} - {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} - {% if zero_ref_pos is not defined or zero_ref_pos[0]|float|round != safe_home_x|float|round or zero_ref_pos[1]|float|round != safe_home_y|float|round %} - CONSOLE_ECHO TYPE="error" TITLE="Zero reference position does not match safe home position" MSG="Please update your bed mesh zero reference position in printer.cfg, like so:_N__N_[bed_mesh]_N_zero_reference_position: {safe_home_x|float|round},{safe_home_y|float|round}_N_" - _STOP_AND_RAISE_ERROR MSG="Zero reference position does not match safe home position" - {% endif %} + {% if beacon_scan_method_automatic %} + CONSOLE_ECHO TYPE="warning" TITLE="Beacon 'automatic' scan method is enabled" MSG="Beacon 'automatic' scan method is enabled. This is not recommended for normal use and may result in inaccurate bed mesh calibration._N_Please disable it in your printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_scan_method_automatic: False" + {% endif %} + + {% if beacon_contact_z_tilt_adjust %} + CONSOLE_ECHO TYPE="warning" TITLE="Beacon contact z-tilt adjust is enabled" MSG="Beacon contact z-tilt adjust is enabled. This is not recommended for normal use and may result in inaccurate z-tilt adjustment._N_Please disable it in your printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_z_tilt_adjust: False" {% endif %} [gcode_macro _START_PRINT_PREFLIGHT_CHECK_BED_MESH] diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index 6f20d55a8..dd2ed92b7 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -53,12 +53,11 @@ gcode: CALCULATE_PRINTABLE_AREA INITIAL_FRONTEND_UPDATE _CHAMBER_FILTER_SANITY_CHECK - {% if printer.configfile.settings.beacon is defined and printer.configfile.settings.bed_mesh.zero_reference_position is defined %} - # Enforce valid zero_reference position configuration. + {% if printer.configfile.settings.beacon is defined %} + # Enforce valid zero_reference position configuration, warn about problematic settings. _BED_MESH_SANITY_CHECK {% endif %} - [delayed_gcode RATOS_LOGO] initial_duration: 2 gcode: From d6744fd7c3230cdd03af76240f9ab26dc672fa16 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 25 May 2025 12:47:34 +0100 Subject: [PATCH 048/139] Macros/Beacon: add [beacon_true_zero_correction] --- configuration/z-probe/beacon.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 960f24f4b..887cdb36c 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -33,6 +33,12 @@ contact_max_hotend_temperature: 275 ##### [beacon_mesh] +##### +# BEACON TRUE ZERO CORRECTION +##### +[ratos_z_offset] +[beacon_true_zero_correction] + # TODO: remove when automatically calculated by configurator [bed_mesh] mesh_min: 20,30 From 007640c4091808e99b499d709f69ff6584d54949 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 26 May 2025 16:11:36 +0100 Subject: [PATCH 049/139] Moonraker: use tg73 fork of Klipper with required bed_mesh fixes. --- configuration/moonraker.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configuration/moonraker.conf b/configuration/moonraker.conf index 723cd58e3..a222d94af 100644 --- a/configuration/moonraker.conf +++ b/configuration/moonraker.conf @@ -112,7 +112,10 @@ info_tags: [update_manager klipper] channel: dev -pinned_commit: b7233d1197d9a2158676ad39d02b80f787054e20 +origin: https://github.com/tg73/klipper.git +pinned_commit: 93c8ad8a017c45f4502e73ad1dc174cf7d3c3b90 +info_tags: + desc=Testing: tg73 Klipper fork with bed_mesh fixes [update_manager moonraker] channel: dev From 0ae402eb9536e1a88e34187909f830f9df54300a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 26 May 2025 16:48:29 +0100 Subject: [PATCH 050/139] Moonraker: remove bogus fields from [update_manager klipper] --- configuration/moonraker.conf | 3 --- 1 file changed, 3 deletions(-) diff --git a/configuration/moonraker.conf b/configuration/moonraker.conf index a222d94af..e2bfb834a 100644 --- a/configuration/moonraker.conf +++ b/configuration/moonraker.conf @@ -112,10 +112,7 @@ info_tags: [update_manager klipper] channel: dev -origin: https://github.com/tg73/klipper.git pinned_commit: 93c8ad8a017c45f4502e73ad1dc174cf7d3c3b90 -info_tags: - desc=Testing: tg73 Klipper fork with bed_mesh fixes [update_manager moonraker] channel: dev From d76bf0a99b8de18fa1cb0ae0abe0ba3df1396083 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 26 May 2025 19:49:17 +0100 Subject: [PATCH 051/139] Beacon/Mesh: fix missing argument --- configuration/klippy/beacon_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 485cdfaca..1b984bbab 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -282,7 +282,7 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - self.create_compensation_mesh(profile, probe_count) + self.create_compensation_mesh(gcmd, profile, probe_count) desc_REMAKE_BEACON_COMPENSATION_MESH = "TESTING! PROFILE='exising comp mesh' NEW_PROFILE='new name' [GAUSSIAN_SIGMA=x]" def cmd_REMAKE_BEACON_COMPENSATION_MESH(self, gcmd): From 23b8bdc6f38c414b9bc7fe803bee3753c35d5abd Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 26 May 2025 19:53:25 +0100 Subject: [PATCH 052/139] Mesh/Beacon: by default don't log mesh points during BED_MESH_CALIBRATE --- configuration/z-probe/beacon.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 887cdb36c..1b20510f4 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -39,9 +39,9 @@ contact_max_hotend_temperature: 275 [ratos_z_offset] [beacon_true_zero_correction] -# TODO: remove when automatically calculated by configurator [bed_mesh] -mesh_min: 20,30 +mesh_min: 20,30 # TODO: remove when automatically calculated by configurator +log_points: False ##### # BEACON CONFIGURATION From 9affdfd5f717d16c24d92d6fcee990b1bde24116 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 26 May 2025 20:36:12 +0100 Subject: [PATCH 053/139] Configuration: default [bed_mesh] split_z_delta to 0.01 in base.cfg to avoid visible surface stripe artefacts --- configuration/printers/base.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration/printers/base.cfg b/configuration/printers/base.cfg index 3332791d7..6598535ea 100644 --- a/configuration/printers/base.cfg +++ b/configuration/printers/base.cfg @@ -41,3 +41,6 @@ enable_force_move: True allow_unknown_gcode_generator: True [exclude_object] + +[bed_mesh] +split_delta_z: 0.01 # Avoid visible surface stripe arfetacts \ No newline at end of file From a3cb1260e1dec7f25ac284d765411fb82118322e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 28 May 2025 16:42:09 +0100 Subject: [PATCH 054/139] Beacon/TrueZero: allow [beacon_true_zero_correction] to be disabled by config --- .../klippy/beacon_true_zero_correction.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index 5dad7a424..a88833b31 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -35,6 +35,9 @@ def __init__(self, config): # Config ####### + # Allow the true zero correction to be disabled. This is useful for testing and debugging, and as an esacpe hatch. + self.disabled = config.getboolean('disabled', False) + # z values greater than z_rejection_threshold are rejected. These typically correspond to early triggering # of beacon contact before the nozzle has touched the bed. From test data, these are rare. Only 0.028% of samples # exceeded 75um (from over 32,000 samples across multiple machines and print surfaces). @@ -42,12 +45,16 @@ def __init__(self, config): # The number of times to probe an additional point if any z values are rejected. self.max_retries = config.getint('max_retries', 10, minval=0, maxval=15) - + # If true, each of the multiple probe locations will itself be probed several times using # the standard beacon error detection logic. From extensive testing, this mode offers no benefit # and should not be used. It is included only as an option for diagnostic purposes. self.use_error_corrected_probing = config.getboolean('use_error_corrected_probing', False) + if self.disabled: + logging.info(f"{self.name}: beacon true zero correction is disabled by configuration.") + return + if config.has_section('beacon'): self.printer.register_event_handler("klippy:connect", self._handle_connect) @@ -109,18 +116,18 @@ def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): zero_xy = self.toolhead.get_position()[:2] retval = self.orig_cmd(gcmd) self._check_homed() - ps = ProbingSession(self, gcmd, zero_xy, self.max_retries) + ps = ProbingSession(self, gcmd, zero_xy) ps.run() return retval class ProbingSession: - def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position, max_retries = 10): + def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position): self.gcmd = gcmd self.tzc = tzc self.zero_xy_position = zero_xy_position - self.max_retries = max_retries + self.max_retries = tzc.max_retries self.retries = 0 self.probe_helper = probe.ProbePointsHelper(self.tzc.config, self._probe_finalize, []) self._finalize_result = None From 4ca15caded383c12fbb973386546ee51ca394ce3 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 30 May 2025 16:08:56 +0100 Subject: [PATCH 055/139] Printers: don't enable mesh fade for all v-core-4 variants - The bed of these machines is likely to be flatter than the surface traced by the nozzle. --- configuration/printers/v-core-4-hybrid/300.cfg | 2 -- configuration/printers/v-core-4-hybrid/400.cfg | 2 -- configuration/printers/v-core-4-hybrid/500.cfg | 2 -- configuration/printers/v-core-4-idex/300.cfg | 2 -- configuration/printers/v-core-4-idex/400.cfg | 2 -- configuration/printers/v-core-4-idex/500.cfg | 2 -- configuration/printers/v-core-4/300.cfg | 2 -- configuration/printers/v-core-4/400.cfg | 2 -- configuration/printers/v-core-4/500.cfg | 2 -- 9 files changed, 18 deletions(-) diff --git a/configuration/printers/v-core-4-hybrid/300.cfg b/configuration/printers/v-core-4-hybrid/300.cfg index f9dca3ee4..73770ac73 100644 --- a/configuration/printers/v-core-4-hybrid/300.cfg +++ b/configuration/printers/v-core-4-hybrid/300.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:270,250 probe_count: 20,20 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4-hybrid/400.cfg b/configuration/printers/v-core-4-hybrid/400.cfg index c52967994..6674e0505 100644 --- a/configuration/printers/v-core-4-hybrid/400.cfg +++ b/configuration/printers/v-core-4-hybrid/400.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:370,350 probe_count: 30,30 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4-hybrid/500.cfg b/configuration/printers/v-core-4-hybrid/500.cfg index fa7c49037..83b770f41 100644 --- a/configuration/printers/v-core-4-hybrid/500.cfg +++ b/configuration/printers/v-core-4-hybrid/500.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:470,450 probe_count: 40,40 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4-idex/300.cfg b/configuration/printers/v-core-4-idex/300.cfg index df077bfe9..c5333a7b8 100644 --- a/configuration/printers/v-core-4-idex/300.cfg +++ b/configuration/printers/v-core-4-idex/300.cfg @@ -26,8 +26,6 @@ horizontal_move_z: 2 mesh_min: 30,32.5 mesh_max: 270,262.5 probe_count: 20,20 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4-idex/400.cfg b/configuration/printers/v-core-4-idex/400.cfg index 40ffaba57..d4aecf363 100644 --- a/configuration/printers/v-core-4-idex/400.cfg +++ b/configuration/printers/v-core-4-idex/400.cfg @@ -26,8 +26,6 @@ horizontal_move_z: 2 mesh_min: 30,32.5 mesh_max: 370,362.5 probe_count: 30,30 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4-idex/500.cfg b/configuration/printers/v-core-4-idex/500.cfg index 3f3ab1181..272a3b93f 100644 --- a/configuration/printers/v-core-4-idex/500.cfg +++ b/configuration/printers/v-core-4-idex/500.cfg @@ -26,8 +26,6 @@ horizontal_move_z: 2 mesh_min: 30,32.5 mesh_max: 470,462.5 probe_count: 40,40 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4/300.cfg b/configuration/printers/v-core-4/300.cfg index f9dca3ee4..73770ac73 100644 --- a/configuration/printers/v-core-4/300.cfg +++ b/configuration/printers/v-core-4/300.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:270,250 probe_count: 20,20 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4/400.cfg b/configuration/printers/v-core-4/400.cfg index c52967994..6674e0505 100644 --- a/configuration/printers/v-core-4/400.cfg +++ b/configuration/printers/v-core-4/400.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:370,350 probe_count: 30,30 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 diff --git a/configuration/printers/v-core-4/500.cfg b/configuration/printers/v-core-4/500.cfg index fa7c49037..83b770f41 100644 --- a/configuration/printers/v-core-4/500.cfg +++ b/configuration/printers/v-core-4/500.cfg @@ -20,8 +20,6 @@ horizontal_move_z: 2 mesh_min: 30,30 mesh_max:470,450 probe_count: 40,40 -fade_start: 1.0 -fade_end: 10.0 mesh_pps: 2,2 algorithm: bicubic bicubic_tension: .2 From 6a0360db7d026384efdce7be020406d5c3aa1ec0 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 30 May 2025 21:33:58 +0100 Subject: [PATCH 056/139] Beacon/TrueZero: support 3 levels of probing strategy via config for beacon_true_zero_correction. --- .../klippy/beacon_true_zero_correction.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index a88833b31..fbfa3c55b 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -46,6 +46,15 @@ def __init__(self, config): # The number of times to probe an additional point if any z values are rejected. self.max_retries = config.getint('max_retries', 10, minval=0, maxval=15) + # Controls the sampling strategy, notably affecting the number of points probed. + # - Level 1: 6 points probed, 1 zero sample, use mean of 3 minimal samples. This is the default and recommended level. + # - Level 2: 10 points probed, 1 zero sample, use mean of 3 minimal samples. This is a more robust probing strategy. + # - Level 3: 12 points probed, 1 zero sample, use mean of 3 minimal samples. This is the most robust probing strategy. + # From extensive testing, level 1 is very effective and efficient, with levels 2 and 3 offering only very modest gains + # and diminishing returns.Levels 2 and 3 are included for diagnostic purposes, but level 1 is recommended for most users. + # The zero sample is the implied zero sample from BEACON_AUTO_CALIBRATE, which is expected to have been invoked. + self.sampling_strategy = config.getint('sampling_strategy', 1, minval=1, maxval=3) + # If true, each of the multiple probe locations will itself be probed several times using # the standard beacon error detection logic. From extensive testing, this mode offers no benefit # and should not be used. It is included only as an option for diagnostic purposes. @@ -144,9 +153,11 @@ def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position): # in probe results. # Number of samples to take, including the implied zero sample from BEACON_AUTO_CALIBRATE - self._take = 7 - # Number of maximal-valued samples to discard - self._drop_top = 4 + self._take = (7, 11, 13)[tzc.sampling_strategy - 1] + + # Number of minimal samples to use in the final calculation. + self._keep = 3 + # The zero-value initial sample is implied from BEACON_AUTO_CALIBRATE, which is expected to have # been invoked immediatley prior to this command, at the same location. self._samples = [0.] @@ -250,7 +261,7 @@ def _probe_finalize(self, _, positions): if len(self._samples) == self._take: # Gathered enough good samples self._samples.sort() - use_samples = self._samples[:-self._drop_top] + use_samples = self._samples[:self._keep] logging.info(f'{self.tzc.name}: samples: {", ".join(f"{z:.6f}" for z in self._samples)} using: {", ".join(f"{z:.6f}" for z in use_samples)}') self._finalize_result = float(np.mean(use_samples)) return 'done' From d6c8272230c32d0906cab278619b08f198d23922 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 2 Jun 2025 14:11:17 +0100 Subject: [PATCH 057/139] Extras/Ratos: add helper method - There are not enough common Beacon helpers to justify a separate file, so adding to ratos.py --- configuration/klippy/ratos.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 734ee1322..58d372446 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,8 +1,8 @@ import os, logging, glob, traceback, inspect, re, math -import json, subprocess, pathlib, time +import json, subprocess, pathlib, multiprocessing import numpy as np +from collections import namedtuple from . import probe -import multiprocessing ##### # RatOS @@ -36,6 +36,7 @@ def __init__(self, config): self.bed_mesh = None self.gm_ratos = None self.toolhead = None + self.beacon = None # Status fields self.last_processed_file_result = None @@ -66,6 +67,8 @@ def _connect(self): self.rmmu_hub = self.printer.lookup_object("rmmu_hub", None) if self.config.has_section("bed_mesh"): self.bed_mesh = self.printer.lookup_object('bed_mesh') + if self.config.has_section("beacon"): + self.beacon = self.printer.lookup_object('beacon') # Register overrides. self.register_command_overrides() @@ -576,7 +579,31 @@ def get_ratos_version(self): except Exception as exc: self.debug_echo("get_ratos_version", ("Exception on run: %s", exc)) return version - + + def get_beacon_probing_regions(self): + """Gets the probing regions configuration for the Beacon probe, or None if not available. + Returns: + BeaconProbingRegions or None: A named tuple containing: + - x_offset: X offset of the Beacon probe + - y_offset: Y offset of the Beacon probe + - proximity_min: Tuple of (min_x, min_y) for proximity probing + - proximity_max: Tuple of (max_x, max_y) for proximity probing + - contact_min: Tuple of (min_x, min_y) for contact probing + - contact_max: Tuple of (max_x, max_y) for contact probing + Returns None if bed_mesh or beacon configuration is not available. + """ + if self.beacon is None: + return None + + return namedtuple('BeaconProbingRegions', + ['x_offset', 'y_offset', 'proximity_min', 'proximity_max', 'contact_min', 'contact_max'])( + x_offset=self.beacon.x_offset, + y_offset=self.beacon.y_offset, + proximity_min=(self.beacon.mesh_helper.def_min_x, self.beacon.mesh_helper.def_min_y), + proximity_max=(self.beacon.mesh_helper.def_max_x, self.beacon.mesh_helper.def_max_y), + contact_min=tuple(self.beacon.mesh_helper.def_contact_min), + contact_max=tuple(self.beacon.mesh_helper.def_contact_max)) + def get_status(self, eventtime): return { 'name': self.name, From 8c312b37d1479bbd05908a98b78b7322e4ffbc9d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 2 Jun 2025 14:11:54 +0100 Subject: [PATCH 058/139] Beacon/TrueZero: add _BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS --- .../klippy/beacon_true_zero_correction.py | 279 +++++++++++------- 1 file changed, 176 insertions(+), 103 deletions(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index fbfa3c55b..97c1507f1 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import math, logging +import math, time, logging import numpy as np from . import probe @@ -25,7 +25,6 @@ def __init__(self, config): self.status = None self.ratos = None - self.gm_ratos = None self.ratos_z_offset = None self.toolhead = None self.dual_carriage = None @@ -77,7 +76,6 @@ def __init__(self, config): def _handle_connect(self): self.ratos = self.printer.lookup_object('ratos') - self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') self.ratos_z_offset = self.printer.lookup_object('ratos_z_offset') self.toolhead = self.printer.lookup_object("toolhead") @@ -93,6 +91,11 @@ def _handle_connect(self): self.cmd_BEACON_AUTO_CALIBRATE, desc=self.desc_BEACON_AUTO_CALIBRATE) + self.gcode.register_command( + '_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS', + self.cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS, + desc=self.desc_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS) + def _handle_homing_move_end(self, homing_state, rails): # Clear the true zero correction offset if the Z axis is homed. # Any existing true zero correction is invalidated when z is re-homed. @@ -105,14 +108,8 @@ def _handle_motor_off(self, print_time): self.ratos_z_offset.set_offset('true_zero_correction', 0) ###### - # commands - ###### - def _check_homed(self, msg = 'Must home all axes first'): - status = self.toolhead.get_status(self.reactor.monotonic()) - homed_axes = status["homed_axes"] - if any(axis not in homed_axes for axis in "xyz"): - raise self.gcode.error( msg ) - + # Commands + ###### desc_BEACON_AUTO_CALIBRATE = "Automatically calibrates the Beacon probe. Extended with RatOS multi-point probing for improved true zero consistency. Use SKIP_MULTIPOINT_PROBING=1 to bypass." def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): # Clear existing offset @@ -130,6 +127,170 @@ def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): return retval + desc_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS = "For developer use only. This command is used to run diagnostics on the Beacon true zero correction system." + def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): + action = gcmd.get('ACTION', '').lower() + if action == 'capture': + point_count = gcmd.get_int('POINT_COUNT', 21, minval=1) + mpp_per_batch = gcmd.get_int('MPP_PER_BATCH', 20, minval=1) + batch_count = gcmd.get_int('BATCH_COUNT', 5, minval=1) + samples = gcmd.get_int('SAMPLES', 1, minval=1) + samples_drop = gcmd.get_int('SAMPLES_DROP', 0, minval=0) + samples_tolerance_retries = gcmd.get_int('SAMPLES_TOLERANCE_RETRIES', 10, minval=0) + + nozzle_tip_dia = self._get_nozzle_tip_diameter() + + # Calculate the nozzle-based min span as the length of the side of a + # square with area four times the footprint of COUNT nozzle tips. + span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * point_count * 4.) + half_span = span / 2 + + self.gcode.run_script_from_command("M84\nG28\nBEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1\nZ_TILT_ADJUST\n_MOVE_TO_SAFE_Z_HOME Z_HOP=1") + + zero_xy_position = self.toolhead.get_position()[:2] + + range_x = (zero_xy_position[0] - half_span, zero_xy_position[0] + half_span) + range_y = (zero_xy_position[1] - half_span, zero_xy_position[1] + half_span) + + self._validate_probing_region(range_x, range_y, span) + + probe_args = dict( + PROBE_METHOD='contact', + SAMPLES=str(samples), + SAMPLES_DROP=str(samples_drop), + SAMPLES_TOLERANCE_RETRIES=str(samples_tolerance_retries) + ) + + sensor = gcmd.get('SENSOR', None) + if sensor: + probe_args['SENSOR'] = sensor + + probe_gcmd = self.gcode.create_gcode_command( + gcmd.get_command(), + gcmd.get_command() + + "".join(" " + k + "=" + v for k, v in probe_args.items()), + probe_args + ) + + timestamp = time.strftime("%Y%m%d_%H%M%S") + with open(f'/home/pi/printer_data/config/mpp_capture_{timestamp}.csv', 'a') as f: + def cb(_, positions): + f.write(','.join(str(p[2]) for p in positions) + '\n') + f.flush() + return 'done' + + probe_helper = probe.ProbePointsHelper(self.config, cb, []) + + for batch_index in range(batch_count): + gcmd.respond_info(f"Batch {batch_index + 1} of {batch_count}") + self.gcode.run_script_from_command("M84\nG28\nBEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 SKIP_MODEL_CREATION=1") + for mpp_index in range(mpp_per_batch): + gcmd.respond_info(f"Batch {batch_index + 1} of {batch_count}, run {mpp_index + 1} of {mpp_per_batch}") + self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=1") + points = self._generate_points(point_count, range_x, range_y, nozzle_tip_dia) + probe_helper.update_probe_points(points, len(points)) + probe_helper.start_probe(probe_gcmd) + else: + raise self.gcode.error(f"Unknown action.") + + ###### + # Helper methods + ###### + def _check_homed(self, msg = 'Must home all axes first'): + status = self.toolhead.get_status(self.reactor.monotonic()) + homed_axes = status["homed_axes"] + if any(axis not in homed_axes for axis in "xyz"): + raise self.gcode.error( msg ) + + def _generate_points(self, n, x_lim, y_lim, min_dist, avoid_centre=True, max_iter=1000): + points = [] + centre = [np.mean(x_lim), np.mean(y_lim)] + iterations = 0 + + while len(points) < n and iterations < max_iter: + # Generate a candidate point uniformly within the given x and y limits. + candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), + np.random.uniform(y_lim[0], y_lim[1])]) + + # Check that candidate is at least min_dist away from every existing point. + if ((not avoid_centre) or np.linalg.norm(candidate - centre) >= min_dist) \ + and all(np.linalg.norm(candidate - p) >= min_dist for p in points): + points.append(candidate.tolist()) # don't leak numpy types + + iterations += 1 + + if len(points) < n: + raise self.gcode.error( + "Could not generate all required probe points within the specified iteration limit. " + "The conditions are too strict.") + + return points + + def _get_nozzle_diameter(self): + extruder_name = 'extruder' + + if self.dual_carriage and self.dual_carriage.dc[1].mode.lower() == 'primary': + extruder_name = 'extruder1' + + extruder = self.printer.lookup_object(extruder_name) + nozzle_diameter = extruder.nozzle_diameter + return nozzle_diameter + + def _get_nozzle_tip_diameter(self, nozzle_diameter=None): + if nozzle_diameter is None: + nozzle_diameter = self._get_nozzle_diameter() + + # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), + # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. + # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier + # of 1.56, or an increase of 1.35. Here we make some effort at a reasonable approximation. + if nozzle_diameter < 0.25: + nozzle_tip_dia = 1.5 * nozzle_diameter + elif nozzle_diameter <= 0.8: + nozzle_tip_dia = 2.5 * nozzle_diameter + else: + nozzle_tip_dia = nozzle_diameter + 1.35 + + return nozzle_tip_dia + + def _prepare_probe_command(self, gcmd): + probe_args = dict( + PROBE_METHOD='contact', + SAMPLES='1', + SAMPLES_DROP='0' + ) if not self.use_error_corrected_probing else dict( + PROBE_METHOD='contact', + SAMPLES='3', + SAMPLES_DROP='1', + SAMPLES_TOLERANCE_RETRIES='10' + ) + + sensor = gcmd.get('SENSOR', None) + if sensor: + probe_args['SENSOR'] = sensor + + return self.gcode.create_gcode_command( + gcmd.get_command(), + gcmd.get_command() + + "".join(" " + k + "=" + v for k, v in probe_args.items()), + probe_args + ) + + def _validate_probing_region(self, range_x, range_y, span): + r = self.ratos.get_beacon_probing_regions() + probable_x = (r.contact_min[0], r.contact_max[0]) + probable_y = (r.contact_min[1], r.contact_max[1]) + + def in_range(r, value): + return r[0] <= value <= r[1] + + if not ( + in_range(probable_x, range_x[0]) and in_range(probable_x, range_x[1]) and + in_range(probable_y, range_y[0]) and in_range(probable_y, range_y[1])): + + self.ratos.console_echo(RATOS_TITLE, 'error', f'The required probing region ({span:.1f}x{span:.1f}) would probe outside the configured contact probing area.') + raise self.gcmd.error('The required probing region would probe outside the contact probing area') + class ProbingSession: def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position): @@ -170,7 +331,7 @@ def run(self): num_points_to_generate = self._take - len(self._samples) + self.max_retries min_span = 9. - nozzle_tip_dia = self._get_nozzle_tip_diameter() + nozzle_tip_dia = self.tzc._get_nozzle_tip_diameter() # Calculate the nozzle-based min span as the length of the side of a # square with area four times the footprint of COUNT nozzle tips. @@ -184,11 +345,11 @@ def run(self): range_x = (self.zero_xy_position[0] - half_span, self.zero_xy_position[0] + half_span) range_y = (self.zero_xy_position[1] - half_span, self.zero_xy_position[1] + half_span) - self._validate_probing_region(range_x, range_y, span) + self.tzc._validate_probing_region(range_x, range_y, span) - probe_gcmd = self._prepare_probe_command() + probe_gcmd = self.tzc._prepare_probe_command(self.gcmd) - self._points = self._generate_points(num_points_to_generate, range_x, range_y, nozzle_tip_dia) + self._points = self.tzc._generate_points(num_points_to_generate, range_x, range_y, nozzle_tip_dia) self._next_points_index = self._take - len(self._samples) self.probe_helper.update_probe_points(self._points[:self._next_points_index], 1) self.probe_helper.start_probe(probe_gcmd) @@ -216,43 +377,6 @@ def _finalize(self): else: raise ValueError('Internal error: unexpected value for _finalize_result') - def _validate_probing_region(self, range_x, range_y, span): - printable_x = (self.tzc.gm_ratos.variables.get('printable_x_min'), self.tzc.gm_ratos.variables.get('printable_x_max')) - printable_y = (self.tzc.gm_ratos.variables.get('printable_y_min'), self.tzc.gm_ratos.variables.get('printable_y_max')) - - def in_range(r, value): - return r[0] <= value <= r[1] - - if not ( - in_range(printable_x, range_x[0]) and in_range(printable_x, range_x[1]) and - in_range(printable_y, range_y[0]) and in_range(printable_y, range_y[1])): - - self.tzc.console_echo(RATOS_TITLE, 'error', f'The required probing region ({span:.1f}x{span:.1f}) would probe outside the printable area.') - raise self.gcmd.error('The required probing region would probe outside the printable area') - - def _prepare_probe_command(self): - probe_args = dict( - PROBE_METHOD='contact', - SAMPLES='1', - SAMPLES_DROP='0' - ) if not self.tzc.use_error_corrected_probing else dict( - PROBE_METHOD='contact', - SAMPLES='3', - SAMPLES_DROP='1', - SAMPLES_TOLERANCE_RETRIES='10' - ) - - sensor = self.gcmd.get('SENSOR', None) - if sensor: - probe_args['SENSOR'] = sensor - - return self.tzc.gcode.create_gcode_command( - self.gcmd.get_command(), - self.gcmd.get_command() - + "".join(" " + k + "=" + v for k, v in probe_args.items()), - probe_args - ) - def _probe_finalize(self, _, positions): zvals = [p[2] for p in positions] logging.info(f'{self.tzc.name}: probed z-values: {", ".join(f"{z:.6f}" for z in zvals)}') @@ -280,58 +404,7 @@ def _probe_finalize(self, _, positions): self.gcmd.respond_info(f'{len(rejects)} z value(s) were out of range, exceeding the number of available retry points.') self._finalize_result = 'retry' return 'done' - - def _generate_points(self, n, x_lim, y_lim, min_dist, avoid_centre=True, max_iter=1000): - points = [] - centre = [np.mean(x_lim), np.mean(y_lim)] - iterations = 0 - - while len(points) < n and iterations < max_iter: - # Generate a candidate point uniformly within the given x and y limits. - candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), - np.random.uniform(y_lim[0], y_lim[1])]) - - # Check that candidate is at least min_dist away from every existing point. - if ((not avoid_centre) or np.linalg.norm(candidate - centre) >= min_dist) \ - and all(np.linalg.norm(candidate - p) >= min_dist for p in points): - points.append(candidate.tolist()) # don't leak numpy types - - iterations += 1 - - if len(points) < n: - raise self.gcode.error( - "Could not generate all required probe points within the specified iteration limit. " - "The conditions are too strict.") - - return points - - def _get_nozzle_diameter(self): - extruder_name = 'extruder' - if self.tzc.dual_carriage and self.tzc.dual_carriage.dc[1].mode.lower() == 'primary': - extruder_name = 'extruder1' - - extruder = self.tzc.printer.lookup_object(extruder_name) - nozzle_diameter = extruder.nozzle_diameter - return nozzle_diameter - - def _get_nozzle_tip_diameter(self, nozzle_diameter=None): - if nozzle_diameter is None: - nozzle_diameter = self._get_nozzle_diameter() - - # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), - # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. - # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier - # of 1.56, or an increase of 1.35. Here we make some effort at a reasonable approximation. - if nozzle_diameter < 0.25: - nozzle_tip_dia = 1.5 * nozzle_diameter - elif nozzle_diameter <= 0.8: - nozzle_tip_dia = 2.5 * nozzle_diameter - else: - nozzle_tip_dia = nozzle_diameter + 1.35 - - return nozzle_tip_dia - # Register the configuration def load_config(config): return BeaconTrueZeroCorrection(config) \ No newline at end of file From 937ec9165e4d0b3766b2b36d9a0a397987206eac Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 2 Jun 2025 14:12:36 +0100 Subject: [PATCH 059/139] Extras: code clean up --- configuration/klippy/beacon_mesh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 1b984bbab..073d38ea9 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,4 +1,5 @@ -import collections, multiprocessing, traceback, logging +import multiprocessing, traceback, logging +from collections import OrderedDict from . import bed_mesh as BedMesh import numpy as np from scipy.ndimage import gaussian_filter @@ -167,7 +168,7 @@ def cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS(self, gcmd): gcmd.respond_info("There is no active bed mesh") return - params = collections.OrderedDict({k: v for k,v in mesh.get_mesh_params().items() if str(k).startswith("ratos_")}) + params = OrderedDict({k: v for k,v in mesh.get_mesh_params().items() if str(k).startswith("ratos_")}) if len(params) == 0: gcmd.respond_info('No extended RatOS bed mesh parameters found') else: From 17489898d83b36d23a6aacc7757356379f47dcac Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 2 Jun 2025 14:44:58 +0100 Subject: [PATCH 060/139] Beacon/TrueZero: _BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS now adds header lines and shows a completion message --- .../klippy/beacon_true_zero_correction.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index 97c1507f1..7e6e52f1b 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import math, time, logging +import math, time, logging, socket import numpy as np from . import probe @@ -173,7 +173,18 @@ def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): ) timestamp = time.strftime("%Y%m%d_%H%M%S") - with open(f'/home/pi/printer_data/config/mpp_capture_{timestamp}.csv', 'a') as f: + filename = f'/home/pi/printer_data/config/mpp_capture_{timestamp}.csv' + + gcmd.respond_info(f"Capturing diagnostic data to {filename}...") + + with open(filename, 'a') as f: + f.write(f"# Beacon True Zero Correction Multi-point Probing Capture at {timestamp} on {socket.gethostname()}\n") + f.write(f"# Point Count: {point_count}, MPP per Batch: {mpp_per_batch}, Batch Count: {batch_count}\n") + f.write(f"# Samples: {samples}, Samples Drop: {samples_drop}, Samples Tolerance Retries: {samples_tolerance_retries}\n") + f.write(f"# Nozzle Tip Diameter: {nozzle_tip_dia:.3f}mm, Span: {span:.3f}mm\n") + f.write(f"# Zero XY Position: {zero_xy_position[0]:.3f}, {zero_xy_position[1]:.3f}\n") + f.write(f"# Range X: {range_x[0]:.3f} to {range_x[1]:.3f}, Range Y: {range_y[0]:.3f} to {range_y[1]:.3f}\n") + def cb(_, positions): f.write(','.join(str(p[2]) for p in positions) + '\n') f.flush() @@ -190,6 +201,8 @@ def cb(_, positions): points = self._generate_points(point_count, range_x, range_y, nozzle_tip_dia) probe_helper.update_probe_points(points, len(points)) probe_helper.start_probe(probe_gcmd) + + gcmd.respond_info(f"Capture complete, data saved to {filename}") else: raise self.gcode.error(f"Unknown action.") From 9f1cbfb314f87e2cd60c399a19414419d6e55062 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 2 Jun 2025 17:31:39 +0100 Subject: [PATCH 061/139] Install: add libopenblas-base to package list --- configuration/scripts/ratos-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/scripts/ratos-install.sh b/configuration/scripts/ratos-install.sh index e456c56e9..41b5b5e6e 100755 --- a/configuration/scripts/ratos-install.sh +++ b/configuration/scripts/ratos-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # This script installs additional dependencies for RatOS. -PKGLIST="python3-numpy python3-matplotlib curl git" +PKGLIST="python3-numpy python3-matplotlib curl git libopenblas-base" SCRIPT_DIR=$( cd -- "$( dirname -- "$(realpath -- "${BASH_SOURCE[0]}")" )" &> /dev/null && pwd ) CFG_DIR=$(realpath "$SCRIPT_DIR/..") From d8aae1734c3814a901b979d1622054c339f8d2cd Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 4 Jun 2025 19:04:01 +0100 Subject: [PATCH 062/139] Extras/Ratos: remove multi-point probing test code --- configuration/klippy/ratos.py | 315 +--------------------------------- 1 file changed, 2 insertions(+), 313 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 58d372446..fb85c75c2 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,8 +1,6 @@ -import os, logging, glob, traceback, inspect, re, math -import json, subprocess, pathlib, multiprocessing -import numpy as np +import os, logging, glob, traceback, inspect, re +import json, subprocess, pathlib from collections import namedtuple -from . import probe ##### # RatOS @@ -99,7 +97,6 @@ def register_commands(self): self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=(self.desc_RAISE_ERROR)) self.gcode.register_command('_TRY', self.cmd_TRY, desc=(self.desc_TRY)) self.gcode.register_command('_DEBUG_ECHO_STACK_TRACE', self.cmd_DEBUG_ECHO_STACK_TRACE, desc=(self.desc_DEBUG_ECHO_STACK_TRACE)) - self.gcode.register_command('MULTI_POINT_PROBE', self.cmd_MULTI_POINT_PROBE, desc=(self.desc_MULTI_POINT_PROBE)) def register_command_overrides(self): self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) @@ -705,314 +702,6 @@ def get_formatted_extended_stack_trace(callback=None, skip=0): lines.append(extra_lines + "\n") return "".join(lines) - - ##### - # Multi-point Probe - ##### - - @staticmethod - def pack_circles_concentric(radius, x_offset = 0., y_offset = 0., rings = 3, include_centre = True): - """ - Pack circles (radius r) using a concentric rings approach. - - Parameters: - - radius: radius of circles - - rings: number of rings, including the single central circle as the first ring. - With the centre circle included, 2 rings produces 7 circles, 3 rings 19, - 4 rings 37, 5 rings 61. - - include_centre: include the central circule in the result. Does not change - the meaning of the `rings` argument. - - Returns: - centres: a list of (x, y) coordinates for the centres of the packed circles. - """ - centres = [] - - # Place the center circle if it fits - if include_centre and rings > 0: - centres.append((x_offset, y_offset)) - - ring = 1 - # For each ring, compute the ring radius as d = ring * 2r. - # (This is a simple choice; more refined methods can use non-uniform ring spacing) - while ring < rings: - d = ring * 2 * radius # distance from center for current ring - - # Maximum circles that fit in this ring (angle between centers at least 2r/d) - n_circles = int(np.floor(2 * np.pi * d / (2 * radius))) - - # Place circles evenly around the ring - for i in range(n_circles): - theta = 2 * np.pi * i / n_circles - x = d * np.cos(theta) - y = d * np.sin(theta) - centres.append((float(x + x_offset), float(y + y_offset))) - ring += 1 - - return centres - - @staticmethod - def random_point_in_circle(radius, center_x, center_y): - # Generate a random angle between 0 and 2π - theta = np.random.uniform(0, 2 * np.pi) - - # Generate a random distance, ensuring uniform distribution within the circle - r = radius * np.sqrt(np.random.uniform(0, 1)) - - # Convert polar coordinates to Cartesian coordinates - x = center_x + r * np.cos(theta) - y = center_y + r * np.sin(theta) - - return x, y - - @staticmethod - def random_point_on_circle(radius, center_x, center_y): - # Generate a random angle in radians - theta = np.random.uniform(0, 2 * np.pi) - - # Compute the x and y coordinates - x = center_x + radius * np.cos(theta) - y = center_y + radius * np.sin(theta) - - return float(x), float(y) - - @staticmethod - def circle_points(n, radius, center_x, center_y): - """Generate 'n' evenly spaced points on a circle of given radius centered at (center_x, center_y).""" - angles = np.linspace(0, 2 * np.pi, n, endpoint=False) - x_points = center_x + radius * np.cos(angles) - y_points = center_y + radius * np.sin(angles) - return np.column_stack((x_points, y_points)).tolist() - - def _generate_points(self, n, x_lim, y_lim, min_dist, max_iter=10000): - """ - Generate n random points within given x and y limits such that - any two points are at least min_dist apart. - - Parameters: - - n: number of points to generate - - x_lim: tuple (min_x, max_x) - - y_lim: tuple (min_y, max_y) - - min_dist: minimum required Euclidean distance between any two points - - max_iter: maximum number of iterations to try (to avoid infinite loops) - - Returns: - - A NumPy array of shape (m, 2) of the generated points, where m <= n. - """ - points = [] - iterations = 0 - - while len(points) < n and iterations < max_iter: - # Generate a candidate point uniformly within the given x and y limits. - candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), - np.random.uniform(y_lim[0], y_lim[1])]) - - # Check that candidate is at least min_dist away from every existing point. - if all(np.linalg.norm(candidate - p) >= min_dist for p in points): - points.append(candidate.tolist()) # don't leak numpy types - - iterations += 1 - - if len(points) < n: - raise self.gcode.error( - "Could not generate all required probe points within the specified iteration limit. " - "The conditions are too strict.") - - return points - - def _check_homed(self, msg = 'Must home first'): - status = self.toolhead.get_status(self.reactor.monotonic()) - homed_axes = status["homed_axes"] - if any(axis not in homed_axes for axis in "xyz"): - raise self.gcode.error( msg ) - - desc_MULTI_POINT_PROBE = "TO DO" - def cmd_MULTI_POINT_PROBE(self, gcmd): - - self._check_homed() - - # - assumes already at desired centre location - # cmd COUNT=5 MIN_SPAN=10 [SAMPLES=1 SAMPLES_DROP=0 PROBE_METHOD=contact] - pattern = gcmd.get('PATTERN', 'random').strip().lower() - if pattern not in ('random', 'concentric', 'circle'): - raise gcmd.error('If specified, PATTERN must be random, concentric or circle') - - extruder_name = 'extruder' - - if self.dual_carriage and self.dual_carriage.dc[1].mode.lower() == 'primary': - extruder_name = 'extruder1' - - extruder = self.printer.lookup_object(extruder_name) - nozzle_diameter = extruder.nozzle_diameter - - # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), - # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. - # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier - # of 1.56, or an increase of 1.35. Here we make some effort at a reasonable approximation. - if nozzle_diameter < 0.25: - nozzle_tip_dia = 1.5 * nozzle_diameter - elif nozzle_diameter <= 0.8: - nozzle_tip_dia = 2.5 * nozzle_diameter - else: - nozzle_tip_dia = nozzle_diameter + 1.35 - - pos = self.toolhead.get_position() - - printable_x = ( self.gm_ratos.variables.get('printable_x_min'), self.gm_ratos.variables.get('printable_x_max') ) - printable_y = ( self.gm_ratos.variables.get('printable_y_min'), self.gm_ratos.variables.get('printable_y_max') ) - - def includes( r, value ): - return r[0] <= value <= r[1] - - if pattern == 'random': - count = gcmd.get_int('COUNT', 5) - min_span = gcmd.get_float('MIN_SPAN', 10.) - - # Calculate the nozzle-based min range as the length of the side of a - # square with area four times the footprint of COUNT nozzle tips. - nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * count * 4.) - span = max(min_span, nozzle_based_min_span) - half_span = span / 2. - - gcmd.respond_info(f"count: {count} min_span: {min_span} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_range: {nozzle_based_min_span:.2f} use_range: {span:.2f}") - self.mpp_save_meta = dict(pattern=0,count=count, min_span=min_span, nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia, nozzle_based_min_span=nozzle_based_min_span, span=span) - self.mpp_filename_suffix = f"-random{count}" - - range_x = (pos[0] - half_span, pos[0] + half_span) - range_y = (pos[1] - half_span, pos[1] + half_span) - - if not ( - includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and - includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): - self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') - raise gcmd.error('The required span would probe outside the printable area') - - points = self._generate_points(count, range_x, range_y, nozzle_tip_dia) - elif pattern == 'concentric': - rings = gcmd.get_int('RINGS', 3, minval=2, maxval=4) - include_centre = gcmd.get_int('INCLUDE_CENTRE', 0) == 1 - jitter_tip_dia_factor = gcmd.get_float('JITTER', 3., minval=0., maxval=50.) - - span = ((((rings * 2) - 1 ) * nozzle_tip_dia)/2) + ( jitter_tip_dia_factor * nozzle_tip_dia ) - - cx, cy = self.random_point_in_circle(jitter_tip_dia_factor * nozzle_tip_dia / 2, pos[0], pos[1]) - - gcmd.respond_info(f"rings: {rings} include_centre: {include_centre} jitter: {jitter_tip_dia_factor:.1f} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} span: {span:.2f} c: {cx:.2f}, {cy:.2f}") - self.mpp_filename_suffix = f"-concentric-r{rings}-ic{'1' if include_centre else '0'}-j{jitter_tip_dia_factor:.1f}" - self.mpp_save_meta = dict(pattern=1,rings=rings,include_centre=include_centre,jitter=jitter_tip_dia_factor,nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia,span=span,centre=(cx,cy)) - - range_x = (pos[0] - span, pos[0] + span) - range_y = (pos[1] - span, pos[1] + span) - - if not ( - includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and - includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): - self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') - raise gcmd.error('The required span would probe outside the printable area') - - points = self.pack_circles_concentric(nozzle_tip_dia/2, cx, cy, rings, include_centre) - elif pattern == 'circle': - dia = gcmd.get_float('DIA', 10.0) - count = gcmd.get_int('COUNT', 60) - - span = dia + nozzle_diameter - cx = pos[0] - cy = pos[1] - - gcmd.respond_info(f"dia: {dia} count: {count} extruder: {extruder.name} nozzle_dia: {nozzle_diameter:.3f} nozzle_tip_dia: {nozzle_tip_dia:.3f} span: {span:.2f} c: {cx:.2f}, {cy:.2f}") - self.mpp_filename_suffix = f"-circle-{dia:.1f}d{count}" - self.mpp_save_meta = dict(pattern=2,count=count,dia=dia,nozzle_diameter=nozzle_diameter, nozzle_tip_dia=nozzle_tip_dia,span=span,centre=(cx,cy)) - - range_x = (pos[0] - span, pos[0] + span) - range_y = (pos[1] - span, pos[1] + span) - - if not ( - includes(printable_x, range_x[0]) and includes(printable_x, range_x[1]) and - includes(printable_y, range_y[0]) and includes(printable_y, range_y[1])): - self.console_echo('MULTI_POINT_PROBE', 'error', f'The required span ({span:.1f}) would probe outside the printable area.') - raise gcmd.error('The required span would probe outside the printable area') - - points = self.circle_points(count, dia/2, cx, cy) - else: - raise gcmd.error(f"Pattern '{pattern}' not implemented.") - - #gcmd.respond_info( "\n".join([f"{p[0]:.2f}, {p[1]:.2f}" for p in points])) - - # TODO: ProbePointsHelper will consider name, horizontal_move_z and speed from config. It's weird to conflate those - # values with [ratos] config. It would seem cleaner to move MULTI_POINT_PROBE into its own file. - probe_helper = probe.ProbePointsHelper(self.config, self.probe_finalize, []) - probe_helper.update_probe_points(points, len(points)) - probe_helper.start_probe(gcmd) - - def probe_finalize(self, offsets, positions): - def percentile_filter(data, margin=5.): - lower_bound = np.percentile(data, margin) - upper_bound = np.percentile(data, 100. - margin) - filtered_data = data[np.logical_and(data >= lower_bound, data <= upper_bound)] # Safe comparison - return filtered_data - - #self.gcode.respond_info(f"offsets:\n{offsets}") - #self.gcode.respond_info(f"positions:\n{positions}") - z = np.array([p[2] for p in positions]) - self.gcode.respond_info(f"mean: {np.mean(z):.5f} median: {np.median(z):.5f} min: {np.min(z):.5f} max: {np.max(z):.5f} spread: {np.max(z)-np.min(z):.5f} sd: {np.std(z):.5f}") - #z5 = percentile_filter(z, 5.) - #self.gcode.respond_info(f"mean5: {np.mean(z5):.5f} median5: {np.median(z5):.5f}") - - #fn = f"/tmp/multi-point-probe{self.mpp_filename_suffix}.csv" - #with open(fn, "a") as f: - # f.write(",".join([str(v) for v in z])) - # f.write("\n") - - self.append_to_mpp_file(positions, offsets) - return 'done' - - def append_to_mpp_file(self, positions, offsets): - parent_conn, child_conn = multiprocessing.Pipe() - - def do(): - try: - child_conn.send( - (False, self._do_append_to_mpp_file(positions, offsets, self.mpp_save_meta, self.mpp_filename_suffix)) - ) - except Exception: - child_conn.send((True, traceback.format_exc())) - child_conn.close() - - child = multiprocessing.Process(target=do) - child.daemon = True - child.start() - reactor = self.reactor - eventtime = reactor.monotonic() - while child.is_alive(): - eventtime = reactor.pause(eventtime + 0.1) - is_err, result = parent_conn.recv() - child.join() - parent_conn.close() - if is_err: - raise Exception("Error appending data to npz file: %s" % (result,)) - else: - is_inner_err, inner_result = result - if is_inner_err: - raise self.gcode.error(inner_result) - else: - return inner_result - - @staticmethod - def _do_append_to_mpp_file(positions, offsets, meta, filename_suffix): - def get_save_map(i): - return { - f'positions_{i}': positions, - f'offsets_{i}': offsets - } | {f'{k}_{i}': np.asanyarray(v) for k,v in meta.items()} - - fn = f"/tmp/multi-point-probe{filename_suffix}.npz" - if os.path.exists(fn): - with np.load(fn) as npz: - count = int(npz['count']) - np.savez_compressed( fn, count=np.array(count+1), **{k:v for k,v in npz.items() if k != 'count'}, **get_save_map(count) ) - else: - np.savez_compressed( fn, count=np.array(1), **get_save_map(0) ) - return (False, None) ##### # Loader From 4221329e7cc0815a45bc3af43f0e310d436a35c3 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 5 Jun 2025 14:55:59 +0100 Subject: [PATCH 063/139] Beacon/Mesh: support KEEP_TEMP_MESHES parameter in BEACON_CREATE_SCAN_COMPENSATION_MESH --- configuration/z-probe/beacon.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 1b20510f4..23d086497 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -838,6 +838,7 @@ gcode: {% set chamber_temp = params.CHAMBER_TEMP|default(0)|int %} {% set profile = params.PROFILE|default("Beacon Scan Compensation")|string %} {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} + {% set keep_temp_meshes = params.KEEP_TEMP_MESHES|default(0) %} # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} @@ -924,7 +925,7 @@ gcode: {% endif %} # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} + CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} KEEP_TEMP_MESHES={keep_temp_meshes} # turn bed and extruder heaters off {% if not automated %} From 3d559c5c3f74ef64076fc8f0532240ee2eb741a3 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 5 Jun 2025 23:02:13 +0100 Subject: [PATCH 064/139] Beacon/AdaptiveHeatSoak: add beacon_adaptive_heat_soak module - work in progress for gathering test data --- .../klippy/beacon_adaptive_heat_soak.py | 384 ++++++++++++++++++ configuration/scripts/ratos-common.sh | 1 + 2 files changed, 385 insertions(+) create mode 100644 configuration/klippy/beacon_adaptive_heat_soak.py diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py new file mode 100644 index 000000000..140cf19df --- /dev/null +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -0,0 +1,384 @@ +# Adaptive heat soak with thermal stability detection using Beacon proximity sensor data +# +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import math, datetime, time, struct, logging +import numpy as np +from . import probe +from multiprocessing import shared_memory, Process, Pipe + +class BeaconZRateSession: + def __init__(self, config, beacon, reactor): + self.config = config + self.name = config.get_name() + self.beacon = beacon + self.reactor = reactor + self._shm = None + self._float_size = struct.calcsize('d') # Size of double in bytes + + def cleanup(self): + if self._shm is not None: + self._shm.close() + self._shm.unlink() + self._shm = None + + def _ensure_shared_memory(self, size): + # Ensure the shared memory is large enough. Don't bother shrinking it. + if self._shm is None or self._shm.size < size * self._float_size: + if self._shm is not None: + self._shm.close() + self._shm.unlink() + self._shm = shared_memory.SharedMemory(create=True, size=size * self._float_size) + + def get_z_rate(self, sample_count): + if sample_count <= 2: + raise ValueError("Sample count must be greater than 2 to calculate a rate.") + + # Time values are stored in the first sample_count elements of the shared memory, and distances + # in the second sample_count elements. + self._ensure_shared_memory(sample_count * 2) + samples = memoryview(self._shm.buf).cast('d') + i = 0 + + def cb(s): + nonlocal i, samples + if i < sample_count: + # s["dist"] is the smoothed distance, we want the unsmoothed data as this is + # cleaner for rate calculations. + time = s["time"] + temp = s["temp"] + data = s["data"] + freq = self.beacon.count_to_freq(data) + dist = self.beacon.freq_to_dist(freq, temp) + samples[i] = time + samples[sample_count + i] = dist + i += 1 + + with self.beacon.streaming_session(cb): + eventtime = self.reactor.monotonic() + while i < sample_count: + eventtime = self.reactor.pause(eventtime + 0.1) + + mid_time = (samples[0] + samples[sample_count - 1]) / 2 + duration = samples[sample_count - 1] - samples[0] + logging.info(f"{self.name}: Captured {i} samples over {duration:.2f} seconds at mid_time {mid_time:.2f}") + + # Set up a pipe to communicate with the child process + parent_conn, child_conn = Pipe() + + child = Process(target=BeaconZRateSession._calculate_z_rate, args=(child_conn, self._shm.name, sample_count)) + child.daemon = True + child.start() + + eventtime = self.reactor.monotonic() + + while child.is_alive(): + eventtime = self.reactor.pause(eventtime + 0.1) + + is_err, result = parent_conn.recv() + + child.join() + parent_conn.close() + + if is_err: + raise Exception(f"Error calculating z-rate: {result}") + else: + return (mid_time, result) + + @staticmethod + def _calculate_z_rate(conn, shm_name, sample_count): + try: + shm = None + try: + shm = shared_memory.SharedMemory(name=shm_name) + samples = memoryview(shm.buf).cast('d') + + if len(samples) < sample_count * 2: + raise ValueError("Not enough samples in shared memory") + + # Time values are stored in the first sample_count elements of the shared memory, and distances + # in the second sample_count elements. The shared memory may be larger than sample_count * 2, + # so we take care to only use the first sample_count * 2 values. + coefficients = np.polyfit( + samples[:sample_count], + samples[sample_count:sample_count * 2], 1) + + slope = coefficients[0] # The slope of the line is the rate of change + slope_nm_per_sec = slope * 1e6 # Convert from millimeters to nanometers per second + conn.send((False, slope_nm_per_sec)) + finally: + if shm is not None: + shm.close() + except Exception as e: + conn.send((True, str(e))) + finally: + conn.close() + +class BeaconAdaptiveHeatSoak: + def __init__(self, config): + self.config = config + self.name = config.get_name() + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.gcode = self.printer.lookup_object('gcode') + + # Configuration values + self.threshold = config.getfloat('threshold', 0.001, + above=0.0, below=1.0) + self.default_horizontal_move_z = config.getfloat('horizontal_move_z', 5., minval=1.0) + self.speed = config.getfloat('speed', 50., above=0.) + self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) + self.maximum_wait = config.getint('maximum_wait', 3600, minval=0) + self.home_position_only = config.getboolean('home_position_only', False) + + # Setup + self.reactor = None + self.ratos = None + self.beacon = None + self.default_probe_points = None + self.toolhead = None + self.first_run = True + + if not self.home_position_only and config.get('points', None) is not None: + self.default_probe_points = config.getlists('points', seps=(',', '\n'), + parser=float, count=2) + + # Register commands + self.gcode.register_command( + 'BEACON_WAIT_FOR_PRINTER_HEAT_SOAK', + self.cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK, + desc=self.desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK) + + self.gcode.register_command( + '_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES', + self.cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES, + desc=self.desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES) + + self.gcode.register_command( + '_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES', + self.cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES, + desc=self.desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES) + + self.printer.register_event_handler("klippy:connect", + self._handle_connect) + + def _handle_connect(self): + self.reactor = self.printer.get_reactor() + self.toolhead = self.printer.lookup_object("toolhead") + self.ratos = self.printer.lookup_object("ratos") + + if self.config.has_section("beacon"): + self.beacon = self.printer.lookup_object('beacon') + + if not self.home_position_only and self.default_probe_points is None: + beacon_regions = self.ratos.get_beacon_probing_regions() + if beacon_regions is not None: + # Generate points in clockwise order starting from top middle (12 o'clock) + x_min, y_min = beacon_regions.proximity_min + x_max, y_max = beacon_regions.proximity_max + mid_x = x_min + (x_max - x_min) / 2 + mid_y = y_min + (y_max - y_min) / 2 + # Put the points in a clockwise order for nice toolhead movement path. + self.default_probe_points = ( + (mid_x, y_max), # 12 o'clock + (x_max, y_max), # 1:30 + (x_max, mid_y), # 3 o'clock + (x_max, y_min), # 4:30 + (mid_x, y_min), # 6 o'clock + (x_min, y_min), # 7:30 + (x_min, mid_y), # 9 o'clock + (x_min, y_max), # 10:30 + (mid_x, mid_y) # center point + ) + + def _handle_first_run(self): + # We've seen issues where the first streaming_session after a restart begins with some bogus data, + # so we throw away some samples to ensure the beacon is ready. + if self.first_run: + self.first_run = False + i = 0 + def cb(_): + nonlocal i + i += 1 + with self.beacon.streaming_session(cb): + # Wait for 1000 samples to be collected + eventtime = self.reactor.monotonic() + while i < 1000: + eventtime = self.reactor.pause(eventtime + 0.1) + + def _monotonic_to_wall_clock(self, monotonic_timestamp): + baseline_monotonic = self.reactor.monotonic() + baseline_wall_time = time.time() + elapsed = monotonic_timestamp - baseline_monotonic + wall_timestamp = baseline_wall_time + elapsed + return datetime.fromtimestamp(wall_timestamp) + + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" + def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): + if self.beacon is None: + raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") + + self._handle_first_run() + + primary_soak_position = self.toolhead.get_position() + + if self.default_probe_points is not None: + # Skip any default probe points that are close to the primary soak position, there's no point probing them. + ref_pos = np.array(primary_soak_position[:2]) + min_distance_from_primary_soak_position = 80 + probe_points = [p for p in self.default_probe_points if np.linalg.norm(np.array(p) - ref_pos) >= min_distance_from_primary_soak_position] + else: + probe_points = None + + maximum_wait = gcmd.get_int('MAXIMUM_WAIT', 90, minval=0) + samples = gcmd.get_int('SAMPLES', 15000, minval=500) + + start_time = self.reactor.monotonic() + + z_rate_session = None + try: + z_rate_session = BeaconZRateSession(self.config, self.beacon, self.reactor) + #max_history_length = 10 + history = [] + while True: + if self.reactor.monotonic() - start_time > maximum_wait: + gcmd.respond_info(f"Maximum wait time of {maximum_wait} seconds exceeded, exiting.") + return + + # Get the Z rate from the beacon + try: + z_rate_result = z_rate_session.get_z_rate(samples) + except Exception as e: + raise self.printer.command_error(f"Error calculating Z rate: {e}") + + gcmd.respond_info(f"Z rate {z_rate_result[1]:.3f} nm/s") + + history.append(z_rate_result) + # if len(history) > max_history_length: + # history.pop(0) + + # # Fit a second-degree polynomial (quadratic) to the data. + # coefs = np.polyfit([h[0] for h in history], + # [h[1] for h in history], 2) + + # poly = np.poly1d(coefs) + + # if abs(z_rate) < self.threshold: + # gcmd.respond_info(f"Thermal stability detected with Z rate {z_rate:.3f} nm/s, exiting.") + # return + + # gcmd.respond_info(f"Z rate {z_rate:.3f} nm/s exceeds threshold {self.threshold}, continuing to wait...") + + finally: + if z_rate_session is not None: + z_rate_session.cleanup() + + def _predict_future_crossing(self, poly, target_value, after): + # To predict when the trend will reach the target_value, solve: + # p(x) = target_value <=> p(x) - target_value = 0 + + # Create the new polynomial q(x) = p(x) - target_value. + q = poly - target_value + + # Compute the roots of q. + roots = q.r + + # Filter out the real roots. + real_roots = [root.real for root in roots if np.isreal(root)] + + # Often, you want to predict a *future* event. + # For example, if x represents time, choose real roots greater than the latest x value. + future_roots = [root for root in real_roots if root > after] + + if future_roots: + # Choose the earliest future time as the predicted crossing. + predicted_x = min(future_roots) + + # Compute the derivative of the fitted polynomial to obtain the slope. + dp = poly.deriv() + predicted_slope = dp(predicted_x) + + print(f"Predicted time (x) when y reaches {target_value}: {predicted_x}") + print(f"Predicted slope at that time: {predicted_slope}") + + else: + print(f"No future crossing found where y reaches {target_value} based on the available data.") + + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." + def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): + if self.beacon is None: + raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") + + self._handle_first_run() + + duration = gcmd.get_int('DURATION', 7200, minval=0) + samples = gcmd.get_int('SAMPLES', 30000, minval=1000) + + timestamp = time.strftime("%Y%m%d_%H%M%S") + filename = f'/home/pi/printer_data/config/beacon_adaptive_heat_soak_z_rates_{timestamp}.txt' + + # Make sure we can open the file before starting capture + with open(filename, 'w') as f: + gcmd.respond_info(f'Capturing diagnostic z-rates for {duration} seconds using {samples} samples per z-rate calculation to file {filename}, please wait...') + start_time = self.reactor.monotonic() + z_rate_session = None + try: + z_rate_session = BeaconZRateSession(self.config, self.beacon, self.reactor) + history = [] + time_zero = None + while self.reactor.monotonic() - start_time < duration: + # Get the Z rate from the beacon + try: + z_rate_result = z_rate_session.get_z_rate(samples) + except Exception as e: + raise self.printer.command_error(f"Error calculating Z rate: {e}") + + gcmd.respond_info(f"Z rate {z_rate_result[1]:.3f} nm/s") + + if time_zero is None: + time_zero = z_rate_result[0] + + history.append((z_rate_result[0] - time_zero, z_rate_result[1])) + + np.savetxt(f, history) + gcmd.respond_info(f'Diagnostic data captured to {filename}') + finally: + if z_rate_session is not None: + z_rate_session.cleanup() + + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." + def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): + if self.beacon is None: + raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") + + self._handle_first_run() + + duration = gcmd.get_int('DURATION', 300, minval=60) + chunk_duration = gcmd.get_int('CHUNK_DURATION', 5, minval=5) + + timestamp = time.strftime("%Y%m%d_%H%M%S") + filename = f'/home/pi/printer_data/config/beacon_adaptive_heat_soak_beacon_samples_{timestamp}.txt' + + with open(filename, 'w') as f: + gcmd.respond_info(f'Capturing diagnostic beacon samples for {duration} seconds in chunks of {chunk_duration} seconds to file {filename}, please wait...') + start_time = self.reactor.monotonic() + while self.reactor.monotonic() - start_time < duration: + samples = [] + def cb(s): + unsmooth_data = s["data"] + unsmooth_freq = self.beacon.count_to_freq(unsmooth_data) + unsmooth_dist = self.beacon.freq_to_dist(unsmooth_freq, s["temp"]) + samples.append((s["time"], s["dist"], unsmooth_dist)) + + with self.beacon.streaming_session(cb): + self.reactor.pause(self.reactor.monotonic() + chunk_duration) + + np.savetxt(f, samples) + f.flush() + + gcmd.respond_info(f'Diagnostic data captured to {filename}') + +def load_config(config): + return BeaconAdaptiveHeatSoak(config) \ No newline at end of file diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index b8bc45c45..bef425599 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -193,6 +193,7 @@ verify_registered_extensions() ["beacon_mesh_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_mesh.py") ["ratos_z_offset_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos_z_offset.py") ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") + ["beacon_adaptive_heatsoak_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_adaptive_heat_soak.py") ) declare -A kinematics_extensions=( From b82a7b8a07d93826c8edf9a613bcc3645033fb62 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 8 Jun 2025 14:59:57 +0100 Subject: [PATCH 065/139] Beacon/AdaptiveHeatSoak: cleanup and initial candidate implementation of BEACON_WAIT_FOR_PRINTER_HEAT_SOAK --- .../klippy/beacon_adaptive_heat_soak.py | 283 ++++++++---------- 1 file changed, 121 insertions(+), 162 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 140cf19df..d4cc0c7d9 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -4,9 +4,8 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import math, datetime, time, struct, logging +import time, struct, logging import numpy as np -from . import probe from multiprocessing import shared_memory, Process, Pipe class BeaconZRateSession: @@ -39,58 +38,70 @@ def get_z_rate(self, sample_count): # Time values are stored in the first sample_count elements of the shared memory, and distances # in the second sample_count elements. self._ensure_shared_memory(sample_count * 2) - samples = memoryview(self._shm.buf).cast('d') - i = 0 - - def cb(s): - nonlocal i, samples - if i < sample_count: - # s["dist"] is the smoothed distance, we want the unsmoothed data as this is - # cleaner for rate calculations. - time = s["time"] - temp = s["temp"] - data = s["data"] - freq = self.beacon.count_to_freq(data) - dist = self.beacon.freq_to_dist(freq, temp) - samples[i] = time - samples[sample_count + i] = dist - i += 1 - - with self.beacon.streaming_session(cb): - eventtime = self.reactor.monotonic() - while i < sample_count: - eventtime = self.reactor.pause(eventtime + 0.1) - - mid_time = (samples[0] + samples[sample_count - 1]) / 2 - duration = samples[sample_count - 1] - samples[0] - logging.info(f"{self.name}: Captured {i} samples over {duration:.2f} seconds at mid_time {mid_time:.2f}") + samples = None + try: + samples = memoryview(self._shm.buf).cast('d') + out_of_range_count = 0 + i = 0 - # Set up a pipe to communicate with the child process - parent_conn, child_conn = Pipe() + def cb(s): + nonlocal i, samples, out_of_range_count + if i < sample_count: + # s["dist"] is the smoothed distance, we want the unsmoothed data as this is + # cleaner for rate calculations. + time = s["time"] + temp = s["temp"] + data = s["data"] + freq = self.beacon.count_to_freq(data) + dist = self.beacon.freq_to_dist(freq, temp) + if dist is None or np.isinf(dist) or np.isnan(dist): + out_of_range_count += 1 + else: + samples[i] = time + samples[sample_count + i] = dist + i += 1 + + with self.beacon.streaming_session(cb): + eventtime = self.reactor.monotonic() + while i < sample_count: + eventtime = self.reactor.pause(eventtime + 0.1) + if out_of_range_count > 0: + # fail fast, most likely a command was called before the beacon was calibrated or positioned correctly + raise Exception(f"Beacon could not measure a valid distance. Beacon must be calibrated and positioned correctly before running this command.") + + mid_time = (samples[0] + samples[sample_count - 1]) / 2 - child = Process(target=BeaconZRateSession._calculate_z_rate, args=(child_conn, self._shm.name, sample_count)) - child.daemon = True - child.start() + # Set up a pipe to communicate with the child process + parent_conn, child_conn = Pipe() - eventtime = self.reactor.monotonic() + child = Process(target=BeaconZRateSession._calculate_z_rate, args=(child_conn, self._shm.name, sample_count)) + child.daemon = True + child.start() - while child.is_alive(): - eventtime = self.reactor.pause(eventtime + 0.1) + eventtime = self.reactor.monotonic() - is_err, result = parent_conn.recv() - - child.join() - parent_conn.close() - - if is_err: - raise Exception(f"Error calculating z-rate: {result}") - else: - return (mid_time, result) + while child.is_alive(): + eventtime = self.reactor.pause(eventtime + 0.1) + + is_err, result = parent_conn.recv() + + child.join() + parent_conn.close() + + if is_err: + raise Exception(result) + else: + return (mid_time, result) + finally: + if samples is not None: + samples.release() + del samples @staticmethod def _calculate_z_rate(conn, shm_name, sample_count): try: shm = None + samples = None try: shm = shared_memory.SharedMemory(name=shm_name) samples = memoryview(shm.buf).cast('d') @@ -109,6 +120,9 @@ def _calculate_z_rate(conn, shm_name, sample_count): slope_nm_per_sec = slope * 1e6 # Convert from millimeters to nanometers per second conn.send((False, slope_nm_per_sec)) finally: + if samples is not None: + samples.release() + del samples if shm is not None: shm.close() except Exception as e: @@ -125,26 +139,26 @@ def __init__(self, config): self.gcode = self.printer.lookup_object('gcode') # Configuration values - self.threshold = config.getfloat('threshold', 0.001, - above=0.0, below=1.0) - self.default_horizontal_move_z = config.getfloat('horizontal_move_z', 5., minval=1.0) - self.speed = config.getfloat('speed', 50., above=0.) - self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) - self.maximum_wait = config.getint('maximum_wait', 3600, minval=0) - self.home_position_only = config.getboolean('home_position_only', False) + + # The default z-rate threshold in nm/s below which we consider the printer to be thermally stable. + self.def_threshold = config.getint('threshold', 15, minval=10) + + # The default number of consecutive z-rate reaidings below the threshold before we consider the + # printer to be thermally stable. This is used to avoid false positives due to noise in the data. + self.def_hold_count = config.getint('hold_count', 3, minval=1) + + # The default maximum wait time in seconds for the printer to reach thermal stability. + self.def_maximum_wait = config.getint('maximum_wait', 5400, minval=0) + + # The default number of samples to take per z-rate calculation. This value is only configurable + # to support testing and debugging, and should not be changed in production. + self.def_samples_per_measurement = config.getint('samples_per_measurement', 30000, minval=30000) # Setup self.reactor = None - self.ratos = None self.beacon = None - self.default_probe_points = None - self.toolhead = None self.first_run = True - if not self.home_position_only and config.get('points', None) is not None: - self.default_probe_points = config.getlists('points', seps=(',', '\n'), - parser=float, count=2) - # Register commands self.gcode.register_command( 'BEACON_WAIT_FOR_PRINTER_HEAT_SOAK', @@ -166,33 +180,10 @@ def __init__(self, config): def _handle_connect(self): self.reactor = self.printer.get_reactor() - self.toolhead = self.printer.lookup_object("toolhead") - self.ratos = self.printer.lookup_object("ratos") if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') - if not self.home_position_only and self.default_probe_points is None: - beacon_regions = self.ratos.get_beacon_probing_regions() - if beacon_regions is not None: - # Generate points in clockwise order starting from top middle (12 o'clock) - x_min, y_min = beacon_regions.proximity_min - x_max, y_max = beacon_regions.proximity_max - mid_x = x_min + (x_max - x_min) / 2 - mid_y = y_min + (y_max - y_min) / 2 - # Put the points in a clockwise order for nice toolhead movement path. - self.default_probe_points = ( - (mid_x, y_max), # 12 o'clock - (x_max, y_max), # 1:30 - (x_max, mid_y), # 3 o'clock - (x_max, y_min), # 4:30 - (mid_x, y_min), # 6 o'clock - (x_min, y_min), # 7:30 - (x_min, mid_y), # 9 o'clock - (x_min, y_max), # 10:30 - (mid_x, mid_y) # center point - ) - def _handle_first_run(self): # We've seen issues where the first streaming_session after a restart begins with some bogus data, # so we throw away some samples to ensure the beacon is ready. @@ -207,14 +198,7 @@ def cb(_): eventtime = self.reactor.monotonic() while i < 1000: eventtime = self.reactor.pause(eventtime + 0.1) - - def _monotonic_to_wall_clock(self, monotonic_timestamp): - baseline_monotonic = self.reactor.monotonic() - baseline_wall_time = time.time() - elapsed = monotonic_timestamp - baseline_monotonic - wall_timestamp = baseline_wall_time + elapsed - return datetime.fromtimestamp(wall_timestamp) - + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon is None: @@ -222,26 +206,24 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): self._handle_first_run() - primary_soak_position = self.toolhead.get_position() - - if self.default_probe_points is not None: - # Skip any default probe points that are close to the primary soak position, there's no point probing them. - ref_pos = np.array(primary_soak_position[:2]) - min_distance_from_primary_soak_position = 80 - probe_points = [p for p in self.default_probe_points if np.linalg.norm(np.array(p) - ref_pos) >= min_distance_from_primary_soak_position] - else: - probe_points = None - - maximum_wait = gcmd.get_int('MAXIMUM_WAIT', 90, minval=0) - samples = gcmd.get_int('SAMPLES', 15000, minval=500) - - start_time = self.reactor.monotonic() + threshold = gcmd.get_float('THRESHOLD', self.def_threshold, minval=10) + target_hold_count = gcmd.get_int('HOLD_COUNT', self.def_hold_count, minval=1) + maximum_wait = gcmd.get_int('MAXIMUM_WAIT', self.def_maximum_wait, minval=0) + samples_per_measurement = gcmd.get_int('SAMPLES_PER_MEASUREMENT', self.def_samples_per_measurement, minval=30000) + moving_average_size = 5 + hold_count = 0 + history = [] z_rate_session = None + + logging.info(f"{self.name}: Starting heat soak with threshold {threshold} nm/s, hold count {target_hold_count}, maximum wait {maximum_wait} seconds, {samples_per_measurement} samples per measurement and moving average size {moving_average_size}") + gcmd.respond_info(f"Waiting for printer to reach thermal stability for up to {maximum_wait} seconds, requiring {target_hold_count} consecutive measurements within the Z-rate threshold of {threshold} nm/s. Please wait...") + + start_time = self.reactor.monotonic() + try: z_rate_session = BeaconZRateSession(self.config, self.beacon, self.reactor) - #max_history_length = 10 - history = [] + while True: if self.reactor.monotonic() - start_time > maximum_wait: gcmd.respond_info(f"Maximum wait time of {maximum_wait} seconds exceeded, exiting.") @@ -249,63 +231,40 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): # Get the Z rate from the beacon try: - z_rate_result = z_rate_session.get_z_rate(samples) + z_rate_result = z_rate_session.get_z_rate(samples_per_measurement) except Exception as e: - raise self.printer.command_error(f"Error calculating Z rate: {e}") - - gcmd.respond_info(f"Z rate {z_rate_result[1]:.3f} nm/s") - - history.append(z_rate_result) - # if len(history) > max_history_length: - # history.pop(0) - - # # Fit a second-degree polynomial (quadratic) to the data. - # coefs = np.polyfit([h[0] for h in history], - # [h[1] for h in history], 2) + logging.error(f"{self.name}: Error calculating Z-rate, wait aborted: {e}") + raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") - # poly = np.poly1d(coefs) - - # if abs(z_rate) < self.threshold: - # gcmd.respond_info(f"Thermal stability detected with Z rate {z_rate:.3f} nm/s, exiting.") - # return - - # gcmd.respond_info(f"Z rate {z_rate:.3f} nm/s exceeds threshold {self.threshold}, continuing to wait...") - + history.append(z_rate_result[1]) + + if len(history) >= moving_average_size: + moving_average = np.mean(history[-moving_average_size:]) + + if moving_average <= threshold: + hold_count += 1 + msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" + else: + if hold_count > 0: + msg = f"Z-rate {moving_average:.1f} nm/s, moved outside threshold of {threshold} nm/s after {hold_count} consecutive measurements" + hold_count = 0 + else: + msg = f"Z-rate {moving_average:.1f} nm/s, not within threshold of {threshold} nm/s" + + gcmd.respond_info(msg) + + if hold_count >= target_hold_count: + gcmd.respond_info(f"Printer is considered thermally stable after {hold_count} consecutive measurements within threshold of {threshold} nm/s.") + for i in range(0, len(history), 20): + chunk = history[i:i+20] + logging.info(f"{self.name}: raw z-rates: {','.join(f'{v:.6e}' for v in chunk)}") + return + else: + logging.info(f"{self.name}: Z-rate {z_rate_result[1]:.3f} nm/s, not enough samples for moving average yet") finally: if z_rate_session is not None: z_rate_session.cleanup() - def _predict_future_crossing(self, poly, target_value, after): - # To predict when the trend will reach the target_value, solve: - # p(x) = target_value <=> p(x) - target_value = 0 - - # Create the new polynomial q(x) = p(x) - target_value. - q = poly - target_value - - # Compute the roots of q. - roots = q.r - - # Filter out the real roots. - real_roots = [root.real for root in roots if np.isreal(root)] - - # Often, you want to predict a *future* event. - # For example, if x represents time, choose real roots greater than the latest x value. - future_roots = [root for root in real_roots if root > after] - - if future_roots: - # Choose the earliest future time as the predicted crossing. - predicted_x = min(future_roots) - - # Compute the derivative of the fitted polynomial to obtain the slope. - dp = poly.deriv() - predicted_slope = dp(predicted_x) - - print(f"Predicted time (x) when y reaches {target_value}: {predicted_x}") - print(f"Predicted slope at that time: {predicted_slope}") - - else: - print(f"No future crossing found where y reaches {target_value} based on the available data.") - desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): if self.beacon is None: @@ -314,14 +273,14 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): self._handle_first_run() duration = gcmd.get_int('DURATION', 7200, minval=0) - samples = gcmd.get_int('SAMPLES', 30000, minval=1000) + samples_per_measurement = gcmd.get_int('SAMPLES_PER_MEASUREMENT', 30000, minval=1000) timestamp = time.strftime("%Y%m%d_%H%M%S") - filename = f'/home/pi/printer_data/config/beacon_adaptive_heat_soak_z_rates_{timestamp}.txt' + filename = f'/home/pi/printer_data/config/beacon_adaptive_heat_soak_z_rates_{samples_per_measurement}_{timestamp}.txt' # Make sure we can open the file before starting capture with open(filename, 'w') as f: - gcmd.respond_info(f'Capturing diagnostic z-rates for {duration} seconds using {samples} samples per z-rate calculation to file {filename}, please wait...') + gcmd.respond_info(f'Capturing diagnostic Z-rates for {duration} seconds using {samples_per_measurement} samples per Z-rate calculation to file {filename}, please wait...') start_time = self.reactor.monotonic() z_rate_session = None try: @@ -331,11 +290,11 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): while self.reactor.monotonic() - start_time < duration: # Get the Z rate from the beacon try: - z_rate_result = z_rate_session.get_z_rate(samples) + z_rate_result = z_rate_session.get_z_rate(samples_per_measurement) except Exception as e: - raise self.printer.command_error(f"Error calculating Z rate: {e}") + raise self.printer.command_error(f"Error calculating Z-rate: {e}") - gcmd.respond_info(f"Z rate {z_rate_result[1]:.3f} nm/s") + gcmd.respond_info(f"Z-rate {z_rate_result[1]:.3f} nm/s") if time_zero is None: time_zero = z_rate_result[0] From 1b09866dfeae519e8916e70d829da2088e15ea0d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 8 Jun 2025 20:44:56 +0100 Subject: [PATCH 066/139] Macros/Beacon: update variable comments with new recommendations - discourage use of beacon contact where it will likely cause problems due to localized variation in measurements on textured surfaces - discourage use of METHOD=automatic, as it captures temporal effects due to how long it takes, and also produces data that is not like-for-like with new-style compensation meshes, which use rapid scans --- configuration/z-probe/beacon.cfg | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 23d086497..f92cf6a4c 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -48,7 +48,8 @@ log_points: False ##### [gcode_macro RatOS] variable_beacon_bed_mesh_scv: 25 # square corner velocity for bed meshing with proximity method -variable_beacon_contact_z_homing: False # Make all G28 calls use contact instead of proximity scan +variable_beacon_contact_z_homing: False # Make all G28 calls use contact instead of proximity scan. This is not recommended + # on textured surfaces due to significant variation in contact measurements. variable_beacon_contact_start_print_true_zero: True # Use contact to determine true Z=0 for the last homing move during START_PRINT variable_beacon_contact_wipe_before_true_zero: True # enables a nozzle wipe at Y10 before true zeroing variable_beacon_contact_true_zero_temp: 150 # nozzle temperature for true zeroing @@ -60,12 +61,14 @@ variable_beacon_contact_calibrate_model_on_print: True # Calibrate a new beacon # will be calibrated every print, however True Zero replaces the model offset anyway. variable_beacon_contact_prime_probing: True # probe for priming with contact method -variable_beacon_contact_expansion_compensation: True # enables the nozzle thermal expansion compensation +variable_beacon_contact_expansion_compensation: True # enables hotend thermal expansion compensation -variable_beacon_contact_bed_mesh: False # bed mesh with contact method +variable_beacon_contact_bed_mesh: False # Bed mesh with contact method. This is not recommended on textured surfaces due + # to significant variation in contact measurements. variable_beacon_contact_bed_mesh_samples: 2 # probe samples for contact bed mesh -variable_beacon_contact_z_tilt_adjust: False # z-tilt adjust with contact method +variable_beacon_contact_z_tilt_adjust: False # z-tilt adjust with contact method. This is not recommended on textured surfaces + # due to significant variation in contact measurements. variable_beacon_contact_z_tilt_adjust_samples: 2 # probe samples for contact z-tilt adjust variable_beacon_scan_compensation_enable: False # Enables beacon scan compensation @@ -77,7 +80,8 @@ variable_beacon_scan_compensation_bed_temp_mismatch_is_error: False # different bed temperature will raise an error. Otherwise, a warning is reported. variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the contact poke test -variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option +variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option. This is generally not recommended, and + # specifically not recommended when beacon_scan_compensation_enable is enabled. ##### # BEACON COMMON From 526c7b58f01ee7bfb66ffce1456345d4d57c3609 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 8 Jun 2025 21:21:10 +0100 Subject: [PATCH 067/139] Macros/Beacon: remove unused code --- configuration/z-probe/beacon.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index f92cf6a4c..7c2118a4c 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -523,9 +523,6 @@ gcode: # ratos variables file {% set svv = printer.save_variables.variables %} - # get reference point coordinates - {% set idex_zcontrolpoint = svv.idex_zcontrolpoint|default(150)|float %} - # wait for noozle to reach the probing temperature RATOS_ECHO PREFIX="BEACON" MSG="Waiting for nozzle to reach {temp}°C..." SET_HEATER_TEMPERATURE HEATER={"extruder" if default_toolhead == 0 else "extruder1"} TARGET={temp} From 46a513cadcff6ee972d46ab988f76698dbdf3eeb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 10 Jun 2025 12:00:36 +0100 Subject: [PATCH 068/139] Macros/Beacon: integrate BEACON_WAIT_FOR_PRINTER_HEAT_SOAK wherever bed soaking takes place --- configuration/macros.cfg | 27 +++++++++++-- configuration/z-probe/beacon.cfg | 67 ++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index b0810a95a..54c126d69 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -317,11 +317,19 @@ gcode: {% if printer["dual_carriage"] is defined %} {% set toolchange_standby_temp = printer["gcode_macro RatOS"].toolchange_standby_temp|default(-1)|float %} {% endif %} + {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} # beacon contact config {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} + # beacon adaptive heat soak config + {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} + {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} + # get macro parameters {% set X0 = params.X0|default(-1)|float %} {% set X1 = params.X1|default(-1)|float %} @@ -590,7 +598,7 @@ gcode: _Z_HOP # move toolhead to the oozeguard if needed - {% if idex_mode != '' and not (printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if idex_mode != '' and not (printer.configfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adpative_heat_soak)) %} PARK_TOOLHEAD {% endif %} @@ -613,9 +621,20 @@ gcode: M190 S{bed_temp} # Wait for bed thermal expansion - {% if bed_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." - G4 P{(bed_heat_soak_time * 1000)} + {% if printer.configfile.settings.beacon is defined and beacon_adpative_heat_soak %} + _MOVE_TO_SAFE_Z_HOME + # Must be close to bed for soaking and for beacon proximity measurements + G1 Z2 F{z_speed} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} + RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." + G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} + {% endif %} + {% else %} + {% if bed_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." + G4 P{(bed_heat_soak_time * 1000)} + {% endif %} {% endif %} # Zero z via contact if enabled. diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 7c2118a4c..fa7042555 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -74,7 +74,10 @@ variable_beacon_contact_z_tilt_adjust_samples: 2 # probe samples for cont variable_beacon_scan_compensation_enable: False # Enables beacon scan compensation variable_beacon_scan_compensation_profile: "Beacon Scan Compensation" # The bed mesh profile name for the scan compensation mesh -variable_beacon_scan_compensation_resolution: 8 # The mesh resolution in mm for scan compensation +variable_beacon_scan_compensation_resolution: 8 # The mesh resolution in mm for compensation mesh creation. It is strongly recommended + # to leave this at the default value of 8mm. Compensation mesh creation uses a + # special filtering algorithm to reduce noise in contact measurements which + # relies on sufficiently detailed mesh resolution. variable_beacon_scan_compensation_bed_temp_mismatch_is_error: False # If True, attempting to use a compensation mesh calibrated for a significantly # different bed temperature will raise an error. Otherwise, a warning is reported. @@ -83,6 +86,17 @@ variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the co variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option. This is generally not recommended, and # specifically not recommended when beacon_scan_compensation_enable is enabled. +variable_beacon_adpative_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements +variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking +variable_beacon_adaptive_heat_soak_threshold: 15 # The threshold in nm/s (nanometers per second) for adaptive heat soaking +variable_beacon_adaptive_heat_soak_hold_count: 3 # The number of consecutive measurements that must be within the threshold for + # adaptive heat soaking to be considered complete +variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 + # The extra time in seconds to wait after adaptive heat soaking is considered complete. + # Typically not needed, but can be useful for printers with very stable gantry designs + # (such as steel rail on steel tube) where the adapative heat soak completes before + # the edges of the bed have thermally stabilized. + ##### # BEACON COMMON ##### @@ -234,6 +248,13 @@ gcode: {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} + # beacon adaptive heat soak config + {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} + {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} + # home and abl the printer if needed _BEACON_HOME_AND_ABL @@ -261,11 +282,19 @@ gcode: TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 # Wait for bed thermal expansion - {% if bed_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." - G4 P{(bed_heat_soak_time * 1000)} + {% if beacon_adpative_heat_soak %} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} + RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." + G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} + {% endif %} + {% else %} + {% if bed_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." + G4 P{(bed_heat_soak_time * 1000)} + {% endif %} {% endif %} - + # Make sure we're at safe z-height before calibration _Z_HOP @@ -847,6 +876,14 @@ gcode: {% set mesh_resolution = printer["gcode_macro RatOS"].beacon_scan_compensation_resolution|float %} {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} {% set hotend_heat_soak_time = printer["gcode_macro RatOS"].hotend_heat_soak_time|default(0)|int %} + {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} + + # beacon adaptive heat soak config + {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} + {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object {% set mesh_config = printer.configfile.config.bed_mesh %} @@ -901,10 +938,24 @@ gcode: # Wait for bed thermal expansion {% if not automated %} - {% if bed_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." - G4 P{(bed_heat_soak_time * 1000)} + {% if beacon_adpative_heat_soak %} + _MOVE_TO_SAFE_Z_HOME + # Must be close to bed for soaking and for beacon proximity measurements + G1 Z2 F{z_speed} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} + RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." + G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} + {% endif %} {% else %} + {% if bed_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." + G4 P{(bed_heat_soak_time * 1000)} + {% endif %} + {% endif %} + + # Soak hotend if not already implicitly covered by bed heat soak + {% if not (beacon_adpative_heat_soak or bed_heat_soak_time > 0) %} # Wait for extruder thermal expansion {% if hotend_heat_soak_time > 0 %} RATOS_ECHO MSG="Heat soaking hotend for {hotend_heat_soak_time} seconds..." From 58101fe56bbf79f00deaae78f5f355b4d90e2801 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 10 Jun 2025 12:24:57 +0100 Subject: [PATCH 069/139] Macros/Beacon: add config section for beacon_adaptive_heat_soak --- configuration/z-probe/beacon.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index fa7042555..157c3b8fd 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -39,6 +39,11 @@ contact_max_hotend_temperature: 275 [ratos_z_offset] [beacon_true_zero_correction] +##### +# BEACON ADAPTIVE HEAT SOAK +##### +[beacon_adaptive_heat_soak] + [bed_mesh] mesh_min: 20,30 # TODO: remove when automatically calculated by configurator log_points: False From 6a2b64c75ff36a326a0fbbd430d328eae64f69f6 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 10 Jun 2025 13:22:25 +0100 Subject: [PATCH 070/139] Macros/Beacon: fix typo --- configuration/macros.cfg | 2 +- configuration/z-probe/beacon.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 54c126d69..ef1b37713 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -324,7 +324,7 @@ gcode: {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 157c3b8fd..793b6fcd0 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -254,7 +254,7 @@ gcode: {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} @@ -884,7 +884,7 @@ gcode: {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} From 29e449c6644a7a5b6e7f212bd30b0e38c7452507 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 10 Jun 2025 15:10:50 +0100 Subject: [PATCH 071/139] Beacon/AdaptiveHeatSoak: fix mistake --- configuration/klippy/beacon_adaptive_heat_soak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index d4cc0c7d9..8a420967f 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -241,7 +241,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if len(history) >= moving_average_size: moving_average = np.mean(history[-moving_average_size:]) - if moving_average <= threshold: + if abs(moving_average) <= threshold: hold_count += 1 msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" else: From d37156fbc406b97ebb68c02bfee8d8010860ebcb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 15 Jun 2025 14:02:14 +0100 Subject: [PATCH 072/139] Beacon/AdaptiveHeatSoak: rework, different sampling strategy, add trend checks. - update beacon.cfg defaults to suit. --- .../klippy/beacon_adaptive_heat_soak.py | 486 ++++++++++-------- configuration/z-probe/beacon.cfg | 10 +- 2 files changed, 283 insertions(+), 213 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 8a420967f..5a5ce49c8 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -4,131 +4,90 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import time, struct, logging +import re, time, logging import numpy as np -from multiprocessing import shared_memory, Process, Pipe class BeaconZRateSession: - def __init__(self, config, beacon, reactor): + def __init__(self, config, beacon, samples_per_mean=1000, window_size=30, window_step=1): self.config = config self.name = config.get_name() + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') + self.reactor = self.printer.get_reactor() self.beacon = beacon - self.reactor = reactor - self._shm = None - self._float_size = struct.calcsize('d') # Size of double in bytes - - def cleanup(self): - if self._shm is not None: - self._shm.close() - self._shm.unlink() - self._shm = None - - def _ensure_shared_memory(self, size): - # Ensure the shared memory is large enough. Don't bother shrinking it. - if self._shm is None or self._shm.size < size * self._float_size: - if self._shm is not None: - self._shm.close() - self._shm.unlink() - self._shm = shared_memory.SharedMemory(create=True, size=size * self._float_size) - - def get_z_rate(self, sample_count): - if sample_count <= 2: - raise ValueError("Sample count must be greater than 2 to calculate a rate.") - - # Time values are stored in the first sample_count elements of the shared memory, and distances - # in the second sample_count elements. - self._ensure_shared_memory(sample_count * 2) - samples = None - try: - samples = memoryview(self._shm.buf).cast('d') - out_of_range_count = 0 - i = 0 - - def cb(s): - nonlocal i, samples, out_of_range_count - if i < sample_count: - # s["dist"] is the smoothed distance, we want the unsmoothed data as this is - # cleaner for rate calculations. - time = s["time"] - temp = s["temp"] - data = s["data"] - freq = self.beacon.count_to_freq(data) - dist = self.beacon.freq_to_dist(freq, temp) - if dist is None or np.isinf(dist) or np.isnan(dist): - out_of_range_count += 1 - else: - samples[i] = time - samples[sample_count + i] = dist - i += 1 - - with self.beacon.streaming_session(cb): - eventtime = self.reactor.monotonic() - while i < sample_count: - eventtime = self.reactor.pause(eventtime + 0.1) - if out_of_range_count > 0: - # fail fast, most likely a command was called before the beacon was calibrated or positioned correctly - raise Exception(f"Beacon could not measure a valid distance. Beacon must be calibrated and positioned correctly before running this command.") - - mid_time = (samples[0] + samples[sample_count - 1]) / 2 - - # Set up a pipe to communicate with the child process - parent_conn, child_conn = Pipe() - - child = Process(target=BeaconZRateSession._calculate_z_rate, args=(child_conn, self._shm.name, sample_count)) - child.daemon = True - child.start() + self.samples_per_mean = samples_per_mean + self.window_size = window_size + self.window_step = window_step + + self._mean_distances = [] + self._times = [] + self._sample_buffer = np.zeros(samples_per_mean, dtype=np.float64) + self._step_phase = window_step - window_size # Ensure that phase will be 0 after populating the initial window_size means + + def _get_next_mean(self): + + first_sample_time = None + last_sample_time = None + bad_sample_count = 0 + i = 0 + + def cb(s): + nonlocal i, bad_sample_count, first_sample_time, last_sample_time + if i < self.samples_per_mean: + dist = s["dist"] + if dist is None or np.isinf(dist) or np.isnan(dist): + bad_sample_count += 1 + else: + self._sample_buffer[i] = dist + if i == 0: + first_sample_time = s["time"] - eventtime = self.reactor.monotonic() + i += 1 + + if i == self.samples_per_mean: + last_sample_time = s["time"] - while child.is_alive(): + with self.beacon.streaming_session(cb): + eventtime = self.reactor.monotonic() + while i < self.samples_per_mean: eventtime = self.reactor.pause(eventtime + 0.1) + if bad_sample_count > 100: + # Not expected. Could be that thermal deflection moved the beacon out of range (too close or too far from the bed). + # We've not seen this happen in practice, but we handle it gracefully just in case. + raise self.printer.command_error(f"{self.name}: Unexpected error: Beacon failed to measure a valid distance for {bad_sample_count} out of {bad_sample_count + i} samples.") - is_err, result = parent_conn.recv() - - child.join() - parent_conn.close() - - if is_err: - raise Exception(result) - else: - return (mid_time, result) - finally: - if samples is not None: - samples.release() - del samples - - @staticmethod - def _calculate_z_rate(conn, shm_name, sample_count): - try: - shm = None - samples = None - try: - shm = shared_memory.SharedMemory(name=shm_name) - samples = memoryview(shm.buf).cast('d') - - if len(samples) < sample_count * 2: - raise ValueError("Not enough samples in shared memory") - - # Time values are stored in the first sample_count elements of the shared memory, and distances - # in the second sample_count elements. The shared memory may be larger than sample_count * 2, - # so we take care to only use the first sample_count * 2 values. - coefficients = np.polyfit( - samples[:sample_count], - samples[sample_count:sample_count * 2], 1) - - slope = coefficients[0] # The slope of the line is the rate of change - slope_nm_per_sec = slope * 1e6 # Convert from millimeters to nanometers per second - conn.send((False, slope_nm_per_sec)) - finally: - if samples is not None: - samples.release() - del samples - if shm is not None: - shm.close() - except Exception as e: - conn.send((True, str(e))) - finally: - conn.close() + if bad_sample_count > 0: + logging.warning(f"{self.name}: {bad_sample_count} out of {bad_sample_count + i} samples were invalid.") + + self._step_phase = (self._step_phase + 1) % self.window_step + + # Beacon samples are approximately evenly spaced, so we can use the first and last sample times to calculate the mean time. + mean_time = (first_sample_time + last_sample_time) / 2 + return (mean_time, np.mean(self._sample_buffer)) + + def get_next_z_rate(self): + while True: + if len(self._mean_distances) == self.window_size: + self._mean_distances.pop(0) + self._times.pop(0) + + # The first call to get_next_z_rate will fill the means list with initial values, + # subsequent calls will use the sliding window approach. + while len(self._mean_distances) < self.window_size: + time, mean = self._get_next_mean() + self._mean_distances.append(mean) + self._times.append(time) + + if self._step_phase == 0: + break + + # Fit a 1-degree polynomial (line) to the data + slope, _ = np.polyfit(self._times, self._mean_distances, 1) + + # Convert from millimeters to nanometers per second + slope_nm_per_sec = slope * 1e6 + + return (self._times[len(self._times) // 2], slope_nm_per_sec) class BeaconAdaptiveHeatSoak: def __init__(self, config): @@ -137,31 +96,28 @@ def __init__(self, config): self.printer = config.get_printer() self.reactor = self.printer.get_reactor() self.gcode = self.printer.lookup_object('gcode') - + # Configuration values # The default z-rate threshold in nm/s below which we consider the printer to be thermally stable. self.def_threshold = config.getint('threshold', 15, minval=10) - # The default number of consecutive z-rate reaidings below the threshold before we consider the - # printer to be thermally stable. This is used to avoid false positives due to noise in the data. - self.def_hold_count = config.getint('hold_count', 3, minval=1) + # The default number of continuous seconds with z-rate below the threshold before we consider the + # printer to be thermally stable. + self.def_hold_count = config.getint('hold_count', 150, minval=1) # The default maximum wait time in seconds for the printer to reach thermal stability. self.def_maximum_wait = config.getint('maximum_wait', 5400, minval=0) - # The default number of samples to take per z-rate calculation. This value is only configurable - # to support testing and debugging, and should not be changed in production. - self.def_samples_per_measurement = config.getint('samples_per_measurement', 30000, minval=30000) + # TODO: Make trend checks configurable. # Setup self.reactor = None self.beacon = None - self.first_run = True # Register commands self.gcode.register_command( - 'BEACON_WAIT_FOR_PRINTER_HEAT_SOAK', + 'BEACON_WAIT_FOR_PRINTER_HEAT_SOAK', self.cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK, desc=self.desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK) @@ -184,135 +140,238 @@ def _handle_connect(self): if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') - def _handle_first_run(self): - # We've seen issues where the first streaming_session after a restart begins with some bogus data, - # so we throw away some samples to ensure the beacon is ready. - if self.first_run: - self.first_run = False - i = 0 - def cb(_): - nonlocal i - i += 1 - with self.beacon.streaming_session(cb): - # Wait for 1000 samples to be collected - eventtime = self.reactor.monotonic() - while i < 1000: - eventtime = self.reactor.pause(eventtime + 0.1) - + def _prepare_for_sampling(self): + # We've seen issues where the first streaming_session after some operations begins with some bogus data, + # so we throw away some samples to ensure the beacon is ready. Suspected operations include: + # - klipper restart + # - BEACON_AUTO_CALIBRATE + bad_samples = 0 + good_samples = 0 + + def cb(s): + nonlocal good_samples, bad_samples + dist = s["dist"] + if dist is None or np.isinf(dist) or np.isnan(dist): + bad_samples += 1 + else: + good_samples += 1 + + with self.beacon.streaming_session(cb): + # Wait up to 5 seconds for 1000 good samples to be collected + # This is a bit arbitrary, but it should be enough to ensure the beacon is ready. + start_time = eventtime = self.reactor.monotonic() + while good_samples < 1000 and (eventtime - start_time) < 5: + eventtime = self.reactor.pause(eventtime + 0.1) + + logging.info(f"{self.name}: Prepared for sampling, collected {good_samples} good samples and {bad_samples} bad samples (total {good_samples+bad_samples} samples).") + + if good_samples < 1000: + raise self.printer.command_error(f"Failed to prepare beacon for sampling, timed out waiting for good samples. Beacon must be calibrated and positioned correctly before running this command.") + + def parse_duples_string(s: str) -> tuple: + """ + Parses a string of duples and returns a tuple of tuple of ints. + + The function expects a string in the format: + "(num, num), (num, num), ... " + + It raises a ValueError if the string doesn't match the expected pattern. + + Examples: + "(20, 30),( 1, 99 ) , (100, 234)" -> ((20, 30), (1, 99), (100, 234)) + """ + # Define a regex that must match the entire string. + full_pattern = r'^\s*\(\s*\d+\s*,\s*\d+\s*\)(?:\s*,\s*\(\s*\d+\s*,\s*\d+\s*\))*\s*$' + if not re.fullmatch(full_pattern, s): + raise ValueError("Input string does not match the expected pattern.") + + # Define a pattern to find each tuple of digits. + tuple_pattern = r'\(\s*(\d+)\s*,\s*(\d+)\s*\)' + matches = re.findall(tuple_pattern, s) + + # Convert the string numbers to integers and pack them into tuples. + return tuple((int(x), int(y)) for x, y in matches) + + def _check_trend_projection(self, moving_average_history, moving_average_history_times, trend_fit_window, trend_projection, threshold): + if len(moving_average_history) < trend_fit_window: + # Not enough data to fit a trend + return False + + # Fit window 200 take about 1.5ms on Pi 4B, so for now we work on the assumption that + # processing can take place in the main thread without blocking the reactor for too long. + # If we need longer fit windows, we may need to move this to a separate process. + + # Keep track of time taken, warn if we risk timer too close error. + start_time = self.reactor.monotonic() + + times = np.array(moving_average_history_times[-trend_fit_window:]) + values = np.array(moving_average_history[-trend_fit_window:]) + + # Fit a linear regression to the last `trend_fit_window` samples + slope, intercept = np.polyfit(times, values, 1) + + check_time = times[-1] + trend_projection + check_value = slope * check_time + intercept + + time_taken = self.reactor.monotonic() - start_time + + if time_taken > 3.0: + logging.warning(f"{self.name}: Trend projection check for fit window size {trend_fit_window} took {1000.*time_taken:.3f} ms, which risks causing a Klipper timer too close error. Consider reducing the trend fit window size.") + + self.reactor.pause(self.reactor.NOW) + + return abs(check_value) <= threshold + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon is None: raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") - self._handle_first_run() + if self.beacon.model is None: + raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") - threshold = gcmd.get_float('THRESHOLD', self.def_threshold, minval=10) + self._prepare_for_sampling() + + threshold = gcmd.get_int('THRESHOLD', self.def_threshold, minval=10) target_hold_count = gcmd.get_int('HOLD_COUNT', self.def_hold_count, minval=1) maximum_wait = gcmd.get_int('MAXIMUM_WAIT', self.def_maximum_wait, minval=0) - samples_per_measurement = gcmd.get_int('SAMPLES_PER_MEASUREMENT', self.def_samples_per_measurement, minval=30000) + # TODO: Hard-coded for now, make configurable later + trend_checks = ((75, 675), (200, 675)) - moving_average_size = 5 + moving_average_size = 150 hold_count = 0 - history = [] - z_rate_session = None - - logging.info(f"{self.name}: Starting heat soak with threshold {threshold} nm/s, hold count {target_hold_count}, maximum wait {maximum_wait} seconds, {samples_per_measurement} samples per measurement and moving average size {moving_average_size}") - gcmd.respond_info(f"Waiting for printer to reach thermal stability for up to {maximum_wait} seconds, requiring {target_hold_count} consecutive measurements within the Z-rate threshold of {threshold} nm/s. Please wait...") - + + # z_rate_history is a circular buffer of the last `moving_average_size` z-rates + z_rate_history = [0] * moving_average_size + z_rate_count = 0 + + # moving_average_history grows as we collect more data. The full history is logged at the end of the wait. + moving_average_history = [] + moving_average_history_times = [] + + gcmd.respond_info(f"Waiting up to {self._format_seconds(maximum_wait)} for printer to reach thermal stability. Please wait...") + start_time = self.reactor.monotonic() - - try: - z_rate_session = BeaconZRateSession(self.config, self.beacon, self.reactor) - + + z_rate_session = BeaconZRateSession(self.config, self.beacon) + + ts = time.strftime("%Y%m%d_%H%M%S") + fn = f"/tmp/heat_soak_{ts}.csv" + + logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") + + with open(fn, "w") as z_rates_file: + z_rates_file.write("time,z_rate\n") + time_zero = None + while True: if self.reactor.monotonic() - start_time > maximum_wait: - gcmd.respond_info(f"Maximum wait time of {maximum_wait} seconds exceeded, exiting.") + gcmd.respond_info(f"Maximum wait time of {self._format_seconds(maximum_wait)} exceeded, wait completed.") return # Get the Z rate from the beacon try: - z_rate_result = z_rate_session.get_z_rate(samples_per_measurement) + z_rate_result = z_rate_session.get_next_z_rate() except Exception as e: - logging.error(f"{self.name}: Error calculating Z-rate, wait aborted: {e}") raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") - - history.append(z_rate_result[1]) - if len(history) >= moving_average_size: - moving_average = np.mean(history[-moving_average_size:]) + if time_zero is None: + time_zero = z_rate_result[0] + + z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") + + z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] + z_rate_count += 1 + + moving_average = None + + if z_rate_count >= moving_average_size: + moving_average = np.mean(z_rate_history) + moving_average_history.append(moving_average) + moving_average_history_times.append(z_rate_result[0]) + + if moving_average is not None: + elapsed = self.reactor.monotonic() - start_time + + # Log on every 15th z-rate to avoid flooding the console + should_log = z_rate_count % 15 == 0 if abs(moving_average) <= threshold: hold_count += 1 - msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" + msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" else: - if hold_count > 0: + if hold_count > 0: msg = f"Z-rate {moving_average:.1f} nm/s, moved outside threshold of {threshold} nm/s after {hold_count} consecutive measurements" hold_count = 0 else: msg = f"Z-rate {moving_average:.1f} nm/s, not within threshold of {threshold} nm/s" - - gcmd.respond_info(msg) - + if hold_count >= target_hold_count: - gcmd.respond_info(f"Printer is considered thermally stable after {hold_count} consecutive measurements within threshold of {threshold} nm/s.") - for i in range(0, len(history), 20): - chunk = history[i:i+20] - logging.info(f"{self.name}: raw z-rates: {','.join(f'{v:.6e}' for v in chunk)}") - return - else: - logging.info(f"{self.name}: Z-rate {z_rate_result[1]:.3f} nm/s, not enough samples for moving average yet") - finally: - if z_rate_session is not None: - z_rate_session.cleanup() + # For increased robustness, we perform one or more linear trend checks. Typically this will + # include a trend fitted to a short history window, and a trend fitted to a longer history window. + # Together, these checks ensure that the Z-rate is not only stable but also not trending towards instability. + all_checks_passed = all( + self._check_trend_projection( + moving_average_history, moving_average_history_times, + trend_check[0], trend_check[1], threshold + ) for trend_check in trend_checks) + + if all_checks_passed: + gcmd.respond_info(f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed.") + return + elif should_log: + gcmd.respond_info(msg + f", waiting for trend checks to pass ({self._format_seconds(elapsed)} elapsed)") + elif should_log: + gcmd.respond_info(msg + f" ({self._format_seconds(elapsed)} elapsed)") desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): if self.beacon is None: raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") - self._handle_first_run() + if self.beacon.model is None: + raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") - duration = gcmd.get_int('DURATION', 7200, minval=0) - samples_per_measurement = gcmd.get_int('SAMPLES_PER_MEASUREMENT', 30000, minval=1000) + self._prepare_for_sampling() + duration = gcmd.get_int('DURATION', 7200, minval=0) timestamp = time.strftime("%Y%m%d_%H%M%S") - filename = f'/home/pi/printer_data/config/beacon_adaptive_heat_soak_z_rates_{samples_per_measurement}_{timestamp}.txt' + filename = gcmd.get('FILENAME', 'beacon_adaptive_heat_soak_z_rates') + f"_V2_{timestamp}.csv" - # Make sure we can open the file before starting capture - with open(filename, 'w') as f: - gcmd.respond_info(f'Capturing diagnostic Z-rates for {duration} seconds using {samples_per_measurement} samples per Z-rate calculation to file {filename}, please wait...') + fullpath = f'/home/pi/printer_data/config/{filename}' + + with open(fullpath, 'w') as f: + f.write("time,z_rate\n") + gcmd.respond_info(f'Capturing diagnostic Z-rates for {duration} seconds using V2 Z-rate calculation to file {fullpath}, please wait...') start_time = self.reactor.monotonic() - z_rate_session = None - try: - z_rate_session = BeaconZRateSession(self.config, self.beacon, self.reactor) - history = [] - time_zero = None - while self.reactor.monotonic() - start_time < duration: - # Get the Z rate from the beacon - try: - z_rate_result = z_rate_session.get_z_rate(samples_per_measurement) - except Exception as e: - raise self.printer.command_error(f"Error calculating Z-rate: {e}") - - gcmd.respond_info(f"Z-rate {z_rate_result[1]:.3f} nm/s") - - if time_zero is None: - time_zero = z_rate_result[0] - - history.append((z_rate_result[0] - time_zero, z_rate_result[1])) - - np.savetxt(f, history) - gcmd.respond_info(f'Diagnostic data captured to {filename}') - finally: - if z_rate_session is not None: - z_rate_session.cleanup() + z_rate_session = BeaconZRateSession(self.config, self.beacon) + time_zero = None + + while self.reactor.monotonic() - start_time < duration: + # Get the Z rate from the beacon + try: + z_rate_result = z_rate_session.get_next_z_rate() + except Exception as e: + raise self.printer.command_error(f"Error calculating Z-rate: {e}") + + gcmd.respond_info(f"Z-rate {z_rate_result[1]:.3f} nm/s") + + if time_zero is None: + time_zero = z_rate_result[0] + + f.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") + + gcmd.respond_info(f'Diagnostic data captured to {fullpath}') desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): if self.beacon is None: raise self.printer.command_error("Beacon is not available. Please ensure RatOS is configured correctly.") - - self._handle_first_run() + + if self.beacon.model is None: + raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") + + self._prepare_for_sampling() duration = gcmd.get_int('DURATION', 300, minval=60) chunk_duration = gcmd.get_int('CHUNK_DURATION', 5, minval=5) @@ -325,7 +384,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): start_time = self.reactor.monotonic() while self.reactor.monotonic() - start_time < duration: samples = [] - def cb(s): + def cb(s): unsmooth_data = s["data"] unsmooth_freq = self.beacon.count_to_freq(unsmooth_data) unsmooth_dist = self.beacon.freq_to_dist(unsmooth_freq, s["temp"]) @@ -339,5 +398,14 @@ def cb(s): gcmd.respond_info(f'Diagnostic data captured to {filename}') + def _format_seconds(self, seconds): + seconds = int(seconds) + if seconds < 60: + return f"{seconds}s" + elif seconds < 3600: + return f"{seconds // 60}m {seconds % 60}s" + else: + return f"{seconds // 3600}h {seconds % 3600 // 60}m {seconds % 60}s" + def load_config(config): return BeaconAdaptiveHeatSoak(config) \ No newline at end of file diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 793b6fcd0..5ba97b3ed 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -92,10 +92,12 @@ variable_beacon_scan_method_automatic: False # Enables the METHOD=aut # specifically not recommended when beacon_scan_compensation_enable is enabled. variable_beacon_adpative_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements -variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking -variable_beacon_adaptive_heat_soak_threshold: 15 # The threshold in nm/s (nanometers per second) for adaptive heat soaking -variable_beacon_adaptive_heat_soak_hold_count: 3 # The number of consecutive measurements that must be within the threshold for - # adaptive heat soaking to be considered complete +variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete +variable_beacon_adaptive_heat_soak_threshold: 15 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive + # heat soaking. +variable_beacon_adaptive_heat_soak_hold_count: 150 # The number of continuous seconds with z-rate within the threshold for + # adaptive heat soaking to be considered potentntially complete (there are + # additional trend checks which also have to pass). variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 # The extra time in seconds to wait after adaptive heat soaking is considered complete. # Typically not needed, but can be useful for printers with very stable gantry designs From 79724efd889b67b08f7fdc8a7639d6161778e7b0 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 15 Jun 2025 17:42:19 +0100 Subject: [PATCH 073/139] Beacon/AdaptiveHeatSoak: improve time formatting --- .../klippy/beacon_adaptive_heat_soak.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 5a5ce49c8..97bf8cd00 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -400,12 +400,22 @@ def cb(s): def _format_seconds(self, seconds): seconds = int(seconds) - if seconds < 60: - return f"{seconds}s" - elif seconds < 3600: - return f"{seconds // 60}m {seconds % 60}s" + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + + if hours > 0: + if minutes > 0 or secs > 0: + if secs > 0: + return f"{hours}h {minutes}m {secs}s" + return f"{hours}h {minutes}m" + return f"{hours}h" + elif minutes > 0: + if secs > 0: + return f"{minutes}m {secs}s" + return f"{minutes}m" else: - return f"{seconds // 3600}h {seconds % 3600 // 60}m {seconds % 60}s" + return f"{secs}s" def load_config(config): return BeaconAdaptiveHeatSoak(config) \ No newline at end of file From 483fce613bf137fee35aea47e3895727af6e9e3c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 15 Jun 2025 17:43:00 +0100 Subject: [PATCH 074/139] Beacon/AdaptiveHeatSoak: stop waiting if the printer shuts down --- .../klippy/beacon_adaptive_heat_soak.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 97bf8cd00..0843fc3f4 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -50,6 +50,8 @@ def cb(s): with self.beacon.streaming_session(cb): eventtime = self.reactor.monotonic() while i < self.samples_per_mean: + if self.printer.is_shutdown(): + raise self.printer.command_error(f"{self.name}: Printer is shutting down") eventtime = self.reactor.pause(eventtime + 0.1) if bad_sample_count > 100: # Not expected. Could be that thermal deflection moved the beacon out of range (too close or too far from the bed). @@ -161,6 +163,8 @@ def cb(s): # This is a bit arbitrary, but it should be enough to ensure the beacon is ready. start_time = eventtime = self.reactor.monotonic() while good_samples < 1000 and (eventtime - start_time) < 5: + if self.printer.is_shutdown(): + raise self.printer.command_error(f"{self.name}: Printer is shutting down") eventtime = self.reactor.pause(eventtime + 0.1) logging.info(f"{self.name}: Prepared for sampling, collected {good_samples} good samples and {bad_samples} bad samples (total {good_samples+bad_samples} samples).") @@ -273,7 +277,10 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): try: z_rate_result = z_rate_session.get_next_z_rate() except Exception as e: - raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") + if self.printer.is_shutdown(): + raise + else: + raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") if time_zero is None: time_zero = z_rate_result[0] @@ -352,7 +359,10 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): try: z_rate_result = z_rate_session.get_next_z_rate() except Exception as e: - raise self.printer.command_error(f"Error calculating Z-rate: {e}") + if self.printer.is_shutdown(): + raise + else: + raise self.printer.command_error(f"Error calculating Z-rate: {e}") gcmd.respond_info(f"Z-rate {z_rate_result[1]:.3f} nm/s") @@ -383,6 +393,9 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): gcmd.respond_info(f'Capturing diagnostic beacon samples for {duration} seconds in chunks of {chunk_duration} seconds to file {filename}, please wait...') start_time = self.reactor.monotonic() while self.reactor.monotonic() - start_time < duration: + if self.printer.is_shutdown(): + raise self.printer.command_error(f"{self.name}: Printer is shutting down") + samples = [] def cb(s): unsmooth_data = s["data"] From 6f5ce41a99ddb462f785f659f9ca000abbca467a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 16 Jun 2025 14:10:31 +0100 Subject: [PATCH 075/139] Beacon/AdaptiveHeatSoak: add minimum wait parameter --- .../klippy/beacon_adaptive_heat_soak.py | 19 +++++++++++++------ configuration/macros.cfg | 5 +++-- configuration/z-probe/beacon.cfg | 11 +++++++---- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 0843fc3f4..fb2d756bd 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -111,6 +111,9 @@ def __init__(self, config): # The default maximum wait time in seconds for the printer to reach thermal stability. self.def_maximum_wait = config.getint('maximum_wait', 5400, minval=0) + # The default minimum wait time in seconds for the printer to reach thermal stability. + self.def_minimum_wait = config.getint('minimum_wait', 0, minval=0) + # TODO: Make trend checks configurable. # Setup @@ -238,7 +241,9 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): threshold = gcmd.get_int('THRESHOLD', self.def_threshold, minval=10) target_hold_count = gcmd.get_int('HOLD_COUNT', self.def_hold_count, minval=1) + minimum_wait = gcmd.get_int('MINIMUM_WAIT', self.def_minimum_wait, minval=0) maximum_wait = gcmd.get_int('MAXIMUM_WAIT', self.def_maximum_wait, minval=0) + # TODO: Hard-coded for now, make configurable later trend_checks = ((75, 675), (200, 675)) @@ -249,11 +254,11 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): z_rate_history = [0] * moving_average_size z_rate_count = 0 - # moving_average_history grows as we collect more data. The full history is logged at the end of the wait. moving_average_history = [] moving_average_history_times = [] - gcmd.respond_info(f"Waiting up to {self._format_seconds(maximum_wait)} for printer to reach thermal stability. Please wait...") + wait_str = f"between {self._format_seconds(minimum_wait)} and {self._format_seconds(maximum_wait)}" if minimum_wait > 0 else f"up to {self._format_seconds(maximum_wait)}" + gcmd.respond_info(f"Waiting for {wait_str} for printer to reach thermal stability. Please wait...") start_time = self.reactor.monotonic() @@ -262,7 +267,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): ts = time.strftime("%Y%m%d_%H%M%S") fn = f"/tmp/heat_soak_{ts}.csv" - logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") + logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") with open(fn, "w") as z_rates_file: z_rates_file.write("time,z_rate\n") @@ -273,7 +278,6 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): gcmd.respond_info(f"Maximum wait time of {self._format_seconds(maximum_wait)} exceeded, wait completed.") return - # Get the Z rate from the beacon try: z_rate_result = z_rate_session.get_next_z_rate() except Exception as e: @@ -324,8 +328,11 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): ) for trend_check in trend_checks) if all_checks_passed: - gcmd.respond_info(f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed.") - return + if elapsed < minimum_wait: + gcmd.respond_info(msg + f", trend checks pass, waiting for minimum of {self._format_seconds(elapsed)} to elapse ({self._format_seconds(elapsed)} elapsed)") + else: + gcmd.respond_info(f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed.") + return elif should_log: gcmd.respond_info(msg + f", waiting for trend checks to pass ({self._format_seconds(elapsed)} elapsed)") elif should_log: diff --git a/configuration/macros.cfg b/configuration/macros.cfg index ef1b37713..35186f70e 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -325,9 +325,10 @@ gcode: # beacon adaptive heat soak config {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get macro parameters @@ -625,7 +626,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2 F{z_speed} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 5ba97b3ed..bd175b59c 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -92,6 +92,7 @@ variable_beacon_scan_method_automatic: False # Enables the METHOD=aut # specifically not recommended when beacon_scan_compensation_enable is enabled. variable_beacon_adpative_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements +variable_beacon_adpative_heat_soak_min_wait: 840 # The minimum time in seconds to wait for adaptive heat soaking to complete. variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete variable_beacon_adaptive_heat_soak_threshold: 15 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive # heat soaking. @@ -257,9 +258,10 @@ gcode: # beacon adaptive heat soak config {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # home and abl the printer if needed @@ -290,7 +292,7 @@ gcode: # Wait for bed thermal expansion {% if beacon_adpative_heat_soak %} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} @@ -887,9 +889,10 @@ gcode: # beacon adaptive heat soak config {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(3)|int %} + {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object @@ -949,7 +952,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2 F{z_speed} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} From 5eb8437c936c4f95098aa3389d393e9d978b809e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 16 Jun 2025 17:52:35 +0100 Subject: [PATCH 076/139] Macros: move calculation of safe home position to ratos.py, expose via status - if beacon is configured, checks that the calculated position is within the probe-able region, klipper shutdown if not - removes a lot of duplicated macro code, now only a single macro line is needed to get safe_home_x and safe_home_y --- configuration/homing.cfg | 25 ++++------------- configuration/klippy/ratos.py | 47 ++++++++++++++++++++++++++------ configuration/macros.cfg | 22 ++------------- configuration/macros/mesh.cfg | 23 ++-------------- configuration/z-probe/beacon.cfg | 9 +----- 5 files changed, 49 insertions(+), 77 deletions(-) diff --git a/configuration/homing.cfg b/configuration/homing.cfg index e5315ada4..3fa1eba25 100644 --- a/configuration/homing.cfg +++ b/configuration/homing.cfg @@ -79,10 +79,7 @@ gcode: {% set homing_x = homing_x if homing_x else homing %} {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} # parameter {% set X = true if params.X|lower == 'true' else false %} @@ -125,11 +122,8 @@ gcode: {% set homing_y = homing_y if homing_y else homing %} {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} - + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} + # parameter {% set X = true if params.X|lower == 'true' else false %} {% set Y = true if params.Y|lower == 'true' else false %} @@ -341,22 +335,13 @@ gcode: description: Move to safe home position with optional Z_HOP (pass Z_HOP=True as parameter) gcode: {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} - {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% if params.Z_HOP is defined %} _Z_HOP {% endif %} - DEBUG_ECHO PREFIX="_MOVE_TO_SAFE_Z_HOME" MSG="axis_maximum.x: {printer.toolhead.axis_maximum.x}, axis_maximum.y: {printer.toolhead.axis_maximum.y}, bed_margin_x: {printer['gcode_macro RatOS'].bed_margin_x}, bed_margin_y: {printer['gcode_macro RatOS'].bed_margin_y}, safe_home_x: {safe_home_x}, safe_home_y: {safe_home_y}, printable_x_max: {printable_x_max}, printable_y_max: {printable_y_max}" + DEBUG_ECHO PREFIX="_MOVE_TO_SAFE_Z_HOME" MSG="axis_maximum.x: {printer.toolhead.axis_maximum.x}, axis_maximum.y: {printer.toolhead.axis_maximum.y}, bed_margin_x: {printer['gcode_macro RatOS'].bed_margin_x}, bed_margin_y: {printer['gcode_macro RatOS'].bed_margin_y}, safe_home_x: {safe_home_x}, safe_home_y: {safe_home_y}, printable_x_max: {printer['gcode_macro RatOS'].printable_x_max}, printable_y_max: {printer['gcode_macro RatOS'].printable_y_max}" # Go to safe home G0 X{safe_home_x} Y{safe_home_y} F{speed} diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index fb85c75c2..043b12d95 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -2,6 +2,18 @@ import json, subprocess, pathlib from collections import namedtuple +BeaconProbingRegions = namedtuple('BeaconProbingRegions', + ['x_offset', 'y_offset', 'proximity_min', 'proximity_max', 'contact_min', 'contact_max']) +""" + A named tuple containing: + - x_offset: X offset of the Beacon probe + - y_offset: Y offset of the Beacon probe + - proximity_min: Tuple of (min_x, min_y) for proximity probing + - proximity_max: Tuple of (max_x, max_y) for proximity probing + - contact_min: Tuple of (min_x, min_y) for contact probing + - contact_max: Tuple of (max_x, max_y) for contact probing +""" + ##### # RatOS ##### @@ -479,7 +491,7 @@ def _process_output(eventtime): if not complete: process.terminate() self.console_echo("Post-processing failed", "error", "Post processing timed out after 30 minutes.") - return False; + return False if process.returncode != 0: # We should've already printed the error message in _interpret_output @@ -488,9 +500,9 @@ def _process_output(eventtime): logging.error(error) self.post_process_success = False - return False; + return False - return self.post_process_success; + return self.post_process_success except Exception as e: raise @@ -577,7 +589,7 @@ def get_ratos_version(self): self.debug_echo("get_ratos_version", ("Exception on run: %s", exc)) return version - def get_beacon_probing_regions(self): + def get_beacon_probing_regions(self) -> BeaconProbingRegions: """Gets the probing regions configuration for the Beacon probe, or None if not available. Returns: BeaconProbingRegions or None: A named tuple containing: @@ -592,8 +604,7 @@ def get_beacon_probing_regions(self): if self.beacon is None: return None - return namedtuple('BeaconProbingRegions', - ['x_offset', 'y_offset', 'proximity_min', 'proximity_max', 'contact_min', 'contact_max'])( + return BeaconProbingRegions( x_offset=self.beacon.x_offset, y_offset=self.beacon.y_offset, proximity_min=(self.beacon.mesh_helper.def_min_x, self.beacon.mesh_helper.def_min_y), @@ -601,11 +612,31 @@ def get_beacon_probing_regions(self): contact_min=tuple(self.beacon.mesh_helper.def_contact_min), contact_max=tuple(self.beacon.mesh_helper.def_contact_max)) - def get_status(self, eventtime): + def get_safe_home_position(self): + printable_x_max = float(self.gm_ratos.variables['printable_x_max']) + printable_y_max = float(self.gm_ratos.variables['printable_y_max']) + safe_home_x = self.gm_ratos.variables.get('safe_home_x', None) + safe_home_y = self.gm_ratos.variables.get('safe_home_y', None) + safe_home_x = printable_x_max / 2 if safe_home_x is None or str(safe_home_x).lower() == 'middle' else float(safe_home_x) + safe_home_y = printable_y_max / 2 if safe_home_y is None or str(safe_home_y).lower() == 'middle' else float(safe_home_y) + + bpr = self.get_beacon_probing_regions() + if bpr is not None: + safe_min_x = max(bpr.proximity_min[0], bpr.contact_min[0]) + safe_max_x = min(bpr.proximity_max[0], bpr.contact_max[0]) + safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) + safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) + if safe_home_x < safe_min_x or safe_home_x > safe_max_x or safe_home_y < safe_min_y or safe_home_y > safe_max_y: + self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x}, {safe_home_y}) is outside this region.") + + return (safe_home_x, safe_home_y) + + def get_status(self, eventtime=None): return { 'name': self.name, 'last_processed_file_result': self.last_processed_file_result, - 'last_check_bed_mesh_profile_exists_result': self.last_check_bed_mesh_profile_exists_result } + 'last_check_bed_mesh_profile_exists_result': self.last_check_bed_mesh_profile_exists_result, + 'safe_home_position': self.get_safe_home_position() } ##### # Stack trace diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 35186f70e..b8452e0f2 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -840,16 +840,7 @@ gcode: {% set max_temp = printer["gcode_macro RatOS"].preheat_extruder_temp|float + 5 %} {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} - {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set idex_mode = '' %} {% if printer["dual_carriage"] is defined %} @@ -915,16 +906,7 @@ gcode: {% set max_temp = printer["gcode_macro RatOS"].preheat_extruder_temp|float + 5 %} {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} - {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} # automated toolhead z-offset calibration config {% set auto_z_offset_calibration = False %} diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 9bc682985..4be60ca12 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -13,16 +13,7 @@ variable_adaptive_mesh: True # True|False = enable adaptive [gcode_macro _BED_MESH_SANITY_CHECK] gcode: {% if printer.configfile.settings.bed_mesh.zero_reference_position is defined %} - {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} @@ -72,17 +63,7 @@ gcode: # config {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} - {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} - + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is not defined %} {% set zero_ref_pos = [safe_home_x, safe_home_y] %} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index bd175b59c..1eebfcf2a 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -714,14 +714,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} - {% set safe_home_x = printer["gcode_macro RatOS"].safe_home_x %} - {% if safe_home_x is not defined or safe_home_x|lower == 'middle' %} - {% set safe_home_x = printable_x_max / 2 %} - {% endif %} - {% set safe_home_y = printer["gcode_macro RatOS"].safe_home_y %} - {% if safe_home_y is not defined or safe_home_y|lower == 'middle' %} - {% set safe_home_y = printable_y_max / 2 %} - {% endif %} + {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set poke_bottom = printer["gcode_macro RatOS"].beacon_contact_poke_bottom_limit|default(-1)|float %} # beacon config From 878a3eefd848402c91106e7181df7f2ed8a1815d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 16 Jun 2025 21:02:23 +0100 Subject: [PATCH 077/139] Homing/Beacon: rationalize beacon homing and true zero behaviour - Remove unnecessary BEACON_AUTO_CALIBRATE in START_PRINT - Move _MOVE_TO_SAFE_Z_HOME to ratos.py, and enable position fuzzing. The associated DEBUG_ECHO is still a text macro for easy editing for debugging. - Fuzz the position of BEACON_AUTO_CALIBRATE calls other than those that set the final critical z for printing - Skip multipoint probing in BEACON_AUTO_CALIBRATE except for calls that set the final critical z for printing --- configuration/homing.cfg | 51 ++++++-------- .../klippy/beacon_true_zero_correction.py | 11 ++- configuration/klippy/ratos.py | 70 ++++++++++++++----- configuration/macros.cfg | 21 ++---- configuration/z-probe/beacon.cfg | 17 +++-- 5 files changed, 100 insertions(+), 70 deletions(-) diff --git a/configuration/homing.cfg b/configuration/homing.cfg index 3fa1eba25..6687662ae 100644 --- a/configuration/homing.cfg +++ b/configuration/homing.cfg @@ -15,8 +15,7 @@ variable_safe_home_x: "middle" # float|middle = z-homing x locat variable_safe_home_y: "middle" # float|middle = z-homing y location variable_driver_type_x: "tmc2209" # tmc2209|tmc2130|tmc5160 = stepper driver type for sensorless homing variable_driver_type_y: "tmc2209" # tmc2209|tmc2130|tmc5160 = stepper driver type for sensorless homing -variable_stowable_probe_stop_on_error: False # internal use only. Do not touch! - +variable_stowable_probe_stop_on_error: False # internal use only. Do not touch! [ratos_homing] axes: xyz @@ -123,7 +122,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} - + # parameter {% set X = true if params.X|lower == 'true' else false %} {% set Y = true if params.Y|lower == 'true' else false %} @@ -153,6 +152,8 @@ gcode: # beacon contact config {% set beacon_contact_z_homing = true if printer["gcode_macro RatOS"].beacon_contact_z_homing|default(false)|lower == 'true' else false %} + {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} + {% set beacon_z_home_position_fuzzing_radius = printer["gcode_macro RatOS"].beacon_z_home_position_fuzzing_radius|default(0)|float %} # parameter {% set X = true if params.X|lower == 'true' else false %} @@ -169,24 +170,25 @@ gcode: {% else %} {% if z_probe == "stowable" %} DEPLOY_PROBE - _MOVE_TO_SAFE_Z_HOME - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 - _MOVE_TO_SAFE_Z_HOME Z_HOP=True + {% endif %} + {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} + {% if beacon_contact_start_print_true_zero %} + # The critical zero probing happens during start print after heat soaking, not here. + _MOVE_TO_SAFE_Z_HOME FUZZY_RADIUS={beacon_z_home_position_fuzzing_radius} + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 {% else %} - G28 Z + # The critical zero probing happens here. + _MOVE_TO_SAFE_Z_HOME + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 {% endif %} - _Z_HOP - STOW_PROBE + _MOVE_TO_SAFE_Z_HOME Z_HOP=True {% else %} _MOVE_TO_SAFE_Z_HOME - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 - _MOVE_TO_SAFE_Z_HOME Z_HOP=True - {% else %} - G28 Z - {% endif %} - _Z_HOP + G28 Z + {% endif %} + _Z_HOP + {% if z_probe == "stowable" %} + STOW_PROBE {% endif %} {% endif %} {% endif %} @@ -331,20 +333,9 @@ gcode: G0 Z{z_hop} F{z_hop_speed} -[gcode_macro _MOVE_TO_SAFE_Z_HOME] -description: Move to safe home position with optional Z_HOP (pass Z_HOP=True as parameter) +[gcode_macro __MOVE_TO_SAFE_Z_HOME_ECHO_DEBUG] gcode: - {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} - {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} - - {% if params.Z_HOP is defined %} - _Z_HOP - {% endif %} - - DEBUG_ECHO PREFIX="_MOVE_TO_SAFE_Z_HOME" MSG="axis_maximum.x: {printer.toolhead.axis_maximum.x}, axis_maximum.y: {printer.toolhead.axis_maximum.y}, bed_margin_x: {printer['gcode_macro RatOS'].bed_margin_x}, bed_margin_y: {printer['gcode_macro RatOS'].bed_margin_y}, safe_home_x: {safe_home_x}, safe_home_y: {safe_home_y}, printable_x_max: {printer['gcode_macro RatOS'].printable_x_max}, printable_y_max: {printer['gcode_macro RatOS'].printable_y_max}" - - # Go to safe home - G0 X{safe_home_x} Y{safe_home_y} F{speed} + DEBUG_ECHO PREFIX="_MOVE_TO_SAFE_Z_HOME" MSG="x: {params.X}, y: {params.Y}, z_hop: {params.Z_HOP}, fuzzy_radius: {params.FUZZY_RADIUS}, axis_maximum.x: {printer.toolhead.axis_maximum.x}, axis_maximum.y: {printer.toolhead.axis_maximum.y}, bed_margin_x: {printer['gcode_macro RatOS'].bed_margin_x}, bed_margin_y: {printer['gcode_macro RatOS'].bed_margin_y}, printable_x_max: {printer['gcode_macro RatOS'].printable_x_max}, printable_y_max: {printer['gcode_macro RatOS'].printable_y_max}" [gcode_macro MAYBE_HOME] description: Only home unhomed axis diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index 7e6e52f1b..c454deb58 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -346,8 +346,15 @@ def run(self): nozzle_tip_dia = self.tzc._get_nozzle_tip_diameter() - # Calculate the nozzle-based min span as the length of the side of a - # square with area four times the footprint of COUNT nozzle tips. + # Calculate the nozzle-based min span as the length of the side of a square with area four times + # the footprint of COUNT nozzle tips. + # + # As an indicative maximum span for mainstream nozzles, a 1.2mm nozzle with 13 points and 15 retries + # would have a minimum span of 23.9 mm. As nozzle diameters increase, so the typical first layer height + # will increase. There will likely be a point where using true zero correction no longer makes sense, + # as any error would be absorbed comfortably by the first layer height. For reference, something like + # a GammaMaster 2.4mm nozzle with 13 points and 15 retries would have a minimum span of 35.2mm. + nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * num_points_to_generate * 4.) span = max(min_span, nozzle_based_min_span) half_span = span / 2. diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 043b12d95..bb5c31c72 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,5 +1,5 @@ import os, logging, glob, traceback, inspect, re -import json, subprocess, pathlib +import json, subprocess, pathlib, random, math from collections import namedtuple BeaconProbingRegions = namedtuple('BeaconProbingRegions', @@ -96,23 +96,24 @@ def load_settings(self): # Gcode commands ##### def register_commands(self): - self.gcode.register_command('HELLO_RATOS', self.cmd_HELLO_RATOS, desc=(self.desc_HELLO_RATOS)) - self.gcode.register_command('CACHE_IS_GRAPH_FILES', self.cmd_CACHE_IS_GRAPH_FILES, desc=(self.desc_CACHE_IS_GRAPH_FILES)) - self.gcode.register_command('SHOW_IS_GRAPH_FILES', self.cmd_SHOW_IS_GRAPH_FILES, desc=(self.desc_SHOW_IS_GRAPH_FILES)) - self.gcode.register_command('CONSOLE_ECHO', self.cmd_CONSOLE_ECHO, desc=(self.desc_CONSOLE_ECHO)) - self.gcode.register_command('RATOS_LOG', self.cmd_RATOS_LOG, desc=(self.desc_RATOS_LOG)) - self.gcode.register_command('PROCESS_GCODE_FILE', self.cmd_PROCESS_GCODE_FILE, desc=(self.desc_PROCESS_GCODE_FILE)) - self.gcode.register_command('ALLOW_UNKNOWN_GCODE_GENERATOR', self.cmd_ALLOW_UNKNOWN_GCODE_GENERATOR, desc=(self.desc_ALLOW_UNKNOWN_GCODE_GENERATOR)) - self.gcode.register_command('BYPASS_GCODE_PROCESSING', self.cmd_BYPASS_GCODE_PROCESSING, desc=(self.desc_BYPASS_GCODE_PROCESSING)) - self.gcode.register_command('_SYNC_GCODE_POSITION', self.cmd_SYNC_GCODE_POSITION, desc=(self.desc_SYNC_GCODE_POSITION)) - self.gcode.register_command('_CHECK_BED_MESH_PROFILE_EXISTS', self.cmd_CHECK_BED_MESH_PROFILE_EXISTS, desc=(self.desc_CHECK_BED_MESH_PROFILE_EXISTS)) - self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=(self.desc_RAISE_ERROR)) - self.gcode.register_command('_TRY', self.cmd_TRY, desc=(self.desc_TRY)) - self.gcode.register_command('_DEBUG_ECHO_STACK_TRACE', self.cmd_DEBUG_ECHO_STACK_TRACE, desc=(self.desc_DEBUG_ECHO_STACK_TRACE)) + self.gcode.register_command('HELLO_RATOS', self.cmd_HELLO_RATOS, desc=self.desc_HELLO_RATOS) + self.gcode.register_command('CACHE_IS_GRAPH_FILES', self.cmd_CACHE_IS_GRAPH_FILES, desc=self.desc_CACHE_IS_GRAPH_FILES) + self.gcode.register_command('SHOW_IS_GRAPH_FILES', self.cmd_SHOW_IS_GRAPH_FILES, desc=self.desc_SHOW_IS_GRAPH_FILES) + self.gcode.register_command('CONSOLE_ECHO', self.cmd_CONSOLE_ECHO, desc=self.desc_CONSOLE_ECHO) + self.gcode.register_command('RATOS_LOG', self.cmd_RATOS_LOG, desc=self.desc_RATOS_LOG) + self.gcode.register_command('PROCESS_GCODE_FILE', self.cmd_PROCESS_GCODE_FILE, desc=self.desc_PROCESS_GCODE_FILE) + self.gcode.register_command('ALLOW_UNKNOWN_GCODE_GENERATOR', self.cmd_ALLOW_UNKNOWN_GCODE_GENERATOR, desc=self.desc_ALLOW_UNKNOWN_GCODE_GENERATOR) + self.gcode.register_command('BYPASS_GCODE_PROCESSING', self.cmd_BYPASS_GCODE_PROCESSING, desc=self.desc_BYPASS_GCODE_PROCESSING) + self.gcode.register_command('_SYNC_GCODE_POSITION', self.cmd_SYNC_GCODE_POSITION, desc=self.desc_SYNC_GCODE_POSITION) + self.gcode.register_command('_CHECK_BED_MESH_PROFILE_EXISTS', self.cmd_CHECK_BED_MESH_PROFILE_EXISTS, desc=self.desc_CHECK_BED_MESH_PROFILE_EXISTS) + self.gcode.register_command('_RAISE_ERROR', self.cmd_RAISE_ERROR, desc=self.desc_RAISE_ERROR) + self.gcode.register_command('_TRY', self.cmd_TRY, desc=self.desc_TRY) + self.gcode.register_command('_DEBUG_ECHO_STACK_TRACE', self.cmd_DEBUG_ECHO_STACK_TRACE, desc=self.desc_DEBUG_ECHO_STACK_TRACE) + self.gcode.register_command('_MOVE_TO_SAFE_Z_HOME', self.cmd_MOVE_TO_SAFE_Z_HOME, desc=self.desc_MOVE_TO_SAFE_Z_HOME) def register_command_overrides(self): - self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=(self.desc_TEST_RESONANCES)) - self.register_override('SHAPER_CALIBRATE', self.override_SHAPER_CALIBRATE, desc=(self.desc_SHAPER_CALIBRATE)) + self.register_override('TEST_RESONANCES', self.override_TEST_RESONANCES, desc=self.desc_TEST_RESONANCES) + self.register_override('SHAPER_CALIBRATE', self.override_SHAPER_CALIBRATE, desc=self.desc_SHAPER_CALIBRATE) def register_override(self, command, func, desc): if self.overridden_commands[command] is not None: @@ -302,7 +303,6 @@ def cmd_PROCESS_GCODE_FILE(self, gcmd): else: self.console_echo('Print aborted', 'error') - ##### # Gcode Post Processor ##### @@ -630,7 +630,41 @@ def get_safe_home_position(self): self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x}, {safe_home_y}) is outside this region.") return (safe_home_x, safe_home_y) - + + desc_MOVE_TO_SAFE_Z_HOME = "Move to safe home position with optional Z_HOP (pass Z_HOP=True as parameter)" + def cmd_MOVE_TO_SAFE_Z_HOME(self, gcmd): + speed = float(self.gm_ratos.variables['macro_travel_speed']) * 60 + fuzzy_radius = gcmd.get_float('FUZZY_RADIUS', 0, minval=0.) + z_hop = gcmd.get('Z_HOP', '').lower() in ('true', 'yes', '1') + x, y = self.get_safe_home_position() + + if fuzzy_radius > 0: + # Set the home position to a random point on a circle with the given radius centred on the safe home position. + angle = random.uniform(0, 2 * math.pi) + x += fuzzy_radius * math.cos(angle) + y += fuzzy_radius * math.sin(angle) + # Limit to the beacon probing region if defined + bpr = self.get_beacon_probing_regions() + if bpr is not None: + safe_min_x = max(bpr.proximity_min[0], bpr.contact_min[0]) + safe_max_x = min(bpr.proximity_max[0], bpr.contact_max[0]) + safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) + safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) + x = max(safe_min_x, min(x, safe_max_x)) + y = max(safe_min_y, min(y, safe_max_y)) + else: + # Limit to printable area if no beacon probing region is defined + printable_x_max = float(self.gm_ratos.variables['printable_x_max']) + printable_y_max = float(self.gm_ratos.variables['printable_y_max']) + x = max(0, min(x, printable_x_max)) + y = max(0, min(y, printable_y_max)) + + if z_hop: + self.gcode.run_script_from_command("_Z_HOP") + + self.gcode.run_script_from_command(f"__MOVE_TO_SAFE_Z_HOME_ECHO_DEBUG SAFE_HOME_X={x} SAFE_HOME_Y={y} FUZZY_RADIUS={fuzzy_radius} Z_HOP={z_hop}") + self.gcode.run_script_from_command(f"G0 X{x} Y{y} F{speed}") + def get_status(self, eventtime=None): return { 'name': self.name, diff --git a/configuration/macros.cfg b/configuration/macros.cfg index b8452e0f2..51a34eb40 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -638,17 +638,6 @@ gcode: {% endif %} {% endif %} - # Zero z via contact if enabled. - {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} - _MOVE_TO_SAFE_Z_HOME - # Calibrate a new beacon model if enabled. This adapts the beacon model to the current build plate. - {% if beacon_contact_calibrate_model_on_print %} - BEACON_AUTO_CALIBRATE - {% else %} - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 - {% endif %} - {% endif %} - # Run the user created "AFTER_HEATING_BED" macro _USER_START_PRINT_AFTER_HEATING_BED { rawparams } @@ -1018,9 +1007,8 @@ gcode: # beacon contact config {% set beacon_contact_true_zero_temp = printer["gcode_macro RatOS"].beacon_contact_true_zero_temp|default(150)|int %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% set beacon_contact_true_zero_location = printer["gcode_macro RatOS"].beacon_contact_true_zero_location|default("front")|lower %} - {% set beacon_contact_true_zero_margin_x = printer["gcode_macro RatOS"].beacon_contact_true_zero_margin_x|default(30)|int %} {% set beacon_contact_wipe_before_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_wipe_before_true_zero|default(false)|lower == 'true' else false %} + {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} # wipe before z-calibration {% if beacon_contact_wipe_before_true_zero %} @@ -1043,7 +1031,12 @@ gcode: TEMPERATURE_WAIT SENSOR={"extruder" if default_toolhead == 0 else "extruder1"} MINIMUM={beacon_contact_true_zero_temp} MAXIMUM={beacon_contact_true_zero_temp + 5} # auto calibration RATOS_ECHO MSG="Beacon contact auto calibration..." - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + # This is the critical probe for true zero. Don't skip multipoint probing, and don't fuzz the position. + {% if beacon_contact_calibrate_model_on_print %} + BEACON_AUTO_CALIBRATE + {% else %} + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + {% endif %} # raise z G0 Z5 F{z_speed} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 1eebfcf2a..e18c7497b 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -105,6 +105,11 @@ variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 # (such as steel rail on steel tube) where the adapative heat soak completes before # the edges of the bed have thermally stabilized. +##### +# INTERNAL USE ONLY! DO NOT TOUCH! +##### +variable_beacon_z_home_position_fuzzing_radius: 20 + ##### # BEACON COMMON ##### @@ -227,7 +232,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME # auto calibrate beacon - BEACON_AUTO_CALIBRATE + BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 # raise toolhead to safe z-height _Z_HOP @@ -308,7 +313,7 @@ gcode: _Z_HOP # auto calibrate beacon - BEACON_AUTO_CALIBRATE + BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 # turn bed and extruder heaters off {% if not automated %} @@ -918,7 +923,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME # Home z with contact - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 # visual feedback _LED_BEACON_CALIBRATION_START @@ -997,7 +1002,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME Z_HOP=True # home z - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 # visual feedback {% if not automated %} @@ -1098,7 +1103,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME Z_HOP=True # Home z with contact - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 # automatic bed leveling {% set needs_rehoming = False %} @@ -1120,7 +1125,7 @@ gcode: # Home again as Z will have changed after automatic bed leveling. {% if needs_rehoming %} - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 {% endif %} [gcode_macro _BEACON_SUBTRACT_CONTACT_EXPANSION_OFFSET] From 832b12723af7bb4a2938b539ed03143bd87d4305 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 18 Jun 2025 12:08:40 +0100 Subject: [PATCH 078/139] Mesh/Beacon/TrueZero: support fuzzy true zero position, and ensure that adaptive mesh includes true zero position - The previous commit fuzzed non-true zero contact probes when true zero probe was enabled, as this was the easier change rather than the most correct behaviour. In fact, along with the other fixes in that commit, the only probing that was fuzzed was if contact homing was enabled. This commit removes that fuzzing, and implements fuzzy true zero. --- configuration/homing.cfg | 6 ++-- configuration/klippy/ratos.py | 36 ++++++++++++++++------ configuration/macros.cfg | 20 +++++++++--- configuration/macros/mesh.cfg | 52 +++++++++++++++++++++++--------- configuration/z-probe/beacon.cfg | 6 +++- 5 files changed, 86 insertions(+), 34 deletions(-) diff --git a/configuration/homing.cfg b/configuration/homing.cfg index 6687662ae..dd545be8c 100644 --- a/configuration/homing.cfg +++ b/configuration/homing.cfg @@ -171,19 +171,17 @@ gcode: {% if z_probe == "stowable" %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} + _MOVE_TO_SAFE_Z_HOME + {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} {% if beacon_contact_start_print_true_zero %} # The critical zero probing happens during start print after heat soaking, not here. - _MOVE_TO_SAFE_Z_HOME FUZZY_RADIUS={beacon_z_home_position_fuzzing_radius} BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 {% else %} # The critical zero probing happens here. - _MOVE_TO_SAFE_Z_HOME BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 {% endif %} _MOVE_TO_SAFE_Z_HOME Z_HOP=True {% else %} - _MOVE_TO_SAFE_Z_HOME G28 Z {% endif %} _Z_HOP diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index bb5c31c72..547a56dcb 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -51,6 +51,7 @@ def __init__(self, config): # Status fields self.last_processed_file_result = None self.last_check_bed_mesh_profile_exists_result = None + self.last_move_to_safe_z_home_position = None self.old_is_graph_files = [] self.load_settings() @@ -627,7 +628,7 @@ def get_safe_home_position(self): safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) if safe_home_x < safe_min_x or safe_home_x > safe_max_x or safe_home_y < safe_min_y or safe_home_y > safe_max_y: - self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x}, {safe_home_y}) is outside this region.") + self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x:.2f}, {safe_home_y:.2f}) is outside this region.") return (safe_home_x, safe_home_y) @@ -639,10 +640,15 @@ def cmd_MOVE_TO_SAFE_Z_HOME(self, gcmd): x, y = self.get_safe_home_position() if fuzzy_radius > 0: - # Set the home position to a random point on a circle with the given radius centred on the safe home position. + # Set the home position to a random point anywhere within the circle centered on the safe home position + # Generate random radius between 0 and fuzzy_radius (sqrt of random ensures uniform distribution) + random_radius = fuzzy_radius * math.sqrt(random.random()) + # Generate random angle angle = random.uniform(0, 2 * math.pi) - x += fuzzy_radius * math.cos(angle) - y += fuzzy_radius * math.sin(angle) + # Calculate new position + x += random_radius * math.cos(angle) + y += random_radius * math.sin(angle) + # Limit to the beacon probing region if defined bpr = self.get_beacon_probing_regions() if bpr is not None: @@ -650,27 +656,37 @@ def cmd_MOVE_TO_SAFE_Z_HOME(self, gcmd): safe_max_x = min(bpr.proximity_max[0], bpr.contact_max[0]) safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) - x = max(safe_min_x, min(x, safe_max_x)) - y = max(safe_min_y, min(y, safe_max_y)) + constrained_x = max(safe_min_x, min(x, safe_max_x)) + constrained_y = max(safe_min_y, min(y, safe_max_y)) else: # Limit to printable area if no beacon probing region is defined printable_x_max = float(self.gm_ratos.variables['printable_x_max']) printable_y_max = float(self.gm_ratos.variables['printable_y_max']) - x = max(0, min(x, printable_x_max)) - y = max(0, min(y, printable_y_max)) + constrained_x = max(0, min(x, printable_x_max)) + constrained_y = max(0, min(y, printable_y_max)) + + if (constrained_x, constrained_y) != (x, y): + logging.warning(f"{self.name}: _MOVE_TO_SAFE_Z_HOME: fuzzy position had to be constrained to fit within the printable or beacon probing regions, the intended fuzzy behaviour may not be achieved.") + x, y = constrained_x, constrained_y if z_hop: self.gcode.run_script_from_command("_Z_HOP") - self.gcode.run_script_from_command(f"__MOVE_TO_SAFE_Z_HOME_ECHO_DEBUG SAFE_HOME_X={x} SAFE_HOME_Y={y} FUZZY_RADIUS={fuzzy_radius} Z_HOP={z_hop}") + self.gcode.run_script_from_command(f"__MOVE_TO_SAFE_Z_HOME_ECHO_DEBUG X={x} Y={y} FUZZY_RADIUS={fuzzy_radius} Z_HOP={z_hop}") self.gcode.run_script_from_command(f"G0 X{x} Y{y} F{speed}") + self.last_move_to_safe_z_home_position = (x, y) + def get_status(self, eventtime=None): return { 'name': self.name, 'last_processed_file_result': self.last_processed_file_result, 'last_check_bed_mesh_profile_exists_result': self.last_check_bed_mesh_profile_exists_result, - 'safe_home_position': self.get_safe_home_position() } + # The configured safe home position + 'safe_home_position': self.get_safe_home_position(), + # The last position moved to by _MOVE_TO_SAFE_Z_HOME, which may differ from safe_home_position + # if FUZZY_RADIUS was used. + 'last_move_to_safe_z_home_position': self.last_move_to_safe_z_home_position } ##### # Stack trace diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 51a34eb40..ff23640b2 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -360,7 +360,7 @@ gcode: _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL BEACON_CONTACT_START_PRINT_TRUE_ZERO={beacon_contact_start_print_true_zero} BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT={beacon_contact_calibrate_model_on_print} {% endif %} - _START_PRINT_PREFLIGHT_CHECK_BED_MESH BED_TEMP={bed_temp} + _START_PRINT_PREFLIGHT_CHECK_AND_RESET_BED_MESH BED_TEMP={bed_temp} # echo first print coordinates RATOS_ECHO MSG="First print coordinates X:{first_x} Y:{first_y}" @@ -981,7 +981,6 @@ gcode: _START_PRINT_AFTER_HEATING_CONTACT_WITH_OPTIONAL_WIPE {% endif %} - [gcode_macro _START_PRINT_AFTER_HEATING_BED_PROBE_FOR_WIPE] gcode: # config @@ -997,6 +996,8 @@ gcode: BEACON_QUERY +# NB: Called only from _START_PRINT_AFTER_HEATING_BED, after bed levelling, and only when +# beacon_contact_start_print_true_zero is true. [gcode_macro _START_PRINT_AFTER_HEATING_CONTACT_WITH_OPTIONAL_WIPE] gcode: # config @@ -1009,6 +1010,8 @@ gcode: {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_wipe_before_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_wipe_before_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} + {% set beacon_contact_start_print_true_zero_fuzzy_position = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero_fuzzy_position|default(false)|lower == 'true' else false %} + {% set fuzzy_radius = printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero_fuzzy_radius|default(0)|float if beacon_contact_start_print_true_zero_fuzzy_position else 0 %} # wipe before z-calibration {% if beacon_contact_wipe_before_true_zero %} @@ -1022,9 +1025,13 @@ gcode: {% endif %} {% endif %} - # moving to safe z-calibration position + # moving to safe z-calibration position, with optional fuzzing. G0 Z5 F{z_speed} - _MOVE_TO_SAFE_Z_HOME + _MOVE_TO_SAFE_Z_HOME FUZZY_RADIUS={fuzzy_radius} + + # Store printer.ratos.last_move_to_safe_z_home_position for later use when setting mesh zero reference + _START_PRINT_AFTER_HEATING_CONTACT_WITH_OPTIONAL_WIPE_STORE_TRUE_ZERO_POSITION + # set probing toolhead to probing temperature RATOS_ECHO MSG="Heating extruder to probing temperature..." SET_HEATER_TEMPERATURE HEATER={"extruder" if default_toolhead == 0 else "extruder1"} TARGET={beacon_contact_true_zero_temp} @@ -1040,6 +1047,11 @@ gcode: # raise z G0 Z5 F{z_speed} +[gcode_macro _START_PRINT_AFTER_HEATING_CONTACT_WITH_OPTIONAL_WIPE_STORE_TRUE_ZERO_POSITION] +gcode: + {% set pos = printer.ratos.last_move_to_safe_z_home_position %} + SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_x VALUE={pos[0]} + SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_y VALUE={pos[1]} [gcode_macro _START_PRINT_AFTER_HEATING_EXTRUDER] gcode: diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 4be60ca12..98d547f7a 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -12,13 +12,17 @@ variable_adaptive_mesh: True # True|False = enable adaptive [gcode_macro _BED_MESH_SANITY_CHECK] gcode: - {% if printer.configfile.settings.bed_mesh.zero_reference_position is defined %} + {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} + {% if zero_ref_pos is defined %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} + {% set beacon_contact_start_print_true_zero_fuzzy_position = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero_fuzzy_position|default(false)|lower == 'true' else false %} {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} - {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} - {% if zero_ref_pos is not defined or zero_ref_pos[0]|float|round != safe_home_x|float|round or zero_ref_pos[1]|float|round != safe_home_y|float|round %} + {% if beacon_contact_start_print_true_zero_fuzzy_position %} + CONSOLE_ECHO TYPE="error" TITLE="Defined zero reference not compatible with fuzzy true zero" MSG="Using fuzzy start print true zero position is not compatible with a defined zero_reference_position value in the [bed_mesh] section in printer.cfg._N_Please remove the zero_reference_position value from the [bed_mesh] section, or disable fuzzy true zero position in printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_start_print_true_zero_fuzzy_position: False" + _STOP_AND_RAISE_ERROR MSG="Defined zero reference not compatible with fuzzy true zero" + {% elif zero_ref_pos[0]|float|round != safe_home_x|float|round or zero_ref_pos[1]|float|round != safe_home_y|float|round %} CONSOLE_ECHO TYPE="error" TITLE="Zero reference position does not match safe home position" MSG="Please update your bed mesh zero reference position in printer.cfg, like so:_N__N_[bed_mesh]_N_zero_reference_position: {safe_home_x|float|round},{safe_home_y|float|round}_N_" _STOP_AND_RAISE_ERROR MSG="Zero reference position does not match safe home position" {% endif %} @@ -41,14 +45,18 @@ gcode: CONSOLE_ECHO TYPE="warning" TITLE="Beacon contact z-tilt adjust is enabled" MSG="Beacon contact z-tilt adjust is enabled. This is not recommended for normal use and may result in inaccurate z-tilt adjustment._N_Please disable it in your printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_z_tilt_adjust: False" {% endif %} -[gcode_macro _START_PRINT_PREFLIGHT_CHECK_BED_MESH] +[gcode_macro _START_PRINT_PREFLIGHT_CHECK_AND_RESET_BED_MESH] gcode: + SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_x VALUE=None + SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_y VALUE=None # Error early if the scan compensation mesh is required but does not exist {% if printer.configfile.settings.beacon is defined %} _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION {rawparams} {% endif %} [gcode_macro _START_PRINT_BED_MESH] +variable_actual_true_zero_position_x: None # Internal use only! This supports the fuzzy true zero position feature. +variable_actual_true_zero_position_y: None # Internal use only! This supports the fuzzy true zero position feature. gcode: # Handle toolhead settings CACHE_TOOLHEAD_SETTINGS KEY="start_print_bed_mesh" @@ -66,7 +74,7 @@ gcode: {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is not defined %} - {% set zero_ref_pos = [safe_home_x, safe_home_y] %} + {% set zero_ref_pos = [safe_home_x if actual_true_zero_position_x is none else actual_true_zero_position_x, safe_home_y if actual_true_zero_position_y is none else actual_true_zero_position_y] %} {% endif %} @@ -92,11 +100,11 @@ gcode: {% if printer["gcode_macro RatOS"].calibrate_bed_mesh|lower == 'true' %} BED_MESH_CLEAR {% if printer["gcode_macro RatOS"].adaptive_mesh|lower == 'true' %} - CALIBRATE_ADAPTIVE_MESH PROFILE="{default_profile}" X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} + CALIBRATE_ADAPTIVE_MESH PROFILE="{default_profile}" X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} ZERO_REF_POS_X={zero_ref_pos[0]} ZERO_REF_POS_Y={zero_ref_pos[1]} {% else %} {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" @@ -110,13 +118,13 @@ gcode: {% endif %} {% endif %} BED_MESH_PROFILE LOAD="{default_profile}" - + SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} {% elif printer["gcode_macro RatOS"].bed_mesh_profile is defined %} BED_MESH_CLEAR BED_MESH_PROFILE LOAD="{printer["gcode_macro RatOS"].bed_mesh_profile}" - + SET_ZERO_REFERENCE_POSITION X={zero_ref_pos[0]} Y={zero_ref_pos[1]} {% endif %} @@ -147,7 +155,7 @@ gcode: {% set x1 = params.X1|default(-1)|float %} {% set y1 = params.Y1|default(-1)|float %} - RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Recieved coordinates X0={x0} Y0={y0} X1={x1} Y1={y1}" + RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Recieved coordinates X0={x0|round(1)} Y0={y0|round(1)} X1={x1|round(1)} Y1={y1|round(1)}" {% if x0 >= x1 or y0 >= y1 %} # coordinates are invalid, fall back to full bed mesh @@ -157,7 +165,7 @@ gcode: BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} - BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" + BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" {% else %} BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} @@ -167,6 +175,20 @@ gcode: BED_MESH_CALIBRATE PROFILE="{default_profile}" {% endif %} {% else %} + # Zero reference position. If both values are specified, the mesh region will be expanded to include the zero reference position. + # This is necessary so that SET_ZERO_REFERENCE_POSITION can meaningfully set the zero reference position for the mesh. + {% if params.ZERO_REF_POS_X is defined and params.ZERO_REF_POS_Y is defined %} + {% set zero_ref_pos_x = params.ZERO_REF_POS_X|float %} + {% set zero_ref_pos_y = params.ZERO_REF_POS_Y|float %} + {% if zero_ref_pos_x < x0 or zero_ref_pos_x > x1 or zero_ref_pos_y < y0 or zero_ref_pos_y > y1 %} + RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Zero reference position X={zero_ref_pos_x|round(1)} Y={zero_ref_pos_y|round(1)} is outside the mesh region, expanding mesh region to include it." + {% set x0 = [x0, zero_ref_pos_x]|min %} + {% set y0 = [y0, zero_ref_pos_y]|min %} + {% set x1 = [x1, zero_ref_pos_x]|max %} + {% set y1 = [y1, zero_ref_pos_y]|max %} + {% endif %} + {% endif %} + # get bed mesh config object {% set mesh_config = printer.configfile.config.bed_mesh %} @@ -187,7 +209,7 @@ gcode: RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Print is using the full bed, falling back to full bed mesh." {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} - BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" + BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} {% if beacon_scan_method_automatic %} BED_MESH_CALIBRATE METHOD=automatic PROFILE="{default_profile}" @@ -273,7 +295,7 @@ gcode: {% endif %} # mesh - RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0} Y0={mesh_y0} X1={mesh_x1} Y1={mesh_y1}" + RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0|round(1)} Y0={mesh_y0|round(1)} X1={mesh_x1|round(1)} Y1={mesh_y1}|round(1)}" {% if printer.configfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 @@ -288,7 +310,7 @@ gcode: {% else %} BED_MESH_CALIBRATE PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} {% endif %} - + # probe for priming {% if should_prime and not probe_first %} {% if printer["dual_carriage"] is not defined %} @@ -309,6 +331,6 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe|lower == 'stowable' %} STOW_PROBE {% endif %} - + {% endif %} {% endif %} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index e18c7497b..cf40a3089 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -56,6 +56,10 @@ variable_beacon_bed_mesh_scv: 25 # square corner velocity variable_beacon_contact_z_homing: False # Make all G28 calls use contact instead of proximity scan. This is not recommended # on textured surfaces due to significant variation in contact measurements. variable_beacon_contact_start_print_true_zero: True # Use contact to determine true Z=0 for the last homing move during START_PRINT +variable_beacon_contact_start_print_true_zero_fuzzy_position: True + # Use a fuzzy (randomized) position for the true zero contact measurement so that + # the nozzle does not always contact the same point on the bed. This can help to + # avoid creating a wear mark on the bed surface from always using the same point. variable_beacon_contact_wipe_before_true_zero: True # enables a nozzle wipe at Y10 before true zeroing variable_beacon_contact_true_zero_temp: 150 # nozzle temperature for true zeroing # WARNING: if you're using a smooth PEI sheet, be careful with the temperature @@ -108,7 +112,7 @@ variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 ##### # INTERNAL USE ONLY! DO NOT TOUCH! ##### -variable_beacon_z_home_position_fuzzing_radius: 20 +variable_beacon_contact_start_print_true_zero_fuzzy_radius: 20 ##### # BEACON COMMON From 9c3cfc18803baafb042fec5d88c123fee719db25 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 18 Jun 2025 18:50:50 +0100 Subject: [PATCH 079/139] Beacon/Mesh: when creating a compensation mesh, store chamber temp and rapid scan mesh bounds - These may be used for compatibility checks later (tbd), capturing data now so that OG comp meshes can be checked in the future. --- configuration/klippy/beacon_mesh.py | 38 +++++++++++++++++++++++++++-- configuration/z-probe/beacon.cfg | 2 +- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 073d38ea9..5fa65a632 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -36,6 +36,10 @@ RATOS_MESH_BED_TEMP_PARAMETER = "ratos_bed_temp" # - the prevailing target bed temp when the mesh was created. For a compensated mesh, it's the # target bed temp of the source measured mesh. +RATOS_MESH_CHAMBER_TEMP_PARAMETER = "ratos_chamber_temp" +# - the demanded chamber temp when the mesh was created. +RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER = "ratos_proximity_mesh_bounds" +# - only for compensation meshes, the bounds of the proximity mesh that was used to make the compensation mesh. left, bottom, right, top (aka min x,y, max x,y) RATOS_MESH_KIND_PARAMETER = "ratos_mesh_kind" RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER = "ratos_beacon_probe_method" # - for measured meshes, it's the probe method of measurement @@ -278,12 +282,13 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) # Using minval=4 to avoid BedMesh defaulting to using Lagrangian interpolation which appears to be broken probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=4) + chamber_temp = gcmd.get_float('CHAMBER_TEMP', 0) if not profile.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - self.create_compensation_mesh(gcmd, profile, probe_count) + self.create_compensation_mesh(gcmd, profile, probe_count, chamber_temp) desc_REMAKE_BEACON_COMPENSATION_MESH = "TESTING! PROFILE='exising comp mesh' NEW_PROFILE='new name' [GAUSSIAN_SIGMA=x]" def cmd_REMAKE_BEACON_COMPENSATION_MESH(self, gcmd): @@ -565,7 +570,7 @@ def _do_local_low_filter(data, lowpass_sigma=1., num_keep=4, num_keep_edge=3, nu # 5. Return the new array. Don't leak numpy types to the caller. return filtered_data.tolist() - def create_compensation_mesh(self, gcmd, profile, probe_count): + def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): if not self.beacon: self.ratos.console_echo("Create compensation mesh error", "error", "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") @@ -626,6 +631,9 @@ def create_compensation_mesh(self, gcmd, profile, probe_count): scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) + scan_mesh_params = scan_before_zmesh.get_mesh_params() + scan_mesh_bounds = (scan_mesh_params["min_x"], scan_mesh_params["min_y"], + scan_mesh_params["max_x"], scan_mesh_params["max_y"]) self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % contact_mesh_name) @@ -696,6 +704,12 @@ def create_compensation_mesh(self, gcmd, profile, probe_count): params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY + + # Store a few fields that might be useful for compatibility checking in the future, + # but the checks don't yet exist. + params[RATOS_MESH_CHAMBER_TEMP_PARAMETER] = chamber_temp + params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = scan_mesh_bounds + new_mesh = BedMesh.ZMesh(params, profile) new_mesh.build_mesh(compensation_mesh_points) self.bed_mesh.set_mesh(new_mesh) @@ -817,6 +831,15 @@ def load_extra_mesh_params(self): mesh_kind = config.getchoice(RATOS_MESH_KIND_PARAMETER, list(RATOS_MESH_KIND_CHOICES)) mesh_probe_method = config.getchoice(RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER, list(RATOS_MESH_BEACON_PROBE_METHOD_CHOICES)) mesh_bed_temp = config.getfloat(RATOS_MESH_BED_TEMP_PARAMETER) + mesh_chamber_temp = config.getfloat(RATOS_MESH_CHAMBER_TEMP_PARAMETER, None) + mesh_proximity_mesh_bounds_str = config.get(RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER, None) + if mesh_proximity_mesh_bounds_str: + # "(min_x,min_y,max_x,max_y)" format + mesh_proximity_mesh_bounds = tuple(float(x) for x in mesh_proximity_mesh_bounds_str.strip("()").split(",")) + if len(mesh_proximity_mesh_bounds) != 4: + raise config.error(f"Invalid value for {RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER}: {mesh_proximity_mesh_bounds_str}") + else: + mesh_proximity_mesh_bounds = None notes = config.get(RATOS_MESH_NOTES_PARAMETER, None) except config.error as ex: self.ratos.console_echo("RatOS Beacon bed mesh management", "error", @@ -828,10 +851,21 @@ def load_extra_mesh_params(self): profile_params[RATOS_MESH_KIND_PARAMETER] = mesh_kind profile_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = mesh_probe_method profile_params[RATOS_MESH_BED_TEMP_PARAMETER] = mesh_bed_temp + if notes: profile_params[RATOS_MESH_NOTES_PARAMETER] = notes else: profile_params.pop(RATOS_MESH_NOTES_PARAMETER, None) + + if mesh_chamber_temp is not None: + profile_params[RATOS_MESH_CHAMBER_TEMP_PARAMETER] = mesh_chamber_temp + else: + profile_params.pop(RATOS_MESH_CHAMBER_TEMP_PARAMETER, None) + + if mesh_proximity_mesh_bounds is not None: + profile_params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = mesh_proximity_mesh_bounds + else: + profile_params.pop(RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER, None) else: self.ratos.console_echo("RatOS Beacon bed mesh management", "warning", f"Bed mesh profile '{profile_name}' was created without extended RatOS Beacon bed mesh support." diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index cf40a3089..31f26a78b 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -989,7 +989,7 @@ gcode: {% endif %} # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} KEEP_TEMP_MESHES={keep_temp_meshes} + CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} CHAMBER_TEMP=chamber_temp KEEP_TEMP_MESHES={keep_temp_meshes} # turn bed and extruder heaters off {% if not automated %} From 4e7556896ddf28062928d1c706b900fea7362c93 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 20 Jun 2025 18:06:30 +0100 Subject: [PATCH 080/139] Beacon/AdaptiveHeatSoak: accommodate noisier z rates that are sometimes encountered, and update default configuration. - configuration update is based on revised noise handling and test data analysis --- configuration/klippy/beacon_adaptive_heat_soak.py | 3 ++- configuration/z-probe/beacon.cfg | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index fb2d756bd..cef7c11d8 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -247,7 +247,8 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): # TODO: Hard-coded for now, make configurable later trend_checks = ((75, 675), (200, 675)) - moving_average_size = 150 + # Moving average size was determined experimentally, and provides a good balance between responsiveness and stability. + moving_average_size = 180 hold_count = 0 # z_rate_history is a circular buffer of the last `moving_average_size` z-rates diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 31f26a78b..767109270 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -96,9 +96,9 @@ variable_beacon_scan_method_automatic: False # Enables the METHOD=aut # specifically not recommended when beacon_scan_compensation_enable is enabled. variable_beacon_adpative_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements -variable_beacon_adpative_heat_soak_min_wait: 840 # The minimum time in seconds to wait for adaptive heat soaking to complete. +variable_beacon_adpative_heat_soak_min_wait: 0 # The minimum time in seconds to wait for adaptive heat soaking to complete. variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete -variable_beacon_adaptive_heat_soak_threshold: 15 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive +variable_beacon_adaptive_heat_soak_threshold: 20 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive # heat soaking. variable_beacon_adaptive_heat_soak_hold_count: 150 # The number of continuous seconds with z-rate within the threshold for # adaptive heat soaking to be considered potentntially complete (there are From 17059c3c90886067595b535ebb2b9a9e975d33b1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 10:58:49 +0100 Subject: [PATCH 081/139] Beacon/AdaptiveHeatSoak: fix reporting of minimum wait time --- configuration/klippy/beacon_adaptive_heat_soak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index cef7c11d8..8e1429fd1 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -330,7 +330,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if all_checks_passed: if elapsed < minimum_wait: - gcmd.respond_info(msg + f", trend checks pass, waiting for minimum of {self._format_seconds(elapsed)} to elapse ({self._format_seconds(elapsed)} elapsed)") + gcmd.respond_info(msg + f", trend checks pass, waiting for minimum of {self._format_seconds(minimum_wait)} to elapse ({self._format_seconds(elapsed)} elapsed)") else: gcmd.respond_info(f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed.") return From 833a73bb209d33f9eab2f05b2878338eaf8d2b52 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 11:00:12 +0100 Subject: [PATCH 082/139] Beacon/AdaptiveHeatSoak: fix spelling of adaptive --- configuration/macros.cfg | 6 +++--- configuration/z-probe/beacon.cfg | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index ff23640b2..e1f0acaf9 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -324,7 +324,7 @@ gcode: {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} @@ -599,7 +599,7 @@ gcode: _Z_HOP # move toolhead to the oozeguard if needed - {% if idex_mode != '' and not (printer.configfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adpative_heat_soak)) %} + {% if idex_mode != '' and not (printer.configfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adaptive_heat_soak)) %} PARK_TOOLHEAD {% endif %} @@ -622,7 +622,7 @@ gcode: M190 S{bed_temp} # Wait for bed thermal expansion - {% if printer.configfile.settings.beacon is defined and beacon_adpative_heat_soak %} + {% if printer.configfile.settings.beacon is defined and beacon_adaptive_heat_soak %} _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2 F{z_speed} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 767109270..ec4f09223 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -95,8 +95,8 @@ variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the co variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option. This is generally not recommended, and # specifically not recommended when beacon_scan_compensation_enable is enabled. -variable_beacon_adpative_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements -variable_beacon_adpative_heat_soak_min_wait: 0 # The minimum time in seconds to wait for adaptive heat soaking to complete. +variable_beacon_adaptive_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements +variable_beacon_adaptive_heat_soak_min_wait: 0 # The minimum time in seconds to wait for adaptive heat soaking to complete. variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete variable_beacon_adaptive_heat_soak_threshold: 20 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive # heat soaking. @@ -266,7 +266,7 @@ gcode: {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} @@ -300,7 +300,7 @@ gcode: TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 # Wait for bed thermal expansion - {% if beacon_adpative_heat_soak %} + {% if beacon_adaptive_heat_soak %} BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." @@ -890,7 +890,7 @@ gcode: {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adpative_heat_soak = true if printer["gcode_macro RatOS"].beacon_adpative_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} @@ -950,7 +950,7 @@ gcode: # Wait for bed thermal expansion {% if not automated %} - {% if beacon_adpative_heat_soak %} + {% if beacon_adaptive_heat_soak %} _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2 F{z_speed} @@ -967,7 +967,7 @@ gcode: {% endif %} # Soak hotend if not already implicitly covered by bed heat soak - {% if not (beacon_adpative_heat_soak or bed_heat_soak_time > 0) %} + {% if not (beacon_adaptive_heat_soak or bed_heat_soak_time > 0) %} # Wait for extruder thermal expansion {% if hotend_heat_soak_time > 0 %} RATOS_ECHO MSG="Heat soaking hotend for {hotend_heat_soak_time} seconds..." From 8fd0a3e05888ef81c9babda20d09d38146571671 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 24 Jun 2025 10:22:03 +0100 Subject: [PATCH 083/139] Beacon/AdaptiveHeatSoak: increase moving average window - in response to additional test data and analysis --- configuration/klippy/beacon_adaptive_heat_soak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 8e1429fd1..d0c5de7f4 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -248,7 +248,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): trend_checks = ((75, 675), (200, 675)) # Moving average size was determined experimentally, and provides a good balance between responsiveness and stability. - moving_average_size = 180 + moving_average_size = 210 hold_count = 0 # z_rate_history is a circular buffer of the last `moving_average_size` z-rates From 733f09a8a7d8d938af68d2ddc453ea72f897aff0 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 24 Jun 2025 12:15:06 +0100 Subject: [PATCH 084/139] Macros/Beacon: fix pre-soak routine in BEACON_CREATE_SCAN_COMPENSATION_MESH --- configuration/z-probe/beacon.cfg | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index ec4f09223..ba1b03a75 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -884,6 +884,7 @@ gcode: # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} + {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} {% set mesh_resolution = printer["gcode_macro RatOS"].beacon_scan_compensation_resolution|float %} {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} {% set hotend_heat_soak_time = printer["gcode_macro RatOS"].hotend_heat_soak_time|default(0)|int %} @@ -924,10 +925,7 @@ gcode: MAYBE_HOME # Go to safe home - _MOVE_TO_SAFE_Z_HOME - - # Home z with contact - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 + _MOVE_TO_SAFE_Z_HOME Z_HOP=True # visual feedback _LED_BEACON_CALIBRATION_START @@ -948,12 +946,20 @@ gcode: TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 {% endif %} + # Do true zero now that the bed is at temperature + {% if beacon_contact_calibrate_model_on_print %} + BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 + {% else %} + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 + {% endif %} + # Wait for bed thermal expansion {% if not automated %} + _MOVE_TO_SAFE_Z_HOME + # Must be close to bed for soaking and for beacon proximity measurements. Use 2.5 instead of 2 to allow for + # more gantry deflection. + G1 Z2.5 F{z_speed} {% if beacon_adaptive_heat_soak %} - _MOVE_TO_SAFE_Z_HOME - # Must be close to bed for soaking and for beacon proximity measurements - G1 Z2 F{z_speed} BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." From c994f90d6059a434a12cae7c3e5b4988bc76350a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 24 Jun 2025 12:16:18 +0100 Subject: [PATCH 085/139] Macros/Beacon: use z=2.5 as heat soaking height to allow for more gantry deflection --- configuration/macros.cfg | 2 +- configuration/z-probe/beacon.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index e1f0acaf9..5a6375af3 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -625,7 +625,7 @@ gcode: {% if printer.configfile.settings.beacon is defined and beacon_adaptive_heat_soak %} _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements - G1 Z2 F{z_speed} + G1 Z2.5 F{z_speed} BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index ba1b03a75..f502fddd6 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -288,7 +288,7 @@ gcode: G90 # lower toolhead to heat soaking z height - G0 Z2 F{z_hop_speed} + G0 Z2.5 F{z_hop_speed} # echo RATOS_ECHO MSG="Waiting for calibration temperature..." From 94c81fdba8354864f928d9880345f740d8c5bb42 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 24 Jun 2025 12:30:15 +0100 Subject: [PATCH 086/139] Macros/Beacon: fix passing of chamber_temp argument --- configuration/z-probe/beacon.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index f502fddd6..72e2764fc 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -995,7 +995,7 @@ gcode: {% endif %} # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} CHAMBER_TEMP=chamber_temp KEEP_TEMP_MESHES={keep_temp_meshes} + CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} CHAMBER_TEMP={chamber_temp} KEEP_TEMP_MESHES={keep_temp_meshes} # turn bed and extruder heaters off {% if not automated %} From 336a85eee4820b24c5c79daa9e59bfddc82c580e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 24 Jun 2025 12:35:13 +0100 Subject: [PATCH 087/139] Macros/Beacon: fix misleading comment --- configuration/macros.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 5a6375af3..688405f7c 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -1038,7 +1038,7 @@ gcode: TEMPERATURE_WAIT SENSOR={"extruder" if default_toolhead == 0 else "extruder1"} MINIMUM={beacon_contact_true_zero_temp} MAXIMUM={beacon_contact_true_zero_temp + 5} # auto calibration RATOS_ECHO MSG="Beacon contact auto calibration..." - # This is the critical probe for true zero. Don't skip multipoint probing, and don't fuzz the position. + # This is the critical probe for true zero. Don't skip multipoint probing. {% if beacon_contact_calibrate_model_on_print %} BEACON_AUTO_CALIBRATE {% else %} From 1330081c0e0775e67454a7c23af998760cb9139e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 26 Jun 2025 10:15:13 +0100 Subject: [PATCH 088/139] Macros: add initconfigfile module --- configuration/klippy/initconfigfile.py | 75 ++++++++++++++++++++++++++ configuration/macros.cfg | 5 ++ configuration/scripts/ratos-common.sh | 1 + 3 files changed, 81 insertions(+) create mode 100644 configuration/klippy/initconfigfile.py diff --git a/configuration/klippy/initconfigfile.py b/configuration/klippy/initconfigfile.py new file mode 100644 index 000000000..6b13f7da0 --- /dev/null +++ b/configuration/klippy/initconfigfile.py @@ -0,0 +1,75 @@ +# Exposes an immutable snapshot of the printer's configuration status from initialization time, +# with singleton-like copy semantics to avoid costly deep copies when accessing the status. +# +# Intended for use in macros accessing `printer.configfile`. Accessing `printer.configfile` +# causes a deep copy of the configuration status, which can be expensive, notably for +# large configurations, potentially leading to `timer too close` errors. +# +# Macros should instead access `printer.initconfigfile` to get the immutable status. Note that +# this status is a fixed snapshot from when Kipper initializes, so it will not reflect changes made +# to the configuration after Kipper initializes - for example, updated PID tuning results and +# bed mesh profile additions or removals. +# +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +from collections.abc import Mapping +import copy + +class ImmutablePrinterConfigStatusWrapper(Mapping): + + def __init__(self, config): + self._printer = config.get_printer() + self._printer.register_event_handler("klippy:connect", + self._handle_connect) + + def _handle_connect(self): + pconfig = self._printer.lookup_object('configfile') + eventtime = self._printer.get_reactor().monotonic() + self._immutable_status = copy.deepcopy(pconfig.get_status(eventtime)) + self._immutable_status.pop('save_config_pending', None) + self._immutable_status.pop('save_config_pending_items', None) + + def get_status(self, eventtime=None): + return self + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + + memo[id(self)] = self + return self + + def __copy__(self): + return self + + def __getitem__(self, key): + return self._immutable_status[key] + + def __contains__(self, key): + return key in self._immutable_status + + def __iter__(self): + return iter(self._immutable_status) + + def __len__(self): + return len(self._immutable_status) + + def __repr__(self): + return f"{self.__class__.__name__}({self._immutable_status!r})" + + def get(self, key, default=None): + return self._immutable_status.get(key, default) + + def keys(self): + return self._immutable_status.keys() + + def values(self): + return self._immutable_status.values() + + def items(self): + return self._immutable_status.items() + +def load_config(config): + return ImmutablePrinterConfigStatusWrapper(config) \ No newline at end of file diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 688405f7c..73372b7bd 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -2,6 +2,11 @@ # To override settings from this file, you can copy and paste the relevant # sections into your printer.cfg and change it there. +##### +# initconfigfile +##### +[initconfigfile] + ##### # INCLUDE MACRO FILES ##### diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index bef425599..846566a82 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -194,6 +194,7 @@ verify_registered_extensions() ["ratos_z_offset_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos_z_offset.py") ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") ["beacon_adaptive_heatsoak_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_adaptive_heat_soak.py") + ["initconfigfile"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/initconfigfile.py") ) declare -A kinematics_extensions=( From 76be52fade4d901267127fa1a3ed07d359513223 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 26 Jun 2025 11:06:35 +0100 Subject: [PATCH 089/139] Macros: use printer.initconfigfile instead of printer.configfile - accessing printer.configfile causes a deep copy of the configuration which can take 10's of milliseconds, which can cause timer too close errors. --- configuration/homing.cfg | 22 ++++---- configuration/macros.cfg | 34 ++++++------- configuration/macros/idex/idex_is.cfg | 4 +- configuration/macros/idex/toolheads.cfg | 10 ++-- configuration/macros/idex/util.cfg | 12 ++--- configuration/macros/idex/vaoc.cfg | 6 +-- configuration/macros/mesh.cfg | 24 ++++----- configuration/macros/overrides.cfg | 10 ++-- configuration/macros/parking.cfg | 2 +- configuration/macros/priming.cfg | 50 +++++++++---------- configuration/macros/util.cfg | 8 +-- .../printers/v-core-3-idex/macros.cfg | 2 +- configuration/printers/v-core-3/macros.cfg | 2 +- .../printers/v-core-4-hybrid/macros.cfg | 2 +- .../printers/v-core-4-idex/macros.cfg | 2 +- configuration/printers/v-core-4/macros.cfg | 2 +- configuration/printers/v-core-pro/macros.cfg | 2 +- configuration/z-probe/beacon.cfg | 20 ++++---- 18 files changed, 107 insertions(+), 107 deletions(-) diff --git a/configuration/homing.cfg b/configuration/homing.cfg index dd545be8c..937e463c0 100644 --- a/configuration/homing.cfg +++ b/configuration/homing.cfg @@ -172,7 +172,7 @@ gcode: DEPLOY_PROBE {% endif %} _MOVE_TO_SAFE_Z_HOME - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_homing %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_homing %} {% if beacon_contact_start_print_true_zero %} # The critical zero probing happens during start print after heat soaking, not here. BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 @@ -233,22 +233,22 @@ gcode: {% for axis in printer["gcode_macro RatOS"].x_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].x_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.configfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} {% endfor %} {% else %} {% set x_driver = printer["gcode_macro RatOS"].driver_type_x|lower ~ " stepper_x" %} - SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.configfile.config[x_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.initconfigfile.config[x_driver].run_current} {% endif %} {% if printer["gcode_macro RatOS"].y_axes is defined %} {% for axis in printer["gcode_macro RatOS"].y_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].y_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.configfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} {% endfor %} {% else %} {% set y_driver = printer["gcode_macro RatOS"].driver_type_y|lower ~ " stepper_y" %} - SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.configfile.config[y_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.initconfigfile.config[y_driver].run_current} {% endif %} # Wait for currents to settle @@ -299,22 +299,22 @@ gcode: {% for axis in printer["gcode_macro RatOS"].x_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].x_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.configfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} {% endfor %} {% else %} {% set x_driver = printer["gcode_macro RatOS"].driver_type_x|lower ~ " stepper_x" %} - SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.configfile.config[x_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.initconfigfile.config[x_driver].run_current} {% endif %} {% if printer["gcode_macro RatOS"].y_axes is defined %} {% for axis in printer["gcode_macro RatOS"].y_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].y_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.configfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} {% endfor %} {% else %} {% set y_driver = printer["gcode_macro RatOS"].driver_type_y|lower ~ " stepper_y" %} - SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.configfile.config[y_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.initconfigfile.config[y_driver].run_current} {% endif %} # Wait for currents to settle @@ -326,8 +326,8 @@ gcode: [gcode_macro _Z_HOP] description: Move Z axis up by Z_HOP distance at Z_HOP_SPEED. In relative mode it will move Z axis up by Z_HOP distance. In absolute mode it will move Z axis to Z_HOP distance. gcode: - {% set z_hop = printer.configfile.config.ratos_homing.z_hop|float %} - {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop = printer.initconfigfile.config.ratos_homing.z_hop|float %} + {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} G0 Z{z_hop} F{z_hop_speed} diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 73372b7bd..bd48f91a5 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -361,7 +361,7 @@ gcode: {% set total_layer_count = params.TOTAL_LAYER_COUNT|default(0)|int %} {% set extruder_first_layer_temp = (params.EXTRUDER_TEMP|default("")).split(",") %} - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL BEACON_CONTACT_START_PRINT_TRUE_ZERO={beacon_contact_start_print_true_zero} BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT={beacon_contact_calibrate_model_on_print} {% endif %} @@ -446,10 +446,10 @@ gcode: {% if printer["dual_carriage"] is defined %} {% set parking_position_t0 = printer["gcode_macro T0"].parking_position|float %} {% set parking_position_t1 = printer["gcode_macro T1"].parking_position|float %} - {% set stepper_x_position_min = printer.configfile.settings.stepper_x.position_min|float %} - {% set stepper_x_position_endstop = printer.configfile.settings.stepper_x.position_endstop|float %} - {% set dual_carriage_position_max = printer.configfile.settings.dual_carriage.position_max|float %} - {% set dual_carriage_position_endstop = printer.configfile.settings.dual_carriage.position_endstop|float %} + {% set stepper_x_position_min = printer.initconfigfile.settings.stepper_x.position_min|float %} + {% set stepper_x_position_endstop = printer.initconfigfile.settings.stepper_x.position_endstop|float %} + {% set dual_carriage_position_max = printer.initconfigfile.settings.dual_carriage.position_max|float %} + {% set dual_carriage_position_endstop = printer.initconfigfile.settings.dual_carriage.position_endstop|float %} {% set x_parking_space = parking_position_t0 - (stepper_x_position_endstop , stepper_x_position_min)|max %} {% set dc_parking_space = (dual_carriage_position_endstop , dual_carriage_position_max)|min - parking_position_t1 %} {% if svv.idex_xoffset|abs >= (x_parking_space - 0.5) or svv.idex_xoffset|abs >= (dc_parking_space - 0.5) %} @@ -459,7 +459,7 @@ gcode: {% endif %} # IDEX copy and mirror mode sanity check - {% if (idex_mode == "copy" or idex_mode == "mirror") and printer.configfile.settings.ratos.enable_gcode_transform %} + {% if (idex_mode == "copy" or idex_mode == "mirror") and printer.initconfigfile.settings.ratos.enable_gcode_transform %} {% if params.MIN_X is not defined or params.MAX_X is not defined %} _LED_START_PRINTING_ERROR @@ -488,7 +488,7 @@ gcode: {% endif %} {% set center_x = printable_x_max / 2.0 %} - {% set safe_distance = printer.configfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} {% set object_width = boundary_box_max_x - boundary_box_min_x %} {% set copy_mode_max_width = center_x %} {% set mirror_mode_max_width = center_x - safe_distance / 2.0 %} @@ -604,7 +604,7 @@ gcode: _Z_HOP # move toolhead to the oozeguard if needed - {% if idex_mode != '' and not (printer.configfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adaptive_heat_soak)) %} + {% if idex_mode != '' and not (printer.initconfigfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adaptive_heat_soak)) %} PARK_TOOLHEAD {% endif %} @@ -627,7 +627,7 @@ gcode: M190 S{bed_temp} # Wait for bed thermal expansion - {% if printer.configfile.settings.beacon is defined and beacon_adaptive_heat_soak %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_adaptive_heat_soak %} _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2.5 F{z_speed} @@ -664,10 +664,10 @@ gcode: {% if idex_mode == '' %} SET_HEATER_TEMPERATURE HEATER="extruder" TARGET={extruder_first_layer_temp[initial_tool]|float} {% else %} - {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} SET_HEATER_TEMPERATURE HEATER="extruder" TARGET={extruder_first_layer_temp[0]|float} {% endif %} - {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} SET_HEATER_TEMPERATURE HEATER="extruder1" TARGET={extruder_first_layer_temp[1]|float} {% endif %} {% endif %} @@ -683,10 +683,10 @@ gcode: {% if idex_mode == '' %} TEMPERATURE_WAIT SENSOR="extruder" MINIMUM={extruder_first_layer_temp[initial_tool]|float} MAXIMUM={extruder_first_layer_temp[initial_tool]|float + 5} {% else %} - {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} TEMPERATURE_WAIT SENSOR="extruder" MINIMUM={extruder_first_layer_temp[0]|float} MAXIMUM={extruder_first_layer_temp[0]|float + 5} {% endif %} - {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} TEMPERATURE_WAIT SENSOR="extruder1" MINIMUM={extruder_first_layer_temp[1]|float} MAXIMUM={extruder_first_layer_temp[1]|float + 5} {% endif %} {% endif %} @@ -785,7 +785,7 @@ gcode: {% endif %} # set nozzle thermal expansion offset and restore runtime offset - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} # the previously called restore gcode state removed the temp offset # we need first to reset the applied offset value in the variables file _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True @@ -941,7 +941,7 @@ gcode: {% endif %} # raise z after heat soaking the beacon - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} _Z_HOP {% endif %} @@ -979,7 +979,7 @@ gcode: {% endif %} # beacon contact homing with optional wipe - {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} {% if beacon_contact_wipe_before_true_zero %} _START_PRINT_AFTER_HEATING_BED_PROBE_FOR_WIPE {% endif %} @@ -1271,7 +1271,7 @@ gcode: {% endif %} # reset nozzle thermal expansion offset - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% if printer["dual_carriage"] is not defined %} # beacon config {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} diff --git a/configuration/macros/idex/idex_is.cfg b/configuration/macros/idex/idex_is.cfg index 67fa6421b..ccb4a8f68 100644 --- a/configuration/macros/idex/idex_is.cfg +++ b/configuration/macros/idex/idex_is.cfg @@ -43,7 +43,7 @@ gcode: {% set act_t = 1 if idex_mode == 'primary' else 0 %} {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed|float * 60 %} {% set adxl_chip = printer["gcode_macro RatOS"].adxl_chip %} - {% set probe_points = printer.configfile.settings.resonance_tester.probe_points[0] %} + {% set probe_points = printer.initconfigfile.settings.resonance_tester.probe_points[0] %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} {% set copy_mode_offset = printable_x_max / 4.0 %} @@ -303,7 +303,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed|float * 60 %} {% set parking_position_t1 = printer["gcode_macro T1"].parking_position|float %} {% set adxl_chip = printer["gcode_macro RatOS"].adxl_chip %} - {% set probe_points = printer.configfile.settings.resonance_tester.probe_points[0] %} + {% set probe_points = printer.initconfigfile.settings.resonance_tester.probe_points[0] %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set default_toolhead_parking_position = printer["gcode_macro T%s" % default_toolhead].parking_position|float %} diff --git a/configuration/macros/idex/toolheads.cfg b/configuration/macros/idex/toolheads.cfg index e87a18e4b..05e408a97 100644 --- a/configuration/macros/idex/toolheads.cfg +++ b/configuration/macros/idex/toolheads.cfg @@ -493,7 +493,7 @@ gcode: SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE=0 RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T0: X={xoffset} Y={yoffset} Z={zoffset}" # set nozzle thermal expansion offset - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={new_t} {% endif %} {% endif %} @@ -560,7 +560,7 @@ gcode: SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE=1 RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T1: X={xoffset} Y={yoffset} Z={zoffset}" # set nozzle thermal expansion offset - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={new_t} {% endif %} {% endif %} @@ -776,8 +776,8 @@ gcode: _IDEX_SINGLE INIT=1 # dual carriage safe distance sanity check - {% if printer.configfile.settings.dual_carriage.safe_distance is defined %} - {% if printer.configfile.settings.dual_carriage.safe_distance|float < 50 %} + {% if printer.initconfigfile.settings.dual_carriage.safe_distance is defined %} + {% if printer.initconfigfile.settings.dual_carriage.safe_distance|float < 50 %} { action_emergency_stop("Dual carriage safe_distance seems to be too low!") } {% endif %} {% else %} @@ -822,7 +822,7 @@ gcode: RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T{t}" SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE={t} # set nozzle thermal expansion offset - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} diff --git a/configuration/macros/idex/util.cfg b/configuration/macros/idex/util.cfg index 0b8a6bfaa..1f71c4646 100644 --- a/configuration/macros/idex/util.cfg +++ b/configuration/macros/idex/util.cfg @@ -11,10 +11,10 @@ gcode: {% set idex_xcontrolpoint = svv.idex_xcontrolpoint|float %} {% set idex_ycontrolpoint = svv.idex_ycontrolpoint|float %} - {% set stepper_x_position_max = printer.configfile.settings.stepper_x.position_max|float %} - {% set stepper_x_position_endstop = printer.configfile.settings.stepper_x.position_endstop|float %} - {% set dual_carriage_position_max = printer.configfile.settings.dual_carriage.position_max|float %} - {% set dual_carriage_position_endstop = printer.configfile.settings.dual_carriage.position_endstop|float %} + {% set stepper_x_position_max = printer.initconfigfile.settings.stepper_x.position_max|float %} + {% set stepper_x_position_endstop = printer.initconfigfile.settings.stepper_x.position_endstop|float %} + {% set dual_carriage_position_max = printer.initconfigfile.settings.dual_carriage.position_max|float %} + {% set dual_carriage_position_endstop = printer.initconfigfile.settings.dual_carriage.position_endstop|float %} {% set line_1 = "_N_[dual_carriage]" %} {% set line_2 = "position_max: %.3f" % (dual_carriage_position_max + idex_xoffset) %} @@ -37,8 +37,8 @@ gcode: gcode: {% if printer["dual_carriage"] is defined %} {% set bed_margin_y = printer["gcode_macro RatOS"].bed_margin_y %} - {% set stepper_y_position_max = printer.configfile.settings.stepper_y.position_max|float %} - {% set stepper_y_position_endstop = printer.configfile.settings.stepper_y.position_endstop|float %} + {% set stepper_y_position_max = printer.initconfigfile.settings.stepper_y.position_max|float %} + {% set stepper_y_position_endstop = printer.initconfigfile.settings.stepper_y.position_endstop|float %} {% set line_1 = "_N_[stepper_y]" %} {% set line_2 = "position_max: %.3f" % (stepper_y_position_max + 1) %} diff --git a/configuration/macros/idex/vaoc.cfg b/configuration/macros/idex/vaoc.cfg index 3ed25a0d3..7dfe34ad1 100644 --- a/configuration/macros/idex/vaoc.cfg +++ b/configuration/macros/idex/vaoc.cfg @@ -114,7 +114,7 @@ gcode: # config {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} - {% set safe_distance = printer.configfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} @@ -301,7 +301,7 @@ gcode: # config {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} - {% set safe_distance = printer.configfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} @@ -630,7 +630,7 @@ gcode: # beacon contact config {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% if is_fixed and is_started and printer["z_offset_probe"] is defined and printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if is_fixed and is_started and printer["z_offset_probe"] is defined and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} # config {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 98d547f7a..4550c618e 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -12,13 +12,13 @@ variable_adaptive_mesh: True # True|False = enable adaptive [gcode_macro _BED_MESH_SANITY_CHECK] gcode: - {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} + {% set zero_ref_pos = printer.initconfigfile.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is defined %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_start_print_true_zero_fuzzy_position = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero_fuzzy_position|default(false)|lower == 'true' else false %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} {% if beacon_contact_start_print_true_zero_fuzzy_position %} CONSOLE_ECHO TYPE="error" TITLE="Defined zero reference not compatible with fuzzy true zero" MSG="Using fuzzy start print true zero position is not compatible with a defined zero_reference_position value in the [bed_mesh] section in printer.cfg._N_Please remove the zero_reference_position value from the [bed_mesh] section, or disable fuzzy true zero position in printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_start_print_true_zero_fuzzy_position: False" _STOP_AND_RAISE_ERROR MSG="Defined zero reference not compatible with fuzzy true zero" @@ -50,7 +50,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_x VALUE=None SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_y VALUE=None # Error early if the scan compensation mesh is required but does not exist - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION {rawparams} {% endif %} @@ -72,7 +72,7 @@ gcode: # config {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} - {% set zero_ref_pos = printer.configfile.settings.bed_mesh.zero_reference_position %} + {% set zero_ref_pos = printer.initconfigfile.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is not defined %} {% set zero_ref_pos = [safe_home_x if actual_true_zero_position_x is none else actual_true_zero_position_x, safe_home_y if actual_true_zero_position_y is none else actual_true_zero_position_y] %} {% endif %} @@ -91,7 +91,7 @@ gcode: {% set beacon_contact_bed_mesh_samples = printer["gcode_macro RatOS"].beacon_contact_bed_mesh_samples|default(2)|int %} {% set beacon_contact_bed_mesh = true if printer["gcode_macro RatOS"].beacon_contact_bed_mesh|default(false)|lower == 'true' else false %} {% set beacon_scan_method_automatic = true if printer["gcode_macro RatOS"].beacon_scan_method_automatic|default(false)|lower == 'true' else false %} - {% if printer.configfile.settings.beacon is defined and not beacon_contact_bed_mesh %} + {% if printer.initconfigfile.settings.beacon is defined and not beacon_contact_bed_mesh %} SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY={beacon_bed_mesh_scv} {% endif %} @@ -102,7 +102,7 @@ gcode: {% if printer["gcode_macro RatOS"].adaptive_mesh|lower == 'true' %} CALIBRATE_ADAPTIVE_MESH PROFILE="{default_profile}" X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} ZERO_REF_POS_X={zero_ref_pos[0]} ZERO_REF_POS_Y={zero_ref_pos[1]} {% else %} - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -160,7 +160,7 @@ gcode: {% if x0 >= x1 or y0 >= y1 %} # coordinates are invalid, fall back to full bed mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Invalid coordinates received. Please check your slicer settings. Falling back to full bed mesh." - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -190,7 +190,7 @@ gcode: {% endif %} # get bed mesh config object - {% set mesh_config = printer.configfile.config.bed_mesh %} + {% set mesh_config = printer.initconfigfile.config.bed_mesh %} # get configured bed mesh area {% set min_x = mesh_config.mesh_min.split(",")[0]|float %} @@ -207,7 +207,7 @@ gcode: {% if mesh_x0 == min_x and mesh_y0 == min_y and mesh_x1 == max_x and mesh_y1 == max_y %} # coordinates are invalid, fall back to full bed mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Print is using the full bed, falling back to full bed mesh." - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -272,9 +272,9 @@ gcode: # IDEX {% set probe_first = printer["gcode_macro RatOS"].nozzle_prime_start_y|lower == "min" or printer["gcode_macro RatOS"].nozzle_prime_start_y|float(printable_y_max) < printable_y_max / 2 %} {% endif %} - {% if printer.configfile.settings.beacon is defined and printer.configfile.settings.beacon.mesh_runs % 2 != 0 and probe_first %} + {% if printer.initconfigfile.settings.beacon is defined and printer.initconfigfile.settings.beacon.mesh_runs % 2 != 0 and probe_first %} {% set probe_first = false %} - {% elif printer.configfile.settings.beacon is defined and printer.configfile.settings.beacon.mesh_runs % 2 == 0 and not probe_first %} + {% elif printer.initconfigfile.settings.beacon is defined and printer.initconfigfile.settings.beacon.mesh_runs % 2 == 0 and not probe_first %} {% set probe_first = true %} {% endif %} @@ -296,7 +296,7 @@ gcode: # mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0|round(1)} Y0={mesh_y0|round(1)} X1={mesh_x1|round(1)} Y1={mesh_y1}|round(1)}" - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 {% else %} diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index 0ff65c95f..02ffcd5f5 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -96,13 +96,13 @@ gcode: {% if idex_mode == "copy" or idex_mode == "mirror" %} M104.1 S{s0} T0 M104.1 S{s1} T1 - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=0 _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=1 {% endif %} {% else %} M104.1 S{s} T{t} - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} @@ -148,7 +148,7 @@ gcode: M109.1 S{s} T{t} # Update the nozzle thermal expansion offset {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} - {% if printer.configfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} @@ -182,7 +182,7 @@ gcode: # call klipper base function SET_HEATER_TEMPERATURE_BASE HEATER="{heater}" TARGET={target} - {% if t != -1 and printer.configfile.settings.beacon is defined %} + {% if t != -1 and printer.initconfigfile.settings.beacon is defined %} # Update the nozzle thermal expansion offset {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} {% if is_printing_gcode %} @@ -261,7 +261,7 @@ rename_existing: SKEW_PROFILE_BASE variable_loaded_profile: "" # internal use only. Do not touch! gcode: {% if params.LOAD is defined %} - {% if printer.configfile.settings["skew_correction %s" % params.LOAD] is defined %} + {% if printer.initconfigfile.settings["skew_correction %s" % params.LOAD] is defined %} SET_GCODE_VARIABLE MACRO=SKEW_PROFILE VARIABLE=loaded_profile VALUE='"{params.LOAD}"' {% endif %} {% endif %} diff --git a/configuration/macros/parking.cfg b/configuration/macros/parking.cfg index 0152aac27..6f0c0f3a4 100644 --- a/configuration/macros/parking.cfg +++ b/configuration/macros/parking.cfg @@ -26,7 +26,7 @@ gcode: {% endif %} # park IDEX toolhead if needed - {% if printer["dual_carriage"] is defined and not (printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if printer["dual_carriage"] is defined and not (printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} {% if printer["gcode_macro RatOS"].start_print_park_x is defined and printer["gcode_macro RatOS"].start_print_park_x != '' %} RATOS_ECHO PREFIX="WARNING" MSG="start_print_park_x is ignored for IDEX printers" {% endif %} diff --git a/configuration/macros/priming.cfg b/configuration/macros/priming.cfg index 60f3d910b..4e7fee639 100644 --- a/configuration/macros/priming.cfg +++ b/configuration/macros/priming.cfg @@ -29,18 +29,18 @@ gcode: {% set beacon_contact_prime_probing = true if printer["gcode_macro RatOS"].beacon_contact_prime_probing|default(false)|lower == 'true' else false %} {% set last_z_offset = 9999.9 %} - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} {% set current_z = printer.toolhead.position.z|float %} {% if beacon_contact_prime_probing %} {% set last_z_offset = printer.beacon.last_z_result %} {% else %} {% set last_z_offset = printer.beacon.last_sample.dist - current_z %} {% endif %} - {% elif printer.configfile.settings.bltouch is defined %} - {% set config_offset = printer.configfile.settings.bltouch.z_offset|float %} + {% elif printer.initconfigfile.settings.bltouch is defined %} + {% set config_offset = printer.initconfigfile.settings.bltouch.z_offset|float %} {% set last_z_offset = printer.probe.last_z_result - config_offset %} - {% elif printer.configfile.settings.probe is defined %} - {% set config_offset = printer.configfile.settings.probe.z_offset|float %} + {% elif printer.initconfigfile.settings.probe is defined %} + {% set config_offset = printer.initconfigfile.settings.probe.z_offset|float %} {% set last_z_offset = printer.probe.last_z_result - config_offset %} {% endif %} RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Saving offset adjustment of {last_z_offset} in {params.VARIABLE|default('last_z_offset')}" @@ -102,24 +102,24 @@ gcode: {% set y_start = printable_y_max - 5 %} {% endif %} {% endif %} - {% set z = printer.configfile.settings.bed_mesh.horizontal_move_z|float %} + {% set z = printer.initconfigfile.settings.bed_mesh.horizontal_move_z|float %} # get bed mesh config object - {% set mesh_config = printer.configfile.config.bed_mesh %} + {% set mesh_config = printer.initconfigfile.config.bed_mesh %} # Get probe offsets - {% if printer.configfile.settings.bltouch is defined %} - {% set x_offset = printer.configfile.settings.bltouch.x_offset|float %} - {% set y_offset = printer.configfile.settings.bltouch.y_offset|float %} - {% set z_offset = printer.configfile.settings.bltouch.z_offset|float %} - {% elif printer.configfile.settings.probe is defined %} - {% set x_offset = printer.configfile.settings.probe.x_offset|float %} - {% set y_offset = printer.configfile.settings.probe.y_offset|float %} - {% set z_offset = printer.configfile.settings.probe.z_offset|float %} - {% elif printer.configfile.settings.beacon is defined %} - {% set x_offset = printer.configfile.settings.beacon.x_offset|float %} - {% set y_offset = printer.configfile.settings.beacon.y_offset|float %} - {% set z_offset = printer.configfile.settings.beacon.trigger_distance|float %} + {% if printer.initconfigfile.settings.bltouch is defined %} + {% set x_offset = printer.initconfigfile.settings.bltouch.x_offset|float %} + {% set y_offset = printer.initconfigfile.settings.bltouch.y_offset|float %} + {% set z_offset = printer.initconfigfile.settings.bltouch.z_offset|float %} + {% elif printer.initconfigfile.settings.probe is defined %} + {% set x_offset = printer.initconfigfile.settings.probe.x_offset|float %} + {% set y_offset = printer.initconfigfile.settings.probe.y_offset|float %} + {% set z_offset = printer.initconfigfile.settings.probe.z_offset|float %} + {% elif printer.initconfigfile.settings.beacon is defined %} + {% set x_offset = printer.initconfigfile.settings.beacon.x_offset|float %} + {% set y_offset = printer.initconfigfile.settings.beacon.y_offset|float %} + {% set z_offset = printer.initconfigfile.settings.beacon.trigger_distance|float %} {% else %} { action_raise_error("No probe, beacon or bltouch section found. Adaptive priming only works with a [probe], [beacon] or [bltouch] section defined.") } {% endif %} @@ -204,12 +204,12 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe|lower == 'stowable' %} ASSERT_PROBE_DEPLOYED {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_prime_probing %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_prime_probing %} PROBE PROBE_METHOD=contact SAMPLES=1 {% else %} PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} BEACON_QUERY {% else %} # Only restore state if we're not using a beacon, so we state at scanning height @@ -259,7 +259,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} {% set fan_speed = printer["gcode_macro RatOS"].nozzle_prime_bridge_fan|float %} - {% set nozzle_diameter = printer.configfile.settings[extruder].nozzle_diameter|float %} + {% set nozzle_diameter = printer.initconfigfile.settings[extruder].nozzle_diameter|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set has_start_offset_t0 = printer["gcode_macro RatOS"].probe_for_priming_result|float(9999.9) != 9999.9 %} {% if printer["dual_carriage"] is defined %} @@ -351,7 +351,7 @@ gcode: {% if has_start_offset_t0 %} {% set start_z_probe_result_t0 = printer["gcode_macro RatOS"].probe_for_priming_result|float(9999.9) %} {% set end_z_probe_result_t0 = printer["gcode_macro RatOS"].probe_for_priming_end_result|float(9999.9) %} - {% if printer.configfile.settings.bltouch is not defined and printer.configfile.settings.probe is not defined and printer.configfile.settings.beacon is not defined %} + {% if printer.initconfigfile.settings.bltouch is not defined and printer.initconfigfile.settings.probe is not defined and printer.initconfigfile.settings.beacon is not defined %} { action_raise_error("No probe or bltouch section found. Adaptive priming only works with [probe], [beacon] or [bltouch].") } {% endif %} {% if start_z_probe_result_t0 == 9999.9 %} @@ -376,7 +376,7 @@ gcode: {% if has_start_offset_t1 %} {% set start_z_probe_result_t1 = printer["gcode_macro RatOS"].probe_for_priming_result_t1|float(9999.9) %} {% set end_z_probe_result_t1 = printer["gcode_macro RatOS"].probe_for_priming_end_result_t1|float(9999.9) %} - {% if printer.configfile.settings.bltouch is not defined and printer.configfile.settings.probe is not defined and printer.configfile.settings.beacon is not defined %} + {% if printer.initconfigfile.settings.bltouch is not defined and printer.initconfigfile.settings.probe is not defined and printer.initconfigfile.settings.beacon is not defined %} { action_raise_error("No probe or bltouch section found. Adaptive priming only works with [probe], [beacon] or [bltouch].") } {% endif %} {% if start_z_probe_result_t1 == 9999.9 %} @@ -407,7 +407,7 @@ gcode: {% endif %} # set nozzle thermal expansion offset - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={current_toolhead} {% endif %} diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index dd2ed92b7..966e42247 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -53,7 +53,7 @@ gcode: CALCULATE_PRINTABLE_AREA INITIAL_FRONTEND_UPDATE _CHAMBER_FILTER_SANITY_CHECK - {% if printer.configfile.settings.beacon is defined %} + {% if printer.initconfigfile.settings.beacon is defined %} # Enforce valid zero_reference position configuration, warn about problematic settings. _BED_MESH_SANITY_CHECK {% endif %} @@ -250,8 +250,8 @@ gcode: gcode: # inverted hybrid core-xy bugfix sanity check {% set inverted = False %} - {% if printer.configfile.settings.ratos_hybrid_corexy is defined and printer.configfile.settings.ratos_hybrid_corexy.inverted is defined %} - {% if printer.configfile.settings.ratos_hybrid_corexy.inverted|lower == 'true' %} + {% if printer.initconfigfile.settings.ratos_hybrid_corexy is defined and printer.initconfigfile.settings.ratos_hybrid_corexy.inverted is defined %} + {% if printer.initconfigfile.settings.ratos_hybrid_corexy.inverted|lower == 'true' %} {% set inverted = True %} {% endif %} {% endif %} @@ -378,7 +378,7 @@ gcode: gcode: {% set ratos_skew_profile = printer["gcode_macro RatOS"].skew_profile|default("") %} {% if ratos_skew_profile != "" %} - {% if printer.configfile.config["skew_correction %s" % ratos_skew_profile] is defined %} + {% if printer.initconfigfile.config["skew_correction %s" % ratos_skew_profile] is defined %} SKEW_PROFILE LOAD={ratos_skew_profile} GET_CURRENT_SKEW {% else %} diff --git a/configuration/printers/v-core-3-idex/macros.cfg b/configuration/printers/v-core-3-idex/macros.cfg index 690fb9528..609f6d023 100644 --- a/configuration/printers/v-core-3-idex/macros.cfg +++ b/configuration/printers/v-core-3-idex/macros.cfg @@ -45,7 +45,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-3/macros.cfg b/configuration/printers/v-core-3/macros.cfg index 0e76e28fa..9f1774fb1 100644 --- a/configuration/printers/v-core-3/macros.cfg +++ b/configuration/printers/v-core-3/macros.cfg @@ -17,7 +17,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4-hybrid/macros.cfg b/configuration/printers/v-core-4-hybrid/macros.cfg index 2a9e6da75..bf4a7ff10 100644 --- a/configuration/printers/v-core-4-hybrid/macros.cfg +++ b/configuration/printers/v-core-4-hybrid/macros.cfg @@ -19,7 +19,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4-idex/macros.cfg b/configuration/printers/v-core-4-idex/macros.cfg index 85c17d4f7..c121a70a2 100644 --- a/configuration/printers/v-core-4-idex/macros.cfg +++ b/configuration/printers/v-core-4-idex/macros.cfg @@ -39,7 +39,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4/macros.cfg b/configuration/printers/v-core-4/macros.cfg index b2000878c..e663dfdf8 100644 --- a/configuration/printers/v-core-4/macros.cfg +++ b/configuration/printers/v-core-4/macros.cfg @@ -13,7 +13,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-pro/macros.cfg b/configuration/printers/v-core-pro/macros.cfg index b2000878c..e663dfdf8 100644 --- a/configuration/printers/v-core-pro/macros.cfg +++ b/configuration/printers/v-core-pro/macros.cfg @@ -13,7 +13,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.configfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 72e2764fc..fdfabc196 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -263,7 +263,7 @@ gcode: # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} - {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} # beacon adaptive heat soak config {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} @@ -359,7 +359,7 @@ gcode: {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} # config - {% set z_hop_speed = printer.configfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} # reset results SET_GCODE_VARIABLE MACRO=BEACON_POKE_TEST VARIABLE=poke_result_1 VALUE=-1 @@ -899,7 +899,7 @@ gcode: {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object - {% set mesh_config = printer.configfile.config.bed_mesh %} + {% set mesh_config = printer.initconfigfile.config.bed_mesh %} # get configured bed mesh area {% set min_x = mesh_config.mesh_min.split(",")[0]|float %} @@ -1050,13 +1050,13 @@ gcode: {% set beacon_contact_calibrate_model_on_print = params.BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT|lower == 'true' %} # Error early with a clear message if a Beacon model is required but not active. - {% if printer.configfile.settings.beacon is defined and not printer.beacon.model %} + {% if printer.initconfigfile.settings.beacon is defined and not printer.beacon.model %} {% set beacon_model_required_for_true_zero = beacon_contact_start_print_true_zero and not beacon_contact_calibrate_model_on_print %} {% set beacon_model_required_for_homing = - printer.configfile.settings.stepper_z.endstop_pin == 'probe:z_virtual_endstop' - and (( printer.configfile.settings.beacon.home_method|default('proximity')|lower == 'proximity' + printer.initconfigfile.settings.stepper_z.endstop_pin == 'probe:z_virtual_endstop' + and (( printer.initconfigfile.settings.beacon.home_method|default('proximity')|lower == 'proximity' and printer["gcode_macro RatOS"].beacon_contact_z_homing|default(false)|lower != 'true') - or ( printer.configfile.settings.beacon.default_probe_method|default('proximity')|lower == 'proximity' + or ( printer.initconfigfile.settings.beacon.default_probe_method|default('proximity')|lower == 'proximity' and printer["gcode_macro RatOS"].beacon_contact_z_tilt_adjust|default(false)|lower != 'true' )) %} {% if beacon_model_required_for_homing or beacon_model_required_for_true_zero %} _LED_START_PRINTING_ERROR @@ -1181,7 +1181,7 @@ rename_existing: _Z_OFFSET_APPLY_PROBE_BASE gcode: {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} # If we're using contact true zero, use RatOS offset management; otherwise, delegate to base - {% if printer.configfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} _BEACON_SET_RUNTIME_OFFSET {rawparams} {% else %} _Z_OFFSET_APPLY_PROBE_BASE {rawparams} @@ -1211,8 +1211,8 @@ gcode: [gcode_macro BED_MESH_CALIBRATE] rename_existing: _BED_MESH_CALIBRATE_BASE gcode: - {% if printer.configfile.settings.beacon is defined %} - {% set beacon_default_probe_method = printer.configfile.settings.beacon.default_probe_method|default('proximity') %} + {% if printer.initconfigfile.settings.beacon is defined %} + {% set beacon_default_probe_method = printer.initconfigfile.settings.beacon.default_probe_method|default('proximity') %} {% set probe_method = params.PROBE_METHOD|default(beacon_default_probe_method)|lower %} {% if probe_method == 'proximity' %} _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" From 7bd164183f70ea3f0686fad73965ad171c564ffa Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 29 Jun 2025 20:53:30 +0100 Subject: [PATCH 090/139] feat(extras): add dynamic_governor --- configuration/klippy/dynamic_governor.py | 225 +++++++++++++++++++++++ configuration/macros.cfg | 5 + configuration/scripts/ratos-common.sh | 1 + configuration/scripts/ratos-install.sh | 2 +- src/scripts/common.sh | 22 +++ 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 configuration/klippy/dynamic_governor.py diff --git a/configuration/klippy/dynamic_governor.py b/configuration/klippy/dynamic_governor.py new file mode 100644 index 000000000..8412a4a53 --- /dev/null +++ b/configuration/klippy/dynamic_governor.py @@ -0,0 +1,225 @@ +# Automatically switches the CPU frequency governor to “performance” +# when any stepper is enabled, and back to “ondemand” when all steppers +# are disabled or Klipper is shutting down. +# +# Note: +# +# Requires the cpufrequtils package to be installed and the cpufreq-set +# command to be sudo-whitelisted for the user running Klipper. +# +# The current implentation assumes that the hardware does not support +# per-cpu frequency governors, so it sets the governor for all CPUs. +# +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import subprocess +import logging + +class DynamicGovernor: + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name() + + # config + if not config.getboolean('enabled', True): + logging.info(f"{self.name}: disabled by config") + return + + self.governor_motors_on = config.get('governor_motors_on', 'performance') + self.governor_motors_off = config.get('governor_motors_off', 'ondemand') + + # Ensure cpufreq-set is available and get the list of valid governors + self._check_cpufrequtils() + + if self.governor_motors_on not in self._valid_governors: + raise self.printer.config_error( + f"{self.name}: governor_motors_on '{self.governor_motors_on}' " + "not in available governors: " + + ', '.join(self._valid_governors) + ) + + if self.governor_motors_off not in self._valid_governors: + raise self.printer.config_error( + f"{self.name}: governor_motors_off '{self.governor_motors_off}' " + "not in available governors: " + + ', '.join(self._valid_governors) + ) + + logging.info(f"{self.name}: governor_motors_on={self.governor_motors_on}, " + f"governor_motors_off={self.governor_motors_off}") + + # Track how many steppers are currently enabled + self._enabled_count = 0 + + # Register stepper enable state callbacks once Klipper is ready + self.printer.register_event_handler('klippy:ready', self._on_ready) + + # Ensure we reset to ondemand on shutdown + self.printer.register_event_handler('klippy:shutdown', self._on_shutdown) + + def _check_cpufrequtils(self): + # Ensure cpufreq-set is available + try: + self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '--help']) + logging.info(f"{self.name}: cpufreq-set is available") + except FileNotFoundError: + raise self.printer.config_error( + f"{self.name}: cpufreq-set command not found. " + "Please install the cpufrequtils package and ensure it is in the PATH." + ) + except subprocess.CalledProcessError as e: + raise self.printer.config_error( + f"{self.name}: cpufreq-set command failed: {e}. " + "Please ensure cpufrequtils is installed correctly." + ) + except Exception as e: + raise self.printer.config_error( + f"{self.name}: Unexpected error checking cpufreq-set: {e}. " + "Please ensure cpufrequtils is installed correctly." + ) + + # Obtain the list of valid governors + try: + output = self._run_subprocess_with_output(['cpufreq-info', '-g'], text=True) + # Parse the output to get the list of governors + # cpufreq-info -g returns a space-separated list of governors + # If it returns a single item, it might be comma-separated + # so we handle both cases. + output = output.strip() + self._valid_governors = output.split() + if len(self._valid_governors) == 1: + # If there's only one item, it might be comma-separated + self._valid_governors = output.split(',') + + # Clean up any whitespace in the results + self._valid_governors = [g.strip() for g in self._valid_governors if g.strip()] + logging.info(f"{self.name}: available CPU governors: {', '.join(self._valid_governors)}") + except FileNotFoundError: + raise self.printer.config_error( + f"{self.name}: cpufreq-info command not found. " + "Please install the cpufrequtils package and ensure it is in the PATH." + ) + except subprocess.CalledProcessError as e: + raise self.printer.config_error( + f"{self.name}: cpufreq-info command failed: {e}. " + "Please ensure cpufrequtils is installed correctly." + ) + except Exception as e: + raise self.printer.config_error( + f"{self.name}: Unexpected error checking cpufreq-info: {e}. " + "Please ensure cpufrequtils is installed correctly." + ) + + def _on_ready(self): + # Lookup the stepper_enable object and register callbacks + stepper_enable = self.printer.lookup_object('stepper_enable') + for stepper_id in stepper_enable.get_steppers(): + se = stepper_enable.lookup_enable(stepper_id) + # If the stepper is already enabled, increment the count + if se.is_enabled: + self._enabled_count += 1 + # Register the state callback for this stepper + se.register_state_callback(self._on_stepper_state) + + # Apply the initial governor based on current state + self._exec_cpufreq(self.governor_motors_on if self._enabled_count > 0 else self.governor_motors_off) + + def _exec_cpufreq(self, governor: str): + """Run cpufreq-set -r -g quietly.""" + try: + self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '-r', '-g', governor]) + logging.info(f"{self.name}: set CPU governor to '{governor}'") + except Exception as e: + logging.warning( + f"{self.name}: failed to set governor to '{governor}': {e}" + ) + + def _on_stepper_state(self, print_time, enabled: bool): + """ + Called whenever a stepper’s enable-pin state changes. + enabled=True → a stepper was turned on + enabled=False → a stepper was turned off + """ + if enabled: + # If transitioning from 0→1 enabled steppers, ramp to performance + if self._enabled_count == 0: + self._exec_cpufreq(self.governor_motors_on) + self._enabled_count += 1 + else: + # Guard against negative counts + if self._enabled_count > 0: + self._enabled_count -= 1 + # If no more steppers are enabled, switch back to ondemand + if self._enabled_count == 0: + self._exec_cpufreq(self.governor_motors_off) + + logging.debug( + f"{self.name}: stepper state changed, enabled_count={self._enabled_count}" + ) + + def _on_shutdown(self): + """Reset governor when Klipper is shutting down or restarting.""" + # Regardless of current state, go back to ondemand + self._exec_cpufreq(self.governor_motors_off) + + def _run_subprocess_with_timeout(self, cmd, timeout_secs=10): + """Run a subprocess command with a timeout. + Raises subprocess.TimeoutExpired if the command does not complete + within the specified timeout. + """ + reactor = self.printer.get_reactor() + process = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + eventtime = reactor.monotonic() + # Poll for completion but don't block indefinitely + for _ in range(int(timeout_secs * 10)): # Try for specified seconds + if process.poll() is not None: + break + eventtime = reactor.pause(eventtime + 0.1) + + if process.returncode is None: + # Still running after timeout, kill it + process.terminate() + raise subprocess.TimeoutExpired(cmd, timeout_secs) + + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, cmd) + + def _run_subprocess_with_output(self, cmd, timeout_secs=10, text=False): + """Run a subprocess command and return its output. + Similar to subprocess.check_output but with a non-blocking timeout. + Returns command output as bytes (or string if text=True). + Raises subprocess.TimeoutExpired if the command does not complete + within the specified timeout. + """ + reactor = self.printer.get_reactor() + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=text + ) + eventtime = reactor.monotonic() + # Poll for completion but don't block indefinitely + for _ in range(int(timeout_secs * 10)): # Try for specified seconds + if process.poll() is not None: + break + eventtime = reactor.pause(eventtime + 0.1) + + if process.returncode is None: + # Still running after timeout, kill it + process.terminate() + raise subprocess.TimeoutExpired(cmd, timeout_secs) + + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, cmd, process.stdout.read()) + + return process.stdout.read() + +def load_config(config): + return DynamicGovernor(config) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index bd48f91a5..81a1e0dc9 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -7,6 +7,11 @@ ##### [initconfigfile] +##### +# dynamic_governor +##### +[dynamic_governor] + ##### # INCLUDE MACRO FILES ##### diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index 846566a82..83a5fa2fd 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -195,6 +195,7 @@ verify_registered_extensions() ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") ["beacon_adaptive_heatsoak_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_adaptive_heat_soak.py") ["initconfigfile"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/initconfigfile.py") + ["dynamic_governor"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/dynamic_governor.py") ) declare -A kinematics_extensions=( diff --git a/configuration/scripts/ratos-install.sh b/configuration/scripts/ratos-install.sh index 41b5b5e6e..cb7e4f8d9 100755 --- a/configuration/scripts/ratos-install.sh +++ b/configuration/scripts/ratos-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # This script installs additional dependencies for RatOS. -PKGLIST="python3-numpy python3-matplotlib curl git libopenblas-base" +PKGLIST="python3-numpy python3-matplotlib curl git libopenblas-base cpufrequtils" SCRIPT_DIR=$( cd -- "$( dirname -- "$(realpath -- "${BASH_SOURCE[0]}")" )" &> /dev/null && pwd ) CFG_DIR=$(realpath "$SCRIPT_DIR/..") diff --git a/src/scripts/common.sh b/src/scripts/common.sh index 4b39a6631..0b23b1509 100755 --- a/src/scripts/common.sh +++ b/src/scripts/common.sh @@ -276,4 +276,26 @@ __EOF $sudo cp --preserve=mode /tmp/031-ratos-configurator-wifi /etc/sudoers.d/031-ratos-configurator-wifi echo "RatOS configurator commands has successfully been whitelisted!" + + $sudo chown root:root /tmp/031-ratos-configurator-scripts + $sudo chmod 440 /tmp/031-ratos-configurator-scripts + $sudo cp --preserve=mode /tmp/031-ratos-configurator-scripts /etc/sudoers.d/031-ratos-configurator-scripts + + echo "RatOS configurator scripts has successfully been whitelisted!" + + # Whitelist klippy extension commands + if [[ -e /etc/sudoers.d/031-ratos-klippy-extensions ]] + then + $sudo rm /etc/sudoers.d/031-ratos-klippy-extensions + fi + touch /tmp/031-ratos-klippy-extensions + cat << __EOF > /tmp/031-ratos-klippy-extensions +${RATOS_USERNAME} ALL=(ALL) NOPASSWD: /usr/bin/cpufreq-set +__EOF + + $sudo chown root:root /tmp/031-ratos-klippy-extensions + $sudo chmod 440 /tmp/031-ratos-klippy-extensions + $sudo cp --preserve=mode /tmp/031-ratos-klippy-extensions /etc/sudoers.d/031-ratos-klippy-extensions + + echo "RatOS klippy extension commands has successfully been whitelisted!" } From cd72303573ac4e1c7017679e573f2bc87f6b86ce Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 30 Jun 2025 17:08:35 +0100 Subject: [PATCH 091/139] refactor(extras): rename initconfigfile to fastconfig --- configuration/homing.cfg | 22 ++++---- .../{initconfigfile.py => fastconfig.py} | 8 +-- configuration/macros.cfg | 38 +++++++------- configuration/macros/idex/idex_is.cfg | 4 +- configuration/macros/idex/toolheads.cfg | 10 ++-- configuration/macros/idex/util.cfg | 12 ++--- configuration/macros/idex/vaoc.cfg | 6 +-- configuration/macros/mesh.cfg | 24 ++++----- configuration/macros/overrides.cfg | 10 ++-- configuration/macros/parking.cfg | 2 +- configuration/macros/priming.cfg | 50 +++++++++---------- configuration/macros/util.cfg | 8 +-- .../printers/v-core-3-idex/macros.cfg | 2 +- configuration/printers/v-core-3/macros.cfg | 2 +- .../printers/v-core-4-hybrid/macros.cfg | 2 +- .../printers/v-core-4-idex/macros.cfg | 2 +- configuration/printers/v-core-4/macros.cfg | 2 +- configuration/printers/v-core-pro/macros.cfg | 2 +- configuration/scripts/ratos-common.sh | 2 +- configuration/z-probe/beacon.cfg | 20 ++++---- 20 files changed, 114 insertions(+), 114 deletions(-) rename configuration/klippy/{initconfigfile.py => fastconfig.py} (86%) diff --git a/configuration/homing.cfg b/configuration/homing.cfg index 937e463c0..de32389a4 100644 --- a/configuration/homing.cfg +++ b/configuration/homing.cfg @@ -172,7 +172,7 @@ gcode: DEPLOY_PROBE {% endif %} _MOVE_TO_SAFE_Z_HOME - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_homing %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_homing %} {% if beacon_contact_start_print_true_zero %} # The critical zero probing happens during start print after heat soaking, not here. BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 @@ -233,22 +233,22 @@ gcode: {% for axis in printer["gcode_macro RatOS"].x_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].x_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.fastconfig.config[stepper_driver].run_current} {% endfor %} {% else %} {% set x_driver = printer["gcode_macro RatOS"].driver_type_x|lower ~ " stepper_x" %} - SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.initconfigfile.config[x_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.fastconfig.config[x_driver].run_current} {% endif %} {% if printer["gcode_macro RatOS"].y_axes is defined %} {% for axis in printer["gcode_macro RatOS"].y_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].y_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.fastconfig.config[stepper_driver].run_current} {% endfor %} {% else %} {% set y_driver = printer["gcode_macro RatOS"].driver_type_y|lower ~ " stepper_y" %} - SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.initconfigfile.config[y_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.fastconfig.config[y_driver].run_current} {% endif %} # Wait for currents to settle @@ -299,22 +299,22 @@ gcode: {% for axis in printer["gcode_macro RatOS"].x_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].x_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.fastconfig.config[stepper_driver].run_current} {% endfor %} {% else %} {% set x_driver = printer["gcode_macro RatOS"].driver_type_x|lower ~ " stepper_x" %} - SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.initconfigfile.config[x_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_x CURRENT={printer.fastconfig.config[x_driver].run_current} {% endif %} {% if printer["gcode_macro RatOS"].y_axes is defined %} {% for axis in printer["gcode_macro RatOS"].y_axes %} {% set stepper = "stepper_" ~ axis|lower %} {% set stepper_driver = printer["gcode_macro RatOS"].y_driver_types[loop.index0] ~ " " ~ stepper %} - SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.initconfigfile.config[stepper_driver].run_current} + SET_TMC_CURRENT STEPPER={stepper} CURRENT={printer.fastconfig.config[stepper_driver].run_current} {% endfor %} {% else %} {% set y_driver = printer["gcode_macro RatOS"].driver_type_y|lower ~ " stepper_y" %} - SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.initconfigfile.config[y_driver].run_current} + SET_TMC_CURRENT STEPPER=stepper_y CURRENT={printer.fastconfig.config[y_driver].run_current} {% endif %} # Wait for currents to settle @@ -326,8 +326,8 @@ gcode: [gcode_macro _Z_HOP] description: Move Z axis up by Z_HOP distance at Z_HOP_SPEED. In relative mode it will move Z axis up by Z_HOP distance. In absolute mode it will move Z axis to Z_HOP distance. gcode: - {% set z_hop = printer.initconfigfile.config.ratos_homing.z_hop|float %} - {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop = printer.fastconfig.config.ratos_homing.z_hop|float %} + {% set z_hop_speed = printer.fastconfig.config.ratos_homing.z_hop_speed|float * 60 %} G0 Z{z_hop} F{z_hop_speed} diff --git a/configuration/klippy/initconfigfile.py b/configuration/klippy/fastconfig.py similarity index 86% rename from configuration/klippy/initconfigfile.py rename to configuration/klippy/fastconfig.py index 6b13f7da0..3db21214b 100644 --- a/configuration/klippy/initconfigfile.py +++ b/configuration/klippy/fastconfig.py @@ -5,10 +5,10 @@ # causes a deep copy of the configuration status, which can be expensive, notably for # large configurations, potentially leading to `timer too close` errors. # -# Macros should instead access `printer.initconfigfile` to get the immutable status. Note that -# this status is a fixed snapshot from when Kipper initializes, so it will not reflect changes made -# to the configuration after Kipper initializes - for example, updated PID tuning results and -# bed mesh profile additions or removals. +# Macros should instead access `printer.fastconfig` to get the immutable status. The +# `settings`, `config` and `warnings` keys behave exactly like the keys of +# `printer.configfile`. The `save_config_pending` and `save_config_pending_items` keys +# are not exposed by `printer.fastconfig`. # # Copyright (C) 2025 Tom Glastonbury # diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 81a1e0dc9..7a935236d 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -3,9 +3,9 @@ # sections into your printer.cfg and change it there. ##### -# initconfigfile +# fastconfig ##### -[initconfigfile] +[fastconfig] ##### # dynamic_governor @@ -366,7 +366,7 @@ gcode: {% set total_layer_count = params.TOTAL_LAYER_COUNT|default(0)|int %} {% set extruder_first_layer_temp = (params.EXTRUDER_TEMP|default("")).split(",") %} - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} _START_PRINT_PREFLIGHT_CHECK_BEACON_MODEL BEACON_CONTACT_START_PRINT_TRUE_ZERO={beacon_contact_start_print_true_zero} BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT={beacon_contact_calibrate_model_on_print} {% endif %} @@ -451,10 +451,10 @@ gcode: {% if printer["dual_carriage"] is defined %} {% set parking_position_t0 = printer["gcode_macro T0"].parking_position|float %} {% set parking_position_t1 = printer["gcode_macro T1"].parking_position|float %} - {% set stepper_x_position_min = printer.initconfigfile.settings.stepper_x.position_min|float %} - {% set stepper_x_position_endstop = printer.initconfigfile.settings.stepper_x.position_endstop|float %} - {% set dual_carriage_position_max = printer.initconfigfile.settings.dual_carriage.position_max|float %} - {% set dual_carriage_position_endstop = printer.initconfigfile.settings.dual_carriage.position_endstop|float %} + {% set stepper_x_position_min = printer.fastconfig.settings.stepper_x.position_min|float %} + {% set stepper_x_position_endstop = printer.fastconfig.settings.stepper_x.position_endstop|float %} + {% set dual_carriage_position_max = printer.fastconfig.settings.dual_carriage.position_max|float %} + {% set dual_carriage_position_endstop = printer.fastconfig.settings.dual_carriage.position_endstop|float %} {% set x_parking_space = parking_position_t0 - (stepper_x_position_endstop , stepper_x_position_min)|max %} {% set dc_parking_space = (dual_carriage_position_endstop , dual_carriage_position_max)|min - parking_position_t1 %} {% if svv.idex_xoffset|abs >= (x_parking_space - 0.5) or svv.idex_xoffset|abs >= (dc_parking_space - 0.5) %} @@ -464,7 +464,7 @@ gcode: {% endif %} # IDEX copy and mirror mode sanity check - {% if (idex_mode == "copy" or idex_mode == "mirror") and printer.initconfigfile.settings.ratos.enable_gcode_transform %} + {% if (idex_mode == "copy" or idex_mode == "mirror") and printer.fastconfig.settings.ratos.enable_gcode_transform %} {% if params.MIN_X is not defined or params.MAX_X is not defined %} _LED_START_PRINTING_ERROR @@ -493,7 +493,7 @@ gcode: {% endif %} {% set center_x = printable_x_max / 2.0 %} - {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.fastconfig.settings.dual_carriage.safe_distance|float %} {% set object_width = boundary_box_max_x - boundary_box_min_x %} {% set copy_mode_max_width = center_x %} {% set mirror_mode_max_width = center_x - safe_distance / 2.0 %} @@ -609,7 +609,7 @@ gcode: _Z_HOP # move toolhead to the oozeguard if needed - {% if idex_mode != '' and not (printer.initconfigfile.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adaptive_heat_soak)) %} + {% if idex_mode != '' and not (printer.fastconfig.settings.beacon is defined and (beacon_contact_start_print_true_zero or beacon_adaptive_heat_soak)) %} PARK_TOOLHEAD {% endif %} @@ -632,7 +632,7 @@ gcode: M190 S{bed_temp} # Wait for bed thermal expansion - {% if printer.initconfigfile.settings.beacon is defined and beacon_adaptive_heat_soak %} + {% if printer.fastconfig.settings.beacon is defined and beacon_adaptive_heat_soak %} _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2.5 F{z_speed} @@ -669,10 +669,10 @@ gcode: {% if idex_mode == '' %} SET_HEATER_TEMPERATURE HEATER="extruder" TARGET={extruder_first_layer_temp[initial_tool]|float} {% else %} - {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero) %} SET_HEATER_TEMPERATURE HEATER="extruder" TARGET={extruder_first_layer_temp[0]|float} {% endif %} - {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero) %} SET_HEATER_TEMPERATURE HEATER="extruder1" TARGET={extruder_first_layer_temp[1]|float} {% endif %} {% endif %} @@ -688,10 +688,10 @@ gcode: {% if idex_mode == '' %} TEMPERATURE_WAIT SENSOR="extruder" MINIMUM={extruder_first_layer_temp[initial_tool]|float} MAXIMUM={extruder_first_layer_temp[initial_tool]|float + 5} {% else %} - {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 0 or both_toolheads or (default_toolhead == 0 and printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero) %} TEMPERATURE_WAIT SENSOR="extruder" MINIMUM={extruder_first_layer_temp[0]|float} MAXIMUM={extruder_first_layer_temp[0]|float + 5} {% endif %} - {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if initial_tool == 1 or both_toolheads or (default_toolhead == 1 and printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero) %} TEMPERATURE_WAIT SENSOR="extruder1" MINIMUM={extruder_first_layer_temp[1]|float} MAXIMUM={extruder_first_layer_temp[1]|float + 5} {% endif %} {% endif %} @@ -790,7 +790,7 @@ gcode: {% endif %} # set nozzle thermal expansion offset and restore runtime offset - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} # the previously called restore gcode state removed the temp offset # we need first to reset the applied offset value in the variables file _BEACON_SET_NOZZLE_TEMP_OFFSET RESET=True @@ -946,7 +946,7 @@ gcode: {% endif %} # raise z after heat soaking the beacon - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} _Z_HOP {% endif %} @@ -984,7 +984,7 @@ gcode: {% endif %} # beacon contact homing with optional wipe - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero %} {% if beacon_contact_wipe_before_true_zero %} _START_PRINT_AFTER_HEATING_BED_PROBE_FOR_WIPE {% endif %} @@ -1276,7 +1276,7 @@ gcode: {% endif %} # reset nozzle thermal expansion offset - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% if printer["dual_carriage"] is not defined %} # beacon config {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} diff --git a/configuration/macros/idex/idex_is.cfg b/configuration/macros/idex/idex_is.cfg index ccb4a8f68..0958fb0fb 100644 --- a/configuration/macros/idex/idex_is.cfg +++ b/configuration/macros/idex/idex_is.cfg @@ -43,7 +43,7 @@ gcode: {% set act_t = 1 if idex_mode == 'primary' else 0 %} {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed|float * 60 %} {% set adxl_chip = printer["gcode_macro RatOS"].adxl_chip %} - {% set probe_points = printer.initconfigfile.settings.resonance_tester.probe_points[0] %} + {% set probe_points = printer.fastconfig.settings.resonance_tester.probe_points[0] %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} {% set copy_mode_offset = printable_x_max / 4.0 %} @@ -303,7 +303,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed|float * 60 %} {% set parking_position_t1 = printer["gcode_macro T1"].parking_position|float %} {% set adxl_chip = printer["gcode_macro RatOS"].adxl_chip %} - {% set probe_points = printer.initconfigfile.settings.resonance_tester.probe_points[0] %} + {% set probe_points = printer.fastconfig.settings.resonance_tester.probe_points[0] %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set default_toolhead_parking_position = printer["gcode_macro T%s" % default_toolhead].parking_position|float %} diff --git a/configuration/macros/idex/toolheads.cfg b/configuration/macros/idex/toolheads.cfg index 05e408a97..bbe45baa5 100644 --- a/configuration/macros/idex/toolheads.cfg +++ b/configuration/macros/idex/toolheads.cfg @@ -493,7 +493,7 @@ gcode: SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE=0 RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T0: X={xoffset} Y={yoffset} Z={zoffset}" # set nozzle thermal expansion offset - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={new_t} {% endif %} {% endif %} @@ -560,7 +560,7 @@ gcode: SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE=1 RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T1: X={xoffset} Y={yoffset} Z={zoffset}" # set nozzle thermal expansion offset - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={new_t} {% endif %} {% endif %} @@ -776,8 +776,8 @@ gcode: _IDEX_SINGLE INIT=1 # dual carriage safe distance sanity check - {% if printer.initconfigfile.settings.dual_carriage.safe_distance is defined %} - {% if printer.initconfigfile.settings.dual_carriage.safe_distance|float < 50 %} + {% if printer.fastconfig.settings.dual_carriage.safe_distance is defined %} + {% if printer.fastconfig.settings.dual_carriage.safe_distance|float < 50 %} { action_emergency_stop("Dual carriage safe_distance seems to be too low!") } {% endif %} {% else %} @@ -822,7 +822,7 @@ gcode: RATOS_ECHO PREFIX="IDEX" MSG="Toolhead offset applied for T{t}" SAVE_VARIABLE VARIABLE=idex_applied_offset VALUE={t} # set nozzle thermal expansion offset - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} diff --git a/configuration/macros/idex/util.cfg b/configuration/macros/idex/util.cfg index 1f71c4646..686fd1889 100644 --- a/configuration/macros/idex/util.cfg +++ b/configuration/macros/idex/util.cfg @@ -11,10 +11,10 @@ gcode: {% set idex_xcontrolpoint = svv.idex_xcontrolpoint|float %} {% set idex_ycontrolpoint = svv.idex_ycontrolpoint|float %} - {% set stepper_x_position_max = printer.initconfigfile.settings.stepper_x.position_max|float %} - {% set stepper_x_position_endstop = printer.initconfigfile.settings.stepper_x.position_endstop|float %} - {% set dual_carriage_position_max = printer.initconfigfile.settings.dual_carriage.position_max|float %} - {% set dual_carriage_position_endstop = printer.initconfigfile.settings.dual_carriage.position_endstop|float %} + {% set stepper_x_position_max = printer.fastconfig.settings.stepper_x.position_max|float %} + {% set stepper_x_position_endstop = printer.fastconfig.settings.stepper_x.position_endstop|float %} + {% set dual_carriage_position_max = printer.fastconfig.settings.dual_carriage.position_max|float %} + {% set dual_carriage_position_endstop = printer.fastconfig.settings.dual_carriage.position_endstop|float %} {% set line_1 = "_N_[dual_carriage]" %} {% set line_2 = "position_max: %.3f" % (dual_carriage_position_max + idex_xoffset) %} @@ -37,8 +37,8 @@ gcode: gcode: {% if printer["dual_carriage"] is defined %} {% set bed_margin_y = printer["gcode_macro RatOS"].bed_margin_y %} - {% set stepper_y_position_max = printer.initconfigfile.settings.stepper_y.position_max|float %} - {% set stepper_y_position_endstop = printer.initconfigfile.settings.stepper_y.position_endstop|float %} + {% set stepper_y_position_max = printer.fastconfig.settings.stepper_y.position_max|float %} + {% set stepper_y_position_endstop = printer.fastconfig.settings.stepper_y.position_endstop|float %} {% set line_1 = "_N_[stepper_y]" %} {% set line_2 = "position_max: %.3f" % (stepper_y_position_max + 1) %} diff --git a/configuration/macros/idex/vaoc.cfg b/configuration/macros/idex/vaoc.cfg index 7dfe34ad1..7e06f77bc 100644 --- a/configuration/macros/idex/vaoc.cfg +++ b/configuration/macros/idex/vaoc.cfg @@ -114,7 +114,7 @@ gcode: # config {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} - {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.fastconfig.settings.dual_carriage.safe_distance|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} @@ -301,7 +301,7 @@ gcode: # config {% set speed = printer["gcode_macro RatOS"].toolchange_travel_speed * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} - {% set safe_distance = printer.initconfigfile.settings.dual_carriage.safe_distance|float %} + {% set safe_distance = printer.fastconfig.settings.dual_carriage.safe_distance|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set printable_y_max = printer["gcode_macro RatOS"].printable_y_max|float %} @@ -630,7 +630,7 @@ gcode: # beacon contact config {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} - {% if is_fixed and is_started and printer["z_offset_probe"] is defined and printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if is_fixed and is_started and printer["z_offset_probe"] is defined and printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero %} # config {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 4550c618e..3d5c7ce90 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -12,13 +12,13 @@ variable_adaptive_mesh: True # True|False = enable adaptive [gcode_macro _BED_MESH_SANITY_CHECK] gcode: - {% set zero_ref_pos = printer.initconfigfile.settings.bed_mesh.zero_reference_position %} + {% set zero_ref_pos = printer.fastconfig.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is defined %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} {% set beacon_contact_start_print_true_zero_fuzzy_position = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero_fuzzy_position|default(false)|lower == 'true' else false %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero %} {% if beacon_contact_start_print_true_zero_fuzzy_position %} CONSOLE_ECHO TYPE="error" TITLE="Defined zero reference not compatible with fuzzy true zero" MSG="Using fuzzy start print true zero position is not compatible with a defined zero_reference_position value in the [bed_mesh] section in printer.cfg._N_Please remove the zero_reference_position value from the [bed_mesh] section, or disable fuzzy true zero position in printer.cfg, like so:_N__N_[gcode_macro RatOS]_N_variable_beacon_contact_start_print_true_zero_fuzzy_position: False" _STOP_AND_RAISE_ERROR MSG="Defined zero reference not compatible with fuzzy true zero" @@ -50,7 +50,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_x VALUE=None SET_GCODE_VARIABLE MACRO=_START_PRINT_BED_MESH VARIABLE=actual_true_zero_position_y VALUE=None # Error early if the scan compensation mesh is required but does not exist - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} _START_PRINT_PREFLIGHT_CHECK_BEACON_SCAN_COMPENSATION {rawparams} {% endif %} @@ -72,7 +72,7 @@ gcode: # config {% set printable_x_max = printer["gcode_macro RatOS"].printable_x_max|float %} {% set safe_home_x, safe_home_y = printer.ratos.safe_home_position %} - {% set zero_ref_pos = printer.initconfigfile.settings.bed_mesh.zero_reference_position %} + {% set zero_ref_pos = printer.fastconfig.settings.bed_mesh.zero_reference_position %} {% if zero_ref_pos is not defined %} {% set zero_ref_pos = [safe_home_x if actual_true_zero_position_x is none else actual_true_zero_position_x, safe_home_y if actual_true_zero_position_y is none else actual_true_zero_position_y] %} {% endif %} @@ -91,7 +91,7 @@ gcode: {% set beacon_contact_bed_mesh_samples = printer["gcode_macro RatOS"].beacon_contact_bed_mesh_samples|default(2)|int %} {% set beacon_contact_bed_mesh = true if printer["gcode_macro RatOS"].beacon_contact_bed_mesh|default(false)|lower == 'true' else false %} {% set beacon_scan_method_automatic = true if printer["gcode_macro RatOS"].beacon_scan_method_automatic|default(false)|lower == 'true' else false %} - {% if printer.initconfigfile.settings.beacon is defined and not beacon_contact_bed_mesh %} + {% if printer.fastconfig.settings.beacon is defined and not beacon_contact_bed_mesh %} SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY={beacon_bed_mesh_scv} {% endif %} @@ -102,7 +102,7 @@ gcode: {% if printer["gcode_macro RatOS"].adaptive_mesh|lower == 'true' %} CALIBRATE_ADAPTIVE_MESH PROFILE="{default_profile}" X0={X[0]} X1={X[1]} Y0={Y[0]} Y1={Y[1]} T={params.T|int} BOTH_TOOLHEADS={params.BOTH_TOOLHEADS} IDEX_MODE={idex_mode} ZERO_REF_POS_X={zero_ref_pos[0]} ZERO_REF_POS_Y={zero_ref_pos[1]} {% else %} - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -160,7 +160,7 @@ gcode: {% if x0 >= x1 or y0 >= y1 %} # coordinates are invalid, fall back to full bed mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Invalid coordinates received. Please check your slicer settings. Falling back to full bed mesh." - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -190,7 +190,7 @@ gcode: {% endif %} # get bed mesh config object - {% set mesh_config = printer.initconfigfile.config.bed_mesh %} + {% set mesh_config = printer.fastconfig.config.bed_mesh %} # get configured bed mesh area {% set min_x = mesh_config.mesh_min.split(",")[0]|float %} @@ -207,7 +207,7 @@ gcode: {% if mesh_x0 == min_x and mesh_y0 == min_y and mesh_x1 == max_x and mesh_y1 == max_y %} # coordinates are invalid, fall back to full bed mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Print is using the full bed, falling back to full bed mesh." - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" {% else %} @@ -272,9 +272,9 @@ gcode: # IDEX {% set probe_first = printer["gcode_macro RatOS"].nozzle_prime_start_y|lower == "min" or printer["gcode_macro RatOS"].nozzle_prime_start_y|float(printable_y_max) < printable_y_max / 2 %} {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and printer.initconfigfile.settings.beacon.mesh_runs % 2 != 0 and probe_first %} + {% if printer.fastconfig.settings.beacon is defined and printer.fastconfig.settings.beacon.mesh_runs % 2 != 0 and probe_first %} {% set probe_first = false %} - {% elif printer.initconfigfile.settings.beacon is defined and printer.initconfigfile.settings.beacon.mesh_runs % 2 == 0 and not probe_first %} + {% elif printer.fastconfig.settings.beacon is defined and printer.fastconfig.settings.beacon.mesh_runs % 2 == 0 and not probe_first %} {% set probe_first = true %} {% endif %} @@ -296,7 +296,7 @@ gcode: # mesh RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0|round(1)} Y0={mesh_y0|round(1)} X1={mesh_x1|round(1)} Y1={mesh_y1}|round(1)}" - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 {% else %} diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index 02ffcd5f5..b50e0f00b 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -96,13 +96,13 @@ gcode: {% if idex_mode == "copy" or idex_mode == "mirror" %} M104.1 S{s0} T0 M104.1 S{s1} T1 - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=0 _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD=1 {% endif %} {% else %} M104.1 S{s} T{t} - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} @@ -148,7 +148,7 @@ gcode: M109.1 S{s} T{t} # Update the nozzle thermal expansion offset {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} - {% if printer.initconfigfile.settings.beacon is defined and is_printing_gcode %} + {% if printer.fastconfig.settings.beacon is defined and is_printing_gcode %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={t} {% endif %} {% endif %} @@ -182,7 +182,7 @@ gcode: # call klipper base function SET_HEATER_TEMPERATURE_BASE HEATER="{heater}" TARGET={target} - {% if t != -1 and printer.initconfigfile.settings.beacon is defined %} + {% if t != -1 and printer.fastconfig.settings.beacon is defined %} # Update the nozzle thermal expansion offset {% set is_printing_gcode = printer["gcode_macro START_PRINT"].is_printing_gcode|default(false)|lower == 'true' %} {% if is_printing_gcode %} @@ -261,7 +261,7 @@ rename_existing: SKEW_PROFILE_BASE variable_loaded_profile: "" # internal use only. Do not touch! gcode: {% if params.LOAD is defined %} - {% if printer.initconfigfile.settings["skew_correction %s" % params.LOAD] is defined %} + {% if printer.fastconfig.settings["skew_correction %s" % params.LOAD] is defined %} SET_GCODE_VARIABLE MACRO=SKEW_PROFILE VARIABLE=loaded_profile VALUE='"{params.LOAD}"' {% endif %} {% endif %} diff --git a/configuration/macros/parking.cfg b/configuration/macros/parking.cfg index 6f0c0f3a4..2d0de9216 100644 --- a/configuration/macros/parking.cfg +++ b/configuration/macros/parking.cfg @@ -26,7 +26,7 @@ gcode: {% endif %} # park IDEX toolhead if needed - {% if printer["dual_carriage"] is defined and not (printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero) %} + {% if printer["dual_carriage"] is defined and not (printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero) %} {% if printer["gcode_macro RatOS"].start_print_park_x is defined and printer["gcode_macro RatOS"].start_print_park_x != '' %} RATOS_ECHO PREFIX="WARNING" MSG="start_print_park_x is ignored for IDEX printers" {% endif %} diff --git a/configuration/macros/priming.cfg b/configuration/macros/priming.cfg index 4e7fee639..df3c07983 100644 --- a/configuration/macros/priming.cfg +++ b/configuration/macros/priming.cfg @@ -29,18 +29,18 @@ gcode: {% set beacon_contact_prime_probing = true if printer["gcode_macro RatOS"].beacon_contact_prime_probing|default(false)|lower == 'true' else false %} {% set last_z_offset = 9999.9 %} - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} {% set current_z = printer.toolhead.position.z|float %} {% if beacon_contact_prime_probing %} {% set last_z_offset = printer.beacon.last_z_result %} {% else %} {% set last_z_offset = printer.beacon.last_sample.dist - current_z %} {% endif %} - {% elif printer.initconfigfile.settings.bltouch is defined %} - {% set config_offset = printer.initconfigfile.settings.bltouch.z_offset|float %} + {% elif printer.fastconfig.settings.bltouch is defined %} + {% set config_offset = printer.fastconfig.settings.bltouch.z_offset|float %} {% set last_z_offset = printer.probe.last_z_result - config_offset %} - {% elif printer.initconfigfile.settings.probe is defined %} - {% set config_offset = printer.initconfigfile.settings.probe.z_offset|float %} + {% elif printer.fastconfig.settings.probe is defined %} + {% set config_offset = printer.fastconfig.settings.probe.z_offset|float %} {% set last_z_offset = printer.probe.last_z_result - config_offset %} {% endif %} RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Saving offset adjustment of {last_z_offset} in {params.VARIABLE|default('last_z_offset')}" @@ -102,24 +102,24 @@ gcode: {% set y_start = printable_y_max - 5 %} {% endif %} {% endif %} - {% set z = printer.initconfigfile.settings.bed_mesh.horizontal_move_z|float %} + {% set z = printer.fastconfig.settings.bed_mesh.horizontal_move_z|float %} # get bed mesh config object - {% set mesh_config = printer.initconfigfile.config.bed_mesh %} + {% set mesh_config = printer.fastconfig.config.bed_mesh %} # Get probe offsets - {% if printer.initconfigfile.settings.bltouch is defined %} - {% set x_offset = printer.initconfigfile.settings.bltouch.x_offset|float %} - {% set y_offset = printer.initconfigfile.settings.bltouch.y_offset|float %} - {% set z_offset = printer.initconfigfile.settings.bltouch.z_offset|float %} - {% elif printer.initconfigfile.settings.probe is defined %} - {% set x_offset = printer.initconfigfile.settings.probe.x_offset|float %} - {% set y_offset = printer.initconfigfile.settings.probe.y_offset|float %} - {% set z_offset = printer.initconfigfile.settings.probe.z_offset|float %} - {% elif printer.initconfigfile.settings.beacon is defined %} - {% set x_offset = printer.initconfigfile.settings.beacon.x_offset|float %} - {% set y_offset = printer.initconfigfile.settings.beacon.y_offset|float %} - {% set z_offset = printer.initconfigfile.settings.beacon.trigger_distance|float %} + {% if printer.fastconfig.settings.bltouch is defined %} + {% set x_offset = printer.fastconfig.settings.bltouch.x_offset|float %} + {% set y_offset = printer.fastconfig.settings.bltouch.y_offset|float %} + {% set z_offset = printer.fastconfig.settings.bltouch.z_offset|float %} + {% elif printer.fastconfig.settings.probe is defined %} + {% set x_offset = printer.fastconfig.settings.probe.x_offset|float %} + {% set y_offset = printer.fastconfig.settings.probe.y_offset|float %} + {% set z_offset = printer.fastconfig.settings.probe.z_offset|float %} + {% elif printer.fastconfig.settings.beacon is defined %} + {% set x_offset = printer.fastconfig.settings.beacon.x_offset|float %} + {% set y_offset = printer.fastconfig.settings.beacon.y_offset|float %} + {% set z_offset = printer.fastconfig.settings.beacon.trigger_distance|float %} {% else %} { action_raise_error("No probe, beacon or bltouch section found. Adaptive priming only works with a [probe], [beacon] or [bltouch] section defined.") } {% endif %} @@ -204,12 +204,12 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe|lower == 'stowable' %} ASSERT_PROBE_DEPLOYED {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_prime_probing %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_prime_probing %} PROBE PROBE_METHOD=contact SAMPLES=1 {% else %} PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} BEACON_QUERY {% else %} # Only restore state if we're not using a beacon, so we state at scanning height @@ -259,7 +259,7 @@ gcode: {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} {% set fan_speed = printer["gcode_macro RatOS"].nozzle_prime_bridge_fan|float %} - {% set nozzle_diameter = printer.initconfigfile.settings[extruder].nozzle_diameter|float %} + {% set nozzle_diameter = printer.fastconfig.settings[extruder].nozzle_diameter|float %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set has_start_offset_t0 = printer["gcode_macro RatOS"].probe_for_priming_result|float(9999.9) != 9999.9 %} {% if printer["dual_carriage"] is defined %} @@ -351,7 +351,7 @@ gcode: {% if has_start_offset_t0 %} {% set start_z_probe_result_t0 = printer["gcode_macro RatOS"].probe_for_priming_result|float(9999.9) %} {% set end_z_probe_result_t0 = printer["gcode_macro RatOS"].probe_for_priming_end_result|float(9999.9) %} - {% if printer.initconfigfile.settings.bltouch is not defined and printer.initconfigfile.settings.probe is not defined and printer.initconfigfile.settings.beacon is not defined %} + {% if printer.fastconfig.settings.bltouch is not defined and printer.fastconfig.settings.probe is not defined and printer.fastconfig.settings.beacon is not defined %} { action_raise_error("No probe or bltouch section found. Adaptive priming only works with [probe], [beacon] or [bltouch].") } {% endif %} {% if start_z_probe_result_t0 == 9999.9 %} @@ -376,7 +376,7 @@ gcode: {% if has_start_offset_t1 %} {% set start_z_probe_result_t1 = printer["gcode_macro RatOS"].probe_for_priming_result_t1|float(9999.9) %} {% set end_z_probe_result_t1 = printer["gcode_macro RatOS"].probe_for_priming_end_result_t1|float(9999.9) %} - {% if printer.initconfigfile.settings.bltouch is not defined and printer.initconfigfile.settings.probe is not defined and printer.initconfigfile.settings.beacon is not defined %} + {% if printer.fastconfig.settings.bltouch is not defined and printer.fastconfig.settings.probe is not defined and printer.fastconfig.settings.beacon is not defined %} { action_raise_error("No probe or bltouch section found. Adaptive priming only works with [probe], [beacon] or [bltouch].") } {% endif %} {% if start_z_probe_result_t1 == 9999.9 %} @@ -407,7 +407,7 @@ gcode: {% endif %} # set nozzle thermal expansion offset - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} _BEACON_SET_NOZZLE_TEMP_OFFSET TOOLHEAD={current_toolhead} {% endif %} diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index 966e42247..22ccc9cdd 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -53,7 +53,7 @@ gcode: CALCULATE_PRINTABLE_AREA INITIAL_FRONTEND_UPDATE _CHAMBER_FILTER_SANITY_CHECK - {% if printer.initconfigfile.settings.beacon is defined %} + {% if printer.fastconfig.settings.beacon is defined %} # Enforce valid zero_reference position configuration, warn about problematic settings. _BED_MESH_SANITY_CHECK {% endif %} @@ -250,8 +250,8 @@ gcode: gcode: # inverted hybrid core-xy bugfix sanity check {% set inverted = False %} - {% if printer.initconfigfile.settings.ratos_hybrid_corexy is defined and printer.initconfigfile.settings.ratos_hybrid_corexy.inverted is defined %} - {% if printer.initconfigfile.settings.ratos_hybrid_corexy.inverted|lower == 'true' %} + {% if printer.fastconfig.settings.ratos_hybrid_corexy is defined and printer.fastconfig.settings.ratos_hybrid_corexy.inverted is defined %} + {% if printer.fastconfig.settings.ratos_hybrid_corexy.inverted|lower == 'true' %} {% set inverted = True %} {% endif %} {% endif %} @@ -378,7 +378,7 @@ gcode: gcode: {% set ratos_skew_profile = printer["gcode_macro RatOS"].skew_profile|default("") %} {% if ratos_skew_profile != "" %} - {% if printer.initconfigfile.config["skew_correction %s" % ratos_skew_profile] is defined %} + {% if printer.fastconfig.config["skew_correction %s" % ratos_skew_profile] is defined %} SKEW_PROFILE LOAD={ratos_skew_profile} GET_CURRENT_SKEW {% else %} diff --git a/configuration/printers/v-core-3-idex/macros.cfg b/configuration/printers/v-core-3-idex/macros.cfg index 609f6d023..656c6f02f 100644 --- a/configuration/printers/v-core-3-idex/macros.cfg +++ b/configuration/printers/v-core-3-idex/macros.cfg @@ -45,7 +45,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-3/macros.cfg b/configuration/printers/v-core-3/macros.cfg index 9f1774fb1..69daa719f 100644 --- a/configuration/printers/v-core-3/macros.cfg +++ b/configuration/printers/v-core-3/macros.cfg @@ -17,7 +17,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4-hybrid/macros.cfg b/configuration/printers/v-core-4-hybrid/macros.cfg index bf4a7ff10..b95be829b 100644 --- a/configuration/printers/v-core-4-hybrid/macros.cfg +++ b/configuration/printers/v-core-4-hybrid/macros.cfg @@ -19,7 +19,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4-idex/macros.cfg b/configuration/printers/v-core-4-idex/macros.cfg index c121a70a2..f6587f786 100644 --- a/configuration/printers/v-core-4-idex/macros.cfg +++ b/configuration/printers/v-core-4-idex/macros.cfg @@ -39,7 +39,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-4/macros.cfg b/configuration/printers/v-core-4/macros.cfg index e663dfdf8..aaa6be363 100644 --- a/configuration/printers/v-core-4/macros.cfg +++ b/configuration/printers/v-core-4/macros.cfg @@ -13,7 +13,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/printers/v-core-pro/macros.cfg b/configuration/printers/v-core-pro/macros.cfg index e663dfdf8..aaa6be363 100644 --- a/configuration/printers/v-core-pro/macros.cfg +++ b/configuration/printers/v-core-pro/macros.cfg @@ -13,7 +13,7 @@ gcode: {% if printer["gcode_macro RatOS"].z_probe == 'stowable' %} DEPLOY_PROBE {% endif %} - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_z_tilt_adjust %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_z_tilt_adjust %} Z_TILT_ADJUST_ORIG PROBE_METHOD=contact SAMPLES={beacon_contact_z_tilt_adjust_samples} {% else %} Z_TILT_ADJUST_ORIG diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index 83a5fa2fd..a8ff4754c 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -194,7 +194,7 @@ verify_registered_extensions() ["ratos_z_offset_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/ratos_z_offset.py") ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") ["beacon_adaptive_heatsoak_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_adaptive_heat_soak.py") - ["initconfigfile"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/initconfigfile.py") + ["fastconfig"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/fastconfig.py") ["dynamic_governor"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/dynamic_governor.py") ) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index fdfabc196..6316e8587 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -263,7 +263,7 @@ gcode: # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} - {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop_speed = printer.fastconfig.config.ratos_homing.z_hop_speed|float * 60 %} # beacon adaptive heat soak config {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} @@ -359,7 +359,7 @@ gcode: {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} # config - {% set z_hop_speed = printer.initconfigfile.config.ratos_homing.z_hop_speed|float * 60 %} + {% set z_hop_speed = printer.fastconfig.config.ratos_homing.z_hop_speed|float * 60 %} # reset results SET_GCODE_VARIABLE MACRO=BEACON_POKE_TEST VARIABLE=poke_result_1 VALUE=-1 @@ -899,7 +899,7 @@ gcode: {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object - {% set mesh_config = printer.initconfigfile.config.bed_mesh %} + {% set mesh_config = printer.fastconfig.config.bed_mesh %} # get configured bed mesh area {% set min_x = mesh_config.mesh_min.split(",")[0]|float %} @@ -1050,13 +1050,13 @@ gcode: {% set beacon_contact_calibrate_model_on_print = params.BEACON_CONTACT_CALIBRATE_MODEL_ON_PRINT|lower == 'true' %} # Error early with a clear message if a Beacon model is required but not active. - {% if printer.initconfigfile.settings.beacon is defined and not printer.beacon.model %} + {% if printer.fastconfig.settings.beacon is defined and not printer.beacon.model %} {% set beacon_model_required_for_true_zero = beacon_contact_start_print_true_zero and not beacon_contact_calibrate_model_on_print %} {% set beacon_model_required_for_homing = - printer.initconfigfile.settings.stepper_z.endstop_pin == 'probe:z_virtual_endstop' - and (( printer.initconfigfile.settings.beacon.home_method|default('proximity')|lower == 'proximity' + printer.fastconfig.settings.stepper_z.endstop_pin == 'probe:z_virtual_endstop' + and (( printer.fastconfig.settings.beacon.home_method|default('proximity')|lower == 'proximity' and printer["gcode_macro RatOS"].beacon_contact_z_homing|default(false)|lower != 'true') - or ( printer.initconfigfile.settings.beacon.default_probe_method|default('proximity')|lower == 'proximity' + or ( printer.fastconfig.settings.beacon.default_probe_method|default('proximity')|lower == 'proximity' and printer["gcode_macro RatOS"].beacon_contact_z_tilt_adjust|default(false)|lower != 'true' )) %} {% if beacon_model_required_for_homing or beacon_model_required_for_true_zero %} _LED_START_PRINTING_ERROR @@ -1181,7 +1181,7 @@ rename_existing: _Z_OFFSET_APPLY_PROBE_BASE gcode: {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} # If we're using contact true zero, use RatOS offset management; otherwise, delegate to base - {% if printer.initconfigfile.settings.beacon is defined and beacon_contact_start_print_true_zero %} + {% if printer.fastconfig.settings.beacon is defined and beacon_contact_start_print_true_zero %} _BEACON_SET_RUNTIME_OFFSET {rawparams} {% else %} _Z_OFFSET_APPLY_PROBE_BASE {rawparams} @@ -1211,8 +1211,8 @@ gcode: [gcode_macro BED_MESH_CALIBRATE] rename_existing: _BED_MESH_CALIBRATE_BASE gcode: - {% if printer.initconfigfile.settings.beacon is defined %} - {% set beacon_default_probe_method = printer.initconfigfile.settings.beacon.default_probe_method|default('proximity') %} + {% if printer.fastconfig.settings.beacon is defined %} + {% set beacon_default_probe_method = printer.fastconfig.settings.beacon.default_probe_method|default('proximity') %} {% set probe_method = params.PROBE_METHOD|default(beacon_default_probe_method)|lower %} {% if probe_method == 'proximity' %} _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" From 954a86c7fdb00c8fe86b434614318402e72286fb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 30 Jun 2025 18:13:12 +0100 Subject: [PATCH 092/139] refactor(extras): move [fastconfig] and [dynamic_governor] to base.cfg --- configuration/macros.cfg | 10 ---------- configuration/printers/base.cfg | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 7a935236d..4233a5ea3 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -2,16 +2,6 @@ # To override settings from this file, you can copy and paste the relevant # sections into your printer.cfg and change it there. -##### -# fastconfig -##### -[fastconfig] - -##### -# dynamic_governor -##### -[dynamic_governor] - ##### # INCLUDE MACRO FILES ##### diff --git a/configuration/printers/base.cfg b/configuration/printers/base.cfg index 6598535ea..bd4249822 100644 --- a/configuration/printers/base.cfg +++ b/configuration/printers/base.cfg @@ -43,4 +43,8 @@ allow_unknown_gcode_generator: True [exclude_object] [bed_mesh] -split_delta_z: 0.01 # Avoid visible surface stripe arfetacts \ No newline at end of file +split_delta_z: 0.01 # Avoid visible surface stripe arfetacts + +[fastconfig] + +[dynamic_governor] \ No newline at end of file From b13703deaf61b58dc09359848071a2aa71c3b652 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 17:58:06 +0100 Subject: [PATCH 093/139] docs: add copyright headers --- configuration/klippy/beacon_mesh.py | 10 +++++++++- configuration/klippy/ratos.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 5fa65a632..a02b2fa34 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,4 +1,12 @@ -import multiprocessing, traceback, logging +# Beacaon contact compensation mesh +# +# Copyright (C) 2024 Helge Keck +# Copyright (C) 2024-2025 Mikkel Schmidt +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import multiprocessing, traceback from collections import OrderedDict from . import bed_mesh as BedMesh import numpy as np diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 547a56dcb..1b245d7d5 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -1,3 +1,11 @@ +# RatOS general purpose module +# +# Copyright (C) 2024 Helge Keck +# Copyright (C) 2024 Mikkel Schmidt +# Copyright (C) 2025 Tom Glastonbury +# +# This file may be distributed under the terms of the GNU GPLv3 license. + import os, logging, glob, traceback, inspect, re import json, subprocess, pathlib, random, math from collections import namedtuple From cf1196ddaf34c7a88d37143a5634ed1b56e935ae Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 17:59:20 +0100 Subject: [PATCH 094/139] refactor(beacon-mesh): remove test code --- configuration/klippy/beacon_mesh.py | 92 ----------------------------- 1 file changed, 92 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index a02b2fa34..460468bf5 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -156,9 +156,6 @@ def register_commands(self): self.gcode.register_command('GET_RATOS_EXTENDED_BED_MESH_PARAMETERS', self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, desc=(self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS)) - self.gcode.register_command('REMAKE_BEACON_COMPENSATION_MESH', - self.cmd_REMAKE_BEACON_COMPENSATION_MESH, - desc=(self.desc_REMAKE_BEACON_COMPENSATION_MESH)) desc_BEACON_MESH_INIT = "Performs Beacon mesh initialization tasks" def cmd_BEACON_MESH_INIT(self, gcmd): @@ -298,12 +295,6 @@ def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): self.create_compensation_mesh(gcmd, profile, probe_count, chamber_temp) - desc_REMAKE_BEACON_COMPENSATION_MESH = "TESTING! PROFILE='exising comp mesh' NEW_PROFILE='new name' [GAUSSIAN_SIGMA=x]" - def cmd_REMAKE_BEACON_COMPENSATION_MESH(self, gcmd): - profile = gcmd.get('PROFILE') - new_profile = gcmd.get('NEW_PROFILE') - self.TESTING_remake_compensation_mesh(profile, new_profile) - desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): if (self.bed_mesh.z_mesh is None): @@ -733,89 +724,6 @@ def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): except BedMesh.BedMeshError as e: self.ratos.console_echo("Create compensation mesh error", "error", str(e)) - # TODO: Remove testing stuff before release - def TESTING_remake_compensation_mesh(self, profile, new_profile, gaussian_sigma=None): - - mesh_before_name = profile + "_SCAN_BEFORE" - mesh_after_name = profile + "_SCAN_AFTER" - contact_mesh_name = profile + "_CONTACT" - - scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) - scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) - contact_zmesh = self._create_zmesh_from_profile(contact_mesh_name) - - contact_mesh_points = contact_zmesh.probed_matrix - contact_params = contact_zmesh.get_mesh_params().copy() - contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) - contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) - - self.ratos.debug_echo("Create compensation mesh", f"Filtering contact mesh") - contact_mesh_points = self._apply_filter(contact_mesh_points) - contact_params[RATOS_MESH_NOTES_PARAMETER] = "contact mesh filtered using local low filter" - - compensation_mesh_points = [] - - try: - if not self.beacon.mesh_helper.dir in ("x", "y"): - raise ValueError(f"Expected 'x' or 'y' for self.beacon.mesh_helper.dir, but got '{self.beacon.mesh_helper.dir}'") - - dir = self.beacon.mesh_helper.dir - y_count = len(contact_mesh_points) - x_count = len(contact_mesh_points[0]) - contact_mesh_point_count = len(contact_mesh_points) * len(contact_mesh_points[0]) - - debug_lines = [] - - for y in range(y_count): - compensation_mesh_points.append([]) - for x in range(x_count): - contact_mesh_index = \ - ((x if y % 2 == 0 else x_count - x - 1) + y * x_count) \ - if dir == "x" else \ - ((y if x % 2 == 0 else y_count - y - 1) + x * y_count) - - blend_factor = contact_mesh_index / (contact_mesh_point_count - 1) - - contact_x_pos = contact_params["min_x"] + x * contact_x_step - contact_y_pos = contact_params["min_y"] + y * contact_y_step - - scan_before_z = scan_before_zmesh.calc_z(contact_x_pos, contact_y_pos) - scan_after_z = scan_after_zmesh.calc_z(contact_x_pos, contact_y_pos) - scan_temporal_crossfade_z = ((1 - blend_factor) * scan_before_z) + (blend_factor * scan_after_z) - - contact_z = contact_mesh_points[y][x] - offset_z = contact_z - scan_temporal_crossfade_z - - compensation_mesh_points[y].append(offset_z) - - #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") - - # For a large mesh (eg, 60x60) this can take 2+ minutes - #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) - - if gaussian_sigma is not None: - params = contact_params.copy() - filtered_profile = new_profile + RATOS_TEMP_CONTACT_MESH_NAME + "_filtered" - new_mesh = BedMesh.ZMesh(params, filtered_profile) - new_mesh.build_mesh(contact_mesh_points) - self.bed_mesh.set_mesh(new_mesh) - self.bed_mesh.save_profile(filtered_profile) - - # Create new mesh - params = contact_params.copy() - params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION - params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() - params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION - params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY - new_mesh = BedMesh.ZMesh(params, new_profile) - new_mesh.build_mesh(compensation_mesh_points) - self.bed_mesh.set_mesh(new_mesh) - self.bed_mesh.save_profile(new_profile) - - self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(new_profile))) - except BedMesh.BedMeshError as e: - self.ratos.console_echo("Create compensation mesh error", "error", str(e)) - def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() From 1c37d8745527dbea6521234afe66d8c1ef0a55bb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 17:59:57 +0100 Subject: [PATCH 095/139] refactor(beacon): move variable to internal section to discourage user use --- configuration/z-probe/beacon.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 6316e8587..34eff47dd 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -100,9 +100,6 @@ variable_beacon_adaptive_heat_soak_min_wait: 0 # The minimum time in seconds t variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete variable_beacon_adaptive_heat_soak_threshold: 20 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive # heat soaking. -variable_beacon_adaptive_heat_soak_hold_count: 150 # The number of continuous seconds with z-rate within the threshold for - # adaptive heat soaking to be considered potentntially complete (there are - # additional trend checks which also have to pass). variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 # The extra time in seconds to wait after adaptive heat soaking is considered complete. # Typically not needed, but can be useful for printers with very stable gantry designs @@ -113,6 +110,7 @@ variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 # INTERNAL USE ONLY! DO NOT TOUCH! ##### variable_beacon_contact_start_print_true_zero_fuzzy_radius: 20 +variable_beacon_adaptive_heat_soak_hold_count: 150 ##### # BEACON COMMON From 5ca569edd9565bece8526803c170afbea44c669a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 23 Jun 2025 20:37:39 +0100 Subject: [PATCH 096/139] refactor(beacon-mesh): make dependency on scipy.ndimage lazy, it's only needed for compensation mesh creation --- configuration/klippy/beacon_mesh.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 460468bf5..04060b212 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -10,7 +10,7 @@ from collections import OrderedDict from . import bed_mesh as BedMesh import numpy as np -from scipy.ndimage import gaussian_filter +import importlib # Temporary mesh names RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" @@ -101,6 +101,9 @@ def __init__(self, config): self.offset_mesh = None self.offset_mesh_points = [[]] + # Loaded on demand if needed + self.scipy_ndimage = None + self.register_commands() self.register_handler() @@ -523,10 +526,21 @@ def do(): else: return result - @staticmethod - def _do_local_low_filter(data, lowpass_sigma=1., num_keep=4, num_keep_edge=3, num_keep_corner=2): + def _gaussian_filter(self, data, sigma, mode): + if not self.scipy_ndimage: + try: + self.scipy_ndimage = importlib.import_module("scipy.ndimage") + except ImportError: + raise Exception( + "Could not load `scipy.ndimage`. To install it, simply run `ratos doctor`. This " + "module is required for Beacon contact compensation mesh creation." + ) + + return self.scipy_ndimage.gaussian_filter(data, sigma=sigma, mode=mode) + + def _do_local_low_filter(self, data, lowpass_sigma=1., num_keep=4, num_keep_edge=3, num_keep_corner=2): # 1. Low-pass filter to obtain general shape - lowpass = gaussian_filter(data, sigma=lowpass_sigma, mode='nearest') + lowpass = self._gaussian_filter(data, sigma=lowpass_sigma, mode='nearest') # 2. Subtract the low-pass filtered version from the original # to get the high-frequency details From 7a6bd7eb93b899aaccb693098442fb7397e48fc6 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 9 Jul 2025 15:55:35 +0100 Subject: [PATCH 097/139] feat(beacon): support basic automatic selection of compensation mesh - Based on bed temperature - Ambiguous match is an error - Automatic profile naming for compensation mesh creation --- configuration/klippy/beacon_mesh.py | 150 ++++++++++++++++++++++++++-- configuration/z-probe/beacon.cfg | 15 +-- 2 files changed, 147 insertions(+), 18 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 04060b212..6e657d091 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -21,7 +21,7 @@ ### RATOS_TEMP_SCAN_MESH_NAME = "__BEACON_TEMP_SCAN_MESH__" RATOS_TEMP_CONTACT_MESH_NAME = "__BEACON_TEMP_CONTACT_MESH__" -RATOS_DEFAULT_COMPENSATION_MESH_NAME = "Beacon Scan Compensation" +RATOS_COMPENSATION_MESH_NAME_AUTO = "auto" RATOS_MESH_VERSION = 1 RATOS_MESH_KIND_MEASURED = "measured" @@ -159,6 +159,9 @@ def register_commands(self): self.gcode.register_command('GET_RATOS_EXTENDED_BED_MESH_PARAMETERS', self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, desc=(self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS)) + self.gcode.register_command('_TEST_COMPENSATION_MESH_AUTO_SELECTION', + self.cmd_TEST_COMPENSATION_MESH_AUTO_SELECTION, + desc=(self.desc_TEST_COMPENSATION_MESH_AUTO_SELECTION)) desc_BEACON_MESH_INIT = "Performs Beacon mesh initialization tasks" def cmd_BEACON_MESH_INIT(self, gcmd): @@ -262,6 +265,9 @@ def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): bed_temp = gcmd.get_float("COMPARE_BED_TEMP", None) bed_temp_is_error = gcmd.get("COMPARE_BED_TEMP_IS_ERROR", "false").strip().lower() in ("1", "true") + if profile.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: + profile = self.auto_select_compensation_mesh(bed_temp) + # eg, caller can use BED_TEMP=-1 when bed temp should not be checked if bed_temp < 0: bed_temp = None @@ -276,26 +282,143 @@ def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): raise self.printer.command_error(f"{subject} is not a valid compensation mesh profile") + def get_profiles(self, kind=None): + # Gets a dictionary of all RatOS-valid profiles, optionally filtered by kind. + profiles = self.bed_mesh.pmgr.get_profiles() + + result = {} + + for profile_name, profile in profiles.items(): + params = profile["mesh_params"] + # Consider only RatOS-valid profiles + if RATOS_MESH_VERSION_PARAMETER in params: + if kind is None or params[RATOS_MESH_KIND_PARAMETER] == kind: + result[profile_name] = profile + + return result + + def auto_select_compensation_mesh(self, bed_temperature=None): + # Automatically selects a compensation mesh based on the specified bed_temperature, or the + # current target bed temperature if bed_temperature is None. + + link_url = "https://os.ratrig.com/docs/configuration/beacon_contact" + link_text = "Beacon Contact Compensation Mesh" + link_line = f'Lean more about {link_text}' + + profiles = self.get_profiles(RATOS_MESH_KIND_COMPENSATION) + + if not profiles: + self.ratos.console_echo("Auto-select compensation mesh error", "error", + "No compensation mesh profiles found. Create a compensation mesh, or disable the_N_" + "Beacon compensation mesh feature._N_" + + link_line) + + raise self.printer.command_error("No compensation mesh profiles found") + + if bed_temperature is None: + bed_temperature = self._get_nominal_bed_temp() + + profile_list = ", ".join(f"{name} ({profile['mesh_params'][RATOS_MESH_BED_TEMP_PARAMETER]}°C)" for name, profile in profiles.items()) + self.ratos.debug_echo("auto_select_compensation_mesh", + f"Available compensation mesh profiles: {profile_list}") + + # Find the closest compensation mesh profile based on bed temperature + best_profiles = [] + best_temp_diff = float('inf') + + for profile_name, profile in profiles.items(): + params = profile["mesh_params"] + profile_bed_temp = params[RATOS_MESH_BED_TEMP_PARAMETER] + temp_diff = abs(profile_bed_temp - bed_temperature) + + if temp_diff < best_temp_diff: + best_temp_diff = temp_diff + best_profiles = [(profile_name, profile_bed_temp)] + elif temp_diff == best_temp_diff: + best_profiles.append((profile_name, profile_bed_temp)) + + # If there are multiple candidate profiles with the same bed temperature, then the result + # is ambiguous, which is considered an error. + distinct_bed_temps = set(temp for _, temp in best_profiles) + if len(distinct_bed_temps) != len(best_profiles): + self.ratos.console_echo("Auto-select compensation mesh error", "error", + "A compensation mesh cannot be selected automatically because there is more than one equally-suitable profile._N_" + "Either delete one of the following profiles, or configure the desired profile explicitly:_N_" + + "_N_".join(f" '{name}' ({temp}°C)" for name, temp in best_profiles) + + f"_N_{link_line}") + + raise self.printer.command_error("Automatic compensation mesh selection is ambiguous") + + # Pick the candidate profile with the highest bed temperature + best_profile, best_temp = max(best_profiles, key=lambda x: x[1]) + + # Check if the temperature difference is too large + if best_temp_diff > self.bed_temp_warning_margin: + self.ratos.console_echo("Auto-select compensation mesh warning", "warning", + f"Selected compensation mesh '{best_profile}' has a bed temperature of {best_temp}°C, " + f"which differs by {best_temp_diff:.1f}°C from the requested {bed_temperature:.1f}°C._N_" + "This may result in inaccurate compensation." + + f"_N_{link_line}") + else: + self.gcode.respond_info( + f"Selected compensation mesh '{best_profile}' with bed temperature {best_temp}°C " + f"(requested: {bed_temperature:.1f}°C, difference: {best_temp_diff:.1f}°C)") + + return best_profile + + desc_TEST_COMPENSATION_MESH_AUTO_SELECTION = "Tests the automatic selection of a compensation mesh. Will raise an error if no suitable mesh is found." + def cmd_TEST_COMPENSATION_MESH_AUTO_SELECTION(self, gcmd): + bed_temp = gcmd.get_float('BED_TEMP', self._get_nominal_bed_temp()) + try: + profile_name = self.auto_select_compensation_mesh(bed_temp) + gcmd.respond_info(f"Auto-selected compensation mesh profile: {profile_name}") + except Exception as e: + raise gcmd.error(str(e)) from e + desc_BEACON_APPLY_SCAN_COMPENSATION = "Compensates a beacon scan mesh with a beacon compensation mesh." def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): - profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) - if not profile.strip(): + profile = gcmd.get('PROFILE', RATOS_COMPENSATION_MESH_NAME_AUTO).strip() + if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") - if not self.apply_scan_compensation(self._create_zmesh_from_profile(profile, purpose="Beacon scan compensation")): + if not self.apply_scan_compensation(profile): raise self.printer.command_error("Could not apply scan compensation") + + def _get_unique_profile_name(self, base_name): + # Obtains a unique profile name based on the base_name. + # If the base_name already exists, appends a number to make it unique. + # Returns a tuple of (unique_name, base_name_is_unique). + profiles = self.bed_mesh.pmgr.get_profiles() + if base_name not in profiles: + return (base_name, True) + + i = 1 + while f"{base_name}_{i}" in profiles: + i += 1 + return (f"{base_name}_{i}", False) + desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): - profile = gcmd.get('PROFILE', RATOS_DEFAULT_COMPENSATION_MESH_NAME) + profile = gcmd.get('PROFILE', RATOS_COMPENSATION_MESH_NAME_AUTO).strip() # Using minval=4 to avoid BedMesh defaulting to using Lagrangian interpolation which appears to be broken probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=4) chamber_temp = gcmd.get_float('CHAMBER_TEMP', 0) - if not profile.strip(): + + if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") + if not probe_count: raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - + + if profile.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: + base_name = f"compensation_bed_{round(self._get_nominal_bed_temp())}C" + profile, is_unique = self._get_unique_profile_name(base_name) + if not is_unique: + self.ratos.console_echo("Create beacon compensation mesh", "info", + f"The default automatic profile name '{base_name}' already exists. The unique name '{profile}' will be used instead.") + gcmd.respond_info(f"Using automatic profile name '{profile}' for the new compensation mesh") + self.create_compensation_mesh(gcmd, profile, probe_count, chamber_temp) desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." @@ -424,9 +547,9 @@ def _validate_extended_parameters(self, ##### # Beacon Scan Compensation ##### - def apply_scan_compensation(self, compensation_zmesh) -> bool: - if not compensation_zmesh: - raise TypeError("Argument compensation_zmesh cannot be None") + def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: + if not comp_mesh_profile_name: + raise TypeError("Argument comp_mesh_profile_name must be provided") error_title = "Apply scan compensation error" try: @@ -439,6 +562,7 @@ def apply_scan_compensation(self, compensation_zmesh) -> bool: measured_mesh_params = measured_zmesh.get_mesh_params() measured_mesh_name = measured_zmesh.get_profile_name() + measured_mesh_bed_temp = measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER] if not self._validate_extended_parameters( measured_mesh_params, @@ -448,6 +572,10 @@ def apply_scan_compensation(self, compensation_zmesh) -> bool: allowed_probe_methods=(RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC)): return False + if comp_mesh_profile_name.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: + comp_mesh_profile_name = self.auto_select_compensation_mesh(measured_mesh_bed_temp) + + compensation_zmesh = self._create_zmesh_from_profile(comp_mesh_profile_name, purpose="Beacon scan compensation") compensation_mesh_params = compensation_zmesh.get_mesh_params() compensation_mesh_name = compensation_zmesh.get_profile_name() @@ -455,7 +583,7 @@ def apply_scan_compensation(self, compensation_zmesh) -> bool: compensation_mesh_params, "Apply scan compensation", f"Specified compensation mesh '{compensation_mesh_name}'", - compare_bed_temp=measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER], + compare_bed_temp=measured_mesh_bed_temp, allowed_kinds=(RATOS_MESH_KIND_COMPENSATION,)): return False diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 34eff47dd..826e1086c 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -54,7 +54,7 @@ log_points: False [gcode_macro RatOS] variable_beacon_bed_mesh_scv: 25 # square corner velocity for bed meshing with proximity method variable_beacon_contact_z_homing: False # Make all G28 calls use contact instead of proximity scan. This is not recommended - # on textured surfaces due to significant variation in contact measurements. + # on textured surfaces due to potential significant variation in contact measurements. variable_beacon_contact_start_print_true_zero: True # Use contact to determine true Z=0 for the last homing move during START_PRINT variable_beacon_contact_start_print_true_zero_fuzzy_position: True # Use a fuzzy (randomized) position for the true zero contact measurement so that @@ -73,16 +73,17 @@ variable_beacon_contact_prime_probing: True # probe for priming with variable_beacon_contact_expansion_compensation: True # enables hotend thermal expansion compensation variable_beacon_contact_bed_mesh: False # Bed mesh with contact method. This is not recommended on textured surfaces due - # to significant variation in contact measurements. + # to potential significant variation in contact measurements. variable_beacon_contact_bed_mesh_samples: 2 # probe samples for contact bed mesh variable_beacon_contact_z_tilt_adjust: False # z-tilt adjust with contact method. This is not recommended on textured surfaces - # due to significant variation in contact measurements. + # due to potential significant variation in contact measurements. variable_beacon_contact_z_tilt_adjust_samples: 2 # probe samples for contact z-tilt adjust variable_beacon_scan_compensation_enable: False # Enables beacon scan compensation -variable_beacon_scan_compensation_profile: "Beacon Scan Compensation" - # The bed mesh profile name for the scan compensation mesh +variable_beacon_scan_compensation_profile: "auto" + # The bed mesh profile name identifying the scan compensation mesh to use, or + # "auto" to automatically select the most appropriate profile based on bed temperature. variable_beacon_scan_compensation_resolution: 8 # The mesh resolution in mm for compensation mesh creation. It is strongly recommended # to leave this at the default value of 8mm. Compensation mesh creation uses a # special filtering algorithm to reduce noise in contact measurements which @@ -875,7 +876,7 @@ gcode: # parameters {% set bed_temp = params.BED_TEMP|default(85)|int %} {% set chamber_temp = params.CHAMBER_TEMP|default(0)|int %} - {% set profile = params.PROFILE|default("Beacon Scan Compensation")|string %} + {% set profile = params.PROFILE|default("auto")|string %} {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} {% set keep_temp_meshes = params.KEEP_TEMP_MESHES|default(0) %} @@ -1069,7 +1070,7 @@ gcode: {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} {% set bed_temp_mismatch_is_error = true if printer["gcode_macro RatOS"].beacon_scan_compensation_bed_temp_mismatch_is_error|default(false)|lower == 'true' else false %} - {% set bed_temp = params.BED_TEMP|default(-1, true)|float %} + {% set bed_temp = params.BED_TEMP|default(0)|float %} {% if bed_temp == 0 %} {% set bed_temp = printer.heater_bed.temperature %} {% endif %} From b8783f510d52b9759b859a0a75870c20df5fc7ba Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 13 Jul 2025 23:48:18 +0100 Subject: [PATCH 098/139] feat(beacon-heatsoak): add ThresholdPredictor --- .../klippy/beacon_adaptive_heat_soak.py | 139 +- ...acon_adaptive_heat_soak_model_training.csv | 1201 +++++++++++++++++ 2 files changed, 1339 insertions(+), 1 deletion(-) create mode 100644 configuration/klippy/beacon_adaptive_heat_soak_model_training.csv diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index d0c5de7f4..e04725de2 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -4,9 +4,129 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import re, time, logging +import re, time, logging, os, multiprocessing, traceback, pygam import numpy as np +class ThresholdPredictor: + def __init__(self, printer): + self.printer = printer + self.reactor = printer.get_reactor() + self._gam = None + + def predict_threshold(self, maximum_z_change_microns, period_seconds): + ''' + Given the specified maximum amount of Z change that should be allowed during the specified + period after the soak completes, predict the threshold value that should be used for the soak. + + The period is typically closely related to the first layer duration. The maximum Z change + is typically associated with the amount of oversquish that is acceptable during the first layer. + + Parameters: + maximum_z_change_microns: The maximum Z change allowed during the period after the + soak completes, in microns. + period_seconds: The time period in seconds after the soak completes, in seconds. + Returns: + The predicted adaptive heat soak threshold in nanometers per second. + ''' + + # Note: The implementation assumes that the method is called infrequently, and speed is not critical. + # Computation is performed in a separate process, and the implementation is reactor-friendly. + # Resources are released between calls to this method. At the time of writing, typical prediction + # time is under 1s on a Raspberry Pi 4B, which is acceptable for the use case. + + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_predict_threshold(maximum_z_change_microns, period_seconds)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise self.printer.command_error("Error predicting adaptive heat soak threshold: %s" % (result,)) + else: + return result + + def _do_predict_threshold(self, z, p): + gam = self._get_model() + X = np.array([[p, z, z / p, 1.0 / p]]) + prediction = gam.predict(X) + if prediction.size == 0: + raise LookupError("Prediction failed, no data available in the model.") + t = float(prediction[0]) + + # Ensure a minimum threshold of 10.0. From experimental data, we observe that thresholds + # below this number approach the noise floor of the system and are not useful. + t = max(t, 10.0) + return t + + def _load_training_data(self): + path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'beacon_adaptive_heat_soak_model_training.csv') + + if not os.path.exists(path): + raise FileNotFoundError(f"Beacon adaptive heat soak model training data file not found: {path}") + + try: + data = np.genfromtxt(path, delimiter=',', names=True) + except Exception as e: + raise Exception(f"Failed to load model training data: {e}") from e + + return data + + def _get_model(self): + if self._gam is not None: + return self._gam + + # We train the model on demand rather the relying on a cached pickled model file. + # This approach is somewhat inefficient but adequate for the current use case, and avoids + # the challenges of robust and reliable pickling and unpickling the model as regards + # package updates and changes to the model. + + data = self._load_training_data() + + Xp = data['period'] # Period + Xz = data['max_z_change'] # Max Z Change + y = data['threshold'] # Threshold + + # Add additional columns to X to support additional smoothing terms in the GAM + X = np.column_stack([ + Xp, # Period + Xz, # Max Z Change + Xz / Xp, # rate + 1.0 / Xp, # inverse period + ]) + + gam = pygam.LinearGAM( + pygam.s(0, n_splines=20) + + pygam.s(1, n_splines=20) + + pygam.te(0, 1, n_splines=[10,10]) + + pygam.s(2, n_splines=20) # smooth on z/p + + pygam.s(3, n_splines=20), # smooth on 1/p + tol=1e-6, + lam=0.6, + spline_order=3, + fit_intercept=True) + + gam.fit(X, y) + + self._gam = gam + return gam + class BeaconZRateSession: def __init__(self, config, beacon, samples_per_mean=1000, window_size=30, window_step=1): self.config = config @@ -136,6 +256,11 @@ def __init__(self, config): self.cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES, desc=self.desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES) + self.gcode.register_command( + '_TEST_PREDICT_ADAPTIVE_HEAT_SOAK_THRESHOLD', + self.cmd_TEST_PREDICT_ADAPTIVE_HEAT_SOAK_THRESHOLD, + desc=self.desc_TEST_PREDICT_ADAPTIVE_HEAT_SOAK_THRESHOLD) + self.printer.register_event_handler("klippy:connect", self._handle_connect) @@ -381,6 +506,18 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): gcmd.respond_info(f'Diagnostic data captured to {fullpath}') + desc_TEST_PREDICT_ADAPTIVE_HEAT_SOAK_THRESHOLD = "For developer use only. Specify Z (maximum z change in microns) and P (period in seconds)." + def cmd_TEST_PREDICT_ADAPTIVE_HEAT_SOAK_THRESHOLD(self, gcmd): + maximum_z_change_microns = gcmd.get_int('Z', 100, minval=1) + period_seconds = gcmd.get_int('P', 300, minval=60) + + start_time = self.reactor.monotonic() + predictor = ThresholdPredictor(self.printer) + threshold = predictor.predict_threshold(maximum_z_change_microns, period_seconds) + end_time = self.reactor.monotonic() + + gcmd.respond_info(f"Predicted adaptive heat soak threshold for maximum Z change of {maximum_z_change_microns} microns over {period_seconds} seconds: {threshold:.2f} nm/s (prediction took {1000. * (end_time - start_time):.1f} ms)") + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): if self.beacon is None: diff --git a/configuration/klippy/beacon_adaptive_heat_soak_model_training.csv b/configuration/klippy/beacon_adaptive_heat_soak_model_training.csv new file mode 100644 index 000000000..c069be203 --- /dev/null +++ b/configuration/klippy/beacon_adaptive_heat_soak_model_training.csv @@ -0,0 +1,1201 @@ +period,max_z_change,threshold +30,29.68986234789574,1200 +90,83.98485468356446,1200 +151,133.8994937983367,1200 +211,180.50057898965142,1200 +272,224.24705950781257,1200 +332,264.30486736032765,1200 +393,303.8394435206859,1200 +453,339.2534193409476,1200 +514,370.7032184500849,1200 +574,400.15612688047656,1200 +635,429.74598355812964,1200 +695,456.229737765516,1200 +756,482.5520011355618,1200 +816,505.54721922290787,1200 +877,528.4189488606916,1200 +937,549.6405621242246,1200 +998,568.8448450115399,1200 +1058,587.6920234628377,1200 +1119,605.6001540936245,1200 +1179,621.1793807691753,1200 +1240,638.1007972154467,1200 +1300,652.7146862833386,1200 +1361,667.3037074157221,1200 +1421,680.7898169375137,1200 +1482,693.5792844614987,1200 +1542,705.6022224446124,1200 +1603,716.7983059722858,1200 +1663,727.1002408755479,1200 +1724,737.0819105767673,1200 +1784,746.8978865784794,1200 +1845,756.1408650805275,1200 +1905,764.9471517807389,1200 +1966,773.2280961145226,1200 +2026,781.4917994473153,1200 +2087,788.7291366499658,1200 +2147,795.599563588032,1200 +2208,801.5398971591233,1200 +2268,807.3430831199146,1200 +2329,812.7283092843254,1200 +2389,818.27140138651,1200 +2450,823.467069975492,1200 +2510,828.4188515057858,1200 +2571,833.338379461836,1200 +2631,838.0656294949915,1200 +2692,842.2972899037852,1200 +2752,846.5899304612185,1200 +2813,850.3965485372789,1200 +2873,854.5385817760089,1200 +2934,858.4676582704519,1200 +2994,861.804723854261,1200 +3055,865.3700654105792,1200 +3115,868.7332971454994,1200 +3176,872.516397077685,1200 +3236,875.4419616151931,1200 +3297,878.4933673751677,1200 +3357,881.8226645259465,1200 +3418,884.0827502554293,1200 +3478,886.6350706587107,1200 +3539,889.7590724293325,1200 +3600,892.4204270070218,1200 +30,24.350258282243942,1000 +90,70.35021695806108,1000 +151,112.39901127385076,1000 +211,151.14466950463725,1000 +272,186.8966842823579,1000 +332,220.16331535623925,1000 +393,252.9236430972934,1000 +453,281.9791875992966,1000 +514,310.67148318978224,1000 +574,336.52945640640763,1000 +635,361.8996195397038,1000 +695,384.3047447103135,1000 +756,406.94676009862036,1000 +816,426.882564354149,1000 +877,445.75264058414024,1000 +937,464.3058099135371,1000 +998,481.77666751458537,1000 +1058,497.1687151367658,1000 +1119,513.3033671586138,1000 +1179,527.587788063222,1000 +1240,542.1798567232358,1000 +1300,555.2029141612204,1000 +1361,567.602206998655,1000 +1421,578.8927814552201,1000 +1482,589.9659016346363,1000 +1542,600.470517826562,1000 +1603,610.3740219716849,1000 +1663,619.5565541923097,1000 +1724,628.6717951630033,1000 +1784,636.9553921856452,1000 +1845,645.4758964201546,1000 +1905,653.4335896841428,1000 +1966,660.6093649725256,1000 +2026,666.5889946392297,1000 +2087,672.7467287559039,1000 +2147,678.2168228308278,1000 +2208,683.7889989190787,1000 +2268,689.1397349785032,1000 +2329,694.4366191228964,1000 +2389,699.0655168365502,1000 +2450,704.0182654829057,1000 +2510,708.599965055035,1000 +2571,712.7993176391647,1000 +2631,716.5412372029627,1000 +2692,720.7534904975557,1000 +2752,724.6578063391846,1000 +2813,728.6616700395693,1000 +2873,731.8979547504268,1000 +2934,735.6352600715131,1000 +2994,738.7874522591369,1000 +3055,742.2967614446784,1000 +3115,744.994183028178,1000 +3176,748.0493553154922,1000 +3236,751.1409537003385,1000 +3297,754.1358394719418,1000 +3357,756.524616378862,1000 +3418,759.2706020707074,1000 +3478,762.0294666379712,1000 +3539,764.0532845945154,1000 +3600,766.4397030840441,1000 +30,23.161860547849926,900 +90,64.65353193869845,900 +151,102.19975603768796,900 +211,136.7584701536756,900 +272,170.7891135107934,900 +332,202.62516041084996,900 +393,231.91473993974375,900 +453,259.44855861443364,900 +514,285.96988009558015,900 +574,309.9241935549469,900 +635,333.09117728144156,900 +695,354.6990490416754,900 +756,374.8052001328657,900 +816,393.567376332688,900 +877,411.6007135164342,900 +937,428.1862142416071,900 +998,444.3776169920858,900 +1058,459.8806572845175,900 +1119,474.2950740882296,900 +1179,488.50113214537066,900 +1240,501.5117506205255,900 +1300,513.728611981376,900 +1361,525.1122381745772,900 +1421,535.545584792864,900 +1482,546.2270878576098,900 +1542,556.1401351632558,900 +1603,565.2129462268109,900 +1663,574.1357164893357,900 +1724,582.5578242428819,900 +1784,590.5935478144394,900 +1845,598.8465654523242,900 +1905,605.6280853352276,900 +1966,611.2643663963717,900 +2026,617.5417088616313,900 +2087,622.9740393255225,900 +2147,628.4912028414084,900 +2208,633.9969358590748,900 +2268,639.0530342038263,900 +2329,643.4763980860661,900 +2389,648.5724510457657,900 +2450,653.0914694714407,900 +2510,657.108389470006,900 +2571,661.1640294187654,900 +2631,665.4023828843045,900 +2692,669.0788978962788,900 +2752,672.983798057894,900 +2813,676.5285483754577,900 +2873,679.8108418009732,900 +2934,683.1673664838089,900 +2994,686.4748254586699,900 +3055,689.2919472922405,900 +3115,692.5213897444486,900 +3176,695.4459967812694,900 +3236,698.2597830460185,900 +3297,700.6990363830354,900 +3357,703.4620846995566,900 +3418,706.1816570590835,900 +3478,707.9686109425053,900 +3539,710.2067799564505,900 +3600,712.44312700945,900 +30,19.743079617215983,800 +90,56.921695190165906,800 +151,92.18504305396868,800 +211,124.93537006582835,800 +272,155.46273708353908,800 +332,184.0069984455555,800 +393,211.3181311059867,800 +453,237.2063507459958,800 +514,260.496199336308,800 +574,282.96648345506594,800 +635,304.39114220698764,800 +695,323.26088403277026,800 +756,342.30432147607587,800 +816,359.9996437579973,800 +877,375.80798984490116,800 +937,392.4135543162953,800 +998,407.2051683618158,800 +1058,421.5746604972802,800 +1119,435.21759114166866,800 +1179,447.7543352997294,800 +1240,459.7911438438989,800 +1300,470.96342340500144,800 +1361,481.42309904037006,800 +1421,491.216330196827,800 +1482,501.11574363521925,800 +1542,510.19551893448,800 +1603,519.144214160987,800 +1663,527.2651580066154,800 +1724,535.7204733722582,800 +1784,542.712666180671,800 +1845,549.6996593731999,800 +1905,555.5809403372825,800 +1966,561.43509005479,800 +2026,566.7068380963394,800 +2087,572.3720047783488,800 +2147,577.4454619497963,800 +2208,582.505582709226,800 +2268,587.2086299484374,800 +2329,592.0778741193951,800 +2389,596.2232378277988,800 +2450,600.5262042931117,800 +2510,604.3074030114515,800 +2571,608.4880089740705,800 +2631,612.387716486727,800 +2692,615.6983078405365,800 +2752,619.238091177501,800 +2813,622.6681612084947,800 +2873,626.4506350160102,800 +2934,629.3688227587193,800 +2994,632.3168144337726,800 +3055,635.7436428242295,800 +3115,637.9448627676179,800 +3176,640.5495328558579,800 +3236,643.6642280729004,800 +3297,646.3305305101048,800 +3357,648.4928831390735,800 +3418,650.6088382642258,800 +3478,652.2486665370254,800 +3539,654.9438054847983,800 +3600,657.5158024794368,800 +30,15.96384883107828,600 +90,43.70347523064379,600 +151,70.66461010158935,600 +211,96.50949280807595,600 +272,120.27831998502711,600 +332,142.3764405332057,600 +393,163.91668890040262,600 +453,183.11375756789528,600 +514,201.92326002972868,600 +574,221.66964956187542,600 +635,235.75779696039478,600 +695,252.05828888796088,600 +756,266.90564438744195,600 +816,281.29664207279154,600 +877,295.1714032646148,600 +937,307.5367761340128,600 +998,319.76311243468047,600 +1058,330.75579764479994,600 +1119,341.23539597764,600 +1179,351.60578203066996,600 +1240,360.99707047874074,600 +1300,370.284861765796,600 +1361,379.39528710336333,600 +1421,387.5167957967367,600 +1482,395.7966563544692,600 +1542,403.00386549733116,600 +1603,410.004420197105,600 +1663,416.6723803280164,600 +1724,422.7606896863007,600 +1784,428.2800637175203,600 +1845,433.41255515119417,600 +1905,438.8630595856346,600 +1966,444.0188866523031,600 +2026,448.69220859350105,600 +2087,453.414380457348,600 +2147,458.0083024729561,600 +2208,461.9547754783746,600 +2268,465.7900591378725,600 +2329,470.1313921291321,600 +2389,473.7953475429756,600 +2450,477.3878295350239,600 +2510,480.385649465092,600 +2571,484.37568102377213,600 +2631,487.51662817391434,600 +2692,490.3296114530476,600 +2752,493.17639760717043,600 +2813,495.86928295690427,600 +2873,498.0545459189425,600 +2934,500.6321591438418,600 +2994,503.71656410184664,600 +3055,506.3779186795359,600 +3115,508.6095045084005,600 +3176,510.6828235843684,600 +3236,512.7562907644722,600 +3297,515.0136622116411,600 +3357,517.5590525411369,600 +3418,520.0175455775693,600 +3478,521.8781692001073,600 +3539,523.9191254048176,600 +3600,525.2634452829586,600 +30,14.304983333593555,550 +90,42.60063735570998,550 +151,66.41391888414694,550 +211,90.36944571913159,550 +272,112.27429849670307,550 +332,131.8129704244203,550 +393,151.92664319816572,550 +453,169.74092239520928,550 +514,192.79237492993389,550 +574,205.3395837154751,550 +635,219.89179711998588,550 +695,234.30758199165427,550 +756,248.72233719299504,550 +816,261.930266946536,550 +877,274.5903209543734,550 +937,286.19615150101265,550 +998,297.1036913796876,550 +1058,307.7228936584693,550 +1119,318.2281434067514,550 +1179,327.5454942268075,550 +1240,337.01273299066486,550 +1300,345.84618258108844,550 +1361,353.9386004576785,550 +1421,362.0341889120963,550 +1482,369.39280215393956,550 +1542,376.0859880592333,550 +1603,382.6911747210279,550 +1663,388.55005215348024,550 +1724,394.070835531394,550 +1784,399.31531180716684,550 +1845,404.634515795824,550 +1905,409.68981521749276,550 +1966,414.2943547627775,550 +2026,419.2059010072293,550 +2087,423.3734643770041,550 +2147,427.7583676960254,550 +2208,431.164653187249,550 +2268,435.6317462652959,550 +2329,439.42962425394296,550 +2389,443.22254919088607,550 +2450,446.0559877028828,550 +2510,449.6783756120466,550 +2571,452.80204730938124,550 +2631,455.6267101736938,550 +2692,458.5302287229539,550 +2752,461.05596581776433,550 +2813,463.38594833720947,550 +2873,465.8011671532628,550 +2934,468.05715701069437,550 +2994,470.519286664981,550 +3055,473.371194446839,550 +3115,475.62083197193294,550 +3176,478.0494806758892,550 +3236,480.0287992272364,550 +3297,481.94524223421365,550 +3357,483.64981572155386,550 +3418,486.12816980961463,550 +3478,487.7966338185221,550 +3539,489.84655326890436,550 +3600,491.39290110045033,550 +30,13.276963663639208,500 +90,37.15723175739379,500 +151,59.90395020177357,500 +211,81.34501642261353,500 +272,101.81339772436286,500 +332,120.38455341925771,500 +393,138.34999864829206,500 +453,157.04204761971857,500 +514,172.47887008239343,500 +574,186.65012735838775,500 +635,201.33698670566764,500 +695,215.4389754500561,500 +756,227.89683320085385,500 +816,239.8223329178138,500 +877,251.2347767871505,500 +937,262.3405041852744,500 +998,272.73849145261966,500 +1058,282.38710173153913,500 +1119,292.03010294270496,500 +1179,301.3580202014489,500 +1240,309.79653415558266,500 +1300,317.82823420633144,500 +1361,325.52577092233173,500 +1421,332.2846701809817,500 +1482,339.031680143622,500 +1542,345.5207891366582,500 +1603,351.0457088756285,500 +1663,356.14758160806787,500 +1724,361.82709975113414,500 +1784,366.9317028254069,500 +1845,371.7641455854251,500 +1905,376.4419960743621,500 +1966,381.21310665838246,500 +2026,385.13665502526965,500 +2087,389.66370997199306,500 +2147,393.27280278223816,500 +2208,397.1508267542297,500 +2268,400.57968345434165,500 +2329,404.06447849821484,500 +2389,407.4849375610263,500 +2450,411.255965645046,500 +2510,413.9038695302288,500 +2571,416.80337636281547,500 +2631,419.68000541757146,500 +2692,421.9359912993789,500 +2752,424.3920116104532,500 +2813,426.47697794451824,500 +2873,428.76737612242437,500 +2934,431.67520417090736,500 +2994,434.0733420577344,500 +3055,436.6781992010235,500 +3115,438.4768051150845,500 +3176,440.55263756008094,500 +3236,442.52265090870173,500 +3297,444.58228011762367,500 +3357,446.6833050336779,500 +3418,448.95558656010485,500 +3478,450.0948713643704,500 +3539,451.8199611282562,500 +3600,453.69389568580004,500 +30,13.290987977724171,450 +90,34.861314289811844,450 +151,54.811094286775074,450 +211,74.40283433809782,450 +272,92.83979231900116,450 +332,115.27082355785797,450 +393,128.23290998536095,450 +453,141.40967709079155,450 +514,155.6375797922043,450 +574,170.00071308663792,450 +635,182.92796615222528,450 +695,195.20240858644604,450 +756,207.2402609075383,450 +816,217.91866428315245,450 +877,228.74981226114107,450 +937,238.70144382076876,450 +998,248.43284444457674,450 +1058,257.43651558269016,450 +1119,266.44914671101276,450 +1179,274.3730817890322,450 +1240,282.4002447756176,450 +1300,289.6444929398439,450 +1361,296.2680156365967,450 +1421,302.78104513168637,450 +1482,308.7545945200934,450 +1542,313.9470445501455,450 +1603,319.63412725301737,450 +1663,324.64503910045187,450 +1724,329.7356873223074,450 +1784,334.2562182818083,450 +1845,339.19722569081364,450 +1905,343.17353676005746,450 +1966,347.8321034539849,450 +2026,351.10754878491684,450 +2087,355.599412231463,450 +2147,359.1953228404011,450 +2208,363.1085937402004,450 +2268,365.9904772862494,450 +2329,369.5387511299971,450 +2389,372.47761374200195,450 +2450,375.36002706209274,450 +2510,378.35646258148336,450 +2571,380.9341893975011,450 +2631,383.03751887436397,450 +2692,385.3475152186811,450 +2752,387.69433142719686,450 +2813,390.1239902507714,450 +2873,392.97082551482345,450 +2934,395.4223799819131,450 +2994,397.64720460247713,450 +3055,399.65114817777226,450 +3115,401.82221078773614,450 +3176,403.34465772423596,450 +3236,405.6484262466993,450 +3297,407.57141242465354,450 +3357,409.3281834656941,450 +3418,410.98994747693473,450 +3478,412.3699822977012,450 +3539,414.31368778263277,450 +3600,415.9456311581689,450 +30,12.20447275631659,400 +90,29.734252596062788,400 +151,47.97012937816942,400 +211,68.43345581459562,400 +272,82.69918956406923,400 +332,97.83760909460159,400 +393,112.4558255439432,400 +453,126.58995334289182,400 +514,139.95111006914112,400 +574,152.34673430496434,400 +635,164.24277338907984,400 +695,174.92317695090094,400 +756,185.70767126794726,400 +816,195.93152914736902,400 +877,205.45422819968007,400 +937,214.72308071167515,400 +998,223.71659233342814,400 +1058,231.65006834782025,400 +1119,239.8100750611834,400 +1179,247.06502986794987,400 +1240,253.80814654781807,400 +1300,260.3048549225473,400 +1361,266.2821892111391,400 +1421,271.6260676184396,400 +1482,277.03539127230704,400 +1542,282.2299293403182,400 +1603,287.340417958706,400 +1663,291.86377050310955,400 +1724,296.85699462391096,400 +1784,300.89632868905653,400 +1845,305.4432833259999,400 +1905,308.72327938807564,400 +1966,313.24427442753097,400 +2026,316.9596729056469,400 +2087,320.8644387786352,400 +2147,323.6179304351805,400 +2208,327.2459189614782,400 +2268,330.25749662468195,400 +2329,333.1736399570908,400 +2389,336.0623732730004,400 +2450,338.6679094996231,400 +2510,340.89234309549136,400 +2571,343.2933096001801,400 +2631,345.5428643616548,400 +2692,348.0215262757955,400 +2752,350.8441796170704,400 +2813,353.18470254263445,400 +2873,355.5279458991116,400 +2934,357.5442619061656,400 +2994,359.49019930951965,400 +3055,361.2010754751028,400 +3115,363.58316665476275,400 +3176,365.34010624941527,400 +3236,367.28907594988857,400 +3297,368.8569238271658,400 +3357,370.237139892606,400 +3418,372.24595292038805,400 +3478,373.9645327882773,400 +3539,374.9811778519646,400 +3600,376.4706414378828,400 +30,9.218173635480639,350 +90,30.978916433793984,350 +151,45.385509536235304,350 +211,58.68859013687893,350 +272,72.87872621760175,350 +332,87.24180954250176,350 +393,100.23367424391529,350 +453,112.1873337743433,350 +514,124.30957423791278,350 +574,134.89835146373446,350 +635,145.54494873678425,350 +695,155.53475876949437,350 +756,165.2451413008489,350 +816,174.17329884913556,350 +877,183.1360806521302,350 +937,191.2542398303592,350 +998,199.0878526199965,350 +1058,206.0758440502035,350 +1119,212.7227605882756,350 +1179,219.23311525156612,350 +1240,225.20981882091723,350 +1300,230.19222432678316,350 +1361,236.00815967851713,350 +1421,241.0002102511478,350 +1482,245.93202869709592,350 +1542,250.66725387189638,350 +1603,255.38396841210886,350 +1663,259.4669781031347,350 +1724,264.1659687921739,350 +1784,267.48817843066956,350 +1845,271.7873591110157,350 +1905,275.26580881827465,350 +1966,279.0755275179903,350 +2026,282.0909589776039,350 +2087,285.8615089908609,350 +2147,288.53527564026774,350 +2208,291.48929918584315,350 +2268,294.534040451896,350 +2329,297.02047426853017,350 +2389,299.3819402126344,350 +2450,301.86984997204627,350 +2510,304.58797817246636,350 +2571,307.437896034809,350 +2631,309.4224106861434,350 +2692,311.77226621412296,350 +2752,313.61875508157107,350 +2813,316.35717098625605,350 +2873,318.7759699304213,350 +2934,320.89705904688117,350 +2994,322.5287835293095,350 +3055,324.9470770553405,350 +3115,325.87569199923564,350 +3176,328.2140530961651,350 +3236,329.8760476922155,350 +3297,331.4920739288457,350 +3357,333.0165622169893,350 +3418,334.33090187455764,350 +3478,336.00423363130676,350 +3539,337.558621952523,350 +3600,338.5719624929186,350 +30,9.725566346055075,300 +90,23.795996171391835,300 +151,37.87973122917231,300 +211,51.754462533256515,300 +272,64.5549596037904,300 +332,76.6409512284331,300 +393,87.84866252513052,300 +453,98.27194856557026,300 +514,108.63996316975044,300 +574,118.47769628045285,300 +635,127.75285965138835,300 +695,136.69541123676674,300 +756,145.01063557286864,300 +816,152.99427302461152,300 +877,160.91509935372824,300 +937,167.55804087960064,300 +998,174.28160637534677,300 +1058,180.43521306552418,300 +1119,186.0066243277389,300 +1179,190.99547563207022,300 +1240,196.63160740338913,300 +1300,201.75793285135944,300 +1361,206.62936173749722,300 +1421,211.21699151135647,300 +1482,216.02206097595456,300 +1542,219.7892795448912,300 +1603,224.02371563539657,300 +1663,227.89479856102173,300 +1724,231.68261595842966,300 +1784,235.2472221906704,300 +1845,238.4572305502337,300 +1905,242.20497718522768,300 +1966,245.65547112869478,300 +2026,248.38033835682506,300 +2087,251.31015389197466,300 +2147,254.221336372216,300 +2208,256.7664285127071,300 +2268,259.3536874260699,300 +2329,262.17177641565377,300 +2389,264.77673551244084,300 +2450,267.3854863666925,300 +2510,269.22616214235836,300 +2571,271.1291908689602,300 +2631,273.6627553903477,300 +2692,276.2665859383492,300 +2752,278.6834496884758,300 +2813,280.63133259757615,300 +2873,282.64248727216113,300 +2934,283.98252746458206,300 +2994,285.9949731581048,300 +3055,287.645159988386,300 +3115,289.4410491809663,300 +3176,291.29827329829595,300 +3236,292.9727659036655,300 +3297,294.31155502832735,300 +3357,295.60309146977806,300 +3418,296.878284872716,300 +3478,298.4008151722053,300 +3539,299.83131154082366,300 +3600,301.6973125047725,300 +30,6.705389586991032,250 +90,18.956845534120134,250 +151,31.441507269706108,250 +211,42.796140121510575,250 +272,53.49528657625115,250 +332,63.01601392475152,250 +393,72.4752180894543,250 +453,81.6324686777283,250 +514,90.16783429865313,250 +574,98.04642603062848,250 +635,105.92184591637033,250 +695,113.22768761522877,250 +756,120.23999810332111,250 +816,127.49666470208115,250 +877,133.91870613919457,250 +937,139.56070232374577,250 +998,144.90257092009767,250 +1058,150.77264426112197,250 +1119,156.10282212555728,250 +1179,160.32395217803753,250 +1240,165.4257481844121,250 +1300,169.7702128392359,250 +1361,174.1895399526585,250 +1421,178.42767768939234,250 +1482,182.80303476148515,250 +1542,186.9038870846157,250 +1603,190.24946079224378,250 +1663,194.0123792567431,250 +1724,197.59129789215626,250 +1784,200.86214796228967,250 +1845,204.86803517454928,250 +1905,207.32814148987347,250 +1966,210.22104441185638,250 +2026,212.97906095118879,250 +2087,215.6449563929316,250 +2147,218.2225382293608,250 +2208,220.62478540068082,250 +2268,223.11622305879462,250 +2329,225.90998393853693,250 +2389,228.21187904944918,250 +2450,229.9874716532713,250 +2510,232.45074480379776,250 +2571,234.67849688281626,250 +2631,236.5704199923846,250 +2692,238.21807423569044,250 +2752,240.4347392369932,250 +2813,242.34556538223774,250 +2873,243.79199027630057,250 +2934,245.3387086809048,250 +2994,246.93943424149666,250 +3055,248.6164191451005,250 +3115,249.8404905085324,250 +3176,250.8156249841188,250 +3236,251.87935646478468,250 +3297,253.6129217381001,250 +3357,254.75279311287613,250 +3418,255.37204785483618,250 +3478,256.01892079510026,250 +3539,257.4689560365623,250 +3600,258.0992862815631,250 +30,6.128197475691877,200 +90,15.777957493310623,200 +151,25.399825156144743,200 +211,34.43181993639223,200 +272,43.503893736762166,200 +332,51.495198423472175,200 +393,59.81639634451517,200 +453,67.06536398068306,200 +514,73.97052501103644,200 +574,80.55237780845539,200 +635,86.56528103162123,200 +695,92.11836846075812,200 +756,97.2985886147253,200 +816,102.65865676488829,200 +877,107.79711563735725,200 +937,112.3972858849346,200 +998,117.31879361343192,200 +1058,121.65708714904952,200 +1119,125.76964269746293,200 +1179,129.39809665005725,200 +1240,133.87181446032457,200 +1300,137.56984939691745,200 +1361,141.2798815998907,200 +1421,144.1254552354477,200 +1482,148.04911774577602,200 +1542,151.15180045730824,200 +1603,153.96666762924485,200 +1663,156.83147769200275,200 +1724,159.3031250842895,200 +1784,161.6857153842194,200 +1845,164.25180697819917,200 +1905,166.44191931641092,200 +1966,168.92102377297613,200 +2026,171.77594716457827,200 +2087,174.0012186579346,200 +2147,176.40564177765157,200 +2208,178.35290719830334,200 +2268,180.1622311731627,200 +2329,182.00222949055785,200 +2389,184.60841637319288,200 +2450,186.23446243131764,200 +2510,188.3928184394232,200 +2571,189.9185962809737,200 +2631,191.2267333190283,200 +2692,193.25986432111438,200 +2752,195.1348515439687,200 +2813,195.75094695755627,200 +2873,197.54775901497726,200 +2934,198.60824797512407,200 +2994,200.03093187155093,200 +3055,201.3756355499596,200 +3115,202.72556540297148,200 +3176,204.2891012752549,200 +3236,204.9161928924833,200 +3297,206.1710136794262,200 +3357,207.2939676318921,200 +3418,208.55805578302864,200 +3478,209.35355734209418,200 +3539,210.4666360443963,200 +3600,211.5402543282986,200 +30,5.861411838181482,150 +90,12.158159132410105,150 +151,18.950961243312918,150 +211,28.98058324684098,150 +272,34.72929733395944,150 +332,38.43795617004289,150 +393,43.46637993041298,150 +453,48.22928680441254,150 +514,52.454520347340974,150 +574,56.69291947871977,150 +635,61.79737864080187,150 +695,66.19347556348555,150 +756,70.24020846508586,150 +816,74.2691353953893,150 +877,78.59981542523553,150 +937,82.17614100105982,150 +998,86.03409651245886,150 +1058,89.6064607244922,150 +1119,92.86630750246616,150 +1179,96.24287357036872,150 +1240,99.54766208308854,150 +1300,102.39156618760501,150 +1361,105.71737116953898,150 +1421,108.45216386251525,150 +1482,111.19868510731374,150 +1542,113.73114956873394,150 +1603,116.48861528209648,150 +1663,119.16094428745953,150 +1724,120.93084843136273,150 +1784,123.02070232520362,150 +1845,125.39800979656104,150 +1905,128.03925500780826,150 +1966,130.54537687127834,150 +2026,132.43660393106404,150 +2087,134.45722562944104,150 +2147,135.91487651125078,150 +2208,137.73241865066814,150 +2268,139.41179322805158,150 +2329,141.15338493999377,150 +2389,143.04374074097007,150 +2450,144.67675264517823,150 +2510,145.9001131922256,150 +2571,147.3789982938871,150 +2631,148.84002016735678,150 +2692,150.13442712055564,150 +2752,151.79304683546445,150 +2813,153.37207276362471,150 +2873,154.30760540007225,150 +2934,155.3819575873348,150 +2994,156.39507101975107,150 +3055,157.41005215268922,150 +3115,158.8270452814196,150 +3176,159.2780314887965,150 +3236,160.9551245557003,150 +3297,161.86741536900365,150 +3357,163.0601288512304,150 +3418,164.08683053788843,150 +3478,164.52484951455256,150 +3539,165.84576713629542,150 +3600,166.47788395958275,150 +30,4.7029107254732025,100 +90,9.58380729819487,100 +151,13.015245040677428,100 +211,18.84908959777755,100 +272,24.18728430195023,100 +332,29.20724516417323,100 +393,34.56089803111564,100 +453,39.36375639179647,100 +514,44.07852882324238,100 +574,47.956684670992615,100 +635,51.616040930056954,100 +695,55.120463069524305,100 +756,58.36228682533357,100 +816,62.17783341775811,100 +877,65.24033429688649,100 +937,67.92396539624536,100 +998,70.18717677121907,100 +1058,72.3302439148419,100 +1119,74.78376545524333,100 +1179,77.46363614845376,100 +1240,79.0111572317341,100 +1300,81.10811709979356,100 +1361,82.96461403991503,100 +1421,84.65193857947935,100 +1482,86.16406131608102,100 +1542,88.67555540495391,100 +1603,91.09673918421436,100 +1663,93.0398031373445,100 +1724,95.07498853241304,100 +1784,96.3856424768619,100 +1845,98.39804262240432,100 +1905,100.0222193524412,100 +1966,101.82539691171337,100 +2026,103.69680267082117,100 +2087,105.36738908310485,100 +2147,106.64659147625002,100 +2208,108.03559273002998,100 +2268,109.3107861329679,100 +2329,110.8333164324572,100 +2389,112.26381280107557,100 +2450,114.06064875906418,100 +2510,114.917532952785,100 +2571,116.02639720122033,100 +2631,116.9956230653811,100 +2692,118.02807239805429,100 +2752,119.33482070334776,100 +2813,119.85422071406606,100 +2873,121.47341246935594,100 +2934,122.47416428734118,100 +2994,123.6575877892617,100 +3055,124.65854100017395,100 +3115,125.08425158519822,100 +3176,126.3784240660832,100 +3236,127.13495779612208,100 +3297,127.81375211021555,100 +3357,128.54863127897192,100 +3418,129.17111164376047,100 +3478,129.82403436294703,100 +3539,130.43781908931965,100 +3600,131.38924063197305,100 +30,3.266846331425768,80 +90,9.5100620176907,80 +151,11.462002399318408,80 +211,14.900882146666845,80 +272,18.86622959325109,80 +332,21.255382292433296,80 +393,25.070502429963653,80 +453,28.17838111166634,80 +514,31.75643641033639,80 +574,34.43544905598628,80 +635,37.43033961849869,80 +695,40.59834024188285,80 +756,43.58139304255633,80 +816,45.940610251272574,80 +877,48.68918864396551,80 +937,51.49744177157572,80 +998,53.52442393010756,80 +1058,55.841266910445256,80 +1119,57.63065367733884,80 +1179,60.47117189027222,80 +1240,62.919560784441046,80 +1300,64.95947558028115,80 +1361,66.63527603042962,80 +1421,68.99197032284928,80 +1482,69.96019072383456,80 +1542,72.25501700033692,80 +1603,73.9575548244843,80 +1663,75.55013767808532,80 +1724,77.08838185119703,80 +1784,78.3724313868488,80 +1845,80.03641635345934,80 +1905,81.63313958929803,80 +1966,82.63078868529988,80 +2026,84.58777092566038,80 +2087,85.71690407133838,80 +2147,87.32942510716566,80 +2208,88.00888102073145,80 +2268,89.23546639152232,80 +2329,90.30536173353153,80 +2389,91.6632821184462,80 +2450,92.17816229263008,80 +2510,93.72385301870258,80 +2571,94.66533162222231,80 +2631,95.80319754812285,80 +2692,96.96713975342618,80 +2752,97.46796264018894,80 +2813,98.63220154198711,80 +2873,99.14567844388944,80 +2934,100.20825907492645,80 +2994,100.68328276422835,80 +3055,101.78239158099018,80 +3115,101.94140353699413,80 +3176,103.06577717544405,80 +3236,103.84567135248267,80 +3297,104.19176140333389,80 +3357,105.25242444581204,80 +3418,105.87806501260638,80 +3478,106.69129519248418,80 +3539,107.56153254860851,80 +3600,108.2657951651538,80 +30,3.041262314070309,60 +90,6.852363983983537,60 +151,9.503819970819677,60 +211,13.097695082201426,60 +272,16.209108080883425,60 +332,18.176026584363626,60 +393,21.570589376084627,60 +453,24.96280390006973,60 +514,26.826191388523284,60 +574,29.551516252494707,60 +635,32.23644650213214,60 +695,34.7581467538904,60 +756,36.3571437111907,60 +816,37.69107416499719,60 +877,38.5592172318743,60 +937,40.46565519835326,60 +998,41.53040853226139,60 +1058,43.38069976049519,60 +1119,44.87011615889196,60 +1179,46.59966928641825,60 +1240,48.517332853374,60 +1300,50.147562592219856,60 +1361,51.46712165880285,60 +1421,52.82803857544354,60 +1482,54.131316315520735,60 +1542,55.60746383723426,60 +1603,57.0843429836284,60 +1663,58.84256301974847,60 +1724,59.738063135337825,60 +1784,60.82039022374033,60 +1845,61.816153247933926,60 +1905,62.81774015172073,60 +1966,64.15535088590059,60 +2026,64.65550662484611,60 +2087,66.29394265190876,60 +2147,67.2635342647859,60 +2208,68.47811797181453,60 +2268,69.45838444653486,60 +2329,69.90478176775105,60 +2389,71.19045766127056,60 +2450,71.95548797867491,60 +2510,72.6193211347952,60 +2571,73.36916146152475,60 +2631,73.9916418263133,60 +2692,74.64456454549986,60 +2752,75.25834927187248,60 +2813,76.20977081452588,60 +2873,76.88220245258844,60 +2934,77.714567512229,60 +2994,78.57569883314181,60 +3055,79.14342702749991,60 +3115,79.90343997248647,60 +3176,80.63419070553368,60 +3236,81.48669984495064,60 +3297,82.03417672763794,60 +3357,82.22508417459994,60 +3418,82.85066041317691,60 +3478,83.56016720483922,60 +3539,84.10446085480316,60 +3600,84.3788748251925,60 +30,2.4055119799249383,40 +90,3.4996337165048885,40 +151,5.609556464470188,40 +211,8.64118393343324,40 +272,9.786234685459704,40 +332,10.549870298072847,40 +393,12.282299790793331,40 +453,13.187913514741581,40 +514,15.517275770457672,40 +574,17.185679983326054,40 +635,18.237024203690794,40 +695,19.423002515774442,40 +756,20.064669978221332,40 +816,21.526597999831097,40 +877,22.485746121282432,40 +937,23.51424476857676,40 +998,23.740816015917176,40 +1058,23.740816015917176,40 +1119,24.54371973025127,40 +1179,25.485009338960253,40 +1240,26.117197931963688,40 +1300,26.952526169716805,40 +1361,27.58461979054914,40 +1421,28.387100268612357,40 +1482,29.56098546673411,40 +1542,29.863205685994444,40 +1603,30.692844006104792,40 +1663,30.801188832233095,40 +1724,31.26057974009126,40 +1784,32.222281273361546,40 +1845,32.94718049181898,40 +1905,33.31910018767803,40 +1966,33.79389065167095,40 +2026,34.20536281497425,40 +2087,34.94191879439711,40 +2147,35.141936325434244,40 +2208,35.48441564931653,40 +2268,35.644819168140884,40 +2329,36.34777224370765,40 +2389,36.66347614676209,40 +2450,37.09074113041436,40 +2510,37.52035402442084,40 +2571,37.896596829871726,40 +2631,38.091288711981065,40 +2692,38.382317075107835,40 +2752,38.382317075107835,40 +2813,39.79303794907287,40 +2873,40.69959520092823,40 +2934,41.42717822891791,40 +2994,41.73696884417279,40 +3055,42.78257556341657,40 +3115,43.43612501448433,40 +3176,43.818372106095126,40 +3236,44.82629062648505,40 +3297,45.80454303345459,40 +3357,46.318699878566804,40 +3418,46.318699878566804,40 +3478,46.63689584084682,40 +3539,47.42634086976602,40 +3600,48.36487257799524,40 +30,1.0553512436466121,20 +90,2.51145345323539,20 +151,4.305203731263646,20 +211,4.815930036369082,20 +272,6.8060443026899975,20 +332,7.666721689971155,20 +393,8.624748097200609,20 +453,9.794032517831852,20 +514,11.26041323477034,20 +574,11.479954011154291,20 +635,11.7875212495926,20 +695,11.84543879790931,20 +756,12.345332810151831,20 +816,13.047505727166367,20 +877,13.734666747058782,20 +937,13.734666747058782,20 +998,13.906300654817642,20 +1058,14.5563245890188,20 +1119,14.835987063706739,20 +1179,16.380166997737888,20 +1240,16.380166997737888,20 +1300,16.380166997737888,20 +1361,16.527528906619636,20 +1421,17.579501618689164,20 +1482,18.682391070220547,20 +1542,19.81343878312248,20 +1603,19.81343878312248,20 +1663,19.81343878312248,20 +1724,19.81343878312248,20 +1784,19.81343878312248,20 +1845,21.2021980494319,20 +1905,21.81888952652764,20 +1966,21.81888952652764,20 +2026,22.387137250735236,20 +2087,22.813240159542147,20 +2147,23.993383227691993,20 +2208,23.993383227691993,20 +2268,23.993383227691993,20 +2329,23.993383227691993,20 +2389,23.993383227691993,20 +2450,23.993383227691993,20 +2510,23.993383227691993,20 +2571,23.993383227691993,20 +2631,23.993383227691993,20 +2692,23.993383227691993,20 +2752,23.993383227691993,20 +2813,23.993383227691993,20 +2873,23.993383227691993,20 +2934,24.729372891218645,20 +2994,24.729372891218645,20 +3055,24.968946540556395,20 +3115,25.252836392864843,20 +3176,25.333262560701144,20 +3236,25.65600493948004,20 +3297,25.699351373748712,20 +3357,25.699351373748712,20 +3418,26.002595599218502,20 +3478,26.917561056730534,20 +3539,27.87887151366285,20 +3600,27.99796168089489,20 +30,3.3487651541018977,10 +90,4.587414390032791,10 +151,6.40420801042967,10 +211,6.56298526169445,10 +272,6.56298526169445,10 +332,9.199723120506235,10 +393,9.775951514975247,10 +453,9.844587992343747,10 +514,9.844587992343747,10 +574,9.844587992343747,10 +635,9.844587992343747,10 +695,9.844587992343747,10 +756,11.287539543539935,10 +816,14.245350453674291,10 +877,14.245350453674291,10 +937,14.245350453674291,10 +998,14.245350453674291,10 +1058,14.245350453674291,10 +1119,14.245350453674291,10 +1179,14.245350453674291,10 +1240,14.245350453674291,10 +1300,14.245350453674291,10 +1361,14.245350453674291,10 +1421,14.245350453674291,10 +1482,14.245350453674291,10 +1542,14.245350453674291,10 +1603,14.245350453674291,10 +1663,14.694894933486921,10 +1724,15.54501497763983,10 +1784,16.371824416017496,10 +1845,16.371824416017496,10 +1905,16.371824416017496,10 +1966,16.371824416017496,10 +2026,16.537093244545282,10 +2087,16.537093244545282,10 +2147,16.537093244545282,10 +2208,16.92072277672159,10 +2268,17.13460285695755,10 +2329,17.13460285695755,10 +2389,17.225773038345032,10 +2450,17.3134854676453,10 +2510,18.03931620723506,10 +2571,18.45110849955305,10 +2631,19.025429969852937,10 +2692,19.051356938267688,10 +2752,19.051356938267688,10 +2813,19.710410666679195,10 +2873,19.710410666679195,10 +2934,20.23982006091502,10 +2994,20.23982006091502,10 +3055,20.23982006091502,10 +3115,20.43086415786618,10 +3176,21.386494550650312,10 +3236,21.386494550650312,10 +3297,21.386494550650312,10 +3357,21.386494550650312,10 +3418,21.386494550650312,10 +3478,21.386494550650312,10 +3539,21.652128467775963,10 +3600,21.69106009464646,10 From dd8eb8d7c81a258a9feb591514ff51486f03ef9c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 14 Jul 2025 18:32:41 +0100 Subject: [PATCH 099/139] refactor(beacon-heatsoak): beacon adaptive heatsoak now uses layer quality and maximum first layer duration as the user-facing settings - But still use an explicit z rate threshold for calibration soaks --- .../klippy/beacon_adaptive_heat_soak.py | 211 +++++++++++------- configuration/macros.cfg | 8 +- configuration/z-probe/beacon.cfg | 44 ++-- 3 files changed, 166 insertions(+), 97 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index e04725de2..a5c9c386b 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -10,8 +10,7 @@ class ThresholdPredictor: def __init__(self, printer): self.printer = printer - self.reactor = printer.get_reactor() - self._gam = None + self.reactor = printer.get_reactor() def predict_threshold(self, maximum_z_change_microns, period_seconds): ''' @@ -62,6 +61,8 @@ def do(): def _do_predict_threshold(self, z, p): gam = self._get_model() + z = float(z) + p = float(p) X = np.array([[p, z, z / p, 1.0 / p]]) prediction = gam.predict(X) if prediction.size == 0: @@ -74,6 +75,11 @@ def _do_predict_threshold(self, z, p): return t def _load_training_data(self): + # The training data was derived from experimental data measured on multiple V-Core 4 machines. + # It predicts z rate thresholds that have been evaluated as suitable for V-Core 4 300, 400 and 500 printers + # with the stock aluminium extrusion and steel linear rail gantry, and also with limited evaluation + # for steel and titanium box-section tube gantries. + path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'beacon_adaptive_heat_soak_model_training.csv') @@ -89,9 +95,6 @@ def _load_training_data(self): return data def _get_model(self): - if self._gam is not None: - return self._gam - # We train the model on demand rather the relying on a cached pickled model file. # This approach is somewhat inefficient but adequate for the current use case, and avoids # the challenges of robust and reliable pickling and unpickling the model as regards @@ -123,8 +126,6 @@ def _get_model(self): fit_intercept=True) gam.fit(X, y) - - self._gam = gam return gam class BeaconZRateSession: @@ -221,12 +222,14 @@ def __init__(self, config): # Configuration values - # The default z-rate threshold in nm/s below which we consider the printer to be thermally stable. - self.def_threshold = config.getint('threshold', 15, minval=10) + # The default layer quality for adaptive heat soak, which is used in conjunction with maximum_first_layer_duration + # to determine the z rate threshold for thermal stability. The greater the quality value, the less oversquish is tolerated. + # 1 = rough, 2 = draft, 3 = normal, 4 = high, 5 = maximum + self.def_layer_quality = config.getint('layer_quality', 3, minval=1, maxval=5) - # The default number of continuous seconds with z-rate below the threshold before we consider the - # printer to be thermally stable. - self.def_hold_count = config.getint('hold_count', 150, minval=1) + # The default maximum first layer duration in seconds, which is used in conjunction with layer_quality to determine + # the z rate threshold for thermal stability. + self.def_maxiumm_first_layer_duration = config.getint('maximum_first_layer_duration', 1800, minval=60, maxval=7200) # The default maximum wait time in seconds for the printer to reach thermal stability. self.def_maximum_wait = config.getint('maximum_wait', 5400, minval=0) @@ -239,6 +242,7 @@ def __init__(self, config): # Setup self.reactor = None self.beacon = None + self.display_status = None # Register commands self.gcode.register_command( @@ -266,6 +270,7 @@ def __init__(self, config): def _handle_connect(self): self.reactor = self.printer.get_reactor() + self.display_status = self.printer.lookup_object('display_status') if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') @@ -354,6 +359,14 @@ def _check_trend_projection(self, moving_average_history, moving_average_history return abs(check_value) <= threshold + def _get_maximum_z_change_microns_for_quality(self, quality): + if quality < 1 or quality > 5: + raise ValueError(f"Invalid layer quality {quality}, must be between 1 and 5.") + + # Returns the maximum Z change in microns for the given layer quality. + # This is a fixed mapping based on empirical data and should not be changed. + return (150, 100, 50, 20, 10)[quality - 1] # Microns for layer quality 1-5 + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon is None: @@ -364,16 +377,34 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): self._prepare_for_sampling() - threshold = gcmd.get_int('THRESHOLD', self.def_threshold, minval=10) - target_hold_count = gcmd.get_int('HOLD_COUNT', self.def_hold_count, minval=1) + threshold = gcmd.get_int('_FORCE_THRESHOLD', None, minval=8) minimum_wait = gcmd.get_int('MINIMUM_WAIT', self.def_minimum_wait, minval=0) maximum_wait = gcmd.get_int('MAXIMUM_WAIT', self.def_maximum_wait, minval=0) + layer_quality = gcmd.get_int('LAYER_QUALITY', self.def_layer_quality, minval=1, maxval=5) + maximum_first_layer_duration = gcmd.get_int('MAXIMUM_FIRST_LAYER_DURATION', self.def_maxiumm_first_layer_duration, minval=60, maxval=7200) - # TODO: Hard-coded for now, make configurable later - trend_checks = ((75, 675), (200, 675)) + if threshold is None: + # Calculate the threshold based on the layer quality and maximum first layer duration + maximum_z_change_microns = self._get_maximum_z_change_microns_for_quality(layer_quality) + + # Add 120 seconds to the maximum first layer duration to account for the time between true zero probing + # and the print starting (needs to cover beacon rapid scan, nozzle heating to full temperature, priming, etc.) + period = maximum_first_layer_duration + 120 - # Moving average size was determined experimentally, and provides a good balance between responsiveness and stability. + predictor = ThresholdPredictor(self.printer) + threshold = int(predictor.predict_threshold( maximum_z_change_microns, period)) + + logging.info(f"{self.name}: Predicted adaptive heat soak threshold for maximum Z change of {maximum_z_change_microns} microns (quality {layer_quality}) over {period} seconds: {threshold:.2f} nm/s") + else: + logging.info(f"{self.name}: Using forced adaptive heat soak threshold: {threshold:.2f} nm/s") + + # The following control values were determined experimentally, and should not be changed + # without careful consideration and reference to the corpus of experimental data. Changing + # these values will also invalidate the threshold predictor training data. + target_hold_count = 150 moving_average_size = 210 + trend_checks = ((75, 675), (200, 675)) + hold_count = 0 # z_rate_history is a circular buffer of the last `moving_average_size` z-rates @@ -384,85 +415,109 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): moving_average_history_times = [] wait_str = f"between {self._format_seconds(minimum_wait)} and {self._format_seconds(maximum_wait)}" if minimum_wait > 0 else f"up to {self._format_seconds(maximum_wait)}" - gcmd.respond_info(f"Waiting for {wait_str} for printer to reach thermal stability. Please wait...") + msg = f"Waiting for {wait_str} for printer to reach thermal stability. Please wait..." + self.display_status.message = msg + self.display_status.progress = 0. + gcmd.respond_info(msg) - start_time = self.reactor.monotonic() + try: + start_time = self.reactor.monotonic() - z_rate_session = BeaconZRateSession(self.config, self.beacon) + z_rate_session = BeaconZRateSession(self.config, self.beacon) - ts = time.strftime("%Y%m%d_%H%M%S") - fn = f"/tmp/heat_soak_{ts}.csv" + ts = time.strftime("%Y%m%d_%H%M%S") + fn = f"/tmp/heat_soak_{ts}.csv" - logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") + logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") - with open(fn, "w") as z_rates_file: - z_rates_file.write("time,z_rate\n") - time_zero = None + with open(fn, "w") as z_rates_file: + z_rates_file.write("time,z_rate\n") + time_zero = None + progress_start_z_rate = None + progress_z_rate_range = None + progress = 0.0 - while True: - if self.reactor.monotonic() - start_time > maximum_wait: - gcmd.respond_info(f"Maximum wait time of {self._format_seconds(maximum_wait)} exceeded, wait completed.") - return + while True: + if self.reactor.monotonic() - start_time > maximum_wait: + gcmd.respond_info(f"Maximum wait time of {self._format_seconds(maximum_wait)} exceeded, wait completed.") + return - try: - z_rate_result = z_rate_session.get_next_z_rate() - except Exception as e: - if self.printer.is_shutdown(): - raise - else: - raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") + try: + z_rate_result = z_rate_session.get_next_z_rate() + except Exception as e: + if self.printer.is_shutdown(): + raise + else: + raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") - if time_zero is None: - time_zero = z_rate_result[0] + if time_zero is None: + time_zero = z_rate_result[0] + progress_start_z_rate = abs(z_rate_result[1]) + progress_z_rate_range = max(1.0, progress_start_z_rate - threshold) - z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") + # Dumb MVP progress implemetation, just to show that something is happening. Max out at 95% to avoid confusion + # while waiting for hold count and trend checks to pass. + progress = max(progress, 0.95 * min(1.0, ((abs(z_rate_result[1]) - progress_start_z_rate) / progress_z_rate_range))) + self.display_status.progress = progress - z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] - z_rate_count += 1 + z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") - moving_average = None + z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] + z_rate_count += 1 - if z_rate_count >= moving_average_size: - moving_average = np.mean(z_rate_history) - moving_average_history.append(moving_average) - moving_average_history_times.append(z_rate_result[0]) + moving_average = None - if moving_average is not None: - elapsed = self.reactor.monotonic() - start_time + if z_rate_count >= moving_average_size: + moving_average = np.mean(z_rate_history) + moving_average_history.append(moving_average) + moving_average_history_times.append(z_rate_result[0]) - # Log on every 15th z-rate to avoid flooding the console - should_log = z_rate_count % 15 == 0 + if moving_average is not None: + elapsed = self.reactor.monotonic() - start_time - if abs(moving_average) <= threshold: - hold_count += 1 - msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" - else: - if hold_count > 0: - msg = f"Z-rate {moving_average:.1f} nm/s, moved outside threshold of {threshold} nm/s after {hold_count} consecutive measurements" - hold_count = 0 + # Log on every 15th z-rate to avoid flooding the console + should_log = z_rate_count % 15 == 0 + + if abs(moving_average) <= threshold: + hold_count += 1 + msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" else: - msg = f"Z-rate {moving_average:.1f} nm/s, not within threshold of {threshold} nm/s" - - if hold_count >= target_hold_count: - # For increased robustness, we perform one or more linear trend checks. Typically this will - # include a trend fitted to a short history window, and a trend fitted to a longer history window. - # Together, these checks ensure that the Z-rate is not only stable but also not trending towards instability. - all_checks_passed = all( - self._check_trend_projection( - moving_average_history, moving_average_history_times, - trend_check[0], trend_check[1], threshold - ) for trend_check in trend_checks) - - if all_checks_passed: - if elapsed < minimum_wait: - gcmd.respond_info(msg + f", trend checks pass, waiting for minimum of {self._format_seconds(minimum_wait)} to elapse ({self._format_seconds(elapsed)} elapsed)") + if hold_count > 0: + msg = f"Z-rate {moving_average:.1f} nm/s, moved outside threshold of {threshold} nm/s after {hold_count} consecutive measurements" + hold_count = 0 else: - gcmd.respond_info(f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed.") - return + msg = f"Z-rate {moving_average:.1f} nm/s, not within threshold of {threshold} nm/s" + + if hold_count >= target_hold_count: + # For increased robustness, we perform one or more linear trend checks. Typically this will + # include a trend fitted to a short history window, and a trend fitted to a longer history window. + # Together, these checks ensure that the Z-rate is not only stable but also not trending towards instability. + all_checks_passed = all( + self._check_trend_projection( + moving_average_history, moving_average_history_times, + trend_check[0], trend_check[1], threshold + ) for trend_check in trend_checks) + + if all_checks_passed: + if elapsed < minimum_wait: + msg += f", trend checks pass, waiting for minimum of {self._format_seconds(minimum_wait)} to elapse ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" + self.display_status.message = msg + gcmd.respond_info(msg) + else: + msg = f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed." + gcmd.respond_info(msg) + return + elif should_log: + msg += f", waiting for trend checks to pass ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" + self.display_status.message = msg + gcmd.respond_info(msg) elif should_log: - gcmd.respond_info(msg + f", waiting for trend checks to pass ({self._format_seconds(elapsed)} elapsed)") - elif should_log: - gcmd.respond_info(msg + f" ({self._format_seconds(elapsed)} elapsed)") + msg += f" ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" + self.display_status.message = msg + gcmd.respond_info(msg) + finally: + self.display_status.message = None + self.display_status.progress = None desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 4233a5ea3..8d8362844 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -325,11 +325,11 @@ gcode: # beacon adaptive heat soak config {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} - {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} - {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} + {% set beacon_adaptive_heat_soak_layer_quality = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_layer_quality|default(3)|int %} + {% set beacon_adaptive_heat_soak_maximum_first_layer_duration = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_maximum_first_layer_duration|default(1800)|int %} # get macro parameters {% set X0 = params.X0|default(-1)|float %} @@ -626,7 +626,7 @@ gcode: _MOVE_TO_SAFE_Z_HOME # Must be close to bed for soaking and for beacon proximity measurements G1 Z2.5 F{z_speed} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK LAYER_QUALITY={beacon_adaptive_heat_soak_layer_quality} MAXIMUM_FIRST_LAYER_DURATION={beacon_adaptive_heat_soak_maximum_first_layer_duration} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 826e1086c..cc4e815da 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -97,21 +97,38 @@ variable_beacon_scan_method_automatic: False # Enables the METHOD=aut # specifically not recommended when beacon_scan_compensation_enable is enabled. variable_beacon_adaptive_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements -variable_beacon_adaptive_heat_soak_min_wait: 0 # The minimum time in seconds to wait for adaptive heat soaking to complete. -variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete -variable_beacon_adaptive_heat_soak_threshold: 20 # The threshold z change rate magnitude in nm/s (nanometers per second) for adaptive - # heat soaking. +variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete. This is + # a sanity limit to prevent the printer from waiting indefinitely for adaptive heat soaking. variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 # The extra time in seconds to wait after adaptive heat soaking is considered complete. # Typically not needed, but can be useful for printers with very stable gantry designs # (such as steel rail on steel tube) where the adapative heat soak completes before # the edges of the bed have thermally stabilized. +variable_beacon_adaptive_heat_soak_layer_quality: 3 # Shorter soak times leave more thermal z deflection to occur during the print, leading to + # imperfect early layers, notably with some oversquish developing over the first layer. Longer soak + # times leave less thermal z deflection to occur during the print, leading to cleaner early + # layers and more consistent z dimensional accuracy. The choice for this trade-off is + # expressed by the layer quality setting. + # + # The quality scale is from 1 to 5, with 1 being the fastest soak time and 5 being the slowest soak time, + # and with 1 having the lowest quality and 5 having the highest quality. + # + # 1 = rough, 2 = draft, 3 = normal, 4 = high, 5 = maximum + +variable_beacon_adaptive_heat_soak_maximum_first_layer_duration: 1800 + # This must be set to the maximum first layer print time for the gcode files you will print. + # For example, if you typically print first layers that take up to 30 minutes, set this to 1800 + # seconds. Longer durations will result in longer adaptive heat soak times. Be sure to set + # this to a correct value. If you print a significantly longer first layer than this value, + # excessive thermal z deflection may occur during the print, leading to very poor first layer + # quality, print failure, or even damage to the bed. + # The value must be between 60 and 7200 seconds. + ##### # INTERNAL USE ONLY! DO NOT TOUCH! ##### variable_beacon_contact_start_print_true_zero_fuzzy_radius: 20 -variable_beacon_adaptive_heat_soak_hold_count: 150 ##### # BEACON COMMON @@ -140,7 +157,6 @@ gcode: CONSOLE_ECHO TITLE="Deprecated gcode variable" TYPE="warning" MSG="Please remove the variable beacon_contact_expansion_multiplier from your config file." {% endif %} - ##### # BEACON CALIBRATION ##### @@ -266,11 +282,11 @@ gcode: # beacon adaptive heat soak config {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} - {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} - {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} + {% set beacon_adaptive_heat_soak_layer_quality = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_layer_quality|default(3)|int %} + {% set beacon_adaptive_heat_soak_maximum_first_layer_duration = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_maximum_first_layer_duration|default(1800)|int %} # home and abl the printer if needed _BEACON_HOME_AND_ABL @@ -300,7 +316,7 @@ gcode: # Wait for bed thermal expansion {% if beacon_adaptive_heat_soak %} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=100 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,3600]|max} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} @@ -891,11 +907,8 @@ gcode: # beacon adaptive heat soak config {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} - {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(840)|int %} + {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} - {% set beacon_adaptive_heat_soak_threshold = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_threshold|default(15)|int %} - {% set beacon_adaptive_heat_soak_hold_count = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_hold_count|default(150)|int %} - {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object {% set mesh_config = printer.fastconfig.config.bed_mesh %} @@ -959,7 +972,8 @@ gcode: # more gantry deflection. G1 Z2.5 F{z_speed} {% if beacon_adaptive_heat_soak %} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK THRESHOLD={beacon_adaptive_heat_soak_threshold} MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={beacon_adaptive_heat_soak_max_wait} HOLD_COUNT={beacon_adaptive_heat_soak_hold_count} + # Force a very stable soak threshold to minimise residual z deflection during probing. + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=12 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,9000]|max} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} From 94ef0acc802e139529154c38f7a81bc1986b0602 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 15 Jul 2025 13:53:42 +0100 Subject: [PATCH 100/139] feat(beacon-heatsoak): user-friendly progress updates --- .../klippy/beacon_adaptive_heat_soak.py | 252 ++++++++++++------ configuration/z-probe/beacon.cfg | 4 +- 2 files changed, 172 insertions(+), 84 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index a5c9c386b..129eaef91 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import re, time, logging, os, multiprocessing, traceback, pygam +import time, logging, os, multiprocessing, traceback, pygam import numpy as np class ThresholdPredictor: @@ -145,6 +145,9 @@ def __init__(self, config, beacon, samples_per_mean=1000, window_size=30, window self._sample_buffer = np.zeros(samples_per_mean, dtype=np.float64) self._step_phase = window_step - window_size # Ensure that phase will be 0 after populating the initial window_size means + def get_estimated_delay_for_first_z_rate(self, beacon_sampling_rate=1000.0): + return self.window_size * (self.samples_per_mean / beacon_sampling_rate) + def _get_next_mean(self): first_sample_time = None @@ -212,6 +215,75 @@ def get_next_z_rate(self): return (self._times[len(self._times) // 2], slope_nm_per_sec) +class BackgroundDisplayStatusProgressHandler: + def __init__( + self, + printer, + msg_fmt = "{spinner} {progress:.0f}%", + display_status_update_interval=0.8, + spinner_sequence="⠋⠙⠹⠸⠼⠴⠦⠧⠇"): + + self.reactor = printer.get_reactor() + self.gcode = printer.lookup_object('gcode') + self.display_status = printer.lookup_object('display_status') + self.display_status_update_interval = display_status_update_interval + self._spinner_sequence = spinner_sequence + self._spinner_phase = 0 + self._timer = None + self._auto_rate_last_eventtime = None + self.msg_fmt = msg_fmt + self._progress = 0.0 + self._auto_rate = 0.0 + + def enable(self): + if self._timer: + return + + self._timer = self.reactor.register_timer( + self._handle_timer, self.reactor.NOW) + + def disable(self): + if self._timer is None: + return + + self.reactor.unregister_timer(self._timer) + self._timer = None + self.display_status.message = None + self.display_status.progress = None + + @property + def progress(self): + return self._progress + + @progress.setter + def progress(self, value): + self._progress = min(1.0, max(0.0, value)) + + def set_auto_rate(self, increment_per_second): + """ + Set the auto rate for the background progress handler. + This is the amount by which the progress will be automatically incremented per second. + """ + self._auto_rate_last_eventtime = None + self._auto_rate = increment_per_second + + def _handle_timer(self, eventtime): + if self._auto_rate_last_eventtime is None: + self._auto_rate_last_eventtime = eventtime + + if self._auto_rate > 0.0: + self._progress = min(1.0, max(0.0, self._progress + self._auto_rate * (eventtime - self._auto_rate_last_eventtime))) + + self._auto_rate_last_eventtime = eventtime + + spinner = self._spinner_sequence[self._spinner_phase] + self._spinner_phase = (self._spinner_phase + 1) % len(self._spinner_sequence) + + if self.msg_fmt is not None: + self.display_status.message = self.msg_fmt.format(progress=self._progress * 100.0, spinner=spinner) + + return self.reactor.monotonic() + self.display_status_update_interval + class BeaconAdaptiveHeatSoak: def __init__(self, config): self.config = config @@ -225,24 +297,23 @@ def __init__(self, config): # The default layer quality for adaptive heat soak, which is used in conjunction with maximum_first_layer_duration # to determine the z rate threshold for thermal stability. The greater the quality value, the less oversquish is tolerated. # 1 = rough, 2 = draft, 3 = normal, 4 = high, 5 = maximum - self.def_layer_quality = config.getint('layer_quality', 3, minval=1, maxval=5) + self.def_layer_quality = config.getint('default_layer_quality', 3, minval=1, maxval=5) # The default maximum first layer duration in seconds, which is used in conjunction with layer_quality to determine # the z rate threshold for thermal stability. - self.def_maxiumm_first_layer_duration = config.getint('maximum_first_layer_duration', 1800, minval=60, maxval=7200) + self.def_maxiumm_first_layer_duration = config.getint('default_maximum_first_layer_duration', 1800, minval=60, maxval=7200) # The default maximum wait time in seconds for the printer to reach thermal stability. - self.def_maximum_wait = config.getint('maximum_wait', 5400, minval=0) + self.def_maximum_wait = config.getint('default_maximum_wait', 5400, minval=0) # The default minimum wait time in seconds for the printer to reach thermal stability. - self.def_minimum_wait = config.getint('minimum_wait', 0, minval=0) + self.def_minimum_wait = config.getint('default_minimum_wait', 0, minval=0) # TODO: Make trend checks configurable. # Setup self.reactor = None self.beacon = None - self.display_status = None # Register commands self.gcode.register_command( @@ -270,21 +341,25 @@ def __init__(self, config): def _handle_connect(self): self.reactor = self.printer.get_reactor() - self.display_status = self.printer.lookup_object('display_status') if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') - def _prepare_for_sampling(self): + def _prepare_for_sampling_and_get_sampling_frequency(self): # We've seen issues where the first streaming_session after some operations begins with some bogus data, # so we throw away some samples to ensure the beacon is ready. Suspected operations include: # - klipper restart # - BEACON_AUTO_CALIBRATE bad_samples = 0 good_samples = 0 + first_sample_time = None + last_sample_time = None def cb(s): - nonlocal good_samples, bad_samples + nonlocal good_samples, bad_samples, first_sample_time, last_sample_time + if (first_sample_time is None): + first_sample_time = s["time"] + last_sample_time = s["time"] dist = s["dist"] if dist is None or np.isinf(dist) or np.isnan(dist): bad_samples += 1 @@ -304,30 +379,8 @@ def cb(s): if good_samples < 1000: raise self.printer.command_error(f"Failed to prepare beacon for sampling, timed out waiting for good samples. Beacon must be calibrated and positioned correctly before running this command.") - - def parse_duples_string(s: str) -> tuple: - """ - Parses a string of duples and returns a tuple of tuple of ints. - - The function expects a string in the format: - "(num, num), (num, num), ... " - - It raises a ValueError if the string doesn't match the expected pattern. - - Examples: - "(20, 30),( 1, 99 ) , (100, 234)" -> ((20, 30), (1, 99), (100, 234)) - """ - # Define a regex that must match the entire string. - full_pattern = r'^\s*\(\s*\d+\s*,\s*\d+\s*\)(?:\s*,\s*\(\s*\d+\s*,\s*\d+\s*\))*\s*$' - if not re.fullmatch(full_pattern, s): - raise ValueError("Input string does not match the expected pattern.") - - # Define a pattern to find each tuple of digits. - tuple_pattern = r'\(\s*(\d+)\s*,\s*(\d+)\s*\)' - matches = re.findall(tuple_pattern, s) - - # Convert the string numbers to integers and pack them into tuples. - return tuple((int(x), int(y)) for x, y in matches) + + return (good_samples + bad_samples) / (last_sample_time - first_sample_time) def _check_trend_projection(self, moving_average_history, moving_average_history_times, trend_fit_window, trend_projection, threshold): if len(moving_average_history) < trend_fit_window: @@ -359,6 +412,13 @@ def _check_trend_projection(self, moving_average_history, moving_average_history return abs(check_value) <= threshold + def get_layer_quality_name(self, quality): + # Returns the name of the layer quality based on the quality value. + if quality < 1 or quality > 5: + raise ValueError(f"Invalid layer quality {quality}, must be between 1 and 5.") + + return ("rough", "draft", "normal", "high", "maximum")[quality - 1] + def _get_maximum_z_change_microns_for_quality(self, quality): if quality < 1 or quality > 5: raise ValueError(f"Invalid layer quality {quality}, must be between 1 and 5.") @@ -366,7 +426,7 @@ def _get_maximum_z_change_microns_for_quality(self, quality): # Returns the maximum Z change in microns for the given layer quality. # This is a fixed mapping based on empirical data and should not be changed. return (150, 100, 50, 20, 10)[quality - 1] # Microns for layer quality 1-5 - + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon is None: @@ -375,14 +435,15 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon.model is None: raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") - self._prepare_for_sampling() - threshold = gcmd.get_int('_FORCE_THRESHOLD', None, minval=8) minimum_wait = gcmd.get_int('MINIMUM_WAIT', self.def_minimum_wait, minval=0) maximum_wait = gcmd.get_int('MAXIMUM_WAIT', self.def_maximum_wait, minval=0) layer_quality = gcmd.get_int('LAYER_QUALITY', self.def_layer_quality, minval=1, maxval=5) maximum_first_layer_duration = gcmd.get_int('MAXIMUM_FIRST_LAYER_DURATION', self.def_maxiumm_first_layer_duration, minval=60, maxval=7200) + params_msg = '' + threshold_origin = "forced" if threshold is not None else "predicted" + if threshold is None: # Calculate the threshold based on the layer quality and maximum first layer duration maximum_z_change_microns = self._get_maximum_z_change_microns_for_quality(layer_quality) @@ -392,11 +453,13 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): period = maximum_first_layer_duration + 120 predictor = ThresholdPredictor(self.printer) - threshold = int(predictor.predict_threshold( maximum_z_change_microns, period)) - - logging.info(f"{self.name}: Predicted adaptive heat soak threshold for maximum Z change of {maximum_z_change_microns} microns (quality {layer_quality}) over {period} seconds: {threshold:.2f} nm/s") + threshold = predictor.predict_threshold(maximum_z_change_microns, period) + params_msg = f"\nto suit layer quality {layer_quality} ({self.get_layer_quality_name(layer_quality)}) with maximum first layer duration of {self._format_seconds(maximum_first_layer_duration)}" + logging.info(f"{self.name}: predicted adaptive heat soak threshold for maximum Z change of {maximum_z_change_microns} microns (quality {layer_quality}) over {period} seconds: {threshold:.2f} nm/s") else: - logging.info(f"{self.name}: Using forced adaptive heat soak threshold: {threshold:.2f} nm/s") + logging.info(f"{self.name}: using forced adaptive heat soak threshold: {threshold:.2f} nm/s") + + beacon_sampling_rate = self._prepare_for_sampling_and_get_sampling_frequency() # The following control values were determined experimentally, and should not be changed # without careful consideration and reference to the corpus of experimental data. Changing @@ -414,28 +477,34 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): moving_average_history = [] moving_average_history_times = [] - wait_str = f"between {self._format_seconds(minimum_wait)} and {self._format_seconds(maximum_wait)}" if minimum_wait > 0 else f"up to {self._format_seconds(maximum_wait)}" - msg = f"Waiting for {wait_str} for printer to reach thermal stability. Please wait..." - self.display_status.message = msg - self.display_status.progress = 0. - gcmd.respond_info(msg) + gcmd.respond_info(f"Adaptive heat soak started, waiting for printer to reach thermal stability{params_msg}.\nCheck printer status for progress. Please wait...") + progress_handler = None try: start_time = self.reactor.monotonic() - z_rate_session = BeaconZRateSession(self.config, self.beacon) + progress_handler = BackgroundDisplayStatusProgressHandler(self.printer, "{spinner} Heat soaking {progress:.1f}%") + + # Automatically increment progress to reach about 5% by the time the first z-rate moving average is available. + estimated_time_to_first_moving_average = \ + z_rate_session.get_estimated_delay_for_first_z_rate(beacon_sampling_rate) \ + + moving_average_size * (z_rate_session.samples_per_mean / beacon_sampling_rate) + + progress_handler.set_auto_rate(0.05 / estimated_time_to_first_moving_average) + progress_handler.enable() ts = time.strftime("%Y%m%d_%H%M%S") fn = f"/tmp/heat_soak_{ts}.csv" - logging.info(f"{self.name}: starting: threshold={threshold}, hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, z_rates_file={fn}") + logging.info(f"{self.name}: starting: threshold={threshold} ({threshold_origin}), est_t_to_first_ma={estimated_time_to_first_moving_average:.1f} hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, layer_quality={layer_quality}, maximum_first_layer_duration={maximum_first_layer_duration}, beacon_sampling_rate={beacon_sampling_rate:.1f}, z_rates_file={fn}") with open(fn, "w") as z_rates_file: z_rates_file.write("time,z_rate\n") time_zero = None + progress_start = None progress_start_z_rate = None progress_z_rate_range = None - progress = 0.0 + progress_on_final_approach = False while True: if self.reactor.monotonic() - start_time > maximum_wait: @@ -452,19 +521,15 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if time_zero is None: time_zero = z_rate_result[0] - progress_start_z_rate = abs(z_rate_result[1]) - progress_z_rate_range = max(1.0, progress_start_z_rate - threshold) - - # Dumb MVP progress implemetation, just to show that something is happening. Max out at 95% to avoid confusion - # while waiting for hold count and trend checks to pass. - progress = max(progress, 0.95 * min(1.0, ((abs(z_rate_result[1]) - progress_start_z_rate) / progress_z_rate_range))) - self.display_status.progress = progress z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") - z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] z_rate_count += 1 + # Throttle logging + should_log = z_rate_count % 20 == 0 + + elapsed = self.reactor.monotonic() - start_time moving_average = None if z_rate_count >= moving_average_size: @@ -473,25 +538,53 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): moving_average_history_times.append(z_rate_result[0]) if moving_average is not None: - elapsed = self.reactor.monotonic() - start_time - - # Log on every 15th z-rate to avoid flooding the console - should_log = z_rate_count % 15 == 0 + if progress_start is None: + progress_handler.set_auto_rate(0) + progress_start = progress_handler.progress + progress_start_z_rate = abs(moving_average) + # This is the amount of z-rate change until we reach the threshold. We add 10% of the threshold + # as we will surely move beyond the threshold. If we are *already* within the threshold + # this happens with a very quick first layer - we must wait for proven z-rate stability: + # we handle this by the max condition, which applies when threshold is larger than the start z-rate; + # this will promptly cause the progress to transition to 95% and enter the final approach phase. + progress_z_rate_range = max(1.0, (progress_start_z_rate - threshold) + 0.1 * threshold) + logging.info(f"{self.name}: first ma: elapsed={elapsed:.1f}, progress_start={progress_start:.2f}, progress_start_z_rate={progress_start_z_rate:.2f}, progress_z_rate_range={progress_z_rate_range:.2f}, moving_average={moving_average:.2f} nm/s") + if progress_start > 0.1: + # This is unexpected. The value should be close to 5%. Force it, even though we'll jump + # progress backwards. + progress_handler.progress = progress_start = 0.1 + logging.warning(f"{self.name}: unexpected progress_start value {progress_start:.2f}, resetting to 0.1 to avoid confusion.") + + # Hold back 5% of progress to avoid confusion while waiting for hold count and trend checks to pass. + # And don't allow progress to decrease. + progress_handler.progress = max( + progress_handler.progress, + progress_start + (0.95 - progress_start) * min(1.0, (progress_start_z_rate - abs(moving_average)) / progress_z_rate_range) + ) + + if progress_handler.progress >= 0.949 and not progress_on_final_approach: + # We're on the final approach to 100% progress. For now, fake a slow approach to 99% + # over the next 10 minutes for user confidence. MVP implementation, we may want to + # improve this later. + progress_on_final_approach = True + progress_handler.set_auto_rate(0.04 / 600.0) + elif progress_handler.progress >= 0.989 and progress_on_final_approach: + # Hold at ~99% + progress_handler.set_auto_rate(0.0) + + all_checks_passed = 'N/A' + min_wait_satisfied = 'N/A' if abs(moving_average) <= threshold: hold_count += 1 - msg = f"Z-rate {moving_average:.1f} nm/s, within threshold of {threshold} nm/s for {hold_count}/{target_hold_count} consecutive measurements" else: - if hold_count > 0: - msg = f"Z-rate {moving_average:.1f} nm/s, moved outside threshold of {threshold} nm/s after {hold_count} consecutive measurements" - hold_count = 0 - else: - msg = f"Z-rate {moving_average:.1f} nm/s, not within threshold of {threshold} nm/s" + hold_count = 0 if hold_count >= target_hold_count: # For increased robustness, we perform one or more linear trend checks. Typically this will # include a trend fitted to a short history window, and a trend fitted to a longer history window. # Together, these checks ensure that the Z-rate is not only stable but also not trending towards instability. + # In testing, this has been shown to reduce the risk of false positives. all_checks_passed = all( self._check_trend_projection( moving_average_history, moving_average_history_times, @@ -500,24 +593,19 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if all_checks_passed: if elapsed < minimum_wait: - msg += f", trend checks pass, waiting for minimum of {self._format_seconds(minimum_wait)} to elapse ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" - self.display_status.message = msg - gcmd.respond_info(msg) + min_wait_satisfied = False else: - msg = f"Printer is considered thermally stable after {self._format_seconds(elapsed)}, wait completed." + msg = f"Adaptive heat soak completed in {self._format_seconds(elapsed)}." gcmd.respond_info(msg) return - elif should_log: - msg += f", waiting for trend checks to pass ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" - self.display_status.message = msg - gcmd.respond_info(msg) - elif should_log: - msg += f" ({self._format_seconds(elapsed)} elapsed) {progress * 100:.1f}%" - self.display_status.message = msg - gcmd.respond_info(msg) + + if should_log: + logging.info(f"{self.name}: elapsed={elapsed:.1f} s, progress={progress_handler.progress * 100.0:.2f}%, moving_average={moving_average:.2f} nm/s, hold_count={hold_count}/{target_hold_count}, all_checks_passed={all_checks_passed}, min_wait_satisfied={min_wait_satisfied}, threshold={threshold:.2f} nm/s") + elif should_log: + logging.info(f"{self.name}: elapsed={elapsed:.1f} s, waiting for first moving average to be available...") finally: - self.display_status.message = None - self.display_status.progress = None + if progress_handler is not None: + progress_handler.disable() desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): @@ -527,7 +615,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): if self.beacon.model is None: raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") - self._prepare_for_sampling() + self._prepare_for_sampling_and_get_sampling_frequency() duration = gcmd.get_int('DURATION', 7200, minval=0) timestamp = time.strftime("%Y%m%d_%H%M%S") @@ -581,7 +669,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_BEACON_SAMPLES(self, gcmd): if self.beacon.model is None: raise self.printer.command_error("Beacon model is not set. Calibrate the Beacon before running this command.") - self._prepare_for_sampling() + self._prepare_for_sampling_and_get_sampling_frequency() duration = gcmd.get_int('DURATION', 300, minval=60) chunk_duration = gcmd.get_int('CHUNK_DURATION', 5, minval=5) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index cc4e815da..f81c01f87 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -111,8 +111,8 @@ variable_beacon_adaptive_heat_soak_layer_quality: 3 # Shorter soak times leave # layers and more consistent z dimensional accuracy. The choice for this trade-off is # expressed by the layer quality setting. # - # The quality scale is from 1 to 5, with 1 being the fastest soak time and 5 being the slowest soak time, - # and with 1 having the lowest quality and 5 having the highest quality. + # The quality scale is from 1 to 5, with 1 giving the fastest soak time but lowest early layer + # quality, and 5 giving the slowest soak time but the highest early layer quality. # # 1 = rough, 2 = draft, 3 = normal, 4 = high, 5 = maximum From 7c7d28e52ad6cae60a5c820e6ff4ef5491190455 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 17 Jul 2025 12:06:52 +0100 Subject: [PATCH 101/139] fix(beacon-mesh): reinstate accidental deletion of macro line --- configuration/z-probe/beacon.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index f81c01f87..93df8c847 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -909,6 +909,7 @@ gcode: {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} + {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} # get bed mesh config object {% set mesh_config = printer.fastconfig.config.bed_mesh %} From d36aba9fa4e38c2943ecad76b61128037e0cf8a0 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Fri, 18 Jul 2025 16:37:41 +0100 Subject: [PATCH 102/139] fix(dynamic-governor): add a delay before switching to the idle governor - this protects against brief transitions when the count of active steppers briefly hits zero, and allows any final time-sensitive tasks to complete when motors are turned off. - also update some nomenclature for clarity --- configuration/klippy/dynamic_governor.py | 83 ++++++++++++++++-------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/configuration/klippy/dynamic_governor.py b/configuration/klippy/dynamic_governor.py index 8412a4a53..f47399fce 100644 --- a/configuration/klippy/dynamic_governor.py +++ b/configuration/klippy/dynamic_governor.py @@ -1,6 +1,7 @@ # Automatically switches the CPU frequency governor to “performance” -# when any stepper is enabled, and back to “ondemand” when all steppers -# are disabled or Klipper is shutting down. +# when the machine is in an active state (that is, when any stepper is enabled), +# and back to “ondemand” when the machine is in an idle state (that is, all steppers +# are disabled or Klipper is shutting down). # # Note: # @@ -27,28 +28,45 @@ def __init__(self, config): logging.info(f"{self.name}: disabled by config") return - self.governor_motors_on = config.get('governor_motors_on', 'performance') - self.governor_motors_off = config.get('governor_motors_off', 'ondemand') + self.active_governor = config.get('active_governor', 'performance') + self.idle_governor = config.get('idle_governor', 'ondemand') + self.idle_governor_delay = config.getint('idle_governor_delay', 30, minval=10) # Ensure cpufreq-set is available and get the list of valid governors self._check_cpufrequtils() - if self.governor_motors_on not in self._valid_governors: + if self.active_governor not in self._valid_governors: raise self.printer.config_error( - f"{self.name}: governor_motors_on '{self.governor_motors_on}' " + f"{self.name}: active_governor '{self.active_governor}' " "not in available governors: " + ', '.join(self._valid_governors) ) - if self.governor_motors_off not in self._valid_governors: + if self.idle_governor not in self._valid_governors: raise self.printer.config_error( - f"{self.name}: governor_motors_off '{self.governor_motors_off}' " + f"{self.name}: idle_governor '{self.idle_governor}' " "not in available governors: " + ', '.join(self._valid_governors) ) - logging.info(f"{self.name}: governor_motors_on={self.governor_motors_on}, " - f"governor_motors_off={self.governor_motors_off}") + logging.info(f"{self.name}: active_governor={self.active_governor}, " + f"idle_governor={self.idle_governor}, idle_governor_delay={self.idle_governor_delay}s") + + # The transition to the idle governor is delayed to avoid switching + # during homing, and to allow for remaining critically-timed operations + # to complete. + def callback(eventtime): + try: + self._exec_cpufreq(self.idle_governor) + except Exception as e: + logging.warning(f"{self.name}: failed to set idle governor: {e}") + return self.printer.get_reactor().NEVER + + self._idle_delay_timer = self.printer.get_reactor().register_timer(callback) + + # Start in active mode. A delayed transition to idle will be scheduled + # in the _on_ready callback if no steppers are enabled. + self._on_active() # Track how many steppers are currently enabled self._enabled_count = 0 @@ -58,7 +76,7 @@ def __init__(self, config): # Ensure we reset to ondemand on shutdown self.printer.register_event_handler('klippy:shutdown', self._on_shutdown) - + def _check_cpufrequtils(self): # Ensure cpufreq-set is available try: @@ -123,18 +141,10 @@ def _on_ready(self): # Register the state callback for this stepper se.register_state_callback(self._on_stepper_state) - # Apply the initial governor based on current state - self._exec_cpufreq(self.governor_motors_on if self._enabled_count > 0 else self.governor_motors_off) - - def _exec_cpufreq(self, governor: str): - """Run cpufreq-set -r -g quietly.""" - try: - self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '-r', '-g', governor]) - logging.info(f"{self.name}: set CPU governor to '{governor}'") - except Exception as e: - logging.warning( - f"{self.name}: failed to set governor to '{governor}': {e}" - ) + # We switched to active mode during construction. Schedule a delayed + # transition to idle if no steppers are enabled. + if self._enabled_count == 0: + self._on_idle() def _on_stepper_state(self, print_time, enabled: bool): """ @@ -145,7 +155,7 @@ def _on_stepper_state(self, print_time, enabled: bool): if enabled: # If transitioning from 0→1 enabled steppers, ramp to performance if self._enabled_count == 0: - self._exec_cpufreq(self.governor_motors_on) + self._on_active() self._enabled_count += 1 else: # Guard against negative counts @@ -153,7 +163,7 @@ def _on_stepper_state(self, print_time, enabled: bool): self._enabled_count -= 1 # If no more steppers are enabled, switch back to ondemand if self._enabled_count == 0: - self._exec_cpufreq(self.governor_motors_off) + self._on_idle() logging.debug( f"{self.name}: stepper state changed, enabled_count={self._enabled_count}" @@ -162,7 +172,28 @@ def _on_stepper_state(self, print_time, enabled: bool): def _on_shutdown(self): """Reset governor when Klipper is shutting down or restarting.""" # Regardless of current state, go back to ondemand - self._exec_cpufreq(self.governor_motors_off) + self._on_idle() + + def _on_active(self): + reactor = self.printer.get_reactor() + reactor.update_timer(self._idle_delay_timer, reactor.NEVER) + self._exec_cpufreq(self.active_governor) + + def _on_idle(self): + reactor = self.printer.get_reactor() + t = reactor.monotonic() + self.idle_governor_delay + logging.info(f"{self.name}: scheduling idle governor switch at {t:.1f}") + reactor.update_timer(self._idle_delay_timer, t) + + def _exec_cpufreq(self, governor: str): + """Run cpufreq-set -r -g quietly.""" + try: + self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '-r', '-g', governor]) + logging.info(f"{self.name}: set CPU governor to '{governor}'") + except Exception as e: + logging.warning( + f"{self.name}: failed to set governor to '{governor}': {e}" + ) def _run_subprocess_with_timeout(self, cmd, timeout_secs=10): """Run a subprocess command with a timeout. From 2328e160696dab9f21b275207f800e6ebab5feb0 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 21 Jul 2025 12:42:35 +0100 Subject: [PATCH 103/139] refactor(beacon-macros): update fully-auto calibration routine BEACON_RATOS_CALIBRATE - Scan compensation mesh creation is always performed if beacon_scan_compensation_enable is true, we don't use the gantry twist check any more. From extensive testing, every printer tested benefits from a compensation mesh, with at least 70um range of correction, typically over 120um. Further, the twist check is not reliable due to the micro-positional variation of beacon contact readings and the low spatial resolution of the twist check not reliably capturing significant details. - Scan compensation mesh creation is now performed before BEACON_FINAL_CALIBRATION, as comp mesh creation requires a more thorough heat soak than BEACON_FINAL_CALIBRATION. --- configuration/z-probe/beacon.cfg | 131 +++++++++++++++---------------- 1 file changed, 64 insertions(+), 67 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 93df8c847..6cc0a3632 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -177,6 +177,7 @@ gcode: # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set beacon_contact_start_print_true_zero = true if printer["gcode_macro RatOS"].beacon_contact_start_print_true_zero|default(false)|lower == 'true' else false %} + {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} # activate IDEX default {% if printer["dual_carriage"] is defined %} @@ -194,19 +195,16 @@ gcode: {% endif %} {% endif %} - # heat chamber if needed - {% if chamber_temp > 0 %} - _CHAMBER_HEATER_ON CHAMBER_TEMP={chamber_temp} + {% set already_heated = False %} + + # scan calibration + {% if beacon_contact_start_print_true_zero and beacon_scan_compensation_enable %} + BEACON_CREATE_SCAN_COMPENSATION_MESH _AUTOMATED=True BED_TEMP={bed_temp} CHAMBER_TEMP={chamber_temp} + {% set already_heated = True %} {% endif %} # final calibration - BEACON_FINAL_CALIBRATION _AUTOMATED=True BED_TEMP={bed_temp} CHAMBER_TEMP={chamber_temp} - - # scan calibration - {% if beacon_contact_start_print_true_zero %} - BEACON_MEASURE_BEACON_OFFSET - _BEACON_MAYBE_SCAN_COMPENSATION - {% endif %} + BEACON_FINAL_CALIBRATION _AUTOMATED=True _AUTOMATED_ALREADY_HEATED={already_heated} BED_TEMP={bed_temp} CHAMBER_TEMP={chamber_temp} # turn bed and extruder heaters off SET_HEATER_TEMPERATURE HEATER={'extruder' if default_toolhead == 0 else 'extruder1'} TARGET=0 @@ -274,6 +272,7 @@ gcode: {% set bed_temp = params.BED_TEMP|default(85)|int %} {% set chamber_temp = params.CHAMBER_TEMP|default(0)|int %} {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} + {% set already_heated = true if params._AUTOMATED_ALREADY_HEATED|default(false)|lower == 'true' else false %} # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} @@ -292,7 +291,7 @@ gcode: _BEACON_HOME_AND_ABL # heat chamber if needed - {% if chamber_temp > 0 and not automated %} + {% if chamber_temp > 0 and not already_heated %} _CHAMBER_HEATER_ON CHAMBER_TEMP={chamber_temp} {% endif %} @@ -302,29 +301,32 @@ gcode: # Absolute positioning G90 - # lower toolhead to heat soaking z height - G0 Z2.5 F{z_hop_speed} + {% if not already_heated %} + # lower toolhead to heat soaking z height + G0 Z2.5 F{z_hop_speed} + + # echo + RATOS_ECHO MSG="Waiting for calibration temperature..." - # echo - RATOS_ECHO MSG="Waiting for calibration temperature..." - - # heat up and wait for bed and extruder calibration temperatures - SET_HEATER_TEMPERATURE HEATER={'extruder' if default_toolhead == 0 else 'extruder1'} TARGET=150 - SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={bed_temp} - TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={bed_temp} MAXIMUM={(bed_temp + 5)} - TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 - - # Wait for bed thermal expansion - {% if beacon_adaptive_heat_soak %} - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=100 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,3600]|max} - {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} - RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." - G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} - {% endif %} - {% else %} - {% if bed_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." - G4 P{(bed_heat_soak_time * 1000)} + # heat up and wait for bed and extruder calibration temperatures + SET_HEATER_TEMPERATURE HEATER={'extruder' if default_toolhead == 0 else 'extruder1'} TARGET=150 + SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={bed_temp} + TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={bed_temp} MAXIMUM={(bed_temp + 5)} + TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 + + # Wait for bed thermal expansion + {% if beacon_adaptive_heat_soak %} + # Soak with a fairly generous threshold. We only need stability + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=100 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,3600]|max} + {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} + RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." + G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} + {% endif %} + {% else %} + {% if bed_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." + G4 P{(bed_heat_soak_time * 1000)} + {% endif %} {% endif %} {% endif %} @@ -513,7 +515,6 @@ gcode: {% set automated = true if params._AUTOMATED|default(false)|lower == 'true' else false %} # config - {% set test_margin = 30 %} {% set speed = printer["gcode_macro RatOS"].macro_travel_speed|float * 60 %} {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set poke_bottom = printer["gcode_macro RatOS"].beacon_contact_poke_bottom_limit|default(-1)|float %} @@ -944,20 +945,18 @@ gcode: _LED_BEACON_CALIBRATION_START # heat chamber if needed - {% if chamber_temp > 0 and not automated %} + {% if chamber_temp > 0 %} _CHAMBER_HEATER_ON CHAMBER_TEMP={chamber_temp} {% endif %} # heat up printer - {% if not automated %} - SET_HEATER_TEMPERATURE HEATER={'extruder' if default_toolhead == 0 else 'extruder1'} TARGET=150 - SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={bed_temp} + SET_HEATER_TEMPERATURE HEATER={'extruder' if default_toolhead == 0 else 'extruder1'} TARGET=150 + SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={bed_temp} - # wait for temperatures - RATOS_ECHO MSG="Please wait..." - TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={bed_temp} MAXIMUM={(bed_temp + 5)} - TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 - {% endif %} + # wait for temperatures + RATOS_ECHO MSG="Please wait..." + TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={bed_temp} MAXIMUM={(bed_temp + 5)} + TEMPERATURE_WAIT SENSOR={'extruder' if default_toolhead == 0 else 'extruder1'} MINIMUM=150 MAXIMUM=155 # Do true zero now that the bed is at temperature {% if beacon_contact_calibrate_model_on_print %} @@ -967,32 +966,30 @@ gcode: {% endif %} # Wait for bed thermal expansion - {% if not automated %} - _MOVE_TO_SAFE_Z_HOME - # Must be close to bed for soaking and for beacon proximity measurements. Use 2.5 instead of 2 to allow for - # more gantry deflection. - G1 Z2.5 F{z_speed} - {% if beacon_adaptive_heat_soak %} - # Force a very stable soak threshold to minimise residual z deflection during probing. - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=12 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,9000]|max} - {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} - RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." - G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} - {% endif %} - {% else %} - {% if bed_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." - G4 P{(bed_heat_soak_time * 1000)} - {% endif %} + _MOVE_TO_SAFE_Z_HOME + # Must be close to bed for soaking and for beacon proximity measurements. Use 2.5 instead of 2 to allow for + # more gantry deflection. + G1 Z2.5 F{z_speed} + {% if beacon_adaptive_heat_soak %} + # Force a very stable soak threshold to minimise residual z deflection during probing. + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=12 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,9000]|max} + {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} + RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." + G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} + {% endif %} + {% else %} + {% if bed_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking bed for {bed_heat_soak_time} seconds..." + G4 P{(bed_heat_soak_time * 1000)} {% endif %} + {% endif %} - # Soak hotend if not already implicitly covered by bed heat soak - {% if not (beacon_adaptive_heat_soak or bed_heat_soak_time > 0) %} - # Wait for extruder thermal expansion - {% if hotend_heat_soak_time > 0 %} - RATOS_ECHO MSG="Heat soaking hotend for {hotend_heat_soak_time} seconds..." - G4 P{(hotend_heat_soak_time * 1000)} - {% endif %} + # Soak hotend if not already implicitly covered by bed heat soak + {% if not (beacon_adaptive_heat_soak or bed_heat_soak_time > 0) %} + # Wait for extruder thermal expansion + {% if hotend_heat_soak_time > 0 %} + RATOS_ECHO MSG="Heat soaking hotend for {hotend_heat_soak_time} seconds..." + G4 P{(hotend_heat_soak_time * 1000)} {% endif %} {% endif %} From 5c3f56a20b00468d621b4430a35dcab4356f3f00 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 21 Jul 2025 14:29:05 +0100 Subject: [PATCH 104/139] feat(extras-ratos): show in display status (like M117) python-side errors and warnings logged via console_echo so they are less-easily missed --- configuration/klippy/ratos.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 1b245d7d5..0667b1cc3 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -55,6 +55,7 @@ def __init__(self, config): self.gm_ratos = None self.toolhead = None self.beacon = None + self.display_status = None # Status fields self.last_processed_file_result = None @@ -79,6 +80,7 @@ def _connect(self): self.sdcard_dirname = self.v_sd.sdcard_dirname self.gm_ratos = self.printer.lookup_object('gcode_macro RatOS') self.toolhead = self.printer.lookup_object("toolhead") + self.display_status = self.printer.lookup_object("display_status") if self.config.has_section("dual_carriage"): self.dual_carriage = self.printer.lookup_object("dual_carriage", None) @@ -552,14 +554,18 @@ def console_echo(self, title, type, msg=''): if type == 'debug': color = "#38bdf8" if type == 'debug': opacity = 0.7 + msg = msg.replace("_N_","\n") + if (type == 'error' or type == 'alert'): - logging.error(title + ": " + msg.replace("_N_","\n")) + logging.error(title + ": " + msg) + self.display_status.message = f"ERROR: {title} (check the console for details)" if (type == 'warning'): - logging.warning(title + ": " + msg.replace("_N_","\n")) + logging.warning(title + ": " + msg) + self.display_status.message = f"WARNING: {title} (check the console for details)" _title = '

' + title + '

' if msg: - _msg = '

' + msg.replace("_N_","\n") + '

' + _msg = '

' + msg + '

' else: _msg = '' From 4e3a5c1fcf4355fea0e0432037efbe52b29da5ab Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 21 Jul 2025 15:33:04 +0100 Subject: [PATCH 105/139] feat(bed-mesh): raise an error if BED_MESH_CALIBRATE is called with unapplied Z-tilt or quad gantry leveling - this behaviour can be overriden using [gcode_macro BED_MESH_CALIBRATE] variable_allow_unapplied_abl: True - combine beacon and stowable BED_MESH_CALIBRATE overrides, relocate to overrides.cfg - fix spelling of "leveling" in a few places --- configuration/klippy/beacon_mesh.py | 4 +-- configuration/macros.cfg | 2 +- configuration/macros/overrides.cfg | 36 ++++++++++++++++++++++++ configuration/z-probe/beacon.cfg | 15 +--------- configuration/z-probe/stowable-probe.cfg | 17 ----------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 6e657d091..12cf365ae 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -719,12 +719,12 @@ def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): if self.z_tilt and not self.z_tilt.z_status.applied: self.ratos.console_echo("Create compensation mesh warning", "warning", - "Z-tilt levelling is configured but has not been applied._N_" + "Z-tilt leveling is configured but has not been applied._N_" "This may result in inaccurate compensation.") if self.qgl and not self.qgl.z_status.applied: self.ratos.console_echo("Create compensation mesh warning", "warning", - "Quad gantry levelling is configured but has not been applied._N_" + "Quad gantry leveling is configured but has not been applied._N_" "This may result in inaccurate compensation.") keep_temp_meshes = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 8d8362844..703fd06a5 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -996,7 +996,7 @@ gcode: BEACON_QUERY -# NB: Called only from _START_PRINT_AFTER_HEATING_BED, after bed levelling, and only when +# NB: Called only from _START_PRINT_AFTER_HEATING_BED, after bed leveling, and only when # beacon_contact_start_print_true_zero is true. [gcode_macro _START_PRINT_AFTER_HEATING_CONTACT_WITH_OPTIONAL_WIPE] gcode: diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index b50e0f00b..ccc03f7c6 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -315,3 +315,39 @@ gcode: SET_VELOCITY_LIMIT_BASE { rawparams } {% endif %} + +[gcode_macro BED_MESH_CALIBRATE] +rename_existing: _BED_MESH_CALIBRATE_BASE +variable_allow_unapplied_abl: False +gcode: + {% set z_probe = printer["gcode_macro RatOS"].z_probe %} + {% if not allow_unapplied_abl %} + {% set msg = None %} + {% if printer.z_tilt is defined and not printer.z_tilt.applied %} + {% set msg = "Z tilt is not applied, please run Z tilt adjustment before bed mesh calibration." %} + {% endif %} + {% if printer.quad_gantry_level is defined and not printer.quad_gantry_level.applied %} + {% set msg = "Quad gantry leveling is not applied, please run quad gantry leveling before bed mesh calibration." %} + {% endif %} + {% if msg %} + CONSOLE_ECHO TITLE="Bed mesh calibration error" TYPE="error" MSG="{msg}" + _RAISE_ERROR MSG="Automatic bed leveling is not applied" + {% endif %} + {% endif %} + {% if printer.fastconfig.settings.beacon is defined %} + {% set beacon_default_probe_method = printer.fastconfig.settings.beacon.default_probe_method|default('proximity') %} + {% set probe_method = params.PROBE_METHOD|default(beacon_default_probe_method)|lower %} + {% if probe_method == 'proximity' %} + _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" + {% endif %} + {% endif %} + {% if z_probe == "stowable" %} + DEPLOY_PROBE + {% endif %} + _BED_MESH_CALIBRATE_BASE {rawparams} + {% if z_probe == "stowable" %} + STOW_PROBE + {% endif %} + {% if printer.fastconfig.settings.beacon is defined %} + _APPLY_RATOS_BED_MESH_PARAMETERS {rawparams} + {% endif %} \ No newline at end of file diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 6cc0a3632..5c2dd1bfc 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1217,17 +1217,4 @@ gcode: # probe BEACON_OFFSET_COMPARE SAMPLES_DROP=1 SAMPLES=3 - BEACON_QUERY - -[gcode_macro BED_MESH_CALIBRATE] -rename_existing: _BED_MESH_CALIBRATE_BASE -gcode: - {% if printer.fastconfig.settings.beacon is defined %} - {% set beacon_default_probe_method = printer.fastconfig.settings.beacon.default_probe_method|default('proximity') %} - {% set probe_method = params.PROBE_METHOD|default(beacon_default_probe_method)|lower %} - {% if probe_method == 'proximity' %} - _CHECK_ACTIVE_BEACON_MODEL_TEMP TITLE="Bed mesh calibration warning" - {% endif %} - {% endif %} - _BED_MESH_CALIBRATE_BASE {rawparams} - _APPLY_RATOS_BED_MESH_PARAMETERS {rawparams} \ No newline at end of file + BEACON_QUERY \ No newline at end of file diff --git a/configuration/z-probe/stowable-probe.cfg b/configuration/z-probe/stowable-probe.cfg index 7c4c69063..984979a0c 100644 --- a/configuration/z-probe/stowable-probe.cfg +++ b/configuration/z-probe/stowable-probe.cfg @@ -179,22 +179,6 @@ gcode: ASSERT_PROBE_STOWED {% endif %} - -[gcode_macro BED_MESH_CALIBRATE] -rename_existing: BED_MESH_CALIBRATE_ORIG -gcode: - {% set args = [] %} - {% for p in params %} - {% if p == 'PROFILE' %} - {% set tmp = args.append('%s="%s"' % (p, params[p])) %} - {% else %} - {% set tmp = args.append('%s=%s' % (p, params[p])) %} - {% endif %} - {% endfor %} - DEPLOY_PROBE - BED_MESH_CALIBRATE_ORIG {args|join(' ')} - STOW_PROBE - [gcode_macro PROBE_CALIBRATE] rename_existing: PROBE_CALIBRATE_ORIG gcode: @@ -216,7 +200,6 @@ gcode: STOW_PROBE RESTORE_GCODE_STATE name=probe_calibrate MOVE=1 MOVE_SPEED={RatOS.macro_travel_speed|float} - [gcode_macro PROBE] rename_existing: PROBE_ORIG gcode: From 8c995b21a3a8a7fa854249f7cb81c9005d6fa9a1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Mon, 21 Jul 2025 15:54:05 +0100 Subject: [PATCH 106/139] refactor(beacon-heatsoak): enable adaptive heatsoak by default only for V-Core 4 variants - Aspects of the adapative heatsoak algorithm are currently tuned to the V-Core 4 design --- configuration/macros.cfg | 2 +- .../printers/v-core-4-hybrid/v-core-4-hybrid.cfg | 3 +++ configuration/printers/v-core-4-idex/v-core-4-idex.cfg | 3 +++ configuration/printers/v-core-4/v-core-4.cfg | 3 +++ configuration/z-probe/beacon.cfg | 9 ++++++--- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 703fd06a5..409d1a1d1 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -324,7 +324,7 @@ gcode: {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} # beacon adaptive heat soak config - {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(false)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} diff --git a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg index fb5776474..a1737e723 100644 --- a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg +++ b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg @@ -4,6 +4,9 @@ [include ../base.cfg] +[gcode_macro RatOS] +variable_beacon_adaptive_heat_soak: True + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin diff --git a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg index c7cb30572..1585d3bb4 100644 --- a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg +++ b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg @@ -4,6 +4,9 @@ [include ../base.cfg] +[gcode_macro RatOS] +variable_beacon_adaptive_heat_soak: True + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin diff --git a/configuration/printers/v-core-4/v-core-4.cfg b/configuration/printers/v-core-4/v-core-4.cfg index 92e6b9fd7..ec649466e 100644 --- a/configuration/printers/v-core-4/v-core-4.cfg +++ b/configuration/printers/v-core-4/v-core-4.cfg @@ -4,6 +4,9 @@ [include ../base.cfg] +[gcode_macro RatOS] +variable_beacon_adaptive_heat_soak: True + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 5c2dd1bfc..339e6bc57 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -96,7 +96,10 @@ variable_beacon_contact_poke_bottom_limit: -1 # The bottom limit for the co variable_beacon_scan_method_automatic: False # Enables the METHOD=automatic scan option. This is generally not recommended, and # specifically not recommended when beacon_scan_compensation_enable is enabled. -variable_beacon_adaptive_heat_soak: True # Enables adaptive heat soaking based on beacon proximity measurements +#variable_beacon_adaptive_heat_soak: False # Enables adaptive heat soaking based on beacon proximity measurements + # This is enabled only for V-Core 4 variants by default, but can be disabled if desired. + # Other printers can opt-in if desired, but proceed with caution as aspects of the adaptive + # heat soak algorithm are currently tuned for the V-Core 4 design. variable_beacon_adaptive_heat_soak_max_wait: 5400 # The maximum time in seconds to wait for adaptive heat soaking to complete. This is # a sanity limit to prevent the printer from waiting indefinitely for adaptive heat soaking. variable_beacon_adaptive_heat_soak_extra_wait_after_completion: 0 @@ -280,7 +283,7 @@ gcode: {% set z_hop_speed = printer.fastconfig.config.ratos_homing.z_hop_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(false)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} @@ -907,7 +910,7 @@ gcode: {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} # beacon adaptive heat soak config - {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(true)|lower == 'true' else false %} + {% set beacon_adaptive_heat_soak = true if printer["gcode_macro RatOS"].beacon_adaptive_heat_soak|default(false)|lower == 'true' else false %} {% set beacon_adaptive_heat_soak_min_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_min_wait|default(0)|int %} {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} From 960fe6a27cb73aa4177fc233d14383529f7cca62 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 22 Jul 2025 09:14:10 +0100 Subject: [PATCH 107/139] refactor(bed-mesh): make the error-if-abl-not-applied behaviour opt-in for now, this needs further consideration (partial revert of 6b74598c) - the previous behaviour breaks the expected story of the Mainsail heightmap page "click home icon, click calibrate" --- configuration/macros/overrides.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index ccc03f7c6..3012d5909 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -318,7 +318,7 @@ gcode: [gcode_macro BED_MESH_CALIBRATE] rename_existing: _BED_MESH_CALIBRATE_BASE -variable_allow_unapplied_abl: False +variable_allow_unapplied_abl: True gcode: {% set z_probe = printer["gcode_macro RatOS"].z_probe %} {% if not allow_unapplied_abl %} From f6127d6a16504d831e8e6bdaafc4df8ad2fda48d Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 22 Jul 2025 09:19:49 +0100 Subject: [PATCH 108/139] fix(vaoc): _VAOC_PROBE_NOZZLE_TEMP_OFFSET now respects the configured hotend_heat_soak_time --- configuration/macros/idex/vaoc.cfg | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/configuration/macros/idex/vaoc.cfg b/configuration/macros/idex/vaoc.cfg index 7e06f77bc..27c52ec7a 100644 --- a/configuration/macros/idex/vaoc.cfg +++ b/configuration/macros/idex/vaoc.cfg @@ -716,6 +716,7 @@ gcode: # config {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} + {% set hotend_heat_soak_time = printer["gcode_macro RatOS"].hotend_heat_soak_time|default(0)|int %} # get IDEX mode {% set idex_mode = printer["dual_carriage"].carriage_1|lower %} @@ -732,9 +733,11 @@ gcode: SET_HEATER_TEMPERATURE HEATER={"extruder" if toolhead == 0 else "extruder1"} TARGET={temp} TEMPERATURE_WAIT SENSOR={"extruder" if toolhead == 0 else "extruder1"} MINIMUM={temp} MAXIMUM={temp + 2} - # wait for temperature to settle down - RATOS_ECHO PREFIX="VAOC" MSG="Waiting for thermal expansion..." - G4 P240000 + # Wait for hotend thermal expansion + {% if hotend_heat_soak_time > 0 %} + RATOS_ECHO PREFIX="VAOC" MSG="Heat soaking hotend for {hotend_heat_soak_time} seconds..." + G4 P{(hotend_heat_soak_time * 1000)} + {% endif %} # probe RATOS_ECHO PREFIX="VAOC" MSG="Probing with nozzle temperature {temp}°C..." From ef6d5153be0b8b4f52360e901e85effc2b55a0f6 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 14:30:36 +0100 Subject: [PATCH 109/139] refactor(extras-ratos): improve handling and logging of out-of-range safe home position --- configuration/klippy/ratos.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 0667b1cc3..2ad474a50 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -630,8 +630,8 @@ def get_beacon_probing_regions(self) -> BeaconProbingRegions: def get_safe_home_position(self): printable_x_max = float(self.gm_ratos.variables['printable_x_max']) printable_y_max = float(self.gm_ratos.variables['printable_y_max']) - safe_home_x = self.gm_ratos.variables.get('safe_home_x', None) - safe_home_y = self.gm_ratos.variables.get('safe_home_y', None) + raw_safe_home_x = safe_home_x = self.gm_ratos.variables.get('safe_home_x', None) + raw_safe_home_y = safe_home_y = self.gm_ratos.variables.get('safe_home_y', None) safe_home_x = printable_x_max / 2 if safe_home_x is None or str(safe_home_x).lower() == 'middle' else float(safe_home_x) safe_home_y = printable_y_max / 2 if safe_home_y is None or str(safe_home_y).lower() == 'middle' else float(safe_home_y) @@ -642,7 +642,9 @@ def get_safe_home_position(self): safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) if safe_home_x < safe_min_x or safe_home_x > safe_max_x or safe_home_y < safe_min_y or safe_home_y > safe_max_y: - self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x:.2f}, {safe_home_y:.2f}) is outside this region.") + if not self.printer.is_shutdown(): + logging.info(f"{self.name}: (safe_home_x, safe_home_y) is not within beacon-probable region: printable_x_max={printable_x_max}, printable_y_max={printable_y_max}, safe_home_x={safe_home_x}, safe_home_y={safe_home_y}, raw_safe_home_x={raw_safe_home_x}, raw_safe_home_y={raw_safe_home_y}, beacon probing region: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y})") + self.printer.invoke_shutdown(f"{self.name}: (safe_home_x, safe_home_y) must be within the region that Beacon can probe: ({safe_min_x}, {safe_min_y}) - ({safe_max_x}, {safe_max_y}). The configured location ({safe_home_x:.2f}, {safe_home_y:.2f}) is outside this region.") return (safe_home_x, safe_home_y) From 0c97db6fb46a8bb46a2286b1999ff8e19cfd4e30 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 14:35:18 +0100 Subject: [PATCH 110/139] fix(macros): avoid potential init delayed gcode race condition - leave RATOS_INIT initial_duration as 0.1. Among other things, this macro calculates the printable area which must happen before other init code. - change other init macros that were using 0.1 to 0.2, with the aim of ensuring that RATOS_INIT has been run first. --- configuration/macros/idex/toolheads.cfg | 2 +- configuration/macros/util.cfg | 4 +++- configuration/printers/v-core-3-hybrid/macros.cfg | 2 +- configuration/printers/v-core-3-idex/macros.cfg | 2 +- configuration/printers/v-core-4-hybrid/macros.cfg | 2 +- configuration/printers/v-core-4-idex/macros.cfg | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/configuration/macros/idex/toolheads.cfg b/configuration/macros/idex/toolheads.cfg index bbe45baa5..448f1e4e0 100644 --- a/configuration/macros/idex/toolheads.cfg +++ b/configuration/macros/idex/toolheads.cfg @@ -763,7 +763,7 @@ gcode: [delayed_gcode _INIT_TOOLHEADS] -initial_duration: 0.1 +initial_duration: 0.2 gcode: # config {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|int %} diff --git a/configuration/macros/util.cfg b/configuration/macros/util.cfg index 22ccc9cdd..6e63841cc 100644 --- a/configuration/macros/util.cfg +++ b/configuration/macros/util.cfg @@ -45,7 +45,9 @@ variable_end_print_retract_filament: 10 # float = total amount of retr gcode: ECHO_RATOS_VARS - +# This must be the first macro to run on startup. Other macros may fail, notably if +# CALCULATE_PRINTABLE_AREA is not run before them. All other delayed_gcode init macros should +# use an initial_duration of at least 0.2 seconds to ensure they run after this one. [delayed_gcode RATOS_INIT] initial_duration: 0.1 gcode: diff --git a/configuration/printers/v-core-3-hybrid/macros.cfg b/configuration/printers/v-core-3-hybrid/macros.cfg index 2a9e6da75..514f80065 100644 --- a/configuration/printers/v-core-3-hybrid/macros.cfg +++ b/configuration/printers/v-core-3-hybrid/macros.cfg @@ -3,7 +3,7 @@ # sections into your printer.cfg and change it there. [delayed_gcode _HYBRID_INIT] -initial_duration: 0.1 +initial_duration: 0.2 gcode: # ensure inverted hybrid corexy configuration VERIFY_HYBRID_INVERTED diff --git a/configuration/printers/v-core-3-idex/macros.cfg b/configuration/printers/v-core-3-idex/macros.cfg index 656c6f02f..a37247887 100644 --- a/configuration/printers/v-core-3-idex/macros.cfg +++ b/configuration/printers/v-core-3-idex/macros.cfg @@ -11,7 +11,7 @@ variable_is_fixed: False # true = VAOC camera is fixed on the printer [delayed_gcode _IDEX_INIT] -initial_duration: 0.1 +initial_duration: 0.2 gcode: ENABLE_DEBUG # ensure IDEX homing order diff --git a/configuration/printers/v-core-4-hybrid/macros.cfg b/configuration/printers/v-core-4-hybrid/macros.cfg index b95be829b..e88aefc33 100644 --- a/configuration/printers/v-core-4-hybrid/macros.cfg +++ b/configuration/printers/v-core-4-hybrid/macros.cfg @@ -3,7 +3,7 @@ # sections into your printer.cfg and change it there. [delayed_gcode _HYBRID_INIT] -initial_duration: 0.1 +initial_duration: 0.2 gcode: # ensure inverted hybrid corexy configuration VERIFY_HYBRID_INVERTED diff --git a/configuration/printers/v-core-4-idex/macros.cfg b/configuration/printers/v-core-4-idex/macros.cfg index f6587f786..23493ef3d 100644 --- a/configuration/printers/v-core-4-idex/macros.cfg +++ b/configuration/printers/v-core-4-idex/macros.cfg @@ -12,7 +12,7 @@ variable_is_fixed: True # true = VAOC camera is fixed on the variable_safe_z: 30 # safe z height for VAOC start and end horizontal moves [delayed_gcode _IDEX_INIT] -initial_duration: 0.1 +initial_duration: 0.2 gcode: # ensure inverted hybrid corexy configuration VERIFY_HYBRID_INVERTED From 59887873cc22cc5448e1cf86658be936b32986eb Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 14:54:49 +0100 Subject: [PATCH 111/139] feat(bed-mesh): before BED_MESH_CALIBRATE, automatically apply ABL and rehome Z if it has not been applied --- configuration/macros/overrides.cfg | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/configuration/macros/overrides.cfg b/configuration/macros/overrides.cfg index 3012d5909..a1e0e1fa3 100644 --- a/configuration/macros/overrides.cfg +++ b/configuration/macros/overrides.cfg @@ -318,20 +318,21 @@ gcode: [gcode_macro BED_MESH_CALIBRATE] rename_existing: _BED_MESH_CALIBRATE_BASE -variable_allow_unapplied_abl: True +variable_abl_and_home_z_if_required: True gcode: {% set z_probe = printer["gcode_macro RatOS"].z_probe %} - {% if not allow_unapplied_abl %} - {% set msg = None %} + {% if abl_and_home_z_if_required %} {% if printer.z_tilt is defined and not printer.z_tilt.applied %} - {% set msg = "Z tilt is not applied, please run Z tilt adjustment before bed mesh calibration." %} + RATOS_ECHO MSG="Adjusting Z tilt..." + Z_TILT_ADJUST + RATOS_ECHO MSG="Rehoming Z after Z tilt adjustment..." + G28 Z {% endif %} {% if printer.quad_gantry_level is defined and not printer.quad_gantry_level.applied %} - {% set msg = "Quad gantry leveling is not applied, please run quad gantry leveling before bed mesh calibration." %} - {% endif %} - {% if msg %} - CONSOLE_ECHO TITLE="Bed mesh calibration error" TYPE="error" MSG="{msg}" - _RAISE_ERROR MSG="Automatic bed leveling is not applied" + RATOS_ECHO MSG="Running quad gantry leveling..." + QUAD_GANTRY_LEVEL + RATOS_ECHO MSG="Rehoming Z after quad gantry leveling..." + G28 Z {% endif %} {% endif %} {% if printer.fastconfig.settings.beacon is defined %} From cf41593521ef699548c7e7f357eeda3052604fc1 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 15:13:25 +0100 Subject: [PATCH 112/139] feat(fastconfig): add defensive initialization check - no specific hazard was identified, but in case future code leads to a pre-init call, there will now be a clear error rather than weird behaviour. --- configuration/klippy/fastconfig.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/fastconfig.py b/configuration/klippy/fastconfig.py index 3db21214b..bc54fbee1 100644 --- a/configuration/klippy/fastconfig.py +++ b/configuration/klippy/fastconfig.py @@ -20,9 +20,11 @@ class ImmutablePrinterConfigStatusWrapper(Mapping): def __init__(self, config): + self.name = config.get_name() self._printer = config.get_printer() self._printer.register_event_handler("klippy:connect", - self._handle_connect) + self._handle_connect) + self._initialized = False def _handle_connect(self): pconfig = self._printer.lookup_object('configfile') @@ -30,8 +32,11 @@ def _handle_connect(self): self._immutable_status = copy.deepcopy(pconfig.get_status(eventtime)) self._immutable_status.pop('save_config_pending', None) self._immutable_status.pop('save_config_pending_items', None) + self._initialized = True def get_status(self, eventtime=None): + if not self._initialized: + raise RuntimeError(f"{self.name}: get_status called before initialization!") return self def __deepcopy__(self, memo): From fb23421739449c3b7683a2b1e2f6c954d7ff2ae5 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 15:18:09 +0100 Subject: [PATCH 113/139] chore(postprocessor): support PrusaSlicer 2.9.1 and 2.9.2 --- src/server/gcode-processor/Actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/gcode-processor/Actions.ts b/src/server/gcode-processor/Actions.ts index 1f354f508..fe54cfc91 100644 --- a/src/server/gcode-processor/Actions.ts +++ b/src/server/gcode-processor/Actions.ts @@ -96,9 +96,9 @@ export function validateGenerator( { cause: gcodeInfo }, ); case GCodeFlavour.PrusaSlicer: - if (!semver.satisfies(gcodeInfo.generatorVersion, '2.8.0 || 2.8.1 || 2.9.0')) { + if (!semver.satisfies(gcodeInfo.generatorVersion, '2.8.0 || 2.8.1 || 2.9.0 || 2.9.1 || 2.9.2')) { throw new SlicerNotSupported( - `Only versions 2.8.0, 2.8.1 and 2.9.0 of PrusaSlicer are supported. Version ${gcodeInfo.generatorVersion} is not supported.`, + `Only release versions 2.8.0, 2.8.1 and 2.9.0 - 2.9.2 of PrusaSlicer are supported. Version ${gcodeInfo.generatorVersion} is not supported.`, { cause: gcodeInfo }, ); } From 7a05aae36151096cda4939114edc2c490a9e973a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 24 Jul 2025 19:52:34 +0100 Subject: [PATCH 114/139] chore(beacon-macros): remove unused macro variable --- configuration/z-probe/beacon.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 339e6bc57..907cbe004 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -652,7 +652,6 @@ gcode: # BEACON NOZZLE TEMPERATURE OFFSET COMPENSATION ##### [gcode_macro _BEACON_SET_NOZZLE_TEMP_OFFSET] -variable_runtime_temp: 0 gcode: # parameters {% set toolhead = params.TOOLHEAD|default(0)|int %} @@ -707,7 +706,6 @@ gcode: {% set required_offset_adjustment = required_expansion_offset - existing_expansion_offset %} SET_GCODE_OFFSET Z_ADJUST={required_offset_adjustment} MOVE=1 SPEED={z_speed} SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE={required_expansion_offset} - SET_GCODE_VARIABLE MACRO=_BEACON_SET_NOZZLE_TEMP_OFFSET VARIABLE=runtime_temp VALUE={temp} # echo RATOS_ECHO PREFIX="BEACON" MSG={'"Nozzle expansion offset of %.6fmm applied to T%s"' % (required_expansion_offset, toolhead)} From 7df7c1a378a9059974fab99906f6bea74891e288 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 26 Jul 2025 11:00:32 +0100 Subject: [PATCH 115/139] fix(idex-beacon): only apply nozzle expansion z offset when applicable - This fixes an issue that occurred after the last toolshift, where the expansion offset for the now-deactivated toolhead heater being set to zero would be applied to the remaining active toolhead, creating an oversquished layer. --- configuration/z-probe/beacon.cfg | 82 ++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 907cbe004..7d35dadb5 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -676,41 +676,63 @@ gcode: {% if reset %} # reset applied offset SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE=0 - {% else %} {% if beacon_contact_start_print_true_zero and beacon_contact_expansion_compensation %} - - {% set offset_before = printer.gcode_move.homing_origin.z %} - - # get coefficient - {% set nozzle_expansion_coefficient_t0 = svv.nozzle_expansion_coefficient_t0|default(0)|float %} + # For IDEX, we must only apply the offset: + # - for single mode, if toolhead is the active toolhead + # - for copy/mirror, if the toolhead is the default toolhead + # Without this check, we could, for example, set the offset for the active toolhead when + # deactivating the other toolhead after the last toolshift. + {% set apply_offset = True %} + {% set idex_info = "" %} {% if printer["dual_carriage"] is defined %} - {% set nozzle_expansion_coefficient_t1 = svv.nozzle_expansion_coefficient_t1|default(0)|float %} + {% set idex_mode = printer["dual_carriage"].carriage_1|lower %} + {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|int %} + {% if idex_mode in ("copy", "mirror") %} + {% set apply_offset = toolhead == default_toolhead %} + {% else %} + {% set apply_offset = (toolhead == 1) == (idex_mode == "primary" ) %} + {% endif %} + {% set idex_info = ", default_toolhead: %s, idex_mode(c1): %s" % (default_toolhead, idex_mode) %} {% endif %} - # get coefficient multiplier - {% set nozzle_expansion_coefficient_multiplier = svv.nozzle_expansion_coefficient_multiplier|default(1.0)|float %} - - # get applied offset - {% set existing_expansion_offset = svv.nozzle_expansion_applied_offset|default(0)|float %} - - # get extruder target temperature - {% set temp = printer['extruder' if toolhead == 0 else 'extruder1'].target|float %} - - # calculate new offset - {% set temp_delta = temp - beacon_contact_true_zero_temp %} - {% set expansion_coefficient = nozzle_expansion_coefficient_t0 if toolhead == 0 else nozzle_expansion_coefficient_t1 %} - {% set required_expansion_offset = nozzle_expansion_coefficient_multiplier * (temp_delta * (expansion_coefficient / 100)) %} - - # set new offset - {% set required_offset_adjustment = required_expansion_offset - existing_expansion_offset %} - SET_GCODE_OFFSET Z_ADJUST={required_offset_adjustment} MOVE=1 SPEED={z_speed} - SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE={required_expansion_offset} - - # echo - RATOS_ECHO PREFIX="BEACON" MSG={'"Nozzle expansion offset of %.6fmm applied to T%s"' % (required_expansion_offset, toolhead)} - _BEACON_SET_NOZZLE_TEMP_OFFSET_LOG_DEBUG MSG="toolhead: {toolhead}, multiplier: {nozzle_expansion_coefficient_multiplier|round(4)}, coefficient: {expansion_coefficient|round(6)}, temp_delta: {temp_delta|round(1)}, existing_expansion_offset: {existing_expansion_offset|round(6)}, required_expansion_offset: {required_expansion_offset|round(6)}, required_offset_adjustment: {required_offset_adjustment|round(6)}, offset_before: {offset_before|round(6)}" - + {% if apply_offset %} + {% set offset_before = printer.gcode_move.homing_origin.z %} + + # get coefficient + {% set nozzle_expansion_coefficient_t0 = svv.nozzle_expansion_coefficient_t0|default(0)|float %} + {% if printer["dual_carriage"] is defined %} + {% set nozzle_expansion_coefficient_t1 = svv.nozzle_expansion_coefficient_t1|default(0)|float %} + {% endif %} + + # get coefficient multiplier + {% set nozzle_expansion_coefficient_multiplier = svv.nozzle_expansion_coefficient_multiplier|default(1.0)|float %} + + # get applied offset + {% set existing_expansion_offset = svv.nozzle_expansion_applied_offset|default(0)|float %} + + # get extruder target temperature + {% set temp = printer['extruder' if toolhead == 0 else 'extruder1'].target|float %} + + # calculate new offset + {% set temp_delta = temp - beacon_contact_true_zero_temp %} + {% set expansion_coefficient = nozzle_expansion_coefficient_t0 if toolhead == 0 else nozzle_expansion_coefficient_t1 %} + {% set required_expansion_offset = nozzle_expansion_coefficient_multiplier * (temp_delta * (expansion_coefficient / 100)) %} + + # set new offset + {% set required_offset_adjustment = required_expansion_offset - existing_expansion_offset %} + SET_GCODE_OFFSET Z_ADJUST={required_offset_adjustment} MOVE=1 SPEED={z_speed} + SAVE_VARIABLE VARIABLE=nozzle_expansion_applied_offset VALUE={required_expansion_offset} + + # echo + RATOS_ECHO PREFIX="BEACON" MSG={'"Nozzle expansion offset of %.6fmm applied to T%s"' % (required_expansion_offset, toolhead)} + _BEACON_SET_NOZZLE_TEMP_OFFSET_LOG_DEBUG MSG="applied: {apply_offset}, toolhead: {toolhead}{idex_info}, multiplier: {nozzle_expansion_coefficient_multiplier|round(4)}, coefficient: {expansion_coefficient|round(6)}, temp_delta: {temp_delta|round(1)}, existing_expansion_offset: {existing_expansion_offset|round(6)}, required_expansion_offset: {required_expansion_offset|round(6)}, required_offset_adjustment: {required_offset_adjustment|round(6)}, offset_before: {offset_before|round(6)}" + {% else %} + # echo + RATOS_ECHO PREFIX="BEACON" MSG="Nozzle expansion offset not applied for T{toolhead}, because it is not the active/applicable toolhead." + DEBUG_ECHO PREFIX="_BEACON_SET_NOZZLE_TEMP_OFFSET" MSG="applied: {apply_offset}, toolhead: {toolhead}{idex_info}" + {% endif %} + {% endif %} {% endif %} From 5623a21be3a9371538dec5f1099bd8067db796df Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 26 Jul 2025 18:03:01 +0100 Subject: [PATCH 116/139] fix(idex): ensure that the `standby` flag for each toolhead is set correctly in `START_PRINT` - This fixes an issue where under-temperature extrusion is attempted on the first toolchange because the inactive toolhead is at standby temperature but is not flagged as being in standby, so does not get heated to printing temperature. --- configuration/macros.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configuration/macros.cfg b/configuration/macros.cfg index 409d1a1d1..d054f3deb 100644 --- a/configuration/macros.cfg +++ b/configuration/macros.cfg @@ -730,9 +730,14 @@ gcode: # put toolhead into standby mode if configured {% if idex_mode != '' %} + # Reset standby flags to ensure consistent state + SET_GCODE_VARIABLE MACRO=T0 VARIABLE=standby VALUE=False + SET_GCODE_VARIABLE MACRO=T1 VARIABLE=standby VALUE=False + {% if idex_mode != "copy" and idex_mode != "mirror" %} {% if toolchange_standby_temp > -1 %} SET_HEATER_TEMPERATURE HEATER={'extruder' if initial_tool == 1 else 'extruder1'} TARGET={toolchange_standby_temp} + SET_GCODE_VARIABLE MACRO=T{0 if initial_tool == 1 else 1} VARIABLE=standby VALUE=True {% endif %} {% endif %} {% endif %} From f446ed884c77606dbf956d75aaa61a92e9a7d737 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 5 Aug 2025 13:54:59 +0100 Subject: [PATCH 117/139] fix(beacon-mesh): improve reactor yielding to avoid timer too close errors - pass the reactor instance on to directly-constructed ZMesh objects - use a non-zero waketime for reactor.pause() --- configuration/klippy/beacon_mesh.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 12cf365ae..5200ceb56 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -12,6 +12,8 @@ import numpy as np import importlib +DEFAULT_REACTOR_PAUSE_OFFSET = 0.006 # 6ms + # Temporary mesh names RATOS_TEMP_SCAN_MESH_BEFORE_NAME = "__BEACON_TEMP_SCAN_MESH_BEFORE__" RATOS_TEMP_SCAN_MESH_ATFER_NAME = "__BEACON_TEMP_SCAN_MESH_AFTER__" @@ -434,7 +436,7 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f}") org_mesh = self.bed_mesh.get_mesh() - new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name()) + new_mesh = BedMesh.ZMesh(org_mesh.get_mesh_params(), org_mesh.get_profile_name(), self.reactor) new_mesh.build_mesh(org_mesh.get_probed_matrix()) new_mesh.set_zero_reference(x_pos, y_pos) self.bed_mesh.set_mesh(new_mesh) @@ -458,7 +460,7 @@ def _create_zmesh_from_profile(self, profile, subject=None, purpose=None): raise self.printer.command_error(f"{subject} not found{purpose}") try: - compensation_zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile) + compensation_zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile, self.reactor) compensation_zmesh.build_mesh(profiles[profile]["points"]) return compensation_zmesh except Exception as e: @@ -609,8 +611,11 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: measured_z = measured_points[y][x] compensation_z = compensation_zmesh.calc_z(x_pos, y_pos) new_z = measured_z + compensation_z - self.ratos.debug_echo("Beacon scan compensation", "measured: %0.4f compensation: %0.4f new: %0.4f" % (measured_z, compensation_z, new_z)) + # Debug disabled: this can produce thousands of lines of output, and also ratos.debug_echo(...) + # is implemented as a gcode_macro call, which is relatively heavy-weight. + # self.ratos.debug_echo("Beacon scan compensation", "measured: %0.4f compensation: %0.4f new: %0.4f" % (measured_z, compensation_z, new_z)) new_points[y].append(new_z) + self.reactor.pause(self.reactor.monotonic() + DEFAULT_REACTOR_PAUSE_OFFSET) measured_zmesh.build_mesh(new_points) # NB: build_mesh does not replace or mutate its params, so no need to reassign measured_mesh_params. @@ -826,7 +831,7 @@ def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") - self.reactor.pause(self.reactor.NOW) + self.reactor.pause(self.reactor.monotonic() + DEFAULT_REACTOR_PAUSE_OFFSET) # For a large mesh (eg, 60x60) this can take 2+ minutes #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) @@ -834,7 +839,7 @@ def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): if keep_temp_meshes: params = contact_params.copy() filtered_profile = contact_mesh_name + "_filtered" - new_mesh = BedMesh.ZMesh(params, filtered_profile) + new_mesh = BedMesh.ZMesh(params, filtered_profile, self.reactor) new_mesh.build_mesh(contact_mesh_points) self.bed_mesh.set_mesh(new_mesh) self.bed_mesh.save_profile(filtered_profile) @@ -851,7 +856,7 @@ def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): params[RATOS_MESH_CHAMBER_TEMP_PARAMETER] = chamber_temp params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = scan_mesh_bounds - new_mesh = BedMesh.ZMesh(params, profile) + new_mesh = BedMesh.ZMesh(params, profile, self.reactor) new_mesh.build_mesh(compensation_mesh_points) self.bed_mesh.set_mesh(new_mesh) self.bed_mesh.save_profile(profile) From 7cd304da11b3ccf425ba9fab3a405ec1c806f00c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 12 Aug 2025 18:54:02 +0100 Subject: [PATCH 118/139] feat(beacon-heatsoak): additionally store z value in diagnostic csv files --- .../klippy/beacon_adaptive_heat_soak.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 129eaef91..01395ced0 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -127,7 +127,7 @@ def _get_model(self): gam.fit(X, y) return gam - + class BeaconZRateSession: def __init__(self, config, beacon, samples_per_mean=1000, window_size=30, window_step=1): self.config = config @@ -208,12 +208,17 @@ def get_next_z_rate(self): break # Fit a 1-degree polynomial (line) to the data - slope, _ = np.polyfit(self._times, self._mean_distances, 1) + slope, intersect = np.polyfit(self._times, self._mean_distances, 1) + + mid_time = (self._times[0] + self._times[-1]) / 2. + + # Get the z value at the mid time + mid_z = slope * mid_time + intersect # Convert from millimeters to nanometers per second slope_nm_per_sec = slope * 1e6 - return (self._times[len(self._times) // 2], slope_nm_per_sec) + return (mid_time, slope_nm_per_sec, mid_z) class BackgroundDisplayStatusProgressHandler: def __init__( @@ -499,7 +504,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): logging.info(f"{self.name}: starting: threshold={threshold} ({threshold_origin}), est_t_to_first_ma={estimated_time_to_first_moving_average:.1f} hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, layer_quality={layer_quality}, maximum_first_layer_duration={maximum_first_layer_duration}, beacon_sampling_rate={beacon_sampling_rate:.1f}, z_rates_file={fn}") with open(fn, "w") as z_rates_file: - z_rates_file.write("time,z_rate\n") + z_rates_file.write("time,z_rate,z\n") time_zero = None progress_start = None progress_start_z_rate = None @@ -522,7 +527,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if time_zero is None: time_zero = z_rate_result[0] - z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") + z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e},{z_rate_result[2]:.8e}\n") z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] z_rate_count += 1 @@ -624,7 +629,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): fullpath = f'/home/pi/printer_data/config/{filename}' with open(fullpath, 'w') as f: - f.write("time,z_rate\n") + f.write("time,z_rate,z\n") gcmd.respond_info(f'Capturing diagnostic Z-rates for {duration} seconds using V2 Z-rate calculation to file {fullpath}, please wait...') start_time = self.reactor.monotonic() z_rate_session = BeaconZRateSession(self.config, self.beacon) @@ -645,7 +650,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): if time_zero is None: time_zero = z_rate_result[0] - f.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e}\n") + f.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e},{z_rate_result[2]:.8e}\n") gcmd.respond_info(f'Diagnostic data captured to {fullpath}') From fa707659daf8b5ab94a3b3f4fec93e9736831c02 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 12 Aug 2025 20:03:12 +0100 Subject: [PATCH 119/139] fix(beacon-heatsoak): improve robustness of soak completion detection - Sometimes we get a very noisy signal on a machine that otherwise gives a normal signal. This is not well understood. We see some correlation with soaking from warm. Informed by extensive data analysis, this fix adds a second level moving average which smooths the noise and is also tested to determine if the soak is complete. This approach may be revised in the future if/when the noise behaviour is better understood. --- .../klippy/beacon_adaptive_heat_soak.py | 126 ++++++++++++++---- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 01395ced0..aa0cc2e05 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -289,6 +289,44 @@ def _handle_timer(self, eventtime): return self.reactor.monotonic() + self.display_status_update_interval +class RunningAverage: + # A running average implementation that maintains a circular buffer of the last `size` values, + # and the current sum of the values in the buffer. Methods are provided to add a new value, + # get the current average, and reset the buffer. The mean is updated efficiently by subtracting the + # oldest value and adding the new value, rather than recalculating the mean from scratch. + + def __init__(self, size): + if size <= 0: + raise ValueError("Size must be greater than 0") + self.size = size + self.buffer = np.zeros(size, dtype=np.float64) + self.index = 0 + self.sum = 0.0 + self.count = 0 + + def get_average(self): + if self.count == 0: + return 0.0 + return float(self.sum / self.count) + + def is_full(self): + return self.count == self.size + + def add(self, value): + if self.count < self.size: + self.count += 1 + else: + self.sum -= self.buffer[self.index] + self.buffer[self.index] = value + self.sum += value + self.index = (self.index + 1) % self.size + + def reset(self): + self.buffer.fill(0.0) + self.index = 0 + self.sum = 0.0 + self.count = 0 + class BeaconAdaptiveHeatSoak: def __init__(self, config): self.config = config @@ -469,19 +507,29 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): # The following control values were determined experimentally, and should not be changed # without careful consideration and reference to the corpus of experimental data. Changing # these values will also invalidate the threshold predictor training data. - target_hold_count = 150 + moving_average_target_hold_count = 150 moving_average_size = 210 - trend_checks = ((75, 675), (200, 675)) + moving_average_trend_checks = ((75, 675), (200, 675)) + + # level_2_moving_average... is the moving average of the moving average + level_2_moving_average_target_hold_count = 150 + level_2_moving_average_size = 400 + level_2_moving_average_trend_checks = ((45, 675),) - hold_count = 0 + moving_average_hold_count = 0 + level_2_moving_average_hold_count = 0 - # z_rate_history is a circular buffer of the last `moving_average_size` z-rates - z_rate_history = [0] * moving_average_size + z_rate_ra = RunningAverage(moving_average_size) z_rate_count = 0 + moving_average_ra = RunningAverage(level_2_moving_average_size) + moving_average_history = [] moving_average_history_times = [] + level_2_moving_average_history = [] + level_2_moving_average_history_times = [] + gcmd.respond_info(f"Adaptive heat soak started, waiting for printer to reach thermal stability{params_msg}.\nCheck printer status for progress. Please wait...") progress_handler = None @@ -501,7 +549,11 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): ts = time.strftime("%Y%m%d_%H%M%S") fn = f"/tmp/heat_soak_{ts}.csv" - logging.info(f"{self.name}: starting: threshold={threshold} ({threshold_origin}), est_t_to_first_ma={estimated_time_to_first_moving_average:.1f} hold_count={target_hold_count}, min_wait={minimum_wait}, max_wait={maximum_wait}, mas={moving_average_size}, trend_checks={trend_checks}, layer_quality={layer_quality}, maximum_first_layer_duration={maximum_first_layer_duration}, beacon_sampling_rate={beacon_sampling_rate:.1f}, z_rates_file={fn}") + logging.info( + f"{self.name}: starting: threshold={threshold} ({threshold_origin}), est_t_to_first_ma={estimated_time_to_first_moving_average:.1f}, min_wait={minimum_wait}, max_wait={maximum_wait}, " + f"layer_quality={layer_quality}, maximum_first_layer_duration={maximum_first_layer_duration}, beacon_sampling_rate={beacon_sampling_rate:.1f}, z_rates_file={fn}, " + f"ma_hold_count={moving_average_target_hold_count}, ma_size={moving_average_size}, ma_trend_checks={moving_average_trend_checks}, " + f"ma2_hold_count={level_2_moving_average_target_hold_count}, ma_size={level_2_moving_average_size}, ma_trend_checks={level_2_moving_average_trend_checks}") with open(fn, "w") as z_rates_file: z_rates_file.write("time,z_rate,z\n") @@ -528,7 +580,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): time_zero = z_rate_result[0] z_rates_file.write(f"{z_rate_result[0] - time_zero:.8e},{z_rate_result[1]:.8e},{z_rate_result[2]:.8e}\n") - z_rate_history[z_rate_count % moving_average_size] = z_rate_result[1] + z_rate_ra.add(z_rate_result[1]) z_rate_count += 1 # Throttle logging @@ -536,13 +588,19 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): elapsed = self.reactor.monotonic() - start_time moving_average = None + level_2_moving_average = None - if z_rate_count >= moving_average_size: - moving_average = np.mean(z_rate_history) + if z_rate_ra.is_full(): + moving_average = z_rate_ra.get_average() + moving_average_ra.add(moving_average) moving_average_history.append(moving_average) moving_average_history_times.append(z_rate_result[0]) - if moving_average is not None: + if moving_average_ra.is_full(): + level_2_moving_average = moving_average_ra.get_average() + level_2_moving_average_history.append(level_2_moving_average) + level_2_moving_average_history_times.append(z_rate_result[0]) + if progress_start is None: progress_handler.set_auto_rate(0) progress_start = progress_handler.progress @@ -577,35 +635,53 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): # Hold at ~99% progress_handler.set_auto_rate(0.0) - all_checks_passed = 'N/A' + moving_average_trend_checks_passed = 'N/A' + level_2_moving_average_trend_checks_passed = 'N/A' min_wait_satisfied = 'N/A' if abs(moving_average) <= threshold: - hold_count += 1 + moving_average_hold_count += 1 else: - hold_count = 0 + moving_average_hold_count = 0 - if hold_count >= target_hold_count: + if moving_average_hold_count >= moving_average_target_hold_count: # For increased robustness, we perform one or more linear trend checks. Typically this will # include a trend fitted to a short history window, and a trend fitted to a longer history window. # Together, these checks ensure that the Z-rate is not only stable but also not trending towards instability. # In testing, this has been shown to reduce the risk of false positives. - all_checks_passed = all( + moving_average_trend_checks_passed = all( self._check_trend_projection( moving_average_history, moving_average_history_times, trend_check[0], trend_check[1], threshold - ) for trend_check in trend_checks) - - if all_checks_passed: - if elapsed < minimum_wait: - min_wait_satisfied = False - else: - msg = f"Adaptive heat soak completed in {self._format_seconds(elapsed)}." - gcmd.respond_info(msg) - return + ) for trend_check in moving_average_trend_checks) + + if level_2_moving_average is not None: + if abs(level_2_moving_average) <= threshold: + level_2_moving_average_hold_count += 1 + else: + level_2_moving_average_hold_count = 0 + + if level_2_moving_average_hold_count >= level_2_moving_average_target_hold_count: + level_2_moving_average_trend_checks_passed = all( + self._check_trend_projection( + level_2_moving_average_history, level_2_moving_average_history_times, + trend_check[0], trend_check[1], threshold + ) for trend_check in level_2_moving_average_trend_checks) + + if moving_average_trend_checks_passed == True or level_2_moving_average_trend_checks_passed == True: + if elapsed < minimum_wait: + min_wait_satisfied = False + else: + msg = f"Adaptive heat soak completed in {self._format_seconds(elapsed)}." + gcmd.respond_info(msg) + return if should_log: - logging.info(f"{self.name}: elapsed={elapsed:.1f} s, progress={progress_handler.progress * 100.0:.2f}%, moving_average={moving_average:.2f} nm/s, hold_count={hold_count}/{target_hold_count}, all_checks_passed={all_checks_passed}, min_wait_satisfied={min_wait_satisfied}, threshold={threshold:.2f} nm/s") + logging.info( + f"{self.name}: elapsed={elapsed:.1f} s, progress={progress_handler.progress * 100.0:.2f}%, " + f"ma={moving_average:.2f} nm/s, ma_hold_count={moving_average_hold_count}/{moving_average_target_hold_count}, ma_trend_checks_passed={moving_average_trend_checks_passed}, " + f"ma2={float('inf') if level_2_moving_average is None else level_2_moving_average:.2f} nm/s, ma2_hold_count={level_2_moving_average_hold_count}/{level_2_moving_average_target_hold_count}, ma2_trend_checks_passed={level_2_moving_average_trend_checks}, " + f"min_wait_satisfied={min_wait_satisfied}, threshold={threshold:.2f} nm/s") elif should_log: logging.info(f"{self.name}: elapsed={elapsed:.1f} s, waiting for first moving average to be available...") finally: From 5256bdb93547e568fc1e782cd5ae569791fba837 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sat, 23 Aug 2025 09:12:47 +0100 Subject: [PATCH 120/139] fix(beacon-heatsoak): increase minimum threshold from 10 to 12.5 - this slightly extends the implied practical noise floor of the system, and is in response to continued usage and data gathering --- configuration/klippy/beacon_adaptive_heat_soak.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index aa0cc2e05..61a176485 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -69,9 +69,9 @@ def _do_predict_threshold(self, z, p): raise LookupError("Prediction failed, no data available in the model.") t = float(prediction[0]) - # Ensure a minimum threshold of 10.0. From experimental data, we observe that thresholds + # Ensure a minimum threshold of 12.5. From experimental data, we observe that thresholds # below this number approach the noise floor of the system and are not useful. - t = max(t, 10.0) + t = max(t, 12.5) return t def _load_training_data(self): From bcf19ab942cad740bdd3cedbe5d3f171e5155b1c Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 26 Aug 2025 15:31:46 +0100 Subject: [PATCH 121/139] fix(beacon-heatsoak): add retry when preparing for sampling as a speculative workaround for occasional failures that have been observed --- .../klippy/beacon_adaptive_heat_soak.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 61a176485..0510f970e 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -389,6 +389,22 @@ def _handle_connect(self): self.beacon = self.printer.lookup_object('beacon') def _prepare_for_sampling_and_get_sampling_frequency(self): + # During internal testing, we've seen one machine that occasionally fails to prepare + # the beacon for sampling. This retry is a speculative workaround for that issuue. The cause + # is unknown, and we don't know if this will help. Time (and wider usage) will tell. + remaining_attempts = 3 + while remaining_attempts > 0: + try: + return self._prepare_for_sampling_and_get_sampling_frequency_core() + except Exception as e: + remaining_attempts -= 1 + if remaining_attempts == 0: + raise + else: + logging.warning(f"{self.name}: Warning: Failed to prepare beacon for sampling, retrying: {e}") + self.reactor.pause(self.reactor.monotonic() + 2.0) + + def _prepare_for_sampling_and_get_sampling_frequency_core(self): # We've seen issues where the first streaming_session after some operations begins with some bogus data, # so we throw away some samples to ensure the beacon is ready. Suspected operations include: # - klipper restart From de1e847d3bd13207965a00f870c5274f7bbcce21 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 27 Aug 2025 20:30:40 +0100 Subject: [PATCH 122/139] refactor(ratos): move BackgroundDisplayStatusProgressHandler class to ratos.py for general reuse --- .../klippy/beacon_adaptive_heat_soak.py | 70 +------------------ configuration/klippy/ratos.py | 69 ++++++++++++++++++ 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 0510f970e..a40ca6925 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -6,6 +6,7 @@ import time, logging, os, multiprocessing, traceback, pygam import numpy as np +from .ratos import BackgroundDisplayStatusProgressHandler class ThresholdPredictor: def __init__(self, printer): @@ -220,75 +221,6 @@ def get_next_z_rate(self): return (mid_time, slope_nm_per_sec, mid_z) -class BackgroundDisplayStatusProgressHandler: - def __init__( - self, - printer, - msg_fmt = "{spinner} {progress:.0f}%", - display_status_update_interval=0.8, - spinner_sequence="⠋⠙⠹⠸⠼⠴⠦⠧⠇"): - - self.reactor = printer.get_reactor() - self.gcode = printer.lookup_object('gcode') - self.display_status = printer.lookup_object('display_status') - self.display_status_update_interval = display_status_update_interval - self._spinner_sequence = spinner_sequence - self._spinner_phase = 0 - self._timer = None - self._auto_rate_last_eventtime = None - self.msg_fmt = msg_fmt - self._progress = 0.0 - self._auto_rate = 0.0 - - def enable(self): - if self._timer: - return - - self._timer = self.reactor.register_timer( - self._handle_timer, self.reactor.NOW) - - def disable(self): - if self._timer is None: - return - - self.reactor.unregister_timer(self._timer) - self._timer = None - self.display_status.message = None - self.display_status.progress = None - - @property - def progress(self): - return self._progress - - @progress.setter - def progress(self, value): - self._progress = min(1.0, max(0.0, value)) - - def set_auto_rate(self, increment_per_second): - """ - Set the auto rate for the background progress handler. - This is the amount by which the progress will be automatically incremented per second. - """ - self._auto_rate_last_eventtime = None - self._auto_rate = increment_per_second - - def _handle_timer(self, eventtime): - if self._auto_rate_last_eventtime is None: - self._auto_rate_last_eventtime = eventtime - - if self._auto_rate > 0.0: - self._progress = min(1.0, max(0.0, self._progress + self._auto_rate * (eventtime - self._auto_rate_last_eventtime))) - - self._auto_rate_last_eventtime = eventtime - - spinner = self._spinner_sequence[self._spinner_phase] - self._spinner_phase = (self._spinner_phase + 1) % len(self._spinner_sequence) - - if self.msg_fmt is not None: - self.display_status.message = self.msg_fmt.format(progress=self._progress * 100.0, spinner=spinner) - - return self.reactor.monotonic() + self.display_status_update_interval - class RunningAverage: # A running average implementation that maintains a circular buffer of the last `size` values, # and the current sum of the values in the buffer. Methods are provided to add a new value, diff --git a/configuration/klippy/ratos.py b/configuration/klippy/ratos.py index 2ad474a50..179ab743e 100644 --- a/configuration/klippy/ratos.py +++ b/configuration/klippy/ratos.py @@ -800,6 +800,75 @@ def get_formatted_extended_stack_trace(callback=None, skip=0): return "".join(lines) +class BackgroundDisplayStatusProgressHandler: + def __init__( + self, + printer, + msg_fmt = "{spinner} {progress:.0f}%", + display_status_update_interval=0.8, + spinner_sequence="⠋⠙⠹⠸⠼⠴⠦⠧⠇"): + + self.reactor = printer.get_reactor() + self.gcode = printer.lookup_object('gcode') + self.display_status = printer.lookup_object('display_status') + self.display_status_update_interval = display_status_update_interval + self._spinner_sequence = spinner_sequence + self._spinner_phase = 0 + self._timer = None + self._auto_rate_last_eventtime = None + self.msg_fmt = msg_fmt + self._progress = 0.0 + self._auto_rate = 0.0 + + def enable(self): + if self._timer: + return + + self._timer = self.reactor.register_timer( + self._handle_timer, self.reactor.NOW) + + def disable(self): + if self._timer is None: + return + + self.reactor.unregister_timer(self._timer) + self._timer = None + self.display_status.message = None + self.display_status.progress = None + + @property + def progress(self): + return self._progress + + @progress.setter + def progress(self, value): + self._progress = min(1.0, max(0.0, value)) + + def set_auto_rate(self, increment_per_second): + """ + Set the auto rate for the background progress handler. + This is the amount by which the progress will be automatically incremented per second. + """ + self._auto_rate_last_eventtime = None + self._auto_rate = increment_per_second + + def _handle_timer(self, eventtime): + if self._auto_rate_last_eventtime is None: + self._auto_rate_last_eventtime = eventtime + + if self._auto_rate > 0.0: + self._progress = min(1.0, max(0.0, self._progress + self._auto_rate * (eventtime - self._auto_rate_last_eventtime))) + + self._auto_rate_last_eventtime = eventtime + + spinner = self._spinner_sequence[self._spinner_phase] + self._spinner_phase = (self._spinner_phase + 1) % len(self._spinner_sequence) + + if self.msg_fmt is not None: + self.display_status.message = self.msg_fmt.format(progress=self._progress * 100.0, spinner=spinner) + + return self.reactor.monotonic() + self.display_status_update_interval + ##### # Loader ##### From 3b02044c7a57fe55cd41ecfe3c149246528224fe Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 30 Jul 2025 21:23:02 +0100 Subject: [PATCH 123/139] feature(beacon-mesh): cotemporal compensation mesh creation - This replaces the previous implementation that was susceptible to non-uniform thermal effects observed in extended testing. --- configuration/klippy/beacon_mesh.py | 1346 ++++++++++++++++++++++----- configuration/z-probe/beacon.cfg | 34 +- 2 files changed, 1102 insertions(+), 278 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 5200ceb56..2fc36d71a 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,16 +1,24 @@ # Beacaon contact compensation mesh # -# Copyright (C) 2024 Helge Keck +# Copyright (C) 2024 Helge Keck # Copyright (C) 2024-2025 Mikkel Schmidt # Copyright (C) 2025 Tom Glastonbury # # This file may be distributed under the terms of the GNU GPLv3 license. +from enum import Enum +import logging +import math import multiprocessing, traceback from collections import OrderedDict -from . import bed_mesh as BedMesh +from typing import Any, Dict, List, Optional, Tuple, NamedTuple import numpy as np import importlib +from dataclasses import dataclass + +from . import bed_mesh as BedMesh +from . import probe +from .ratos import BeaconProbingRegions, BackgroundDisplayStatusProgressHandler DEFAULT_REACTOR_PAUSE_OFFSET = 0.006 # 6ms @@ -34,12 +42,14 @@ # - a compensated mesh. A measured proximity mesh that was compensated with a compensation mesh. RATOS_MESH_KIND_CHOICES = (RATOS_MESH_KIND_MEASURED, RATOS_MESH_KIND_COMPENSATION, RATOS_MESH_KIND_COMPENSATED) -RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY = "proximity" +RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY = "proximity" # - rapid scan RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC = "proximity_automatic" # - stop and sample (with diving if needed) RATOS_MESH_BEACON_PROBE_METHOD_CONTACT = "contact" -RATOS_MESH_BEACON_PROBE_METHOD_CHOICES = (RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC, RATOS_MESH_BEACON_PROBE_METHOD_CONTACT) +RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_OFFSET_ALIGNED = "cotemporal_offset_aligned" +RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_POINT_BY_POINT = "cotemporal_point_by_point" +RATOS_MESH_BEACON_PROBE_METHOD_CHOICES = (RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC, RATOS_MESH_BEACON_PROBE_METHOD_CONTACT, RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_OFFSET_ALIGNED, RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_POINT_BY_POINT) RATOS_MESH_VERSION_PARAMETER = "ratos_mesh_version" # - versioning of the extra metadata attached to meshes by ratos @@ -64,12 +74,16 @@ RATOS_MESH_KIND_PARAMETER, RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER) +class RatOSBeaconMeshError(Exception): + pass + ##### # Beacon Mesh ##### class BeaconMesh: - bed_temp_warning_margin = 15 + BED_TEMP_WARNING_MARGIN = 15.0 + POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD = 25.0 @staticmethod def format_pretty_list(items, conjunction="or"): @@ -79,7 +93,7 @@ def format_pretty_list(items, conjunction="or"): return items[0] else: return ", ".join(items[:-1]) + f" {conjunction} " + items[-1] - + ##### # Initialize ##### @@ -89,7 +103,8 @@ def __init__(self, config): self.name = config.get_name() self.gcode = self.printer.lookup_object('gcode') self.reactor = self.printer.get_reactor() - + self._cotemporal_probing_helper = CotemporalProbingHelper(self.config) + # These are loaded on klippy:connect. self.beacon = None self.ratos = None @@ -137,33 +152,36 @@ def _connect(self): ##### def register_commands(self): if self.config.has_section("beacon"): - self.gcode.register_command('_BEACON_MESH_INIT', - self.cmd_BEACON_MESH_INIT, - desc=(self.desc_BEACON_MESH_INIT)) - self.gcode.register_command('BEACON_APPLY_SCAN_COMPENSATION', - self.cmd_BEACON_APPLY_SCAN_COMPENSATION, - desc=(self.desc_BEACON_APPLY_SCAN_COMPENSATION)) - self.gcode.register_command('CREATE_BEACON_COMPENSATION_MESH', - self.cmd_CREATE_BEACON_COMPENSATION_MESH, - desc=(self.desc_CREATE_BEACON_COMPENSATION_MESH)) - self.gcode.register_command('SET_ZERO_REFERENCE_POSITION', - self.cmd_SET_ZERO_REFERENCE_POSITION, - desc=(self.desc_SET_ZERO_REFERENCE_POSITION)) - self.gcode.register_command('_CHECK_ACTIVE_BEACON_MODEL_TEMP', - self.cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP, - desc=(self.desc_CHECK_ACTIVE_BEACON_MODEL_TEMP)) + self.gcode.register_command('_BEACON_MESH_INIT', + self.cmd_BEACON_MESH_INIT, + desc=self.desc_BEACON_MESH_INIT) + self.gcode.register_command('BEACON_APPLY_SCAN_COMPENSATION', + self.cmd_BEACON_APPLY_SCAN_COMPENSATION, + desc=self.desc_BEACON_APPLY_SCAN_COMPENSATION) + self.gcode.register_command('_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE', + self.cmd_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE, + desc=self.desc_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE) + self.gcode.register_command('SET_ZERO_REFERENCE_POSITION', + self.cmd_SET_ZERO_REFERENCE_POSITION, + desc=self.desc_SET_ZERO_REFERENCE_POSITION) + self.gcode.register_command('_CHECK_ACTIVE_BEACON_MODEL_TEMP', + self.cmd_CHECK_ACTIVE_BEACON_MODEL_TEMP, + desc=self.desc_CHECK_ACTIVE_BEACON_MODEL_TEMP) self.gcode.register_command('_VALIDATE_COMPENSATION_MESH_PROFILE', - self.cmd_VALIDATE_COMPENSATION_MESH_PROFILE, - desc=(self.desc_VALIDATE_COMPENSATION_MESH_PROFILE)) - self.gcode.register_command('_APPLY_RATOS_BED_MESH_PARAMETERS', - self.cmd_APPLY_RATOS_BED_MESH_PARAMETERS, - desc=(self.desc_APPLY_RATOS_BED_MESH_PARAMETERS)) + self.cmd_VALIDATE_COMPENSATION_MESH_PROFILE, + desc=self.desc_VALIDATE_COMPENSATION_MESH_PROFILE) + self.gcode.register_command('_APPLY_RATOS_BED_MESH_PARAMETERS', + self.cmd_APPLY_RATOS_BED_MESH_PARAMETERS, + desc=self.desc_APPLY_RATOS_BED_MESH_PARAMETERS) self.gcode.register_command('GET_RATOS_EXTENDED_BED_MESH_PARAMETERS', - self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, - desc=(self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS)) + self.cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS, + desc=self.desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS) self.gcode.register_command('_TEST_COMPENSATION_MESH_AUTO_SELECTION', - self.cmd_TEST_COMPENSATION_MESH_AUTO_SELECTION, - desc=(self.desc_TEST_COMPENSATION_MESH_AUTO_SELECTION)) + self.cmd_TEST_COMPENSATION_MESH_AUTO_SELECTION, + desc=self.desc_TEST_COMPENSATION_MESH_AUTO_SELECTION) + self.gcode.register_command('_BED_MESH_SUBTRACT', + self.cmd_BED_MESH_SUBTRACT, + desc=self.desc_BED_MESH_SUBTRACT) desc_BEACON_MESH_INIT = "Performs Beacon mesh initialization tasks" def cmd_BEACON_MESH_INIT(self, gcmd): @@ -171,7 +189,7 @@ def cmd_BEACON_MESH_INIT(self, gcmd): if self.bed_mesh: # Load additional RatOS mesh params self.load_extra_mesh_params() - # run klippers inompatible profile check which is never called by bed_mesh + # run klippers incompatible profile check which is never called by bed_mesh self.bed_mesh.pmgr._check_incompatible_profiles() desc_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS = "Writes the extended RatOS bed mesh parameters to console for the active bed mesh" @@ -179,7 +197,7 @@ def cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS(self, gcmd): if self.bed_mesh is None: gcmd.respond_info("The [bed_mesh] component is not active") return - + mesh = self.bed_mesh.get_mesh() if mesh is None: gcmd.respond_info("There is no active bed mesh") @@ -195,7 +213,7 @@ def cmd_GET_RATOS_EXTENDED_BED_MESH_PARAMETERS(self, gcmd): def cmd_APPLY_RATOS_BED_MESH_PARAMETERS(self, gcmd): # This should only be called by our override of BED_MESH_CALIBRATE immediately after the call to the original # macro, and with the same rawargs as passed to BED_MESH_CALIBRATE. - + mesh = self.bed_mesh.get_mesh() if mesh is None: raise gcmd.error("Expected an active bed mesh, but there is none") @@ -211,7 +229,7 @@ def cmd_APPLY_RATOS_BED_MESH_PARAMETERS(self, gcmd): ratos_probe_method = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC if method == "automatic" else RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY else: ratos_probe_method = RATOS_MESH_BEACON_PROBE_METHOD_CONTACT - + bed_temp = self._get_nominal_bed_temp() params = mesh.get_mesh_params() @@ -226,10 +244,10 @@ def cmd_APPLY_RATOS_BED_MESH_PARAMETERS(self, gcmd): f"{RATOS_MESH_BED_TEMP_PARAMETER}: {params[RATOS_MESH_BED_TEMP_PARAMETER]}_N_" f"{RATOS_MESH_KIND_PARAMETER}: {params[RATOS_MESH_KIND_PARAMETER]}_N_" f"{RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER}: {params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]}") - + self.ratos.debug_echo("_APPLY_RATOS_BED_MESH_PARAMETERS_FOR_MEASURED", msg) - self.bed_mesh.pmgr.save_profile( mesh.get_profile_name() ) + self.bed_mesh.pmgr.save_profile( mesh.get_profile_name() ) def _get_nominal_bed_temp(self): target_temp = self.heater_bed.heater.target_temp if self.heater_bed else 0. @@ -251,17 +269,17 @@ def check_active_beacon_model_temp(self, margin=20, title='Active Beacon model t model_temp = self.beacon.model.temp if coil_temp < model_temp - margin or coil_temp > model_temp + margin: - self.ratos.console_echo(title, "warning", + self.ratos.console_echo(title, "warning", "The active Beacon model ('%s') is calibrated for a temperature that is %0.2fC different than the current Beacon coil temperature._N_" "This may result in inaccurate compensation." % (self.beacon.model.name, abs(coil_temp - model_temp))) desc_VALIDATE_COMPENSATION_MESH_PROFILE = "Raises an error if the speficied profile is not a valid compensation mesh, and warns if there is a significant temperature difference" def cmd_VALIDATE_COMPENSATION_MESH_PROFILE(self, gcmd): - + profile = gcmd.get("PROFILE").strip() if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") - + title = gcmd.get("TITLE", "Validate compensation mesh profile") subject = gcmd.get("SUBJECT", None) bed_temp = gcmd.get_float("COMPARE_BED_TEMP", None) @@ -293,13 +311,13 @@ def get_profiles(self, kind=None): for profile_name, profile in profiles.items(): params = profile["mesh_params"] # Consider only RatOS-valid profiles - if RATOS_MESH_VERSION_PARAMETER in params: + if RATOS_MESH_VERSION_PARAMETER in params: if kind is None or params[RATOS_MESH_KIND_PARAMETER] == kind: result[profile_name] = profile - + return result - def auto_select_compensation_mesh(self, bed_temperature=None): + def auto_select_compensation_mesh(self, bed_temperature=None): # Automatically selects a compensation mesh based on the specified bed_temperature, or the # current target bed temperature if bed_temperature is None. @@ -310,13 +328,13 @@ def auto_select_compensation_mesh(self, bed_temperature=None): profiles = self.get_profiles(RATOS_MESH_KIND_COMPENSATION) if not profiles: - self.ratos.console_echo("Auto-select compensation mesh error", "error", + self.ratos.console_echo("Auto-select compensation mesh error", "error", "No compensation mesh profiles found. Create a compensation mesh, or disable the_N_" "Beacon compensation mesh feature._N_" + link_line) raise self.printer.command_error("No compensation mesh profiles found") - + if bed_temperature is None: bed_temperature = self._get_nominal_bed_temp() @@ -327,23 +345,23 @@ def auto_select_compensation_mesh(self, bed_temperature=None): # Find the closest compensation mesh profile based on bed temperature best_profiles = [] best_temp_diff = float('inf') - + for profile_name, profile in profiles.items(): params = profile["mesh_params"] profile_bed_temp = params[RATOS_MESH_BED_TEMP_PARAMETER] temp_diff = abs(profile_bed_temp - bed_temperature) - + if temp_diff < best_temp_diff: best_temp_diff = temp_diff best_profiles = [(profile_name, profile_bed_temp)] elif temp_diff == best_temp_diff: best_profiles.append((profile_name, profile_bed_temp)) - + # If there are multiple candidate profiles with the same bed temperature, then the result # is ambiguous, which is considered an error. distinct_bed_temps = set(temp for _, temp in best_profiles) if len(distinct_bed_temps) != len(best_profiles): - self.ratos.console_echo("Auto-select compensation mesh error", "error", + self.ratos.console_echo("Auto-select compensation mesh error", "error", "A compensation mesh cannot be selected automatically because there is more than one equally-suitable profile._N_" "Either delete one of the following profiles, or configure the desired profile explicitly:_N_" + "_N_".join(f" '{name}' ({temp}°C)" for name, temp in best_profiles) @@ -355,8 +373,8 @@ def auto_select_compensation_mesh(self, bed_temperature=None): best_profile, best_temp = max(best_profiles, key=lambda x: x[1]) # Check if the temperature difference is too large - if best_temp_diff > self.bed_temp_warning_margin: - self.ratos.console_echo("Auto-select compensation mesh warning", "warning", + if best_temp_diff > self.BED_TEMP_WARNING_MARGIN: + self.ratos.console_echo("Auto-select compensation mesh warning", "warning", f"Selected compensation mesh '{best_profile}' has a bed temperature of {best_temp}°C, " f"which differs by {best_temp_diff:.1f}°C from the requested {bed_temperature:.1f}°C._N_" "This may result in inaccurate compensation." @@ -365,7 +383,7 @@ def auto_select_compensation_mesh(self, bed_temperature=None): self.gcode.respond_info( f"Selected compensation mesh '{best_profile}' with bed temperature {best_temp}°C " f"(requested: {bed_temperature:.1f}°C, difference: {best_temp_diff:.1f}°C)") - + return best_profile desc_TEST_COMPENSATION_MESH_AUTO_SELECTION = "Tests the automatic selection of a compensation mesh. Will raise an error if no suitable mesh is found." @@ -382,7 +400,7 @@ def cmd_BEACON_APPLY_SCAN_COMPENSATION(self, gcmd): profile = gcmd.get('PROFILE', RATOS_COMPENSATION_MESH_NAME_AUTO).strip() if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") - + if not self.apply_scan_compensation(profile): raise self.printer.command_error("Could not apply scan compensation") @@ -393,46 +411,91 @@ def _get_unique_profile_name(self, base_name): profiles = self.bed_mesh.pmgr.get_profiles() if base_name not in profiles: return (base_name, True) - + i = 1 while f"{base_name}_{i}" in profiles: i += 1 - + return (f"{base_name}_{i}", False) - - desc_CREATE_BEACON_COMPENSATION_MESH = "Creates the beacon compensation mesh by calibrating and diffing a contact and a scan mesh." - def cmd_CREATE_BEACON_COMPENSATION_MESH(self, gcmd): + + desc_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE = \ + "Do not invoke this command directly, use BEACON_CREATE_SCAN_COMPENSATION_MESH instead." \ + "Performs the core operation of creating a beacon compensation mesh based on the difference between proximity and contact probes." + def cmd_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE(self, gcmd): + if not self.beacon: + self.ratos.console_echo("Create compensation mesh error", "error", + "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") + raise gcmd.error("Beacon module not loaded") + profile = gcmd.get('PROFILE', RATOS_COMPENSATION_MESH_NAME_AUTO).strip() - # Using minval=4 to avoid BedMesh defaulting to using Lagrangian interpolation which appears to be broken - probe_count = BedMesh.parse_gcmd_pair(gcmd, 'PROBE_COUNT', minval=4) + + if gcmd.get('PROBE_COUNT', None) is not None: + # Sanity check: RatOS scripts know about this, and this command should not be called directly by users, + # but just in case... + raise gcmd.error("Parameter 'PROBE_COUNT' is no longer supported.") + + desired_spacing = gcmd.get_float("DESIRED_SPACING", float(self.gm_ratos.variables.get('beacon_scan_compensation_desired_spacing', 10.))) + minimum_spacing = gcmd.get_float("MINIMUM_SPACING", desired_spacing * 0.8) chamber_temp = gcmd.get_float('CHAMBER_TEMP', 0) + + if desired_spacing < minimum_spacing: + raise gcmd.error("Parameter 'DESIRED_SPACING' must be greater than or equal to 'MINIMUM_SPACING'") if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") - if not probe_count: - raise gcmd.error("Value for parameter 'PROBE_COUNT' must be specified") - if profile.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: base_name = f"compensation_bed_{round(self._get_nominal_bed_temp())}C" profile, is_unique = self._get_unique_profile_name(base_name) if not is_unique: - self.ratos.console_echo("Create beacon compensation mesh", "info", + self.ratos.console_echo("Create beacon compensation mesh", "info", f"The default automatic profile name '{base_name}' already exists. The unique name '{profile}' will be used instead.") gcmd.respond_info(f"Using automatic profile name '{profile}' for the new compensation mesh") - - self.create_compensation_mesh(gcmd, profile, probe_count, chamber_temp) + + if self.z_tilt and not self.z_tilt.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Z-tilt leveling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + + if self.qgl and not self.qgl.z_status.applied: + self.ratos.console_echo("Create compensation mesh warning", "warning", + "Quad gantry leveling is configured but has not been applied._N_" + "This may result in inaccurate compensation.") + + keep_temp_meshes = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') + + logging.info(f"{self.name}: keep_temp_meshes: {keep_temp_meshes}") + + beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' + + # Go to safe home + self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") + + if beacon_contact_calibrate_model_on_print: + # Calibrate a fresh model + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1") + else: + if self.beacon.model is None: + self.ratos.console_echo("Create compensation mesh error", "error", + "No active Beacon model is selected._N_Make sure you've performed initial Beacon calibration.") + raise gcmd.error("No active Beacon model selected") + + self.check_active_beacon_model_temp(title="Create compensation mesh warning") + + self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 SKIP_MODEL_CREATION=1") + + self.create_compensation_mesh(gcmd, profile, desired_spacing, minimum_spacing, chamber_temp, keep_temp_meshes) desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): if (self.bed_mesh.z_mesh is None): - self.ratos.console_echo("Set zero reference position error", "error", + self.ratos.console_echo("Set zero reference position error", "error", "No bed mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") return - + x_pos = gcmd.get_float('X') - y_pos = gcmd.get_float('Y') - + y_pos = gcmd.get_float('Y') + self.ratos.debug_echo("SET_ZERO_REFERENCE_POSITION", f"X:{x_pos:.2f} Y:{y_pos:.2f}") org_mesh = self.bed_mesh.get_mesh() @@ -440,38 +503,38 @@ def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): new_mesh.build_mesh(org_mesh.get_probed_matrix()) new_mesh.set_zero_reference(x_pos, y_pos) self.bed_mesh.set_mesh(new_mesh) - + self.bed_mesh.pmgr.save_profile(new_mesh.get_profile_name()) - self.ratos.console_echo("Set zero reference position", "info", + self.ratos.console_echo("Set zero reference position", "info", f"Zero reference position saved for profile '{new_mesh.get_profile_name()}'") def _create_zmesh_from_profile(self, profile, subject=None, purpose=None): if not profile: raise TypeError("Argument profile cannot be None") - + if subject is None: subject = f"Profile '{profile}'" if purpose: purpose = f" for {purpose}" - + profiles = self.bed_mesh.pmgr.get_profiles() if profile not in profiles: raise self.printer.command_error(f"{subject} not found{purpose}") - + try: - compensation_zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile, self.reactor) - compensation_zmesh.build_mesh(profiles[profile]["points"]) - return compensation_zmesh + zmesh = BedMesh.ZMesh(profiles[profile]["mesh_params"], profile, self.reactor) + zmesh.build_mesh(profiles[profile]["points"]) + return zmesh except Exception as e: raise self.printer.command_error(f"Could not load {subject[0].lower()}{subject[1:]}{purpose}: {str(e)}") from e # Logs to console for any problems with extended mesh parameters. Returns True if the extended parameters are present - # and valid, otherwise False. Version must be the current version. - def _validate_extended_parameters(self, + # and valid, otherwise False. Version must be the current version. + def _validate_extended_parameters(self, params, title, - subject="Mesh", + subject="Mesh", compare_bed_temp=None, compare_bed_temp_is_error=False, allowed_kinds=RATOS_MESH_KIND_CHOICES, @@ -479,7 +542,7 @@ def _validate_extended_parameters(self, if not params: raise TypeError("Argument params cannot be None") - + # - Earlier versions stored in config will have been migrated where possible by load_extra_mesh_params() # - load_extra_mesh_params() will only deserialize and apply a valid config, never a partial or unmigratable config. # - the only scenario where we should encounter a partial or invalid set of params is when they have been @@ -491,59 +554,59 @@ def _validate_extended_parameters(self, if not all(p in params for p in RATOS_REQUIRED_MESH_PARAMETERS): missing = [p for p in RATOS_REQUIRED_MESH_PARAMETERS if p not in params] self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"missing parameters: {', '.join(missing)}") - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} has incomplete extended metadata.") return False - + if params[RATOS_MESH_VERSION_PARAMETER] != RATOS_MESH_VERSION: - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} is not compatible with this version of RatOS.") return False - + if params[RATOS_MESH_KIND_PARAMETER] not in RATOS_MESH_KIND_CHOICES: self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_KIND_PARAMETER} value '{params[RATOS_MESH_KIND_PARAMETER]}'") - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} has invalid extended metadata.") return False if params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] not in RATOS_MESH_BEACON_PROBE_METHOD_CHOICES: self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER} value '{params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]}'") - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} has invalid extended metadata.") return False bed_temp = params[RATOS_MESH_BED_TEMP_PARAMETER] if not isinstance(bed_temp, float): self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BED_TEMP_PARAMETER} value type {type(params[RATOS_MESH_BED_TEMP_PARAMETER])}") - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} has invalid extended metadata.") return False - + if bed_temp < 0: self.ratos.debug_echo("BeaconMesh._validate_extended_parameters", f"invalid {RATOS_MESH_BED_TEMP_PARAMETER} value {bed_temp}") - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} has invalid extended metadata.") return False if params[RATOS_MESH_KIND_PARAMETER] not in allowed_kinds: - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} must be a {self.format_pretty_list(allowed_kinds)} mesh. A {params[RATOS_MESH_KIND_PARAMETER]} mesh cannot be used.") return False if params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] not in allowed_probe_methods: - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"{subject} must be a {self.format_pretty_list(allowed_probe_methods)} probe method mesh. A {params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER]} probe method mesh cannot be used.") return False - if compare_bed_temp is not None and (compare_bed_temp < bed_temp - self.bed_temp_warning_margin or compare_bed_temp > bed_temp + self.bed_temp_warning_margin): + if compare_bed_temp is not None and (compare_bed_temp < bed_temp - self.BED_TEMP_WARNING_MARGIN or compare_bed_temp > bed_temp + self.BED_TEMP_WARNING_MARGIN): self.ratos.console_echo( error_title if compare_bed_temp_is_error else warning_title, - "error" if compare_bed_temp_is_error else "warning", + "error" if compare_bed_temp_is_error else "warning", f"{subject} was created with a bed temperature that differs by {abs(bed_temp - compare_bed_temp)}._N_" "This may result in innaccurate compensation.") if compare_bed_temp_is_error: return False - + return True ##### @@ -552,16 +615,16 @@ def _validate_extended_parameters(self, def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: if not comp_mesh_profile_name: raise TypeError("Argument comp_mesh_profile_name must be provided") - + error_title = "Apply scan compensation error" try: measured_zmesh = self.bed_mesh.z_mesh - + if not measured_zmesh: - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", "No mesh loaded._N_Either generate a new bed mesh or load it via BED_MESH_PROFILE LOAD=\"[profile_name]\"") return False - + measured_mesh_params = measured_zmesh.get_mesh_params() measured_mesh_name = measured_zmesh.get_profile_name() measured_mesh_bed_temp = measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER] @@ -576,11 +639,11 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: if comp_mesh_profile_name.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: comp_mesh_profile_name = self.auto_select_compensation_mesh(measured_mesh_bed_temp) - - compensation_zmesh = self._create_zmesh_from_profile(comp_mesh_profile_name, purpose="Beacon scan compensation") + + compensation_zmesh = self._create_zmesh_from_profile(comp_mesh_profile_name, purpose="Beacon scan compensation") compensation_mesh_params = compensation_zmesh.get_mesh_params() compensation_mesh_name = compensation_zmesh.get_profile_name() - + if not self._validate_extended_parameters( compensation_mesh_params, "Apply scan compensation", @@ -588,9 +651,9 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: compare_bed_temp=measured_mesh_bed_temp, allowed_kinds=(RATOS_MESH_KIND_COMPENSATION,)): return False - + if measured_mesh_name == compensation_mesh_name: - self.ratos.console_echo(error_title, "error", + self.ratos.console_echo(error_title, "error", f"Compensation profile name '{compensation_mesh_name}' is the same as the scan profile name '{measured_mesh_name}'") return False @@ -623,16 +686,16 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: self.bed_mesh.save_profile(measured_mesh_name) self.bed_mesh.set_mesh(measured_zmesh) - self.ratos.console_echo("Beacon scan compensation", "debug", + self.ratos.console_echo("Beacon scan compensation", "debug", f"Measured mesh '{measured_mesh_name}' compensated with compensation mesh '{compensation_mesh_name}'") - + return True - + except BedMesh.BedMeshError as e: self.ratos.console_echo(error_title, "error", str(e)) return False - def _apply_filter(self, data): + def _apply_local_low_filter(self, data): parent_conn, child_conn = multiprocessing.Pipe() def do(): @@ -655,7 +718,7 @@ def do(): child.join() parent_conn.close() if is_err: - raise Exception("Error applying filter: %s" % (result,)) + raise Exception("Error applying local-low filter: %s" % (result,)) else: return result @@ -669,9 +732,9 @@ def _gaussian_filter(self, data, sigma, mode): "module is required for Beacon contact compensation mesh creation." ) - return self.scipy_ndimage.gaussian_filter(data, sigma=sigma, mode=mode) + return self.scipy_ndimage.gaussian_filter(data, sigma=sigma, mode=mode) - def _do_local_low_filter(self, data, lowpass_sigma=1., num_keep=4, num_keep_edge=3, num_keep_corner=2): + def _do_local_low_filter(self, data, lowpass_sigma=1.): # 1. Low-pass filter to obtain general shape lowpass = self._gaussian_filter(data, sigma=lowpass_sigma, mode='nearest') @@ -686,12 +749,12 @@ def _do_local_low_filter(self, data, lowpass_sigma=1., num_keep=4, num_keep_edge rows, cols = data.shape for i in range(rows): for j in range(cols): - # Get the 3x3 neighborhood around the current point within the high-frequency details + # Get the 5x5 neighborhood around the current point within the high-frequency details neighbours = [] neighbour_coords = [] neighbour_distances = [] - for di in [-1, 0, 1]: - for dj in [-1, 0, 1]: + for di in [-2, -1, 0, 1, 2]: + for dj in [-2, -1, 0, 1, 2]: ni, nj = i + di, j + dj if 0 <= ni < rows and 0 <= nj < cols: neighbours.append(high_freq_details[ni, nj]) @@ -699,7 +762,7 @@ def _do_local_low_filter(self, data, lowpass_sigma=1., num_keep=4, num_keep_edge neighbour_distances.append((di**2 + dj**2)**0.5) # Identify the indices of the N lowest values from the neighborhood - lowest_indices = np.argsort(neighbours)[:num_keep if len(neighbours) > 6 else num_keep_edge if len(neighbours) > 4 else num_keep_corner] + lowest_indices = np.argsort(neighbours)[:math.floor(len(neighbours) / 2)] # Select the corresponding values from the original array lowest_values = [data[neighbour_coords[idx]] for idx in lowest_indices] @@ -715,169 +778,231 @@ def _do_local_low_filter(self, data, lowpass_sigma=1., num_keep=4, num_keep_edge # 5. Return the new array. Don't leak numpy types to the caller. return filtered_data.tolist() - - def create_compensation_mesh(self, gcmd, profile, probe_count, chamber_temp): - if not self.beacon: - self.ratos.console_echo("Create compensation mesh error", "error", - "Beacon module not loaded._N_Make sure you've configured Beacon as your z probe.") - return - if self.z_tilt and not self.z_tilt.z_status.applied: - self.ratos.console_echo("Create compensation mesh warning", "warning", - "Z-tilt leveling is configured but has not been applied._N_" - "This may result in inaccurate compensation.") - - if self.qgl and not self.qgl.z_status.applied: - self.ratos.console_echo("Create compensation mesh warning", "warning", - "Quad gantry leveling is configured but has not been applied._N_" - "This may result in inaccurate compensation.") - - keep_temp_meshes = gcmd.get('KEEP_TEMP_MESHES', '0').strip().lower() in ('1', 'true', 'yes') - samples = gcmd.get_int('SAMPLES', 1) - samples_drop = gcmd.get_int('SAMPLES_DROP', 0) - - gcmd.respond_info(f"keep_temp_meshes: {keep_temp_meshes}, samples: {samples} samples_drop: {samples_drop}") - - beacon_contact_calibrate_model_on_print = str(self.gm_ratos.variables['beacon_contact_calibrate_model_on_print']).lower() == 'true' - - # Go to safe home - self.gcode.run_script_from_command("_MOVE_TO_SAFE_Z_HOME Z_HOP=True") - - if beacon_contact_calibrate_model_on_print: - # Calibrate a fresh model - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1") - else: - if self.beacon.model is None: - self.ratos.console_echo("Create compensation mesh error", "error", - "No active Beacon model is selected._N_Make sure you've performed initial Beacon calibration.") - return + def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spacing, chamber_temp, keep_temp_meshes): + try: + bpr: BeaconProbingRegions = self.ratos.get_beacon_probing_regions() + safe_min_x = max(bpr.proximity_min[0], bpr.contact_min[0]) + safe_max_x = min(bpr.proximity_max[0], bpr.contact_max[0]) + safe_min_y = max(bpr.proximity_min[1], bpr.contact_min[1]) + safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) + + if (bpr.contact_min != bpr.proximity_min or bpr.contact_max != bpr.proximity_max): + logging.info(f'{self.name}: Beacon probing regions contact and proximity bounds do not match, the compensation mesh bounds will be reduced to the intersecting region.') + + use_offset_aligned = self._cotemporal_probing_helper.can_use_offset_aligned_probing(minimum_spacing) + skip_local_low_filter = False + primary_axis = None + extra_notes = "" + + if use_offset_aligned: + pattern = "offset-aligned" + primary_axis, probe_count_x, probe_count_y, max_x, max_y, actions = self._cotemporal_probing_helper.generate_probe_action_sequence_beacon_offset_aligned( + desired_spacing, + minimum_spacing, + (safe_min_x, safe_min_y), + (safe_max_x, safe_max_y) + ) - self.check_active_beacon_model_temp(title="Create compensation mesh warning") - - self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 SKIP_MODEL_CREATION=1") + gcmd.respond_info( + f"Using {pattern} cotemporal probing strategy:\n" + f"Generated {len(actions)} probe actions for the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})\n" + f"Mesh points: {probe_count_x} x {probe_count_y}, max coordinates: ({max_x:.2f}, {max_y:.2f})") - mesh_before_name = RATOS_TEMP_SCAN_MESH_BEFORE_NAME if not keep_temp_meshes else profile + "_SCAN_BEFORE" - mesh_after_name = RATOS_TEMP_SCAN_MESH_ATFER_NAME if not keep_temp_meshes else profile + "_SCAN_AFTER" - contact_mesh_name = RATOS_TEMP_CONTACT_MESH_NAME if not keep_temp_meshes else profile + "_CONTACT" + # TODO: Handle faulty regions! - # create 'before' temp scan mesh - self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE " - "PROFILE='%s'" % (mesh_before_name)) + progress_handler = None + try: + progress_handler = BackgroundDisplayStatusProgressHandler(self.printer, "{spinner} Probing {progress:.1f}%") + progress_handler.enable() + + results = self._cotemporal_probing_helper.run_probe_action_sequence( + gcmd, + probe_count_x, probe_count_y, + actions, + progress_handler=progress_handler + ) + finally: + if progress_handler: + progress_handler.disable() + + contact_points = [[results[y][x].contact_z for x in range(len(results[y]))] for y in range(len(results))] + proximity_points = [[results[y][x].proximity_z for x in range(len(results[y]))] for y in range(len(results))] + + else: + # This is the simple but slow point-by-point probing strategy. + # Note that the filtering logic is geared towards quite high-resolution meshes (eg, 10mm spacing). + # Low-resolution meshes are not recommended. + pattern = "point-by-point" + + # Require at least 4 points in each axis to avoid breaking filter and interpolation logic. + probe_count_x = max(4, int((safe_max_x - safe_min_x) / desired_spacing + 1)) + probe_count_y = max(4, int((safe_max_y - safe_min_y) / desired_spacing + 1)) + + # There's some rounding of the distance between points, so the actual max coordinates are + # returned by generate_mesh_points. + max_x, max_y, points = self.generate_mesh_points( + probe_count_x, probe_count_y, + [safe_min_x, safe_min_y], + [safe_max_x, safe_max_y]) + + x_spacing = (max_x - safe_min_x) / (probe_count_x - 1) + y_spacing = (max_y - safe_min_y) / (probe_count_y - 1) + + force_multipoint_probing = ( + x_spacing > self.POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD or + y_spacing > self.POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD + ) - # create contact mesh - self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE PROBE_METHOD=contact SAMPLES=%d SAMPLES_DROP=%d SAMPLES_TOLERANCE_RETRIES=10 " - "PROBE_COUNT=%d,%d PROFILE='%s'" % (samples, samples_drop, probe_count[0], probe_count[1], contact_mesh_name)) + if force_multipoint_probing: + skip_local_low_filter = True + extra_notes += ", using multi-sample probing due to large spacing (local-low filter skipped)" + logging.info(f"{self.name}: Using multi-sample probing for point-by-point probing strategy due to large spacing (x_spacing: {x_spacing:.2f}, y_spacing: {y_spacing:.2f})") - # create 'after' temp scan mesh - self.gcode.run_script_from_command( - "BED_MESH_CALIBRATE " - "PROFILE='%s'" % (mesh_after_name)) + contact_z = None + results = [[None] * probe_count_x for _ in range(probe_count_y)] - scan_before_zmesh = self._create_zmesh_from_profile(mesh_before_name) - scan_after_zmesh = self._create_zmesh_from_profile(mesh_after_name) - scan_mesh_params = scan_before_zmesh.get_mesh_params() - scan_mesh_bounds = (scan_mesh_params["min_x"], scan_mesh_params["min_y"], - scan_mesh_params["max_x"], scan_mesh_params["max_y"]) - - self.gcode.run_script_from_command("BED_MESH_PROFILE LOAD='%s'" % contact_mesh_name) - - contact_mesh_points = self.bed_mesh.pmgr.get_profiles()[contact_mesh_name]["points"][:] - contact_params = self.bed_mesh.z_mesh.get_mesh_params() - contact_x_step = ((contact_params["max_x"] - contact_params["min_x"]) / (contact_params["x_count"] - 1)) - contact_y_step = ((contact_params["max_y"] - contact_params["min_y"]) / (contact_params["y_count"] - 1)) - - self.ratos.debug_echo("Create compensation mesh", "Filtering contact mesh") - contact_mesh_points = self._apply_filter(contact_mesh_points) - contact_params[RATOS_MESH_NOTES_PARAMETER] = "contact mesh filtered using local low filter" - - compensation_mesh_points = [] - - eventtime = self.reactor.monotonic() - - try: - if not self.beacon.mesh_helper.dir in ("x", "y"): - raise ValueError(f"Expected 'x' or 'y' for self.beacon.mesh_helper.dir, but got '{self.beacon.mesh_helper.dir}'") - - dir = self.beacon.mesh_helper.dir - y_count = len(contact_mesh_points) - x_count = len(contact_mesh_points[0]) - contact_mesh_point_count = len(contact_mesh_points) * len(contact_mesh_points[0]) - - debug_lines = [] - - for y in range(y_count): - compensation_mesh_points.append([]) - for x in range(x_count): - contact_mesh_index = \ - ((x if y % 2 == 0 else x_count - x - 1) + y * x_count) \ - if dir == "x" else \ - ((y if x % 2 == 0 else y_count - y - 1) + x * y_count) - - blend_factor = contact_mesh_index / (contact_mesh_point_count - 1) - - contact_x_pos = contact_params["min_x"] + x * contact_x_step - contact_y_pos = contact_params["min_y"] + y * contact_y_step - - scan_before_z = scan_before_zmesh.calc_z(contact_x_pos, contact_y_pos) - scan_after_z = scan_after_zmesh.calc_z(contact_x_pos, contact_y_pos) - scan_temporal_crossfade_z = ((1 - blend_factor) * scan_before_z) + (blend_factor * scan_after_z) + gcmd.respond_info( + f"Using {pattern} cotemporal probing strategy:\n" + f"Generated {len(points)} probe points for the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})\n" + f"Mesh points: {probe_count_x} x {probe_count_y}, max coordinates: ({max_x:.2f}, {max_y:.2f}), spacing: ({x_spacing:.2f}, {y_spacing:.2f})" + + (", using multi-sample probing due to large spacing" if force_multipoint_probing else "")) + + # TODO: Handle faulty regions! - contact_z = contact_mesh_points[y][x] - offset_z = contact_z - scan_temporal_crossfade_z + progress_handler = None + try: + progress_handler = BackgroundDisplayStatusProgressHandler(self.printer, "{spinner} Probing {progress:.1f}%") + progress_handler.enable() - compensation_mesh_points[y].append(offset_z) + for i, point in enumerate(points): + progress_handler.progress = (i + 1) / len(points) - #debug_lines.append( f"xi: {x} yi: {y} x: {contact_x_pos:.1f} y: {contact_y_pos:.1f} cmi: {contact_mesh_index} blend: {blend_factor:.3f} scan_before: {scan_before_z:.4f} scan_after: {scan_after_z:.4f} blended_scan_z: {scan_temporal_crossfade_z:.4f} contact_z: {contact_z:.4f} offset_z: {offset_z:.4f}") + contact_z, proximity_z = self._cotemporal_probing_helper.probe_single_location( + gcmd, + point[2:], + None if force_multipoint_probing else contact_z) - self.reactor.pause(self.reactor.monotonic() + DEFAULT_REACTOR_PAUSE_OFFSET) + results[point[1]][point[0]] = (point[2], point[3], contact_z, proximity_z) + finally: + if progress_handler: + progress_handler.disable() - # For a large mesh (eg, 60x60) this can take 2+ minutes - #self.ratos.debug_echo("Create compensation mesh", "_N_".join(debug_lines)) + gcmd.respond_info(f"Probed {len(points)} points in the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})") - if keep_temp_meshes: - params = contact_params.copy() - filtered_profile = contact_mesh_name + "_filtered" - new_mesh = BedMesh.ZMesh(params, filtered_profile, self.reactor) - new_mesh.build_mesh(contact_mesh_points) - self.bed_mesh.set_mesh(new_mesh) - self.bed_mesh.save_profile(filtered_profile) + contact_points = [[results[y][x][2] for x in range(len(results[y]))] for y in range(len(results))] + proximity_points = [[results[y][x][3] for x in range(len(results[y]))] for y in range(len(results))] - # Create new mesh - params = contact_params.copy() - params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION - params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() - params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION - params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY + extra_params = {} + extra_params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + extra_params[RATOS_MESH_BED_TEMP_PARAMETER] = self._get_nominal_bed_temp() + extra_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_MEASURED + extra_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_OFFSET_ALIGNED if use_offset_aligned else RATOS_MESH_BEACON_PROBE_METHOD_COTEMPORAL_POINT_BY_POINT + extra_params[RATOS_MESH_NOTES_PARAMETER] = f"input mesh for cotemporal mesh created using {pattern} sampling pattern" # Store a few fields that might be useful for compatibility checking in the future, # but the checks don't yet exist. - params[RATOS_MESH_CHAMBER_TEMP_PARAMETER] = chamber_temp - params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = scan_mesh_bounds + extra_params[RATOS_MESH_CHAMBER_TEMP_PARAMETER] = chamber_temp + extra_params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = (safe_min_x, safe_min_y, safe_max_x, safe_max_y) + + if primary_axis is not None: + deridged_contact_points = self._apply_deridging_filter(contact_points, primary_axis) + deridged_proximity_points = self._apply_deridging_filter(proximity_points, primary_axis) + + contact_rmse = self._get_mesh_difference_rmse(contact_points, deridged_contact_points) + proximity_rmse = self._get_mesh_difference_rmse(proximity_points, deridged_proximity_points) + + extra_notes += f", deridged (primary axis: {primary_axis}, contact RMSE: {contact_rmse:.4f}, proximity RMSE: {proximity_rmse:.4f})" + + filtered_contact_points = deridged_contact_points if skip_local_low_filter else self._apply_local_low_filter(deridged_contact_points) + + if keep_temp_meshes: + self._install_and_save_new_mesh( + f"{profile}_CONTACT", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + contact_points + ) + + self._install_and_save_new_mesh( + f"{profile}_CONTACT_DERIDGED", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + deridged_contact_points + ) + + self._install_and_save_new_mesh( + f"{profile}_PROXIMITY", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + proximity_points + ) + + self._install_and_save_new_mesh( + f"{profile}_PROXIMITY_DERIDGED", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + deridged_proximity_points + ) + + proximity_points = deridged_proximity_points + else: + filtered_contact_points = contact_points if skip_local_low_filter else self._apply_local_low_filter(contact_points) + + if keep_temp_meshes: + self._install_and_save_new_mesh( + f"{profile}_CONTACT", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + contact_points + ) + + self._install_and_save_new_mesh( + f"{profile}_PROXIMITY", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + proximity_points + ) + + if keep_temp_meshes and not skip_local_low_filter: + self._install_and_save_new_mesh( + f"{profile}_CONTACT_FILTERED", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + filtered_contact_points + ) - new_mesh = BedMesh.ZMesh(params, profile, self.reactor) - new_mesh.build_mesh(compensation_mesh_points) - self.bed_mesh.set_mesh(new_mesh) - self.bed_mesh.save_profile(profile) + comp_points = [[filtered_contact_points[y][x] - proximity_points[y][x] for x in range(len(proximity_points[y]))] for y in range(len(proximity_points))] + extra_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_COMPENSATION + extra_params[RATOS_MESH_NOTES_PARAMETER] = f"cotemporal compensation mesh created using {pattern} sampling pattern{extra_notes}" - if not keep_temp_meshes: - # Remove temp meshes - self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % contact_mesh_name) - self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % mesh_before_name) - self.gcode.run_script_from_command("BED_MESH_PROFILE REMOVE='%s'" % mesh_after_name) + self._install_and_save_new_mesh( + f"{profile}", + extra_params, + (safe_min_x, safe_min_y), + (max_x, max_y), + comp_points + ) - self.ratos.console_echo("Create compensation mesh", "debug", "Compensation Mesh %s created" % (str(profile))) - except BedMesh.BedMeshError as e: - self.ratos.console_echo("Create compensation mesh error", "error", str(e)) + gcmd.respond_info(f"Compensation mesh created with profile '{profile}'") + + except RatOSBeaconMeshError as e: + raise gcmd.error(f"Failed to create compensation mesh: {str(e)}") from e def load_extra_mesh_params(self): profiles = self.bed_mesh.pmgr.get_profiles() - + for profile_name in profiles.keys(): profile = profiles[profile_name] profile_params = profile["mesh_params"] - + # Try to find the config section for this profile # Handle profile names with spaces correctly try: @@ -888,7 +1013,7 @@ def load_extra_mesh_params(self): continue version = config.getint(RATOS_MESH_VERSION_PARAMETER, None) - + if version == 1: try: mesh_kind = config.getchoice(RATOS_MESH_KIND_PARAMETER, list(RATOS_MESH_KIND_CHOICES)) @@ -909,12 +1034,12 @@ def load_extra_mesh_params(self): f"Bed mesh profile '{profile_name}' configuration is invalid: {str(ex)}") self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) continue - + profile_params[RATOS_MESH_VERSION_PARAMETER] = version profile_params[RATOS_MESH_KIND_PARAMETER] = mesh_kind profile_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = mesh_probe_method profile_params[RATOS_MESH_BED_TEMP_PARAMETER] = mesh_bed_temp - + if notes: profile_params[RATOS_MESH_NOTES_PARAMETER] = notes else: @@ -929,7 +1054,7 @@ def load_extra_mesh_params(self): profile_params[RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER] = mesh_proximity_mesh_bounds else: profile_params.pop(RATOS_MESH_PROXIMITY_MESH_BOUNDS_PARAMETER, None) - else: + else: self.ratos.console_echo("RatOS Beacon bed mesh management", "warning", f"Bed mesh profile '{profile_name}' was created without extended RatOS Beacon bed mesh support." if version is None else @@ -937,6 +1062,713 @@ def load_extra_mesh_params(self): self.bed_mesh.pmgr.incompatible_profiles.append(profile_name) continue + desc_BED_MESH_SUBTRACT = "For diagnostic use. Subtracts mesh A from mesh B and creates a new mesh with the result. The new mesh will have the grid of the PRIMARY mesh." + def cmd_BED_MESH_SUBTRACT(self, gcmd): + profile_a = gcmd.get('A').strip() + profile_b = gcmd.get('B').strip() + primary= gcmd.get('PRIMARY', 'a').strip().lower() + if profile_a == profile_b: + raise gcmd.error("Profiles A and B must be different.") + if primary not in ('a', 'b'): + raise gcmd.error(f"Invalid PRIMARY value '{primary}'. Must be 'A' or 'B'.") + + zmesh_a = self._create_zmesh_from_profile(profile_a) + zmesh_b = self._create_zmesh_from_profile(profile_b) + + pri, sec = (zmesh_a, zmesh_b) if primary == 'a' else (zmesh_b, zmesh_a) + + pri_points = pri.probed_matrix + sec_points = sec.probed_matrix + diff_points = np.full_like(pri_points, 0.) + + grid_is_same = pri.mesh_x_min == sec.mesh_x_min and \ + pri.mesh_x_max == sec.mesh_x_max and \ + pri.mesh_y_min == sec.mesh_y_min and \ + pri.mesh_y_max == sec.mesh_y_max and \ + len(pri_points) == len(sec_points) and \ + len(pri_points[0]) == len(sec_points[0]) + + for y in range(len(pri_points)): + for x in range(len(pri_points[0])): + pri_z = pri_points[y][x] + x_pos = pri.mesh_x_min + x * ((pri.mesh_x_max - pri.mesh_x_min) / (len(pri.probed_matrix[0]) - 1)) + y_pos = pri.mesh_y_min + y * ((pri.mesh_y_max - pri.mesh_y_min) / (len(pri.probed_matrix) - 1)) + sec_z = sec_points[y][x] if grid_is_same else sec.calc_z(x_pos, y_pos) + diff = pri_z - sec_z if primary == 'a' else sec_z - pri_z + diff_points[y][x] = diff + if ( x == 0 or x == len(pri_points[0]) - 1 ) and (y == 0 or y == len(pri_points) - 1): + # Only log the corners + gcmd.respond_info( + f"Subtracting {profile_b} from {profile_a} at point {x}, {y} ({x_pos:.2f}, {y_pos:.2f}): " + f"pri={pri_z:.4f}, sec={sec_z:.4f}, diff={diff:.4f}" + ) + + # Some of the parameters values used here are not strictly correct, but they are "safe" + # given that this mesh is synthetic, this method is a diagnostic tool, and it wasn't worth the effort to + # invent new parameter values. + extra_params = {} + extra_params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION + extra_params[RATOS_MESH_BED_TEMP_PARAMETER] = 0 + extra_params[RATOS_MESH_KIND_PARAMETER] = RATOS_MESH_KIND_MEASURED + extra_params[RATOS_MESH_BEACON_PROBE_METHOD_PARAMETER] = RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY + extra_params[RATOS_MESH_NOTES_PARAMETER] = f"Mesh subtraction of '{profile_a}' minus '{profile_b}' based on the grid of '{profile_a if primary == 'a' else profile_b}'." + + self._install_and_save_new_mesh( + f"{profile_a}_MINUS_{profile_b}", + extra_params, + (pri.mesh_x_min, pri.mesh_y_min), + (pri.mesh_x_max, pri.mesh_y_max), + diff_points.tolist() + ) + + def _get_mesh_difference_rmse(self, points_a: List[List[float]], points_b: List[List[float]]) -> float: + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_get_mesh_difference_rmse(points_a, points_b)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise Exception("Error applying deridging filter: %s" % (result,)) + else: + return result + + def _do_get_mesh_difference_rmse(self, points_a: List[List[float]], points_b: List[List[float]]) -> float: + """ + Calculate the RMSE (Root Mean Square Error) between two sets of mesh points. + :param points_a: First set of mesh points. + :param points_b: Second set of mesh points. + :return: RMSE value. + """ + np_a = np.array(points_a) + np_b = np.array(points_b) + + if np_a.shape != np_b.shape: + raise ValueError("The two point sets must have the same shape.") + + diff = np_a - np_b + rmse = np.sqrt(np.mean(np.square(diff))) + return rmse + + def _apply_deridging_filter(self, input_points: List[List[float]], primary_axis: str) -> List[List[float]]: + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_apply_deridging_filter(input_points, primary_axis)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise Exception("Error applying deridging filter: %s" % (result,)) + else: + return result + + def _do_apply_deridging_filter(self, input_points: List[List[float]], primary_axis: str) -> List[List[float]]: + """ + Apply a de-ridging filter to the input points along the specified primary axis. + :param input_points: List of points to filter, where each point is a list of coordinates. + :param primary_axis: The primary axis along which to apply the filter ('x' or 'y'). + :return: Filtered list of points. + :raises ValueError: If the primary_axis is not 'x' or 'y'. + """ + arr = np.array(input_points) + if primary_axis == 'y': + # Filter along axis 1 (columns) + result = np.zeros_like(arr) + # Middle points + result[:, 1:-1] = ( + 0.25 * arr[:, :-2] + + 0.5 * arr[:, 1:-1] + + 0.25 * arr[:, 2:] + ) + # Left edge + result[:, 0] = 0.5 * arr[:, 0] + 0.5 * arr[:, 1] + # Right edge + result[:, -1] = 0.5 * arr[:, -1] + 0.5 * arr[:, -2] + elif primary_axis == 'x': + # Filter along axis 0 (rows) + result = np.zeros_like(arr) + # Middle points + result[1:-1, :] = ( + 0.25 * arr[:-2, :] + + 0.5 * arr[1:-1, :] + + 0.25 * arr[2:, :] + ) + # Top edge + result[0, :] = 0.5 * arr[0, :] + 0.5 * arr[1, :] + # Bottom edge + result[-1, :] = 0.5 * arr[-1, :] + 0.5 * arr[-2, :] + else: + raise ValueError(f"Invalid primary_axis: {primary_axis}") + + return result.tolist() + + def _install_and_save_new_mesh( + self, + profile_name, + extra_params:Dict[str, Any], + mesh_min:Tuple[float,float], + mesh_max:Tuple[float,float], + probed_points:List[List[float]], + *, + mesh_pps:Optional[Tuple[int, int]] = None, + algorithm:Optional[str] = None, + ): + + x_count = len(probed_points[0]) + y_count = len(probed_points) + + cmd_params = dict( + PROBE_COUNT=f"{x_count},{y_count}", + MESH_MIN=f"{mesh_min[0]:.3f},{mesh_min[1]:.3f}", + MESH_MAX=f"{mesh_max[0]:.3f},{mesh_max[1]:.3f}", + ) + + if mesh_pps: + cmd_params['MESH_PPS'] = f"{mesh_pps[0]},{mesh_pps[1]}" + + if algorithm: + cmd_params['ALGORITHM'] = algorithm + + self.bed_mesh.set_mesh(None) # Clear any existing mesh before setting the new one + bed_mesh_calibrate_like_command = self.gcode.create_gcode_command( + "_", "_", + cmd_params + ) + + try: + self.bed_mesh.bmc.update_config(bed_mesh_calibrate_like_command) + except BedMesh.BedMeshError as e: + raise RatOSBeaconMeshError(f"Error updating bed mesh config: {str(e)}") + + params = dict(self.bed_mesh.bmc.mesh_config) + params.update(extra_params) + params['min_x'] = mesh_min[0] + params['max_x'] = mesh_max[0] + params['min_y'] = mesh_min[1] + params['max_y'] = mesh_max[1] + + z_mesh = BedMesh.ZMesh(params, profile_name, self.reactor) + + try: + z_mesh.build_mesh(probed_points) + except BedMesh.BedMeshError as e: + raise RatOSBeaconMeshError(str(e)) + + self.bed_mesh.set_mesh(z_mesh) + self.bed_mesh.save_profile(profile_name) + + # This method originally adapted from Klipper's bed_mesh.py module, Copyright (C) 2018-2019 Eric Callahan + def generate_mesh_points(self, x_count, y_count, mesh_min, mesh_max) -> Tuple[float, float, List[Tuple[int, int, float, float]]]: + min_x, min_y = mesh_min + max_x, max_y = mesh_max + x_dist = (max_x - min_x) / (x_count - 1) + y_dist = (max_y - min_y) / (y_count - 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 RatOSBeaconMeshError(f"{self.name}: min/max points too close together") + + max_x = min_x + x_dist * (x_count - 1) + max_y = min_y + y_dist * (y_count - 1) + pos_y = min_y + points = [] + for i in range(y_count): + for j in range(x_count): + if not i % 2: + # move in positive directon + pos_x = min_x + j * x_dist + idx_x = j + else: + # move in negative direction + pos_x = max_x - j * x_dist + idx_x = x_count - j - 1 + + # rectangular bed, append + points.append((idx_x, i, pos_x, pos_y)) + pos_y += y_dist + return (max_x, max_y, points) + +class ProbeCommandKind(Enum): + CONTACT_SINGLE = 1 + CONTACT_MULTI = 2 + PROXIMITY = 3 + +class ProbeAction(NamedTuple): + """ + Represents a single probing action. + + Attributes: + is_contact: + True if this is a contact probe, False if this is a proximity probe. + idx_x: The x index of the point in the mesh. + idx_y: The y index of the point in the mesh. + pos_x: The x coordinate of the toolhead at which the probing action should take place (ie, proximity actions are adjusted for the beacon offset). + pos_y: The y coordinate of the toolhead at which the probing action should take place (ie, proximity actions are adjusted for the beacon offset). + """ + is_contact: bool + idx_x: int + idx_y: int + pos_x: float + pos_y: float + +@dataclass +class ProbeActionResult: + """ + Represents the result of a probing action. + + Attributes: + contact_z: The z value from the contact probe. + proximity_z: The z value from the proximity probe. + contact_time: The reactor monotonic time when the contact probe was completed. + proximity_time: The reactor monotonic time when the proximity probe was completed. + """ + contact_z: Optional[float] = None + proximity_z: Optional[float] = None + contact_time: Optional[float] = None + proximity_time: Optional[float] = None + +class CotemporalProbingHelper: + + # For offset-aligned probing, this is the maximum allowed offset in the direction perpendicular to the + # primary movement. For example, if the primary movement is 'x', this is the maximum allowed offset in + # the 'y' direction. With offset-aligned algorithm, we don't move the probe off the primary axis, so + # MAXIMUM_SECONDARY_BEACON_OFFSET limits how far off-axis we allow the probe to be. + # NB: Beacons mounted off-axis have not been tested, so this value is speculative. + MAXIMUM_SECONDARY_BEACON_OFFSET = 2.0 # mm + + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.gcode = self.printer.lookup_object("gcode") + + self.beacon = None + self._probe_finalize = None + self._probe_helper = None + self._beacon_proximity_offsets = None + + if not config.has_section("beacon"): + return + + self._probe_helper = probe.ProbePointsHelper(config, self._call_probe_finalize, []) + self.printer.register_event_handler("klippy:connect", self._connect) + + def _connect(self): + self.beacon = self.printer.lookup_object("beacon") + + # NB: We can't rely on beacon.get_offsets() because the output depends on beacon._current_probe: basically, + # beacon.get_offsets() appears to be designed for API compliance in limited circumstances, not for general use. + # Futher, ProbePointsHelper.use_xy_offsets() relies on beacon.get_offsets(), and the same limited + # circumstances restriction applies. Our use case here is not one of those circumstances. + # + # Further, any direct or indirect reliance on beacon.get_offsets() has a nasty risk of leading to very confusing + # bugs, as the value returned when called outside designed-for limited circumstances depends on whether the + # last expected-circumstance probe was a contact or proximity probe. + self._beacon_proximity_offsets = (self.beacon.x_offset, self.beacon.y_offset, self.beacon.trigger_distance) + + def do_contact_probe(self, gcmd, position, contact_reference_z: Optional[float]=None, delta_contact_z_limit=0.075) -> float: + """ + Perform a contact probe at a single position. + :param gcmd: Gcode command object + :param position: A single [x, y] coordinate to probe. + :param contact_reference_z: The reference z value to compare against for contact probing validity checking. + :param delta_contact_z_limit: The maximum allowed difference between the contact probe z value and the reference z value. + :return: The z value from the contact probe. + """ + if not self.beacon: + raise RatOSBeaconMeshError("Beacon module is not loaded") + + try: + contact_z = None + contact_complete = False + contact_force_multi = False + + self._probe_helper.update_probe_points([position], 1) + + while not contact_complete: + contact_cmd = None + + if contact_force_multi or contact_reference_z is None: + contact_cmd = self._get_probe_command(gcmd, ProbeCommandKind.CONTACT_MULTI) + def contact_cb(_, positions): + nonlocal contact_z, contact_complete + if len(positions) != 1: + raise RatOSBeaconMeshError(f"Expected exactly one position from contact probe, got {len(positions)}") + contact_z = positions[0][2] + contact_complete = True + return "done" + else: + contact_cmd = self._get_probe_command(gcmd, ProbeCommandKind.CONTACT_SINGLE) + def contact_cb(_, positions): + nonlocal contact_z, contact_complete, contact_force_multi + if len(positions) != 1: + raise RatOSBeaconMeshError(f"Expected exactly one position from contact probe, got {len(positions)}") + z = positions[0][2] + dz = abs(z - contact_reference_z) + if dz > delta_contact_z_limit: + self.gcode.respond_info(f"Single-contact probe z={z:.4f} is not within limit of {delta_contact_z_limit:.4f} from reference z={contact_reference_z:.4f}, retrying with multi-contact") + contact_force_multi = True + else: + contact_z = z + contact_complete = True + + return "done" + + self._probe_finalize = contact_cb + self._probe_helper.start_probe(contact_cmd) + + return contact_z + finally: + self._probe_finalize = None + + def do_proximity_probe(self, gcmd, position, use_offset=False) -> float: + """ + Perform a proximity probe at a single position. + :param gcmd: Gcode command object + :param position: A single [x, y] coordinate to probe. + :param use_offset: If True, apply the beacon proximity offsets to the position. + :return: The z value from the proximity probe. + """ + if not self.beacon: + raise RatOSBeaconMeshError("Beacon module is not loaded") + + try: + proximity_z = None + + if use_offset: + position = [ + position[0] - self._beacon_proximity_offsets[0], + position[1] - self._beacon_proximity_offsets[1] + ] + + proximity_cmd = self._get_probe_command(gcmd, ProbeCommandKind.PROXIMITY) + + def proximity_cb(_, positions): + nonlocal proximity_z + if len(positions) != 1: + raise RatOSBeaconMeshError(f"Expected exactly one position from proximity probe, got {len(positions)}") + proximity_z = positions[0][2] + return "done" + + self._probe_helper.update_probe_points([position], 1) + self._probe_finalize = proximity_cb + self._probe_helper.start_probe(proximity_cmd) + + return proximity_z - self._beacon_proximity_offsets[2] + finally: + self._probe_finalize = None + + def probe_single_location(self, gcmd, position, contact_reference_z: Optional[float], delta_contact_z_limit=0.075) -> Tuple[float, float]: + """ + Probe contact and proximity at a single location. + :param gcmd: Gcode command object + :param position: A single [x, y] coordinate to probe. + :param contact_reference_z: The reference z value to compare against for contact probing validity checking. + :param delta_contact_z_limit: The maximum allowed difference between the contact probe z value and the reference z value. + :return: A tuple of (contact_z, proximity_z) where contact_z is the z value from the contact probe and proximity_z is the z value from the proximity probe. + """ + if not self.beacon: + # We don't expect to be called if the beacon module is not loaded. + raise RatOSBeaconMeshError("Beacon module is not loaded") + + contact_z = self.do_contact_probe( + gcmd, + position, + contact_reference_z=contact_reference_z, + delta_contact_z_limit=delta_contact_z_limit) + + proximity_z = self.do_proximity_probe( + gcmd, + position, + use_offset=True) + + return contact_z, proximity_z + + def _call_probe_finalize(self, offsets, positions): + if self._probe_finalize is None: + raise RatOSBeaconMeshError("_probe_finalize callback is not set") + + return self._probe_finalize(offsets, positions) + + def _get_probe_command(self, gcmd, kind: ProbeCommandKind): + #PROBE PROBE_METHOD=contact PROBE_SPEED=3 LIFT_SPEED=15 SAMPLES=5 SAMPLE_RETRACT_DIST=3 SAMPLES_TOLERANCE=0.005 SAMPLES_TOLERANCE_RETRIES=10 SAMPLES_RESULT=median + if kind == ProbeCommandKind.CONTACT_SINGLE: + probe_args = dict( + PROBE_METHOD='contact', + SAMPLES='1', + SAMPLES_DROP='0', + HORIZONTAL_MOVE_Z=str(self._beacon_proximity_offsets[2]) + ) + elif kind == ProbeCommandKind.CONTACT_MULTI: + probe_args = dict( + PROBE_METHOD='contact', + SAMPLES='3', + SAMPLES_DROP='1', + SAMPLES_TOLERANCE_RETRIES='15' + ) + elif kind == ProbeCommandKind.PROXIMITY: + probe_args = dict( + PROBE_METHOD='proximity', + SAMPLES='1', + SAMPLES_DROP='0', + HORIZONTAL_MOVE_Z=str(self._beacon_proximity_offsets[2]) + ) + else: + raise RatOSBeaconMeshError(f"Unknown ProbeCommandKind: {kind}") + + sensor = gcmd.get('SENSOR', None) + if sensor: + probe_args['SENSOR'] = sensor + + return self.gcode.create_gcode_command( + gcmd.get_command(), + gcmd.get_command() + + "".join(" " + k + "=" + v for k, v in probe_args.items()), + probe_args + ) + + def run_probe_action_sequence( + self, + gcmd, + count_x:int, + count_y:int, + probe_actions:List[ProbeAction], + *, + delta_contact_z_limit=0.075, + progress_handler:Optional[BackgroundDisplayStatusProgressHandler]=None) -> List[List[ProbeActionResult]]: + """ + Perform a sequence of probing actions. + :param gcmd: Gcode command object + :param probe_actions: A list of ProbeAction objects representing the probing actions to perform. + :return: A grid of tuples (contact_z, proximity_z, contact_time, proximity_time) where contact_z is the z value from the contact + probe, proximity_z is the z value from the proximity probe and time_difference is the + time that elapsed between the contact and proximity probes in seconds. + """ + if not self.beacon: + # We don't expect to be called if the beacon module is not loaded. + raise RatOSBeaconMeshError("Beacon module is not loaded") + + results = [[ProbeActionResult() for _ in range(count_x)] for _ in range(count_y)] + + last_contact_z = None + + for i, action in enumerate(probe_actions): + if action.idx_x < 0 or action.idx_x >= count_x or action.idx_y < 0 or action.idx_y >= count_y: + raise RatOSBeaconMeshError(f"ProbeAction indices out of bounds: idx_x={action.idx_x}, idx_y={action.idx_y}, count_x={count_x}, count_y={count_y}") + + action_result = results[action.idx_y][action.idx_x] + + if action.is_contact: + # Perform a contact probe + contact_z = self.do_contact_probe( + gcmd, + [action.pos_x, action.pos_y], + last_contact_z, + delta_contact_z_limit=delta_contact_z_limit + ) + last_contact_z = contact_z + action_result.contact_z = contact_z + action_result.contact_time = self.reactor.monotonic() + else: + proximity_z = self.do_proximity_probe( + gcmd, + [action.pos_x, action.pos_y]) + action_result.proximity_z = proximity_z + action_result.proximity_time = self.reactor.monotonic() + + if progress_handler: + progress_handler.progress = (i + 1) / len(probe_actions) + + return results + + def can_use_offset_aligned_probing(self, minimum_spacing) -> bool: + """ + Deterimes if offset-aligned probing can be used with the current beacon offsets and minimum spacing. + :param minimum_spacing: The minimum allowed spacing of mesh points in mm. + :return: True if offset-aligned probing can be used, False otherwise. + """ + offsets = self._beacon_proximity_offsets + if offsets[0] < 5. and offsets[1] < 5.: + # It's not physically possible to have the beacon overlap with the nozzle. The check above + # is actually more permissive than current beacon physical dimensions so it allows for future + # beacon hardware revisions. + raise RatOSBeaconMeshError(f"The configured Beacon sensor offset ({offsets[0]:.3f}, {offsets[1]:.3f}) is not valid.") + + primary_axis = 'x' if abs(offsets[0]) > abs(offsets[1]) else 'y' + primary_offset = offsets[0] if primary_axis == 'x' else offsets[1] + secondary_offset = offsets[1] if primary_axis == 'x' else offsets[0] + abs_primary_offset = abs(primary_offset) + abs_secondary_offset = abs(secondary_offset) + + if abs_secondary_offset > self.MAXIMUM_SECONDARY_BEACON_OFFSET: + # This happens when the beacon is not mounted off to one side of the nozzle predominantly in the + # x axis or predominantly in the y axis, for example if the beacon is mounted diagnonally offset from the nozzle. + return False + + if abs_primary_offset < minimum_spacing: + # If the primary axis offset is smaller than the finest resolution allowed. + return False + + return True + + def generate_probe_action_sequence_beacon_offset_aligned( + self, + desired_spacing:float, + minimum_spacing:float, + mesh_min:Tuple[float,float], + mesh_max:Tuple[float,float]) -> Tuple[int, int, float, float, List[ProbeAction]]: + """ + Generate a sampling sequence for a rectangular bed with the primary axis of movement aligned to primary axis of the beacon mounting offset. + The actual resolution of the mesh will be the primary axis beacon offset divided by some whole number. + :param desired_spacing: The desired spacing of mesh points in mm. + :param minimum_spacing: The minimum allowed spacing of mesh points in mm. + :param mesh_min: Minimum x, y coordinates of the mesh (min_x, min_y) + :param mesh_max: Maximum x, y coordinates of the mesh (max_x, max_y) + :return: A tuple of (count_x, count_y, max_x, max_y, points) where: + count_x and count_y are the number of points in the mesh. + max_x and max_y are the maximum x, y coordinates of the mesh (max_x, max_y). + points is a list of tuples (is_contact, idx_x, idx_y, pos_x, pos_y), where: + idx_x and idx_y are the indices of the point in the mesh, pos_x and pos_y are the coordinates of the + toolhead at which the probing action should take place (ie, proximity actions are for adjusted for the beacon offset). + """ + if desired_spacing < minimum_spacing: + raise RatOSBeaconMeshError( + f"The desired spacing ({desired_spacing:.3f} mm) is less than the minimum spacing allowed ({minimum_spacing:.3f} mm).") + + # Maximum allowed secondary offset in mm + + offsets = self._beacon_proximity_offsets + if offsets[0] < 5. and offsets[1] < 5.: + # It's not physically possible to have the beacon overlap with the nozzle. The check above + # is actually more permissive than current beacon physical dimensions so it allows for future + # beacon hardware revisions. + raise RatOSBeaconMeshError(f"The configured Beacon sensor offset ({offsets[0]:.3f}, {offsets[1]:.3f}) is not valid.") + + primary_axis = 'x' if abs(offsets[0]) > abs(offsets[1]) else 'y' + secondary_axis = 'y' if primary_axis == 'x' else 'x' + primary_offset = offsets[0] if primary_axis == 'x' else offsets[1] + secondary_offset = offsets[1] if primary_axis == 'x' else offsets[0] + abs_primary_offset = abs(primary_offset) + abs_secondary_offset = abs(secondary_offset) + + if abs_secondary_offset > self.MAXIMUM_SECONDARY_BEACON_OFFSET: + # This happens when the beacon is not mounted off to one side of the nozzle predominantly in the + # x axis or predominantly in the y axis, for example if the beacon is mounted diagnonally offset from the nozzle. + raise RatOSBeaconMeshError( + f"The secondary Beacon sensor offset (|{secondary_axis}|={abs_secondary_offset}) is too large for use with the offset-aligned data collection method. " + f"Maximum allowed secondary offset is {self.MAXIMUM_SECONDARY_BEACON_OFFSET:.3f} mm.") + + if abs_primary_offset < minimum_spacing: + # If the primary axis offset is smaller than the finest resolution allowed, we can't use this method. + raise RatOSBeaconMeshError( + f"The primary Beacon sensor offset (|{primary_axis}|={abs_primary_offset}) is smaller than the finest resolution allowed ({minimum_spacing:.3f} mm). " + f"To use offset-aligned data collection, the finest resolution allowed must be decreased.") + + offset_divisor = round(abs_primary_offset / desired_spacing) + if offset_divisor < 1: + offset_divisor = 1 + elif offset_divisor > 1 and abs_primary_offset / offset_divisor < minimum_spacing: + offset_divisor -= 1 + + # Round the resolution to the nearest hundredth of a millimeter, beacause Klipper's bed_mesh module + # does this too. I'm not certain why, might be simply to keep numbers tidy for display. + resolution = round(abs_primary_offset / offset_divisor, 2) + + x_count = int((mesh_max[0] - mesh_min[0]) / resolution + 1) + y_count = int((mesh_max[1] - mesh_min[1]) / resolution + 1) + + max_x = mesh_min[0] + resolution * (x_count - 1) + max_y = mesh_min[1] + resolution * (y_count - 1) + + primary_count, secondary_count = (x_count, y_count) if primary_axis == 'x' else (y_count, x_count) + + # - We always start probing at mesh_min + # - We start by probing along the primary axis, moving in the positive direction. + # - We then move to the next point along the secondary axis, and then probe along the primary axis in the negative direction. + # - We repeat this until we have probed all points. + + # For each line of probing along the primary axis: + # - We must determine if the beacon offset is leading or trailing the nozzle. This is + # determined by the sign of the primary axis offset compared to the primary axis direction. + # If the sign of the primary axis direction is the same as the sign of the primary axis offset, + # the beacon is leading the nozzle, otherwise it is trailing. + # - If the beacon is leading the nozzle, we probe the proximity point first, then the contact point. + # - If the beacon is trailing the nozzle, we probe the contact point first, then the proximity point. + # - The toolhead location progresses monotonically along the primary axis. + # - If offset_divisor is greater than 1, we must probe the first (offset_divisor - 1) proximity or + # contact points (accorinding to whether the beacon is leading or trailing), and thereafter we + # probe both contact and proximity points at the same toolhead location, although the location + # measured by proximity will be offset by the beacon offset in the primary axis direction. + + probe_actions = [] + def append_probe_action(is_contact, primary_idx, secondary_idx): + x_index = primary_idx if primary_axis == 'x' else secondary_idx + y_index = secondary_idx if primary_axis == 'x' else primary_idx + x_pos = mesh_min[0] + x_index * resolution + y_pos = mesh_min[1] + y_index * resolution + if not is_contact: + if primary_axis == 'x': + x_pos -= primary_offset + else: + y_pos -= primary_offset + + probe_actions.append(ProbeAction(is_contact, x_index, y_index, x_pos, y_pos)) + + for secondary_idx in range(secondary_count): + # Determine if the beacon is leading or trailing the nozzle + beacon_leading = ( primary_offset > 0 ) == ( secondary_idx % 2 == 0 ) + + def primary_idx_from_line_idx(primary_line_idx): + if secondary_idx % 2 == 0: + return primary_line_idx + else: + return primary_count - primary_line_idx - 1 + + # Add any initial probe actions for the first (offset_divisor - 1) points + for primary_line_idx in range(offset_divisor - 1): + append_probe_action(not beacon_leading, primary_idx_from_line_idx(primary_line_idx), secondary_idx) + pass + + # Add probe actions where contact and proximity are probed at the same toolhead position + for primary_line_idx in range(primary_count - (offset_divisor - 1)): + append_probe_action(not beacon_leading, primary_idx_from_line_idx(primary_line_idx + offset_divisor - 1), secondary_idx) + append_probe_action(beacon_leading, primary_idx_from_line_idx(primary_line_idx), secondary_idx) + + # Add any final probe actions for the last (offset_divisor - 1) points + for primary_line_idx in range(offset_divisor - 1): + append_probe_action(beacon_leading, primary_idx_from_line_idx(primary_count - (offset_divisor - 1) + primary_line_idx), secondary_idx) + + return primary_axis, x_count, y_count, max_x, max_y, probe_actions ##### # Loader diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 7d35dadb5..87767f980 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -84,10 +84,11 @@ variable_beacon_scan_compensation_enable: False # Enables beacon scan co variable_beacon_scan_compensation_profile: "auto" # The bed mesh profile name identifying the scan compensation mesh to use, or # "auto" to automatically select the most appropriate profile based on bed temperature. -variable_beacon_scan_compensation_resolution: 8 # The mesh resolution in mm for compensation mesh creation. It is strongly recommended - # to leave this at the default value of 8mm. Compensation mesh creation uses a - # special filtering algorithm to reduce noise in contact measurements which - # relies on sufficiently detailed mesh resolution. +variable_beacon_scan_compensation_desired_spacing: 10 # The desired spacing between probe points in mm for compensation mesh creation. + # It is strongly recommended to leave this at the default value of 10mm. The compensation + # mesh creation algorithm is designed and tested around this value, and using a significantly + # different value may lead to suboptimal results. The actual spacing used depends on various + # factors including the probe-able region and beacon mounting offset. variable_beacon_scan_compensation_bed_temp_mismatch_is_error: False # If True, attempting to use a compensation mesh calibrated for a significantly # different bed temperature will raise an error. Otherwise, a warning is reported. @@ -159,6 +160,9 @@ gcode: {% if printer["gcode_macro RatOS"].beacon_contact_expansion_multiplier is defined %} CONSOLE_ECHO TITLE="Deprecated gcode variable" TYPE="warning" MSG="Please remove the variable beacon_contact_expansion_multiplier from your config file." {% endif %} + {% if printer["gcode_macro RatOS"].beacon_scan_compensation_resolution is defined %} + CONSOLE_ECHO TITLE="Deprecated gcode variable" TYPE="warning" MSG="The variable beacon_scan_compensation_resolution is no longer used, please remove it from your config file." + {% endif %} ##### # BEACON CALIBRATION @@ -924,7 +928,7 @@ gcode: {% set default_toolhead = printer["gcode_macro RatOS"].default_toolhead|default(0)|int %} {% set beacon_scan_compensation_enable = true if printer["gcode_macro RatOS"].beacon_scan_compensation_enable|default(false)|lower == 'true' else false %} {% set beacon_contact_calibrate_model_on_print = true if printer["gcode_macro RatOS"].beacon_contact_calibrate_model_on_print|default(false)|lower == 'true' else false %} - {% set mesh_resolution = printer["gcode_macro RatOS"].beacon_scan_compensation_resolution|float %} + {% set desired_spacing = printer["gcode_macro RatOS"].beacon_scan_compensation_desired_spacing|float %} {% set bed_heat_soak_time = printer["gcode_macro RatOS"].bed_heat_soak_time|default(0)|int %} {% set hotend_heat_soak_time = printer["gcode_macro RatOS"].hotend_heat_soak_time|default(0)|int %} {% set z_speed = printer["gcode_macro RatOS"].macro_z_speed|float * 60 %} @@ -935,19 +939,6 @@ gcode: {% set beacon_adaptive_heat_soak_max_wait = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_max_wait|default(5400)|int %} {% set beacon_adaptive_heat_soak_extra_wait_after_completion = printer["gcode_macro RatOS"].beacon_adaptive_heat_soak_extra_wait_after_completion|default(0)|int %} - # get bed mesh config object - {% set mesh_config = printer.fastconfig.config.bed_mesh %} - - # get configured bed mesh area - {% set min_x = mesh_config.mesh_min.split(",")[0]|float %} - {% set min_y = mesh_config.mesh_min.split(",")[1]|float %} - {% set max_x = mesh_config.mesh_max.split(",")[0]|float %} - {% set max_y = mesh_config.mesh_max.split(",")[1]|float %} - - # calculate probe counts - {% set probe_count_x = ((max_x - min_x) / mesh_resolution + 1)|int %} - {% set probe_count_y = ((max_y - min_y) / mesh_resolution + 1)|int %} - {% if not beacon_scan_compensation_enable %} RATOS_ECHO MSG="Beacon scan compensation is disabled!" @@ -994,8 +985,9 @@ gcode: # more gantry deflection. G1 Z2.5 F{z_speed} {% if beacon_adaptive_heat_soak %} - # Force a very stable soak threshold to minimise residual z deflection during probing. - BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=12 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,9000]|max} + # The cotemporal compensation mesh creation strategy is tolerant to some residual z-rate drift, so we can + # use a relatively moderate threshold to avoid excessive soak times. + BEACON_WAIT_FOR_PRINTER_HEAT_SOAK _FORCE_THRESHOLD=100 MINIMUM_WAIT={beacon_adaptive_heat_soak_min_wait} MAXIMUM_WAIT={[beacon_adaptive_heat_soak_max_wait,9000]|max} {% if beacon_adaptive_heat_soak_extra_wait_after_completion > 0 %} RATOS_ECHO MSG="Waiting for an additional {beacon_adaptive_heat_soak_extra_wait_after_completion} seconds after heat soak completion..." G4 P{(beacon_adaptive_heat_soak_extra_wait_after_completion * 1000)} @@ -1029,7 +1021,7 @@ gcode: {% endif %} # create compensation mesh - CREATE_BEACON_COMPENSATION_MESH PROFILE="{profile}" PROBE_COUNT={probe_count_x},{probe_count_y} CHAMBER_TEMP={chamber_temp} KEEP_TEMP_MESHES={keep_temp_meshes} + _BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE PROFILE="{profile}" DESIRED_SPACING={desired_spacing} CHAMBER_TEMP={chamber_temp} KEEP_TEMP_MESHES={keep_temp_meshes} # turn bed and extruder heaters off {% if not automated %} From 5595c597e4e577e6ccdf014743b454f60427fb6f Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 27 Aug 2025 20:15:32 +0100 Subject: [PATCH 124/139] fix(configuration): skip pointless home/calibrate after creating a compensation mesh in non-automated workflow --- configuration/z-probe/beacon.cfg | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/configuration/z-probe/beacon.cfg b/configuration/z-probe/beacon.cfg index 87767f980..bacd1cc55 100644 --- a/configuration/z-probe/beacon.cfg +++ b/configuration/z-probe/beacon.cfg @@ -1034,11 +1034,13 @@ gcode: _CHAMBER_HEATER_OFF {% endif %} - # Go to safe home - _MOVE_TO_SAFE_Z_HOME Z_HOP=True + {% if automated %} + # Go to safe home + _MOVE_TO_SAFE_Z_HOME Z_HOP=True - # home z - BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 + # home z + BEACON_AUTO_CALIBRATE SKIP_MODEL_CREATION=1 SKIP_MULTIPOINT_PROBING=1 + {% endif %} # visual feedback {% if not automated %} From adfca62affe71382f3d7cfab2fe6ebf9407d77b3 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 28 Aug 2025 11:46:34 +0100 Subject: [PATCH 125/139] fix(configuration): set split_delta_z to 1um for V-Core 4 variants - This can produce a visible improvement in surface quality, and appears to have little if any impact on CPU usage. The change was not applied more widely as other machines may have lower spec host boards, and the change has not been tested on those machines. --- configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg | 3 +++ configuration/printers/v-core-4-idex/v-core-4-idex.cfg | 3 +++ configuration/printers/v-core-4/v-core-4.cfg | 3 +++ 3 files changed, 9 insertions(+) diff --git a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg index a1737e723..8b2b329b8 100644 --- a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg +++ b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg @@ -7,6 +7,9 @@ [gcode_macro RatOS] variable_beacon_adaptive_heat_soak: True +[bed_mesh] +split_delta_z: 0.001 # Avoid visible surface stripe arfetacts + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin diff --git a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg index 1585d3bb4..f1f99b694 100644 --- a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg +++ b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg @@ -7,6 +7,9 @@ [gcode_macro RatOS] variable_beacon_adaptive_heat_soak: True +[bed_mesh] +split_delta_z: 0.001 # Avoid visible surface stripe arfetacts + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin diff --git a/configuration/printers/v-core-4/v-core-4.cfg b/configuration/printers/v-core-4/v-core-4.cfg index ec649466e..39f31d1d9 100644 --- a/configuration/printers/v-core-4/v-core-4.cfg +++ b/configuration/printers/v-core-4/v-core-4.cfg @@ -7,6 +7,9 @@ [gcode_macro RatOS] variable_beacon_adaptive_heat_soak: True +[bed_mesh] +split_delta_z: 0.001 # Avoid visible surface stripe arfetacts + [heater_bed] heater_pin: heater_bed_heating_pin sensor_pin: heater_bed_sensor_pin From 77ea6c666ed24febb23a59e5038dbf1c6ee316e4 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Thu, 28 Aug 2025 20:18:00 +0100 Subject: [PATCH 126/139] feature(beacon-mesh): support faulty regions in cotemporal compensation mesh creation --- configuration/klippy/beacon_mesh.py | 221 +++++++++++++++++++++++----- 1 file changed, 184 insertions(+), 37 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 2fc36d71a..56a5e27c4 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -5,6 +5,11 @@ # Copyright (C) 2025 Tom Glastonbury # # This file may be distributed under the terms of the GNU GPLv3 license. +# +# Contains portions of code adapted from beacon.py (GPLv3) https://github.com/beacon3d/beacon_klipper +# Copyright (C) 2020-2023 Matt Baker +# Copyright (C) 2020-2023 Lasse Dalegaard +# Copyright (C) 2023 Beacon from enum import Enum import logging @@ -77,6 +82,18 @@ class RatOSBeaconMeshError(Exception): pass +class Region: + def __init__(self, x_min, x_max, y_min, y_max): + self.x_min = x_min + self.x_max = x_max + self.y_min = y_min + self.y_max = y_max + + def is_point_within(self, x, y): + return (x > self.x_min and x < self.x_max) and ( + y > self.y_min and y < self.y_max + ) + ##### # Beacon Mesh ##### @@ -120,6 +137,7 @@ def __init__(self, config): # Loaded on demand if needed self.scipy_ndimage = None + self.scipy = None self.register_commands() self.register_handler() @@ -428,19 +446,19 @@ def cmd_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE(self, gcmd): raise gcmd.error("Beacon module not loaded") profile = gcmd.get('PROFILE', RATOS_COMPENSATION_MESH_NAME_AUTO).strip() - + if gcmd.get('PROBE_COUNT', None) is not None: # Sanity check: RatOS scripts know about this, and this command should not be called directly by users, # but just in case... raise gcmd.error("Parameter 'PROBE_COUNT' is no longer supported.") - + desired_spacing = gcmd.get_float("DESIRED_SPACING", float(self.gm_ratos.variables.get('beacon_scan_compensation_desired_spacing', 10.))) minimum_spacing = gcmd.get_float("MINIMUM_SPACING", desired_spacing * 0.8) chamber_temp = gcmd.get_float('CHAMBER_TEMP', 0) if desired_spacing < minimum_spacing: raise gcmd.error("Parameter 'DESIRED_SPACING' must be greater than or equal to 'MINIMUM_SPACING'") - + if not profile: raise gcmd.error("Value for parameter 'PROFILE' must be specified") @@ -484,7 +502,7 @@ def cmd_BEACON_CREATE_SCAN_COMPENSATION_MESH_CORE(self, gcmd): self.gcode.run_script_from_command("BEACON_AUTO_CALIBRATE SKIP_MULTIPOINT_PROBING=1 SKIP_MODEL_CREATION=1") - self.create_compensation_mesh(gcmd, profile, desired_spacing, minimum_spacing, chamber_temp, keep_temp_meshes) + self.create_compensation_mesh(gcmd, profile, desired_spacing, minimum_spacing, chamber_temp, keep_temp_meshes) desc_SET_ZERO_REFERENCE_POSITION = "Sets the zero reference position for the currently loaded bed mesh." def cmd_SET_ZERO_REFERENCE_POSITION(self, gcmd): @@ -718,7 +736,7 @@ def do(): child.join() parent_conn.close() if is_err: - raise Exception("Error applying local-low filter: %s" % (result,)) + raise RatOSBeaconMeshError("Error applying local-low filter: %s" % (result,)) else: return result @@ -732,7 +750,7 @@ def _gaussian_filter(self, data, sigma, mode): "module is required for Beacon contact compensation mesh creation." ) - return self.scipy_ndimage.gaussian_filter(data, sigma=sigma, mode=mode) + return self.scipy_ndimage.gaussian_filter(data, sigma=sigma, mode=mode) def _do_local_low_filter(self, data, lowpass_sigma=1.): # 1. Low-pass filter to obtain general shape @@ -788,7 +806,11 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci safe_max_y = min(bpr.proximity_max[1], bpr.contact_max[1]) if (bpr.contact_min != bpr.proximity_min or bpr.contact_max != bpr.proximity_max): - logging.info(f'{self.name}: Beacon probing regions contact and proximity bounds do not match, the compensation mesh bounds will be reduced to the intersecting region.') + logging.info(f'{self.name}: beacon probing regions contact and proximity bounds do not match, the compensation mesh bounds will be reduced to the intersecting region.') + + if self._cotemporal_probing_helper.faulty_regions: + gcmd.respond_info(f"{len(self._cotemporal_probing_helper.faulty_regions)} faulty proximity probing regions are configured. Proximity values for points within these regions will be interpolated.") + logging.info(f"{self.name}: faulty proximity probing regions: {self._cotemporal_probing_helper.faulty_regions}") use_offset_aligned = self._cotemporal_probing_helper.can_use_offset_aligned_probing(minimum_spacing) skip_local_low_filter = False @@ -804,19 +826,20 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci (safe_max_x, safe_max_y) ) + x_spacing = (max_x - safe_min_x) / (probe_count_x - 1) + y_spacing = (max_y - safe_min_y) / (probe_count_y - 1) + gcmd.respond_info( f"Using {pattern} cotemporal probing strategy:\n" f"Generated {len(actions)} probe actions for the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})\n" - f"Mesh points: {probe_count_x} x {probe_count_y}, max coordinates: ({max_x:.2f}, {max_y:.2f})") - - # TODO: Handle faulty regions! + f"Mesh points: {probe_count_x} x {probe_count_y}, max coordinates: ({max_x:.2f}, {max_y:.2f}), spacing: ({x_spacing:.2f}, {y_spacing:.2f})") progress_handler = None try: progress_handler = BackgroundDisplayStatusProgressHandler(self.printer, "{spinner} Probing {progress:.1f}%") progress_handler.enable() - results = self._cotemporal_probing_helper.run_probe_action_sequence( + faulty_proximity_count, results = self._cotemporal_probing_helper.run_probe_action_sequence( gcmd, probe_count_x, probe_count_y, actions, @@ -824,7 +847,7 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci ) finally: if progress_handler: - progress_handler.disable() + progress_handler.disable() contact_points = [[results[y][x].contact_z for x in range(len(results[y]))] for y in range(len(results))] proximity_points = [[results[y][x].proximity_z for x in range(len(results[y]))] for y in range(len(results))] @@ -850,7 +873,7 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci y_spacing = (max_y - safe_min_y) / (probe_count_y - 1) force_multipoint_probing = ( - x_spacing > self.POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD or + x_spacing > self.POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD or y_spacing > self.POINT_BY_POINT_FORCE_MULTIPOINT_SPACING_THRESHOLD ) @@ -860,15 +883,14 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci logging.info(f"{self.name}: Using multi-sample probing for point-by-point probing strategy due to large spacing (x_spacing: {x_spacing:.2f}, y_spacing: {y_spacing:.2f})") contact_z = None + faulty_proximity_count = 0 results = [[None] * probe_count_x for _ in range(probe_count_y)] - + gcmd.respond_info( f"Using {pattern} cotemporal probing strategy:\n" f"Generated {len(points)} probe points for the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})\n" f"Mesh points: {probe_count_x} x {probe_count_y}, max coordinates: ({max_x:.2f}, {max_y:.2f}), spacing: ({x_spacing:.2f}, {y_spacing:.2f})" + (", using multi-sample probing due to large spacing" if force_multipoint_probing else "")) - - # TODO: Handle faulty regions! progress_handler = None try: @@ -883,15 +905,22 @@ def create_compensation_mesh(self, gcmd, profile, desired_spacing, minimum_spaci point[2:], None if force_multipoint_probing else contact_z) + if math.isnan(proximity_z): + faulty_proximity_count += 1 + results[point[1]][point[0]] = (point[2], point[3], contact_z, proximity_z) finally: if progress_handler: - progress_handler.disable() + progress_handler.disable() gcmd.respond_info(f"Probed {len(points)} points in the region from ({safe_min_x:.2f}, {safe_min_y:.2f}) to ({safe_max_x:.2f}, {safe_max_y:.2f})") contact_points = [[results[y][x][2] for x in range(len(results[y]))] for y in range(len(results))] - proximity_points = [[results[y][x][3] for x in range(len(results[y]))] for y in range(len(results))] + proximity_points = [[results[y][x][3] for x in range(len(results[y]))] for y in range(len(results))] + + if faulty_proximity_count > 0: + gcmd.respond_info(f"{faulty_proximity_count} faulty region proximity probe values will be interpolated.") + proximity_points = self._interpolate_faulty_region_values(proximity_points, x_spacing, y_spacing) extra_params = {} extra_params[RATOS_MESH_VERSION_PARAMETER] = RATOS_MESH_VERSION @@ -1144,10 +1173,10 @@ def do(): child.join() parent_conn.close() if is_err: - raise Exception("Error applying deridging filter: %s" % (result,)) + raise RatOSBeaconMeshError("Error calculating mesh difference RMSE: %s" % (result,)) else: return result - + def _do_get_mesh_difference_rmse(self, points_a: List[List[float]], points_b: List[List[float]]) -> float: """ Calculate the RMSE (Root Mean Square Error) between two sets of mesh points. @@ -1188,7 +1217,7 @@ def do(): child.join() parent_conn.close() if is_err: - raise Exception("Error applying deridging filter: %s" % (result,)) + raise RatOSBeaconMeshError("Error applying deridging filter: %s" % (result,)) else: return result @@ -1319,6 +1348,87 @@ def generate_mesh_points(self, x_count, y_count, mesh_min, mesh_max) -> Tuple[fl pos_y += y_dist return (max_x, max_y, points) + def _interpolate_faulty_region_values(self, points: List[List[float]], x_spacing: float, y_spacing: float) -> List[List[float]]: + parent_conn, child_conn = multiprocessing.Pipe() + + def do(): + try: + child_conn.send( + (False, self._do_interpolate_faulty_region_values(points, x_spacing, y_spacing)) + ) + except Exception: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + + child = multiprocessing.Process(target=do) + child.daemon = True + child.start() + reactor = self.reactor + eventtime = reactor.monotonic() + while child.is_alive(): + eventtime = reactor.pause(eventtime + 0.1) + is_err, result = parent_conn.recv() + child.join() + parent_conn.close() + if is_err: + raise RatOSBeaconMeshError("Error interpolating faulty region values: %s" % (result,)) + else: + return result + + def _do_interpolate_faulty_region_values(self, points: List[List[float]], x_spacing, y_spacing) -> List[List[float]]: + # Replace faulty points with interpolated values, modifying the input array in place. Return the modified array. + # x_spacing and y_spacing are the distances between points in the mesh, used to determine adjacency. + # points is a 2D array of floats, where faulty points are NaN. + + if not self.scipy: + try: + self.scipy = importlib.import_module("scipy") + except ImportError: + raise Exception( + "Could not load `scipy`. To install it, simply run `ratos doctor`. This " + "module is required for Beacon contact compensation mesh creation." + ) + + if not hasattr(self.scipy.interpolate, "RBFInterpolator"): + raise Exception( + "The RBFInterpolator class is missing from the scipy module. Try using `ratos doctor`. This " + "class is required for Beacon contact compensation mesh creation." + ) + + pp = np.array(points) + + # Find faulty points (NaN) + mask_faulty = np.isnan(pp) + + # If no faulty points, return as is + if not np.any(mask_faulty): + return points + + y_count, x_count = pp.shape + + # Build coordinate arrays + xs = np.arange(x_count) * x_spacing + ys = np.arange(y_count) * y_spacing + grid_x, grid_y = np.meshgrid(xs, ys) + + # Get valid points + valid_mask = ~mask_faulty + valid_x = grid_x[valid_mask].flatten() + valid_y = grid_y[valid_mask].flatten() + valid_z = pp[valid_mask].flatten() + + # Prepare coordinates for interpolation + interp_coords = np.column_stack((valid_x, valid_y)) + query_coords = np.column_stack((grid_x[mask_faulty], grid_y[mask_faulty])) + + # Interpolate faulty points + interpolated = self.scipy.interpolate.RBFInterpolator(interp_coords, valid_z, neighbors=64)(query_coords) + + # Fill in repaired values + pp[mask_faulty] = interpolated + + return pp.tolist() + class ProbeCommandKind(Enum): CONTACT_SINGLE = 1 CONTACT_MULTI = 2 @@ -1360,8 +1470,8 @@ class ProbeActionResult: class CotemporalProbingHelper: - # For offset-aligned probing, this is the maximum allowed offset in the direction perpendicular to the - # primary movement. For example, if the primary movement is 'x', this is the maximum allowed offset in + # For offset-aligned probing, this is the maximum allowed offset in the direction perpendicular to the + # primary movement. For example, if the primary movement is 'x', this is the maximum allowed offset in # the 'y' direction. With offset-aligned algorithm, we don't move the probe off the primary axis, so # MAXIMUM_SECONDARY_BEACON_OFFSET limits how far off-axis we allow the probe to be. # NB: Beacons mounted off-axis have not been tested, so this value is speculative. @@ -1376,10 +1486,27 @@ def __init__(self, config): self._probe_finalize = None self._probe_helper = None self._beacon_proximity_offsets = None + self.faulty_regions = [] if not config.has_section("beacon"): return + if config.has_section("bed_mesh"): + mesh_config = config.getsection("bed_mesh") + + for i in list(range(1, 100, 1)): + start = mesh_config.getfloatlist( + "faulty_region_%d_min" % (i,), None, count=2 + ) + if start is None: + break + end = mesh_config.getfloatlist("faulty_region_%d_max" % (i,), count=2) + x_min = min(start[0], end[0]) + x_max = max(start[0], end[0]) + y_min = min(start[1], end[1]) + y_max = max(start[1], end[1]) + self.faulty_regions.append(Region(x_min, x_max, y_min, y_max)) + self._probe_helper = probe.ProbePointsHelper(config, self._call_probe_finalize, []) self.printer.register_event_handler("klippy:connect", self._connect) @@ -1396,6 +1523,16 @@ def _connect(self): # last expected-circumstance probe was a contact or proximity probe. self._beacon_proximity_offsets = (self.beacon.x_offset, self.beacon.y_offset, self.beacon.trigger_distance) + def _is_faulty_coordinate(self, x, y, add_offsets=False): + if add_offsets: + xo, yo = self.beacon.x_offset, self.beacon.y_offset + x += xo + y += yo + for r in self.faulty_regions: + if r.is_point_within(x, y): + return True + return False + def do_contact_probe(self, gcmd, position, contact_reference_z: Optional[float]=None, delta_contact_z_limit=0.075) -> float: """ Perform a contact probe at a single position. @@ -1451,12 +1588,12 @@ def contact_cb(_, positions): finally: self._probe_finalize = None - def do_proximity_probe(self, gcmd, position, use_offset=False) -> float: + def do_proximity_probe(self, gcmd, position, subtract_offset=False) -> float: """ Perform a proximity probe at a single position. :param gcmd: Gcode command object :param position: A single [x, y] coordinate to probe. - :param use_offset: If True, apply the beacon proximity offsets to the position. + :param subtract_offset: If True, subtract the beacon proximity offsets from the position. :return: The z value from the proximity probe. """ if not self.beacon: @@ -1465,12 +1602,16 @@ def do_proximity_probe(self, gcmd, position, use_offset=False) -> float: try: proximity_z = None - if use_offset: + if subtract_offset: position = [ position[0] - self._beacon_proximity_offsets[0], position[1] - self._beacon_proximity_offsets[1] ] + if self._is_faulty_coordinate(position[0], position[1], add_offsets=True): + gcmd.respond_info(f"Skipping proximity probe at faulty region coordinate ({position[0] + self._beacon_proximity_offsets[0]:.2f}, {position[1] + self._beacon_proximity_offsets[1]:.2f})") + return float('nan') + proximity_cmd = self._get_probe_command(gcmd, ProbeCommandKind.PROXIMITY) def proximity_cb(_, positions): @@ -1510,7 +1651,7 @@ def probe_single_location(self, gcmd, position, contact_reference_z: Optional[fl proximity_z = self.do_proximity_probe( gcmd, position, - use_offset=True) + subtract_offset=True) return contact_z, proximity_z @@ -1558,20 +1699,22 @@ def _get_probe_command(self, gcmd, kind: ProbeCommandKind): ) def run_probe_action_sequence( - self, - gcmd, - count_x:int, + self, + gcmd, + count_x:int, count_y:int, probe_actions:List[ProbeAction], *, delta_contact_z_limit=0.075, - progress_handler:Optional[BackgroundDisplayStatusProgressHandler]=None) -> List[List[ProbeActionResult]]: + progress_handler:Optional[BackgroundDisplayStatusProgressHandler]=None) -> Tuple[int, List[List[ProbeActionResult]]]: """ Perform a sequence of probing actions. :param gcmd: Gcode command object :param probe_actions: A list of ProbeAction objects representing the probing actions to perform. - :return: A grid of tuples (contact_z, proximity_z, contact_time, proximity_time) where contact_z is the z value from the contact - probe, proximity_z is the z value from the proximity probe and time_difference is the + :return: A tuple of (faulty_count, results) where: + faulty_count is the number of faulty proximity points detected during probing. + results is a grid of tuples (contact_z, proximity_z, contact_time, proximity_time) where contact_z is the z value from the contact + probe, proximity_z is the z value from the proximity probe and time_difference is the time that elapsed between the contact and proximity probes in seconds. """ if not self.beacon: @@ -1580,6 +1723,7 @@ def run_probe_action_sequence( results = [[ProbeActionResult() for _ in range(count_x)] for _ in range(count_y)] + faulty_count = 0 last_contact_z = None for i, action in enumerate(probe_actions): @@ -1605,11 +1749,14 @@ def run_probe_action_sequence( [action.pos_x, action.pos_y]) action_result.proximity_z = proximity_z action_result.proximity_time = self.reactor.monotonic() + if math.isnan(proximity_z): + # Faulty point detected + faulty_count += 1 if progress_handler: progress_handler.progress = (i + 1) / len(probe_actions) - return results + return (faulty_count, results) def can_use_offset_aligned_probing(self, minimum_spacing) -> bool: """ @@ -1638,9 +1785,9 @@ def can_use_offset_aligned_probing(self, minimum_spacing) -> bool: if abs_primary_offset < minimum_spacing: # If the primary axis offset is smaller than the finest resolution allowed. return False - + return True - + def generate_probe_action_sequence_beacon_offset_aligned( self, desired_spacing:float, @@ -1658,7 +1805,7 @@ def generate_probe_action_sequence_beacon_offset_aligned( count_x and count_y are the number of points in the mesh. max_x and max_y are the maximum x, y coordinates of the mesh (max_x, max_y). points is a list of tuples (is_contact, idx_x, idx_y, pos_x, pos_y), where: - idx_x and idx_y are the indices of the point in the mesh, pos_x and pos_y are the coordinates of the + idx_x and idx_y are the indices of the point in the mesh, pos_x and pos_y are the coordinates of the toolhead at which the probing action should take place (ie, proximity actions are for adjusted for the beacon offset). """ if desired_spacing < minimum_spacing: From 44a41741ee86829a23d90f8edbdec59ceee52844 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Sun, 21 Sep 2025 23:38:34 +0100 Subject: [PATCH 127/139] revert(extras): remove dynamic_governor During further testing it became clear that changing the governor while klipper is running can occasionally lead to timer too close or other driver communication issues, and for the time being no workaround was found. So removing the feature for now. --- configuration/klippy/dynamic_governor.py | 256 ----------------------- configuration/printers/base.cfg | 4 +- configuration/scripts/ratos-common.sh | 1 - configuration/scripts/ratos-install.sh | 2 +- src/scripts/common.sh | 22 -- 5 files changed, 2 insertions(+), 283 deletions(-) delete mode 100644 configuration/klippy/dynamic_governor.py diff --git a/configuration/klippy/dynamic_governor.py b/configuration/klippy/dynamic_governor.py deleted file mode 100644 index f47399fce..000000000 --- a/configuration/klippy/dynamic_governor.py +++ /dev/null @@ -1,256 +0,0 @@ -# Automatically switches the CPU frequency governor to “performance” -# when the machine is in an active state (that is, when any stepper is enabled), -# and back to “ondemand” when the machine is in an idle state (that is, all steppers -# are disabled or Klipper is shutting down). -# -# Note: -# -# Requires the cpufrequtils package to be installed and the cpufreq-set -# command to be sudo-whitelisted for the user running Klipper. -# -# The current implentation assumes that the hardware does not support -# per-cpu frequency governors, so it sets the governor for all CPUs. -# -# Copyright (C) 2025 Tom Glastonbury -# -# This file may be distributed under the terms of the GNU GPLv3 license. - -import subprocess -import logging - -class DynamicGovernor: - def __init__(self, config): - self.printer = config.get_printer() - self.name = config.get_name() - - # config - if not config.getboolean('enabled', True): - logging.info(f"{self.name}: disabled by config") - return - - self.active_governor = config.get('active_governor', 'performance') - self.idle_governor = config.get('idle_governor', 'ondemand') - self.idle_governor_delay = config.getint('idle_governor_delay', 30, minval=10) - - # Ensure cpufreq-set is available and get the list of valid governors - self._check_cpufrequtils() - - if self.active_governor not in self._valid_governors: - raise self.printer.config_error( - f"{self.name}: active_governor '{self.active_governor}' " - "not in available governors: " - + ', '.join(self._valid_governors) - ) - - if self.idle_governor not in self._valid_governors: - raise self.printer.config_error( - f"{self.name}: idle_governor '{self.idle_governor}' " - "not in available governors: " - + ', '.join(self._valid_governors) - ) - - logging.info(f"{self.name}: active_governor={self.active_governor}, " - f"idle_governor={self.idle_governor}, idle_governor_delay={self.idle_governor_delay}s") - - # The transition to the idle governor is delayed to avoid switching - # during homing, and to allow for remaining critically-timed operations - # to complete. - def callback(eventtime): - try: - self._exec_cpufreq(self.idle_governor) - except Exception as e: - logging.warning(f"{self.name}: failed to set idle governor: {e}") - return self.printer.get_reactor().NEVER - - self._idle_delay_timer = self.printer.get_reactor().register_timer(callback) - - # Start in active mode. A delayed transition to idle will be scheduled - # in the _on_ready callback if no steppers are enabled. - self._on_active() - - # Track how many steppers are currently enabled - self._enabled_count = 0 - - # Register stepper enable state callbacks once Klipper is ready - self.printer.register_event_handler('klippy:ready', self._on_ready) - - # Ensure we reset to ondemand on shutdown - self.printer.register_event_handler('klippy:shutdown', self._on_shutdown) - - def _check_cpufrequtils(self): - # Ensure cpufreq-set is available - try: - self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '--help']) - logging.info(f"{self.name}: cpufreq-set is available") - except FileNotFoundError: - raise self.printer.config_error( - f"{self.name}: cpufreq-set command not found. " - "Please install the cpufrequtils package and ensure it is in the PATH." - ) - except subprocess.CalledProcessError as e: - raise self.printer.config_error( - f"{self.name}: cpufreq-set command failed: {e}. " - "Please ensure cpufrequtils is installed correctly." - ) - except Exception as e: - raise self.printer.config_error( - f"{self.name}: Unexpected error checking cpufreq-set: {e}. " - "Please ensure cpufrequtils is installed correctly." - ) - - # Obtain the list of valid governors - try: - output = self._run_subprocess_with_output(['cpufreq-info', '-g'], text=True) - # Parse the output to get the list of governors - # cpufreq-info -g returns a space-separated list of governors - # If it returns a single item, it might be comma-separated - # so we handle both cases. - output = output.strip() - self._valid_governors = output.split() - if len(self._valid_governors) == 1: - # If there's only one item, it might be comma-separated - self._valid_governors = output.split(',') - - # Clean up any whitespace in the results - self._valid_governors = [g.strip() for g in self._valid_governors if g.strip()] - logging.info(f"{self.name}: available CPU governors: {', '.join(self._valid_governors)}") - except FileNotFoundError: - raise self.printer.config_error( - f"{self.name}: cpufreq-info command not found. " - "Please install the cpufrequtils package and ensure it is in the PATH." - ) - except subprocess.CalledProcessError as e: - raise self.printer.config_error( - f"{self.name}: cpufreq-info command failed: {e}. " - "Please ensure cpufrequtils is installed correctly." - ) - except Exception as e: - raise self.printer.config_error( - f"{self.name}: Unexpected error checking cpufreq-info: {e}. " - "Please ensure cpufrequtils is installed correctly." - ) - - def _on_ready(self): - # Lookup the stepper_enable object and register callbacks - stepper_enable = self.printer.lookup_object('stepper_enable') - for stepper_id in stepper_enable.get_steppers(): - se = stepper_enable.lookup_enable(stepper_id) - # If the stepper is already enabled, increment the count - if se.is_enabled: - self._enabled_count += 1 - # Register the state callback for this stepper - se.register_state_callback(self._on_stepper_state) - - # We switched to active mode during construction. Schedule a delayed - # transition to idle if no steppers are enabled. - if self._enabled_count == 0: - self._on_idle() - - def _on_stepper_state(self, print_time, enabled: bool): - """ - Called whenever a stepper’s enable-pin state changes. - enabled=True → a stepper was turned on - enabled=False → a stepper was turned off - """ - if enabled: - # If transitioning from 0→1 enabled steppers, ramp to performance - if self._enabled_count == 0: - self._on_active() - self._enabled_count += 1 - else: - # Guard against negative counts - if self._enabled_count > 0: - self._enabled_count -= 1 - # If no more steppers are enabled, switch back to ondemand - if self._enabled_count == 0: - self._on_idle() - - logging.debug( - f"{self.name}: stepper state changed, enabled_count={self._enabled_count}" - ) - - def _on_shutdown(self): - """Reset governor when Klipper is shutting down or restarting.""" - # Regardless of current state, go back to ondemand - self._on_idle() - - def _on_active(self): - reactor = self.printer.get_reactor() - reactor.update_timer(self._idle_delay_timer, reactor.NEVER) - self._exec_cpufreq(self.active_governor) - - def _on_idle(self): - reactor = self.printer.get_reactor() - t = reactor.monotonic() + self.idle_governor_delay - logging.info(f"{self.name}: scheduling idle governor switch at {t:.1f}") - reactor.update_timer(self._idle_delay_timer, t) - - def _exec_cpufreq(self, governor: str): - """Run cpufreq-set -r -g quietly.""" - try: - self._run_subprocess_with_timeout(['sudo', '-n', 'cpufreq-set', '-r', '-g', governor]) - logging.info(f"{self.name}: set CPU governor to '{governor}'") - except Exception as e: - logging.warning( - f"{self.name}: failed to set governor to '{governor}': {e}" - ) - - def _run_subprocess_with_timeout(self, cmd, timeout_secs=10): - """Run a subprocess command with a timeout. - Raises subprocess.TimeoutExpired if the command does not complete - within the specified timeout. - """ - reactor = self.printer.get_reactor() - process = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - eventtime = reactor.monotonic() - # Poll for completion but don't block indefinitely - for _ in range(int(timeout_secs * 10)): # Try for specified seconds - if process.poll() is not None: - break - eventtime = reactor.pause(eventtime + 0.1) - - if process.returncode is None: - # Still running after timeout, kill it - process.terminate() - raise subprocess.TimeoutExpired(cmd, timeout_secs) - - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, cmd) - - def _run_subprocess_with_output(self, cmd, timeout_secs=10, text=False): - """Run a subprocess command and return its output. - Similar to subprocess.check_output but with a non-blocking timeout. - Returns command output as bytes (or string if text=True). - Raises subprocess.TimeoutExpired if the command does not complete - within the specified timeout. - """ - reactor = self.printer.get_reactor() - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=text - ) - eventtime = reactor.monotonic() - # Poll for completion but don't block indefinitely - for _ in range(int(timeout_secs * 10)): # Try for specified seconds - if process.poll() is not None: - break - eventtime = reactor.pause(eventtime + 0.1) - - if process.returncode is None: - # Still running after timeout, kill it - process.terminate() - raise subprocess.TimeoutExpired(cmd, timeout_secs) - - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, cmd, process.stdout.read()) - - return process.stdout.read() - -def load_config(config): - return DynamicGovernor(config) diff --git a/configuration/printers/base.cfg b/configuration/printers/base.cfg index bd4249822..b5be12de6 100644 --- a/configuration/printers/base.cfg +++ b/configuration/printers/base.cfg @@ -45,6 +45,4 @@ allow_unknown_gcode_generator: True [bed_mesh] split_delta_z: 0.01 # Avoid visible surface stripe arfetacts -[fastconfig] - -[dynamic_governor] \ No newline at end of file +[fastconfig] \ No newline at end of file diff --git a/configuration/scripts/ratos-common.sh b/configuration/scripts/ratos-common.sh index a8ff4754c..363fcea3b 100755 --- a/configuration/scripts/ratos-common.sh +++ b/configuration/scripts/ratos-common.sh @@ -195,7 +195,6 @@ verify_registered_extensions() ["beacon_true_zero_correction_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_true_zero_correction.py") ["beacon_adaptive_heatsoak_extension"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/beacon_adaptive_heat_soak.py") ["fastconfig"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/fastconfig.py") - ["dynamic_governor"]=$(realpath "${RATOS_PRINTER_DATA_DIR}/config/RatOS/klippy/dynamic_governor.py") ) declare -A kinematics_extensions=( diff --git a/configuration/scripts/ratos-install.sh b/configuration/scripts/ratos-install.sh index cb7e4f8d9..41b5b5e6e 100755 --- a/configuration/scripts/ratos-install.sh +++ b/configuration/scripts/ratos-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # This script installs additional dependencies for RatOS. -PKGLIST="python3-numpy python3-matplotlib curl git libopenblas-base cpufrequtils" +PKGLIST="python3-numpy python3-matplotlib curl git libopenblas-base" SCRIPT_DIR=$( cd -- "$( dirname -- "$(realpath -- "${BASH_SOURCE[0]}")" )" &> /dev/null && pwd ) CFG_DIR=$(realpath "$SCRIPT_DIR/..") diff --git a/src/scripts/common.sh b/src/scripts/common.sh index 0b23b1509..4b39a6631 100755 --- a/src/scripts/common.sh +++ b/src/scripts/common.sh @@ -276,26 +276,4 @@ __EOF $sudo cp --preserve=mode /tmp/031-ratos-configurator-wifi /etc/sudoers.d/031-ratos-configurator-wifi echo "RatOS configurator commands has successfully been whitelisted!" - - $sudo chown root:root /tmp/031-ratos-configurator-scripts - $sudo chmod 440 /tmp/031-ratos-configurator-scripts - $sudo cp --preserve=mode /tmp/031-ratos-configurator-scripts /etc/sudoers.d/031-ratos-configurator-scripts - - echo "RatOS configurator scripts has successfully been whitelisted!" - - # Whitelist klippy extension commands - if [[ -e /etc/sudoers.d/031-ratos-klippy-extensions ]] - then - $sudo rm /etc/sudoers.d/031-ratos-klippy-extensions - fi - touch /tmp/031-ratos-klippy-extensions - cat << __EOF > /tmp/031-ratos-klippy-extensions -${RATOS_USERNAME} ALL=(ALL) NOPASSWD: /usr/bin/cpufreq-set -__EOF - - $sudo chown root:root /tmp/031-ratos-klippy-extensions - $sudo chmod 440 /tmp/031-ratos-klippy-extensions - $sudo cp --preserve=mode /tmp/031-ratos-klippy-extensions /etc/sudoers.d/031-ratos-klippy-extensions - - echo "RatOS klippy extension commands has successfully been whitelisted!" } From 4472eb56537ea5c52ed74d2ced293872f4cfb226 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 2 Dec 2025 17:27:00 +0000 Subject: [PATCH 128/139] refactor(extras): extract common logic into a helper function --- configuration/klippy/ratos_z_offset.py | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/configuration/klippy/ratos_z_offset.py b/configuration/klippy/ratos_z_offset.py index 746a61931..cee0ca985 100644 --- a/configuration/klippy/ratos_z_offset.py +++ b/configuration/klippy/ratos_z_offset.py @@ -10,14 +10,14 @@ class RatOSZOffset: def __init__(self, config): self.printer = config.get_printer() - self.name = config.get_name() + self.name = config.get_name() self.printer.register_event_handler("klippy:connect", self._handle_connect) self.next_transform = None self.offsets = {} self.status = None self.combined_offset = 0. - + self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command('GET_RATOS_Z_OFFSET', self.cmd_GET_RATOS_Z_OFFSET, @@ -26,11 +26,11 @@ def __init__(self, config): desc=self.desc_SET_RATOS_Z_OFFSET) self.gcode.register_command('CLEAR_RATOS_Z_OFFSET', self.cmd_CLEAR_RATOS_Z_OFFSET, desc=self.desc_CLEAR_RATOS_Z_OFFSET) - + def _handle_connect(self): gcode_move = self.printer.lookup_object('gcode_move') self.next_transform = gcode_move.set_move_transform(self, force=True) - + ###### # commands ###### @@ -46,13 +46,8 @@ def cmd_GET_RATOS_Z_OFFSET(self, gcmd): desc_SET_RATOS_Z_OFFSET = "Set a RatOS Z offset" def cmd_SET_RATOS_Z_OFFSET(self, gcmd): name = gcmd.get('NAME').lower().strip() - if name not in OFFSET_NAMES: - raise gcmd.error(f"Offset name '{name}' is not recognized.") offset = gcmd.get_float('OFFSET') - if offset == 0.: - self.offsets.pop(name, None) - else: - self.offsets[name] = offset + self._validate_and_set_offset(name, offset) self._offset_changed() desc_CLEAR_RATOS_Z_OFFSET = "Clear a RatOS Z offset. This is equivalent to setting the offset to zero." @@ -61,14 +56,14 @@ def cmd_CLEAR_RATOS_Z_OFFSET(self, gcmd): if names == 'all': self.offsets = {} else: - names = [n.lower().strip() for n in names.split(',')] + names = [n.lower().strip() for n in names.split(',')] if any(n not in OFFSET_NAMES for n in names): msg = f"One or more offset names are not recognized: {', '.join(n for n in names if n not in OFFSET_NAMES)}" raise gcmd.error(msg) for n in names: self.offsets.pop(n, None) self._offset_changed() - + def _offset_changed(self): self.combined_offset = sum(self.offsets.values(), 0.) gcode_move = self.printer.lookup_object('gcode_move') @@ -79,13 +74,17 @@ def _offset_changed(self): def set_offset(self, name:str, offset:float): if name: name = name.strip().lower() + self._validate_and_set_offset(name, offset) + self._offset_changed() + + def _validate_and_set_offset(self, name, offset): + """Validate and set an offset value.""" if name not in OFFSET_NAMES: raise self.gcode.error(f"Offset name '{name}' is not recognized.") if offset == 0.: self.offsets.pop(name, None) else: self.offsets[name] = float(offset) - self._offset_changed() ###### # gcode_move transform compliance @@ -96,7 +95,7 @@ def get_position(self): pos = self.next_transform.get_position()[:] pos[2] -= offset return pos - + def move(self, newpos, speed): # Apply correction offset = self.combined_offset @@ -110,11 +109,11 @@ def move(self, newpos, speed): def _update_status(self): self.status = dict(self.offsets) self.status[COMBINED_OFFSET_KEY] = self.combined_offset - + def get_status(self, eventtime=None): if self.status is None: self._update_status() return self.status - + def load_config(config): - return RatOSZOffset(config) \ No newline at end of file + return RatOSZOffset(config) \ No newline at end of file From 81571c92ec423cc14593e38fbe971c0d791e7205 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 2 Dec 2025 17:28:13 +0000 Subject: [PATCH 129/139] chore(cleanup): fix minor niggles --- .../klippy/beacon_adaptive_heat_soak.py | 86 +++++++++---------- .../klippy/beacon_true_zero_correction.py | 86 +++++++++---------- 2 files changed, 86 insertions(+), 86 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index a40ca6925..eeb88cb6d 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -11,7 +11,7 @@ class ThresholdPredictor: def __init__(self, printer): self.printer = printer - self.reactor = printer.get_reactor() + self.reactor = printer.get_reactor() def predict_threshold(self, maximum_z_change_microns, period_seconds): ''' @@ -20,7 +20,7 @@ def predict_threshold(self, maximum_z_change_microns, period_seconds): The period is typically closely related to the first layer duration. The maximum Z change is typically associated with the amount of oversquish that is acceptable during the first layer. - + Parameters: maximum_z_change_microns: The maximum Z change allowed during the period after the soak completes, in microns. @@ -35,7 +35,7 @@ def predict_threshold(self, maximum_z_change_microns, period_seconds): # time is under 1s on a Raspberry Pi 4B, which is acceptable for the use case. parent_conn, child_conn = multiprocessing.Pipe() - + def do(): try: child_conn.send( @@ -58,7 +58,7 @@ def do(): if is_err: raise self.printer.command_error("Error predicting adaptive heat soak threshold: %s" % (result,)) else: - return result + return result def _do_predict_threshold(self, z, p): gam = self._get_model() @@ -69,38 +69,38 @@ def _do_predict_threshold(self, z, p): if prediction.size == 0: raise LookupError("Prediction failed, no data available in the model.") t = float(prediction[0]) - + # Ensure a minimum threshold of 12.5. From experimental data, we observe that thresholds # below this number approach the noise floor of the system and are not useful. t = max(t, 12.5) return t - + def _load_training_data(self): # The training data was derived from experimental data measured on multiple V-Core 4 machines. # It predicts z rate thresholds that have been evaluated as suitable for V-Core 4 300, 400 and 500 printers # with the stock aluminium extrusion and steel linear rail gantry, and also with limited evaluation # for steel and titanium box-section tube gantries. - + path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'beacon_adaptive_heat_soak_model_training.csv') - + if not os.path.exists(path): raise FileNotFoundError(f"Beacon adaptive heat soak model training data file not found: {path}") - + try: data = np.genfromtxt(path, delimiter=',', names=True) except Exception as e: raise Exception(f"Failed to load model training data: {e}") from e - + return data - + def _get_model(self): # We train the model on demand rather the relying on a cached pickled model file. - # This approach is somewhat inefficient but adequate for the current use case, and avoids - # the challenges of robust and reliable pickling and unpickling the model as regards + # This approach is somewhat inefficient but adequate for the current use case, and avoids + # the challenges of robust and reliable pickling and unpickling the model as regards # package updates and changes to the model. - + data = self._load_training_data() Xp = data['period'] # Period @@ -117,7 +117,7 @@ def _get_model(self): gam = pygam.LinearGAM( pygam.s(0, n_splines=20) - + pygam.s(1, n_splines=20) + + pygam.s(1, n_splines=20) + pygam.te(0, 1, n_splines=[10,10]) + pygam.s(2, n_splines=20) # smooth on z/p + pygam.s(3, n_splines=20), # smooth on 1/p @@ -125,7 +125,7 @@ def _get_model(self): lam=0.6, spline_order=3, fit_intercept=True) - + gam.fit(X, y) return gam @@ -148,7 +148,7 @@ def __init__(self, config, beacon, samples_per_mean=1000, window_size=30, window def get_estimated_delay_for_first_z_rate(self, beacon_sampling_rate=1000.0): return self.window_size * (self.samples_per_mean / beacon_sampling_rate) - + def _get_next_mean(self): first_sample_time = None @@ -235,12 +235,12 @@ def __init__(self, size): self.index = 0 self.sum = 0.0 self.count = 0 - + def get_average(self): if self.count == 0: return 0.0 return float(self.sum / self.count) - + def is_full(self): return self.count == self.size @@ -252,7 +252,7 @@ def add(self, value): self.buffer[self.index] = value self.sum += value self.index = (self.index + 1) % self.size - + def reset(self): self.buffer.fill(0.0) self.index = 0 @@ -369,8 +369,8 @@ def cb(s): logging.info(f"{self.name}: Prepared for sampling, collected {good_samples} good samples and {bad_samples} bad samples (total {good_samples+bad_samples} samples).") if good_samples < 1000: - raise self.printer.command_error(f"Failed to prepare beacon for sampling, timed out waiting for good samples. Beacon must be calibrated and positioned correctly before running this command.") - + raise self.printer.command_error("Failed to prepare beacon for sampling, timed out waiting for good samples. Beacon must be calibrated and positioned correctly before running this command.") + return (good_samples + bad_samples) / (last_sample_time - first_sample_time) def _check_trend_projection(self, moving_average_history, moving_average_history_times, trend_fit_window, trend_projection, threshold): @@ -407,17 +407,17 @@ def get_layer_quality_name(self, quality): # Returns the name of the layer quality based on the quality value. if quality < 1 or quality > 5: raise ValueError(f"Invalid layer quality {quality}, must be between 1 and 5.") - - return ("rough", "draft", "normal", "high", "maximum")[quality - 1] - + + return ("rough", "draft", "normal", "high", "maximum")[quality - 1] + def _get_maximum_z_change_microns_for_quality(self, quality): if quality < 1 or quality > 5: raise ValueError(f"Invalid layer quality {quality}, must be between 1 and 5.") - + # Returns the maximum Z change in microns for the given layer quality. # This is a fixed mapping based on empirical data and should not be changed. return (150, 100, 50, 20, 10)[quality - 1] # Microns for layer quality 1-5 - + desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK = "Wait for printer to reach thermal stability using Beacon to monitor deflection changes" def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.beacon is None: @@ -448,11 +448,11 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): params_msg = f"\nto suit layer quality {layer_quality} ({self.get_layer_quality_name(layer_quality)}) with maximum first layer duration of {self._format_seconds(maximum_first_layer_duration)}" logging.info(f"{self.name}: predicted adaptive heat soak threshold for maximum Z change of {maximum_z_change_microns} microns (quality {layer_quality}) over {period} seconds: {threshold:.2f} nm/s") else: - logging.info(f"{self.name}: using forced adaptive heat soak threshold: {threshold:.2f} nm/s") + logging.info(f"{self.name}: using forced adaptive heat soak threshold: {threshold:.2f} nm/s") beacon_sampling_rate = self._prepare_for_sampling_and_get_sampling_frequency() - # The following control values were determined experimentally, and should not be changed + # The following control values were determined experimentally, and should not be changed # without careful consideration and reference to the corpus of experimental data. Changing # these values will also invalidate the threshold predictor training data. moving_average_target_hold_count = 150 @@ -490,7 +490,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): estimated_time_to_first_moving_average = \ z_rate_session.get_estimated_delay_for_first_z_rate(beacon_sampling_rate) \ + moving_average_size * (z_rate_session.samples_per_mean / beacon_sampling_rate) - + progress_handler.set_auto_rate(0.05 / estimated_time_to_first_moving_average) progress_handler.enable() @@ -509,7 +509,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): progress_start = None progress_start_z_rate = None progress_z_rate_range = None - progress_on_final_approach = False + progress_on_final_approach = False while True: if self.reactor.monotonic() - start_time > maximum_wait: @@ -522,7 +522,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if self.printer.is_shutdown(): raise else: - raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") + raise self.printer.command_error(f"Error calculating Z-rate, wait ended prematurely: {e}") from e if time_zero is None: time_zero = z_rate_result[0] @@ -532,7 +532,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): z_rate_count += 1 # Throttle logging - should_log = z_rate_count % 20 == 0 + should_log = z_rate_count % 20 == 0 elapsed = self.reactor.monotonic() - start_time moving_average = None @@ -548,14 +548,14 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): level_2_moving_average = moving_average_ra.get_average() level_2_moving_average_history.append(level_2_moving_average) level_2_moving_average_history_times.append(z_rate_result[0]) - + if progress_start is None: progress_handler.set_auto_rate(0) - progress_start = progress_handler.progress + progress_start = progress_handler.progress progress_start_z_rate = abs(moving_average) # This is the amount of z-rate change until we reach the threshold. We add 10% of the threshold # as we will surely move beyond the threshold. If we are *already* within the threshold - # this happens with a very quick first layer - we must wait for proven z-rate stability: + # this happens with a very quick first layer - we must wait for proven z-rate stability: # we handle this by the max condition, which applies when threshold is larger than the start z-rate; # this will promptly cause the progress to transition to 95% and enter the final approach phase. progress_z_rate_range = max(1.0, (progress_start_z_rate - threshold) + 0.1 * threshold) @@ -563,7 +563,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if progress_start > 0.1: # This is unexpected. The value should be close to 5%. Force it, even though we'll jump # progress backwards. - progress_handler.progress = progress_start = 0.1 + progress_handler.progress = progress_start = 0.1 logging.warning(f"{self.name}: unexpected progress_start value {progress_start:.2f}, resetting to 0.1 to avoid confusion.") # Hold back 5% of progress to avoid confusion while waiting for hold count and trend checks to pass. @@ -575,7 +575,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): if progress_handler.progress >= 0.949 and not progress_on_final_approach: # We're on the final approach to 100% progress. For now, fake a slow approach to 99% - # over the next 10 minutes for user confidence. MVP implementation, we may want to + # over the next 10 minutes for user confidence. MVP implementation, we may want to # improve this later. progress_on_final_approach = True progress_handler.set_auto_rate(0.04 / 600.0) @@ -602,7 +602,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): moving_average_history, moving_average_history_times, trend_check[0], trend_check[1], threshold ) for trend_check in moving_average_trend_checks) - + if level_2_moving_average is not None: if abs(level_2_moving_average) <= threshold: level_2_moving_average_hold_count += 1 @@ -623,7 +623,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): msg = f"Adaptive heat soak completed in {self._format_seconds(elapsed)}." gcmd.respond_info(msg) return - + if should_log: logging.info( f"{self.name}: elapsed={elapsed:.1f} s, progress={progress_handler.progress * 100.0:.2f}%, " @@ -634,7 +634,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): logging.info(f"{self.name}: elapsed={elapsed:.1f} s, waiting for first moving average to be available...") finally: if progress_handler is not None: - progress_handler.disable() + progress_handler.disable() desc_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES = "For developer use only. This command is used to run diagnostics for Beacon adaptive heat soak." def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): @@ -667,7 +667,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK_CAPTURE_Z_RATES(self, gcmd): if self.printer.is_shutdown(): raise else: - raise self.printer.command_error(f"Error calculating Z-rate: {e}") + raise self.printer.command_error(f"Error calculating Z-rate: {e}") from e gcmd.respond_info(f"Z-rate {z_rate_result[1]:.3f} nm/s") @@ -733,7 +733,7 @@ def _format_seconds(self, seconds): hours = seconds // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 - + if hours > 0: if minutes > 0 or secs > 0: if secs > 0: diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index c454deb58..f08a72f53 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -21,7 +21,7 @@ def __init__(self, config): self.printer = config.get_printer() self.reactor = self.printer.get_reactor() self.gcode = self.printer.lookup_object('gcode') - self.name = config.get_name() + self.name = config.get_name() self.status = None self.ratos = None @@ -29,7 +29,7 @@ def __init__(self, config): self.toolhead = None self.dual_carriage = None self.orig_cmd = None - + ####### # Config ####### @@ -62,18 +62,18 @@ def __init__(self, config): if self.disabled: logging.info(f"{self.name}: beacon true zero correction is disabled by configuration.") return - + if config.has_section('beacon'): self.printer.register_event_handler("klippy:connect", self._handle_connect) self.printer.register_event_handler("homing:home_rails_end", self._handle_homing_move_end) self.printer.register_event_handler("stepper_enable:motor_off", - self._handle_motor_off) - + self._handle_motor_off) + else: logging.info(f"{self.name}: beacon is not configured, beacon true zero correction disabled.") - + def _handle_connect(self): self.ratos = self.printer.lookup_object('ratos') self.ratos_z_offset = self.printer.lookup_object('ratos_z_offset') @@ -83,22 +83,22 @@ def _handle_connect(self): self.dual_carriage = self.printer.lookup_object("dual_carriage", None) self.orig_cmd = self.gcode.register_command(BEACON_AUTO_CALIBRATE, None) - if self.orig_cmd == None: + if self.orig_cmd is None: raise self.printer.config_error(f"{BEACON_AUTO_CALIBRATE} command is not registered, {self.name} cannot be enabled. Ensure that [beacon] occurs before [{self.name}] in the configuration.") - + self.gcode.register_command( - BEACON_AUTO_CALIBRATE, + BEACON_AUTO_CALIBRATE, self.cmd_BEACON_AUTO_CALIBRATE, desc=self.desc_BEACON_AUTO_CALIBRATE) self.gcode.register_command( - '_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS', + '_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS', self.cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS, desc=self.desc_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS) def _handle_homing_move_end(self, homing_state, rails): # Clear the true zero correction offset if the Z axis is homed. - # Any existing true zero correction is invalidated when z is re-homed. + # Any existing true zero correction is invalidated when z is re-homed. if 2 in homing_state.get_axes(): self.ratos_z_offset.set_offset('true_zero_correction', 0) @@ -109,7 +109,7 @@ def _handle_motor_off(self, print_time): ###### # Commands - ###### + ###### desc_BEACON_AUTO_CALIBRATE = "Automatically calibrates the Beacon probe. Extended with RatOS multi-point probing for improved true zero consistency. Use SKIP_MULTIPOINT_PROBING=1 to bypass." def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): # Clear existing offset @@ -118,7 +118,7 @@ def cmd_BEACON_AUTO_CALIBRATE(self, gcmd): skip = gcmd.get('SKIP_MULTIPOINT_PROBING', '').lower() in ('1', 'true', 'yes') if skip: return self.orig_cmd(gcmd) - + zero_xy = self.toolhead.get_position()[:2] retval = self.orig_cmd(gcmd) self._check_homed() @@ -139,7 +139,7 @@ def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): samples_tolerance_retries = gcmd.get_int('SAMPLES_TOLERANCE_RETRIES', 10, minval=0) nozzle_tip_dia = self._get_nozzle_tip_diameter() - + # Calculate the nozzle-based min span as the length of the side of a # square with area four times the footprint of COUNT nozzle tips. span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * point_count * 4.) @@ -171,7 +171,7 @@ def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): + "".join(" " + k + "=" + v for k, v in probe_args.items()), probe_args ) - + timestamp = time.strftime("%Y%m%d_%H%M%S") filename = f'/home/pi/printer_data/config/mpp_capture_{timestamp}.csv' @@ -184,12 +184,12 @@ def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): f.write(f"# Nozzle Tip Diameter: {nozzle_tip_dia:.3f}mm, Span: {span:.3f}mm\n") f.write(f"# Zero XY Position: {zero_xy_position[0]:.3f}, {zero_xy_position[1]:.3f}\n") f.write(f"# Range X: {range_x[0]:.3f} to {range_x[1]:.3f}, Range Y: {range_y[0]:.3f} to {range_y[1]:.3f}\n") - + def cb(_, positions): f.write(','.join(str(p[2]) for p in positions) + '\n') f.flush() return 'done' - + probe_helper = probe.ProbePointsHelper(self.config, cb, []) for batch_index in range(batch_count): @@ -224,35 +224,35 @@ def _generate_points(self, n, x_lim, y_lim, min_dist, avoid_centre=True, max_ite # Generate a candidate point uniformly within the given x and y limits. candidate = np.array([np.random.uniform(x_lim[0], x_lim[1]), np.random.uniform(y_lim[0], y_lim[1])]) - + # Check that candidate is at least min_dist away from every existing point. if ((not avoid_centre) or np.linalg.norm(candidate - centre) >= min_dist) \ and all(np.linalg.norm(candidate - p) >= min_dist for p in points): points.append(candidate.tolist()) # don't leak numpy types - + iterations += 1 - + if len(points) < n: raise self.gcode.error( "Could not generate all required probe points within the specified iteration limit. " "The conditions are too strict.") - + return points def _get_nozzle_diameter(self): extruder_name = 'extruder' - + if self.dual_carriage and self.dual_carriage.dc[1].mode.lower() == 'primary': extruder_name = 'extruder1' - + extruder = self.printer.lookup_object(extruder_name) nozzle_diameter = extruder.nozzle_diameter return nozzle_diameter - + def _get_nozzle_tip_diameter(self, nozzle_diameter=None): if nozzle_diameter is None: nozzle_diameter = self._get_nozzle_diameter() - + # Based on V6 standard, total nozzle tip diameter is typically 2.5 times hole diameter (spec'd up to 0.8mm), # except below 0.25mm where it's 1.5 times hole diameter. FIN specifies 2.0 times hole diameter. # Slice GammaMaster 2.4mm nozzle has ~3.75mm tip (from their published STEP model), a multiplier @@ -263,7 +263,7 @@ def _get_nozzle_tip_diameter(self, nozzle_diameter=None): nozzle_tip_dia = 2.5 * nozzle_diameter else: nozzle_tip_dia = nozzle_diameter + 1.35 - + return nozzle_tip_dia def _prepare_probe_command(self, gcmd): @@ -288,7 +288,7 @@ def _prepare_probe_command(self, gcmd): + "".join(" " + k + "=" + v for k, v in probe_args.items()), probe_args ) - + def _validate_probing_region(self, range_x, range_y, span): r = self.ratos.get_beacon_probing_regions() probable_x = (r.contact_min[0], r.contact_max[0]) @@ -302,10 +302,10 @@ def in_range(r, value): in_range(probable_y, range_y[0]) and in_range(probable_y, range_y[1])): self.ratos.console_echo(RATOS_TITLE, 'error', f'The required probing region ({span:.1f}x{span:.1f}) would probe outside the configured contact probing area.') - raise self.gcmd.error('The required probing region would probe outside the contact probing area') + raise self.gcmd.error('The required probing region would probe outside the contact probing area') class ProbingSession: - + def __init__(self, tzc:BeaconTrueZeroCorrection, gcmd, zero_xy_position): self.gcmd = gcmd self.tzc = tzc @@ -340,12 +340,12 @@ def run(self): if self._has_run: raise Exception("ProbingSession has already been run, and cannot be run more than once.") self._has_run = True - + num_points_to_generate = self._take - len(self._samples) + self.max_retries min_span = 9. nozzle_tip_dia = self.tzc._get_nozzle_tip_diameter() - + # Calculate the nozzle-based min span as the length of the side of a square with area four times # the footprint of COUNT nozzle tips. # @@ -357,10 +357,10 @@ def run(self): nozzle_based_min_span = math.sqrt(math.pi * (nozzle_tip_dia/2)**2 * num_points_to_generate * 4.) span = max(min_span, nozzle_based_min_span) - half_span = span / 2. + half_span = span / 2. logging.info(f"{self.tzc.name}: count: {num_points_to_generate} min_span: {min_span} nozzle_tip_dia: {nozzle_tip_dia:.3f} nozzle_based_min_span: {nozzle_based_min_span:.2f} use_span: {span:.2f}") - + # Calculate probing region range_x = (self.zero_xy_position[0] - half_span, self.zero_xy_position[0] + half_span) range_y = (self.zero_xy_position[1] - half_span, self.zero_xy_position[1] + half_span) @@ -373,24 +373,24 @@ def run(self): self._next_points_index = self._take - len(self._samples) self.probe_helper.update_probe_points(self._points[:self._next_points_index], 1) self.probe_helper.start_probe(probe_gcmd) - + self._finalize() - + def _finalize(self): if self._finalize_result == 'retry': self.tzc.ratos.console_echo( - RATOS_TITLE, - 'error', + RATOS_TITLE, + 'error', 'One or more z values were out of range, maximum retries exceeded.') raise self.gcmd.error('One or more z values were out of range, maximum retries exceeded.') elif isinstance(self._finalize_result, float): if self._finalize_result < -0.2: # Sanity check to reduce the risk of bed damage self.tzc.ratos.console_echo( - RATOS_TITLE, - 'error', + RATOS_TITLE, + 'error', f'The measured true zero correction {self._finalize_result:.6f} is below the safety limit of -0.2mm._N_This is not expected behaviour.') - raise self.gcmd.error(f'Measured correction is below safety limit') + raise self.gcmd.error('Measured correction is below safety limit') logging.info(f'{self.tzc.name}: applying correction {self._finalize_result:.6f}') self.gcmd.respond_info(f'Applying true zero correction of {self._finalize_result*1000.:.1f} µm') self.tzc.ratos_z_offset.set_offset('true_zero_correction', self._finalize_result) @@ -409,7 +409,7 @@ def _probe_finalize(self, _, positions): logging.info(f'{self.tzc.name}: samples: {", ".join(f"{z:.6f}" for z in self._samples)} using: {", ".join(f"{z:.6f}" for z in use_samples)}') self._finalize_result = float(np.mean(use_samples)) return 'done' - + rejects = [z for z in zvals if z >= self.tzc.z_rejection_threshold] logging.info(f'{self.tzc.name}: rejected z-values: {", ".join(f"{z:.6f}" for z in rejects)}') @@ -420,11 +420,11 @@ def _probe_finalize(self, _, positions): self.probe_helper.update_probe_points(self._points[self._next_points_index:self._next_points_index + len(rejects)], 1) self._next_points_index += len(rejects) return 'retry' - + self.gcmd.respond_info(f'{len(rejects)} z value(s) were out of range, exceeding the number of available retry points.') self._finalize_result = 'retry' return 'done' - + # Register the configuration def load_config(config): return BeaconTrueZeroCorrection(config) \ No newline at end of file From 7be68532e09ff8d4a876e371cd625063b45c4b01 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 2 Dec 2025 18:02:11 +0000 Subject: [PATCH 130/139] fix(extras): move lookup to after validation --- configuration/klippy/beacon_mesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 56a5e27c4..558682c01 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -645,7 +645,6 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: measured_mesh_params = measured_zmesh.get_mesh_params() measured_mesh_name = measured_zmesh.get_profile_name() - measured_mesh_bed_temp = measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER] if not self._validate_extended_parameters( measured_mesh_params, @@ -655,6 +654,8 @@ def apply_scan_compensation(self, comp_mesh_profile_name) -> bool: allowed_probe_methods=(RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY, RATOS_MESH_BEACON_PROBE_METHOD_PROXIMITY_AUTOMATIC)): return False + measured_mesh_bed_temp = measured_mesh_params[RATOS_MESH_BED_TEMP_PARAMETER] + if comp_mesh_profile_name.lower() == RATOS_COMPENSATION_MESH_NAME_AUTO: comp_mesh_profile_name = self.auto_select_compensation_mesh(measured_mesh_bed_temp) From 62b6f6fb393497ae37a3776c9ff3267e485976f6 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 2 Dec 2025 18:11:38 +0000 Subject: [PATCH 131/139] fix(extras): log correct value and use strict `is True` check --- configuration/klippy/beacon_adaptive_heat_soak.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index eeb88cb6d..686a957b2 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -616,7 +616,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): trend_check[0], trend_check[1], threshold ) for trend_check in level_2_moving_average_trend_checks) - if moving_average_trend_checks_passed == True or level_2_moving_average_trend_checks_passed == True: + if moving_average_trend_checks_passed is True or level_2_moving_average_trend_checks_passed is True: if elapsed < minimum_wait: min_wait_satisfied = False else: @@ -628,7 +628,7 @@ def cmd_BEACON_WAIT_FOR_PRINTER_HEAT_SOAK(self, gcmd): logging.info( f"{self.name}: elapsed={elapsed:.1f} s, progress={progress_handler.progress * 100.0:.2f}%, " f"ma={moving_average:.2f} nm/s, ma_hold_count={moving_average_hold_count}/{moving_average_target_hold_count}, ma_trend_checks_passed={moving_average_trend_checks_passed}, " - f"ma2={float('inf') if level_2_moving_average is None else level_2_moving_average:.2f} nm/s, ma2_hold_count={level_2_moving_average_hold_count}/{level_2_moving_average_target_hold_count}, ma2_trend_checks_passed={level_2_moving_average_trend_checks}, " + f"ma2={float('inf') if level_2_moving_average is None else level_2_moving_average:.2f} nm/s, ma2_hold_count={level_2_moving_average_hold_count}/{level_2_moving_average_target_hold_count}, ma2_trend_checks_passed={level_2_moving_average_trend_checks_passed}, " f"min_wait_satisfied={min_wait_satisfied}, threshold={threshold:.2f} nm/s") elif should_log: logging.info(f"{self.name}: elapsed={elapsed:.1f} s, waiting for first moving average to be available...") From 660efe3a978b44ffebfc361d4af45386696a91de Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Tue, 2 Dec 2025 18:20:54 +0000 Subject: [PATCH 132/139] fix(extras): don't access non-existent class member, and add belt and braces check for a None return --- configuration/klippy/beacon_true_zero_correction.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index f08a72f53..01aaa3e49 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -291,6 +291,11 @@ def _prepare_probe_command(self, gcmd): def _validate_probing_region(self, range_x, range_y, span): r = self.ratos.get_beacon_probing_regions() + + if r is None: + # This should not be possible, as this code should only be called when beacon and bed_mesh are present. + raise self.gcode.error('get_beacon_probing_regions() unexpectedly returned None, this should not be possible.') + probable_x = (r.contact_min[0], r.contact_max[0]) probable_y = (r.contact_min[1], r.contact_max[1]) @@ -302,7 +307,7 @@ def in_range(r, value): in_range(probable_y, range_y[0]) and in_range(probable_y, range_y[1])): self.ratos.console_echo(RATOS_TITLE, 'error', f'The required probing region ({span:.1f}x{span:.1f}) would probe outside the configured contact probing area.') - raise self.gcmd.error('The required probing region would probe outside the contact probing area') + raise self.gcode.error('The required probing region would probe outside the contact probing area') class ProbingSession: From 856c53485300d93ffc743759c935a26ed54c6fda Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 11:21:30 +0000 Subject: [PATCH 133/139] chore(beacon-heatsoak): clean up self.reactor initialization --- configuration/klippy/beacon_adaptive_heat_soak.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/configuration/klippy/beacon_adaptive_heat_soak.py b/configuration/klippy/beacon_adaptive_heat_soak.py index 686a957b2..0fb6d78e2 100644 --- a/configuration/klippy/beacon_adaptive_heat_soak.py +++ b/configuration/klippy/beacon_adaptive_heat_soak.py @@ -287,7 +287,6 @@ def __init__(self, config): # TODO: Make trend checks configurable. # Setup - self.reactor = None self.beacon = None # Register commands @@ -315,8 +314,6 @@ def __init__(self, config): self._handle_connect) def _handle_connect(self): - self.reactor = self.printer.get_reactor() - if self.config.has_section("beacon"): self.beacon = self.printer.lookup_object('beacon') From dc557534af03cf24cb14769764596461c3e39a21 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 11:24:04 +0000 Subject: [PATCH 134/139] chore(cleanup): fix comment typo --- configuration/klippy/beacon_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index 558682c01..fda1915a6 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1,4 +1,4 @@ -# Beacaon contact compensation mesh +# Beacon contact compensation mesh # # Copyright (C) 2024 Helge Keck # Copyright (C) 2024-2025 Mikkel Schmidt From 8d5981a2eafe6fb45ad63adea28c42e23640a95e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 11:26:28 +0000 Subject: [PATCH 135/139] fix(beacon-mesh): preserve traceback when raising exceptions --- configuration/klippy/beacon_mesh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/klippy/beacon_mesh.py b/configuration/klippy/beacon_mesh.py index fda1915a6..5ed9b505c 100644 --- a/configuration/klippy/beacon_mesh.py +++ b/configuration/klippy/beacon_mesh.py @@ -1298,7 +1298,7 @@ def _install_and_save_new_mesh( try: self.bed_mesh.bmc.update_config(bed_mesh_calibrate_like_command) except BedMesh.BedMeshError as e: - raise RatOSBeaconMeshError(f"Error updating bed mesh config: {str(e)}") + raise RatOSBeaconMeshError(f"Error updating bed mesh config: {str(e)}") from e params = dict(self.bed_mesh.bmc.mesh_config) params.update(extra_params) @@ -1312,7 +1312,7 @@ def _install_and_save_new_mesh( try: z_mesh.build_mesh(probed_points) except BedMesh.BedMeshError as e: - raise RatOSBeaconMeshError(str(e)) + raise RatOSBeaconMeshError(str(e)) from e self.bed_mesh.set_mesh(z_mesh) self.bed_mesh.save_profile(profile_name) From 500e2ad377928036332b42e0d49b759aeef36e35 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 11:29:35 +0000 Subject: [PATCH 136/139] fix(macros): fix unbalanced brace typo --- configuration/macros/mesh.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index 3d5c7ce90..c4fcbf247 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -295,7 +295,7 @@ gcode: {% endif %} # mesh - RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0|round(1)} Y0={mesh_y0|round(1)} X1={mesh_x1|round(1)} Y1={mesh_y1}|round(1)}" + RATOS_ECHO PREFIX="Adaptive Mesh" MSG="mesh coordinates X0={mesh_x0|round(1)} Y0={mesh_y0|round(1)} X1={mesh_x1|round(1)} Y1={mesh_y1|round(1)}" {% if printer.fastconfig.settings.beacon is defined %} {% if beacon_contact_bed_mesh %} BED_MESH_CALIBRATE PROBE_METHOD=contact USE_CONTACT_AREA=1 SAMPLES={beacon_contact_bed_mesh_samples} PROFILE="{default_profile}" ALGORITHM={algorithm} MESH_MIN={mesh_x0},{mesh_y0} MESH_MAX={mesh_x1},{mesh_y1} PROBE_COUNT={mesh_count_x},{mesh_count_y} RELATIVE_REFERENCE_INDEX=-1 From 84276ee7feacaafb905b8ef6659284345b875d1e Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 11:32:16 +0000 Subject: [PATCH 137/139] chore(spelling): fix spelling in comment --- configuration/printers/base.cfg | 2 +- configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg | 2 +- configuration/printers/v-core-4-idex/v-core-4-idex.cfg | 2 +- configuration/printers/v-core-4/v-core-4.cfg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration/printers/base.cfg b/configuration/printers/base.cfg index b5be12de6..38a3e1e5f 100644 --- a/configuration/printers/base.cfg +++ b/configuration/printers/base.cfg @@ -43,6 +43,6 @@ allow_unknown_gcode_generator: True [exclude_object] [bed_mesh] -split_delta_z: 0.01 # Avoid visible surface stripe arfetacts +split_delta_z: 0.01 # Avoid visible surface stripe artifacts [fastconfig] \ No newline at end of file diff --git a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg index 8b2b329b8..a0e1a5066 100644 --- a/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg +++ b/configuration/printers/v-core-4-hybrid/v-core-4-hybrid.cfg @@ -8,7 +8,7 @@ variable_beacon_adaptive_heat_soak: True [bed_mesh] -split_delta_z: 0.001 # Avoid visible surface stripe arfetacts +split_delta_z: 0.001 # Avoid visible surface stripe artifacts [heater_bed] heater_pin: heater_bed_heating_pin diff --git a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg index f1f99b694..d449cb8c6 100644 --- a/configuration/printers/v-core-4-idex/v-core-4-idex.cfg +++ b/configuration/printers/v-core-4-idex/v-core-4-idex.cfg @@ -8,7 +8,7 @@ variable_beacon_adaptive_heat_soak: True [bed_mesh] -split_delta_z: 0.001 # Avoid visible surface stripe arfetacts +split_delta_z: 0.001 # Avoid visible surface stripe artifacts [heater_bed] heater_pin: heater_bed_heating_pin diff --git a/configuration/printers/v-core-4/v-core-4.cfg b/configuration/printers/v-core-4/v-core-4.cfg index 39f31d1d9..fb1eefefc 100644 --- a/configuration/printers/v-core-4/v-core-4.cfg +++ b/configuration/printers/v-core-4/v-core-4.cfg @@ -8,7 +8,7 @@ variable_beacon_adaptive_heat_soak: True [bed_mesh] -split_delta_z: 0.001 # Avoid visible surface stripe arfetacts +split_delta_z: 0.001 # Avoid visible surface stripe artifacts [heater_bed] heater_pin: heater_bed_heating_pin From a49b49cae99a60286ecd95407e428153029117f4 Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 12:04:16 +0000 Subject: [PATCH 138/139] chore(spelling): fix typo --- configuration/macros/mesh.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/macros/mesh.cfg b/configuration/macros/mesh.cfg index c4fcbf247..72fe8d686 100644 --- a/configuration/macros/mesh.cfg +++ b/configuration/macros/mesh.cfg @@ -155,7 +155,7 @@ gcode: {% set x1 = params.X1|default(-1)|float %} {% set y1 = params.Y1|default(-1)|float %} - RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Recieved coordinates X0={x0|round(1)} Y0={y0|round(1)} X1={x1|round(1)} Y1={y1|round(1)}" + RATOS_ECHO PREFIX="Adaptive Mesh" MSG="Received coordinates X0={x0|round(1)} Y0={y0|round(1)} X1={x1|round(1)} Y1={y1|round(1)}" {% if x0 >= x1 or y0 >= y1 %} # coordinates are invalid, fall back to full bed mesh From afc89dafb08ce82dfdf889c33bb2584aae2684fd Mon Sep 17 00:00:00 2001 From: Tom Glastonbury Date: Wed, 3 Dec 2025 12:18:57 +0000 Subject: [PATCH 139/139] fix(beacon-true-zero): avoid hard-coded path --- configuration/klippy/beacon_true_zero_correction.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configuration/klippy/beacon_true_zero_correction.py b/configuration/klippy/beacon_true_zero_correction.py index 01aaa3e49..7f8a6a5af 100644 --- a/configuration/klippy/beacon_true_zero_correction.py +++ b/configuration/klippy/beacon_true_zero_correction.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. +import os import math, time, logging, socket import numpy as np from . import probe @@ -173,7 +174,9 @@ def cmd_BEACON_TRUE_ZERO_CORRECTION_DIAGNOSTICS(self, gcmd): ) timestamp = time.strftime("%Y%m%d_%H%M%S") - filename = f'/home/pi/printer_data/config/mpp_capture_{timestamp}.csv' + config_file = self.printer.get_start_args()['config_file'] + config_dir = os.path.dirname(config_file) + filename = os.path.join(config_dir, f'mpp_capture_{timestamp}.csv') gcmd.respond_info(f"Capturing diagnostic data to {filename}...")