├── .gitignore ├── babel.cfg ├── MANIFEST.in ├── requirements.txt ├── .editorconfig ├── octoprint_preheat ├── static │ ├── css │ │ └── preheat.css │ └── js │ │ └── preheat.js ├── templates │ └── preheat_settings.jinja2 └── __init__.py ├── README.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | *.iml 5 | build 6 | dist 7 | *.egg* 8 | .DS_Store 9 | *.zip 10 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: */**.py] 2 | [jinja2: */**.jinja2] 3 | extensions=jinja2.ext.autoescape, jinja2.ext.with_ 4 | 5 | [javascript: */**.js] 6 | extract_messages = gettext, ngettext 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include octoprint_preheat/templates * 3 | recursive-include octoprint_preheat/translations * 4 | recursive-include octoprint_preheat/static * 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ### 2 | # This file is only here to make sure that something like 3 | # 4 | # pip install -e . 5 | # 6 | # works as expected. Requirements can be found in setup.py. 7 | ### 8 | 9 | . 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [**.py] 13 | indent_style = tab 14 | 15 | [**.js] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /octoprint_preheat/static/css/preheat.css: -------------------------------------------------------------------------------- 1 | #touch body .progress-text-centered { 2 | margin-top: 60px !important; 3 | } 4 | 5 | #touch body #state .accordion-inner { 6 | padding-top: 45px !important; 7 | } 8 | 9 | .preheat_settings_item { 10 | overflow: auto; 11 | } 12 | 13 | .preheat_long_label { 14 | width: 300px !important; 15 | margin-right: 10px; 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preheat Button 2 | 3 | This Octoprint plugin adds a preheat button to preheat the nozzle and bed to the printing temperature of the selected gcode file. 4 | This can be done manually but this plugin makes it more convenient. 5 | If the target temperature is not zero, the button will instead turn off nozzle heating (cooldown). 6 | 7 | ![Screenshot](https://i.imgur.com/5eTx0pb.png) 8 | 9 | ## Setup 10 | 11 | Install via the bundled [Plugin Manager](https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager) 12 | or manually using this URL: 13 | 14 | https://github.com/marian42/octoprint-preheat/archive/master.zip 15 | 16 | ## Troubleshooting 17 | 18 | If you have a printer that adds image data to the top of the G-Code file (Prusa Mini), the default of checking the first 1000 lines for set temperature commands might not be enough. Use the setting labeled "Max number of lines to look for preheat commands" to adjust how many lines are looked at. 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | ######################################################################################################################## 4 | ### Do not forget to adjust the following variables to your own plugin. 5 | 6 | # The plugin's identifier, has to be unique 7 | plugin_identifier = "preheat" 8 | 9 | # The plugin's python package, should be "octoprint_", has to be unique 10 | plugin_package = "octoprint_preheat" 11 | 12 | # The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the 13 | # plugin module 14 | plugin_name = "Preheat" 15 | 16 | # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module 17 | plugin_version = "0.8.0" 18 | 19 | # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin 20 | # module 21 | plugin_description = """Automatically heat printhead to the printing temperature of the current gcode file""" 22 | 23 | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module 24 | plugin_author = "Marian Kleineberg" 25 | 26 | # The plugin's author's mail address. 27 | plugin_author_email = "mail@marian42.de" 28 | 29 | # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module 30 | plugin_url = "https://github.com/marian42/octoprint-preheat" 31 | 32 | # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module 33 | plugin_license = "AGPLv3" 34 | 35 | # Any additional requirements besides OctoPrint should be listed here 36 | plugin_requires = [] 37 | 38 | ### -------------------------------------------------------------------------------------------------------------------- 39 | ### More advanced options that you usually shouldn't have to touch follow after this point 40 | ### -------------------------------------------------------------------------------------------------------------------- 41 | 42 | # Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will 43 | # already be installed automatically if they exist. Note that if you add something here you'll also need to update 44 | # MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your 45 | # files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598 46 | plugin_additional_data = [] 47 | 48 | # Any additional python packages you need to install with your plugin that are not contained in .* 49 | plugin_additional_packages = [] 50 | 51 | # Any python packages within .* you do NOT want to install with your plugin 52 | plugin_ignored_packages = [] 53 | 54 | # Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, 55 | # define dependency links or other things like that, this is the place to go. Will be merged recursively with the 56 | # default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using 57 | # octoprint.util.dict_merge. 58 | # 59 | # Example: 60 | # plugin_requires = ["someDependency==dev"] 61 | # additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} 62 | additional_setup_parameters = {} 63 | 64 | ######################################################################################################################## 65 | 66 | from setuptools import setup 67 | 68 | try: 69 | import octoprint_setuptools 70 | except: 71 | print("Could not import OctoPrint's setuptools, are you sure you are running that under " 72 | "the same python installation that OctoPrint is installed under?") 73 | import sys 74 | sys.exit(-1) 75 | 76 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 77 | identifier=plugin_identifier, 78 | package=plugin_package, 79 | name=plugin_name, 80 | version=plugin_version, 81 | description=plugin_description, 82 | author=plugin_author, 83 | mail=plugin_author_email, 84 | url=plugin_url, 85 | license=plugin_license, 86 | requires=plugin_requires, 87 | additional_packages=plugin_additional_packages, 88 | ignored_packages=plugin_ignored_packages, 89 | additional_data=plugin_additional_data 90 | ) 91 | 92 | if len(additional_setup_parameters): 93 | from octoprint.util import dict_merge 94 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 95 | 96 | setup(**setup_parameters) 97 | -------------------------------------------------------------------------------- /octoprint_preheat/static/js/preheat.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | function PreheatViewModel(parameters) { 3 | var self = this; 4 | self.settings = undefined; 5 | self.btnPreheat = undefined; 6 | self.btnPreheatIcon = undefined; 7 | self.btnPreheatText = undefined; 8 | 9 | self.MODE_PREHEAT = 0; 10 | self.MODE_COOLDOWN = 1; 11 | 12 | self.mode = self.MODE_PREHEAT; 13 | 14 | self.loginState = parameters[0]; 15 | self.temperatureState = parameters[1]; 16 | self.printerState = parameters[2]; 17 | self.settings = parameters[3]; 18 | 19 | self.preheat = function() { 20 | $.ajax({ 21 | url: API_BASEURL + "plugin/preheat", 22 | type: "POST", 23 | dataType: "json", 24 | data: JSON.stringify({ 25 | command: "preheat" 26 | }), 27 | contentType: "application/json; charset=UTF-8", 28 | error: function (data, status) { 29 | var options = { 30 | title: "Preheating failed.", 31 | text: data.responseText, 32 | hide: true, 33 | buttons: { 34 | sticker: false, 35 | closer: true 36 | }, 37 | type: "error" 38 | }; 39 | 40 | new PNotify(options); 41 | } 42 | }); 43 | }; 44 | 45 | self.setTemperaturesToZero = function() { 46 | var targets = {}; 47 | for (tool of Object.keys(self.temperatureState.tools())) { 48 | targets["tool" + tool] = 0; 49 | } 50 | 51 | $.ajax({ 52 | url: API_BASEURL + "printer/tool", 53 | type: "POST", 54 | dataType: "json", 55 | data: JSON.stringify({ 56 | command: "target", 57 | targets: targets 58 | }), 59 | contentType: "application/json; charset=UTF-8" 60 | }); 61 | if (self.temperatureState.hasBed()) { 62 | $.ajax({ 63 | url: API_BASEURL + "printer/bed", 64 | type: "POST", 65 | dataType: "json", 66 | data: JSON.stringify({ 67 | command: "target", 68 | target: 0 69 | }), 70 | contentType: "application/json; charset=UTF-8" 71 | }); 72 | } 73 | if (self.temperatureState.hasChamber()) { 74 | $.ajax({ 75 | url: API_BASEURL + "printer/chamber", 76 | type: "POST", 77 | dataType: "json", 78 | data: JSON.stringify({ 79 | command: "target", 80 | target: 0 81 | }), 82 | contentType: "application/json; charset=UTF-8" 83 | }); 84 | } 85 | }; 86 | 87 | self.cooldown = function() { 88 | if (self.settings.settings.plugins.preheat.use_m109()) { 89 | $.ajax({ 90 | url: API_BASEURL + "printer/command", 91 | type: "POST", 92 | dataType: "json", 93 | data: JSON.stringify({ 94 | commands: ["M108"], 95 | parameters: {} 96 | }), 97 | contentType: "application/json; charset=UTF-8", 98 | success: function() { 99 | self.setTemperaturesToZero(); 100 | } 101 | }); 102 | } else { 103 | self.setTemperaturesToZero(); 104 | } 105 | }; 106 | 107 | self.btnPreheatClick = function() { 108 | if (self.mode == self.MODE_PREHEAT) { 109 | self.preheat(); 110 | } 111 | if (self.mode == self.MODE_COOLDOWN) { 112 | self.cooldown(); 113 | } 114 | } 115 | 116 | self.initializeButton = function() { 117 | var buttonContainer = $('#job_print')[0].parentElement; 118 | buttonContainer.children[0].style.width = "100%"; 119 | buttonContainer.children[0].style.marginBottom = "10px"; 120 | buttonContainer.children[0].classList.remove("span4"); 121 | buttonContainer.children[1].style.marginLeft = "0"; 122 | 123 | self.btnPreheat = document.createElement("button"); 124 | self.btnPreheat.id = "job_preheat"; 125 | self.btnPreheat.classList.add("btn"); 126 | self.btnPreheat.classList.add("span4"); 127 | self.btnPreheat.addEventListener("click", self.btnPreheatClick); 128 | 129 | self.btnPreheatIcon = document.createElement("i"); 130 | self.btnPreheat.appendChild(self.btnPreheatIcon); 131 | 132 | self.btnPreheatText = document.createElement("span"); 133 | self.btnPreheat.appendChild(self.btnPreheatText); 134 | 135 | self.btnPreheatText.textContent = " Preheat"; 136 | self.btnPreheatIcon.classList.add("fa", "fa-fire"); 137 | 138 | buttonContainer.appendChild(self.btnPreheat); 139 | }; 140 | 141 | self.anyTemperatureTarget = function() { 142 | if (self.temperatureState.hasBed() && self.temperatureState.bedTemp.target() != 0) { 143 | return true; 144 | } 145 | 146 | for (var i = 0; i < self.temperatureState.tools().length; i++) { 147 | if (self.temperatureState.tools()[i].target() != 0) { 148 | return true; 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | 155 | self.updateButton = function() { 156 | if (!self.anyTemperatureTarget()) { 157 | self.mode = self.MODE_PREHEAT; 158 | self.btnPreheat.title = "Preheats the nozzle for the loaded gcode file."; 159 | self.btnPreheatText.textContent = " Preheat"; 160 | self.btnPreheatIcon.classList.add("fa-fire"); 161 | self.btnPreheatIcon.classList.remove("fa-snowflake-o"); 162 | } else { 163 | self.mode = self.MODE_COOLDOWN; 164 | self.btnPreheat.title = "Disables tool heating."; 165 | self.btnPreheatText.textContent = " Cool"; 166 | self.btnPreheatIcon.classList.add("fa-snowflake-o"); 167 | self.btnPreheatIcon.classList.remove("fa-fire"); 168 | } 169 | 170 | var target = 0; 171 | if (self.temperatureState.tools().length > 0) { 172 | target = self.temperatureState.tools()[0].target(); 173 | } 174 | 175 | self.btnPreheat.disabled = !self.temperatureState.isReady() 176 | || self.temperatureState.isPrinting() 177 | || !self.loginState.isUser() 178 | || (target == 0 && self.printerState.filename() == null && !self.settings.settings.plugins.preheat.use_fallback_when_no_file_selected()); 179 | }; 180 | 181 | self.onDataUpdaterPluginMessage = function(plugin, data) { 182 | if (plugin == "preheat" && data.type == "preheat_complete") { 183 | new PNotify({ 184 | title: 'Preheat complete', 185 | text: 'Printing temperatures reached.', 186 | type: 'success' 187 | }); 188 | } 189 | if (plugin == "preheat" && data.type == "preheat_warning") { 190 | new PNotify({ 191 | title: 'Preheating cancelled', 192 | text: data.message, 193 | type: 'warning' 194 | }); 195 | } 196 | } 197 | 198 | self.initializeButton(); 199 | self.fromCurrentData = function() { self.updateButton(); }; 200 | self.updateButton(); 201 | } 202 | 203 | OCTOPRINT_VIEWMODELS.push([ 204 | PreheatViewModel, 205 | ["loginStateViewModel", "temperatureViewModel", "printerStateViewModel", "settingsViewModel"], 206 | [] 207 | ]); 208 | }); 209 | -------------------------------------------------------------------------------- /octoprint_preheat/templates/preheat_settings.jinja2: -------------------------------------------------------------------------------- 1 |
2 |
3 | Enable Preheating 4 | 5 |
6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 |

27 | 28 | Fallback Temperatures 29 |
30 | Fallback temperatures are used if the gcode file is on the SD card or if no heating command was found in the gcode file. 31 | Set these to 0 to not preheat in these cases. 32 |
33 | 34 |
35 | 36 |
37 | 38 | ° 39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 | ° 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 | ° 55 |
56 |
57 | 58 |
59 | 60 |
61 |

62 | 63 | Temperature offsets 64 |
65 | Temperature offsets are added to the initial temperatures in the gcode file in addition to the current temperature offsets configured in Octoprint. Use negative values to lower preheat temperatures. Does not affect fallback temperatures. 66 |
67 |
68 | 69 |
70 | 71 | ° 72 |
73 |
74 |
75 |
76 | 77 |
78 | 79 | ° 80 |
81 |
82 |
83 |
84 | 85 |
86 | 87 | ° 88 |
89 |
90 |

91 | 92 | Sequenced Preheating 93 |
94 | Preheat bed and chamber first, heat printheads once the bed and chamber has 95 | reached its target temperature. 96 | If this option is disabled, the bed, chamber and printheads are heated right away. 97 | If you don't have a heated bed or chamber, this option makes no difference. 98 |
99 | 100 |
101 | 102 |
103 |

104 | Before Preheating 105 |
106 | 107 |
108 | 109 |
110 | 111 |
112 | 113 |
114 |
115 | 116 |

117 | After Preheating is complete 118 |
119 | 120 |
121 | 122 |
123 |
124 |
125 | 126 |
127 | 128 |
129 | 130 |
131 | 132 |
133 |
134 | 135 |

136 | 137 | Use M109/M190/M191 commands 138 |
139 | Use M109/M190/M191 commands to set the temperature in addition to the regular temperature commands. 140 | This will instruct the printer to wait until the temperature is reached and in turn will activate LEDs on some printers. 141 |
142 | 143 |
144 | 145 |
146 | 147 |

148 | 149 | Preheating on file select 150 |
151 | Automatically trigger preheating when a file is selected 152 |
153 | 154 |
155 | 156 |
157 |
158 |
159 |
160 | -------------------------------------------------------------------------------- /octoprint_preheat/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import 3 | 4 | from flask_login import current_user 5 | 6 | import octoprint.filemanager 7 | import octoprint.plugin 8 | from octoprint.util.comm import strip_comment 9 | from octoprint.printer import PrinterInterface 10 | 11 | import flask 12 | import time 13 | from threading import Thread 14 | 15 | __plugin_pythoncompat__ = ">=2.7,<4" 16 | 17 | class PreheatError(Exception): 18 | def __init__(self, message): 19 | super(PreheatError, self).__init__(message) 20 | self.message = message 21 | 22 | class PreheatAPIPlugin( 23 | octoprint.plugin.TemplatePlugin, 24 | octoprint.plugin.SimpleApiPlugin, 25 | octoprint.plugin.AssetPlugin, 26 | octoprint.plugin.SettingsPlugin, 27 | octoprint.plugin.EventHandlerPlugin, 28 | ): 29 | 30 | def get_settings_defaults(self): 31 | return dict(enable_tool = True, 32 | enable_bed = True, 33 | enable_chamber = True, 34 | fallback_tool = 0, 35 | fallback_bed = 0, 36 | fallback_chamber = 0, 37 | offset_tool=0, # offsets from plugin 38 | offset_bed=0, 39 | offset_chamber=0, 40 | wait_for_bed = False, 41 | preheat_on_file_select = False, 42 | on_start_send_gcode = False, 43 | on_start_send_gcode_command = "M117 Preheating... ; Update LCD", 44 | on_complete_show_popup = False, 45 | on_conplete_send_gcode = False, 46 | on_conplete_send_gcode_command = "M117 Preheat complete. ; Update LCD\nM300 S660 P200 ; Beep", 47 | use_fallback_when_no_file_selected = False, 48 | max_gcode_lines = 1000, 49 | use_m109 = False 50 | ) 51 | 52 | 53 | def get_template_configs(self): 54 | return [ 55 | dict(type="settings", custom_bindings = False) 56 | ] 57 | 58 | 59 | def get_assets(self): 60 | return dict( 61 | js = ["js/preheat.js"], 62 | css = ["css/preheat.css"] 63 | ) 64 | 65 | 66 | def get_api_commands(self): 67 | return dict( 68 | preheat = [] 69 | ) 70 | 71 | 72 | def parse_line(self, line, tool="tool0"): 73 | line = strip_comment(line) 74 | 75 | temperature = None 76 | for item in line.split(" "): 77 | if item.startswith("S"): 78 | try: 79 | value = float(item[1:]) 80 | if value > 0: 81 | temperature = value 82 | except ValueError: 83 | self._logger.warn("Error parsing heat command: {}".format(line)) 84 | pass 85 | if item.startswith("T"): 86 | new_tool = "tool" + item[1:].strip() 87 | if PrinterInterface.valid_heater_regex.match(new_tool): 88 | tool = new_tool 89 | 90 | return tool, temperature 91 | 92 | 93 | def read_temperatures_from_file(self, path_on_disk): 94 | enable_bed = self._settings.get_boolean(["enable_bed"]) 95 | enable_tool = self._settings.get_boolean(["enable_tool"]) 96 | enable_chamber = self._settings.get_boolean(["enable_chamber"]) 97 | 98 | file = open(path_on_disk, 'r') 99 | line = file.readline() 100 | max_lines = self._settings.get_int(["max_gcode_lines"]) 101 | temperatures = dict() 102 | current_tool = "tool0" 103 | try: 104 | with open(path_on_disk, "r") as file: 105 | while max_lines > 0: 106 | line = file.readline() 107 | if line == "": 108 | break 109 | if line.startswith("T"): # Select tool 110 | new_tool = "tool" + strip_comment(line)[1:].strip() 111 | if new_tool == "tool": 112 | new_tool = "tool0" 113 | if PrinterInterface.valid_heater_regex.match(new_tool): 114 | current_tool = new_tool 115 | if enable_tool and (line.startswith("M104") or line.startswith("M109")): # Set tool temperature 116 | tool, temperature = self.parse_line(line, current_tool) 117 | if temperature != None and tool not in temperatures: 118 | temperatures[tool] = temperature 119 | if enable_bed and (line.startswith("M190") or line.startswith("M140")): # Set bed temperature 120 | _, temperature = self.parse_line(line) 121 | if temperature != None and "bed" not in temperatures: 122 | temperatures["bed"] = temperature 123 | if enable_chamber and (line.startswith("M191") or line.startswith("M141")): # Set chamber temperature 124 | _, temperature = self.parse_line(line) 125 | if temperature != None and "chamber" not in temperatures: 126 | temperatures["chamber"] = temperature 127 | 128 | max_lines -= 1 129 | except: 130 | self._logger.exception("Something went wrong while trying to read the preheat temperature from {}".format(path_on_disk)) 131 | 132 | return temperatures 133 | 134 | 135 | def get_fallback_temperatures(self): 136 | enable_bed = self._settings.get_boolean(["enable_bed"]) 137 | enable_tool = self._settings.get_boolean(["enable_tool"]) 138 | enable_chamber = self._settings.get_boolean(["enable_chamber"]) 139 | fallback_tool = self._settings.get_float(["fallback_tool"]) 140 | fallback_bed = self._settings.get_float(["fallback_bed"]) 141 | fallback_chamber = self._settings.get_float(["fallback_chamber"]) 142 | 143 | printer_profile = self._printer._printerProfileManager.get_current_or_default() 144 | 145 | result = dict() 146 | 147 | if enable_bed and fallback_bed > 0 and printer_profile["heatedBed"]: 148 | result["bed"] = fallback_bed 149 | 150 | if enable_chamber and fallback_chamber > 0 and printer_profile["heatedChamber"]: 151 | result["chamber"] = fallback_chamber 152 | 153 | if enable_tool and fallback_tool > 0: 154 | extruder_count = printer_profile["extruder"]["count"] 155 | for i in range(extruder_count): 156 | tool = "tool" + str(i) 157 | result[tool] = fallback_tool 158 | 159 | return result 160 | 161 | 162 | def get_temperatures(self, file_name=None): 163 | if not self._settings.get_boolean(["enable_bed"]) and \ 164 | not self._settings.get_boolean(["enable_tool"]) and \ 165 | not self._settings.get_boolean(["enable_chamber"]): 166 | raise PreheatError("Preheating is disabled in the plugin settings.") 167 | 168 | if file_name is not None: 169 | path_on_disk = octoprint.server.fileManager.path_on_disk(octoprint.filemanager.FileDestinations.LOCAL, file_name) 170 | temperatures = self.read_temperatures_from_file(path_on_disk) 171 | temperatures = self.apply_offsets_from_plugin(temperatures) 172 | 173 | elif (self._printer.get_current_job()["file"]["path"] == None): 174 | if self._settings.get_boolean(["use_fallback_when_no_file_selected"]): 175 | temperatures = self.get_fallback_temperatures() 176 | else: 177 | raise PreheatError("No gcode file loaded.") 178 | 179 | elif self._printer.get_current_job()["file"]["origin"] == octoprint.filemanager.FileDestinations.SDCARD: 180 | temperatures = self.get_fallback_temperatures() 181 | 182 | if len(temperatures) == 0: 183 | raise PreheatError("Can't read the temperature from a gcode file stored on the SD card.") 184 | else: 185 | self._logger.info("Can't read the temperatures from the SD card, using fallback temperatures.") 186 | 187 | else: 188 | file_name = self._printer.get_current_job()["file"]["path"] 189 | path_on_disk = octoprint.server.fileManager.path_on_disk(octoprint.filemanager.FileDestinations.LOCAL, file_name) 190 | temperatures = self.read_temperatures_from_file(path_on_disk) 191 | temperatures = self.apply_offsets_from_plugin(temperatures) 192 | 193 | if len(temperatures) == 0: 194 | temperatures = self.get_fallback_temperatures() 195 | if len(temperatures) == 0: 196 | raise PreheatError("Could not find any preheat commands in the gcode file. You can configure fallback temperatures for this case.") 197 | else: 198 | self._logger.info("Could not find any preheat commands in the gcode file, using fallback temperatures.") 199 | 200 | offsets = self._printer.get_current_data()["offsets"] 201 | for tool in temperatures: 202 | if tool in offsets: 203 | temperatures[tool] += offsets[tool] 204 | 205 | return temperatures 206 | 207 | def apply_offsets_from_plugin(self, temperatures): 208 | for tool, temperature in temperatures.items(): 209 | temperatures[tool] = self.apply_offset(tool, temperature) 210 | return temperatures 211 | 212 | def apply_offset(self, tool, temperature): 213 | if tool.startswith('tool'): 214 | tool = 'tool' 215 | type_of_offset = 'offset_' + tool 216 | offset = self._settings.get_float([type_of_offset]) or 0 217 | if offset > 50: 218 | self._logger.warn( 219 | "Ignoring preheat temperature offset of {}° as it is above 50°.".format(tool)) 220 | else: 221 | temperature = max(0, temperature + offset) 222 | if (offset != 0): 223 | self._logger.info('Applied preheat offset of {}° for {}.'.format(offset, tool)) 224 | if temperature >= 260 and offset >= 10: 225 | self._logger.warn( 226 | "Preheat offset results in very high temperature of {}°".format(temperature)) 227 | return temperature 228 | 229 | def check_state(self): 230 | if not self._printer.is_operational() or self._printer.is_printing(): 231 | raise PreheatError("Can't set the temperature because the printer is not ready.") 232 | 233 | 234 | def preheat_and_wait(self, preheat_temperatures): 235 | self.preheat_immediately(preheat_temperatures) 236 | 237 | current_temperatures = self._printer.get_current_temperatures() 238 | initial_targets = {tool: current_temperatures[tool]["target"] for tool in preheat_temperatures.keys()} 239 | for tool, temperature in preheat_temperatures.items(): 240 | initial_targets[tool] = temperature 241 | 242 | time_waited = 0 243 | TIME_STEP = 0.4 244 | 245 | while (True): 246 | time.sleep(TIME_STEP) 247 | time_waited += TIME_STEP 248 | 249 | current_temperatures = self._printer.get_current_temperatures() 250 | 251 | if time_waited > 10: 252 | for tool in initial_targets: 253 | if current_temperatures[tool]["target"] != initial_targets[tool]: 254 | raise PreheatError("Preheating cancelled because the temperature was changed manually.") 255 | 256 | if not self._printer.is_operational() or self._printer.is_printing(): 257 | raise PreheatError("Preheating cancelled because the printer state changed.") 258 | 259 | complete = [abs(current_temperatures[tool]["actual"] - preheat_temperatures[tool]) < 4 for tool in preheat_temperatures] 260 | if all(complete): 261 | return 262 | 263 | 264 | def notify_preheat_complete(self): 265 | self._logger.info("Preheating complete.") 266 | if self._settings.get_boolean(["on_complete_show_popup"]): 267 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="preheat_complete")) 268 | if self._settings.get_boolean(["on_conplete_send_gcode"]): 269 | command = self._settings.get(["on_conplete_send_gcode_command"]) 270 | self._printer.commands(command.split("\n")) 271 | 272 | 273 | def is_notify_on_complete_enabled(self): 274 | return self._settings.get_boolean(["on_complete_show_popup"]) \ 275 | or self._settings.get_boolean(["on_conplete_send_gcode"]) 276 | 277 | def preheat_thread(self, preheat_temperatures): 278 | try: 279 | shoud_wait_for_bed = self._settings.get_boolean(["wait_for_bed"]) and "bed" in preheat_temperatures 280 | should_wait_for_chamber = self._settings.get_boolean(["wait_for_bed"]) and self._settings.get_boolean(["enable_chamber"]) and "chamber" in preheat_temperatures 281 | if shoud_wait_for_bed or should_wait_for_chamber: 282 | items_to_wait_for = {} 283 | if shoud_wait_for_bed: 284 | items_to_wait_for["bed"] = preheat_temperatures["bed"] 285 | if should_wait_for_chamber: 286 | items_to_wait_for["chamber"] = preheat_temperatures["chamber"] 287 | self.preheat_and_wait(items_to_wait_for) 288 | 289 | if self.is_notify_on_complete_enabled(): 290 | self.preheat_and_wait(preheat_temperatures) 291 | self.notify_preheat_complete() 292 | else: 293 | self.preheat_immediately(preheat_temperatures) 294 | except PreheatError as error: 295 | self._logger.warn("Preheat error: " + str(error.message)) 296 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="preheat_warning", message=error.message)) 297 | 298 | 299 | def preheat_immediately(self, preheat_temperatures): 300 | for tool, target in preheat_temperatures.items(): 301 | self._logger.info("Preheating " + tool + " to " + str(target) + ".") 302 | 303 | self._printer.set_temperature(tool, target) 304 | 305 | if self._settings.get_boolean(["use_m109"]): 306 | self._logger.info("Using M109/M190/M191 commands to set the temperature.") 307 | if tool.startswith("tool"): 308 | command = "M109 S{0:d} T{1:s}".format(int(target), tool[4:]) 309 | elif tool == "bed": 310 | command = "M190 S{0:d}".format(int(target)) 311 | elif tool == "chamber": 312 | command = "M191 S{0:d}".format(int(target)) 313 | else: 314 | continue 315 | self._printer.commands(command) 316 | 317 | def preheat(self, file_name=None): 318 | self.check_state() 319 | 320 | if self._settings.get_boolean(["on_start_send_gcode"]): 321 | command = self._settings.get(["on_start_send_gcode_command"]) 322 | self._printer.commands(command.split("\n")) 323 | 324 | preheat_temperatures = self.get_temperatures(file_name) 325 | 326 | use_thread = self._settings.get_boolean(["wait_for_bed"]) or self.is_notify_on_complete_enabled() 327 | 328 | if use_thread: 329 | thread = Thread(target = self.preheat_thread, args = (preheat_temperatures, )) 330 | thread.start() 331 | else: 332 | self.preheat_immediately(preheat_temperatures) 333 | 334 | def on_api_command(self, command, data): 335 | if command == "preheat": 336 | if current_user.is_anonymous(): 337 | return "Insufficient rights", 403 338 | try: 339 | self.preheat() 340 | except PreheatError as error: 341 | self._logger.info("Preheat error: " + str(error.message)) 342 | return str(error.message), 405 343 | 344 | def cooldown(self): 345 | enable_bed = self._settings.get_boolean(["enable_bed"]) 346 | enable_tool = self._settings.get_boolean(["enable_tool"]) 347 | enable_chamber = self._settings.get_boolean(["enable_chamber"]) 348 | 349 | printer_profile = self._printer._printerProfileManager.get_current_or_default() 350 | 351 | if enable_bed and printer_profile["heatedBed"]: 352 | self._printer.set_temperature("bed", 0) 353 | 354 | if enable_chamber and printer_profile["heatedChamber"]: 355 | self._printer.set_temperature("chamber", 0) 356 | 357 | if enable_tool: 358 | extruder_count = printer_profile["extruder"]["count"] 359 | for i in range(extruder_count): 360 | self._printer.set_temperature("tool{0:d}".format(i), 0) 361 | 362 | def on_event(self, event, payload): 363 | self._logger.debug("Event received: " + event) 364 | enable_preheat_on_file_select = self._settings.get_boolean(["preheat_on_file_select"]) 365 | 366 | if enable_preheat_on_file_select and event == "FileSelected": 367 | self.preheat(file_name=payload["path"]) 368 | elif enable_preheat_on_file_select and event == "FileDeselected": 369 | self._logger.info("FileDeselected event received, cooling down") 370 | self.cooldown() 371 | 372 | def get_gcode_script_variables(self, comm, script_type, script_name, *args, **kwargs): 373 | if not script_type == "gcode": 374 | return None 375 | 376 | prefix = None 377 | postfix = None 378 | try: 379 | variables = self.get_temperatures() 380 | except PreheatError: 381 | variables = {} 382 | return prefix, postfix, variables 383 | 384 | 385 | def get_update_information(self, *args, **kwargs): 386 | return dict( 387 | preheat = dict( 388 | displayName=self._plugin_name, 389 | displayVersion=self._plugin_version, 390 | 391 | type="github_release", 392 | current=self._plugin_version, 393 | user="marian42", 394 | repo="octoprint-preheat", 395 | 396 | pip="https://github.com/marian42/octoprint-preheat/archive/{target}.zip" 397 | ) 398 | ) 399 | 400 | 401 | __plugin_name__ = "Preheat Button" 402 | __plugin_implementation__ = PreheatAPIPlugin() 403 | 404 | __plugin_hooks__ = { 405 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, 406 | "octoprint.comm.protocol.scripts": __plugin_implementation__.get_gcode_script_variables 407 | } 408 | --------------------------------------------------------------------------------