klipper/klippy/extras/virtual_sdcard.py

253 lines
10 KiB
Python

# 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
VALID_GCODE_EXTS = ['gcode', 'g', 'gco']
class VirtualSD:
def __init__(self, config):
printer = config.get_printer()
printer.register_event_handler("klippy:shutdown", self.handle_shutdown)
# 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
# Print Stat Tracking
self.print_stats = printer.load_object(config, 'print_stats')
# Work timer
self.reactor = printer.get_reactor()
self.must_pause_work = self.cmd_from_sd = 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)
self.gcode.register_command(
"SDCARD_RESET_FILE", self.cmd_SDCARD_RESET_FILE,
desc=self.cmd_SDCARD_RESET_FILE_help)
self.gcode.register_command(
"SDCARD_PRINT_FILE", self.cmd_SDCARD_PRINT_FILE,
desc=self.cmd_SDCARD_PRINT_FILE_help)
def handle_shutdown(self):
if self.work_timer is not None:
self.must_pause_work = True
try:
readpos = max(self.file_position - 1024, 0)
readcount = self.file_position - readpos
self.current_file.seek(readpos)
data = self.current_file.read(readcount + 128)
except:
logging.exception("virtual_sdcard shutdown read")
return
logging.info("Virtual sdcard (%d): %s\nUpcoming (%d): %s",
readpos, repr(data[:readcount]),
self.file_position, repr(data[readcount:]))
def stats(self, eventtime):
if self.work_timer is None:
return False, ""
return True, "sd_pos=%d" % (self.file_position,)
def get_file_list(self, check_subdirs=False):
if check_subdirs:
flist = []
for root, dirs, files in os.walk(
self.sdcard_dirname, followlinks=True):
for name in files:
ext = name[name.rfind('.')+1:]
if ext not in VALID_GCODE_EXTS:
continue
full_path = os.path.join(root, name)
r_path = full_path[len(self.sdcard_dirname) + 1:]
size = os.path.getsize(full_path)
flist.append((r_path, size))
return sorted(flist, key=lambda f: f[0].lower())
else:
dname = self.sdcard_dirname
try:
filenames = os.listdir(self.sdcard_dirname)
return [(fname, os.path.getsize(os.path.join(dname, fname)))
for fname in sorted(filenames, key=str.lower)
if not fname.startswith('.')
and os.path.isfile((os.path.join(dname, fname)))]
except:
logging.exception("virtual_sdcard get_file_list")
raise self.gcode.error("Unable to get file list")
def get_status(self, eventtime):
progress = 0.
if self.file_size:
progress = float(self.file_position) / self.file_size
is_active = self.is_active()
return {'progress': progress, 'is_active': is_active,
'file_position': self.file_position}
def is_active(self):
return self.work_timer is not None
def do_pause(self):
if self.work_timer is not None:
self.must_pause_work = True
while self.work_timer is not None and not self.cmd_from_sd:
self.reactor.pause(self.reactor.monotonic() + .001)
# G-Code commands
def cmd_error(self, gcmd):
raise gcmd.error("SD write not supported")
def _reset_file(self):
if self.current_file is not None:
self.do_pause()
self.current_file.close()
self.current_file = None
self.file_position = self.file_size = 0.
self.print_stats.reset()
cmd_SDCARD_RESET_FILE_help = "Clears a loaded SD File. Stops the print "\
"if necessary"
def cmd_SDCARD_RESET_FILE(self, gcmd):
if self.cmd_from_sd:
raise gcmd.error(
"SDCARD_RESET_FILE cannot be run from the sdcard")
self._reset_file()
cmd_SDCARD_PRINT_FILE_help = "Loads a SD file and starts the print. May "\
"include files in subdirectories."
def cmd_SDCARD_PRINT_FILE(self, gcmd):
if self.work_timer is not None:
raise gcmd.error("SD busy")
self._reset_file()
filename = gcmd.get("FILENAME")
if filename[0] == '/':
filename = filename[1:]
self._load_file(gcmd, filename, check_subdirs=True)
self.cmd_M24(gcmd)
def cmd_M20(self, gcmd):
# List SD card
files = self.get_file_list()
gcmd.respond_raw("Begin file list")
for fname, fsize in files:
gcmd.respond_raw("%s %d" % (fname, fsize))
gcmd.respond_raw("End file list")
def cmd_M21(self, gcmd):
# Initialize SD card
gcmd.respond_raw("SD card ok")
def cmd_M23(self, gcmd):
# Select SD file
if self.work_timer is not None:
raise gcmd.error("SD busy")
self._reset_file()
try:
orig = gcmd.get_commandline()
filename = orig[orig.find("M23") + 4:].split()[0].strip()
if '*' in filename:
filename = filename[:filename.find('*')].strip()
except:
raise gcmd.error("Unable to extract filename")
if filename.startswith('/'):
filename = filename[1:]
self._load_file(gcmd, filename)
def _load_file(self, gcmd, filename, check_subdirs=False):
files = self.get_file_list(check_subdirs)
files_by_lower = { fname.lower(): fname for fname, fsize in files }
fname = filename
try:
if fname not in files:
fname = files_by_lower[fname.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 gcmd.error("Unable to open file")
gcmd.respond_raw("File opened:%s Size:%d" % (filename, fsize))
gcmd.respond_raw("File selected")
self.current_file = f
self.file_position = 0
self.file_size = fsize
self.print_stats.set_current_file(filename)
def cmd_M24(self, gcmd):
# Start/resume SD print
if self.work_timer is not None:
raise gcmd.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, gcmd):
# Pause SD print
self.do_pause()
def cmd_M26(self, gcmd):
# Set SD position
if self.work_timer is not None:
raise gcmd.error("SD busy")
pos = gcmd.get_int('S', minval=0)
self.file_position = pos
def cmd_M27(self, gcmd):
# Report SD print status
if self.current_file is None:
gcmd.respond_raw("Not SD printing.")
return
gcmd.respond_raw("SD printing byte %d/%d"
% (self.file_position, self.file_size))
# Background work timer
def work_handler(self, eventtime):
logging.info("Starting SD card print (position %d)", self.file_position)
self.reactor.unregister_timer(self.work_timer)
try:
self.current_file.seek(self.file_position)
except:
logging.exception("virtual_sdcard seek")
self.work_timer = None
return self.reactor.NEVER
self.print_stats.note_start()
gcode_mutex = self.gcode.get_mutex()
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")
break
if not data:
# End of file
self.current_file.close()
self.current_file = None
logging.info("Finished SD card print")
self.gcode.respond_raw("Done printing file")
break
lines = data.split('\n')
lines[0] = partial_input + lines[0]
partial_input = lines.pop()
lines.reverse()
self.reactor.pause(self.reactor.NOW)
continue
# Pause if any other request is pending in the gcode class
if gcode_mutex.test():
self.reactor.pause(self.reactor.monotonic() + 0.100)
continue
# Dispatch command
self.cmd_from_sd = True
try:
self.gcode.run_script(lines[-1])
except self.gcode.error as e:
self.print_stats.note_error(str(e))
break
except:
logging.exception("virtual_sdcard dispatch")
break
self.cmd_from_sd = False
self.file_position += len(lines.pop()) + 1
logging.info("Exiting SD card print (position %d)", self.file_position)
self.work_timer = None
self.cmd_from_sd = False
if self.current_file is not None:
self.print_stats.note_pause()
else:
self.print_stats.note_complete()
return self.reactor.NEVER
def load_config(config):
return VirtualSD(config)