/ octoprint_flashforge / __init__.py
__init__.py
1 # coding=utf-8 2 from __future__ import absolute_import 3 4 import usb1 5 import octoprint.plugin 6 from octoprint.settings import settings, default_settings 7 from octoprint.util import dict_merge 8 from . import flashforge 9 10 11 class FlashForgePlugin(octoprint.plugin.SettingsPlugin, 12 octoprint.plugin.AssetPlugin, 13 octoprint.plugin.TemplatePlugin): 14 15 16 VENDOR_IDS = {0x0315: "PowerSpec", 0x2a89: "Dremel", 0x2b71: "FlashForge"} 17 PRINTER_IDS = { 18 "PowerSpec": {0x0001: "Ultra 3DPrinter (C)"}, 19 "Dremel": {0x8889: "Dremel IdeaBuilder 3D20"}, 20 "FlashForge": {0x0001: "Dreamer", 0x000A: "Dreamer NX", 0x0002: "Finder v1", 0x0005: "Inventor", 0x0007: "Finder v2", 21 0x0009: "Guider IIs", 22 0x00e7: "Creator Max", 0x00ee: "Finder v2.12", 23 0x00f6: "PowerSpec Ultra 3DPrinter (B)", 0x00ff: "PowerSpec Ultra 3DPrinter (A)"}} 24 FILE_PACKET_SIZE = 1024 25 26 27 def __init__(self): 28 import logging 29 global default_settings 30 31 self._logger = logging.getLogger("octoprint.plugins.flashforge") 32 self._logger.debug("__init__") 33 self._comm = None 34 self._serial_obj = None 35 self._currentFile = None 36 self._upload_percent = 0 37 self._vendor_id = 0 38 self._vendor_name = "" 39 self._device_id = 0 40 # FlashForge friendly default connection settings 41 self._conn_settings = { 42 'neverSendChecksum': True, 43 'sdAlwaysAvailable': True, 44 'timeout': { 45 'temperature': 2, 46 'temperatureAutoreport': 0, 47 'sdStatusAutoreport': 0 48 }, 49 'helloCommand': "M601 S0", 50 'abortHeatupOnCancel': False 51 } 52 self._feature_settings = { 53 'autoUppercaseBlacklist': ['M146'] # LED control requires lowercase r,g,b 54 } 55 default_settings["serial"] = dict_merge(default_settings["serial"], self._conn_settings) 56 default_settings["feature"] = dict_merge(default_settings["feature"], self._feature_settings) 57 58 59 ##~~ SettingsPlugin mixin 60 def get_settings_defaults(self): 61 # put your plugin's default settings here 62 return dict( 63 ledStatus=1, 64 ledColor=[255, 255, 255] 65 ) 66 67 68 ##~~ AssetPlugin mixin 69 def get_assets(self): 70 # Define your plugin's asset files to automatically include in the 71 # core UI here. 72 return dict( 73 js=["js/flashforge.js", "js/color-picker.min.js"], 74 css=["css/color-picker.min.css"] 75 ) 76 77 78 ##~~ Softwareupdate hook 79 def get_update_information(self): 80 # Define the configuration for your plugin to use with the Software Update 81 # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update 82 # for details. 83 return dict( 84 flashforge=dict( 85 displayName="FlashForge Plugin", 86 displayVersion=self._plugin_version, 87 88 # version check: github repository 89 type="github_release", 90 user="Mrnt", 91 repo="OctoPrint-FlashForge", 92 current=self._plugin_version, 93 94 # update method: pip 95 pip="https://github.com/Mrnt/OctoPrint-FlashForge/archive/{target_version}.zip" 96 ) 97 ) 98 99 100 # Look for a supported printer 101 def detect_printer(self): 102 self._device_id = 0 103 with usb1.USBContext() as usbcontext: 104 for device in usbcontext.getDeviceIterator(skip_on_error=True): 105 vendor_id = device.getVendorID() 106 device_id = device.getProductID() 107 try: 108 device_name = device.getProduct() 109 except: 110 device_name = 'unknown' 111 self._logger.debug("Found device '{}' with Vendor ID: {:#06X}, USB ID: {:#06X}".format(device_name, vendor_id, device_id)) 112 # get USB interface details to diagnose connectivity issues 113 for configuration in device.iterConfigurations(): 114 for interface in configuration: 115 for setting in interface: 116 self._logger.debug( 117 " setting number: 0x{:02x}, class: 0x{:02x}, subclass: 0x{:02x}, protocol: 0x{:02x}, #endpoints: {}, descriptor: {}".format( 118 setting.getNumber(), setting.getClass(), setting.getSubClass(), 119 setting.getProtocol(), setting.getNumEndpoints(), setting.getDescriptor())) 120 for endpoint in setting: 121 self._logger.debug( 122 " endpoint address: 0x{:02x}, attributes: 0x{:02x}, max packet size: {}".format( 123 endpoint.getAddress(), endpoint.getAttributes(), 124 endpoint.getMaxPacketSize())) 125 126 if vendor_id in self.VENDOR_IDS: 127 vendor_name = self.VENDOR_IDS[vendor_id] 128 if device_id in self.PRINTER_IDS[vendor_name]: 129 self._logger.info("Found a {} {}".format(vendor_name, self.PRINTER_IDS[vendor_name][device_id])) 130 self._vendor_id = vendor_id 131 self._vendor_name = vendor_name 132 self._device_id = device_id 133 break 134 else: 135 raise flashforge.FlashForgeError("Found an unsupported {} printer '{}' with USB ID: {:#06X}".format(vendor_name, device_name, device_id)) 136 137 return self._device_id != 0 138 139 140 def printer_factory(self, comm, port, baudrate, read_timeout, *args, **kwargs): 141 """ OctoPrint hook - Called when creating printer connection 142 143 Test for presence of a supported printer and then try to connect 144 """ 145 if not port == "AUTO": 146 return None 147 148 if not self.detect_printer(): 149 raise flashforge.FlashForgeError("No FlashForge printer detected - please ensure it is connected and turned on.") 150 151 self._comm = comm 152 serial_obj = flashforge.FlashForge(self, comm, self._vendor_id, self._device_id, read_timeout=float(read_timeout)) 153 return serial_obj 154 155 156 def get_extension_tree(self, *args, **kwargs): 157 """ OctoPrint hook - Return supported file extensions for SD upload 158 159 Note not called when printer connects, only when starting up and when the printer disconnects 160 """ 161 self._logger.debug("get_extension_tree()") 162 return dict( 163 machinecode=dict( 164 g3drem=["g3drem"], # Dremel 165 gx=["gx"] # Every other FlashForge based printer 166 ) 167 ) 168 169 170 def on_connect(self, serial_obj): 171 self._logger.debug("on_connect()") 172 self._serial_obj = serial_obj 173 174 175 def on_disconnect(self): 176 self._logger.debug("on_disconnect()") 177 self._serial_obj = None 178 179 180 def valid_command(self, command): 181 gcode = command.split(b' ', 1)[0] 182 return (gcode[0] in b"GM") and gcode not in [b"M117"] 183 184 185 # Called when gcode commands are being placed in the queue by OctoPrint: 186 # Mostly important for control panel or translating and printing non FlashPrint file directly from OctoPrint 187 def rewrite_gcode(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): 188 if self._serial_obj: 189 190 self._logger.debug("rewrite_gcode(): gcode:{}, cmd:{}".format(gcode, cmd)) 191 192 #TODO: filter M146 and other commands? when printing from SD because they cause comms to hang 193 194 # M20 list SD card, M21 init SD card - do not do if we are busy, seems to cause issues 195 if (gcode == "M20" or gcode == "M21") and not self._serial_obj.is_ready(): 196 cmd = [] 197 198 # M25 = pause 199 elif gcode == "M25": 200 # pause during cancel causes issues 201 if comm_instance.isCancelling(): 202 cmd = [] 203 204 # M26 is sent by OctoPrint during SD prints: 205 # M26 in Marlin = set SD card position : Flashforge = cancel 206 elif gcode == "M26": 207 # M26 S0 generated during OctoPrint cancel - use it to send cancel 208 if cmd == "M26 S0" and comm_instance.isCancelling(): 209 cmd = [("M26", cmd_type), ("M119", "status_polling")] 210 else: 211 cmd = [] 212 213 # also get printer status when getting SD progress 214 elif gcode == "M27": 215 cmd = [("M119", "status_polling"), (cmd, cmd_type)] 216 217 # also get printer status when getting temp status 218 elif gcode == "M105": 219 cmd = [("M119", "status_polling"), (cmd, cmd_type)] 220 221 # M106 S0 is sent by OctoPrint control panel: 222 # M106 S0 in Marlin = fan off : FlashForge uses M107 for fan off 223 elif gcode == "M106": 224 if "S0" in cmd: 225 cmd = ["M107"] 226 227 # M110 is sent by OctoPrint as default hello but also when connected: 228 # M110 Set line number/hello in Marlin : FlashForge uses M601 S0 to take control via USB 229 elif gcode == "M110": 230 cmd = [] 231 232 # also get printer status when connecting 233 elif gcode == "M115": 234 cmd = [("M119", "status_polling"), ("M27", "sd_status_polling"), (cmd, cmd_type)] 235 236 # M400 is sent by OctoPrint on cancel: 237 # M400 in Marlin = wait for moves to finish : Flashforge = ? - instead send something inert so on_M400_sent is triggered in OctoPrint 238 elif gcode == "M400": 239 cmd = [("M119", "status_polling")] 240 241 return cmd 242 243 244 # Uploading files directly to internal SD card 245 def upload_to_sd(self, printer, filename, path, sd_upload_started, sd_upload_succeeded, sd_upload_failed, *args, 246 **kwargs): 247 248 if not self._serial_obj: 249 return 250 251 def process_upload(): 252 error = "" 253 254 # rewrite: 255 self._upload_percent = 0 256 chunk_start_index = 0 257 258 self._serial_obj.makeexclusive(True) 259 error = "could not start tx" 260 261 # make sure heaters are off 262 self._serial_obj.sendcommand(b"M104 S0 T0") 263 self._serial_obj.sendcommand(b"M104 S0 T1") 264 self._serial_obj.sendcommand(b"M140 S0") 265 266 ok, answer = self._serial_obj.sendcommand(b"M28 %d 0:/user/%s" % (file_size, remote_name.encode()), 5000) 267 if not ok: 268 error = "file transfer not started {}".format(answer) 269 else: 270 self._logger.debug("M28 file tx started") 271 error = "" 272 273 try: 274 while chunk_start_index < file_size: 275 chunk_end_index = min(chunk_start_index + self.FILE_PACKET_SIZE, file_size) 276 chunk = bgcode[chunk_start_index:chunk_end_index] 277 if not chunk: 278 error = "unexpected eof" 279 break 280 281 if self._serial_obj.writeraw(chunk, False): 282 upload_percent = 100.0 * chunk_end_index / file_size 283 self.upload_percent = int(upload_percent) 284 self._logger.debug("Sent: %.2f%% %d/%d" % (self.upload_percent, chunk_end_index, file_size)) 285 else: 286 error = "File transfer interrupted" 287 break 288 289 chunk_start_index += self.FILE_PACKET_SIZE 290 291 if not error: 292 result, response = self._serial_obj.sendcommand(b"M29", 10000) 293 if result and b"CMD M28" in response: 294 response = self._serial_obj.readraw(1000) 295 if result and b"failed" not in response: 296 sd_upload_succeeded(filename, remote_name, 10) 297 else: 298 error = "File transfer incomplete" 299 300 except flashforge.FlashForgeError as error: 301 error = "File transfer incomplete" 302 pass 303 304 if error: 305 self._logger.debug("Upload failed: {}".format(error)) 306 sd_upload_failed(filename, remote_name, 10) 307 self._serial_obj.makeexclusive(False) 308 raise flashforge.FlashForgeError(error) 309 310 self._serial_obj.makeexclusive(False) 311 # NB M23 select will also trigger a print on FlashForge 312 self._comm.selectFile("0:/user/%s\r\n" % remote_name, True) 313 # TODO: need to set the correct file size for the progress indicator 314 315 316 import threading 317 from octoprint import util as util 318 319 bgcode = b"" 320 file_size = 0 321 """ 322 unfortunately we cannot get the list of files on the SD card from FlashForge so we just name the remote file 323 the same as the source and hope for the best 324 """ 325 remote_name = filename 326 327 file = open(path, "rb") 328 bgcode = file.read() 329 file_size = len(bgcode) 330 file.close() 331 332 self._logger.info("Starting SDCard upload from {} to {}".format(filename, remote_name)) 333 sd_upload_started(filename, remote_name) 334 335 thread = threading.Thread(target=process_upload, name="SD Uploader") 336 thread.daemon = True 337 thread.start() 338 339 return remote_name 340 341 342 343 # If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py 344 # ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that 345 # can be overwritten via __plugin_xyz__ control properties. See the documentation for that. 346 __plugin_name__ = "FlashForge Plugin" 347 __plugin_pythoncompat__ = ">=2.7,<4" 348 349 350 def __plugin_load__(): 351 global __plugin_implementation__ 352 __plugin_implementation__ = FlashForgePlugin() 353 354 global __plugin_hooks__ 355 __plugin_hooks__ = { 356 "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, 357 "octoprint.comm.transport.serial.factory": __plugin_implementation__.printer_factory, 358 "octoprint.filemanager.extension_tree": __plugin_implementation__.get_extension_tree, 359 "octoprint.comm.protocol.gcode.queuing": __plugin_implementation__.rewrite_gcode, 360 "octoprint.printer.sdcardupload": __plugin_implementation__.upload_to_sd 361 } 362