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 <kevin@koconnor.net>
This commit is contained in:
parent
f77e1b67f6
commit
08a1183a01
|
@ -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]
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue