Пример #1
0
    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())
Пример #2
0
    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())
Пример #3
0
class MoonrakerOutputDevice(OutputDevice):
    def __init__(self, config):
        self._name_id = "moonraker-upload"
        super().__init__(self._name_id)

        self._url = config.get("url", "")
        self._api_key = config.get("api_key", "")
        self._power_device = config.get("power_device", "")
        self._output_format = config.get("output_format", "gcode")
        if self._output_format and self._output_format != "ufp":
            self._output_format = "gcode"
        self._upload_start_print_job = config.get("upload_start_print_job",
                                                  False)
        self._upload_remember_state = config.get("upload_remember_state",
                                                 False)
        self._upload_autohide_messagebox = config.get(
            "upload_autohide_messagebox", False)
        self._trans_input = config.get("trans_input", "")
        self._trans_output = config.get("trans_output", "")
        self._trans_remove = config.get("trans_remove", "")

        self.application = CuraApplication.getInstance()
        global_container_stack = self.application.getGlobalContainerStack()
        self._name = global_container_stack.getName()

        description = catalog.i18nc("@action:button",
                                    "Upload to {0}").format(self._name)
        self.setShortDescription(description)
        self.setDescription(description)

        self._stage = OutputStage.ready
        self._stream = None
        self._message = None

        self._timeout_cnt = 0

        Logger.log(
            "d",
            "New MoonrakerOutputDevice '{}' created | URL: {} | API-Key: {}".
            format(
                self._name_id,
                self._url,
                self._api_key,
            ))
        self._resetState()

    def requestWrite(self, node, fileName=None, *args, **kwargs):
        if self._stage != OutputStage.ready:
            raise OutputDeviceError.DeviceBusyError()

        # Make sure post-processing plugin are run on the gcode
        self.writeStarted.emit(self)

        # The presliced print should always be send using `GCodeWriter`
        print_info = CuraApplication.getInstance().getPrintInformation()
        if self._output_format != "ufp" or not print_info or print_info.preSliced:
            self._output_format = "gcode"
            code_writer = cast(
                MeshWriter,
                PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
            self._stream = StringIO()
        else:
            code_writer = cast(
                MeshWriter,
                PluginRegistry.getInstance().getPluginObject("UFPWriter"))
            self._stream = BytesIO()

        if not code_writer.write(self._stream, None):
            Logger.log("e",
                       "MeshWriter failed: %s" % code_writer.getInformation())
            return

        # Prepare filename for upload
        if fileName:
            fileName = os.path.basename(fileName)
        else:
            fileName = "%s." % Application.getInstance().getPrintInformation(
            ).jobName

        # Translate filename
        if self._trans_input and self._trans_output:
            transFileName = fileName.translate(
                fileName.maketrans(
                    self._trans_input, self._trans_output,
                    self._trans_remove if self._trans_remove else ""))
            fileName = transFileName

        self._fileName = fileName + "." + self._output_format

        # Display upload dialog
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            'resources', 'qml', 'MoonrakerUpload.qml')
        self._dialog = CuraApplication.getInstance().createQmlComponent(
            path, {"manager": self})
        self._dialog.textChanged.connect(self.onFilenameChanged)
        self._dialog.accepted.connect(self.onFilenameAccepted)
        self._dialog.show()
        self._dialog.findChild(QObject, "nameField").setProperty(
            'text', self._fileName)
        self._dialog.findChild(QObject, "nameField").select(
            0,
            len(self._fileName) - len(self._output_format) - 1)
        self._dialog.findChild(QObject, "nameField").setProperty('focus', True)
        self._dialog.findChild(QObject, "printField").setProperty(
            'checked', self._upload_start_print_job)

    def onFilenameChanged(self):
        fileName = self._dialog.findChild(
            QObject, "nameField").property('text').strip()

        forbidden_characters = ":*?\"<>|"
        for forbidden_character in forbidden_characters:
            if forbidden_character in fileName:
                self._dialog.setProperty('validName', False)
                self._dialog.setProperty(
                    'validationError',
                    '*cannot contain {}'.format(forbidden_characters))
                return

        if fileName == '.' or fileName == '..':
            self._dialog.setProperty('validName', False)
            self._dialog.setProperty('validationError',
                                     '*cannot be "." or ".."')
            return

        self._dialog.setProperty('validName', len(fileName) > 0)
        self._dialog.setProperty('validationError', 'Filename too short')

    def onFilenameAccepted(self):
        self._fileName = self._dialog.findChild(
            QObject, "nameField").property('text').strip()
        if not self._fileName.endswith(
                '.' + self._output_format) and '.' not in self._fileName:
            self._fileName += '.' + self._output_format
        Logger.log("d", "Filename set to: " + self._fileName)

        self._startPrint = self._dialog.findChild(
            QObject, "printField").property('checked')
        if self._upload_remember_state:
            self._upload_start_print_job = self._startPrint
            s = get_config()
            s["upload_start_print_job"] = self._startPrint
            save_config(s)
        Logger.log("d", "Print set to: " + str(self._startPrint))

        self._dialog.deleteLater()

        Logger.log("d", "Connecting to Moonraker at {} ...".format(self._url))
        # show a status message with spinner
        messageText = self._getConnectMsgText()
        self._message = Message(catalog.i18nc("@info:status", messageText), 0,
                                False)
        self._message.show()

        if self._power_device:
            self.getPrinterDeviceStatus()
        else:
            self.getPrinterInfo()

    def checkPrinterState(self, reply=None):
        if reply:
            res = self._verifyReply(reply)
            state = res['result']['state']

            if self._startPrint:
                if state == 'ready':
                    # printer is online
                    self.onInstanceOnline(reply)
                else:
                    self.handlePrinterConnection()
            elif state != 'error':
                # printer can queue job
                self.onInstanceOnline(reply)
            else:  # set counter to max before call?
                self.handlePrinterConnection()

    def checkPrinterDeviceStatus(self, reply):
        if reply:
            res = self._verifyReply(reply)
            device = self._power_device
            if "," in device:
                devices = [x.strip() for x in self._power_device.split(',')]
                device = devices[0]

            power_status = res['result'][device]
            log_msg = "Power device [power {}] status == '{}';".format(
                device, power_status)
            log_msg += " self._startPrint is {}".format(self._startPrint)

            if power_status == 'on':
                log_msg += " Calling getPrinterInfo()"
                Logger.log("d", log_msg)
                self.getPrinterInfo()
            elif power_status == 'off':
                if self._startPrint:
                    log_msg += " Calling postPrinterDevicePowerOn()"
                    Logger.log("d", log_msg)
                    self.postPrinterDevicePowerOn()
                else:
                    log_msg += " Sending FIRMWARE_RESTART before calling getPrinterInfo()"
                    Logger.log("d", log_msg)
                    postData = json.dumps({}).encode()
                    self._sendRequest('printer/firmware_restart',
                                      data=postData,
                                      dataIsJSON=True,
                                      on_success=self.getPrinterInfo)

    def getPrinterDeviceStatus(self):
        device = self._power_device
        if "," in device:
            devices = [x.strip() for x in self._power_device.split(',')]
            device = devices[0]

        Logger.log("d",
                   "Checking printer device [power {}] status".format(device))
        self._sendRequest(
            'machine/device_power/device?device={}'.format(device),
            on_success=self.checkPrinterDeviceStatus)

    def postPrinterDevicePowerOn(self, reply=None):
        device = self._power_device
        if "," in device:
            devices = [x.strip() for x in self._power_device.split(',')]
            for dev in devices:
                Logger.log(
                    "d",
                    "Turning on Moonraker power device [power {}]".format(dev))
                postJSON = '{}'.encode()
                params = {'device': dev, 'action': 'on'}
                req = 'machine/device_power/device?' + urllib.parse.urlencode(
                    params)
                self._sendRequest(req,
                                  data=postJSON,
                                  dataIsJSON=True,
                                  on_success=self.getPrinterInfo)
        else:
            Logger.log(
                "d",
                "Turning on (single) Moonraker power device [power {}]".format(
                    self._power_device))
            postJSON = '{}'.encode()
            params = {'device': self._power_device, 'action': 'on'}
            req = 'machine/device_power/device?' + urllib.parse.urlencode(
                params)
            self._sendRequest(req,
                              data=postJSON,
                              dataIsJSON=True,
                              on_success=self.getPrinterInfo)

    def onMoonrakerConnectionTimeoutError(self):
        messageText = "Error: Connection to Moonraker at {} timed out.".format(
            self._url)
        self._message.setLifetimeTimer(0)
        self._message.setText(messageText)

        browseMessageText = "Check your Moonraker and Klipper settings."
        browseMessageText += "\nA FIRMWARE_RESTART may be necessary."
        if self._power_device:
            browseMessageText += "\nAlso check [power {}] stanza in moonraker.conf".format(
                self._power_device)

        self._message = Message(
            catalog.i18nc("@info:status", browseMessageText), 0, False)
        self._message.addAction(
            "open_browser", catalog.i18nc("@action:button", "Open Browser"),
            "globe",
            catalog.i18nc("@info:tooltip", "Open browser to Moonraker."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self.writeError.emit(self)
        self._resetState()

    def handlePrinterConnection(self, reply=None, error=None):
        self._timeout_cnt += 1
        timeout_cnt_max = 20

        if self._timeout_cnt > timeout_cnt_max:
            self.onMoonrakerConnectionTimeoutError()
        else:
            sleep(0.5)
            self._message.setText(self._getConnectMsgText())
            self.getPrinterInfo()

    def getPrinterInfo(self, reply=None):
        self._sendRequest('printer/info',
                          on_success=self.checkPrinterState,
                          on_error=self.handlePrinterConnection)

    def onInstanceOnline(self, reply):
        # remove connection timeout message
        self._timeout_cnt
        self._message.hide()
        self._message = None

        self._stage = OutputStage.writing
        # show a progress message
        self._message = Message(
            catalog.i18nc("@info:progress",
                          "Uploading to {}...").format(self._name), 0, False,
            -1)
        self._message.show()

        if self._stage != OutputStage.writing:
            return  # never gets here now?
        if reply.error() != QNetworkReply.NoError:
            Logger.log("d", "Stopping due to reply error: " + reply.error())
            return

        Logger.log("d", "Uploading " + self._output_format + "...")
        self._stream.seek(0)
        self._postData = QByteArray()
        if isinstance(self._stream, BytesIO):
            self._postData.append(self._stream.getvalue())
        else:
            self._postData.append(self._stream.getvalue().encode())
        self._sendRequest('server/files/upload',
                          name=self._fileName,
                          data=self._postData,
                          on_success=self.onCodeUploaded)

    def onCodeUploaded(self, reply):
        if self._stage != OutputStage.writing:
            return
        if reply.error() != QNetworkReply.NoError:
            Logger.log("d", "Stopping due to reply error: " + reply.error())
            return

        Logger.log("d", "Upload completed")
        self._stream.close()
        self._stream = None

        if self._message:
            self._message.hide()
            self._message = None

        messageText = "Upload of '{}' to {} successfully completed"

        if self._startPrint:
            messageText += " and print job initialized."
        else:
            messageText += "."
        self._message = Message(
            catalog.i18nc(
                "@info:status",
                messageText.format(os.path.basename(self._fileName),
                                   self._name)),
            30 if self._upload_autohide_messagebox else 0, True)
        self._message.addAction(
            "open_browser", catalog.i18nc("@action:button", "Open Browser"),
            "globe",
            catalog.i18nc("@info:tooltip", "Open browser to Moonraker."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self.writeSuccess.emit(self)
        self._resetState()

    def _onProgress(self, progress):
        if self._message:
            self._message.setProgress(progress)
        self.writeProgress.emit(self, progress)

    def _getConnectMsgText(self):
        spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
        return "Connecting to Moonraker at {}     {}".format(
            self._url, spinner[self._timeout_cnt % len(spinner)])

    def _resetState(self):
        Logger.log("d", "Reset state")
        if self._stream:
            self._stream.close()
        self._stream = None
        self._stage = OutputStage.ready
        self._fileName = None
        self._startPrint = None
        self._postData = None
        self._timeout_cnt = 0

    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, reply, error):
        Logger.log("e", repr(error))
        if self._message:
            self._message.hide()
            self._message = None

        errorString = ''
        if reply:
            errorString = reply.errorString()

        message = Message(
            catalog.i18nc("@info:status",
                          "There was a network error: {} {}").format(
                              error, errorString), 0, False)
        message.show()

        self.writeError.emit(self)
        self._resetState()

    def _verifyReply(self, reply):
        # Logger.log("d", "reply: %s" % str(byte_string, 'utf-8'))

        byte_string = reply.readAll()
        response = ''
        try:
            response = json.loads(str(byte_string, 'utf-8'))
        except json.JSONDecodeError:
            Logger.log("d",
                       "Reply is not a JSON: %s" % str(byte_string, 'utf-8'))
            self.handlePrinterConnection()

        return response

    def _sendRequest(self,
                     path,
                     name=None,
                     data=None,
                     dataIsJSON=False,
                     on_success=None,
                     on_error=None):
        url = self._url + path

        headers = {
            'User-Agent': 'Cura Plugin Moonraker',
            'Accept': 'application/json, text/plain',
            'Connection': 'keep-alive'
        }

        if self._api_key:
            headers['X-API-Key'] = self._api_key

        postData = data
        if data is not None:
            if not dataIsJSON:
                # Create multi_part request
                parts = QHttpMultiPart(QHttpMultiPart.FormDataType)

                part_file = QHttpPart()
                part_file.setHeader(
                    QNetworkRequest.ContentDispositionHeader,
                    QVariant('form-data; name="file"; filename="/' + name +
                             '"'))
                part_file.setHeader(QNetworkRequest.ContentTypeHeader,
                                    QVariant('application/octet-stream'))
                part_file.setBody(data)
                parts.append(part_file)

                part_root = QHttpPart()
                part_root.setHeader(QNetworkRequest.ContentDispositionHeader,
                                    QVariant('form-data; name="root"'))
                part_root.setBody(b"gcodes")
                parts.append(part_root)

                if self._startPrint:
                    part_print = QHttpPart()
                    part_print.setHeader(
                        QNetworkRequest.ContentDispositionHeader,
                        QVariant('form-data; name="print"'))
                    part_print.setBody(b"true")
                    parts.append(part_print)

                headers[
                    'Content-Type'] = 'multipart/form-data; boundary=' + str(
                        parts.boundary().data(), encoding='utf-8')

                postData = parts
            else:
                # postData is JSON
                headers['Content-Type'] = 'application/json'

            self.application.getHttpRequestManager().post(
                url,
                headers,
                postData,
                callback=on_success,
                error_callback=on_error if on_error else self._onNetworkError,
                upload_progress_callback=self._onUploadProgress
                if not dataIsJSON else None)
        else:
            self.application.getHttpRequestManager().get(
                url,
                headers,
                callback=on_success,
                error_callback=on_error if on_error else self._onNetworkError)