Refactoring & documentation

CURA-1339
This commit is contained in:
Jaime van Kessel 2016-04-14 13:55:33 +02:00
parent 9ee6323177
commit 4b5c118ed2
3 changed files with 28 additions and 270 deletions

View file

@ -1,244 +0,0 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Signal import Signal, SignalEmitter
from . import USBPrinterOutputDevice
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutputDevice import ConnectionState
from UM.Qt.ListModel import ListModel
from UM.Message import Message
from cura.CuraApplication import CuraApplication
import threading
import platform
import glob
import time
import os.path
from UM.Extension import Extension
from PyQt5.QtQml import QQmlComponent, QQmlContext
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
class USBPrinterManager(QObject, SignalEmitter, OutputDevicePlugin, Extension):
def __init__(self, parent = None):
QObject.__init__(self, parent)
SignalEmitter.__init__(self)
OutputDevicePlugin.__init__(self)
Extension.__init__(self)
self._serial_port_list = []
self._printer_connections = {}
self._printer_connections_model = None
self._update_thread = threading.Thread(target = self._updateThread)
self._update_thread.setDaemon(True)
self._check_updates = True
self._firmware_view = None
## Add menu item to top menu of the application.
self.setMenuName(i18n_catalog.i18nc("@title:menu","Firmware"))
self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Update Firmware"), self.updateAllFirmware)
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.
addConnectionSignal = Signal()
printerConnectionStateChanged = pyqtSignal()
progressChanged = pyqtSignal()
@pyqtProperty(float, notify = progressChanged)
def progress(self):
progress = 0
for printer_name, connection in self._printer_connections.items(): # TODO: @UnusedVariable "printer_name"
progress += connection.progress
return progress / len(self._printer_connections)
def start(self):
self._check_updates = True
self._update_thread.start()
def stop(self):
self._check_updates = False
try:
self._update_thread.join()
except RuntimeError:
pass
def _updateThread(self):
while self._check_updates:
result = self.getSerialPortList(only_list_usb = True)
self._addRemovePorts(result)
time.sleep(5)
## Show firmware interface.
# This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None:
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._firmware_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._firmware_context.setContextProperty("manager", self)
self._firmware_view = component.create(self._firmware_context)
self._firmware_view.show()
@pyqtSlot()
def updateAllFirmware(self):
if not self._printer_connections:
Message(i18n_catalog.i18nc("@info","Cannot update firmware, there were no connected printers found.")).show()
return
self.spawnFirmwareInterface("")
for printer_connection in self._printer_connections:
try:
self._printer_connections[printer_connection].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
except FileNotFoundError:
self._printer_connections[printer_connection].setProgress(100, 100)
Logger.log("w", "No firmware found for printer %s", printer_connection)
continue
@pyqtSlot(str, result = bool)
def updateFirmwareBySerial(self, serial_port):
if serial_port in self._printer_connections:
self.spawnFirmwareInterface(self._printer_connections[serial_port].getSerialPort())
try:
self._printer_connections[serial_port].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
except FileNotFoundError:
self._firmware_view.close()
Logger.log("e", "Could not find firmware required for this machine")
return False
return True
return False
## Return the singleton instance of the USBPrinterManager
@classmethod
def getInstance(cls, engine = None, script_engine = None):
# Note: Explicit use of class name to prevent issues with inheritance.
if USBPrinterManager._instance is None:
USBPrinterManager._instance = cls()
return USBPrinterManager._instance
def _getDefaultFirmwareName(self):
machine_instance = Application.getInstance().getMachineManager().getActiveMachineInstance()
machine_type = machine_instance.getMachineDefinition().getId()
if platform.system() == "Linux":
baudrate = 115200
else:
baudrate = 250000
# NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg.
# https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2
# The *.hex files are stored at a seperate repository:
# https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware
machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex",
"ultimaker_original" : "MarlinUltimaker-{baudrate}.hex",
"ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
"ultimaker2" : "MarlinUltimaker2.hex",
"ultimaker2_go" : "MarlinUltimaker2go.hex",
"ultimaker2plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_extended" : "MarlinUltimaker2extended.hex",
"ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
}
machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex",
}
##TODO: Add check for multiple extruders
hex_file = None
if machine_type in machine_without_extras.keys(): # The machine needs to be defined here!
if machine_type in machine_with_heated_bed.keys() and machine_instance.getMachineSettingValue("machine_heated_bed"):
Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_type)
hex_file = machine_with_heated_bed[machine_type] # Return firmware with heated bed enabled
else:
Logger.log("d", "Choosing basic firmware for machine %s.", machine_type)
hex_file = machine_without_extras[machine_type] # Return "basic" firmware
else:
Logger.log("e", "There is no firmware for machine %s.", machine_type)
if hex_file:
return hex_file.format(baudrate=baudrate)
else:
Logger.log("e", "Could not find any firmware for machine %s.", machine_type)
raise FileNotFoundError()
def _addRemovePorts(self, serial_ports):
# First, find and add all new or changed keys
for serial_port in list(serial_ports):
if serial_port not in self._serial_port_list:
self.addConnectionSignal.emit(serial_port) #Hack to ensure its created in main thread
continue
self._serial_port_list = list(serial_ports)
connections_to_remove = []
for port, connection in self._printer_connections.items():
if port not in self._serial_port_list:
connection.close()
connections_to_remove.append(port)
for port in connections_to_remove:
del self._printer_connections[port]
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addConnection(self, serial_port):
connection = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
connection.connect()
connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
connection.progressChanged.connect(self.progressChanged)
self._printer_connections[serial_port] = connection
def _onPrinterConnectionStateChanged(self, serial_port):
try:
if self._printer_connections[serial_port].connectionState == ConnectionState.CONNECTED:
self.getOutputDeviceManager().addOutputDevice(self._printer_connections[serial_port])
else:
self.getOutputDeviceManager().removeOutputDevice(serial_port)
self.printerConnectionStateChanged.emit()
except KeyError:
pass # no output device by this device_id found in connection list.
@pyqtProperty(QObject , notify = printerConnectionStateChanged)
def connectedPrinterList(self):
self._printer_connections_model = ListModel()
self._printer_connections_model.addRoleName(Qt.UserRole + 1,"name")
self._printer_connections_model.addRoleName(Qt.UserRole + 2, "printer")
for connection in self._printer_connections:
if self._printer_connections[connection].connectionState == ConnectionState.CONNECTED:
self._printer_connections_model.appendItem({"name":connection, "printer": self._printer_connections[connection]})
return self._printer_connections_model
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed
def getSerialPortList(self, only_list_usb = False):
base_list = []
if platform.system() == "Windows":
import winreg
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
i = 0
while True:
values = winreg.EnumValue(key, i)
if not only_list_usb or "USBSER" in values[0]:
base_list += [values[1]]
i += 1
except Exception as e:
pass
else:
if only_list_usb:
base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.usb*")
base_list = filter(lambda s: "Bluetooth" not in s, base_list) # Filter because mac sometimes puts them in the list
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/*")
return list(base_list)
_instance = None

View file

@ -72,6 +72,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Current Z stage location # Current Z stage location
self._current_z = 0 self._current_z = 0
# Check if endstops are ever pressed (used for first run)
self._x_min_endstop_pressed = False self._x_min_endstop_pressed = False
self._y_min_endstop_pressed = False self._y_min_endstop_pressed = False
self._z_min_endstop_pressed = False self._z_min_endstop_pressed = False
@ -140,14 +141,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
for layer in gcode_list: for layer in gcode_list:
self._gcode.extend(layer.split("\n")) self._gcode.extend(layer.split("\n"))
#Reset line number. If this is not done, first line is sometimes ignored # Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110") self._gcode.insert(0, "M110")
self._gcode_position = 0 self._gcode_position = 0
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()
self.writeFinished.emit(self) self.writeFinished.emit(self)
@ -162,7 +163,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
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 function (threaded) that actually uploads the firmware.
def _updateFirmware(self): def _updateFirmware(self):
self.setProgress(0, 100) self.setProgress(0, 100)
@ -182,7 +183,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except Exception: except Exception:
pass pass
time.sleep(1) # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases. # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
time.sleep(1)
if not programmer.isConnected(): if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware") Logger.log("e", "Unable to connect with serial. Could not update firmware")
@ -238,7 +240,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except Exception as e: except Exception as e:
Logger.log("i", "Could not establish connection on %s, unknown reasons. Device is not arduino based." % self._serial_port) 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 useful, but it does give some debugging information. # If the programmer connected, we know its an atmega based version.
# Not all that useful, 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)
Logger.log("d","Attempting to connect to printer with serial %s on baud rate %s", self._serial_port, baud_rate) Logger.log("d","Attempting to connect to printer with serial %s on baud rate %s", self._serial_port, baud_rate)
if self._serial is None: if self._serial is None:
@ -259,7 +262,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
while timeout_time > time.time(): while timeout_time > time.time():
line = self._readline() line = self._readline()
if line is None: if line is None:
# Something went wrong with reading, could be that close was called. # Something went wrong with reading, could be that close was called.
self.setConnectionState(ConnectionState.CLOSED) self.setConnectionState(ConnectionState.CLOSED)
return return
@ -273,10 +276,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
Logger.log("i", "Established printer connection on port %s" % self._serial_port) Logger.log("i", "Established printer connection on port %s" % self._serial_port)
return return
self._sendCommand("M105") # Send M105 as long as we are listening, otherwise we end up in an undefined state self._sendCommand("M105") # Send M105 as long as we are listening, otherwise we end up in an undefined state
Logger.log("e", "Baud rate detection for %s failed", self._serial_port) Logger.log("e", "Baud rate detection for %s failed", self._serial_port)
self.close() # Unable to connect, wrap up. self.close() # Unable to connect, wrap up.
self.setConnectionState(ConnectionState.CLOSED) self.setConnectionState(ConnectionState.CLOSED)
## Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those. ## Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those.
@ -312,7 +315,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._listen_thread.daemon = True self._listen_thread.daemon = True
self._serial = None self._serial = None
## 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):
@ -395,7 +397,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
while self._connection_state == ConnectionState.CONNECTED: while self._connection_state == ConnectionState.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 time.time() > temperature_request_timeout: if time.time() > temperature_request_timeout:
if self._num_extruders > 0: if self._num_extruders > 0:
@ -408,8 +410,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if line.startswith(b"Error:"): if line.startswith(b"Error:"):
# Oh YEAH, consistency. # Oh YEAH, consistency.
# Marlin reports a MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # Marlin reports a 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 !!" # 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. # So we can have an extra newline in the most common case. Awesome work people.
if re.match(b"Error:[0-9]\n", line): if re.match(b"Error:[0-9]\n", line):
line = line.rstrip() + self._readline() line = line.rstrip() + self._readline()
@ -418,12 +420,12 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if not self.hasError(): if not self.hasError():
self._setErrorState(line[6:]) self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): #Temperature message elif b" T:" in line or line.startswith(b"T:"): # Temperature message
try: try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1))) self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
except: except:
pass pass
if b"B:" in line: # Check if it's a bed temperature if b"B:" in line: # Check if it's a bed temperature
try: try:
self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1))) self._setBedTemperature(float(re.search(b"B: *([0-9\.]*)", line).group(1)))
except Exception as e: except Exception as e:
@ -435,7 +437,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._is_printing: if self._is_printing:
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 (basically, send next command)
if b"ok" in line: if b"ok" in line:
ok_timeout = time.time() + 5 ok_timeout = time.time() + 5
@ -443,14 +445,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._sendCommand(self._command_queue.get()) self._sendCommand(self._command_queue.get())
else: else:
self._sendNextGcodeLine() self._sendNextGcodeLine()
elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs" elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs"
try: try:
self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1]) self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1])
except: except:
if b"rs" in line: if b"rs" in line:
self._gcode_position = int(line.split()[1]) self._gcode_position = int(line.split()[1])
else: # Request the temperature on comm timeout (every 2 seconds) when we are not printing.) else: # Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
if line == b"": if line == b"":
if self._num_extruders > 0: if self._num_extruders > 0:
self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders
@ -472,7 +474,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
line = line.strip() line = line.strip()
try: try:
if line == "M0" or line == "M1": 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. 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: if ("G0" in line or "G1" in line) and "Z" in line:
z = float(re.search("Z([0-9\.]*)", line).group(1)) z = float(re.search("Z([0-9\.]*)", line).group(1))
if self._current_z != z: if self._current_z != z:
@ -484,13 +486,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
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.progressChanged.emit()
## 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.progressChanged.emit()
## Cancel the current print. Printer connection wil continue to listen. ## Cancel the current print. Printer connection wil continue to listen.
@ -507,7 +509,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## 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 is not 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):
@ -516,7 +518,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
try: try:
ret = self._serial.readline() ret = self._serial.readline()
except Exception as e: except Exception as e:
Logger.log("e","Unexpected error while reading serial port. %s" %e) Logger.log("e", "Unexpected error while reading serial port. %s" % e)
self._setErrorState("Printer has been disconnected") self._setErrorState("Printer has been disconnected")
self.close() self.close()
return None return None
@ -530,7 +532,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _onFirmwareUpdateComplete(self): def _onFirmwareUpdateComplete(self):
self._update_firmware_thread.join() self._update_firmware_thread.join()
self._update_firmware_thread = threading.Thread(target= self._updateFirmware) self._update_firmware_thread = threading.Thread(target = self._updateFirmware)
self._update_firmware_thread.daemon = True self._update_firmware_thread.daemon = True
self.connect() self.connect()

View file

@ -1,7 +1,7 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the AGPLv3 or higher.
from . import USBPrinterManager from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -19,5 +19,5 @@ def getMetaData():
} }
def register(app): def register(app):
qmlRegisterSingletonType(USBPrinterManager.USBPrinterManager, "UM", 1, 0, "USBPrinterManager", USBPrinterManager.USBPrinterManager.getInstance) qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "UM", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"extension":USBPrinterManager.USBPrinterManager.getInstance(),"output_device": USBPrinterManager.USBPrinterManager.getInstance() } return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}