diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py new file mode 100644 index 0000000000..1a0a0c1d33 --- /dev/null +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -0,0 +1,247 @@ +# 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") + + +## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer. +class USBPrinterOutputDeviceManager(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._usb_output_devices = {} + self._usb_output_devices_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.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + + addUSBOutputDeviceSignal = Signal() + connectionStateChanged = pyqtSignal() + + progressChanged = pyqtSignal() + + @pyqtProperty(float, notify = progressChanged) + def progress(self): + progress = 0 + for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name" + progress += device.progress + + return progress / len(self._usb_output_devices) + + 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._usb_output_devices: + Message(i18n_catalog.i18nc("@info","Cannot update firmware, there were no connected printers found.")).show() + return + + self.spawnFirmwareInterface("") + for printer_connection in self._usb_output_devices: + try: + self._usb_output_devices[printer_connection].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName())) + except FileNotFoundError: + self._usb_output_devices[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._usb_output_devices: + self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort()) + try: + self._usb_output_devices[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 USBPrinterOutputDeviceManager._instance is None: + USBPrinterOutputDeviceManager._instance = cls() + + return USBPrinterOutputDeviceManager._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() + + ## Helper to identify serial ports (and scan for them) + 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.addUSBOutputDeviceSignal.emit(serial_port) # Hack to ensure its created in main thread + continue + self._serial_port_list = list(serial_ports) + + devices_to_remove = [] + for port, device in self._usb_output_devices.items(): + if port not in self._serial_port_list: + device.close() + devices_to_remove.append(port) + + for port in devices_to_remove: + del self._usb_output_devices[port] + + ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + def addOutputDevice(self, serial_port): + device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) + device.connect() + device.connectionStateChanged.connect(self._onConnectionStateChanged) + device.progressChanged.connect(self.progressChanged) + self._usb_output_devices[serial_port] = device + + ## If one of the states of the connected devices change, we might need to add / remove them from the global list. + def _onConnectionStateChanged(self, serial_port): + try: + if self._usb_output_devices[serial_port].connectionState == ConnectionState.CONNECTED: + self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port]) + else: + self.getOutputDeviceManager().removeOutputDevice(serial_port) + self.connectionStateChanged.emit() + except KeyError: + pass # no output device by this device_id found in connection list. + + + @pyqtProperty(QObject , notify = connectionStateChanged) + def connectedPrinterList(self): + self._usb_output_devices_model = ListModel() + self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name") + self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer") + for connection in self._usb_output_devices: + if self._usb_output_devices[connection].connectionState == ConnectionState.CONNECTED: + self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]}) + return self._usb_output_devices_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