├── MANIFEST.in ├── .gitignore ├── screenshot_1.png ├── README.md ├── octoprint_multi_colors ├── templates │ └── multi_colors_tab.jinja2 ├── static │ └── js │ │ └── multi_colors.js └── __init__.py └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | *.py[cod] 4 | *.bak 5 | -------------------------------------------------------------------------------- /screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonshineSG/OctoPrint-MultiColors/HEAD/screenshot_1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctoPrint-MultiColors 2 | 3 | Octoprint plugin to inject GCODE for filament change at selected layer or at the specified heigth. It depends what slicer do you use. 4 | 5 | Note: this plugin does NOT work with files on the SD card. 6 | 7 | ![screenshot](screenshot_1.png) 8 | 9 | 10 | ### Setup 11 | 12 | Install via the bundled Plugin Manager or manually using this URL: 13 | 14 | https://github.com/MoonshineSG/Octoprint-MultiColors/archive/master.zip 15 | 16 | ### Donate 17 | 18 | Accepting [beer tips](https://paypal.me/ovidiuhossu)... 19 | -------------------------------------------------------------------------------- /octoprint_multi_colors/templates/multi_colors_tab.jinja2: -------------------------------------------------------------------------------- 1 |
2 |

Fillament change for multi color printing

3 | 4 |
5 |
6 |
7 |

8 | 9 |
10 | 11 |
12 |
13 | Layer number or height where the GCODE will be injected (space or comma separated). Code inserted usually at the begining of the new layer. 14 |
15 | 16 |
17 | 18 |
Apply changes to a copy of the gcode file. The new file will have "_multi" added at the end of it's name

19 | 20 |
Save GCODE (and regex) for future use and execute code injection
21 |
22 |
23 | 24 |
Advanced Options
25 |
26 | 27 |
28 |
29 | {layer} is a place holder for layers iteration. 30 |
31 |

32 | 33 |
34 | 35 | GCODE to be injected after the line matched by the regular expression (enter one GCODE per line)
36 | The final code will look something like this

37 | 38 | [....]
39 | matched_line
40 | INJECTED_GCODE_LINE_1
41 | INJECTED_GCODE_LINE_2
42 | INJECTED_GCODE_LINE_3
43 | [...] 44 |
45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /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 = "multi_colors" 8 | 9 | # The plugin's python package, should be "octoprint_", has to be unique 10 | plugin_package = "octoprint_multi_colors" 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 = "OctoPrint MultiColors" 15 | 16 | # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module 17 | plugin_version = "1.0.17" 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 = """Inject GCODE at specified layers to allow multi color printing""" 22 | 23 | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module 24 | plugin_author = "ovidiu" 25 | 26 | # The plugin's author's mail address. 27 | plugin_author_email = "github@ovidiu.me" 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/MoonshineSG/OctoPrint-MultiColors" 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. 44 | plugin_additional_data = [] 45 | 46 | # Any additional python packages you need to install with your plugin that are not contains in .* 47 | plugin_addtional_packages = [] 48 | 49 | # Any python packages within .* you do NOT want to install with your plugin 50 | plugin_ignored_packages = [] 51 | 52 | # Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, 53 | # define dependency links or other things like that, this is the place to go. Will be merged recursively with the 54 | # default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using 55 | # octoprint.util.dict_merge. 56 | # 57 | # Example: 58 | # plugin_requires = ["someDependency==dev"] 59 | # additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} 60 | additional_setup_parameters = {} 61 | 62 | ######################################################################################################################## 63 | 64 | from setuptools import setup 65 | 66 | try: 67 | import octoprint_setuptools 68 | except: 69 | print("Could not import OctoPrint's setuptools, are you sure you are running that under " 70 | "the same python installation that OctoPrint is installed under?") 71 | import sys 72 | sys.exit(-1) 73 | 74 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 75 | identifier=plugin_identifier, 76 | package=plugin_package, 77 | name=plugin_name, 78 | version=plugin_version, 79 | description=plugin_description, 80 | author=plugin_author, 81 | mail=plugin_author_email, 82 | url=plugin_url, 83 | license=plugin_license, 84 | requires=plugin_requires, 85 | additional_packages=plugin_addtional_packages, 86 | ignored_packages=plugin_ignored_packages, 87 | additional_data=plugin_additional_data 88 | ) 89 | 90 | if len(additional_setup_parameters): 91 | from octoprint.util import dict_merge 92 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 93 | 94 | setup(**setup_parameters) 95 | -------------------------------------------------------------------------------- /octoprint_multi_colors/static/js/multi_colors.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | function multiColorViewModel(viewModels) { 4 | var self = this; 5 | 6 | self.loginState = viewModels[0]; 7 | self.printer = viewModels[1]; 8 | self.filesViewModel = viewModels[2]; 9 | 10 | self.gcode = ko.observable(); 11 | self.layers = ko.observable(); 12 | self.message = ko.observable(); 13 | self.enabled = ko.observable(); 14 | self.find_string = ko.observable(); 15 | self.duplicate = ko.observable(); 16 | self.can_duplicate = ko.observable(); 17 | 18 | self.NO_FILE = "First, select a GCODE file for printing..."; 19 | self.NO_SD = "Injecting GCODE on SD card files not yet supported"; 20 | 21 | self.onBeforeBinding = function () { 22 | self.isAdmin = viewModels[0].isAdmin; 23 | self.enabled(false); 24 | self.message(self.NO_FILE); 25 | } 26 | 27 | self._update = function(file_name){ 28 | if (self.printer.isPrinting() || self.printer.isPaused() ) { 29 | self.message("Don't use this while printing..."); 30 | self.enabled(false); 31 | } else { 32 | if (file_name != null) { 33 | self.filename = file_name; 34 | self.message( _.sprintf('Processing file "%(filename)s"...', {filename: self.filename}) ); 35 | self.enabled(true); 36 | 37 | self.duplicate(JSON.parse(localStorage.getItem("multicolors.duplicate"))); 38 | if ( file_name.substring(0, file_name.lastIndexOf('.')).endsWith("_multi") ) { 39 | self.can_duplicate(false); 40 | self.duplicate(false); 41 | self.message( _.sprintf('File "%(filename)s" already processed!! ', {filename: self.filename}) ); 42 | } else { 43 | self.can_duplicate(true); 44 | } 45 | } else { 46 | self.message(self.NO_FILE); 47 | self.enabled(false); 48 | } 49 | } 50 | } 51 | 52 | self.onTabChange = function(current, previous) { 53 | if (current == "#tab_plugin_multi_colors") { 54 | if ( self.printer.sd() ) { 55 | self.message(self.NO_SD); 56 | } else { 57 | self._update(self.printer.filepath()); 58 | } 59 | } 60 | } 61 | 62 | self.onEventFileDeselected = function(payload) { 63 | self._update(null); 64 | } 65 | 66 | self.onAfterBinding = function(payload) { 67 | self.onTabChange("#tab_plugin_multi_colors", null); 68 | self._sendData({"command":"settings"}, function(data){ self.gcode(data.gcode); self.find_string(data.find_string)}); 69 | } 70 | 71 | self.onEventFileSelected = function(payload) { 72 | if (payload.origin == "local") { 73 | self._update(payload.path); 74 | } else { 75 | self.message(self.NO_SD); 76 | } 77 | } 78 | 79 | self._sendData = function(data, callback) { 80 | try { 81 | OctoPrint.postJson("api/plugin/multi_colors", data) 82 | .done(function(data) { 83 | if (callback) callback(data); 84 | }); 85 | } catch(err) { //fallback to pre-devel version 86 | $.ajax({ 87 | url: API_BASEURL + "plugin/multi_colors", 88 | type: "POST", 89 | dataType: "json", 90 | timeout: 10000, 91 | contentType: "application/json; charset=UTF-8", 92 | data: JSON.stringify(data) 93 | }).done(function(data){if (typeof callback === "function") callback(data);}); 94 | } 95 | }; 96 | 97 | self.changeLayers = function(){ 98 | if (self.layers() == undefined || self.layers().trim() == "") { 99 | showMessageDialog({ title: "Layers please!", message: "Please enter at least on layer where the GCODe should be injected." }); 100 | return; 101 | } 102 | if (self.gcode() == undefined || self.gcode().trim() == "") { 103 | showMessageDialog({ title: "GCODE please!", message: "Please enter the GCODE to inject." }); 104 | return; 105 | } 106 | if (self.find_string() == undefined || self.find_string().trim() == "") { 107 | showMessageDialog({ title: "Regex please!", message: "Please enter a valid regex (advanced settings)." }); 108 | return; 109 | } 110 | if (self.can_duplicate()){ 111 | localStorage.setItem("multicolors.duplicate", self.duplicate()); 112 | } 113 | if ( ! self.duplicate() ) { 114 | showConfirmationDialog({ 115 | message: gettext("This will insert additional gcode in your original file."), 116 | cancel: gettext("No"), 117 | proceed: gettext("Yes"), 118 | onproceed: function() { 119 | self.proceed(); 120 | } 121 | }); 122 | } else { 123 | self.proceed(); 124 | } 125 | } 126 | 127 | self.proceed = function(){ 128 | self._sendData({"command":"process", "duplicate":self.duplicate(), "file":self.filename, "gcode":self.gcode(), "layers":self.layers(), "find_string":self.find_string().trim() }, 129 | function(data){ 130 | new PNotify({title:"Colors", text:data.message, type: data.status}); 131 | self.filesViewModel.requestData({force:true}); 132 | if (data.status != "error" && !self.duplicate()) { 133 | self.filesViewModel.loadFile({origin:"local", path:self.filename}); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | OCTOPRINT_VIEWMODELS.push([ 140 | multiColorViewModel, 141 | ["loginStateViewModel", "printerStateViewModel", "filesViewModel", ], 142 | ["#multi_color_layer"] 143 | ]); 144 | 145 | }); -------------------------------------------------------------------------------- /octoprint_multi_colors/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | import octoprint.plugin 5 | import octoprint.events 6 | from octoprint.events import Events 7 | from octoprint.server import printer 8 | from octoprint.filemanager import FileDestinations 9 | 10 | import logging 11 | 12 | from flask import jsonify 13 | import os.path 14 | from os import linesep 15 | import datetime 16 | import mmap 17 | import re 18 | import contextlib 19 | from shutil import copyfile 20 | import os 21 | import io 22 | 23 | class MultiColorsPlugin(octoprint.plugin.AssetPlugin, 24 | octoprint.plugin.SimpleApiPlugin, 25 | octoprint.plugin.TemplatePlugin, 26 | octoprint.plugin.SettingsPlugin): 27 | 28 | def initialize(self): 29 | #self._logger.setLevel(logging.DEBUG) 30 | self.gcode_file = os.path.join(self.get_plugin_data_folder(),"gcode.txt") 31 | self.regex_file = os.path.join(self.get_plugin_data_folder(),"regex.txt") 32 | self._logger.info("MultiColors init") 33 | 34 | def get_template_configs(self): 35 | return [ 36 | dict(type="tab", template="multi_colors_tab.jinja2", custom_bindings=True) 37 | ] 38 | 39 | def get_assets(self): 40 | return dict( 41 | js=["js/multi_colors.js"] 42 | ) 43 | 44 | def is_api_adminonly(self): 45 | return True 46 | 47 | def get_api_commands(self): 48 | return dict( 49 | settings=[], 50 | process=["file", "gcode", "layers", "find_string"], 51 | ) 52 | 53 | def rename(self, fname): 54 | name, ext = os.path.splitext(fname) 55 | if name.endswith("_multi"): 56 | return fname 57 | else: 58 | return "%s_multi%s"%(name, ext) 59 | 60 | def on_api_command(self, command, data): 61 | self._logger.info("on_api_command called: '{command}' / '{data}'".format(**locals())) 62 | if command == "settings": 63 | return jsonify(dict(gcode = self.load_gcode(), find_string = self.load_regex())) 64 | elif command == "process": 65 | self.save_gcode(data.get('gcode')) 66 | self.save_regex(data.get('find_string')) 67 | 68 | #selected file 69 | gcode_file = os.path.join(self._settings.global_get_basefolder('uploads'), data.get('file') ) 70 | 71 | #create temp file 72 | work_copy = "%s.tmp"%gcode_file 73 | copyfile(gcode_file, work_copy) 74 | 75 | #code injection 76 | ret, message = self.inject_gcode(work_copy, data.get('layers').replace(",", " ").split(), data.get('find_string'), data.get('gcode')) 77 | 78 | 79 | if ret != "error": 80 | if data.get('duplicate'): 81 | gcode_file = os.path.join(self._settings.global_get_basefolder('uploads'), self.rename(data.get('file')) ) 82 | 83 | #copy temp file 84 | copyfile(work_copy, gcode_file) 85 | 86 | #start file analysis 87 | self._file_manager.analyse(FileDestinations.LOCAL, gcode_file) 88 | 89 | #load 90 | self._printer.select_file(gcode_file, False) 91 | 92 | #cleanup 93 | os.remove( work_copy ) 94 | 95 | return jsonify(dict(status=ret, message=message)) 96 | 97 | def inject_gcode(self, file, layers, find_string, gcode): 98 | try: 99 | self._logger.info("File to modify '%s'"%file) 100 | 101 | marker = "; multi color" 102 | line_found = False 103 | with io.open(file, "r", encoding="utf-8") as f: 104 | line_found = any(marker in line for line in f) 105 | 106 | found = 0 107 | 108 | replace = r'\1{2}{0}{2}{1}{2}'.format(marker, gcode, linesep).encode('utf-8') 109 | for layer in layers: 110 | with io.open(file, 'r+', encoding="utf-8") as f: 111 | self._logger.info("Trying to insert multi color code for layer '%s'..."%layer) 112 | search = re.compile(r'({0}(\r\n?|\n))'.format( find_string.format(layer = layer.strip())).encode('utf-8') , re.MULTILINE) 113 | self._logger.debug(search.pattern) 114 | with contextlib.closing(mmap.mmap(f.fileno(), 0)) as m: 115 | test = re.search(search, m) 116 | if test: 117 | found += 1 118 | result = re.sub(search, replace, m) 119 | f.truncate() 120 | f.write(result.decode("utf-8")) 121 | f.flush() 122 | else: 123 | self._logger.info("Failed to insert code for layer %s"%layer) 124 | 125 | needed = len(layers) 126 | if needed == found: 127 | if line_found: 128 | return "info", "ATTENTION!!! This file has been processed before!!!. You might get double pause. %s GCODE injected successfuly."%found 129 | else: 130 | return "success", "%s GCODE injected successfuly."%found 131 | else: 132 | return "error", "Injecting GCODE failed. Replaced %s out of %s needed."%(found, needed) 133 | except Exception as e: 134 | self._logger.error(e) 135 | return "error", "Injecting GCODE failed [%s]"%e 136 | 137 | def load_regex(self): 138 | data = self._load_data(self.regex_file) 139 | if data == "__default__": 140 | return ";layer {layer}.*?" 141 | return data 142 | 143 | def save_regex(self, data): 144 | self._save_data(self.regex_file, data) 145 | 146 | def load_gcode(self): 147 | data = self._load_data(self.gcode_file) 148 | if data == "__default__": 149 | return """M117 Change filament 150 | M0""" 151 | return data 152 | 153 | def save_gcode(self, data): 154 | self._save_data(self.gcode_file, data) 155 | 156 | def _load_data(self, data_file): 157 | data = "__default__" 158 | if os.path.isfile(data_file): 159 | with open(data_file, 'r') as f: 160 | data = f.read() 161 | return data 162 | 163 | def _save_data(self, data_file, data): 164 | with open(data_file, 'w') as f: 165 | f.write(data) 166 | 167 | def get_version(self): 168 | return self._plugin_version 169 | 170 | def get_update_information(self): 171 | return dict( 172 | multi_colors=dict( 173 | displayName="Multi Colors", 174 | displayVersion=self._plugin_version, 175 | 176 | # version check: github repository 177 | type="github_release", 178 | user="MoonshineSG", 179 | repo="OctoPrint-MultiColors", 180 | current=self._plugin_version, 181 | 182 | # update method: pip 183 | pip="https://github.com/MoonshineSG/OctoPrint-MultiColors/archive/{target_version}.zip" 184 | ) 185 | ) 186 | 187 | __plugin_name__ = "Multi Colors" 188 | __plugin_description__ = "Inject GCODE at specified layers to allow multi color printing." 189 | __plugin_pythoncompat__ = ">=2.7,<4" 190 | 191 | def __plugin_load__(): 192 | global __plugin_implementation__ 193 | __plugin_implementation__ = MultiColorsPlugin() 194 | 195 | global __plugin_hooks__ 196 | __plugin_hooks__ = { 197 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information 198 | } 199 | --------------------------------------------------------------------------------