/ 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