From 08a1183a01ab84484e284e4f8dbb9427a5b614cf Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 10 Oct 2017 11:12:15 -0400 Subject: [PATCH] virtual_sdcard: Initial support for virtual sdcard Add support for directly printing from a local file on the host. This may be useful if the host cpu is not fast enough to run OctoPrint well. Signed-off-by: Kevin O'Connor --- config/example-extras.cfg | 13 +++ klippy/extras/virtual_sdcard.py | 155 ++++++++++++++++++++++++++++++++ klippy/gcode.py | 13 ++- 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 klippy/extras/virtual_sdcard.py diff --git a/config/example-extras.cfg b/config/example-extras.cfg index 787a4a29..32221e2c 100644 --- a/config/example-extras.cfg +++ b/config/example-extras.cfg @@ -303,6 +303,19 @@ # that axis. The default is to not force a position for the axis. +# A virtual sdcard may be useful if the host machine is not fast +# enough to run OctoPrint well. It allows the Klipper host software to +# directly print gcode files stored in a directory on the host using +# standard sdcard G-Code commands (eg, M24). +#[virtual_sdcard] +#path: ~/.octoprint/uploads/ +# The path of the local directory on the host machine to look for +# g-code files. This is a read-only directory (sdcard file writes +# are not supported). One may point this to OctoPrint's upload +# directory (generally ~/.octoprint/uploads/ ). This parameter must +# be provided. + + # Replicape support - see the generic-replicape.cfg file for further # details. #[replicape] diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py new file mode 100644 index 00000000..689edc0b --- /dev/null +++ b/klippy/extras/virtual_sdcard.py @@ -0,0 +1,155 @@ +# Virtual sdcard support (print files directly from a host g-code file) +# +# Copyright (C) 2018 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import os, logging + +class VirtualSD: + def __init__(self, config): + printer = config.get_printer() + # sdcard state + sd = config.get('path') + self.sdcard_dirname = os.path.normpath(os.path.expanduser(sd)) + self.current_file = None + self.file_position = self.file_size = 0 + # Work timer + self.reactor = printer.get_reactor() + self.must_pause_work = False + self.work_timer = None + # Register commands + self.gcode = printer.lookup_object('gcode') + for cmd in ['M20', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27']: + self.gcode.register_command(cmd, getattr(self, 'cmd_' + cmd)) + for cmd in ['M28', 'M29', 'M30']: + self.gcode.register_command(cmd, self.cmd_error) + def printer_state(self, state): + if state == 'shutdown' and self.work_timer is not None: + self.must_pause_work = True + def get_file_list(self): + dname = self.sdcard_dirname + try: + filenames = os.listdir(self.sdcard_dirname) + return [(fname, os.path.getsize(os.path.join(dname, fname))) + for fname in filenames] + except: + logging.exception("virtual_sdcard get_file_list") + raise self.gcode.error("Unable to get file list") + # G-Code commands + def cmd_error(self, params): + raise self.gcode.error("SD write not supported") + def cmd_M20(self, params): + # List SD card + files = self.get_file_list() + self.gcode.respond("Begin file list") + for fname, fsize in files: + self.gcode.respond("%s %d" % (fname, fsize)) + self.gcode.respond("End file list") + def cmd_M21(self, params): + # Initialize SD card + self.gcode.respond("SD card ok") + def cmd_M23(self, params): + # Select SD file + if self.work_timer is not None: + raise self.gcode.error("SD busy") + if self.current_file is not None: + self.current_file.close() + self.current_file = None + self.file_position = self.file_size = 0 + try: + orig = params['#original'] + filename = orig[orig.find("M23") + 4:].split()[0].strip() + except: + raise self.gcode.error("Unable to extract filename") + if filename.startswith('/'): + filename = filename[1:] + files = self.get_file_list() + files_by_lower = { fname.lower(): fname for fname, fsize in files } + try: + fname = files_by_lower[filename.lower()] + fname = os.path.join(self.sdcard_dirname, fname) + f = open(fname, 'rb') + f.seek(0, os.SEEK_END) + fsize = f.tell() + f.seek(0) + except: + logging.exception("virtual_sdcard file open") + raise self.gcode.error("Unable to open file") + self.gcode.respond("File opened:%s Size:%d" % (filename, fsize)) + self.gcode.respond("File selected") + self.current_file = f + self.file_position = 0 + self.file_size = fsize + def cmd_M24(self, params): + # Start/resume SD print + if self.work_timer is not None: + raise self.gcode.error("SD busy") + self.must_pause_work = False + self.work_timer = self.reactor.register_timer( + self.work_handler, self.reactor.NOW) + def cmd_M25(self, params): + # Pause SD print + if self.work_timer is not None: + self.must_pause_work = True + def cmd_M26(self, params): + # Set SD position + if self.work_timer is not None: + raise self.gcode.error("SD busy") + pos = self.gcode.get_int('S', params) + self.file_position = pos + def cmd_M27(self, params): + # Report SD print status + if self.current_file is None or self.work_timer is None: + self.gcode.respond("Not SD printing.") + return + self.gcode.respond("SD printing byte %d/%d" % ( + self.file_position, self.file_size)) + # Background work timer + def work_handler(self, eventtime): + self.reactor.unregister_timer(self.work_timer) + try: + self.current_file.seek(self.file_position) + except: + logging.exception("virtual_sdcard seek") + self.gcode.error("Unable to seek file") + self.work_timer = None + return self.reactor.NEVER + partial_input = "" + lines = [] + while not self.must_pause_work: + if not lines: + # Read more data + try: + data = self.current_file.read(8192) + except: + logging.exception("virtual_sdcard read") + self.gcode.respond_error("Error on virtual sdcard read") + break + if not data: + # End of file + self.current_file.close() + self.current_file = None + self.gcode.respond("Done printing file") + break + lines = data.split('\n') + lines[0] = partial_input + lines[0] + partial_input = lines.pop() + lines.reverse() + continue + # Dispatch command + try: + res = self.gcode.process_batch(lines[-1]) + if not res: + self.reactor.pause(self.reactor.monotonic() + 0.100) + continue + except self.gcode.error as e: + break + except: + logging.exception("virtual_sdcard dispatch") + break + self.file_position += len(lines.pop()) + 1 + self.work_timer = None + return self.reactor.NEVER + +def load_config(config): + return VirtualSD(config) diff --git a/klippy/gcode.py b/klippy/gcode.py index 5090a4c5..2a23106a 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -123,7 +123,7 @@ class GCodeParser: self.speed_factor, self.extrude_factor, self.speed)) logging.info("\n".join(out)) # Parse input into commands - args_r = re.compile('([A-Z_]+|[A-Z*])') + args_r = re.compile('([A-Z_]+|[A-Z*/])') def process_commands(self, commands, need_ack=True): for line in commands: # Ignore comments and leading/trailing spaces @@ -205,6 +205,17 @@ class GCodeParser: pending_commands = self.pending_commands if self.fd_handle is None: self.fd_handle = self.reactor.register_fd(self.fd, self.process_data) + def process_batch(self, command): + if self.is_processing_data: + return False + self.is_processing_data = True + try: + self.process_commands([command], need_ack=False) + finally: + if self.pending_commands: + self.process_pending() + self.is_processing_data = False + return True def run_script(self, script): prev_need_ack = self.need_ack try: