2017-10-10 17:12:15 +02:00
|
|
|
# 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
|
|
|
|
|
2020-07-29 12:20:51 +02:00
|
|
|
VALID_GCODE_EXTS = ['gcode', 'g', 'gco']
|
|
|
|
|
2017-10-10 17:12:15 +02:00
|
|
|
class VirtualSD:
|
|
|
|
def __init__(self, config):
|
|
|
|
printer = config.get_printer()
|
2019-01-08 15:15:40 +01:00
|
|
|
printer.register_event_handler("klippy:shutdown", self.handle_shutdown)
|
2017-10-10 17:12:15 +02:00
|
|
|
# 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
|
2020-07-31 21:23:03 +02:00
|
|
|
# Print Stat Tracking
|
|
|
|
self.print_stats = printer.load_object(config, 'print_stats')
|
2017-10-10 17:12:15 +02:00
|
|
|
# Work timer
|
|
|
|
self.reactor = printer.get_reactor()
|
2019-11-24 20:30:26 +01:00
|
|
|
self.must_pause_work = self.cmd_from_sd = False
|
2021-04-17 12:54:12 +02:00
|
|
|
self.next_file_position = 0
|
2017-10-10 17:12:15 +02:00
|
|
|
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)
|
2020-07-29 12:14:02 +02:00
|
|
|
self.gcode.register_command(
|
|
|
|
"SDCARD_RESET_FILE", self.cmd_SDCARD_RESET_FILE,
|
|
|
|
desc=self.cmd_SDCARD_RESET_FILE_help)
|
2020-07-29 12:20:51 +02:00
|
|
|
self.gcode.register_command(
|
|
|
|
"SDCARD_PRINT_FILE", self.cmd_SDCARD_PRINT_FILE,
|
|
|
|
desc=self.cmd_SDCARD_PRINT_FILE_help)
|
2019-01-08 15:15:40 +01:00
|
|
|
def handle_shutdown(self):
|
|
|
|
if self.work_timer is not None:
|
2017-10-10 17:12:15 +02:00
|
|
|
self.must_pause_work = True
|
2018-04-24 00:24:58 +02:00
|
|
|
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,)
|
2020-07-29 12:20:51 +02:00
|
|
|
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")
|
2018-02-21 02:50:06 +01:00
|
|
|
def get_status(self, eventtime):
|
2021-06-08 19:33:35 +02:00
|
|
|
return {
|
|
|
|
'file_path': self.file_path(),
|
|
|
|
'progress': self.progress(),
|
|
|
|
'is_active': self.is_active(),
|
|
|
|
'file_position': self.file_position,
|
|
|
|
'file_size': self.file_size,
|
|
|
|
}
|
|
|
|
def file_path(self):
|
|
|
|
if self.current_file:
|
|
|
|
return self.current_file.name
|
|
|
|
return None
|
|
|
|
def progress(self):
|
2020-07-29 12:23:38 +02:00
|
|
|
if self.file_size:
|
2021-06-08 19:33:35 +02:00
|
|
|
return float(self.file_position) / self.file_size
|
|
|
|
else:
|
|
|
|
return 0.
|
2019-01-21 18:41:42 +01:00
|
|
|
def is_active(self):
|
|
|
|
return self.work_timer is not None
|
2019-02-24 18:28:30 +01:00
|
|
|
def do_pause(self):
|
|
|
|
if self.work_timer is not None:
|
|
|
|
self.must_pause_work = True
|
2019-11-24 20:30:26 +01:00
|
|
|
while self.work_timer is not None and not self.cmd_from_sd:
|
2019-10-30 19:21:28 +01:00
|
|
|
self.reactor.pause(self.reactor.monotonic() + .001)
|
2021-03-26 16:21:10 +01:00
|
|
|
def do_resume(self):
|
|
|
|
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)
|
2021-06-14 21:09:55 +02:00
|
|
|
def do_cancel(self):
|
|
|
|
if self.current_file is not None:
|
|
|
|
self.do_pause()
|
|
|
|
self.current_file.close()
|
|
|
|
self.current_file = None
|
|
|
|
self.print_stats.note_cancel()
|
|
|
|
self.file_position = self.file_size = 0.
|
2017-10-10 17:12:15 +02:00
|
|
|
# G-Code commands
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_error(self, gcmd):
|
|
|
|
raise gcmd.error("SD write not supported")
|
2020-07-29 12:14:02 +02:00
|
|
|
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.
|
2020-07-31 21:23:03 +02:00
|
|
|
self.print_stats.reset()
|
2020-07-29 12:14:02 +02:00
|
|
|
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()
|
2020-07-29 12:20:51 +02:00
|
|
|
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)
|
2021-04-17 12:54:12 +02:00
|
|
|
self.do_resume()
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_M20(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# List SD card
|
|
|
|
files = self.get_file_list()
|
2020-04-25 01:47:31 +02:00
|
|
|
gcmd.respond_raw("Begin file list")
|
2017-10-10 17:12:15 +02:00
|
|
|
for fname, fsize in files:
|
2020-04-25 01:47:31 +02:00
|
|
|
gcmd.respond_raw("%s %d" % (fname, fsize))
|
|
|
|
gcmd.respond_raw("End file list")
|
|
|
|
def cmd_M21(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Initialize SD card
|
2020-04-25 01:47:31 +02:00
|
|
|
gcmd.respond_raw("SD card ok")
|
|
|
|
def cmd_M23(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Select SD file
|
|
|
|
if self.work_timer is not None:
|
2020-04-25 01:47:31 +02:00
|
|
|
raise gcmd.error("SD busy")
|
2020-07-29 12:20:51 +02:00
|
|
|
self._reset_file()
|
2017-10-10 17:12:15 +02:00
|
|
|
try:
|
2020-04-25 01:47:31 +02:00
|
|
|
orig = gcmd.get_commandline()
|
2017-10-10 17:12:15 +02:00
|
|
|
filename = orig[orig.find("M23") + 4:].split()[0].strip()
|
2018-03-02 17:58:00 +01:00
|
|
|
if '*' in filename:
|
|
|
|
filename = filename[:filename.find('*')].strip()
|
2017-10-10 17:12:15 +02:00
|
|
|
except:
|
2020-04-25 01:47:31 +02:00
|
|
|
raise gcmd.error("Unable to extract filename")
|
2017-10-10 17:12:15 +02:00
|
|
|
if filename.startswith('/'):
|
|
|
|
filename = filename[1:]
|
2020-07-29 12:20:51 +02:00
|
|
|
self._load_file(gcmd, filename)
|
|
|
|
def _load_file(self, gcmd, filename, check_subdirs=False):
|
|
|
|
files = self.get_file_list(check_subdirs)
|
2021-05-09 00:24:50 +02:00
|
|
|
flist = [f[0] for f in files]
|
2017-10-10 17:12:15 +02:00
|
|
|
files_by_lower = { fname.lower(): fname for fname, fsize in files }
|
2021-02-26 01:57:45 +01:00
|
|
|
fname = filename
|
2017-10-10 17:12:15 +02:00
|
|
|
try:
|
2021-05-09 00:24:50 +02:00
|
|
|
if fname not in flist:
|
2021-02-26 01:57:45 +01:00
|
|
|
fname = files_by_lower[fname.lower()]
|
2017-10-10 17:12:15 +02:00
|
|
|
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")
|
2020-04-25 01:47:31 +02:00
|
|
|
raise gcmd.error("Unable to open file")
|
|
|
|
gcmd.respond_raw("File opened:%s Size:%d" % (filename, fsize))
|
|
|
|
gcmd.respond_raw("File selected")
|
2017-10-10 17:12:15 +02:00
|
|
|
self.current_file = f
|
|
|
|
self.file_position = 0
|
|
|
|
self.file_size = fsize
|
2020-07-31 21:23:03 +02:00
|
|
|
self.print_stats.set_current_file(filename)
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_M24(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Start/resume SD print
|
2021-03-26 16:21:10 +01:00
|
|
|
self.do_resume()
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_M25(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Pause SD print
|
2019-02-24 18:28:30 +01:00
|
|
|
self.do_pause()
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_M26(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Set SD position
|
|
|
|
if self.work_timer is not None:
|
2020-04-25 01:47:31 +02:00
|
|
|
raise gcmd.error("SD busy")
|
|
|
|
pos = gcmd.get_int('S', minval=0)
|
2017-10-10 17:12:15 +02:00
|
|
|
self.file_position = pos
|
2020-04-25 01:47:31 +02:00
|
|
|
def cmd_M27(self, gcmd):
|
2017-10-10 17:12:15 +02:00
|
|
|
# Report SD print status
|
2019-01-21 18:41:42 +01:00
|
|
|
if self.current_file is None:
|
2020-04-25 01:47:31 +02:00
|
|
|
gcmd.respond_raw("Not SD printing.")
|
2017-10-10 17:12:15 +02:00
|
|
|
return
|
2020-04-25 01:47:31 +02:00
|
|
|
gcmd.respond_raw("SD printing byte %d/%d"
|
|
|
|
% (self.file_position, self.file_size))
|
2021-04-17 12:54:12 +02:00
|
|
|
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
|
2017-10-10 17:12:15 +02:00
|
|
|
# Background work timer
|
|
|
|
def work_handler(self, eventtime):
|
2018-04-24 00:24:58 +02:00
|
|
|
logging.info("Starting SD card print (position %d)", self.file_position)
|
2017-10-10 17:12:15 +02:00
|
|
|
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
|
2020-07-31 21:23:03 +02:00
|
|
|
self.print_stats.note_start()
|
2019-06-09 19:33:21 +02:00
|
|
|
gcode_mutex = self.gcode.get_mutex()
|
2017-10-10 17:12:15 +02:00
|
|
|
partial_input = ""
|
|
|
|
lines = []
|
2021-06-14 21:09:55 +02:00
|
|
|
error_message = None
|
2017-10-10 17:12:15 +02:00
|
|
|
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
|
2018-04-24 00:24:58 +02:00
|
|
|
logging.info("Finished SD card print")
|
2020-04-24 21:54:18 +02:00
|
|
|
self.gcode.respond_raw("Done printing file")
|
2017-10-10 17:12:15 +02:00
|
|
|
break
|
|
|
|
lines = data.split('\n')
|
|
|
|
lines[0] = partial_input + lines[0]
|
|
|
|
partial_input = lines.pop()
|
|
|
|
lines.reverse()
|
2018-08-03 02:27:34 +02:00
|
|
|
self.reactor.pause(self.reactor.NOW)
|
2017-10-10 17:12:15 +02:00
|
|
|
continue
|
2019-06-09 19:33:21 +02:00
|
|
|
# Pause if any other request is pending in the gcode class
|
|
|
|
if gcode_mutex.test():
|
|
|
|
self.reactor.pause(self.reactor.monotonic() + 0.100)
|
|
|
|
continue
|
2017-10-10 17:12:15 +02:00
|
|
|
# Dispatch command
|
2019-11-24 20:30:26 +01:00
|
|
|
self.cmd_from_sd = True
|
2021-04-17 12:54:12 +02:00
|
|
|
line = lines.pop()
|
|
|
|
next_file_position = self.file_position + len(line) + 1
|
|
|
|
self.next_file_position = next_file_position
|
2017-10-10 17:12:15 +02:00
|
|
|
try:
|
2021-04-17 12:54:12 +02:00
|
|
|
self.gcode.run_script(line)
|
2017-10-10 17:12:15 +02:00
|
|
|
except self.gcode.error as e:
|
2021-06-14 21:09:55 +02:00
|
|
|
error_message = str(e)
|
2017-10-10 17:12:15 +02:00
|
|
|
break
|
|
|
|
except:
|
|
|
|
logging.exception("virtual_sdcard dispatch")
|
|
|
|
break
|
2019-11-24 20:30:26 +01:00
|
|
|
self.cmd_from_sd = False
|
2021-04-17 12:54:12 +02:00
|
|
|
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 = ""
|
2018-04-24 00:24:58 +02:00
|
|
|
logging.info("Exiting SD card print (position %d)", self.file_position)
|
2017-10-10 17:12:15 +02:00
|
|
|
self.work_timer = None
|
2019-11-24 20:30:26 +01:00
|
|
|
self.cmd_from_sd = False
|
2021-06-14 21:09:55 +02:00
|
|
|
if error_message is not None:
|
|
|
|
self.print_stats.note_error(error_message)
|
|
|
|
elif self.current_file is not None:
|
2020-07-31 21:23:03 +02:00
|
|
|
self.print_stats.note_pause()
|
|
|
|
else:
|
2020-08-03 18:21:56 +02:00
|
|
|
self.print_stats.note_complete()
|
2017-10-10 17:12:15 +02:00
|
|
|
return self.reactor.NEVER
|
|
|
|
|
|
|
|
def load_config(config):
|
|
|
|
return VirtualSD(config)
|