From 717ec0419fedf4f7cf22d58c41f5edd6a6cea94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 23 Sep 2013 17:59:22 +0200 Subject: [PATCH 1/9] Changed SD filename generation, made SD handling more reliable as a whole --- octoprint/printer.py | 33 ++++++++--- octoprint/server.py | 2 +- octoprint/static/js/app/main.js | 94 +++++++++++++++++++++++++----- octoprint/templates/dialogs.jinja2 | 4 +- octoprint/templates/index.jinja2 | 4 +- octoprint/util/__init__.py | 28 +++++++++ octoprint/util/comm.py | 4 +- octoprint/util/virtual.py | 1 + 8 files changed, 139 insertions(+), 31 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index 323546ad..9429bbc1 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -69,6 +69,7 @@ class Printer(): # sd handling self._sdPrinting = False self._sdStreaming = False + self._sdFilelistAvailable = threading.Event() self._selectedFile = None @@ -432,6 +433,7 @@ class Printer(): def mcSdFiles(self, files): self._sendTriggerUpdateCallbacks("gcodeFiles") + self._sdFilelistAvailable.set() def mcFileSelected(self, filename, filesize, sd): self._setJobData(filename, filesize, sd) @@ -465,34 +467,47 @@ class Printer(): #~~ sd file handling def getSdFiles(self): - if self._comm is None: - return + if self._comm is None or not self._comm.isSdReady(): + return [] return self._comm.getSdFiles() def addSdFile(self, filename, path): - if not self._comm or self._comm.isBusy(): + if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): return - self._comm.startFileTransfer(path, filename[:8].lower() + ".gco") + + self.refreshSdFiles(blocking=True) + existingSdFiles = self._comm.getSdFiles() + + sdFilename = util.getDosFilename(filename, existingSdFiles) + self._comm.startFileTransfer(path, sdFilename) def deleteSdFile(self, filename): - if not self._comm: + if not self._comm or not self._comm.isSdReady(): return self._comm.deleteSdFile(filename) def initSdCard(self): - if not self._comm: + if not self._comm or self._comm.isSdReady(): return self._comm.initSdCard() def releaseSdCard(self): - if not self._comm: + if not self._comm or not self._comm.isSdReady(): return self._comm.releaseSdCard() - def refreshSdFiles(self): - if not self._comm: + def refreshSdFiles(self, blocking=False): + """ + Refreshs the list of file stored on the SD card attached to printer (if available and printer communication + available). Optional blocking parameter allows making the method block (max 10s) until the file list has been + received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation. + """ + if not self._comm or not self._comm.isSdReady(): return + self._sdFilelistAvailable.clear() self._comm.refreshSdFiles() + if blocking: + self._sdFilelistAvailable.wait(10000) #~~ state reports diff --git a/octoprint/server.py b/octoprint/server.py index 54ff8baa..730c8e46 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -451,7 +451,7 @@ def deleteGcodeFile(): printer.unselectFile() if not (currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused())): - if currentSd: + if sd: printer.deleteSdFile(filename) else: gcodeManager.removeFile(filename) diff --git a/octoprint/static/js/app/main.js b/octoprint/static/js/app/main.js index ee11483f..795be042 100644 --- a/octoprint/static/js/app/main.js +++ b/octoprint/static/js/app/main.js @@ -82,23 +82,29 @@ $(function() { } } - var localTarget; - if (CONFIG_SD_SUPPORT) { - localTarget = $("#drop_locally"); - } else { - localTarget = $("#drop"); + function enable_local_dropzone() { + $("#gcode_upload").fileupload({ + dataType: "json", + dropZone: localTarget, + formData: {target: "local"}, + done: gcode_upload_done, + fail: gcode_upload_fail, + progressall: gcode_upload_progress + }); } - $("#gcode_upload").fileupload({ - dataType: "json", - dropZone: localTarget, - formData: {target: "local"}, - done: gcode_upload_done, - fail: gcode_upload_fail, - progressall: gcode_upload_progress - }); + function disable_local_dropzone() { + $("#gcode_upload").fileupload({ + dataType: "json", + dropZone: null, + formData: {target: "local"}, + done: gcode_upload_done, + fail: gcode_upload_fail, + progressall: gcode_upload_progress + }); + } - if (CONFIG_SD_SUPPORT) { + function enable_sd_dropzone() { $("#gcode_upload_sd").fileupload({ dataType: "json", dropZone: $("#drop_sd"), @@ -109,6 +115,62 @@ $(function() { }); } + function disable_sd_dropzone() { + $("#gcode_upload_sd").fileupload({ + dataType: "json", + dropZone: null, + formData: {target: "sd"}, + done: gcode_upload_done, + fail: gcode_upload_fail, + progressall: gcode_upload_progress + }); + } + + var localTarget; + if (CONFIG_SD_SUPPORT) { + localTarget = $("#drop_locally"); + } else { + localTarget = $("#drop"); + } + + loginStateViewModel.isUser.subscribe(function(newValue) { + if (newValue === true) { + enable_local_dropzone(); + } else { + disable_local_dropzone(); + } + }); + + if (loginStateViewModel.isUser()) { + enable_local_dropzone(); + } else { + disable_local_dropzone(); + } + + if (CONFIG_SD_SUPPORT) { + printerStateViewModel.isSdReady.subscribe(function(newValue) { + if (newValue === true && loginStateViewModel.isUser()) { + enable_sd_dropzone(); + } else { + disable_sd_dropzone(); + } + }); + + loginStateViewModel.isUser.subscribe(function(newValue) { + if (newValue === true && printerStateViewModel.isSdReady()) { + enable_sd_dropzone(); + } else { + disable_sd_dropzone(); + } + }); + + if (printerStateViewModel.isSdReady() && loginStateViewModel.isUser()) { + enable_sd_dropzone(); + } else { + disable_sd_dropzone(); + } + } + $(document).bind("dragover", function (e) { var dropOverlay = $("#drop_overlay"); var dropZone = $("#drop"); @@ -146,7 +208,7 @@ $(function() { if (foundLocal) { dropZoneLocalBackground.addClass("hover"); dropZoneSdBackground.removeClass("hover"); - } else if (foundSd) { + } else if (foundSd && printerStateViewModel.isSdReady()) { dropZoneSdBackground.addClass("hover"); dropZoneLocalBackground.removeClass("hover"); } else if (found) { @@ -201,7 +263,7 @@ $(function() { ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog")); ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); - ko.applyBindings(loginStateViewModel, document.getElementById("drop_overlay")); + ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay")); var timelapseElement = document.getElementById("timelapse"); if (timelapseElement) { diff --git a/octoprint/templates/dialogs.jinja2 b/octoprint/templates/dialogs.jinja2 index ca28988e..97a3ea09 100644 --- a/octoprint/templates/dialogs.jinja2 +++ b/octoprint/templates/dialogs.jinja2 @@ -17,13 +17,13 @@ -
+
{% if enableSdSupport %}

Upload locally
-

Upload to SD
+

Upload to SD
(SD not initialized)
{% else %}

Upload
diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2 index 320252a3..8ffec32d 100644 --- a/octoprint/templates/index.jinja2 +++ b/octoprint/templates/index.jinja2 @@ -211,10 +211,10 @@ Upload - + Upload to SD - + {% else %} diff --git a/octoprint/util/__init__.py b/octoprint/util/__init__.py index 23d8a238..ff6e5924 100644 --- a/octoprint/util/__init__.py +++ b/octoprint/util/__init__.py @@ -6,6 +6,7 @@ import os import traceback import sys import time +import re from octoprint.settings import settings @@ -110,3 +111,30 @@ def getRemoteAddress(request): if forwardedFor is not None: return forwardedFor.split(",")[0] return request.remote_addr + + +def getDosFilename(input, existingFilenames, extension=None): + if input is None: + return None + + if extension is None: + extension = "gco" + + filename, ext = input.rsplit(".", 1) + return findCollisionfreeName(filename, extension, existingFilenames) + + +def findCollisionfreeName(input, extension, existingFilenames): + filename = re.sub(r"\s+", "_", input.lower().translate(None, ".\"/\\[]:;=,")) + + counter = 1 + power = 1 + while counter < (10 * power): + result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension + if result not in existingFilenames: + return result + counter += 1 + if counter == 10 * power: + power += 1 + + raise ValueError("Can't create a collision free filename") diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index db555393..18cc3cd7 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -609,6 +609,7 @@ class MachineCom(object): # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) + line = "ok" elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 @@ -641,7 +642,7 @@ class MachineCom(object): pass ##~~ Parsing for pause triggers - if pauseTriggers: + if pauseTriggers and not self.isStreaming(): if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: @@ -800,6 +801,7 @@ class MachineCom(object): self._callback.mcFileTransferDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("TransferDone", filename) + self.refreshSdFiles() else: self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) diff --git a/octoprint/util/virtual.py b/octoprint/util/virtual.py index 6a445593..cc70578a 100644 --- a/octoprint/util/virtual.py +++ b/octoprint/util/virtual.py @@ -76,6 +76,7 @@ class VirtualPrinter(): return else: self.lastN = linenumber + data = data.split(None, 1)[1].strip() data += "\n" From b51da4bd1aca6ce228e246946445aad0283260fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 23 Sep 2013 21:57:54 +0200 Subject: [PATCH 2/9] Properly restart sd prints Fixes #258 (cherry picked from commit 076c676) --- octoprint/util/comm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 18cc3cd7..5be9c2a5 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -330,6 +330,7 @@ class MachineCom(object): if self._currentFile is None: raise ValueError("No file selected for printing") + wasPaused = self.isPaused() self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) eventManager().fire("PrintStarted", self._currentFile.getFilename()) @@ -337,7 +338,7 @@ class MachineCom(object): try: self._currentFile.start() if self.isSdFileSelected(): - if self.isPaused(): + if wasPaused: self.sendCommand("M26 S0") self._currentFile.setFilepos(0) self.sendCommand("M24") From f62e868c021939137c3835dab0a5c834841d58f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 13 Oct 2013 19:33:19 +0200 Subject: [PATCH 3/9] Unbind old click handlers from confirmation dialog before use. Fixes #279 --- octoprint/static/js/app/viewmodels/navigation.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/octoprint/static/js/app/viewmodels/navigation.js b/octoprint/static/js/app/viewmodels/navigation.js index c12ce0ed..39f71f9e 100644 --- a/octoprint/static/js/app/viewmodels/navigation.js +++ b/octoprint/static/js/app/viewmodels/navigation.js @@ -23,7 +23,12 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV } if (action.confirm) { $("#confirmation_dialog .confirmation_dialog_message").text(action.confirm); - $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); callback(); }); + $("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click"); + $("#confirmation_dialog .confirmation_dialog_acknowledge").bind("click", function(e) { + e.preventDefault(); + $("#confirmation_dialog").modal("hide"); + callback(); + }); $("#confirmation_dialog").modal("show"); } else { callback(); From 5a6cd58f199b886c7cfd6aae17f1c9753d498aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 20 Oct 2013 14:35:03 +0200 Subject: [PATCH 4/9] Make swallowOkAfterResend default to True, part of the fix for #166 --- octoprint/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint/settings.py b/octoprint/settings.py index 7e0fab73..386a8ede 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -59,7 +59,7 @@ default_settings = { "waitForStartOnConnect": False, "alwaysSendChecksum": False, "sdSupport": True, - "swallowOkAfterResend": False + "swallowOkAfterResend": True }, "folder": { "uploads": None, From 48f5a44bc5373ea849a754ff358ceff6647e864b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 20 Oct 2013 14:36:40 +0200 Subject: [PATCH 5/9] SwallowOkAfterResend isn't repetier only anymore --- octoprint/templates/settings.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint/templates/settings.jinja2 b/octoprint/templates/settings.jinja2 index f543793d..8c32f6bb 100644 --- a/octoprint/templates/settings.jinja2 +++ b/octoprint/templates/settings.jinja2 @@ -206,7 +206,7 @@
From a7eb5f64933313877a40a39e458e27a8fdfb8bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 20 Oct 2013 14:37:10 +0200 Subject: [PATCH 6/9] Removed obsolete GcodeLoader in printer module --- octoprint/printer.py | 77 -------------------------------------------- 1 file changed, 77 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index 9429bbc1..eee862ec 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -568,83 +568,6 @@ class Printer(): def isLoading(self): return self._gcodeLoader is not None -class GcodeLoader(threading.Thread): - """ - The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate - thread while constantly notifying interested listeners about the current progress. - The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion. - """ - - def __init__(self, filename, progressCallback, loadedCallback): - threading.Thread.__init__(self) - - self._progressCallback = progressCallback - self._loadedCallback = loadedCallback - - self._filename = filename - self._gcodeList = None - - def run(self): - #Send an initial M110 to reset the line counter to zero. - prevLineType = lineType = "CUSTOM" - gcodeList = ["M110 N0"] - filesize = os.stat(self._filename).st_size - with open(self._filename, "r") as file: - for line in file: - if line.startswith(";TYPE:"): - lineType = line[6:].strip() - if ";" in line: - line = line[0:line.find(";")] - line = line.strip() - if len(line) > 0: - if prevLineType != lineType: - gcodeList.append((line, lineType, )) - else: - gcodeList.append(line) - prevLineType = lineType - self._onLoadingProgress(float(file.tell()) / float(filesize)) - - self._gcodeList = gcodeList - self._loadedCallback(self._filename, self._gcodeList) - - def _onLoadingProgress(self, progress): - self._progressCallback(self._filename, progress, "loading") - - def _onParsingProgress(self, progress): - self._progressCallback(self._filename, progress, "parsing") - -class SdFileStreamer(threading.Thread): - def __init__(self, comm, filename, file, progressCallback, finishCallback): - threading.Thread.__init__(self) - - self._comm = comm - self._filename = filename - self._file = file - self._progressCallback = progressCallback - self._finishCallback = finishCallback - - def run(self): - if self._comm.isBusy(): - return - - name = self._filename[:self._filename.rfind(".")] - sdFilename = name[:8].lower() + ".gco" - try: - size = os.stat(self._file).st_size - with open(self._file, "r") as f: - self._comm.startSdFileTransfer(sdFilename) - for line in f: - if ";" in line: - line = line[0:line.find(";")] - line = line.strip() - if len(line) > 0: - self._comm.sendCommand(line) - time.sleep(0.001) # do not send too fast - self._progressCallback(sdFilename, float(f.tell()) / float(size)) - finally: - self._comm.endSdFileTransfer(sdFilename) - self._finishCallback(sdFilename) - class StateMonitor(object): def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): self._ratelimit = ratelimit From 20df2b330bb9c22e79f8f700771510f8a463fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 20 Oct 2013 14:39:38 +0200 Subject: [PATCH 7/9] Preparing official "release" of a first rc to help in packaging --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8d2bc6cc..9297d050 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages -VERSION = "0.1.0" +VERSION = "1.0.0-rc1" def params(): name = "OctoPrint" From 204128e1ca8620045587ec4511387cb55cccbadb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 10 Nov 2013 18:55:36 +0100 Subject: [PATCH 8/9] Fixed #296 (cherry picked from commit fdfa433) --- octoprint/templates/settings.jinja2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/octoprint/templates/settings.jinja2 b/octoprint/templates/settings.jinja2 index 8c32f6bb..2c5b8348 100644 --- a/octoprint/templates/settings.jinja2 +++ b/octoprint/templates/settings.jinja2 @@ -260,13 +260,13 @@ °C
- +
- +
@@ -286,13 +286,13 @@
- +
- +
From 2f3c3c0c202ffd7662011d96c20074c380e99831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 20 Nov 2013 20:18:42 +0100 Subject: [PATCH 9/9] Properly calculate time deltas (forgot the days) Fixes #313 --- octoprint/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint/util/__init__.py b/octoprint/util/__init__.py index ff6e5924..8ffd953f 100644 --- a/octoprint/util/__init__.py +++ b/octoprint/util/__init__.py @@ -28,7 +28,7 @@ def isAllowedFile(filename, extensions): def getFormattedTimeDelta(d): if d is None: return None - hours = d.seconds // 3600 + hours = d.days * 24 + d.seconds // 3600 minutes = (d.seconds % 3600) // 60 seconds = d.seconds % 60 return "%02d:%02d:%02d" % (hours, minutes, seconds)