From 7f2476e51313185e38a1ff1cc9a7055692d41e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 11 Aug 2015 12:02:26 +0200 Subject: [PATCH] Better tracking of printer connection state Introduced three new events: * CONNECTING - fired just before starting the connection process * DISCONNECTING - fired just before starting the (active) disconnection process * PRINTER_STATE_CHANGED - fired every time the printer state changes Also introduced new GCODE script beforePrinterDisconnected, which will get sent just before the printer gets (actively) disconnected. Also enabled communication object to wait for the send queue to be emptied before closing it, in order to allow sending all lines from the disconnect script. --- docs/events/index.rst | 17 ++ docs/features/gcode_scripts.rst | 4 + src/octoprint/events.py | 5 + src/octoprint/printer/__init__.py | 25 +++ src/octoprint/printer/standard.py | 31 +++- src/octoprint/server/api/settings.py | 1 + .../static/js/app/viewmodels/settings.js | 5 +- .../dialogs/settings/gcodescripts.jinja2 | 7 + src/octoprint/util/comm.py | 172 +++++++++++------- 9 files changed, 194 insertions(+), 73 deletions(-) diff --git a/docs/events/index.rst b/docs/events/index.rst index 1dfde900..70aa5d2f 100644 --- a/docs/events/index.rst +++ b/docs/events/index.rst @@ -106,6 +106,9 @@ ClientClosed Printer communication --------------------- +Connecting + The server is attempting to connect to the printer. + Connected The server has connected to the printer. @@ -114,6 +117,11 @@ Connected * ``port``: the connected serial port * ``baudrate``: the baud rate +Disconnecting + The server is going to disconnect from the printer. Note that this + event might not always be sent when the server and printer get disconnected + from each other. Do not depend on this for critical life cycle management. + Disconnected The server has disconnected from the printer @@ -124,6 +132,15 @@ Error * ``error``: the error string +PrinterStateChanged + The state of the printer changed. + + Payload: + + * ``state_id``: Id of the new state. See + :func:`~octoprint.printer.PrinterInterface.get_state_id` for possible values. + * ``state_string``: Text representation of the new state. + File handling ------------- diff --git a/docs/features/gcode_scripts.rst b/docs/features/gcode_scripts.rst index 011e15c3..22ad482b 100644 --- a/docs/features/gcode_scripts.rst +++ b/docs/features/gcode_scripts.rst @@ -26,6 +26,10 @@ Predefined Scripts The following GCODE scripts are sent by OctoPrint automatically: * ``afterPrinterConnected``: Sent after OctoPrint successfully connected to a printer. Defaults to an empty script. + * ``beforePrinterDisconnected``: Sent just before OctoPrint (actively) closes the connection to the printer. Defaults + to an empty script. Note that this will *not* be sent for unexpected connection cut offs, e.g. in case of errors + on the serial line, only when the user clicks the "Disconnect" button or the printer requests a disconnect via an + :ref:`action command ` . * ``beforePrintStarted``: Sent just before a print job is started. Defaults to an empty script. * ``afterPrintCancelled``: Sent just after a print job was cancelled. Defaults to the :ref:`bundled script listed below `. diff --git a/src/octoprint/events.py b/src/octoprint/events.py index 2cc765d9..33fc04cb 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -28,9 +28,14 @@ class Events(object): STARTUP = "Startup" # connect/disconnect to printer + CONNECTING = "Connecting" CONNECTED = "Connected" + DISCONNECTING = "Disconnecting" DISCONNECTED = "Disconnected" + # State changes + PRINTER_STATE_CHANGED = "PrinterStateChanged" + # connect/disconnect by client CLIENT_OPENED = "ClientOpened" CLIENT_CLOSED = "ClientClosed" diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index 59265604..fffd168f 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -259,6 +259,31 @@ class PrinterInterface(object): """ raise NotImplementedError() + def get_state_id(self): + """ + Identifier of the current communication state. + + Possible values are: + + * OPEN_SERIAL + * DETECT_SERIAL + * DETECT_BAUDRATE + * CONNECTING + * OPERATIONAL + * PRINTING + * PAUSED + * CLOSED + * ERROR + * CLOSED_WITH_ERROR + * TRANFERING_FILE + * OFFLINE + * UNKNOWN + * NONE + + Returns: + (str) A unique identifier corresponding to the current communication state. + """ + def get_current_data(self): """ Returns: diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index c332182c..c89b3b12 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -191,7 +191,9 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): will be attempted. """ if self._comm is not None: - self._comm.close() + self.disconnect() + + eventManager().fire(Events.CONNECTING) self._printerProfileManager.select(profile) self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager) @@ -199,11 +201,11 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): """ Closes the connection to the printer. """ + eventManager().fire(Events.DISCONNECTING) if self._comm is not None: self._comm.close() - self._comm = None - self._printerProfileManager.deselect() - eventManager().fire(Events.DISCONNECTED) + else: + eventManager().fire(Events.DISCONNECTED) def get_transport(self): @@ -426,14 +428,17 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): payload["origin"] = FileDestinations.SDCARD eventManager().fire(Events.PRINT_FAILED, payload) - def get_state_string(self): - """ - Returns a human readable string corresponding to the current communication state. - """ + def get_state_string(self, state=None): if self._comm is None: return "Offline" else: - return self._comm.getStateString() + return self._comm.getStateString(state=state) + + def get_state_id(self, state=None): + if self._comm is None: + return "OFFLINE" + else: + return self._comm.getStateId(state=state) def get_current_data(self): return self._stateMonitor.get_current_data() @@ -561,6 +566,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._state = state self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) + payload = dict( + state_id=self.get_state_id(self._state), + state_string=self.get_state_string(self._state) + ) + eventManager().fire(Events.PRINTER_STATE_CHANGED, payload) + def _addLog(self, log): self._log.append(log) self._stateMonitor.add_log(log) @@ -775,6 +786,8 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._setProgressData(0, None, None, None) self._setCurrentZ(None) self._setJobData(None, None, None) + self._printerProfileManager.deselect() + eventManager().fire(Events.DISCONNECTED) self._setState(state) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 91728c1c..2e6c07ee 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -105,6 +105,7 @@ def getSettings(): "scripts": { "gcode": { "afterPrinterConnected": None, + "beforePrinterDisconnected": None, "beforePrintStarted": None, "afterPrintCancelled": None, "afterPrintDone": None, diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 1c5a42c4..e6c94354 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -145,6 +145,7 @@ $(function() { self.scripts_gcode_afterPrintPaused = ko.observable(undefined); self.scripts_gcode_beforePrintResumed = ko.observable(undefined); self.scripts_gcode_afterPrinterConnected = ko.observable(undefined); + self.scripts_gcode_beforePrinterDisconnected = ko.observable(undefined); self.temperature_profiles = ko.observableArray(undefined); self.temperature_cutoff = ko.observable(undefined); @@ -440,6 +441,7 @@ $(function() { self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused); self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed); self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected); + self.scripts_gcode_beforePrinterDisconnected(response.scripts.gcode.beforePrinterDisconnected); self.temperature_profiles(response.temperature.profiles); self.temperature_cutoff(response.temperature.cutoff); @@ -535,7 +537,8 @@ $(function() { "afterPrintCancelled": self.scripts_gcode_afterPrintCancelled(), "afterPrintPaused": self.scripts_gcode_afterPrintPaused(), "beforePrintResumed": self.scripts_gcode_beforePrintResumed(), - "afterPrinterConnected": self.scripts_gcode_afterPrinterConnected() + "afterPrinterConnected": self.scripts_gcode_afterPrinterConnected(), + "beforePrinterDisconnected": self.scripts_gcode_beforePrinterDisconnected() } }, "server": { diff --git a/src/octoprint/templates/dialogs/settings/gcodescripts.jinja2 b/src/octoprint/templates/dialogs/settings/gcodescripts.jinja2 index 024907ff..6fc08268 100644 --- a/src/octoprint/templates/dialogs/settings/gcodescripts.jinja2 +++ b/src/octoprint/templates/dialogs/settings/gcodescripts.jinja2 @@ -35,4 +35,11 @@ +
+ +
+ +
+ {{ _('This will only be executed when closing the connection actively. If the connection to the printer is suddenly lost nothing will be sent.') }} +
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 3e239076..b844cb8a 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -271,37 +271,51 @@ class MachineCom(object): def getState(self): return self._state - def getStateString(self): - if self._state == self.STATE_NONE: + def getStateId(self, state=None): + if state is None: + state = self._state + + possible_states = filter(lambda x: x.startswith("STATE_"), self.__class__.__dict__.keys()) + for possible_state in possible_states: + if getattr(self, possible_state) == state: + return possible_state[len("STATE_"):] + + return "UNKNOWN" + + def getStateString(self, state=None): + if state is None: + state = self._state + + if state == self.STATE_NONE: return "Offline" - if self._state == self.STATE_OPEN_SERIAL: + if state == self.STATE_OPEN_SERIAL: return "Opening serial port" - if self._state == self.STATE_DETECT_SERIAL: + if state == self.STATE_DETECT_SERIAL: return "Detecting serial port" - if self._state == self.STATE_DETECT_BAUDRATE: + if state == self.STATE_DETECT_BAUDRATE: return "Detecting baudrate" - if self._state == self.STATE_CONNECTING: + if state == self.STATE_CONNECTING: return "Connecting" - if self._state == self.STATE_OPERATIONAL: + if state == self.STATE_OPERATIONAL: return "Operational" - if self._state == self.STATE_PRINTING: + if state == self.STATE_PRINTING: if self.isSdFileSelected(): return "Printing from SD" elif self.isStreaming(): return "Sending file to SD" else: return "Printing" - if self._state == self.STATE_PAUSED: + if state == self.STATE_PAUSED: return "Paused" - if self._state == self.STATE_CLOSED: + if state == self.STATE_CLOSED: return "Closed" - if self._state == self.STATE_ERROR: + if state == self.STATE_ERROR: return "Error: %s" % (self.getErrorString()) - if self._state == self.STATE_CLOSED_WITH_ERROR: + if state == self.STATE_CLOSED_WITH_ERROR: return "Error: %s" % (self.getErrorString()) - if self._state == self.STATE_TRANSFERING_FILE: + if state == self.STATE_TRANSFERING_FILE: return "Transfering file to SD" - return "?%d?" % (self._state) + return "Unknown State (%d)" % (self._state) def getErrorString(self): return self._errorValue @@ -382,7 +396,25 @@ class MachineCom(object): ##~~ external interface - def close(self, isError = False): + def close(self, is_error=False, wait=True, *args, **kwargs): + """ + Closes the connection to the printer. + + If ``is_error`` is False, will attempt to send the ``beforePrinterDisconnected`` + gcode script. If ``is_error`` is False and ``wait`` is True, will wait + until all messages in the send queue (including the ``beforePrinterDisconnected`` + gcode script) have been sent to the printer. + + Arguments: + is_error (bool): Whether the closing takes place due to an error (True) + or not (False, default) + wait (bool): Whether to wait for all messages in the send + queue to be processed before closing (True, default) or not (False) + """ + + # legacy parameters + is_error = kwargs.get("isError", is_error) + if self._temperature_timer is not None: try: self._temperature_timer.cancel() @@ -395,16 +427,25 @@ class MachineCom(object): except: pass - self._monitoring_active = False - self._send_queue_active = False + def deactivate_monitoring_and_send_queue(): + self._monitoring_active = False + self._send_queue_active = False printing = self.isPrinting() or self.isPaused() if self._serial is not None: - if isError: + if not is_error: + self.sendGcodeScript("beforePrinterDisconnected") + if wait: + self._send_queue.join() + + deactivate_monitoring_and_send_queue() + self._serial.close() + if is_error: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) - self._serial.close() + else: + deactivate_monitoring_and_send_queue() self._serial = None if settings().getBoolean(["feature", "sdSupport"]): @@ -419,7 +460,6 @@ class MachineCom(object): "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_FAILED, payload) - eventManager().fire(Events.DISCONNECTED) def setTemperatureOffset(self, offsets): self._tempOffsets.update(offsets) @@ -1055,7 +1095,7 @@ class MachineCom(object): except: self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string())) else: - self.close() + self.close(wait=False) self._errorValue = "No more baudrates to test, and no suitable baudrate found." self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) @@ -1072,7 +1112,7 @@ class MachineCom(object): elif "ok" in line: self._onConnected() elif time.time() > self._timeout: - self.close() + self.close(wait=False) ### Operational elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: @@ -1324,7 +1364,7 @@ class MachineCom(object): except: self._log("Unexpected error while reading serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() - self.close(True) + self.close(is_error=True) return None if ret == '': #self._log("Recv: TIMEOUT") @@ -1509,52 +1549,58 @@ class MachineCom(object): # wait until we have something in the queue entry = self._send_queue.get() - # make sure we are still active - if not self._send_queue_active: - break + try: + # make sure we are still active + if not self._send_queue_active: + break - # fetch command and optional linenumber from queue - command, linenumber, command_type = entry + # fetch command and optional linenumber from queue + command, linenumber, command_type = entry - # some firmwares (e.g. Smoothie) might support additional in-band communication that will not - # stick to the acknowledgement behaviour of GCODE, so we check here if we have a GCODE command - # at hand here and only clear our clear_to_send flag later if that's the case - gcode = self.gcode_command_for_cmd(command) + # some firmwares (e.g. Smoothie) might support additional in-band communication that will not + # stick to the acknowledgement behaviour of GCODE, so we check here if we have a GCODE command + # at hand here and only clear our clear_to_send flag later if that's the case + gcode = self.gcode_command_for_cmd(command) - if linenumber is not None: - # line number predetermined - this only happens for resends, so we'll use the number and - # send directly without any processing (since that already took place on the first sending!) - self._doSendWithChecksum(command, linenumber) - - else: - # trigger "sending" phase - command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode) - - if command is None: - # so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue - continue - - # now comes the part where we increase line numbers and send stuff - no turning back now - if (gcode is not None or self._sendChecksumWithUnknownCommands) and (self.isPrinting() or self._alwaysSendChecksum): - linenumber = self._currentLine - self._addToLastLines(command) - self._currentLine += 1 + if linenumber is not None: + # line number predetermined - this only happens for resends, so we'll use the number and + # send directly without any processing (since that already took place on the first sending!) self._doSendWithChecksum(command, linenumber) + else: - self._doSendWithoutChecksum(command) + # trigger "sending" phase + command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode) - # trigger "sent" phase and use up one "ok" - self._process_command_phase("sent", command, command_type, gcode=gcode) + if command is None: + # so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue + continue - # we only need to use up a clear if the command we just sent was either a gcode command or if we also - # require ack's for unknown commands - use_up_clear = self._unknownCommandsNeedAck - if gcode is not None: - use_up_clear = True + # now comes the part where we increase line numbers and send stuff - no turning back now + if (gcode is not None or self._sendChecksumWithUnknownCommands) and (self.isPrinting() or self._alwaysSendChecksum): + linenumber = self._currentLine + self._addToLastLines(command) + self._currentLine += 1 + self._doSendWithChecksum(command, linenumber) + else: + self._doSendWithoutChecksum(command) - # if we need to use up a clear, do that now - if use_up_clear: - self._clear_to_send.clear() + # trigger "sent" phase and use up one "ok" + self._process_command_phase("sent", command, command_type, gcode=gcode) + + # we only need to use up a clear if the command we just sent was either a gcode command or if we also + # require ack's for unknown commands + use_up_clear = self._unknownCommandsNeedAck + if gcode is not None: + use_up_clear = True + + # if we need to use up a clear, do that now + if use_up_clear: + self._clear_to_send.clear() + + finally: + # no matter _how_ we exit this block, we signal that we + # are done processing the last fetched queue entry + self._send_queue.task_done() # now we just wait for the next clear and then start again self._clear_to_send.wait() @@ -1645,12 +1691,12 @@ class MachineCom(object): self._logger.exception("Unexpected error while writing to serial port") self._log("Unexpected error while writing to serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() - self.close(True) + self.close(is_error=True) except: self._logger.exception("Unexpected error while writing to serial port") self._log("Unexpected error while writing to serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() - self.close(True) + self.close(is_error=True) ##~~ command handlers