├── .editorconfig ├── .gitignore ├── MANIFEST.in ├── README.md ├── babel.cfg ├── continuousprint ├── __init__.py ├── static │ └── js │ │ └── continuousprint.js └── templates │ ├── README.txt │ ├── continuousprint_settings.jinja2 │ └── continuousprint_tab.jinja2 ├── requirements.txt └── setup.py /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | *.iml 5 | build 6 | dist 7 | *.egg* 8 | .DS_Store 9 | *.zip 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include continuousprint/templates * 3 | recursive-include continuousprint/translations * 4 | recursive-include continuousprint/static * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Continuous Print Queue Plugin 2 | 3 | Octoprint plugin that allows users to generate a print queue, specify a print bed clearning script and run the queue which will print-clear-print until the end of the queue. 4 | 5 | ## Setup 6 | 7 | Install manually using this URL: 8 | 9 | https://github.com/nukeem/continuousprint/archive/master.zip 10 | 11 | 12 | 13 | ## Configuration 14 | 15 | Make sure you have a method of clearning the bed automatically and have set the print bed clearing script or you'll end up messing the first print. 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /continuousprint/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import 3 | 4 | import octoprint.plugin 5 | import flask, json 6 | from octoprint.server.util.flask import restricted_access 7 | from octoprint.events import eventManager, Events 8 | 9 | class ContinuousprintPlugin(octoprint.plugin.SettingsPlugin, 10 | octoprint.plugin.TemplatePlugin, 11 | octoprint.plugin.AssetPlugin, 12 | octoprint.plugin.StartupPlugin, 13 | octoprint.plugin.BlueprintPlugin, 14 | octoprint.plugin.EventHandlerPlugin): 15 | 16 | print_history = [] 17 | enabled = False 18 | paused = False 19 | 20 | 21 | ##~~ SettingsPlugin mixin 22 | def get_settings_defaults(self): 23 | return dict( 24 | cp_queue="[]", 25 | cp_bed_clearing_script="M17 ;enable steppers\nM91 ; Set relative for lift\nG0 Z10 ; lift z by 10\nG90 ;back to absolute positioning\nM190 R25 ; set bed to 25 for cooldown\nG4 S90 ; wait for temp stabalisation\nM190 R30 ;verify temp below threshold\nG0 X200 Y235 ;move to back corner\nG0 X110 Y235 ;move to mid bed aft\nG0 Z1v ;come down to 1MM from bed\nG0 Y0 ;wipe forward\nG0 Y235 ;wipe aft\nG28 ; home", 26 | cp_queue_finished="M18 ; disable steppers\nM104 T0 S0 ; extruder heater off\nM140 S0 ; heated bed heater off\nM300 S880 P300 ; beep to show its finished" 27 | ) 28 | 29 | 30 | 31 | 32 | ##~~ StartupPlugin mixin 33 | def on_after_startup(self): 34 | self._logger.info("Continuous Print Plugin started") 35 | 36 | 37 | 38 | 39 | ##~~ Event hook 40 | def on_event(self, event, payload): 41 | from octoprint.events import Events 42 | ## Print complete check it was the print in the bottom of the queue and not just any print 43 | if event == Events.PRINT_DONE: 44 | if self.enabled == True: 45 | self.complete_print(payload) 46 | 47 | # On fail stop all prints 48 | if event == Events.PRINT_FAILED or event == Events.PRINT_CANCELLED: 49 | self.enabled = False # Set enabled to false 50 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="error", msg="Print queue cancelled")) 51 | 52 | if event == Events.PRINTER_STATE_CHANGED: 53 | # If the printer is operational and the last print succeeded then we start next print 54 | state = self._printer.get_state_id() 55 | if state == "OPERATIONAL": 56 | self.start_next_print() 57 | 58 | if event == Events.FILE_SELECTED: 59 | # Add some code to clear the print at the bottom 60 | self._logger.info("File selected") 61 | bed_clearing_script=self._settings.get(["cp_bed_clearing_script"]) 62 | 63 | if event == Events.UPDATED_FILES: 64 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="updatefiles", msg="")) 65 | 66 | def complete_print(self, payload): 67 | queue = json.loads(self._settings.get(["cp_queue"])) 68 | if payload["path"]==queue[0]["path"]: 69 | # Remove the print from the queue 70 | queue.pop(0) 71 | self._settings.set(["cp_queue"], json.dumps(queue)) 72 | self._settings.save() 73 | 74 | # Add to the history 75 | self.print_history.append(dict( 76 | name = payload["name"], 77 | time = payload["time"] 78 | )) 79 | 80 | # Clear down the bed 81 | self.clear_bed() 82 | 83 | # Tell the UI to reload 84 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="reload", msg="")) 85 | else: 86 | enabled = False 87 | 88 | def parse_gcode(self, input_script): 89 | script = [] 90 | for x in input_script: 91 | if x.find("[PAUSE]", 0) > -1: 92 | self.paused = True 93 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="paused", msg="Queue paused")) 94 | else: 95 | script.append(x) 96 | return script; 97 | 98 | def clear_bed(self): 99 | self._logger.info("Clearing bed") 100 | bed_clearing_script=self._settings.get(["cp_bed_clearing_script"]).split("\n") 101 | self._printer.commands(self.parse_gcode(bed_clearing_script)) 102 | 103 | def complete_queue(self): 104 | self.enabled = False # Set enabled to false 105 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="complete", msg="Print Queue Complete")) 106 | queue_finished_script = self._settings.get(["cp_queue_finished"]).split("\n") 107 | self._printer.commands(self.parse_gcode(queue_finished_script)) 108 | 109 | def start_next_print(self): 110 | if self.enabled == True and self.paused == False: 111 | queue = json.loads(self._settings.get(["cp_queue"])) 112 | if len(queue) > 0: 113 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="popup", msg="Starting print: " + queue[0]["name"])) 114 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="reload", msg="")) 115 | 116 | sd = False 117 | if queue[0]["sd"] == "true": 118 | sd = True 119 | try: 120 | self._printer.select_file(queue[0]["path"], sd) 121 | self._logger.info(queue[0]["path"]) 122 | self._printer.start_print() 123 | except InvalidFileLocation: 124 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="popup", msg="ERROR file not found")) 125 | except InvalidFileType: 126 | self._plugin_manager.send_plugin_message(self._identifier, dict(type="popup", msg="ERROR file not gcode")) 127 | else: 128 | self.complete_queue() 129 | 130 | 131 | ##~~ APIs 132 | @octoprint.plugin.BlueprintPlugin.route("/queue", methods=["GET"]) 133 | @restricted_access 134 | def get_queue(self): 135 | queue = json.loads(self._settings.get(["cp_queue"])) 136 | 137 | for x in self.print_history: 138 | queue.append(x) 139 | 140 | return flask.jsonify(queue=queue) 141 | 142 | @octoprint.plugin.BlueprintPlugin.route("/queueup", methods=["GET"]) 143 | @restricted_access 144 | def queue_up(self): 145 | index = int(flask.request.args.get("index", 0)) 146 | queue = json.loads(self._settings.get(["cp_queue"])) 147 | orig = queue[index] 148 | queue[index] = queue[index-1] 149 | queue[index-1] = orig 150 | self._settings.set(["cp_queue"], json.dumps(queue)) 151 | self._settings.save() 152 | return flask.jsonify(queue=queue) 153 | 154 | @octoprint.plugin.BlueprintPlugin.route("/queuedown", methods=["GET"]) 155 | @restricted_access 156 | def queue_down(self): 157 | index = int(flask.request.args.get("index", 0)) 158 | queue = json.loads(self._settings.get(["cp_queue"])) 159 | orig = queue[index] 160 | queue[index] = queue[index+1] 161 | queue[index+1] = orig 162 | self._settings.set(["cp_queue"], json.dumps(queue)) 163 | self._settings.save() 164 | return flask.jsonify(queue=queue) 165 | 166 | @octoprint.plugin.BlueprintPlugin.route("/addqueue", methods=["POST"]) 167 | @restricted_access 168 | def add_queue(self): 169 | queue = json.loads(self._settings.get(["cp_queue"])) 170 | queue.append(dict( 171 | name=flask.request.form["name"], 172 | path=flask.request.form["path"], 173 | sd=flask.request.form["sd"] 174 | )) 175 | self._settings.set(["cp_queue"], json.dumps(queue)) 176 | self._settings.save() 177 | return flask.make_response("success", 200) 178 | 179 | @octoprint.plugin.BlueprintPlugin.route("/removequeue", methods=["DELETE"]) 180 | @restricted_access 181 | def remove_queue(self): 182 | queue = json.loads(self._settings.get(["cp_queue"])) 183 | self._logger.info(flask.request.args.get("index", 0)) 184 | queue.pop(int(flask.request.args.get("index", 0))) 185 | self._settings.set(["cp_queue"], json.dumps(queue)) 186 | self._settings.save() 187 | return flask.make_response("success", 200) 188 | 189 | @octoprint.plugin.BlueprintPlugin.route("/startqueue", methods=["GET"]) 190 | @restricted_access 191 | def start_queue(self): 192 | self.print_history = [] 193 | self.paused = False 194 | self.enabled = True # Set enabled to true 195 | self.start_next_print() 196 | return flask.make_response("success", 200) 197 | 198 | @octoprint.plugin.BlueprintPlugin.route("/resumequeue", methods=["GET"]) 199 | @restricted_access 200 | def resume_queue(self): 201 | self.paused = False 202 | self.start_next_print() 203 | return flask.make_response("success", 200) 204 | 205 | ##~~ TemplatePlugin 206 | def get_template_vars(self): 207 | return dict( 208 | cp_enabled=self.enabled, 209 | cp_bed_clearing_script=self._settings.get(["cp_bed_clearing_script"]), 210 | cp_queue_finished=self._settings.get(["cp_queue_finished"]), 211 | cp_paused=self.paused 212 | ) 213 | def get_template_configs(self): 214 | return [ 215 | dict(type="settings", custom_bindings=False, template="continuousprint_settings.jinja2"), 216 | dict(type="tab", custom_bindings=False, template="continuousprint_tab.jinja2") 217 | ] 218 | 219 | ##~~ AssetPlugin 220 | def get_assets(self): 221 | return dict( 222 | js=["js/continuousprint.js"] 223 | ) 224 | 225 | 226 | ##~~ Softwareupdate hook 227 | def get_update_information(self): 228 | # Define the configuration for your plugin to use with the Software Update 229 | # Plugin here. See https://docs.octoprint.org/en/master/bundledplugins/softwareupdate.html 230 | # for details. 231 | return dict( 232 | continuousprint=dict( 233 | displayName="Continuous Print Plugin", 234 | displayVersion=self._plugin_version, 235 | 236 | # version check: github repository 237 | type="github_release", 238 | user="nukeem", 239 | repo="continuousprint", 240 | current=self._plugin_version, 241 | 242 | # update method: pip 243 | pip="https://github.com/nukeem/continuousprint/archive/{target_version}.zip" 244 | ) 245 | ) 246 | 247 | 248 | __plugin_name__ = "Continuous Print" 249 | __plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 250 | 251 | def __plugin_load__(): 252 | global __plugin_implementation__ 253 | __plugin_implementation__ = ContinuousprintPlugin() 254 | 255 | global __plugin_hooks__ 256 | __plugin_hooks__ = { 257 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information 258 | } 259 | 260 | -------------------------------------------------------------------------------- /continuousprint/static/js/continuousprint.js: -------------------------------------------------------------------------------- 1 | /* 2 | * View model for OctoPrint-Print-Queue 3 | * 4 | * Author: Michael New 5 | * License: AGPLv3 6 | */ 7 | 8 | $(function() { 9 | function ContinuousPrintViewModel(parameters) { 10 | var self = this; 11 | self.params = parameters; 12 | 13 | self.printerState = parameters[0]; 14 | self.loginState = parameters[1]; 15 | self.files = parameters[2]; 16 | self.settings = parameters[3]; 17 | self.is_paused = ko.observable(); 18 | self.onBeforeBinding = function() { 19 | self.loadQueue(); 20 | self.is_paused(false); 21 | } 22 | 23 | 24 | self.loadQueue = function() { 25 | $('#queue_list').html(""); 26 | $.ajax({ 27 | url: "plugin/continuousprint/queue", 28 | type: "GET", 29 | dataType: "json", 30 | headers: { 31 | "X-Api-Key":UI_API_KEY, 32 | }, 33 | success:function(r){ 34 | if (r.queue.length > 0) { 35 | for(var i = 0; i < r.queue.length; i++) { 36 | var file = r.queue[i]; 37 | var row; 38 | if (file["time"] == undefined) { 39 | var other = " "; 40 | if (i == 0) other = ""; 41 | if (i == 1) other = " "; 42 | row = $("