diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..d4c79c2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,60 @@ +Please read the "guidelines for contributing" that are linked ^-- just +up there. Also read the FAQ: https://github.com/foosel/OctoPrint/wiki/FAQ. + +This is a bug and feature tracker, please only use it to report bugs +or request features within OctoPrint (not OctoPi, not any OctoPrint +plugins and not unofficial OctoPrint versions). Mark requests with +a [Request] prefix in the title please. Fully fill out the bug reporting +template for bug reports. + +Do not seek support here ("I need help with ..."), that belongs on +the mailing list or the G+ community (both linked in the "guidelines +for contributing" linked above, read it!), NOT here. + +Thank you! + +---- + +#### What were you doing? + +[Please be as specific as possible here. The maintainers will need to reproduce +your issue in order to fix it and that is not possible if they don't know +what you did to get it to happen in the first place. If you encountered +a problem with specific files of any sorts, make sure to also include a link to a file +with which to reproduce the problem.] + +#### What did you expect to happen? + +#### What happened instead? + +#### Branch & Commit or Version of OctoPrint + +[Can be found in the lower left corner of the web interface.] + +#### Printer model & used firmware incl. version + +[If applicable, always include if unsure.] + +#### Browser and Version of Browser, Operating System running Browser + +[If applicable, always include if unsure.] + +#### Link to octoprint.log + +[On gist.github.com or pastebin.com. Always include and never truncate.] + +#### Link to contents of terminal tab or serial.log + +[On gist.github.com or pastebin.com. If applicable, always include if unsure or +reporting communication issues. Never truncate.] + +#### Link to contents of Javascript console in the browser + +[On gist.github.com or pastebin.com or alternatively a screenshot. If applicable - +always include if unsure or reporting UI issues.] + +#### Screenshot(s) showing the problem: + +[If applicable. Always include if unsure or reporting UI issues.] + +I have read the FAQ. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..b4d5c417 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,50 @@ +Thank you for your interest into contributing to OctoPrint, it's +highly appreciated! + +Please make sure you have read the "guidelines for contributing" as +linked just above this form, there's a section on Pull Requests in there +as well that contains important information. + +As a summary, please make sure you have ticked all points on this +checklist: + + * [ ] Your changes are not possible to do through a plugin and relevant + to a large audience (ideally all users of OctoPrint) + * [ ] If your changes are large or otherwise disruptive: You have + made sure your changes don't interfere with current development by + talking it through with the maintainers, e.g. through a + Brainstorming ticket + * [ ] Your PR targets OctoPrint's devel branch (not master, + maintenance or anything else) + * [ ] Your PR was opened from a custom branch on your repository + (no PRs from your version of master, maintenance or devel please), + e.g. dev/my_new_feature + * [ ] Your PR only contains relevant changes: no unrelated files, + no dead code, ideally only one commit - rebase your PR if necessary! + * [ ] Your changes follow the coding style + * [ ] If your changes include style sheets: You have modified the + .less source files, not the .css files (those are generated with + lessc) + * [ ] You have tested your changes (please state how!) - ideally you + have added unit tests + * [ ] You have run the existing unit tests against your changes and + nothing broke + * [ ] You have added yourself to the AUTHORS.md file :) + +Feel free to delete all this help text, then describe +your PR further. You may use the template provided below to do that. +The more details the better! + +---- + +#### What does this PR do and why is it necessary? + +#### How was it tested? How can it be tested by the reviewer? + +#### Any background context you want to provide? + +#### What are the relevant tickets if any? + +#### Screenshots (if appropriate) + +#### Further notes diff --git a/.versioneer-lookup b/.versioneer-lookup index 3172981e..148123e0 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -14,11 +14,11 @@ master HEAD \(detached.* -# maintenance is currently the branch for preparation of maintenance release 1.2.10 +# maintenance is currently the branch for preparation of maintenance release 1.2.11 # so are any fix/... and improve/... branches -maintenance 1.2.10 abe68adac8f465a31bf4f3f3933190c1fc242cda pep440-dev -fix/.* 1.2.10 abe68adac8f465a31bf4f3f3933190c1fc242cda pep440-dev -improve/.* 1.2.10 abe68adac8f465a31bf4f3f3933190c1fc242cda pep440-dev +maintenance 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev +fix/.* 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev +improve/.* 1.2.11 692166f067329cd3d6fdc84389e0dd76184c5e0c pep440-dev # every other branch is a development branch and thus gets resolved to 1.3.0-dev for now .* 1.3.0 198d3450d94be1a2 pep440-dev diff --git a/AUTHORS.md b/AUTHORS.md index f3bbce2c..0323d8f2 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -60,6 +60,7 @@ date of first contribution): * ["bwgan"](https://github.com/bwgan) * [Siim Raud](https://github.com/2ndalpha) * ["geoporalis"](https://github.com/geoporalis) + * [Andrew Malota](https://github.com/2bitoperations) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and diff --git a/CHANGELOG.md b/CHANGELOG.md index 110478f5..53c88959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,34 @@ * [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree webcam rotation for iOS Safari. +## 1.2.10 (2016-03-16) + +### Improvements + + * Improved performance of console output during plugin installation/deinstallation + * Slight performance improvements in the communication layer + * Log small log excerpt to `octoprint.log` upon encountering a communication error. + * Changed wording in "firmware error" notifications to better reflect that there was an error while communicating with the printer, since the error condition can also be triggered by serial errors while trying to establish a connection to the printer or when already connected. + * Support downloading ".mp4" timelapse files. You'll need a [custom wrapper script for timelapse rendering](https://github.com/guysoft/OctoPi/issues/184) for this to be relevant to you. See also [#1255](https://github.com/foosel/OctoPrint/pull/1255) + * The communication layer will now wait up to 10s after clicking disconnect in order to send any left-over lines from its buffers. + * Moved less commonly used configuration options in Serial settings into "Advanced options" roll-out. + +### Bug Fixes + + * [#1224](https://github.com/foosel/OctoPrint/issues/1224) - Fixed an issue introduced by the fix for [#1196](https://github.com/foosel/OctoPrint/issues/1196) that had the "Upload to SD" button stop working correctly. + * [#1226](https://github.com/foosel/OctoPrint/issues/1226) - Fixed an issue causing an error on disconnect after or cancelling of an SD print, caused by the unsuccessful attempt to record print recovery data for the file on the printer's SD card. + * [#1268](https://github.com/foosel/OctoPrint/issues/1268) - Only add bed temperature line to temperature management specific start gcode in CuraEngine invocation if a bed temperature is actually set in the slicing profile. + * [#1271](https://github.com/foosel/OctoPrint/issues/1271) - If a communication timeout occurs during an active resend request, OctoPrint will now not send an `M105` with an increased line number anymore but repeat the last resent command instead. + * [#1272](https://github.com/foosel/OctoPrint/issues/1272) - Don't add an extra `ok` for `M28` response. + * [#1273](https://github.com/foosel/OctoPrint/issues/1273) - Add an extra `ok` for `M29` response, but only if configured such in "Settings" > "Serial" > "Advanced options" > "Generate additional ok for M29" + * [#1274](https://github.com/foosel/OctoPrint/issues/1274) - Trigger `M20` only once after finishing uploading to SD + * [#1275](https://github.com/foosel/OctoPrint/issues/1275) - Prevent `M105` "cascade" due to communication timeouts + * Fixed wrong tracking of extruder heating up for `M109 Tn` commands in multi-extruder setups. + * Fixed start of SD file uploads not sending an `M110`. + * Fixed job data not being reset when disconnecting while printing. + +([Commits](https://github.com/foosel/OctoPrint/compare/1.2.9...1.2.10)) + ## 1.2.9 (2016-02-10) ### Improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e849c97..a43ece0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,12 +106,26 @@ following section *completely*. Thank you! :) fixing it. Take the time to filter through possible duplicates and be really sure that your problem definitely is a new one. Try more than one search query (e.g. do not only search for "webcam" if you happen to run into an issue - with your webcam, also search for "timelapse" etc). + with your webcam, also search for "timelapse" etc). Do not only read the subject lines + of tickets that look like they might be related, but also read the ticket itself! + + **Very important:** Please make absolutely sure that if you find a bug that looks like + it is the same as your's, it actually behaves the same as your's. E.g. if someone gives steps + to reproduce his bug that looks like your's, reproduce the bug like that if possible, + and only add a "me too" if you actually can reproduce the same + issue. Also **provide all information** as [described below](#what-should-i-include-in-a-bug-report) + and whatever was additionally requested over the course of the ticket + even if you "only" add to an existing ticket. The more information available regarding a bug, the higher + the chances of reproducing and solving it. But "me too" on an actually unrelated ticket + makes it more difficult due to on top of having to figure out the original problem + there's now also a [red herring](https://en.wikipedia.org/wiki/Red_herring) interfering - so please be + very diligent here! ### What should I include in a bug report? Always use the following template (you can remove what's within `[...]`, that's -only provided here as some additional information for you): +only provided here as some additional information for you), **even if only adding a +"me too" to an existing ticket**: #### What were you doing? @@ -157,6 +171,8 @@ only provided here as some additional information for you): I have read the FAQ. +Copy-paste this template **completely**. Do not skip any lines! + ### Where can I find which version and branch I'm on? You can find out all of them by taking a look into the lower left corner of the @@ -216,43 +232,67 @@ See [How to open the Javascript Console in different browsers](https://webmaster implement your feature as a plugin, create a "Brainstorming" ticket to get the discussion going on how best to solve *this* in OctoPrint's plugin system - maybe that's the actual PR you have been waiting for to contribute :) -2. If you plan to make **any large changes to the code or appearance, please - open a "Brainstorming" ticket first** so that we can determine if it's a - good time for your specific pull request. It might be that we're currently - in the process of making heavy changes to the code locations you'd target - as well, or your approach doesn't fit the general "project vision", and - that would just cause unnecessary work and frustration for everyone or +2. If you plan to make **any large or otherwise disruptive changes to the + code or appearance, please open a "Brainstorming" ticket first** so + that we can determine if it's a good time for your specific pull + request. It might be that we're currently in the process of making + heavy changes to the code locations you'd target as well, or your + approach doesn't fit the general "project vision", and that would + just cause unnecessary work and frustration for everyone or possibly get the PR rejected. 3. Create your pull request **from a custom branch** on your end (e.g. `dev/myNewFeature`)[1] **against the `devel` branch**. Create **one pull request per feature/bug fix**. If your PR contains an important bug fix, we will make sure to backport it to the `maintenance` branch to also include it in the next release. -4. Make sure you **follow the current coding style**. This means: - +4. Make sure there are **only relevant changes** included in your PR. No + changes to unrelated files, no additional files that don't belong (e.g. + commits of your full virtual environment). Make sure your PR consists + **ideally of only one commit** (use git's rebase and squash functionality). +5. Make sure you **follow the current coding style**. This means: * Tabs instead of spaces in the Python files[2] * Spaces instead of tabs in the Javascript sources * English language (code, variables, comments, ...) * Comments where necessary: Tell *why* the code does something like it does it, structure your code * Following the general architecture - - If your PR needs to make changes to the Stylesheets, change the ``.less`` files - from which the CSS is compiled. -5. **Test your changes thoroughly**. That also means testing with usage + * If your PR needs to make changes to the Stylesheets, change the + ``.less`` files from which the CSS is compiled. + * Make sure you do not add dead code (e.g. commented out left-overs + from experiments). +6. Ensure your changes **pass the existing unit tests**. PRs that break + those cannot be accepted. +7. **Test your changes thoroughly**. That also means testing with usage scenarios you don't normally use, e.g. if you only use access control, test without and vice versa. If you only test with your printer, test with the virtual printer and vice versa. State in your pull request how you tested your changes. Ideally **add unit tests** - OctoPrint severely lacks in that department, but we are trying to change that, so any new code already covered with a test suite helps a lot! -6. In your pull request's description, **state what your pull request does**, +8. In your pull request's description, **state what your pull request does**, as in, what feature does it implement, what bug does it fix. The more thoroughly you explain your intent behind the PR here, the higher the - chances it will get merged fast. -7. Important: Don't forget to **add yourself to the [AUTHORS](./AUTHORS.md) + chances it will get merged fast. There is a template provided below + that can help you here. +9. Don't forget to **add yourself to the [AUTHORS](./AUTHORS.md) file** :) +Template to use for Pull Request descriptions: + +``` +#### What does this PR do and why is it necessary? + +#### How was it tested? How can it be tested by the reviewer? + +#### Any background context you want to provide? + +#### What are the relevant tickets if any? + +#### Screenshots (if appropriate) + +#### Further notes +``` + ## What do the branches mean? There are three main branches in OctoPrint: @@ -324,6 +364,9 @@ the local version identifier to allow for an exact determination of the active c * 2015-12-01: Heavily reworked to include examples, better structure and all information in one document. * 2016-02-10: Added information about branch structure and versioning. + * 2016-02-16: Added requirement to add information from template to existing + tickets as well, explained issue with "me too" red herrings. + * 2016-03-14: Some more requirements for PRs, and a PR template. ## Footnotes * [1] - If you are wondering why, the problem is that anything that you add diff --git a/src/octoprint/plugins/cura/profile.py b/src/octoprint/plugins/cura/profile.py index 61072336..15fdc53e 100644 --- a/src/octoprint/plugins/cura/profile.py +++ b/src/octoprint/plugins/cura/profile.py @@ -50,7 +50,7 @@ defaults = dict( wall_thickness=0.8, solid_layer_thickness=0.6, print_temperature=[220, 0, 0, 0], - print_bed_temperature=70, + print_bed_temperature=0, platform_adhesion=PlatformAdhesionTypes.NONE, filament_diameter=[2.85, 0, 0, 0], filament_flow=100.0, diff --git a/src/octoprint/plugins/virtual_printer/virtual.py b/src/octoprint/plugins/virtual_printer/virtual.py index e77b6a76..8dd1528b 100644 --- a/src/octoprint/plugins/virtual_printer/virtual.py +++ b/src/octoprint/plugins/virtual_printer/virtual.py @@ -72,6 +72,7 @@ class VirtualPrinter(object): self._selectedSdFileSize = None self._selectedSdFilePos = None self._writingToSd = False + self._writingToSdHandle = None self._newSdFilePos = None self._heatupThread = None @@ -84,6 +85,8 @@ class VirtualPrinter(object): self._echoOnM117 = settings().getBoolean(["devel", "virtualPrinter", "echoOnM117"]) + self._brokenM29 = settings().getBoolean(["devel", "virtualPrinter", "brokenM29"]) + self.currentLine = 0 self.lastN = 0 @@ -100,6 +103,10 @@ class VirtualPrinter(object): self._killed = False + self._triggerResendAt100 = True + self._triggerResendWithTimeoutAt105 = True + self._triggeredResendWithTimeoutAt105 = False + readThread = threading.Thread(target=self._processIncoming) readThread.start() @@ -154,6 +161,10 @@ class VirtualPrinter(object): linenumber = int(re.search("N([0-9]+)", data).group(1)) self.lastN = linenumber self.currentLine = linenumber + + self._triggerResendAt100 = True + self._triggerResendWithTimeoutAt105 = True + self._sendOk() continue elif data.startswith("N"): @@ -162,10 +173,18 @@ class VirtualPrinter(object): if linenumber != expected: self._triggerResend(actual=linenumber) continue - elif self.currentLine == 101: + elif linenumber == 100 and self._triggerResendAt100: # simulate a resend at line 100 + self._triggerResendAt100 = False self._triggerResend(expected=100) continue + elif linenumber == 105 and self._triggerResendWithTimeoutAt105 and not self._writingToSd: + # simulate a resend with timeout at line 105 + self._triggerResendWithTimeoutAt105 = False + self._triggerResend(expected=105) + self._dont_answer = True + self.lastN = linenumber + continue else: self.lastN = linenumber data = data.split(None, 1)[1].strip() @@ -173,9 +192,8 @@ class VirtualPrinter(object): data += "\n" # shortcut for writing to SD - if self._writingToSd and not self._selectedSdFile is None and not "M29" in data: - with open(self._selectedSdFile, "a") as f: - f.write(data) + if self._writingToSdHandle is not None and not "M29" in data: + self._writingToSdHandle.write(data) self._sendOk() continue @@ -696,60 +714,89 @@ class VirtualPrinter(object): else: self._send("error writing to file") + handle = None + try: + handle = open(file, "w") + except: + self.outgoing.put("error writing to file") + if handle is not None: + try: + handle.close() + except: + pass + self._writingToSdHandle = handle self._writingToSd = True self._selectedSdFile = file self._send("Writing to file: %s" % filename) def _finishSdFile(self): + try: + self._writingToSdHandle.close() + except: + pass + finally: + self._writingToSdHandle = None self._writingToSd = False self._selectedSdFile = None self.outgoing.put("Done saving file") def _sdPrintingWorker(self): self._selectedSdFilePos = 0 - with open(self._selectedSdFile, "r") as f: - for line in iter(f.readline, ""): - if self._killed: - break + try: + with open(self._selectedSdFile, "r") as f: + for line in iter(f.readline, ""): + if self._killed: + break - # reset position if requested by client - if self._newSdFilePos is not None: - f.seek(self._newSdFilePos) - self._newSdFilePos = None + # reset position if requested by client + if self._newSdFilePos is not None: + f.seek(self._newSdFilePos) + self._newSdFilePos = None - # read current file position - self._selectedSdFilePos = f.tell() + # read current file position + self._selectedSdFilePos = f.tell() - # if we are paused, wait for unpausing - self._sdPrintingSemaphore.wait() + # if we are paused, wait for unpausing + self._sdPrintingSemaphore.wait() - # set target temps - if 'M104' in line or 'M109' in line: - self._parseHotendCommand(line) - if 'M140' in line or 'M190' in line: - self._parseBedCommand(line) + # set target temps + if 'M104' in line or 'M109' in line: + self._parseHotendCommand(line) + elif 'M140' in line or 'M190' in line: + self._parseBedCommand(line) + elif line.startswith("G0") or line.startswith("G1") or line.startswith("G2") or line.startswith("G3"): + # simulate reprap buffered commands via a Queue with maxsize which internally simulates the moves + self.buffered.put(line) - time.sleep(settings().getFloat(["devel", "virtualPrinter", "throttle"])) + except AttributeError: + if self.outgoing is not None: + raise - self._sdPrintingSemaphore.clear() - self._selectedSdFilePos = 0 - self._sdPrinter = None - self._send("Done printing file") + if not self._killed: + self._sdPrintingSemaphore.clear() + self._selectedSdFilePos = 0 + self._sdPrinter = None + self.outgoing.put("Done printing file") def _waitForHeatup(self, heater): delta = 1 delay = 1 - if heater.startswith("tool"): - toolNum = int(heater[len("tool"):]) - while not self._killed and (self.temp[toolNum] < self.targetTemp[toolNum] - delta or self.temp[toolNum] > self.targetTemp[toolNum] + delta): - self._simulateTemps(delta=delta) - self._send("T:%0.2f" % self.temp[toolNum]) - time.sleep(delay) - elif heater == "bed": - while not self._killed and (self.bedTemp < self.bedTargetTemp - delta or self.bedTemp > self.bedTargetTemp + delta): - self._simulateTemps(delta=delta) - self._send("B:%0.2f" % self.bedTemp) - time.sleep(delay) + + try: + if heater.startswith("tool"): + toolNum = int(heater[len("tool"):]) + while not self._killed and (self.temp[toolNum] < self.targetTemp[toolNum] - delta or self.temp[toolNum] > self.targetTemp[toolNum] + delta): + self._simulateTemps(delta=delta) + self.outgoing.put("T:%0.2f" % self.temp[toolNum]) + time.sleep(delay) + elif heater == "bed": + while not self._killed and (self.bedTemp < self.bedTargetTemp - delta or self.bedTemp > self.bedTargetTemp + delta): + self._simulateTemps(delta=delta) + self.outgoing.put("B:%0.2f" % self.bedTemp) + time.sleep(delay) + except AttributeError: + if self.outgoing is not None: + raise def _deleteSdFile(self, filename): if filename.startswith("/"): @@ -817,13 +864,13 @@ class VirtualPrinter(object): try: line = self.outgoing.get(timeout=self._read_timeout) - time.sleep(settings().getFloat(["devel", "virtualPrinter", "throttle"])) self._seriallog.info(">>> {}".format(line.strip())) return line except Queue.Empty: return "" def close(self): + self._killed = True self.incoming = None self.outgoing = None self.buffered = None diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 3147403b..e5929960 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -17,7 +17,7 @@ import time from octoprint import util as util from octoprint.events import eventManager, Events -from octoprint.filemanager import FileDestinations +from octoprint.filemanager import FileDestinations, NoSuchStorage from octoprint.plugin import plugin_manager, ProgressPlugin from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript from octoprint.printer.estimation import TimeEstimationHelper @@ -62,10 +62,6 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._currentZ = None - self._progress = None - self._printTime = None - self._printTimeLeft = None - self._printAfterSelect = False self._posAfterSelect = None @@ -93,7 +89,8 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): on_update=self._sendCurrentDataCallbacks, on_add_temperature=self._sendAddTemperatureCallbacks, on_add_log=self._sendAddLogCallbacks, - on_add_message=self._sendAddMessageCallbacks + on_add_message=self._sendAddMessageCallbacks, + on_get_progress=self._updateProgressDataCallback ) self._stateMonitor.reset( state={"text": self.get_state_string(), "flags": self._getStateFlags()}, @@ -366,7 +363,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._printAfterSelect = printAfterSelect self._posAfterSelect = pos self._comm.selectFile("/" + path if sd else path, sd) - self._setProgressData(0, None, None, None) + self._setProgressData(completion=0) self._setCurrentZ(None) def unselect_file(self): @@ -374,7 +371,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): return self._comm.unselectFile() - self._setProgressData(0, None, None, None) + self._setProgressData(completion=0) self._setCurrentZ(None) def start_print(self, pos=None): @@ -405,7 +402,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._fileManager.delete_recovery_data() self._lastProgressReport = None - self._setProgressData(0, None, None, None) + self._setProgressData(completion=0) self._setCurrentZ(None) self._comm.startPrint(pos=pos) @@ -429,7 +426,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): # reset progress, height, print time self._setCurrentZ(None) - self._setProgressData(None, None, None, None) + self._setProgressData() # mark print as failure if self._selectedFile is not None: @@ -608,7 +605,24 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): return result - def _setProgressData(self, progress, filepos, printTime, cleanedPrintTime): + def _setProgressData(self, completion=None, filepos=None, printTime=None, printTimeLeft=None): + self._stateMonitor.set_progress(dict(completion=int(completion * 100) if completion is not None else None, + filepos=filepos, + printTime=int(printTime) if printTime is not None else None, + printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None)) + + def _updateProgressDataCallback(self): + if self._comm is None: + progress = None + filepos = None + printTime = None + cleanedPrintTime = None + else: + progress = self._comm.getPrintProgress() + filepos = self._comm.getPrintFilepos() + printTime = self._comm.getPrintTime() + cleanedPrintTime = self._comm.getCleanedPrintTime() + estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime) totalPrintTime = estimatedTotalPrintTime @@ -624,16 +638,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): sub_progress = 1.0 totalPrintTime = (1 - sub_progress) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime - self._progress = progress - self._printTime = printTime - self._printTimeLeft = totalPrintTime - cleanedPrintTime if (totalPrintTime is not None and cleanedPrintTime is not None) else None - - self._stateMonitor.set_progress({ - "completion": self._progress * 100 if self._progress is not None else None, - "filepos": filepos, - "printTime": int(self._printTime) if self._printTime is not None else None, - "printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None - }) + printTimeLeft = totalPrintTime - cleanedPrintTime if (totalPrintTime is not None and cleanedPrintTime is not None) else None if progress: progress_int = int(progress * 100) @@ -641,6 +646,10 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._lastProgressReport = progress_int self._reportPrintProgressToPlugins(progress_int) + return dict(completion=progress * 100 if progress is not None else None, + filepos=filepos, + printTime=int(printTime) if printTime is not None else None, + printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None) def _addTemperatureData(self, temp, bedTemp): currentTimeUtc = int(time.time()) @@ -795,11 +804,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._analysisQueue.resume() # printing done, put those cpu cycles to good use elif state == comm.MachineCom.STATE_PRINTING: self._analysisQueue.pause() # do not analyse files while printing - elif state == comm.MachineCom.STATE_CLOSED or state == comm.MachineCom.STATE_CLOSED_WITH_ERROR: + + if state == comm.MachineCom.STATE_CLOSED or state == comm.MachineCom.STATE_CLOSED_WITH_ERROR: if self._comm is not None: self._comm = None - self._setProgressData(0, None, None, None) + self._setProgressData(completion=0) self._setCurrentZ(None) self._setJobData(None, None, None) self._printerProfileManager.deselect() @@ -820,7 +830,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): Triggers storage of new values for printTime, printTimeLeft and the current progress. """ - self._setProgressData(self._comm.getPrintProgress(), self._comm.getPrintFilepos(), self._comm.getPrintTime(), self._comm.getCleanedPrintTime()) + self._stateMonitor.trigger_progress_update() def on_comm_z_change(self, newZ): """ @@ -850,7 +860,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): def on_comm_print_job_done(self): self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), True, self._printerProfileManager.get_current_or_default()["id"]) - self._setProgressData(1.0, self._selectedFile["filesize"], self._comm.getPrintTime(), 0) + self._setProgressData(completion=1.0, filepos=self._selectedFile["filesize"], printTime=self._comm.getPrintTime(), printTimeLeft=0) self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) self._fileManager.delete_recovery_data() @@ -858,7 +868,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._sdStreaming = True self._setJobData(filename, filesize, True) - self._setProgressData(0.0, 0, 0, None) + self._setProgressData(completion=0.0, filepos=0, printTime=0) self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) def on_comm_file_transfer_done(self, filename): @@ -871,22 +881,28 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): self._setCurrentZ(None) self._setJobData(None, None, None) - self._setProgressData(None, None, None, None) + self._setProgressData() self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) def on_comm_force_disconnect(self): self.disconnect() def on_comm_record_fileposition(self, origin, name, pos): - self._fileManager.save_recovery_data(origin, name, pos) + try: + self._fileManager.save_recovery_data(origin, name, pos) + except NoSuchStorage: + pass + except: + self._logger.exception("Error while trying to persist print recovery data") class StateMonitor(object): - def __init__(self, interval=0.5, on_update=None, on_add_temperature=None, on_add_log=None, on_add_message=None): + def __init__(self, interval=0.5, on_update=None, on_add_temperature=None, on_add_log=None, on_add_message=None, on_get_progress=None): self._interval = interval self._update_callback = on_update self._on_add_temperature = on_add_temperature self._on_add_log = on_add_log self._on_add_message = on_add_message + self._on_get_progress = on_get_progress self._state = None self._job_data = None @@ -895,16 +911,24 @@ class StateMonitor(object): self._current_z = None self._progress = None + self._progress_dirty = False + self._offsets = {} self._change_event = threading.Event() self._state_lock = threading.Lock() + self._progress_lock = threading.Lock() self._last_update = time.time() self._worker = threading.Thread(target=self._work) self._worker.daemon = True self._worker.start() + def _get_current_progress(self): + if callable(self._on_get_progress): + return self._on_get_progress() + return self._progress + def reset(self, state=None, job_data=None, progress=None, current_z=None): self.set_state(state) self.set_job_data(job_data) @@ -936,9 +960,16 @@ class StateMonitor(object): self._job_data = job_data self._change_event.set() + def trigger_progress_update(self): + with self._progress_lock: + self._progress_dirty = True + self._change_event.set() + def set_progress(self, progress): - self._progress = progress - self._change_event.set() + with self._progress_lock: + self._progress_dirty = False + self._progress = progress + self._change_event.set() def set_temp_offsets(self, offsets): self._offsets = offsets @@ -948,19 +979,24 @@ class StateMonitor(object): while True: self._change_event.wait() - with self._state_lock: - now = time.time() - delta = now - self._last_update - additional_wait_time = self._interval - delta - if additional_wait_time > 0: - time.sleep(additional_wait_time) + now = time.time() + delta = now - self._last_update + additional_wait_time = self._interval - delta + if additional_wait_time > 0: + time.sleep(additional_wait_time) + with self._state_lock: data = self.get_current_data() self._update_callback(data) self._last_update = time.time() self._change_event.clear() def get_current_data(self): + with self._progress_lock: + if self._progress_dirty: + self._progress = self._get_current_progress() + self._progress_dirty = False + return { "state": self._state, "job": self._job_data, diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index fd44d701..8937f35d 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -93,6 +93,7 @@ def getSettings(): "helloCommand": s.get(["serial", "helloCommand"]), "ignoreErrorsFromFirmware": s.getBoolean(["serial", "ignoreErrorsFromFirmware"]), "disconnectOnErrors": s.getBoolean(["serial", "disconnectOnErrors"]), + "triggerOkForM29": s.getBoolean(["serial", "triggerOkForM29"]) }, "folder": { "uploads": s.getBaseFolder("uploads"), @@ -246,6 +247,7 @@ def _saveSettings(data): if "helloCommand" in data["serial"]: s.set(["serial", "helloCommand"], data["serial"]["helloCommand"]) if "ignoreErrorsFromFirmware" in data["serial"]: s.setBoolean(["serial", "ignoreErrorsFromFirmware"], data["serial"]["ignoreErrorsFromFirmware"]) if "disconnectOnErrors" in data["serial"]: s.setBoolean(["serial", "disconnectOnErrors"], data["serial"]["disconnectOnErrors"]) + if "triggerOkForM29" in data["serial"]: s.setBoolean(["serial", "triggerOkForM29"], data["serial"]["triggerOkForM29"]) oldLog = s.getBoolean(["serial", "log"]) if "log" in data["serial"].keys(): s.setBoolean(["serial", "log"], data["serial"]["log"]) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 6b29ef12..3faa63ad 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -91,7 +91,11 @@ default_settings = { "checksumRequiringCommands": ["M110"], "helloCommand": "M110 N0", "disconnectOnErrors": True, - "ignoreErrorsFromFirmware": False + "ignoreErrorsFromFirmware": False, + "logResends": False, + + # command specific flags + "triggerOkForM29": True }, "server": { "host": "0.0.0.0", @@ -315,6 +319,7 @@ default_settings = { "waitInterval": 1.0, "supportM112": True, "echoOnM117": True, + "brokenM29": True, "supportF": False } } diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index 55bf1fdd..eb86cc7e 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -289,16 +289,16 @@ function DataUpdater(allViewModels) { } else if (type == "PrintCancelled") { if (payload.firmwareError) { new PNotify({ - title: gettext("Unhandled firmware error"), - text: _.sprintf(gettext("The firmware reported an unhandled error. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s"), payload), + title: gettext("Unhandled communication error"), + text: _.sprintf(gettext("There was an unhandled error while talking to the printer. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s"), payload), type: "error", hide: false }); } } else if (type == "Error") { new PNotify({ - title: gettext("Unhandled firmware error"), - text: _.sprintf(gettext("The firmware reported an unhandled error. Due to that OctoPrint disconnected. Error: %(error)s"), payload), + title: gettext("Unhandled communication error"), + text: _.sprintf(gettext("The was an unhandled error while talking to the printer. Due to that OctoPrint disconnected. Error: %(error)s"), payload), type: "error", hide: false }); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 3d6f63ae..3bb9242a 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -145,6 +145,7 @@ $(function() { self.serial_helloCommand = ko.observable(undefined); self.serial_ignoreErrorsFromFirmware = ko.observable(undefined); self.serial_disconnectOnErrors = ko.observable(undefined); + self.serial_triggerOkForM29 = ko.observable(undefined); self.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); diff --git a/src/octoprint/translations/de/LC_MESSAGES/messages.mo b/src/octoprint/translations/de/LC_MESSAGES/messages.mo index 22c641e2..dd2b7896 100644 Binary files a/src/octoprint/translations/de/LC_MESSAGES/messages.mo and b/src/octoprint/translations/de/LC_MESSAGES/messages.mo differ diff --git a/src/octoprint/translations/de/LC_MESSAGES/messages.po b/src/octoprint/translations/de/LC_MESSAGES/messages.po index edeec609..6a96ee7d 100644 --- a/src/octoprint/translations/de/LC_MESSAGES/messages.po +++ b/src/octoprint/translations/de/LC_MESSAGES/messages.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: OctoPrint\n" "Report-Msgid-Bugs-To: i18n@octoprint.org\n" -"POT-Creation-Date: 2016-02-10 11:12+0100\n" -"PO-Revision-Date: 2016-02-10 11:25+0100\n" +"POT-Creation-Date: 2016-03-16 09:14+0100\n" +"PO-Revision-Date: 2016-03-16 09:21+0100\n" "Last-Translator: Gina Häußge \n" "Language: de\n" "Language-Team: German (http://www.transifex.com/projects/p/octoprint/language/de/)\n" @@ -521,6 +521,7 @@ msgstr "Das sieht nicht aus wie ein valides Pluginarchiv. Valide Pluginarchive s #: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:182 #: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:63 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:96 #: src/octoprint/templates/tabs/terminal.jinja2:27 msgid "Advanced options" msgstr "Erweiterte Optionen" @@ -1041,18 +1042,18 @@ msgstr "%(local)s nach %(remote)s gestreamt, dauerte %(time).2f Sekunden" #: src/octoprint/static/js/app/dataupdater.js:352 #: src/octoprint/static/js/app/dataupdater.js:360 -msgid "Unhandled firmware error" -msgstr "Unbehandelter Fehler der Firmware" +msgid "Unhandled communication error" +msgstr "Unbehandelter Kommunikationsfehler" #: src/octoprint/static/js/app/dataupdater.js:353 #, python-format -msgid "The firmware reported an unhandled error. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s" -msgstr "Die Firmware hat einen durch OctoPrint unbehandelten Fehler gemeldet. Daher wurder der laufende Druckauftrag abgebrochen. Fehler: %(firmwareError)s" +msgid "There was an unhandled error while talking to the printer. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s" +msgstr "Es gab einen unbehandelten Fehler bei der Kommunikation mit dem Drucker. Daher wurder der laufende Druckauftrag abgebrochen. Fehler: %(firmwareError)s" #: src/octoprint/static/js/app/dataupdater.js:361 #, python-format -msgid "The firmware reported an unhandled error. Due to that OctoPrint disconnected. Error: %(error)s" -msgstr "Die Firmware hat einen durch OctoPrint unbehandelten Fehler gemeldet. Daher hat OctoPrint die Verbindung getrennt. Fehler: %(error)s" +msgid "The was an unhandled error while talking to the printer. Due to that OctoPrint disconnected. Error: %(error)s" +msgstr "Es gab einen unbehandelten Fehler bei der Kommunikation mit dem Drucker. Daher hat OctoPrint die Verbindung getrennt. Fehler: %(error)s" #: src/octoprint/static/js/app/helpers.js:385 #, python-format @@ -1923,7 +1924,7 @@ msgstr "Falls der freie Plattenplatz unter diese Schwellwerte fallen sollte wird #: src/octoprint/templates/dialogs/settings/folders.jinja2:47 #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:69 -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 #: src/octoprint/templates/tabs/gcodeviewer.jinja2:66 #: src/octoprint/templates/tabs/timelapse.jinja2:13 msgid "Warning" @@ -2132,30 +2133,66 @@ msgid "Log communication to serial.log (might negatively impact performance)" msgstr "Logge die Kommunikation in das serial.log (kann die Performance negativ beeinflussen)" #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:74 -msgid "Long running commands" -msgstr "Lang laufende Befehle" - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 -msgid "Use this to specify the commands known to take a long time to complete without output from your printer and hence might cause timeout issues. Just the G or M code, comma separated." -msgstr "Nutze diese Option, um solche Befehle zu definieren, von denen Du weißt, dass sie eine längere Zeit lang laufen, währenddessen keinen Output produzieren und daher Timeoutprobleme verursachen könnten. Nur den G- oder M-Code, kommasepariert." - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:81 msgid "Additional serial ports" msgstr "Zusätzliche serielle Ports" -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:84 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 #, python-format msgid "Use this to define additional glob patterns matching serial ports to list for connecting against, e.g. /dev/ttyAMA*. One entry per line." msgstr "Nutze diese Einstellung um zusätzliche glob patterns zu konfigurieren, die auf serielle Ports deines Druckers matchen, z.B. /dev/ttyAMA*. Ein Eintrag pro Zeile." -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:83 msgid "Not only cancel ongoing prints but also disconnect on unhandled errors from the firmware." msgstr "Bei unbehalten Firmwarefehlern nicht nur den Druckauftrag abbrechen, sondern auch die Verbindung zum Drucker trennen." -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 msgid "Ignore any unhandled errors from the firmware. Only use this if your firmware sends stuff prefixed with \"Error\" that is not an actual error. Might mask printer issues, be careful!" msgstr "Alle unbehalten Firmwarefehler ignorieren. Nur nutzen wenn Deine Firmware Dinge mit \"Error\" sendet die nicht wirklich Fehler sind. Könnte Druckerprobleme maskieren, vorsicht!" +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:98 +msgid "Command to send to the firmware on first handshake attempt." +msgstr "Kommando, das als erster Handshakeversuch an die Firmware gesendet werden soll" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:99 +msgid "\"Hello\" command" +msgstr "\"Hallo\"-Befehl" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:102 +msgid "Use this to specify a different command than the default M110 to send to the printer on initial connection to trigger a communication handshake." +msgstr "Nutze diese Einstellung um einen anderen Befehl als M110 beim initialen Verbindungsaufbau zum drucker zu senden." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:105 +msgid "Commands that are know to run long and hence should suppress communication timeouts from being triggered." +msgstr "Befehle, von denen bekannt ist, dass sie lang zur Ausführung benötigen und daher das Auslösen von Kommunikationstimeouts unterdrücken sollten." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:106 +msgid "Long running commands" +msgstr "Lang laufende Befehle" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:109 +msgid "Use this to specify the commands known to take a long time to complete without output from your printer and hence might cause timeout issues. Just the G or M code, comma separated." +msgstr "Nutze diese Option, um solche Befehle zu definieren, von denen Du weißt, dass sie eine längere Zeit lang laufen, währenddessen keinen Output produzieren und daher Timeoutprobleme verursachen könnten. Nur den G- oder M-Code, kommasepariert." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:112 +msgid "Commands that always require a line number and checksum to be sent with them." +msgstr "Befehle, die immer mit einer Prüfsumme und Zeilennummer gesendet werden müssen." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:113 +msgid "Commands that always require a checksum" +msgstr "Befehle, die immer eine Prüfsumme benötigen" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:116 +msgid "Use this to specify which commands always need to be sent with a checksum. Comma separated list." +msgstr "Nutze diese Einstellung um Befehle zu spezifizieren, die immer mit Prüfsumme gesendet werden müssen. Komma-separierte Liste." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Generate additional ok for M29" +msgstr "Zusätzliches ok für M29 generieren" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Most Marlin < v1.1.0" +msgstr "Viele Marlin < v1.1.0" + #: src/octoprint/templates/dialogs/settings/server.jinja2:2 msgid "Commands" msgstr "Befehle" diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 9120e1f4..351c9582 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -18,6 +18,7 @@ import threading from functools import wraps import warnings import contextlib +import Queue as queue logger = logging.getLogger(__name__) @@ -877,12 +878,9 @@ class RepeatedTimer(threading.Thread): class CountedEvent(object): - def __init__(self, value=0, max=None, name=None): - logger_name = __name__ + ".CountedEvent" + (".{name}".format(name=name) if name is not None else "") - self._logger = logging.getLogger(logger_name) - + def __init__(self, value=0, maximum=None, **kwargs): self._counter = 0 - self._max = max + self._max = kwargs.get("max", maximum) self._mutex = threading.Lock() self._event = threading.Event() @@ -907,17 +905,14 @@ class CountedEvent(object): return self._counter == 0 def _internal_set(self, value): - self._logger.debug("New counter value: {value}".format(value=value)) self._counter = value if self._counter <= 0: self._counter = 0 self._event.clear() - self._logger.debug("Cleared event") else: if self._max is not None and self._counter > self._max: self._counter = self._max self._event.set() - self._logger.debug("Set event") class InvariantContainer(object): @@ -951,3 +946,43 @@ class InvariantContainer(object): def __iter__(self): return self._data.__iter__() + + +class TypedQueue(queue.Queue): + + def __init__(self, maxsize=0): + queue.Queue.__init__(self, maxsize=maxsize) + self._lookup = set() + + def put(self, item, item_type=None, *args, **kwargs): + queue.Queue.put(self, (item, item_type), *args, **kwargs) + + def get(self, *args, **kwargs): + item, _ = queue.Queue.get(self, *args, **kwargs) + return item + + def _put(self, item): + _, item_type = item + if item_type is not None: + if item_type in self._lookup: + raise TypeAlreadyInQueue(item_type, "Type {} is already in queue".format(item_type)) + else: + self._lookup.add(item_type) + + queue.Queue._put(self, item) + + def _get(self): + item = queue.Queue._get(self) + _, item_type = item + + if item_type is not None: + self._lookup.discard(item_type) + + return item + + +class TypeAlreadyInQueue(Exception): + def __init__(self, t, *args, **kwargs): + Exception.__init__(self, *args, **kwargs) + self.type = t + diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 992f17c4..d67c07cf 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -24,7 +24,8 @@ from octoprint.settings import settings, default_settings 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 +from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer, \ + to_unicode, bom_aware_open, TypedQueue, TypeAlreadyInQueue try: import _winreg @@ -224,19 +225,29 @@ class MachineCom(object): self._bedTemp = None self._temperatureTargetSetThreshold = 25 self._tempOffsets = dict() - self._commandQueue = queue.Queue() + self._command_queue = TypedQueue() self._currentZ = None self._heatupWaitStartTime = None self._heatupWaitTimeLost = 0.0 self._pauseWaitStartTime = None self._pauseWaitTimeLost = 0.0 self._currentTool = 0 + self._formerTool = None self._long_running_command = False self._heating = False self._connection_closing = False self._timeout = None + self._timeout_intervals = dict() + for key, value in settings().get(["serial", "timeout"], merged=True, asdict=True).items(): + try: + self._timeout_intervals[key] = float(value) + except: + pass + + self._hello_command = settings().get(["serial", "helloCommand"]) + self._trigger_ok_for_m29 = settings().getBoolean(["serial", "triggerOkForM29"]) self._hello_command = settings().get(["serial", "helloCommand"]) @@ -256,9 +267,24 @@ class MachineCom(object): self._resendSwallowRepetitionsCounter = 0 self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"]) + self._resendActive = False + + self._terminal_log = deque([], 20) + self._disconnect_on_errors = settings().getBoolean(["serial", "disconnectOnErrors"]) self._ignore_errors = settings().getBoolean(["serial", "ignoreErrorsFromFirmware"]) + self._log_resends = settings().getBoolean(["serial", "logResends"]) + + # don't log more resends than 5 / 10s + self._log_resends_rate_start = None + self._log_resends_rate_count = 0 + self._log_resends_max = 5 + self._log_resends_rate_frame = 10 + + self._long_running_commands = settings().get(["serial", "longRunningCommands"]) + self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"]) + self._clear_to_send = CountedEvent(max=10, name="comm.clear_to_send") self._send_queue = TypedQueue() self._temperature_timer = None @@ -291,10 +317,6 @@ class MachineCom(object): # print job self._currentFile = None - # regexes - - self._long_running_commands = settings().get(["serial", "longRunningCommands"]) - # multithreading locks self._sendNextLock = threading.Lock() self._sendingLock = threading.RLock() @@ -327,7 +349,8 @@ class MachineCom(object): self._callback.on_comm_sd_files([]) if self._currentFile is not None: - self._recordFilePosition() + if self.isBusy(): + self._recordFilePosition() self._currentFile.close() oldState = self.getStateString() @@ -336,9 +359,16 @@ class MachineCom(object): self._callback.on_comm_state_change(newState) def _log(self, message): + self._terminal_log.append(message) self._callback.on_comm_log(message) self._serialLogger.debug(message) + def _to_logfile_with_terminal(self, message=None, level=logging.INFO): + log = "Last lines in terminal:\n" + "\n".join(map(lambda x: "| " + x, list(self._terminal_log))) + if message is not None: + log = message + "\n| " + log + self._logger.log(level, log) + def _addToLastLines(self, cmd): self._lastLines.append(cmd) @@ -472,7 +502,7 @@ class MachineCom(object): ##~~ external interface - def close(self, is_error=False, wait=True, *args, **kwargs): + def close(self, is_error=False, wait=True, timeout=10.0, *args, **kwargs): """ Closes the connection to the printer. @@ -516,7 +546,13 @@ class MachineCom(object): if not is_error: self.sendGcodeScript("beforePrinterDisconnected") if wait: - self._send_queue.join() + if timeout is not None: + stop = time.time() + timeout + while (self._command_queue.unfinished_tasks or self._send_queue.unfinished_tasks) and time.time() < stop: + time.sleep(0.1) + else: + self._command_queue.join() + self._send_queue.join() deactivate_monitoring_and_send_queue() @@ -551,7 +587,7 @@ class MachineCom(object): self._tempOffsets.update(offsets) def fakeOk(self): - self._clear_to_send.set() + self._handle_ok() def sendCommand(self, cmd, cmd_type=None, processed=False, force=False): cmd = to_unicode(cmd, errors="replace") @@ -561,7 +597,10 @@ class MachineCom(object): return if self.isPrinting() and not self.isSdFileSelected(): - self._commandQueue.put((cmd, cmd_type)) + try: + self._command_queue.put((cmd, cmd_type), item_type=cmd_type) + except TypeAlreadyInQueue as e: + self._logger.debug("Type already in command queue: " + e.type) elif self.isOperational() or force: self._sendCommand(cmd, cmd_type=cmd_type) @@ -659,7 +698,7 @@ class MachineCom(object): self.sendCommand("M24") - self._sd_status_timer = RepeatedTimer(lambda: get_interval("sdStatus", default_value=1.0), self._poll_sd_status, run_first=True) + self._sd_status_timer = RepeatedTimer(self._timeout_intervals.get("sdStatus", 1.0), self._poll_sd_status, run_first=True) self._sd_status_timer.start() else: if pos is not None and isinstance(pos, int) and pos > 0: @@ -682,6 +721,8 @@ class MachineCom(object): logging.info("Printer is not operation or busy") return + self.resetLineNumbers() + self._currentFile = StreamingGcodeFileInformation(filename, localFilename, remoteFilename) self._currentFile.start() @@ -720,6 +761,10 @@ class MachineCom(object): if not self.isOperational() or self.isStreaming(): return + if not self.isBusy() or self._currentFile is None: + # we aren't even printing, nothing to cancel... + return + self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): @@ -791,25 +836,6 @@ class MachineCom(object): def getSdFiles(self): return self._sdFiles - def startSdFileTransfer(self, filename): - if not self._sdEnabled: - return - - if not self.isOperational() or self.isBusy(): - return - self._changeState(self.STATE_TRANSFERING_FILE) - self.sendCommand("M28 %s" % filename.lower()) - - def endSdFileTransfer(self, filename): - if not self._sdEnabled: - return - - if not self.isOperational() or self.isBusy(): - return - self.sendCommand("M29 %s" % filename.lower()) - self._changeState(self.STATE_OPERATIONAL) - self.refreshSdFiles() - def deleteSdFile(self, filename): if not self._sdEnabled: return @@ -939,7 +965,7 @@ class MachineCom(object): self._changeState(self.STATE_CONNECTING) #Start monitoring the serial port. - self._timeout = get_new_timeout("communication") + self._timeout = get_new_timeout("communication", self._timeout_intervals) startSeen = False supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"]) @@ -958,7 +984,7 @@ class MachineCom(object): if line is None: break if line.strip() is not "": - self._timeout = get_new_timeout("communication") + self._timeout = get_new_timeout("communication", self._timeout_intervals) ##~~ debugging output handling if line.startswith("//"): @@ -985,13 +1011,20 @@ class MachineCom(object): else: continue + def convert_line(line): + if line is None: + return None, None + stripped_line = line.strip() + return stripped_line, stripped_line.lower() + ##~~ Error handling line = self._handleErrors(line) + line, lower_line = convert_line(line) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not "End file list" in line: - preprocessed_line = line.strip().lower() + preprocessed_line = lower_line fileinfo = preprocessed_line.rsplit(None, 1) if len(fileinfo) > 1: # we might have extended file information here, so let's split filename and size and try to make them a bit nicer @@ -1017,10 +1050,29 @@ class MachineCom(object): self._sdFiles.append((filename, size)) continue - ##~~ process oks - if line.strip().startswith("ok") or (self.isPrinting() and supportWait and line.strip().startswith("wait")): - self._clear_to_send.set() - self._long_running_command = False + handled = False + + # process oks + if line.startswith("ok") or (self.isPrinting() and supportWait and line == "wait"): + # ok only considered handled if it's alone on the line, might be + # a response to an M105 or an M114 + self._handle_ok() + handled = (line == "wait" or line == "ok" or not ("T:" in line or "T0:" in line or "B:" in line or "C:" in line)) + + # process resends + elif lower_line.startswith("resend") or lower_line.startswith("rs"): + self._handleResendRequest(line) + handled = True + + # process timeouts + elif line == "" and time.time() > self._timeout: + # timeout only considered handled if the printer is printing + self._handle_timeout() + handled = self.isPrinting() + + # we don't have to process the rest if the line has already been handled fully + if handled and self._state not in (self.STATE_CONNECTING, self.STATE_DETECT_BAUDRATE): + continue ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ' B:' in line or line.startswith('B:'): @@ -1059,12 +1111,6 @@ class MachineCom(object): except ValueError: pass - #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. - if 'ok' in line and self._heatupWaitStartTime: - self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) - self._heatupWaitStartTime = None - self._heating = False - ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False @@ -1111,8 +1157,6 @@ class MachineCom(object): }) elif 'Writing to file' in line and self.isStreaming(): self._changeState(self.STATE_PRINTING) - self._clear_to_send.set() - line = "ok" elif 'Done printing file' in line and self.isSdPrinting(): # printer is reporting file finished printing self._sdFilePos = 0 @@ -1130,15 +1174,19 @@ class MachineCom(object): except: pass elif 'Done saving file' in line: - self.refreshSdFiles() + if self._trigger_ok_for_m29: + # workaround for most versions of Marlin out in the wild + # not sending an ok after saving a file + self._handle_ok() elif 'File deleted' in line and line.strip().endswith("ok"): - # buggy Marlin version that doesn't send a proper \r after the "File deleted" statement, fixed in + # buggy Marlin version that doesn't send a proper line break after the "File deleted" statement, fixed in # current versions - self._clear_to_send.set() + self._handle_ok() ##~~ Message handling - elif line.strip() != '' \ - and line.strip() != 'ok' and not line.startswith("wait") \ + elif line != '' \ + and not line.startswith("ok") \ + and not line.startswith("wait") \ and not line.startswith('Resend:') \ and line != 'echo:Unknown command:""\n' \ and self.isOperational(): @@ -1179,11 +1227,12 @@ class MachineCom(object): self._serial.timeout = connection_timeout self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 - self._timeout = get_new_timeout("communication") + self._timeout = get_new_timeout("communication", self._timeout_intervals) self._serial.write('\n') self.sayHello() except: - self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string())) + self._log("Unexpected error while setting baudrate {}: {}".format(baudrate, get_exception_string())) + self._logger.exception("Unexpceted error while setting baudrate {}".format(baudrate)) else: self.close(wait=False) self._errorValue = "No more baudrates to test, and no suitable baudrate found." @@ -1191,35 +1240,19 @@ class MachineCom(object): eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) elif 'start' in line or 'ok' in line: self._onConnected() - self._clear_to_send.set() + if 'start' in line: + self._clear_to_send.set() ### Connection attempt elif self._state == self.STATE_CONNECTING: if "start" in line and not startSeen: startSeen = True self.sayHello() - elif "ok" in line: + elif line.startswith("ok"): self._onConnected() elif time.time() > self._timeout: self.close(wait=False) - ### Operational & Printing - if self._state in (self.STATE_OPERATIONAL, self.STATE_PAUSED, self.STATE_PRINTING): - if line == "" and time.time() > self._timeout: - if not self._long_running_command: - self._log("Communication timeout, forcing a line") - self._sendCommand("M105") - self._clear_to_send.set() - else: - self._logger.debug("Ran into a communication timeout, but a command known to be a long runner is currently active") - - elif "ok" in line or (self._state == self.STATE_PRINTING and supportWait and "wait" in line): - # a wait while printing means our printer's buffer ran out, probably due to some ok getting - # swallowed, so we treat it the same as an ok here to take up communication again - self._handle_ok() - - elif line.lower().startswith("resend") or line.lower().startswith("rs"): - self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") @@ -1231,16 +1264,55 @@ class MachineCom(object): self._log("Connection closed, closing down monitor") def _handle_ok(self): + self._clear_to_send.set() + + # reset long running commands, persisted current tools and heatup counters on ok + + self._long_running_command = False + + if self._formerTool is not None: + self._currentTool = self._formerTool + self._formerTool = None + + if self._heatupWaitStartTime: + self._heatupWaitTimeLost = self._heatupWaitTimeLost + (time.time() - self._heatupWaitStartTime) + self._heatupWaitStartTime = None + self._heating = False + if not self._state in (self.STATE_PRINTING, self.STATE_OPERATIONAL, self.STATE_PAUSED): return - if self._resendSwallowNextOk: - self._resendSwallowNextOk = False - elif self._resendDelta is not None: + # process queues ongoing resend requests and queues if we are operational + + if self._resendDelta is not None: self._resendNextCommand() else: + self._resendActive = False self._continue_sending() + return + + def _handle_timeout(self): + if self._state not in (self.STATE_PRINTING,): + return + + if self._long_running_command: + self._logger.debug("Ran into a communication timeout, but a command known to be a long runner is currently active") + return + + general_message = "Configure long running commands or increase communication timeout if that happens regularly on specific commands or long moves." + if self._resendActive: + self._log("Communication timeout while printing and during an active resend, resending same line again to trigger response from printer. " + general_message) + self._resendSameCommand() + self._clear_to_send.set() + + else: + self._log("Communication timeout while printing, trying to trigger response from printer. " + general_message) + self._sendCommand("M105", cmd_type="temperature") + self._clear_to_send.set() + + return + def _continue_sending(self): if self._state == self.STATE_PRINTING: if not self._sendFromQueue() and not self.isSdPrinting(): @@ -1287,22 +1359,22 @@ class MachineCom(object): """ Polls the temperature after the temperature timeout, re-enqueues itself. - If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll - will be done. + If the printer is not operational, closing the connection, not printing from sd, busy with a long running + command or heating, no poll will be done. """ - if self.isOperational() 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._manualStreaming: self.sendCommand("M105", cmd_type="temperature_poll") def _poll_sd_status(self): """ Polls the sd printing status after the sd status timeout, re-enqueues itself. - If the printer is not operational, not printing from sd, busy with a long running command or heating, no poll - will be done. + If the printer is not operational, closing the connection, not printing from sd, busy with a long running + command or heating, no poll will be done. """ - if self.isOperational() 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._heating: self.sendCommand("M27", cmd_type="sd_status_poll") def _onConnected(self): @@ -1328,41 +1400,49 @@ class MachineCom(object): target_default = 2.0 if self.isBusy(): - return get_interval("temperature", default_value=busy_default) + return self._timeout_intervals.get("temperature", busy_default) for temp in [self._temp[k][1] for k in self._temp.keys()]: if temp > self._temperatureTargetSetThreshold: - return get_interval("temperatureTargetSet", default_value=target_default) + return self._timeout_intervals.get("temperatureTargetSet", target_default) if self._bedTemp and len(self._bedTemp) > 0 and self._bedTemp[1] > self._temperatureTargetSetThreshold: - return get_interval("temperatureTargetSet", default_value=target_default) + return self._timeout_intervals.get("temperatureTargetSet", target_default) - return get_interval("temperature", default_value=busy_default) + return self._timeout_intervals.get("temperature", busy_default) def _sendFromQueue(self): # We loop here to make sure that if we do NOT send the first command # from the queue, we'll send the second (if there is one). We do not # want to get stuck here by throwing away commands. while True: - if self._commandQueue.empty() or self.isStreaming(): - # no command queue or irrelevant command queue => return + if self.isStreaming(): + # command queue irrelevant return False - entry = self._commandQueue.get() - if isinstance(entry, tuple): - if not len(entry) == 2: - # something with that entry is broken, ignore it and fetch - # the next one - continue - cmd, cmd_type = entry - else: - cmd = entry - cmd_type = None + try: + entry = self._command_queue.get(block=False) + except queue.Empty: + # nothing in command queue + return False - if self._sendCommand(cmd, cmd_type=cmd_type): - # we actually did add this cmd to the send queue, so let's - # return, we are done here - return True + try: + if isinstance(entry, tuple): + if not len(entry) == 2: + # something with that entry is broken, ignore it and fetch + # the next one + continue + cmd, cmd_type = entry + else: + cmd = entry + cmd_type = None + + if self._sendCommand(cmd, cmd_type=cmd_type): + # we actually did add this cmd to the send queue, so let's + # return, we are done here + return True + finally: + self._command_queue.task_done() def _detectPort(self, close): programmer = stk500v2.Stk500v2() @@ -1375,10 +1455,13 @@ class MachineCom(object): programmer.connect(p) serial_obj = programmer.leaveISP() except ispBase.IspError as (e): - self._log("Error while connecting to %s: %s" % (p, str(e))) + error_message = "Error while connecting to %s: %s" % (p, str(e)) + self._log(error_message) + self._logger.exception(error_message) except: - self._log("Unexpected error while connecting to serial port: %s %s" % (p, get_exception_string())) - + error_message = "Unexpected error while connecting to serial port: %s %s" % (p, get_exception_string()) + self._log(error_message) + self._logger.exception(error_message) if serial_obj is not None: if (close): serial_obj.close() @@ -1425,7 +1508,9 @@ class MachineCom(object): self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name)) + error_message = "Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name) + self._log(error_message) + self._logger.exception(error_message) if "failed to set custom baud rate" in exception_string.lower(): self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian") @@ -1448,7 +1533,7 @@ class MachineCom(object): lower_line = line.lower() # No matter the state, if we see an error, goto the error state and store the error for reference. - if line.startswith('Error:') or line.startswith('!!'): + if lower_line.startswith('error:') or line.startswith('!!'): #Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" @@ -1458,7 +1543,7 @@ class MachineCom(object): if 'line number' in lower_line or 'checksum' in lower_line or 'format error' in lower_line or 'expected line' in lower_line: #Skip the communication errors, as those get corrected. - self._lastCommError = line[6:] if line.startswith("Error:") else line[2:] + self._lastCommError = line[6:] if lower_line.startswith("error:") else line[2:] pass elif 'volume.init' in lower_line or "openroot" in lower_line or 'workdir' in lower_line\ or "error writing to file" in lower_line or "cannot open" in lower_line\ @@ -1469,7 +1554,8 @@ class MachineCom(object): #Ignore unkown command errors, it could be a typo or some missing feature pass elif not self.isError(): - error_text = line[6:] if line.startswith("Error:") else line[2:] + error_text = line[6:] if lower_line.startswith("error:") else line[2:] + self._to_logfile_with_terminal("Received an error from the printer's firmware: {}".format(error_text), level=logging.WARN) if not self._ignore_errors: if self._disconnect_on_errors: self._errorValue = error_text @@ -1497,11 +1583,12 @@ class MachineCom(object): self.close(is_error=True) return None - try: - self._log("Recv: %s" % sanitize_ascii(ret)) - except ValueError as e: - self._log("WARN: While reading last line: %s" % e) - self._log("Recv: %r" % ret) + if ret != "": + try: + self._log("Recv: " + sanitize_ascii(ret)) + except ValueError as e: + self._log("WARN: While reading last line: %s" % e) + self._log("Recv: " + repr(ret)) for name, hook in self._received_message_hooks.items(): try: @@ -1564,53 +1651,97 @@ class MachineCom(object): if "rs" in line: lineToResend = int(line.split()[1]) - if lineToResend is not None: - self._resendSwallowNextOk = True + if lineToResend is None: + return False - lastCommError = self._lastCommError - self._lastCommError = None + if self._resendDelta is None and lineToResend == self._currentLine: + # We don't expect to have an active resend request and the printer is requesting a resend of + # a line we haven't yet sent. + # + # This means the printer got a line from us with N = self._currentLine - 1 but had already + # acknowledged that. This can happen if the last line was resent due to a timeout during + # an active (prior) resend request. + # + # We will ignore this resend request and just continue normally. + self._logger.debug("Ignoring resend request for line %d == current line, we haven't sent that yet so the printer got N-1 twice from us, probably due to a timeout" % lineToResend) + return False - resendDelta = self._currentLine - lineToResend + lastCommError = self._lastCommError + self._lastCommError = None - if lastCommError is not None \ - and ("line number" in lastCommError.lower() or "expected line" in lastCommError.lower()) \ - and lineToResend == self._lastResendNumber \ - and self._resendDelta is not None and self._currentResendCount < self._resendDelta: - self._logger.debug("Ignoring resend request for line %d, that still originates from lines we sent before we got the first resend request" % lineToResend) - self._currentResendCount += 1 - return + resendDelta = self._currentLine - lineToResend - # If we ignore resend repetitions (Repetier firmware...), check if we - # need to do this now. If the same line number has been requested we - # already saw and resent, we'll ignore it up to times. - if self._resendSwallowRepetitions and lineToResend == self._lastResendNumber and self._resendSwallowRepetitionsCounter > 0: - self._logger.debug("Ignoring resend request for line %d, that is probably a repetition sent by the firmware to ensure it arrives, not a real request" % lineToResend) - self._resendSwallowRepetitionsCounter -= 1 - return + if lastCommError is not None \ + and ("line number" in lastCommError.lower() or "expected line" in lastCommError.lower()) \ + and lineToResend == self._lastResendNumber \ + and self._resendDelta is not None and self._currentResendCount < self._resendDelta: + self._logger.debug("Ignoring resend request for line %d, that still originates from lines we sent before we got the first resend request" % lineToResend) + self._currentResendCount += 1 + return True - self._resendDelta = resendDelta - self._lastResendNumber = lineToResend - self._currentResendCount = 0 - self._resendSwallowRepetitionsCounter = settings().getInt(["feature", "identicalResendsCountdown"]) + # If we ignore resend repetitions (Repetier firmware...), check if we + # need to do this now. If the same line number has been requested we + # already saw and resent, we'll ignore it up to times. + if self._resendSwallowRepetitions and lineToResend == self._lastResendNumber and self._resendSwallowRepetitionsCounter > 0: + self._logger.debug("Ignoring resend request for line %d, that is probably a repetition sent by the firmware to ensure it arrives, not a real request" % lineToResend) + self._resendSwallowRepetitionsCounter -= 1 + return True - if self._resendDelta > len(self._lastLines) or len(self._lastLines) == 0 or self._resendDelta < 0: - self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend - self._logger.warn(self._errorValue) - if self.isPrinting(): - # abort the print, there's nothing we can do to rescue it now - self._changeState(self.STATE_ERROR) - eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) - else: - # reset resend delta, we can't do anything about it - self._resendDelta = None + self._resendActive = True + self._resendDelta = resendDelta + self._lastResendNumber = lineToResend + self._currentResendCount = 0 + self._resendSwallowRepetitionsCounter = settings().getInt(["feature", "identicalResendsCountdown"]) + + if self._resendDelta > len(self._lastLines) or len(self._lastLines) == 0 or self._resendDelta < 0: + self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend + self._log(self._errorValue) + self._logger.warn(self._errorValue + ". Printer requested line {}, current line is {}, line history has {} entries.".format(lineToResend, self._currentLine, len(self._lastLines))) + if self.isPrinting(): + # abort the print, there's nothing we can do to rescue it now + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) else: - self._resendNextCommand() + # reset resend delta, we can't do anything about it + self._resendDelta = None - def _resendNextCommand(self): + # if we log resends, make sure we don't log more resends than the set rate within a window + # + # this it to prevent the log from getting flooded for extremely bad communication issues + if self._log_resends: + now = time.time() + new_rate_window = self._log_resends_rate_start is None or self._log_resends_rate_start + self._log_resends_rate_frame < now + in_rate = self._log_resends_rate_count < self._log_resends_max + + if new_rate_window or in_rate: + if new_rate_window: + self._log_resends_rate_start = now + self._log_resends_rate_count = 0 + + self._to_logfile_with_terminal("Got a resend request from the printer: requested line = {}, current line = {}".format(lineToResend, self._currentLine)) + self._log_resends_rate_count += 1 + + return True + + def _resendSameCommand(self): + self._resendNextCommand(again=True) + + def _resendNextCommand(self, again=False): self._lastCommError = None # Make sure we are only handling one sending job at a time with self._sendingLock: + if again: + # If we are about to last line from the active resend request + # again, we first need to increment resend delta. It might already + # be set to None if the last resend line was already sent, so + # if that's the case we set it to 0. It will then be incremented, + # the last line will be sent again, and then the delta will be + # decremented and set to None again, completing the cycle. + if self._resendDelta is None: + self._resendDelta = 0 + self._resendDelta += 1 + cmd = self._lastLines[-self._resendDelta] lineNumber = self._currentLine - self._resendDelta @@ -1629,24 +1760,22 @@ class MachineCom(object): return False gcode = None - if not self.isStreaming(): - # trigger the "queuing" phase only if we are not streaming to sd right now - cmd, cmd_type, gcode = self._process_command_phase("queuing", cmd, cmd_type, gcode=gcode) - if cmd is None: - # command is no more, return - return False + # trigger the "queuing" phase only if we are not streaming to sd right now + cmd, cmd_type, gcode = self._process_command_phase("queuing", cmd, cmd_type, gcode=gcode) - if gcode and gcode in gcodeToEvent: - # if this is a gcode bound to an event, trigger that now - eventManager().fire(gcodeToEvent[gcode]) + if cmd is None: + # command is no more, return + return False + + if not self.isStreaming() and gcode and gcode in gcodeToEvent: + # if this is a gcode bound to an event, trigger that now + eventManager().fire(gcodeToEvent[gcode]) # actually enqueue the command for sending self._enqueue_for_sending(cmd, command_type=cmd_type) - if not self.isStreaming(): - # trigger the "queued" phase only if we are not streaming to sd right now - self._process_command_phase("queued", cmd, cmd_type, gcode=gcode) + self._process_command_phase("queued", cmd, cmd_type, gcode=gcode) return True @@ -1663,9 +1792,9 @@ class MachineCom(object): """ try: - self._send_queue.put((command, linenumber, command_type)) + self._send_queue.put((command, linenumber, command_type), item_type=command_type) except TypeAlreadyInQueue as e: - self._logger.debug("Type already in queue: " + e.type) + self._logger.debug("Type already in send queue: " + e.type) def _send_loop(self): """ @@ -1696,7 +1825,7 @@ class MachineCom(object): if linenumber is not None: # line number predetermined - this only happens for resends, so we'll use the number and # send directly without any processing (since that already took place on the first sending!) - self._doSendWithChecksum(command, linenumber) + self._do_send_with_checksum(command, linenumber) else: # trigger "sending" phase @@ -1725,13 +1854,13 @@ 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._alwaysSendChecksum or (self.isPrinting() and not self._neverSendChecksum) + checksum_enabled = self.isPrinting() or self._alwaysSendChecksum command_to_send = command.encode("ascii", errors="replace") if command_requiring_checksum or (command_allowing_checksum and checksum_enabled): - self._doIncrementAndSendWithChecksum(command_to_send) + self._do_increment_and_send_with_checksum(command_to_send) else: - self._doSendWithoutChecksum(command_to_send) + self._do_send_without_checksum(command_to_send) # trigger "sent" phase and use up one "ok" self._process_command_phase("sent", command, command_type, gcode=gcode) @@ -1763,7 +1892,7 @@ class MachineCom(object): self._log("Closing down send loop") def _process_command_phase(self, phase, command, command_type=None, gcode=None): - if phase not in ("queuing", "queued", "sending", "sent"): + if self.isStreaming() or phase not in ("queuing", "queued", "sending", "sent"): return command, command_type, gcode if gcode is None: @@ -1827,24 +1956,26 @@ class MachineCom(object): ##~~ actual sending via serial - def _doIncrementAndSendWithChecksum(self, cmd): + def _do_increment_and_send_with_checksum(self, cmd): with self._line_mutex: linenumber = self._currentLine self._addToLastLines(cmd) self._currentLine += 1 - self._doSendWithChecksum(cmd, linenumber) + self._do_send_with_checksum(cmd, linenumber) - def _doSendWithChecksum(self, cmd, lineNumber): - commandToSend = "N%d %s" % (lineNumber, cmd) - checksum = reduce(lambda x,y:x^y, map(ord, commandToSend)) - commandToSend = "%s*%d" % (commandToSend, checksum) - self._doSendWithoutChecksum(commandToSend) + def _do_send_with_checksum(self, command, linenumber): + command_to_send = "N" + str(linenumber) + " " + command + checksum = 0 + for c in command_to_send: + checksum ^= ord(c) + command_to_send = command_to_send + "*" + str(checksum) + self._do_send_without_checksum(command_to_send) - def _doSendWithoutChecksum(self, cmd): + def _do_send_without_checksum(self, cmd): if self._serial is None: return - self._log("Send: %s" % cmd) + self._log("Send: " + str(cmd)) try: self._serial.write(cmd + '\n') except serial.SerialTimeoutException: @@ -1911,11 +2042,17 @@ class MachineCom(object): return None, # Don't send bed commands if we don't have a heated bed _gcode_M190_queuing = _gcode_M140_queuing - def _gcode_M104_sent(self, cmd, cmd_type=None): + def _gcode_M104_sent(self, cmd, cmd_type=None, wait=False): toolNum = self._currentTool toolMatch = regexes_parameters["intT"].search(cmd) + if toolMatch: toolNum = int(toolMatch.group("value")) + + if wait: + self._formerTool = self._currentTool + self._currentTool = toolNum + match = regexes_parameters["floatS"].search(cmd) if match: try: @@ -1928,7 +2065,7 @@ class MachineCom(object): except ValueError: pass - def _gcode_M140_sent(self, cmd, cmd_type=None): + def _gcode_M140_sent(self, cmd, cmd_type=None, wait=False): match = regexes_parameters["floatS"].search(cmd) if match: try: @@ -1945,13 +2082,13 @@ class MachineCom(object): self._heatupWaitStartTime = time.time() self._long_running_command = True self._heating = True - self._gcode_M104_sent(cmd, cmd_type) + self._gcode_M104_sent(cmd, cmd_type, wait=True) def _gcode_M190_sent(self, cmd, cmd_type=None): self._heatupWaitStartTime = time.time() self._long_running_command = True self._heating = True - self._gcode_M140_sent(cmd, cmd_type) + self._gcode_M140_sent(cmd, cmd_type, wait=True) def _gcode_M110_sending(self, cmd, cmd_type=None): newLineNumber = None @@ -1974,8 +2111,8 @@ class MachineCom(object): def _gcode_M112_queuing(self, cmd, cmd_type=None): # emergency stop, jump the queue with the M112 - self._doSendWithoutChecksum("M112") - self._doIncrementAndSendWithChecksum("M112") + self._do_send_without_checksum("M112") + self._do_increment_and_send_with_checksum("M112") # No idea if the printer is still listening or if M112 won. Just in case # we'll now try to also manually make sure all heaters are shut off - better @@ -1983,9 +2120,9 @@ class MachineCom(object): # is irrelevant whether the printer has sent enough ack's or not, we # are going to shutdown the connection in a second anyhow. for tool in range(self._printerProfileManager.get_current_or_default()["extruder"]["count"]): - self._doIncrementAndSendWithChecksum("M104 T{tool} S0".format(tool=tool)) + self._do_increment_and_send_with_checksum("M104 T{tool} S0".format(tool=tool)) if self._printerProfileManager.get_current_or_default()["heatedBed"]: - self._doIncrementAndSendWithChecksum("M140 S0") + self._do_increment_and_send_with_checksum("M140 S0") # close to reset host state self._errorValue = "Closing serial port due to emergency stop M112." @@ -2011,7 +2148,7 @@ 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") + _timeout + self._timeout = get_new_timeout("communication", self._timeout_intervals) + _timeout ##~~ command phase handlers @@ -2152,8 +2289,6 @@ class PrintingGcodeFileInformation(PrintingFileInformation): self._handle = None - self._first_line = None - self._offsets_callback = offsets_callback self._current_tool_callback = current_tool_callback @@ -2161,6 +2296,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation): raise IOError("File %s does not exist" % self._filename) self._size = os.stat(self._filename).st_size self._pos = 0 + self._read_lines = 0 def seek(self, offset): if self._handle is None: @@ -2168,12 +2304,14 @@ class PrintingGcodeFileInformation(PrintingFileInformation): self._handle.seek(offset) self._pos = self._handle.tell() + self._read_lines = 0 def start(self): """ Opens the file for reading and determines the file size. """ PrintingFileInformation.start(self) + self._read_lines = 0 self._handle = bom_aware_open(self._filename, encoding="utf-8", errors="replace") def close(self): @@ -2203,19 +2341,29 @@ class PrintingGcodeFileInformation(PrintingFileInformation): while processed is None: if self._handle is None: # file got closed just now + self._pos = self._size + self._report_stats() return None line = to_unicode(self._handle.readline()) if not line: self.close() - processed = process_gcode_line(line, offsets=offsets, current_tool=current_tool) + processed = self._process(line, offsets, current_tool) self._pos = self._handle.tell() - + self._read_lines += 1 return processed except Exception as e: self.close() self._logger.exception("Exception while processing line") raise e + def _process(self, line, offsets, current_tool): + return process_gcode_line(line, offsets=offsets, current_tool=current_tool) + + def _report_stats(self): + duration = time.time() - self._start_time + self._logger.info("Finished in {:.3f} s.".format(duration)) + pass + class StreamingGcodeFileInformation(PrintingGcodeFileInformation): def __init__(self, path, localFilename, remoteFilename): PrintingGcodeFileInformation.__init__(self, path) @@ -2232,56 +2380,22 @@ class StreamingGcodeFileInformation(PrintingGcodeFileInformation): def getRemoteFilename(self): return self._remoteFilename + def _process(self, line, offsets, current_tool): + return process_gcode_line(line) -class TypedQueue(queue.Queue): + def _report_stats(self): + duration = time.time() - self._start_time + stats = dict(lines=self._read_lines, + rate=float(self._read_lines) / duration, + time_per_line=duration * 1000.0 / float(self._read_lines), + duration=duration) + self._logger.info("Finished in {duration:.3f} s. Approx. transfer rate of {rate:.3f} lines/s or {time_per_line:.3f} ms per line".format(**stats)) - def __init__(self, maxsize=0): - queue.Queue.__init__(self, maxsize=maxsize) - self._lookup = [] - - def _put(self, item): - if isinstance(item, tuple) and len(item) == 3: - cmd, line, cmd_type = item - if cmd_type is not None: - if cmd_type in self._lookup: - raise TypeAlreadyInQueue(cmd_type, "Type {cmd_type} is already in queue".format(**locals())) - else: - self._lookup.append(cmd_type) - - queue.Queue._put(self, item) - - def _get(self): - item = queue.Queue._get(self) - - if isinstance(item, tuple) and len(item) == 3: - cmd, line, cmd_type = item - if cmd_type is not None and cmd_type in self._lookup: - self._lookup.remove(cmd_type) - - return item - - -class TypeAlreadyInQueue(Exception): - def __init__(self, t, *args, **kwargs): - Exception.__init__(self, *args, **kwargs) - self.type = t - - -def get_new_timeout(type): +def get_new_timeout(type, intervals): now = time.time() - return now + get_interval(type) + return now + intervals.get(type, 0.0) -def get_interval(type, default_value=0.0): - if type not in default_settings["serial"]["timeout"]: - return default_value - else: - value = settings().getFloat(["serial", "timeout", type]) - if not value: - return default_value - else: - return value - _temp_command_regex = re.compile("^M(?P104|109|140|190)(\s+T(?P\d+)|\s+S(?P[-+]?\d*\.?\d*))+") def apply_temperature_offsets(line, offsets, current_tool=None): @@ -2344,6 +2458,9 @@ def process_gcode_line(line, offsets=None, current_tool=None): return line def convert_pause_triggers(configured_triggers): + if not configured_triggers: + return dict() + triggers = { "enable": [], "disable": [], @@ -2373,6 +2490,9 @@ def convert_pause_triggers(configured_triggers): def convert_feedback_controls(configured_controls): + if not configured_controls: + return dict(), None + def preprocess_feedback_control(control, result): if "key" in control and "regex" in control and "template" in control: # key is always the md5sum of the regex @@ -2568,3 +2688,92 @@ def gcode_command_for_cmd(cmd): # this should never happen return None + +# --- Test code for speed testing the comm layer via command line follows + + +def upload_cli(): + """ + Usage: python -m octoprint.util.comm + + Uploads to on SD card of printer on port , using baudrate . + """ + + import sys + from octoprint.util import Object + + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") + logger = logging.getLogger(__name__) + + # fetch port, baudrate, filename and target from commandline + if len(sys.argv) < 5: + print("Usage: comm.py ") + sys.exit(-1) + + port = sys.argv[1] + baudrate = sys.argv[2] + path = sys.argv[3] + target = sys.argv[4] + + # init settings & plugin manager + settings(init=True) + octoprint.plugin.plugin_manager(init=True) + + # create dummy callback + class MyMachineComCallback(MachineComPrintCallback): + progress_interval = 1 + + def __init__(self, path, target): + self.finished = threading.Event() + self.finished.clear() + + self.comm = None + self.error = False + self.started = False + + self._path = path + self._target = target + + def on_comm_file_transfer_started(self, filename, filesize): + # transfer started, report + logger.info("Started file transfer of {}, size {}B".format(filename, filesize)) + self.started = True + + def on_comm_file_transfer_done(self, filename): + # transfer done, report, print stats and finish + logger.info("Finished file transfer of {}".format(filename)) + self.finished.set() + + def on_comm_state_change(self, state): + if state in (MachineCom.STATE_ERROR, MachineCom.STATE_CLOSED_WITH_ERROR): + # report and exit on errors + logger.error("Error/closed with error, exiting.") + self.error = True + self.finished.set() + + elif state in (MachineCom.STATE_OPERATIONAL,) and not self.started: + # start transfer once we are operational + self.comm.startFileTransfer(self._path, os.path.basename(self._path), self._target) + + callback = MyMachineComCallback(path, target) + + # mock printer profile manager + profile = dict(heatedBed=False, + extruder=dict(count=1)) + printer_profile_manager = Object() + printer_profile_manager.get_current_or_default = lambda: profile + + # initialize serial + comm = MachineCom(port=port, baudrate=baudrate, callbackObject=callback, printerProfileManager=printer_profile_manager) + callback.comm = comm + + # wait for file transfer to finish + callback.finished.wait() + + # close connection + comm.close() + + logger.info("Done, exiting...") + +if __name__ == "__main__": + upload_cli() diff --git a/translations/de/LC_MESSAGES/messages.mo b/translations/de/LC_MESSAGES/messages.mo index 22c641e2..dd2b7896 100644 Binary files a/translations/de/LC_MESSAGES/messages.mo and b/translations/de/LC_MESSAGES/messages.mo differ diff --git a/translations/de/LC_MESSAGES/messages.po b/translations/de/LC_MESSAGES/messages.po index edeec609..6a96ee7d 100644 --- a/translations/de/LC_MESSAGES/messages.po +++ b/translations/de/LC_MESSAGES/messages.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: OctoPrint\n" "Report-Msgid-Bugs-To: i18n@octoprint.org\n" -"POT-Creation-Date: 2016-02-10 11:12+0100\n" -"PO-Revision-Date: 2016-02-10 11:25+0100\n" +"POT-Creation-Date: 2016-03-16 09:14+0100\n" +"PO-Revision-Date: 2016-03-16 09:21+0100\n" "Last-Translator: Gina Häußge \n" "Language: de\n" "Language-Team: German (http://www.transifex.com/projects/p/octoprint/language/de/)\n" @@ -521,6 +521,7 @@ msgstr "Das sieht nicht aus wie ein valides Pluginarchiv. Valide Pluginarchive s #: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:182 #: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:63 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:96 #: src/octoprint/templates/tabs/terminal.jinja2:27 msgid "Advanced options" msgstr "Erweiterte Optionen" @@ -1041,18 +1042,18 @@ msgstr "%(local)s nach %(remote)s gestreamt, dauerte %(time).2f Sekunden" #: src/octoprint/static/js/app/dataupdater.js:352 #: src/octoprint/static/js/app/dataupdater.js:360 -msgid "Unhandled firmware error" -msgstr "Unbehandelter Fehler der Firmware" +msgid "Unhandled communication error" +msgstr "Unbehandelter Kommunikationsfehler" #: src/octoprint/static/js/app/dataupdater.js:353 #, python-format -msgid "The firmware reported an unhandled error. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s" -msgstr "Die Firmware hat einen durch OctoPrint unbehandelten Fehler gemeldet. Daher wurder der laufende Druckauftrag abgebrochen. Fehler: %(firmwareError)s" +msgid "There was an unhandled error while talking to the printer. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s" +msgstr "Es gab einen unbehandelten Fehler bei der Kommunikation mit dem Drucker. Daher wurder der laufende Druckauftrag abgebrochen. Fehler: %(firmwareError)s" #: src/octoprint/static/js/app/dataupdater.js:361 #, python-format -msgid "The firmware reported an unhandled error. Due to that OctoPrint disconnected. Error: %(error)s" -msgstr "Die Firmware hat einen durch OctoPrint unbehandelten Fehler gemeldet. Daher hat OctoPrint die Verbindung getrennt. Fehler: %(error)s" +msgid "The was an unhandled error while talking to the printer. Due to that OctoPrint disconnected. Error: %(error)s" +msgstr "Es gab einen unbehandelten Fehler bei der Kommunikation mit dem Drucker. Daher hat OctoPrint die Verbindung getrennt. Fehler: %(error)s" #: src/octoprint/static/js/app/helpers.js:385 #, python-format @@ -1923,7 +1924,7 @@ msgstr "Falls der freie Plattenplatz unter diese Schwellwerte fallen sollte wird #: src/octoprint/templates/dialogs/settings/folders.jinja2:47 #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:69 -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 #: src/octoprint/templates/tabs/gcodeviewer.jinja2:66 #: src/octoprint/templates/tabs/timelapse.jinja2:13 msgid "Warning" @@ -2132,30 +2133,66 @@ msgid "Log communication to serial.log (might negatively impact performance)" msgstr "Logge die Kommunikation in das serial.log (kann die Performance negativ beeinflussen)" #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:74 -msgid "Long running commands" -msgstr "Lang laufende Befehle" - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 -msgid "Use this to specify the commands known to take a long time to complete without output from your printer and hence might cause timeout issues. Just the G or M code, comma separated." -msgstr "Nutze diese Option, um solche Befehle zu definieren, von denen Du weißt, dass sie eine längere Zeit lang laufen, währenddessen keinen Output produzieren und daher Timeoutprobleme verursachen könnten. Nur den G- oder M-Code, kommasepariert." - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:81 msgid "Additional serial ports" msgstr "Zusätzliche serielle Ports" -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:84 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 #, python-format msgid "Use this to define additional glob patterns matching serial ports to list for connecting against, e.g. /dev/ttyAMA*. One entry per line." msgstr "Nutze diese Einstellung um zusätzliche glob patterns zu konfigurieren, die auf serielle Ports deines Druckers matchen, z.B. /dev/ttyAMA*. Ein Eintrag pro Zeile." -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:83 msgid "Not only cancel ongoing prints but also disconnect on unhandled errors from the firmware." msgstr "Bei unbehalten Firmwarefehlern nicht nur den Druckauftrag abbrechen, sondern auch die Verbindung zum Drucker trennen." -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 msgid "Ignore any unhandled errors from the firmware. Only use this if your firmware sends stuff prefixed with \"Error\" that is not an actual error. Might mask printer issues, be careful!" msgstr "Alle unbehalten Firmwarefehler ignorieren. Nur nutzen wenn Deine Firmware Dinge mit \"Error\" sendet die nicht wirklich Fehler sind. Könnte Druckerprobleme maskieren, vorsicht!" +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:98 +msgid "Command to send to the firmware on first handshake attempt." +msgstr "Kommando, das als erster Handshakeversuch an die Firmware gesendet werden soll" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:99 +msgid "\"Hello\" command" +msgstr "\"Hallo\"-Befehl" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:102 +msgid "Use this to specify a different command than the default M110 to send to the printer on initial connection to trigger a communication handshake." +msgstr "Nutze diese Einstellung um einen anderen Befehl als M110 beim initialen Verbindungsaufbau zum drucker zu senden." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:105 +msgid "Commands that are know to run long and hence should suppress communication timeouts from being triggered." +msgstr "Befehle, von denen bekannt ist, dass sie lang zur Ausführung benötigen und daher das Auslösen von Kommunikationstimeouts unterdrücken sollten." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:106 +msgid "Long running commands" +msgstr "Lang laufende Befehle" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:109 +msgid "Use this to specify the commands known to take a long time to complete without output from your printer and hence might cause timeout issues. Just the G or M code, comma separated." +msgstr "Nutze diese Option, um solche Befehle zu definieren, von denen Du weißt, dass sie eine längere Zeit lang laufen, währenddessen keinen Output produzieren und daher Timeoutprobleme verursachen könnten. Nur den G- oder M-Code, kommasepariert." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:112 +msgid "Commands that always require a line number and checksum to be sent with them." +msgstr "Befehle, die immer mit einer Prüfsumme und Zeilennummer gesendet werden müssen." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:113 +msgid "Commands that always require a checksum" +msgstr "Befehle, die immer eine Prüfsumme benötigen" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:116 +msgid "Use this to specify which commands always need to be sent with a checksum. Comma separated list." +msgstr "Nutze diese Einstellung um Befehle zu spezifizieren, die immer mit Prüfsumme gesendet werden müssen. Komma-separierte Liste." + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Generate additional ok for M29" +msgstr "Zusätzliches ok für M29 generieren" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Most Marlin < v1.1.0" +msgstr "Viele Marlin < v1.1.0" + #: src/octoprint/templates/dialogs/settings/server.jinja2:2 msgid "Commands" msgstr "Befehle" diff --git a/translations/messages.pot b/translations/messages.pot index df0a199a..a56736bb 100644 --- a/translations/messages.pot +++ b/translations/messages.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: OctoPrint 1.2.9.dev71+g92c65cb.dirty\n" +"Project-Id-Version: OctoPrint 1.2.10.dev50+g6305c4d\n" "Report-Msgid-Bugs-To: i18n@octoprint.org\n" -"POT-Creation-Date: 2016-02-10 11:12+0100\n" +"POT-Creation-Date: 2016-03-16 09:14+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -552,6 +552,7 @@ msgstr "" #: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:182 #: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:63 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:96 #: src/octoprint/templates/tabs/terminal.jinja2:27 msgid "Advanced options" msgstr "" @@ -1126,21 +1127,21 @@ msgstr "" #: src/octoprint/static/js/app/dataupdater.js:352 #: src/octoprint/static/js/app/dataupdater.js:360 -msgid "Unhandled firmware error" +msgid "Unhandled communication error" msgstr "" #: src/octoprint/static/js/app/dataupdater.js:353 #, python-format msgid "" -"The firmware reported an unhandled error. Due to that the ongoing print " -"job was cancelled. Error: %(firmwareError)s" +"There was an unhandled error while talking to the printer. Due to that " +"the ongoing print job was cancelled. Error: %(firmwareError)s" msgstr "" #: src/octoprint/static/js/app/dataupdater.js:361 #, python-format msgid "" -"The firmware reported an unhandled error. Due to that OctoPrint " -"disconnected. Error: %(error)s" +"The was an unhandled error while talking to the printer. Due to that " +"OctoPrint disconnected. Error: %(error)s" msgstr "" #: src/octoprint/static/js/app/helpers.js:385 @@ -2038,7 +2039,7 @@ msgstr "" #: src/octoprint/templates/dialogs/settings/folders.jinja2:47 #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:69 -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 #: src/octoprint/templates/tabs/gcodeviewer.jinja2:66 #: src/octoprint/templates/tabs/timelapse.jinja2:13 msgid "Warning" @@ -2266,21 +2267,10 @@ msgid "Log communication to serial.log (might negatively impact performance)" msgstr "" #: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:74 -msgid "Long running commands" -msgstr "" - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 -msgid "" -"Use this to specify the commands known to take a long time to complete " -"without output from your printer and hence might cause timeout issues. " -"Just the G or M code, comma separated." -msgstr "" - -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:81 msgid "Additional serial ports" msgstr "" -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:84 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:77 #, python-format msgid "" "Use this to define additional glob patterns" @@ -2288,19 +2278,75 @@ msgid "" "/dev/ttyAMA*. One entry per line." msgstr "" -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:83 msgid "" "Not only cancel ongoing prints but also disconnect on unhandled errors " "from the firmware." msgstr "" -#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:97 +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90 msgid "" "Ignore any unhandled errors from the firmware. Only use this if your " "firmware sends stuff prefixed with \"Error\" that is not an actual error." " Might mask printer issues, be careful!" msgstr "" +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:98 +msgid "Command to send to the firmware on first handshake attempt." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:99 +msgid "\"Hello\" command" +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:102 +msgid "" +"Use this to specify a different command than the default " +"M110 to send to the printer on initial connection to trigger" +" a communication handshake." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:105 +msgid "" +"Commands that are know to run long and hence should suppress " +"communication timeouts from being triggered." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:106 +msgid "Long running commands" +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:109 +msgid "" +"Use this to specify the commands known to take a long time to complete " +"without output from your printer and hence might cause timeout issues. " +"Just the G or M code, comma separated." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:112 +msgid "" +"Commands that always require a line number and checksum to be sent with " +"them." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:113 +msgid "Commands that always require a checksum" +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:116 +msgid "" +"Use this to specify which commands always need to be " +"sent with a checksum. Comma separated list." +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Generate additional ok for M29" +msgstr "" + +#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:121 +msgid "Most Marlin < v1.1.0" +msgstr "" + #: src/octoprint/templates/dialogs/settings/server.jinja2:2 msgid "Commands" msgstr ""