Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArjen Hiemstra <ahiemstra@heimr.nl>2015-04-30 13:30:37 +0300
committerArjen Hiemstra <ahiemstra@heimr.nl>2015-04-30 13:30:37 +0300
commit63e8cf72a3032797f25b0cc2f14a0350d0a6a9d8 (patch)
tree36dce821df38ef7dd7dc2124dc8835549b7c09cc /plugins/USBPrinting/PrinterConnection.py
parent3823afd8cca1fe92b69082d51d0d50946b904e91 (diff)
parentb28ca0881a6c2564f5447476f7b21de5645c10bd (diff)
Add 'plugins/USBPrinting/' from commit 'b28ca0881a6c2564f5447476f7b21de5645c10bd'
git-subtree-dir: plugins/USBPrinting git-subtree-mainline: 3823afd8cca1fe92b69082d51d0d50946b904e91 git-subtree-split: b28ca0881a6c2564f5447476f7b21de5645c10bd
Diffstat (limited to 'plugins/USBPrinting/PrinterConnection.py')
-rw-r--r--plugins/USBPrinting/PrinterConnection.py470
1 files changed, 470 insertions, 0 deletions
diff --git a/plugins/USBPrinting/PrinterConnection.py b/plugins/USBPrinting/PrinterConnection.py
new file mode 100644
index 0000000000..463b230582
--- /dev/null
+++ b/plugins/USBPrinting/PrinterConnection.py
@@ -0,0 +1,470 @@
+from .avr_isp import stk500v2, ispBase, intelHex
+import serial
+import threading
+import time
+import queue
+import re
+import functools
+
+from UM.Application import Application
+from UM.Signal import Signal, SignalEmitter
+from UM.Resources import Resources
+from UM.Logger import Logger
+
+class PrinterConnection(SignalEmitter):
+ def __init__(self, serial_port):
+ super().__init__()
+
+ self._serial = None
+ self._serial_port = serial_port
+ self._error_state = None
+
+ self._connect_thread = threading.Thread(target = self._connect)
+ self._connect_thread.daemon = True
+
+ # Printer is connected
+ self._is_connected = False
+
+ # Printer is in the process of connecting
+ self._is_connecting = False
+
+ # The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable
+ # response. If the baudrate is correct, this should make sense, else we get giberish.
+ self._required_responses_auto_baud = 10
+
+ self._progress = 0
+
+ self._listen_thread = threading.Thread(target=self._listen)
+ self._listen_thread.daemon = True
+
+ self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
+ self._update_firmware_thread.demon = True
+
+ self._heatup_wait_start_time = time.time()
+
+ ## Queue for commands that need to be send. Used when command is sent when a print is active.
+ self._command_queue = queue.Queue()
+
+ self._is_printing = False
+
+ ## Set when print is started in order to check running time.
+ self._print_start_time = None
+ self._print_start_time_100 = None
+
+ ## Keep track where in the provided g-code the print is
+ self._gcode_position = 0
+
+ # List of gcode lines to be printed
+ self._gcode = None
+
+ # Number of extruders
+ self._extruder_count = 1
+
+ # Temperatures of all extruders
+ self._extruder_temperatures = [0] * self._extruder_count
+
+ # Target temperatures of all extruders
+ self._target_extruder_temperatures = [0] * self._extruder_count
+
+ #Target temperature of the bed
+ self._target_bed_temperature = 0
+
+ # Temperature of the bed
+ self._bed_temperature = 0
+
+ # Current Z stage location
+ self._current_z = 0
+
+ # In order to keep the connection alive we request the temperature every so often from a different extruder.
+ # This index is the extruder we requested data from the last time.
+ self._temperature_requested_extruder_index = 0
+
+ self._updating_firmware = False
+
+ self._firmware_file_name = None
+
+ # TODO: Might need to add check that extruders can not be changed when it started printing or loading these settings from settings object
+ def setNumExtuders(self, num):
+ self._extruder_count = num
+ self._extruder_temperatures = [0] * self._extruder_count
+ self._target_extruder_temperatures = [0] * self._extruder_count
+
+ ## Is the printer actively printing
+ def isPrinting(self):
+ if not self._is_connected or self._serial is None:
+ return False
+ return self._is_printing
+
+ ## Start a print based on a g-code.
+ # \param gcode_list List with gcode (strings).
+ def printGCode(self, gcode_list):
+ if self.isPrinting() or not self._is_connected:
+ return
+ self._gcode = gcode_list
+
+ #Reset line number. If this is not done, first line is sometimes ignored
+ self._gcode.insert(0, "M110")
+ self._gcode_position = 0
+ self._print_start_time_100 = None
+ self._is_printing = True
+ self._print_start_time = time.time()
+
+ for i in range(0, 4): #Push first 4 entries before accepting other inputs
+ self._sendNextGcodeLine()
+
+ ## Get the serial port string of this connection.
+ # \return serial port
+ def getSerialPort(self):
+ return self._serial_port
+
+ ## Try to connect the serial. This simply starts the thread, which runs _connect.
+ def connect(self):
+ if not self._updating_firmware and not self._connect_thread.isAlive():
+ self._connect_thread.start()
+
+ ## Private fuction (threaded) that actually uploads the firmware.
+ def _updateFirmware(self):
+ if self._is_connecting or self._is_connected:
+ self.close()
+ hex_file = intelHex.readHex(self._firmware_file_name)
+
+ if len(hex_file) == 0:
+ Logger.log('e', "Unable to read provided hex file. Could not update firmware")
+ return
+
+ programmer = stk500v2.Stk500v2()
+ programmer.progressCallback = self.setProgress
+ programmer.connect(self._serial_port)
+
+ time.sleep(1) # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
+
+ if not programmer.isConnected():
+ Logger.log('e', "Unable to connect with serial. Could not update firmware")
+ return
+
+ self._updating_firmware = True
+
+ try:
+ programmer.programChip(hex_file)
+ self._updating_firmware = False
+ except Exception as e:
+ Logger.log("e", "Exception while trying to update firmware %s" %e)
+ self._updating_firmware = False
+ return
+ programmer.close()
+
+ ## Upload new firmware to machine
+ # \param filename full path of firmware file to be uploaded
+ def updateFirmware(self, file_name):
+ self._firmware_file_name = file_name
+ self._update_firmware_thread.start()
+
+ ## Private connect function run by thread. Can be started by calling connect.
+ def _connect(self):
+ self._is_connecting = True
+ programmer = stk500v2.Stk500v2()
+ try:
+ programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it's an arduino based usb device.
+ self._serial = programmer.leaveISP()
+ except ispBase.IspError as e:
+ Logger.log('i', "Could not establish connection on %s: %s. Device is not arduino based." %(self._serial_port,str(e)))
+ except Exception as e:
+ Logger.log('i', "Could not establish connection on %s, unknown reasons. Device is not arduino based." % self._serial_port)
+
+ # If the programmer connected, we know its an atmega based version. Not all that usefull, but it does give some debugging information.
+ for baud_rate in self._getBaudrateList(): # Cycle all baud rates (auto detect)
+
+ if self._serial is None:
+ try:
+ self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout=3, writeTimeout=10000)
+ except serial.SerialException:
+ Logger.log('i', "Could not open port %s" % self._serial_port)
+ return
+ else:
+ if not self.setBaudRate(baud_rate):
+ continue # Could not set the baud rate, go to the next
+ time.sleep(1.5) # Ensure that we are not talking to the bootloader. 1.5 sec seems to be the magic number
+ sucesfull_responses = 0
+ timeout_time = time.time() + 5
+ self._serial.write(b"\n")
+ self._sendCommand("M105") # Request temperature, as this should (if baudrate is correct) result in a command with 'T:' in it
+ while timeout_time > time.time():
+ line = self._readline()
+ if line is None:
+ self.setIsConnected(False) # Something went wrong with reading, could be that close was called.
+ return
+
+ if b"T:" in line:
+ self._serial.timeout = 0.5
+ self._serial.write(b"\n")
+ self._sendCommand("M105")
+ sucesfull_responses += 1
+ if sucesfull_responses >= self._required_responses_auto_baud:
+ self._serial.timeout = 2 #Reset serial timeout
+ self.setIsConnected(True)
+ Logger.log('i', "Established printer connection on port %s" % self._serial_port)
+ return
+ self.close() # Unable to connect, wrap up.
+ self.setIsConnected(False)
+
+ ## Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those.
+ def setBaudRate(self, baud_rate):
+ try:
+ self._serial.baudrate = baud_rate
+ return True
+ except Exception as e:
+ return False
+
+ def setIsConnected(self, state):
+ self._is_connecting = False
+ if self._is_connected != state:
+ self._is_connected = state
+ self.connectionStateChanged.emit(self._serial_port)
+ if self._is_connected:
+ self._listen_thread.start() #Start listening
+ '''Application.getInstance().addOutputDevice(self._serial_port, {
+ 'id': self._serial_port,
+ 'function': self.printGCode,
+ 'description': 'Print with USB {0}'.format(self._serial_port),
+ 'icon': 'print_usb',
+ 'priority': 1
+ })'''
+
+ else:
+ Logger.log('w', "Printer connection state was not changed")
+
+ connectionStateChanged = Signal()
+
+ ## Close the printer connection
+ def close(self):
+ if self._connect_thread.isAlive():
+ self._connect_thread.join()
+ if self._serial is not None:
+ self.setIsConnected(False)
+ self._listen_thread.join()
+ self._serial.close()
+
+ self._serial = None
+
+ def isConnected(self):
+ return self._is_connected
+
+ ## Directly send the command, withouth checking connection state (eg; printing).
+ # \param cmd string with g-code
+ def _sendCommand(self, cmd):
+ if self._serial is None:
+ return
+ if 'M109' in cmd or 'M190' in cmd:
+ self._heatup_wait_start_time = time.time()
+ if 'M104' in cmd or 'M109' in cmd:
+ try:
+ t = 0
+ if 'T' in cmd:
+ t = int(re.search('T([0-9]+)', cmd).group(1))
+ self._target_extruder_temperatures[t] = float(re.search('S([0-9]+)', cmd).group(1))
+ except:
+ pass
+ if 'M140' in cmd or 'M190' in cmd:
+ try:
+ self._target_bed_temperature = float(re.search('S([0-9]+)', cmd).group(1))
+ except:
+ pass
+ #Logger.log('i','Sending: %s' % (cmd))
+ try:
+ command = (cmd + '\n').encode()
+ #self._serial.write(b'\n')
+ self._serial.write(command)
+ except serial.SerialTimeoutException:
+ Logger.log("w","Serial timeout while writing to serial port, trying again.")
+ try:
+ time.sleep(0.5)
+ self._serial.write((cmd + '\n').encode())
+ except Exception as e:
+ Logger.log("e","Unexpected error while writing serial port %s " % e)
+ self._setErrorState("Unexpected error while writing serial port %s " % e)
+ self.close()
+ except Exception as e:
+ Logger.log('e',"Unexpected error while writing serial port %s" % e)
+ self._setErrorState("Unexpected error while writing serial port %s " % e)
+ self.close()
+
+ ## Ensure that close gets called when object is destroyed
+ def __del__(self):
+ self.close()
+
+ ## Send a command to printer.
+ # \param cmd string with g-code
+ def sendCommand(self, cmd):
+ if self.isPrinting():
+ self._command_queue.put(cmd)
+ elif self.isConnected():
+ self._sendCommand(cmd)
+
+ ## Set the error state with a message.
+ # \param error String with the error message.
+ def _setErrorState(self, error):
+ self._error_state = error
+ self.onError.emit(error)
+
+ onError = Signal()
+
+ ## Private function to set the temperature of an extruder
+ # \param index index of the extruder
+ # \param temperature recieved temperature
+ def _setExtruderTemperature(self, index, temperature):
+ try:
+ self._extruder_temperatures[index] = temperature
+ self.onExtruderTemperatureChange.emit(self._serial_port, index, temperature)
+ except Exception as e:
+ pass
+
+ onExtruderTemperatureChange = Signal()
+
+ ## Private function to set the temperature of the bed.
+ # As all printers (as of time of writing) only support a single heated bed,
+ # these are not indexed as with extruders.
+ def _setBedTemperature(self, temperature):
+ self._bed_temperature = temperature
+ self.onBedTemperatureChange.emit(self._serial_port,temperature)
+
+ onBedTemperatureChange = Signal()
+
+
+ ## Listen thread function.
+ def _listen(self):
+ Logger.log('i', "Printer connection listen thread started for %s" % self._serial_port)
+ temperature_request_timeout = time.time()
+ ok_timeout = time.time()
+ while self._is_connected:
+ line = self._readline()
+
+ if line is None:
+ break # None is only returned when something went wrong. Stop listening
+
+ if line.startswith(b'Error:'):
+ # Oh YEAH, consistency.
+ # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
+ # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
+ # So we can have an extra newline in the most common case. Awesome work people.
+ if re.match(b'Error:[0-9]\n', line):
+ line = line.rstrip() + self._readline()
+
+ # Skip the communication errors, as those get corrected.
+ if b'Extruder switched off' in line or b'Temperature heated bed switched off' in line or b'Something is wrong, please turn off the printer.' in line:
+ if not self.hasError():
+ self._setErrorState(line[6:])
+ elif b' T:' in line or line.startswith(b'T:'): #Temperature message
+ try:
+ self._setExtruderTemperature(self._temperature_requested_extruder_index,float(re.search(b"T: *([0-9\.]*)", line).group(1)))
+ except:
+ pass
+ if b'B:' in line: # Check if it's a bed temperature
+ try:
+ self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
+ except Exception as e:
+ pass
+ #TODO: temperature changed callback
+
+ if self._is_printing:
+ if time.time() > temperature_request_timeout: # When printing, request temperature every 5 seconds.
+ if self._extruder_count > 0:
+ self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._extruder_count
+ self.sendCommand("M105 T%d" % (self._temperature_requested_extruder_index))
+ else:
+ self.sendCommand("M105")
+ temperature_request_timeout = time.time() + 5
+
+ if line == b'' and time.time() > ok_timeout:
+ line = b'ok' # Force a timeout (basicly, send next command)
+
+ if b'ok' in line:
+ ok_timeout = time.time() + 5
+ if not self._command_queue.empty():
+ self._sendCommand(self._command_queue.get())
+ else:
+ self._sendNextGcodeLine()
+ elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with 'resend' and 'rs'
+ try:
+ self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1])
+ except:
+ if b"rs" in line:
+ self._gcode_position = int(line.split()[1])
+
+ else: # Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
+ if line == b'':
+ if self._extruder_count > 0:
+ self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._extruder_count
+ self.sendCommand("M105 T%d" % self._temperature_requested_extruder_index)
+ else:
+ self.sendCommand("M105")
+ Logger.log('i', "Printer connection listen thread stopped for %s" % self._serial_port)
+
+ ## Send next Gcode in the gcode list
+ def _sendNextGcodeLine(self):
+ if self._gcode_position >= len(self._gcode):
+ return
+ if self._gcode_position == 100:
+ self._print_start_time_100 = time.time()
+ line = self._gcode[self._gcode_position]
+
+ if ';' in line:
+ line = line[:line.find(';')]
+ line = line.strip()
+ try:
+ if line == 'M0' or line == 'M1':
+ line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
+ if ('G0' in line or 'G1' in line) and 'Z' in line:
+ z = float(re.search('Z([0-9\.]*)', line).group(1))
+ if self._current_z != z:
+ self._current_z = z
+ except Exception as e:
+ Logger.log('e', "Unexpected error with printer connection: %s" % e)
+ self._setErrorState("Unexpected error: %s" %e)
+ checksum = functools.reduce(lambda x,y: x^y, map(ord, 'N%d%s' % (self._gcode_position, line)))
+
+ self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
+ self._gcode_position += 1
+ self.setProgress(( self._gcode_position / len(self._gcode)) * 100)
+ self.progressChanged.emit(self._progress, self._serial_port)
+
+ progressChanged = Signal()
+
+ ## Set the progress of the print.
+ # It will be normalized (based on max_progress) to range 0 - 100
+ def setProgress(self, progress, max_progress = 100):
+ self._progress = progress / max_progress * 100 #Convert to scale of 0-100
+ self.progressChanged.emit(self._progress, self._serial_port)
+
+ ## Cancel the current print. Printer connection wil continue to listen.
+ def cancelPrint(self):
+ self._gcode_position = 0
+ self.setProgress(0)
+ self._gcode = []
+
+ # Turn of temperatures
+ self._sendCommand("M140 S0")
+ self._sendCommand("M109 S0")
+ self._is_printing = False
+
+ ## Check if the process did not encounter an error yet.
+ def hasError(self):
+ return self._error_state != None
+
+ ## private read line used by printer connection to listen for data on serial port.
+ def _readline(self):
+ if self._serial is None:
+ return None
+ try:
+ ret = self._serial.readline()
+ except Exception as e:
+ Logger.log('e',"Unexpected error while reading serial port. %s" %e)
+ self._setErrorState("Printer has been disconnected")
+ self.close()
+ return None
+ return ret
+
+ ## Create a list of baud rates at which we can communicate.
+ # \return list of int
+ def _getBaudrateList(self):
+ ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
+ return ret