mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-20 13:17:51 -06:00
Merge remote-tracking branch 'UM3NPP/master' into UM3NPP_merge
Contributes to CURA-2862
This commit is contained in:
commit
c48d064eca
10 changed files with 1889 additions and 4 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
# Compiled and generated things.
|
#Compiled and generated things.
|
||||||
build
|
build
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
|
@ -32,4 +32,5 @@ plugins/Doodle3D-cura-plugin
|
||||||
plugins/GodMode
|
plugins/GodMode
|
||||||
plugins/PostProcessingPlugin
|
plugins/PostProcessingPlugin
|
||||||
plugins/UM3NetworkPrinting
|
plugins/UM3NetworkPrinting
|
||||||
plugins/X3GWriter
|
plugins/X3GWriter
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
project(cura NONE)
|
project(cura NONE)
|
||||||
cmake_minimum_required(VERSION 2.8.12)
|
cmake_minimum_required(VERSION 2.8.12)
|
||||||
|
|
||||||
|
|
148
DiscoverUM3Action.py
Normal file
148
DiscoverUM3Action.py
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
from cura.MachineAction import MachineAction
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject
|
||||||
|
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
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"))
|
369
DiscoverUM3Action.qml
Normal file
369
DiscoverUM3Action.qml
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
import UM 1.2 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 1.1
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtQuick.Window 2.1
|
||||||
|
|
||||||
|
Cura.MachineAction
|
||||||
|
{
|
||||||
|
id: base
|
||||||
|
anchors.fill: parent;
|
||||||
|
property var selectedPrinter: null
|
||||||
|
property bool completeProperties: true
|
||||||
|
property var connectingToPrinter: null
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: dialog ? dialog : null
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
onNextClicked:
|
||||||
|
{
|
||||||
|
// Connect to the printer if the MachineAction is currently shown
|
||||||
|
if(base.parent.wizard == dialog)
|
||||||
|
{
|
||||||
|
connectToPrinter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectToPrinter()
|
||||||
|
{
|
||||||
|
if(base.selectedPrinter && base.completeProperties)
|
||||||
|
{
|
||||||
|
var printerKey = base.selectedPrinter.getKey()
|
||||||
|
if(connectingToPrinter != printerKey) {
|
||||||
|
// prevent an infinite loop
|
||||||
|
connectingToPrinter = printerKey;
|
||||||
|
manager.setKey(printerKey);
|
||||||
|
completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
anchors.fill: parent;
|
||||||
|
id: discoverUM3Action
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
|
SystemPalette { id: palette }
|
||||||
|
UM.I18nCatalog { id: catalog; name:"cura" }
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: pageTitle
|
||||||
|
width: parent.width
|
||||||
|
text: catalog.i18nc("@title:window", "Connect to Networked Printer")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.pointSize: 18
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: pageDescription
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: catalog.i18nc("@label", "To print directly to your printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your printer, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your printer from the list below:")
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
spacing: UM.Theme.getSize("default_lining").width
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: addButton
|
||||||
|
text: catalog.i18nc("@action:button", "Add");
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
manualPrinterDialog.showDialog("", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: editButton
|
||||||
|
text: catalog.i18nc("@action:button", "Edit")
|
||||||
|
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: removeButton
|
||||||
|
text: catalog.i18nc("@action:button", "Remove")
|
||||||
|
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||||
|
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: rediscoverButton
|
||||||
|
text: catalog.i18nc("@action:button", "Refresh")
|
||||||
|
onClicked: manager.restartDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
id: contentRow
|
||||||
|
width: parent.width
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
|
ScrollView
|
||||||
|
{
|
||||||
|
id: objectListContainer
|
||||||
|
frameVisible: true
|
||||||
|
width: parent.width
|
||||||
|
height: base.height - contentRow.y - discoveryTip.height
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
parent: viewport
|
||||||
|
anchors.fill: parent
|
||||||
|
color: palette.light
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: listview
|
||||||
|
model: manager.foundDevices
|
||||||
|
onModelChanged:
|
||||||
|
{
|
||||||
|
var selectedKey = manager.getStoredKey();
|
||||||
|
for(var i = 0; i < model.length; i++) {
|
||||||
|
if(model[i].getKey() == selectedKey)
|
||||||
|
{
|
||||||
|
currentIndex = i;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentIndex = -1;
|
||||||
|
}
|
||||||
|
width: parent.width
|
||||||
|
currentIndex: -1
|
||||||
|
onCurrentIndexChanged:
|
||||||
|
{
|
||||||
|
base.selectedPrinter = listview.model[currentIndex];
|
||||||
|
// Only allow connecting if the printer has responded to API query since the last refresh
|
||||||
|
base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
|
||||||
|
}
|
||||||
|
Component.onCompleted: manager.startDiscovery()
|
||||||
|
delegate: Rectangle
|
||||||
|
{
|
||||||
|
height: childrenRect.height
|
||||||
|
color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase
|
||||||
|
width: parent.width
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: listview.model[index].name
|
||||||
|
color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea
|
||||||
|
{
|
||||||
|
anchors.fill: parent;
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
if(!parent.ListView.isCurrentItem)
|
||||||
|
{
|
||||||
|
parent.ListView.view.currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: discoveryTip
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
//: Tips label
|
||||||
|
//TODO: get actual link from webteam
|
||||||
|
text: catalog.i18nc("@label", "If your printer is not listed, read the <a href='%1'>network-printing troubleshooting guide</a>").arg("https://ultimaker.com/en/troubleshooting");
|
||||||
|
onLinkActivated: Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
visible: base.selectedPrinter ? true : false
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: base.selectedPrinter ? base.selectedPrinter.name : ""
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Grid
|
||||||
|
{
|
||||||
|
visible: base.completeProperties
|
||||||
|
width: parent.width
|
||||||
|
columns: 2
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: catalog.i18nc("@label", "Type")
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text:
|
||||||
|
{
|
||||||
|
if(base.selectedPrinter)
|
||||||
|
{
|
||||||
|
if(base.selectedPrinter.printerType == "ultimaker3")
|
||||||
|
{
|
||||||
|
return catalog.i18nc("@label", "Ultimaker 3")
|
||||||
|
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
|
||||||
|
{
|
||||||
|
return catalog.i18nc("@label", "Ultimaker 3 Extended")
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: catalog.i18nc("@label", "Firmware version")
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: catalog.i18nc("@label", "Address")
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width * 0.5
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: base.selectedPrinter != null && !base.completeProperties
|
||||||
|
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:button", "Connect")
|
||||||
|
enabled: (base.selectedPrinter && base.completeProperties) ? true : false
|
||||||
|
onClicked: connectToPrinter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Dialog
|
||||||
|
{
|
||||||
|
id: manualPrinterDialog
|
||||||
|
property string printerKey
|
||||||
|
property alias addressText: addressField.text
|
||||||
|
|
||||||
|
title: catalog.i18nc("@title:window", "Printer Address")
|
||||||
|
|
||||||
|
minimumWidth: 400 * Screen.devicePixelRatio
|
||||||
|
minimumHeight: 120 * Screen.devicePixelRatio
|
||||||
|
width: minimumWidth
|
||||||
|
height: minimumHeight
|
||||||
|
|
||||||
|
signal showDialog(string key, string address)
|
||||||
|
onShowDialog:
|
||||||
|
{
|
||||||
|
printerKey = key;
|
||||||
|
|
||||||
|
addressText = address;
|
||||||
|
addressField.selectAll();
|
||||||
|
addressField.focus = true;
|
||||||
|
|
||||||
|
manualPrinterDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted:
|
||||||
|
{
|
||||||
|
manager.setManualPrinter(printerKey, addressText)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@alabel","Enter the IP address or hostname of your printer on the network.")
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField
|
||||||
|
{
|
||||||
|
id: addressField
|
||||||
|
width: parent.width
|
||||||
|
maximumLength: 40
|
||||||
|
validator: RegExpValidator
|
||||||
|
{
|
||||||
|
regExp: /[a-zA-Z0-9\.\-\_]*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rightButtons: [
|
||||||
|
Button {
|
||||||
|
text: catalog.i18nc("@action:button","Cancel")
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
manualPrinterDialog.reject()
|
||||||
|
manualPrinterDialog.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Button {
|
||||||
|
text: catalog.i18nc("@action:button", "Ok")
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
|
manualPrinterDialog.accept()
|
||||||
|
manualPrinterDialog.hide()
|
||||||
|
}
|
||||||
|
enabled: manualPrinterDialog.addressText.trim() != ""
|
||||||
|
isDefault: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
2
LICENSE
2
LICENSE
|
@ -658,4 +658,4 @@ specific requirements.
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
1019
NetworkPrinterOutputDevice.py
Normal file
1019
NetworkPrinterOutputDevice.py
Normal file
File diff suppressed because it is too large
Load diff
202
NetworkPrinterOutputDevicePlugin.py
Normal file
202
NetworkPrinterOutputDevicePlugin.py
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||||
|
from . import NetworkPrinterOutputDevice
|
||||||
|
|
||||||
|
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Signal import Signal, signalemitter
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
|
||||||
|
# Zero-Conf is used to detect printers, which are saved in a dict.
|
||||||
|
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||||
|
@signalemitter
|
||||||
|
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._zero_conf = None
|
||||||
|
self._browser = None
|
||||||
|
self._printers = {}
|
||||||
|
|
||||||
|
self._api_version = "1"
|
||||||
|
self._api_prefix = "/api/v" + self._api_version + "/"
|
||||||
|
|
||||||
|
self._network_manager = QNetworkAccessManager()
|
||||||
|
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
||||||
|
|
||||||
|
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
|
||||||
|
# authentication requests.
|
||||||
|
self._old_printers = []
|
||||||
|
|
||||||
|
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||||
|
self.addPrinterSignal.connect(self.addPrinter)
|
||||||
|
self.removePrinterSignal.connect(self.removePrinter)
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
|
||||||
|
|
||||||
|
# Get list of manual printers from preferences
|
||||||
|
self._preferences = Preferences.getInstance()
|
||||||
|
self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
|
||||||
|
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||||
|
|
||||||
|
addPrinterSignal = Signal()
|
||||||
|
removePrinterSignal = Signal()
|
||||||
|
printerListChanged = Signal()
|
||||||
|
|
||||||
|
## Start looking for devices on network.
|
||||||
|
def start(self):
|
||||||
|
self.startDiscovery()
|
||||||
|
|
||||||
|
def startDiscovery(self):
|
||||||
|
self.stop()
|
||||||
|
if self._browser:
|
||||||
|
self._browser.cancel()
|
||||||
|
self._browser = None
|
||||||
|
self._old_printers = [printer_name for printer_name in self._printers]
|
||||||
|
self._printers = {}
|
||||||
|
self.printerListChanged.emit()
|
||||||
|
# After network switching, one must make a new instance of Zeroconf
|
||||||
|
# On windows, the instance creation is very fast (unnoticable). Other platforms?
|
||||||
|
self._zero_conf = Zeroconf()
|
||||||
|
self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged])
|
||||||
|
|
||||||
|
# Look for manual instances from preference
|
||||||
|
for address in self._manual_instances:
|
||||||
|
if address:
|
||||||
|
self.addManualPrinter(address)
|
||||||
|
|
||||||
|
def addManualPrinter(self, address):
|
||||||
|
if address not in self._manual_instances:
|
||||||
|
self._manual_instances.append(address)
|
||||||
|
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||||
|
|
||||||
|
name = address
|
||||||
|
instance_name = "manual:%s" % address
|
||||||
|
properties = { b"name": name.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" }
|
||||||
|
|
||||||
|
if instance_name not in self._printers:
|
||||||
|
# Add a preliminary printer instance
|
||||||
|
self.addPrinter(instance_name, address, properties)
|
||||||
|
|
||||||
|
self.checkManualPrinter(address)
|
||||||
|
|
||||||
|
def removeManualPrinter(self, key, address = None):
|
||||||
|
if key in self._printers:
|
||||||
|
if not address:
|
||||||
|
address = self._printers[key].ipAddress
|
||||||
|
self.removePrinter(key)
|
||||||
|
|
||||||
|
if address in self._manual_instances:
|
||||||
|
self._manual_instances.remove(address)
|
||||||
|
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||||
|
|
||||||
|
def checkManualPrinter(self, address):
|
||||||
|
# Check if a printer exists at this address
|
||||||
|
# If a printer responds, it will replace the preliminary printer created above
|
||||||
|
url = QUrl("http://" + address + self._api_prefix + "system")
|
||||||
|
name_request = QNetworkRequest(url)
|
||||||
|
self._network_manager.get(name_request)
|
||||||
|
|
||||||
|
## Handler for all requests that have finished.
|
||||||
|
def _onNetworkRequestFinished(self, reply):
|
||||||
|
reply_url = reply.url().toString()
|
||||||
|
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||||
|
|
||||||
|
if reply.operation() == QNetworkAccessManager.GetOperation:
|
||||||
|
if "system" in reply_url: # Name returned from printer.
|
||||||
|
if status_code == 200:
|
||||||
|
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||||
|
address = reply.url().host()
|
||||||
|
name = ("%s (%s)" % (system_info["name"], address))
|
||||||
|
|
||||||
|
instance_name = "manual:%s" % address
|
||||||
|
properties = { b"name": name.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true" }
|
||||||
|
if instance_name in self._printers:
|
||||||
|
# Only replace the printer if it is still in the list of (manual) printers
|
||||||
|
self.removePrinter(instance_name)
|
||||||
|
self.addPrinter(instance_name, address, properties)
|
||||||
|
|
||||||
|
## Stop looking for devices on network.
|
||||||
|
def stop(self):
|
||||||
|
if self._zero_conf is not None:
|
||||||
|
self._zero_conf.close()
|
||||||
|
|
||||||
|
def getPrinters(self):
|
||||||
|
return self._printers
|
||||||
|
|
||||||
|
def reCheckConnections(self):
|
||||||
|
active_machine = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not active_machine:
|
||||||
|
return
|
||||||
|
|
||||||
|
for key in self._printers:
|
||||||
|
if key == active_machine.getMetaDataEntry("um_network_key"):
|
||||||
|
self._printers[key].connect()
|
||||||
|
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||||
|
else:
|
||||||
|
if self._printers[key].isConnected():
|
||||||
|
self._printers[key].close()
|
||||||
|
|
||||||
|
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||||
|
def addPrinter(self, name, address, properties):
|
||||||
|
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
|
||||||
|
self._printers[printer.getKey()] = printer
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||||
|
if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
|
||||||
|
self._printers[printer.getKey()].connect()
|
||||||
|
printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||||
|
self.printerListChanged.emit()
|
||||||
|
|
||||||
|
def removePrinter(self, name):
|
||||||
|
printer = self._printers.pop(name, None)
|
||||||
|
if printer:
|
||||||
|
if printer.isConnected():
|
||||||
|
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
||||||
|
printer.disconnect()
|
||||||
|
self.printerListChanged.emit()
|
||||||
|
|
||||||
|
## Handler for when the connection state of one of the detected printers changes
|
||||||
|
def _onPrinterConnectionStateChanged(self, key):
|
||||||
|
if key not in self._printers:
|
||||||
|
return
|
||||||
|
if self._printers[key].isConnected():
|
||||||
|
self.getOutputDeviceManager().addOutputDevice(self._printers[key])
|
||||||
|
else:
|
||||||
|
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||||
|
|
||||||
|
## Handler for zeroConf detection
|
||||||
|
def _onServiceChanged(self, zeroconf, service_type, name, state_change):
|
||||||
|
if state_change == ServiceStateChange.Added:
|
||||||
|
Logger.log("d", "Bonjour service added: %s" % name)
|
||||||
|
|
||||||
|
# First try getting info from zeroconf cache
|
||||||
|
info = ServiceInfo(service_type, name, properties = {})
|
||||||
|
for record in zeroconf.cache.entries_with_name(name.lower()):
|
||||||
|
info.update_record(zeroconf, time.time(), record)
|
||||||
|
|
||||||
|
for record in zeroconf.cache.entries_with_name(info.server):
|
||||||
|
info.update_record(zeroconf, time.time(), record)
|
||||||
|
if info.address:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Request more data if info is not complete
|
||||||
|
if not info.address:
|
||||||
|
Logger.log("d", "Trying to get address of %s", name)
|
||||||
|
info = zeroconf.get_service_info(service_type, name)
|
||||||
|
|
||||||
|
if info:
|
||||||
|
if info.properties.get(b"type", None) == b'printer':
|
||||||
|
address = '.'.join(map(lambda n: str(n), info.address))
|
||||||
|
self.addPrinterSignal.emit(str(name), address, info.properties)
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Could not get information about %s" % name)
|
||||||
|
|
||||||
|
elif state_change == ServiceStateChange.Removed:
|
||||||
|
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||||
|
self.removePrinterSignal.emit(str(name))
|
|
@ -1,3 +1,4 @@
|
||||||
|
<<<<<<< HEAD
|
||||||
Cura
|
Cura
|
||||||
====
|
====
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ Dependencies
|
||||||
This will be needed at runtime to perform the actual slicing.
|
This will be needed at runtime to perform the actual slicing.
|
||||||
* [PySerial](https://github.com/pyserial/pyserial)
|
* [PySerial](https://github.com/pyserial/pyserial)
|
||||||
Only required for USB printing support.
|
Only required for USB printing support.
|
||||||
|
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf)
|
||||||
|
Only required to detect mDNS-enabled printers
|
||||||
|
|
||||||
Configuring Cura
|
Configuring Cura
|
||||||
----------------
|
----------------
|
||||||
|
|
124
UM3InfoComponents.qml
Normal file
124
UM3InfoComponents.qml
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import UM 1.2 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 1.1
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtQuick.Window 2.1
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: base
|
||||||
|
|
||||||
|
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
|
||||||
|
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
|
||||||
|
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||||
|
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
objectName: "networkPrinterConnectButton"
|
||||||
|
visible: isUM3
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
height: UM.Theme.getSize("save_button_save_to_button").height
|
||||||
|
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer")
|
||||||
|
text: catalog.i18nc("@action:button", "Request Access")
|
||||||
|
style: UM.Theme.styles.sidebar_action_button
|
||||||
|
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication()
|
||||||
|
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
height: UM.Theme.getSize("save_button_save_to_button").height
|
||||||
|
tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer")
|
||||||
|
text: catalog.i18nc("@action:button", "Connect")
|
||||||
|
style: UM.Theme.styles.sidebar_action_button
|
||||||
|
onClicked: connectActionDialog.show()
|
||||||
|
visible: !printerConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Dialog
|
||||||
|
{
|
||||||
|
id: connectActionDialog
|
||||||
|
Loader
|
||||||
|
{
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "DiscoverUM3Action.qml"
|
||||||
|
}
|
||||||
|
rightButtons: Button
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:button", "Close")
|
||||||
|
iconName: "dialog-close"
|
||||||
|
onClicked: connectActionDialog.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
objectName: "networkPrinterConnectionInfo"
|
||||||
|
visible: isUM3
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer")
|
||||||
|
text: catalog.i18nc("@action:button", "Request Access")
|
||||||
|
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication()
|
||||||
|
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
visible: printerConnected
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
Repeater
|
||||||
|
{
|
||||||
|
model: Cura.ExtrudersModel { simpleNames: true }
|
||||||
|
Label { text: model.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
Repeater
|
||||||
|
{
|
||||||
|
id: nozzleColumn
|
||||||
|
model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].hotendIds : null
|
||||||
|
Label { text: nozzleColumn.model[index] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
Repeater
|
||||||
|
{
|
||||||
|
id: materialColumn
|
||||||
|
model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].materialNames : null
|
||||||
|
Label { text: materialColumn.model[index] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura")
|
||||||
|
text: catalog.i18nc("@action:button", "Activate Configuration")
|
||||||
|
visible: printerConnected
|
||||||
|
onClicked: manager.loadConfigurationFromPrinter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.I18nCatalog{id: catalog; name:"cura"}
|
||||||
|
}
|
20
__init__.py
Normal file
20
__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the AGPLv3 or higher.
|
||||||
|
from . import NetworkPrinterOutputDevicePlugin
|
||||||
|
from . import DiscoverUM3Action
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {
|
||||||
|
"plugin": {
|
||||||
|
"name": "UM3 Network Connection",
|
||||||
|
"author": "Ultimaker",
|
||||||
|
"description": catalog.i18nc("@info:whatsthis", "Manages network connections to Ultimaker 3 printers"),
|
||||||
|
"version": "1.0",
|
||||||
|
"api": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
Loading…
Add table
Add a link
Reference in a new issue