From b8d39a35750ff7495f5d27eae0526a0dc9a3c767 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 23 Apr 2022 12:09:13 +0200 Subject: [PATCH] upgrade to Cura 5/API 8.0 and fix simulation progress report --- CHANGELOG.md | 129 ++++++++++++++++--------------- DuetRRFAction.py | 5 +- DuetRRFOutputDevice.py | 50 ++++++++---- DuetRRFPlugin.py | 2 +- plugin.json | 4 +- resources/qml/DuetRRFAction.qml | 45 ++++++----- resources/qml/UploadFilename.qml | 4 +- thumbnails.py | 17 ++-- 8 files changed, 138 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d501aa4..f7bb7da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,145 +1,146 @@ # Changelog of Cura-DuetRRFPlugin -## unreleased - * upgrade to QtControls 2, because Cura will upgrade to Qt 6 which removes QtControls 1. +## v1.2.6: 2022-04-23 +* bump compatibility for Cura 5.0 / API 8.0, for older Cura versions, please use plugin v1.2.5 or older +* fixed simulation progress reports ## v1.2.5: 2022-02-11 - * bump compatibility for Cura 4.13 / API 7.9, oldest supported release is now 4.11 / 7.7 - * embed QOI thumbnail images of the sliced scene into the uploaded gcode file - * add plugin metadata as comment to uploaded gcode file +* bump compatibility for Cura 4.13 / API 7.9, oldest supported release is now 4.11 / 7.7 +* embed QOI thumbnail images of the sliced scene into the uploaded gcode file +* add plugin metadata as comment to uploaded gcode file ## v1.2.4: 2021-09-19 - * auto-dismiss success message notifications after 15sec +* auto-dismiss success message notifications after 15sec ## v1.2.3: 2021-01-30 - * move deleting of unmapped settings to action button on message - * correctly bump plugin version metadata +* move deleting of unmapped settings to action button on message +* correctly bump plugin version metadata ## v1.2.2: 2021-01-27 - * add OutputDevices on currently active printer after saving config - * remove OutputDevices on currently active printer after deleting config +* add OutputDevices on currently active printer after saving config +* remove OutputDevices on currently active printer after deleting config ## v1.2.1: 2021-01-21 - * fix button width on high-dpi screens - * fix race condition when checking for unmapped settings +* fix button width on high-dpi screens +* fix race condition when checking for unmapped settings ## v1.2.0: 2021-01-10 - * store settings in local preferences instead of sharable metadata - * use managed HttpRequestManager instead of low-level QNetworkAccessManager - * add a "Configure" output device for easy initial setup +* store settings in local preferences instead of sharable metadata +* use managed HttpRequestManager instead of low-level QNetworkAccessManager +* add a "Configure" output device for easy initial setup ## v1.1.0: 2020-11-13 - * BREAKING CHANGE: migrate settings to be printer-specific - * auto-migrate legacy settings for active printers if printer name matches - * bump compatibility for Cura 4.8 / API 7.4 +* BREAKING CHANGE: migrate settings to be printer-specific +* auto-migrate legacy settings for active printers if printer name matches +* bump compatibility for Cura 4.8 / API 7.4 ## v1.0.11: 2020-08-29 - * bump compatibility for Cura 4.7 / API 7.3 - * fix Cura crashes with non-latin strings in Message boxes +* bump compatibility for Cura 4.7 / API 7.3 +* fix Cura crashes with non-latin strings in Message boxes ## v1.0.10: 2020-07-22 - * fix Cura crashes when using Duet3 with SBC +* fix Cura crashes when using Duet3 with SBC ## v1.0.9: 2020-05-02 - * bump compatibility for Cura 4.6 / API 7.2 - * fix simulation result reply for RRF HTTP API +* bump compatibility for Cura 4.6 / API 7.2 +* fix simulation result reply for RRF HTTP API ## v1.0.8: 2020-04-20 - * support new HTTP API for Duet3 with SBC running DuetSoftwareFramework +* support new HTTP API for Duet3 with SBC running DuetSoftwareFramework ## v1.0.7: 2020-04-04 - * bump compatibility for Cura 4.5 / API 7.1 +* bump compatibility for Cura 4.5 / API 7.1 ## v1.0.6: 2020-04-04 - * correctly encode special characters in filenames - * mention Duet 3 controllers +* correctly encode special characters in filenames +* mention Duet 3 controllers ## v1.0.5: 2019-11-10 - * sanitize filename and forbid certain characters +* sanitize filename and forbid certain characters ## v1.0.4: 2019-11-10 - * bump compatibility for Cura 4.4 / API 7.0 +* bump compatibility for Cura 4.4 / API 7.0 ## v1.0.3: 2019-02-02 - * require Cura 4.0 API compatibility - * if you are on Cura 3.6, please install v1.0.2 of this plugin +* require Cura 4.0 API compatibility +* if you are on Cura 3.6, please install v1.0.2 of this plugin ## v1.0.2: 2019-01-06 - * fix layout issues on Cura 4.0-beta - * bump compatibility for Cura 4.0 / API 6.0 +* fix layout issues on Cura 4.0-beta +* bump compatibility for Cura 4.0 / API 6.0 ## v1.0.1: 2018-12-19 - * do not try to delete the gcode file before uploading: +* do not try to delete the gcode file before uploading: RRF safely handles this, https://forum.duet3d.com/topic/8194/cura-duet-reprap-firmware-integration-question ## v1.0.0: 2018-11-25 - * tested with Cura 3.6 - * add Duet3D icon - permission granted by Think3dPrint3d - * fixed a update issue when changing connection details - * replace deprecated preferences API in favor of the new one - * code cleanup +* tested with Cura 3.6 +* add Duet3D icon - permission granted by Think3dPrint3d +* fixed a update issue when changing connection details +* replace deprecated preferences API in favor of the new one +* code cleanup ## v0.0.20: 2018-10-06 - * fix broken dialogs +* fix broken dialogs ## v0.0.19: 2018-10-05 - * fix more Cura 3.5 incompatibilities +* fix more Cura 3.5 incompatibilities ## v0.0.18: 2018-10-05 - * fix Cura 3.5 incompatibility - * bump API code +* fix Cura 3.5 incompatibility +* bump API code ## v0.0.17: 2018-08-10 - * fix missing settings at the end of the gcode file - * make use of the default GCodeWrite that Cura uses for "Save to File" +* fix missing settings at the end of the gcode file +* make use of the default GCodeWrite that Cura uses for "Save to File" ## v0.0.16: 2018-04-11 - * improve simulation mode +* improve simulation mode ## v0.0.15: 2018-04-01 - * fix message box progress +* fix message box progress ## v0.0.14: 2018-04-01 - * fixes #10 +* fixes #10 ## v0.0.13: 2018-03-28 - * fixes gcode retrieval for Cura 3.0, 3.1, 3.2, and the latest 3.3 beta - * also see https://github.com/Ultimaker/Cura/commit/495fc8bbd705f5145fe8312207b3f048a7dcc106#diff-a4cb192cad5ce77939fdd0bf0600208d +* fixes gcode retrieval for Cura 3.0, 3.1, 3.2, and the latest 3.3 beta +* also see https://github.com/Ultimaker/Cura/commit/495fc8bbd705f5145fe8312207b3f048a7dcc106#diff-a4cb192cad5ce77939fdd0bf0600208d ## v0.0.12: 2018-03-21 - * fix removal error +* fix removal error ## v0.0.11: 2018-03-20 - * fixes and slashes +* fixes and slashes ## v0.0.10: 2018-03-20 - * improve setting parsing +* improve setting parsing ## v0.0.9: 2018-03-01 - * updated plugin location paths - * updated RRF status letter +* updated plugin location paths +* updated RRF status letter ## v0.0.8: 2018-02-17 - * hide message after click +* hide message after click ## v0.0.7: 2018-02-17 - * fix disconnect params +* fix disconnect params ## v0.0.6: 2018-02-15 - * improve error handling +* improve error handling ## v0.0.5: 2018-02-13 - * fix Cura 3.2 incompatibility to access the generated G-code. +* fix Cura 3.2 incompatibility to access the generated G-code. ## v0.0.4: 2017-11-21 - * send disconnect when done +* send disconnect when done ## v0.0.3: 2017-11-07 - * fix error handling +* fix error handling ## v0.0.2: 2017-11-07 - * add a file-rename dialog - * add network error handling +* add a file-rename dialog +* add network error handling ## v0.0.1: 2017-10-21 - * initial commit +* initial commit diff --git a/DuetRRFAction.py b/DuetRRFAction.py index e2cea22..27fd987 100644 --- a/DuetRRFAction.py +++ b/DuetRRFAction.py @@ -1,9 +1,8 @@ import os -import json import re -from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast +from typing import Optional -from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal +from PyQt6.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from cura.CuraApplication import CuraApplication from cura.MachineAction import MachineAction diff --git a/DuetRRFOutputDevice.py b/DuetRRFOutputDevice.py index 223afab..46477e9 100644 --- a/DuetRRFOutputDevice.py +++ b/DuetRRFOutputDevice.py @@ -7,9 +7,9 @@ from typing import cast from enum import Enum -from PyQt5.QtNetwork import QNetworkReply -from PyQt5.QtCore import QUrl, QObject, QByteArray, QTimer -from PyQt5.QtGui import QDesktopServices +from PyQt6.QtNetwork import QNetworkReply +from PyQt6.QtCore import QUrl, QObject, QByteArray, QTimer +from PyQt6.QtGui import QDesktopServices from cura.CuraApplication import CuraApplication @@ -266,7 +266,7 @@ def _assemble_final_gcode(self, gcode_stream, thumbnail_stream): def _check_duet3_sbc(self, reply, error): Logger.log("d", "rr_connect failed with error " + str(error)) - if error == QNetworkReply.ContentNotFoundError: + if error == QNetworkReply.NetworkError.ContentNotFoundError: Logger.log("d", "error indicates Duet3+SBC - let's try the DuetSoftwareFramework API instead...") self._use_rrf_http_api = False # let's try the newer DuetSoftwareFramework for Duet3+SBC API instead self._send('machine/status', @@ -278,7 +278,7 @@ def _check_duet3_sbc(self, reply, error): def _onUploadReady(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -303,7 +303,7 @@ def _onUploadReady(self, reply): def _onUploadDone(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -382,7 +382,7 @@ def _onReadyToPrint(self): def _onPrintStarted(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -409,7 +409,7 @@ def _onPrintStarted(self, reply): def _onSimulationPrintStarted(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -437,13 +437,13 @@ def _onCheckStatus(self): def _onStatusReceived(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return Logger.log("d", "Status received - decoding...") reply_body = bytes(reply.readAll()).decode() - Logger.log("d", "Status: " + reply_body) + # Logger.log("d", "Status: " + reply_body) status = json.loads(reply_body) if self._use_rrf_http_api: @@ -451,13 +451,29 @@ def _onStatusReceived(self, reply): # RRF 1.21RC3 and later uses M while simulating busy = status["status"] in ['P', 'M'] else: - busy = status["result"]["state"]["status"] == 'simulating' + s = status.get("state", {}).get("status", None) + if not s: + # we might not have received a full status report, assume we are still simulating and busy + busy = True + else: + busy = s == 'simulating' + + progress = 0.0 + try: + if "fractionPrinted" in status: + progress = float(status["fractionPrinted"]) + else: + file_size = status.get("job", {}).get("file", {}).get("size", None) + file_position = status.get("job", {}).get("filePosition", 0) + progress = int(file_position) / int(file_size) * 100.0 + except: + pass if busy: # still simulating - if self._message and "fractionPrinted" in status: - self._message.setProgress(float(status["fractionPrinted"])) - QTimer.singleShot(1000, self._onCheckStatus) + if self._message: + self._message.setProgress(progress) + QTimer.singleShot(2000, self._onCheckStatus) else: Logger.log("d", "Simulation print finished") @@ -477,7 +493,7 @@ def _onStatusReceived(self, reply): def _onM37Reported(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -492,7 +508,7 @@ def _onM37Reported(self, reply): def _onReported(self, reply): if self._stage != OutputStage.writing: return - if reply.error() != QNetworkReply.NoError: + if reply.error() != QNetworkReply.NetworkError.NoError: Logger.log("d", "Stopping due to reply error: " + reply.error()) return @@ -541,7 +557,7 @@ def _onUploadProgress(self, bytesSent, bytesTotal): self.writeProgress.emit(self, progress) def _onNetworkError(self, reply, error): - # https://doc.qt.io/qt-5/qnetworkreply.html#NetworkError-enum + # https://doc.qt.io/qt-6/qnetworkreply.html#NetworkError-enum Logger.log("e", repr(error)) if self._message: self._message.hide() diff --git a/DuetRRFPlugin.py b/DuetRRFPlugin.py index cc95d60..60cf20b 100644 --- a/DuetRRFPlugin.py +++ b/DuetRRFPlugin.py @@ -1,6 +1,6 @@ import json -from PyQt5.QtCore import Qt, QTimer +from PyQt6.QtCore import QTimer from cura.CuraApplication import CuraApplication from cura.Settings.CuraContainerRegistry import CuraContainerRegistry diff --git a/plugin.json b/plugin.json index b711633..97e2d1c 100644 --- a/plugin.json +++ b/plugin.json @@ -2,6 +2,6 @@ "name": "DuetRRF", "author": "Thomas Kriechbaumer", "description": "Upload and Print to Duet 2 Wifi / Duet 2 Ethernet / Duet 2 Maestro / Duet 3 with RepRapFirmware.", - "version": "1.2.5", - "supported_sdk_versions": ["7.7.0", "7.8.0", "7.9.0"] + "version": "1.2.6", + "supported_sdk_versions": ["8.0.0"] } diff --git a/resources/qml/DuetRRFAction.qml b/resources/qml/DuetRRFAction.qml index 571e0af..d5562e6 100644 --- a/resources/qml/DuetRRFAction.qml +++ b/resources/qml/DuetRRFAction.qml @@ -1,6 +1,6 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.2 +import QtQuick.Layouts 2.7 import QtQuick.Window 2.2 import UM 1.5 as UM @@ -10,8 +10,12 @@ import Cura 1.1 as Cura Cura.MachineAction { id: base; + anchors.fill: parent; property var finished: manager.finished + property var selectedInstance: null + property bool validUrl: true; + onFinishedChanged: if(manager.finished) {completed()} function reset() @@ -19,22 +23,21 @@ Cura.MachineAction manager.reset() } - anchors.fill: parent; - property var selectedInstance: null - - property bool validUrl: true; - Component.onCompleted: { actionDialog.minimumWidth = screenScaleFactor * 500; - actionDialog.minimumHeight = screenScaleFactor * 255; - actionDialog.maximumWidth = screenScaleFactor * 500; - actionDialog.maximumHeight = screenScaleFactor * 255; + actionDialog.minimumHeight = screenScaleFactor * 300; } Column { - anchors.fill: parent + spacing: UM.Theme.getSize("thin_margin").height; + anchors { + fill: parent; + leftMargin: UM.Theme.getSize("thin_margin").width; + rightMargin: UM.Theme.getSize("thin_margin").width; + top: parent.top; + topMargin: UM.Theme.getSize("thin_margin").height; + } - Item { width: parent.width; } UM.Label { text: catalog.i18nc("@label", "Duet Address (URL)") } @@ -50,7 +53,6 @@ Cura.MachineAction } } - Item { width: parent.width; } UM.Label { text: catalog.i18nc("@label", "Duet Password (if you used M551)") } @@ -63,7 +65,6 @@ Cura.MachineAction anchors.right: parent.right } - Item { width: parent.width; } UM.Label { text: catalog.i18nc("@label", "HTTP Basic Auth: user (if you run a reverse proxy)") } @@ -76,7 +77,6 @@ Cura.MachineAction anchors.right: parent.right } - Item { width: parent.width; } UM.Label { text: catalog.i18nc("@label", "HTTP Basic Auth: password (if you run a reverse proxy)") } @@ -89,16 +89,21 @@ Cura.MachineAction anchors.right: parent.right } - Item { width: parent.width; } - UM.Label { - visible: !base.validUrl - text: catalog.i18nc("@error", "URL not valid. Example: http://192.168.1.42/") - color: "red" + Item { + width: errorMsgLabel.implicitWidth + height: errorMsgLabel.implicitHeight + UM.Label { + id: errorMsgLabel + visible: !base.validUrl + text: catalog.i18nc("@error", "URL not valid. Example: http://192.168.1.42/") + color: "red" + Layout.fillWidth: true + } } Row { anchors.horizontalCenter: parent.horizontalCenter - width: childrenRect.width + width: parent.width spacing: UM.Theme.getSize("default_margin").width Cura.PrimaryButton { diff --git a/resources/qml/UploadFilename.qml b/resources/qml/UploadFilename.qml index 8f69f2a..47634e6 100644 --- a/resources/qml/UploadFilename.qml +++ b/resources/qml/UploadFilename.qml @@ -1,7 +1,7 @@ import QtQuick 2.10 -import QtQuick.Controls 2.1 -import QtQuick.Dialogs 1.2 +import QtQuick.Controls 2.3 import QtQuick.Window 2.2 +import QtQuick.Dialogs import UM 1.5 as UM import Cura 1.1 as Cura diff --git a/thumbnails.py b/thumbnails.py index 0e12704..1d9522b 100644 --- a/thumbnails.py +++ b/thumbnails.py @@ -1,11 +1,10 @@ -import os import base64 import traceback from io import StringIO -from PyQt5 import QtCore -from PyQt5.QtCore import QCoreApplication, QBuffer -from PyQt5.QtGui import QImage +from PyQt6 import QtCore +from PyQt6.QtCore import QCoreApplication, QBuffer +from PyQt6.QtGui import QImage from UM.Application import Application from UM.Logger import Logger @@ -33,7 +32,7 @@ def render_scene(): zooms = 0 while not satisfied and zooms < 5: preview_pass.render() - pixel_output = preview_pass.getOutput().convertToFormat(QImage.Format_ARGB32) + pixel_output = preview_pass.getOutput().convertToFormat(QImage.Format.Format_ARGB32) # pixel_output.save(os.path.expanduser(f"~/Downloads/foo-a-zoom-{zooms}.png"), "PNG") min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output) @@ -63,19 +62,19 @@ def render_thumbnail(pixel_output, width, height): # scale to desired width and height pixel_output = pixel_output.scaled( width, height, - aspectRatioMode=QtCore.Qt.KeepAspectRatio, - transformMode=QtCore.Qt.SmoothTransformation + aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatio, + transformMode=QtCore.Qt.TransformationMode.SmoothTransformation ) Logger.log("d", f"Scaled thumbnail to {width=}, {height=}.") # pixel_output.save(os.path.expanduser("~/Downloads/foo-c-scaled.png"), "PNG") # center image within desired width and height if one dimension is too small if pixel_output.width() < width: - d = (width - pixel_output.width()) / 2. + d = int((width - pixel_output.width()) / 2.) pixel_output = pixel_output.copy(-d, 0, width, pixel_output.height()) Logger.log("d", f"Centered thumbnail horizontally {d=}.") if pixel_output.height() < height: - d = (height - pixel_output.height()) / 2. + d = int((height - pixel_output.height()) / 2.) pixel_output = pixel_output.copy(0, -d, pixel_output.width(), height) Logger.log("d", f"Centered thumbnail vertically {d=}.") # pixel_output.save(os.path.expanduser("~/Downloads/foo-d-aspect-fixed.png"), "PNG")