From fdf15248d0a373ff780adc7970d54656b35755be Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:31:24 +0100 Subject: [PATCH 01/20] feat: Add `screw_factor` and `screw_direction` options --- docs/Config_Reference.md | 15 +++++- klippy/extras/screws_tilt_adjust.py | 77 +++++++++++++++++------------ 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1caa18a61..179b46550 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1486,10 +1486,23 @@ information. #screw_thread: CW-M3 # The type of screw used for bed leveling, M3, M4, or M5, and the # rotation direction of the knob that is used to level the bed. -# Accepted values: CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M8, CCW-M8. +# Accepted values: CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, +# CW-M6, CCW-M6, CW-M8, CCW-M8. # Default value is CW-M3 which most printers use. A clockwise # rotation of the knob decreases the gap between the nozzle and the # bed. Conversely, a counter-clockwise rotation increases the gap. +# This option cannot be used together with 'screw_factor' or +# 'screw_direction'. +#screw_factor: +# The thread pitch (in mm) of the bed leveling screw. This allows +# using any screw size, not just the predefined ones in +# 'screw_thread'. The default is 0.5 (M3 thread pitch). This option +# cannot be used together with 'screw_thread'. +#screw_direction: +# The rotation direction of the knob used to level the bed. Accepted +# values: CW, CCW. The default is CW. A clockwise rotation of the +# knob decreases the gap between the nozzle and the bed. This option +# cannot be used together with 'screw_thread'. #use_probe_xy_offsets: False # If True, apply the `[probe]` XY offsets to the probed positions. The # default is False. diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 5ca685af4..e22c61277 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -30,21 +30,50 @@ def __init__(self, config): raise config.error( "screws_tilt_adjust: Must have at least three screws" ) - self.threads = { - "CW-M3": 0, - "CCW-M3": 1, - "CW-M4": 2, - "CCW-M4": 3, - "CW-M5": 4, - "CCW-M5": 5, - "CW-M6": 6, - "CCW-M6": 7, - "CW-M8": 8, - "CCW-M8": 9, - } - self.thread = config.getchoice( - "screw_thread", self.threads, default="CW-M3" - ) + # Screw parameters: support both legacy 'screw_thread' and + # universal 'screw_factor'/'screw_direction' options. + screw_thread = config.get("screw_thread", None) + screw_factor = config.getfloat("screw_factor", None, above=0.) + screw_direction = config.get("screw_direction", None) + if screw_thread is not None: + if screw_factor is not None or screw_direction is not None: + raise config.error( + "screws_tilt_adjust: 'screw_thread' cannot be used " + "together with 'screw_factor' or 'screw_direction'" + ) + thread_map = { + "CW-M3": (0.5, "CW"), + "CCW-M3": (0.5, "CCW"), + "CW-M4": (0.7, "CW"), + "CCW-M4": (0.7, "CCW"), + "CW-M5": (0.8, "CW"), + "CCW-M5": (0.8, "CCW"), + "CW-M6": (1.0, "CW"), + "CCW-M6": (1.0, "CCW"), + "CW-M8": (1.25, "CW"), + "CCW-M8": (1.25, "CCW"), + } + if screw_thread not in thread_map: + raise config.error( + "screws_tilt_adjust: Invalid screw_thread '%s'. " + "Accepted values: %s" + % (screw_thread, ", ".join(sorted(thread_map.keys()))) + ) + self.screw_factor, self.screw_direction = thread_map[ + screw_thread + ] + else: + self.screw_factor = ( + screw_factor if screw_factor is not None else 0.5 + ) + self.screw_direction = ( + screw_direction if screw_direction is not None else "CW" + ) + if self.screw_direction not in ("CW", "CCW"): + raise config.error( + "screws_tilt_adjust: 'screw_direction' must be " + "'CW' or 'CCW'" + ) # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] self.probe_helper = probe.ProbePointsHelper( @@ -89,21 +118,7 @@ def get_status(self, eventtime): def probe_finalize(self, offsets, positions): self.results = {} self.max_diff_error = False - # Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6 - # and CCW-M6 - threads_factor = { - 0: 0.5, - 1: 0.5, - 2: 0.7, - 3: 0.7, - 4: 0.8, - 5: 0.8, - 6: 1.0, - 7: 1.0, - 8: 1.25, - 9: 1.25, - } - is_clockwise_thread = (self.thread & 1) == 0 + is_clockwise_thread = self.screw_direction == "CW" screw_diff = [] # Process the read Z values if self.direction is not None: @@ -146,7 +161,7 @@ def probe_finalize(self, offsets, positions): if abs(diff) < 0.001: adjust = 0 else: - adjust = diff / threads_factor.get(self.thread, 0.5) + adjust = diff / self.screw_factor if is_clockwise_thread: sign = "CW" if adjust >= 0 else "CCW" else: From 49fde1394f8e2cb1c01bd3fa20b67d9b75de5f59 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:39:09 +0100 Subject: [PATCH 02/20] chore: Clean up whitespace and line breaks in screws_tilt_adjust.py --- klippy/extras/screws_tilt_adjust.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index e22c61277..a447f70f2 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -33,7 +33,7 @@ def __init__(self, config): # Screw parameters: support both legacy 'screw_thread' and # universal 'screw_factor'/'screw_direction' options. screw_thread = config.get("screw_thread", None) - screw_factor = config.getfloat("screw_factor", None, above=0.) + screw_factor = config.getfloat("screw_factor", None, above=0.0) screw_direction = config.get("screw_direction", None) if screw_thread is not None: if screw_factor is not None or screw_direction is not None: @@ -59,9 +59,7 @@ def __init__(self, config): "Accepted values: %s" % (screw_thread, ", ".join(sorted(thread_map.keys()))) ) - self.screw_factor, self.screw_direction = thread_map[ - screw_thread - ] + self.screw_factor, self.screw_direction = thread_map[screw_thread] else: self.screw_factor = ( screw_factor if screw_factor is not None else 0.5 @@ -71,8 +69,7 @@ def __init__(self, config): ) if self.screw_direction not in ("CW", "CCW"): raise config.error( - "screws_tilt_adjust: 'screw_direction' must be " - "'CW' or 'CCW'" + "screws_tilt_adjust: 'screw_direction' must be 'CW' or 'CCW'" ) # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] From bb984151aab09c336f5cbd638f9d068087bf15de Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:47:44 +0100 Subject: [PATCH 03/20] docs: Add detailed explanation for `screw_factor` in screws_tilt_adjust --- docs/Config_Reference.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 179b46550..24623f67f 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1498,6 +1498,16 @@ information. # using any screw size, not just the predefined ones in # 'screw_thread'. The default is 0.5 (M3 thread pitch). This option # cannot be used together with 'screw_thread'. +# Calculation: screw_factor is the bed movement for one full turn of +# the leveling screw. For most single-start metric screws, this is +# the thread pitch itself (for example, M3x0.5 -> 0.5, M4x0.7 -> 0.7, +# M5x0.8 -> 0.8). For multi-start screws, use the lead +# (lead = pitch * number_of_starts). +# Relation to adjustment output: required turns are calculated as +# abs(z_error) / screw_factor, then shown as full turns and minutes +# (01:20 = 1 turn + 20/60 turn). +# Note: this is not the same as a stepper's 'rotation_distance' value +# unless your manual bed screw is exactly that same screw/lead. #screw_direction: # The rotation direction of the knob used to level the bed. Accepted # values: CW, CCW. The default is CW. A clockwise rotation of the From 32fd2d97dc20fef6c232dd9e9b24cc2bd2ec4a54 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:55:31 +0100 Subject: [PATCH 04/20] test: Add comprehensive unit tests for screws_tilt_adjust --- test/test_screws_tilt_adjust.py | 255 ++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 test/test_screws_tilt_adjust.py diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py new file mode 100644 index 000000000..343222d54 --- /dev/null +++ b/test/test_screws_tilt_adjust.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import configparser +from unittest.mock import MagicMock, patch + +import pytest +from klippy_testing import PrinterShim + +BASE_CONFIG = """\ +[danger_options] + +[screws_tilt_adjust] +screw1: 10,30 +screw1_name: front left screw +screw2: 155,30 +screw2_name: front right screw +screw3: 155,190 +screw3_name: rear right screw +""" + + +def _make_config(tmp_path, extra=""): + cfg = tmp_path / "printer.cfg" + cfg.write_text(BASE_CONFIG + extra) + return cfg + + +def _build_sta(tmp_path, extra=""): + cfg_file = _make_config(tmp_path, extra) + start_args = {"config_file": str(cfg_file)} + with PrinterShim(start_args) as printer: + config = printer.load_config() + sta_section = config.getsection("screws_tilt_adjust") + with patch("klippy.extras.probe.ProbePointsHelper"): + from klippy.extras.screws_tilt_adjust import ScrewsTiltAdjust + + sta = ScrewsTiltAdjust(sta_section) + return sta + + +# --- Config parsing: legacy screw_thread --- + + +@pytest.mark.parametrize( + "thread,expected_factor,expected_dir", + [ + ("CW-M3", 0.5, "CW"), + ("CCW-M3", 0.5, "CCW"), + ("CW-M4", 0.7, "CW"), + ("CCW-M4", 0.7, "CCW"), + ("CW-M5", 0.8, "CW"), + ("CCW-M5", 0.8, "CCW"), + ("CW-M6", 1.0, "CW"), + ("CCW-M6", 1.0, "CCW"), + ("CW-M8", 1.25, "CW"), + ("CCW-M8", 1.25, "CCW"), + ], +) +def test_legacy_screw_thread(tmp_path, thread, expected_factor, expected_dir): + sta = _build_sta(tmp_path, f"screw_thread: {thread}") + assert sta.screw_factor == expected_factor + assert sta.screw_direction == expected_dir + + +# --- Config parsing: new universal params --- + + +def test_defaults_no_screw_params(tmp_path): + sta = _build_sta(tmp_path) + assert sta.screw_factor == 0.5 + assert sta.screw_direction == "CW" + + +def test_custom_screw_factor_and_direction(tmp_path): + sta = _build_sta(tmp_path, "screw_factor: 1.5\nscrew_direction: CCW") + assert sta.screw_factor == 1.5 + assert sta.screw_direction == "CCW" + + +def test_custom_screw_factor_only(tmp_path): + sta = _build_sta(tmp_path, "screw_factor: 2.0") + assert sta.screw_factor == 2.0 + assert sta.screw_direction == "CW" + + +def test_custom_screw_direction_only(tmp_path): + sta = _build_sta(tmp_path, "screw_direction: CCW") + assert sta.screw_factor == 0.5 + assert sta.screw_direction == "CCW" + + +# --- Config parsing: error cases --- + + +def test_error_screw_thread_with_screw_factor(tmp_path): + with pytest.raises(configparser.Error, match="cannot be used together"): + _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_factor: 0.5") + + +def test_error_screw_thread_with_screw_direction(tmp_path): + with pytest.raises(configparser.Error, match="cannot be used together"): + _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_direction: CW") + + +def test_error_invalid_screw_direction(tmp_path): + with pytest.raises(configparser.Error, match="must be"): + _build_sta(tmp_path, "screw_direction: INVALID") + + +def test_error_invalid_screw_thread(tmp_path): + with pytest.raises(configparser.Error, match="Invalid screw_thread"): + _build_sta(tmp_path, "screw_thread: CW-M99") + + +def test_error_screw_factor_zero(tmp_path): + with pytest.raises(configparser.Error): + _build_sta(tmp_path, "screw_factor: 0") + + +def test_error_screw_factor_negative(tmp_path): + with pytest.raises(configparser.Error): + _build_sta(tmp_path, "screw_factor: -1.0") + + +# --- probe_finalize calculation tests --- + + +def _make_sta_for_calc(screw_factor, screw_direction, screws, direction=None): + """Build a minimal ScrewsTiltAdjust-like object for calculation tests.""" + sta = object.__new__( + __import__( + "klippy.extras.screws_tilt_adjust", fromlist=["ScrewsTiltAdjust"] + ).ScrewsTiltAdjust + ) + sta.screw_factor = screw_factor + sta.screw_direction = screw_direction + sta.screws = screws + sta.direction = direction + sta.max_diff = None + sta.max_diff_error = False + sta.results = {} + sta.gcode = MagicMock() + sta.gcode.error = Exception + return sta + + +def test_probe_finalize_no_adjustment_needed(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CW", screws) + positions = [[10, 30, 1.0], [155, 30, 1.0], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw1"]["is_base"] is True + assert sta.results["screw2"]["adjust"] == "00:00" + assert sta.results["screw3"]["adjust"] == "00:00" + + +def test_probe_finalize_cw_adjustment(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CW", screws) + # Base z=1.0, screw2 z=0.5 → diff=0.5, turns=0.5/0.5=1.0 CW + positions = [[10, 30, 1.0], [155, 30, 0.5], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw1"]["is_base"] is True + assert sta.results["screw2"]["sign"] == "CW" + assert sta.results["screw2"]["adjust"] == "01:00" + assert sta.results["screw3"]["adjust"] == "00:00" + + +def test_probe_finalize_ccw_adjustment(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CW", screws) + # Base z=1.0, screw2 z=1.5 → diff=-0.5, turns=-0.5/0.5=-1.0 → CCW + positions = [[10, 30, 1.0], [155, 30, 1.5], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw2"]["sign"] == "CCW" + assert sta.results["screw2"]["adjust"] == "01:00" + + +def test_probe_finalize_partial_turn(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CW", screws) + # Base z=1.0, screw2 z=0.75 → diff=0.25, turns=0.25/0.5=0.5 → 00:30 + positions = [[10, 30, 1.0], [155, 30, 0.75], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw2"]["sign"] == "CW" + assert sta.results["screw2"]["adjust"] == "00:30" + + +def test_probe_finalize_custom_factor(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + # Using screw_factor=1.5 (e.g. a large lead screw) + sta = _make_sta_for_calc(1.5, "CW", screws) + # Base z=1.0, screw2 z=0.25 → diff=0.75, turns=0.75/1.5=0.5 → 00:30 + positions = [[10, 30, 1.0], [155, 30, 0.25], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw2"]["sign"] == "CW" + assert sta.results["screw2"]["adjust"] == "00:30" + + +def test_probe_finalize_ccw_screw_direction(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CCW", screws) + # Base z=1.0, screw2 z=0.5 → diff=0.5, adjust=1.0 + # CCW thread: positive adjust → sign CCW + positions = [[10, 30, 1.0], [155, 30, 0.5], [155, 190, 1.0]] + sta.probe_finalize([0, 0, 0], positions) + + assert sta.results["screw2"]["sign"] == "CCW" + assert sta.results["screw2"]["adjust"] == "01:00" + + +def test_probe_finalize_max_deviation_error(): + screws = [ + ((10, 30), "front left"), + ((155, 30), "front right"), + ((155, 190), "rear right"), + ] + sta = _make_sta_for_calc(0.5, "CW", screws) + sta.max_diff = 0.1 + # diff of 0.5 exceeds max_diff of 0.1 + positions = [[10, 30, 1.0], [155, 30, 0.5], [155, 190, 1.0]] + + with pytest.raises(Exception, match="exceeds configured limits"): + sta.probe_finalize([0, 0, 0], positions) + + assert sta.max_diff_error is True From c1f3135c0b028444109e683c16c2f1154f672246 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:18:03 +0100 Subject: [PATCH 05/20] refactor: Rename `screw_factor` to `screw_pitch` for clarity --- docs/Config_Reference.md | 8 +++---- klippy/extras/screws_tilt_adjust.py | 16 ++++++------- test/test_screws_tilt_adjust.py | 36 ++++++++++++++--------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 24623f67f..eb4534963 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1491,20 +1491,20 @@ information. # Default value is CW-M3 which most printers use. A clockwise # rotation of the knob decreases the gap between the nozzle and the # bed. Conversely, a counter-clockwise rotation increases the gap. -# This option cannot be used together with 'screw_factor' or +# This option cannot be used together with 'screw_pitch' or # 'screw_direction'. -#screw_factor: +#screw_pitch: # The thread pitch (in mm) of the bed leveling screw. This allows # using any screw size, not just the predefined ones in # 'screw_thread'. The default is 0.5 (M3 thread pitch). This option # cannot be used together with 'screw_thread'. -# Calculation: screw_factor is the bed movement for one full turn of +# Calculation: screw_pitch is the bed movement for one full turn of # the leveling screw. For most single-start metric screws, this is # the thread pitch itself (for example, M3x0.5 -> 0.5, M4x0.7 -> 0.7, # M5x0.8 -> 0.8). For multi-start screws, use the lead # (lead = pitch * number_of_starts). # Relation to adjustment output: required turns are calculated as -# abs(z_error) / screw_factor, then shown as full turns and minutes +# abs(z_error) / screw_pitch, then shown as full turns and minutes # (01:20 = 1 turn + 20/60 turn). # Note: this is not the same as a stepper's 'rotation_distance' value # unless your manual bed screw is exactly that same screw/lead. diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index a447f70f2..1b5ad8111 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -31,15 +31,15 @@ def __init__(self, config): "screws_tilt_adjust: Must have at least three screws" ) # Screw parameters: support both legacy 'screw_thread' and - # universal 'screw_factor'/'screw_direction' options. + # universal 'screw_pitch'/'screw_direction' options. screw_thread = config.get("screw_thread", None) - screw_factor = config.getfloat("screw_factor", None, above=0.0) + screw_pitch = config.getfloat("screw_pitch", None, above=0.0) screw_direction = config.get("screw_direction", None) if screw_thread is not None: - if screw_factor is not None or screw_direction is not None: + if screw_pitch is not None or screw_direction is not None: raise config.error( "screws_tilt_adjust: 'screw_thread' cannot be used " - "together with 'screw_factor' or 'screw_direction'" + "together with 'screw_pitch' or 'screw_direction'" ) thread_map = { "CW-M3": (0.5, "CW"), @@ -59,10 +59,10 @@ def __init__(self, config): "Accepted values: %s" % (screw_thread, ", ".join(sorted(thread_map.keys()))) ) - self.screw_factor, self.screw_direction = thread_map[screw_thread] + self.screw_pitch, self.screw_direction = thread_map[screw_thread] else: - self.screw_factor = ( - screw_factor if screw_factor is not None else 0.5 + self.screw_pitch = ( + screw_pitch if screw_pitch is not None else 0.5 ) self.screw_direction = ( screw_direction if screw_direction is not None else "CW" @@ -158,7 +158,7 @@ def probe_finalize(self, offsets, positions): if abs(diff) < 0.001: adjust = 0 else: - adjust = diff / self.screw_factor + adjust = diff / self.screw_pitch if is_clockwise_thread: sign = "CW" if adjust >= 0 else "CCW" else: diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index 343222d54..7948ef141 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -58,7 +58,7 @@ def _build_sta(tmp_path, extra=""): ) def test_legacy_screw_thread(tmp_path, thread, expected_factor, expected_dir): sta = _build_sta(tmp_path, f"screw_thread: {thread}") - assert sta.screw_factor == expected_factor + assert sta.screw_pitch == expected_factor assert sta.screw_direction == expected_dir @@ -67,34 +67,34 @@ def test_legacy_screw_thread(tmp_path, thread, expected_factor, expected_dir): def test_defaults_no_screw_params(tmp_path): sta = _build_sta(tmp_path) - assert sta.screw_factor == 0.5 + assert sta.screw_pitch == 0.5 assert sta.screw_direction == "CW" -def test_custom_screw_factor_and_direction(tmp_path): - sta = _build_sta(tmp_path, "screw_factor: 1.5\nscrew_direction: CCW") - assert sta.screw_factor == 1.5 +def test_custom_screw_pitch_and_direction(tmp_path): + sta = _build_sta(tmp_path, "screw_pitch: 1.5\nscrew_direction: CCW") + assert sta.screw_pitch == 1.5 assert sta.screw_direction == "CCW" -def test_custom_screw_factor_only(tmp_path): - sta = _build_sta(tmp_path, "screw_factor: 2.0") - assert sta.screw_factor == 2.0 +def test_custom_screw_pitch_only(tmp_path): + sta = _build_sta(tmp_path, "screw_pitch: 2.0") + assert sta.screw_pitch == 2.0 assert sta.screw_direction == "CW" def test_custom_screw_direction_only(tmp_path): sta = _build_sta(tmp_path, "screw_direction: CCW") - assert sta.screw_factor == 0.5 + assert sta.screw_pitch == 0.5 assert sta.screw_direction == "CCW" # --- Config parsing: error cases --- -def test_error_screw_thread_with_screw_factor(tmp_path): +def test_error_screw_thread_with_screw_pitch(tmp_path): with pytest.raises(configparser.Error, match="cannot be used together"): - _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_factor: 0.5") + _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_pitch: 0.5") def test_error_screw_thread_with_screw_direction(tmp_path): @@ -112,27 +112,27 @@ def test_error_invalid_screw_thread(tmp_path): _build_sta(tmp_path, "screw_thread: CW-M99") -def test_error_screw_factor_zero(tmp_path): +def test_error_screw_pitch_zero(tmp_path): with pytest.raises(configparser.Error): - _build_sta(tmp_path, "screw_factor: 0") + _build_sta(tmp_path, "screw_pitch: 0") -def test_error_screw_factor_negative(tmp_path): +def test_error_screw_pitch_negative(tmp_path): with pytest.raises(configparser.Error): - _build_sta(tmp_path, "screw_factor: -1.0") + _build_sta(tmp_path, "screw_pitch: -1.0") # --- probe_finalize calculation tests --- -def _make_sta_for_calc(screw_factor, screw_direction, screws, direction=None): +def _make_sta_for_calc(screw_pitch, screw_direction, screws, direction=None): """Build a minimal ScrewsTiltAdjust-like object for calculation tests.""" sta = object.__new__( __import__( "klippy.extras.screws_tilt_adjust", fromlist=["ScrewsTiltAdjust"] ).ScrewsTiltAdjust ) - sta.screw_factor = screw_factor + sta.screw_pitch = screw_pitch sta.screw_direction = screw_direction sta.screws = screws sta.direction = direction @@ -212,7 +212,7 @@ def test_probe_finalize_custom_factor(): ((155, 30), "front right"), ((155, 190), "rear right"), ] - # Using screw_factor=1.5 (e.g. a large lead screw) + # Using screw_pitch=1.5 (e.g. a large lead screw) sta = _make_sta_for_calc(1.5, "CW", screws) # Base z=1.0, screw2 z=0.25 → diff=0.75, turns=0.75/1.5=0.5 → 00:30 positions = [[10, 30, 1.0], [155, 30, 0.25], [155, 190, 1.0]] From a92906cbbd43817f730ff369a75ddfe6016c488b Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:21:09 +0100 Subject: [PATCH 06/20] docs: Simplify `screw_direction` description in screws_tilt_adjust --- docs/Config_Reference.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index eb4534963..431bbed22 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1510,8 +1510,7 @@ information. # unless your manual bed screw is exactly that same screw/lead. #screw_direction: # The rotation direction of the knob used to level the bed. Accepted -# values: CW, CCW. The default is CW. A clockwise rotation of the -# knob decreases the gap between the nozzle and the bed. This option +# values: CW, CCW. The default is CW. Rotation in this direction decreases the gap between the nozzle and the bed. This option # cannot be used together with 'screw_thread'. #use_probe_xy_offsets: False # If True, apply the `[probe]` XY offsets to the probed positions. The From 1deb78fcd3cb7d52ef0d2d5cfc0a2c06b40d0bea Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:21:47 +0100 Subject: [PATCH 07/20] refactor: Use `getchoice()` for `screw_direction` validation --- klippy/extras/screws_tilt_adjust.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 1b5ad8111..7cf48af70 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -34,7 +34,7 @@ def __init__(self, config): # universal 'screw_pitch'/'screw_direction' options. screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) - screw_direction = config.get("screw_direction", None) + screw_direction = config.getchoice("screw_direction", {"CW": "CW", "CCW": "CCW"}, None) if screw_thread is not None: if screw_pitch is not None or screw_direction is not None: raise config.error( @@ -67,10 +67,6 @@ def __init__(self, config): self.screw_direction = ( screw_direction if screw_direction is not None else "CW" ) - if self.screw_direction not in ("CW", "CCW"): - raise config.error( - "screws_tilt_adjust: 'screw_direction' must be 'CW' or 'CCW'" - ) # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] self.probe_helper = probe.ProbePointsHelper( From 669b96cf557580b64e08326c3d3750d192e341a2 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:23:40 +0100 Subject: [PATCH 08/20] refactor: Extract screw thread mapping to module-level constant --- klippy/extras/screws_tilt_adjust.py | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 7cf48af70..d480969c9 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -8,6 +8,20 @@ from . import probe +# Screw thread mapping: thread_name -> (pitch, direction) +SCREW_THREAD_MAP = { + "CW-M3": (0.5, "CW"), + "CCW-M3": (0.5, "CCW"), + "CW-M4": (0.7, "CW"), + "CCW-M4": (0.7, "CCW"), + "CW-M5": (0.8, "CW"), + "CCW-M5": (0.8, "CCW"), + "CW-M6": (1.0, "CW"), + "CCW-M6": (1.0, "CCW"), + "CW-M8": (1.25, "CW"), + "CCW-M8": (1.25, "CCW"), +} + class ScrewsTiltAdjust: def __init__(self, config): @@ -41,25 +55,13 @@ def __init__(self, config): "screws_tilt_adjust: 'screw_thread' cannot be used " "together with 'screw_pitch' or 'screw_direction'" ) - thread_map = { - "CW-M3": (0.5, "CW"), - "CCW-M3": (0.5, "CCW"), - "CW-M4": (0.7, "CW"), - "CCW-M4": (0.7, "CCW"), - "CW-M5": (0.8, "CW"), - "CCW-M5": (0.8, "CCW"), - "CW-M6": (1.0, "CW"), - "CCW-M6": (1.0, "CCW"), - "CW-M8": (1.25, "CW"), - "CCW-M8": (1.25, "CCW"), - } - if screw_thread not in thread_map: + if screw_thread not in SCREW_THREAD_MAP: raise config.error( "screws_tilt_adjust: Invalid screw_thread '%s'. " "Accepted values: %s" - % (screw_thread, ", ".join(sorted(thread_map.keys()))) + % (screw_thread, ", ".join(sorted(SCREW_THREAD_MAP.keys()))) ) - self.screw_pitch, self.screw_direction = thread_map[screw_thread] + self.screw_pitch, self.screw_direction = SCREW_THREAD_MAP[screw_thread] else: self.screw_pitch = ( screw_pitch if screw_pitch is not None else 0.5 From 90acb6b0f383ca6fda87cbbca15ec72a6e0e4499 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:25:20 +0100 Subject: [PATCH 09/20] refactor: Make `screw_thread` lookup case-insensitive --- klippy/extras/screws_tilt_adjust.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index d480969c9..6cdf35cf3 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -55,13 +55,14 @@ def __init__(self, config): "screws_tilt_adjust: 'screw_thread' cannot be used " "together with 'screw_pitch' or 'screw_direction'" ) - if screw_thread not in SCREW_THREAD_MAP: + screw_thread_result = SCREW_THREAD_MAP.get(screw_thread.upper()) + if screw_thread_result is None: raise config.error( "screws_tilt_adjust: Invalid screw_thread '%s'. " "Accepted values: %s" % (screw_thread, ", ".join(sorted(SCREW_THREAD_MAP.keys()))) ) - self.screw_pitch, self.screw_direction = SCREW_THREAD_MAP[screw_thread] + self.screw_pitch, self.screw_direction = screw_thread_result else: self.screw_pitch = ( screw_pitch if screw_pitch is not None else 0.5 From 054081d7fef6ca125341544ec2463f4c21449bf0 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:30:58 +0100 Subject: [PATCH 10/20] docs: Remove default values for `screw_pitch` and `screw_direction` --- docs/Config_Reference.md | 16 +++++++++------- klippy/extras/screws_tilt_adjust.py | 13 +++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 431bbed22..1d55cfd88 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1488,16 +1488,17 @@ information. # rotation direction of the knob that is used to level the bed. # Accepted values: CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, # CW-M6, CCW-M6, CW-M8, CCW-M8. -# Default value is CW-M3 which most printers use. A clockwise -# rotation of the knob decreases the gap between the nozzle and the +# A clockwise rotation of the knob decreases the gap between the nozzle and the # bed. Conversely, a counter-clockwise rotation increases the gap. # This option cannot be used together with 'screw_pitch' or -# 'screw_direction'. +# 'screw_direction'. Either this option must be specified, or both +# 'screw_pitch' and 'screw_direction' must be specified. #screw_pitch: # The thread pitch (in mm) of the bed leveling screw. This allows # using any screw size, not just the predefined ones in -# 'screw_thread'. The default is 0.5 (M3 thread pitch). This option -# cannot be used together with 'screw_thread'. +# 'screw_thread'. This option cannot be used together with 'screw_thread'. +# Must be specified together with 'screw_direction' if 'screw_thread' +# is not used. # Calculation: screw_pitch is the bed movement for one full turn of # the leveling screw. For most single-start metric screws, this is # the thread pitch itself (for example, M3x0.5 -> 0.5, M4x0.7 -> 0.7, @@ -1510,8 +1511,9 @@ information. # unless your manual bed screw is exactly that same screw/lead. #screw_direction: # The rotation direction of the knob used to level the bed. Accepted -# values: CW, CCW. The default is CW. Rotation in this direction decreases the gap between the nozzle and the bed. This option -# cannot be used together with 'screw_thread'. +# values: CW, CCW. Rotation in this direction decreases the gap between the nozzle and the bed. This option +# cannot be used together with 'screw_thread'. Must be specified +# together with 'screw_pitch' if 'screw_thread' is not used. #use_probe_xy_offsets: False # If True, apply the `[probe]` XY offsets to the probed positions. The # default is False. diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 6cdf35cf3..eb7416ae0 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -64,12 +64,13 @@ def __init__(self, config): ) self.screw_pitch, self.screw_direction = screw_thread_result else: - self.screw_pitch = ( - screw_pitch if screw_pitch is not None else 0.5 - ) - self.screw_direction = ( - screw_direction if screw_direction is not None else "CW" - ) + if screw_pitch is None or screw_direction is None: + raise config.error( + "screws_tilt_adjust: Must specify either 'screw_thread' " + "or both 'screw_pitch' and 'screw_direction'" + ) + self.screw_pitch = screw_pitch + self.screw_direction = screw_direction # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] self.probe_helper = probe.ProbePointsHelper( From 6cfdc0ce83c1ad6674ed5a2c13ce1f51f5cbdcd3 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:33:06 +0100 Subject: [PATCH 11/20] refactor: Format `screw_direction` getchoice call for readability --- klippy/extras/screws_tilt_adjust.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index eb7416ae0..7ba0880e9 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -48,7 +48,9 @@ def __init__(self, config): # universal 'screw_pitch'/'screw_direction' options. screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) - screw_direction = config.getchoice("screw_direction", {"CW": "CW", "CCW": "CCW"}, None) + screw_direction = config.getchoice( + "screw_direction", {"CW": "CW", "CCW": "CCW"}, None + ) if screw_thread is not None: if screw_pitch is not None or screw_direction is not None: raise config.error( From 122674acc3bed1ac757945d1b83bd97d20e2a754 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:40:19 +0100 Subject: [PATCH 12/20] refactor: Defer `screw_direction` validation until after `screw_thread` check --- klippy/extras/screws_tilt_adjust.py | 17 +++++++++----- test/test_screws_tilt_adjust.py | 35 ++++++++++++++++------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 7ba0880e9..ce7c58c13 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -48,11 +48,11 @@ def __init__(self, config): # universal 'screw_pitch'/'screw_direction' options. screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) - screw_direction = config.getchoice( - "screw_direction", {"CW": "CW", "CCW": "CCW"}, None - ) if screw_thread is not None: - if screw_pitch is not None or screw_direction is not None: + if ( + screw_pitch is not None + or config.get("screw_direction", None) is not None + ): raise config.error( "screws_tilt_adjust: 'screw_thread' cannot be used " "together with 'screw_pitch' or 'screw_direction'" @@ -66,13 +66,18 @@ def __init__(self, config): ) self.screw_pitch, self.screw_direction = screw_thread_result else: - if screw_pitch is None or screw_direction is None: + if ( + screw_pitch is None + or config.get("screw_direction", None) is None + ): raise config.error( "screws_tilt_adjust: Must specify either 'screw_thread' " "or both 'screw_pitch' and 'screw_direction'" ) self.screw_pitch = screw_pitch - self.screw_direction = screw_direction + self.screw_direction = config.getchoice( + "screw_direction", {"CW": "CW", "CCW": "CCW"} + ) # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] self.probe_helper = probe.ProbePointsHelper( diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index 7948ef141..4ede8cec9 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -65,10 +65,9 @@ def test_legacy_screw_thread(tmp_path, thread, expected_factor, expected_dir): # --- Config parsing: new universal params --- -def test_defaults_no_screw_params(tmp_path): - sta = _build_sta(tmp_path) - assert sta.screw_pitch == 0.5 - assert sta.screw_direction == "CW" +def test_error_no_screw_params(tmp_path): + with pytest.raises(configparser.Error): + _build_sta(tmp_path) def test_custom_screw_pitch_and_direction(tmp_path): @@ -77,16 +76,14 @@ def test_custom_screw_pitch_and_direction(tmp_path): assert sta.screw_direction == "CCW" -def test_custom_screw_pitch_only(tmp_path): - sta = _build_sta(tmp_path, "screw_pitch: 2.0") - assert sta.screw_pitch == 2.0 - assert sta.screw_direction == "CW" +def test_error_screw_pitch_only(tmp_path): + with pytest.raises(configparser.Error): + _build_sta(tmp_path, "screw_pitch: 2.0") -def test_custom_screw_direction_only(tmp_path): - sta = _build_sta(tmp_path, "screw_direction: CCW") - assert sta.screw_pitch == 0.5 - assert sta.screw_direction == "CCW" +def test_error_screw_direction_only(tmp_path): + with pytest.raises(configparser.Error, match="Must specify either"): + _build_sta(tmp_path, "screw_direction: CCW") # --- Config parsing: error cases --- @@ -103,8 +100,8 @@ def test_error_screw_thread_with_screw_direction(tmp_path): def test_error_invalid_screw_direction(tmp_path): - with pytest.raises(configparser.Error, match="must be"): - _build_sta(tmp_path, "screw_direction: INVALID") + with pytest.raises(configparser.Error): + _build_sta(tmp_path, "screw_pitch: 0.5\nscrew_direction: INVALID") def test_error_invalid_screw_thread(tmp_path): @@ -114,12 +111,18 @@ def test_error_invalid_screw_thread(tmp_path): def test_error_screw_pitch_zero(tmp_path): with pytest.raises(configparser.Error): - _build_sta(tmp_path, "screw_pitch: 0") + _build_sta(tmp_path, "screw_pitch: 0\nscrew_direction: CW") def test_error_screw_pitch_negative(tmp_path): with pytest.raises(configparser.Error): - _build_sta(tmp_path, "screw_pitch: -1.0") + _build_sta(tmp_path, "screw_pitch: -1.0\nscrew_direction: CW") + + +def test_legacy_screw_thread_case_insensitive(tmp_path): + sta = _build_sta(tmp_path, "screw_thread: cw-m3") + assert sta.screw_pitch == 0.5 + assert sta.screw_direction == "CW" # --- probe_finalize calculation tests --- From 2341eb69dd072b9a37d4fc9ff4d0429c58a95d33 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:46:03 +0100 Subject: [PATCH 13/20] Update docs/Config_Reference.md Co-authored-by: dalegaard --- docs/Config_Reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1d55cfd88..7dd1a10ab 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1483,7 +1483,7 @@ information. #horizontal_move_z: 5 # The height (in mm) that the head should be commanded to move to # just prior to starting a probe operation. The default is 5. -#screw_thread: CW-M3 +#screw_thread: # The type of screw used for bed leveling, M3, M4, or M5, and the # rotation direction of the knob that is used to level the bed. # Accepted values: CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, From 34aa908d7250d72634c02b7fb277383ff65b01c1 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:50:48 +0100 Subject: [PATCH 14/20] chore: Consolidate screw configuration validation logic --- klippy/extras/screws_tilt_adjust.py | 26 ++++++++++++-------------- test/test_screws_tilt_adjust.py | 6 +++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index ce7c58c13..cc470ef30 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -48,15 +48,16 @@ def __init__(self, config): # universal 'screw_pitch'/'screw_direction' options. screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) + screw_direction = config.get("screw_direction", None) + if not ( + (screw_thread is not None) + ^ (screw_pitch is not None or screw_direction is not None) + ): + raise config.error( + "screws_tilt_adjust: Must specify either 'screw_thread' " + "or both 'screw_pitch' and 'screw_direction', but not both" + ) if screw_thread is not None: - if ( - screw_pitch is not None - or config.get("screw_direction", None) is not None - ): - raise config.error( - "screws_tilt_adjust: 'screw_thread' cannot be used " - "together with 'screw_pitch' or 'screw_direction'" - ) screw_thread_result = SCREW_THREAD_MAP.get(screw_thread.upper()) if screw_thread_result is None: raise config.error( @@ -66,13 +67,10 @@ def __init__(self, config): ) self.screw_pitch, self.screw_direction = screw_thread_result else: - if ( - screw_pitch is None - or config.get("screw_direction", None) is None - ): + if screw_pitch is None or screw_direction is None: raise config.error( - "screws_tilt_adjust: Must specify either 'screw_thread' " - "or both 'screw_pitch' and 'screw_direction'" + "screws_tilt_adjust: Must specify both 'screw_pitch' " + "and 'screw_direction'" ) self.screw_pitch = screw_pitch self.screw_direction = config.getchoice( diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index 4ede8cec9..1fccc759b 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -82,7 +82,7 @@ def test_error_screw_pitch_only(tmp_path): def test_error_screw_direction_only(tmp_path): - with pytest.raises(configparser.Error, match="Must specify either"): + with pytest.raises(configparser.Error, match="Must specify both"): _build_sta(tmp_path, "screw_direction: CCW") @@ -90,12 +90,12 @@ def test_error_screw_direction_only(tmp_path): def test_error_screw_thread_with_screw_pitch(tmp_path): - with pytest.raises(configparser.Error, match="cannot be used together"): + with pytest.raises(configparser.Error, match="but not both"): _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_pitch: 0.5") def test_error_screw_thread_with_screw_direction(tmp_path): - with pytest.raises(configparser.Error, match="cannot be used together"): + with pytest.raises(configparser.Error, match="but not both"): _build_sta(tmp_path, "screw_thread: CW-M3\nscrew_direction: CW") From c8293e1c17eaace7d2a57b12089e6e2e4c0087c1 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:23:26 +0200 Subject: [PATCH 15/20] chore: update the docs --- docs/Config_Reference.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 7dd1a10ab..119c9853c 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1495,24 +1495,24 @@ information. # 'screw_pitch' and 'screw_direction' must be specified. #screw_pitch: # The thread pitch (in mm) of the bed leveling screw. This allows -# using any screw size, not just the predefined ones in +# the use of any screw size, not just those predefined in # 'screw_thread'. This option cannot be used together with 'screw_thread'. -# Must be specified together with 'screw_direction' if 'screw_thread' +# It must be specified together with 'screw_direction' if 'screw_thread' # is not used. -# Calculation: screw_pitch is the bed movement for one full turn of -# the leveling screw. For most single-start metric screws, this is -# the thread pitch itself (for example, M3x0.5 -> 0.5, M4x0.7 -> 0.7, -# M5x0.8 -> 0.8). For multi-start screws, use the lead -# (lead = pitch * number_of_starts). -# Relation to adjustment output: required turns are calculated as -# abs(z_error) / screw_pitch, then shown as full turns and minutes -# (01:20 = 1 turn + 20/60 turn). -# Note: this is not the same as a stepper's 'rotation_distance' value -# unless your manual bed screw is exactly that same screw/lead. +# Calculation: screw_pitch is the displacement of the print bed for one full +# rotation of the Z-axis screw. For most metric screws, this corresponds +# to the thread pitch itself (e.g., M3x0.5 -> 0.5, M4x0.7 -> 0.7, +# M5x0.8 -> 0.8). +# Relation to adjustment output: the required turns are calculated as +# abs(z_error) / screw_pitch, then displayed as full turns and minutes +# (01:20 = 1 turn + 20/60 turns). +# NOTE: in an ideal case, the value of 'screw_pitch' should be equal to the 'rotation_distance' of the Z axis. +# However, since the sampling point does not overlap with the screw, a slightly different 'screw_pitch' +# value may be needed to achieve accurate calibration. #screw_direction: -# The rotation direction of the knob used to level the bed. Accepted -# values: CW, CCW. Rotation in this direction decreases the gap between the nozzle and the bed. This option -# cannot be used together with 'screw_thread'. Must be specified +# The rotation direction of the knob used to level the bed. Accepted values: +# CW, CCW. Rotating in this direction reduces the gap between the nozzle and the bed. +# This option cannot be used together with 'screw_thread'. It must be specified # together with 'screw_pitch' if 'screw_thread' is not used. #use_probe_xy_offsets: False # If True, apply the `[probe]` XY offsets to the probed positions. The From a0628bebbd1df88d7cfb2c55f7a4e928053abbf3 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:49:21 +0200 Subject: [PATCH 16/20] refactor: Replace screw thread map with indexed lookup table --- klippy/extras/screws_tilt_adjust.py | 53 ++++++++++++++++++----------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index cc470ef30..2dbded900 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -8,20 +8,6 @@ from . import probe -# Screw thread mapping: thread_name -> (pitch, direction) -SCREW_THREAD_MAP = { - "CW-M3": (0.5, "CW"), - "CCW-M3": (0.5, "CCW"), - "CW-M4": (0.7, "CW"), - "CCW-M4": (0.7, "CCW"), - "CW-M5": (0.8, "CW"), - "CCW-M5": (0.8, "CCW"), - "CW-M6": (1.0, "CW"), - "CCW-M6": (1.0, "CCW"), - "CW-M8": (1.25, "CW"), - "CCW-M8": (1.25, "CCW"), -} - class ScrewsTiltAdjust: def __init__(self, config): @@ -44,9 +30,22 @@ def __init__(self, config): raise config.error( "screws_tilt_adjust: Must have at least three screws" ) - # Screw parameters: support both legacy 'screw_thread' and - # universal 'screw_pitch'/'screw_direction' options. - screw_thread = config.get("screw_thread", None) + self.threads = { + "CW-M3": 0, + "CCW-M3": 1, + "CW-M4": 2, + "CCW-M4": 3, + "CW-M5": 4, + "CCW-M5": 5, + "CW-M6": 6, + "CCW-M6": 7, + "CW-M8": 8, + "CCW-M8": 9, + } + screw_thread = config.getchoice( + "screw_thread", self.threads, default="CW-M3" + ) + screw_pitch = config.getfloat("screw_pitch", None, above=0.0) screw_direction = config.get("screw_direction", None) if not ( @@ -120,7 +119,21 @@ def get_status(self, eventtime): def probe_finalize(self, offsets, positions): self.results = {} self.max_diff_error = False - is_clockwise_thread = self.screw_direction == "CW" + # Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6 + # and CCW-M6 + threads_factor = { + 0: 0.5, + 1: 0.5, + 2: 0.7, + 3: 0.7, + 4: 0.8, + 5: 0.8, + 6: 1.0, + 7: 1.0, + 8: 1.25, + 9: 1.25, + } + is_clockwise_thread = (self.thread & 1) == 0 screw_diff = [] # Process the read Z values if self.direction is not None: @@ -163,7 +176,7 @@ def probe_finalize(self, offsets, positions): if abs(diff) < 0.001: adjust = 0 else: - adjust = diff / self.screw_pitch + adjust = diff / threads_factor.get(self.thread, 0.5) if is_clockwise_thread: sign = "CW" if adjust >= 0 else "CCW" else: @@ -192,4 +205,4 @@ def probe_finalize(self, offsets, positions): def load_config(config): - return ScrewsTiltAdjust(config) + return ScrewsTiltAdjust(config) \ No newline at end of file From c137209fd5b3585ff0a5160b290405d3a7f1f786 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:07:41 +0200 Subject: [PATCH 17/20] refactor: Replace screw_thread map with indexed lookup and manual validation --- klippy/extras/screws_tilt_adjust.py | 41 ++++++++++++++++++++++------- test/test_screws_tilt_adjust.py | 11 ++------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 2dbded900..435c1cb74 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -42,10 +42,19 @@ def __init__(self, config): "CW-M8": 8, "CCW-M8": 9, } - screw_thread = config.getchoice( - "screw_thread", self.threads, default="CW-M3" - ) - + threads_factor = { + 0: 0.5, + 1: 0.5, + 2: 0.7, + 3: 0.7, + 4: 0.8, + 5: 0.8, + 6: 1.0, + 7: 1.0, + 8: 1.25, + 9: 1.25, + } + screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) screw_direction = config.get("screw_direction", None) if not ( @@ -57,14 +66,18 @@ def __init__(self, config): "or both 'screw_pitch' and 'screw_direction', but not both" ) if screw_thread is not None: - screw_thread_result = SCREW_THREAD_MAP.get(screw_thread.upper()) - if screw_thread_result is None: + # Validate the screw_thread choice manually + if screw_thread not in self.threads: raise config.error( "screws_tilt_adjust: Invalid screw_thread '%s'. " "Accepted values: %s" - % (screw_thread, ", ".join(sorted(SCREW_THREAD_MAP.keys()))) + % (screw_thread, ", ".join(sorted(self.threads.keys()))) ) - self.screw_pitch, self.screw_direction = screw_thread_result + # config.get returns the string key directly + thread_value = self.threads[screw_thread] + self.thread = screw_thread + self.screw_pitch = threads_factor[thread_value] + self.screw_direction = "CW" if (thread_value & 1) == 0 else "CCW" else: if screw_pitch is None or screw_direction is None: raise config.error( @@ -75,6 +88,7 @@ def __init__(self, config): self.screw_direction = config.getchoice( "screw_direction", {"CW": "CW", "CCW": "CCW"} ) + self.thread = None # Initialize ProbePointsHelper points = [coord for coord, name in self.screws] self.probe_helper = probe.ProbePointsHelper( @@ -133,7 +147,10 @@ def probe_finalize(self, offsets, positions): 8: 1.25, 9: 1.25, } - is_clockwise_thread = (self.thread & 1) == 0 + if hasattr(self, 'thread') and self.thread is not None: + is_clockwise_thread = (self.threads[self.thread] & 1) == 0 + else: + is_clockwise_thread = self.screw_direction == "CW" screw_diff = [] # Process the read Z values if self.direction is not None: @@ -176,7 +193,11 @@ def probe_finalize(self, offsets, positions): if abs(diff) < 0.001: adjust = 0 else: - adjust = diff / threads_factor.get(self.thread, 0.5) + if hasattr(self, 'thread') and self.thread is not None: + factor = threads_factor[self.threads[self.thread]] + else: + factor = self.screw_pitch + adjust = diff / factor if is_clockwise_thread: sign = "CW" if adjust >= 0 else "CCW" else: diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index 1fccc759b..e5e90c302 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -82,7 +82,7 @@ def test_error_screw_pitch_only(tmp_path): def test_error_screw_direction_only(tmp_path): - with pytest.raises(configparser.Error, match="Must specify both"): + with pytest.raises(configparser.Error, match="Must specify both 'screw_pitch' and 'screw_direction'"): _build_sta(tmp_path, "screw_direction: CCW") @@ -105,7 +105,7 @@ def test_error_invalid_screw_direction(tmp_path): def test_error_invalid_screw_thread(tmp_path): - with pytest.raises(configparser.Error, match="Invalid screw_thread"): + with pytest.raises(configparser.Error, match="Invalid screw_thread 'CW-M99'. Accepted values:"): _build_sta(tmp_path, "screw_thread: CW-M99") @@ -118,13 +118,6 @@ def test_error_screw_pitch_negative(tmp_path): with pytest.raises(configparser.Error): _build_sta(tmp_path, "screw_pitch: -1.0\nscrew_direction: CW") - -def test_legacy_screw_thread_case_insensitive(tmp_path): - sta = _build_sta(tmp_path, "screw_thread: cw-m3") - assert sta.screw_pitch == 0.5 - assert sta.screw_direction == "CW" - - # --- probe_finalize calculation tests --- From 8508b0ca5ea1c8650a712e149010f07a21ea9e5b Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:13:42 +0200 Subject: [PATCH 18/20] refactor: Move screw_thread validation to use getchoice with default value --- klippy/extras/screws_tilt_adjust.py | 4 ++++ test/test_screws_tilt_adjust.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 435c1cb74..19be5cd90 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -41,7 +41,11 @@ def __init__(self, config): "CCW-M6": 7, "CW-M8": 8, "CCW-M8": 9, + None: None } + self.thread = config.getchoice( + "screw_thread", self.threads, default="CW-M3" + ) threads_factor = { 0: 0.5, 1: 0.5, diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index e5e90c302..115f126fb 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -105,7 +105,7 @@ def test_error_invalid_screw_direction(tmp_path): def test_error_invalid_screw_thread(tmp_path): - with pytest.raises(configparser.Error, match="Invalid screw_thread 'CW-M99'. Accepted values:"): + with pytest.raises(configparser.Error, match="Choice 'CW-M99' for option 'screw_thread' in section 'screws_tilt_adjust' is not a valid choice"): _build_sta(tmp_path, "screw_thread: CW-M99") From 93b500cb34f0a4f742a01bd5a5235d393cbcd84b Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:14:14 +0200 Subject: [PATCH 19/20] refactor: Simplify screw configuration validation logic with explicit boolean variables --- klippy/extras/screws_tilt_adjust.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 19be5cd90..82718ee7b 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -61,10 +61,9 @@ def __init__(self, config): screw_thread = config.get("screw_thread", None) screw_pitch = config.getfloat("screw_pitch", None, above=0.0) screw_direction = config.get("screw_direction", None) - if not ( - (screw_thread is not None) - ^ (screw_pitch is not None or screw_direction is not None) - ): + has_screw_thread = screw_thread is not None + has_screw_pitch = screw_pitch is not None or screw_direction is not None + if has_screw_thread == has_screw_pitch: raise config.error( "screws_tilt_adjust: Must specify either 'screw_thread' " "or both 'screw_pitch' and 'screw_direction', but not both" From 01919e40d95af2f3ecdd15340afca7b01b3b5527 Mon Sep 17 00:00:00 2001 From: Jack_up <36533859+GiacomoGuaresi@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:18:25 +0200 Subject: [PATCH 20/20] style: Apply consistent code formatting in screws_tilt_adjust module --- klippy/extras/screws_tilt_adjust.py | 8 ++++---- test/test_screws_tilt_adjust.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 82718ee7b..f4baeb1a9 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -41,7 +41,7 @@ def __init__(self, config): "CCW-M6": 7, "CW-M8": 8, "CCW-M8": 9, - None: None + None: None, } self.thread = config.getchoice( "screw_thread", self.threads, default="CW-M3" @@ -150,7 +150,7 @@ def probe_finalize(self, offsets, positions): 8: 1.25, 9: 1.25, } - if hasattr(self, 'thread') and self.thread is not None: + if hasattr(self, "thread") and self.thread is not None: is_clockwise_thread = (self.threads[self.thread] & 1) == 0 else: is_clockwise_thread = self.screw_direction == "CW" @@ -196,7 +196,7 @@ def probe_finalize(self, offsets, positions): if abs(diff) < 0.001: adjust = 0 else: - if hasattr(self, 'thread') and self.thread is not None: + if hasattr(self, "thread") and self.thread is not None: factor = threads_factor[self.threads[self.thread]] else: factor = self.screw_pitch @@ -229,4 +229,4 @@ def probe_finalize(self, offsets, positions): def load_config(config): - return ScrewsTiltAdjust(config) \ No newline at end of file + return ScrewsTiltAdjust(config) diff --git a/test/test_screws_tilt_adjust.py b/test/test_screws_tilt_adjust.py index 115f126fb..2dc559f2e 100644 --- a/test/test_screws_tilt_adjust.py +++ b/test/test_screws_tilt_adjust.py @@ -82,7 +82,10 @@ def test_error_screw_pitch_only(tmp_path): def test_error_screw_direction_only(tmp_path): - with pytest.raises(configparser.Error, match="Must specify both 'screw_pitch' and 'screw_direction'"): + with pytest.raises( + configparser.Error, + match="Must specify both 'screw_pitch' and 'screw_direction'", + ): _build_sta(tmp_path, "screw_direction: CCW") @@ -105,7 +108,10 @@ def test_error_invalid_screw_direction(tmp_path): def test_error_invalid_screw_thread(tmp_path): - with pytest.raises(configparser.Error, match="Choice 'CW-M99' for option 'screw_thread' in section 'screws_tilt_adjust' is not a valid choice"): + with pytest.raises( + configparser.Error, + match="Choice 'CW-M99' for option 'screw_thread' in section 'screws_tilt_adjust' is not a valid choice", + ): _build_sta(tmp_path, "screw_thread: CW-M99") @@ -118,6 +124,7 @@ def test_error_screw_pitch_negative(tmp_path): with pytest.raises(configparser.Error): _build_sta(tmp_path, "screw_pitch: -1.0\nscrew_direction: CW") + # --- probe_finalize calculation tests ---