Posted 29 July 2026
In a prior post, Grok help me figure out why I was having so much trouble trying to change my Voron 2.4 printer from ‘dock’ to ‘no_dock’ configuration. Solving this problem exposed a pretty significant flaw in the way that config_switch.py managed the process of changing the printer.cfg file to reflect the new configuration. After getting everything to work, I suggested we generate a pull request to Vin’s Klipper-toolchanger repo to make the config swap process a bit more dependable. This post is intended to document this effort.
We started out by making sure we had all the preparatory work done.
- I copied yet another set of config files to my PC
- 1. Update my clone of Vin-y’s klipper-toolchanger repo on my printer, so all I had to do was update it with:
cd ~/klipper-toolchanger,git pull. Now my local repo is the same as Vin-y’s main branchcd ~/klipper-toolchanger git status # just to see where you are git pull origin main - Created a new branch so we can make our changes without touching the main branch.Just type this one command and show me the result:bash
git checkout -b fix-config-switch-in-place - In VS Code on my PC, I navigated to /home/pi/klipper-toolchanger/klipper/extras/config_switch.py and replaced its contents with the file generated by Grok.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# Copyright (C) 2024 Chinh Nhan Vo <nhanvo29@proton.me> # Improved 2026 by Frank Paynter / Grok – fix section replacement, marker handling, # and prevent progressive corruption of printer.cfg # # This file may be distributed under the terms of the GNU GPLv3 license. # import os import logging import shutil from datetime import datetime class ConfigSwitch: def __init__(self, config): self.printer = config.get_printer() self.gcode = self.printer.lookup_object('gcode') ## Register Commands self.gcode.register_command('SAVE_CONFIG_MODE', self.cmd_SAVE_CONFIG_MODE, desc=self.cmd_SAVE_CONFIG_MODE_help) self.gcode.register_command('TOGGLE_CONFIG_MODE', self.cmd_TOGGLE_CONFIG_MODE, desc=self.cmd_TOGGLE_CONFIG_MODE_help) # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def _get_paths(self): home_dir = os.path.expanduser("~") printer_config = os.path.join(home_dir, "printer_data/config/printer.cfg") config_dir = os.path.join(home_dir, "printer_data/config/config") return { "printer": printer_config, "dir": config_dir, "wt": os.path.join(config_dir, "config_wt_dock.cfg"), "no": os.path.join(config_dir, "config_no_dock.cfg"), "temp": os.path.join(config_dir, "printer.temp"), } def _ensure_config_dir(self, config_dir): if not os.path.exists(config_dir): os.makedirs(config_dir, exist_ok=True) def _read_lines(self, path): with open(path, "r") as f: return f.readlines() def _write_lines(self, path, lines): with open(path, "w") as f: f.writelines(lines) def _find_markers(self, lines): """Return (start_idx, end_idx) inclusive, or raise.""" start = end = None for i, line in enumerate(lines): stripped = line.strip() if "#;<" in stripped: if start is not None: raise self.gcode.error("Multiple '#;<' markers found in printer.cfg") start = i if "#;>" in stripped: if end is not None: raise self.gcode.error("Multiple '#;>' markers found in printer.cfg") end = i if start is None or end is None: raise self.gcode.error( "Missing '#;<' or '#;>' marker in printer.cfg – " "the swappable section is not defined" ) if end <= start: raise self.gcode.error("'#;>' appears before '#;<' – markers are inverted") return start, end def _get_current_dock_state(self, lines): for line in lines: if "variable_dock:" in line.strip(): if "True" in line: return True if "False" in line: return False raise self.gcode.error( "Could not find a valid 'variable_dock: True/False' line inside the marked section" ) # ------------------------------------------------------------------ # SAVE_CONFIG_MODE # ------------------------------------------------------------------ cmd_SAVE_CONFIG_MODE_help = "Save the marked session-variable section to the appropriate config_*.cfg" def cmd_SAVE_CONFIG_MODE(self, gcmd): paths = self._get_paths() self._ensure_config_dir(paths["dir"]) lines = self._read_lines(paths["printer"]) start, end = self._find_markers(lines) dock = self._get_current_dock_state(lines[start:end+1]) dest = paths["wt"] if dock else paths["no"] section = lines[start:end+1] # inclusive – keeps both markers self._write_lines(dest, section) name = "config_wt_dock.cfg" if dock else "config_no_dock.cfg" self.gcode.respond_info(f"Session variables (incl. markers) saved to config/{name}") # ------------------------------------------------------------------ # TOGGLE_CONFIG_MODE # ------------------------------------------------------------------ cmd_TOGGLE_CONFIG_MODE_help = "Replace the marked section with the opposite config_*.cfg (in-place)" def cmd_TOGGLE_CONFIG_MODE(self, gcmd): paths = self._get_paths() self._ensure_config_dir(paths["dir"]) lines = self._read_lines(paths["printer"]) start, end = self._find_markers(lines) dock = self._get_current_dock_state(lines[start:end+1]) # Choose the opposite file source = paths["no"] if dock else paths["wt"] if not os.path.exists(source): raise self.gcode.error(f"Missing source file: {source}") new_section = self._read_lines(source) # Sanity-check the incoming section has the markers if not any("#;<" in l for l in new_section) or not any("#;>" in l for l in new_section): raise self.gcode.error( f"{os.path.basename(source)} is missing '#;<' or '#;>' – " "run SAVE_CONFIG_MODE first on a clean printer.cfg" ) # Optional safety backup timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup = paths["printer"] + f".bak_{timestamp}" shutil.copy2(paths["printer"], backup) self.gcode.respond_info(f"Backup created: {os.path.basename(backup)}") # Build the new file: everything before start + new section + everything after end new_lines = lines[:start] + new_section + lines[end+1:] self._write_lines(paths["printer"], new_lines) self.gcode.respond_info( f"Toggled to {'no-dock' if dock else 'with-dock'} configuration " f"(section replaced in-place)" ) def load_config(config): return ConfigSwitch(config) |
Then we restarted the printer. Klipper came up cleanly, so that’s a good sign!
After testing the new script with some round-trip ‘TOGGLE_CONFIG_MODE‘ commands, the new script looks like it is performing correctly.
Grok then guided me through the process of updating the local repo:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
pi@mainsailos:~/printer_data $ cd ~/klipper-toolchanger pi@mainsailos:~/klipper-toolchanger $ git status On branch main Your branch is behind 'origin/main' by 31 commits, and can be fast-forwarded. (use "git pull" to update your local branch) nothing to commit, working tree clean pi@mainsailos:~/klipper-toolchanger $ git pull Updating 4082d29..e7be868 Fast-forward klipper/extras/tools_calibrate.py | 44 +++++++++++++------------------------------- macros/homing.cfg | 5 +++-- macros/print_time_default.cfg | 69 +++++++++++++++++++++++++++++++++++++-------------------------------- macros/tool_calibrate.cfg | 25 +++++++++++++++++-------- macros/toolchanger.cfg | 2 +- scripts/misschanger_settings.cfg | 17 ++++++++++------- 6 files changed, 81 insertions(+), 81 deletions(-) pi@mainsailos:~/klipper-toolchanger $ git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean pi@mainsailos:~/klipper-toolchanger $ |
Create a new branch to make the changes without changing the ‘main’ branch:
|
1 |
git checkout -b fix-config-switch-in-place |
Then in VS Code we overwrote the existing klipper-toolchanger / klipper / extras / config_switch.py with the new version (see above), saved the file and then restarted Klipper with “sudo systemctl restart klipper”
Testing:
At the start of the test program, ‘printer.cfg’ contains the ‘no_dock’ configuration. Then I ran SAVE_CONFIG_MODE fro the mainsail console, and got the following output:
|
1 2 3 4 |
1:02 PM Session variables (incl. markers) saved to config/config_no_dock.cfg 1:02 PM SAVE_CONFIG_MODE |
Most notably, now the ‘config_no_dock.cfg’ file has both the ‘#;<‘ and ‘#;>’ markers – yay!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
#;< # Section Variable marker #--------------------------------------------------------------- #--------------------------------------------------------------- #07/23/26 Updated to include current T0/T1 cfg files [include 251103_T0-Nitehawk-Revo-LDO.cfg] #[include 251103_T1-Nitehawk-Revo-LDO.cfg] #--------------------------------------------------------------- #################################################################################### ## 10/19/25 [gcode_macro _limits] section added ## See 19 October blog update for details #################################################################################### [gcode_macro _limits] variable_effective_min_y: 0.0 #this value gets changed to '120.0' in 'config_wt_dock' gcode: # Empty, just for variables [gcode_macro PRINT_END] # Use PRINT_END for the slicer ending script - please customise for your slicer of choice gcode: # safe anti-stringing move coords {% set th = printer.toolhead %} {% set x_safe = th.position.x + 20 * (1 if th.axis_maximum.x - th.position.x > 20 else -1) %} {% set y_safe = th.position.y + 20 * (1 if th.axis_maximum.y - th.position.y > 20 else -1) %} {% set z_safe = [th.position.z + 2, th.axis_maximum.z]|min %} SAVE_GCODE_STATE NAME=STATE_PRINT_END M400 ; wait for buffer to clear G92 E0 ; zero the extruder G1 E-5.0 F1800 ; retract filament TURN_OFF_HEATERS G90 ; absolute positioning G0 X{x_safe} Y{y_safe} Z{z_safe} F20000 ; move nozzle to remove stringing ;G0 X{th.axis_maximum.x//2} Y{th.axis_maximum.y - 2} F3600 ; park nozzle at rear G0 X{th.axis_maximum.x//2} Y{th.axis_maximum.y - 10} F3600 ; park nozzle at rear 06/17/25 gfp adj to avoid 'clunk' at end #G0 Z25 F3600 #10/17/25 gfp No dock G0 Z100 F3600 #07/27/26 gfp moved 'No dock' extruder end positin higher for easier print-plate removal/replacement M107 ; turn off fan BED_MESH_CLEAR RESTORE_GCODE_STATE NAME=STATE_PRINT_END #--------------------------------------------------------------- #gfp 09/19/25 edited for 300x300 bed [gcode_macro _home] variable_xh: 150.0 variable_yh: 150.0 #Dock NOT installed # Dock is installed: True or False variable_dock: False gcode: #--------------------------------------------------------------- #05/30/25 added gfp [bed_mesh] speed: 120 horizontal_move_z: 5 mesh_min: 30, 35 #07/05/25 gfp: 30 was not enough for Y mesh_max: 275, 250 probe_count: 5, 5 #06/01/25 chg to 5,5 zero_reference_position: 150,150 #for use with stock z endstop. Added 06/01/25 gfp #gfp 09/19/25 moved here per MissChanger setup instructions [quad_gantry_level] #-------------------------------------------------------------------- ## Gantry Corners for 300mm Build ## Uncomment for 300mm build gantry_corners: -60,-10 360,370 ## Probe points points: #Dock NOT installed 50,25 50,225 250,225 250,25 #;> # End of Session Variables section |
To go the other way, we edited the ‘config_wt_dock.cfg’ file to add the closing marker ‘#;>’ to the end of the file, and then ran TOGGLE_CONFIG_MODE again to swap out the ‘no_dock’ “Session Variables” with teh ‘with_dock’ ones and then checked to make sure the Voron would reboot successfully (it did – yay!) and the “session variables” and ‘SAVE_CONFIG’ sections were correct.
Pull Request:
First we committed all the changes (principally the revised ‘config_switch.py’ script) to the local repo:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
cd ~/klipper-toolchanger git status git add klipper/extras/config_switch.py git commit -m "Fix CONFIG_TOGGLE: replace section in-place instead of appending Previously the toggle stripped the marked block and appended the new config at the end of printer.cfg. This caused progressive corruption, lost the SAVE_CONFIG position, and dropped the closing #;> marker. The new implementation: - Strictly replaces only the text between (and including) the #;< / #;> markers - Leaves all other content (including SAVE_CONFIG) untouched and in order - Always writes both markers into the config_*.cfg files - Creates a timestamped backup before every toggle - Adds clear error checking for missing/duplicate markers Tested with multiple dock / no-dock round-trips on a Voron 2.4 + MissChanger." |
Then before we do anything else, we need to fork Vin-y’s klipper-toolchanger repo to my Github account with
- Navigate to Vin-y’s Github klipper-toolchanger repo with: https://github.com/VIN-y/klipper-toolchanger
- Create a fork
- Push the local repo’s ‘fix-config-switch-in-place’ branch to the newly created fork with:
When the push is performed, Github creates a link that can be used for the pull request:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
pi@mainsailos:~/klipper-toolchanger $ git remote add myfork https://github.com/paynterf/klipper-toolchanger.git git push -u myfork fix-config-switch-in-place Enumerating objects: 9, done. Counting objects: 100% (9/9), done. Delta compression using up to 4 threads Compressing objects: 100% (4/4), done. Writing objects: 100% (5/5), 2.45 KiB | 2.45 MiB/s, done. Total 5 (delta 2), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (2/2), completed with 2 local objects. remote: remote: Create a pull request for 'fix-config-switch-in-place' on GitHub by visiting: remote: https://github.com/paynterf/klipper-toolchanger/pull/new/fix-config-switch-in-place remote: To https://github.com/paynterf/klipper-toolchanger.git * [new branch] fix-config-switch-in-place -> fix-config-switch-in-place branch 'fix-config-switch-in-place' set up to track 'myfork/fix-config-switch-in-place'. pi@mainsailos:~/klipper-toolchanger $ |
Here’s what the pull request looks like on Github, just before clicking the ‘Create Pull Request button:

And here’s what it looks like after creating the pull request:

It feels good to be able to contribute something of worth to the MISSChanger community, as I am certain that others have run into similar problems with TOGGLE_CONFIG_MODE.
Stay Tuned,
Frank