diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 265572d0..345a4413 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -133,6 +133,12 @@ mapped from the same fields in ``config.yaml`` unless otherwise noted: - * - ``feature.modelSizeDetection`` - + * - ``feature.firmwareDetection`` + - + * - ``feature.printCancelConfirmation`` + - + * - ``feature.blockWhileDwelling`` + - * - ``folder.uploads`` - * - ``folder.timelapse`` diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index 3a49a670..9670645a 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -519,6 +519,23 @@ Use the following settings to enable or disable OctoPrint features: # If ignoredIdenticalResends is true, how many consecutive identical resends to ignore identicalResendsCount: 7 + # Whether to support F on its own as a valid GCODE command (true) or not (false) + supportFAsCommand: false + + # Whether to enable model size detection and warning (true) or not (false) + modelSizeDetection: true + + # Whether to attempt to auto detect the firmware of the printer and adjust settings + # accordingly (true) or not and rely on manual configuration (false) + firmwareDetection: true + + # Whether to show a confirmation on print cancelling (true) or not (false) + printCancelConfirmation: true + + # Whether to block all sending to the printer while a G4 (dwell) command is active (true, repetier) + # or not (false) + blockWhileDwelling: false + .. _sec-configuration-config_yaml-folder: Folder @@ -565,6 +582,45 @@ Use the following settings to set custom paths for folders used by OctoPrint: # Absolute path where to store (GCODE) scripts scripts: /path/to/scripts/folder +.. _sec-configuration-config_yaml-gcodeanalysis: + +GCODE Analysis +-------------- + +Settings pertaining to the server side GCODE analysis implementation. + +.. code-block:: yaml + + # Maximum number of extruders to support/to sanity check for + maxExtruders: 10 + + # Pause between each processed GCODE line in normal priority mode, seconds + throttle_normalprio: 0.01 + + # Pause between each processed GCODE line in high priority mode (e.g. on fresh + # uploads), seconds + throttle_highprio: 0.0 + +.. _sec-configuration-config_yaml-gcodeviewer: + +GCODE Viewer +------------ + +Settings pertaining to the built in GCODE Viewer. + +.. code-block:: yaml + + # Whether to enable the GCODE viewer in the UI + enabled: true + + # Maximum size a GCODE file may have on mobile devices to automatically be loaded + # into the viewer, defaults to 2MB + mobileSizeThreshold: 2097152 + + # Maximum size a GCODE file may have to automatically be loaded into the viewer, + # defaults to 20MB + sizeThreshold: 20971520 + .. _sec-configuration-config_yaml-plugins: Plugin settings @@ -679,8 +735,34 @@ Use the following settings to configure the serial connection to the printer: connection: 2 # Timeout during serial communication, in seconds. - # Defaults to 5 sec - communication: 5 + # Defaults to 30 sec + communication: 30 + + # Timeout after which to query temperature when no target is set + temperature: 5 + + # Timeout after which to query temperature when a target is set + temperatureTargetSet: 2 + + # Timeout after which to query the SD status while SD printing + sdStatus: 1 + + # Maximum number of consecutive communication timeouts after which the printer will be considered + # dead and OctoPrint disconnects with an error. + maxCommunicationTimeouts: + + # max. timeouts when the printer is idle + idle: 2 + + # max. timeouts when the printer is printing + printing: 5 + + # max. timeouts when a long running command is active + long: 5 + + # Maximum number of write attempts to serial during which nothing can be written before the communication + # with the printer is considered dead and OctoPrint will disconnect with an error + maxWritePasses: # Use this to define additional patterns to consider for serial port listing. Must be a valid # "glob" pattern (see http://docs.python.org/2/library/glob.html). Defaults to not set. @@ -702,6 +784,7 @@ Use the following settings to configure the serial connection to the printer: - G32 - M400 - M226 + - M600 # Commands which need to always be send with a checksum. Defaults to only M110 checksumRequiringCommands: @@ -944,10 +1027,12 @@ Use `Javascript regular expressions + | Will cause to be enqueued for use, + | will be used instead of actual "ok" # Reply Timing / Sleeping @@ -527,7 +557,7 @@ class VirtualPrinter(object): sleep_after | Sleeps s after each execution of sleep_after_next - | Sleeps s after execution of + | Sleeps s after execution of next """ for line in usage.split("\n"): self._send("echo: {}".format(line.strip())) @@ -556,6 +586,7 @@ class VirtualPrinter(object): sleep_after_match = VirtualPrinter.sleep_after_regex.match(data) sleep_after_next_match = VirtualPrinter.sleep_after_next_regex.match(data) custom_action_match = VirtualPrinter.custom_action_regex.match(data) + prepare_ok_match = VirtualPrinter.prepare_ok_regex.match(data) if sleep_match is not None: interval = int(sleep_match.group(1)) @@ -576,6 +607,9 @@ class VirtualPrinter(object): params = custom_action_match.group(2) params = params.strip() if params is not None else "" self._send("// action:{action} {params}".format(**locals()).strip()) + elif prepare_ok_match is not None: + ok = prepare_ok_match.group(1) + self._prepared_oks.append(ok) except: pass @@ -665,7 +699,7 @@ class VirtualPrinter(object): output += " @:64\n" if includeOk: - output = "ok " + output + output = "{} {}".format(self._ok(), output) self._send(output) def _parseHotendCommand(self, line, wait=False, support_r=False): @@ -1041,9 +1075,9 @@ class VirtualPrinter(object): return if settings().getBoolean(["devel", "virtualPrinter", "okWithLinenumber"]): - self._send("ok %d" % self.lastN) + self._send("{} {}".format(self._ok(), self.lastN)) else: - self._send("ok") + self._send(self._ok()) def _sendWaitAfterTimeout(self, timeout=5): time.sleep(timeout) @@ -1054,6 +1088,12 @@ class VirtualPrinter(object): if self.outgoing is not None: self.outgoing.put(line) + def _ok(self): + ok = "ok" + if self._prepared_oks: + ok = self._prepared_oks.pop(0) + return ok + class CharCountingQueue(queue.Queue): def __init__(self, maxsize, name=None): diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 90fc3c6b..0276eb01 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -126,7 +126,8 @@ def getSettings(): "ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"]), "modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"]), "firmwareDetection": s.getBoolean(["feature", "firmwareDetection"]), - "printCancelConfirmation": s.getBoolean(["feature", "printCancelConfirmation"]) + "printCancelConfirmation": s.getBoolean(["feature", "printCancelConfirmation"]), + "blockWhileDwelling": s.getBoolean(["feature", "blockWhileDwelling"]) }, "serial": { "port": connectionOptions["portPreference"], @@ -311,6 +312,7 @@ def _saveSettings(data): if "modelSizeDetection" in data["feature"]: s.setBoolean(["feature", "modelSizeDetection"], data["feature"]["modelSizeDetection"]) if "firmwareDetection" in data["feature"]: s.setBoolean(["feature", "firmwareDetection"], data["feature"]["firmwareDetection"]) if "printCancelConfirmation" in data["feature"]: s.setBoolean(["feature", "printCancelConfirmation"], data["feature"]["printCancelConfirmation"]) + if "blockWhileDwelling" in data["feature"]: s.setBoolean(["feature", "blockWhileDwelling"], data["feature"]["blockWhileDwelling"]) if "serial" in data.keys(): if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"]) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 7d677ac0..5bbb736a 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -168,7 +168,6 @@ default_settings = { "options": {}, "postRoll": 0, "fps": 25, - "capturePostRoll": True }, "cleanTmpAfterDays": 7 }, @@ -203,7 +202,8 @@ default_settings = { "supportFAsCommand": False, "modelSizeDetection": True, "firmwareDetection": True, - "printCancelConfirmation": True + "printCancelConfirmation": True, + "blockWhileDwelling": False }, "folder": { "uploads": None, @@ -357,7 +357,10 @@ default_settings = { "brokenM29": True, "supportF": False, "firmwareName": "Virtual Marlin 1.0", - "sharedNozzle": False + "sharedNozzle": False, + "sendBusy": False, + "simulateReset": True, + "preparedOks": [] } } } diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 98b6d631..8badcc3e 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -883,3 +883,36 @@ var getQueryParameterByName = function(name, url) { if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, " ")); }; + +/** + * Escapes unprintable ASCII characters in the provided string. + * + * E.g. turns a null byte in the string into "\x00". + * + * Only characters 0 to 31, 127 and 255 will be escaped, that + * should leave printable characters and unicode alone. + * + * Originally based on + * https://gist.github.com/mathiasbynens/1243213#gistcomment-53590 + * + * @param str The string to escape + * @returns {string} + */ +var escapeUnprintableCharacters = function(str) { + var result = ""; + var index = 0; + var charCode; + + while (!isNaN(charCode = str.charCodeAt(index))) { + if (charCode < 32 || charCode == 127 || charCode == 255) { + // special hex chars + result += "\\x" + (charCode > 15 ? "" : "0") + charCode.toString(16) + } else { + // anything else + result += str[index]; + } + + index++; + } + return result; +}; diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 7b9394e5..5f1241f4 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -139,6 +139,7 @@ $(function() { self.feature_modelSizeDetection = ko.observable(undefined); self.feature_firmwareDetection = ko.observable(undefined); self.feature_printCancelConfirmation = ko.observable(undefined); + self.feature_blockWhileDwelling = ko.observable(undefined); self.serial_port = ko.observable(); self.serial_baudrate = ko.observable(); diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index b524afcc..baa58678 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -201,7 +201,7 @@ $(function() { if (type == undefined) { type = "line"; } - return {line: line, type: type} + return {line: escapeUnprintableCharacters(line), type: type} }; self._processStateData = function(data) { diff --git a/src/octoprint/templates/dialogs/settings/features.jinja2 b/src/octoprint/templates/dialogs/settings/features.jinja2 index 1e7c8592..c90bebab 100644 --- a/src/octoprint/templates/dialogs/settings/features.jinja2 +++ b/src/octoprint/templates/dialogs/settings/features.jinja2 @@ -88,6 +88,13 @@ +
+
+ +
+
diff --git a/src/octoprint/templates/dialogs/settings/serialconnection.jinja2 b/src/octoprint/templates/dialogs/settings/serialconnection.jinja2 index c4c3a380..2fe76ca4 100644 --- a/src/octoprint/templates/dialogs/settings/serialconnection.jinja2 +++ b/src/octoprint/templates/dialogs/settings/serialconnection.jinja2 @@ -44,7 +44,7 @@
-
+
@@ -138,25 +138,28 @@
+ {{ _('Set to 0 to disable consecutive timeout detection and handling.') }}
+ {{ _('Set to 0 to disable consecutive timeout detection and handling.') }}
+ {{ _('Set to 0 to disable consecutive timeout detection and handling.') }}
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index fb1e1118..6f9f727f 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -285,6 +285,7 @@ class MachineCom(object): self._long_running_command = False self._heating = False + self._dwelling_until = False self._connection_closing = False self._timeout = None @@ -316,6 +317,7 @@ class MachineCom(object): self._unknownCommandsNeedAck = settings().getBoolean(["feature", "unknownCommandsNeedAck"]) self._sdAlwaysAvailable = settings().getBoolean(["feature", "sdAlwaysAvailable"]) self._sdRelativePath = settings().getBoolean(["feature", "sdRelativePath"]) + self._blockWhileDwelling = settings().getBoolean(["feature", "blockWhileDwelling"]) self._currentLine = 1 self._line_mutex = threading.RLock() self._resendDelta = None @@ -1059,6 +1061,7 @@ class MachineCom(object): self.sayHello() while self._monitoring_active: + now = time.time() try: line = self._readline() if line is None: @@ -1067,6 +1070,9 @@ class MachineCom(object): self._consecutive_timeouts = 0 self._timeout = get_new_timeout("communication", self._timeout_intervals) + if self._dwelling_until and now > self._dwelling_until: + self._dwelling_until = False + ##~~ debugging output handling if line.startswith("//"): debugging_output = line[2:].strip() @@ -1095,7 +1101,7 @@ class MachineCom(object): def convert_line(line): if line is None: return None, None - stripped_line = line.strip() + stripped_line = line.strip().strip("\0") return stripped_line, stripped_line.lower() ##~~ Error handling @@ -1148,7 +1154,7 @@ class MachineCom(object): handled = True # process timeouts - elif line == "" and time.time() > self._timeout: + elif line == "" and (not self._blockWhileDwelling or not self._dwelling_until or now > self._dwelling_until) and now > self._timeout: # timeout only considered handled if the printer is printing self._handle_timeout() handled = self.isPrinting() @@ -1238,6 +1244,7 @@ class MachineCom(object): self._alwaysSendChecksum = True self._resendSwallowRepetitions = True + self._blockWhileDwelling = True supportRepetierTargetTemp = True disable_external_heatup_detection = True @@ -1377,7 +1384,10 @@ class MachineCom(object): if "start" in line and not startSeen: startSeen = True self.sayHello() - elif line.startswith("ok"): + elif line.startswith("ok") or (supportWait and line == "wait"): + if line == "wait": + # if it was a wait we probably missed an ok, so let's simulate that now + self._handle_ok() self._onConnected() elif time.time() > self._timeout: self._log("There was a timeout while trying to connect to the printer") @@ -1551,7 +1561,7 @@ class MachineCom(object): command or heating, no poll will be done. """ - if self.isOperational() and not self._connection_closing and not self.isStreaming() and not self._long_running_command and not self._heating and not self._manualStreaming: + if self.isOperational() and not self._connection_closing and not self.isStreaming() and not self._long_running_command and not self._heating and not self._dwelling_until and not self._manualStreaming: self.sendCommand("M105", cmd_type="temperature_poll") def _poll_sd_status(self): @@ -1562,7 +1572,7 @@ class MachineCom(object): command or heating, no poll will be done. """ - if self.isOperational() and not self._connection_closing and self.isSdPrinting() and not self._long_running_command and not self._heating: + if self.isOperational() and not self._connection_closing and self.isSdPrinting() and not self._long_running_command and not self._dwelling_until and not self._heating: self.sendCommand("M27", cmd_type="sd_status_poll") def _onConnected(self): @@ -2019,6 +2029,12 @@ class MachineCom(object): if not self._send_queue_active: break + # sleep if we are dwelling + now = time.time() + if self._blockWhileDwelling and self._dwelling_until and now < self._dwelling_until: + time.sleep(self._dwelling_until - now) + self._dwelling_until = False + # fetch command, command type and optional linenumber and sent callback from queue command, linenumber, command_type, on_sent = entry @@ -2423,7 +2439,9 @@ class MachineCom(object): _timeout = float(p_match.group("value")) / 1000.0 elif s_match: _timeout = float(s_match.group("value")) + self._timeout = get_new_timeout("communication", self._timeout_intervals) + _timeout + self._dwelling_until = time.time() + _timeout ##~~ command phase handlers