mirror of
				https://github.com/Klipper3d/klipper.git
				synced 2025-10-31 18:36:09 +01:00 
			
		
		
		
	Split up the main GCodeParser class into GCodeDispatch and GCodeMove classes. The GCodeMove class is now available using the "gcode_move" printer object name. This split simplifies the gcode.py code. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
		
			
				
	
	
		
			251 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			251 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 }
 | |
|         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 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)
 |