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