/ octoprint_flashforge / flashforge.py
flashforge.py
  1  import usb1
  2  import threading
  3  
  4  try:
  5  	import queue
  6  	import re
  7  except ImportError:
  8  	import Queue as queue
  9  
 10  
 11  regex_SDPrintProgress = re.compile("(?P<current>[0-9]+)/(?P<total>[0-9]+)")
 12  """
 13  Regex matching SD print progress from M27.
 14  """
 15  
 16  
 17  class FlashForgeError(Exception):
 18  	def __init__(self, message, error=0):
 19  		super(FlashForgeError, self).__init__(("{} ({})" if error else "{}").format(message, error))
 20  		self.error = error
 21  
 22  
 23  class FlashForge(object):
 24  	BUFFER_SIZE = 512
 25  
 26  	STATE_UNKNOWN = 0
 27  	STATE_READY = 1
 28  	STATE_BUILDING = 2
 29  	STATE_SD_BUILDING = 3
 30  	STATE_SD_PAUSED = 4
 31  	STATE_HOMING = 5
 32  	STATE_BUSY = 6
 33  
 34  	PRINTING_STATES = (STATE_BUILDING, STATE_SD_BUILDING, STATE_HOMING)
 35  
 36  
 37  	def __init__(self, plugin, comm, vendor_id, device_id, seriallog_handler=None, read_timeout=10.0, write_timeout=10.0):
 38  		import logging
 39  		self._logger = logging.getLogger("octoprint.plugins.flashforge")
 40  		self._logger.debug("FlashForge.__init__()")
 41  
 42  		self._plugin = plugin
 43  		self._comm = comm
 44  		self._read_timeout = read_timeout
 45  		self._write_timeout = write_timeout
 46  		self._incoming = queue.Queue()
 47  		self._readlock = threading.Lock()
 48  		self._writelock = threading.Lock()
 49  		self._printerstate = self.STATE_UNKNOWN
 50  
 51  		self._context = usb1.USBContext()
 52  		self._usb_cmd_endpoint_in = 0
 53  		self._usb_cmd_endpoint_out = 0
 54  		self._usb_sd_endpoint_in = 0
 55  		self._usb_sd_endpoint_out = 0
 56  
 57  		try:
 58  			self._handle = self._context.openByVendorIDAndProductID(vendor_id, device_id)
 59  		except usb1.USBError as usberror:
 60  			if usberror.value == -3:
 61  				raise FlashForgeError(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n\r\n"
 62  									  "Unable to connect to FlashForge printer - permission error.\r\n\r\n"
 63  									  "If you are using OctoPi/Linux add permission to access this device by editing file:\r\n /etc/udev/rules.d/99-octoprint.rules\r\n\r\n"
 64  									  "and adding the line:\r\n"
 65  									  "SUBSYSTEM==\"usb\", ATTR{{idVendor}}==\"{:04x}\", MODE=\"0666\"\r\n\r\n"
 66  									  "You can do this as follows:\r\n"
 67  									  "1) Connect to your OctoPi/Octoprint device using ssh\r\n"
 68  									  "2) Type the following to open a text editor:\r\n"
 69  									  "sudo nano /etc/udev/rules.d/99-octoprint.rules\r\n"
 70  									  "3) Add the following line:\r\n"
 71    									  "SUBSYSTEM==\"usb\", ATTR{{idVendor}}==\"{:04x}\", MODE=\"0666\"\r\n"
 72  									  "4) Save the file and close the editor\r\n"
 73  									  "5) Verify the file permissions are set to \"rw-r--r--\" by typing:\r\n"
 74  									  "ls /etc/udev/rules.d/99-octoprint.rules\r\n"
 75  									  "6) Reboot your system for the rule to take effect.\r\n\r\n"
 76  									  "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\r\n\r\n".format(vendor_id, vendor_id))
 77  			else:
 78  				raise FlashForgeError('Unable to connect to FlashForge printer - may already be in use', usberror)
 79  		else:
 80  			if self._handle:
 81  				try:
 82  					self._handle.claimInterface(0)
 83  					self._logger.debug("claimed USB interface")
 84  					device = self._handle.getDevice()
 85  					# look for an in and out endpoint pair:
 86  					for configuration in device.iterConfigurations():
 87  						for interface in configuration:
 88  							for setting in interface:
 89  								self._logger.debug(" setting number: 0x{:02x}, class: 0x{:02x}, subclass: 0x{:02x}, protocol: 0x{:02x}, #endpoints: {}".format(
 90  									setting.getNumber(), setting.getClass(), setting.getSubClass(), setting.getProtocol(), setting.getNumEndpoints()))
 91  								endpoint_in = 0
 92  								endpoint_out = 0
 93  								for endpoint in setting:
 94  									self._logger.debug("  found endpoint type {} at address 0x{:02x}, max packet size {}".
 95  										format(usb1.libusb1.libusb_transfer_type.get(endpoint.getAttributes()),
 96  										endpoint.getAddress(),
 97  										endpoint.getMaxPacketSize()))
 98  									if usb1.libusb1.libusb_transfer_type.get(endpoint.getAttributes()) == 'LIBUSB_TRANSFER_TYPE_BULK':
 99  										address = endpoint.getAddress()
100  										if address & usb1.libusb1.LIBUSB_ENDPOINT_IN:
101  											endpoint_in = address
102  										else:
103  											endpoint_out = address
104  										if endpoint_in and endpoint_out:
105  											# we have a pair of endpoints, assign them as needed
106  											# assume first pair is for commands, second for SD upload
107  											if not self._usb_cmd_endpoint_out:
108  												self._usb_cmd_endpoint_in = endpoint_in
109  												self._usb_cmd_endpoint_out = endpoint_out
110  												endpoint_in = endpoint_out = 0
111  											elif not self._usb_sd_endpoint_out:
112  												self._usb_sd_endpoint_in = endpoint_in
113  												self._usb_sd_endpoint_out = endpoint_out
114  												break
115  								else:
116  									continue
117  								break
118  							else:
119  								continue
120  							break
121  						else:
122  							continue
123  						break
124  					# if we don't have endpoints for SD upload then use the regular ones
125  					if not self._usb_sd_endpoint_out:
126  						self._usb_sd_endpoint_in = self._usb_cmd_endpoint_in
127  						self._usb_sd_endpoint_out = self._usb_cmd_endpoint_out
128  					self._logger.debug(
129  						"  cmd_endpoint_out 0x{:02x}, cmd_endpoint_in 0x{:02x}".
130  						format(self._usb_cmd_endpoint_out, self._usb_cmd_endpoint_in))
131  					self._logger.debug(
132  						"  sd_endpoint_out 0x{:02x}, sd_endpoint_in 0x{:02x}".
133  						format(self._usb_sd_endpoint_out, self._usb_sd_endpoint_in))
134  					if not (self._usb_cmd_endpoint_in and self._usb_cmd_endpoint_out):
135  						self.close()
136  						raise FlashForgeError('Unable to find USB endpoints - turn on debug output and check octoprint.log')
137  					self._plugin.on_connect(self)
138  
139  				except usb1.USBError as usberror:
140  					raise FlashForgeError('Unable to connect to FlashForge printer - may already be in use', usberror)
141  
142  			else:
143  				self._logger.debug("No FlashForge printer found")
144  				raise FlashForgeError('No FlashForge Printer found')
145  
146  
147  	@property
148  	def timeout(self):
149  		"""Return timeout for reads. OctoPrint Serial Factory property"""
150  
151  		self._logger.debug("FlashForge.timeout()")
152  		return self._read_timeout
153  
154  
155  	@timeout.setter
156  	def timeout(self, value):
157  		"""Set timeout for reads. OctoPrint Serial Factory property"""
158  
159  		self._logger.debug("Setting read timeout to {}s".format(value))
160  		self._read_timeout = value
161  
162  
163  	@property
164  	def write_timeout(self):
165  		"""Return timeout for writes. OctoPrint Serial Factory property"""
166  
167  		self._logger.debug("FlashForge.write_timeout()")
168  		return self._write_timeout
169  
170  
171  	@write_timeout.setter
172  	def write_timeout(self, value):
173  		"""Set timeout for writes. OctoPrint Serial Factory property"""
174  
175  		self._logger.debug("Setting write timeout to {}s".format(value))
176  		self._write_timeout = value
177  
178  
179  	def is_ready(self):
180  		"""Return true if the printer is idle"""
181  
182  		return self._printerstate == self.STATE_READY
183  
184  
185  	def is_printing(self):
186  		"""Return true if the printer is in any printing state"""
187  
188  		return self._printerstate in self.PRINTING_STATES
189  
190  
191  	def write(self, data):
192  		"""Write commands to printer. OctoPrint Serial Factory method
193  
194  		Formats the commands sent by OctoPrint to make them FlashForge friendly.
195  		"""
196  
197  		self._logger.debug("FlashForge.write() called by thread {}".format(threading.currentThread().getName()))
198  
199  		# save the length for return on success
200  		data_len = len(data)
201  		self._writelock.acquire()
202  
203  		# strip carriage return, etc so we can terminate lines the FlashForge way
204  		data = data.strip(b" \r\n")
205  		# try to filter out garbage commands (we need to replace with something harmless)
206  		# do this here instead of octoprint.comm.protocol.gcode.sending hook so DisplayLayerProgress plugin will work
207  		if len(data) and not self._plugin.valid_command(data):
208  			data = b"M119"
209  
210  		try:
211  			self._logger.debug("FlashForge.write() {0}".format(data.decode()))
212  			self._handle.bulkWrite(self._usb_cmd_endpoint_out, b"~%s\r\n" % data, int(self._write_timeout * 1000.0))
213  			self._writelock.release()
214  			return data_len
215  		except usb1.USBError as usberror:
216  			self._writelock.release()
217  			raise FlashForgeError('USB Error write()', usberror)
218  
219  
220  	def writeraw(self, data, command = True):
221  		"""Write raw data to printer.
222  
223  		data: bytearray to send
224  		command: True to send g-code, False to send to upload SD card
225  		"""
226  
227  		self._logger.debug("FlashForge.writeraw() called by thread {}".format(threading.currentThread().getName()))
228  
229  		try:
230  			self._handle.bulkWrite(self._usb_cmd_endpoint_out if command else self._usb_sd_endpoint_out, data)
231  			return len(data)
232  		except usb1.USBError as usberror:
233  			raise FlashForgeError('USB Error writeraw()', usberror)
234  
235  
236  	def readline(self):
237  		"""Read line worth of response from printer. OctoPrint Serial Factory method
238  
239  		Read response from the printer and store as a series of \r\n terminated lines
240  		OctoPrint reads response line by line..
241  
242  		Returns:
243  			List of lines returned from the printer
244  		"""
245  
246  		self._logger.debug("FlashForge.readline() called by thread {}".format(threading.currentThread().getName()))
247  
248  		self._readlock.acquire()
249  
250  		if not self._incoming.empty():
251  			self._readlock.release()
252  			return self._incoming.get_nowait()
253  
254  		data = self.readraw()
255  
256  		# translate returned data into something OctoPrint understands
257  		if len(data):
258  			if b"CMD M27 " in data:
259  				# need to filter out bogus SD print progress from cancelled or paused prints
260  				if b"printing byte" in data and self._printerstate in [self.STATE_READY, self.STATE_SD_PAUSED]:
261  					match = regex_SDPrintProgress.search(data.decode())
262  					if match:
263  						try:
264  							current = int(match.group("current"))
265  							total = int(match.group("total"))
266  						except:
267  							pass
268  						else:
269  							if self._printerstate == self.STATE_READY and current >= total:
270  								# Ultra 3D: after completing print it still indicates SD card progress
271  								data = b"CMD M27 Received.\r\nDone printing file\r\nok\r\n"
272  							elif self._printerstate == self.STATE_SD_PAUSED:
273  								# when paused still indicates printing
274  								data = b"CMD M27 Received.\r\nPrinting paused\r\nok\r\n"
275  							else:
276  								# after print is cancelled M27 always looks like its printing from sd card
277  								data = b"CMD M27 Received.\r\nNot SD printing\r\nok\r\n"
278  
279  			elif b"CMD M114 " in data:
280  				# looks like get current position returns A: and B: for extruders?
281  				data = data.replace(b" A:", b" E0:").replace(b" B:", b" E1:")
282  
283  			elif b"CMD M119 " in data:
284  				if b"MachineStatus: READY" in data:
285  					self._printerstate = self.STATE_READY
286  				elif b"MachineStatus: BUILDING_FROM_SD" in data:
287  					if b"MoveMode: PAUSED" in data:
288  						self._printerstate = self.STATE_SD_PAUSED
289  					else:
290  						self._printerstate = self.STATE_SD_BUILDING
291  				else:
292  					self._printerstate = self.STATE_BUSY
293  
294  
295  			# turn data into list of lines
296  			datalines = data.splitlines()
297  			for i, line in enumerate(datalines):
298  				self._incoming.put(line)
299  
300  				# if M20 (list SD card files) does not return anything, make it look like an empty file list
301  				if b"CMD M20 " in line and datalines[i+1] and datalines[i+1] == b"ok":
302  					# fetch SD card list does not get anything so fake out a result
303  					self._incoming.put("Begin file list")
304  					self._incoming.put("End file list")
305  
306  		else:
307  			self._incoming.put(data)
308  
309  		self._readlock.release()
310  		return self._incoming.get_nowait()
311  
312  
313  	def readraw(self, timeout=-1):
314  		"""
315  		Read everything available from the from the printer
316  
317  		Returns:
318  			String containing response from the printer
319  		"""
320  
321  		data = b''
322  		if timeout == -1:
323  			timeout = int(self._read_timeout * 1000.0)
324  		self._logger.debug("FlashForge.readraw() called by thread: {}, timeout: {}".format(threading.currentThread().getName(), timeout))
325  
326  		try:
327  			# read data from USB until ok signals end or timeout
328  			while not data.strip().endswith(b"ok"):
329  				data += self._handle.bulkRead(self._usb_cmd_endpoint_in, self.BUFFER_SIZE, timeout)
330  
331  		except usb1.USBError as usberror:
332  			if not usberror.value == -7:  # LIBUSB_ERROR_TIMEOUT:
333  				raise FlashForgeError("USB Error readraw()", usberror)
334  			else:
335  				self._logger.debug("FlashForge.readraw() error: {}".format(usberror))
336  
337  		self._logger.debug("FlashForge.readraw() {}".format(data.decode().replace("\r\n", " | ")))
338  		return data
339  
340  
341  	def sendcommand(self, cmd, timeout=-1, readresponse=True):
342  		self._logger.debug("FlashForge.sendcommand() {}".format(cmd.decode()))
343  
344  		self.writeraw(b"~%s\r\n" % cmd)
345  		if not readresponse:
346  			return True, None
347  
348  		# read response, make sure we are getting the command we sent
349  		gcode = b"CMD %s " % cmd.split(b" ", 1)[0]
350  		response = b" "
351  		while response and gcode not in response:
352  			response = self.readraw(timeout)
353  		if b"ok\r\n" in response:
354  			self._logger.debug("FlashForge.sendcommand() got an ok")
355  			return True, response
356  		return False, response
357  
358  
359  	def makeexclusive(self, exclusive):
360  		"""	Obtain exclusive use of the connection for the current thread"""
361  
362  		if exclusive:
363  			self._readlock.acquire()
364  			self._writelock.acquire()
365  		else:
366  			self._readlock.release()
367  			self._writelock.release()
368  
369  
370  	def close(self):
371  		"""	Close USB connection and cleanup. OctoPrint Serial Factory method"""
372  
373  		self._logger.debug("FlashForge.close()")
374  		self._incoming = None
375  		self._plugin.on_disconnect()
376  		if self._handle:
377  			try:
378  				self._handle.releaseInterface(0)
379  			except Exception:
380  				pass
381  			try:
382  				self._handle.close()
383  			except usb1.USBError as usberror:
384  				raise FlashForgeError("Error releasing USB", usberror)
385  			self._handle = None