USBPrinterOutputDeviceManager.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import threading
  4. import platform
  5. import time
  6. import serial.tools.list_ports
  7. from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
  8. from UM.Logger import Logger
  9. from UM.Resources import Resources
  10. from UM.Signal import Signal, signalemitter
  11. from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
  12. from UM.i18n import i18nCatalog
  13. from cura.PrinterOutputDevice import ConnectionState
  14. from cura.CuraApplication import CuraApplication
  15. from . import USBPrinterOutputDevice
  16. i18n_catalog = i18nCatalog("cura")
  17. ## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
  18. @signalemitter
  19. class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
  20. addUSBOutputDeviceSignal = Signal()
  21. progressChanged = pyqtSignal()
  22. def __init__(self, application, parent = None):
  23. if USBPrinterOutputDeviceManager.__instance is not None:
  24. raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
  25. USBPrinterOutputDeviceManager.__instance = self
  26. super().__init__(parent = parent)
  27. self._application = application
  28. self._serial_port_list = []
  29. self._usb_output_devices = {}
  30. self._usb_output_devices_model = None
  31. self._update_thread = threading.Thread(target = self._updateThread)
  32. self._update_thread.setDaemon(True)
  33. self._check_updates = True
  34. self._application.applicationShuttingDown.connect(self.stop)
  35. # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
  36. self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
  37. self._application.globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices)
  38. # The method updates/reset the USB settings for all connected USB devices
  39. def updateUSBPrinterOutputDevices(self):
  40. for key, device in self._usb_output_devices.items():
  41. if isinstance(device, USBPrinterOutputDevice.USBPrinterOutputDevice):
  42. device.resetDeviceSettings()
  43. def start(self):
  44. self._check_updates = True
  45. self._update_thread.start()
  46. def stop(self, store_data: bool = True):
  47. self._check_updates = False
  48. def _onConnectionStateChanged(self, serial_port):
  49. if serial_port not in self._usb_output_devices:
  50. return
  51. changed_device = self._usb_output_devices[serial_port]
  52. if changed_device.connectionState == ConnectionState.connected:
  53. self.getOutputDeviceManager().addOutputDevice(changed_device)
  54. else:
  55. self.getOutputDeviceManager().removeOutputDevice(serial_port)
  56. def _updateThread(self):
  57. while self._check_updates:
  58. container_stack = self._application.getGlobalContainerStack()
  59. if container_stack is None:
  60. time.sleep(5)
  61. continue
  62. port_list = [] # Just an empty list; all USB devices will be removed.
  63. if container_stack.getMetaDataEntry("supports_usb_connection"):
  64. machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")]
  65. if "text/x-gcode" in machine_file_formats:
  66. port_list = self.getSerialPortList(only_list_usb=True)
  67. self._addRemovePorts(port_list)
  68. time.sleep(5)
  69. @pyqtSlot(result = str)
  70. def getDefaultFirmwareName(self):
  71. # Check if there is a valid global container stack
  72. global_container_stack = self._application.getGlobalContainerStack()
  73. if not global_container_stack:
  74. Logger.log("e", "There is no global container stack. Can not update firmware.")
  75. self._firmware_view.close()
  76. return ""
  77. # The bottom of the containerstack is the machine definition
  78. machine_id = global_container_stack.getBottom().id
  79. machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value")
  80. if platform.system() == "Linux":
  81. baudrate = 115200
  82. else:
  83. baudrate = 250000
  84. # NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg.
  85. # https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2
  86. # The *.hex files are stored at a seperate repository:
  87. # https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware
  88. machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex",
  89. "bq_hephestos_2" : "MarlinHephestos2.hex",
  90. "ultimaker_original" : "MarlinUltimaker-{baudrate}.hex",
  91. "ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
  92. "ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex",
  93. "ultimaker2" : "MarlinUltimaker2.hex",
  94. "ultimaker2_go" : "MarlinUltimaker2go.hex",
  95. "ultimaker2_plus" : "MarlinUltimaker2plus.hex",
  96. "ultimaker2_extended" : "MarlinUltimaker2extended.hex",
  97. "ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
  98. }
  99. machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex",
  100. "ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex",
  101. }
  102. ##TODO: Add check for multiple extruders
  103. hex_file = None
  104. if machine_id in machine_without_extras.keys(): # The machine needs to be defined here!
  105. if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed:
  106. Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id)
  107. hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled
  108. else:
  109. Logger.log("d", "Choosing basic firmware for machine %s.", machine_id)
  110. hex_file = machine_without_extras[machine_id] # Return "basic" firmware
  111. else:
  112. Logger.log("w", "There is no firmware for machine %s.", machine_id)
  113. if hex_file:
  114. try:
  115. return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
  116. except FileNotFoundError:
  117. Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
  118. return ""
  119. else:
  120. Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
  121. return ""
  122. ## Helper to identify serial ports (and scan for them)
  123. def _addRemovePorts(self, serial_ports):
  124. # First, find and add all new or changed keys
  125. for serial_port in list(serial_ports):
  126. if serial_port not in self._serial_port_list:
  127. self.addUSBOutputDeviceSignal.emit(serial_port) # Hack to ensure its created in main thread
  128. continue
  129. self._serial_port_list = list(serial_ports)
  130. for port, device in self._usb_output_devices.items():
  131. if port not in self._serial_port_list:
  132. device.close()
  133. ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
  134. def addOutputDevice(self, serial_port):
  135. device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
  136. device.connectionStateChanged.connect(self._onConnectionStateChanged)
  137. self._usb_output_devices[serial_port] = device
  138. device.connect()
  139. ## Create a list of serial ports on the system.
  140. # \param only_list_usb If true, only usb ports are listed
  141. def getSerialPortList(self, only_list_usb = False):
  142. base_list = []
  143. for port in serial.tools.list_ports.comports():
  144. if not isinstance(port, tuple):
  145. port = (port.device, port.description, port.hwid)
  146. if only_list_usb and not port[2].startswith("USB"):
  147. continue
  148. base_list += [port[0]]
  149. return list(base_list)
  150. __instance = None
  151. @classmethod
  152. def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager":
  153. return cls.__instance