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 <jason.mcmullan@gmail.com>
This commit is contained in:
Jason S. McMullan 2021-04-17 06:54:12 -04:00 committed by KevinOConnor
parent 4ea434796b
commit 913649de2e
8 changed files with 577 additions and 3 deletions

View file

@ -0,0 +1,73 @@
# Sdcard file looping support
#
# Copyright (C) 2021 Jason S. McMullan <jason.mcmullan@gmail.com>
#
# 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)

View file

@ -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