mirror of
				https://github.com/Klipper3d/klipper.git
				synced 2025-11-03 20:05:49 +01:00 
			
		
		
		
	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:
		@@ -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]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										155
									
								
								klippy/extras/virtual_sdcard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								klippy/extras/virtual_sdcard.py
									
									
									
									
									
										Normal file
									
								
							@@ -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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user