Advertisement
Guest User

Untitled

a guest
Aug 30th, 2017
115
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C 34.76 KB | None | 0 0
  1. from UM.i18n import i18nCatalog
  2. from UM.Application import Application
  3. from UM.Logger import Logger
  4. from UM.Signal import signalemitter
  5. from UM.Message import Message
  6. from UM.Util import parseBool
  7.  
  8. from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
  9.  
  10. from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
  11. from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication
  12. from PyQt5.QtGui import QImage, QDesktopServices
  13.  
  14. import json
  15. import os.path
  16. from time import time
  17.  
  18. i18n_catalog = i18nCatalog("cura")
  19.  
  20.  
  21. ##  OctoPrint connected (wifi / lan) printer using the OctoPrint API
  22. @signalemitter
  23. class OctoPrintOutputDevice(PrinterOutputDevice):
  24.     def __init__(self, key, address, port, properties):
  25.         super().__init__(key)
  26.  
  27.         self._address = address
  28.         self._port = port
  29.         self._path = properties.get(b"path", b"/").decode("utf-8")
  30.         if self._path[-1:] != "/":
  31.             self._path += "/"
  32.         self._key = key
  33.         self._properties = properties  # Properties dict as provided by zero conf
  34.  
  35.         self._gcode = None
  36.         self._auto_print = True
  37.  
  38.         # We start with a single extruder, but update this when we get data from octoprint
  39.         self._num_extruders_set = False
  40.         self._num_extruders = 1
  41.  
  42.         # Try to get version information from plugin.json
  43.         plugin_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugin.json")
  44.         try:
  45.             with open(plugin_file_path) as plugin_file:
  46.                 plugin_info = json.load(plugin_file)
  47.                 plugin_version = plugin_info["version"]
  48.         except:
  49.             # The actual version info is not critical to have so we can continue
  50.             plugin_version = "Unknown"
  51.             Logger.logException("w", "Could not get version information for the plugin")
  52.  
  53.         self._user_agent_header = "User-Agent".encode()
  54.         self._user_agent = ("%s/%s %s/%s" % (
  55.             Application.getInstance().getApplicationName(),
  56.             Application.getInstance().getVersion(),
  57.             "OctoPrintPlugin",
  58.             Application.getInstance().getVersion()
  59.         )).encode()
  60.  
  61.         self._api_prefix = "api/"
  62.         self._api_header = "X-Api-Key".encode()
  63.         self._api_key = None
  64.  
  65.         self._protocol = "https" if properties.get(b'useHttps') == b"true" else "http"
  66.         self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path)
  67.         self._api_url = self._base_url + self._api_prefix
  68.  
  69.         self._basic_auth_header = "Authentication".encode()
  70.         self._basic_auth_data = None
  71.         basic_auth_username = properties.get(b"userName", b"").decode("utf-8")
  72.         basic_auth_password = properties.get(b"password", b"").decode("utf-8")
  73.         if basic_auth_username and basic_auth_password:
  74.             self._basic_auth_data = ("%s:%s" % (basic_auth_username, basic_auth_username)).encode()
  75.  
  76.         self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
  77.  
  78.         self.setPriority(2) # Make sure the output device gets selected above local file output
  79.         self.setName(key)
  80.         self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
  81.         self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
  82.         self.setIconName("print")
  83.         self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))
  84.  
  85.         #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
  86.         #   hook itself into the event loop, which results in events never being fired / done.
  87.         self._manager = QNetworkAccessManager()
  88.         self._manager.finished.connect(self._onRequestFinished)
  89.  
  90.         ##  Ensure that the qt networking stuff isn't garbage collected (unless we want it to)
  91.         self._settings_reply = None
  92.         self._printer_reply = None
  93.         self._job_reply = None
  94.         self._command_reply = None
  95.  
  96.         self._image_reply = None
  97.         self._stream_buffer = b""
  98.         self._stream_buffer_start_index = -1
  99.  
  100.         self._post_reply = None
  101.         self._post_multi_part = None
  102.         self._post_part = None
  103.  
  104.         self._progress_message = None
  105.         self._error_message = None
  106.         self._connection_message = None
  107.  
  108.         self._update_timer = QTimer()
  109.         self._update_timer.setInterval(2000)  # TODO; Add preference for update interval
  110.         self._update_timer.setSingleShot(False)
  111.         self._update_timer.timeout.connect(self._update)
  112.  
  113.         self._camera_image_id = 0
  114.         self._camera_image = QImage()
  115.         self._camera_mirror = ""
  116.         self._camera_rotation = 0
  117.         self._camera_url = ""
  118.         self._camera_shares_proxy = False
  119.  
  120.         self._sd_supported = False
  121.  
  122.         self._connection_state_before_timeout = None
  123.  
  124.         self._last_response_time = None
  125.         self._last_request_time = None
  126.         self._response_timeout_time = 5
  127.         self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec.
  128.         self._recreate_network_manager_count = 1
  129.  
  130.         self._preheat_timer = QTimer()
  131.         self._preheat_timer.setSingleShot(True)
  132.         self._preheat_timer.timeout.connect(self.cancelPreheatBed)
  133.  
  134.     def getProperties(self):
  135.         return self._properties
  136.  
  137.     @pyqtSlot(str, result = str)
  138.     def getProperty(self, key):
  139.         key = key.encode("utf-8")
  140.         if key in self._properties:
  141.             return self._properties.get(key, b"").decode("utf-8")
  142.         else:
  143.             return ""
  144.  
  145.     ##  Get the unique key of this machine
  146.     #   \return key String containing the key of the machine.
  147.     @pyqtSlot(result = str)
  148.     def getKey(self):
  149.         return self._key
  150.  
  151.     ##  Set the API key of this OctoPrint instance
  152.     def setApiKey(self, api_key):
  153.         self._api_key = api_key.encode()
  154.  
  155.     ##  Name of the instance (as returned from the zeroConf properties)
  156.     @pyqtProperty(str, constant = True)
  157.     def name(self):
  158.         return self._key
  159.  
  160.     ##  Version (as returned from the zeroConf properties)
  161.     @pyqtProperty(str, constant=True)
  162.     def octoprintVersion(self):
  163.         return self._properties.get(b"version", b"").decode("utf-8")
  164.  
  165.     ## IPadress of this instance
  166.     @pyqtProperty(str, constant=True)
  167.     def ipAddress(self):
  168.         return self._address
  169.  
  170.     ## port of this instance
  171.     @pyqtProperty(int, constant=True)
  172.     def port(self):
  173.         return self._port
  174.  
  175.     ## path of this instance
  176.     @pyqtProperty(str, constant=True)
  177.     def path(self):
  178.         return self._path
  179.  
  180.     ## absolute url of this instance
  181.     @pyqtProperty(str, constant=True)
  182.     def baseURL(self):
  183.         return self._base_url
  184.  
  185.     cameraOrientationChanged = pyqtSignal()
  186.  
  187.     @pyqtProperty("QVariantMap", notify = cameraOrientationChanged)
  188.     def cameraOrientation(self):
  189.         return {
  190.             "mirror": self._camera_mirror,
  191.             "rotation": self._camera_rotation,
  192.         }
  193.  
  194.     def _startCamera(self):
  195.         global_container_stack = Application.getInstance().getGlobalContainerStack()
  196.         if not global_container_stack or not parseBool(global_container_stack.getMetaDataEntry("octoprint_show_camera", False)) or self._camera_url == "":
  197.             return
  198.  
  199.         # Start streaming mjpg stream
  200.         url = QUrl(self._camera_url)
  201.         image_request = QNetworkRequest(url)
  202.         image_request.setRawHeader(self._user_agent_header, self._user_agent)
  203.         if self._camera_shares_proxy and self._basic_auth_data:
  204.             image_request.setRawHeader(self._basic_auth_header, self._basic_auth_data)
  205.         self._image_reply = self._manager.get(image_request)
  206.         self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
  207.  
  208.     def _stopCamera(self):
  209.         if self._image_reply:
  210.             self._image_reply.abort()
  211.             self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
  212.             self._image_reply = None
  213.         image_request = None
  214.  
  215.         self._stream_buffer = b""
  216.         self._stream_buffer_start_index = -1
  217.  
  218.         self._camera_image = QImage()
  219.         self.newImage.emit()
  220.  
  221.     def _update(self):
  222.         if self._last_response_time:
  223.             time_since_last_response = time() - self._last_response_time
  224.         else:
  225.             time_since_last_response = 0
  226.         if self._last_request_time:
  227.             time_since_last_request = time() - self._last_request_time
  228.         else:
  229.             time_since_last_request = float("inf") # An irrelevantly large number of seconds
  230.  
  231.         # Connection is in timeout, check if we need to re-start the connection.
  232.         # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
  233.         # Re-creating the QNetworkManager seems to fix this issue.
  234.         if self._last_response_time and self._connection_state_before_timeout:
  235.             if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
  236.                 self._recreate_network_manager_count += 1
  237.                 # It can happen that we had a very long timeout (multiple times the recreate time).
  238.                 # In that case we should jump through the point that the next update won't be right away.
  239.                 while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
  240.                     self._recreate_network_manager_count += 1
  241.                 Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response)
  242.                 self._createNetworkManager()
  243.                 return
  244.  
  245.         # Check if we have an connection in the first place.
  246.         if not self._manager.networkAccessible():
  247.             if not self._connection_state_before_timeout:
  248.                 Logger.log("d", "The network connection seems to be disabled. Going into timeout mode")
  249.                 self._connection_state_before_timeout = self._connection_state
  250.                 self.setConnectionState(ConnectionState.error)
  251.                 self._connection_message = Message(i18n_catalog.i18nc("@info:status",
  252.                                                                       "The connection with the network was lost."))
  253.                 self._connection_message.show()
  254.                 # Check if we were uploading something. Abort if this is the case.
  255.                 # Some operating systems handle this themselves, others give weird issues.
  256.                 try:
  257.                     if self._post_reply:
  258.                         Logger.log("d", "Stopping post upload because the connection was lost.")
  259.                         try:
  260.                             self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
  261.                         except TypeError:
  262.                             pass  # The disconnection can fail on mac in some cases. Ignore that.
  263.  
  264.                         self._post_reply.abort()
  265.                         self._progress_message.hide()
  266.                 except RuntimeError:
  267.                     self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
  268.             return
  269.         else:
  270.             if not self._connection_state_before_timeout:
  271.                 self._recreate_network_manager_count = 1
  272.  
  273.         # Check that we aren't in a timeout state
  274.         if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
  275.             if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
  276.                 # Go into timeout state.
  277.                 Logger.log("d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time)
  278.                 self._connection_state_before_timeout = self._connection_state
  279.                 self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections."))
  280.                 self._connection_message.show()
  281.                 self.setConnectionState(ConnectionState.error)
  282.  
  283.         ## Request 'general' printer data
  284.         self._printer_reply = self._manager.get(self._createApiRequest("printer"))
  285.  
  286.         ## Request print_job data
  287.         self._job_reply = self._manager.get(self._createApiRequest("job"))
  288.  
  289.     def _createNetworkManager(self):
  290.         if self._manager:
  291.             self._manager.finished.disconnect(self._onRequestFinished)
  292.  
  293.         self._manager = QNetworkAccessManager()
  294.         self._manager.finished.connect(self._onRequestFinished)
  295.  
  296.     def _createApiRequest(self, end_point):
  297.         request = QNetworkRequest(QUrl(self._api_url + end_point))
  298.         request.setRawHeader(self._user_agent_header, self._user_agent)
  299.         request.setRawHeader(self._api_header, self._api_key)
  300.         if self._basic_auth_data:
  301.             request.setRawHeader(self._basic_auth_header, self._basic_auth_data)
  302.         return request
  303.  
  304.     def close(self):
  305.         self._updateJobState("")
  306.         self.setConnectionState(ConnectionState.closed)
  307.         if self._progress_message:
  308.             self._progress_message.hide()
  309.         if self._error_message:
  310.             self._error_message.hide()
  311.         self._update_timer.stop()
  312.  
  313.         self._stopCamera()
  314.  
  315.     def requestWrite(self, node, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
  316.         self.writeStarted.emit(self)
  317.         self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
  318.  
  319.         self.startPrint()
  320.  
  321.     def isConnected(self):
  322.         return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
  323.  
  324.     ##  Start requesting data from the instance
  325.     def connect(self):
  326.         self._createNetworkManager()
  327.  
  328.         self.setConnectionState(ConnectionState.connecting)
  329.         self._update()  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
  330.         Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url)
  331.         self._update_timer.start()
  332.  
  333.         self._last_response_time = None
  334.         self.setAcceptsCommands(False)
  335.         self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format(self._key))
  336.  
  337.         ## Request 'settings' dump
  338.         self._settings_reply = self._manager.get(self._createApiRequest("settings"))
  339.  
  340.     ##  Stop requesting data from the instance
  341.     def disconnect(self):
  342.         Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url)
  343.         self.close()
  344.  
  345.     newImage = pyqtSignal()
  346.  
  347.     @pyqtProperty(QUrl, notify = newImage)
  348.     def cameraImage(self):
  349.         self._camera_image_id += 1
  350.         # There is an image provider that is called "camera". In order to ensure that the image qml object, that
  351.         # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
  352.         # as new (instead of relying on cached version and thus forces an update.
  353.         temp = "image://camera/" + str(self._camera_image_id)
  354.         return QUrl(temp, QUrl.TolerantMode)
  355.  
  356.     def getCameraImage(self):
  357.         return self._camera_image
  358.  
  359.     def _setJobState(self, job_state):
  360.         if job_state == "abort":
  361.             command = "cancel"
  362.         elif job_state == "print":
  363.             if self.jobState == "paused":
  364.                 command = "pause"
  365.             else:
  366.                 command = "start"
  367.         elif job_state == "pause":
  368.             command = "pause"
  369.  
  370.         if command:
  371.             self._sendJobCommand(command)
  372.  
  373.     def startPrint(self):
  374.         global_container_stack = Application.getInstance().getGlobalContainerStack()
  375.         if not global_container_stack:
  376.             return
  377.  
  378.         self._auto_print = parseBool(global_container_stack.getMetaDataEntry("octoprint_auto_print", True))
  379.  
  380.         if self.jobState not in ["ready", ""]:
  381.             if self.jobState == "offline":
  382.                 self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is offline. Unable to start a new job."))
  383.             elif self._auto_print:
  384.                 self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job."))
  385.             else:
  386.                 # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled
  387.                 self._error_message = None
  388.  
  389.             if self._error_message:
  390.                 self._error_message.show()
  391.                 return
  392.  
  393.         self._preheat_timer.stop()
  394.  
  395.         if self._auto_print:
  396.             Application.getInstance().showPrintMonitor.emit(True)
  397.  
  398.         try:
  399.             self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1)
  400.             self._progress_message.addAction("Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
  401.             self._progress_message.actionTriggered.connect(self._cancelSendGcode)
  402.             self._progress_message.show()
  403.  
  404.             ## Mash the data into single string
  405.             single_string_file_data = ""
  406.             last_process_events = time()
  407.             for line in self._gcode:
  408.                 single_string_file_data += line
  409.                 if time() > last_process_events + 0.05:
  410.                     # Ensure that the GUI keeps updated at least 20 times per second.
  411.                     QCoreApplication.processEvents()
  412.                     last_process_events = time()
  413.  
  414.             job_name = Application.getInstance().getPrintInformation().jobName.strip()
  415.             if job_name is "":
  416.                 job_name = "untitled_print"
  417.             file_name = "%s.gcode" % job_name
  418.  
  419.             ##  Create multi_part request
  420.             self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
  421.  
  422.             ##  Create parts (to be placed inside multipart)
  423.             self._post_part = QHttpPart()
  424.             self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
  425.             self._post_part.setBody(b"true")
  426.             self._post_multi_part.append(self._post_part)
  427.  
  428.             if self._auto_print:
  429.                 self._post_part = QHttpPart()
  430.                 self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
  431.                 self._post_part.setBody(b"true")
  432.                 self._post_multi_part.append(self._post_part)
  433.  
  434.             self._post_part = QHttpPart()
  435.             self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name)
  436.             self._post_part.setBody(single_string_file_data.encode())
  437.             self._post_multi_part.append(self._post_part)
  438.  
  439.             destination = "local"
  440.             if self._sd_supported and parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)):
  441.                 destination = "sdcard"
  442.  
  443.             ##  Post request + data
  444.             post_request = self._createApiRequest("files/" + destination)
  445.             self._post_reply = self._manager.post(post_request, self._post_multi_part)
  446.             self._post_reply.uploadProgress.connect(self._onUploadProgress)
  447.  
  448.             self._gcode = None
  449.  
  450.         except IOError:
  451.             self._progress_message.hide()
  452.             self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint."))
  453.             self._error_message.show()
  454.         except Exception as e:
  455.             self._progress_message.hide()
  456.             Logger.log("e", "An exception occurred in network connection: %s" % str(e))
  457.  
  458.     def _cancelSendGcode(self, message_id, action_id):
  459.         if self._post_reply:
  460.             Logger.log("d", "Stopping upload because the user pressed cancel.")
  461.             try:
  462.                 self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
  463.             except TypeError:
  464.                 pass  # The disconnection can fail on mac in some cases. Ignore that.
  465.  
  466.             self._post_reply.abort()
  467.             self._post_reply = None
  468.         self._progress_message.hide()
  469.  
  470.     def _sendCommand(self, command):
  471.         self._sendCommandToApi("printer/command", command)
  472.         Logger.log("d", "Sent gcode command to OctoPrint instance: %s", command)
  473.  
  474.     def _sendJobCommand(self, command):
  475.         self._sendCommandToApi("job", command)
  476.         Logger.log("d", "Sent job command to OctoPrint instance: %s", command)
  477.  
  478.     def _sendCommandToApi(self, end_point, command):
  479.         command_request = self._createApiRequest(end_point)
  480.         command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
  481.  
  482.         data = "{\"command\": \"%s\"}" % command
  483.         self._command_reply = self._manager.post(command_request, data.encode())
  484.  
  485.     ##  Pre-heats the heated bed of the printer.
  486.     #
  487.     #   \param temperature The temperature to heat the bed to, in degrees
  488.     #   Celsius.
  489.     #   \param duration How long the bed should stay warm, in seconds.
  490.     @pyqtSlot(float, float)
  491.     def preheatBed(self, temperature, duration):
  492.         self._setTargetBedTemperature(temperature)
  493.         if duration > 0:
  494.             self._preheat_timer.setInterval(duration * 1000)
  495.             self._preheat_timer.start()
  496.         else:
  497.             self._preheat_timer.stop()
  498.  
  499.     ##  Cancels pre-heating the heated bed of the printer.
  500.     #
  501.     #   If the bed is not pre-heated, nothing happens.
  502.     @pyqtSlot()
  503.     def cancelPreheatBed(self):
  504.         self._setTargetBedTemperature(0)
  505.         self._preheat_timer.stop()
  506.  
  507.     def _setTargetBedTemperature(self, temperature):
  508.         Logger.log("d", "Setting bed temperature to %s", temperature)
  509.         self._sendCommand("M140 S%s" % temperature)
  510.  
  511.     def _setTargetHotendTemperature(self, index, temperature):
  512.         Logger.log("d", "Setting hotend %s temperature to %s", index, temperature)
  513.         self._sendCommand("M104 T%s S%s" % (index, temperature))
  514.  
  515.     def _setHeadPosition(self, x, y , z, speed):
  516.         self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
  517.  
  518.     def _setHeadX(self, x, speed):
  519.         self._sendCommand("G0 X%s F%s" % (x, speed))
  520.  
  521.     def _setHeadY(self, y, speed):
  522.         self._sendCommand("G0 Y%s F%s" % (y, speed))
  523.  
  524.     def _setHeadZ(self, z, speed):
  525.         self._sendCommand("G0 Y%s F%s" % (z, speed))
  526.  
  527.     def _homeHead(self):
  528.         self._sendCommand("G28")
  529.  
  530.     def _homeBed(self):
  531.         self._sendCommand("G28 Z")
  532.  
  533.     def _moveHead(self, x, y, z, speed):
  534.         self._sendCommand("G91")
  535.         self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
  536.         self._sendCommand("G90")
  537.  
  538.     ##  Handler for all requests that have finished.
  539.     def _onRequestFinished(self, reply):
  540.         if reply.error() == QNetworkReply.TimeoutError:
  541.             Logger.log("w", "Received a timeout on a request to the instance")
  542.             self._connection_state_before_timeout = self._connection_state
  543.             self.setConnectionState(ConnectionState.error)
  544.             return
  545.  
  546.         if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
  547.             if self._last_response_time:
  548.                 Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time)
  549.             self.setConnectionState(self._connection_state_before_timeout)
  550.             self._connection_state_before_timeout = None
  551.  
  552.         if reply.error() == QNetworkReply.NoError:
  553.             self._last_response_time = time()
  554.  
  555.         http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
  556.         if not http_status_code:
  557.             # Received no or empty reply
  558.             return
  559.  
  560.         if reply.operation() == QNetworkAccessManager.GetOperation:
  561.             if self._api_prefix + "printer" in reply.url().toString():  # Status update from /printer.
  562.                 if http_status_code == 200:
  563.                     if not self.acceptsCommands:
  564.                         self.setAcceptsCommands(True)
  565.                         self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))
  566.  
  567.                     if self._connection_state == ConnectionState.connecting:
  568.                         self.setConnectionState(ConnectionState.connected)
  569.                     json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  570.  
  571.                     if "temperature" in json_data:
  572.                         if not self._num_extruders_set:
  573.                             self._num_extruders = 0
  574.                             while "tool%d" % self._num_extruders in json_data["temperature"]:
  575.                                 self._num_extruders = self._num_extruders + 1
  576.  
  577.                             # Reinitialise from PrinterOutputDevice to match the new _num_extruders
  578.                             self._hotend_temperatures = [0] * self._num_extruders
  579.                             self._target_hotend_temperatures = [0] * self._num_extruders
  580.  
  581.                             self._num_extruders_set = True
  582.  
  583.                         # Check for hotend temperatures
  584.                         for index in range(0, self._num_extruders):
  585.                             temperature = json_data["temperature"]["tool%d" % index]["actual"] if ("tool%d" % index) in json_data["temperature"] else 0
  586.                             self._setHotendTemperature(index, temperature)
  587.  
  588.                         bed_temperature = json_data["temperature"]["bed"]["actual"] if "bed" in json_data["temperature"] else 0
  589.                         self._setBedTemperature(bed_temperature)
  590.  
  591.                     job_state = "offline"
  592.                     if "state" in json_data:
  593.                         if json_data["state"]["flags"]["error"]:
  594.                             job_state = "error"
  595.                         elif json_data["state"]["flags"]["paused"]:
  596.                             job_state = "paused"
  597.                         elif json_data["state"]["flags"]["printing"]:
  598.                             job_state = "printing"
  599.                         elif json_data["state"]["flags"]["ready"]:
  600.                             job_state = "ready"
  601.                     self._updateJobState(job_state)
  602.  
  603.                 elif http_status_code == 401:
  604.                     self._updateJobState("offline")
  605.                     self.setConnectionText(i18n_catalog.i18nc("@info:status", "OctoPrint on {0} does not allow access to print").format(self._key))
  606.                 elif http_status_code == 409:
  607.                     if self._connection_state == ConnectionState.connecting:
  608.                         self.setConnectionState(ConnectionState.connected)
  609.  
  610.                     self._updateJobState("offline")
  611.                     self.setConnectionText(i18n_catalog.i18nc("@info:status", "The printer connected to OctoPrint on {0} is not operational").format(self._key))
  612.                 else:
  613.                     self._updateJobState("offline")
  614.                     Logger.log("w", "Received an unexpected returncode: %d", http_status_code)
  615.  
  616.             elif self._api_prefix + "job" in reply.url().toString():  # Status update from /job:
  617.                 if http_status_code == 200:
  618.                     json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  619.  
  620.                     progress = json_data["progress"]["completion"]
  621.                     if progress:
  622.                         self.setProgress(progress)
  623.  
  624.                     if json_data["progress"]["printTime"]:
  625.                         self.setTimeElapsed(json_data["progress"]["printTime"])
  626.                         if json_data["progress"]["printTimeLeft"]:
  627.                             self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
  628.                         elif json_data["job"]["estimatedPrintTime"]:
  629.                             self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
  630.                         elif progress > 0:
  631.                             self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
  632.                         else:
  633.                             self.setTimeTotal(0)
  634.                     else:
  635.                         self.setTimeElapsed(0)
  636.                         self.setTimeTotal(0)
  637.                     self.setJobName(json_data["job"]["file"]["name"])
  638.                 else:
  639.                     pass  # TODO: Handle errors
  640.  
  641.             elif self._api_prefix + "settings" in reply.url().toString():  # OctoPrint settings dump from /settings:
  642.                 if http_status_code == 200:
  643.                     json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  644.  
  645.                     if "feature" in json_data:
  646.                         self._sd_supported = json_data["feature"]["sdSupport"]
  647.  
  648.                     if "webcam" in json_data:
  649.                         self._camera_shares_proxy = False
  650.                         stream_url = json_data["webcam"]["streamUrl"]
  651.                         if stream_url == "":
  652.                             self._camera_url = ""
  653.                         elif stream_url[:4].lower() == "http": # absolute uri
  654.                             self._camera_url = stream_url
  655.                         elif stream_url[:2] == "//": # protocol-relative
  656.                             self._camera_url = "%s:%s" % (self._protocol, stream_url)
  657.                         elif stream_url[:1] == ":": # domain-relative (on another port)
  658.                             self._camera_url = "%s://%s%s" % (self._protocol, self._address, stream_url)
  659.                         elif stream_url[:1] == "/": # domain-relative (on same port)
  660.                             self._camera_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, stream_url)
  661.                             self._camera_shares_proxy = True
  662.                         else: # safe default: use mjpgstreamer on the same domain
  663.                             self._camera_url = "%s://%s:8080/?action=stream" % (self._protocol, self._address)
  664.  
  665.                         Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url)
  666.  
  667.                         self._camera_rotation = -90 if json_data["webcam"]["rotate90"] else 0
  668.                         if json_data["webcam"]["flipH"] and json_data["webcam"]["flipV"]:
  669.                             self._camera_mirror = False
  670.                             self._camera_rotation += 180
  671.                         elif json_data["webcam"]["flipH"]:
  672.                             self._camera_mirror = True
  673.                         elif json_data["webcam"]["flipV"]:
  674.                             self._camera_mirror = True
  675.                             self._camera_rotation += 180
  676.                         else:
  677.                             self._camera_mirror = False
  678.                         self.cameraOrientationChanged.emit()
  679.  
  680.         elif reply.operation() == QNetworkAccessManager.PostOperation:
  681.             if self._api_prefix + "files" in reply.url().toString():  # Result from /files command:
  682.                 if http_status_code == 201:
  683.                     Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString())
  684.                 else:
  685.                     pass  # TODO: Handle errors
  686.  
  687.                 reply.uploadProgress.disconnect(self._onUploadProgress)
  688.                 self._progress_message.hide()
  689.                 global_container_stack = Application.getInstance().getGlobalContainerStack()
  690.                 if not self._auto_print:
  691.                     location = reply.header(QNetworkRequest.LocationHeader)
  692.                     if location:
  693.                         file_name = QUrl(reply.header(QNetworkRequest.LocationHeader).toString()).fileName()
  694.                         message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint as {0}").format(file_name))
  695.                     else:
  696.                         message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint"))
  697.                     message.addAction("open_browser", i18n_catalog.i18nc("@action:button", "Open OctoPrint..."), "globe",
  698.                                         i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface"))
  699.                     message.actionTriggered.connect(self._onMessageActionTriggered)
  700.                     message.show()
  701.  
  702.             elif self._api_prefix + "job" in reply.url().toString():  # Result from /job command:
  703.                 if http_status_code == 204:
  704.                     Logger.log("d", "Octoprint command accepted")
  705.                 else:
  706.                     pass  # TODO: Handle errors
  707.  
  708.         else:
  709.             Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation())
  710.  
  711.     def _onStreamDownloadProgress(self, bytes_received, bytes_total):
  712.         self._stream_buffer += self._image_reply.readAll()
  713.  
  714.         if self._stream_buffer_start_index == -1:
  715.             self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
  716.         stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
  717.  
  718.         if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
  719.             jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
  720.             self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
  721.             self._stream_buffer_start_index = -1
  722.  
  723.             self._camera_image.loadFromData(jpg_data)
  724.             self.newImage.emit()
  725.  
  726.     def _onUploadProgress(self, bytes_sent, bytes_total):
  727.         if bytes_total > 0:
  728.             # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
  729.             # timeout responses if this happens.
  730.             self._last_response_time = time()
  731.  
  732.             progress = bytes_sent / bytes_total * 100
  733.             if progress < 100:
  734.                 if progress > self._progress_message.getProgress():
  735.                     self._progress_message.setProgress(progress)
  736.             else:
  737.                 self._progress_message.hide()
  738.                 self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1)
  739.                 self._progress_message.show()
  740.         else:
  741.             self._progress_message.setProgress(0)
  742.  
  743.     def _onMessageActionTriggered(self, message, action):
  744.         if action == "open_browser":
  745.             QDesktopServices.openUrl(QUrl(self._base_url))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement