def _buildSendPrintJobHttpRequest(self, require_printer_name): api_url = QUrl(self._api_base_uri + "print_jobs/") request = QNetworkRequest(api_url) # Create multipart request and add the g-code. self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) # Add gcode part = QHttpPart() part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file"; filename="%s"' % self._file_name) gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") compressed_gcode = self._compressGcode(gcode) if compressed_gcode is None: return None # User aborted print, so stop trying. part.setBody(compressed_gcode) self._multipart.append(part) # require_printer_name "" means automatic if require_printer_name: self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name)) user_name = self.__get_username() if user_name is None: user_name = "unknown" self._multipart.append(self.__createKeyValueHttpPart("owner", user_name)) self._addUserAgentHeader(request) return request
def __createKeyValueHttpPart(self, key, value): metadata_part = QHttpPart() metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain') metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key)) metadata_part.setBody(bytearray(value, "utf8")) return metadata_part
def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) post_part.setBody(body_data) self.postFormWithParts(target, [post_part], on_finished, on_progress)
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: part = QHttpPart() if not content_header.startswith("form-data;"): content_header = "form_data; " + content_header part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header) if content_type is not None: part.setHeader(QNetworkRequest.ContentTypeHeader, content_type) part.setBody(data) return part
def sendAttachments(self, featureid, files): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for fileName, fileData in files.items(): file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="filedata"; filename="%s"' % fileName) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(fileData) multi_part.append(file_part) content = self.sendRequest( '/upload/%s' % featureid, {'token':self.token}, 'post', multi_part ) return json.loads(content)
def sendGeoJSON(self, data, filename, projectid, data_format): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) format_part = QHttpPart() format_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="format"') format_part.setBody(str(data_format).encode('utf-8')) file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file[0]"; filename="%s.sqlite"' % filename) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(data) multi_part.append(format_part) multi_part.append(file_part) content = self.sendRequest( '/upload_gis/%s/new' % projectid, {'token':self.token}, 'post', multi_part ) return json.loads(content)
def generate_multipart_data(text_dict=None, file_dict=None): multipart_data = QHttpMultiPart(QHttpMultiPart.FormDataType) if text_dict: for key, value in text_dict.items(): text_part = QHttpPart() text_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data;name=\"%s\"" % key) text_part.setBody(str(value).encode("utf-8")) multipart_data.append(text_part) if file_dict: for key, file in file_dict.items(): file_part = QHttpPart() filename = QFileInfo(file.fileName()).fileName() file_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"; filename=\"%s\"" % (key, filename)) file_part.setBodyDevice(file) file.setParent(multipart_data) multipart_data.append(file_part) return multipart_data
def sendRaster(self, data, filename, projectid, crs): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) name_part = QHttpPart() name_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="name"') name_part.setBody(filename.encode('utf-8')) crs_part = QHttpPart() crs_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="srs"') crs_part.setBody(str(crs).encode('utf-8')) file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file[0]"; filename="%s.tiff"' % filename) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(data) multi_part.append(name_part) multi_part.append(crs_part) multi_part.append(file_part) content = self.sendRequest( '/rasters/%s' % projectid, {'token':self.token}, 'post', multi_part ) if content: return json.loads(content)
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## We start with a single extruder, but update this when we get data from octoprint self._num_extruders_set = False self._num_extruders = 1 self._api_version = "1" self._api_prefix = "api/" self._api_header = "X-Api-Key" self._api_key = None protocol = "https" if properties.get(b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._camera_url = "%s://%s:8080/?action=stream" % (protocol, self._address) self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = None self._progress_message = None self._error_message = None self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 self._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) def getProperties(self): return self._properties @pyqtSlot(str, result = str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result = str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url def _startCamera(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack or not parseBool(global_container_stack.getMetaDataEntry("octoprint_show_camera", False)): return # Start streaming mjpg stream url = QUrl(self._camera_url) self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) def _stopCamera(self): if self._image_reply: self._image_reply.abort() self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) self._image_reply = None self._image_request = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._camera_image = QImage() self.newImage.emit() def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float("inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log("d", "The network connection seems to be disabled. Going into timeout mode") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. 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) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections.")) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl(self._api_url + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._api_url + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._stopCamera() def requestWrite(self, node, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): self.writeStarted.emit(self) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format(self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) self.close() newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) def cameraImage(self): self._camera_image_id += 1 # There is an image provider that is called "camera". In order to ensure that the image qml object, that # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl # as new (instead of relying on cached version and thus forces an update. temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendJobCommand(command) def startPrint(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: return if self.jobState not in ["ready", ""]: if self.jobState == "offline": self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is offline. Unable to start a new job.")) else: self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job.")) self._error_message.show() return self._preheat_timer.stop() self._auto_print = parseBool(global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction("Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation().jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if self._auto_print: self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) destination = "local" if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)): destination = "sdcard" url = QUrl(self._api_url + "files/" + destination) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None self._progress_message.hide() def _sendCommand(self, command): self._sendCommandToApi("printer/command", command) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", command) def _sendJobCommand(self, command): self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, endpoint, command): url = QUrl(self._api_url + endpoint) self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) ## Pre-heats the heated bed of the printer. # # \param temperature The temperature to heat the bed to, in degrees # Celsius. # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature, duration): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() ## Cancels pre-heating the heated bed of the printer. # # If the bed is not pre-heated, nothing happens. @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y , z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) if "temperature" in json_data: if not self._num_extruders_set: self._num_extruders = 0 while "tool%d" % self._num_extruders in json_data["temperature"]: self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] if ("tool%d" % index) in json_data["temperature"] else 0 self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] if "bed" in json_data["temperature"] else 0 self._setBedTemperature(bed_temperature) job_state = "offline" if "state" in json_data: if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) elif http_status_code == 401: self._updateJobState("offline") self.setConnectionText(i18n_catalog.i18nc("@info:status", "OctoPrint on {0} does not allow access to print").format(self._key)) elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._updateJobState("offline") self.setConnectionText(i18n_catalog.i18nc("@info:status", "The printer connected to OctoPrint on {0} is not operational").format(self._key)) else: self._updateJobState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance().getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl(reply.header(QNetworkRequest.LocationHeader).toString()).fileName() message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction("open_browser", i18n_catalog.i18nc("@action:button", "Open OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() elif self._api_prefix + "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onStreamDownloadProgress(self, bytes_received, bytes_total): self._stream_buffer += self._image_reply.readAll() if self._stream_buffer_start_index == -1: self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8') stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2] self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] self._stream_buffer_start_index = -1 self._camera_image.loadFromData(jpg_data) self.newImage.emit() def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._progress_message = None self._error_message = None self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_timer = QTimer() self._camera_timer.setInterval( 500) # Todo: Add preference for camera update interval self._camera_timer.setSingleShot(False) self._camera_timer.timeout.connect(self._update_camera) self._camera_image_id = 0 self._camera_image = QImage() def getProperties(self): return self._properties ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the printer (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this printer @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self.setConnectionState(ConnectionState.closed) self._update_timer.stop() self._camera_timer.stop() def requestWrite(self, node, file_name=None, filter_by_machine=False): self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from printer def connect(self): self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() newImage = pyqtSignal() @pyqtProperty(QUrl, notify=newImage) def cameraImage(self): self._camera_image_id += 1 temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" data = "{\"command\": \"%s\"}" % command self._job_reply = self._manager.post(self._job_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def startPrint(self): if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Printer is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to send data to printer. Is another job still active?" )) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) ## Handler for all requests that have finished. def _onFinished(self, reply): http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) printer_state = "offline" if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "ready" self._updateJobState(printer_state) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal( json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal( json_data["job"]["estimatedPrintTime"]) elif progress > 0: self.setTimeTotal( json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0)
class MKSOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, instance_id: str, address: str, properties: dict, **kwargs) -> None: super().__init__(device_id=instance_id, address=address, properties=properties, **kwargs) self._address = address self._port = 8080 self._key = instance_id self._properties = properties self._target_bed_temperature = 0 self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem4x.qml") # self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 3 ) # Make sure the output device gets selected above local file output and Octoprint XD self._active_machine = CuraApplication.getInstance().getMachineManager( ).activeMachine self.setName(instance_id) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print over TFT")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over TFT")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to TFT on {0}").format(self._key)) Application.getInstance().globalContainerStackChanged.connect( self._onGlobalContainerChanged) self._socket = None self._gl = None self._command_queue = Queue() self._isPrinting = False self._isPause = False self._isSending = False self._gcode = None self._isConnect = False self._printing_filename = "" self._printing_progress = 0 self._printing_time = 0 self._start_time = 0 self._pause_time = 0 self.last_update_time = 0 self.angle = 10 self._connection_state_before_timeout = None self._sdFileList = False self.sdFiles = [] self._mdialog = None self._mfilename = None self._uploadpath = '' self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._screenShot = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_reply = None self._post_multi_part = None self._post_part = None self._last_file_name = None self._last_file_path = None self._progress_message = None self._error_message = None self._connection_message = None self.__additional_components_view = None self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) self._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) self._exception_message = None self._output_controller = GenericOutputController(self) self._number_of_extruders = 1 self._camera_url = "" # Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) CuraApplication.getInstance().getCuraSceneController( ).activeBuildPlateChanged.connect(self.CreateMKSController) def _onOutputDevicesChanged(self): Logger.log("d", "MKS _onOutputDevicesChanged") def connect(self): if self._socket is not None: self._socket.close() self._socket = QTcpSocket() self._socket.connectToHost(self._address, self._port) global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() self.setShortDescription( i18n_catalog.i18nc( "@action:button", "Print over " + global_container_stack.getName())) self.setDescription( i18n_catalog.i18nc( "@properties:tooltip", "Print over " + global_container_stack.getName())) Logger.log("d", "MKS socket connecting ") # self._socket.waitForConnected(2000) self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Connecting)) self._setAcceptsCommands(True) self._socket.readyRead.connect(self.on_read) self._update_timer.start() def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" @pyqtSlot(result=str) def getKey(self): return self._key @pyqtProperty(str, constant=True) def address(self): return self._properties.get(b"address", b"").decode("utf-8") @pyqtProperty(str, constant=True) def name(self): return self._properties.get(b"name", b"").decode("utf-8") @pyqtProperty(str, constant=True) def firmwareVersion(self): return self._properties.get(b"firmware_version", b"").decode("utf-8") @pyqtProperty(str, constant=True) def ipAddress(self): return self._address @pyqtSlot(float, float) def preheatBed(self, temperature, duration): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() @pyqtSlot() def printtest(self): self.sendCommand("M104 S0\r\n M140 S0\r\n M106 S255") @pyqtSlot() def printer_state(self): if len(self._printers) <= 0: return "offline" return self.printers[0].state @pyqtSlot() def selectfile(self): if self._last_file_name: return True else: return False @pyqtSlot(str) def deleteSDFiles(self, filename): # filename = "几何图.gcode" self._sendCommand("M30 1:/" + filename) self.sdFiles.remove(filename) self._sendCommand("M20") @pyqtSlot(str) def printSDFiles(self, filename): self._sendCommand("M23 " + filename) self._sendCommand("M24") @pyqtSlot() def selectFileToUplload(self): preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") filename, _ = QFileDialog.getOpenFileName( None, "choose file", preferences.getValue("mkswifi/savepath"), "Gcode(*.gcode;*.g;*.goc)") preferences.setValue("mkswifi/savepath", filename) self._uploadpath = filename if ".g" in filename.lower(): # Logger.log("d", "selectfile:"+filename) if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self.uploadfunc(filename) def closeMDialog(self): if self._mdialog: self._mdialog.close() def renameupload(self, filename): if self._mfilename and ".g" in self._mfilename.text().lower(): filename = filename[:filename. rfind("/")] + "/" + self._mfilename.text() if self._mfilename.text() in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self._mdialog.close() self.uploadfunc(filename) def uploadfunc(self, filename): preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") self._update_timer.stop() self._isSending = True self._preheat_timer.stop() single_string_file_data = "" try: f = open(self._uploadpath, "r") single_string_file_data = f.read() file_name = filename[filename.rfind("/") + 1:] self._last_file_name = file_name self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"), option_text=i18n_catalog.i18nc("@label", "Print jobs"), option_state=preferences.getValue("mkswifi/autoprint")) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.optionToggled.connect( self._onOptionStateChanged) self._progress_message.show() self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) post_request = QNetworkRequest( QUrl("http://%s/upload?X-Filename=%s" % (self._address, file_name))) post_request.setRawHeader(b'Content-Type', b'application/octet-stream') post_request.setRawHeader(b'Connection', b'keep-alive') self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._post_reply.sslErrors.connect(self._onUploadError) self._gcode = None except IOError as e: Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Send file to printer failed.")) self._error_message.show() self._update_timer.start() except Exception as e: self._update_timer.start() self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) @pyqtProperty("QVariantList") def getSDFiles(self): self._sendCommand("M20") return list(self.sdFiles) def _setTargetBedTemperature(self, temperature): if not self._updateTargetBedTemperature(temperature): return self._sendCommand(["M140 S%s" % temperature]) @pyqtSlot(str) def sendCommand(self, cmd): self._sendCommand(cmd) def _sendCommand(self, cmd): # Logger.log("d", "_sendCommand %s" % str(cmd)) if self._socket and self._socket.state() == 2 or self._socket.state( ) == 3: if isinstance(cmd, str): self._command_queue.put(cmd + "\r\n") elif isinstance(cmd, list): for eachCommand in cmd: self._command_queue.put(eachCommand + "\r\n") def disconnect(self): # self._updateJobState("") self._isConnect = False self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Closed)) if self._socket is not None: self._socket.readyRead.disconnect(self.on_read) self._socket.close() if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() def isConnected(self): return self._isConnect def isBusy(self): return self._isPrinting or self._isPause def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) self._update_timer.stop() self._isSending = True # imagebuff = self._gl.glReadPixels(0, 0, 800, 800, self._gl.GL_RGB, # self._gl.GL_UNSIGNED_BYTE) active_build_plate = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate scene = CuraApplication.getInstance().getController().getScene() gcode_dict = getattr(scene, "gcode_dict", None) if not gcode_dict: return self._gcode = gcode_dict.get(active_build_plate, None) # Logger.log("d", "mks ready for print") self.startPrint() def startPrint(self): global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None if self.isBusy(): self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._error_message.show() return job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" job_name = "cura_file" filename = "%s.gcode" % job_name if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return self._startPrint(filename) def recheckfilename(self): if self._mfilename and ".g" in self._mfilename.text().lower(): filename = self._mfilename.text() if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self._mdialog.close() self._startPrint(filename) def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self.startPrint() else: CuraApplication.getInstance().getController().setActiveStage( "PrepareStage") def _startPrint(self, file_name="cura_file.gcode"): self._preheat_timer.stop() self._screenShot = utils.take_screenshot() try: preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") # CuraApplication.getInstance().showPrintMonitor.emit(True) self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"), option_text=i18n_catalog.i18nc("@label", "Print jobs"), option_state=preferences.getValue("mkswifi/autoprint")) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.optionToggled.connect( self._onOptionStateChanged) self._progress_message.show() # job_name = Application.getInstance().getPrintInformation().jobName.strip() # if job_name is "": # job_name = "untitled_print" # job_name = "cura_file" # file_name = "%s.gcode" % job_name self._last_file_name = file_name Logger.log( "d", "mks: " + file_name + Application.getInstance(). getPrintInformation().jobName.strip()) single_string_file_data = "" if self._screenShot: single_string_file_data += utils.add_screenshot( self._screenShot, 50, 50, ";simage:") single_string_file_data += utils.add_screenshot( self._screenShot, 200, 200, ";;gimage:") single_string_file_data += "\r" last_process_events = time.time() for line in self._gcode: single_string_file_data += line if time.time() > last_process_events + 0.05: QCoreApplication.processEvents() last_process_events = time.time() self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentTypeHeader, b'application/octet-stream') self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) post_request = QNetworkRequest( QUrl("http://%s/upload?X-Filename=%s" % (self._address, file_name))) post_request.setRawHeader(b'Content-Type', b'application/octet-stream') post_request.setRawHeader(b'Connection', b'keep-alive') self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._post_reply.sslErrors.connect(self._onUploadError) # Logger.log("d", "http://%s:80/upload?X-Filename=%s" % (self._address, file_name)) self._gcode = None except IOError as e: Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Send file to printer failed.")) self._error_message.show() self._update_timer.start() except Exception as e: self._update_timer.start() self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _printFile(self): self._sendCommand("M23 " + self._last_file_name) self._sendCommand("M24") def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time.time() if new_progress > self._progress_message.getProgress(): self._progress_message.show( ) # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _onUploadError(self, reply, sslerror): Logger.log("d", "Upload Error") def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Z%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28 X Y") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand( ["G91", "G0 X%s Y%s Z%s F%s" % (x, y, z, speed), "G90"]) def _update(self): if self._socket is not None and (self._socket.state() == 2 or self._socket.state() == 3): _send_data = "M105\r\nM997\r\n" if self.isBusy(): _send_data += "M994\r\nM992\r\nM27\r\n" while self._command_queue.qsize() > 0: _queue_data = self._command_queue.get() if "M23" in _queue_data: self._socket.writeData(_queue_data.encode()) continue if "M24" in _queue_data: self._socket.writeData(_queue_data.encode()) continue _send_data += _queue_data # Logger.log("d", "_send_data: \r\n%s" % _send_data) self._socket.writeData(_send_data.encode()) self._socket.flush() # self._socket.waitForReadyRead() else: Logger.log("d", "MKS wifi reconnecting") self.disconnect() self.connect() def _setJobState(self, job_state): if job_state == "abort": command = "M26" elif job_state == "print": if self._isPause: command = "M25" else: command = "M24" elif job_state == "pause": command = "M25" if command: self._sendCommand(command) @pyqtSlot() def cancelPrint(self): self._sendCommand("M26") @pyqtSlot() def pausePrint(self): if self.printers[0].state == "paused": self._sendCommand("M24") else: self._sendCommand("M25") @pyqtSlot() def resumePrint(self): self._sendCommand("M25") def on_read(self): if not self._socket: self.disconnect() return try: if not self._isConnect: self._isConnect = True if self._connection_state != UnifiedConnectionState.Connected: self._sendCommand("M20") self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Connected)) self.setConnectionText( i18n_catalog.i18nc("@info:status", "TFT Connect succeed")) # ss = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # while self._socket.canReadLine(): # ss = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # ss_list = ss.split("\r\n") if not self._printers: self._createPrinterList() printer = self.printers[0] while self._socket.canReadLine(): s = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # Logger.log("d", "mks recv: "+s) s = s.replace("\r", "").replace("\n", "") # if time.time() - self.last_update_time > 10 or time.time() - self.last_update_time<-10: # Logger.log("d", "mks time:"+str(self.last_update_time)+str(time.time())) # self._sendCommand("M20") # self.last_update_time = time.time() if "T" in s and "B" in s and "T0" in s: t0_temp = s[s.find("T0:") + len("T0:"):s.find("T1:")] t1_temp = s[s.find("T1:") + len("T1:"):s.find("@:")] bed_temp = s[s.find("B:") + len("B:"):s.find("T0:")] t0_nowtemp = float(t0_temp[0:t0_temp.find("/")]) t0_targettemp = float(t0_temp[t0_temp.find("/") + 1:len(t0_temp)]) t1_nowtemp = float(t1_temp[0:t1_temp.find("/")]) t1_targettemp = float(t1_temp[t1_temp.find("/") + 1:len(t1_temp)]) bed_nowtemp = float(bed_temp[0:bed_temp.find("/")]) bed_targettemp = float(bed_temp[bed_temp.find("/") + 1:len(bed_temp)]) # cura 3.4 new api printer.updateBedTemperature(bed_nowtemp) printer.updateTargetBedTemperature(bed_targettemp) extruder = printer.extruders[0] extruder.updateTargetHotendTemperature(t0_targettemp) extruder.updateHotendTemperature(t0_nowtemp) # self._number_of_extruders = 1 # extruder = printer.extruders[1] # extruder.updateHotendTemperature(t1_nowtemp) # extruder.updateTargetHotendTemperature(t1_targettemp) # only on lower 3.4 # self._setBedTemperature(bed_nowtemp) # self._updateTargetBedTemperature(bed_targettemp) # if self._num_extruders > 1: # self._setHotendTemperature(1, t1_nowtemp) # self._updateTargetHotendTemperature(1, t1_targettemp) # self._setHotendTemperature(0, t0_nowtemp) # self._updateTargetHotendTemperature(0, t0_targettemp) continue if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob if s.startswith("M997"): job_state = "offline" if "IDLE" in s: self._isPrinting = False self._isPause = False job_state = 'idle' elif "PRINTING" in s: self._isPrinting = True self._isPause = False job_state = 'printing' elif "PAUSE" in s: self._isPrinting = False self._isPause = True job_state = 'paused' print_job.updateState(job_state) printer.updateState(job_state) # self._updateJobState(job_state) continue # print_job.updateState('idle') # printer.updateState('idle') if s.startswith("M994"): if self.isBusy() and s.rfind("/") != -1: self._printing_filename = s[s.rfind("/") + 1:s.rfind(";")] else: self._printing_filename = "" print_job.updateName(self._printing_filename) # self.setJobName(self._printing_filename) continue if s.startswith("M992"): if self.isBusy(): tm = s[s.find("M992") + len("M992"):len(s)].replace( " ", "") mms = tm.split(":") self._printing_time = int(mms[0]) * 3600 + int( mms[1]) * 60 + int(mms[2]) else: self._printing_time = 0 # Logger.log("d", self._printing_time) print_job.updateTimeElapsed(self._printing_time) # self.setTimeElapsed(self._printing_time) # print_job.updateTimeTotal(self._printing_time) # self.setTimeTotal(self._printing_time) continue if s.startswith("M27"): if self.isBusy(): self._printing_progress = float( s[s.find("M27") + len("M27"):len(s)].replace( " ", "")) totaltime = self._printing_time / self._printing_progress * 100 else: self._printing_progress = 0 totaltime = self._printing_time * 100 # Logger.log("d", self._printing_time) # Logger.log("d", totaltime) # self.setProgress(self._printing_progress) print_job.updateTimeTotal(self._printing_time) print_job.updateTimeElapsed(self._printing_time * 2 - totaltime) continue if 'Begin file list' in s: self._sdFileList = True self.sdFiles = [] self.last_update_time = time.time() continue if 'End file list' in s: self._sdFileList = False continue if self._sdFileList: s = s.replace("\n", "").replace("\r", "") if s.lower().endswith("gcode") or s.lower().endswith( "gco") or s.lower.endswith("g"): self.sdFiles.append(s) continue except Exception as e: print(e) def _updateTargetBedTemperature(self, temperature): if self._target_bed_temperature == temperature: return False self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() return True def _updateTargetHotendTemperature(self, index, temperature): if self._target_hotend_temperatures[index] == temperature: return False self._target_hotend_temperatures[index] = temperature self.targetHotendTemperaturesChanged.emit() return True def _createPrinterList(self): printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _onRequestFinished(self, reply): http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) self._isSending = True self._update_timer.start() self._sendCommand("M20") preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") # preferences.addPreference("mkswifi/savepath", "") # preferences.setValue("mkswifi/autoprint", str(self._progress_message.getOptionState())) if preferences.getValue("mkswifi/autoprint"): self._printFile() if not http_status_code: return def _onOptionStateChanged(self, optstate): preferences = Application.getInstance().getPreferences() preferences.setValue("mkswifi/autoprint", str(optstate)) def _cancelSendGcode(self, message_id, action_id): self._update_timer.start() self._isSending = False self._progress_message.hide() self._post_reply.abort() def CreateMKSController(self): Logger.log("d", "Creating additional ui components for mkscontroller.") # self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"mkscontroller": self}) self.__additional_components_view = Application.getInstance( ).createQmlComponent(self._monitor_view_qml_path, {"manager": self}) # trlist = CuraApplication.getInstance()._additional_components # for comp in trlist: Logger.log("w", "create mkscontroller ") if not self.__additional_components_view: Logger.log("w", "Could not create ui components for tft35.") return def _onGlobalContainerChanged(self) -> None: self._global_container_stack = Application.getInstance( ).getGlobalContainerStack() definitions = self._global_container_stack.definition.findDefinitions( key="cooling") Logger.log("d", definitions[0].label)
class RepetierServerOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties["path"] if "path" in properties else "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## Todo: Hardcoded value now; we should probably read this from the machine definition and Repetier-Server. self._num_extruders_set = False self._num_extruders = 1 self._slug = "fanera1" self._api_version = "1" self._api_prefix = "printer/api/" + self._slug self._api_header = "X-Api-Key" self._api_key = None self._base_url = "http://%s:%d%s" % (self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._model_url = self._base_url + "printer/model/" + self._slug + "?a=upload" self._camera_url = "http://%s:8080/?action=snapshot" % self._address self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Repetier-Server")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Repetier-Server")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Repetier-Server on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = None self._progress_message = None self._error_message = None self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_timer = QTimer() self._camera_timer.setInterval( 500) # Todo: Add preference for camera update interval self._camera_timer.setSingleShot(False) self._camera_timer.timeout.connect(self._update_camera) self._camera_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this Repetier-Server instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url def _update_camera(self): ## Request new image url = QUrl(self._camera_url) self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float( "inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log( "d", "The network connection seems to be disabled. Going into timeout mode" ) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. Logger.log( "d", "We did not receive a response for %s seconds, so it seems Repetier-Server is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Repetier-Server was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data urlString = self._api_url + "?a=stateList" #Logger.log("d","XXX URL: " + urlString) url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._api_url + "?a=listPrinter") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() def requestWrite(self, node, file_name=None, filter_by_machine=False): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): #self.close() # Ensure that previous connection (if any) is killed. global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() if parseBool( global_container_stack.getMetaDataEntry( "octoprint_show_camera", False)): self._update_camera() self._camera_timer.start() else: self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Repetier-Server on {0}").format( self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with ip %s stopped", self._key, self._address) self.close() newImage = pyqtSignal() @pyqtProperty(QUrl, notify=newImage) def cameraImage(self): self._camera_image_id += 1 # There is an image provider that is called "camera". In order to ensure that the image qml object, that # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl # as new (instead of relying on cached version and thus forces an update. temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): # if job_state == "abort": # command = "cancel" # elif job_state == "print": # if self.jobState == "paused": # command = "pause" # else: # command = "start" # elif job_state == "pause": # command = "pause" urlString = "" if job_state == "abort": command = "cancel" urlString = self._api_url + '?a=stopJob' elif job_state == "print": if self.jobState == "paused": command = "pause" urlString = self._api_url + '?a=send&data={"cmd":"@pause"}' else: command = "start" urlString = self._api_url + '?a=continueJob' elif job_state == "pause": command = "pause" urlString = self._api_url + '?a=send&data={"cmd":"@pause"}' Logger.log("d", "XXX:Command:" + command) Logger.log("d", "XXX:Command:" + urlString) if urlString: url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) # if command: # self._sendCommand(command) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("repetier_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier-Server is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Repetier-Server"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) # self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") # self._post_part.setBody(b"true") # self._post_multi_part.append(self._post_part) # if self._auto_print: # self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") # self._post_part.setBody(b"true") # self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) # destination = "local" # if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)): # destination = "sdcard" # TODO ?? url = QUrl(self._model_url + "&name=" + file_name) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to Repetier-Server.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl(self._api_url + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to Repetier-Server instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if "stateList" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to Repetier-Server on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) if not self._num_extruders_set: self._num_extruders = 0 ## TODO # while "extruder" % self._num_extruders in json_data[self._slug]: # self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders # self._hotend_temperatures = [0] * self._num_extruders # self._target_hotend_temperatures = [0] * self._num_extruders # self._num_extruders_set = True # TODO # Check for hotend temperatures # for index in range(0, self._num_extruders): # temperature = json_data[self._slug]["extruder"]["tempRead"] # self._setHotendTemperature(index, temperature) temperature = json_data[ self._slug]["extruder"][0]["tempRead"] self._setHotendTemperature(0, temperature) bed_temperature = json_data[ self._slug]["heatedBed"]["tempRead"] #bed_temperature_set = json_data[self._slug]["heatedBed"]["tempSet"] self._setBedTemperature(bed_temperature) elif http_status_code == 401: self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Repetier-Server on {0} does not allow access to print" ).format(self._key)) else: pass # TODO: Handle errors elif "listPrinter" in reply.url().toString( ): # Status update from /job: if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) for printer in json_data: if printer["slug"] == self._slug: job_name = printer["job"] self.setJobName(job_name) job_state = "offline" # if printer["state"]["flags"]["error"]: # job_state = "error" if printer["paused"] == "true": job_state = "paused" elif job_name != "none": job_state = "printing" self.setProgress(printer["done"]) elif job_name == "none": job_state = "ready" self._updateJobState(job_state) # progress = json_data["progress"]["completion"] # if progress: # self.setProgress(progress) # if json_data["progress"]["printTime"]: # self.setTimeElapsed(json_data["progress"]["printTime"]) # if json_data["progress"]["printTimeLeft"]: # self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) # elif json_data["job"]["estimatedPrintTime"]: # self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) # elif progress > 0: # self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) # else: # self.setTimeTotal(0) # else: # self.setTimeElapsed(0) # self.setTimeTotal(0) # self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if http_status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "model" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on Repetier-Server instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) modelList = json_data["data"] lastModel = modelList[len(modelList) - 1] # Logger.log("d", "XXX1:len"+str(len(modelList))) # Logger.log("d", "XXX1:lastModel"+str(lastModel)) modelId = lastModel["id"] # "http://%s:%d%s" % (self._address, self._port, self._path) urlString = self._api_url + '?a=copyModel&data={"id": %s}' % ( modelId) Logger.log("d", "XXX1: modelId: " + str(modelId)) url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) Logger.log("d", "XXX1: modelId: " + str(urlString)) reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to Repetier-Server as {0}").format( file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to Repetier-Server")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Repetier-Server..."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the Repetier-Server web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Repetier-Server command accepted") else: pass # TODO: Handle errors else: Logger.log( "d", "RepetierServerOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on Repetier-Server"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
def _startPrint(self): if self._auto_print and not self._forced_queue: Application.getInstance().getController().setActiveStage( "MonitorStage") # cancel any ongoing preheat timer before starting a print try: self._printers[0].stopPreheatTimers() except AttributeError: # stopPreheatTimers was added after Cura 3.3 beta pass self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") post_part.setBody(b"true") self._post_multi_part.append(post_part) if self._auto_print and not self._forced_queue: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") post_part.setBody(b"true") self._post_multi_part.append(post_part) post_part = QHttpPart() post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(post_part) destination = "local" if self._sd_supported and parseBool(Application.getInstance( ).getGlobalContainerStack().getMetaDataEntry("octoprint_store_sd", False)): destination = "sdcard" try: ## Post request + data post_request = self._createApiRequest("files/" + destination) self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._gcode = []
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders_set = False self._num_extruders = 1 self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = None self._progress_message = None self._error_message = None self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_timer = QTimer() self._camera_timer.setInterval(500) # Todo: Add preference for camera update interval self._camera_timer.setSingleShot(False) self._camera_timer.timeout.connect(self._update_camera) self._camera_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._response_timeout_time = 5 def getProperties(self): return self._properties ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result = str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. 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) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections.")) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() def requestWrite(self, node, file_name = None, filter_by_machine = False): Application.getInstance().showPrintMonitor.emit(True) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() self._last_response_time = None self.setAcceptsCommands(False) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with ip %s stopped", self._key, self._address) self.close() newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) def cameraImage(self): self._camera_image_id += 1 temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendCommand(command) def startPrint(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: return if self.jobState != "ready" and self.jobState != "": self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if global_container_stack.getMetaDataEntry("octoprint_auto_print", True): self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl("http://" + self._address + self._api_prefix + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y , z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: self.setAcceptsCommands(True) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) if not self._num_extruders_set: self._num_extruders = 0 while "tool%d" % self._num_extruders in json_data["temperature"]: self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) job_state = "offline" if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) elif http_status_code == 401: self.setAcceptsCommands(False) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if http_status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and not global_container_stack.getMetaDataEntry("octoprint_auto_print", True): message = Message(catalog.i18nc("@info:status", "Saved to OctoPrint as {1}").format(reply.header(QNetworkRequest.LocationHeader).toString())) message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to OctoPrint.")) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: progress = bytes_sent / bytes_total * 100 if progress < 100: self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl("http://" + self._address))
def genReqStr(value: Dict[str, Any], prefix="", multiPart=None) -> QHttpMultiPart: if not multiPart: multiPart = QHttpMultiPart(QHttpMultiPart.FormDataType) if isinstance( value, (QtCore.QFile, io.BufferedReader)): # file content must be a QFile object # FIXME: This is broken. IoBufferedReader will be read to memory entirely - not streamed!! if "name" in dir(value): fileName = value.name elif "fileName" in dir(value): fileName = value.fileName() try: mimetype, encoding = mimetypes.guess_type(fileName, strict=False) mimetype = mimetype or "application/octet-stream" except: mimetype = "application/octet-stream" filePart = QHttpPart() filePart.setHeader(QNetworkRequest.ContentTypeHeader, mimetype) filePart.setHeader( QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"; filename="{1}"'.format( prefix, os.path.basename(fileName))) if isinstance(value, io.BufferedReader): filePart.setBody(value.read()) else: filePart.setBodyDevice(value) value.setParent(multiPart) multiPart.append(filePart) elif isinstance(value, list): if not value: otherPart = QHttpPart() otherPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") otherPart.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) otherPart.setBody(b"") multiPart.append(otherPart) elif any([isinstance(x, dict) for x in value]): for idx, v in enumerate(value): NetworkService.genReqStr( v, (prefix + "." if prefix else "") + str(idx), multiPart) else: for val in value: logger.debug("serializing param item %r of list value %r", val, prefix) textPart = QHttpPart() textPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") textPart.setHeader( QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) textPart.setBody(str(val).encode("utf-8")) multiPart.append(textPart) elif isinstance(value, dict): if prefix: prefix += "." for k, v in value.items(): NetworkService.genReqStr(v, prefix + k, multiPart) #elif value is None: #return multiPart else: if value is None: value = "" otherPart = QHttpPart() otherPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") otherPart.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) otherPart.setBody(str(value).encode("utf-8")) multiPart.append(otherPart) return multiPart
def uploadGCode(self, data): try: job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name == "": job_name = "untitled_print" global_stack = Application.getInstance().getGlobalContainerStack() machine_manager = Application.getInstance().getMachineManager() cura_printer_type = machine_manager.activeDefinitionId printer_type = ConnectPrinterIdTranslation.curaPrinterIdToConnect( cura_printer_type) # Fall back to marlin or makerbot generic if printer is not supported on WiFi-Box if printer_type is None: gcode_flavor = global_stack.getProperty( "machine_gcode_flavor", "value") if gcode_flavor == "RepRap (Marlin/Sprinter)": printer_type = "marlin_generic" elif gcode_flavor == "MakerBot": printer_type = "makerbot_generic" else: printer_type = cura_printer_type sliceInfo = { 'printer': { 'type': printer_type, 'title': global_stack.getName() }, 'material': { 'type': global_stack.material.getId(), 'title': global_stack.material.getName() }, 'filamentThickness': global_stack.getProperty("material_diameter", "value"), 'temperature': global_stack.getProperty("material_print_temperature", "value"), 'name': job_name } gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list") gcode = ";%s\n" % json.dumps(sliceInfo) for line in gcode_list: gcode += line multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for prop_name, prop_value in data["data"]["reservation"][ "fields"].items(): part = QHttpPart() part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"" % prop_name) part.setBody(prop_value.encode()) multi_part.append(part) gcode_part = QHttpPart() gcode_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"") gcode_part.setBody(gcode.encode()) multi_part.append(gcode_part) upload_url = QUrl(data["data"]["reservation"]["url"]) Logger.log("d", "{}", upload_url) self._post_reply = self._manager.post(QNetworkRequest(upload_url), multi_part) self._post_reply.uploadProgress.connect(self._onProgress) self._post_reply.error.connect(self._onNetworkError) self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Doodle3D Connect"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._onMessageActionTriggered) self._progress_message.show() multi_part.setParent(self._post_reply) except Exception as e: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to send data to Doodle3D Connect. Is another job still active?" )) self._progress_message.show() Logger.log( "e", "An exception occured during G-code upload: %s" % str(e))
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._progress_message = None self._error_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_timer = QTimer() self._camera_timer.setInterval(500) # Todo: Add preference for camera update interval self._camera_timer.setSingleShot(False) self._camera_timer.timeout.connect(self._update_camera) self._camera_image_id = 0 self._camera_image = QImage() def getProperties(self): return self._properties ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result = str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the printer (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this printer @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self.setConnectionState(ConnectionState.closed) self._update_timer.stop() self._camera_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from printer def connect(self): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) def cameraImage(self): self._camera_image_id += 1 temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" data = "{\"command\": \"%s\"}" % command self._job_reply = self._manager.post(self._job_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def startPrint(self): if self.jobState != "ready" and self.jobState != "": self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) ## Handler for all requests that have finished. def _onFinished(self, reply): http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) printer_state = "offline" if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "ready" self._updateJobState(printer_state) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(json_data["job"]["estimatedPrintTime"]) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0)
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, key, address: str, port, properties, parent=None): super().__init__(device_id=key, address=address, properties=properties, parent=parent) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True self._forced_queue = False # We start with a single extruder, but update this when we get data from octoprint self._number_of_extruders_set = False self._number_of_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion(), "OctoPrintPlugin", Application.getInstance().getVersion())).encode() self._api_prefix = "api/" self._api_header = "X-Api-Key".encode() self._api_key = None self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._post_reply = None self._post_multi_part = None self._post_part = None self._progress_message = None self._error_message = None self._connection_message = None self._queued_gcode_commands = [] self._queued_gcode_timer = QTimer() self._queued_gcode_timer.setInterval(0) self._queued_gcode_timer.setSingleShot(True) self._queued_gcode_timer.timeout.connect(self._sendQueuedGcode) self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_mirror = "" self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 self._output_controller = GenericOutputController(self) def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## IPadress of this instance # Overridden from NetworkedPrinterOutputDevice because OctoPrint does not # send the ip address with zeroconf @pyqtProperty(str, constant=True) def address(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self): return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float( "inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log( "d", "The network connection seems to be disabled. Going into timeout mode" ) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. 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) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with OctoPrint was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data self._printer_reply = self._manager.get( self._createApiRequest("printer")) ## Request print_job data self._job_reply = self._manager.get(self._createApiRequest("job")) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def _createApiRequest(self, end_point): request = QNetworkRequest(QUrl(self._api_url + end_point)) request.setRawHeader(self._user_agent_header, self._user_agent) request.setRawHeader(self._api_header, self._api_key) if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self): self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) active_build_plate = Application.getInstance().getBuildPlateModel( ).activeBuildPlate scene = Application.getInstance().getController().getScene() gcode_dict = getattr(scene, "gcode_dict", None) if not gcode_dict: return self._gcode = gcode_dict.get(active_build_plate, None) self.startPrint() ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self._setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format( self._key)) ## Request 'settings' dump self._settings_reply = self._manager.get( self._createApiRequest("settings")) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) self.close() def pausePrint(self): self._sendJobCommand("pause") def resumePrint(self): if not self._printers[0].activePrintJob: return if self._printers[0].activePrintJob.state == "paused": self._sendJobCommand("pause") else: self._sendJobCommand("start") def cancelPrint(self): self._sendJobCommand("cancel") def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None self._auto_print = parseBool( global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) self._forced_queue = False if self.activePrinter.state not in ["idle", ""]: Logger.log( "d", "Tried starting a print, but current state is %s" % self.activePrinter.state) if self.activePrinter.state == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "The printer is offline. Unable to start a new job.")) elif self._auto_print: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint is busy. Unable to start a new job.")) else: # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled self._error_message = None if self._error_message: self._error_message.addAction( "Queue", i18n_catalog.i18nc("@action:button", "Queue job"), None, i18n_catalog.i18nc( "@action:tooltip", "Queue this print job so it can be printed later")) self._error_message.actionTriggered.connect(self._queuePrint) self._error_message.show() return self._startPrint() def _queuePrint(self, message_id, action_id): if self._error_message: self._error_message.hide() self._forced_queue = True self._startPrint() def _startPrint(self): if self._auto_print and not self._forced_queue: Application.getInstance().getController().setActiveStage( "MonitorStage") try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if self._auto_print and not self._forced_queue: self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) destination = "local" if self._sd_supported and parseBool(Application.getInstance( ).getGlobalContainerStack().getMetaDataEntry( "octoprint_store_sd", False)): destination = "sdcard" ## Post request + data post_request = self._createApiRequest("files/" + destination) self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None if self._progress_message: self._progress_message.hide() def sendCommand(self, command): self._queued_gcode_commands.append(command) self._queued_gcode_timer.start() # Send gcode commands that are queued in quick succession as a single batch def _sendQueuedGcode(self): if self._queued_gcode_commands: self._sendCommandToApi("printer/command", self._queued_gcode_commands) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", self._queued_gcode_commands) self._queued_gcode_commands = [] def _sendJobCommand(self, command): self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, end_point, commands): command_request = self._createApiRequest(end_point) command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if isinstance(commands, list): data = json.dumps({"commands": commands}) else: data = json.dumps({"command": commands}) self._command_reply = self._manager.post(command_request, data.encode()) ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString( ): # Status update from /printer. if not self._printers: self._createPrinterList() # An OctoPrint instance has a single printer. printer = self._printers[0] if http_status_code == 200: if not self.acceptsCommands: self._setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to OctoPrint on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "temperature" in json_data: if not self._number_of_extruders_set: self._number_of_extruders = 0 while "tool%d" % self._number_of_extruders in json_data[ "temperature"]: self._number_of_extruders += 1 if self._number_of_extruders > 1: # Recreate list of printers to match the new _number_of_extruders self._createPrinterList() printer = self._printers[0] if self._number_of_extruders > 0: self._number_of_extruders_set = True # Check for hotend temperatures for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] if ("tool%d" % index) in json_data["temperature"]: hotend_temperatures = json_data["temperature"][ "tool%d" % index] extruder.updateTargetHotendTemperature( hotend_temperatures["target"]) extruder.updateHotendTemperature( hotend_temperatures["actual"]) else: extruder.updateTargetHotendTemperature(0) extruder.updateHotendTemperature(0) if "bed" in json_data["temperature"]: bed_temperatures = json_data["temperature"]["bed"] printer.updateBedTemperature( bed_temperatures["actual"]) printer.updateTargetBedTemperature( bed_temperatures["target"]) else: printer.updateBedTemperature(0) printer.updateTargetBedTemperature(0) printer_state = "offline" if "state" in json_data: if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "idle" printer.updateState(printer_state) elif http_status_code == 401: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} does not allow access to print"). format(self._key)) pass elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to OctoPrint on {0} is not operational" ).format(self._key)) else: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "job" in reply.url().toString( ): # Status update from /job: if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job_state = "offline" if "state" in json_data: if json_data["state"] == "Error": print_job_state = "error" elif json_data["state"] == "Paused": print_job_state = "paused" elif json_data["state"] == "Printing": print_job_state = "printing" elif json_data["state"] == "Operational": print_job_state = "ready" printer.updateState("idle") print_job.updateState(print_job_state) if json_data["progress"]["printTime"]: print_job.updateTimeElapsed( json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: print_job.updateTimeTotal( json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: print_job.updateTimeTotal( max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif json_data["progress"][ "completion"]: # not 0 or None or "" print_job.updateTimeTotal( json_data["progress"]["printTime"] / (json_data["progress"]["completion"] / 100)) else: print_job.updateTimeTotal(0) else: print_job.updateTimeElapsed(0) print_job.updateTimeTotal(0) print_job.updateName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif self._api_prefix + "settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "feature" in json_data and "sdSupport" in json_data[ "feature"]: self._sd_supported = json_data["feature"]["sdSupport"] if "webcam" in json_data and "streamUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False stream_url = json_data["webcam"]["streamUrl"] if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url) if "rotate90" in json_data["webcam"]: self._camera_rotation = -90 if json_data["webcam"][ "rotate90"] else 0 if json_data["webcam"]["flipH"] and json_data[ "webcam"]["flipV"]: self._camera_mirror = False self._camera_rotation += 180 elif json_data["webcam"]["flipH"]: self._camera_mirror = True elif json_data["webcam"]["flipV"]: self._camera_mirror = True self._camera_rotation += 180 else: self._camera_mirror = False self.cameraOrientationChanged.emit() elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() if self._forced_queue or not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif self._api_prefix + "job" in reply.url().toString( ): # Result from /job command (eg start/pause): if http_status_code == 204: Logger.log("d", "Octoprint job command accepted") else: pass # TODO: Handle errors elif self._api_prefix + "printer/command" in reply.url().toString( ): # Result from /printer/command (gcode statements): if http_status_code == 204: Logger.log("d", "Octoprint gcode command(s) accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _createPrinterList(self): printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) printer.setCamera(NetworkCamera(self._camera_url)) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class SculptoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties["path"] if "path" in properties else "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## We start with a single extruder, but update this when we get data from Sculptoprint self._num_extruders = 1 self._api_version = "1" self._api_prefix = "api/" self._api_header = "X-Api-Key" self._api_key = None self._base_url = "http://%s:%d/" % (self._address, self._port) self._api_url = self._base_url + self._api_prefix self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print on Sculpto")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print on Sculpto")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Sculpto on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = None self._progress_message = None self._error_message = None self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._connection_state_before_timeout = None self._is_printing = False self.estimated_total = 0 self.starting_time = 0 self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this SculptoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def SculptoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float( "inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log( "d", "The network connection seems to be disabled. Going into timeout mode" ) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. Logger.log( "d", "We did not receive a response for %s seconds, so it seems SculptoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Sculpto was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl(self._base_url + "temperature") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._base_url + "progress") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self.newImage.emit() def requestWrite(self, node, file_name=None, filter_by_machine=False): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): #self.close() # Ensure that previous connection (if any) is killed. global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Sculpto on {0}").format( self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with ip %s stopped", self._key, self._address) self.close() newImage = pyqtSignal() def _setJobState(self, job_state): if job_state == "abort": Logger.log("d", "Should abort!") return #command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendCommand(command) def stopPrint(self): self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"cool\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) url = QUrl(self._base_url + "stop_print") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("sculptoprint_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sculpto is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Sculpto"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl(self._base_url + "upload_and_print") self.setJobName(file_name) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to SculptoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl(self._api_url + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to SculptoPrint instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return Logger.log("w", "Got response") if reply.operation() == QNetworkAccessManager.GetOperation: if "temperature" in reply.url().toString( ): # Status update from /temperature. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to SculptoPrint on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) self._setHotendTemperature(0, json_data['payload']) ''' job_state = "offline" if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) ''' elif http_status_code == 401: self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Sculpto on {0} does not allow access to print"). format(self._key)) else: pass # TODO: Handle errors elif "progress" in reply.url().toString( ): # Status update from /progress: if http_status_code == 500: self.setTimeElapsed(0) self.setTimeTotal(0) self._updateJobState("ready") self.setProgress(0) self.setJobName("Waiting for Print") self._is_printing = False if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", "Progress: {0}".format(json_data["payload"])) self.setProgress(json_data["payload"]) self._updateJobState("printing") self._is_printing = True self.setTimeElapsed(time() - self.starting_time) ''' progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) ''' else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "upload_and_print" in reply.url().toString( ): # Result from /upload_and_print command: if http_status_code == 201: Logger.log("d", "Successfully uploaded and printing!") print_information = Application.getInstance( ).getPrintInformation().currentPrintTime self.estimated_total = int( print_information.getDisplayString(0)) self.starting_time = time() self.setTimeTotal(self.estimated_total) self.setTimeElapsed(0) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to SculptoPrint as {0}").format( file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to SculptoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open SculptoPrint..."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the SculptoPrint web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif "stop_print" in reply.url().toString( ): # Result from /stop_print command: if http_status_code == 200: Logger.log("d", "Printing stopped") self._updateJobState("ready") self.setProgress(0) self._is_printing = False else: pass # TODO: Handle errors else: Logger.log( "d", "SculptoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc( "@info:status", "Storing gcode on Sculpto and starting print"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
def _sendRequest(self, path, name=None, data=None, dataIsJSON=False, on_success=None, on_error=None): url = self._url + path headers = { 'User-Agent': 'Cura Plugin Moonraker', 'Accept': 'application/json, text/plain', 'Connection': 'keep-alive' } if self._api_key: headers['X-API-Key'] = self._api_key postData = data if data is not None: if not dataIsJSON: # Create multi_part request parts = QHttpMultiPart(QHttpMultiPart.FormDataType) part_file = QHttpPart() part_file.setHeader( QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="file"; filename="/' + name + '"')) part_file.setHeader(QNetworkRequest.ContentTypeHeader, QVariant('application/octet-stream')) part_file.setBody(data) parts.append(part_file) part_root = QHttpPart() part_root.setHeader(QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="root"')) part_root.setBody(b"gcodes") parts.append(part_root) if self._startPrint: part_print = QHttpPart() part_print.setHeader( QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="print"')) part_print.setBody(b"true") parts.append(part_print) headers[ 'Content-Type'] = 'multipart/form-data; boundary=' + str( parts.boundary().data(), encoding='utf-8') postData = parts else: # postData is JSON headers['Content-Type'] = 'application/json' self.application.getHttpRequestManager().post( url, headers, postData, callback=on_success, error_callback=on_error if on_error else self._onNetworkError, upload_progress_callback=self._onUploadProgress if not dataIsJSON else None) else: self.application.getHttpRequestManager().get( url, headers, callback=on_success, error_callback=on_error if on_error else self._onNetworkError)
def _startPrint(self) -> None: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._auto_print and not self._forced_queue: CuraApplication.getInstance().getController().setActiveStage( "MonitorStage") # cancel any ongoing preheat timer before starting a print try: self._printers[0].stopPreheatTimers() except AttributeError: # stopPreheatTimers was added after Cura 3.3 beta pass self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), "", "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() job_name = CuraApplication.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request post_parts = [] # type: List[QHttpPart] ## Create parts (to be placed inside multipart) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") post_part.setBody(b"true") post_parts.append(post_part) if self._auto_print and not self._forced_queue: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") post_part.setBody(b"true") post_parts.append(post_part) post_part = QHttpPart() post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) post_part.setBody(self._gcode_stream.getvalue().encode()) post_parts.append(post_part) destination = "local" if self._sd_supported and parseBool( global_container_stack.getMetaDataEntry( "octoprint_store_sd", False)): destination = "sdcard" try: ## Post request + data post_request = self._createEmptyRequest("files/" + destination) self._post_reply = self.postFormWithParts( "files/" + destination, post_parts, on_finished=self._onRequestFinished, on_progress=self._onUploadProgress) except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._gcode_stream = StringIO()
class RepetierOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True # We start with a single extruder, but update this when we get data from Repetier self._num_extruders_set = False self._num_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion(), "RepetierPlugin", Application.getInstance().getVersion())).encode() #base_url + "printer/api/" + self._key + self._api_prefix = "printer/api/" + self._key self._job_prefix = "printer/job/" + self._key self._api_header = "x-api-key".encode() self._api_key = None self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._job_url = self._base_url + self._job_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Repetier")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Repetier")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Repetier on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_reply = None self._post_multi_part = None self._post_part = None self._progress_message = None self._error_message = None self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_image_id = 0 self._camera_image = QImage() self._camera_mirror = "" self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 self._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this Repetier instance def setApiKey(self, api_key): self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def RepetierVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## IP address of this instance @pyqtProperty(str, constant=True) def address(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self): return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } def _startCamera(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack or not parseBool( global_container_stack.getMetaDataEntry( "repetier_show_camera", False)) or self._camera_url == "": return # Start streaming mjpg stream url = QUrl(self._camera_url) image_request = QNetworkRequest(url) image_request.setRawHeader(self._user_agent_header, self._user_agent) if self._camera_shares_proxy and self._basic_auth_data: image_request.setRawHeader(self._basic_auth_header, self._basic_auth_data) self._image_reply = self._manager.get(image_request) self._image_reply.downloadProgress.connect( self._onStreamDownloadProgress) def _stopCamera(self): if self._image_reply: self._image_reply.abort() self._image_reply.downloadProgress.disconnect( self._onStreamDownloadProgress) self._image_reply = None image_request = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._camera_image = QImage() self.newImage.emit() def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float( "inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log( "d", "The network connection seems to be disabled. Going into timeout mode" ) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. Logger.log( "d", "We did not receive a response for %s seconds, so it seems Repetier is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Repetier was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data self._printer_reply = self._manager.get( self._createApiRequest("stateList")) ## Request print_job data self._job_reply = self._manager.get( self._createApiRequest("listPrinter")) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def _createApiRequest(self, end_point): ##Logger.log("d", "Debug: %s", end_point) if "upload" in end_point: request = QNetworkRequest(QUrl(self._job_url + "?a=" + end_point)) else: request = QNetworkRequest(QUrl(self._api_url + "?a=" + end_point)) request.setRawHeader(self._user_agent_header, self._user_agent) request.setRawHeader(self._api_header, self._api_key) if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._stopCamera() def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Repetier on {0}").format( self._base_url)) ## Request 'settings' dump self._settings_reply = self._manager.get( self._createApiRequest("getPrinterConfig")) self._settings_reply = self._manager.get( self._createApiRequest("stateList")) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) self.close() newImage = pyqtSignal() @pyqtProperty(QUrl, notify=newImage) def cameraImage(self): self._camera_image_id += 1 # There is an image provider that is called "camera". In order to ensure that the image qml object, that # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl # as new (instead of relying on cached version and thus forces an update. temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) def getCameraImage(self): return self._camera_image def _setJobState(self, job_state): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendJobCommand(command) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("repetier_auto_print", True)) if self.jobState not in ["ready", ""]: if self.jobState == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier is offline. Unable to start a new job.")) elif self._auto_print: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier is busy. Unable to start a new job.")) else: # allow queueing the job even if Repetier is currently busy if autoprinting is disabled self._error_message = None if self._error_message: self._error_message.show() return self._preheat_timer.stop() if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Repetier"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() ##Logger.log("d", "debug Print job: [%s]", job_name) if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"a\"") self._post_part.setBody(b"upload") self._post_multi_part.append(self._post_part) ##if self._auto_print: ## self._post_part = QHttpPart() ## self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"" % file_name) ## self._post_part.setBody(b"upload") ## self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"filename\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"name\"") b = bytes(file_name, 'utf-8') self._post_part.setBody(b) self._post_multi_part.append(self._post_part) destination = "local" if self._sd_supported and parseBool( global_container_stack.getMetaDataEntry( "Repetier_store_sd", False)): destination = "sdcard" ## Post request + data #post_request = self._createApiRequest("files/" + destination) post_request = self._createApiRequest("upload") self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to Repetier.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None self._progress_message.hide() def _sendCommand(self, command): #self._sendCommandToApi("printer/command", command) self._sendCommandToApi("send", "&data={\"cmd\":\"" + command + "\"}") Logger.log("d", "Sent gcode command to Repetier instance: %s", command) def _sendJobCommand(self, command): #self._sendCommandToApi("job", command) if (command == "pause"): if (self.jobState == "paused"): self._manager.get(self._createApiRequest("continueJob")) ##self._sendCommandToApi("send", "&data={\"cmd\":\"continueJob\"}") else: self._sendCommandToApi("send", "&data={\"cmd\":\"@pause\"}") if (command == "cancel"): self._manager.get(self._createApiRequest("stopJob")) ##self._sendCommandToApi("send", "&data={\"cmd\":\"stopJob\"}") Logger.log( "d", "Sent job command to Repetier instance: %s %s" % (command, self.jobState)) def _sendCommandToApi(self, end_point, command): command_request = self._createApiRequest(end_point) command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(command_request, data.encode()) ## Pre-heats the heated bed of the printer. # # \param temperature The temperature to heat the bed to, in degrees # Celsius. # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature, duration): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() ## Cancels pre-heating the heated bed of the printer. # # If the bed is not pre-heated, nothing happens. @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() ## Changes the target bed temperature on the Repetier instance. # # /param temperature The new target temperature of the bed. def _setTargetBedTemperature(self, temperature): if not self._updateTargetBedTemperature(temperature): Logger.log("d", "Target bed temperature is already set to %s", temperature) return Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) ## Updates the target bed temperature from the printer, and emit a signal if it was changed. # # /param temperature The new target temperature of the bed. # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature def _updateTargetBedTemperature(self, temperature): if self._target_bed_temperature == temperature: return False self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() return True ## Changes the target bed temperature on the Repetier instance. # # /param index The index of the hotend. # /param temperature The new target temperature of the bed. def _setTargetHotendTemperature(self, index, temperature): if not self._updateTargetHotendTemperature(index, temperature): Logger.log("d", "Target hotend %s temperature is already set to %s", index, temperature) return Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) ## Updates the target hotend temperature from the printer, and emit a signal if it was changed. # # /param index The index of the hotend. # /param temperature The new target temperature of the hotend. # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature def _updateTargetHotendTemperature(self, index, temperature): if self._target_hotend_temperatures[index] == temperature: return False self._target_hotend_temperatures[index] = temperature self.targetHotendTemperaturesChanged.emit() return True def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "?a=stateList" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to Repetier on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} #if "temperature" in json_data: if "numExtruder" in json_data[self._key]: self._num_extruders = 0 #while "tool%d" % self._num_extruders in json_data["temperature"]: # self._num_extruders = self._num_extruders + 1 self._num_extruders = json_data[ self._key]["numExtruder"] # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [ 0 ] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): if "extruder" in json_data[self._key]: hotend_temperatures = json_data[ self._key]["extruder"] self._setHotendTemperature( index, hotend_temperatures[index]["tempRead"]) self._updateTargetHotendTemperature( index, hotend_temperatures[index]["tempSet"]) else: self._setHotendTemperature(index, 0) self._updateTargetHotendTemperature(index, 0) if "heatedBed" in json_data[self._key]: bed_temperatures = json_data[ self._key]["heatedBed"] self._setBedTemperature( bed_temperatures["tempRead"]) self._updateTargetBedTemperature( bed_temperatures["tempSet"]) else: self._setBedTemperature(0) self._updateTargetBedTemperature(0) elif http_status_code == 401: self._updateJobState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Repetier on {0} does not allow access to print"). format(self._key)) elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._updateJobState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to Repetier on {0} is not operational" ).format(self._key)) else: self._updateJobState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "?a=listPrinter" in reply.url().toString( ): # Status update from /job: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} job_state = "ready" if "job" in json_data[0]: if json_data[0]["job"] != "none": job_state = "printing" if "paused" in json_data[0]: if json_data[0]["paused"] != False: job_state = "paused" #if "state" in json_data: # if json_data["state"]["flags"]["error"]: # job_state = "error" # elif json_data["state"]["flags"]["paused"]: # job_state = "paused" # elif json_data["state"]["flags"]["printing"]: # job_state = "printing" # elif json_data["state"]["flags"]["ready"]: # job_state = "ready" self._updateJobState(job_state) #progress = json_data["progress"]["completion"] if "done" in json_data[0]: progress = json_data[0]["done"] if progress: self.setProgress(progress) if "start" in json_data[0]: if json_data[0]["start"]: ##self.setTimeElapsed(json_data[0]["start"]) ##self.setTimeElapsed(datetime.datetime.fromtimestamp(json_data[0]["start"]).strftime('%Y-%m-%d %H:%M:%S')) if json_data[0]["printedTimeComp"]: self.setTimeTotal( json_data[0]["start"] - json_data[0]["printedTimeComp"]) if json_data[0]["printTime"]: self.setTimeElapsed(json_data[0]["start"] - json_data[0]["printTime"]) elif progress > 0: self.setTimeTotal(json_data[0]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data[0]["job"]) else: pass # TODO: Handle errors elif self._api_prefix + "?a=getPrinterConfig" in reply.url( ).toString(): # Repetier settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} if "general" in json_data and "sdcard" in json_data[ "general"]: self._sd_supported = json_data["general"]["sdcard"] if "webcam" in json_data and "dynamicUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False Logger.log("d", "RepetierOutputDevice: Checking streamurl") stream_url = json_data["webcam"]["dynamicUrl"].replace( "127.0.0.1", self._address) Logger.log("d", "RepetierOutputDevice: stream_url: %s", stream_url) if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set Repetier camera url to %s", self._camera_url) self._camera_rotation = 180 self._camera_mirror = False self.cameraOrientationChanged.emit() elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "?a=listModels" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on Repetier instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to Repetier as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to Repetier")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Repetier..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the Repetier web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif self._api_prefix + "?a=send" in reply.url().toString( ): # Result from /job command: if http_status_code == 204: Logger.log("d", "Repetier command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "RepetierOutputDevice got an unhandled operation %s", reply.operation()) def _onStreamDownloadProgress(self, bytes_received, bytes_total): self._stream_buffer += self._image_reply.readAll() if self._stream_buffer_start_index == -1: self._stream_buffer_start_index = self._stream_buffer.indexOf( b'\xff\xd8') stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: jpg_data = self._stream_buffer[ self._stream_buffer_start_index:stream_buffer_end_index + 2] self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] self._stream_buffer_start_index = -1 self._camera_image.loadFromData(jpg_data) self.newImage.emit() def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() ##self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on Repetier"), 0, False, -1) ##self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))