From 4c1ba0fd0b2e13bd539181d0c51a55cf8ccd7535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 16:10:23 +0200 Subject: [PATCH 1/6] [doc] => --- src/octoprint/plugin/types.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 6349781c..aa4b8fe2 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -181,7 +181,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): The right part of the navigation bar located at the top of the UI can be enriched with additional links. Note that with the current implementation, plugins will always be located *to the left* of the existing links. - The included template must be called ``_navbar.jinja2`` (e.g. ``myplugin_navbar.jinja2``) unless + The included template must be called ``_navbar.jinja2`` (e.g. ``myplugin_navbar.jinja2``) unless overridden by the configuration supplied through :func:`get_template_configs`. The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The @@ -192,7 +192,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): The left side bar containing Connection, State and Files sections can be enriched with additional sections. Note that with the current implementations, plugins will always be located *beneath* the existing sections. - The included template must be called ``_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``) unless + The included template must be called ``_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``) unless overridden by the configuration supplied through :func:`get_template_configs`. The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The @@ -204,7 +204,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): plugins. Note that with the current implementation, plugins will always be located *to the right* of the existing tabs. - The included template must be called ``_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``) unless + The included template must be called ``_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``) unless overridden by the configuration supplied through :func:`get_template_configs`. The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The @@ -216,7 +216,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): will always be listed beneath the "Plugins" header in the settings link list, ordered alphabetically after their displayed name. - The included template must be called ``_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``) unless + The included template must be called ``_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``) unless overridden by the configuration supplied through :func:`get_template_configs`. The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. The @@ -272,11 +272,11 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): * - template - Name of the template to inject, default value depends on the ``type``: - * ``navbar``: ``_navbar.jinja2`` - * ``sidebar``: ``_sidebar.jinja2`` - * ``tab``: ``_tab.jinja2`` - * ``settings``: ``_settings.jinja2`` - * ``generic``: ``.jinja2`` + * ``navbar``: ``_navbar.jinja2`` + * ``sidebar``: ``_sidebar.jinja2`` + * ``tab``: ``_tab.jinja2`` + * ``settings``: ``_settings.jinja2`` + * ``generic``: ``.jinja2`` * - suffix - Suffix to attach to the component identifier and the div identifier of the injected template. Will be @@ -311,7 +311,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): (and the divs will be ``tab_plugin_myplugin_1st`` and ``tab_plugin_myplugin_2nd``). * - div - - Id for the div containing the component. If not provided, defaults to ``_plugin_`` plus + - Id for the div containing the component. If not provided, defaults to ``_plugin_`` plus the ``suffix`` if provided or required. * - replaces - Id of the component this one replaces, might be either one of the core components or a component @@ -370,7 +370,7 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): .. note:: As already outlined above, each template type has a default template name (i.e. the default navbar template - of a plugin is called ``_navbar.jinja2``), which may be overridden using the template configuration. + of a plugin is called ``_navbar.jinja2``), which may be overridden using the template configuration. If a plugin needs to include more than one template of a given type, it needs to provide an entry for each of those, since the implicit default template will only be included automatically if no other templates of that type are defined. From 57651804628dad62d011738454d12e9771db56ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 17:15:57 +0200 Subject: [PATCH 2/6] Fix: moved line number incrementing to send loop Should fix issues where too fast a temperature polling could cause skipped line numbers, triggering nasty resend loops in the process. --- src/octoprint/util/comm.py | 171 ++++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 70 deletions(-) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 88f0e098..f1155bb1 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -420,7 +420,7 @@ class MachineCom(object): if self.isPrinting() and not self.isSdFileSelected(): self._commandQueue.put((cmd, cmd_type)) elif self.isOperational(): - self._sendCommand((cmd, cmd_type)) + self._sendCommand(cmd, cmd_type=cmd_type) def sendGcodeScript(self, scriptName, replacements=None): context = dict() @@ -520,7 +520,7 @@ class MachineCom(object): self.sendCommand(line) # now make sure we actually do something, up until now we only filled up the queue - self._sendFromQueue(sendChecksum=True) + self._sendFromQueue() except: self._logger.exception("Error while trying to start printing") self._errorValue = get_exception_string() @@ -621,7 +621,7 @@ class MachineCom(object): self.sendCommand(line) # now make sure we actually do something, up until now we only filled up the queue - self._sendFromQueue(sendChecksum=True) + self._sendFromQueue() eventManager().fire(Events.PRINT_RESUMED, payload) elif pause and self.isPrinting(): @@ -1074,7 +1074,7 @@ class MachineCom(object): self._resendNextCommand() else: - if self._sendFromQueue(sendChecksum=True): + if self._sendFromQueue(): pass elif not self.isSdPrinting(): self._sendNext() @@ -1164,9 +1164,18 @@ class MachineCom(object): eventManager().fire(Events.CONNECTED, payload) self.sendGcodeScript("afterPrinterConnected", replacements=dict(event=payload)) - def _sendFromQueue(self, sendChecksum=False): + def _sendFromQueue(self): if not self._commandQueue.empty() and not self.isStreaming(): - self._sendCommand(self._commandQueue.get(), sendChecksum) + entry = self._commandQueue.get() + if isinstance(entry, tuple): + if not len(entry) == 2: + return False + cmd, cmd_type = entry + else: + cmd = entry + cmd_type = None + + self._sendCommand(cmd, cmd_type=cmd_type) return True else: return False @@ -1325,7 +1334,7 @@ class MachineCom(object): with self._sendNextLock: line = self._getNext() if line is not None: - self._sendCommand(line, True) + self._sendCommand(line) self._callback.on_comm_progress() def _handleResendRequest(self, line): @@ -1385,64 +1394,53 @@ class MachineCom(object): self._lastResendNumber = None self._currentResendCount = 0 - def _sendCommand(self, cmd_tuple, sendChecksum=False): - if isinstance(cmd_tuple, tuple): - cmd, cmd_type = cmd_tuple - else: - cmd = cmd_tuple - cmd_type = None - + def _sendCommand(self, cmd, cmd_type=None): # Make sure we are only handling one sending job at a time with self._sendingLock: if self._serial is None: return if not self.isStreaming(): - for hook in self._gcode_hooks: - hook_cmd = self._gcode_hooks[hook](self, cmd, cmd_type=cmd_type, send_checksum=sendChecksum) + for name, hook in self._gcode_hooks.items(): + hook_cmd = hook(self, cmd, cmd_type=cmd_type) + + if hook_cmd is None: + cmd = None # hook might have returned (cmd, sendChecksum) or (cmd, sendChecksum, cmd_type), split that - if isinstance(hook_cmd, tuple): + elif isinstance(hook_cmd, tuple): if len(hook_cmd) == 2: hook_cmd, cmd_type = hook_cmd elif len(hook_cmd) == 3: - hook_cmd, cmd_type, sendChecksum = hook_cmd - - # hook might have returned None for cmd, if so stop processing, we won't send this command - # to the printer - if hook_cmd is None: - cmd = None - break + # legacy hook handler, ignore returned send_checksum + hook_cmd, cmd_type, _ = hook_cmd # if hook_cmd is a string, we'll replace cmd with it (it's been rewritten by the hook handler - elif isinstance(hook_cmd, basestring): + if isinstance(hook_cmd, basestring): cmd = hook_cmd + else: + self._logger.warn("Hook {name} returned unintelligible result, ignoring it: {hook_cmd!r}".format(**locals())) + continue + + if cmd is None: + break + # try to parse the cmd and extract the gcode type - gcode = self._regex_command.search(cmd) - if gcode: - gcode = gcode.group(1) + if cmd is not None: + gcode = self._regex_command.search(cmd) + if gcode: + gcode = gcode.group(1) - # fire events if necessary - if gcode in gcodeToEvent: - eventManager().fire(gcodeToEvent[gcode]) + # fire events if necessary + if gcode in gcodeToEvent: + eventManager().fire(gcodeToEvent[gcode]) - # send it through the specific handler if it exists - gcodeHandler = "_gcode_" + gcode - if hasattr(self, gcodeHandler): - cmd = getattr(self, gcodeHandler)(cmd) + # send it through the specific handler if it exists + cmd = self._process_command_phase("queue", cmd, gcode=gcode) if cmd is not None: - self._doSend(cmd, send_checksum=sendChecksum, cmd_type=cmd_type) - - def _doSend(self, cmd, send_checksum=False, cmd_type=None): - if send_checksum or self._alwaysSendChecksum: - lineNumber = self._currentLine - self._addToLastLines(cmd) - self._currentLine += 1 - self._enqueue_for_sending(cmd, linenumber=lineNumber, command_type=cmd_type) - else: - self._enqueue_for_sending(cmd, command_type=cmd_type) + self._enqueue_for_sending(cmd, command_type=cmd_type) def gcode_command_for_cmd(self, cmd): """ @@ -1497,20 +1495,54 @@ class MachineCom(object): if not self._send_queue_active: break - # fetch command and optional linenumber from queue, send it + # fetch command and optional linenumber from queue command, linenumber, _ = entry + + gcode_match = self._regex_command.search(command) + is_gcode = gcode_match is not None + + if is_gcode: + # trigger "sending" phase + command = self._process_command_phase("sending", command, gcode=gcode_match.group(1)) + if linenumber is not None: + # line number predetermined, use that self._doSendWithChecksum(command, linenumber) else: - self._doSendWithoutChecksum(command) + if self.isPrinting() or self._alwaysSendChecksum: + linenumber = self._currentLine + self._addToLastLines(command) + self._currentLine += 1 + self._doSendWithChecksum(command, linenumber) + else: + self._doSendWithoutChecksum(command) - # we just used up one ok, clear it, wait for the next clear + if is_gcode: + # trigger "sent" phase + self._process_command_phase("sent", command, gcode=gcode_match.group(1)) + + # wait for the next clear self._clear_to_send.clear() self._clear_to_send.wait() except: self._logger.exception("Caught an exception in the send loop") self._log("Closing down send loop") + def _process_command_phase(self, phase, command, gcode=None): + if gcode is None: + gcode_match = self._regex_command.search(command) + if gcode_match is None: + return command + + gcode = gcode.group(1) + + # send it through the specific handler if it exists + gcodeHandler = "_gcode_" + gcode + "_" + phase + if hasattr(self, gcodeHandler): + command = getattr(self, gcodeHandler)(command) + + return command + ##~~ actual sending via serial def _doSendWithChecksum(self, cmd, lineNumber): @@ -1538,13 +1570,13 @@ class MachineCom(object): ##~~ command handlers - def _gcode_T(self, cmd): + def _gcode_T_sent(self, cmd): toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: self._currentTool = int(toolMatch.group(1)) return cmd - def _gcode_G0(self, cmd): + def _gcode_G0_sent(self, cmd): if 'Z' in cmd: match = self._regex_paramZFloat.search(cmd) if match: @@ -1556,14 +1588,14 @@ class MachineCom(object): except ValueError: pass return cmd - _gcode_G1 = _gcode_G0 + _gcode_G0_sent = _gcode_G0_sent - def _gcode_M0(self, cmd): + def _gcode_M0_queue(self, cmd): self.setPause(True) - return "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. - _gcode_M1 = _gcode_M0 + return None # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + _gcode_M1_queued = _gcode_M0_queue - def _gcode_M104(self, cmd): + def _gcode_M104_sent(self, cmd): toolNum = self._currentTool toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: @@ -1581,7 +1613,7 @@ class MachineCom(object): pass return cmd - def _gcode_M140(self, cmd): + def _gcode_M140_sent(self, cmd): match = self._regex_paramSInt.search(cmd) if match: try: @@ -1595,19 +1627,19 @@ class MachineCom(object): pass return cmd - def _gcode_M109(self, cmd): + def _gcode_M109_sent(self, cmd): self._heatupWaitStartTime = time.time() self._blocking_command = True self._heating = True - return self._gcode_M104(cmd) + return self._gcode_M104_sent(cmd) - def _gcode_M190(self, cmd): + def _gcode_M190_sent(self, cmd): self._heatupWaitStartTime = time.time() self._blocking_command = True self._heating = True - return self._gcode_M140(cmd) + return self._gcode_M140_sent(cmd) - def _gcode_M110(self, cmd): + def _gcode_M110_sending(self, cmd): newLineNumber = None match = self._regex_paramNInt.search(cmd) if match: @@ -1619,20 +1651,19 @@ class MachineCom(object): newLineNumber = 0 # send M110 command with new line number - self._enqueue_for_sending(cmd, linenumber=newLineNumber) - self._currentLine = newLineNumber + 1 + self._currentLine = newLineNumber # after a reset of the line number we have no way to determine what line exactly the printer now wants self._lastLines.clear() self._resendDelta = None - return None + return cmd - def _gcode_M112(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum + def _gcode_M112_queue(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum self.cancelPrint() return cmd - def _gcode_G4(self, cmd): + def _gcode_G4_sent(self, cmd): # we are intending to dwell for a period of time, increase the timeout to match cmd = cmd.upper() p_idx = cmd.find('P') @@ -1648,12 +1679,12 @@ class MachineCom(object): self._blocking_command = True return cmd - def _gcode_G28(self, cmd): + def _gcode_G28_sending(self, cmd): self._blocking_command = True return cmd - _gcode_G29 = _gcode_G28 - _gcode_G30 = _gcode_G28 - _gcode_G32 = _gcode_G28 + _gcode_G29_sending = _gcode_G28_sending + _gcode_G30_sending = _gcode_G28_sending + _gcode_G32_sending = _gcode_G28_sending ### MachineCom callback ################################################################################################ From 4ea6bcd1de4d5ec3ebb565c5f24db1dd3b8a2d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 17:24:34 +0200 Subject: [PATCH 3/6] Smoothie compat fix: Only wait for ok's after GCODE commands Any unknown commands should just be piped through and not use up an acknowledgement. This new behaviour can be overridden via the new feature flag "unknownCommandsNeedAck", which will restore the former behaviour causing even unknown commands to use up an "ok". Also no line numbering or checksumming will happen. Shouldn't usually be of relevance for other firmwares, but in case of any compatibility issues introduced by this also added a new feature flag "sendChecksumWithNonGcode" that causes even unknown commands to be sent with a checksum if necessary. --- docs/configuration/config_yaml.rst | 10 +++++++++- src/octoprint/plugins/virtual_printer/virtual.py | 8 +++++--- src/octoprint/settings.py | 2 ++ src/octoprint/util/comm.py | 16 +++++++++++++--- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index 11981684..c8640400 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -373,10 +373,18 @@ Use the following settings to enable or disable OctoPrint features: # during connect. waitForStartOnConnect: false - # Specifies whether OctoPrint should send linenumber + checksum with every command. Needed for + # Specifies whether OctoPrint should send linenumber + checksum with every printer command. Needed for # successful communication with Repetier firmware alwaysSendChecksum: false + # Specifies whether OctoPrint should also send linenumber + checksum with commands that are *not* + # detected as valid GCODE (as in, they do not match the regular expression "^\s*([GM]\d+|T)"). + sendChecksumWithUnknownCommands: false + + # Specifies whether OctoPrint should also use up acknowledgments ("ok") for commands that are *not* + # detected as valid GCODE (as in, they do not match the regular expression "^\s*([GM]\d+|T)"). + unknownCommandsNeedAck: false + # Whether to ignore the first ok after a resend response. Needed for successful communication with # Repetier firmware swallowOkAfterResend: false diff --git a/src/octoprint/plugins/virtual_printer/virtual.py b/src/octoprint/plugins/virtual_printer/virtual.py index 8f54cb28..a94afa93 100644 --- a/src/octoprint/plugins/virtual_printer/virtual.py +++ b/src/octoprint/plugins/virtual_printer/virtual.py @@ -149,6 +149,11 @@ class VirtualPrinter(): self._sendOk() continue + if data.strip() == "version": + from octoprint._version import get_versions + self.outgoing.put("OctoPrint VirtualPrinter v" + get_versions()["version"]) + continue + if len(data.strip()) > 0 and self._okBeforeCommandOutput: self._sendOk() @@ -394,7 +399,6 @@ class VirtualPrinter(): pass if tool >= settings().getInt(["devel", "virtualPrinter", "numExtruders"]): - self._sendOk() return try: @@ -406,7 +410,6 @@ class VirtualPrinter(): self._waitForHeatup("tool%d" % tool) if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]): self.outgoing.put("TargetExtr%d:%d" % (tool, self.targetTemp[tool])) - self._sendOk() def _parseBedCommand(self, line): try: @@ -418,7 +421,6 @@ class VirtualPrinter(): self._waitForHeatup("bed") if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleTargetTemperature"]): self.outgoing.put("TargetBed:%d" % self.bedTargetTemp) - self._sendOk() def _performMove(self, line): matchX = re.search("X([0-9.]+)", line) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 8257cb6a..331b3e8f 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -132,6 +132,8 @@ default_settings = { "temperatureGraph": True, "waitForStartOnConnect": False, "alwaysSendChecksum": False, + "sendChecksumWithUnknownCommands": False, + "unknownCommandsNeedAck": False, "sdSupport": True, "sdAlwaysAvailable": False, "swallowOkAfterResend": True, diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index f1155bb1..13bbd783 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -155,6 +155,8 @@ class MachineCom(object): self._timeout = None self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) + self._sendChecksumWithUnknownCommands = settings().getBoolean(["feature", "sendChecksumWithUnknownCommands"]) + self._unknownCommandsNeedAck = settings().getBoolean(["feature", "unknownCommandsNeedAck"]) self._currentLine = 1 self._resendDelta = None self._lastLines = deque([], 50) @@ -1498,6 +1500,9 @@ class MachineCom(object): # fetch command and optional linenumber from queue command, linenumber, _ = 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_match = self._regex_command.search(command) is_gcode = gcode_match is not None @@ -1509,7 +1514,7 @@ class MachineCom(object): # line number predetermined, use that self._doSendWithChecksum(command, linenumber) else: - if self.isPrinting() or self._alwaysSendChecksum: + if (is_gcode or self._sendChecksumWithUnknownCommands) and (self.isPrinting() or self._alwaysSendChecksum): linenumber = self._currentLine self._addToLastLines(command) self._currentLine += 1 @@ -1517,12 +1522,17 @@ class MachineCom(object): else: self._doSendWithoutChecksum(command) + use_up_clear = not self._unknownCommandsNeedAck if is_gcode: - # trigger "sent" phase + # trigger "sent" phase and use up one "ok" self._process_command_phase("sent", command, gcode=gcode_match.group(1)) + use_up_clear = True + + # if we need to use up a clear, do that now + if use_up_clear: + self._clear_to_send.clear() # wait for the next clear - self._clear_to_send.clear() self._clear_to_send.wait() except: self._logger.exception("Caught an exception in the send loop") From fb5aaffdc17e834e8aade42eca9727a30091e8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 18:13:39 +0200 Subject: [PATCH 4/6] Added a "fake ack" button to terminal tab, as counter measure for lost "ok"s In case an ok gets lost on the line, this allows to have the communication between OctoPrint and the printer take up again. --- docs/api/connection.rst | 25 ++++++++++++++++++- src/octoprint/printer/__init__.py | 8 ++++++ src/octoprint/printer/standard.py | 6 +++++ src/octoprint/server/api/connection.py | 5 +++- .../static/js/app/viewmodels/terminal.js | 10 ++++++++ src/octoprint/templates/tabs/terminal.jinja2 | 9 +++++++ src/octoprint/util/comm.py | 3 +++ 7 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/api/connection.rst b/docs/api/connection.rst index 49904369..fc44ff8b 100644 --- a/docs/api/connection.rst +++ b/docs/api/connection.rst @@ -75,6 +75,12 @@ Issue a connection command disconnect Instructs OctoPrint to disconnect from the printer. + fake_ack + Fakes an acknowledgement message for OctoPrint in case one got lost on the serial line and the communication + with the printer since stalled. This should only be used in "emergencies" (e.g. to save prints), the reason + for the lost acknowledgement should always be properly investigated and removed instead of depending on this + "symptom solver". + **Example Connect Request** .. sourcecode:: http @@ -114,7 +120,24 @@ Issue a connection command HTTP/1.1 204 No Content - :json string command: The command to issue, either ``connect`` or ``disconnect`` + **Example FakeAck Request** + + .. sourcecode:: http + + POST /api/connection HTTP/1.1 + Host: example.com + Content-Type: application/json + X-Api-Key: abcdef... + + { + "command": "fake_ack" + } + + .. sourcecode:: http + + HTTP/1.1 204 No Content + + :json string command: The command to issue, either ``connect``, ``disconnect`` or ``fake_ack``. :json string port: ``connect`` command: The port to connect to. If left out either the existing ``portPreference`` will be used, or if that is not available OctoPrint will attempt auto detection. Must be part of the available ports. diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index 3f97deda..e0989a93 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -97,6 +97,14 @@ class PrinterInterface(object): """ raise NotImplementedError() + def fake_ack(self): + """ + Fakes an acknowledgement for the communication layer. If the communication between OctoPrint and the printer + gets stuck due to lost "ok" responses from the server due to communication issues, this can be used to get + things going again. + """ + raise NotImplementedError() + def commands(self, commands): """ Sends the provided ``commands`` to the printer. diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 03e169cd..48d89cc3 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -212,6 +212,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): return self._comm.getTransport() getTransport = util.deprecated("getTransport has been renamed to get_transport", since="1.2.0-dev-590", includedoc="Replaced by :func:`get_transport`") + def fake_ack(self): + if self._comm is None: + return + + self._comm.fakeOk() + def commands(self, commands): """ Sends one or more gcode commands to the printer. diff --git a/src/octoprint/server/api/connection.py b/src/octoprint/server/api/connection.py index 7f858c7d..75f9b1d8 100644 --- a/src/octoprint/server/api/connection.py +++ b/src/octoprint/server/api/connection.py @@ -33,7 +33,8 @@ def connectionState(): def connectionCommand(): valid_commands = { "connect": [], - "disconnect": [] + "disconnect": [], + "fake_ack": [] } command, data, response = get_json_command_from_request(request, valid_commands) @@ -68,6 +69,8 @@ def connectionCommand(): printer.connect(port=port, baudrate=baudrate, profile=printerProfile) elif command == "disconnect": printer.disconnect() + elif command == "fake_ack": + printer.fake_ack() return NO_CONTENT diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index 235dd0c8..6ce008f6 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -173,6 +173,16 @@ $(function() { } }; + self.fakeAck = function() { + $.ajax({ + url: API_BASEURL + "connection", + type: "POST", + dataType: "json", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify({"command": "fake_ack"}) + }); + }; + self.handleKeyDown = function(event) { var keyCode = event.keyCode; diff --git a/src/octoprint/templates/tabs/terminal.jinja2 b/src/octoprint/templates/tabs/terminal.jinja2 index 13a1cc3e..18d2a30a 100644 --- a/src/octoprint/templates/tabs/terminal.jinja2 +++ b/src/octoprint/templates/tabs/terminal.jinja2 @@ -20,3 +20,12 @@ {{ _('Hint: Use the arrow up/down keys to recall commands sent previously') }} + +
+ +
+ + {{ _("If acknowledgements (\"ok\"s) sent by the firmware get lost due to issues with the serial communication to your printer, OctoPrint's communication with it can become stuck. If that happens, this can help. Please be advised that such occurences hint at general communication issues with your printer which will probably negatively influence your printing results and which you should therefore try to resolve!") }} +
+
+ diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 13bbd783..3546b1ed 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -412,6 +412,9 @@ class MachineCom(object): def setTemperatureOffset(self, offsets): self._tempOffsets.update(offsets) + def fakeOk(self): + self._clear_to_send.set() + def sendCommand(self, cmd, cmd_type=None, processed=False): cmd = cmd.encode('ascii', 'replace') if not processed: From e96dcadc709f83c08436744ad2de2b1645d29d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 18:42:05 +0200 Subject: [PATCH 5/6] Fix: Receiving a "start" from the firmware triggers another handshake attempt Fixes #869 --- src/octoprint/util/comm.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 3546b1ed..15346858 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -791,12 +791,12 @@ class MachineCom(object): #Start monitoring the serial port. self._timeout = get_new_timeout("communication") - startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) + startSeen = False supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"]) # enqueue an M105 first thing - self._sendCommand("M105") - if startSeen: + if not settings().getBoolean(["feature", "waitForStartOnConnect"]): + self._sendCommand("M110") self._clear_to_send.set() while self._monitoring_active: @@ -1018,7 +1018,7 @@ class MachineCom(object): self._baudrateDetectRetry -= 1 self._serial.write('\n') self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry)) - self._sendCommand("M105") + self._sendCommand("M110") self._clear_to_send.set() else: baudrate = self._baudrateDetectList.pop(0) @@ -1029,11 +1029,11 @@ class MachineCom(object): self._baudrateDetectRetry = 5 self._timeout = get_new_timeout("communication") self._serial.write('\n') - self._sendCommand("M105") + self._sendCommand("M110") self._clear_to_send.set() except: self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string())) - elif 'start' in line or ('ok' in line and 'T:' in line): + elif 'start' in line or 'ok' in line: self._onConnected() self._clear_to_send.set() @@ -1041,8 +1041,9 @@ class MachineCom(object): elif self._state == self.STATE_CONNECTING: if "start" in line and not startSeen: startSeen = True + self._sendCommand("M110") self._clear_to_send.set() - elif "ok" in line and startSeen: + elif "ok" in line: self._onConnected() elif time.time() > self._timeout: self.close() From 5e54f3a52384948a8b44c8467b85381ad7c9fc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Apr 2015 18:43:00 +0200 Subject: [PATCH 6/6] Persist package name for plugins loaded from entry point This allows later deinstallation even if module and package name differ. --- src/octoprint/plugin/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 99ec798f..26d4ca41 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -542,10 +542,11 @@ class PluginManager(object): url=module_pkginfo.home_page, license=module_pkginfo.license )) + package_name = module_pkginfo.name plugin = self._import_plugin_from_module(key, **kwargs) if plugin: - plugin.origin = ("entry_point", group, module_name) + plugin.origin = ("entry_point", group, module_name, package_name) plugin.enabled = False result[key] = plugin