diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 68496930..1d8ebd1f 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -393,7 +393,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._printAfterSelect = printAfterSelect self._posAfterSelect = pos - self._comm.selectFile("/" + path if sd and not settings().getBoolean(["feature", "sdRelativePath"]) else path, sd) + self._comm.selectFile("/" + path if sd else path, sd) self._setProgressData(completion=0) self._setCurrentZ(None) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 8ccc0e12..339a77a2 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -104,7 +104,8 @@ def getSettings(): "keyboardControl": s.getBoolean(["feature", "keyboardControl"]), "pollWatched": s.getBoolean(["feature", "pollWatched"]), "ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"]), - "modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"]) + "modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"]), + "firmwareDetection": s.getBoolean(["feature", "firmwareDetection"]) }, "serial": { "port": connectionOptions["portPreference"], @@ -274,6 +275,7 @@ def _saveSettings(data): if "pollWatched" in data["feature"]: s.setBoolean(["feature", "pollWatched"], data["feature"]["pollWatched"]) if "ignoreIdenticalResends" in data["feature"]: s.setBoolean(["feature", "ignoreIdenticalResends"], data["feature"]["ignoreIdenticalResends"]) 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 "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 20cb42b3..fdface85 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -197,7 +197,8 @@ default_settings = { "ignoreIdenticalResends": False, "identicalResendsCountdown": 7, "supportFAsCommand": False, - "modelSizeDetection": True + "modelSizeDetection": True, + "firmwareDetection": True }, "folder": { "uploads": None, diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 96381f30..12e4703c 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -133,6 +133,7 @@ $(function() { self.feature_pollWatched = ko.observable(undefined); self.feature_ignoreIdenticalResends = ko.observable(undefined); self.feature_modelSizeDetection = ko.observable(undefined); + self.feature_firmwareDetection = ko.observable(undefined); self.serial_port = ko.observable(); self.serial_baudrate = ko.observable(); diff --git a/src/octoprint/templates/dialogs/settings/features.jinja2 b/src/octoprint/templates/dialogs/settings/features.jinja2 index ab7a8843..3d146167 100644 --- a/src/octoprint/templates/dialogs/settings/features.jinja2 +++ b/src/octoprint/templates/dialogs/settings/features.jinja2 @@ -34,53 +34,67 @@ +
-
-
- +
+
+
+ +
-
-
-
- +
+
+ +
-
-
-
- +
+
+ +
-
-
-
- +
+
+ +
-
-
- -
- - - +
+
+ +
+
+
+ +
+ + + +
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index d4e43ee6..5d111e74 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -28,7 +28,7 @@ from octoprint.events import eventManager, Events from octoprint.filemanager import valid_file_type from octoprint.filemanager.destinations import FileDestinations from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer, \ - to_unicode, bom_aware_open, TypedQueue, TypeAlreadyInQueue + to_unicode, bom_aware_open, TypedQueue, TypeAlreadyInQueue, chunks try: import _winreg @@ -124,6 +124,9 @@ Groups will be as follows: * ``e``: E coordinate """ +regex_firmware_splitter = re.compile("\s*([A-Z0-9_]+):") +"""Regex to use for splitting M115 responses.""" + def serialList(): baselist=[] if os.name=="nt": @@ -310,6 +313,8 @@ class MachineCom(object): self._neverSendChecksum = settings().getBoolean(["feature", "neverSendChecksum"]) self._sendChecksumWithUnknownCommands = settings().getBoolean(["feature", "sendChecksumWithUnknownCommands"]) self._unknownCommandsNeedAck = settings().getBoolean(["feature", "unknownCommandsNeedAck"]) + self._sdAlwaysAvailable = settings().getBoolean(["feature", "sdAlwaysAvailable"]) + self._sdRelativePath = settings().getBoolean(["feature", "sdRelativePath"]) self._currentLine = 1 self._line_mutex = threading.RLock() self._resendDelta = None @@ -319,7 +324,9 @@ class MachineCom(object): self._currentResendCount = 0 self._resendSwallowRepetitions = settings().getBoolean(["feature", "ignoreIdenticalResends"]) self._resendSwallowRepetitionsCounter = 0 - self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"]) + + self._firmwareDetection = settings().getBoolean(["feature", "firmwareDetection"]) + self._firmwareInfoReceived = not self._firmwareDetection self._supportResendsWithoutOk = settings().getBoolean(["serial", "supportResendsWithoutOk"]) @@ -795,6 +802,10 @@ class MachineCom(object): if not self.isOperational(): # printer is not connected, can't use SD return + + if filename.startswith("/") and self._sdRelativePath: + filename = filename[1:] + self._sdFileToSelect = filename self.sendCommand("M23 %s" % filename) else: @@ -920,7 +931,7 @@ class MachineCom(object): return self.sendCommand("M21") - if settings().getBoolean(["feature", "sdAlwaysAvailable"]): + if self._sdAlwaysAvailable: self._sdAvailable = True self.refreshSdFiles() self._callback.on_comm_sd_state_change(self._sdAvailable) @@ -1207,6 +1218,33 @@ class MachineCom(object): except ValueError: pass + ##~~ firmware name & version + elif "FIRMWARE_NAME:" in line: + # looks like a response to M115 + data = parse_firmware_line(line) + firmware_name = data.get("FIRMWARE_NAME") + self._logger.info("Printer reports firmware name \"{}\"".format(firmware_name)) + + if not self._firmwareInfoReceived and firmware_name: + if "repetier" in firmware_name.lower(): + self._logger.info("Detected Repetier firmware, enabling relevant features for issue free communication") + + self._alwaysSendChecksum = True + self._resendSwallowRepetitions = True + supportRepetierTargetTemp = True + disable_external_heatup_detection = True + + sd_always_available = self._sdAlwaysAvailable + self._sdAlwaysAvailable = True + if not sd_always_available and not self._sdAvailable: + self.initSdCard() + + elif "reprapfirmware" in firmware_name.lower(): + self._logger.info("Detected RepRapFirmware, enabling relevant features for issue free communication") + self._sdRelativePath = True + + self._firmwareInfoReceived = True + ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False @@ -1528,6 +1566,8 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) self.resetLineNumbers() + if self._firmwareDetection: + self.sendCommand("M115") if self._sdAvailable: self.refreshSdFiles() @@ -2012,7 +2052,9 @@ class MachineCom(object): # now comes the part where we increase line numbers and send stuff - no turning back now command_requiring_checksum = gcode is not None and gcode in self._checksum_requiring_commands command_allowing_checksum = gcode is not None or self._sendChecksumWithUnknownCommands - checksum_enabled = self.isPrinting() or self._alwaysSendChecksum + checksum_enabled = not self._neverSendChecksum and (self.isPrinting() or + self._alwaysSendChecksum or + not self._firmwareInfoReceived) command_to_send = command.encode("ascii", errors="replace") if command_requiring_checksum or (command_allowing_checksum and checksum_enabled): @@ -2916,6 +2958,26 @@ def parse_temperature_line(line, current): return max(maxToolNum, current), canonicalize_temperatures(result, current) +def parse_firmware_line(line): + """ + Parses the provided firmware info line. + + The result will be a dictionary mapping from the contained keys to the contained + values. + + Arguments: + line (str): the line to parse + + Returns: + dict: a dictionary with the parsed data + """ + + result = dict() + split_line = regex_firmware_splitter.split(line.strip())[1:] # first entry is empty start of trimmed string + for key, value in chunks(split_line, 2): + result[key] = value + return result + def gcode_command_for_cmd(cmd): """ Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").