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()
def _createView(self): ## Load all scripts in the scripts folder self.loadAllScripts(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "scripts")) path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context)
def createChangelogWindow(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath("ChangeLogPlugin"), "ChangeLog.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._changelog_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._changelog_context.setContextProperty("manager", self) self._changelog_window = component.create(self._changelog_context)
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)
def _createCostView(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath( "PrintCostCalculator"), "PrintCostCalculator.qml")) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext( Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._cost_view = self._component.create(self._context)
class PauseBackend(QObject, Extension): def __init__(self, parent=None): super().__init__(parent=parent) self._additional_component = None self._additional_components_view = None Application.getInstance().engineCreatedSignal.connect( self._createAdditionalComponentsView) def _createAdditionalComponentsView(self): Logger.log( "d", "Creating additional ui components for Pause Backend plugin.") path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath( "PauseBackendPlugin"), "PauseBackend.qml")) self._additional_component = QQmlComponent( Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._additional_components_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._additional_components_context.setContextProperty("manager", self) self._additional_components_view = self._additional_component.create( self._additional_components_context) if not self._additional_components_view: Logger.log( "w", "Could not create additional components for Pause Backend plugin." ) return Application.getInstance().addAdditionalComponent( "saveButton", self._additional_components_view.findChild(QObject, "pauseResumeButton")) @pyqtSlot() def pauseBackend(self): backend = Application.getInstance().getBackend() backend._change_timer.timeout.disconnect(backend.slice) backend._terminate() backend.backendStateChange.emit(BackendState.Error) @pyqtSlot() def resumeBackend(self): backend = Application.getInstance().getBackend() backend._change_timer.timeout.connect(backend.slice) backend.forceSlice()
def createUserAgreementWindow(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "UserAgreement.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._user_agreement_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._user_agreement_context.setContextProperty("manager", self) self._user_agreement_window = component.create( self._user_agreement_context)
def _createConfigUI(self): if self._ui_view is None: Logger.log("d", "Creating ImageReader config UI") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._ui_context.setContextProperty("manager", self) self._ui_view = component.create(self._ui_context) self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint); self._disable_size_callbacks = False
def _createDialog(self): Logger.log("d", "PluginBrowser") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "PluginBrowser.qml")) self._qml_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("manager", self) self._dialog = self._qml_component.create(self._qml_context) if self._dialog is None: Logger.log("e", "QQmlComponent status %s", self._qml_component.status()) Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
def _createMonitorViewFromQML(self): path = QUrl.fromLocalFile(self._monitor_view_qml_path) # Because of garbage collection we need to keep this referenced by python. self._monitor_component = QQmlComponent(Application.getInstance()._engine, path) # Check if the context was already requested before (Printer output device might have multiple items in the future) if self._qml_context is None: self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("OutputDevice", self) self._monitor_item = self._monitor_component.create(self._qml_context) if self._monitor_item is None: Logger.log("e", "QQmlComponent status %s", self._monitor_component.status()) Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString())
def spawnPrintView(self): if self._print_view is None: path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._print_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._print_context.setContextProperty("OutputDevice", self) self._print_view = component.create(self._print_context) if component.isError(): Logger.log("e", " Errors creating component: \n%s", "\n".join( [e.toString() for e in component.errors()])) if self._print_view is not None: self._print_view.show()
class DialogHandler(QObject, Extension): def __init__(self, parent=None): super().__init__(parent) self._config_dialog = None self._tutorial_dialog = None self.addMenuItem(i18n_catalog.i18n("Configure"), self._openConfigDialog) self.addMenuItem( i18n_catalog.i18n("How to install Cura SolidWorks macro"), self._openTutorialDialog) def _openConfigDialog(self): if not self._config_dialog: self._config_dialog = self._createDialog("ConfigDialog.qml") self._config_dialog.show() def _openTutorialDialog(self): if not self._tutorial_dialog: self._tutorial_dialog = self._createDialog( "MacroTutorialDialog.qml") self._tutorial_dialog.show() def _createDialog(self, dialog_qml): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), dialog_qml)) self._qml_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._qml_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("manager", self) dialog = self._qml_component.create(self._qml_context) if dialog is None: Logger.log("e", "QQmlComponent status %s", self._qml_component.status()) Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString()) return dialog @pyqtSlot() def openMacroAndIconDirectory(self): plugin_dir = os.path.join(PluginRegistry.getInstance().getPluginPath( self.getPluginId())) macro_dir = os.path.join(plugin_dir, "macro") os.system("explorer.exe \"%s\"" % macro_dir)
def _createViewFromQML(self): path = QUrl.fromLocalFile( os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context)
def createChangelogWindow(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._changelog_context.setContextProperty("manager", self) self._changelog_window = component.create(self._changelog_context)
def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for OctoPrint-connected printers.") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("OctoPrintPlugin"), "OctoPrintComponents.qml")) self._additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._additional_components_context.setContextProperty("manager", self) self._additional_components_view = self._additional_component.create(self._additional_components_context) if not self._additional_components_view: Logger.log("w", "Could not create additional components for OctoPrint-connected printers.") return Application.getInstance().addAdditionalComponent("monitorButtons", self._additional_components_view.findChild(QObject, "openOctoPrintButton"))
def _createDialog(self, qml): path = QUrl.fromLocalFile( os.path.join(os.path.dirname(os.path.abspath(__file__)), qml)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext( Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) dialog = self._component.create(self._context) if dialog is None: Logger.log("e", "QQmlComponent status %s", self._component.status()) Logger.log("e", "QQmlComponent errorString %s", self._component.errorString()) raise RuntimeError(self._component.errorString()) return dialog
def __init__(self, app): super().__init__() self.app = app selection = libdice.dice.languages1(app, self, QUrl) self.languagerelevant() context = self.rootContext() self.scrollmodel = model2.PersonModel() context.setContextProperty("scrollmodel", self.scrollmodel) #self.libdice_strlist = [self.tr('lin'), self.tr('log'), self.tr('root'), self.tr('poly'), self.tr('exp'), self.tr('kombi'), self.tr('logistic'), self.tr('rand'), self.tr('gewicht'), self.tr('add'), self.tr('mul'), self.tr("Wuerfelwurf: "),self.tr(" (Wuerfelaugen ")] blub = [self.tr('test')] #print(str(blub[0])) #print(blub) #print(str(libdice.dice.randfkt2.values())) self.load(':/main.qml') langimg = self.rootObjects()[0].findChild(QObject, "langimg") # print("UrL: "+str(selection[1].fileName())) #langimg.setProperty("source",(":/"+selection[1].fileName())) langimg.setProperty("source", selection[1]) #rado = self.rootObjects()[0].findChild(QObject, "radios") #rado.setProperty("onClicked", self.radu() ) #layout = QVBoxLayout() #layout.addWidget(QPushButton('Top')) #layout.addWidget(QPushButton('Bottom')) context = QQmlContext(self.rootContext())
def _createViewFromQML(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext( Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
def run(): directory = os.path.dirname(os.path.abspath(__file__)) appIcon = QIcon() appIcon.addFile(os.path.join(directory, "python.png"), QSize(64, 64)) app.setWindowIcon(appIcon) from . import registerQmlTypes registerQmlTypes() engine = QQmlApplicationEngine() context = QQmlContext(engine) mainQml = QUrl.fromLocalFile(os.path.join(directory, "Main.qml")) component = QQmlComponent(engine) component.loadUrl(mainQml) if component.isError(): for error in component.errors(): print("Error: ", error.toString()) dialog = component.create(context) return app.exec_()
class UserAgreement(QObject, Extension): def __init__(self, parent=None): super(UserAgreement, self).__init__() self._user_agreement_window = None self._user_agreement_context = None Application.getInstance().engineCreatedSignal.connect( self._onEngineCreated) Preferences.getInstance().addPreference( "general/accepted_user_agreement", False) def _onEngineCreated(self): if not Preferences.getInstance().getValue( "general/accepted_user_agreement"): self.showUserAgreement() def showUserAgreement(self): if not self._user_agreement_window: self.createUserAgreementWindow() self._user_agreement_window.show() @pyqtSlot(bool) def didAgree(self, userChoice): if userChoice: Logger.log("i", "User agreed to the user agreement") Preferences.getInstance().setValue( "general/accepted_user_agreement", True) self._user_agreement_window.hide() else: Logger.log("i", "User did NOT agree to the user agreement") Preferences.getInstance().setValue( "general/accepted_user_agreement", False) CuraApplication.getInstance().quit() CuraApplication.getInstance().setNeedToShowUserAgreement(False) def createUserAgreementWindow(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "UserAgreement.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._user_agreement_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._user_agreement_context.setContextProperty("manager", self) self._user_agreement_window = component.create( self._user_agreement_context)
def createQmlComponent(self, qml_file_path: str, context_properties: Dict[str, "QObject"]=None) -> Optional["QObject"]: path = QUrl.fromLocalFile(qml_file_path) component = QQmlComponent(self._engine, path) result_context = QQmlContext(self._engine.rootContext()) if context_properties is not None: for name, value in context_properties.items(): result_context.setContextProperty(name, value) result = component.create(result_context) for err in component.errors(): Logger.log("e", str(err.toString())) if result is None: return None # We need to store the context with the qml object, else the context gets garbage collected and the qml objects # no longer function correctly/application crashes. result.attached_context = result_context return result
def spawnFirmwareInterface(self, serial_port): if self._firmware_view is None: path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("Doodle3D"), "SettingsWindow.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()
def createControlInterface(self): if self._control_view is None: Logger.log("d", "Creating control interface for printer connection") 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)
def _createViewFromQML(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
def _createConfigUI(self): if self._ui_view is None: Logger.log('d', 'Creating SVGReader CDT UI') path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath('SVGReader'), 'CDTUI.qml')) component = QQmlComponent(Application.getInstance()._qml_engine, path) self._ui_context = QQmlContext( Application.getInstance()._qml_engine.rootContext()) self._ui_context.setContextProperty('manager', self) self._ui_view = component.create(self._ui_context) self._ui_view.setFlags(self._ui_view.flags() & ~(Qt.WindowCloseButtonHint) & ~(Qt.WindowMinimizeButtonHint) & ~(Qt.WindowMaximizeButtonHint)) self._disable_size_callbacks = False
def _createDialog(self, dialog_qml, directory=None): if directory is None: directory = PluginRegistry.getInstance().getPluginPath( self.getPluginId()) path = QUrl.fromLocalFile(os.path.join(directory, dialog_qml)) self._qml_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._qml_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("manager", self) dialog = self._qml_component.create(self._qml_context) if dialog is None: Logger.log("e", "QQmlComponent status %s", self._qml_component.status()) Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString()) return dialog
class SolidWorksUiCommons(): def _createDialog(self, dialog_qml, directory=None): if directory is None: directory = PluginRegistry.getInstance().getPluginPath( self.getPluginId()) path = QUrl.fromLocalFile(os.path.join(directory, dialog_qml)) self._qml_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._qml_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("manager", self) dialog = self._qml_component.create(self._qml_context) if dialog is None: Logger.log("e", "QQmlComponent status %s", self._qml_component.status()) Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString()) return dialog @pyqtSlot(int, result=bool) def isVersionOperational(self, major_version): return major_version in self.reader.operational_versions @pyqtSlot(int, str, result=bool) def getTechnicalInfoPerVersion(self, revision, name): return bool(self.reader.technical_infos_per_version[revision][name]) @pyqtSlot(result=list) def getVersionsList(self): versions = list(self.reader.technical_infos_per_version.keys()) versions.sort() versions.reverse() return versions @pyqtSlot(result=int) def getVersionsCount(self): return int(len(list(self.reader.technical_infos_per_version.keys()))) @pyqtSlot(int, result=str) def getFriendlyName(self, major_revision): return self.reader.getFriendlyName(major_revision)
def _createView(self): Logger.log("d", "Creating post processing plugin view.") ## Load all scripts in the scripts folder try: self.loadAllScripts(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "scripts")) except Exception as e: print("Exception occured", e) # TODO: Debug code (far to general catch. Remove this once done testing) path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")) self._component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) Logger.log("d", "Post processing view created.") Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
def createQmlComponent(self, qml_file_path: str, context_properties: Dict[str, "QObject"] = None) -> Optional["QObject"]: if self._qml_engine is None: # Protect in case the engine was not initialized yet return None path = QUrl.fromLocalFile(qml_file_path) component = QQmlComponent(self._qml_engine, path) result_context = QQmlContext(self._qml_engine.rootContext()) #type: ignore #MyPy doens't realise that self._qml_engine can't be None here. if context_properties is not None: for name, value in context_properties.items(): result_context.setContextProperty(name, value) result = component.create(result_context) for err in component.errors(): Logger.log("e", str(err.toString())) if result is None: return None # We need to store the context with the qml object, else the context gets garbage collected and the qml objects # no longer function correctly/application crashes. result.attached_context = result_context return result
def _createDialogue(self): #Create a QML component from the Hello.qml file. qml_file = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "SelectionInfo.qml")) component = QQmlComponent(Application.getInstance()._engine, qml_file) qml_context = QQmlContext( Application.getInstance()._engine.rootContext( )) #List the QML component as subcomponent of the main window. return component.create(qml_context)
class PauseBackend(QObject, Extension): def __init__(self, parent = None): super().__init__(parent = parent) self._additional_component = None self._additional_components_view = None Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for Pause Backend plugin.") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PauseBackendPlugin"), "PauseBackend.qml")) self._additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._additional_components_context.setContextProperty("manager", self) self._additional_components_view = self._additional_component.create(self._additional_components_context) if not self._additional_components_view: Logger.log("w", "Could not create additional components for Pause Backend plugin.") return Application.getInstance().addAdditionalComponent("saveButton", self._additional_components_view.findChild(QObject, "pauseResumeButton")) @pyqtSlot() def pauseBackend(self): backend = Application.getInstance().getBackend() backend._change_timer.timeout.disconnect(backend.slice) backend._terminate() backend.backendStateChange.emit(BackendState.Error) @pyqtSlot() def resumeBackend(self): backend = Application.getInstance().getBackend() backend._change_timer.timeout.connect(backend.slice) backend.forceSlice()
def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for Pause Backend plugin.") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PauseBackendPlugin"), "PauseBackend.qml")) self._additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._additional_components_context.setContextProperty("manager", self) self._additional_components_view = self._additional_component.create(self._additional_components_context) if not self._additional_components_view: Logger.log("w", "Could not create additional components for Pause Backend plugin.") return Application.getInstance().addAdditionalComponent("saveButton", self._additional_components_view.findChild(QObject, "pauseResumeButton"))
def _createView(self): Logger.log("d", "Creating post processing plugin view.") ## Load all scripts in the scripts folder try: self.loadAllScripts(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "scripts")) except Exception as e: print("Exception occured", e) # TODO: Debug code (far to general catch. Remove this once done testing) path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")) self._component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) Logger.log("d", "Post processing view created.")
def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") path = QUrl.fromLocalFile( os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml") ) self.__additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self.__additional_components_context.setContextProperty("manager", self) self.__additional_components_view = self.__additional_component.create(self.__additional_components_context) if not self.__additional_components_view: Logger.log("w", "Could not create ui components for UM3.") return Application.getInstance().addAdditionalComponent( "monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton") ) Application.getInstance().addAdditionalComponent( "machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo") )
class ImageReaderUI(QObject): show_config_ui_trigger = pyqtSignal() def __init__(self, image_reader): super(ImageReaderUI, self).__init__() self.image_reader = image_reader self._ui_view = None self.show_config_ui_trigger.connect(self._actualShowConfigUI) self.defaultWidth = 120 self.defaultDepth = 120 self._aspect = 1 self._width = self.defaultWidth self._depth = self.defaultDepth self.base_height = 1 self.peak_height = 10 self.smoothing = 1 self.image_color_invert = False; self._ui_lock = threading.Lock() self._cancelled = False self._disable_size_callbacks = False def setWidthAndDepth(self, width, depth): self._aspect = width / depth self._width = width self._depth = depth def getWidth(self): return self._width def getDepth(self): return self._depth def getCancelled(self): return self._cancelled def waitForUIToClose(self): self._ui_lock.acquire() self._ui_lock.release() def showConfigUI(self): self._ui_lock.acquire() self._cancelled = False self.show_config_ui_trigger.emit() def _actualShowConfigUI(self): self._disable_size_callbacks = True if self._ui_view is None: self._createConfigUI() self._ui_view.show() self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width)) self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth)) self._disable_size_callbacks = False self._ui_view.findChild(QObject, "Base_Height").setProperty("text", str(self.base_height)) self._ui_view.findChild(QObject, "Peak_Height").setProperty("text", str(self.peak_height)) self._ui_view.findChild(QObject, "Smoothing").setProperty("value", self.smoothing) def _createConfigUI(self): if self._ui_view is None: Logger.log("d", "Creating ImageReader config UI") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._ui_context.setContextProperty("manager", self) self._ui_view = component.create(self._ui_context) self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint); self._disable_size_callbacks = False @pyqtSlot() def onOkButtonClicked(self): self._cancelled = False self._ui_view.close() self._ui_lock.release() @pyqtSlot() def onCancelButtonClicked(self): self._cancelled = True self._ui_view.close() self._ui_lock.release() @pyqtSlot(str) def onWidthChanged(self, value): if self._ui_view and not self._disable_size_callbacks: if len(value) > 0: self._width = float(value) else: self._width = 0 self._depth = self._width / self._aspect self._disable_size_callbacks = True self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth)) self._disable_size_callbacks = False @pyqtSlot(str) def onDepthChanged(self, value): if self._ui_view and not self._disable_size_callbacks: if len(value) > 0: self._depth = float(value) else: self._depth = 0 self._width = self._depth * self._aspect self._disable_size_callbacks = True self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width)) self._disable_size_callbacks = False @pyqtSlot(str) def onBaseHeightChanged(self, value): if (len(value) > 0): self.base_height = float(value) else: self.base_height = 0 @pyqtSlot(str) def onPeakHeightChanged(self, value): if (len(value) > 0): self.peak_height = float(value) else: self.peak_height = 0 @pyqtSlot(float) def onSmoothingChanged(self, value): self.smoothing = int(value) @pyqtSlot(int) def onImageColorInvertChanged(self, value): if (value == 1): self.image_color_invert = True else: self.image_color_invert = False
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") 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 self._end_stop_thread = threading.Thread(target = self._pollEndStop) self._end_stop_thread.deamon = True self._poll_endstop = -1 # 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 = 3 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.daemon = 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 = [] # 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 self._x_min_endstop_pressed = False self._y_min_endstop_pressed = False self._z_min_endstop_pressed = False self._x_max_endstop_pressed = False self._y_max_endstop_pressed = False self._z_max_endstop_pressed = False # 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 self._control_view = None onError = pyqtSignal() progressChanged = pyqtSignal() extruderTemperatureChanged = pyqtSignal() bedTemperatureChanged = pyqtSignal() firmwareUpdateComplete = pyqtSignal() endstopStateChanged = pyqtSignal(str ,bool, arguments = ["key","state"]) @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 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 @pyqtSlot() def startPrint(self): self.writeStarted.emit(self) gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.printGCode(gcode_list) ## 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: Logger.log("d", "Printer is busy or not connected, aborting print") self.writeError.emit(self) return self._gcode.clear() for layer in gcode_list: self._gcode.extend(layer.split("\n")) #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() self.writeFinished.emit(self) ## 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() self.setProgress(100, 100) self.firmwareUpdateComplete.emit() ## Upload new firmware to machine # \param filename full path of firmware file to be uploaded def updateFirmware(self, file_name): Logger.log("i", "Updating firmware of %s using %s", self._serial_port, file_name) self._firmware_file_name = file_name self._update_firmware_thread.start() @pyqtSlot() def startPollEndstop(self): if self._poll_endstop == -1: self._poll_endstop = True self._end_stop_thread.start() @pyqtSlot() def stopPollEndstop(self): self._poll_endstop = False def _pollEndStop(self): while self._is_connected and self._poll_endstop: self.sendCommand("M119") time.sleep(0.5) ## Private connect function run by thread. Can be started by calling connect. def _connect(self): Logger.log("d", "Attempting to connect to %s", self._serial_port) 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) 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: 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) continue 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 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._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) 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 else: Logger.log("w", "Printer connection state was not changed") connectionStateChanged = Signal() ## Close the printer connection def close(self): Logger.log("d", "Closing the printer connection.") if self._connect_thread.isAlive(): try: self._connect_thread.join() except Exception as e: pass # This should work, but it does fail sometimes for some reason self._connect_thread = threading.Thread(target=self._connect) self._connect_thread.daemon = True if self._serial is not None: self.setIsConnected(False) try: self._listen_thread.join() except: pass self._serial.close() self._listen_thread = threading.Thread(target=self._listen) self._listen_thread.daemon = True self._serial = None def isConnected(self): return self._is_connected @pyqtSlot(int) def heatupNozzle(self, temperature): Logger.log("d", "Setting nozzle temperature to %s", temperature) self._sendCommand("M104 S%s" % temperature) @pyqtSlot(int) def heatupBed(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) @pyqtSlot("long", "long","long") def moveHead(self, x, y, z): Logger.log("d","Moving head to %s, %s , %s", x, y, z) self._sendCommand("G0 X%s Y%s Z%s"%(x,y,z)) @pyqtSlot() def homeHead(self): self._sendCommand("G28") ## 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 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() def createControlInterface(self): if self._control_view is None: Logger.log("d", "Creating control interface for printer connection") 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. # \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() ## 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.extruderTemperatureChanged.emit() except Exception as e: pass ## 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.bedTemperatureChanged.emit() def requestWrite(self, node, file_name = None): self.showControlInterface() def _setEndstopState(self, endstop_key, value): if endstop_key == b'x_min': if self._x_min_endstop_pressed != value: self.endstopStateChanged.emit('x_min', value) self._x_min_endstop_pressed = value elif endstop_key == b'y_min': if self._y_min_endstop_pressed != value: self.endstopStateChanged.emit('y_min', value) self._y_min_endstop_pressed = value elif endstop_key == b'z_min': if self._z_min_endstop_pressed != value: self.endstopStateChanged.emit('z_min', value) self._z_min_endstop_pressed = value ## 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 time.time() > temperature_request_timeout: 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.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 elif b"_min" in line or b"_max" in line: tag, value = line.split(b':', 1) self._setEndstopState(tag,(b'H' in value or b'TRIGGERED' in value)) if self._is_printing: 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() ## 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() ## Cancel the current print. Printer connection wil continue to listen. @pyqtSlot() def cancelPrint(self): self._gcode_position = 0 self.setProgress(0) self._gcode = [] # Turn of temperatures self._sendCommand("M140 S0") self._sendCommand("M104 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 = [115200, 250000, 230400, 57600, 38400, 19200, 9600] return ret def _onFirmwareUpdateComplete(self): self._update_firmware_thread.join() self._update_firmware_thread = threading.Thread(target= self._updateFirmware) self._update_firmware_thread.daemon = True self.connect()
class Doodle3D(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 self.updatetrigger = False # Add menu item to top menu of the application. self.setMenuName(i18n_catalog.i18nc("@title:menu", "Doodle3D")) self.addMenuItem(i18n_catalog.i18nc("@item:inlistbox", "Enable Scan devices..."), 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() settingChanged = pyqtSignal() """ 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.updatetrigger==True: result = self.getSerialPortList() #Logger.log("d","Connected Boxes: %s" % result) thereturn = self._addRemovePorts(result) if thereturn == False: self.updatetrigger=False break 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("Doodle3D"), "SettingsWindow.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() def updateAllFirmware(self): self.updatetrigger = True try: self._update_thread.start() except RuntimeError: Logger.log("d","[Doodle3D] Thread already started") """ 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: 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 Doodle3D._instance is None: Doodle3D._instance = cls() return Doodle3D._instance def _getDefaultFirmwareName(self): machine_type = Application.getInstance().getMachineManager().getActiveMachineInstance().getMachineDefinition().getId() firmware_name = "" baudrate = 250000 if sys.platform.startswith("linux"): baudrate = 115200 if machine_type == "ultimaker_original": firmware_name = "MarlinUltimaker" firmware_name += "-%d" % (baudrate) elif machine_type == "ultimaker_original_plus": firmware_name = "MarlinUltimaker-UMOP-%d" % (baudrate) elif machine_type == "Witbox": return "MarlinWitbox.hex" elif machine_type == "ultimaker2go": return "MarlinUltimaker2go.hex" elif machine_type == "ultimaker2extended": return "MarlinUltimaker2extended.hex" elif machine_type == "ultimaker2": return "MarlinUltimaker2.hex" # TODO: Add check for multiple extruders if firmware_name != "": firmware_name += ".hex" return firmware_name def _addRemovePorts(self, serial_ports): # First, find and add all new or changed keys if serial_ports == None: return False for boxIP, boxIDENT in serial_ports.items(): if boxIP not in self._serial_port_list: self.addConnectionSignal.emit(boxIP,boxIDENT) continue self._serial_port_list = serial_ports # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, serial_port, wifiboxid): connection = PrinterConnection.PrinterConnection(serial_port, wifiboxid) connection.connect() connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) self._printer_connections[serial_port] = connection def _onPrinterConnectionStateChanged(self, serial_port): if self._printer_connections[serial_port].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printer_connections[serial_port]) else: self.getOutputDeviceManager().removeOutputDevice(serial_port) self.printerConnectionStateChanged.emit() @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].isConnected(): 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. def getSerialPortList(self): base_list = {} # Get response from api/list.php and retrieve local ip # from each individual boxes found on the local network boxesListResponse = self.get("connect.doodle3d.com", "/api/list.php") if (boxesListResponse == False): return boxes = boxesListResponse['data'] for index in range(len(boxes)): box = boxes[index] # Check if the boxes are alive try: self.get(box['localip'], "/d3dapi/network/alive") except: # Run this exception for the boxes that aren't alive (anymore) if box['localip'] in self._printer_connections: self._printer_connections[box['localip']]._is_connected = False self._printer_connections[box['localip']].close() del self._printer_connections[box['localip']] self.getOutputDeviceManager().removeOutputDevice(box['localip']) else: pass else: # Boxes that are alive will be formed together into the base_list base_list[box['localip']] = box['wifiboxid'] return base_list # Takes Domain and Path and returns decoded JSON response back def get(self, domain, path): try: #print('get: ', domain, path) connect = http.client.HTTPConnection(domain) connect.request("GET", path) response = connect.getresponse() #print(' response: ', response.status, response.reason) jsonresponse = response.read() #print(' ', jsonresponse) return json.loads(jsonresponse.decode()) except Exception as e: pass return False _instance = None
class MachineAction(QObject, PluginObject): ## Create a new Machine action. # \param key unique key of the machine action # \param label Human readable label used to identify the machine action. def __init__(self, key, label=""): super().__init__() self._key = key self._label = label self._qml_url = "" self._component = None self._context = None self._view = None self._finished = False labelChanged = pyqtSignal() onFinished = pyqtSignal() def getKey(self): return self._key @pyqtProperty(str, notify=labelChanged) def label(self): return self._label def setLabel(self, label): if self._label != label: self._label = label self.labelChanged.emit() ## Reset the action to it's default state. # This should not be re-implemented by child classes, instead re-implement _reset. # /sa _reset @pyqtSlot() def reset(self): self._component = None self._finished = False self._reset() ## Protected implementation of reset. # /sa reset() def _reset(self): pass @pyqtSlot() def setFinished(self): self._finished = True self._reset() self.onFinished.emit() @pyqtProperty(bool, notify=onFinished) def finished(self): return self._finished ## Protected helper to create a view object based on provided QML. def _createViewFromQML(self): path = QUrl.fromLocalFile( os.path.join( PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext( Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) @pyqtProperty(QObject, constant=True) def displayItem(self): if not self._component: self._createViewFromQML() return self._view
class MachineAction(QObject, PluginObject): ## Create a new Machine action. # \param key unique key of the machine action # \param label Human readable label used to identify the machine action. def __init__(self, key, label = ""): super().__init__() self._key = key self._label = label self._qml_url = "" self._component = None self._context = None self._view = None self._finished = False labelChanged = pyqtSignal() onFinished = pyqtSignal() def getKey(self): return self._key @pyqtProperty(str, notify = labelChanged) def label(self): return self._label def setLabel(self, label): if self._label != label: self._label = label self.labelChanged.emit() ## Reset the action to it's default state. # This should not be re-implemented by child classes, instead re-implement _reset. # /sa _reset @pyqtSlot() def reset(self): self._component = None self._finished = False self._reset() ## Protected implementation of reset. # /sa reset() def _reset(self): pass @pyqtSlot() def setFinished(self): self._finished = True self._reset() self.onFinished.emit() @pyqtProperty(bool, notify = onFinished) def finished(self): return self._finished ## Protected helper to create a view object based on provided QML. def _createViewFromQML(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) @pyqtProperty(QObject, constant = True) def displayItem(self): if not self._component: self._createViewFromQML() return self._view
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 = PrinterConnection.PrinterConnection(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): if self._printer_connections[serial_port].isConnected(): self.getOutputDeviceManager().addOutputDevice( self._printer_connections[serial_port]) else: self.getOutputDeviceManager().removeOutputDevice(serial_port) self.printerConnectionStateChanged.emit() @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].isConnected(): 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
class DiscoverUM3Action(MachineAction): def __init__(self): super().__init__("DiscoverUM3Action", catalog.i18nc("@action", "Connect via Network")) self._qml_url = "DiscoverUM3Action.qml" self._network_plugin = None self.__additional_components_context = None self.__additional_component = None self.__additional_components_view = None Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) self._last_zeroconf_event_time = time.time() self._zeroconf_change_grace_period = ( 0.25 ) # Time to wait after a zeroconf service change before allowing a zeroconf reset printersChanged = pyqtSignal() @pyqtSlot() def startDiscovery(self): if not self._network_plugin: self._network_plugin = ( Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") ) self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() ## Re-filters the list of printers. @pyqtSlot() def reset(self): self.printersChanged.emit() @pyqtSlot() def restartDiscovery(self): # Ensure that there is a bit of time after a printer has been discovered. # This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often. # It's most likely that the QML engine is still creating delegates, where the python side already deleted or # garbage collected the data. # Whatever the case, waiting a bit ensures that it doesn't crash. if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period: if not self._network_plugin: self.startDiscovery() else: self._network_plugin.startDiscovery() @pyqtSlot(str, str) def removeManualPrinter(self, key, address): if not self._network_plugin: return self._network_plugin.removeManualPrinter(key, address) @pyqtSlot(str, str) def setManualPrinter(self, key, address): if key != "": # This manual printer replaces a current manual printer self._network_plugin.removeManualPrinter(key) if address != "": self._network_plugin.addManualPrinter(address) def _onPrinterDiscoveryChanged(self, *args): self._last_zeroconf_event_time = time.time() self.printersChanged.emit() @pyqtProperty("QVariantList", notify=printersChanged) def foundDevices(self): if self._network_plugin: if Application.getInstance().getGlobalContainerStack(): global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() else: global_printer_type = "unknown" printers = list(self._network_plugin.getPrinters().values()) # TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet. printers = [ printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown" ] printers.sort(key=lambda k: k.name) return printers else: return [] @pyqtSlot(str) def setKey(self, key): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: global_container_stack.setMetaDataEntry("um_network_key", key) # Delete old authentication data. global_container_stack.removeMetaDataEntry("network_authentication_id") global_container_stack.removeMetaDataEntry("network_authentication_key") else: global_container_stack.addMetaDataEntry("um_network_key", key) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() @pyqtSlot(result=str) def getStoredKey(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: return global_container_stack.getMetaDataEntry("um_network_key") return "" @pyqtSlot() def loadConfigurationFromPrinter(self): machine_manager = Application.getInstance().getMachineManager() hotend_ids = machine_manager.printerOutputDevices[0].hotendIds for index in range(len(hotend_ids)): machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index]) material_ids = machine_manager.printerOutputDevices[0].materialIds for index in range(len(material_ids)): machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index]) def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") path = QUrl.fromLocalFile( os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml") ) self.__additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self.__additional_components_context.setContextProperty("manager", self) self.__additional_components_view = self.__additional_component.create(self.__additional_components_context) if not self.__additional_components_view: Logger.log("w", "Could not create ui components for UM3.") return Application.getInstance().addAdditionalComponent( "monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton") ) Application.getInstance().addAdditionalComponent( "machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo") )
def load(self, path: str, is_first_call: bool = True) -> None: if path == self._path: return with open(os.path.join(path, "theme.json"), encoding = "utf-8") as f: Logger.log("d", "Loading theme file: %s", os.path.join(path, "theme.json")) data = json.load(f) # Iteratively load inherited themes try: theme_id = data["metadata"]["inherits"] self.load(Resources.getPath(Resources.Themes, theme_id), is_first_call = False) except FileNotFoundError: Logger.log("e", "Could not find inherited theme %s", theme_id) except KeyError: pass # No metadata or no inherits keyword in the theme.json file if "colors" in data: for name, color in data["colors"].items(): c = QColor(color[0], color[1], color[2], color[3]) self._colors[name] = c fonts_dir = os.path.join(path, "fonts") if os.path.isdir(fonts_dir): for file in os.listdir(fonts_dir): if "ttf" in file: QFontDatabase.addApplicationFont(os.path.join(fonts_dir, file)) if "fonts" in data: system_font_size = QCoreApplication.instance().font().pointSize() for name, font in data["fonts"].items(): q_font = QFont() q_font.setFamily(font.get("family", QCoreApplication.instance().font().family())) if font.get("bold"): q_font.setBold(font.get("bold", False)) else: q_font.setWeight(font.get("weight", 50)) q_font.setLetterSpacing(QFont.AbsoluteSpacing, font.get("letterSpacing", 0)) q_font.setItalic(font.get("italic", False)) q_font.setPointSize(int(font.get("size", 1) * system_font_size)) q_font.setCapitalization(QFont.AllUppercase if font.get("capitalize", False) else QFont.MixedCase) self._fonts[name] = q_font if "sizes" in data: for name, size in data["sizes"].items(): s = QSizeF() s.setWidth(round(size[0] * self._em_width)) s.setHeight(round(size[1] * self._em_height)) self._sizes[name] = s iconsdir = os.path.join(path, "icons") if os.path.isdir(iconsdir): for icon in os.listdir(iconsdir): name = os.path.splitext(icon)[0] self._icons[name] = QUrl.fromLocalFile(os.path.join(iconsdir, icon)) imagesdir = os.path.join(path, "images") if os.path.isdir(imagesdir): for image in os.listdir(imagesdir): name = os.path.splitext(image)[0] self._images[name] = QUrl.fromLocalFile(os.path.join(imagesdir, image)) styles = os.path.join(path, "styles.qml") if os.path.isfile(styles): c = QQmlComponent(self._engine, styles) context = QQmlContext(self._engine, self._engine) context.setContextProperty("Theme", self) self._styles = c.create(context) if c.isError(): for error in c.errors(): Logger.log("e", error.toString()) Logger.log("d", "Loaded theme %s", path) self._path = path # only emit the theme loaded signal once after all the themes in the inheritance chain have been loaded if is_first_call: self.themeLoaded.emit()
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): def __init__(self, parent = None): super().__init__(parent = parent) 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): # Check if there is a valid global container stack global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: Logger.log("e", "There is no global container stack. Can not update firmware.") self._firmware_view.close() return "" # The bottom of the containerstack is the machine definition machine_id = global_container_stack.getBottom().id machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value") 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", "bq_hephestos_2" : "MarlinHephestos2.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_id in machine_without_extras.keys(): # The machine needs to be defined here! if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed: Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id) hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled else: Logger.log("d", "Choosing basic firmware for machine %s.", machine_id) hex_file = machine_without_extras[machine_id] # Return "basic" firmware else: Logger.log("e", "There is no firmware for machine %s.", machine_id) if hex_file: return hex_file.format(baudrate=baudrate) else: Logger.log("e", "Could not find any firmware for machine %s.", machine_id) 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.connectionStateChanged.connect(self._onConnectionStateChanged) device.connect() 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 #@UnresolvedImport 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
class MachineAction(QObject, PluginObject): def __init__(self, key, label = ""): super().__init__() self._key = key self._label = label self._qml_url = "" self._component = None self._context = None self._view = None self._finished = False labelChanged = pyqtSignal() onFinished = pyqtSignal() def getKey(self): return self._key @pyqtProperty(str, notify = labelChanged) def label(self): return self._label def setLabel(self, label): if self._label != label: self._label = label self.labelChanged.emit() ## Reset the action to it's default state. # This should not be re-implemented by child classes, instead re-implement _reset. # /sa _reset @pyqtSlot() def reset(self): self._finished = False self._reset() ## Protected implementation of reset. # /sa reset() def _reset(self): pass @pyqtSlot() def setFinished(self): self._finished = True self._reset() self.onFinished.emit() @pyqtProperty(bool, notify = onFinished) def finished(self): return self._finished def _createViewFromQML(self): path = QUrl.fromLocalFile( os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) @pyqtProperty(QObject, constant = True) def displayItem(self): if not self._component: self._createViewFromQML() return self._view
class PrinterOutputDevice(QObject, OutputDevice): def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) self._container_registry = ContainerRegistry.getInstance() self._target_bed_temperature = 0 self._bed_temperature = 0 self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._material_ids = [""] * self._num_extruders self._hotend_ids = [""] * self._num_extruders self._progress = 0 self._head_x = 0 self._head_y = 0 self._head_z = 0 self._connection_state = ConnectionState.closed self._connection_text = "" self._time_elapsed = 0 self._time_total = 0 self._job_state = "" self._job_name = "" self._error_text = "" self._accepts_commands = True self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds. self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still. self._preheat_bed_timer.setSingleShot(True) self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed) self._printer_state = "" self._printer_type = "unknown" self._camera_active = False self._monitor_view_qml_path = "" self._monitor_component = None self._monitor_item = None self._qml_context = None def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): raise NotImplementedError("requestWrite needs to be implemented") ## Signals # Signal to be emitted when bed temp is changed bedTemperatureChanged = pyqtSignal() # Signal to be emitted when target bed temp is changed targetBedTemperatureChanged = pyqtSignal() # Signal when the progress is changed (usually when this output device is printing / sending lots of data) progressChanged = pyqtSignal() # Signal to be emitted when hotend temp is changed hotendTemperaturesChanged = pyqtSignal() # Signal to be emitted when target hotend temp is changed targetHotendTemperaturesChanged = pyqtSignal() # Signal to be emitted when head position is changed (x,y,z) headPositionChanged = pyqtSignal() # Signal to be emitted when either of the material ids is changed materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"]) # Signal to be emitted when either of the hotend ids is changed hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"]) # Signal that is emitted every time connection state is changed. # it also sends it's own device_id (for convenience sake) connectionStateChanged = pyqtSignal(str) connectionTextChanged = pyqtSignal() timeElapsedChanged = pyqtSignal() timeTotalChanged = pyqtSignal() jobStateChanged = pyqtSignal() jobNameChanged = pyqtSignal() errorTextChanged = pyqtSignal() acceptsCommandsChanged = pyqtSignal() printerStateChanged = pyqtSignal() printerTypeChanged = pyqtSignal() # Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally). preheatBedRemainingTimeChanged = pyqtSignal() @pyqtProperty(QObject, constant=True) def monitorItem(self): # Note that we specifically only check if the monitor component is created. # It could be that it failed to actually create the qml item! If we check if the item was created, it will try to # create the item (and fail) every time. if not self._monitor_component: self._createMonitorViewFromQML() return self._monitor_item def _createMonitorViewFromQML(self): path = QUrl.fromLocalFile(self._monitor_view_qml_path) # Because of garbage collection we need to keep this referenced by python. self._monitor_component = QQmlComponent(Application.getInstance()._engine, path) # Check if the context was already requested before (Printer output device might have multiple items in the future) if self._qml_context is None: self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._qml_context.setContextProperty("OutputDevice", self) self._monitor_item = self._monitor_component.create(self._qml_context) if self._monitor_item is None: Logger.log("e", "QQmlComponent status %s", self._monitor_component.status()) Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString()) @pyqtProperty(str, notify=printerTypeChanged) def printerType(self): return self._printer_type @pyqtProperty(str, notify=printerStateChanged) def printerState(self): return self._printer_state @pyqtProperty(str, notify = jobStateChanged) def jobState(self): return self._job_state def _updatePrinterType(self, printer_type): if self._printer_type != printer_type: self._printer_type = printer_type self.printerTypeChanged.emit() def _updatePrinterState(self, printer_state): if self._printer_state != printer_state: self._printer_state = printer_state self.printerStateChanged.emit() def _updateJobState(self, job_state): if self._job_state != job_state: self._job_state = job_state self.jobStateChanged.emit() @pyqtSlot(str) def setJobState(self, job_state): self._setJobState(job_state) def _setJobState(self, job_state): Logger.log("w", "_setJobState is not implemented by this output device") @pyqtSlot() def startCamera(self): self._camera_active = True self._startCamera() def _startCamera(self): Logger.log("w", "_startCamera is not implemented by this output device") @pyqtSlot() def stopCamera(self): self._camera_active = False self._stopCamera() def _stopCamera(self): Logger.log("w", "_stopCamera is not implemented by this output device") @pyqtProperty(str, notify = jobNameChanged) def jobName(self): return self._job_name def setJobName(self, name): if self._job_name != name: self._job_name = name self.jobNameChanged.emit() ## Gives a human-readable address where the device can be found. @pyqtProperty(str, constant = True) def address(self): Logger.log("w", "address is not implemented by this output device.") ## A human-readable name for the device. @pyqtProperty(str, constant = True) def name(self): Logger.log("w", "name is not implemented by this output device.") return "" @pyqtProperty(str, notify = errorTextChanged) def errorText(self): return self._error_text ## Set the error-text that is shown in the print monitor in case of an error def setErrorText(self, error_text): if self._error_text != error_text: self._error_text = error_text self.errorTextChanged.emit() @pyqtProperty(bool, notify = acceptsCommandsChanged) def acceptsCommands(self): return self._accepts_commands ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def setAcceptsCommands(self, accepts_commands): if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands self.acceptsCommandsChanged.emit() ## Get the bed temperature of the bed (if any) # This function is "final" (do not re-implement) # /sa _getBedTemperature implementation function @pyqtProperty(float, notify = bedTemperatureChanged) def bedTemperature(self): return self._bed_temperature ## Set the (target) bed temperature # This function is "final" (do not re-implement) # /param temperature new target temperature of the bed (in deg C) # /sa _setTargetBedTemperature implementation function @pyqtSlot(int) def setTargetBedTemperature(self, temperature): self._setTargetBedTemperature(temperature) if self._target_bed_temperature != temperature: self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() ## The total duration of the time-out to pre-heat the bed, in seconds. # # \return The duration of the time-out to pre-heat the bed, in seconds. @pyqtProperty(int, constant = True) def preheatBedTimeout(self): return self._preheat_bed_timeout ## The remaining duration of the pre-heating of the bed. # # This is formatted in M:SS format. # \return The duration of the time-out to pre-heat the bed, formatted. @pyqtProperty(str, notify = preheatBedRemainingTimeChanged) def preheatBedRemainingTime(self): if not self._preheat_bed_timer.isActive(): return "" period = self._preheat_bed_timer.remainingTime() if period <= 0: return "" minutes, period = divmod(period, 60000) #60000 milliseconds in a minute. seconds, _ = divmod(period, 1000) #1000 milliseconds in a second. if minutes <= 0 and seconds <= 0: return "" return "%d:%02d" % (minutes, seconds) ## Time the print has been printing. # Note that timeTotal - timeElapsed should give time remaining. @pyqtProperty(float, notify = timeElapsedChanged) def timeElapsed(self): return self._time_elapsed ## Total time of the print # Note that timeTotal - timeElapsed should give time remaining. @pyqtProperty(float, notify=timeTotalChanged) def timeTotal(self): return self._time_total @pyqtSlot(float) def setTimeTotal(self, new_total): if self._time_total != new_total: self._time_total = new_total self.timeTotalChanged.emit() @pyqtSlot(float) def setTimeElapsed(self, time_elapsed): if self._time_elapsed != time_elapsed: self._time_elapsed = time_elapsed self.timeElapsedChanged.emit() ## Home the head of the connected printer # This function is "final" (do not re-implement) # /sa _homeHead implementation function @pyqtSlot() def homeHead(self): self._homeHead() ## Home the head of the connected printer # This is an implementation function and should be overriden by children. def _homeHead(self): Logger.log("w", "_homeHead is not implemented by this output device") ## Home the bed of the connected printer # This function is "final" (do not re-implement) # /sa _homeBed implementation function @pyqtSlot() def homeBed(self): self._homeBed() ## Home the bed of the connected printer # This is an implementation function and should be overriden by children. # /sa homeBed def _homeBed(self): Logger.log("w", "_homeBed is not implemented by this output device") ## Protected setter for the bed temperature of the connected printer (if any). # /parameter temperature Temperature bed needs to go to (in deg celsius) # /sa setTargetBedTemperature def _setTargetBedTemperature(self, temperature): Logger.log("w", "_setTargetBedTemperature is not implemented by this output device") ## Pre-heats the heated bed of the printer. # # \param temperature The temperature to heat the bed to, in degrees # Celsius. # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature, duration): Logger.log("w", "preheatBed is not implemented by this output device.") ## Cancels pre-heating the heated bed of the printer. # # If the bed is not pre-heated, nothing happens. @pyqtSlot() def cancelPreheatBed(self): Logger.log("w", "cancelPreheatBed is not implemented by this output device.") ## Protected setter for the current bed temperature. # This simply sets the bed temperature, but ensures that a signal is emitted. # /param temperature temperature of the bed. def _setBedTemperature(self, temperature): if self._bed_temperature != temperature: self._bed_temperature = temperature self.bedTemperatureChanged.emit() ## Get the target bed temperature if connected printer (if any) @pyqtProperty(int, notify = targetBedTemperatureChanged) def targetBedTemperature(self): return self._target_bed_temperature ## Set the (target) hotend temperature # This function is "final" (do not re-implement) # /param index the index of the hotend that needs to change temperature # /param temperature The temperature it needs to change to (in deg celsius). # /sa _setTargetHotendTemperature implementation function @pyqtSlot(int, int) def setTargetHotendTemperature(self, index, temperature): self._setTargetHotendTemperature(index, temperature) if self._target_hotend_temperatures[index] != temperature: self._target_hotend_temperatures[index] = temperature self.targetHotendTemperaturesChanged.emit() ## Implementation function of setTargetHotendTemperature. # /param index Index of the hotend to set the temperature of # /param temperature Temperature to set the hotend to (in deg C) # /sa setTargetHotendTemperature def _setTargetHotendTemperature(self, index, temperature): Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device") @pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged) def targetHotendTemperatures(self): return self._target_hotend_temperatures @pyqtProperty("QVariantList", notify = hotendTemperaturesChanged) def hotendTemperatures(self): return self._hotend_temperatures ## Protected setter for the current hotend temperature. # This simply sets the hotend temperature, but ensures that a signal is emitted. # /param index Index of the hotend # /param temperature temperature of the hotend (in deg C) def _setHotendTemperature(self, index, temperature): if self._hotend_temperatures[index] != temperature: self._hotend_temperatures[index] = temperature self.hotendTemperaturesChanged.emit() @pyqtProperty("QVariantList", notify = materialIdChanged) def materialIds(self): return self._material_ids @pyqtProperty("QVariantList", notify = materialIdChanged) def materialNames(self): result = [] for material_id in self._material_ids: if material_id is None: result.append(i18n_catalog.i18nc("@item:material", "No material loaded")) continue containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) if containers: result.append(containers[0].getName()) else: result.append(i18n_catalog.i18nc("@item:material", "Unknown material")) return result ## List of the colours of the currently loaded materials. # # The list is in order of extruders. If there is no material in an # extruder, the colour is shown as transparent. # # The colours are returned in hex-format AARRGGBB or RRGGBB # (e.g. #800000ff for transparent blue or #00ff00 for pure green). @pyqtProperty("QVariantList", notify = materialIdChanged) def materialColors(self): result = [] for material_id in self._material_ids: if material_id is None: result.append("#00000000") #No material. continue containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) if containers: result.append(containers[0].getMetaDataEntry("color_code")) else: result.append("#00000000") #Unknown material. return result ## Protected setter for the current material id. # /param index Index of the extruder # /param material_id id of the material def _setMaterialId(self, index, material_id): if material_id and material_id != "" and material_id != self._material_ids[index]: Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id)) self._material_ids[index] = material_id self.materialIdChanged.emit(index, material_id) @pyqtProperty("QVariantList", notify = hotendIdChanged) def hotendIds(self): return self._hotend_ids ## Protected setter for the current hotend id. # /param index Index of the extruder # /param hotend_id id of the hotend def _setHotendId(self, index, hotend_id): if hotend_id and hotend_id != self._hotend_ids[index]: Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id)) self._hotend_ids[index] = hotend_id self.hotendIdChanged.emit(index, hotend_id) elif not hotend_id: Logger.log("d", "Removing hotend id of hotend %d.", index) self._hotend_ids[index] = None self.hotendIdChanged.emit(index, None) ## Let the user decide if the hotends and/or material should be synced with the printer # NB: the UX needs to be implemented by the plugin def materialHotendChangedMessage(self, callback): Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'") callback(QMessageBox.Yes) ## Attempt to establish connection def connect(self): raise NotImplementedError("connect needs to be implemented") ## Attempt to close the connection def close(self): raise NotImplementedError("close needs to be implemented") @pyqtProperty(bool, notify = connectionStateChanged) def connectionState(self): return self._connection_state ## Set the connection state of this output device. # /param connection_state ConnectionState enum. def setConnectionState(self, connection_state): if self._connection_state != connection_state: self._connection_state = connection_state self.connectionStateChanged.emit(self._id) @pyqtProperty(str, notify = connectionTextChanged) def connectionText(self): return self._connection_text ## Set a text that is shown on top of the print monitor tab def setConnectionText(self, connection_text): if self._connection_text != connection_text: self._connection_text = connection_text self.connectionTextChanged.emit() ## Ensure that close gets called when object is destroyed def __del__(self): self.close() ## Get the x position of the head. # This function is "final" (do not re-implement) @pyqtProperty(float, notify = headPositionChanged) def headX(self): return self._head_x ## Get the y position of the head. # This function is "final" (do not re-implement) @pyqtProperty(float, notify = headPositionChanged) def headY(self): return self._head_y ## Get the z position of the head. # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. # This function is "final" (do not re-implement) @pyqtProperty(float, notify = headPositionChanged) def headZ(self): return self._head_z ## Update the saved position of the head # This function should be called when a new position for the head is received. def _updateHeadPosition(self, x, y ,z): position_changed = False if self._head_x != x: self._head_x = x position_changed = True if self._head_y != y: self._head_y = y position_changed = True if self._head_z != z: self._head_z = z position_changed = True if position_changed: self.headPositionChanged.emit() ## Set the position of the head. # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. # This function is "final" (do not re-implement) # /param x new x location of the head. # /param y new y location of the head. # /param z new z location of the head. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadPosition implementation function @pyqtSlot("long", "long", "long") @pyqtSlot("long", "long", "long", "long") def setHeadPosition(self, x, y, z, speed = 3000): self._setHeadPosition(x, y , z, speed) ## Set the X position of the head. # This function is "final" (do not re-implement) # /param x x position head needs to move to. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadx implementation function @pyqtSlot("long") @pyqtSlot("long", "long") def setHeadX(self, x, speed = 3000): self._setHeadX(x, speed) ## Set the Y position of the head. # This function is "final" (do not re-implement) # /param y y position head needs to move to. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadY implementation function @pyqtSlot("long") @pyqtSlot("long", "long") def setHeadY(self, y, speed = 3000): self._setHeadY(y, speed) ## Set the Z position of the head. # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. # This function is "final" (do not re-implement) # /param z z position head needs to move to. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadZ implementation function @pyqtSlot("long") @pyqtSlot("long", "long") def setHeadZ(self, z, speed = 3000): self._setHeadY(z, speed) ## Move the head of the printer. # Note that this is a relative move. If you want to move the head to a specific position you can use # setHeadPosition # This function is "final" (do not re-implement) # /param x distance in x to move # /param y distance in y to move # /param z distance in z to move # /param speed Speed by which it needs to move (in mm/minute) # /sa _moveHead implementation function @pyqtSlot("long", "long", "long") @pyqtSlot("long", "long", "long", "long") def moveHead(self, x = 0, y = 0, z = 0, speed = 3000): self._moveHead(x, y, z, speed) ## Implementation function of moveHead. # /param x distance in x to move # /param y distance in y to move # /param z distance in z to move # /param speed Speed by which it needs to move (in mm/minute) # /sa moveHead def _moveHead(self, x, y, z, speed): Logger.log("w", "_moveHead is not implemented by this output device") ## Implementation function of setHeadPosition. # /param x new x location of the head. # /param y new y location of the head. # /param z new z location of the head. # /param speed Speed by which it needs to move (in mm/minute) # /sa setHeadPosition def _setHeadPosition(self, x, y, z, speed): Logger.log("w", "_setHeadPosition is not implemented by this output device") ## Implementation function of setHeadX. # /param x new x location of the head. # /param speed Speed by which it needs to move (in mm/minute) # /sa setHeadX def _setHeadX(self, x, speed): Logger.log("w", "_setHeadX is not implemented by this output device") ## Implementation function of setHeadY. # /param y new y location of the head. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadY def _setHeadY(self, y, speed): Logger.log("w", "_setHeadY is not implemented by this output device") ## Implementation function of setHeadZ. # /param z new z location of the head. # /param speed Speed by which it needs to move (in mm/minute) # /sa _setHeadZ def _setHeadZ(self, z, speed): Logger.log("w", "_setHeadZ is not implemented by this output device") ## Get the progress of any currently active process. # This function is "final" (do not re-implement) # /sa _getProgress # /returns float progress of the process. -1 indicates that there is no process. @pyqtProperty(float, notify = progressChanged) def progress(self): return self._progress ## Set the progress of any currently active process # /param progress Progress of the process. def setProgress(self, progress): if self._progress != progress: self._progress = progress self.progressChanged.emit()
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 name, connection in self._printer_connections.items(): 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() baudrate = 250000 if sys.platform.startswith("linux"): baudrate = 115200 if machine_type == "ultimaker_original": firmware_name = "MarlinUltimaker" if machine_instance.getMachineSettingValue("machine_heated_bed"): #Has heated bed upgrade kit? firmware_name += "-HBK" firmware_name += "-%d" % (baudrate) return firmware_name + ".hex" elif machine_type == "ultimaker_original_plus": firmware_name = "MarlinUltimaker-UMOP-%d" % (baudrate) return firmware_name + ".hex" elif machine_type == "bq_witbox": return "MarlinWitbox.hex" elif machine_type == "ultimaker2_go": return "MarlinUltimaker2go.hex" elif machine_type == "ultimaker2_extended": return "MarlinUltimaker2extended.hex" elif machine_type == "ultimaker2": return "MarlinUltimaker2.hex" elif machine_type == "ultimaker2plus": return "MarlinUltimaker2plus.hex" elif machine_type == "ultimaker2_extended_plus": return "MarlinUltimaker2extended-plus.hex" else: Logger.log("e", "I don't know of any firmware for machine %s.", machine_type) raise FileNotFoundError() ##TODO: Add check for multiple extruders 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 = PrinterConnection.PrinterConnection(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): if self._printer_connections[serial_port].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printer_connections[serial_port]) else: self.getOutputDeviceManager().removeOutputDevice(serial_port) self.printerConnectionStateChanged.emit() @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].isConnected(): 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
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): def __init__(self, parent = None): super().__init__(parent = parent) 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 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() firmwareUpdateChange = 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) @pyqtProperty(int, notify = progressChanged) def errorCode(self): for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name" if device._error_code: return device._error_code return 0 ## Return True if all printers finished firmware update @pyqtProperty(float, notify = firmwareUpdateChange) def firmwareUpdateCompleteStatus(self): complete = True for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name" if not device.firmwareUpdateFinished: complete = False return complete def start(self): self._check_updates = True self._update_thread.start() def stop(self): self._check_updates = False 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(str) def updateAllFirmware(self, file_name): if file_name.startswith("file://"): file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want if not self._usb_output_devices: Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected.")).show() return for printer_connection in self._usb_output_devices: self._usb_output_devices[printer_connection].resetFirmwareUpdate() self.spawnFirmwareInterface("") for printer_connection in self._usb_output_devices: try: self._usb_output_devices[printer_connection].updateFirmware(file_name) except FileNotFoundError: # Should only happen in dev environments where the resources/firmware folder is absent. self._usb_output_devices[printer_connection].setProgress(100, 100) Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name) Message(i18n_catalog.i18nc("@info", "Could not find firmware required for the printer at %s.") % printer_connection).show() self._firmware_view.close() continue @pyqtSlot(str, str, result = bool) def updateFirmwareBySerial(self, serial_port, file_name): 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(file_name) except FileNotFoundError: self._firmware_view.close() Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name) 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 @pyqtSlot(result = str) def getDefaultFirmwareName(self): # Check if there is a valid global container stack global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: Logger.log("e", "There is no global container stack. Can not update firmware.") self._firmware_view.close() return "" # The bottom of the containerstack is the machine definition machine_id = global_container_stack.getBottom().id machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value") 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", "bq_hephestos_2" : "MarlinHephestos2.hex", "ultimaker_original" : "MarlinUltimaker-{baudrate}.hex", "ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex", "ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex", "ultimaker2" : "MarlinUltimaker2.hex", "ultimaker2_go" : "MarlinUltimaker2go.hex", "ultimaker2_plus" : "MarlinUltimaker2plus.hex", "ultimaker2_extended" : "MarlinUltimaker2extended.hex", "ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex", } machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex", "ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex", } ##TODO: Add check for multiple extruders hex_file = None if machine_id in machine_without_extras.keys(): # The machine needs to be defined here! if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed: Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id) hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled else: Logger.log("d", "Choosing basic firmware for machine %s.", machine_id) hex_file = machine_without_extras[machine_id] # Return "basic" firmware else: Logger.log("w", "There is no firmware for machine %s.", machine_id) if hex_file: return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate)) else: Logger.log("w", "Could not find any firmware for machine %s.", machine_id) return "" ## 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.connectionStateChanged.connect(self._onConnectionStateChanged) device.connect() device.progressChanged.connect(self.progressChanged) device.firmwareUpdateChange.connect(self.firmwareUpdateChange) 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: Logger.log("w", "Connection state of %s changed, but it was not found in the 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 = [] for port in serial.tools.list_ports.comports(): if not isinstance(port, tuple): port = (port.device, port.description, port.hwid) if only_list_usb and not port[2].startswith("USB"): continue base_list += [port[0]] return list(base_list) _instance = None # type: "USBPrinterOutputDeviceManager"
class WorkspaceDialog(QObject): showDialogSignal = pyqtSignal() def __init__(self, parent = None): super().__init__(parent) self._component = None self._context = None self._view = None self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() self._default_strategy = "override" self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, "material": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) self._has_quality_changes_conflict = False self._has_machine_conflict = False self._has_material_conflict = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() materialConflictChanged = pyqtSignal() @pyqtProperty(bool, notify = machineConflictChanged) def machineConflict(self): return self._has_machine_conflict @pyqtProperty(bool, notify=qualityChangesConflictChanged) def qualityChangesConflict(self): return self._has_quality_changes_conflict @pyqtProperty(bool, notify=materialConflictChanged) def materialConflict(self): return self._has_material_conflict @pyqtSlot(str, str) def setResolveStrategy(self, key, strategy): if key in self._result: self._result[key] = strategy def setMaterialConflict(self, material_conflict): self._has_material_conflict = material_conflict self.materialConflictChanged.emit() def setMachineConflict(self, machine_conflict): self._has_machine_conflict = machine_conflict self.machineConflictChanged.emit() def setQualityChangesConflict(self, quality_changes_conflict): self._has_quality_changes_conflict = quality_changes_conflict self.qualityChangesConflictChanged.emit() def getResult(self): if "machine" in self._result and not self._has_machine_conflict: self._result["machine"] = None if "quality_changes" in self._result and not self._has_quality_changes_conflict: self._result["quality_changes"] = None if "material" in self._result and not self._has_material_conflict: self._result["material"] = None return self._result def _createViewFromQML(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. if threading.current_thread() != threading.main_thread(): self._lock.acquire() # Reset the result self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, "material": self._default_strategy} self._visible = True self.showDialogSignal.emit() @pyqtSlot() ## Used to notify the dialog so the lock can be released. def notifyClosed(self): if self._result is None: self._result = {} self._lock.release() def hide(self): self._visible = False self._lock.release() self._view.hide() @pyqtSlot() def onOkButtonClicked(self): self._view.hide() self.hide() @pyqtSlot() def onCancelButtonClicked(self): self._view.hide() self.hide() self._result = {} ## Block thread until the dialog is closed. def waitForClose(self): if self._visible: if threading.current_thread() != threading.main_thread(): self._lock.acquire() self._lock.release() else: # If this is not run from a separate thread, we need to ensure that the events are still processed. while self._visible: time.sleep(1 / 50) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. def __show(self): if self._view is None: self._createViewFromQML() if self._view: self._view.show()
class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice): printJobsChanged = pyqtSignal() printersChanged = pyqtSignal() selectedPrinterChanged = pyqtSignal() def __init__(self, key, address, properties, api_prefix): super().__init__(key, address, properties, api_prefix) # Store the address of the master. self._master_address = address name_property = properties.get(b"name", b"") if name_property: name = name_property.decode("utf-8") else: name = key self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated self.setName(name) description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network") self.setShortDescription(description) self.setDescription(description) self._stage = OutputStage.ready host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "") if host_override: Logger.log( "w", "Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host", host_override) self._host = "http://" + host_override else: self._host = "http://" + address # is the same as in NetworkPrinterOutputDevicePlugin self._cluster_api_version = "1" self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" self._api_base_uri = self._host + self._cluster_api_prefix self._file_name = None self._progress_message = None self._request = None self._reply = None # The main reason to keep the 'multipart' form data on the object # is to prevent the Python GC from claiming it too early. self._multipart = None self._print_view = None self._request_job = [] self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") self._print_jobs = [] self._print_job_by_printer_uuid = {} self._print_job_by_uuid = {} # Print jobs by their own uuid self._printers = [] self._printers_dict = {} # by unique_name self._connected_printers_type_count = [] self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection self._selected_printer = self._automatic_printer self._cluster_status_update_timer = QTimer() self._cluster_status_update_timer.setInterval(5000) self._cluster_status_update_timer.setSingleShot(False) self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus) self._can_pause = True self._can_abort = True self._can_pre_heat_bed = False self._can_control_manually = False self._cluster_size = int(properties.get(b"cluster_size", 0)) self._cleanupRequest() #These are texts that are to be translated for future features. temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.") temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3) temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished. temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed. ## No authentication, so requestAuthentication should do exactly nothing @pyqtSlot() def requestAuthentication(self, message_id = None, action_id = "Retry"): pass # Cura Connect doesn't do any authorization def setAuthenticationState(self, auth_state): self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated def _verifyAuthentication(self): pass def _checkAuthentication(self): Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done") @pyqtProperty(QObject, notify=selectedPrinterChanged) def controlItem(self): # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. if not self._control_component: self._createControlViewFromQML() name = self._selected_printer.get("friendly_name") if name == self._automatic_printer.get("friendly_name") or name == "": return self._control_item # Let cura use the default. return None @pyqtSlot(int, result = str) def getTimeCompleted(self, time_remaining): current_time = time.time() datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute) @pyqtSlot(int, result = str) def getDateCompleted(self, time_remaining): current_time = time.time() datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() @pyqtProperty(int, constant = True) def clusterSize(self): return self._cluster_size @pyqtProperty(str, notify=selectedPrinterChanged) def name(self): # Show the name of the selected printer. # This is not the nicest way to do this, but changes to the Cura UI are required otherwise. name = self._selected_printer.get("friendly_name") if name != self._automatic_printer.get("friendly_name"): return name # Return name of cluster master. return self._properties.get(b"name", b"").decode("utf-8") def connect(self): super().connect() self._cluster_status_update_timer.start() def close(self): super().close() self._cluster_status_update_timer.stop() def _setJobState(self, job_state): if not self._selected_printer: return selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"] if selected_printer_uuid not in self._print_job_by_printer_uuid: return print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"] url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action") put_request = QNetworkRequest(url) put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = '{"action": "' + job_state + '"}' self._manager.put(put_request, data.encode()) def _requestClusterStatus(self): # TODO: Handle timeout. We probably want to know if the cluster is still reachable or not. url = QUrl(self._api_base_uri + "printers/") printers_request = QNetworkRequest(url) self._addUserAgentHeader(printers_request) self._manager.get(printers_request) # See _finishedPrintersRequest() if self._printers: # if printers is not empty url = QUrl(self._api_base_uri + "print_jobs/") print_jobs_request = QNetworkRequest(url) self._addUserAgentHeader(print_jobs_request) self._manager.get(print_jobs_request) # See _finishedPrintJobsRequest() def _finishedPrintJobsRequest(self, reply): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid print job state message: Not valid JSON.") return self.setPrintJobs(json_data) def _finishedPrintersRequest(self, reply): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid print job state message: Not valid JSON.") return self.setPrinters(json_data) def materialHotendChangedMessage(self, callback): # When there is just one printer, the activate configuration option is enabled if (self._cluster_size == 1): super().materialHotendChangedMessage(callback = callback) def _startCameraStream(self): ## Request new image url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream") self._image_request = QNetworkRequest(url) self._addUserAgentHeader(self._image_request) self._image_reply = self._manager.get(self._image_request) self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) def spawnPrintView(self): if self._print_view is None: path = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._print_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._print_context.setContextProperty("OutputDevice", self) self._print_view = component.create(self._print_context) if component.isError(): Logger.log("e", " Errors creating component: \n%s", "\n".join( [e.toString() for e in component.errors()])) if self._print_view is not None: self._print_view.show() ## Store job info, show Print view for settings def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self._selected_printer = self._automatic_printer # reset to default option self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] if self._stage != OutputStage.ready: if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() return if len(self._printers) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. self.selectAutomaticPrinter() self.sendPrintJob() else: # Cluster has no printers, warn the user of this. if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers.")) self._error_message.show() ## Actually send the print job, called from the dialog # :param: require_printer_name: name of printer, or "" @pyqtSlot() def sendPrintJob(self): nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job require_printer_name = self._selected_printer["unique_name"] self._send_gcode_start = time.time() Logger.log("d", "Sending print job [%s] to host..." % file_name) if self._stage != OutputStage.ready: Logger.log("d", "Unable to send print job as the state is %s", self._stage) raise OutputDeviceError.DeviceBusyError() self._stage = OutputStage.uploading self._file_name = "%s.gcode.gz" % file_name self._showProgressMessage() new_request = self._buildSendPrintJobHttpRequest(require_printer_name) if new_request is None or self._stage != OutputStage.uploading: return self._request = new_request self._reply = self._manager.post(self._request, self._multipart) self._reply.uploadProgress.connect(self._onUploadProgress) # See _finishedPostPrintJobRequest() def _buildSendPrintJobHttpRequest(self, require_printer_name): api_url = QUrl(self._api_base_uri + "print_jobs/") request = QNetworkRequest(api_url) # Create multipart request and add the g-code. self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) # Add gcode part = QHttpPart() part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file"; filename="%s"' % self._file_name) gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") compressed_gcode = self._compressGcode(gcode) if compressed_gcode is None: return None # User aborted print, so stop trying. part.setBody(compressed_gcode) self._multipart.append(part) # require_printer_name "" means automatic if require_printer_name: self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name)) user_name = self.__get_username() if user_name is None: user_name = "unknown" self._multipart.append(self.__createKeyValueHttpPart("owner", user_name)) self._addUserAgentHeader(request) return request def _compressGcode(self, gcode): self._compressing_print = True batched_line = "" max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB byte_array_file_data = b"" def _compressDataAndNotifyQt(data_to_append): compressed_data = gzip.compress(data_to_append.encode("utf-8")) self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used. QCoreApplication.processEvents() # Ensure that the GUI does not freeze. # Pretend that this is a response, as zipping might take a bit of time. self._last_response_time = time.time() return compressed_data if gcode is None: Logger.log("e", "Unable to find sliced gcode, returning empty.") return byte_array_file_data for line in gcode: if not self._compressing_print: self._progress_message.hide() return None # Stop trying to zip, abort was called. batched_line += line # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file. # Compressing line by line in this case is extremely slow, so we need to batch them. if len(batched_line) < max_chars_per_line: continue byte_array_file_data += _compressDataAndNotifyQt(batched_line) batched_line = "" # Also compress the leftovers. if batched_line: byte_array_file_data += _compressDataAndNotifyQt(batched_line) return byte_array_file_data def __createKeyValueHttpPart(self, key, value): metadata_part = QHttpPart() metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain') metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key)) metadata_part.setBody(bytearray(value, "utf8")) return metadata_part def __get_username(self): try: return getpass.getuser() except: Logger.log("d", "Could not get the system user name, returning 'unknown' instead.") return None def _finishedPrintJobPostRequest(self, reply): self._stage = OutputStage.ready if self._progress_message: self._progress_message.hide() self._progress_message = None self.writeFinished.emit(self) if reply.error(): self._showRequestFailedMessage(reply) self.writeError.emit(self) else: self._showRequestSucceededMessage() self.writeSuccess.emit(self) self._cleanupRequest() def _showRequestFailedMessage(self, reply): if reply is not None: Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format( cluster_name = self.getName(), error_string = str(reply.errorString()), error = str(reply.error()))) error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.") message = Message(text=error_message_template.format( cluster_name = self.getName())) message.show() def _showRequestSucceededMessage(self): confirmation_message_template = i18n_catalog.i18nc( "@info:status", "Sent {file_name} to group {cluster_name}." ) file_name = os.path.basename(self._file_name).split(".")[0] message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name) message = Message(text=message_text) button_text = i18n_catalog.i18nc("@action:button", "Show print jobs") button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.") message.addAction("open_browser", button_text, "globe", button_tooltip) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() def setPrintJobs(self, print_jobs): #TODO: hack, last seen messes up the check, so drop it. for job in print_jobs: del job["last_seen"] # Strip any extensions job["name"] = self._removeGcodeExtension(job["name"]) if self._print_jobs != print_jobs: old_print_jobs = self._print_jobs self._print_jobs = print_jobs self._notifyFinishedPrintJobs(old_print_jobs, print_jobs) self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs) # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer # for some reason. ugh. self._print_job_by_printer_uuid = {} self._print_job_by_uuid = {} for print_job in print_jobs: if "printer_uuid" in print_job and print_job["printer_uuid"] is not None: self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job self._print_job_by_uuid[print_job["uuid"]] = print_job self.printJobsChanged.emit() def _removeGcodeExtension(self, name): parts = name.split(".") if parts[-1].upper() == "GZ": parts = parts[:-1] if parts[-1].upper() == "GCODE": parts = parts[:-1] return ".".join(parts) def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs): """Notify the user when any of their print jobs have just completed. Arguments: old_print_jobs -- the previous list of print job status information as returned by the cluster REST API. new_print_jobs -- the current list of print job status information as returned by the cluster REST API. """ if old_print_jobs is None: return username = self.__get_username() if username is None: return our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs) our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"] our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs) our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"] old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs]) for print_job in our_new_finished_print_jobs: if print_job["uuid"] in old_not_finished_print_job_uuids: printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"]) if printer_name is None: printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown") message_text = (i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.") .format(printer_name=printer_name, job_name=print_job["name"])) message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished")) Application.getInstance().showMessage(message) Application.getInstance().showToastMessage( i18n_catalog.i18nc("@info:status", "Print finished"), message_text) def __filterOurPrintJobs(self, print_jobs): username = self.__get_username() return [print_job for print_job in print_jobs if print_job["owner"] == username] def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs): if old_print_jobs is None: return old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs)) new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs)) old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs]) for print_job in new_change_required_print_jobs: if print_job["uuid"] not in old_change_required_print_job_uuids: printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"]) if printer_name is None: # don't report on yet unknown printers continue message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") .format(printer_name=printer_name, job_name=print_job["name"])) message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required")) Application.getInstance().showMessage(message) Application.getInstance().showToastMessage( i18n_catalog.i18nc("@label:status", "Action required"), message_text) def __filterConfigChangePrintJobs(self, print_jobs): return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs) def __isConfigurationChangeRequiredPrintJob(self, print_job): if print_job["status"] == "queued": changes_required = print_job.get("configuration_changes_required", []) return len(changes_required) != 0 return False def __getPrinterNameFromUuid(self, printer_uuid): for printer in self._printers: if printer["uuid"] == printer_uuid: return printer["friendly_name"] return None def setPrinters(self, printers): if self._printers != printers: self._connected_printers_type_count = [] printers_count = {} self._printers = printers self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name for printer in printers: variant = printer["machine_variant"] if variant in printers_count: printers_count[variant] += 1 else: printers_count[variant] = 1 for type in printers_count: self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]}) self.printersChanged.emit() @pyqtProperty("QVariantList", notify=printersChanged) def connectedPrintersTypeCount(self): return self._connected_printers_type_count @pyqtProperty("QVariantList", notify=printersChanged) def connectedPrinters(self): return self._printers @pyqtProperty(int, notify=printJobsChanged) def numJobsPrinting(self): num_jobs_printing = 0 for job in self._print_jobs: if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]: num_jobs_printing += 1 return num_jobs_printing @pyqtProperty(int, notify=printJobsChanged) def numJobsQueued(self): num_jobs_queued = 0 for job in self._print_jobs: if job["status"] == "queued": num_jobs_queued += 1 return num_jobs_queued @pyqtProperty("QVariantMap", notify=printJobsChanged) def printJobsByUUID(self): return self._print_job_by_uuid @pyqtProperty("QVariantMap", notify=printJobsChanged) def printJobsByPrinterUUID(self): return self._print_job_by_printer_uuid @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self): return self._print_jobs @pyqtProperty("QVariantList", notify=printersChanged) def printers(self): return [self._automatic_printer, ] + self._printers @pyqtSlot(str, str) def selectPrinter(self, unique_name, friendly_name): self.stopCamera() self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name} Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name) # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. if unique_name == "": self._address = self._master_address else: self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] self.selectedPrinterChanged.emit() def _updateJobState(self, job_state): name = self._selected_printer.get("friendly_name") if name == "" or name == "Automatic": # TODO: This is now a bit hacked; If no printer is selected, don't show job state. if self._job_state != "": self._job_state = "" self.jobStateChanged.emit() else: if self._job_state != job_state: self._job_state = job_state self.jobStateChanged.emit() @pyqtSlot() def selectAutomaticPrinter(self): self.stopCamera() self._selected_printer = self._automatic_printer self.selectedPrinterChanged.emit() @pyqtProperty("QVariant", notify=selectedPrinterChanged) def selectedPrinterName(self): return self._selected_printer.get("unique_name", "") def getPrintJobsUrl(self): return self._host + "/print_jobs" def getPrintersUrl(self): return self._host + "/printers" def _showProgressMessage(self): progress_message_template = i18n_catalog.i18nc("@info:progress", "Sending <filename>{file_name}</filename> to group {cluster_name}") file_name = os.path.basename(self._file_name).split(".")[0] self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._onMessageActionTriggered) self._progress_message.show() def _addUserAgentHeader(self, request): request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin") def _cleanupRequest(self): self._request = None self._stage = OutputStage.ready self._file_name = None def _onFinished(self, reply): super()._onFinished(reply) reply_url = reply.url().toString() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 500: Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url)) return if reply.error() == QNetworkReply.ContentOperationNotPermittedError: # It was probably "/api/v1/materials" for legacy UM3 return if reply.error() == QNetworkReply.ContentNotFoundError: # It was probably "/api/v1/print_job" for legacy UM3 return if reply.operation() == QNetworkAccessManager.PostOperation: if self._cluster_api_prefix + "print_jobs" in reply_url: self._finishedPrintJobPostRequest(reply) return # We need to do this check *after* we process the post operation! # If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this. if reply.error() != QNetworkReply.NoError: Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error()) return elif reply.operation() == QNetworkAccessManager.GetOperation: if self._cluster_api_prefix + "print_jobs" in reply_url: self._finishedPrintJobsRequest(reply) elif self._cluster_api_prefix + "printers" in reply_url: self._finishedPrintersRequest(reply) @pyqtSlot() def openPrintJobControlPanel(self): Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) @pyqtSlot() def openPrinterControlPanel(self): Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl(self.getPrintersUrl())) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) if action == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_print = False if self._reply: self._reply.abort() self._stage = OutputStage.ready Application.getInstance().showPrintMonitor.emit(False) @pyqtSlot(int, result=str) def formatDuration(self, seconds): return Duration(seconds).getDisplayString(DurationFormat.Format.Short) ## For cluster below def _get_plugin_directory_name(self): current_file_absolute_path = os.path.realpath(__file__) directory_path = os.path.dirname(current_file_absolute_path) _, directory_name = os.path.split(directory_path) return directory_name @property def _plugin_path(self): return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())
class WorkspaceDialog(QObject): showDialogSignal = pyqtSignal() def __init__(self, parent = None): super().__init__(parent) self._component = None self._context = None self._view = None self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() self._default_strategy = "override" self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, "definition_changes": self._default_strategy, "material": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) self._has_quality_changes_conflict = False self._has_definition_changes_conflict = False self._has_machine_conflict = False self._has_material_conflict = False self._num_visible_settings = 0 self._num_user_settings = 0 self._active_mode = "" self._quality_name = "" self._num_settings_overriden_by_quality_changes = 0 self._quality_type = "" self._machine_name = "" self._machine_type = "" self._variant_type = "" self._material_labels = [] self._extruders = [] self._objects_on_plate = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() definitionChangesConflictChanged = pyqtSignal() materialConflictChanged = pyqtSignal() numVisibleSettingsChanged = pyqtSignal() activeModeChanged = pyqtSignal() qualityNameChanged = pyqtSignal() numSettingsOverridenByQualityChangesChanged = pyqtSignal() qualityTypeChanged = pyqtSignal() machineNameChanged = pyqtSignal() materialLabelsChanged = pyqtSignal() objectsOnPlateChanged = pyqtSignal() numUserSettingsChanged = pyqtSignal() machineTypeChanged = pyqtSignal() variantTypeChanged = pyqtSignal() extrudersChanged = pyqtSignal() @pyqtProperty(str, notify=variantTypeChanged) def variantType(self): return self._variant_type def setVariantType(self, variant_type): if self._variant_type != variant_type: self._variant_type = variant_type self.variantTypeChanged.emit() @pyqtProperty(str, notify=machineTypeChanged) def machineType(self): return self._machine_type def setMachineType(self, machine_type): self._machine_type = machine_type self.machineTypeChanged.emit() def setNumUserSettings(self, num_user_settings): if self._num_user_settings != num_user_settings: self._num_user_settings = num_user_settings self.numVisibleSettingsChanged.emit() @pyqtProperty(int, notify=numUserSettingsChanged) def numUserSettings(self): return self._num_user_settings @pyqtProperty(bool, notify=objectsOnPlateChanged) def hasObjectsOnPlate(self): return self._objects_on_plate def setHasObjectsOnPlate(self, objects_on_plate): if self._objects_on_plate != objects_on_plate: self._objects_on_plate = objects_on_plate self.objectsOnPlateChanged.emit() @pyqtProperty("QVariantList", notify = materialLabelsChanged) def materialLabels(self): return self._material_labels def setMaterialLabels(self, material_labels): if self._material_labels != material_labels: self._material_labels = material_labels self.materialLabelsChanged.emit() @pyqtProperty("QVariantList", notify=extrudersChanged) def extruders(self): return self._extruders def setExtruders(self, extruders): if self._extruders != extruders: self._extruders = extruders self.extrudersChanged.emit() @pyqtProperty(str, notify = machineNameChanged) def machineName(self): return self._machine_name def setMachineName(self, machine_name): if self._machine_name != machine_name: self._machine_name = machine_name self.machineNameChanged.emit() @pyqtProperty(str, notify=qualityTypeChanged) def qualityType(self): return self._quality_type def setQualityType(self, quality_type): if self._quality_type != quality_type: self._quality_type = quality_type self.qualityTypeChanged.emit() @pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged) def numSettingsOverridenByQualityChanges(self): return self._num_settings_overriden_by_quality_changes def setNumSettingsOverridenByQualityChanges(self, num_settings_overriden_by_quality_changes): self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes self.numSettingsOverridenByQualityChangesChanged.emit() @pyqtProperty(str, notify=qualityNameChanged) def qualityName(self): return self._quality_name def setQualityName(self, quality_name): if self._quality_name != quality_name: self._quality_name = quality_name self.qualityNameChanged.emit() @pyqtProperty(str, notify=activeModeChanged) def activeMode(self): return self._active_mode def setActiveMode(self, active_mode): if active_mode == 0: self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended") else: self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom") self.activeModeChanged.emit() @pyqtProperty(int, constant = True) def totalNumberOfSettings(self): return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys()) @pyqtProperty(int, notify = numVisibleSettingsChanged) def numVisibleSettings(self): return self._num_visible_settings def setNumVisibleSettings(self, num_visible_settings): if self._num_visible_settings != num_visible_settings: self._num_visible_settings = num_visible_settings self.numVisibleSettingsChanged.emit() @pyqtProperty(bool, notify = machineConflictChanged) def machineConflict(self): return self._has_machine_conflict @pyqtProperty(bool, notify=qualityChangesConflictChanged) def qualityChangesConflict(self): return self._has_quality_changes_conflict @pyqtProperty(bool, notify=definitionChangesConflictChanged) def definitionChangesConflict(self): return self._has_definition_changes_conflict @pyqtProperty(bool, notify=materialConflictChanged) def materialConflict(self): return self._has_material_conflict @pyqtSlot(str, str) def setResolveStrategy(self, key, strategy): if key in self._result: self._result[key] = strategy ## Close the backend: otherwise one could end up with "Slicing..." @pyqtSlot() def closeBackend(self): Application.getInstance().getBackend().close() def setMaterialConflict(self, material_conflict): if self._has_material_conflict != material_conflict: self._has_material_conflict = material_conflict self.materialConflictChanged.emit() def setMachineConflict(self, machine_conflict): if self._has_machine_conflict != machine_conflict: self._has_machine_conflict = machine_conflict self.machineConflictChanged.emit() def setQualityChangesConflict(self, quality_changes_conflict): if self._has_quality_changes_conflict != quality_changes_conflict: self._has_quality_changes_conflict = quality_changes_conflict self.qualityChangesConflictChanged.emit() def setDefinitionChangesConflict(self, definition_changes_conflict): if self._has_definition_changes_conflict != definition_changes_conflict: self._has_definition_changes_conflict = definition_changes_conflict self.definitionChangesConflictChanged.emit() def getResult(self): if "machine" in self._result and not self._has_machine_conflict: self._result["machine"] = None if "quality_changes" in self._result and not self._has_quality_changes_conflict: self._result["quality_changes"] = None if "definition_changes" in self._result and not self._has_definition_changes_conflict: self._result["definition_changes"] = None if "material" in self._result and not self._has_material_conflict: self._result["material"] = None return self._result def _createViewFromQML(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: Logger.log("c", "QQmlComponent status %s", self._component.status()) Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. if threading.current_thread() != threading.main_thread(): self._lock.acquire() # Reset the result self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, "definition_changes": self._default_strategy, "material": self._default_strategy} self._visible = True self.showDialogSignal.emit() @pyqtSlot() ## Used to notify the dialog so the lock can be released. def notifyClosed(self): self._result = {} self._visible = False self._lock.release() def hide(self): self._visible = False self._lock.release() self._view.hide() @pyqtSlot() def onOkButtonClicked(self): self._view.hide() self.hide() @pyqtSlot() def onCancelButtonClicked(self): self._view.hide() self.hide() self._result = {} ## Block thread until the dialog is closed. def waitForClose(self): if self._visible: if threading.current_thread() != threading.main_thread(): self._lock.acquire() self._lock.release() else: # If this is not run from a separate thread, we need to ensure that the events are still processed. while self._visible: time.sleep(1 / 50) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. def __show(self): if self._view is None: self._createViewFromQML() if self._view: self._view.show()
class PostProcessingPlugin(QObject, Extension): def __init__(self, parent = None): super().__init__(parent) self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) self._view = None # Loaded scripts are all scripts that can be used self._loaded_scripts = {} self._script_labels = {} # Script list contains instances of scripts in loaded_scripts. There can be duplicates and they will be executed in sequence. self._script_list = [] self._selected_script_index = 0 @pyqtSlot(int, result = "QVariant") def getSettingModel(self, index): return self._script_list[index].getSettingsModel() @pyqtSlot(str, "QVariant") ## Called when the setting is changed. def setSettingValue(self, key, value): setting = self._script_list[self._selected_script_index].getSettings().getSettingByKey(key) if setting: setting.setValue(value) #self._script_list[self._selected_script_index].getSettings().setSettingValue selectedIndexChanged = pyqtSignal() @pyqtProperty("QVariant", notify = selectedIndexChanged) def selectedScriptSettingsModel(self): try: return self._script_list[self._selected_script_index].getSettingsModel() except: return None @pyqtSlot() def execute(self): scene = Application.getInstance().getController().getScene() if hasattr(scene, "gcode_list"): gcode_list = getattr(scene, "gcode_list") if gcode_list: for script in self._script_list: try: gcode_list = script.execute(gcode_list) except Exception as e: print(e) pass setattr(scene, "gcode_list", gcode_list) @pyqtSlot(int) def setSelectedScriptIndex(self, index): self._selected_script_index = index self.selectedIndexChanged.emit() @pyqtProperty(int, notify = selectedIndexChanged) def selectedScriptIndex(self): return self._selected_script_index @pyqtSlot(int, int) def moveScript(self, index, new_index): if new_index < 0 or new_index > len(self._script_list)-1: return #nothing needs to be done else: # Magical switch code. self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index] self.scriptListChanged.emit() self.selectedIndexChanged.emit() #Ensure that settings are updated ## Remove a script from the active script list by index. @pyqtSlot(int) def removeScriptByIndex(self, index): self._script_list.pop(index) if len(self._script_list) - 1 < self._selected_script_index: self._selected_script_index = len(self._script_list) - 1 self.scriptListChanged.emit() self.selectedIndexChanged.emit() #Ensure that settings are updated ## Load all scripts from provided path. This should probably only be done on init. def loadAllScripts(self, path): scripts = pkgutil.iter_modules(path = [path]) for loader, script_name, ispkg in scripts: if script_name not in sys.modules: # Import module loaded_script = __import__("PostProcessingPlugin.scripts."+ script_name, fromlist = [script_name]) loaded_class = getattr(loaded_script, script_name) temp_object = loaded_class() try: setting_data = temp_object.getSettingData() if "label" in setting_data and "key" in setting_data: self._script_labels[setting_data["key"]] = setting_data["label"] self._loaded_scripts[setting_data["key"]] = loaded_class else: Logger.log("w", "Script %s.py has no label or key", script_name) self._script_labels[script_name] = script_name self._loaded_scripts[script_name] = loaded_class #self._script_list.append(loaded_class()) self.loadedScriptListChanged.emit() except AttributeError: Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name) except NotImplementedError: Logger.log("e", "Script %s.py has no implemented settings",script_name) loadedScriptListChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = loadedScriptListChanged) def loadedScriptList(self): return list(self._loaded_scripts.keys()) @pyqtSlot(str, result = str) def getScriptLabelByKey(self, key): return self._script_labels[key] scriptListChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = scriptListChanged) def scriptList(self): script_list = [script.getSettingData()["key"] for script in self._script_list] return script_list @pyqtSlot(str) def addScriptToList(self, key): self._script_list.append(self._loaded_scripts[key]()) self.setSelectedScriptIndex(len(self._script_list) - 1) self.scriptListChanged.emit() ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. def _createView(self): ## Load all scripts in the scripts folder self.loadAllScripts(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "scripts")) path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")) self._component = QQmlComponent(Application.getInstance()._engine, path) self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) ## Show the (GUI) popup of the post processing plugin. def showPopup(self): if self._view is None: self._createView() self._view.show()
class ChangeLog(Extension, QObject,): def __init__(self, parent = None): QObject.__init__(self, parent) Extension.__init__(self) self._changelog_window = None self._changelog_context = None version_string = Application.getInstance().getVersion() if version_string is not "master": self._version = Version(version_string) else: self._version = None self._change_logs = None Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "15.05.90") #First version of CURA with uranium #self.showChangelog() def getChangeLogs(self): if not self._change_logs: self.loadChangeLogs() return self._change_logs @pyqtSlot(result = str) def getChangeLogString(self): logs = self.getChangeLogs() latest_version = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown")) result = "" for version in logs: result += "<h1>" + str(version) + "</h1><br>" result += "" for change in logs[version]: result += "<b>" + str(change) + "</b><br>" for line in logs[version][change]: result += str(line) + "<br>" result += "<br>" pass return result def loadChangeLogs(self): self._change_logs = {} with open(os.path.join(PluginRegistry.getInstance().getPluginPath("ChangeLogPlugin"), "ChangeLog.txt"), 'r') as f: open_version = None open_header = None for line in f: line = line.replace("\n","") if "[" in line and "]" in line: line = line.replace("[","") line = line.replace("]","") open_version = Version(line) self._change_logs[Version(line)] = {} elif line.startswith("*"): open_header = line.replace("*","") self._change_logs[open_version][open_header] = [] else: if line != "": self._change_logs[open_version][open_header].append(line) def _onEngineCreated(self): if not self._version: return #We're on dev branch. if self._version > Preferences.getInstance().getValue("general/latest_version_changelog_shown"): self.showChangelog() def showChangelog(self): if not self._changelog_window: self.createChangelogWindow() self._changelog_window.show() Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion()) def hideChangelog(self): if self._changelog_window: self._changelog_window.hide() def createChangelogWindow(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ChangeLogPlugin"), "ChangeLog.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._changelog_context.setContextProperty("manager", self) self._changelog_window = component.create(self._changelog_context)
def load(self, path, is_first_call = True): if path == self._path: return with open(os.path.join(path, "theme.json"), encoding = "utf-8") as f: Logger.log("d", "Loading theme file: %s", os.path.join(path, "theme.json")) data = json.load(f) # Iteratively load inherited themes try: theme_id = data["metadata"]["inherits"] self.load(Resources.getPath(Resources.Themes, theme_id), is_first_call = False) except FileNotFoundError: Logger.log("e", "Could not find inherited theme %s", theme_id) except KeyError: pass # No metadata or no inherits keyword in the theme.json file if "colors" in data: for name, color in data["colors"].items(): c = QColor(color[0], color[1], color[2], color[3]) self._colors[name] = c fontsdir = os.path.join(path, "fonts") if os.path.isdir(fontsdir): for file in os.listdir(fontsdir): if "ttf" in file: QFontDatabase.addApplicationFont(os.path.join(fontsdir, file)) if "fonts" in data: system_font_size = QCoreApplication.instance().font().pointSize() for name, font in data["fonts"].items(): f = QFont() f.setFamily(font.get("family", QCoreApplication.instance().font().family())) if font.get("bold"): f.setBold(font.get("bold", False)) else: f.setWeight(font.get("weight", 50)) f.setLetterSpacing(QFont.AbsoluteSpacing, font.get("letterSpacing", 0)) f.setItalic(font.get("italic", False)) f.setPointSize(int(font.get("size", 1) * system_font_size)) f.setCapitalization(QFont.AllUppercase if font.get("capitalize", False) else QFont.MixedCase) self._fonts[name] = f if "sizes" in data: for name, size in data["sizes"].items(): s = QSizeF() s.setWidth(round(size[0] * self._em_width)) s.setHeight(round(size[1] * self._em_height)) self._sizes[name] = s iconsdir = os.path.join(path, "icons") if os.path.isdir(iconsdir): for icon in os.listdir(iconsdir): name = os.path.splitext(icon)[0] self._icons[name] = QUrl.fromLocalFile(os.path.join(iconsdir, icon)) imagesdir = os.path.join(path, "images") if os.path.isdir(imagesdir): for image in os.listdir(imagesdir): name = os.path.splitext(image)[0] self._images[name] = QUrl.fromLocalFile(os.path.join(imagesdir, image)) styles = os.path.join(path, "styles.qml") if os.path.isfile(styles): c = QQmlComponent(self._engine, styles) context = QQmlContext(self._engine, self._engine) context.setContextProperty("Theme", self) self._styles = c.create(context) if c.isError(): for error in c.errors(): Logger.log("e", error.toString()) Logger.log("d", "Loaded theme %s", path) self._path = path # only emit the theme loaded signal once after all the themes in the inheritance chain have been loaded if is_first_call: self.themeLoaded.emit()
class DuetRRFOutputDevice(OutputDevice): def __init__(self, name="DuetRRF", url="http://printer.local", duet_password="******", http_user=None, http_password=None, device_type=DeviceType.print): self._device_type = device_type if device_type == DeviceType.print: description = catalog.i18nc("@action:button", "Print on {0}").format(name) name_id = name + "-print" priority = 30 elif device_type == DeviceType.simulate: description = catalog.i18nc("@action:button", "Simulate on {0}").format(name) name_id = name + "-simulate" priority = 20 elif device_type == DeviceType.upload: description = catalog.i18nc("@action:button", "Upload to {0}").format(name) name_id = name + "-upload" priority = 10 else: assert False super().__init__(name_id) self.setShortDescription(description) self.setDescription(description) self.setPriority(priority) self._stage = OutputStage.ready self._name = name if not url.endswith('/'): url += '/' self._url = url self._duet_password = duet_password self._http_user = http_user self._http_password = http_password self._qnam = QtNetwork.QNetworkAccessManager() self._stream = None self._cleanupRequest() if hasattr(self, '_message'): self._message.hide() self._message = None def _timestamp(self): return ("time", datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')) def _send(self, command, query=None, next_stage=None, data=None): enc_query = urllib.parse.urlencode(query or dict()) if enc_query: command += '?' + enc_query self._request = QtNetwork.QNetworkRequest( QUrl(self._url + "rr_" + command)) self._request.setRawHeader(b'User-Agent', b'Cura Plugin DuetRRF') self._request.setRawHeader(b'Accept', b'application/json, text/javascript') self._request.setRawHeader(b'Connection', b'keep-alive') if self._http_user and self._http_password: self._request.setRawHeader( b'Authorization', b'Basic ' + base64.b64encode("{}:{}".format( self._http_user, self._http_password).encode())) if data: self._request.setRawHeader(b'Content-Type', b'application/octet-stream') self._reply = self._qnam.post(self._request, data) self._reply.uploadProgress.connect(self._onUploadProgress) else: self._reply = self._qnam.get(self._request) if next_stage: self._reply.finished.connect(next_stage) self._reply.error.connect(self._onNetworkError) def requestWrite(self, node, fileName=None, *args, **kwargs): if self._stage != OutputStage.ready: raise OutputDeviceError.DeviceBusyError() if fileName: fileName = os.path.splitext(fileName)[0] + '.gcode' else: fileName = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName self._fileName = fileName path = QUrl.fromLocalFile( os.path.join(os.path.dirname(os.path.abspath(__file__)), 'UploadFilename.qml')) self._component = QQmlComponent(Application.getInstance()._engine, path) Logger.log("d", "Errors:", self._component.errors()) self._context = QQmlContext( Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._dialog = self._component.create(self._context) self._dialog.textChanged.connect(self.onFilenameChanged) self._dialog.accepted.connect(self.onFilenameAccepted) self._dialog.open() self._dialog.findChild(QObject, "nameField").setProperty( 'text', self._fileName) self._dialog.findChild(QObject, "nameField").select(0, len(self._fileName) - 6) self._dialog.findChild(QObject, "nameField").setProperty('focus', True) def onFilenameChanged(self): fileName = self._dialog.findChild(QObject, "nameField").property('text') self._dialog.setProperty('validName', len(fileName) > 0) def onFilenameAccepted(self): self._fileName = self._dialog.findChild(QObject, "nameField").property('text') if not self._fileName.endswith('.gcode') and '.' not in self._fileName: self._fileName += '.gcode' Logger.log("d", "Filename set to: " + self._fileName) self._dialog.deleteLater() # create the temp file for the gcode self._stream = StringIO() self._stage = OutputStage.writing self.writeStarted.emit(self) # show a progress message self._message = Message( catalog.i18nc("@info:progress", "Uploading to {}").format(self._name), 0, False, -1) self._message.show() Logger.log("d", "Loading gcode...") # find the G-code for the active build plate to print active_build_plate_id = Application.getInstance().getBuildPlateModel( ).activeBuildPlate gcode_dict = getattr( Application.getInstance().getController().getScene(), "gcode_dict") gcode = gcode_dict[active_build_plate_id] # send all the gcode to self._stream lines = len(gcode) nextYield = time() + 0.05 i = 0 for line in gcode: i += 1 self._stream.write(line) if time() > nextYield: self._onProgress(i / lines) QCoreApplication.processEvents() nextYield = time() + 0.05 # start Logger.log("d", "Connecting...") self._send('connect', [("password", self._duet_password), self._timestamp()], self.onConnected) def onConnected(self): if self._stage != OutputStage.writing: return Logger.log("d", "Connected") Logger.log("d", "Uploading...") self._stream.seek(0) self._postData = QByteArray() self._postData.append(self._stream.getvalue().encode()) self._send('upload', [("name", "0:/gcodes/" + self._fileName), self._timestamp()], self.onUploadDone, self._postData) def onUploadDone(self): if self._stage != OutputStage.writing: return Logger.log("d", "Upload done") self._stream.close() self.stream = None if self._device_type == DeviceType.simulate: Logger.log("d", "Simulating...") if self._message: self._message.hide() text = catalog.i18nc("@info:progress", "Simulating print on {}").format(self._name) self._message = Message(text, 0, False, -1) self._message.show() self._send('gcode', [("gcode", "M37 S1")], self.onReadyToPrint) elif self._device_type == DeviceType.print: self.onReadyToPrint() elif self._device_type == DeviceType.upload: self._send('disconnect') if self._message: self._message.hide() text = "Uploaded file {} to {}.".format( os.path.basename(self._fileName), self._name) self._message = Message(catalog.i18nc("@info:status", text)) self._message.addAction( "open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl.")) self._message.actionTriggered.connect( self._onMessageActionTriggered) self._message.show() self.writeSuccess.emit(self) self._cleanupRequest() def onReadyToPrint(self): if self._stage != OutputStage.writing: return Logger.log("d", "Ready to print") self._send('gcode', [("gcode", "M32 /gcodes/" + self._fileName)], self.onPrintStarted) def onPrintStarted(self): if self._stage != OutputStage.writing: return Logger.log("d", "Print started") if self._device_type == DeviceType.simulate: self.onCheckStatus() else: self._send('disconnect') if self._message: self._message.hide() text = "Print started on {} with file {}".format( self._name, self._fileName) self._message = Message(catalog.i18nc("@info:status", text)) self._message.addAction( "open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl.")) self._message.actionTriggered.connect( self._onMessageActionTriggered) self._message.show() self.writeSuccess.emit(self) self._cleanupRequest() def onSimulatedPrintFinished(self): if self._stage != OutputStage.writing: return Logger.log("d", "Simulation print finished") self._send('gcode', [("gcode", "M37 S0")], self.onSimulationStopped) def onCheckStatus(self): if self._stage != OutputStage.writing: return Logger.log("d", "Check status") self._send('status', [("type", "3")], self.onStatusReceived) def onStatusReceived(self): if self._stage != OutputStage.writing: return Logger.log("d", "Status received") status_bytes = bytes(self._reply.readAll()) Logger.log("d", status_bytes) status = json.loads(status_bytes.decode()) if status["status"] in ['P', 'M']: # still simulating # RRF 1.21RC2 and earlier used P while simulating # RRF 1.21RC3 and later uses M while simulating if self._message and "fractionPrinted" in status: self._message.setProgress(float(status["fractionPrinted"])) QTimer.singleShot(1000, self.onCheckStatus) else: # not printing any more (or error?) self.onSimulatedPrintFinished() def onSimulationStopped(self): if self._stage != OutputStage.writing: return Logger.log("d", "Simulation stopped") self._send('gcode', [("gcode", "M37")], self.onReporting) def onReporting(self): if self._stage != OutputStage.writing: return Logger.log("d", "Reporting") self._send('reply', [], self.onReported) def onReported(self): if self._stage != OutputStage.writing: return Logger.log("d", "Reported") self._send('disconnect') if self._message: self._message.hide() reply_body = bytes(self._reply.readAll()).decode() text = "Simulation performed on {} with file {}:\n{}".format( self._name, self._fileName, reply_body) self._message = Message(catalog.i18nc("@info:status", text)) self._message.addAction( "open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl.")) self._message.actionTriggered.connect(self._onMessageActionTriggered) self._message.show() self.writeSuccess.emit(self) self._cleanupRequest() def _onProgress(self, progress): if self._message: self._message.setProgress(progress) self.writeProgress.emit(self, progress) def _cleanupRequest(self): self._reply = None self._request = None if self._stream: self._stream.close() self._stream = None self._stage = OutputStage.ready self._fileName = None def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._url)) if self._message: self._message.hide() self._message = None def _onUploadProgress(self, bytesSent, bytesTotal): if bytesTotal > 0: self._onProgress(int(bytesSent * 100 / bytesTotal)) def _onNetworkError(self, errorCode): Logger.log("e", "_onNetworkError: %s", repr(errorCode)) if self._message: self._message.hide() self._message = None if self._reply: errorString = self._reply.errorString() else: errorString = '' message = Message( catalog.i18nc("@info:status", "There was a network error: {} {}").format( errorCode, errorString)) message.show() self.writeError.emit(self) self._cleanupRequest()
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): def __init__(self, parent=None): super().__init__(parent=parent) 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 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() firmwareUpdateChange = 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) @pyqtProperty(int, notify=progressChanged) def errorCode(self): for printer_name, device in self._usb_output_devices.items( ): # TODO: @UnusedVariable "printer_name" if device._error_code: return device._error_code return 0 ## Return True if all printers finished firmware update @pyqtProperty(float, notify=firmwareUpdateChange) def firmwareUpdateCompleteStatus(self): complete = True for printer_name, device in self._usb_output_devices.items( ): # TODO: @UnusedVariable "printer_name" if not device.firmwareUpdateFinished: complete = False return complete def start(self): self._check_updates = True self._update_thread.start() def stop(self): self._check_updates = False 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(str) def updateAllFirmware(self, file_name): if file_name.startswith("file://"): file_name = QUrl(file_name).toLocalFile( ) # File dialogs prepend the path with file://, which we don't need / want if not self._usb_output_devices: Message( i18n_catalog.i18nc( "@info", "Unable to update firmware because there are no printers connected." )).show() return for printer_connection in self._usb_output_devices: self._usb_output_devices[printer_connection].resetFirmwareUpdate() self.spawnFirmwareInterface("") for printer_connection in self._usb_output_devices: try: self._usb_output_devices[printer_connection].updateFirmware( file_name) except FileNotFoundError: # Should only happen in dev environments where the resources/firmware folder is absent. self._usb_output_devices[printer_connection].setProgress( 100, 100) Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name) Message( i18n_catalog.i18nc( "@info", "Could not find firmware required for the printer at %s." ) % printer_connection).show() self._firmware_view.close() continue @pyqtSlot(str, str, result=bool) def updateFirmwareBySerial(self, serial_port, file_name): 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(file_name) except FileNotFoundError: self._firmware_view.close() Logger.log( "e", "Could not find firmware required for this machine called '%s'", file_name) 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 @pyqtSlot(result=str) def getDefaultFirmwareName(self): # Check if there is a valid global container stack global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: Logger.log( "e", "There is no global container stack. Can not update firmware.") self._firmware_view.close() return "" # The bottom of the containerstack is the machine definition machine_id = global_container_stack.getBottom().id machine_has_heated_bed = global_container_stack.getProperty( "machine_heated_bed", "value") 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", "bq_hephestos_2": "MarlinHephestos2.hex", "ultimaker_original": "MarlinUltimaker-{baudrate}.hex", "ultimaker_original_plus": "MarlinUltimaker-UMOP-{baudrate}.hex", "ultimaker_original_dual": "MarlinUltimaker-{baudrate}-dual.hex", "ultimaker2": "MarlinUltimaker2.hex", "ultimaker2_go": "MarlinUltimaker2go.hex", "ultimaker2_plus": "MarlinUltimaker2plus.hex", "ultimaker2_extended": "MarlinUltimaker2extended.hex", "ultimaker2_extended_plus": "MarlinUltimaker2extended-plus.hex", } machine_with_heated_bed = { "ultimaker_original": "MarlinUltimaker-HBK-{baudrate}.hex", "ultimaker_original_dual": "MarlinUltimaker-HBK-{baudrate}-dual.hex", } ##TODO: Add check for multiple extruders hex_file = None if machine_id in machine_without_extras.keys( ): # The machine needs to be defined here! if machine_id in machine_with_heated_bed.keys( ) and machine_has_heated_bed: Logger.log( "d", "Choosing firmware with heated bed enabled for machine %s.", machine_id) hex_file = machine_with_heated_bed[ machine_id] # Return firmware with heated bed enabled else: Logger.log("d", "Choosing basic firmware for machine %s.", machine_id) hex_file = machine_without_extras[ machine_id] # Return "basic" firmware else: Logger.log("w", "There is no firmware for machine %s.", machine_id) if hex_file: return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate)) else: Logger.log("w", "Could not find any firmware for machine %s.", machine_id) return "" ## 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.connectionStateChanged.connect(self._onConnectionStateChanged) device.connect() device.progressChanged.connect(self.progressChanged) device.firmwareUpdateChange.connect(self.firmwareUpdateChange) 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 # type: ignore @UnresolvedImport 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*") + glob.glob( "/dev/tty.wchusb*") + glob.glob("/dev/cu.wchusb*") 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/tty.wchusb*") + glob.glob( "/dev/cu.wchusb*") + glob.glob( "/dev/rfcomm*") + glob.glob("/dev/serial/by-id/*") return list(base_list) _instance = None # type: "USBPrinterOutputDeviceManager"
class ChangeLog(Extension, QObject,): def __init__(self, parent = None): QObject.__init__(self, parent) Extension.__init__(self) self._changelog_window = None self._changelog_context = None version_string = Application.getInstance().getVersion() if version_string is not "master": self._version = Version(version_string) else: self._version = None self._change_logs = None Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog) #self.showChangelog() def getChangeLogs(self): if not self._change_logs: self.loadChangeLogs() return self._change_logs @pyqtSlot(result = str) def getChangeLogString(self): logs = self.getChangeLogs() result = "" for version in logs: result += "<h1>" + str(version) + "</h1><br>" result += "" for change in logs[version]: if str(change) != "": result += "<b>" + str(change) + "</b><br>" for line in logs[version][change]: result += str(line) + "<br>" result += "<br>" pass return result def loadChangeLogs(self): self._change_logs = collections.OrderedDict() with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f: open_version = None open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog for line in f: line = line.replace("\n","") if "[" in line and "]" in line: line = line.replace("[","") line = line.replace("]","") open_version = Version(line) open_header = "" self._change_logs[open_version] = collections.OrderedDict() elif line.startswith("*"): open_header = line.replace("*","") self._change_logs[open_version][open_header] = [] elif line != "": if open_header not in self._change_logs[open_version]: self._change_logs[open_version][open_header] = [] self._change_logs[open_version][open_header].append(line) def _onEngineCreated(self): if not self._version: return #We're on dev branch. if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master": latest_version_shown = Version("0.0.0") else: latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown")) if self._version > latest_version_shown: self.showChangelog() def showChangelog(self): if not self._changelog_window: self.createChangelogWindow() self._changelog_window.show() Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion()) def hideChangelog(self): if self._changelog_window: self._changelog_window.hide() def createChangelogWindow(self): path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._changelog_context.setContextProperty("manager", self) self._changelog_window = component.create(self._changelog_context)