mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-13 17:57:55 -06:00
Updates to ensure that usb printing now works via output device API
This commit is contained in:
parent
a47c8f7ef6
commit
04b8961297
3 changed files with 189 additions and 281 deletions
|
@ -8,96 +8,143 @@ import time
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
import functools
|
import functools
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Signal import Signal, SignalEmitter
|
from UM.Signal import Signal, SignalEmitter
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
|
from UM.OutputDevice import OutputDeviceError
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
|
||||||
|
from PyQt5.QtQuick import QQuickView
|
||||||
|
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||||
|
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
class PrinterConnection(OutputDevice, QObject, SignalEmitter):
|
||||||
|
def __init__(self, serial_port, parent = None):
|
||||||
|
QObject.__init__(self, parent)
|
||||||
|
OutputDevice.__init__(self, serial_port)
|
||||||
|
SignalEmitter.__init__(self)
|
||||||
|
#super().__init__(serial_port)
|
||||||
|
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
|
||||||
|
self.setShortDescription(catalog.i18nc("@action:button", "Print with USB"))
|
||||||
|
self.setDescription(catalog.i18nc("@info:tooltip", "Print with USB"))
|
||||||
|
self.setIconName("print")
|
||||||
|
|
||||||
class PrinterConnection(SignalEmitter):
|
|
||||||
def __init__(self, serial_port):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._serial = None
|
self._serial = None
|
||||||
self._serial_port = serial_port
|
self._serial_port = serial_port
|
||||||
self._error_state = None
|
self._error_state = None
|
||||||
|
|
||||||
self._connect_thread = threading.Thread(target = self._connect)
|
self._connect_thread = threading.Thread(target = self._connect)
|
||||||
self._connect_thread.daemon = True
|
self._connect_thread.daemon = True
|
||||||
|
|
||||||
# Printer is connected
|
# Printer is connected
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
|
|
||||||
# Printer is in the process of connecting
|
# Printer is in the process of connecting
|
||||||
self._is_connecting = False
|
self._is_connecting = False
|
||||||
|
|
||||||
# The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable
|
# 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.
|
# response. If the baudrate is correct, this should make sense, else we get giberish.
|
||||||
self._required_responses_auto_baud = 10
|
self._required_responses_auto_baud = 10
|
||||||
|
|
||||||
self._progress = 0
|
self._progress = 0
|
||||||
|
|
||||||
self._listen_thread = threading.Thread(target=self._listen)
|
self._listen_thread = threading.Thread(target=self._listen)
|
||||||
self._listen_thread.daemon = True
|
self._listen_thread.daemon = True
|
||||||
|
|
||||||
self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
|
self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
|
||||||
self._update_firmware_thread.demon = True
|
self._update_firmware_thread.demon = True
|
||||||
|
|
||||||
self._heatup_wait_start_time = time.time()
|
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.
|
## Queue for commands that need to be send. Used when command is sent when a print is active.
|
||||||
self._command_queue = queue.Queue()
|
self._command_queue = queue.Queue()
|
||||||
|
|
||||||
self._is_printing = False
|
self._is_printing = False
|
||||||
|
|
||||||
## Set when print is started in order to check running time.
|
## Set when print is started in order to check running time.
|
||||||
self._print_start_time = None
|
self._print_start_time = None
|
||||||
self._print_start_time_100 = None
|
self._print_start_time_100 = None
|
||||||
|
|
||||||
## Keep track where in the provided g-code the print is
|
## Keep track where in the provided g-code the print is
|
||||||
self._gcode_position = 0
|
self._gcode_position = 0
|
||||||
|
|
||||||
# List of gcode lines to be printed
|
# List of gcode lines to be printed
|
||||||
self._gcode = []
|
self._gcode = []
|
||||||
|
|
||||||
# Number of extruders
|
# Number of extruders
|
||||||
self._extruder_count = 1
|
self._extruder_count = 1
|
||||||
|
|
||||||
# Temperatures of all extruders
|
# Temperatures of all extruders
|
||||||
self._extruder_temperatures = [0] * self._extruder_count
|
self._extruder_temperatures = [0] * self._extruder_count
|
||||||
|
|
||||||
# Target temperatures of all extruders
|
# Target temperatures of all extruders
|
||||||
self._target_extruder_temperatures = [0] * self._extruder_count
|
self._target_extruder_temperatures = [0] * self._extruder_count
|
||||||
|
|
||||||
#Target temperature of the bed
|
#Target temperature of the bed
|
||||||
self._target_bed_temperature = 0
|
self._target_bed_temperature = 0
|
||||||
|
|
||||||
# Temperature of the bed
|
# Temperature of the bed
|
||||||
self._bed_temperature = 0
|
self._bed_temperature = 0
|
||||||
|
|
||||||
# Current Z stage location
|
# Current Z stage location
|
||||||
self._current_z = 0
|
self._current_z = 0
|
||||||
|
|
||||||
# In order to keep the connection alive we request the temperature every so often from a different extruder.
|
# 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.
|
# This index is the extruder we requested data from the last time.
|
||||||
self._temperature_requested_extruder_index = 0
|
self._temperature_requested_extruder_index = 0
|
||||||
|
|
||||||
self._updating_firmware = False
|
self._updating_firmware = False
|
||||||
|
|
||||||
self._firmware_file_name = None
|
self._firmware_file_name = None
|
||||||
|
|
||||||
|
self._control_view = None
|
||||||
|
|
||||||
|
onError = pyqtSignal()
|
||||||
|
progressChanged = pyqtSignal()
|
||||||
|
extruderTemperatureChanged = pyqtSignal()
|
||||||
|
bedTemperatureChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = progressChanged)
|
||||||
|
def progress(self):
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = extruderTemperatureChanged)
|
||||||
|
def extruderTemperature(self):
|
||||||
|
return self._extruder_temperatures[0]
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||||
|
def bedTemperature(self):
|
||||||
|
return self._bed_temperature
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = onError)
|
||||||
|
def error(self):
|
||||||
|
return self._error_state
|
||||||
|
|
||||||
# TODO: Might need to add check that extruders can not be changed when it started printing or loading these settings from settings object
|
# 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):
|
def setNumExtuders(self, num):
|
||||||
self._extruder_count = num
|
self._extruder_count = num
|
||||||
self._extruder_temperatures = [0] * self._extruder_count
|
self._extruder_temperatures = [0] * self._extruder_count
|
||||||
self._target_extruder_temperatures = [0] * self._extruder_count
|
self._target_extruder_temperatures = [0] * self._extruder_count
|
||||||
|
|
||||||
## Is the printer actively printing
|
## Is the printer actively printing
|
||||||
def isPrinting(self):
|
def isPrinting(self):
|
||||||
if not self._is_connected or self._serial is None:
|
if not self._is_connected or self._serial is None:
|
||||||
return False
|
return False
|
||||||
return self._is_printing
|
return self._is_printing
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def startPrint(self):
|
||||||
|
gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list")
|
||||||
|
self.printGCode(gcode_list)
|
||||||
|
|
||||||
## Start a print based on a g-code.
|
## Start a print based on a g-code.
|
||||||
# \param gcode_list List with gcode (strings).
|
# \param gcode_list List with gcode (strings).
|
||||||
def printGCode(self, gcode_list):
|
def printGCode(self, gcode_list):
|
||||||
|
@ -115,20 +162,20 @@ class PrinterConnection(SignalEmitter):
|
||||||
self._print_start_time_100 = None
|
self._print_start_time_100 = None
|
||||||
self._is_printing = True
|
self._is_printing = True
|
||||||
self._print_start_time = time.time()
|
self._print_start_time = time.time()
|
||||||
|
|
||||||
for i in range(0, 4): #Push first 4 entries before accepting other inputs
|
for i in range(0, 4): #Push first 4 entries before accepting other inputs
|
||||||
self._sendNextGcodeLine()
|
self._sendNextGcodeLine()
|
||||||
|
|
||||||
## Get the serial port string of this connection.
|
## Get the serial port string of this connection.
|
||||||
# \return serial port
|
# \return serial port
|
||||||
def getSerialPort(self):
|
def getSerialPort(self):
|
||||||
return self._serial_port
|
return self._serial_port
|
||||||
|
|
||||||
## Try to connect the serial. This simply starts the thread, which runs _connect.
|
## Try to connect the serial. This simply starts the thread, which runs _connect.
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if not self._updating_firmware and not self._connect_thread.isAlive():
|
if not self._updating_firmware and not self._connect_thread.isAlive():
|
||||||
self._connect_thread.start()
|
self._connect_thread.start()
|
||||||
|
|
||||||
## Private fuction (threaded) that actually uploads the firmware.
|
## Private fuction (threaded) that actually uploads the firmware.
|
||||||
def _updateFirmware(self):
|
def _updateFirmware(self):
|
||||||
if self._is_connecting or self._is_connected:
|
if self._is_connecting or self._is_connected:
|
||||||
|
@ -173,10 +220,10 @@ class PrinterConnection(SignalEmitter):
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
Logger.log("d", "Attempting to connect to %s", self._serial_port)
|
Logger.log("d", "Attempting to connect to %s", self._serial_port)
|
||||||
self._is_connecting = True
|
self._is_connecting = True
|
||||||
programmer = stk500v2.Stk500v2()
|
programmer = stk500v2.Stk500v2()
|
||||||
try:
|
try:
|
||||||
programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it"s an arduino based usb device.
|
programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it"s an arduino based usb device.
|
||||||
self._serial = programmer.leaveISP()
|
self._serial = programmer.leaveISP()
|
||||||
except ispBase.IspError as e:
|
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)))
|
Logger.log("i", "Could not establish connection on %s: %s. Device is not arduino based." %(self._serial_port,str(e)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -186,31 +233,28 @@ class PrinterConnection(SignalEmitter):
|
||||||
self._is_connecting = False
|
self._is_connecting = False
|
||||||
Logger.log("i", "Could not establish connection on %s, unknown reasons.", self._serial_port)
|
Logger.log("i", "Could not establish connection on %s, unknown reasons.", self._serial_port)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the programmer connected, we know its an atmega based version. Not all that usefull, but it does give some debugging information.
|
# 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)
|
for baud_rate in self._getBaudrateList(): # Cycle all baud rates (auto detect)
|
||||||
|
|
||||||
if self._serial is None:
|
if self._serial is None:
|
||||||
try:
|
try:
|
||||||
self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout=3, writeTimeout=10000)
|
self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout = 3, writeTimeout = 10000)
|
||||||
except serial.SerialException:
|
except serial.SerialException:
|
||||||
Logger.log("i", "Could not open port %s" % self._serial_port)
|
Logger.log("i", "Could not open port %s" % self._serial_port)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if not self.setBaudRate(baud_rate):
|
if not self.setBaudRate(baud_rate):
|
||||||
continue # Could not set the baud rate, go to the next
|
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
|
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
|
sucesfull_responses = 0
|
||||||
timeout_time = time.time() + 15
|
timeout_time = time.time() + 5
|
||||||
self._serial.write(b"\n")
|
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
|
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():
|
while timeout_time > time.time():
|
||||||
line = self._readline()
|
line = self._readline()
|
||||||
if line is None:
|
if line is None:
|
||||||
self.setIsConnected(False) # Something went wrong with reading, could be that close was called.
|
self.setIsConnected(False) # Something went wrong with reading, could be that close was called.
|
||||||
return
|
return
|
||||||
|
|
||||||
if b"T:" in line:
|
if b"T:" in line:
|
||||||
self._serial.timeout = 0.5
|
self._serial.timeout = 0.5
|
||||||
self._sendCommand("M105")
|
self._sendCommand("M105")
|
||||||
|
@ -240,21 +284,12 @@ class PrinterConnection(SignalEmitter):
|
||||||
self.connectionStateChanged.emit(self._serial_port)
|
self.connectionStateChanged.emit(self._serial_port)
|
||||||
if self._is_connected:
|
if self._is_connected:
|
||||||
self._listen_thread.start() #Start listening
|
self._listen_thread.start() #Start listening
|
||||||
#Application.getInstance().addOutputDevice(self._serial_port, {
|
|
||||||
#"id": self._serial_port,
|
|
||||||
#"function": self.printGCode,
|
|
||||||
#"shortDescription": "Print with USB",
|
|
||||||
#"description": "Print with USB {0}".format(self._serial_port),
|
|
||||||
#"icon": "save",
|
|
||||||
#"priority": 1
|
|
||||||
#})
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Printer connection state was not changed")
|
Logger.log("w", "Printer connection state was not changed")
|
||||||
|
|
||||||
connectionStateChanged = Signal()
|
connectionStateChanged = Signal()
|
||||||
|
|
||||||
## Close the printer connection
|
## Close the printer connection
|
||||||
def close(self):
|
def close(self):
|
||||||
if self._connect_thread.isAlive():
|
if self._connect_thread.isAlive():
|
||||||
try:
|
try:
|
||||||
|
@ -269,12 +304,12 @@ class PrinterConnection(SignalEmitter):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
self._serial.close()
|
self._serial.close()
|
||||||
|
|
||||||
self._serial = None
|
self._serial = None
|
||||||
|
|
||||||
def isConnected(self):
|
def isConnected(self):
|
||||||
return self._is_connected
|
return self._is_connected
|
||||||
|
|
||||||
## Directly send the command, withouth checking connection state (eg; printing).
|
## Directly send the command, withouth checking connection state (eg; printing).
|
||||||
# \param cmd string with g-code
|
# \param cmd string with g-code
|
||||||
def _sendCommand(self, cmd):
|
def _sendCommand(self, cmd):
|
||||||
|
@ -296,10 +331,9 @@ class PrinterConnection(SignalEmitter):
|
||||||
self._target_bed_temperature = float(re.search("S([0-9]+)", cmd).group(1))
|
self._target_bed_temperature = float(re.search("S([0-9]+)", cmd).group(1))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
#Logger.log("i","Sending: %s" % (cmd))
|
|
||||||
try:
|
try:
|
||||||
command = (cmd + "\n").encode()
|
command = (cmd + "\n").encode()
|
||||||
#self._serial.write(b"\n")
|
self._serial.write(b"\n")
|
||||||
self._serial.write(command)
|
self._serial.write(command)
|
||||||
except serial.SerialTimeoutException:
|
except serial.SerialTimeoutException:
|
||||||
Logger.log("w","Serial timeout while writing to serial port, trying again.")
|
Logger.log("w","Serial timeout while writing to serial port, trying again.")
|
||||||
|
@ -314,11 +348,26 @@ class PrinterConnection(SignalEmitter):
|
||||||
Logger.log("e","Unexpected error while writing serial port %s" % e)
|
Logger.log("e","Unexpected error while writing serial port %s" % e)
|
||||||
self._setErrorState("Unexpected error while writing serial port %s " % e)
|
self._setErrorState("Unexpected error while writing serial port %s " % e)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
## Ensure that close gets called when object is destroyed
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
def createControlInterface(self):
|
||||||
|
if self._control_view is None:
|
||||||
|
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "ControlWindow.qml"))
|
||||||
|
component = QQmlComponent(Application.getInstance()._engine, path)
|
||||||
|
self._control_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||||
|
self._control_context.setContextProperty("manager", self)
|
||||||
|
self._control_view = component.create(self._control_context)
|
||||||
|
|
||||||
|
## Show control interface.
|
||||||
|
# This will create the view if its not already created.
|
||||||
|
def showControlInterface(self):
|
||||||
|
if self._control_view is None:
|
||||||
|
self.createControlInterface()
|
||||||
|
self._control_view.show()
|
||||||
|
|
||||||
## Send a command to printer.
|
## Send a command to printer.
|
||||||
# \param cmd string with g-code
|
# \param cmd string with g-code
|
||||||
def sendCommand(self, cmd):
|
def sendCommand(self, cmd):
|
||||||
|
@ -326,37 +375,33 @@ class PrinterConnection(SignalEmitter):
|
||||||
self._command_queue.put(cmd)
|
self._command_queue.put(cmd)
|
||||||
elif self.isConnected():
|
elif self.isConnected():
|
||||||
self._sendCommand(cmd)
|
self._sendCommand(cmd)
|
||||||
|
|
||||||
## Set the error state with a message.
|
## Set the error state with a message.
|
||||||
# \param error String with the error message.
|
# \param error String with the error message.
|
||||||
def _setErrorState(self, error):
|
def _setErrorState(self, error):
|
||||||
self._error_state = error
|
self._error_state = error
|
||||||
self.onError.emit(error)
|
self.onError.emit()
|
||||||
|
|
||||||
onError = Signal()
|
|
||||||
|
|
||||||
## Private function to set the temperature of an extruder
|
## Private function to set the temperature of an extruder
|
||||||
# \param index index of the extruder
|
# \param index index of the extruder
|
||||||
# \param temperature recieved temperature
|
# \param temperature recieved temperature
|
||||||
def _setExtruderTemperature(self, index, temperature):
|
def _setExtruderTemperature(self, index, temperature):
|
||||||
try:
|
try:
|
||||||
self._extruder_temperatures[index] = temperature
|
self._extruder_temperatures[index] = temperature
|
||||||
self.onExtruderTemperatureChange.emit(self._serial_port, index, temperature)
|
self.extruderTemperatureChanged.emit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
onExtruderTemperatureChange = Signal()
|
|
||||||
|
|
||||||
## Private function to set the temperature of the bed.
|
## Private function to set the temperature of the bed.
|
||||||
# As all printers (as of time of writing) only support a single heated bed,
|
# As all printers (as of time of writing) only support a single heated bed,
|
||||||
# these are not indexed as with extruders.
|
# these are not indexed as with extruders.
|
||||||
def _setBedTemperature(self, temperature):
|
def _setBedTemperature(self, temperature):
|
||||||
self._bed_temperature = temperature
|
self._bed_temperature = temperature
|
||||||
self.onBedTemperatureChange.emit(self._serial_port,temperature)
|
self.bedTemperatureChanged.emit()
|
||||||
|
|
||||||
onBedTemperatureChange = Signal()
|
def requestWrite(self, node):
|
||||||
|
self.showControlInterface()
|
||||||
|
|
||||||
## Listen thread function.
|
## Listen thread function.
|
||||||
def _listen(self):
|
def _listen(self):
|
||||||
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
|
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
|
||||||
|
@ -364,10 +409,10 @@ class PrinterConnection(SignalEmitter):
|
||||||
ok_timeout = time.time()
|
ok_timeout = time.time()
|
||||||
while self._is_connected:
|
while self._is_connected:
|
||||||
line = self._readline()
|
line = self._readline()
|
||||||
|
|
||||||
if line is None:
|
if line is None:
|
||||||
break # None is only returned when something went wrong. Stop listening
|
break # None is only returned when something went wrong. Stop listening
|
||||||
|
|
||||||
if line.startswith(b"Error:"):
|
if line.startswith(b"Error:"):
|
||||||
# Oh YEAH, consistency.
|
# Oh YEAH, consistency.
|
||||||
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
|
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
|
||||||
|
@ -401,10 +446,10 @@ class PrinterConnection(SignalEmitter):
|
||||||
else:
|
else:
|
||||||
self.sendCommand("M105")
|
self.sendCommand("M105")
|
||||||
temperature_request_timeout = time.time() + 5
|
temperature_request_timeout = time.time() + 5
|
||||||
|
|
||||||
if line == b"" and time.time() > ok_timeout:
|
if line == b"" and time.time() > ok_timeout:
|
||||||
line = b"ok" # Force a timeout (basicly, send next command)
|
line = b"ok" # Force a timeout (basicly, send next command)
|
||||||
|
|
||||||
if b"ok" in line:
|
if b"ok" in line:
|
||||||
ok_timeout = time.time() + 5
|
ok_timeout = time.time() + 5
|
||||||
if not self._command_queue.empty():
|
if not self._command_queue.empty():
|
||||||
|
@ -449,26 +494,25 @@ class PrinterConnection(SignalEmitter):
|
||||||
Logger.log("e", "Unexpected error with printer connection: %s" % e)
|
Logger.log("e", "Unexpected error with printer connection: %s" % e)
|
||||||
self._setErrorState("Unexpected error: %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)))
|
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._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
|
||||||
self._gcode_position += 1
|
self._gcode_position += 1
|
||||||
self.setProgress(( self._gcode_position / len(self._gcode)) * 100)
|
self.setProgress(( self._gcode_position / len(self._gcode)) * 100)
|
||||||
self.progressChanged.emit(self._progress, self._serial_port)
|
self.progressChanged.emit()
|
||||||
|
|
||||||
progressChanged = Signal()
|
|
||||||
|
|
||||||
## Set the progress of the print.
|
## Set the progress of the print.
|
||||||
# It will be normalized (based on max_progress) to range 0 - 100
|
# It will be normalized (based on max_progress) to range 0 - 100
|
||||||
def setProgress(self, progress, max_progress = 100):
|
def setProgress(self, progress, max_progress = 100):
|
||||||
self._progress = (progress / max_progress) * 100 #Convert to scale of 0-100
|
self._progress = (progress / max_progress) * 100 #Convert to scale of 0-100
|
||||||
self.progressChanged.emit(self._progress, self._serial_port)
|
self.progressChanged.emit()
|
||||||
|
|
||||||
## Cancel the current print. Printer connection wil continue to listen.
|
## Cancel the current print. Printer connection wil continue to listen.
|
||||||
|
@pyqtSlot()
|
||||||
def cancelPrint(self):
|
def cancelPrint(self):
|
||||||
self._gcode_position = 0
|
self._gcode_position = 0
|
||||||
self.setProgress(0)
|
self.setProgress(0)
|
||||||
self._gcode = []
|
self._gcode = []
|
||||||
|
|
||||||
# Turn of temperatures
|
# Turn of temperatures
|
||||||
self._sendCommand("M140 S0")
|
self._sendCommand("M140 S0")
|
||||||
self._sendCommand("M104 S0")
|
self._sendCommand("M104 S0")
|
||||||
|
@ -477,7 +521,7 @@ class PrinterConnection(SignalEmitter):
|
||||||
## Check if the process did not encounter an error yet.
|
## Check if the process did not encounter an error yet.
|
||||||
def hasError(self):
|
def hasError(self):
|
||||||
return self._error_state != None
|
return self._error_state != None
|
||||||
|
|
||||||
## private read line used by printer connection to listen for data on serial port.
|
## private read line used by printer connection to listen for data on serial port.
|
||||||
def _readline(self):
|
def _readline(self):
|
||||||
if self._serial is None:
|
if self._serial is None:
|
||||||
|
@ -490,7 +534,7 @@ class PrinterConnection(SignalEmitter):
|
||||||
self.close()
|
self.close()
|
||||||
return None
|
return None
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
## Create a list of baud rates at which we can communicate.
|
## Create a list of baud rates at which we can communicate.
|
||||||
# \return list of int
|
# \return list of int
|
||||||
def _getBaudrateList(self):
|
def _getBaudrateList(self):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import platform
|
import platform
|
||||||
|
@ -26,34 +27,43 @@ from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
class USBPrinterManager(QObject, SignalEmitter, Extension):
|
class USBPrinterManager(QObject, SignalEmitter, OutputDevicePlugin, Extension):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
QObject.__init__(self, parent)
|
||||||
|
SignalEmitter.__init__(self)
|
||||||
|
OutputDevicePlugin.__init__(self)
|
||||||
|
Extension.__init__(self)
|
||||||
self._serial_port_list = []
|
self._serial_port_list = []
|
||||||
self._printer_connections = []
|
self._printer_connections = {}
|
||||||
self._check_ports_thread = threading.Thread(target = self._updateConnectionList)
|
self._update_thread = threading.Thread(target = self._updateThread)
|
||||||
self._check_ports_thread.daemon = True
|
self._update_thread.setDaemon(True)
|
||||||
self._check_ports_thread.start()
|
|
||||||
|
|
||||||
self._progress = 0
|
|
||||||
|
|
||||||
self._control_view = None
|
self._check_updates = True
|
||||||
self._firmware_view = None
|
self._firmware_view = None
|
||||||
self._extruder_temp = 0
|
|
||||||
self._bed_temp = 0
|
|
||||||
self._error_message = ""
|
|
||||||
|
|
||||||
## Add menu item to top menu of the application.
|
## Add menu item to top menu of the application.
|
||||||
self.setMenuName("Firmware")
|
self.setMenuName("Firmware")
|
||||||
self.addMenuItem(i18n_catalog.i18n("Update Firmware"), self.updateAllFirmware)
|
self.addMenuItem(i18n_catalog.i18n("Update Firmware"), self.updateAllFirmware)
|
||||||
|
|
||||||
Application.getInstance().applicationShuttingDown.connect(self._onApplicationShuttingDown)
|
Application.getInstance().applicationShuttingDown.connect(self.stop)
|
||||||
|
self.addConnectionSignal.connect(self.addConnection) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||||
pyqtError = pyqtSignal(str, arguments = ["error"])
|
|
||||||
processingProgress = pyqtSignal(float, arguments = ["amount"])
|
addConnectionSignal = Signal()
|
||||||
pyqtExtruderTemperature = pyqtSignal(float, arguments = ["amount"])
|
|
||||||
pyqtBedTemperature = pyqtSignal(float, arguments = ["amount"])
|
def start(self):
|
||||||
|
self._check_updates = True
|
||||||
|
self._update_thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._check_updates = False
|
||||||
|
self._update_thread.join()
|
||||||
|
|
||||||
|
def _updateThread(self):
|
||||||
|
while self._check_updates:
|
||||||
|
result = self.getSerialPortList(only_list_usb = True)
|
||||||
|
self._addRemovePorts(result)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
## Show firmware interface.
|
## Show firmware interface.
|
||||||
# This will create the view if its not already created.
|
# This will create the view if its not already created.
|
||||||
def spawnFirmwareInterface(self, serial_port):
|
def spawnFirmwareInterface(self, serial_port):
|
||||||
|
@ -66,65 +76,7 @@ class USBPrinterManager(QObject, SignalEmitter, Extension):
|
||||||
self._firmware_view = component.create(self._firmware_context)
|
self._firmware_view = component.create(self._firmware_context)
|
||||||
|
|
||||||
self._firmware_view.show()
|
self._firmware_view.show()
|
||||||
|
|
||||||
## Show control interface.
|
|
||||||
# This will create the view if its not already created.
|
|
||||||
def spawnControlInterface(self,serial_port):
|
|
||||||
if self._control_view is None:
|
|
||||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "ControlWindow.qml"))
|
|
||||||
|
|
||||||
component = QQmlComponent(Application.getInstance()._engine, path)
|
|
||||||
self._control_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
|
||||||
self._control_context.setContextProperty("manager", self)
|
|
||||||
|
|
||||||
self._control_view = component.create(self._control_context)
|
|
||||||
|
|
||||||
self._control_view.show()
|
|
||||||
|
|
||||||
@pyqtProperty(float,notify = processingProgress)
|
|
||||||
def progress(self):
|
|
||||||
return self._progress
|
|
||||||
|
|
||||||
@pyqtProperty(float,notify = pyqtExtruderTemperature)
|
|
||||||
def extruderTemperature(self):
|
|
||||||
return self._extruder_temp
|
|
||||||
|
|
||||||
@pyqtProperty(float,notify = pyqtBedTemperature)
|
|
||||||
def bedTemperature(self):
|
|
||||||
return self._bed_temp
|
|
||||||
|
|
||||||
@pyqtProperty(str,notify = pyqtError)
|
|
||||||
def error(self):
|
|
||||||
return self._error_message
|
|
||||||
|
|
||||||
## Check all serial ports and create a PrinterConnection object for them.
|
|
||||||
# Note that this does not validate if the serial ports are actually usable!
|
|
||||||
# This (the validation) is only done when the connect function is called.
|
|
||||||
def _updateConnectionList(self):
|
|
||||||
while True:
|
|
||||||
temp_serial_port_list = self.getSerialPortList(only_list_usb = True)
|
|
||||||
if temp_serial_port_list != self._serial_port_list: # Something changed about the list since we last changed something.
|
|
||||||
disconnected_ports = [port for port in self._serial_port_list if port not in temp_serial_port_list ]
|
|
||||||
self._serial_port_list = temp_serial_port_list
|
|
||||||
for serial_port in self._serial_port_list:
|
|
||||||
if self.getConnectionByPort(serial_port) is None: # If it doesn't already exist, add it
|
|
||||||
if not os.path.islink(serial_port): # Only add the connection if it's a non symbolic link
|
|
||||||
connection = PrinterConnection.PrinterConnection(serial_port)
|
|
||||||
connection.connect()
|
|
||||||
connection.connectionStateChanged.connect(self.serialConectionStateCallback)
|
|
||||||
connection.progressChanged.connect(self.onProgress)
|
|
||||||
connection.onExtruderTemperatureChange.connect(self.onExtruderTemperature)
|
|
||||||
connection.onBedTemperatureChange.connect(self.onBedTemperature)
|
|
||||||
connection.onError.connect(self.onError)
|
|
||||||
self._printer_connections.append(connection)
|
|
||||||
|
|
||||||
for serial_port in disconnected_ports: # Close connections and remove them from list.
|
|
||||||
connection = self.getConnectionByPort(serial_port)
|
|
||||||
if connection != None:
|
|
||||||
self._printer_connections.remove(connection)
|
|
||||||
connection.close()
|
|
||||||
time.sleep(5) # Throttle, as we don"t need this information to be updated every single second.
|
|
||||||
|
|
||||||
def updateAllFirmware(self):
|
def updateAllFirmware(self):
|
||||||
self.spawnFirmwareInterface("")
|
self.spawnFirmwareInterface("")
|
||||||
for printer_connection in self._printer_connections:
|
for printer_connection in self._printer_connections:
|
||||||
|
@ -132,13 +84,13 @@ class USBPrinterManager(QObject, SignalEmitter, Extension):
|
||||||
printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName()))
|
printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName()))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def updateFirmwareBySerial(self, serial_port):
|
def updateFirmwareBySerial(self, serial_port):
|
||||||
printer_connection = self.getConnectionByPort(serial_port)
|
printer_connection = self.getConnectionByPort(serial_port)
|
||||||
if printer_connection is not None:
|
if printer_connection is not None:
|
||||||
self.spawnFirmwareInterface(printer_connection.getSerialPort())
|
self.spawnFirmwareInterface(printer_connection.getSerialPort())
|
||||||
printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName()))
|
printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName()))
|
||||||
|
|
||||||
def _getDefaultFirmwareName(self):
|
def _getDefaultFirmwareName(self):
|
||||||
machine_type = Application.getInstance().getActiveMachine().getTypeID()
|
machine_type = Application.getInstance().getActiveMachine().getTypeID()
|
||||||
firmware_name = ""
|
firmware_name = ""
|
||||||
|
@ -160,120 +112,35 @@ class USBPrinterManager(QObject, SignalEmitter, Extension):
|
||||||
return "MarlinUltimaker2.hex"
|
return "MarlinUltimaker2.hex"
|
||||||
|
|
||||||
##TODO: Add check for multiple extruders
|
##TODO: Add check for multiple extruders
|
||||||
|
|
||||||
if firmware_name != "":
|
if firmware_name != "":
|
||||||
firmware_name += ".hex"
|
firmware_name += ".hex"
|
||||||
return firmware_name
|
return firmware_name
|
||||||
|
|
||||||
## Callback for extruder temperature change
|
def _addRemovePorts(self, serial_ports):
|
||||||
def onExtruderTemperature(self, serial_port, index, temperature):
|
# First, find and add all new or changed keys
|
||||||
self._extruder_temp = temperature
|
for serial_port in list(serial_ports):
|
||||||
self.pyqtExtruderTemperature.emit(temperature)
|
if serial_port not in self._serial_port_list:
|
||||||
|
self.addConnectionSignal.emit(serial_port) #Hack to ensure its created in main thread
|
||||||
## Callback for bed temperature change
|
continue
|
||||||
def onBedTemperature(self, serial_port,temperature):
|
self._serial_port_list = list(serial_ports)
|
||||||
self._bed_temp = temperature
|
|
||||||
self.pyqtBedTemperature.emit(temperature)
|
|
||||||
|
|
||||||
## Callback for error
|
|
||||||
def onError(self, error):
|
|
||||||
self._error_message = error if type(error) is str else error.decode("utf-8")
|
|
||||||
self.pyqtError.emit(self._error_message)
|
|
||||||
|
|
||||||
## Callback for progress change
|
|
||||||
def onProgress(self, progress, serial_port):
|
|
||||||
self._progress = progress
|
|
||||||
self.processingProgress.emit(progress)
|
|
||||||
|
|
||||||
## Attempt to connect with all possible connections.
|
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||||
def connectAllConnections(self):
|
def addConnection(self, serial_port):
|
||||||
for connection in self._printer_connections:
|
connection = PrinterConnection.PrinterConnection(serial_port)
|
||||||
connection.connect()
|
connection.connect()
|
||||||
|
connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||||
## Send gcode to printer and start printing
|
self._printer_connections[serial_port] = connection
|
||||||
def sendGCodeByPort(self, serial_port, gcode_list):
|
|
||||||
printer_connection = self.getConnectionByPort(serial_port)
|
def _onPrinterConnectionStateChanged(self, serial_port):
|
||||||
if printer_connection is not None:
|
if self._printer_connections[serial_port].isConnected():
|
||||||
printer_connection.printGCode(gcode_list)
|
self.getOutputDeviceManager().addOutputDevice(self._printer_connections[serial_port])
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def cancelPrint(self):
|
|
||||||
for printer_connection in self.getActiveConnections():
|
|
||||||
printer_connection.cancelPrint()
|
|
||||||
|
|
||||||
## Send gcode to all active printers.
|
|
||||||
# \return True if there was at least one active connection.
|
|
||||||
def sendGCodeToAllActive(self, gcode_list):
|
|
||||||
for printer_connection in self.getActiveConnections():
|
|
||||||
printer_connection.printGCode(gcode_list)
|
|
||||||
if len(self.getActiveConnections()):
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
return False
|
self.getOutputDeviceManager().removeOutputDevice(serial_port)
|
||||||
|
|
||||||
## Send a command to a printer indentified by port
|
|
||||||
# \param serial port String indentifieing the port
|
|
||||||
# \param command String with the g-code command to send.
|
|
||||||
# \return True if connection was found, false otherwise
|
|
||||||
def sendCommandByPort(self, serial_port, command):
|
|
||||||
printer_connection = self.getConnectionByPort(serial_port)
|
|
||||||
if printer_connection is not None:
|
|
||||||
printer_connection.sendCommand(command)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Send a command to all active (eg; connected) printers
|
|
||||||
# \param command String with the g-code command to send.
|
|
||||||
# \return True if at least one connection was found, false otherwise
|
|
||||||
def sendCommandToAllActive(self, command):
|
|
||||||
for printer_connection in self.getActiveConnections():
|
|
||||||
printer_connection.sendCommand(command)
|
|
||||||
if len(self.getActiveConnections()):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Callback if the connection state of a connection is changed.
|
|
||||||
# This adds or removes the connection as a possible output device.
|
|
||||||
def serialConectionStateCallback(self, serial_port):
|
|
||||||
connection = self.getConnectionByPort(serial_port)
|
|
||||||
if connection.isConnected():
|
|
||||||
Application.getInstance().addOutputDevice(serial_port, {
|
|
||||||
"id": serial_port,
|
|
||||||
"function": self.spawnControlInterface,
|
|
||||||
"description": "Print with USB {0}".format(serial_port),
|
|
||||||
"shortDescription": "Print with USB",
|
|
||||||
"icon": "save",
|
|
||||||
"priority": 1
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
Application.getInstance().removeOutputDevice(serial_port)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def startPrint(self):
|
|
||||||
gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list", None)
|
|
||||||
if gcode_list:
|
|
||||||
final_list = []
|
|
||||||
for gcode in gcode_list:
|
|
||||||
final_list += gcode.split("\n")
|
|
||||||
self.sendGCodeToAllActive(gcode_list)
|
|
||||||
|
|
||||||
## Get a list of printer connection objects that are connected.
|
|
||||||
def getActiveConnections(self):
|
|
||||||
return [connection for connection in self._printer_connections if connection.isConnected()]
|
|
||||||
|
|
||||||
## Get a printer connection object by serial port
|
|
||||||
def getConnectionByPort(self, serial_port):
|
|
||||||
for printer_connection in self._printer_connections:
|
|
||||||
if serial_port == printer_connection.getSerialPort():
|
|
||||||
return printer_connection
|
|
||||||
return None
|
|
||||||
|
|
||||||
## Create a list of serial ports on the system.
|
## Create a list of serial ports on the system.
|
||||||
# \param only_list_usb If true, only usb ports are listed
|
# \param only_list_usb If true, only usb ports are listed
|
||||||
def getSerialPortList(self,only_list_usb=False):
|
def getSerialPortList(self, only_list_usb = False):
|
||||||
base_list = []
|
base_list = []
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
import winreg
|
import winreg
|
||||||
|
@ -293,8 +160,4 @@ class USBPrinterManager(QObject, SignalEmitter, Extension):
|
||||||
base_list = filter(lambda s: "Bluetooth" not in s, base_list) # Filter because mac sometimes puts them in the list
|
base_list = filter(lambda s: "Bluetooth" not in s, base_list) # Filter because mac sometimes puts them in the list
|
||||||
else:
|
else:
|
||||||
base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob("/dev/serial/by-id/*")
|
base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob("/dev/serial/by-id/*")
|
||||||
return base_list
|
return list(base_list)
|
||||||
|
|
||||||
def _onApplicationShuttingDown(self):
|
|
||||||
for connection in self._printer_connections:
|
|
||||||
connection.close()
|
|
|
@ -13,9 +13,10 @@ def getMetaData():
|
||||||
"name": "USB printing",
|
"name": "USB printing",
|
||||||
"author": "Ultimaker",
|
"author": "Ultimaker",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
|
"api": 2,
|
||||||
"description": i18n_catalog.i18nc("USB Printing plugin description","Accepts G-Code and sends them to a printer. Plugin can also update firmware")
|
"description": i18n_catalog.i18nc("USB Printing plugin description","Accepts G-Code and sends them to a printer. Plugin can also update firmware")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return {"extension":USBPrinterManager.USBPrinterManager()}
|
return {"extension":USBPrinterManager.USBPrinterManager(),"output_device": USBPrinterManager.USBPrinterManager() }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue