/ 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