diff --git a/plugins/USBPrinting/PrinterConnection.py b/plugins/USBPrinting/PrinterConnection.py index cc4a04c275..d19e5d2f19 100644 --- a/plugins/USBPrinting/PrinterConnection.py +++ b/plugins/USBPrinting/PrinterConnection.py @@ -8,96 +8,143 @@ import time import queue import re import functools +import os +import os.path from UM.Application import Application from UM.Signal import Signal, SignalEmitter from UM.Resources import Resources from UM.Logger import Logger +from UM.OutputDevice.OutputDevice import OutputDevice +from UM.OutputDevice import OutputDeviceError +from UM.PluginRegistry import PluginRegistry + +from PyQt5.QtQuick import QQuickView +from PyQt5.QtQml import QQmlComponent, QQmlContext +from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + +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") -class PrinterConnection(SignalEmitter): - def __init__(self, serial_port): - super().__init__() - 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 - + # 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 = 10 - + 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.demon = 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 - + # 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() + + @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): + 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): @@ -115,20 +162,20 @@ class PrinterConnection(SignalEmitter): 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() - + ## 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: @@ -173,10 +220,10 @@ class PrinterConnection(SignalEmitter): def _connect(self): Logger.log("d", "Attempting to connect to %s", self._serial_port) self._is_connecting = True - programmer = stk500v2.Stk500v2() + 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() + 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: @@ -186,31 +233,28 @@ class PrinterConnection(SignalEmitter): self._is_connecting = False Logger.log("i", "Could not establish connection on %s, unknown reasons.", self._serial_port) return - + # 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) - if self._serial is None: try: - self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout=3, writeTimeout=10000) + 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) return - else: + 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() + 15 + 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 self._sendCommand("M105") @@ -240,21 +284,12 @@ class PrinterConnection(SignalEmitter): self.connectionStateChanged.emit(self._serial_port) if self._is_connected: self._listen_thread.start() #Start listening - #Application.getInstance().addOutputDevice(self._serial_port, { - #"id": self._serial_port, - #"function": self.printGCode, - #"shortDescription": "Print with USB", - #"description": "Print with USB {0}".format(self._serial_port), - #"icon": "save", - #"priority": 1 - #}) - else: Logger.log("w", "Printer connection state was not changed") - - connectionStateChanged = Signal() - - ## Close the printer connection + + connectionStateChanged = Signal() + + ## Close the printer connection def close(self): if self._connect_thread.isAlive(): try: @@ -269,12 +304,12 @@ class PrinterConnection(SignalEmitter): except: pass self._serial.close() - + self._serial = None - + def isConnected(self): return self._is_connected - + ## Directly send the command, withouth checking connection state (eg; printing). # \param cmd string with g-code def _sendCommand(self, cmd): @@ -296,10 +331,9 @@ class PrinterConnection(SignalEmitter): self._target_bed_temperature = float(re.search("S([0-9]+)", cmd).group(1)) except: pass - #Logger.log("i","Sending: %s" % (cmd)) try: command = (cmd + "\n").encode() - #self._serial.write(b"\n") + self._serial.write(b"\n") self._serial.write(command) except serial.SerialTimeoutException: Logger.log("w","Serial timeout while writing to serial port, trying again.") @@ -314,11 +348,26 @@ class PrinterConnection(SignalEmitter): 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: + 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): @@ -326,37 +375,33 @@ class PrinterConnection(SignalEmitter): 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(error) - - onError = Signal() - + 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.onExtruderTemperatureChange.emit(self._serial_port, index, temperature) + self.extruderTemperatureChanged.emit() except Exception as e: pass - - onExtruderTemperatureChange = Signal() - + ## 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.onBedTemperatureChange.emit(self._serial_port,temperature) - - onBedTemperatureChange = Signal() - - + self.bedTemperatureChanged.emit() + + def requestWrite(self, node): + self.showControlInterface() + ## Listen thread function. def _listen(self): Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port) @@ -364,10 +409,10 @@ class PrinterConnection(SignalEmitter): 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 line.startswith(b"Error:"): # Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" @@ -401,10 +446,10 @@ class PrinterConnection(SignalEmitter): else: self.sendCommand("M105") temperature_request_timeout = time.time() + 5 - + 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(): @@ -449,26 +494,25 @@ class PrinterConnection(SignalEmitter): 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(self._progress, self._serial_port) - - progressChanged = Signal() - + 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(self._progress, self._serial_port) - + 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") @@ -477,7 +521,7 @@ class PrinterConnection(SignalEmitter): ## 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: @@ -490,7 +534,7 @@ class PrinterConnection(SignalEmitter): self.close() return None return ret - + ## Create a list of baud rates at which we can communicate. # \return list of int def _getBaudrateList(self): diff --git a/plugins/USBPrinting/USBPrinterManager.py b/plugins/USBPrinting/USBPrinterManager.py index 066ae585da..21c7fb82c2 100644 --- a/plugins/USBPrinting/USBPrinterManager.py +++ b/plugins/USBPrinting/USBPrinterManager.py @@ -9,6 +9,7 @@ from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry +from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin import threading import platform @@ -26,34 +27,43 @@ from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") -class USBPrinterManager(QObject, SignalEmitter, Extension): +class USBPrinterManager(QObject, SignalEmitter, OutputDevicePlugin, Extension): def __init__(self, parent = None): - super().__init__(parent) + QObject.__init__(self, parent) + SignalEmitter.__init__(self) + OutputDevicePlugin.__init__(self) + Extension.__init__(self) self._serial_port_list = [] - self._printer_connections = [] - self._check_ports_thread = threading.Thread(target = self._updateConnectionList) - self._check_ports_thread.daemon = True - self._check_ports_thread.start() - - self._progress = 0 + self._printer_connections = {} + self._update_thread = threading.Thread(target = self._updateThread) + self._update_thread.setDaemon(True) - self._control_view = None + self._check_updates = True self._firmware_view = None - self._extruder_temp = 0 - self._bed_temp = 0 - self._error_message = "" - + ## Add menu item to top menu of the application. self.setMenuName("Firmware") self.addMenuItem(i18n_catalog.i18n("Update Firmware"), self.updateAllFirmware) - Application.getInstance().applicationShuttingDown.connect(self._onApplicationShuttingDown) - - pyqtError = pyqtSignal(str, arguments = ["error"]) - processingProgress = pyqtSignal(float, arguments = ["amount"]) - pyqtExtruderTemperature = pyqtSignal(float, arguments = ["amount"]) - pyqtBedTemperature = pyqtSignal(float, arguments = ["amount"]) - + 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() + + def start(self): + self._check_updates = True + self._update_thread.start() + + def stop(self): + self._check_updates = False + self._update_thread.join() + + 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): @@ -66,65 +76,7 @@ class USBPrinterManager(QObject, SignalEmitter, Extension): self._firmware_view = component.create(self._firmware_context) self._firmware_view.show() - - ## Show control interface. - # This will create the view if its not already created. - def spawnControlInterface(self,serial_port): - 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) - - self._control_view.show() - - @pyqtProperty(float,notify = processingProgress) - def progress(self): - return self._progress - - @pyqtProperty(float,notify = pyqtExtruderTemperature) - def extruderTemperature(self): - return self._extruder_temp - - @pyqtProperty(float,notify = pyqtBedTemperature) - def bedTemperature(self): - return self._bed_temp - - @pyqtProperty(str,notify = pyqtError) - def error(self): - return self._error_message - - ## Check all serial ports and create a PrinterConnection object for them. - # Note that this does not validate if the serial ports are actually usable! - # This (the validation) is only done when the connect function is called. - def _updateConnectionList(self): - while True: - temp_serial_port_list = self.getSerialPortList(only_list_usb = True) - if temp_serial_port_list != self._serial_port_list: # Something changed about the list since we last changed something. - disconnected_ports = [port for port in self._serial_port_list if port not in temp_serial_port_list ] - self._serial_port_list = temp_serial_port_list - for serial_port in self._serial_port_list: - if self.getConnectionByPort(serial_port) is None: # If it doesn't already exist, add it - if not os.path.islink(serial_port): # Only add the connection if it's a non symbolic link - connection = PrinterConnection.PrinterConnection(serial_port) - connection.connect() - connection.connectionStateChanged.connect(self.serialConectionStateCallback) - connection.progressChanged.connect(self.onProgress) - connection.onExtruderTemperatureChange.connect(self.onExtruderTemperature) - connection.onBedTemperatureChange.connect(self.onBedTemperature) - connection.onError.connect(self.onError) - self._printer_connections.append(connection) - - for serial_port in disconnected_ports: # Close connections and remove them from list. - connection = self.getConnectionByPort(serial_port) - if connection != None: - self._printer_connections.remove(connection) - connection.close() - time.sleep(5) # Throttle, as we don"t need this information to be updated every single second. - def updateAllFirmware(self): self.spawnFirmwareInterface("") for printer_connection in self._printer_connections: @@ -132,13 +84,13 @@ class USBPrinterManager(QObject, SignalEmitter, Extension): printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName())) except FileNotFoundError: continue - + def updateFirmwareBySerial(self, serial_port): printer_connection = self.getConnectionByPort(serial_port) if printer_connection is not None: self.spawnFirmwareInterface(printer_connection.getSerialPort()) printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName())) - + def _getDefaultFirmwareName(self): machine_type = Application.getInstance().getActiveMachine().getTypeID() firmware_name = "" @@ -160,120 +112,35 @@ class USBPrinterManager(QObject, SignalEmitter, Extension): return "MarlinUltimaker2.hex" ##TODO: Add check for multiple extruders - + if firmware_name != "": firmware_name += ".hex" return firmware_name - ## Callback for extruder temperature change - def onExtruderTemperature(self, serial_port, index, temperature): - self._extruder_temp = temperature - self.pyqtExtruderTemperature.emit(temperature) - - ## Callback for bed temperature change - def onBedTemperature(self, serial_port,temperature): - self._bed_temp = temperature - self.pyqtBedTemperature.emit(temperature) - - ## Callback for error - def onError(self, error): - self._error_message = error if type(error) is str else error.decode("utf-8") - self.pyqtError.emit(self._error_message) - - ## Callback for progress change - def onProgress(self, progress, serial_port): - self._progress = progress - self.processingProgress.emit(progress) + 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) - ## Attempt to connect with all possible connections. - def connectAllConnections(self): - for connection in self._printer_connections: - connection.connect() - - ## Send gcode to printer and start printing - def sendGCodeByPort(self, serial_port, gcode_list): - printer_connection = self.getConnectionByPort(serial_port) - if printer_connection is not None: - printer_connection.printGCode(gcode_list) - return True - return False - - @pyqtSlot() - def cancelPrint(self): - for printer_connection in self.getActiveConnections(): - printer_connection.cancelPrint() - - ## Send gcode to all active printers. - # \return True if there was at least one active connection. - def sendGCodeToAllActive(self, gcode_list): - for printer_connection in self.getActiveConnections(): - printer_connection.printGCode(gcode_list) - if len(self.getActiveConnections()): - return True + ## 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) + 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: - return False - - ## Send a command to a printer indentified by port - # \param serial port String indentifieing the port - # \param command String with the g-code command to send. - # \return True if connection was found, false otherwise - def sendCommandByPort(self, serial_port, command): - printer_connection = self.getConnectionByPort(serial_port) - if printer_connection is not None: - printer_connection.sendCommand(command) - return True - return False - - ## Send a command to all active (eg; connected) printers - # \param command String with the g-code command to send. - # \return True if at least one connection was found, false otherwise - def sendCommandToAllActive(self, command): - for printer_connection in self.getActiveConnections(): - printer_connection.sendCommand(command) - if len(self.getActiveConnections()): - return True - else: - return False - - ## Callback if the connection state of a connection is changed. - # This adds or removes the connection as a possible output device. - def serialConectionStateCallback(self, serial_port): - connection = self.getConnectionByPort(serial_port) - if connection.isConnected(): - Application.getInstance().addOutputDevice(serial_port, { - "id": serial_port, - "function": self.spawnControlInterface, - "description": "Print with USB {0}".format(serial_port), - "shortDescription": "Print with USB", - "icon": "save", - "priority": 1 - }) - else: - Application.getInstance().removeOutputDevice(serial_port) - - @pyqtSlot() - def startPrint(self): - gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list", None) - if gcode_list: - final_list = [] - for gcode in gcode_list: - final_list += gcode.split("\n") - self.sendGCodeToAllActive(gcode_list) - - ## Get a list of printer connection objects that are connected. - def getActiveConnections(self): - return [connection for connection in self._printer_connections if connection.isConnected()] - - ## Get a printer connection object by serial port - def getConnectionByPort(self, serial_port): - for printer_connection in self._printer_connections: - if serial_port == printer_connection.getSerialPort(): - return printer_connection - return None + self.getOutputDeviceManager().removeOutputDevice(serial_port) ## 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): + def getSerialPortList(self, only_list_usb = False): base_list = [] if platform.system() == "Windows": import winreg @@ -293,8 +160,4 @@ class USBPrinterManager(QObject, SignalEmitter, Extension): 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 base_list - - def _onApplicationShuttingDown(self): - for connection in self._printer_connections: - connection.close() + return list(base_list) \ No newline at end of file diff --git a/plugins/USBPrinting/__init__.py b/plugins/USBPrinting/__init__.py index 1afa25c8a2..baf4ec6eb4 100644 --- a/plugins/USBPrinting/__init__.py +++ b/plugins/USBPrinting/__init__.py @@ -13,9 +13,10 @@ def getMetaData(): "name": "USB printing", "author": "Ultimaker", "version": "1.0", + "api": 2, "description": i18n_catalog.i18nc("USB Printing plugin description","Accepts G-Code and sends them to a printer. Plugin can also update firmware") } } - + def register(app): - return {"extension":USBPrinterManager.USBPrinterManager()} + return {"extension":USBPrinterManager.USBPrinterManager(),"output_device": USBPrinterManager.USBPrinterManager() }