From 913649de2e8057e849be2a54e08d34302ee37dad Mon Sep 17 00:00:00 2001 From: "Jason S. McMullan" Date: Sat, 17 Apr 2021 06:54:12 -0400 Subject: [PATCH] loop_sdcard: Add loopable SD card file sections To support continuous belt printing, add nestable repeat loop support via an `[sdcard_loop]` module. Supported G-Code: - SDCARD_LOOP_BEGIN COUNT=n ; Loop for N times, or infinitely if N is 0 - SDCARD_LOOP_END ; End of loop - SDCARD_LOOP_DESIST ; Complete all loops without iterating Marlin M808 compatibility example in `config/sample-macros.cfg`: - M808 Ln ; Loop for N times, or infinitely if N is 0 - M808 ; End of loop - M808 K ; Complete all loops without iterating Added unit tests in test/klippy/sdcard_loop.test See https://reprap.org/wiki/G-code#M808:_Set_or_Goto_Repeat_Marker Signed-off-by: Jason S. McMullan --- config/sample-macros.cfg | 12 + docs/Config_Reference.md | 14 ++ docs/G-Codes.md | 7 + klippy/extras/sdcard_loop.py | 73 +++++++ klippy/extras/virtual_sdcard.py | 26 ++- test/klippy/sdcard_loop.cfg | 89 ++++++++ test/klippy/sdcard_loop.test | 9 + test/klippy/sdcard_loop/big.gcode | 350 ++++++++++++++++++++++++++++++ 8 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 klippy/extras/sdcard_loop.py create mode 100644 test/klippy/sdcard_loop.cfg create mode 100644 test/klippy/sdcard_loop.test create mode 100644 test/klippy/sdcard_loop/big.gcode diff --git a/config/sample-macros.cfg b/config/sample-macros.cfg index ea37c584..0eb049ac 100644 --- a/config/sample-macros.cfg +++ b/config/sample-macros.cfg @@ -176,3 +176,15 @@ gcode: "Humidity: %.2f%%" % ( sensor.temperature, sensor.humidity))} + +# SDCard 'looping' (aka Marlin M808 commands) support +# +# Support SDCard looping +[sdcard_loop] + +# 'Marlin' style M808 compatibility macro for SDCard looping +[gcode_macro M808] +gcode: + {% if params.K is not defined and params.L is defined %}SDCARD_LOOP_BEGIN COUNT={params.L|int}{% endif %} + {% if params.K is not defined and params.L is not defined %}SDCARD_LOOP_END{% endif %} + {% if params.K is defined and params.L is not defined %}SDCARD_LOOP_DESIST{% endif %} diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 30c2468a..812dd7a9 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1312,6 +1312,20 @@ path: # be provided. ``` +## [sdcard_loop] + +Some printers with stage-clearing features, such as a part ejector or +a belt printer, can find use in looping sections of the sdcard file. +(For example, to print the same part over and over, or repeat the +a section of a part for a chain or other repeated pattern). + +See the `config/sample-macros.cfg` file for a Marlin compatible M808 +G-Code macro. + +``` +[sdcard_loop] +``` + ## [force_move] Support manually moving stepper motors for diagnostic purposes. Note, diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 2f012bf5..cb84fa65 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -66,6 +66,13 @@ In addition, the following extended commands are availble when the - Load a file and start SD print: `SDCARD_PRINT_FILE FILENAME=` - Unload file and clear SD state: `SDCARD_RESET_FILE` +When the [sdcard_loop config section](Config_Reference.md#sdcard_loop) is +enabled, the following extended commands are available. +- Begin a looped section in the SD print: `SDCARD_LOOP_BEGIN COUNT=` + - A count of 0 indicates that the section should be looped indefinately. +- End a looped section in the SD print: `SDCARD_LOOP_END` +- Complete existing loops without further iterations: `SDCARD_LOOP_DESIST` + ## G-Code arcs The following standard G-Code commands are available if a diff --git a/klippy/extras/sdcard_loop.py b/klippy/extras/sdcard_loop.py new file mode 100644 index 00000000..3cba979f --- /dev/null +++ b/klippy/extras/sdcard_loop.py @@ -0,0 +1,73 @@ +# Sdcard file looping support +# +# Copyright (C) 2021 Jason S. McMullan +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import logging + +class SDCardLoop: + def __init__(self, config): + printer = config.get_printer() + self.sdcard = printer.load_object(config, "virtual_sdcard") + self.gcode = printer.lookup_object('gcode') + self.gcode.register_command( + "SDCARD_LOOP_BEGIN", self.cmd_SDCARD_LOOP_BEGIN, + desc=self.cmd_SDCARD_LOOP_BEGIN_help) + self.gcode.register_command( + "SDCARD_LOOP_END", self.cmd_SDCARD_LOOP_END, + desc=self.cmd_SDCARD_LOOP_END_help) + self.gcode.register_command( + "SDCARD_LOOP_DESIST", self.cmd_SDCARD_LOOP_DESIST, + desc=self.cmd_SDCARD_LOOP_DESIST_help) + self.loop_stack = [] + cmd_SDCARD_LOOP_BEGIN_help = "Begins a looped section in the SD file." + def cmd_SDCARD_LOOP_BEGIN(self, gcmd): + count = gcmd.get_int("COUNT", minval=0) + if not self.loop_begin(count): + raise gcmd.error("Only permitted in SD file.") + cmd_SDCARD_LOOP_END_help = "Ends a looped section in the SD file." + def cmd_SDCARD_LOOP_END(self, gcmd): + if not self.loop_end(): + raise gcmd.error("Only permitted in SD file.") + cmd_SDCARD_LOOP_DESIST_help = "Stops iterating the current loop stack." + def cmd_SDCARD_LOOP_DESIST(self, gcmd): + if not self.loop_desist(): + raise gcmd.error("Only permitted outside of a SD file.") + def loop_begin(self, count): + if not self.sdcard.is_cmd_from_sd(): + # Can only run inside of an SD file + return False + self.loop_stack.append((count, self.sdcard.get_file_position())) + return True + def loop_end(self): + if not self.sdcard.is_cmd_from_sd(): + # Can only run inside of an SD file + return False + # If the stack is empty, no need to skip back + if len(self.loop_stack) == 0: + return True + # Get iteration count and return position + count, position = self.loop_stack.pop() + if count == 0: # Infinite loop + self.sdcard.set_file_position(position) + self.loop_stack.append((0, position)) + elif count == 1: # Last repeat + # Nothing to do + pass + else: + # At the next opportunity, seek back to the start of the loop + self.sdcard.set_file_position(position) + # Decrement the count by 1, and add the position back to the stack + self.loop_stack.append((count - 1, position)) + return True + def loop_desist(self): + if self.sdcard.is_cmd_from_sd(): + # Can only run outside of an SD file + return False + logging.info("Desisting existing SD loops") + self.loop_stack = [] + return True + +def load_config(config): + return SDCardLoop(config) diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index ccce3afe..66fc5ade 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -21,6 +21,7 @@ class VirtualSD: # Work timer self.reactor = printer.get_reactor() self.must_pause_work = self.cmd_from_sd = False + self.next_file_position = 0 self.work_timer = None # Register commands self.gcode = printer.lookup_object('gcode') @@ -124,7 +125,7 @@ class VirtualSD: if filename[0] == '/': filename = filename[1:] self._load_file(gcmd, filename, check_subdirs=True) - self.cmd_M24(gcmd) + self.do_resume() def cmd_M20(self, gcmd): # List SD card files = self.get_file_list() @@ -191,6 +192,12 @@ class VirtualSD: return gcmd.respond_raw("SD printing byte %d/%d" % (self.file_position, self.file_size)) + def get_file_position(self): + return self.next_file_position + def set_file_position(self, pos): + self.next_file_position = pos + def is_cmd_from_sd(self): + return self.cmd_from_sd # Background work timer def work_handler(self, eventtime): logging.info("Starting SD card print (position %d)", self.file_position) @@ -232,8 +239,11 @@ class VirtualSD: continue # Dispatch command self.cmd_from_sd = True + line = lines.pop() + next_file_position = self.file_position + len(line) + 1 + self.next_file_position = next_file_position try: - self.gcode.run_script(lines[-1]) + self.gcode.run_script(line) except self.gcode.error as e: self.print_stats.note_error(str(e)) break @@ -241,7 +251,17 @@ class VirtualSD: logging.exception("virtual_sdcard dispatch") break self.cmd_from_sd = False - self.file_position += len(lines.pop()) + 1 + self.file_position = self.next_file_position + # Do we need to skip around? + if self.next_file_position != next_file_position: + try: + self.current_file.seek(self.file_position) + except: + logging.exception("virtual_sdcard seek") + self.work_timer = None + return self.reactor.NEVER + lines = [] + partial_input = "" logging.info("Exiting SD card print (position %d)", self.file_position) self.work_timer = None self.cmd_from_sd = False diff --git a/test/klippy/sdcard_loop.cfg b/test/klippy/sdcard_loop.cfg new file mode 100644 index 00000000..27844512 --- /dev/null +++ b/test/klippy/sdcard_loop.cfg @@ -0,0 +1,89 @@ +# Test config for sdcard_loop +[virtual_sdcard] +path: test/klippy/sdcard_loop + +[display_status] + +# Override to support unlimited belt size +# (homing Z simply resets its virtual position to 0.0) +[homing_override] +axes: xyz +set_position_x: 0 +set_position_y: 0 +set_position_z: 0 +gcode: + G92 X0 Y0 Z0 + + +[stepper_x] +step_pin: ar54 +dir_pin: ar55 +enable_pin: !ar38 +step_distance: .0125 +endstop_pin: ^ar3 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: ar60 +dir_pin: !ar61 +enable_pin: !ar56 +step_distance: .0125 +endstop_pin: ^ar14 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: ar46 +dir_pin: ar48 +enable_pin: !ar62 +step_distance: .0025 +endstop_pin: ^ar18 +position_endstop: 0.5 +position_max: 200000000 + +[extruder] +step_pin: ar26 +dir_pin: ar28 +enable_pin: !ar24 +step_distance: .004242 +nozzle_diameter: 0.500 +filament_diameter: 3.500 +heater_pin: ar10 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: analog13 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 210 + +[heater_bed] +heater_pin: ar8 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: analog14 +control: watermark +min_temp: 0 +max_temp: 110 + +[mcu] +serial: /dev/ttyACM0 +pin_map: arduino + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[sdcard_loop] + +[gcode_macro M808] +gcode: + {% if params.K is not defined and params.L is defined %}SDCARD_LOOP_BEGIN COUNT={params.L|int}{% endif %} + {% if params.K is not defined and params.L is not defined %}SDCARD_LOOP_END{% endif %} + {% if params.K is defined and params.L is not defined %}SDCARD_LOOP_DESIST{% endif %} diff --git a/test/klippy/sdcard_loop.test b/test/klippy/sdcard_loop.test new file mode 100644 index 00000000..dd9db434 --- /dev/null +++ b/test/klippy/sdcard_loop.test @@ -0,0 +1,9 @@ +; Virtual SD card unit tests + +DICTIONARY atmega2560.dict +CONFIG sdcard_loop.cfg + +G28 +SDCARD_LOOP_DESIST +; Verify long-name functions +SDCARD_PRINT_FILE FILENAME=big.gcode diff --git a/test/klippy/sdcard_loop/big.gcode b/test/klippy/sdcard_loop/big.gcode new file mode 100644 index 00000000..f5e52041 --- /dev/null +++ b/test/klippy/sdcard_loop/big.gcode @@ -0,0 +1,350 @@ +; Large example, to test M808 looping with `partial_data` +M808 L20 +; Looping +G92 E0 +G91 +G1 E1 ; This is line 0 +G1 E1 ; This is line 1 +G1 E1 ; This is line 2 +G1 E1 ; This is line 3 +G1 E1 ; This is line 4 +G1 E1 ; This is line 5 +G1 E1 ; This is line 6 +G1 E1 ; This is line 7 +G1 E1 ; This is line 8 +G1 E1 ; This is line 9 +G1 E1 ; This is line 10 +G1 E1 ; This is line 11 +G1 E1 ; This is line 12 +G1 E1 ; This is line 13 +G1 E1 ; This is line 14 +G1 E1 ; This is line 15 +G1 E1 ; This is line 16 +G1 E1 ; This is line 17 +G1 E1 ; This is line 18 +G1 E1 ; This is line 19 +G1 E1 ; This is line 20 +G1 E1 ; This is line 21 +G1 E1 ; This is line 22 +G1 E1 ; This is line 23 +G1 E1 ; This is line 24 +G1 E1 ; This is line 25 +G1 E1 ; This is line 26 +G1 E1 ; This is line 27 +G1 E1 ; This is line 28 +G1 E1 ; This is line 29 +G1 E1 ; This is line 30 +G1 E1 ; This is line 31 +G1 E1 ; This is line 32 +G1 E1 ; This is line 33 +G1 E1 ; This is line 34 +G1 E1 ; This is line 35 +G1 E1 ; This is line 36 +G1 E1 ; This is line 37 +G1 E1 ; This is line 38 +G1 E1 ; This is line 39 +G1 E1 ; This is line 40 +G1 E1 ; This is line 41 +G1 E1 ; This is line 42 +G1 E1 ; This is line 43 +G1 E1 ; This is line 44 +G1 E1 ; This is line 45 +G1 E1 ; This is line 46 +G1 E1 ; This is line 47 +G1 E1 ; This is line 48 +G1 E1 ; This is line 49 +G1 E1 ; This is line 50 +G1 E1 ; This is line 51 +G1 E1 ; This is line 52 +G1 E1 ; This is line 53 +G1 E1 ; This is line 54 +G1 E1 ; This is line 55 +G1 E1 ; This is line 56 +G1 E1 ; This is line 57 +G1 E1 ; This is line 58 +G1 E1 ; This is line 59 +G1 E1 ; This is line 60 +G1 E1 ; This is line 61 +G1 E1 ; This is line 62 +G1 E1 ; This is line 63 +G1 E1 ; This is line 64 +G1 E1 ; This is line 65 +G1 E1 ; This is line 66 +G1 E1 ; This is line 67 +G1 E1 ; This is line 68 +G1 E1 ; This is line 69 +G1 E1 ; This is line 70 +G1 E1 ; This is line 71 +G1 E1 ; This is line 72 +G1 E1 ; This is line 73 +G1 E1 ; This is line 74 +G1 E1 ; This is line 75 +G1 E1 ; This is line 76 +G1 E1 ; This is line 77 +G1 E1 ; This is line 78 +G1 E1 ; This is line 79 +G1 E1 ; This is line 80 +G1 E1 ; This is line 81 +G1 E1 ; This is line 82 +G1 E1 ; This is line 83 +G1 E1 ; This is line 84 +G1 E1 ; This is line 85 +G1 E1 ; This is line 86 +G1 E1 ; This is line 87 +G1 E1 ; This is line 88 +G1 E1 ; This is line 89 +G1 E1 ; This is line 90 +G1 E1 ; This is line 91 +G1 E1 ; This is line 92 +G1 E1 ; This is line 93 +G1 E1 ; This is line 94 +G1 E1 ; This is line 95 +G1 E1 ; This is line 96 +G1 E1 ; This is line 97 +G1 E1 ; This is line 98 +G1 E1 ; This is line 99 +G1 E1 ; This is line 100 +G1 E1 ; This is line 101 +G1 E1 ; This is line 102 +G1 E1 ; This is line 103 +G1 E1 ; This is line 104 +G1 E1 ; This is line 105 +G1 E1 ; This is line 106 +G1 E1 ; This is line 107 +G1 E1 ; This is line 108 +G1 E1 ; This is line 109 +G1 E1 ; This is line 110 +G1 E1 ; This is line 111 +G1 E1 ; This is line 112 +G1 E1 ; This is line 113 +G1 E1 ; This is line 114 +G1 E1 ; This is line 115 +G1 E1 ; This is line 116 +G1 E1 ; This is line 117 +G1 E1 ; This is line 118 +G1 E1 ; This is line 119 +G1 E1 ; This is line 120 +G1 E1 ; This is line 121 +G1 E1 ; This is line 122 +G1 E1 ; This is line 123 +G1 E1 ; This is line 124 +G1 E1 ; This is line 125 +G1 E1 ; This is line 126 +G1 E1 ; This is line 127 +G1 E1 ; This is line 128 +G1 E1 ; This is line 129 +G1 E1 ; This is line 130 +G1 E1 ; This is line 131 +G1 E1 ; This is line 132 +G1 E1 ; This is line 133 +G1 E1 ; This is line 134 +G1 E1 ; This is line 135 +G1 E1 ; This is line 136 +G1 E1 ; This is line 137 +G1 E1 ; This is line 138 +G1 E1 ; This is line 139 +G1 E1 ; This is line 140 +G1 E1 ; This is line 141 +G1 E1 ; This is line 142 +G1 E1 ; This is line 143 +G1 E1 ; This is line 144 +G1 E1 ; This is line 145 +G1 E1 ; This is line 146 +G1 E1 ; This is line 147 +G1 E1 ; This is line 148 +G1 E1 ; This is line 149 +G1 E1 ; This is line 150 +G1 E1 ; This is line 151 +G1 E1 ; This is line 152 +G1 E1 ; This is line 153 +G1 E1 ; This is line 154 +G1 E1 ; This is line 155 +G1 E1 ; This is line 156 +G1 E1 ; This is line 157 +G1 E1 ; This is line 158 +G1 E1 ; This is line 159 +G1 E1 ; This is line 160 +G1 E1 ; This is line 161 +G1 E1 ; This is line 162 +G1 E1 ; This is line 163 +G1 E1 ; This is line 164 +G1 E1 ; This is line 165 +G1 E1 ; This is line 166 +G1 E1 ; This is line 167 +G1 E1 ; This is line 168 +G1 E1 ; This is line 169 +G1 E1 ; This is line 170 +G1 E1 ; This is line 171 +G1 E1 ; This is line 172 +G1 E1 ; This is line 173 +G1 E1 ; This is line 174 +G1 E1 ; This is line 175 +G1 E1 ; This is line 176 +G1 E1 ; This is line 177 +G1 E1 ; This is line 178 +G1 E1 ; This is line 179 +G1 E1 ; This is line 180 +G1 E1 ; This is line 181 +G1 E1 ; This is line 182 +G1 E1 ; This is line 183 +G1 E1 ; This is line 184 +G1 E1 ; This is line 185 +G1 E1 ; This is line 186 +G1 E1 ; This is line 187 +G1 E1 ; This is line 188 +G1 E1 ; This is line 189 +G1 E1 ; This is line 190 +G1 E1 ; This is line 191 +G1 E1 ; This is line 192 +G1 E1 ; This is line 193 +G1 E1 ; This is line 194 +G1 E1 ; This is line 195 +G1 E1 ; This is line 196 +G1 E1 ; This is line 197 +G1 E1 ; This is line 198 +G1 E1 ; This is line 199 +G1 E1 ; This is line 200 +G1 E1 ; This is line 201 +G1 E1 ; This is line 202 +G1 E1 ; This is line 203 +G1 E1 ; This is line 204 +G1 E1 ; This is line 205 +G1 E1 ; This is line 206 +G1 E1 ; This is line 207 +G1 E1 ; This is line 208 +G1 E1 ; This is line 209 +G1 E1 ; This is line 210 +G1 E1 ; This is line 211 +G1 E1 ; This is line 212 +G1 E1 ; This is line 213 +G1 E1 ; This is line 214 +G1 E1 ; This is line 215 +G1 E1 ; This is line 216 +G1 E1 ; This is line 217 +G1 E1 ; This is line 218 +G1 E1 ; This is line 219 +G1 E1 ; This is line 220 +G1 E1 ; This is line 221 +G1 E1 ; This is line 222 +G1 E1 ; This is line 223 +G1 E1 ; This is line 224 +G1 E1 ; This is line 225 +G1 E1 ; This is line 226 +G1 E1 ; This is line 227 +G1 E1 ; This is line 228 +G1 E1 ; This is line 229 +G1 E1 ; This is line 230 +G1 E1 ; This is line 231 +G1 E1 ; This is line 232 +G1 E1 ; This is line 233 +G1 E1 ; This is line 234 +G1 E1 ; This is line 235 +G1 E1 ; This is line 236 +G1 E1 ; This is line 237 +G1 E1 ; This is line 238 +G1 E1 ; This is line 239 +G1 E1 ; This is line 240 +G1 E1 ; This is line 241 +G1 E1 ; This is line 242 +G1 E1 ; This is line 243 +G1 E1 ; This is line 244 +G1 E1 ; This is line 245 +G1 E1 ; This is line 246 +G1 E1 ; This is line 247 +G1 E1 ; This is line 248 +G1 E1 ; This is line 249 +G1 E1 ; This is line 250 +G1 E1 ; This is line 251 +G1 E1 ; This is line 252 +G1 E1 ; This is line 253 +G1 E1 ; This is line 254 +G1 E1 ; This is line 255 +G1 E1 ; This is line 256 +G1 E1 ; This is line 257 +G1 E1 ; This is line 258 +G1 E1 ; This is line 259 +G1 E1 ; This is line 260 +G1 E1 ; This is line 261 +G1 E1 ; This is line 262 +G1 E1 ; This is line 263 +G1 E1 ; This is line 264 +G1 E1 ; This is line 265 +G1 E1 ; This is line 266 +G1 E1 ; This is line 267 +G1 E1 ; This is line 268 +G1 E1 ; This is line 269 +G1 E1 ; This is line 270 +G1 E1 ; This is line 271 +G1 E1 ; This is line 272 +G1 E1 ; This is line 273 +G1 E1 ; This is line 274 +G1 E1 ; This is line 275 +G1 E1 ; This is line 276 +G1 E1 ; This is line 277 +G1 E1 ; This is line 278 +G1 E1 ; This is line 279 +G1 E1 ; This is line 280 +G1 E1 ; This is line 281 +G1 E1 ; This is line 282 +G1 E1 ; This is line 283 +G1 E1 ; This is line 284 +G1 E1 ; This is line 285 +G1 E1 ; This is line 286 +G1 E1 ; This is line 287 +G1 E1 ; This is line 288 +G1 E1 ; This is line 289 +G1 E1 ; This is line 290 +G1 E1 ; This is line 291 +G1 E1 ; This is line 292 +G1 E1 ; This is line 293 +G1 E1 ; This is line 294 +G1 E1 ; This is line 295 +G1 E1 ; This is line 296 +G1 E1 ; This is line 297 +G1 E1 ; This is line 298 +G1 E1 ; This is line 299 +G1 E1 ; This is line 300 +G1 E1 ; This is line 301 +G1 E1 ; This is line 302 +G1 E1 ; This is line 303 +G1 E1 ; This is line 304 +G1 E1 ; This is line 305 +G1 E1 ; This is line 306 +G1 E1 ; This is line 307 +G1 E1 ; This is line 308 +G1 E1 ; This is line 309 +G1 E1 ; This is line 310 +G1 E1 ; This is line 311 +G1 E1 ; This is line 312 +G1 E1 ; This is line 313 +G1 E1 ; This is line 314 +G1 E1 ; This is line 315 +G1 E1 ; This is line 316 +G1 E1 ; This is line 317 +G1 E1 ; This is line 318 +G1 E1 ; This is line 319 +G1 E1 ; This is line 320 +G1 E1 ; This is line 321 +G1 E1 ; This is line 322 +G1 E1 ; This is line 323 +G1 E1 ; This is line 324 +G1 E1 ; This is line 325 +G1 E1 ; This is line 326 +G1 E1 ; This is line 327 +G1 E1 ; This is line 328 +G1 E1 ; This is line 329 +G1 E1 ; This one breaks +G90 +M808 +G1 E330 ; Should pass +SDCARD_LOOP_BEGIN COUNT=5 +; Sub-loop +G1 X100 Y100 +G91 +M808 L3 +; Inner-loop +G1 Z1 +M808 +G90 +SDCARD_LOOP_END +; Done