From 9776bba6bcc1b396d8469fd77df4cd3095519b54 Mon Sep 17 00:00:00 2001 From: make-ing Date: Fri, 16 Oct 2015 18:48:42 +0200 Subject: [PATCH 01/14] added new comm_acc2 file and switched to it --- src/octoprint/printer/__init__.py | 2 +- src/octoprint/printer/standard.py | 2 +- src/octoprint/util/comm_acc2.py | 801 ++++++++++++++++++++++++++++++ 3 files changed, 803 insertions(+), 2 deletions(-) create mode 100644 src/octoprint/util/comm_acc2.py diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index b518cb20..2a4446db 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import re -import octoprint.util.comm_acc as comm +import octoprint.util.comm_acc2 as comm import octoprint.util as util from octoprint.settings import settings diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index b0827966..5fee8558 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -22,7 +22,7 @@ from octoprint.plugin import plugin_manager, ProgressPlugin from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript from octoprint.printer.estimation import TimeEstimationHelper from octoprint.settings import settings -from octoprint.util import comm_acc as comm +from octoprint.util import comm_acc2 as comm from octoprint.util import InvariantContainer diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py new file mode 100644 index 00000000..5b679813 --- /dev/null +++ b/src/octoprint/util/comm_acc2.py @@ -0,0 +1,801 @@ +# coding=utf-8 +from __future__ import absolute_import +__author__ = "Florian Becker based on work by Gina Häußge and David Braam" +__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" +__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License" + +import os +import threading +import logging +import glob +import time +import serial +import re + +from yaml import load as yamlload +from yaml import dump as yamldump +from subprocess import call as subprocesscall + +import octoprint.plugin + +from octoprint.events import eventManager, Events +from octoprint.settings import settings, default_settings +from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii + +### MachineCom ######################################################################################################### +class MachineCom(object): + STATE_NONE = 0 + STATE_OPEN_SERIAL = 1 + STATE_DETECT_SERIAL = 2 + STATE_CONNECTING = 3 + STATE_OPERATIONAL = 4 + STATE_PRINTING = 5 + STATE_PAUSED = 6 + STATE_CLOSED = 7 + STATE_ERROR = 8 + STATE_CLOSED_WITH_ERROR = 9 + STATE_LOCKED = 10 + STATE_HOMING = 11 + STATE_FLASHING = 12 + + def __init__(self, port=None, baudrate=None, callbackObject=None, printerProfileManager=None): + self._logger = logging.getLogger(__name__) + self._serialLogger = logging.getLogger("SERIAL") + + if port is None: + port = settings().get(["serial", "port"]) + if baudrate is None: + settingsBaudrate = settings().getInt(["serial", "baudrate"]) + if settingsBaudrate is None: + baudrate = 0 + else: + baudrate = settingsBaudrate + if callbackObject is None: + callbackObject = MachineComPrintCallback() + + self._port = port + self._baudrate = baudrate + self._callback = callbackObject + self._printerProfileManager = printerProfileManager + + 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 + + # hooks + self._pluginManager = octoprint.plugin.plugin_manager() + self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory") + + self._state_parse_dict = {self.STATE_NONE:self._state_none_handle, + self.STATE_CONNECTING:self._state_connecting_handle, + self.STATE_LOCKED:self._state_locked_handle, + self.STATE_HOMING:self._state_homing_handle, + self.STATE_OPERATIONAL:self._state_operational_handle} + + # monitoring thread + self._monitoring_active = True + self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread") + self.monitoring_thread.daemon = True + self.monitoring_thread.start() + + # sending thread + self._sending_active = True + self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread") + self.sending_thread.daemon = True + self.sending_thread.start() + + def _monitor_loop(self): + pause_triggers = convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"])) + + #Open the serial port. + if not self._openSerial(): + return + + self._log("Connected to: %s, starting monitor" % self._serial) + self._changeState(self.STATE_CONNECTING) + self._timeout = get_new_timeout("communication") + + supportWait = settings().getBoolean(["feature", "supportWait"]) + + while self._monitoring_active: + try: + line = self._readline() + if line is None: + break + if line.strip() is not "": + self._timeout = get_new_timeout("communication") + + # parse line depending on state + self._state_parse_dict[self._state](self, line) + + return + + + + + + + # # GRBL Position update + # if self._grbl : + # if(self._state == self.STATE_HOMING and 'ok' in line): + # self._changeState(self.STATE_OPERATIONAL) + # self._onHomingDone(); + # + # # TODO check if "Alarm" is enough + # if("Alarm lock" in line): + # self._changeState(self.STATE_LOCKED) + # if("['$H'|'$X' to unlock]" in line): + # self._changeState(self.STATE_LOCKED) + # + # # TODO maybe better in _gcode_X_sent ... + # if("Idle" in line and (self._state == self.STATE_LOCKED) ): + # self._changeState(self.STATE_OPERATIONAL) + # + # # TODO highly experimental. needs testing. + # if("Hold" in line and self._state == self.STATE_PRINTING): + # self._changeState(self.STATE_PAUSED) + # #if("Run" in line and self._state == self.STATE_PAUSED): + # # self._changeState(self.STATE_PRINTING) + # + # if 'MPos:' in line: + # self._update_grbl_pos(line) + # + # if("ALARM: Hard/soft limit" in line): + # errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle" + # self._log(errorMsg) + # self._errorValue = errorMsg + # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + # eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()}) + # self._openSerial() + # self._changeState(self.STATE_CONNECTING) + # + # if("Invalid gcode" in line and self._state == self.STATE_PRINTING): + # # TODO Pause machine instead of resetting it. + # errorMsg = line + # self._log(errorMsg) + # self._errorValue = errorMsg + # # self._changeState(self.STATE_ERROR) + # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + # self._openSerial() + # self._changeState(self.STATE_CONNECTING) + # + # if("Grbl" in line and self._state == self.STATE_PRINTING): + # errorMsg = "Machine reset." + # self._log(errorMsg) + # self._errorValue = errorMsg + # self._changeState(self.STATE_LOCKED) + # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + # + # if("Grbl" in line): + # versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) + # if(versionMatch): + # versionDict = versionMatch.groupdict() + # self._writeGrblVersionToFile(versionDict) + # if self._compareGrblVersion(versionDict) is False: + # self._flashGrbl() + # + # if("error:" in line): + # self.handle_grbl_error(line) + # + # # ##~~ SD file list + # # # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing + # # if self._sdFileList and not "End file list" in line: + # # preprocessed_line = line.strip().lower() + # # fileinfo = preprocessed_line.rsplit(None, 1) + # # if len(fileinfo) > 1: + # # # we might have extended file information here, so let's split filename and size and try to make them a bit nicer + # # filename, size = fileinfo + # # try: + # # size = int(size) + # # except ValueError: + # # # whatever that was, it was not an integer, so we'll just use the whole line as filename and set size to None + # # filename = preprocessed_line + # # size = None + # # else: + # # # no extended file information, so only the filename is there and we set size to None + # # filename = preprocessed_line + # # size = None + # # + # # if valid_file_type(filename, "machinecode"): + # # if filter_non_ascii(filename): + # # self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) + # # else: + # # if not filename.startswith("/"): + # # # file from the root of the sd -- we'll prepend a / + # # filename = "/" + filename + # # self._sdFiles.append((filename, size)) + # # continue + # + # ##~~ process oks + # if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")): + # self._clear_to_send.set() + # self._long_running_command = False + # + # # ##~~ Temperature processing + # # if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ' B:' in line or line.startswith('B:'): + # # if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating: + # # self._logger.debug("Externally triggered heatup detected") + # # self._heating = True + # # self._heatupWaitStartTime = time.time() + # # self._processTemperatures(line) + # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) + # # + # # elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line): + # # matchExtr = self._regex_repetierTempExtr.match(line) + # # matchBed = self._regex_repetierTempBed.match(line) + # # + # # if matchExtr is not None: + # # toolNum = int(matchExtr.group(1)) + # # try: + # # target = float(matchExtr.group(2)) + # # if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): + # # (actual, oldTarget) = self._temp[toolNum] + # # self._temp[toolNum] = (actual, target) + # # else: + # # self._temp[toolNum] = (None, target) + # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) + # # except ValueError: + # # pass + # # elif matchBed is not None: + # # try: + # # target = float(matchBed.group(1)) + # # if self._bedTemp is not None and isinstance(self._bedTemp, tuple): + # # (actual, oldTarget) = self._bedTemp + # # self._bedTemp = (actual, target) + # # else: + # # self._bedTemp = (None, target) + # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) + # # except ValueError: + # # pass + # + # # #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. + # # if 'ok' in line and self._heatupWaitStartTime: + # # self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) + # # self._heatupWaitStartTime = None + # # self._heating = False + # + # # ##~~ SD Card handling + # # elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: + # # self._sdAvailable = False + # # self._sdFiles = [] + # # self._callback.on_comm_sd_state_change(self._sdAvailable) + # # elif 'Not SD printing' in line: + # # if self.isSdFileSelected() and self.isPrinting(): + # # # something went wrong, printer is reporting that we actually are not printing right now... + # # self._sdFilePos = 0 + # # self._changeState(self.STATE_OPERATIONAL) + # # elif 'SD card ok' in line and not self._sdAvailable: + # # self._sdAvailable = True + # # self.refreshSdFiles() + # # self._callback.on_comm_sd_state_change(self._sdAvailable) + # # elif 'Begin file list' in line: + # # self._sdFiles = [] + # # self._sdFileList = True + # # elif 'End file list' in line: + # # self._sdFileList = False + # # self._callback.on_comm_sd_files(self._sdFiles) + # # elif 'SD printing byte' in line and self.isSdPrinting(): + # # # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" + # # match = self._regex_sdPrintingByte.search(line) + # # self._currentFile.setFilepos(int(match.group(1))) + # # self._callback.on_comm_progress() + # # elif 'File opened' in line and not self._ignore_select: + # # # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" + # # match = self._regex_sdFileOpened.search(line) + # # if self._sdFileToSelect: + # # name = self._sdFileToSelect + # # self._sdFileToSelect = None + # # else: + # # name = match.group(1) + # # self._currentFile = PrintingSdFileInformation(name, int(match.group(2))) + # # elif 'File selected' in line: + # # if self._ignore_select: + # # self._ignore_select = False + # # elif self._currentFile is not None: + # # # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" + # # self._callback.on_comm_file_selected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) + # # eventManager().fire(Events.FILE_SELECTED, { + # # "file": self._currentFile.getFilename(), + # # "origin": self._currentFile.getFileLocation() + # # }) + # # elif 'Writing to file' in line: + # # # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" + # # self._changeState(self.STATE_PRINTING) + # # self._clear_to_send.set() + # # line = "ok" + # # elif 'Done printing file' in line and self.isSdPrinting(): + # # # printer is reporting file finished printing + # # self._sdFilePos = 0 + # # self._callback.on_comm_print_job_done() + # # self._changeState(self.STATE_OPERATIONAL) + # # eventManager().fire(Events.PRINT_DONE, { + # # "file": self._currentFile.getFilename(), + # # "filename": os.path.basename(self._currentFile.getFilename()), + # # "origin": self._currentFile.getFileLocation(), + # # "time": self.getPrintTime() + # # }) + # # if self._sd_status_timer is not None: + # # try: + # # self._sd_status_timer.cancel() + # # except: + # # pass + # # elif 'Done saving file' in line: + # # self.refreshSdFiles() + # # elif 'File deleted' in line and line.strip().endswith("ok"): + # # # buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in + # # # current versions + # # self._clear_to_send.set() + # + # ##~~ Message handling + # elif line.strip() != '' \ + # and line.strip() != 'ok' and not line.startswith("wait") \ + # and not line.startswith('Resend:') \ + # and line != 'echo:Unknown command:""\n' \ + # and line != "Unsupported statement" \ + # and self.isOperational(): + # self._callback.on_comm_message(line) + # + # ##~~ Parsing for feedback commands + # if feedback_controls and feedback_matcher and not "_all" in feedback_errors: + # try: + # self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors) + # except: + # # something went wrong while feedback matching + # self._logger.exception("Error while trying to apply feedback control matching, disabling it") + # feedback_errors.append("_all") + # + # ##~~ Parsing for pause triggers + # + # if pause_triggers and not self.isStreaming(): + # if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None: + # self.setPause(True) + # elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None: + # self.setPause(False) + # elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None: + # self.setPause(not self.isPaused()) + # + # + # ### Operational + # elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: + # if "ok" in line: + # # if we still have commands to process, process them + # if self._resendSwallowNextOk: + # self._resendSwallowNextOk = False + # elif self._resendDelta is not None: + # self._resendNextCommand() + # elif self._sendFromQueue(): + # pass + # + # # resend -> start resend procedure from requested line + # elif line.lower().startswith("resend") or line.lower().startswith("rs"): + # self._handleResendRequest(line) + # + # ### Printing + # elif self._state == self.STATE_PRINTING: + # if line == "" and time.time() > self._timeout: + # if not self._long_running_command: + # self._log("Communication timeout during printing, forcing a line") + # self._sendCommand("?") + # self._clear_to_send.set() + # else: + # self._logger.debug("Ran into a communication timeout, but a command known to be a long runner is currently active") + # + # if "ok" in line or (supportWait and "wait" in line): + # # a wait while printing means our printer's buffer ran out, probably due to some ok getting + # # swallowed, so we treat it the same as an ok here teo take up communication again + # if self._resendSwallowNextOk: + # self._resendSwallowNextOk = False + # + # elif self._resendDelta is not None: + # self._resendNextCommand() + # + # else: + # if self._sendFromQueue(): + # pass + # elif not self.isSdPrinting(): + # self._sendNext() + # + # elif line.lower().startswith("resend") or line.lower().startswith("rs"): + # self._handleResendRequest(line) + except: + self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") + + errorMsg = "See octoprint.log for details" + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Connection closed, closing down monitor") + + def _send_loop(self): + pass + + def _openSerial(self): + def default(_, port, baudrate, read_timeout): + if port is None or port == 'AUTO': + # no known port, try auto detection + self._changeState(self.STATE_DETECT_SERIAL) + ser = self._detectPort(True) + if ser is None: + self._errorValue = 'Failed to autodetect serial port, please set it manually.' + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Failed to autodetect serial port, please set it manually.") + return None + port = ser.port + + # connect to regular serial port + self._log("Connecting to: %s" % port) + if baudrate == 0: + baudrates = baudrateList() + ser = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD) + else: + ser = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD) + ser.close() + ser.parity = serial.PARITY_NONE + ser.open() + return ser + + serial_factories = self._serial_factory_hooks.items() + [("default", default)] + for name, factory in serial_factories: + try: + serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"])) + except (OSError, serial.SerialException): + exception_string = get_exception_string() + self._errorValue = "Connection error, see Terminal tab" + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name)) + if "failed to set custom baud rate" in exception_string.lower(): + self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian") + return False + if serial_obj is not None: + # 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 + + def _readline(self): + if self._serial is None: + return None + try: + ret = self._serial.readline() + if('ok' in ret or 'error' in ret): + if(len(self.acc_line_lengths) > 0): + #print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0]) + del self.acc_line_lengths[0] # Delete the commands character count corresponding to the last 'ok' + except serial.SerialException: + self._log("Unexpected error while reading serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + return None + if ret == '': return '' + try: + self._log("Recv: %s" % sanitize_ascii(ret)) + except ValueError as e: + self._log("WARN: While reading last line: %s" % e) + self._log("Recv: %r" % ret) + return ret + + def _state_none_handle(self, line): + pass + + def _state_connecting_handle(self, line): + if line.startswith("Grbl"): + versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) + if(versionMatch): + versionDict = versionMatch.groupdict() + self._writeGrblVersionToFile(versionDict) + if self._compareGrblVersion(versionDict) is False: + self._flashGrbl() + self._onConnected(self.STATE_LOCKED) + + def _state_locked_handle(self, line): + pass + + def _state_homing_handle(self, line): + if line.startswith("ok"): + self._changeState(self.STATE_OPERATIONAL) + + def _state_operational_handle(self, line): + pass + + # internal state management + def _changeState(self, newState): + if self._state == newState: + return + + if newState == self.STATE_PRINTING: + if self._status_timer is not None: + self._status_timer.cancel() + 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.start() + + if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: + if self._currentFile is not None: + self._currentFile.close() + self._log("entered state closed / closed with error. reseting character counter.") + self.acc_line_lengths = [] + + oldState = self.getStateString() + self._state = newState + self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) + self._callback.on_comm_state_change(newState) + + def _onConnected(self, nextState): + self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"]) + + if(nextState is None): + self._changeState(self.STATE_LOCKED) + else: + self._changeState(nextState) + + payload = dict(port=self._port, baudrate=self._baudrate) + eventManager().fire(Events.CONNECTED, payload) + + def _detectPort(self, close): + self._log("Serial port list: %s" % (str(serialList()))) + for p in serialList(): + try: + self._log("Connecting to: %s" % (p)) + serial_obj = serial.Serial(p) + if close: + serial_obj.close() + return serial_obj + except (OSError, serial.SerialException) as e: + self._log("Error while connecting to %s: %s" % (p, str(e))) + return None + + def _poll_status(self): + if self.isOperational() and not self._long_running_command: + self.sendCommand("?", cmd_type="status_poll") + + def _log(self, message): + self._callback.on_comm_log(message) + self._serialLogger.debug(message) + + def _compareGrblVersion(self, versionDict): + cwd = os.path.dirname(__file__) + with open(cwd + "/../grbl/grblVersionRequirement.yml", 'r') as infile: + grblReqDict = yamlload(infile) + requiredGrblVer = str(grblReqDict['grbl']) + '_' + str(grblReqDict['git']) + if grblReqDict['dirty'] is True: + requiredGrblVer += '-dirty' + actualGrblVer = str(versionDict['grbl']) + '_' + str(versionDict['git']) + if versionDict['dirty'] is not(None): + actualGrblVer += '-dirty' + # compare actual and required grbl version + self._requiredGrblVer = requiredGrblVer + self._actualGrblVer = actualGrblVer + print repr(requiredGrblVer) + print repr(actualGrblVer) + if requiredGrblVer != actualGrblVer: + self._log("unsupported grbl version detected...") + self._log("required: " + requiredGrblVer) + self._log("detected: " + actualGrblVer) + return False + else: + return True + + def _flashGrbl(self): + self._changeState(self.STATE_FLASHING) + self._serial.close() + cwd = os.path.dirname(__file__) + pathToGrblHex = cwd + "/../grbl/grbl.hex" + + # TODO check if avrdude is installed. + # TODO log in logfile as well, not only to the serial monitor (use self._logger.info()... ) + params = ["avrdude", "-patmega328p", "-carduino", "-b" + str(self._baudrate), "-P" + str(self._port), "-D", "-Uflash:w:" + pathToGrblHex] + rc = subprocesscall(params) + + if rc is False: + self._log("successfully flashed new grbl version") + self._openSerial() + self._changeState(self.STATE_CONNECTING) + else: + self._log("error during flashing of new grbl version") + self._errorValue = "avrdude returncode: %s" % rc + self._changeState(self.STATE_CLOSED_WITH_ERROR) + + @staticmethod + def _writeGrblVersionToFile(versionDict): + if versionDict['dirty'] == '-dirty': + versionDict['dirty'] = True + versionDict['lastConnect'] = time.time() + versionFile = os.path.join(settings().getBaseFolder("logs"), 'grbl_Version.yml') + with open(versionFile, 'w') as outfile: + outfile.write(yamldump(versionDict, default_flow_style=True)) + + def sendCommand(self, cmd, cmd_type=None): + pass + + def getStateString(self): + if self._state == self.STATE_NONE: + return "Offline" + if self._state == self.STATE_OPEN_SERIAL: + return "Opening serial port" + if self._state == self.STATE_DETECT_SERIAL: + return "Detecting serial port" + if self._state == self.STATE_CONNECTING: + return "Connecting" + if self._state == self.STATE_OPERATIONAL: + return "Operational" + if self._state == self.STATE_PRINTING: + return "Printing" + if self._state == self.STATE_PAUSED: + return "Paused" + if self._state == self.STATE_CLOSED: + return "Closed" + if self._state == self.STATE_ERROR: + return "Error: %s" % (self.getErrorString()) + if self._state == self.STATE_CLOSED_WITH_ERROR: + return "Error: %s" % (self.getErrorString()) + if self._state == self.STATE_LOCKED: + return "Locked" + if self._state == self.STATE_HOMING: + return "Homing" + if self._state == self.STATE_FLASHING: + return "Flashing" + return "?%d?" % (self._state) + + def isOperational(self): + return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED + + def isPrinting(self): + return self._state == self.STATE_PRINTING + + def isPaused(self): + return self._state == self.STATE_PAUSED + + def getErrorString(self): + return self._errorValue + + def close(self, isError = False): + if self._status_timer is not None: + try: + self._status_timer.cancel() + self._status_timer = None + except AttributeError: + pass + + self._monitoring_active = False + self._sending_active = False + + printing = self.isPrinting() or self.isPaused() + if self._serial is not None: + if isError: + self._changeState(self.STATE_CLOSED_WITH_ERROR) + else: + self._changeState(self.STATE_CLOSED) + self._serial.close() + self._serial = None + + if printing: + payload = None + if self._currentFile is not None: + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + eventManager().fire(Events.PRINT_FAILED, payload) + eventManager().fire(Events.DISCONNECTED) + +### MachineCom callback ################################################################################################ +class MachineComPrintCallback(object): + def on_comm_log(self, message): + pass + + def on_comm_temperature_update(self, temp, bedTemp): + pass + + def on_comm_state_change(self, state): + pass + + def on_comm_message(self, message): + pass + + def on_comm_progress(self): + pass + + def on_comm_print_job_done(self): + pass + + def on_comm_z_change(self, newZ): + pass + + def on_comm_file_selected(self, filename, filesize, sd): + pass + + def on_comm_sd_state_change(self, sdReady): + pass + + def on_comm_sd_files(self, files): + pass + + def on_comm_file_transfer_started(self, filename, filesize): + pass + + def on_comm_file_transfer_done(self, filename): + pass + + def on_comm_force_disconnect(self): + pass + + def on_comm_pos_update(self, MPos, WPos): + pass + +def convert_pause_triggers(configured_triggers): + triggers = { + "enable": [], + "disable": [], + "toggle": [] + } + for trigger in configured_triggers: + if not "regex" in trigger or not "type" in trigger: + continue + + try: + regex = trigger["regex"] + t = trigger["type"] + if t in triggers: + # make sure regex is valid + re.compile(regex) + # add to type list + triggers[t].append(regex) + except re.error: + # invalid regex or something like this, we'll just skip this entry + pass + + result = dict() + for t in triggers.keys(): + if len(triggers[t]) > 0: + result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t]))) + return result + +def get_new_timeout(t): + now = time.time() + return now + get_interval(t) + +def get_interval(t): + if t not in default_settings["serial"]["timeout"]: + return 0 + else: + return settings().getFloat(["serial", "timeout", type]) + +def serialList(): + baselist = [glob.glob("/dev/ttyUSB*"), + + glob.glob("/dev/ttyACM*"), + + glob.glob("/dev/tty.usb*"), + + glob.glob("/dev/cu.*"), + + glob.glob("/dev/cuaU*"), + + glob.glob("/dev/rfcomm*")] + + additionalPorts = settings().get(["serial", "additionalPorts"]) + for additional in additionalPorts: + baselist += glob.glob(additional) + + prev = settings().get(["serial", "port"]) + if prev in baselist: + baselist.remove(prev) + baselist.insert(0, prev) + if settings().getBoolean(["devel", "virtualPrinter", "enabled"]): + baselist.append("VIRTUAL") + return baselist + +def baudrateList(): + ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600] + prev = settings().getInt(["serial", "baudrate"]) + if prev in ret: + ret.remove(prev) + ret.insert(0, prev) + return ret From 04415d86567abe0e7aa3f095abbe4545973b6cbc Mon Sep 17 00:00:00 2001 From: make-ing Date: Sat, 17 Oct 2015 16:14:55 +0200 Subject: [PATCH 02/14] fixed bad operator bug --- src/octoprint/util/comm_acc2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 5b679813..ad10081b 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -774,11 +774,11 @@ def get_interval(t): def serialList(): baselist = [glob.glob("/dev/ttyUSB*"), - + glob.glob("/dev/ttyACM*"), - + glob.glob("/dev/tty.usb*"), - + glob.glob("/dev/cu.*"), - + glob.glob("/dev/cuaU*"), - + glob.glob("/dev/rfcomm*")] + glob.glob("/dev/ttyACM*"), + glob.glob("/dev/tty.usb*"), + glob.glob("/dev/cu.*"), + glob.glob("/dev/cuaU*"), + glob.glob("/dev/rfcomm*")] additionalPorts = settings().get(["serial", "additionalPorts"]) for additional in additionalPorts: From 078fafbdb6e0c8f57556a07de78e6dfd37bcb3cb Mon Sep 17 00:00:00 2001 From: make-ing Date: Mon, 19 Oct 2015 14:00:52 +0200 Subject: [PATCH 03/14] a lot of changes --- src/octoprint/util/comm_acc2.py | 294 +++++++++++++++++++++++++++++++- 1 file changed, 285 insertions(+), 9 deletions(-) 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) From ac458f6a246737e7790f98935c8bf4ded3983d8d Mon Sep 17 00:00:00 2001 From: make-ing Date: Mon, 19 Oct 2015 14:11:24 +0200 Subject: [PATCH 04/14] removed a lot of stuff in monitoring loop --- src/octoprint/util/comm_acc2.py | 337 +++----------------------------- 1 file changed, 27 insertions(+), 310 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 0d647686..fe442140 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -117,302 +117,11 @@ class MachineCom(object): break if line.strip() is not "": self._timeout = get_new_timeout("communication") - # parse line depending on state self._state_parse_dict[self._state](self, line) - return - - - - - - - # # GRBL Position update - # if self._grbl : - # if(self._state == self.STATE_HOMING and 'ok' in line): - # self._changeState(self.STATE_OPERATIONAL) - # self._onHomingDone(); - # - # # TODO check if "Alarm" is enough - # if("Alarm lock" in line): - # self._changeState(self.STATE_LOCKED) - # if("['$H'|'$X' to unlock]" in line): - # self._changeState(self.STATE_LOCKED) - # - # # TODO maybe better in _gcode_X_sent ... - # if("Idle" in line and (self._state == self.STATE_LOCKED) ): - # self._changeState(self.STATE_OPERATIONAL) - # - # # TODO highly experimental. needs testing. - # if("Hold" in line and self._state == self.STATE_PRINTING): - # self._changeState(self.STATE_PAUSED) - # #if("Run" in line and self._state == self.STATE_PAUSED): - # # self._changeState(self.STATE_PRINTING) - # - # if 'MPos:' in line: - # self._update_grbl_pos(line) - # - # if("ALARM: Hard/soft limit" in line): - # errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle" - # self._log(errorMsg) - # self._errorValue = errorMsg - # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - # eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()}) - # self._openSerial() - # self._changeState(self.STATE_CONNECTING) - # - # if("Invalid gcode" in line and self._state == self.STATE_PRINTING): - # # TODO Pause machine instead of resetting it. - # errorMsg = line - # self._log(errorMsg) - # self._errorValue = errorMsg - # # self._changeState(self.STATE_ERROR) - # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - # self._openSerial() - # self._changeState(self.STATE_CONNECTING) - # - # if("Grbl" in line and self._state == self.STATE_PRINTING): - # errorMsg = "Machine reset." - # self._log(errorMsg) - # self._errorValue = errorMsg - # self._changeState(self.STATE_LOCKED) - # eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - # - # if("Grbl" in line): - # versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) - # if(versionMatch): - # versionDict = versionMatch.groupdict() - # self._writeGrblVersionToFile(versionDict) - # if self._compareGrblVersion(versionDict) is False: - # self._flashGrbl() - # - # if("error:" in line): - # self.handle_grbl_error(line) - # - # # ##~~ SD file list - # # # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing - # # if self._sdFileList and not "End file list" in line: - # # preprocessed_line = line.strip().lower() - # # fileinfo = preprocessed_line.rsplit(None, 1) - # # if len(fileinfo) > 1: - # # # we might have extended file information here, so let's split filename and size and try to make them a bit nicer - # # filename, size = fileinfo - # # try: - # # size = int(size) - # # except ValueError: - # # # whatever that was, it was not an integer, so we'll just use the whole line as filename and set size to None - # # filename = preprocessed_line - # # size = None - # # else: - # # # no extended file information, so only the filename is there and we set size to None - # # filename = preprocessed_line - # # size = None - # # - # # if valid_file_type(filename, "machinecode"): - # # if filter_non_ascii(filename): - # # self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) - # # else: - # # if not filename.startswith("/"): - # # # file from the root of the sd -- we'll prepend a / - # # filename = "/" + filename - # # self._sdFiles.append((filename, size)) - # # continue - # - # ##~~ process oks - # if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")): - # self._clear_to_send.set() - # self._long_running_command = False - # - # # ##~~ Temperature processing - # # if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ' B:' in line or line.startswith('B:'): - # # if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating: - # # self._logger.debug("Externally triggered heatup detected") - # # self._heating = True - # # self._heatupWaitStartTime = time.time() - # # self._processTemperatures(line) - # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) - # # - # # elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line): - # # matchExtr = self._regex_repetierTempExtr.match(line) - # # matchBed = self._regex_repetierTempBed.match(line) - # # - # # if matchExtr is not None: - # # toolNum = int(matchExtr.group(1)) - # # try: - # # target = float(matchExtr.group(2)) - # # if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): - # # (actual, oldTarget) = self._temp[toolNum] - # # self._temp[toolNum] = (actual, target) - # # else: - # # self._temp[toolNum] = (None, target) - # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) - # # except ValueError: - # # pass - # # elif matchBed is not None: - # # try: - # # target = float(matchBed.group(1)) - # # if self._bedTemp is not None and isinstance(self._bedTemp, tuple): - # # (actual, oldTarget) = self._bedTemp - # # self._bedTemp = (actual, target) - # # else: - # # self._bedTemp = (None, target) - # # self._callback.on_comm_temperature_update(self._temp, self._bedTemp) - # # except ValueError: - # # pass - # - # # #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. - # # if 'ok' in line and self._heatupWaitStartTime: - # # self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) - # # self._heatupWaitStartTime = None - # # self._heating = False - # - # # ##~~ SD Card handling - # # elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: - # # self._sdAvailable = False - # # self._sdFiles = [] - # # self._callback.on_comm_sd_state_change(self._sdAvailable) - # # elif 'Not SD printing' in line: - # # if self.isSdFileSelected() and self.isPrinting(): - # # # something went wrong, printer is reporting that we actually are not printing right now... - # # self._sdFilePos = 0 - # # self._changeState(self.STATE_OPERATIONAL) - # # elif 'SD card ok' in line and not self._sdAvailable: - # # self._sdAvailable = True - # # self.refreshSdFiles() - # # self._callback.on_comm_sd_state_change(self._sdAvailable) - # # elif 'Begin file list' in line: - # # self._sdFiles = [] - # # self._sdFileList = True - # # elif 'End file list' in line: - # # self._sdFileList = False - # # self._callback.on_comm_sd_files(self._sdFiles) - # # elif 'SD printing byte' in line and self.isSdPrinting(): - # # # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" - # # match = self._regex_sdPrintingByte.search(line) - # # self._currentFile.setFilepos(int(match.group(1))) - # # self._callback.on_comm_progress() - # # elif 'File opened' in line and not self._ignore_select: - # # # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" - # # match = self._regex_sdFileOpened.search(line) - # # if self._sdFileToSelect: - # # name = self._sdFileToSelect - # # self._sdFileToSelect = None - # # else: - # # name = match.group(1) - # # self._currentFile = PrintingSdFileInformation(name, int(match.group(2))) - # # elif 'File selected' in line: - # # if self._ignore_select: - # # self._ignore_select = False - # # elif self._currentFile is not None: - # # # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" - # # self._callback.on_comm_file_selected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) - # # eventManager().fire(Events.FILE_SELECTED, { - # # "file": self._currentFile.getFilename(), - # # "origin": self._currentFile.getFileLocation() - # # }) - # # elif 'Writing to file' in line: - # # # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" - # # self._changeState(self.STATE_PRINTING) - # # self._clear_to_send.set() - # # line = "ok" - # # elif 'Done printing file' in line and self.isSdPrinting(): - # # # printer is reporting file finished printing - # # self._sdFilePos = 0 - # # self._callback.on_comm_print_job_done() - # # self._changeState(self.STATE_OPERATIONAL) - # # eventManager().fire(Events.PRINT_DONE, { - # # "file": self._currentFile.getFilename(), - # # "filename": os.path.basename(self._currentFile.getFilename()), - # # "origin": self._currentFile.getFileLocation(), - # # "time": self.getPrintTime() - # # }) - # # if self._sd_status_timer is not None: - # # try: - # # self._sd_status_timer.cancel() - # # except: - # # pass - # # elif 'Done saving file' in line: - # # self.refreshSdFiles() - # # elif 'File deleted' in line and line.strip().endswith("ok"): - # # # buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in - # # # current versions - # # self._clear_to_send.set() - # - # ##~~ Message handling - # elif line.strip() != '' \ - # and line.strip() != 'ok' and not line.startswith("wait") \ - # and not line.startswith('Resend:') \ - # and line != 'echo:Unknown command:""\n' \ - # and line != "Unsupported statement" \ - # and self.isOperational(): - # self._callback.on_comm_message(line) - # - # ##~~ Parsing for feedback commands - # if feedback_controls and feedback_matcher and not "_all" in feedback_errors: - # try: - # self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors) - # except: - # # something went wrong while feedback matching - # self._logger.exception("Error while trying to apply feedback control matching, disabling it") - # feedback_errors.append("_all") - # - # ##~~ Parsing for pause triggers - # - # if pause_triggers and not self.isStreaming(): - # if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None: - # self.setPause(True) - # elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None: - # self.setPause(False) - # elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None: - # self.setPause(not self.isPaused()) - # - # - # ### Operational - # elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: - # if "ok" in line: - # # if we still have commands to process, process them - # if self._resendSwallowNextOk: - # self._resendSwallowNextOk = False - # elif self._resendDelta is not None: - # self._resendNextCommand() - # elif self._sendFromQueue(): - # pass - # - # # resend -> start resend procedure from requested line - # elif line.lower().startswith("resend") or line.lower().startswith("rs"): - # self._handleResendRequest(line) - # - # ### Printing - # elif self._state == self.STATE_PRINTING: - # if line == "" and time.time() > self._timeout: - # if not self._long_running_command: - # self._log("Communication timeout during printing, forcing a line") - # self._sendCommand("?") - # self._clear_to_send.set() - # else: - # self._logger.debug("Ran into a communication timeout, but a command known to be a long runner is currently active") - # - # if "ok" in line or (supportWait and "wait" in line): - # # a wait while printing means our printer's buffer ran out, probably due to some ok getting - # # swallowed, so we treat it the same as an ok here teo take up communication again - # if self._resendSwallowNextOk: - # self._resendSwallowNextOk = False - # - # elif self._resendDelta is not None: - # self._resendNextCommand() - # - # else: - # if self._sendFromQueue(): - # pass - # elif not self.isSdPrinting(): - # self._sendNext() - # - # elif line.lower().startswith("resend") or line.lower().startswith("rs"): - # self._handleResendRequest(line) except: - self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") - + self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg @@ -422,24 +131,32 @@ class MachineCom(object): def _send_loop(self): 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() + try: + 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() + except: + self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam") + errorMsg = "See octoprint.log for details" + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def _sendCommand(self, cmd): if sum([len(x) for x in self._acc_line_buffer]) + len(cmd) +1 < self.RX_BUFFER_SIZE: From d6bfb032141d6800f8037e7cdc0e02797c6cd739 Mon Sep 17 00:00:00 2001 From: make-ing Date: Mon, 19 Oct 2015 14:36:03 +0200 Subject: [PATCH 05/14] changed some stuff --- src/octoprint/util/comm_acc2.py | 121 +++++++++++++++++--------------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index fe442140..fd375710 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -11,7 +11,6 @@ import glob import time import serial import re -import queue from yaml import load as yamlload from yaml import dump as yamldump @@ -69,6 +68,7 @@ class MachineCom(object): self._acc_line_buffer = [] self._pauseWaitTimeLost = 0.0 self._send_event = threading.Event() + self._send_event.clear() self._real_time_commands={'poll_status':False, 'feed_hold':False, @@ -130,6 +130,10 @@ class MachineCom(object): self._log("Connection closed, closing down monitor") def _send_loop(self): + # first wait until serial connection is established + self._send_event.wait() + self._send_event.clear() + while self._sending_active: try: if self._real_time_commands['poll_status']: @@ -266,6 +270,7 @@ class MachineCom(object): self._writeGrblVersionToFile(versionDict) if self._compareGrblVersion(versionDict) is False: self._flashGrbl() + self._send_event.set() self._onConnected(self.STATE_LOCKED) def _state_locked_handle(self, line): @@ -585,6 +590,63 @@ class MachineComPrintCallback(object): def on_comm_pos_update(self, MPos, WPos): pass +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 + class PrintingGcodeFileInformation(PrintingFileInformation): """ Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures @@ -650,63 +712,6 @@ class PrintingGcodeFileInformation(PrintingFileInformation): 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": [], From 0c9ddbfd85e34a63e89f295f21683f3be108d2f3 Mon Sep 17 00:00:00 2001 From: make-ing Date: Mon, 19 Oct 2015 15:05:56 +0200 Subject: [PATCH 06/14] added debug message --- src/octoprint/util/comm_acc2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index fd375710..a6c3b43a 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -20,6 +20,7 @@ import octoprint.plugin from octoprint.events import eventManager, Events from octoprint.settings import settings, default_settings +from octoprint.filemanager.destinations import FileDestinations from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii ### MachineCom ######################################################################################################### @@ -215,6 +216,7 @@ class MachineCom(object): if serial_obj is not None: # first hook to succeed wins, but any can pass on to the next self._changeState(self.STATE_OPEN_SERIAL) + self._log(repr(self._serial)) self._serial = serial_obj return True return False From 796fbeacdd331afe32a8cc793cff42b26494fe0a Mon Sep 17 00:00:00 2001 From: make-ing Date: Tue, 20 Oct 2015 16:21:01 +0200 Subject: [PATCH 07/14] a lot of changes --- src/octoprint/util/comm_acc2.py | 340 +++++++++++++++++++++++++------- 1 file changed, 268 insertions(+), 72 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index a6c3b43a..f3f3e71f 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 @@ -18,8 +19,8 @@ from subprocess import call as subprocesscall import octoprint.plugin -from octoprint.events import eventManager, Events from octoprint.settings import settings, default_settings +from octoprint.events import eventManager, Events from octoprint.filemanager.destinations import FileDestinations from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii @@ -45,6 +46,8 @@ class MachineCom(object): if port is None: port = settings().get(["serial", "port"]) + elif isinstance(port, list): + port = port[0] if baudrate is None: settingsBaudrate = settings().getInt(["serial", "baudrate"]) if settingsBaudrate is None: @@ -67,9 +70,13 @@ class MachineCom(object): self._currentFile = None self._status_timer = None self._acc_line_buffer = [] + self._cmd = None self._pauseWaitTimeLost = 0.0 - self._send_event = threading.Event() - self._send_event.clear() + self._commandQueue = Queue.Queue() + self._send_event = CountedEvent(max=20) + + # regular expressions + self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])") self._real_time_commands={'poll_status':False, 'feed_hold':False, @@ -81,10 +88,18 @@ class MachineCom(object): self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory") self._state_parse_dict = {self.STATE_NONE:self._state_none_handle, - self.STATE_CONNECTING:self._state_connecting_handle, - self.STATE_LOCKED:self._state_locked_handle, - self.STATE_HOMING:self._state_homing_handle, - self.STATE_OPERATIONAL:self._state_operational_handle} + self.STATE_OPEN_SERIAL:self._state_none_handle, + self.STATE_DETECT_SERIAL:self._state_none_handle, + self.STATE_CONNECTING:self._state_connecting_handle, + self.STATE_OPERATIONAL:self._state_operational_handle, + self.STATE_PRINTING:self._state_printing_handle, + self.STATE_PAUSED:self._state_none_handle, + self.STATE_CLOSED:self._state_none_handle, + self.STATE_ERROR:self._state_none_handle, + self.STATE_CLOSED_WITH_ERROR:self._state_none_handle, + self.STATE_LOCKED:self._state_locked_handle, + self.STATE_HOMING:self._state_homing_handle, + self.STATE_FLASHING:self._state_none_handle} # monitoring thread self._monitoring_active = True @@ -99,8 +114,6 @@ class MachineCom(object): self.sending_thread.start() def _monitor_loop(self): - pause_triggers = convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"])) - #Open the serial port. if not self._openSerial(): return @@ -109,8 +122,6 @@ class MachineCom(object): self._changeState(self.STATE_CONNECTING) self._timeout = get_new_timeout("communication") - supportWait = settings().getBoolean(["feature", "supportWait"]) - while self._monitoring_active: try: line = self._readline() @@ -119,8 +130,7 @@ class MachineCom(object): if line.strip() is not "": self._timeout = get_new_timeout("communication") # parse line depending on state - self._state_parse_dict[self._state](self, line) - return + self._state_parse_dict[self._state](line) except: self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam") errorMsg = "See octoprint.log for details" @@ -138,21 +148,28 @@ class MachineCom(object): while self._sending_active: try: if self._real_time_commands['poll_status']: - self._sendCommand('?') + self.sendCommand('?') self._real_time_commands['poll_status']=False elif self._real_time_commands['feed_hold']: - self._sendCommand('!') + self.sendCommand('!') self._real_time_commands['feed_hold']=False elif self._real_time_commands['cycle_start']: - self._sendCommand('~') + self.sendCommand('~') self._real_time_commands['cycle_start']=False elif self._real_time_commands['soft_reset']: - self._sendCommand(b'\x18') + 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()) + #else: + #if not self._commandQueue.empty() and not self.isHoming(): + # self._sendCommand(self._commandQueue.get()) + #elif self.isPrinting(): + # self._sendCommand(self._getNext()) + if self.isPrinting() and self._commandQueue.empty(): + cmd = self._getNext() + if cmd is not None: + self.sendCommand(cmd) + + self._sendCommand() self._send_event.wait(1) self._send_event.clear() except: @@ -163,12 +180,19 @@ class MachineCom(object): self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - 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) + def _sendCommand(self): + if self._cmd is None and self._commandQueue.empty(): + return + elif self._cmd is None: + self._cmd = self._commandQueue.get() + if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-10: + self._log("Send: %s" % self._cmd) + self._acc_line_buffer.append(self._cmd) try: - self._serial.write(cmd + '\n') + self._serial.write(self._cmd + '\n') + # trigger "sent" phase and use up one "ok" + self._process_command_phase("sent", self._cmd) + self._cmd = None except serial.SerialException: self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() @@ -215,8 +239,8 @@ class MachineCom(object): return False if serial_obj is not None: # first hook to succeed wins, but any can pass on to the next - self._changeState(self.STATE_OPEN_SERIAL) self._log(repr(self._serial)) + self._changeState(self.STATE_OPEN_SERIAL) self._serial = serial_obj return True return False @@ -227,9 +251,9 @@ class MachineCom(object): try: ret = self._serial.readline() if('ok' in ret or 'error' in ret): - if(len(self.acc_line_lengths) > 0): + if(len(self._acc_line_buffer) > 0): #print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0]) - del self.acc_line_lengths[0] # Delete the commands character count corresponding to the last 'ok' + del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok' except serial.SerialException: self._log("Unexpected error while reading serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() @@ -266,12 +290,12 @@ class MachineCom(object): def _state_connecting_handle(self, line): if line.startswith("Grbl"): - versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) - if(versionMatch): - versionDict = versionMatch.groupdict() - self._writeGrblVersionToFile(versionDict) - if self._compareGrblVersion(versionDict) is False: - self._flashGrbl() + #versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) + #if(versionMatch): + # versionDict = versionMatch.groupdict() + # self._writeGrblVersionToFile(versionDict) + # if self._compareGrblVersion(versionDict) is False: + # self._flashGrbl() self._send_event.set() self._onConnected(self.STATE_LOCKED) @@ -283,7 +307,96 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) def _state_operational_handle(self, line): - pass + if line.startswith("ok"): + self._send_event.set() + elif line.startswith("<"): + self._update_grbl_pos(line) + + def _state_printing_handle(self, line): + if line.startswith("ok"): + self._send_event.set() + + def _update_grbl_pos(self, line): + # line example: + # + try: + idx_mx_begin = line.index('MPos:') + 5 + idx_mx_end = line.index('.', idx_mx_begin) + 2 + idx_my_begin = line.index(',', idx_mx_end) + 1 + idx_my_end = line.index('.', idx_my_begin) + 2 + #idx_mz_begin = line.index(',', idx_my_end) + 1 + #idx_mz_end = line.index('.', idx_mz_begin) + 2 + + idx_wx_begin = line.index('WPos:') + 5 + idx_wx_end = line.index('.', idx_wx_begin) + 2 + idx_wy_begin = line.index(',', idx_wx_end) + 1 + idx_wy_end = line.index('.', idx_wy_begin) + 2 + #idx_wz_begin = line.index(',', idx_wy_end) + 1 + #idx_wz_end = line.index('.', idx_wz_begin) + 2 + + #idx_intensity_begin = line.index('S:', idx_wz_end) + 2 + #idx_intensity_end = line.index(',', idx_intensity_begin) + + #idx_laserstate_begin = line.index('laser ', idx_intensity_end) + 6 + #idx_laserstate_end = line.index(':', idx_laserstate_begin) + + #payload = { + #"mx": line[idx_mx_begin:idx_mx_end], + #"my": line[idx_my_begin:idx_my_end], + #"mz": line[idx_mz_begin:idx_mz_end], + #"wx": line[idx_wx_begin:idx_wx_end], + #"wy": line[idx_wy_begin:idx_wy_end], + #"wz": line[idx_wz_begin:idx_wz_end], + #"laser": line[idx_laserstate_begin:idx_laserstate_end], + #"intensity": line[idx_intensity_begin:idx_intensity_end] + #} + mx = float(line[idx_mx_begin:idx_mx_end]) + my = float(line[idx_my_begin:idx_my_end]) + wx = float(line[idx_wx_begin:idx_wx_end]) + wy = float(line[idx_wy_begin:idx_wy_end]) + self._callback.on_comm_pos_update([mx, my, 0], [wx, wy, 0]) + #eventManager().fire(Events.RT_STATE, payload) + except ValueError: + pass + + def _process_command_phase(self, phase, command, command_type=None, gcode=None): + if phase not in ("queuing", "queued", "sending", "sent"): + return command, command_type, gcode + + if gcode is None: + gcode = self._gcode_command_for_cmd(command) + + # if it's a gcode command send it through the specific handler if it exists + if gcode is not None: + gcodeHandler = "_gcode_" + gcode + "_" + phase + if hasattr(self, gcodeHandler): + handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type) + command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result) + + # finally return whatever we resulted on + return command, command_type, gcode + + def _gcode_command_for_cmd(self, cmd): + """ + Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0"). + + Arguments: + cmd (str): The command to try to parse. + + Returns: + str or None: The GCODE command identifier if it could be parsed, or None if not. + """ + if not cmd: + return None + + if cmd == '!': return 'Hold' + if cmd == '~': return 'Resume' + + gcode = self._regex_command.search(cmd) + if not gcode: + return None + + return gcode.group(1) # internal state management def _changeState(self, newState): @@ -339,6 +452,7 @@ class MachineCom(object): def _poll_status(self): if self.isOperational(): self._real_time_commands['poll_status']=True + self._send_event.set() def _log(self, message): self._callback.on_comm_log(message) @@ -396,6 +510,47 @@ class MachineCom(object): with open(versionFile, 'w') as outfile: outfile.write(yamldump(versionDict, default_flow_style=True)) + def _handle_command_handler_result(self, command, command_type, gcode, handler_result): + original_tuple = (command, command_type, gcode) + + if handler_result is None: + # handler didn't return anything, we'll just continue + return original_tuple + + if isinstance(handler_result, basestring): + # handler did return just a string, we'll turn that into a 1-tuple now + handler_result = (handler_result,) + elif not isinstance(handler_result, (tuple, list)): + # handler didn't return an expected result format, we'll just ignore it and continue + return original_tuple + + hook_result_length = len(handler_result) + if hook_result_length == 1: + # handler returned just the command + command, = handler_result + elif hook_result_length == 2: + # handler returned command and command_type + command, command_type = handler_result + else: + # handler returned a tuple of an unexpected length + return original_tuple + + gcode = self._gcode_command_for_cmd(command) + return command, command_type, gcode + + ##~~ command handlers + def _gcode_H_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_HOMING) + return cmd + + def _gcode_Hold_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_PAUSED) + return cmd + + def _gcode_Resume_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_PRINTING) + return cmd + def sendCommand(self, cmd, cmd_type=None, processed=False): cmd = cmd.encode('ascii', 'replace') if not processed: @@ -403,45 +558,42 @@ class MachineCom(object): 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 + if cmd[0] == "/": + specialcmd = cmd[1:].lower() + if "togglestatusreport" in specialcmd: + if self._status_timer is None: + self._status_timer = RepeatedTimer(1, self._poll_status) + self._status_timer.start() + else: + self._status_timer.cancel() + self._status_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._status_timer is not None: + self._status_timer.cancel() + self._status_timer = RepeatedTimer(frequency, self._poll_status) + self._status_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) + self._commandQueue.put(cmd) + self._send_event.set() def selectFile(self, filename, sd): if self.isBusy(): @@ -455,6 +607,34 @@ class MachineCom(object): }) self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) + def startPrint(self): + if not self.isOperational() or self.isPrinting(): + return + + if self._currentFile is None: + raise ValueError("No file selected for printing") + + try: + # ensure fan is on whatever gcode follows. + self.sendCommand("M08") + + self._currentFile.start() + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + eventManager().fire(Events.PRINT_STARTED, payload) + #self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload)) + + self._changeState(self.STATE_PRINTING) + except: + self._logger.exception("Error while trying to start printing") + self._errorValue = get_exception_string() + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + def getStateString(self): if self._state == self.STATE_NONE: return "Offline" @@ -502,9 +682,24 @@ class MachineCom(object): def isHoming(self): return self._state == self.STATE_HOMING + def isFlashing(self): + return self._state == self.STATE_FLASHING + def isBusy(self): return self.isPrinting() or self.isPaused() + def isError(self): + return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR + + def isClosedOrError(self): + return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED + + def isSdReady(self): + return False + + def isStreaming(self): + return False + def getErrorString(self): return self._errorValue @@ -744,7 +939,8 @@ def convert_pause_triggers(configured_triggers): def process_gcode_line(line): line = strip_comment(line).strip() - if len(line): + line = line.replace(" ", "") + if not len(line): return None return line @@ -770,7 +966,7 @@ def get_interval(t): if t not in default_settings["serial"]["timeout"]: return 0 else: - return settings().getFloat(["serial", "timeout", type]) + return settings().getFloat(["serial", "timeout", t]) def serialList(): baselist = [glob.glob("/dev/ttyUSB*"), From 0b5395e21d5f39ba06c25d3168313aaba00dc1f9 Mon Sep 17 00:00:00 2001 From: make-ing Date: Tue, 20 Oct 2015 17:22:03 +0200 Subject: [PATCH 08/14] changed send_event.set() handling into _readline() --- src/octoprint/util/comm_acc2.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index f3f3e71f..6c8e623d 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -254,6 +254,7 @@ class MachineCom(object): if(len(self._acc_line_buffer) > 0): #print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0]) del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok' + self._send_event.set() except serial.SerialException: self._log("Unexpected error while reading serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() @@ -307,14 +308,12 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) def _state_operational_handle(self, line): - if line.startswith("ok"): - self._send_event.set() - elif line.startswith("<"): + if line.startswith("<"): self._update_grbl_pos(line) def _state_printing_handle(self, line): - if line.startswith("ok"): - self._send_event.set() + if line.startswith("<"): + self._update_grbl_pos(line) def _update_grbl_pos(self, line): # line example: From 6ff1ad52e77cb175404684b8b5f008605ec9242f Mon Sep 17 00:00:00 2001 From: make-ing Date: Tue, 20 Oct 2015 18:03:47 +0200 Subject: [PATCH 09/14] added cancelPrint method --- src/octoprint/util/comm_acc2.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 6c8e623d..cdeaf43a 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -159,11 +159,7 @@ class MachineCom(object): elif self._real_time_commands['soft_reset']: self.sendCommand(b'\x18') self._real_time_commands['soft_reset']=False - #else: - #if not self._commandQueue.empty() and not self.isHoming(): - # self._sendCommand(self._commandQueue.get()) - #elif self.isPrinting(): - # self._sendCommand(self._getNext()) + if self.isPrinting() and self._commandQueue.empty(): cmd = self._getNext() if cmd is not None: @@ -185,6 +181,7 @@ class MachineCom(object): return elif self._cmd is None: self._cmd = self._commandQueue.get() + self._commandQueue.task_done() if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-10: self._log("Send: %s" % self._cmd) self._acc_line_buffer.append(self._cmd) @@ -634,6 +631,26 @@ class MachineCom(object): self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + def cancelPrint(self): + if not self.isOperational(): + return + + self._commandQueue=Queue.Queue() + self.sendCommand('M5') + self.sendCommand('G0X0Y0') + self.sendCommand('M9') + self._commandQueue.join() + + self._changeState(self.STATE_OPERATIONAL) + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + + eventManager().fire(Events.PRINT_CANCELLED, payload) + def getStateString(self): if self._state == self.STATE_NONE: return "Offline" From 0f2edfd82e1133da83b7e3b00455ad3e25a30b79 Mon Sep 17 00:00:00 2001 From: make-ing Date: Tue, 20 Oct 2015 18:12:30 +0200 Subject: [PATCH 10/14] added setPause() method --- src/octoprint/util/comm_acc2.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index cdeaf43a..387b955e 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -71,6 +71,7 @@ class MachineCom(object): self._status_timer = None self._acc_line_buffer = [] self._cmd = None + self._pauseWaitStartTime = None self._pauseWaitTimeLost = 0.0 self._commandQueue = Queue.Queue() self._send_event = CountedEvent(max=20) @@ -651,6 +652,28 @@ class MachineCom(object): eventManager().fire(Events.PRINT_CANCELLED, payload) + def setPause(self, pause): + if not self._currentFile: + return + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + + if not pause and self.isPaused(): + if self._pauseWaitStartTime: + self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) + self._pauseWaitStartTime = None + self.sendCommand('~') + eventManager().fire(Events.PRINT_RESUMED, payload) + elif pause and self.isPrinting(): + if not self._pauseWaitStartTime: + self._pauseWaitStartTime = time.time() + self.sendCommand('!') + eventManager().fire(Events.PRINT_PAUSED, payload) + def getStateString(self): if self._state == self.STATE_NONE: return "Offline" From 312af22a731e9ecd19f88653a050300ae51755c4 Mon Sep 17 00:00:00 2001 From: make-ing Date: Wed, 21 Oct 2015 14:55:54 +0200 Subject: [PATCH 11/14] changed some stuff --- src/octoprint/util/comm_acc2.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 387b955e..44a5e5b6 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -94,7 +94,7 @@ class MachineCom(object): self.STATE_CONNECTING:self._state_connecting_handle, self.STATE_OPERATIONAL:self._state_operational_handle, self.STATE_PRINTING:self._state_printing_handle, - self.STATE_PAUSED:self._state_none_handle, + self.STATE_PAUSED:self._state_paused_handle, self.STATE_CLOSED:self._state_none_handle, self.STATE_ERROR:self._state_none_handle, self.STATE_CLOSED_WITH_ERROR:self._state_none_handle, @@ -182,7 +182,6 @@ class MachineCom(object): return elif self._cmd is None: self._cmd = self._commandQueue.get() - self._commandQueue.task_done() if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-10: self._log("Send: %s" % self._cmd) self._acc_line_buffer.append(self._cmd) @@ -191,6 +190,7 @@ class MachineCom(object): # trigger "sent" phase and use up one "ok" self._process_command_phase("sent", self._cmd) self._cmd = None + self._commandQueue.task_done() except serial.SerialException: self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() @@ -313,6 +313,10 @@ class MachineCom(object): if line.startswith("<"): self._update_grbl_pos(line) + def _state_paused_handle(self, line): + if line.startswith("<"): + self._update_grbl_pos(line) + def _update_grbl_pos(self, line): # line example: # @@ -405,7 +409,7 @@ class MachineCom(object): self._status_timer.cancel() self._status_timer = RepeatedTimer(1, self._poll_status) self._status_timer.start() - elif newState == self.STATE_OPERATIONAL: + elif newState == self.STATE_OPERATIONAL or newState == self.STATE_PAUSED: if self._status_timer is not None: self._status_timer.cancel() self._status_timer = RepeatedTimer(2, self._poll_status) @@ -636,7 +640,8 @@ class MachineCom(object): if not self.isOperational(): return - self._commandQueue=Queue.Queue() + with self._commandQueue.mutex: + self._commandQueue.queue.clear() self.sendCommand('M5') self.sendCommand('G0X0Y0') self.sendCommand('M9') From 79c10ea410be92e78ad08518e22bffb28b931cfb Mon Sep 17 00:00:00 2001 From: make-ing Date: Wed, 21 Oct 2015 15:57:34 +0200 Subject: [PATCH 12/14] changed a lot of stuff --- src/octoprint/util/comm_acc2.py | 72 +++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 44a5e5b6..40ca341d 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -69,6 +69,7 @@ class MachineCom(object): self._serial = None self._currentFile = None self._status_timer = None + self._pool_status_queued = False self._acc_line_buffer = [] self._cmd = None self._pauseWaitStartTime = None @@ -112,7 +113,6 @@ class MachineCom(object): self._sending_active = True self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread") self.sending_thread.daemon = True - self.sending_thread.start() def _monitor_loop(self): #Open the serial port. @@ -130,7 +130,6 @@ class MachineCom(object): break if line.strip() is not "": self._timeout = get_new_timeout("communication") - # parse line depending on state self._state_parse_dict[self._state](line) except: self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam") @@ -142,23 +141,19 @@ class MachineCom(object): self._log("Connection closed, closing down monitor") def _send_loop(self): - # first wait until serial connection is established - self._send_event.wait() - self._send_event.clear() - while self._sending_active: try: if self._real_time_commands['poll_status']: - self.sendCommand('?') + self._sendCommand('?') self._real_time_commands['poll_status']=False elif self._real_time_commands['feed_hold']: - self.sendCommand('!') + self._sendCommand('!') self._real_time_commands['feed_hold']=False elif self._real_time_commands['cycle_start']: - self.sendCommand('~') + self._sendCommand('~') self._real_time_commands['cycle_start']=False elif self._real_time_commands['soft_reset']: - self.sendCommand(b'\x18') + self._sendCommand(b'\x18') self._real_time_commands['soft_reset']=False if self.isPrinting() and self._commandQueue.empty(): @@ -167,7 +162,7 @@ class MachineCom(object): self.sendCommand(cmd) self._sendCommand() - self._send_event.wait(1) + self._send_event.wait(0.5) self._send_event.clear() except: self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam") @@ -177,20 +172,31 @@ class MachineCom(object): self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - def _sendCommand(self): - if self._cmd is None and self._commandQueue.empty(): - return - elif self._cmd is None: - self._cmd = self._commandQueue.get() - if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-10: - self._log("Send: %s" % self._cmd) - self._acc_line_buffer.append(self._cmd) + def _sendCommand(self, cmd=None): + if cmd is None: + if self._cmd is None and self._commandQueue.empty(): + return + elif self._cmd is None: + self._cmd = self._commandQueue.get() + if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE: + self._log("Send: %s" % self._cmd) + self._acc_line_buffer.append(self._cmd) + try: + self._serial.write(self._cmd + '\n') + # trigger "sent" phase and use up one "ok" + self._process_command_phase("sent", self._cmd) + self._cmd = None + self._send_event.set() + except serial.SerialException: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + else: + self._log("Send: %s" % cmd) + self._acc_line_buffer.append(cmd) try: - self._serial.write(self._cmd + '\n') - # trigger "sent" phase and use up one "ok" - self._process_command_phase("sent", self._cmd) - self._cmd = None - self._commandQueue.task_done() + self._serial.write(cmd + '\n') + self._process_command_phase("sent", cmd) except serial.SerialException: self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() @@ -251,6 +257,8 @@ class MachineCom(object): if('ok' in ret or 'error' in ret): if(len(self._acc_line_buffer) > 0): #print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0]) + if self._acc_line_buffer[0] == '?': + self._pool_status_queued=False del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok' self._send_event.set() except serial.SerialException: @@ -295,7 +303,6 @@ class MachineCom(object): # self._writeGrblVersionToFile(versionDict) # if self._compareGrblVersion(versionDict) is False: # self._flashGrbl() - self._send_event.set() self._onConnected(self.STATE_LOCKED) def _state_locked_handle(self, line): @@ -434,6 +441,8 @@ class MachineCom(object): else: self._changeState(nextState) + self.sending_thread.start() + payload = dict(port=self._port, baudrate=self._baudrate) eventManager().fire(Events.CONNECTED, payload) @@ -452,8 +461,10 @@ class MachineCom(object): def _poll_status(self): if self.isOperational(): - self._real_time_commands['poll_status']=True - self._send_event.set() + if self._pool_status_queued is False: + self._real_time_commands['poll_status']=True + self._pool_status_queued = True + self._send_event.set() def _log(self, message): self._callback.on_comm_log(message) @@ -645,7 +656,6 @@ class MachineCom(object): self.sendCommand('M5') self.sendCommand('G0X0Y0') self.sendCommand('M9') - self._commandQueue.join() self._changeState(self.STATE_OPERATIONAL) @@ -671,12 +681,14 @@ class MachineCom(object): if self._pauseWaitStartTime: self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) self._pauseWaitStartTime = None - self.sendCommand('~') + self._real_time_commands['cycle_start']=True + self._send_event.set() eventManager().fire(Events.PRINT_RESUMED, payload) elif pause and self.isPrinting(): if not self._pauseWaitStartTime: self._pauseWaitStartTime = time.time() - self.sendCommand('!') + self._real_time_commands['feed_hold']=True + self._send_event.set() eventManager().fire(Events.PRINT_PAUSED, payload) def getStateString(self): From 8fed6d5d7ebee95f38c8aadb887cd537271c0e8e Mon Sep 17 00:00:00 2001 From: make-ing Date: Thu, 22 Oct 2015 15:21:52 +0200 Subject: [PATCH 13/14] changed a lot of stuff --- src/octoprint/events.py | 3 +- src/octoprint/util/comm_acc2.py | 196 +++++++++++++++++++++++--------- 2 files changed, 147 insertions(+), 52 deletions(-) diff --git a/src/octoprint/events.py b/src/octoprint/events.py index f91720a7..4bf3a7a0 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -86,9 +86,10 @@ class Events(object): # Settings SETTINGS_UPDATED = "SettingsUpdated" - + # GRBL LIMITS_HIT = "LimitsHit" + SOFT_RESET = "Soft-Reset" RT_STATE = "RealTimeState" diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 40ca341d..7e12ff80 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -65,6 +65,7 @@ class MachineCom(object): self.RX_BUFFER_SIZE = 127 self._state = self.STATE_NONE + self._grbl_state = None self._errorValue = "Unknown Error" self._serial = None self._currentFile = None @@ -76,6 +77,7 @@ class MachineCom(object): self._pauseWaitTimeLost = 0.0 self._commandQueue = Queue.Queue() self._send_event = CountedEvent(max=20) + self._finished_currentFile = False # regular expressions self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])") @@ -89,20 +91,6 @@ class MachineCom(object): self._pluginManager = octoprint.plugin.plugin_manager() self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory") - self._state_parse_dict = {self.STATE_NONE:self._state_none_handle, - self.STATE_OPEN_SERIAL:self._state_none_handle, - self.STATE_DETECT_SERIAL:self._state_none_handle, - self.STATE_CONNECTING:self._state_connecting_handle, - self.STATE_OPERATIONAL:self._state_operational_handle, - self.STATE_PRINTING:self._state_printing_handle, - self.STATE_PAUSED:self._state_paused_handle, - self.STATE_CLOSED:self._state_none_handle, - self.STATE_ERROR:self._state_none_handle, - self.STATE_CLOSED_WITH_ERROR:self._state_none_handle, - self.STATE_LOCKED:self._state_locked_handle, - self.STATE_HOMING:self._state_homing_handle, - self.STATE_FLASHING:self._state_none_handle} - # monitoring thread self._monitoring_active = True self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread") @@ -130,7 +118,18 @@ class MachineCom(object): break if line.strip() is not "": self._timeout = get_new_timeout("communication") - self._state_parse_dict[self._state](line) + if line.startswith('<'): # status report + self._handle_status_report(line) + elif line.startswith('ok'): # ok message :) + self._handle_ok_message() + elif line.startswith('err'): # error message + self._handle_error_message(line) + elif line.startswith('ALA'): # ALARM message + self._handle_alarm_message(line) + elif line.startswith('['): # feedback message + self._handle_feedback_message(line) + elif line.startswith('Grb'): # Grbl startup message + self._handle_startup_message() except: self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam") errorMsg = "See octoprint.log for details" @@ -143,23 +142,14 @@ class MachineCom(object): def _send_loop(self): while self._sending_active: try: - 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 - + self._process_rt_commands() if self.isPrinting() and self._commandQueue.empty(): cmd = self._getNext() if cmd is not None: self.sendCommand(cmd) + self._callback.on_comm_progress() + elif len(self._acc_line_buffer) == 0: + self._set_print_finished() self._sendCommand() self._send_event.wait(0.5) @@ -178,7 +168,7 @@ class MachineCom(object): return elif self._cmd is None: self._cmd = self._commandQueue.get() - if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE: + if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-1: self._log("Send: %s" % self._cmd) self._acc_line_buffer.append(self._cmd) try: @@ -202,6 +192,20 @@ class MachineCom(object): self._errorValue = get_exception_string() self.close(True) + def _process_rt_commands(self): + 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 + def _openSerial(self): def default(_, port, baudrate, read_timeout): if port is None or port == 'AUTO': @@ -275,22 +279,91 @@ class MachineCom(object): 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) + if self._finished_currentFile is False: + line = self._currentFile.getNext() + if line is None: + self._finished_currentFile = True + return line + else: + return None - self.sendCommand("M5") - self.sendCommand("G0X0Y0") - self.sendCommand("M9") - return line + def _set_print_finished(self): + self._callback.on_comm_print_job_done() + self._changeState(self.STATE_OPERATIONAL) + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation(), + "time": self.getPrintTime() + } + eventManager().fire(Events.PRINT_DONE, payload) + self.sendCommand("M5") + self.sendCommand("G0X0Y0") + self.sendCommand("M9") + + def _handle_status_report(self, line): + self._grbl_state = line[1:].split(',')[0] + self._update_grbl_pos(line) + + def _handle_ok_message(self): + if self._state == self.STATE_HOMING: + self._changeState(self.STATE_OPERATIONAL) + + def _handle_error_message(self, line): + self._errorValue = line + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._changeState(self.STATE_LOCKED) + + def _handle_alarm_message(self, line): + if "Hard/soft limit" in line: + errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()}) + elif "Abort during cycle" in line: + errorMsg = "Soft-reset detected. Please reset the machine and do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + eventManager().fire(Events.SOFT_RESET, {"error": self.getErrorString()}) + elif "Probe fail" in line: + errorMsg = "Probing has failed. Please reset the machine and do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._openSerial() + self._changeState(self.STATE_CONNECTING) + + def _handle_feedback_message(self, line): + if line[1:].startswith('Res'): # [Reset to continue] + pass + elif line[1:].startswith('\'$H'): # ['$H'|'$X' to unlock] + pass + elif line[1:].startswith('Cau'): # [Caution: Unlocked] + pass + elif line[1:].startswith('Ena'): # [Enabled] + pass + elif line[1:].startswith('Dis'): # [Disabled] + pass + + def _handle_startup_message(self): + if self.isOperational(): + errorMsg = "Machine reset." + self._cmd = None + self._acc_line_buffer = [] + self._pauseWaitStartTime = None + self._pauseWaitTimeLost = 0.0 + self._pool_status_queued = False + self._send_event.clear(completely=True) + with self._commandQueue.mutex: + self._commandQueue.queue.clear() + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_LOCKED) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + else: + self._onConnected(self.STATE_LOCKED) def _state_none_handle(self, line): pass @@ -414,7 +487,7 @@ 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 = RepeatedTimer(0.5, self._poll_status) self._status_timer.start() elif newState == self.STATE_OPERATIONAL or newState == self.STATE_PAUSED: if self._status_timer is not None: @@ -441,7 +514,8 @@ class MachineCom(object): else: self._changeState(nextState) - self.sending_thread.start() + if not self.sending_thread.isAlive(): + self.sending_thread.start() payload = dict(port=self._port, baudrate=self._baudrate) eventManager().fire(Events.CONNECTED, payload) @@ -466,6 +540,11 @@ class MachineCom(object): self._pool_status_queued = True self._send_event.set() + def _soft_reset(self): + if self.isOperational(): + self._real_time_commands['soft_reset']=True + self._send_event.set() + def _log(self, message): self._callback.on_comm_log(message) self._serialLogger.debug(message) @@ -620,7 +699,7 @@ class MachineCom(object): self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) def startPrint(self): - if not self.isOperational() or self.isPrinting(): + if not self.isOperational(): return if self._currentFile is None: @@ -631,6 +710,7 @@ class MachineCom(object): self.sendCommand("M08") self._currentFile.start() + self._finished_currentFile = False payload = { "file": self._currentFile.getFilename(), @@ -653,11 +733,9 @@ class MachineCom(object): with self._commandQueue.mutex: self._commandQueue.queue.clear() - self.sendCommand('M5') - self.sendCommand('G0X0Y0') - self.sendCommand('M9') + self._soft_reset() - self._changeState(self.STATE_OPERATIONAL) + self._changeState(self.STATE_LOCKED) payload = { "file": self._currentFile.getFilename(), @@ -720,6 +798,22 @@ class MachineCom(object): return "Flashing" return "?%d?" % (self._state) + def getPrintProgress(self): + if self._currentFile is None: + return None + return self._currentFile.getProgress() + + def getPrintFilepos(self): + if self._currentFile is None: + return None + return self._currentFile.getFilepos() + + def getCleanedPrintTime(self): + printTime = self.getPrintTime() + if printTime is None: + return None + return printTime + def getConnection(self): return self._port, self._baudrate From c30fff811808547df1dd24d72d1ca52924d9bd73 Mon Sep 17 00:00:00 2001 From: make-ing Date: Thu, 22 Oct 2015 17:15:09 +0200 Subject: [PATCH 14/14] fixed status report bug! Grbl does not send ok for realtime commandsgit add .! if so, then you probably send a \n with it. --- src/octoprint/util/comm_acc2.py | 51 ++++++--------------------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py index 7e12ff80..cab692b4 100644 --- a/src/octoprint/util/comm_acc2.py +++ b/src/octoprint/util/comm_acc2.py @@ -70,7 +70,6 @@ class MachineCom(object): self._serial = None self._currentFile = None self._status_timer = None - self._pool_status_queued = False self._acc_line_buffer = [] self._cmd = None self._pauseWaitStartTime = None @@ -183,9 +182,8 @@ class MachineCom(object): self.close(True) else: self._log("Send: %s" % cmd) - self._acc_line_buffer.append(cmd) try: - self._serial.write(cmd + '\n') + self._serial.write(cmd) self._process_command_phase("sent", cmd) except serial.SerialException: self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) @@ -261,8 +259,6 @@ class MachineCom(object): if('ok' in ret or 'error' in ret): if(len(self._acc_line_buffer) > 0): #print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0]) - if self._acc_line_buffer[0] == '?': - self._pool_status_queued=False del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok' self._send_event.set() except serial.SerialException: @@ -303,6 +299,12 @@ class MachineCom(object): def _handle_status_report(self, line): self._grbl_state = line[1:].split(',')[0] + if self._grbl_state == 'Queue': + if not self.isPaused(): + self.setPause(True) + elif self._grbl_state == 'Run': + if self.isPaused(): + self.setPause(False) self._update_grbl_pos(line) def _handle_ok_message(self): @@ -354,7 +356,6 @@ class MachineCom(object): self._acc_line_buffer = [] self._pauseWaitStartTime = None self._pauseWaitTimeLost = 0.0 - self._pool_status_queued = False self._send_event.clear(completely=True) with self._commandQueue.mutex: self._commandQueue.queue.clear() @@ -365,38 +366,6 @@ class MachineCom(object): else: self._onConnected(self.STATE_LOCKED) - def _state_none_handle(self, line): - pass - - def _state_connecting_handle(self, line): - if line.startswith("Grbl"): - #versionMatch = re.search("Grbl (?P.+?)(_(?P[0-9a-f]{7})(?P-dirty)?)? \[.+\]", line) - #if(versionMatch): - # versionDict = versionMatch.groupdict() - # self._writeGrblVersionToFile(versionDict) - # if self._compareGrblVersion(versionDict) is False: - # self._flashGrbl() - self._onConnected(self.STATE_LOCKED) - - def _state_locked_handle(self, line): - pass - - def _state_homing_handle(self, line): - if line.startswith("ok"): - self._changeState(self.STATE_OPERATIONAL) - - def _state_operational_handle(self, line): - if line.startswith("<"): - self._update_grbl_pos(line) - - def _state_printing_handle(self, line): - if line.startswith("<"): - self._update_grbl_pos(line) - - def _state_paused_handle(self, line): - if line.startswith("<"): - self._update_grbl_pos(line) - def _update_grbl_pos(self, line): # line example: # @@ -535,10 +504,8 @@ class MachineCom(object): def _poll_status(self): if self.isOperational(): - if self._pool_status_queued is False: - self._real_time_commands['poll_status']=True - self._pool_status_queued = True - self._send_event.set() + self._real_time_commands['poll_status']=True + self._send_event.set() def _soft_reset(self): if self.isOperational():