aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/example-extras.cfg13
-rw-r--r--klippy/extras/virtual_sdcard.py155
-rw-r--r--klippy/gcode.py13
3 files changed, 180 insertions, 1 deletions
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 <kevin@koconnor.net>
+#
+# 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: