diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index ad10081b..0d647686 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -11,6 +11,7 @@ import glob import time import serial import re +import queue from yaml import load as yamlload from yaml import dump as yamldump @@ -58,13 +59,21 @@ class MachineCom(object): self._callback = callbackObject self._printerProfileManager = printerProfileManager + self.RX_BUFFER_SIZE = 127 + self._state = self.STATE_NONE self._errorValue = "Unknown Error" self._serial = None self._currentFile = None - self._clear_to_send = CountedEvent(max=10, name="comm.clear_to_send") - self._long_running_command = False self._status_timer = None + self._acc_line_buffer = [] + self._pauseWaitTimeLost = 0.0 + self._send_event = threading.Event() + + self._real_time_commands={'poll_status':False, + 'feed_hold':False, + 'cycle_start':False, + 'soft_reset':False} # hooks self._pluginManager = octoprint.plugin.plugin_manager() @@ -412,7 +421,36 @@ class MachineCom(object): self._log("Connection closed, closing down monitor") def _send_loop(self): - pass + while self._sending_active: + if self._real_time_commands['poll_status']: + self._sendCommand('?') + self._real_time_commands['poll_status']=False + elif self._real_time_commands['feed_hold']: + self._sendCommand('!') + self._real_time_commands['feed_hold']=False + elif self._real_time_commands['cycle_start']: + self._sendCommand('~') + self._real_time_commands['cycle_start']=False + elif self._real_time_commands['soft_reset']: + self._sendCommand(b'\x18') + self._real_time_commands['soft_reset']=False + elif self.isOperational() or self.isPaused(): + pass # TODO send buffered command + elif self.isPrinting(): + self._sendCommand(self._getNext()) + self._send_event.wait(1) + self._send_event.clear() + + def _sendCommand(self, cmd): + if sum([len(x) for x in self._acc_line_buffer]) + len(cmd) +1 < self.RX_BUFFER_SIZE: + self._log("Send: %s" % cmd) + self._acc_line_buffer.append(cmd) + try: + self._serial.write(cmd + '\n') + except serial.SerialException: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) def _openSerial(self): def default(_, port, baudrate, read_timeout): @@ -457,7 +495,6 @@ class MachineCom(object): # first hook to succeed wins, but any can pass on to the next self._changeState(self.STATE_OPEN_SERIAL) self._serial = serial_obj - self._clear_to_send.clear() return True return False @@ -483,6 +520,24 @@ class MachineCom(object): self._log("Recv: %r" % ret) return ret + def _getNext(self): + line = self._currentFile.getNext() + if line is None: + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation(), + "time": self.getPrintTime() + } + self._callback.on_comm_print_job_done() + self._changeState(self.STATE_OPERATIONAL) + eventManager().fire(Events.PRINT_DONE, payload) + + self.sendCommand("M5") + self.sendCommand("G0X0Y0") + self.sendCommand("M9") + return line + def _state_none_handle(self, line): pass @@ -514,10 +569,12 @@ class MachineCom(object): if newState == self.STATE_PRINTING: if self._status_timer is not None: self._status_timer.cancel() + self._status_timer = RepeatedTimer(1, self._poll_status) + self._status_timer.start() elif newState == self.STATE_OPERATIONAL: if self._status_timer is not None: self._status_timer.cancel() - self._status_timer = RepeatedTimer(1, self._poll_status, run_first=True) + self._status_timer = RepeatedTimer(2, self._poll_status) self._status_timer.start() if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: @@ -556,8 +613,8 @@ class MachineCom(object): return None def _poll_status(self): - if self.isOperational() and not self._long_running_command: - self.sendCommand("?", cmd_type="status_poll") + if self.isOperational(): + self._real_time_commands['poll_status']=True def _log(self, message): self._callback.on_comm_log(message) @@ -615,8 +672,64 @@ class MachineCom(object): with open(versionFile, 'w') as outfile: outfile.write(yamldump(versionDict, default_flow_style=True)) - def sendCommand(self, cmd, cmd_type=None): - pass + def sendCommand(self, cmd, cmd_type=None, processed=False): + cmd = cmd.encode('ascii', 'replace') + if not processed: + cmd = process_gcode_line(cmd) + if not cmd: + return + + # if cmd[0] == "/": + # specialcmd = cmd[1:].lower() + # if "togglestatusreport" in specialcmd: + # if self._temperature_timer is None: + # self._temperature_timer = RepeatedTimer(1, self._poll_temperature, run_first=True) + # self._temperature_timer.start() + # else: + # self._temperature_timer.cancel() + # self._temperature_timer = None + # elif "setstatusfrequency" in specialcmd: + # data = specialcmd.split(' ') + # try: + # frequency = float(data[1]) + # except ValueError: + # self._log("No frequency setting found! Using 1 sec.") + # frequency = 1 + # if self._temperature_timer is not None: + # self._temperature_timer.cancel() + # + # self._temperature_timer = RepeatedTimer(frequency, self._poll_temperature, run_first=True) + # self._temperature_timer.start() + # elif "disconnect" in specialcmd: + # self.close() + # else: + # self._log("Command not Found! %s" % cmd) + # self._log("available commands are:") + # self._log(" /togglestatusreport") + # self._log(" /setstatusfrequency ") + # self._log(" /disconnect") + # return + + eepromCmd = re.search("^\$[0-9]+=.+$", cmd) + if(eepromCmd and self.isPrinting()): + self._log("Warning: Configuration changes during print are not allowed!") + + if self.isPrinting(): + self._commandQueue.put((cmd, cmd_type)) + elif self.isOperational() or self.isLocked() or self.isHoming(): + self._sendCommand(cmd, cmd_type=cmd_type) + + def selectFile(self, filename, sd): + if self.isBusy(): + return + + self._currentFile = PrintingGcodeFileInformation(filename) + eventManager().fire(Events.FILE_SELECTED, { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + }) + self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) def getStateString(self): if self._state == self.STATE_NONE: @@ -647,6 +760,9 @@ class MachineCom(object): return "Flashing" return "?%d?" % (self._state) + def getConnection(self): + return self._port, self._baudrate + def isOperational(self): return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED @@ -656,9 +772,24 @@ class MachineCom(object): def isPaused(self): return self._state == self.STATE_PAUSED + def isLocked(self): + return self._state == self.STATE_LOCKED + + def isHoming(self): + return self._state == self.STATE_HOMING + + def isBusy(self): + return self.isPrinting() or self.isPaused() + def getErrorString(self): return self._errorValue + def getPrintTime(self): + if self._currentFile is None or self._currentFile.getStartTime() is None: + return None + else: + return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost + def close(self, isError = False): if self._status_timer is not None: try: @@ -670,6 +801,9 @@ class MachineCom(object): self._monitoring_active = False self._sending_active = False + self.sending_thread.join() + self.monitoring_thread.join() + printing = self.isPrinting() or self.isPaused() if self._serial is not None: if isError: @@ -734,6 +868,128 @@ class MachineComPrintCallback(object): def on_comm_pos_update(self, MPos, WPos): pass +class PrintingGcodeFileInformation(PrintingFileInformation): + """ + Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures + that the file is closed in case of an error. + """ + + def __init__(self, filename, offsets_callback=None, current_tool_callback=None): + PrintingFileInformation.__init__(self, filename) + + self._handle = None + + self._first_line = None + + self._offsets_callback = offsets_callback + self._current_tool_callback = current_tool_callback + + if not os.path.exists(self._filename) or not os.path.isfile(self._filename): + raise IOError("File %s does not exist" % self._filename) + self._size = os.stat(self._filename).st_size + self._pos = 0 + + def start(self): + """ + Opens the file for reading and determines the file size. + """ + PrintingFileInformation.start(self) + self._handle = open(self._filename, "r") + + def close(self): + """ + Closes the file if it's still open. + """ + PrintingFileInformation.close(self) + if self._handle is not None: + try: + self._handle.close() + except: + pass + self._handle = None + + def getNext(self): + """ + Retrieves the next line for printing. + """ + if self._handle is None: + raise ValueError("File %s is not open for reading" % self._filename) + + try: + processed = None + while processed is None: + if self._handle is None: + # file got closed just now + return None + line = self._handle.readline() + if not line: + self.close() + processed = process_gcode_line(line) + self._pos = self._handle.tell() + + return processed + except Exception as e: + self.close() + self._logger.exception("Exception while processing line") + raise e + +class PrintingFileInformation(object): + """ + Encapsulates information regarding the current file being printed: file name, current position, total size and + time the print started. + Allows to reset the current file position to 0 and to calculate the current progress as a floating point + value between 0 and 1. + """ + + def __init__(self, filename): + self._logger = logging.getLogger(__name__) + self._filename = filename + self._pos = 0 + self._size = None + self._start_time = None + + def getStartTime(self): + return self._start_time + + def getFilename(self): + return self._filename + + def getFilesize(self): + return self._size + + def getFilepos(self): + return self._pos + + def getFileLocation(self): + return FileDestinations.LOCAL + + def getProgress(self): + """ + The current progress of the file, calculated as relation between file position and absolute size. Returns -1 + if file size is None or < 1. + """ + if self._size is None or not self._size > 0: + return -1 + return float(self._pos) / float(self._size) + + def reset(self): + """ + Resets the current file position to 0. + """ + self._pos = 0 + + def start(self): + """ + Marks the print job as started and remembers the start time. + """ + self._start_time = time.time() + + def close(self): + """ + Closes the print job. + """ + pass + def convert_pause_triggers(configured_triggers): triggers = { "enable": [], @@ -762,6 +1018,26 @@ def convert_pause_triggers(configured_triggers): result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t]))) return result +def process_gcode_line(line): + line = strip_comment(line).strip() + if len(line): + return None + return line + +def strip_comment(line): + if not ";" in line: + # shortcut + return line + + escaped = False + result = [] + for c in line: + if c == ";" and not escaped: + break + result += c + escaped = (c == "\\") and not escaped + return "".join(result) + def get_new_timeout(t): now = time.time() return now + get_interval(t)