From 11bdc176a0be87bd70dfbc93e9d8bc7b0305a63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 12:08:25 +0200 Subject: [PATCH 1/9] Fix minute comparison for dumb estimate fallback Probably solves the core reason that caused #1428 to be reported in the first place, totally inaccurate early linear approximations (when nothing better is available) making it to the user. It should now display "Calcuting..." until the approximation stabilizes OR the configured max percentage or max time without an estimate are reached. --- src/octoprint/printer/standard.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 229adcd2..83b81a9f 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -761,15 +761,16 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): printTimeLeft = dumbTotalPrintTime - cleanedPrintTime printTimeLeftOrigin = "linear" - elif progress > self._timeEstimationForceDumbFromPercent or \ - cleanedPrintTime * 60 >= self._timeEstimationForceDumbAfterMin: - # more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/ - printTimeLeft = dumbTotalPrintTime - cleanedPrintTime + else: printTimeLeftOrigin = "linear" + if progress > self._timeEstimationForceDumbFromPercent or \ + cleanedPrintTime >= self._timeEstimationForceDumbAfterMin * 60: + # more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/ + printTimeLeft = dumbTotalPrintTime - cleanedPrintTime - if printTimeLeft < 0: + if printTimeLeft is not None and printTimeLeft < 0: # shouldn't actually happen, but let's make sure - return None, None + printTimeLeft = None return printTimeLeft, printTimeLeftOrigin From cd736c44dfee932d550cb1c9b47d6fda1d41ed8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 12:08:58 +0200 Subject: [PATCH 2/9] Improved fuzzy print time display --- src/octoprint/static/js/app/helpers.js | 84 +++++++++++++++++++ .../static/js/app/viewmodels/files.js | 2 +- .../static/js/app/viewmodels/printerstate.js | 10 +-- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 40924fe0..bbbc9bf4 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -387,6 +387,90 @@ function formatFuzzyEstimation(seconds, base) { return m.fromNow(true); } +function formatFuzzyPrintTime(totalSeconds) { + if (!totalSeconds || totalSeconds < 0) return "-"; + + var d = moment.duration(totalSeconds, "seconds"); + + var seconds = d.seconds(); + var minutes = d.minutes(); + var hours = d.hours(); + var days = d.days(); + + var replacements = { + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + totalSeconds: totalSeconds + }; + + var text = "-"; + + if (days >= 1) { + // days + if (hours >= 14) { + replacements.days += 1; + text = gettext("%(days)d days"); + } else if (hours > 10 && hours < 14) { + text = gettext("%(days)d.5 days"); + } else { + if (days == 1) { + text = gettext("%(days)d day"); + } else { + text = gettext("%(days)d days"); + } + } + } else if (days == 0 && hours >= 1) { + // only hours + if (hours == 1) { + // between 1 and 2 hours, slightly different rules than for other hours + if (minutes <= 10) { + text = gettext("1 hour"); + } else if (minutes > 10 && minutes <= 30) { + text = gettext("1.5 hours"); + } else { + text = gettext("2 hours"); + } + } else { + // over two hours + if (minutes < 15) { + text = gettext("%(hours)d hours"); + } else if (minutes >= 15 && minutes < 45) { + text = gettext("%(hours)d.5 hours"); + } else { + replacements.hours += 1; + text = gettext("%(hours)d hours"); + } + } + } else if (days == 0 && hours == 0 && minutes >= 1) { + // only minutes + if (minutes < 2) { + text = gettext("a minute"); + } else if (minutes < 30) { + if (seconds > 30) { + replacements.minutes += 1; + } + text = gettext("%(minutes)d minutes"); + } else if (minutes <= 40) { + text = gettext("40 minutes"); + } else if (minutes <= 50) { + text = gettext("50 minutes"); + } else { + text = gettext("1 hour"); + } + } else { + // only seconds + if (seconds < 30) { + text = gettext("a couple of seconds"); + } else { + text = gettext("less than a minute"); + } + } + + return _.sprintf(text, replacements); +} + function formatDate(unixTimestamp) { if (!unixTimestamp) return "-"; return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm")); diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index ef4ffe4b..829a4fd1 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -349,7 +349,7 @@ $(function() { } } } - output += gettext("Estimated Print Time") + ": " + formatDuration(data["gcodeAnalysis"]["estimatedPrintTime"]) + "
"; + output += gettext("Estimated Print Time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "
"; } if (data["prints"] && data["prints"]["last"]) { output += gettext("Last Printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "
"; diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index 8076c7ae..e4ff49fa 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -52,9 +52,9 @@ $(function() { self.estimatedPrintTimeString = ko.pureComputed(function() { if (self.lastPrintTime()) - return formatFuzzyEstimation(self.lastPrintTime()); + return formatFuzzyPrintTime(self.lastPrintTime()); if (self.estimatedPrintTime()) - return formatFuzzyEstimation(self.estimatedPrintTime()); + return formatFuzzyPrintTime(self.estimatedPrintTime()); return "-"; }); self.byteString = ko.pureComputed(function() { @@ -78,17 +78,17 @@ $(function() { if (!self.printTime() || !(self.isPrinting() || self.isPaused())) { return "-"; } else { - return gettext("Calculating..."); + return gettext("Still stabilizing..."); } } else { - return formatFuzzyEstimation(self.printTimeLeft()); + return formatFuzzyPrintTime(self.printTimeLeft()); } }); self.printTimeLeftOriginString = ko.pureComputed(function() { var value = self.printTimeLeftOrigin(); switch (value) { case "linear": { - return gettext("Based on a linear approximation (accuracy highly dependent on the model)"); + return gettext("Based on a linear approximation (very low accuracy, especially at the beginning of the print)"); } case "analysis": { return gettext("Based on the estimate from analysis of file (medium accuracy)"); From 7772fdc7bfcb9ef2cd9c30342cbcd68856f05309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 12:09:13 +0200 Subject: [PATCH 3/9] Tooltips for state panel --- src/octoprint/templates/sidebar/state.jinja2 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/octoprint/templates/sidebar/state.jinja2 b/src/octoprint/templates/sidebar/state.jinja2 index a910caee..f0c2636f 100644 --- a/src/octoprint/templates/sidebar/state.jinja2 +++ b/src/octoprint/templates/sidebar/state.jinja2 @@ -1,15 +1,16 @@ -{{ _('Machine State') }}:
+{{ _('State') }}:

-{{ _('File') }}:  (SD)
-{{ _('Timelapse') }}:
+{{ _('File') }}:  (SD)
+{{ _('Timelapse') }}:
-
+
-{{ _('Approx. Total Print Time') }}:
+{{ _('Approx. Total Print Time') }}:

-{{ _('Print Time') }}:
-{{ _('Print Time Left') }}:
-{{ _('Printed') }}:
+{{ _('Print Time') }}:
+{{ _('Print Time Left') }}:
+{{ _('Printed') }}:
+
From 5bef0926a63cd9d3a3a23d9414ae6946af90c777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 14:10:54 +0200 Subject: [PATCH 4/9] Add [ and ] to valid chars in filenames See #1434 --- src/octoprint/filemanager/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index f3d4dd5f..fc0f6034 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -313,7 +313,7 @@ class LocalFileStorage(StorageInterface): from slugify import Slugify self._slugify = Slugify() - self._slugify.safe_chars = "-_.() " + self._slugify.safe_chars = "-_.()[] " self._old_metadata = None self._initialize_metadata() From e8cac14c2f138501f23d38bda619fd460ac667c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 14:11:34 +0200 Subject: [PATCH 5/9] Make sure uploads folder only has sanitized entries Should make sure stuff doesn't break when people perform manual file operations on the uploads folder (e.g. uploading files that don't match the sanitization scheme). Should solve #1434 --- src/octoprint/filemanager/storage.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index fc0f6034..b4578fff 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -10,6 +10,7 @@ import logging import os import pylru import tempfile +import shutil import octoprint.filemanager @@ -895,6 +896,28 @@ class LocalFileStorage(StorageInterface): entry_path = os.path.join(path, entry) + sanitized = self.sanitize_name(entry) + if sanitized != entry: + # entry is not sanitized yet, let's take care of that + sanitized_path = os.path.join(path, sanitized) + sanitized_name, sanitized_ext = os.path.splitext(sanitized) + + counter = 1 + while os.path.exists(sanitized_path): + counter += 1 + sanitized = self.sanitize_name("{}_({}){}".format(sanitized_name, counter, sanitized_ext)) + sanitized_path = os.path.join(path, sanitized) + + try: + shutil.move(entry_path, sanitized_path) + + self._logger.info("Sanitized \"{}\" to \"{}\"".format(entry_path, sanitized_path)) + entry = sanitized + entry_path = sanitized_path + except: + self._logger.exception("Error while trying to rename \"{}\" to \"{}\", ignoring file".format(entry_path, sanitized_path)) + continue + # file handling if os.path.isfile(entry_path): file_type = octoprint.filemanager.get_file_type(entry) From 39372a947579d51f3f21cfb4d7b3fca928106408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 14:15:13 +0200 Subject: [PATCH 6/9] Also sanitize leading . That way hidden files that are uploaded will be "de-hidden". Note that NO "de-hiding" of existing files will take place! --- src/octoprint/filemanager/storage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index b4578fff..0272dc16 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -624,7 +624,11 @@ class LocalFileStorage(StorageInterface): if "/" in name or "\\" in name: raise ValueError("name must not contain / or \\") - return self._slugify(name).replace(" ", "_") + result = self._slugify(name).replace(" ", "_") + if result.startswith("."): + # hidden files under *nix + result = result[1:] + return result def sanitize_path(self, path): """ From d81ec15f43de0a3983b0b436dc2be097ed1b43fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 15:42:06 +0200 Subject: [PATCH 7/9] Improvements in fuzzy print time algorithm Also use new fuzzy print time in gcode viewer to have some consistency. --- src/octoprint/static/js/app/helpers.js | 71 +++++++++++++------ .../static/js/app/viewmodels/gcode.js | 4 +- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index bbbc9bf4..82e5f343 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -363,7 +363,7 @@ function bytesFromSize(size) { function formatDuration(seconds) { if (!seconds) return "-"; - if (seconds < 0) return "00:00:00"; + if (seconds < 1) return "00:00:00"; var s = seconds % 60; var m = (seconds % 3600) / 60; @@ -373,8 +373,7 @@ function formatDuration(seconds) { } function formatFuzzyEstimation(seconds, base) { - if (!seconds) return "-"; - if (seconds < 0) return "-"; + if (!seconds || seconds < 1) return "-"; var m; if (base != undefined) { @@ -388,14 +387,30 @@ function formatFuzzyEstimation(seconds, base) { } function formatFuzzyPrintTime(totalSeconds) { - if (!totalSeconds || totalSeconds < 0) return "-"; + /** + * Formats a print time estimate in a very fuzzy way. + * + * Accuracy decreases the higher the estimation is: + * + * * less than 30s: "a couple of seconds" + * * 30s to a minute: "less than a minute" + * * 1 to 30min: rounded to full minutes, above 30s is minute + 1 ("27 minutes", "2 minutes") + * * 30min to 40min: "40 minutes" + * * 40min to 50min: "50 minutes" + * * 50min to 1h: "1 hour" + * * 1 to 12h: rounded to half hours, 15min to 45min is ".5", above that hour + 1 ("4 hours", "2.5 hours") + * * 12 to 24h: rounded to full hours, above 30min is hour + 1, over 23.5h is "1 day" + * * Over a day: rounded to half days, 8h to 16h is ".5", above that days + 1 ("1 day", "4 days", "2.5 days") + */ + + if (!totalSeconds || totalSeconds < 1) return "-"; var d = moment.duration(totalSeconds, "seconds"); var seconds = d.seconds(); var minutes = d.minutes(); var hours = d.hours(); - var days = d.days(); + var days = d.asDays(); var replacements = { days: days, @@ -409,10 +424,10 @@ function formatFuzzyPrintTime(totalSeconds) { if (days >= 1) { // days - if (hours >= 14) { + if (hours >= 16) { replacements.days += 1; text = gettext("%(days)d days"); - } else if (hours > 10 && hours < 14) { + } else if (hours >= 8 && hours < 16) { text = gettext("%(days)d.5 days"); } else { if (days == 1) { @@ -421,32 +436,44 @@ function formatFuzzyPrintTime(totalSeconds) { text = gettext("%(days)d days"); } } - } else if (days == 0 && hours >= 1) { + } else if (hours >= 1) { // only hours - if (hours == 1) { - // between 1 and 2 hours, slightly different rules than for other hours - if (minutes <= 10) { - text = gettext("1 hour"); - } else if (minutes > 10 && minutes <= 30) { - text = gettext("1.5 hours"); - } else { - text = gettext("2 hours"); - } - } else { - // over two hours + if (hours < 12) { if (minutes < 15) { - text = gettext("%(hours)d hours"); + // less than .15 => .0 + if (hours == 1) { + text = gettext("%(hours)d hour"); + } else { + text = gettext("%(hours)d hours"); + } } else if (minutes >= 15 && minutes < 45) { + // between .25 and .75 => .5 text = gettext("%(hours)d.5 hours"); } else { + // over .75 => hours + 1 replacements.hours += 1; text = gettext("%(hours)d hours"); } + } else { + if (hours == 23 && minutes > 30) { + // over 23.5 hours => 1 day + text = gettext("1 day"); + } else { + if (minutes > 30) { + // over .5 => hours + 1 + replacements.hours += 1; + } + text = gettext("%(hours)d hours"); + } } - } else if (days == 0 && hours == 0 && minutes >= 1) { + } else if (minutes >= 1) { // only minutes if (minutes < 2) { - text = gettext("a minute"); + if (seconds < 30) { + text = gettext("a minute"); + } else { + text = gettext("2 minutes"); + } } else if (minutes < 30) { if (seconds > 30) { replacements.minutes += 1; diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index c7aabdd1..f1aac086 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -435,7 +435,7 @@ $(function() { } else { var output = []; output.push(gettext("Model size") + ": " + model.width.toFixed(2) + "mm × " + model.depth.toFixed(2) + "mm × " + model.height.toFixed(2) + "mm"); - output.push(gettext("Estimated total print time") + ": " + formatFuzzyEstimation(model.printTime)); + output.push(gettext("Estimated total print time") + ": " + formatFuzzyPrintTime(model.printTime)); output.push(gettext("Estimated layer height") + ": " + model.layerHeight.toFixed(2) + gettext("mm")); output.push(gettext("Layer count") + ": " + model.layersPrinted.toFixed(0) + " " + gettext("printed") + ", " + model.layersTotal.toFixed(0) + " " + gettext("visited")); @@ -472,7 +472,7 @@ $(function() { } } } - output.push(gettext("Print time for layer") + ": " + formatFuzzyEstimation(layer.printTime)); + output.push(gettext("Print time for layer") + ": " + formatFuzzyPrintTime(layer.printTime)); self.ui_layerInfo(output.join("
")); From b05beb27b5005592a15fd1c15ac44296e14c4ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2016 16:13:35 +0200 Subject: [PATCH 8/9] Don't sanitize . or .. folder names --- src/octoprint/filemanager/storage.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 0272dc16..5041a804 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -625,7 +625,7 @@ class LocalFileStorage(StorageInterface): raise ValueError("name must not contain / or \\") result = self._slugify(name).replace(" ", "_") - if result.startswith("."): + if result and result != "." and result != ".." and result[0] == ".": # hidden files under *nix result = result[1:] return result @@ -636,8 +636,11 @@ class LocalFileStorage(StorageInterface): relative path elements (e.g. ``..``) and sanitizes folder names using :func:`sanitize_name`. Final path is the absolute path including leading ``basefolder`` path. """ - if path[0] == "/" or path[0] == ".": + if path[0] == "/": path = path[1:] + elif path[0] == "." and path[1] == "/": + path = path[2:] + path_elements = path.split("/") joined_path = self.basefolder for path_element in path_elements: From 217e54d81cf1844e768181ef64751eef2398f96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 16 Aug 2016 09:29:29 +0200 Subject: [PATCH 9/9] Make sure we are still in printing state before sending next line We might just have "eaten" a line used for triggering a pause (e.g. an M0) and thus might not be eligible to send the next line from the streamed file anymore. Solves #1448 --- src/octoprint/util/comm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 835887ae..7b52046c 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -1672,6 +1672,10 @@ class MachineCom(object): with self._sendNextLock: while self._active: # we loop until we've actually enqueued a line for sending + if self._state != self.STATE_PRINTING: + # we are no longer printing, return false + return False + line = self._getNext() if line is None: # end of file, return false