From 07382d39182ae7f1b9c27695bb4d0ed5f65d1e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 17 Nov 2016 17:13:36 +0100 Subject: [PATCH] Add (optional) firmware auto detection If enabled (which it is by default), OctoPrint will now send an M115 to the printer on initial connection in order to try to figure out what kind of firmware it is. For FIRMWARE_NAME values containing "repetier" (case insensitive), all Repetier- specific flags will be set on the comm layer. For FIRMWARE_NAME values containing "reprapfirmware", all RepRapFirmware- specific flags will be set on the comm layer. For now no other handling will be performed. --- src/octoprint/printer/standard.py | 2 +- src/octoprint/server/api/settings.py | 4 +- src/octoprint/settings.py | 3 +- .../static/js/app/viewmodels/settings.js | 1 + .../dialogs/settings/features.jinja2 | 88 +++++++++++-------- src/octoprint/util/comm.py | 70 ++++++++++++++- 6 files changed, 124 insertions(+), 44 deletions(-) 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").