class mainApp(): def getRequest(self): url = QUrl("https://jsonplaceholder.typicode.com/users/1") self.request = QNetworkRequest(url) self.manager = QNetworkAccessManager() self.manager.get(self.request) self.manager.finished.connect(self.getResponse) def postRequest(self): url = QUrl("http://httpbin.org/post") dataDict = {} dataDict["fname"] = "Budi" dataDict["lname"] = "Anton" dataJson = json.dumps(dataDict) data = QByteArray() data.append(dataJson) self.request = QNetworkRequest(url) self.request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.manager = QNetworkAccessManager() self.manager.post(self.request, data) self.manager.finished.connect(self.getResponse) def getResponse(self, reply): self.result = reply.readAll() print(str(self.result, "utf-8")) QApplication.quit()
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) layout = QVBoxLayout() self.slider = QSlider(Qt.Horizontal, self) self.slider.valueChanged.connect(self.doRequest) layout.addWidget(self.slider) w = QWidget() w.setLayout(layout) self.setCentralWidget(w) self.show() def doRequest(self): print(self.slider.value()) data = QByteArray() data.append("'brightness':") data.append(str(self.slider.value())) #url = "https://httpbin.org/post" url = "https://postman-echo.com/post" req = QNetworkRequest(QUrl(url)) req.setHeader(QNetworkRequest.ContentTypeHeader, '"Content-Type": "application/json; charset=UTF-8"',) self.nam = QNetworkAccessManager() self.nam.finished.connect(self.handleResponse) self.nam.post(req, data) def handleResponse(self, reply): er = reply.error() if er == QNetworkReply.NoError: bytes_string = reply.readAll() json_array = json.loads(str(bytes_string, 'utf-8')) for key, value in json_array.items(): print(key, value) else: print("Error occurred: ", er) print(reply.errorString())
class ShareServiceInteractor: def __init__(self, view): self.view = view self.buffer = QBuffer() self.network_manager = QNetworkAccessManager(self.view) self.network_manager.finished.connect(self.on_received_response) def on_received_response(self, reply: QNetworkReply): if reply.error() != QNetworkReply.NoError: error_msg = "Unable to create new print share: {}".format(reply.errorString()) logging.error(error_msg) app_settings.app_data_writer.signals.exchange_share_failed.emit(error_msg) return share_location = reply.rawHeader(QByteArray(bytes("Location", encoding="utf-8"))) app_settings.app_data_writer.signals.exchange_share_created.emit(share_location.data().decode()) reply.deleteLater() self.buffer.close() def create_document(self, raw_html): app_config = app_settings.load_configuration() url: QUrl = QUrl(app_config.print_server + "/prints") base64_encoded = str_to_base64_encoded_bytes(raw_html) jdoc = {"document": bytes_to_str(base64_encoded)} jdoc_str = json.dumps(jdoc) self.buffer.setData(str_to_bytes(jdoc_str)) network_request = QNetworkRequest(url) network_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") in_progress_reply = self.network_manager.post(network_request, self.buffer) self.view.finished.connect(in_progress_reply.abort)
def post(self, url, data): if isinstance(data, dict): data = json.dumps(data) if isinstance(data, str): data = QByteArray(len(data), data) elif isinstance(data, bytes): data = QByteArray(data) else: data = str(data) data = QByteArray(len(data), data) return QNetworkAccessManager.post(self, self._getRequest(url), data)
def post(self, url, data): if isinstance(data, dict): data = json.dumps(data) if isinstance(data, str): data = QByteArray(len(data), data) elif isinstance(data, bytes): data = QByteArray(data) else: data = str(data) data = QByteArray(len(data), data) return QNetworkAccessManager.post(self, self._getRequest(url), data)
class UrlSchemeHandler(QWebEngineUrlSchemeHandler): AttrType = QNetworkRequest.User + 1 def __init__(self, *args, **kwargs): super(UrlSchemeHandler, self).__init__(*args, **kwargs) self._manager = QNetworkAccessManager(self) self._manager.finished.connect(self.onFinished) def requestStarted(self, request): # 拦截 # request.fail(QWebEngineUrlRequestJob.RequestDenied) # print('initiator:', request.initiator()) print('requestMethod:', request.requestMethod()) print('requestHeaders:', request.requestHeaders()) url = request.requestUrl() if url.scheme().startswith('myurl'): url.setScheme(url.scheme().replace('myurl', 'http')) print('requestUrl:', url) # 构造真实请求 req = QNetworkRequest(url) req.setAttribute(self.AttrType, request) # 记录 for headerName, headerValue in request.requestHeaders().items(): req.setRawHeader(headerName, headerValue) method = request.requestMethod() # TODO: 这里需要把浏览器内部的cookie获取出来重新设置 if method == b'GET': self._manager.get(req) # TODO: 这里貌似没法得到POST的数据,ajax的请求貌似也有问题 elif method == b'POST': self._manager.post(req) def onFinished(self, reply): req = reply.request() # 获取请求 o_req = req.attribute(self.AttrType, None) if o_req: # Notice: 这里可以对数据做修改再返回 # TODO: 可能还存在 QNetworkAccessManager 与浏览器之间的 cookie 同步问题 o_req.reply( req.header(QNetworkRequest.ContentTypeHeader) or b'text/html', reply) o_req.destroyed.connect(reply.deleteLater)
class Fetch(): data = QtCore.pyqtSignal(dict) def __init__(self, parent): self.session = QNetworkAccessManager(parent) self.cookies = QNetworkCookieJar() self.parent = parent self.session.setCookieJar(self.cookies) def base_handler(self, reply: QNetworkReply): try: response = json.loads(str(reply.readAll(), encoding='utf-8')) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) except: self.parent.warn.add_warn("Http解析错误") return if reply.error() != QNetworkReply.NoError: self.handler_error(response, status_code) else: self.data.emit(response) def get(self, url, param=None): url = QtCore.QUrl(parse_url(url, param)) request = QNetworkRequest(url) reply = self.session.get(request) return reply def post(self, url, param=None, data=None, json=True): if isinstance(data, dict): f = '' for i in data: f += '{}={}&'.format(i, data[i]) data = f[:-1] byte_data = QtCore.QByteArray() byte_data.append(data) url = QtCore.QUrl(parse_url(url, param)) request = QNetworkRequest(url) if json: request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json') reply = self.session.post(request, byte_data) return reply def handler_error(self, response, status_code): if isinstance(response, dict): message = response.get('error', 'unknown') self.parent.warn.add_warn('网络请求错误,错误码为{},原因为{}'.format(status_code, message))
class RequestWidget(QWidget, Requset_Ui): """ Виджет выполнения запроса на сервер """ send = pyqtSignal() read_response = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject') finish = pyqtSignal('PyQt_PyObject') exit = pyqtSignal(bool) error = pyqtSignal('PyQt_PyObject') def on_error(self, error): QMessageBox().critical(self, "Ошибка", str(error)) def __init__(self, worker: ClientWorker, parent=None, *args, **kwargs): QWidget.__init__(self, parent) Requset_Ui.setupUi(self, **kwargs) self.manager = QNetworkAccessManager(self) self.worker = worker self.button.clicked.connect(self.send) self.send.connect(self.on_run) self.manager.finished.connect(self.on_response) self.read_response.connect(self.worker.on_response) self.worker.finish.connect(self.finish) self.progress_bar.finish.connect(self.exit) self.progress_bar.finish.connect(self.close) self.error.connect(self.on_error) @pyqtSlot(QNetworkReply, name='on_response') def on_response(self, reply: QNetworkReply): self.progress_bar.set_part(10, 1, 'Чтение ответа') error_code = reply.error() if error_code == QNetworkReply.NoError: bytes_string = reply.readAll() json_ar = json.loads(str(bytes_string, 'utf-8')) print(json_ar) if json_ar['status'] == 'OK': self.progress_bar.increment() self.read_response.emit(json_ar['data'], self.progress_bar) self.finish.emit(json_ar['data']) self.progress_bar.on_finish('Успешно синхронизированно') else: self.progress_bar.abord() self.button.setEnabled(True) self.error.emit(str(error_code)) self.progress_bar.on_finish( f'Завершено с ошибкой {json_ar["message"]}') else: print(error_code) self.progress_bar.abord() self.button.setEnabled(True) self.error.emit(error_code) self.progress_bar.on_finish(f'Завершено с ошибкой {error_code}') @pyqtSlot(name='on_run') def on_run(self, *args): self.progress_bar.set_part(25, 1, 'Отправка сообщения') self.request = QNetworkRequest(QUrl(self.worker.address)) self.request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.manager.post( self.request, bytearray(JsonParser.dump(self.worker.data), encoding='utf8')) self.progress_bar.increment() self.button.setEnabled(False)
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, instance_id: str, address: str, port: int, properties: dict, parent=None) -> None: super().__init__(device_id=instance_id, 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._id = instance_id self._properties = properties # Properties dict as provided by zero conf self._gcode = [] 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()) ) # NetworkedPrinterOutputDevice defines this as string, so we encode this later 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") name = self._id matches = re.search(r"^\"(.*)\"\._octoprint\._tcp.local$", name) if matches: name = matches.group(1) self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(name) 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._id)) # 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._progress_message = None self._error_message = None self._connection_message = None self._queued_gcode_commands = [] # type: List[str] 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._plugin_data = {} #type: Dict[str, Any] 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 getId(self): return self._id ## 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._name ## 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.encode()) 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().getMultiBuildPlateModel( ).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._id, 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._id)) ## 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._id, 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 not self._auto_print: # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled self._error_message = None elif self.activePrinter.state == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "The printer 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.")) 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") # 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 = [] 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 error_handled = False 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._id)) 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"] actual_temperature = bed_temperatures[ "actual"] if bed_temperatures[ "actual"] is not None else -1 printer.updateBedTemperature(actual_temperature) target_temperature = bed_temperatures[ "target"] if bed_temperatures[ "target"] is not None else -1 printer.updateTargetBedTemperature( target_temperature) else: printer.updateBedTemperature(-1) printer.updateTargetBedTemperature(0) printer_state = "offline" if "state" in json_data: flags = json_data["state"]["flags"] if flags["error"] or flags["closedOrError"]: printer_state = "error" elif flags["paused"] or flags["pausing"]: printer_state = "paused" elif flags["printing"]: printer_state = "printing" elif flags["cancelling"]: printer_state = "aborted" elif flags["ready"] or flags["operational"]: 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._id)) error_handled = True 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._id)) error_handled = True 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"] == "Pausing": print_job_state = "pausing" elif json_data["state"] == "Paused": print_job_state = "paused" elif json_data["state"] == "Printing": print_job_state = "printing" elif json_data["state"] == "Cancelling": print_job_state = "abort" elif json_data["state"] == "Operational": print_job_state = "ready" printer.updateState("idle") print_job.updateState(print_job_state) print_time = json_data["progress"]["printTime"] if print_time: print_job.updateTimeElapsed(print_time) if json_data["progress"][ "completion"]: # not 0 or None or "" print_job.updateTimeTotal( print_time / (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 # See generic error handler below 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 self._camera_url != "" and len(self._printers) > 0: self._printers[0].setCamera( NetworkCamera(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() if "plugins" in json_data: self._plugin_data = json_data["plugins"] can_update_firmware = "firmwareupdater" in self._plugin_data self._output_controller.setCanUpdateFirmware( can_update_firmware) 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 # See generic error handler below 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 # See generic error handler below 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 # See generic error handler below else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) if not error_handled and http_status_code >= 400: # Received an error reply error_string = reply.attribute( QNetworkRequest.HttpReasonPhraseAttribute).decode("utf-8") if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint returned an error: {0}.").format(error_string)) self._error_message.show() return 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) if self._camera_url != "": 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 DiscoverRepetierAction(MachineAction): def __init__(self, parent: QObject = None) -> None: super().__init__("DiscoverRepetierAction", catalog.i18nc("@action", "Connect Repetier")) self._qml_url = "DiscoverRepetierAction.qml" self._application = CuraApplication.getInstance() self._network_plugin = None # 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._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onRequestFinished) self._printers = [""] self._printerlist_reply = None self._settings_reply = None self._settings_reply_timeout = None # type: Optional[NetworkReplyTimeout] self._instance_supports_appkeys = False self._appkey_reply = None # type: Optional[QNetworkReply] self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_instance_id = "" self._appkey_poll_timer = QTimer() self._appkey_poll_timer.setInterval(500) self._appkey_poll_timer.setSingleShot(True) self._appkey_poll_timer.timeout.connect(self._pollApiKey) # 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) self._plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue self._plugin_version = "0.0" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent = ( "%s/%s %s/%s" % (self._application.getApplicationName(), self._application.getVersion(), "RepetierIntegration", self._plugin_version)).encode() self._settings_instance = None self._instance_responded = False self._instance_in_error = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False self._instance_webcamflip_y = False self._instance_webcamflip_x = False self._instance_webcamrot90 = False self._instance_webcamrot270 = False # Load keys cache from preferences self._preferences = self._application.getPreferences() self._preferences.addPreference("Repetier/keys_cache", "") try: self._keys_cache = json.loads( self._preferences.getValue("Repetier/keys_cache")) except ValueError: self._keys_cache = {} if not isinstance(self._keys_cache, dict): self._keys_cache = {} self._additional_components = None ContainerRegistry.getInstance().containerAdded.connect( self._onContainerAdded) self._application.engineCreatedSignal.connect( self._createAdditionalComponentsView) @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: return self._plugin_version @pyqtSlot() def startDiscovery(self) -> None: if not self._plugin_id: return if not self._network_plugin: self._network_plugin = cast( RepetierOutputDevicePlugin, self._application.getOutputDeviceManager(). getOutputDevicePlugin(self._plugin_id)) if not self._network_plugin: return self._network_plugin.addInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.removeInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.instanceListChanged.connect( self._onInstanceDiscovery) self.instancesChanged.emit() else: # Restart bonjour discovery self._network_plugin.startDiscovery() def _onInstanceDiscovery(self, *args) -> None: self.instancesChanged.emit() @pyqtSlot(str) def removeManualInstance(self, name: str) -> None: if not self._network_plugin: return self._network_plugin.removeManualInstance(name) @pyqtSlot(str, str, int, str, bool, str, str, str) def setManualInstance(self, name, address, port, path, useHttps, userName, password, repetierid): if not self._network_plugin: return # This manual printer could replace a current manual printer self._network_plugin.removeManualInstance(name) self._network_plugin.addManualInstance(name, address, port, path, useHttps, userName, password, repetierid) def _onContainerAdded(self, container: "ContainerInterface") -> None: # Add this action as a supported action to all machine definitions if (isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection")): self._application.getMachineActionManager().addSupportedAction( container.getId(), self.getKey()) instancesChanged = pyqtSignal() appKeysSupportedChanged = pyqtSignal() appKeyReceived = pyqtSignal() instanceIdChanged = pyqtSignal() @pyqtProperty("QVariantList", notify=instancesChanged) def discoveredInstances(self) -> List[Any]: if self._network_plugin: instances = list(self._network_plugin.getInstances().values()) instances.sort(key=lambda k: k.name) return instances else: return [] @pyqtSlot(str) def setInstanceId(self, key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: global_container_stack.setMetaDataEntry("repetier_id", key) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() self.instanceIdChanged.emit() @pyqtProperty(str, notify=instanceIdChanged) def instanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" return global_container_stack.getMetaDataEntry("repetier_id", "") @pyqtSlot(str) @pyqtSlot(result=str) def getInstanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: Logger.log( "d", "getInstancdId - self._application.getGlobalContainerStack() returned nothing" ) return "" return global_container_stack.getMetaDataEntry("repetier_id", "") @pyqtSlot(str) def requestApiKey(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url: return ## Request appkey self._appkey_instance_id = instance_id self._appkey_request = self._createRequest( QUrl(base_url + "plugin/appkeys/request"), basic_auth_username, basic_auth_password) self._appkey_request.setRawHeader(b"Content-Type", b"application/json") data = json.dumps({"app": "Cura"}) self._appkey_reply = self._network_manager.post( self._appkey_request, data.encode()) @pyqtSlot() def cancelApiKeyRequest(self) -> None: if self._appkey_reply: if self._appkey_reply.isRunning(): self._appkey_reply.abort() self._appkey_reply = None self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_poll_timer.stop() def _pollApiKey(self) -> None: if not self._appkey_request: return self._appkey_reply = self._network_manager.get(self._appkey_request) @pyqtSlot(str) def probeAppKeySupport(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url or not instance: return instance.getAdditionalData() self._instance_supports_appkeys = False self.appKeysSupportedChanged.emit() appkey_probe_request = self._createRequest( QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username, basic_auth_password) self._appkey_reply = self._network_manager.get(appkey_probe_request) @pyqtSlot(str) def getPrinterList(self, base_url): self._instance_responded = False url = QUrl("http://" + base_url + "/printer/info") Logger.log("d", "getPrinterList:" + url.toString()) settings_request = QNetworkRequest(url) settings_request.setRawHeader("User-Agent".encode(), self._user_agent) self._printerlist_reply = self._network_manager.get(settings_request) return self._printers @pyqtSlot(str, str, str, str, str, str) def testApiKey(self, instance_id: str, base_url, api_key, basic_auth_username="", basic_auth_password="", work_id="") -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) self._instance_responded = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_webcamflip_y = False self._instance_webcamflip_x = False self._instance_webcamrot90 = False self._instance_webcamrot270 = False self._instance_supports_camera = False self.selectedInstanceSettingsChanged.emit() if self._settings_reply: if self._settings_reply.isRunning(): self._settings_reply.abort() self._settings_reply = None if self._settings_reply_timeout: self._settings_reply_timeout = None if ((api_key != "") and (api_key != None) and (work_id != "")): Logger.log( "d", "Trying to access Repetier instance at %s with the provided API key." % base_url) Logger.log("d", "Using %s as work_id" % work_id) Logger.log("d", "Using %s as api_key" % api_key) url = QUrl(base_url + "/printer/api/" + work_id + "?a=getPrinterConfig&apikey=" + api_key) settings_request = QNetworkRequest(url) settings_request.setRawHeader("x-api-key".encode(), api_key.encode()) settings_request.setRawHeader("User-Agent".encode(), self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") settings_request.setRawHeader("Authorization".encode(), ("Basic %s" % data).encode()) self._settings_reply = self._network_manager.get(settings_request) self._settings_instance = instance else: self.getPrinterList(base_url) @pyqtSlot(str) def setApiKey(self, api_key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return global_container_stack.setMetaDataEntry("repetier_api_key", api_key) self._keys_cache[self.getInstanceId()] = api_key keys_cache = base64.b64encode( json.dumps(self._keys_cache).encode("ascii")).decode("ascii") self._preferences.setValue("Repetier/keys_cache", keys_cache) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() # Get the stored API key of this machine # \return key String containing the key of the machine. @pyqtSlot(str, result=str) def getApiKey(self, instance_id: str) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" Logger.log( "d", "APIKEY read %s" % global_container_stack.getMetaDataEntry("repetier_api_key", "")) if instance_id == self.getInstanceId(): api_key = global_container_stack.getMetaDataEntry( "repetier_api_key", "") else: api_key = self._keys_cache.get(instance_id, "") return api_key selectedInstanceSettingsChanged = pyqtSignal() @pyqtProperty(list) def getPrinters(self): return self._printers @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceResponded(self) -> bool: return self._instance_responded @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceInError(self) -> bool: return self._instance_in_error @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceApiKeyAccepted(self) -> bool: return self._instance_api_key_accepted @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsSd(self) -> bool: return self._instance_supports_sd @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamFlipY(self): return self._instance_webcamflip_y @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamFlipX(self): return self._instance_webcamflip_x @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamRot90(self): return self._instance_webcamrot90 @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamRot270(self): return self._instance_webcamrot270 @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsCamera(self) -> bool: return self._instance_supports_camera @pyqtSlot(str, str, str) def setContainerMetaDataEntry(self, container_id: str, key: str, value: str) -> None: containers = ContainerRegistry.getInstance().findContainers( id=container_id) if not containers: Logger.log( "w", "Could not set metadata of container %s because it was not found.", container_id) return containers[0].setMetaDataEntry(key, value) @pyqtSlot(bool) def applyGcodeFlavorFix(self, apply_fix: bool) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode" if global_container_stack.getProperty("machine_gcode_flavor", "value") == gcode_flavor: # No need to add a definition_changes container if the setting is not going to be changed return # Make sure there is a definition_changes container to store the machine settings definition_changes_container = global_container_stack.definitionChanges if definition_changes_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer( global_container_stack, global_container_stack.getId() + "_settings") definition_changes_container.setProperty("machine_gcode_flavor", "value", gcode_flavor) # Update the has_materials metadata flag after switching gcode flavor definition = global_container_stack.getBottom() if (not definition or definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False)): # In other words: only continue for the UM2 (extended), but not for the UM2+ return has_materials = global_container_stack.getProperty( "machine_gcode_flavor", "value") != "UltiGCode" material_container = global_container_stack.material if has_materials: global_container_stack.setMetaDataEntry("has_materials", True) # Set the material container to a sane default if material_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): search_criteria = { "type": "material", "definition": "fdmprinter", "id": global_container_stack.getMetaDataEntry( "preferred_material") } materials = ContainerRegistry.getInstance( ).findInstanceContainers(**search_criteria) if materials: global_container_stack.material = materials[0] else: # The metadata entry is stored in an ini, and ini files are parsed as strings only. # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. if "has_materials" in global_container_stack.getMetaData(): global_container_stack.removeMetaDataEntry("has_materials") global_container_stack.material = ContainerRegistry.getInstance( ).getEmptyInstanceContainer() self._application.globalContainerStackChanged.emit() @pyqtSlot(str) def openWebPage(self, url: str) -> None: QDesktopServices.openUrl(QUrl(url)) def _createAdditionalComponentsView(self) -> None: Logger.log( "d", "Creating additional ui components for Repetier-connected printers." ) path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "RepetierComponents.qml") self._additional_components = self._application.createQmlComponent( path, {"manager": self}) if not self._additional_components: Logger.log( "w", "Could not create additional components for Repetier-connected printers." ) return self._application.addAdditionalComponent( "monitorButtons", self._additional_components.findChild(QObject, "openRepetierButton")) def _onRequestFailed(self, reply: QNetworkReply) -> None: if reply.operation() == QNetworkAccessManager.GetOperation: if "api/settings" in reply.url().toString( ): # Repetier settings dump from /settings: Logger.log( "w", "Connection refused or timeout when trying to access Repetier at %s" % reply.url().toString()) self._instance_in_error = True self.selectedInstanceSettingsChanged.emit() # Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: if reply.error() == QNetworkReply.TimeoutError: QMessageBox.warning(None, 'Connection Timeout', 'Connection Timeout') return http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: #QMessageBox.warning(None,'Connection Attempt2',http_status_code) # Received no or empty reply Logger.log("d", "Received no or empty reply") return if reply.operation() == QNetworkAccessManager.GetOperation: Logger.log("d", reply.url().toString()) if "printer/info" in reply.url().toString( ): # Repetier settings dump from printer/info: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", reply.url().toString()) Logger.log("d", json_data) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} if "printers" in json_data: Logger.log("d", "DiscoverRepetierAction: printers: %s", len(json_data["printers"])) if len(json_data["printers"]) > 0: self._printers = [""] for printerinfo in json_data["printers"]: Logger.log("d", "Slug: %s", printerinfo["slug"]) self._printers.append(printerinfo["slug"]) if "apikey" in json_data: Logger.log("d", "DiscoverRepetierAction: apikey: %s", json_data["apikey"]) global_container_stack = self._application.getGlobalContainerStack( ) if not global_container_stack: return global_container_stack.setMetaDataEntry( "repetier_api_key", json_data["apikey"]) self._keys_cache[ self.getInstanceId()] = json_data["apikey"] keys_cache = base64.b64encode( json.dumps(self._keys_cache).encode( "ascii")).decode("ascii") self._preferences.setValue("Repetier/keys_cache", keys_cache) self.appKeyReceived.emit() if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() if "getPrinterConfig" in reply.url().toString( ): # Repetier settings dump from getPrinterConfig: if http_status_code == 200: Logger.log("d", "API key accepted by Repetier.") self._instance_api_key_accepted = True try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", reply.url().toString()) Logger.log("d", json_data) 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._instance_supports_sd = json_data["general"][ "sdcard"] if "webcam" in json_data and "dynamicUrl" in json_data[ "webcam"]: Logger.log( "d", "DiscoverRepetierAction: Checking streamurl") Logger.log("d", "DiscoverRepetierAction: %s", reply.url()) stream_url = json_data["webcam"]["dynamicUrl"].replace( "127.0.0.1", re.findall(r'[0-9]+(?:\.[0-9]+){3}', reply.url().toString())[0]) Logger.log("d", "DiscoverRepetierAction: stream_url: %s", stream_url) Logger.log("d", "DiscoverRepetierAction: reply_url: %s", reply.url()) if stream_url: #not empty string or None self._instance_supports_camera = True if "webcams" in json_data: Logger.log("d", "DiscoverRepetierAction: webcams: %s", len(json_data["webcams"])) if len(json_data["webcams"]) > 0: if "dynamicUrl" in json_data["webcams"][0]: Logger.log( "d", "DiscoverRepetierAction: Checking streamurl" ) stream_url = json_data["webcams"][0][ "dynamicUrl"].replace( "127.0.0.1", re.findall(r'[0-9]+(?:\.[0-9]+){3}', reply.url().toString())[0]) Logger.log( "d", "DiscoverRepetierAction: stream_url: %s", stream_url) Logger.log( "d", "DiscoverRepetierAction: reply_url: %s", reply.url()) if stream_url: #not empty string or None self._instance_supports_camera = True elif http_status_code == 401: Logger.log("d", "Invalid API key for Repetier.") self._instance_api_key_accepted = False self._instance_in_error = True self._instance_responded = True self.selectedInstanceSettingsChanged.emit() def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest: request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) request.setRawHeader(b"User-Agent", self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") request.setRawHeader(b"Authorization", ("Basic %s" % data).encode()) # ignore SSL errors (eg for self-signed certificates) ssl_configuration = QSslConfiguration.defaultConfiguration() ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone) request.setSslConfiguration(ssl_configuration) return request ## Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before def _deobfuscateString(self, source: str) -> str: try: return base64.b64decode(source.encode("ascii")).decode("ascii") except UnicodeDecodeError: return source def _getInstanceInfo( self, instance_id: str ) -> Tuple[Optional[RepetierOutputDevice], str, str, str]: if not self._network_plugin: return (None, "", "", "") instance = self._network_plugin.getInstanceById(instance_id) if not instance: return (None, "", "", "") return (instance, instance.baseURL, instance.getProperty("userName"), instance.getProperty("password"))
class MyWindowClass(QtWidgets.QMainWindow, form_auto.Ui_MainWindow): def __init__(self, parent=None): QtWidgets.QMainWindow.__init__(self, parent) self.setupUi(self) self.setupNet() self.login_status = False self.Username = '******' self.Usercart = [] self.Shoplist = [] self.last_error_msg = None actRefresh = QtWidgets.QAction(QIcon('icon_refresh.png'), 'Refresh', self) actRefresh.triggered.connect(self.refresh) actRegister = QtWidgets.QAction(QIcon('icon_reg.png'), 'Register', self) actRegister.triggered.connect(self.regDialog) actLogin = QtWidgets.QAction(QIcon('icon_login.png'), 'Log in', self) actLogin.triggered.connect(self.loginDialog) actLogout = QtWidgets.QAction(QIcon('icon_logout.png'), 'Log out', self) actLogout.triggered.connect(self.logoutDialog) actQuit = QtWidgets.QAction(QIcon('icon_quit.png'), 'Quit', self) actQuit.triggered.connect(self.clExit) self.actUsername = QtWidgets.QAction(self.Username, self) self.actCart = QtWidgets.QAction(QIcon('icon_cart.png'), 'Cart', self) self.actCart.setEnabled(False) self.actCart.triggered.connect(self.cartPrepare) self.toolbar = self.addToolBar('Tools') self.toolbar.addAction(actRefresh) self.toolbar.addAction(actLogin) self.toolbar.addAction(self.actUsername) self.toolbar.addAction(self.actCart) self.menuUser.addAction(actLogin) self.menuUser.addAction(self.actCart) self.menuUser.addSeparator() self.menuUser.addAction(actLogout) self.menuQuit.addAction(actRegister) self.menuQuit.addSeparator() self.menuQuit.addAction(actQuit) self.menubar.addAction(self.menuQuit.menuAction()) self.menubar.addAction(self.menuUser.menuAction()) self.getContent() #self.populateTable() self.statusBar().showMessage('Ready.') def setupNet(self): self.manager = QNetworkAccessManager() self.url_list = { "login": "******", "register": "http://127.0.0.1:8000/shop_site/cl_reg/", "content": "http://127.0.0.1:8000/shop_site/cl_index/", "logout": "http://127.0.0.1:8000/shop_site/cl_logout/", "cart": "http://127.0.0.1:8000/shop_site/cl_cart/", "checkout": "http://127.0.0.1:8000/shop_site/cl_check/" } def regDialog(self): dr = QtWidgets.QDialog(parent=self) dr.setWindowTitle("Register in Nothing Shop") dr.setWindowModality(QtCore.Qt.ApplicationModal) dr_layoutV = QtWidgets.QVBoxLayout() label_name = QtWidgets.QLabel(dr) label_name.setText('Enter your name:') dr_layoutV.addWidget(label_name) text_name = QtWidgets.QLineEdit(dr) dr_layoutV.addWidget(text_name) label_pass1 = QtWidgets.QLabel(dr) label_pass1.setText('Enter your password:'******'Enter your password again:') dr_layoutV.addWidget(label_pass2) text_pass2 = QtWidgets.QLineEdit(dr) text_pass2.setEchoMode(2) dr_layoutV.addWidget(text_pass2) dr_layoutH = QtWidgets.QHBoxLayout() submit = QtWidgets.QPushButton("Register", dr) dr_layoutH.addWidget(submit) cancel = QtWidgets.QPushButton("Cancel", dr) dr_layoutH.addWidget(cancel) dr_layoutV.addLayout(dr_layoutH) submit.clicked.connect( partial(self.register, text_name, text_pass1, text_pass2)) cancel.clicked.connect(dr.close) dr.setLayout(dr_layoutV) dr.exec_() def register(self, name, pass1, pass2): print("Registering...") if not name.text() or not pass1.text() or not pass2.text(): msg = QtWidgets.QMessageBox.warning( self, 'Unfilled fields', 'You didn\'t fill all required fields!', QtWidgets.QMessageBox.Ok) return if pass1.text() != pass2.text(): msg = QtWidgets.QMessageBox.warning( self, 'Passwords mismatch', 'Passwords entered in both fields are not same!', QtWidgets.QMessageBox.Ok) return self.statusBar().showMessage('Registering...') url = QtCore.QUrl(self.url_list['register']) request = QNetworkRequest() request.setUrl(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = QtCore.QByteArray() data.append(''.join(['user='******'&'])) data.append(''.join(['password='******'Registration failed', 'Registration failed due to:\n' + bytes(self.replyObjectReg.readAll()).decode("utf-8"), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('Registration failed.') return elif self.replyObjectReg.error() != QNetworkReply.NoError: msg = QtWidgets.QMessageBox.warning( self, 'Registration failed', ''.join([ 'An error was encountered during connecting to shop server.\nError code: ', str(self.replyObjectReg.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('An error was encountered.') return msg = QtWidgets.QMessageBox.information( self, 'Registration successful', 'You are now registered in Nothing Shop!\n You can now use entered username and password to log in!', QtWidgets.QMessageBox.Ok) self.findChild(QtWidgets.QDialog).close() def loginDialog(self): if self.login_status: msg = QtWidgets.QMessageBox.information( self, 'Information', ''.join(['You are already logged in as:\n', self.Username]), QtWidgets.QMessageBox.Ok) else: dl = QtWidgets.QDialog(parent=self) dl.setWindowTitle("Log in Nothing Shop") dl.setWindowModality(QtCore.Qt.ApplicationModal) dl.setFixedSize(220, 110) dl_layoutV = QtWidgets.QVBoxLayout() text_log = QtWidgets.QLineEdit(dl) text_log.setPlaceholderText('Enter your username here...') dl_layoutV.addWidget(text_log) text_pass = QtWidgets.QLineEdit(dl) text_pass.setEchoMode(2) text_pass.setPlaceholderText('Enter your password here...') dl_layoutV.addWidget(text_pass) dl_layoutH = QtWidgets.QHBoxLayout() submit = QtWidgets.QPushButton("Login", dl) dl_layoutH.addWidget(submit) cancel = QtWidgets.QPushButton("Cancel", dl) dl_layoutH.addWidget(cancel) dl_layoutV.addLayout(dl_layoutH) submit.clicked.connect(partial(self.login, text_log, text_pass)) cancel.clicked.connect(dl.close) dl.setLayout(dl_layoutV) dl.exec_() def login(self, name_field, pass_field): print("Logging in...") self.statusBar().showMessage('Logging in...') self.Username = name_field.text() url = QtCore.QUrl(self.url_list['login']) request = QNetworkRequest() request.setUrl(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = QtCore.QByteArray() data.append(''.join(['user='******'&'])) data.append(''.join(['password='******'Login failed', 'Wrong username or password.\nPlease try again.', QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('Login attempt failed.') return elif self.replyObjectLogin.error() != QNetworkReply.NoError: msg = QtWidgets.QMessageBox.warning( self, 'Login failed', ''.join([ 'An error was encountered during connecting to shop server.\n Error code: ', str(self.replyObjectReg.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('An error was encountered.') return self.user_token = bytes( self.replyObjectLogin.readAll()).decode("utf-8") self.login_status = True self.findChild(QtWidgets.QDialog).close() self.actUsername.setText(''.join(['Logged in as ', self.Username])) self.actCart.setEnabled(True) self.statusBar().showMessage('Logged in.') def logoutDialog(self): if self.login_status: msg = QtWidgets.QMessageBox.question( self, 'Confirmation', 'Are you sure you want to log out?', QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if msg == QtWidgets.QMessageBox.Yes: self.user_token = None self.login_status = False self.Username = '******' self.Usercart = [] self.actUsername.setText(self.Username) self.actCart.setEnabled(False) self.statusBar().showMessage('Logged out.') else: msg = QtWidgets.QMessageBox.question( self, 'Information', 'You are already logged out!', QtWidgets.QMessageBox.Ok) def refresh(self): try: self.scrollAreaWidgetContents.setParent(None) except AttributeError: pass self.getContent() def getContent(self): print("Getting content...") url = QtCore.QUrl(self.url_list['content']) request = QNetworkRequest() request.setUrl(url) self.replyObject = self.manager.get(request) self.replyObject.finished.connect(self.populateShopList) def populateShopList(self): print('Populating list...') if self.replyObject.error() == QNetworkReply.ConnectionRefusedError: msg = QtWidgets.QMessageBox.warning( self, 'Shoplist fetching failed', 'Shop server is currently refusing connections.\nCheck your internet connection.', QtWidgets.QMessageBox.Ok) self.statusBar().showMessage("Couldn't connect to shop server.") return elif self.replyObject.error() != QNetworkReply.NoError: msg = QtWidgets.QMessageBox.warning( self, 'Shoplist fetching failed', ''.join([ 'An error was encountered during connecting to shop server.\n Error code: ', str(self.replyObject.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('An error was encountered.') return answerAsJson = bytes(self.replyObject.readAll()).decode("utf-8") try: answerAsText = json.loads(answerAsJson) except json.decoder.JSONDecodeError: msg = QtWidgets.QMessageBox.warning( self, 'Shoplist fetching failed', 'An error was encountered while parsing shoplist data.\nPlease try again.', QtWidgets.QMessageBox.Ok) return self.Shoplist.clear() for item in answerAsText: self.Shoplist.append( Merch(item['id'], item['name'], item['desc'], item['quantity'], item['price'])) data = urllib.request.urlopen(''.join( ['http://127.0.0.1:8000/static/', item['image']])) self.Shoplist[-1].image = QtGui.QPixmap() self.Shoplist[-1].image.loadFromData(data.read()) self.populateTable() def populateTable(self): print("Making tables...") self.scrollAreaWidgetContents = QtWidgets.QWidget() self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 620, 419)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.merchList.setWidget(self.scrollAreaWidgetContents) self.mainVerticalLayer = QtWidgets.QVBoxLayout( self.scrollAreaWidgetContents) self.mainVerticalLayer.setObjectName("mainVerticalLayer") MerchBoxList = [] ImageLabelList = [] NameLabelList = [] DescBoxList = [] RemainLabelList = [] PriceLabelList = [] MerchNumList = [] BuyButtonList = [] MerchBoxHWidgetList = [] MerchBoxH1LayoutList = [] MerchBoxH2LayoutList = [] MerchBoxV1LayoutList = [] MerchBoxV2LayoutList = [] for merch in self.Shoplist: MerchBoxList.append( QtWidgets.QGroupBox(self.scrollAreaWidgetContents)) MerchBoxList[-1].setTitle(merch.name) MerchBoxHWidgetList.append(QtWidgets.QWidget(MerchBoxList[-1])) # Columnt 1 - Image MerchBoxH1LayoutList.append( QtWidgets.QHBoxLayout(MerchBoxHWidgetList[-1])) MerchBoxH1LayoutList[-1].setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint) MerchBoxH1LayoutList[-1].setContentsMargins(0, 0, 10, 6) MerchBoxH1LayoutList[-1].setSpacing(10) ImageLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) ImageLabelList[-1].setMinimumSize(QtCore.QSize(150, 150)) ImageLabelList[-1].setAlignment(QtCore.Qt.AlignCenter) ImageLabelList[-1].setPixmap( merch.image.scaled(ImageLabelList[-1].size(), 1)) MerchBoxH1LayoutList[-1].addWidget(ImageLabelList[-1]) # Column 2 - Name, description MerchBoxV1LayoutList.append(QtWidgets.QVBoxLayout()) MerchBoxV1LayoutList[-1].setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint) MerchBoxV1LayoutList[-1].setContentsMargins(0, 0, 10, 10) MerchBoxV1LayoutList[-1].setSpacing(10) NameLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) NameLabelList[-1].setText(merch.name) MerchBoxV1LayoutList[-1].addWidget(NameLabelList[-1]) DescBoxList.append(QtWidgets.QTextEdit(MerchBoxHWidgetList[-1])) DescBoxList[-1].setPlainText(merch.desc) DescBoxList[-1].setReadOnly(True) MerchBoxV1LayoutList[-1].addWidget(DescBoxList[-1]) MerchBoxH1LayoutList[-1].addLayout(MerchBoxV1LayoutList[-1]) # Column 3 - Remaining, Price, Put in cart controls MerchBoxV2LayoutList.append(QtWidgets.QVBoxLayout()) MerchBoxV2LayoutList[-1].setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint) MerchBoxV2LayoutList[-1].setContentsMargins(0, 0, 10, 10) MerchBoxV2LayoutList[-1].setSpacing(10) RemainLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) RemainLabelList[-1].setAlignment(QtCore.Qt.AlignCenter) RemainLabelList[-1].setText('Remaining: ' + str(merch.quant)) MerchBoxV2LayoutList[-1].addWidget(RemainLabelList[-1]) PriceLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) PriceLabelList[-1].setAlignment(QtCore.Qt.AlignCenter) PriceLabelList[-1].setText('Price: $' + str(merch.price)) MerchBoxV2LayoutList[-1].addWidget(PriceLabelList[-1]) MerchBoxH2LayoutList.append(QtWidgets.QHBoxLayout()) MerchBoxH2LayoutList[-1].setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint) MerchBoxH2LayoutList[-1].setContentsMargins(0, 0, 0, 0) MerchBoxH2LayoutList[-1].setSpacing(10) MerchNumList.append(QtWidgets.QSpinBox(MerchBoxHWidgetList[-1])) MerchNumList[-1].setMinimum(1) MerchNumList[-1].setMaximum(merch.quant) MerchBoxH2LayoutList[-1].addWidget(MerchNumList[-1]) BuyButtonList.append(QtWidgets.QPushButton( MerchBoxHWidgetList[-1])) BuyButtonList[-1].setText("In cart") BuyButtonList[-1].clicked.connect( partial(self.putCart, merch, MerchNumList[-1], True)) if merch.quant == 0: BuyButtonList[-1].setEnabled(False) MerchBoxH2LayoutList[-1].addWidget(BuyButtonList[-1]) MerchBoxV2LayoutList[-1].addLayout(MerchBoxH2LayoutList[-1]) MerchBoxH1LayoutList[-1].addLayout(MerchBoxV2LayoutList[-1]) MerchBoxV1LayoutList[-1].addStretch(1) MerchBoxH2LayoutList[-1].addStretch(1) MerchBoxH1LayoutList[-1].setStretch(0, 3) MerchBoxH1LayoutList[-1].setStretch(1, 8) MerchBoxH1LayoutList[-1].setStretch(2, 2) MerchBoxList[-1].setLayout(MerchBoxH1LayoutList[-1]) self.mainVerticalLayer.addWidget(MerchBoxList[-1]) self.merchList.setWidget(self.scrollAreaWidgetContents) self.horizontalLayout_3.addWidget(self.merchList) def cartPrepare(self): print("Fetching user cart...") self.statusBar().showMessage('Fetching user cart...') url = QtCore.QUrl(self.url_list['cart']) request = QNetworkRequest() request.setUrl(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = QtCore.QByteArray() data.append(''.join(['token=', self.user_token, '&'])) data.append(''.join(['submethod=', 'get'])) self.replyObjectCart = self.manager.post(request, data) self.replyObjectCart.finished.connect(self.cartDialog) def cartDialog(self): if self.replyObjectCart.error( ) == QNetworkReply.AuthenticationRequiredError: msg = QtWidgets.QMessageBox.warning( self, 'Login error', ''.join([ 'There was a problem with your credentials.\nPlease try logging in again.\n Error code: ', str(self.replyObjectCart.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage("Couldn't fetch user cart.") return answerAsJson = bytes(self.replyObjectCart.readAll()).decode("utf-8") answerAsText = json.loads(answerAsJson) self.Usercart.clear() for item in answerAsText: self.Usercart.append({ "merch": next(x for x in self.Shoplist if x.id == item['id']), "num": item['num'] }) dc = QtWidgets.QDialog(parent=self) dc.setWindowTitle(''.join(["Shop Cart - ", self.Username])) dc.setWindowModality(QtCore.Qt.ApplicationModal) #dc.resize(600, 420) dc_layoutV = QtWidgets.QVBoxLayout() lazylist = [] BoxList = [] BoxWidgetList = [] BoxLayoutList = [] ImgList = [] NameList = [] NumList = [] NumEditList = [] RemButtonList = [] if self.Usercart: for item in self.Usercart: BoxList.append(QtWidgets.QGroupBox(dc)) BoxList[-1].setObjectName(''.join( ['Box', str(item['merch'].id)])) BoxWidgetList.append(QtWidgets.QWidget(BoxList[-1])) BoxLayoutList.append(QtWidgets.QHBoxLayout(BoxWidgetList[-1])) BoxLayoutList[-1].setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint) BoxLayoutList[-1].setContentsMargins(5, 5, 5, 5) BoxLayoutList[-1].setSpacing(10) ImgList.append(QtWidgets.QLabel(BoxWidgetList[-1])) ImgList[-1].setMinimumSize(QtCore.QSize(50, 50)) ImgList[-1].setAlignment(QtCore.Qt.AlignCenter) ImgList[-1].setPixmap(item['merch'].image.scaled( ImgList[-1].size(), 1)) BoxLayoutList[-1].addWidget(ImgList[-1]) NameList.append(QtWidgets.QLabel(BoxWidgetList[-1])) NameList[-1].setAlignment(QtCore.Qt.AlignCenter) NameList[-1].setText(item['merch'].name) BoxLayoutList[-1].addWidget(NameList[-1]) NumList.append(QtWidgets.QLabel(BoxWidgetList[-1])) NumList[-1].setAlignment(QtCore.Qt.AlignCenter) NumList[-1].setText(''.join( ['In cart: ', str(item['num']), ' units'])) NumList[-1].setObjectName(''.join( ['NumLabel', str(item['merch'].id)])) BoxLayoutList[-1].addWidget(NumList[-1]) NumEditList.append(QtWidgets.QSpinBox(BoxWidgetList[-1])) NumEditList[-1].setMinimum(1) NumEditList[-1].setMaximum(item['num']) BoxLayoutList[-1].addWidget(NumEditList[-1]) RemButtonList.append(QtWidgets.QPushButton(BoxWidgetList[-1])) RemButtonList[-1].setText('Remove from cart') RemButtonList[-1].clicked.connect( partial(self.putCart, item['merch'], NumEditList[-1], False)) BoxLayoutList[-1].addWidget(RemButtonList[-1]) BoxList[-1].setLayout(BoxLayoutList[-1]) dc_layoutV.addWidget(BoxList[-1]) else: nothinglabel = QtWidgets.QLabel(dc) nothinglabel.setText('Your cart is empty!') dc_layoutV.addWidget(nothinglabel) submitbtn = QtWidgets.QPushButton("Purchase!", dc) submitbtn.clicked.connect(self.purchaseDialog) submitbtn.setObjectName('submitbtn') if not self.Usercart: submitbtn.setEnabled(False) dc_layoutV.addWidget(submitbtn) cancelbtn = QtWidgets.QPushButton("Close cart...", dc) dc_layoutV.addWidget(cancelbtn) cancelbtn.clicked.connect(dc.close) dc.setLayout(dc_layoutV) dc.exec_() def putCart(self, merch, num_field, add): print("Putting merchandize in cart...") self.statusBar().showMessage('Putting merchandize in cart...') if self.login_status: url = QtCore.QUrl(self.url_list['cart']) request = QNetworkRequest() request.setUrl(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = QtCore.QByteArray() data.append(''.join(['token=', self.user_token, '&'])) data.append(''.join(['submethod=', 'change', '&'])) data.append(''.join(['merchid=', str(merch.id), '&'])) if add: data.append(''.join(['merchnum=', str(num_field.value())])) else: data.append(''.join(['merchnum=-', str(num_field.value())])) self.replyObjectPutCart = self.manager.post(request, data) self.replyObjectPutCart.finished.connect(self.putCartResult) else: msg = QtWidgets.QMessageBox.information( self, 'Information', 'You need to log in first\nto use shopping cart!', QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('Not logged in yet.') def putCartResult(self): print("A thing is happening!") if self.replyObjectPutCart.error( ) == QNetworkReply.AuthenticationRequiredError: self.statusBar().showMessage("Authentication failed.") msg = QtWidgets.QMessageBox.warning( self, 'Authentication failed', 'Your login session has expired.\nPlease relog into shop and try again.', QtWidgets.QMessageBox.Ok) return elif self.replyObjectPutCart.error() != QNetworkReply.NoError: msg = QtWidgets.QMessageBox.warning( self, 'Authentication failed', ''.join([ 'An error was encountered during connecting to shop server.\n Error code: ', str(self.replyObjectPutCart.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('An error was encountered.') return answerAsJson = bytes(self.replyObjectPutCart.readAll()).decode("utf-8") answerAsText = json.loads(answerAsJson) print(answerAsText) if 'merch+' in answerAsText: print("A merch+ is happening!") next(x for x in self.Shoplist if x.id == answerAsText['merch+']).quant = answerAsText['quantity'] self.scrollAreaWidgetContents.setParent(None) self.populateTable() self.statusBar().showMessage('Placed merchandize in cart.') elif 'merch-' in answerAsText: print("A merch- is happening!") item = next(x for x in self.Usercart if x['merch'].id == answerAsText['merch-']) item['num'] -= answerAsText['quantity'] item['merch'].quant += answerAsText['quantity'] dc = self.findChild(QtWidgets.QDialog) if item['num'] < 1: dc.findChild(QtWidgets.QGroupBox, ''.join(['Box', str(item['merch'].id)])).hide() self.Usercart.remove(item) if not self.Usercart: dc.findChild(QtWidgets.QPushButton, 'submitbtn').setEnabled(False) else: label = dc.findChild( QtWidgets.QLabel, ''.join(['NumLabel', str(item['merch'].id)])) label.setText(''.join( ['In cart: ', str(item['num']), ' units'])) self.scrollAreaWidgetContents.setParent(None) self.populateTable() self.statusBar().showMessage('Removed merchandize from cart.') def purchaseDialog(self): dp = QtWidgets.QDialog(parent=self) dp.setWindowTitle("Checkout") dp.setWindowModality(QtCore.Qt.ApplicationModal) dp_layoutV = QtWidgets.QVBoxLayout() label_address = QtWidgets.QLabel(dp) label_address.setText('Enter your address:') dp_layoutV.addWidget(label_address) text_address = QtWidgets.QTextEdit(dp) dp_layoutV.addWidget(text_address) label_date = QtWidgets.QLabel(dp) label_date.setText( 'Enter date when you want to receive your purchase:') dp_layoutV.addWidget(label_date) text_date = QtWidgets.QLineEdit(dp) dp_layoutV.addWidget(text_date) label_mail = QtWidgets.QLabel(dp) label_mail.setText('Enter your mail to receive confirmation letter:') dp_layoutV.addWidget(label_mail) text_mail = QtWidgets.QLineEdit(dp) dp_layoutV.addWidget(text_mail) dp_layoutH = QtWidgets.QHBoxLayout() submit = QtWidgets.QPushButton("Purchase!", dp) dp_layoutH.addWidget(submit) cancel = QtWidgets.QPushButton("Cancel", dp) dp_layoutH.addWidget(cancel) dp_layoutV.addLayout(dp_layoutH) submit.clicked.connect(self.purchase) cancel.clicked.connect(dp.close) dp.setLayout(dp_layoutV) dp.exec_() def purchase(self): print("Confirming purchase...") self.statusBar().showMessage('Confirming purchase...') url = QtCore.QUrl(self.url_list['checkout']) request = QNetworkRequest() request.setUrl(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = QtCore.QByteArray() data.append(''.join(['token=', self.user_token])) self.replyObjectPur = self.manager.post(request, data) self.replyObjectPur.finished.connect(self.purchaseFinalize) def purchaseFinalize(self): if self.replyObjectPur.error( ) == QNetworkReply.AuthenticationRequiredError: self.statusBar().showMessage("Authentication failed.") msg = QtWidgets.QMessageBox.warning( self, 'Authentication failed', 'Your login session has expired.\nPlease relog into shop and try again.', QtWidgets.QMessageBox.Ok) return elif self.replyObjectPur.error() != QNetworkReply.NoError: msg = QtWidgets.QMessageBox.warning( self, 'Confirmation failed', ''.join([ 'An error was encountered during connecting to shop server.\n Error code: ', str(self.replyObjectPutCart.error()) ]), QtWidgets.QMessageBox.Ok) self.statusBar().showMessage('An error was encountered.') return msg = QtWidgets.QMessageBox.information( self, 'Purchase completed!', 'Congratulations! Your merchandize is already on it\'s way!', QtWidgets.QMessageBox.Ok) for window in self.findChildren(QtWidgets.QDialog): window.close() def clExit(self): app.quit()
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 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))
class MyWindowClass(PyQt5.QtWidgets.QMainWindow, form_auto.Ui_MainWindow): def __init__(self, parent = None): PyQt5.QtWidgets.QMainWindow.__init__(self, parent) self.setupUi(self) self.setupNet() self.login_status = False self.Username = '******' actRefresh = QtWidgets.QAction('Refresh', self) actRefresh.triggered.connect(self.refresh) actLogin = QtWidgets.QAction('Login', self) actLogin.triggered.connect(self.loginDialog) self.actUsername = QtWidgets.QAction(self.Username, self) self.toolbar = self.addToolBar('Refresh') self.toolbar.addAction(actRefresh) self.toolbar.addAction(actLogin) self.toolbar.addAction(self.actUsername) self.actionQuit.triggered.connect(self.clExit) self.getContent() self.statusBar().showMessage('Ready') def setupNet(self): self.manager = QNetworkAccessManager() self.url_list = {"login": "******", "content": "http://127.0.0.1:8000/shop_site/cl_index/", "logout": "http://127.0.0.1:8000/shop_site/cl_logout/", "checkout": "http://127.0.0.1:8000/shop_site/cl_checkout/"} def login(self, name_field, pass_field): print("Logging in...") self.Username = name_field.text() url = PyQt5.QtCore.QUrl(self.url_list['login']) request = QNetworkRequest() request.setUrl(url) request.setHeader(PyQt5.QtNetwork.QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") data = PyQt5.QtCore.QByteArray() data.append(''.join(['user='******'&'])) data.append(''.join(['password='******'Failed') elif self.replyObjectLogin.error() == PyQt5.QtNetwork.QNetworkReply.NoError: self.user_token = bytes(self.replyObjectLogin.readAll()).decode("utf-8") self.login_status = True self.actUsername.setText(''.join(['Logged in as ', self.Username])) self.statusBar().showMessage('Logged in') def refresh(self): self.scrollAreaWidgetContents.setParent(None) self.getContent() def getContent(self): print("Getting content...") url = PyQt5.QtCore.QUrl(self.url_list['content']) request = QNetworkRequest() request.setUrl(url) self.replyObject = self.manager.get(request) self.replyObject.finished.connect(self.populateTable) def loginDialog(self): dl = PyQt5.QtWidgets.QDialog(parent = self) dl.setWindowTitle("Log in Nothing Shop") dl.setWindowModality(PyQt5.QtCore.Qt.ApplicationModal) dl.setFixedSize(220, 110) dl_layoutV = QtWidgets.QVBoxLayout() text_log = QtWidgets.QLineEdit(dl) text_log.setPlaceholderText('Enter your username here...') dl_layoutV.addWidget(text_log) text_pass = QtWidgets.QLineEdit(dl) text_pass.setEchoMode(2) text_pass.setPlaceholderText('Enter your password here...') dl_layoutV.addWidget(text_pass) dl_layoutH = QtWidgets.QHBoxLayout() submit = QtWidgets.QPushButton("Login", dl) dl_layoutH.addWidget(submit) cancel = QtWidgets.QPushButton("Cancel", dl) dl_layoutH.addWidget(cancel) dl_layoutV.addLayout(dl_layoutH) submit.clicked.connect(partial(self.login, text_log, text_pass)) cancel.clicked.connect(dl.close) dl.setLayout(dl_layoutV) dl.exec_() def populateTable(self): answerAsJson = bytes(self.replyObject.readAll()).decode("utf-8") answerAsText = json.loads(answerAsJson) self.scrollAreaWidgetContents = QtWidgets.QWidget() self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 620, 419)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.merchList.setWidget(self.scrollAreaWidgetContents) self.mainVerticalLayer = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.mainVerticalLayer.setObjectName("mainVerticalLayer") MerchBoxList = [] ImageLabelList = [] NameLabelList = [] DescBoxList = [] RemainLabelList = [] MerchNumList = [] BuyButtonList = [] MerchBoxHWidgetList = [] MerchBoxH1LayoutList = [] MerchBoxH2LayoutList = [] MerchBoxV1LayoutList = [] MerchBoxV2LayoutList = [] pixmap1 = PyQt5.QtGui.QPixmap('no_image.png') for merch in answerAsText: MerchBoxList.append(QtWidgets.QGroupBox(self.scrollAreaWidgetContents)) #MerchBoxList[-1].setObjectName("merchBox1") MerchBoxList[-1].setTitle(merch['name']) MerchBoxHWidgetList.append(QtWidgets.QWidget(MerchBoxList[-1])) #MerchBoxHWidgetList[-1].setGeometry(QtCore.QRect(5, 15, 620, 160)) MerchBoxH1LayoutList.append(QtWidgets.QHBoxLayout(MerchBoxHWidgetList[-1])) MerchBoxH1LayoutList[-1].setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) MerchBoxH1LayoutList[-1].setContentsMargins(0, 0, 10, 6) MerchBoxH1LayoutList[-1].setSpacing(10) ImageLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) ImageLabelList[-1].setMinimumSize(QtCore.QSize(150, 150)) ImageLabelList[-1].setAlignment(QtCore.Qt.AlignCenter) ImageLabelList[-1].setPixmap(pixmap1) #self.merchImage1.setObjectName("merchImage1") MerchBoxH1LayoutList[-1].addWidget(ImageLabelList[-1]) MerchBoxV1LayoutList.append(QtWidgets.QVBoxLayout()) MerchBoxV1LayoutList[-1].setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) MerchBoxV1LayoutList[-1].setContentsMargins(0, 0, 10, 10) MerchBoxV1LayoutList[-1].setSpacing(10) #MerchBoxV1LayoutList[-1].setObjectName("verticalLayout") NameLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) NameLabelList[-1].setText(merch['name']) #self.nameLabel1.setObjectName("nameLabel1") MerchBoxV1LayoutList[-1].addWidget(NameLabelList[-1]) DescBoxList.append(QtWidgets.QTextEdit(MerchBoxHWidgetList[-1])) DescBoxList[-1].setPlainText(merch['desc']) DescBoxList[-1].setReadOnly(True) #self.merchDesc1.setObjectName("merchDesc1") MerchBoxV1LayoutList[-1].addWidget(DescBoxList[-1]) MerchBoxH1LayoutList[-1].addLayout(MerchBoxV1LayoutList[-1]) MerchBoxV2LayoutList.append(QtWidgets.QVBoxLayout()) MerchBoxV2LayoutList[-1].setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) MerchBoxV2LayoutList[-1].setContentsMargins(0, 0, 10, 10) MerchBoxV2LayoutList[-1].setSpacing(10) #MerchBoxV2LayoutList[-1].setObjectName("verticalLayout_2") RemainLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1])) RemainLabelList[-1].setAlignment(QtCore.Qt.AlignCenter) RemainLabelList[-1].setText('Remaining: ' + str(merch['quantity'])) #self.RemainingLabel1.setObjectName("RemainingLabel1") MerchBoxV2LayoutList[-1].addWidget(RemainLabelList[-1]) MerchBoxH2LayoutList.append(QtWidgets.QHBoxLayout()) MerchBoxH2LayoutList[-1].setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) MerchBoxH2LayoutList[-1].setContentsMargins(0, 0, 0, 0) MerchBoxH2LayoutList[-1].setSpacing(10) #self.horizontalLayout.setObjectName("horizontalLayout") MerchNumList.append(QtWidgets.QSpinBox(MerchBoxHWidgetList[-1])) MerchNumList[-1].setMinimum(1) MerchNumList[-1].setMaximum(merch['quantity']) #self.merchNum.setObjectName("merchNum") MerchBoxH2LayoutList[-1].addWidget(MerchNumList[-1]) BuyButtonList.append(QtWidgets.QPushButton(MerchBoxHWidgetList[-1])) BuyButtonList[-1].setText("Buy") #self.BuyButton1.setObjectName("BuyButton1") MerchBoxH2LayoutList[-1].addWidget(BuyButtonList[-1]) MerchBoxV2LayoutList[-1].addLayout(MerchBoxH2LayoutList[-1]) MerchBoxH1LayoutList[-1].addLayout(MerchBoxV2LayoutList[-1]) MerchBoxV1LayoutList[-1].addStretch(1) #MerchBoxV2LayoutList[-1].addStretch(1) #MerchBoxH1LayoutList[-1].addStretch(1) MerchBoxH2LayoutList[-1].addStretch(1) MerchBoxH1LayoutList[-1].setStretch(0, 3) MerchBoxH1LayoutList[-1].setStretch(1, 8) MerchBoxH1LayoutList[-1].setStretch(2, 2) MerchBoxList[-1].setLayout(MerchBoxH1LayoutList[-1]) self.mainVerticalLayer.addWidget(MerchBoxList[-1]) self.merchList.setWidget(self.scrollAreaWidgetContents) self.horizontalLayout_3.addWidget(self.merchList) def clExit(self): app.quit()
class HttpClient(QObject): sig_ended = pyqtSignal(bool) def __init__(self): super().__init__() self.network_manager = QNetworkAccessManager() self.request = QNetworkRequest() self.request.setRawHeader(b"accept", b"application/json") self.request.setRawHeader( b'user-agent', b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, ' b'like Gecko) Chrome/66.0.3359.139 Safari/537.36') self._ended = True self._reply = None self._text = b'' self._string = '' self._status_code = None self._json = None self._headers = None self._connect_to_slot() def reply(self): return self._reply def json(self): return self._json def status(self): return self._status_code def text(self): return self._string def headers(self): return self._headers def content_type(self): content_type = self._headers['content-type'] if 'text/html' in content_type: return 'html' elif 'test/plain' in content_type: return 'text' elif 'application/json' in content_type: return 'json' def _save_header(self, raw_headers): h = {} for t in raw_headers: h.update({str.lower(bytes(t[0]).decode()): bytes(t[1]).decode()}) self._headers = h def set_header(self, header): """ header must consist of strings of dict :param header: dict """ if isinstance(header, dict): for k in header: self.request.setRawHeader(k.encode(), header[k].encode()) def get(self, url: str, header=None): """ Get http request :param url: :param header: """ self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.get(self.request) def post(self, url: str, header: list(tuple()) = None, data: bytes = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.post(self.request, data) def put(self, url: str, header: list(tuple()) = None, data: bytes = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.put(self.request, data) def delete(self, url: str, header: list(tuple()) = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.deleteResource(self.request) def _connect_to_slot(self): self.network_manager.finished.connect(self.slot_reply_finished) def slot_reply_finished(self, data: QNetworkReply): self._reply = data self._text = data.readAll() self._string = bytes(self._text).decode() self._status_code = data.attribute( QNetworkRequest.HttpStatusCodeAttribute) self._save_header(data.rawHeaderPairs()) if self.content_type() == 'json': if len(self._string): self._json = json.loads(self._string) else: self._json = None if self._status_code >= 400: print(self._string) self.sig_ended.emit(True) data.deleteLater()
class Ycm(QObject, CategoryMixin): """YCMD instance control""" YCMD_CMD = ['ycmd'] """Base ycmd command. Useful if ycmd is not in `PATH` or set permanent arguments """ IDLE_SUICIDE = 120 """Maximum time after which ycmd should quit if it has received no requests. A periodic ping is sent by `Ycm` objects. """ CHECK_REPLY_SIGNATURE = True TIMEOUT = 10 def __init__(self, **kwargs): super(Ycm, self).__init__(**kwargs) self.addr = None """Address of the ycmd server.""" self.port = 0 """TCP port of the ycmd server.""" self._ready = False self.secret = '' self.config = {} self.proc = QProcess() self.proc.started.connect(self.procStarted) self.proc.errorOccurred.connect(self.procError) self.proc.finished.connect(self.procFinished) self.pingTimer = QTimer(self) self.pingTimer.timeout.connect(self.ping) self.network = QNetworkAccessManager() qApp().aboutToQuit.connect(self.stop) self.addCategory('ycm_control') def makeConfig(self): self.secret = generate_key() self.config['hmac_secret'] = b64encode(self.secret).decode('ascii') fd, path = tempfile.mkstemp() with open(path, 'w') as fd: fd.write(json.dumps(self.config)) fd.flush() return path def checkReply(self, reply): """Check the ycmd reply is a success. Checks the `reply` has a HTTP 200 status code and the signature is valid. In case of error, raises a :any:`ServerError`. :type reply: QNetworkReply """ reply.content = bytes(reply.readAll()) if reply.error(): raise ServerError(reply.error() + 1000, reply.errorString(), reply.content) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code != 200: data = reply.content.decode('utf-8') try: data = json.loads(data) except (ValueError, JSONDecodeError): LOGGER.info('ycmd replied non-json body: %r', data) raise ServerError(status_code, data) if not self.CHECK_REPLY_SIGNATURE: return actual = b64decode(bytes(reply.rawHeader(HMAC_HEADER))) expected = self._hmacDigest(reply.content) if not hmac.compare_digest(expected, actual): raise RuntimeError('Server signature did not match') def _jsonReply(self, reply): body = reply.content.decode('utf-8') return json.loads(body) def _hmacDigest(self, msg): return hmac.new(self.secret, msg, hashlib.sha256).digest() def _sign(self, verb, path, body=b''): digests = [self._hmacDigest(part) for part in [verb, path, body]] return self._hmacDigest(b''.join(digests)) def _doGet(self, path): url = urlunsplit(('http', self.addr, path, '', '')) sig = self._sign(b'GET', path.encode('utf-8'), b'') headers = { HMAC_HEADER: b64encode(sig) } request = QNetworkRequest(QUrl(url)) for hname in headers: request.setRawHeader(hname, headers[hname]) reply = self.network.get(request) return reply def _doPost(self, path, **params): url = urlunsplit(('http', self.addr, path, '', '')) body = json.dumps(params) sig = self._sign(b'POST', path.encode('utf-8'), body.encode('utf-8')) headers = { HMAC_HEADER: b64encode(sig), 'Content-Type': 'application/json' } request = QNetworkRequest(QUrl(url)) for hname in headers: request.setRawHeader(hname, headers[hname]) reply = self.network.post(request, body) return reply def ping(self): def handleReply(): self.checkReply(reply) if not self._ready: self._ready = True self.pingTimer.start(60000) self.ready.emit() reply = self._doGet('/healthy') reply.finished.connect(handleReply) reply.finished.connect(reply.deleteLater) def start(self): if not self.port: self.port = generate_port() self.addr = 'localhost:%s' % self.port path = self.makeConfig() _, outlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.out.log') _, errlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.err.log') LOGGER.info('ycmd will log to %r and %r', outlogpath, errlogpath) cmd = (self.YCMD_CMD + [ '--idle_suicide_seconds', str(self.IDLE_SUICIDE), '--port', str(self.port), '--options_file', path, '--stdout', outlogpath, '--stderr', errlogpath, ]) LOGGER.debug('will run %r', cmd) self.proc.start(cmd[0], cmd[1:]) self._ready = False @Slot() def stop(self, wait=0.2): if self.proc.state() == QProcess.NotRunning: return self.proc.terminate() if self.proc.state() == QProcess.NotRunning: return time.sleep(wait) self.proc.kill() def isRunning(self): return self.proc.state() == QProcess.Running def connectTo(self, addr): self.addr = addr self._ready = False self.pingTimer.start(1000) @Slot() def procStarted(self): LOGGER.debug('daemon has started') self.pingTimer.start(1000) @Slot(int, QProcess.ExitStatus) def procFinished(self, code, status): LOGGER.info('daemon has exited with status %r and code %r', status, code) self.pingTimer.stop() self._ready = False @Slot(QProcess.ProcessError) def procError(self, error): LOGGER.warning('daemon failed to start (%r): %s', error, self.errorString()) ready = Signal() def _commonPostDict(self, filepath, filetype, contents): d = { 'filepath': filepath, 'filetype': filetype, 'file_data': { filepath: { 'filetypes': [filetype], 'contents': contents } }, 'line_num': 1, # useless but required 'column_num': 1, } return d def _postSimpleRequest(self, urlpath, filepath, filetype, contents, **kwargs): d = self._commonPostDict(filepath, filetype, contents) d.update(**kwargs) return self._doPost(urlpath, **d) def acceptExtraConf(self, filepath, filetype, contents): reply = self._postSimpleRequest('/load_extra_conf_file', filepath, filetype, contents) reply.finished.connect(reply.deleteLater) def rejectExtraConf(self, filepath, filetype, contents): reply = self._postSimpleRequest('/ignore_extra_conf_file', filepath, filetype, contents, _ignore_body=True) reply.finished.connect(reply.deleteLater) def sendParse(self, filepath, filetype, contents, retry_extra=True): d = { 'event_name': 'FileReadyToParse' } reply = self._postSimpleRequest('/event_notification', filepath, filetype, contents, **d) def handleReply(): try: self.checkReply(reply) except ServerError as exc: excdata = exc.args[1] if (isinstance(excdata, dict) and 'exception' in excdata and excdata['exception']['TYPE'] == 'UnknownExtraConf' and retry_extra): confpath = excdata['exception']['extra_conf_file'] LOGGER.info('ycmd encountered %r and wonders if it should be loaded', confpath) accepted = sendIntent(None, 'queryExtraConf', conf=confpath) if accepted: LOGGER.info('extra conf %r will be loaded', confpath) self.acceptExtraConf(confpath, filetype, contents) else: LOGGER.info('extra conf %r will be rejected', confpath) self.rejectExtraConf(confpath, filetype, contents) return self.sendParse(filepath, filetype, contents, retry_extra=False) raise reply.finished.connect(handleReply) reply.finished.connect(reply.deleteLater) if 0: def querySubcommandsList(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/defined_subcommands', filepath, filetype, contents) def _querySubcommand(self, filepath, filetype, contents, line, col, *args): d = { 'command_arguments': list(args) } return self._postSimpleRequest('/run_completer_command', filepath, filetype, contents, **d) def queryGoTo(self, *args): res = self._querySubcommand(*args) if res.get('filepath'): return { 'filepath': res['filepath'], 'line': res['line_num'], 'column': res['column_num'], } def queryInfo(self, *args): res = self._querySubcommand(*args) return res.get('message', '') or res.get('detailed_info', '') def queryCompletions(self, filepath, filetype, contents, line, col): d = { 'line_num': line, 'column_num': col, } return self._postSimpleRequest('/completions', filepath, filetype, contents, **d) if 0: def queryDiagnostic(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/detailed_diagnostic', filepath, filetype, contents) def queryDebug(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/debug_info', filepath, filetype, contents)
class SaveManager: manager = QNetworkAccessManager() def __init__(self, app): self.app = app self.manager.finished.connect(self.saveServerFinished) def save(self, image): # save file on disk path = QFileDialog().getSaveFileName(self.app, 'Save image', '', '*.jpg') image.save(path[0]) def saveBuffer(self, image): cb: QClipboard = QApplication.clipboard() cb.setImage(image, mode=cb.Clipboard) QMessageBox.information(self.app, 'Copy', 'Image copied to buffer') def saveServer(self, image): name = 'tmp.png' url = 'https://joxi-server.herokuapp.com/save' # save file on disk image.save(f"./{name}") with open(f"./{name}", 'rb') as img: files = { 'image': (name, img, 'multipart/form-data', { 'Expires': '0' }) } with requests.Session() as s: # create pyqt QNetworkRequest from requests module r = s.post(url, files=files) request = r.request request.prepare(method="POST", url=url) request_qt = QNetworkRequest(QUrl(url)) for header, value in request.headers.items(): request_qt.setRawHeader(bytes(header, encoding="utf-8"), bytes(value, encoding="utf-8")) # set handlers self.manager = QNetworkAccessManager() self.manager.finished.connect(self.saveServerFinished) self.manager.post(request_qt, request.body) def saveServerFinished(self, reply: QNetworkReply): # if error on server occured if reply.error(): QMessageBox.critical(self.app, 'Query error', reply.errorString()) return try: data = loads(bytes(reply.readAll())) QMessageBox.information(self.app, 'Success query', data['message']) except Exception as e: QMessageBox.critical(self.app, 'Query error', 'Error in parsing') print(e) return # save path to clipboard cb: QClipboard = QApplication.clipboard() cb.setText(data['path'], mode=cb.Clipboard)
class JSControllerObject(QObject): http_request_finished = pyqtSignal(int, int, str) def __init__(self, parent): super().__init__(parent) self.parent = parent self.network_manager = QNetworkAccessManager() def http_response(self, callback_id, reply): if not self.job(): return if reply.error() == 0: data_str = reply.readAll().data().decode(encoding='UTF-8') self.http_request_finished.emit(callback_id, reply.error(), data_str) else: self.http_request_finished.emit(callback_id, reply.error(), '') reply.deleteLater() def post_finished(self, network_reply): error = network_reply.error() url = network_reply.url() url_str = url.toString() if error != 0: request = network_reply.request() request_headers_string = 'Request:\n' for header in request.rawHeaderList(): request_headers_string += '{}: {}\n'.format( header.data().decode(encoding=HTTP_HEADER_CHARSET), request.rawHeader(header).data().decode( encoding=HTTP_HEADER_CHARSET)) response_headers_string = 'Response:\n' for header in network_reply.rawHeaderList(): response_headers_string += '{}: {}\n'.format( header.data().decode(encoding=HTTP_HEADER_CHARSET), network_reply.rawHeader(header).data().decode( encoding=HTTP_HEADER_CHARSET)) logger.error( self.prepend_id( 'e_id="{eid};{estr}" url="{url}"\n{req_h}\n{res_h}'.format( eid=error, estr=network_reply.errorString(), url=url_str, req_h=request_headers_string, res_h=response_headers_string))) else: logger.info('Post successful {}'.format(url_str)) @pyqtSlot(str, str) def post_request(self, url, data): if not self.job(): logger.error( self.prepend_id( 'Invalid State. post_request called when no current job')) return logger.info( self.prepend_id("Posting {} request to {}".format(self.job(), url))) req = QNetworkRequest(QUrl(url)) req.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json') network_reply = self.network_manager.post(req, data.encode('UTF-8')) network_reply.finished.connect( lambda: self.post_finished(network_reply)) @pyqtSlot(QVariant) def log_message(self, message): logger.info(self.prepend_id('js: {}'.format(message))) @pyqtSlot(QVariant) def log_error(self, message): logger.error(self.prepend_id('js: {}'.format(message))) @pyqtProperty(QVariant) def job_dict(self): return self.parent.current_job.dict() def job(self): return self.parent.current_job @pyqtSlot(int, str) def http_request(self, callback_id, url): if not self.parent.current_job: logger.error( self.prepend_id( 'Invalid State. http_request called when no current job')) return qnetwork_reply = self.network_manager.get(QNetworkRequest(QUrl(url))) qnetwork_reply.finished.connect( lambda: self.http_response(callback_id, qnetwork_reply)) @pyqtProperty(str) def current_state(self): if self.parent.current_job.state: return self.parent.current_job.state else: return 'main' @pyqtSlot() def done(self): if not self.parent.current_job: logger.error( self.prepend_id( 'Invalid State. done called when no current job')) return logger.info(self.prepend_id('Done Job {}'.format(self.job()))) self.parent.reset() self.parent.job_finished.emit() @pyqtSlot() def abort(self, retry_after_sec=60): if not self.parent.current_job: logger.error( self.prepend_id( 'Invalid State. abort called when no current job')) return logger.error(self.prepend_id('Job aborting {}'.format(self.job()))) retry_job = self.job().get_retry_job() if retry_job.retry <= MAX_RETRIES: logger.info( self.prepend_id('Retrying :{} after {}sec'.format( retry_job, retry_after_sec))) QTimer.singleShot( retry_after_sec * 1000, Qt.VeryCoarseTimer, lambda: self.parent.new_job_received.emit(retry_job)) else: logger.error( self.prepend_id('Max Retries reached:{}'.format(retry_job))) self.parent.reset() self.parent.job_finished.emit() def prepend_id(self, message): return '[{}] {}'.format(self.parent.id, message) @pyqtSlot(QVariant) def load(self, job_dict): if not self.parent.current_job: logger.error( self.prepend_id( 'Invalid State. load called when no current job')) return self.parent.new_job_received.emit( self.parent.current_job.new_state(**job_dict))
class D3DCloudPrintOutputDevice(OutputDevice): def __init__(self): super().__init__("d3dcloudprint") self.setPriority(1) self.setName("Doodle3D WiFi-Box") self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Doodle3D WiFi-Box")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Doodle3D WiFi-Box")) self.setIconName("print") self._progress_message = None self.base_url = "http://connect.doodle3d.com" self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) self.uploading = False def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): if not self.uploading: self.startUpload() def startUpload(self): Logger.log("d", "Upload to Doodle3D connect started") self.uploading = True self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Connecting 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() # Request upload credentials url = QUrl("https://gcodeserver.doodle3d.com/upload") self._post_reply = self._manager.post(QNetworkRequest(url), QByteArray()) self._post_reply.error.connect(self._onNetworkError) 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)) def _onProgress(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. 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 Doodle3D Connect"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the G-code server") if self._post_reply: self._post_reply.abort() self._post_reply.uploadProgress.disconnect(self._onProgress) self._post_reply = None self._progress_message.hide() return status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not status_code: Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) self.uploading = False self._progress_message.hide() return reply_url = reply.url().toString() if "upload" in reply_url: try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid upload credentials request reply from Connect: Not valid JSON." ) return self.gcodeId = json_data["data"]["id"] self._progress_message.hide() self.uploadGCode(json_data) elif "amazonaws" in reply_url: if status_code in [200, 201, 202, 204]: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "File sent to Doodle3D Connect"), 0) self._progress_message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Connect.."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the Doodle3D Connect web interface")) self._progress_message.actionTriggered.connect( self._onMessageActionTriggered) self._progress_message.show() self.uploading = False Logger.log("d", "uploaded to amazon") else: self._progress_message.hide() Logger.log("w", "Unexpected status code in reply from AWS S3") if reply == self._post_reply: self._post_reply = None def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl( QUrl("%s?uuid=%s" % (self.base_url, self.gcodeId))) elif action == "Cancel": Logger.log("d", "canceled upload to Doodle3D Connect") self._progress_message.hide() if self._post_reply: self._post_reply.abort() self._post_reply = None else: Logger.log("d", "unknown action: %s" % action) def _onNetworkError(self, error): Logger.log( "w", "Network error: %s, %s" % (error, self._post_reply.errorString())) def _onSslError(reply, errors): for error in errors: Logger.log("w", "%s" % error)
class DiscoverOctoPrintAction(MachineAction): def __init__(self, parent: QObject = None) -> None: super().__init__("DiscoverOctoPrintAction", catalog.i18nc("@action", "Connect OctoPrint")) self._qml_url = "DiscoverOctoPrintAction.qml" self._application = CuraApplication.getInstance() self._network_plugin = None # type: Optional[OctoPrintOutputDevicePlugin] # 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._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onRequestFinished) self._settings_reply = None # type: Optional[QNetworkReply] self._appkeys_supported = False self._appkey_reply = None # type: Optional[QNetworkReply] self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_instance_id = "" self._appkey_poll_timer = QTimer() self._appkey_poll_timer.setInterval(500) self._appkey_poll_timer.setSingleShot(True) self._appkey_poll_timer.timeout.connect(self._pollApiKey) # 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) self._plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue self._plugin_version = "0.0" Logger.logException("w", "Could not get version information for the plugin") self._user_agent = ("%s/%s %s/%s" % ( self._application.getApplicationName(), self._application.getVersion(), "OctoPrintPlugin", self._plugin_version )).encode() self._instance_responded = False self._instance_in_error = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False # Load keys cache from preferences self._preferences = self._application.getPreferences() self._preferences.addPreference("octoprint/keys_cache", "") try: self._keys_cache = json.loads(self._deobfuscateString(self._preferences.getValue("octoprint/keys_cache"))) except ValueError: self._keys_cache = {} # type: Dict[str, Any] if not isinstance(self._keys_cache, dict): self._keys_cache = {} # type: Dict[str, Any] self._additional_components = None # type:Optional[QObject] ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) self._application.engineCreatedSignal.connect(self._createAdditionalComponentsView) @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: return self._plugin_version @pyqtSlot() def startDiscovery(self) -> None: if not self._network_plugin: self._network_plugin = self._application.getOutputDeviceManager().getOutputDevicePlugin(self._plugin_id) if not self._network_plugin: return self._network_plugin.addInstanceSignal.connect(self._onInstanceDiscovery) self._network_plugin.removeInstanceSignal.connect(self._onInstanceDiscovery) self._network_plugin.instanceListChanged.connect(self._onInstanceDiscovery) self.instancesChanged.emit() else: # Restart bonjour discovery self._network_plugin.startDiscovery() def _onInstanceDiscovery(self, *args) -> None: self.instancesChanged.emit() @pyqtSlot(str) def removeManualInstance(self, name: str) -> None: if not self._network_plugin: return self._network_plugin.removeManualInstance(name) @pyqtSlot(str, str, int, str, bool, str, str) def setManualInstance(self, name: str, address: str, port: int, path: str, useHttps: bool, userName: str = "", password: str = "") -> None: if not self._network_plugin: return # This manual printer could replace a current manual printer self._network_plugin.removeManualInstance(name) self._network_plugin.addManualInstance(name, address, port, path, useHttps, userName, password) def _onContainerAdded(self, container: "ContainerInterface") -> None: # Add this action as a supported action to all machine definitions if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"): self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey()) instancesChanged = pyqtSignal() appKeysSupportedChanged = pyqtSignal() appKeyReceived = pyqtSignal() @pyqtProperty("QVariantList", notify = instancesChanged) def discoveredInstances(self) -> List[Any]: if self._network_plugin: instances = list(self._network_plugin.getInstances().values()) instances.sort(key = lambda k: k.name) return instances else: return [] @pyqtSlot(str) def setInstanceId(self, key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: global_container_stack.setMetaDataEntry("octoprint_id", key) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() @pyqtSlot(result = str) def getInstanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" return global_container_stack.getMetaDataEntry("octoprint_id", "") @pyqtSlot(str, str, str, str) def requestApiKey(self, instance_id: str, base_url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None: ## Request appkey self._appkey_instance_id = instance_id self._appkey_request = self._createRequest(QUrl(base_url + "plugin/appkeys/request"), basic_auth_username, basic_auth_password) self._appkey_request.setRawHeader(b"Content-Type", b"application/json") data = json.dumps({"app": "Cura"}) self._appkey_reply = self._network_manager.post(self._appkey_request, data.encode()) @pyqtSlot() def cancelApiKeyRequest(self) -> None: if self._appkey_reply: self._appkey_reply.abort() self._appkey_reply = None self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_poll_timer.stop() def _pollApiKey(self) -> None: if not self._appkey_request: return self._appkey_reply = self._network_manager.get(self._appkey_request) @pyqtSlot(str, str, str) def probeAppKeySupport(self, base_url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None: self._appkeys_supported = False self.appKeysSupportedChanged.emit() appkey_probe_request = self._createRequest(QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username, basic_auth_password) self._appkey_request = self._network_manager.get(appkey_probe_request) @pyqtSlot(str, str, str, str) def testApiKey(self, base_url: str, api_key: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None: self._instance_responded = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False self.selectedInstanceSettingsChanged.emit() if api_key != "": Logger.log("d", "Trying to access OctoPrint instance at %s with the provided API key." % base_url) ## Request 'settings' dump settings_request = self._createRequest(QUrl(base_url + "api/settings"), basic_auth_username, basic_auth_password) settings_request.setRawHeader("X-Api-Key".encode(), api_key.encode()) self._settings_reply = self._network_manager.get(settings_request) else: if self._settings_reply: self._settings_reply.abort() self._settings_reply = None @pyqtSlot(str) def setApiKey(self, api_key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return global_container_stack.setMetaDataEntry("octoprint_api_key", base64.b64encode(api_key.encode("ascii")).decode("ascii")) self._keys_cache[self.getInstanceId()] = api_key keys_cache = base64.b64encode(json.dumps(self._keys_cache).encode("ascii")).decode("ascii") self._preferences.setValue("octoprint/keys_cache", keys_cache) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() ## Get the stored API key of an instance, or the one stored in the machine instance # \return key String containing the key of the machine. @pyqtSlot(str, result=str) def getApiKey(self, instance_id: str) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" if instance_id == self.getInstanceId(): api_key = self._deobfuscateString(global_container_stack.getMetaDataEntry("octoprint_api_key", "")) else: api_key = self._keys_cache.get(instance_id, "") return api_key selectedInstanceSettingsChanged = pyqtSignal() @pyqtProperty(bool, notify = selectedInstanceSettingsChanged) def instanceResponded(self) -> bool: return self._instance_responded @pyqtProperty(bool, notify = selectedInstanceSettingsChanged) def instanceInError(self) -> bool: return self._instance_in_error @pyqtProperty(bool, notify = selectedInstanceSettingsChanged) def instanceApiKeyAccepted(self) -> bool: return self._instance_api_key_accepted @pyqtProperty(bool, notify = selectedInstanceSettingsChanged) def instanceSupportsSd(self) -> bool: return self._instance_supports_sd @pyqtProperty(bool, notify = selectedInstanceSettingsChanged) def instanceSupportsCamera(self) -> bool: return self._instance_supports_camera @pyqtProperty(bool, notify = appKeysSupportedChanged) def instanceSupportsAppKeys(self) -> bool: return self._appkeys_supported @pyqtSlot(str, str, str) def setContainerMetaDataEntry(self, container_id: str, key: str, value: str) -> None: containers = ContainerRegistry.getInstance().findContainers(id = container_id) if not containers: Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id) return containers[0].setMetaDataEntry(key, value) @pyqtSlot(bool) def applyGcodeFlavorFix(self, apply_fix: bool) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode" if global_container_stack.getProperty("machine_gcode_flavor", "value") == gcode_flavor: # No need to add a definition_changes container if the setting is not going to be changed return # Make sure there is a definition_changes container to store the machine settings definition_changes_container = global_container_stack.definitionChanges if definition_changes_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer( global_container_stack, global_container_stack.getId() + "_settings") definition_changes_container.setProperty("machine_gcode_flavor", "value", gcode_flavor) # Update the has_materials metadata flag after switching gcode flavor definition = global_container_stack.getBottom() if not definition or definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False): # In other words: only continue for the UM2 (extended), but not for the UM2+ return has_materials = global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode" material_container = global_container_stack.material if has_materials: global_container_stack.setMetaDataEntry("has_materials", True) # Set the material container to a sane default if material_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): search_criteria = { "type": "material", "definition": "fdmprinter", "id": global_container_stack.getMetaDataEntry("preferred_material")} materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria) if materials: global_container_stack.material = materials[0] else: # The metadata entry is stored in an ini, and ini files are parsed as strings only. # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. if "has_materials" in global_container_stack.getMetaData(): global_container_stack.removeMetaDataEntry("has_materials") global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer() self._application.globalContainerStackChanged.emit() @pyqtSlot(str) def openWebPage(self, url: str) -> None: QDesktopServices.openUrl(QUrl(url)) def _createAdditionalComponentsView(self) -> None: Logger.log("d", "Creating additional ui components for OctoPrint-connected printers.") path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "OctoPrintComponents.qml") self._additional_components = self._application.createQmlComponent(path, {"manager": self}) if not self._additional_components: Logger.log("w", "Could not create additional components for OctoPrint-connected printers.") return self._application.addAdditionalComponent("monitorButtons", self._additional_components.findChild(QObject, "openOctoPrintButton")) ## Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.PostOperation: if "/plugin/appkeys/request" in reply.url().toString(): # Initial AppKey request if http_status_code == 201 or http_status_code == 202: Logger.log("w", "Start polling for AppKeys decision") if not self._appkey_request: return self._appkey_request.setUrl(reply.header(QNetworkRequest.LocationHeader)) self._appkey_request.setRawHeader(b"Content-Type", b"") self._appkey_poll_timer.start() elif http_status_code == 404: Logger.log("w", "This instance of OctoPrint does not support AppKeys") self._appkey_request = None # type: Optional[QNetworkRequest] else: response = bytes(reply.readAll()).decode() Logger.log("w", "Unknown response when requesting an AppKey: %d. OctoPrint said %s" % (http_status_code, response)) self._appkey_request = None # type: Optional[QNetworkRequest] if reply.operation() == QNetworkAccessManager.GetOperation: if "/plugin/appkeys/probe" in reply.url().toString(): # Probe for AppKey support if http_status_code == 204: self._appkeys_supported = True else: self._appkeys_supported = False self.appKeysSupportedChanged.emit() if "/plugin/appkeys/request" in reply.url().toString(): # Periodic AppKey request poll if http_status_code == 202: self._appkey_poll_timer.start() elif http_status_code == 200: Logger.log("d", "AppKey granted") self._appkey_request = None # type: Optional[QNetworkRequest] try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received invalid JSON from octoprint instance.") return self._keys_cache[self._appkey_instance_id] = json_data["api_key"] self.appKeyReceived.emit() elif http_status_code == 404: Logger.log("d", "AppKey denied") self._appkey_request = None # type: Optional[QNetworkRequest] else: response = bytes(reply.readAll()).decode() Logger.log("w", "Unknown response when waiting for an AppKey: %d. OctoPrint said %s" % (http_status_code, response)) self._appkey_request = None # type: Optional[QNetworkRequest] if "api/settings" in reply.url().toString(): # OctoPrint settings dump from /settings: self._instance_in_error = False if http_status_code == 200: Logger.log("d", "API key accepted by OctoPrint.") self._instance_api_key_accepted = True 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._instance_supports_sd = json_data["feature"]["sdSupport"] if "webcam" in json_data and "streamUrl" in json_data["webcam"]: stream_url = json_data["webcam"]["streamUrl"] if stream_url: #not empty string or None self._instance_supports_camera = True elif http_status_code == 401: Logger.log("d", "Invalid API key for OctoPrint.") self._instance_api_key_accepted = False elif http_status_code == 502 or http_status_code == 503: Logger.log("d", "OctoPrint is not running.") self._instance_api_key_accepted = False self._instance_in_error = True self._instance_responded = True self.selectedInstanceSettingsChanged.emit() def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest: request = QNetworkRequest(url) request.setRawHeader("User-Agent".encode(), self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode(("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") request.setRawHeader("Authorization".encode(), ("Basic %s" % data).encode()) return request ## Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before def _deobfuscateString(self, source: str) -> str: try: return base64.b64decode(source.encode("ascii")).decode("ascii") except UnicodeDecodeError: return source
class VAbstractNetworkClient(QObject): """Позволяет приложению отправлять сетевые запросы и получать на них ответы. Вся работа с сетью должна быть инкапсулирована в его наследниках. """ @staticmethod def contentTypeFrom(reply: QNetworkReply, default=None): """Определяет и возвращает MIME-тип содержимого (со всеми вспомогательными данными, напр., кодировкой) из http-заголовка `Content-type` в ответе `reply`. Если тип содержимого определить невозможно, возвращает `default`. """ assert reply contentType = reply.header(QNetworkRequest.ContentTypeHeader) if contentType: assert isinstance(contentType, str) # TODO: Delete me! return contentType return default @staticmethod def encodingFrom(reply: QNetworkReply, default: str = "utf-8") -> str: """Определяет и возвращает кодировку содержимого из http-заголовка `Content-type` в ответе `reply`. Если кодировку определить невозможно, возвращает `default`. """ missing = object() contentType = VAbstractNetworkClient.contentTypeFrom(reply, missing) if contentType is missing: return default try: charset = contentType.split(";")[1] assert "charset" in charset encoding = charset.split("=")[1] return encoding.strip() except: return default @staticmethod def waitForFinished(reply: QNetworkReply, timeout: int = -1): """Блокирует вызывающий метод на время, пока не будет завершен сетевой ответ `reply` (то есть пока не испустится сигнал `reply.finished`), или пока не истечет `timeout` миллисекунд. Если `timeout` меньше 0 (по умолчанию), то по данному таймеру блокировка отменяться не будет. """ if reply.isFinished(): return event_loop = QEventLoop() reply.finished.connect(event_loop.quit) if timeout >= 0: timer = QTimer() timer.setInterval(timeout) timer.setSingleShot(True) timer.timeout.connect(event_loop.quit) # Если блокировка отменится до истечения таймера, то при выходе из метода таймер остановится и уничтожится. timer.start() event_loop.exec() reply.finished.disconnect(event_loop.quit) networkAccessManagerChanged = pyqtSignal(QNetworkAccessManager, arguments=['manager']) """Сигнал об изменении менеджера доступа к сети. :param QNetworkAccessManager manager: Новый менеджер доступа к сети. """ baseUrlChanged = pyqtSignal(QUrl, arguments=['url']) """Сигнал об изменении базового url-а. :param QUrl url: Новый базовый url. """ replyFinished = pyqtSignal(QNetworkReply, arguments=['reply']) """Сигнал о завершении ответа на сетевой запрос. :param QNetworkReply reply: Завершенный сетевой запрос. """ def __init__(self, parent: QObject = None): super().__init__(parent) self.__networkAccessManager = QNetworkAccessManager(parent=self) self.__baseUrl = QUrl() def getNetworkAccessManager(self) -> QNetworkAccessManager: """Возвращает менеджер доступа к сети.""" return self.__networkAccessManager def setNetworkAccessManager(self, manager: QNetworkAccessManager): """Устанавливает менеджер доступа к сети.""" assert manager if manager is self.__networkAccessManager: return if self.__networkAccessManager.parent() is self: self.__networkAccessManager.deleteLater() self.__networkAccessManager = manager self.networkAccessManagerChanged.emit(manager) networkAccessManager = pyqtProperty(type=QNetworkAccessManager, fget=getNetworkAccessManager, fset=setNetworkAccessManager, notify=networkAccessManagerChanged, doc="Менеджер доступа к сети.") def getBaseUrl(self) -> QUrl: """Возвращает базовый url.""" return QUrl(self.__baseUrl) def setBaseUrl(self, url: QUrl): """Устанавливает базовый url.""" if url == self.__baseUrl: return self.__baseUrl = QUrl(url) self.baseUrlChanged.emit(url) baseUrl = pyqtProperty(type=QUrl, fget=getBaseUrl, fset=setBaseUrl, notify=baseUrlChanged, doc="Базовый url.") def _connectReplySignals(self, reply: QNetworkReply): """Соединяет сигналы ответа с сигналами клиента.""" reply.finished.connect(lambda: self.replyFinished.emit(reply)) # TODO: Добавить сюда подключение остальных сигналов. def _get(self, request: QNetworkRequest) -> QNetworkReply: """Запускает отправку GET-запроса и возвращает ответ :class:`QNetworkReply` на него.""" reply = self.__networkAccessManager.get(request) self._connectReplySignals(reply) return reply def _head(self, request: QNetworkRequest) -> QNetworkReply: """Запускает отправку HEAD-запроса и возвращает ответ :class:`QNetworkReply` на него.""" reply = self.__networkAccessManager.head(request) self._connectReplySignals(reply) return reply def _post(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку POST-запроса и возвращает ответ :class:`QNetworkReply` на него. _post(self, request: QNetworkRequest) -> QNetworkReply. _post(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _post(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.post(request, data) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"POST") self._connectReplySignals(reply) return reply def _put(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку PUT-запроса и возвращает ответ :class:`QNetworkReply` на него. _put(self, request: QNetworkRequest) -> QNetworkReply. _put(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _put(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.put(request, data) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"PUT") self._connectReplySignals(reply) return reply def _delete(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку DELETE-запроса и возвращает ответ :class:`QNetworkReply` на него. _delete(self, request: QNetworkRequest) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.deleteResource(request) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"DELETE") self._connectReplySignals(reply) return reply def _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data=None) -> QNetworkReply: """Запускает отправку пользовательского запроса и возвращает ответ :class:`QNetworkReply` на него. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytes) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytearray) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QByteArray) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QIODevice) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QHttpMultiPart) -> QNetworkReply. """ reply = self.__networkAccessManager.sendCustomRequest( request, verb, data) self._connectReplySignals(reply) return reply
class Node(QObject): def __init__(self, node_id: str, parent=None): QObject.__init__(self, parent) self._temperature = 293 self._node_id = node_id self._server_url = "localhost" self._access_card = "" self.updateServerUrl(self._server_url) self._all_chart_data = {} self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onNetworkFinished) self._data = None self._enabled = True self._incoming_connections = [] self._outgoing_connections = [] self._onFinishedCallbacks = {} # type: Dict[QNetworkReply, Callable[[QNetworkReply], None]] self._description = "" self._static_properties = {} self._performance = 1 self._target_performance = 1 self._min_performance = 0.5 self._max_performance = 1 self._max_safe_temperature = 500 self._heat_convection = 1.0 self._heat_emissivity = 1.0 self._modifiers = [] self._active = True self._update_timer = QTimer() self._update_timer.setInterval(30000) self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self.partialUpdate) # Timer that is used when the server could not be reached. self._failed_update_timer = QTimer() self._failed_update_timer.setInterval(30000) self._failed_update_timer.setSingleShot(True) self._failed_update_timer.timeout.connect(self.fullUpdate) self._additional_properties = [] self._converted_additional_properties = {} self.server_reachable = False self._optimal_temperature = 200 self._is_temperature_dependant = False self._resources_required = [] self._optional_resources_required = [] self._resources_received = [] self._resources_produced = [] self._resources_provided = [] self._health = 100 self._max_amount_stored = 0 self._amount_stored = 0 self._effectiveness_factor = 0 self._random_delay_timer = QTimer() self._random_delay_timer.setInterval(random.randint(0, 29000)) self._random_delay_timer.setSingleShot(True) self._random_delay_timer.timeout.connect(self.fullUpdate) self._random_delay_timer.start() temperatureChanged = Signal() historyPropertiesChanged = Signal() historyDataChanged = Signal() enabledChanged = Signal() incomingConnectionsChanged = Signal() outgoingConnectionsChanged = Signal() performanceChanged = Signal() staticPropertiesChanged = Signal() modifiersChanged = Signal() additionalPropertiesChanged = Signal() minPerformanceChanged = Signal() maxPerformanceChanged = Signal() maxSafeTemperatureChanged = Signal() heatConvectionChanged = Signal() heatEmissivityChanged = Signal() serverReachableChanged = Signal(bool) isTemperatureDependantChanged = Signal() optimalTemperatureChanged = Signal() targetPerformanceChanged = Signal() resourcesRequiredChanged = Signal() optionalResourcesRequiredChanged = Signal() resourcesReceivedChanged = Signal() resourcesProducedChanged = Signal() resourcesProvidedChanged = Signal() healthChanged = Signal() maxAmountStoredChanged = Signal() amountStoredChanged = Signal() effectivenessFactorChanged = Signal() activeChanged = Signal() def setAccessCard(self, access_card): self._access_card = access_card self._updateUrlsWithAuth(self._server_url, access_card) def _updateUrlsWithAuth(self, server_url, access_card): self._performance_url = f"{self._server_url}/node/{self._node_id}/performance/?accessCardID={self._access_card}" def updateServerUrl(self, server_url): if server_url == "": return self._server_url = f"http://{server_url}:5000" self._source_url = f"{self._server_url}/node/{self._node_id}/" self._incoming_connections_url = f"{self._server_url}/node/{self._node_id}/connections/incoming/" self._all_chart_data_url = f"{self._server_url}/node/{self._node_id}/all_property_chart_data/?showLast=50" self._outgoing_connections_url = f"{self._server_url}/node/{self._node_id}/connections/outgoing/" self._static_properties_url = f"{self._server_url}/node/{self._node_id}/static_properties/" self._modifiers_url = f"{self._server_url}/node/{self._node_id}/modifiers/" self._updateUrlsWithAuth(self._server_url, self._access_card) def get(self, url: str, callback: Callable[[QNetworkReply], None]) -> None: reply = self._network_manager.get(QNetworkRequest(QUrl(url))) self._onFinishedCallbacks[reply] = callback def fullUpdate(self) -> None: """ Request all data of this node from the server :return: """ self.get(self._incoming_connections_url, self._onIncomingConnectionsFinished) self.get(self._outgoing_connections_url, self._onOutgoingConnectionsFinished) self.get(self._static_properties_url, self._onStaticPropertiesFinished) self.partialUpdate() self._update_timer.start() @Slot() def partialUpdate(self) -> None: """ Request all the data that is dynamic :return: """ self.get(self._source_url, self._onSourceUrlFinished) self.get(self._all_chart_data_url, self._onChartDataFinished) self.get(self._modifiers_url, self._onModifiersChanged) def _setServerReachable(self, server_reachable: bool): if self.server_reachable != server_reachable: self.server_reachable = server_reachable self.serverReachableChanged.emit(self.server_reachable) def _readData(self, reply: QNetworkReply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 404: print("Node was not found!") return # For some magical reason, it segfaults if I convert the readAll() data directly to bytes. # So, yes, the extra .data() is needed. data = bytes(reply.readAll().data()) if not data or status_code == 503: self._failed_update_timer.start() self._update_timer.stop() self._setServerReachable(False) return None self._setServerReachable(True) try: return json.loads(data) except json.decoder.JSONDecodeError: return None def updateAdditionalProperties(self, data): if self._additional_properties != data: self._additional_properties = data self._converted_additional_properties = {} # Clear the list and convert them in a way that we can use them in a repeater. for additional_property in data: self._converted_additional_properties[additional_property["key"]] = { "value": additional_property["value"], "max_value": additional_property["max_value"]} self.additionalPropertiesChanged.emit() def _onModifiersChanged(self, reply: QNetworkReply): result = self._readData(reply) if result is None: result = [] if self._modifiers != result: self._modifiers = result self.modifiersChanged.emit() def _onPerformanceChanged(self, reply: QNetworkReply): print("CALLBAAACk") result = self._readData(reply) if not result: return if self._performance != result: self._performance = result self.performanceChanged.emit() @Slot(float) def setPerformance(self, performance): data = "{\"performance\": %s}" % performance self._target_performance = performance self.targetPerformanceChanged.emit() reply = self._network_manager.put(QNetworkRequest(QUrl(self._performance_url)), data.encode()) self._onFinishedCallbacks[reply] = self._onPerformanceChanged @Slot(str) def addModifier(self, modifier: str): data = "{\"modifier_name\": \"%s\"}" % modifier request = QNetworkRequest(QUrl(self._modifiers_url)) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") reply = self._network_manager.post(request, data.encode()) self._onFinishedCallbacks[reply] = self._onModifiersChanged @Property(float, notify=performanceChanged) def performance(self): return self._performance @Property(float, notify=targetPerformanceChanged) def targetPerformance(self): return self._target_performance @Property("QVariantList", notify=resourcesRequiredChanged) def resourcesRequired(self): return self._resources_required @Property("QVariantList", notify=resourcesProducedChanged) def resourcesProduced(self): return self._resources_produced @Property("QVariantList", notify=resourcesProvidedChanged) def resourcesProvided(self): return self._resources_provided @Property("QVariantList", notify=resourcesReceivedChanged) def resourcesReceived(self): return self._resources_received @Property("QVariantList", notify=optionalResourcesRequiredChanged) def optionalResourcesRequired(self): return self._optional_resources_required @Property("QVariantList", notify=modifiersChanged) def modifiers(self): return self._modifiers @Property(float, notify=minPerformanceChanged) def min_performance(self): return self._min_performance @Property(float, notify=maxPerformanceChanged) def max_performance(self): return self._max_performance @Property(float, notify=healthChanged) def health(self): return self._health def _onStaticPropertiesFinished(self, reply: QNetworkReply) -> None: result = self._readData(reply) if not result: return if self._static_properties != result: self._static_properties = result self.staticPropertiesChanged.emit() def _onIncomingConnectionsFinished(self, reply: QNetworkReply): result = self._readData(reply) if not result: return self._incoming_connections = result self.incomingConnectionsChanged.emit() def _onOutgoingConnectionsFinished(self, reply: QNetworkReply): result = self._readData(reply) if not result: return self._outgoing_connections = result self.outgoingConnectionsChanged.emit() @Property("QVariantList", notify=incomingConnectionsChanged) def incomingConnections(self): return self._incoming_connections @Property(str, notify=staticPropertiesChanged) def description(self): return self._static_properties.get("description", "") @Property(str, notify=staticPropertiesChanged) def node_type(self): return self._static_properties.get("node_type", "") @Property(str, notify=staticPropertiesChanged) def custom_description(self): return self._static_properties.get("custom_description", "") @Property("QStringList", notify=staticPropertiesChanged) def supported_modifiers(self): return self._static_properties.get("supported_modifiers", "") @Property(bool, notify=staticPropertiesChanged) def hasSettablePerformance(self): return self._static_properties.get("has_settable_performance", False) @Property(str, notify=staticPropertiesChanged) def label(self): return self._static_properties.get("label", self._node_id) @Property(float, notify=staticPropertiesChanged) def surface_area(self): return self._static_properties.get("surface_area", 0) @Property(float, notify=isTemperatureDependantChanged) def isTemperatureDependant(self): return self._is_temperature_dependant @Property(float, notify=activeChanged) def active(self): return self._active @Property(float, notify=optimalTemperatureChanged) def optimalTemperature(self): return self._optimal_temperature @Property(float, notify=maxSafeTemperatureChanged) def max_safe_temperature(self): return self._max_safe_temperature @Property(float, notify=heatConvectionChanged) def heat_convection(self): return self._heat_convection @Property(float, notify=heatEmissivityChanged) def heat_emissivity(self): return self._heat_emissivity @Property("QVariantList", notify=outgoingConnectionsChanged) def outgoingConnections(self): return self._outgoing_connections def _onSourceUrlFinished(self, reply: QNetworkReply): data = self._readData(reply) if not data: return self._updateProperty("temperature", data["temperature"] - 273.15 ) self._updateProperty("enabled", bool(data["enabled"])) self._updateProperty("active", bool(data["active"])) self._updateProperty("performance", data["performance"]) self._updateProperty("min_performance", data["min_performance"]) self._updateProperty("max_performance", data["max_performance"]) self._updateProperty("max_safe_temperature", data["max_safe_temperature"] - 273.15) self._updateProperty("heat_convection", data["heat_convection"]) self._updateProperty("heat_emissivity", data["heat_emissivity"]) self._updateProperty("is_temperature_dependant", data["is_temperature_dependant"]) self._updateProperty("optimal_temperature", data["optimal_temperature"] - 273.15) self._updateProperty("target_performance", data["target_performance"]) self._updateProperty("health", data["health"]) self._updateProperty("effectiveness_factor", data["effectiveness_factor"]) # We need to update the resources a bit different to prevent recreation of QML items. # As such we use tiny QObjects with their own getters and setters. # If an object is already in the list with the right type, don't recreate it (just update it's value) self.updateResourceList("optional_resources_required", data["optional_resources_required"]) self.updateResourceList("resources_received", data["resources_received"]) self.updateResourceList("resources_required", data["resources_required"]) self.updateResourceList("resources_produced", data["resources_produced"]) self.updateResourceList("resources_provided", data["resources_provided"]) self.updateAdditionalProperties(data["additional_properties"]) def updateResourceList(self, property_name, data): list_to_check = getattr(self, "_" + property_name) list_updated = False for item in data: item_found = False for resource in list_to_check: if item["resource_type"] == resource.type: item_found = True resource.value = item["value"] break if not item_found: list_updated = True list_to_check.append(NodeResource(item["resource_type"], item["value"])) if list_updated: signal_name = "".join(x.capitalize() for x in property_name.split("_")) signal_name = signal_name[0].lower() + signal_name[1:] + "Changed" getattr(self, signal_name).emit() def _updateProperty(self, property_name, property_value): if getattr(self, "_" + property_name) != property_value: setattr(self, "_" + property_name, property_value) signal_name = "".join(x.capitalize() for x in property_name.split("_")) signal_name = signal_name[0].lower() + signal_name[1:] + "Changed" getattr(self, signal_name).emit() def _onPutUpdateFinished(self, reply: QNetworkReply): pass def _onChartDataFinished(self, reply: QNetworkReply): data = self._readData(reply) if not data: return # Offset is given in the reply, but it's not a list of data. Remove it here. if "offset" in data: del data["offset"] all_keys = set(data.keys()) keys_changed = False data_changed = False if set(self._all_chart_data.keys()) != all_keys: keys_changed = True if self._all_chart_data != data: data_changed = True self._all_chart_data = data if data_changed: self.historyDataChanged.emit() if keys_changed: self.historyPropertiesChanged.emit() def _onNetworkFinished(self, reply: QNetworkReply): if reply in self._onFinishedCallbacks: self._onFinishedCallbacks[reply](reply) del self._onFinishedCallbacks[reply] else: print("GOT A RESPONSE WITH NO CALLBACK!", reply.readAll()) @Property(str, constant=True) def id(self): return self._node_id @Property(bool, notify = enabledChanged) def enabled(self): return self._enabled @Property(float, notify=amountStoredChanged) def amount_stored(self): return self._amount_stored @Property(float, notify=effectivenessFactorChanged) def effectiveness_factor(self): return self._effectiveness_factor @Property(float, notify=temperatureChanged) def temperature(self): return self._temperature @Property("QVariantList", notify=historyPropertiesChanged) def allHistoryProperties(self): return list(self._all_chart_data.keys()) @Property("QVariantMap", notify=historyDataChanged) def historyData(self): return self._all_chart_data @Property("QVariantMap", notify=additionalPropertiesChanged) def additionalProperties(self): return self._converted_additional_properties @Slot() def toggleEnabled(self): url = self._source_url + "enabled/" reply = self._network_manager.put(QNetworkRequest(url), QByteArray()) self._onFinishedCallbacks[reply] = self._onPutUpdateFinished # Already trigger an update, so the interface feels snappy self._enabled = not self._enabled self.enabledChanged.emit()
class E5XmlRpcClient(QObject): """ Class implementing a xmlrpc client for Qt. """ def __init__(self, url, parent=None): """ Constructor @param url xmlrpc handler URL (string or QUrl) @param parent parent object (QObject) """ super(E5XmlRpcClient, self).__init__(parent) # attributes for the network objects self.__networkManager = QNetworkAccessManager(self) self.__networkManager.proxyAuthenticationRequired.connect( proxyAuthenticationRequired) self.__networkManager.finished.connect(self.__replyFinished) if SSL_AVAILABLE: self.__sslErrorHandler = E5SslErrorHandler(self) self.__networkManager.sslErrors.connect(self.__sslErrors) self.__callmap = {} self.__request = QNetworkRequest(QUrl(url)) self.__request.setRawHeader(b"User-Agent", b"E5XmlRpcClient/1.0") self.__request.setHeader(QNetworkRequest.ContentTypeHeader, "text/xml") def setUrl(self, url): """ Public slot to set the xmlrpc handler URL. @param url xmlrpc handler URL (string or QUrl) """ url = QUrl(url) if url.isValid(): self.__request.setUrl(url) def call(self, method, args, responseCallback, errorCallback): """ Public method to call the remote server. @param method name of the remote method to be called (string) @param args tuple of method arguments (tuple) @param responseCallback method to be called with the returned result as a tuple (function) @param errorCallback method to be called in case of an error with error code and error string (function) """ assert isinstance(args, tuple), \ "argument must be tuple or Fault instance" data = xmlrpc.dumps(args, method).encode("utf-8") reply = self.__networkManager.post( self.__request, QByteArray(data)) self.__callmap[reply] = (responseCallback, errorCallback) def abort(self): """ Public method to abort all calls. """ for reply in list(self.__callmap): if reply.isRunning(): reply.abort() def __sslErrors(self, reply, errors): """ Private slot to handle SSL errors. @param reply reference to the reply object (QNetworkReply) @param errors list of SSL errors (list of QSslError) """ ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] if ignored == E5SslErrorHandler.NotIgnored and reply in self.__callmap: self.__callmap[reply][1](xmlrpc.TRANSPORT_ERROR, self.tr( "SSL Error")) def __replyFinished(self, reply): """ Private slot handling the receipt of a reply. @param reply reference to the finished reply (QNetworkReply) """ if reply not in self.__callmap: return if reply.error() != QNetworkReply.NoError: self.__callmap[reply][1](xmlrpc.TRANSPORT_ERROR, reply.errorString()) else: data = bytes(reply.readAll()).decode("utf-8") try: data = xmlrpc.loads(data)[0] self.__callmap[reply][0](data) except xmlrpc.Fault as fault: self.__callmap[reply][1](fault.faultCode, fault.faultString) reply.deleteLater() del self.__callmap[reply]
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 HTTPClient(QObject): """An HTTP client based on QNetworkAccessManager. Intended for APIs, automatically decodes data. Attributes: _nam: The QNetworkAccessManager used. _timers: A {QNetworkReply: QTimer} dict. Signals: success: Emitted when the operation succeeded. arg: The received data. error: Emitted when the request failed. arg: The error message, as string. """ success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._nam = QNetworkAccessManager(self) self._timers = {} def post(self, url, data=None): """Create a new POST request. Args: url: The URL to post to, as QUrl. data: A dict of data to send. """ if data is None: data = {} encoded_data = urllib.parse.urlencode(data).encode('utf-8') request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') reply = self._nam.post(request, encoded_data) self._handle_reply(reply) def get(self, url): """Create a new GET request. Emits success/error when done. Args: url: The URL to access, as QUrl. """ request = QNetworkRequest(url) reply = self._nam.get(request) self._handle_reply(reply) def _handle_reply(self, reply): """Handle a new QNetworkReply.""" if reply.isFinished(): self.on_reply_finished(reply) else: timer = QTimer(self) timer.setInterval(10000) timer.timeout.connect(reply.abort) timer.start() self._timers[reply] = timer reply.finished.connect(functools.partial( self.on_reply_finished, reply)) def on_reply_finished(self, reply): """Read the data and finish when the reply finished. Args: reply: The QNetworkReply which finished. """ timer = self._timers.pop(reply) if timer is not None: timer.stop() timer.deleteLater() if reply.error() != QNetworkReply.NoError: self.error.emit(reply.errorString()) return try: data = bytes(reply.readAll()).decode('utf-8') except UnicodeDecodeError: self.error.emit("Invalid UTF-8 data received in reply!") return self.success.emit(data)
class LoginWindow(QWidget): tryLogin = pyqtSignal(dict, name="tryLogin") loginSuccess = pyqtSignal(name="loginSuccess") def __init__(self, state, device): super().__init__() self.settings = QSettings("Capstone", "posture-of-success") self.state = state self.device = device self.is_waiting = False self.login = QNetworkAccessManager() self.login.finished.connect(self.login_response) self.error_dialog = QErrorMessage() # Setup UI self.setWindowTitle("성공의 자세") self.setWindowIcon(QIcon('icon.png')) self.setGeometry(300, 300, 560, 460) label = QLabel("로그인") label.setProperty("class", "huge") self.id_field = QLineEdit(self.settings.value("login/id", "")) self.id_field.setPlaceholderText("Email") self.pw_field = QLineEdit(self.settings.value("login/pw", "")) self.pw_field.setPlaceholderText("Password") self.pw_field.setEchoMode(QLineEdit.Password) self.pw_field.returnPressed.connect(self.login_clicked) fields_box = QVBoxLayout() fields_box.addWidget(self.id_field) fields_box.addWidget(self.pw_field) self.login_button = QPushButton("로그인") register_button = QPushButton("회원가입") register_button.setProperty("class", "inverted") self.save_login_checkbox = QCheckBox("자동 로그인") self.device_status = QLabel() grid = QGridLayout() grid.addWidget(self.save_login_checkbox, 0, 0) grid.addWidget(self.login_button, 0, 1) grid.addWidget(self.device_status, 1, 0) grid.addWidget(register_button, 1, 1) grid.setSpacing(20) login_box = QVBoxLayout() login_box.addStretch(1) login_box.addWidget(label) login_box.addLayout(fields_box) login_box.addLayout(grid) login_box.addStretch(1) login_box.setContentsMargins(50, 50, 50, 50) login_box.setSpacing(20) frame = QWidget() frame.setLayout(login_box) frame.setProperty("class", "frame") main_layout = QVBoxLayout() main_layout.addWidget(frame) main_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(main_layout) self.setProperty("class", "root") self.setContentsMargins(0, 0, 0, 0) self.setAttribute(Qt.WA_TranslucentBackground) # self.setWindowFlags(Qt.FramelessWindowHint) # Connect self.update_status() self.device.connectedChanged.connect(self.update_status) self.login_button.clicked.connect(self.login_clicked) register_button.clicked.connect(register) if self.settings.value("login/id", "") != "": self.save_login_checkbox.setChecked(True) self.login_clicked() def login_clicked(self): if not self.is_waiting: print("login clicked") self.is_waiting = True self.login_button.setEnabled(False) data = { "email": self.id_field.text(), "password": self.pw_field.text() } req = QNetworkRequest(QUrl(SERVER_BASE_ADDR + "/api/device/signin")) req.setRawHeader("Content-Type".encode('utf-8'), "application/json".encode('utf-8')) self.login.post(req, json.dumps(data).encode('utf-8')) def login_response(self, response: QNetworkReply): self.is_waiting = False self.login_button.setEnabled(True) err = response.error() if err == QNetworkReply.NoError: reply = str(response.readAll(), 'utf-8') reply_json = json.loads(reply) print(reply_json) if "success" in reply_json and reply_json["success"]: self.state.login(reply_json["email"], reply_json["score"]) self.state.sensor_values = eval(reply_json["sensor_data"]) self.loginSuccess.emit() self.close() else: self.error_dialog.showMessage('아이디나 비밀번호가 맞지 않습니다!') else: self.error_dialog.showMessage("서버 연결에 실패했습니다. 에러 코드=" + str(err)) def save_login(self, id, pw): self.settings.setValue("login/id", id) self.settings.setValue("login/pw", pw) self.settings.sync() def update_status(self): if self.device.is_connected(): self.device_status.setText("장치가 연결되었습니다.") else: self.device_status.setText("장치 연결을 기다리는 중...") def logout(self): self.state.logout() self.show() def closeEvent(self, event: QCloseEvent): if self.save_login_checkbox.isChecked(): print("saving login info") self.save_login(self.id_field.text(), self.pw_field.text()) else: print("deleting login info") self.save_login("", "") if not self.state.is_logged_in(): QCoreApplication.exit()
class CloudApiClient: # The cloud URL to use for this remote cluster. ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) ## Initializes a new cloud API client. # \param account: The user's account object # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: super().__init__() self._manager = QNetworkAccessManager() self._account = account self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] # In order to avoid garbage collection we keep the callbacks in this list. self._anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Gets the account used for the API. @property def account(self) -> Account: return self._account ## Retrieves all the clusters for the user that is currently logged in. # \param on_finished: The function to be called after the result is parsed. def getClusters( self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: url = "{}/clusters".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse) ## Retrieves the status of the given cluster. # \param cluster_id: The ID of the cluster. # \param on_finished: The function to be called after the result is parsed. def getClusterStatus( self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterStatus) ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. # \param on_finished: The function to be called after the result is parsed. def requestUpload( self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) self._addCallback(reply, on_finished, CloudPrintJobResponse) ## Uploads a print job tool path to the cloud. # \param print_job: The object received after requesting an upload with `self.requestUpload`. # \param mesh: The tool path data to be uploaded. # \param on_finished: The function to be called after the upload is successful. # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. # \param cluster_id: The ID of the cluster. # \param job_id: The ID of the print job. # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) reply = self._manager.post(self._createEmptyRequest(url), b"") self._addCallback(reply, on_finished, CloudPrintResponse) ## Send a print job action to the cluster for the given print job. # \param cluster_id: The ID of the cluster. # \param cluster_job_id: The ID of the print job within the cluster. # \param action: The name of the action to execute. def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None: body = b"" if data: try: body = json.dumps({"data": data}).encode() except JSONDecodeError as err: Logger.log("w", "Could not encode body: %s", err) return url = "{}/clusters/{}/print_jobs/{}/action/{}".format( self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) self._manager.post(self._createEmptyRequest(url), body) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) access_token = self._account.accessToken if access_token: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code), id=str(time()), http_status="500") Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: if "data" in response: data = response["data"] if isinstance(data, list): results = [model_class(**c) for c in data] # type: List[CloudApiClientModel] on_finished_list = cast( Callable[[List[CloudApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**data) # type: CloudApiClientModel on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished) on_finished_item(result) elif "errors" in response: self._on_error( [CloudError(**error) for error in response["errors"]]) else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either # a list or a single item. # \param model: The type of the model to convert the response to. def _addCallback( self, reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: def parse() -> None: # Don't try to parse the reply if we didn't get one if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) is None: return status_code, response = self._parseReply(reply) self._anti_gc_callbacks.remove(parse) self._parseModels(response, on_finished, model) return self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)
class Ycm(QObject, CategoryMixin): """YCMD instance control""" YCMD_CMD = ['ycmd'] """Base ycmd command. Useful if ycmd is not in `PATH` or set permanent arguments """ IDLE_SUICIDE = 120 """Maximum time after which ycmd should quit if it has received no requests. A periodic ping is sent by `Ycm` objects. """ CHECK_REPLY_SIGNATURE = True TIMEOUT = 10 def __init__(self, **kwargs): super(Ycm, self).__init__(**kwargs) self.addr = None """Address of the ycmd server.""" self.port = 0 """TCP port of the ycmd server.""" self._ready = False self.secret = '' self.config = {} self.proc = QProcess() self.proc.started.connect(self.procStarted) self.proc.errorOccurred.connect(self.procError) self.proc.finished.connect(self.procFinished) self.pingTimer = QTimer(self) self.pingTimer.timeout.connect(self.ping) self.network = QNetworkAccessManager() qApp().aboutToQuit.connect(self.stop) self.addCategory('ycm_control') def makeConfig(self): self.secret = generate_key() self.config['hmac_secret'] = b64encode(self.secret).decode('ascii') fd, path = tempfile.mkstemp() with open(path, 'w') as fd: fd.write(json.dumps(self.config)) fd.flush() return path def checkReply(self, reply): """Check the ycmd reply is a success. Checks the `reply` has a HTTP 200 status code and the signature is valid. In case of error, raises a :any:`ServerError`. :type reply: QNetworkReply """ reply.content = bytes(reply.readAll()) if reply.error(): raise ServerError(reply.error() + 1000, reply.errorString(), reply.content) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code != 200: data = reply.content.decode('utf-8') try: data = json.loads(data) except (ValueError, JSONDecodeError): LOGGER.info('ycmd replied non-json body: %r', data) raise ServerError(status_code, data) if not self.CHECK_REPLY_SIGNATURE: return actual = b64decode(bytes(reply.rawHeader(HMAC_HEADER))) expected = self._hmacDigest(reply.content) if not hmac.compare_digest(expected, actual): raise RuntimeError('Server signature did not match') def _jsonReply(self, reply): body = reply.content.decode('utf-8') return json.loads(body) def _hmacDigest(self, msg): return hmac.new(self.secret, msg, hashlib.sha256).digest() def _sign(self, verb, path, body=b''): digests = [self._hmacDigest(part) for part in [verb, path, body]] return self._hmacDigest(b''.join(digests)) def _doGet(self, path): url = urlunsplit(('http', self.addr, path, '', '')) sig = self._sign(b'GET', path.encode('utf-8'), b'') headers = {HMAC_HEADER: b64encode(sig)} request = QNetworkRequest(QUrl(url)) for hname in headers: request.setRawHeader(hname, headers[hname]) LOGGER_REQUESTS.debug('GET %r', url) reply = self.network.get(request) return reply def _doPost(self, path, **params): url = urlunsplit(('http', self.addr, path, '', '')) body = json.dumps(params).encode('utf-8') sig = self._sign(b'POST', path.encode('utf-8'), body) headers = { HMAC_HEADER: b64encode(sig), b'Content-Type': b'application/json' } request = QNetworkRequest(QUrl(url)) for hname in headers: request.setRawHeader(hname, headers[hname]) LOGGER_REQUESTS.debug('POST %r with data %r', url, body) reply = self.network.post(request, body) return reply def ping(self): def handleReply(): self.checkReply(reply) if not self._ready: self._ready = True self.pingTimer.start(60000) self.ready.emit() reply = self._doGet('/healthy') reply.finished.connect(handleReply) reply.finished.connect(reply.deleteLater) def start(self): if not self.port: self.port = generate_port() self.addr = 'localhost:%s' % self.port path = self.makeConfig() _, outlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.out.log') _, errlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.err.log') LOGGER.info('ycmd will log to %r and %r', outlogpath, errlogpath) cmd = (self.YCMD_CMD + [ '--idle_suicide_seconds', str(self.IDLE_SUICIDE), '--port', str(self.port), '--options_file', path, '--stdout', outlogpath, '--stderr', errlogpath, ]) LOGGER.debug('will run %r', cmd) self.proc.start(cmd[0], cmd[1:]) self._ready = False @Slot() def stop(self, wait=0.2): if self.proc.state() == QProcess.NotRunning: return self.proc.terminate() if self.proc.state() == QProcess.NotRunning: return time.sleep(wait) self.proc.kill() def isRunning(self): return self.proc.state() == QProcess.Running def connectTo(self, addr): self.addr = addr self._ready = False self.pingTimer.start(1000) @Slot() def procStarted(self): LOGGER.debug('daemon has started') self.pingTimer.start(1000) @Slot(int, QProcess.ExitStatus) def procFinished(self, code, status): LOGGER.info('daemon has exited with status %r and code %r', status, code) self.pingTimer.stop() self._ready = False @Slot(QProcess.ProcessError) def procError(self, error): LOGGER.warning('daemon failed to start (%r): %s', error, self.errorString()) ready = Signal() def _commonPostDict(self, filepath, filetype, contents, line=1, column=1): d = { 'filepath': filepath, 'filetype': filetype, 'file_data': { filepath: { 'filetypes': [filetype], 'contents': contents } }, 'line_num': line, 'column_num': column, } return d def _postSimpleRequest(self, urlpath, filepath, filetype, contents, **kwargs): d = self._commonPostDict(filepath, filetype, contents) d.update(**kwargs) return self._doPost(urlpath, **d) def acceptExtraConf(self, filepath, filetype, contents): reply = self._postSimpleRequest('/load_extra_conf_file', filepath, filetype, contents) reply.finished.connect(reply.deleteLater) def rejectExtraConf(self, filepath, filetype, contents): reply = self._postSimpleRequest('/ignore_extra_conf_file', filepath, filetype, contents, _ignore_body=True) reply.finished.connect(reply.deleteLater) def sendParse(self, filepath, filetype, contents, retry_extra=True): d = {'event_name': 'FileReadyToParse'} reply = self._postSimpleRequest('/event_notification', filepath, filetype, contents, **d) def handleReply(): try: self.checkReply(reply) except ServerError as exc: excdata = exc.args[1] if (isinstance(excdata, dict) and 'exception' in excdata and excdata['exception']['TYPE'] == 'UnknownExtraConf' and retry_extra): confpath = excdata['exception']['extra_conf_file'] LOGGER.info( 'ycmd encountered %r and wonders if it should be loaded', confpath) accepted = sendIntent(None, 'queryExtraConf', conf=confpath) if accepted: LOGGER.info('extra conf %r will be loaded', confpath) self.acceptExtraConf(confpath, filetype, contents) else: LOGGER.info('extra conf %r will be rejected', confpath) self.rejectExtraConf(confpath, filetype, contents) return self.sendParse(filepath, filetype, contents, retry_extra=False) raise reply.finished.connect(handleReply) reply.finished.connect(reply.deleteLater) def querySubcommandsList(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/defined_subcommands', filepath, filetype, contents) def querySubcommand(self, filepath, filetype, contents, line, col, *args): d = { 'command_arguments': list(args), 'line_num': line, 'column_num': col, } return self._postSimpleRequest('/run_completer_command', filepath, filetype, contents, **d) def queryCompletions(self, filepath, filetype, contents, line, col): d = { 'line_num': line, 'column_num': col, } return self._postSimpleRequest('/completions', filepath, filetype, contents, **d) if 0: def queryDiagnostic(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/detailed_diagnostic', filepath, filetype, contents) def queryDebug(self, filepath, filetype, contents, line, col): return self._postSimpleRequest('/debug_info', filepath, filetype, contents)
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 DiscoverOctoPrintAction(MachineAction): def __init__(self, parent: QObject = None) -> None: super().__init__("DiscoverOctoPrintAction", catalog.i18nc("@action", "Connect OctoPrint")) self._qml_url = os.path.join("qml", "DiscoverOctoPrintAction.qml") self._application = CuraApplication.getInstance() self._network_plugin = None # type: Optional[OctoPrintOutputDevicePlugin] # 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._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onRequestFinished) self._settings_reply = None # type: Optional[QNetworkReply] self._settings_reply_timeout = None # type: Optional[NetworkReplyTimeout] self._instance_supports_appkeys = False self._appkey_reply = None # type: Optional[QNetworkReply] self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_instance_id = "" self._appkey_poll_timer = QTimer() self._appkey_poll_timer.setInterval(500) self._appkey_poll_timer.setSingleShot(True) self._appkey_poll_timer.timeout.connect(self._pollApiKey) # 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) self._plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue self._plugin_version = "0.0" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent = ("%s/%s %s/%s" % (self._application.getApplicationName(), self._application.getVersion(), "OctoPrintPlugin", self._plugin_version)).encode() self._settings_instance = None # type: Optional[OctoPrintOutputDevice] self._instance_responded = False self._instance_in_error = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False self._instance_installed_plugins = [] # type: List[str] self._power_plugins_manager = PowerPlugins() # Load keys cache from preferences self._preferences = self._application.getPreferences() self._preferences.addPreference("octoprint/keys_cache", "") try: self._keys_cache = json.loads( self._deobfuscateString( self._preferences.getValue("octoprint/keys_cache"))) except ValueError: self._keys_cache = {} # type: Dict[str, Any] if not isinstance(self._keys_cache, dict): self._keys_cache = {} # type: Dict[str, Any] self._additional_components = None # type:Optional[QObject] ContainerRegistry.getInstance().containerAdded.connect( self._onContainerAdded) self._application.engineCreatedSignal.connect( self._createAdditionalComponentsView) @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: return self._plugin_version @pyqtSlot() def startDiscovery(self) -> None: if not self._plugin_id: return if not self._network_plugin: self._network_plugin = cast( OctoPrintOutputDevicePlugin, self._application.getOutputDeviceManager(). getOutputDevicePlugin(self._plugin_id)) if not self._network_plugin: return self._network_plugin.addInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.removeInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.instanceListChanged.connect( self._onInstanceDiscovery) self.instancesChanged.emit() else: # Restart bonjour discovery self._network_plugin.startDiscovery() def _onInstanceDiscovery(self, *args) -> None: self.instancesChanged.emit() @pyqtSlot(str) def removeManualInstance(self, name: str) -> None: if not self._network_plugin: return self._network_plugin.removeManualInstance(name) @pyqtSlot(str, str, int, str, bool, str, str) def setManualInstance(self, name: str, address: str, port: int, path: str, useHttps: bool, userName: str = "", password: str = "") -> None: if not self._network_plugin: return # This manual printer could replace a current manual printer self._network_plugin.removeManualInstance(name) self._network_plugin.addManualInstance(name, address, port, path, useHttps, userName, password) def _onContainerAdded(self, container: "ContainerInterface") -> None: # Add this action as a supported action to all machine definitions if (isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection")): self._application.getMachineActionManager().addSupportedAction( container.getId(), self.getKey()) instancesChanged = pyqtSignal() appKeysSupportedChanged = pyqtSignal() appKeyReceived = pyqtSignal() instanceIdChanged = pyqtSignal() @pyqtProperty("QVariantList", notify=instancesChanged) def discoveredInstances(self) -> List[Any]: if self._network_plugin: instances = list(self._network_plugin.getInstances().values()) instances.sort(key=lambda k: k.name) return instances else: return [] @pyqtSlot(str) def setInstanceId(self, key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: global_container_stack.setMetaDataEntry("octoprint_id", key) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() self.instanceIdChanged.emit() @pyqtProperty(str, notify=instanceIdChanged) def instanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" return global_container_stack.getMetaDataEntry("octoprint_id", "") @pyqtSlot(str) def requestApiKey(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url: return ## Request appkey self._appkey_instance_id = instance_id self._appkey_request = self._createRequest( QUrl(base_url + "plugin/appkeys/request"), basic_auth_username, basic_auth_password) self._appkey_request.setRawHeader(b"Content-Type", b"application/json") data = json.dumps({"app": "Cura"}) self._appkey_reply = self._network_manager.post( self._appkey_request, data.encode()) @pyqtSlot() def cancelApiKeyRequest(self) -> None: if self._appkey_reply: if self._appkey_reply.isRunning(): self._appkey_reply.abort() self._appkey_reply = None self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_poll_timer.stop() def _pollApiKey(self) -> None: if not self._appkey_request: return self._appkey_reply = self._network_manager.get(self._appkey_request) @pyqtSlot(str) def probeAppKeySupport(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url or not instance: return instance.getAdditionalData() self._instance_supports_appkeys = False self.appKeysSupportedChanged.emit() appkey_probe_request = self._createRequest( QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username, basic_auth_password) self._appkey_reply = self._network_manager.get(appkey_probe_request) @pyqtSlot(str, str) def testApiKey(self, instance_id: str, api_key: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url: return self._instance_responded = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False self._instance_installed_plugins = [] # type: List[str] self.selectedInstanceSettingsChanged.emit() if self._settings_reply: if self._settings_reply.isRunning(): self._settings_reply.abort() self._settings_reply = None if self._settings_reply_timeout: self._settings_reply_timeout = None if api_key != "": Logger.log( "d", "Trying to access OctoPrint instance at %s with the provided API key." % base_url) ## Request 'settings' dump settings_request = self._createRequest( QUrl(base_url + "api/settings"), basic_auth_username, basic_auth_password) settings_request.setRawHeader(b"X-Api-Key", api_key.encode()) self._settings_reply = self._network_manager.get(settings_request) self._settings_reply_timeout = NetworkReplyTimeout( self._settings_reply, 5000, self._onRequestFailed) self._settings_instance = instance @pyqtSlot(str) def setApiKey(self, api_key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return global_container_stack.setMetaDataEntry( "octoprint_api_key", base64.b64encode(api_key.encode("ascii")).decode("ascii")) self._keys_cache[self.instanceId] = api_key keys_cache = base64.b64encode( json.dumps(self._keys_cache).encode("ascii")).decode("ascii") self._preferences.setValue("octoprint/keys_cache", keys_cache) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() ## Get the stored API key of an instance, or the one stored in the machine instance # \return key String containing the key of the machine. @pyqtSlot(str, result=str) def getApiKey(self, instance_id: str) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" if instance_id == self.instanceId: api_key = self._deobfuscateString( global_container_stack.getMetaDataEntry( "octoprint_api_key", "")) else: api_key = self._keys_cache.get(instance_id, "") return api_key selectedInstanceSettingsChanged = pyqtSignal() @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceResponded(self) -> bool: return self._instance_responded @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceInError(self) -> bool: return self._instance_in_error @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceApiKeyAccepted(self) -> bool: return self._instance_api_key_accepted @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsSd(self) -> bool: return self._instance_supports_sd @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsCamera(self) -> bool: return self._instance_supports_camera @pyqtProperty("QStringList", notify=selectedInstanceSettingsChanged) def instanceInstalledPlugins(self) -> List[str]: return self._instance_installed_plugins @pyqtProperty("QVariantList", notify=selectedInstanceSettingsChanged) def instanceAvailablePowerPlugins(self) -> List[Dict[str, str]]: available_plugins = self._power_plugins_manager.getAvailablePowerPlugs( ) return [{ "key": plug_id, "text": plug_data["name"] } for (plug_id, plug_data) in available_plugins.items()] @pyqtProperty(bool, notify=appKeysSupportedChanged) def instanceSupportsAppKeys(self) -> bool: return self._instance_supports_appkeys @pyqtSlot(str, str, str) def setContainerMetaDataEntry(self, container_id: str, key: str, value: str) -> None: containers = ContainerRegistry.getInstance().findContainers( id=container_id) if not containers: Logger.log( "w", "Could not set metadata of container %s because it was not found.", container_id) return containers[0].setMetaDataEntry(key, value) @pyqtSlot(bool) def applyGcodeFlavorFix(self, apply_fix: bool) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode" if global_container_stack.getProperty("machine_gcode_flavor", "value") == gcode_flavor: # No need to add a definition_changes container if the setting is not going to be changed return # Make sure there is a definition_changes container to store the machine settings definition_changes_container = global_container_stack.definitionChanges if definition_changes_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer( global_container_stack, global_container_stack.getId() + "_settings") definition_changes_container.setProperty("machine_gcode_flavor", "value", gcode_flavor) # Update the has_materials metadata flag after switching gcode flavor definition = global_container_stack.getBottom() if (not definition or definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False)): # In other words: only continue for the UM2 (extended), but not for the UM2+ return has_materials = global_container_stack.getProperty( "machine_gcode_flavor", "value") != "UltiGCode" material_container = global_container_stack.material if has_materials: global_container_stack.setMetaDataEntry("has_materials", True) # Set the material container to a sane default if material_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): search_criteria = { "type": "material", "definition": "fdmprinter", "id": global_container_stack.getMetaDataEntry( "preferred_material") } materials = ContainerRegistry.getInstance( ).findInstanceContainers(**search_criteria) if materials: global_container_stack.material = materials[0] else: # The metadata entry is stored in an ini, and ini files are parsed as strings only. # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. if "has_materials" in global_container_stack.getMetaData(): global_container_stack.removeMetaDataEntry("has_materials") global_container_stack.material = ContainerRegistry.getInstance( ).getEmptyInstanceContainer() self._application.globalContainerStackChanged.emit() @pyqtSlot(str) def openWebPage(self, url: str) -> None: QDesktopServices.openUrl(QUrl(url)) def _createAdditionalComponentsView(self) -> None: Logger.log( "d", "Creating additional ui components for OctoPrint-connected printers." ) path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qml", "OctoPrintComponents.qml") self._additional_components = self._application.createQmlComponent( path, {"manager": self}) if not self._additional_components: Logger.log( "w", "Could not create additional components for OctoPrint-connected printers." ) return self._application.addAdditionalComponent( "monitorButtons", self._additional_components.findChild(QObject, "openOctoPrintButton")) def _onRequestFailed(self, reply: QNetworkReply) -> None: if reply.operation() == QNetworkAccessManager.GetOperation: if "api/settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: Logger.log( "w", "Connection refused or timeout when trying to access OctoPrint at %s" % reply.url().toString()) self._instance_in_error = True self.selectedInstanceSettingsChanged.emit() ## Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply self._onRequestFailed(reply) return if reply.operation() == QNetworkAccessManager.PostOperation: if "/plugin/appkeys/request" in reply.url().toString( ): # Initial AppKey request if http_status_code == 201 or http_status_code == 202: Logger.log("w", "Start polling for AppKeys decision") if not self._appkey_request: return self._appkey_request.setUrl( reply.header(QNetworkRequest.LocationHeader)) self._appkey_request.setRawHeader(b"Content-Type", b"") self._appkey_poll_timer.start() elif http_status_code == 404: Logger.log( "w", "This instance of OctoPrint does not support AppKeys") self._appkey_request = None # type: Optional[QNetworkRequest] else: response = bytes(reply.readAll()).decode() Logger.log( "w", "Unknown response when requesting an AppKey: %d. OctoPrint said %s" % (http_status_code, response)) self._appkey_request = None # type: Optional[QNetworkRequest] if reply.operation() == QNetworkAccessManager.GetOperation: if "/plugin/appkeys/probe" in reply.url().toString( ): # Probe for AppKey support if http_status_code == 204: self._instance_supports_appkeys = True else: self._instance_supports_appkeys = False self.appKeysSupportedChanged.emit() if "/plugin/appkeys/request" in reply.url().toString( ): # Periodic AppKey request poll if http_status_code == 202: self._appkey_poll_timer.start() elif http_status_code == 200: Logger.log("d", "AppKey granted") self._appkey_request = None # type: Optional[QNetworkRequest] try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") return api_key = json_data["api_key"] self._keys_cache[self._appkey_instance_id] = api_key global_container_stack = self._application.getGlobalContainerStack( ) if global_container_stack: global_container_stack.setMetaDataEntry( "octoprint_api_key", base64.b64encode( api_key.encode("ascii")).decode("ascii")) self.appKeyReceived.emit() elif http_status_code == 404: Logger.log("d", "AppKey denied") self._appkey_request = None # type: Optional[QNetworkRequest] else: response = bytes(reply.readAll()).decode() Logger.log( "w", "Unknown response when waiting for an AppKey: %d. OctoPrint said %s" % (http_status_code, response)) self._appkey_request = None # type: Optional[QNetworkRequest] if "api/settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: self._instance_in_error = False if http_status_code == 200: Logger.log("d", "API key accepted by OctoPrint.") self._instance_api_key_accepted = True 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._instance_supports_sd = json_data["feature"][ "sdSupport"] if "webcam" in json_data and "streamUrl" in json_data[ "webcam"]: stream_url = json_data["webcam"]["streamUrl"] if stream_url: #not empty string or None self._instance_supports_camera = True if "plugins" in json_data: self._power_plugins_manager.parsePluginData( json_data["plugins"]) self._instance_installed_plugins = list( json_data["plugins"].keys()) api_key = bytes(reply.request().rawHeader( b"X-Api-Key")).decode("utf-8") self.setApiKey(api_key) # store api key in key cache if self._settings_instance: self._settings_instance.setApiKey(api_key) self._settings_instance.resetOctoPrintUserName() self._settings_instance.getAdditionalData() self._settings_instance.parseSettingsData(json_data) self._settings_instance = None elif http_status_code == 401: Logger.log("d", "Invalid API key for OctoPrint.") self._instance_api_key_accepted = False elif http_status_code == 502 or http_status_code == 503: Logger.log("d", "OctoPrint is not running.") self._instance_api_key_accepted = False self._instance_in_error = True self._instance_responded = True self.selectedInstanceSettingsChanged.emit() def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest: request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) request.setRawHeader(b"User-Agent", self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") request.setRawHeader(b"Authorization", ("Basic %s" % data).encode()) # ignore SSL errors (eg for self-signed certificates) ssl_configuration = QSslConfiguration.defaultConfiguration() ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone) request.setSslConfiguration(ssl_configuration) return request ## Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before def _deobfuscateString(self, source: str) -> str: try: return base64.b64decode(source.encode("ascii")).decode("ascii") except UnicodeDecodeError: return source def _getInstanceInfo( self, instance_id: str ) -> Tuple[Optional[OctoPrintOutputDevice], str, str, str]: if not self._network_plugin: return (None, "", "", "") instance = self._network_plugin.getInstanceById(instance_id) if not instance: return (None, "", "", "") return (instance, instance.baseURL, instance.getProperty("userName"), instance.getProperty("password"))
class E5XmlRpcClient(QObject): """ Class implementing a xmlrpc client for Qt. """ def __init__(self, url, parent=None): """ Constructor @param url xmlrpc handler URL (string or QUrl) @param parent parent object (QObject) """ super(E5XmlRpcClient, self).__init__(parent) # attributes for the network objects self.__networkManager = QNetworkAccessManager(self) self.__networkManager.proxyAuthenticationRequired.connect( proxyAuthenticationRequired) self.__networkManager.finished.connect(self.__replyFinished) if SSL_AVAILABLE: self.__sslErrorHandler = E5SslErrorHandler(self) self.__networkManager.sslErrors.connect(self.__sslErrors) self.__callmap = {} self.__request = QNetworkRequest(QUrl(url)) self.__request.setRawHeader(b"User-Agent", b"E5XmlRpcClient/1.0") self.__request.setHeader(QNetworkRequest.ContentTypeHeader, "text/xml") def setUrl(self, url): """ Public slot to set the xmlrpc handler URL. @param url xmlrpc handler URL (string or QUrl) """ url = QUrl(url) if url.isValid(): self.__request.setUrl(url) def call(self, method, args, responseCallback, errorCallback): """ Public method to call the remote server. @param method name of the remote method to be called (string) @param args tuple of method arguments (tuple) @param responseCallback method to be called with the returned result as a tuple (function) @param errorCallback method to be called in case of an error with error code and error string (function) """ assert isinstance(args, tuple), \ "argument must be tuple or Fault instance" data = xmlrpc.dumps(args, method).encode("utf-8") reply = self.__networkManager.post( self.__request, QByteArray(data)) self.__callmap[reply] = (responseCallback, errorCallback) def abort(self): """ Public method to abort all calls. """ for reply in list(self.__callmap): if reply.isRunning(): reply.abort() def __sslErrors(self, reply, errors): """ Private slot to handle SSL errors. @param reply reference to the reply object (QNetworkReply) @param errors list of SSL errors (list of QSslError) """ ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] if ignored == E5SslErrorHandler.NotIgnored and reply in self.__callmap: self.__callmap[reply][1](xmlrpc.TRANSPORT_ERROR, self.tr( "SSL Error")) def __replyFinished(self, reply): """ Private slot handling the receipt of a reply. @param reply reference to the finished reply (QNetworkReply) """ if reply not in self.__callmap: return if reply.error() != QNetworkReply.NoError: self.__callmap[reply][1](xmlrpc.TRANSPORT_ERROR, reply.errorString()) else: data = bytes(reply.readAll()).decode("utf-8") try: data = xmlrpc.loads(data)[0] self.__callmap[reply][0](data) except xmlrpc.Fault as fault: self.__callmap[reply][1](fault.faultCode, fault.faultString) reply.deleteLater() del self.__callmap[reply]
class NetWorker(QObject): ifcAddrChanged = pyqtSignal(str, name='ifcAddrChanged', arguments=['ip']) currentIfcAddrChanged = pyqtSignal() wifiStatus = pyqtSignal(str, str, int, int, name='wifiStatus', arguments=['essid', 'freq', 'quality', 'level']) wifiStatusChanged = pyqtSignal() @pyqtProperty(str, notify=currentIfcAddrChanged) def currentIfcAddr(self): return self.ifcAddr.toString() @pyqtProperty(str, notify=wifiStatusChanged) def currentWifiESSID(self): return self.essid @pyqtProperty(str, notify=wifiStatusChanged) def currentWifiFreq(self): return self.freq @pyqtProperty(int, notify=wifiStatusChanged) def currentWifiQuality(self): return self.quality @pyqtProperty(int, notify=wifiStatusChanged) def currentWifiLevel(self): return self.level def __init__(self, loglevel='WARNING', ifcName='wlan0'): QObject.__init__(self) self.logger = Logger(name='ratt.networker') self.logger.setLogLevelStr(loglevel) self.debug = self.logger.isDebug() self.mgr = QNetworkAccessManager() self.sslConfig = QSslConfiguration() self.sslSupported = QSslSocket.supportsSsl() self.setAuth() self.setSSLCertConfig() self.mgr.authenticationRequired.connect( self.handleAuthenticationRequired) self.ifcName = ifcName self.ifcAddr = QHostAddress() self.statusTimer = QTimer() self.statusTimer.setSingleShot(False) self.statusTimer.timeout.connect(self.slotStatusTimer) self.statusTimer.start(5000) self.essid = '' self.freq = '' self.quality = 0 self.level = 0 def slotStatusTimer(self): ip = self.getIfcAddress(ifc=self.ifcName) if ip != self.ifcAddr: self.ifcAddr = ip self.ifcAddrChanged.emit(ip.toString()) self.currentIfcAddrChanged.emit() results = {} if self.getWifiStatus(results): self.essid = results['essid'] self.freq = results['freq'] self.quality = results['quality'] self.level = results['level'] self.wifiStatus.emit(self.essid, self.freq, self.quality, self.level) self.wifiStatusChanged.emit() # ['wlan0', 'IEEE', '802.11', 'ESSID:"FooBar"', 'Mode:Managed', 'Frequency:2.412', # 'GHz', 'Access', 'Point:', '00:11:22:33:44:55', 'Bit', 'Rate=65', 'Mb/s', 'Tx-Power=31', # 'dBm', 'Retry', 'short', 'limit:7', 'RTS', 'thr:off', 'Fragment', 'thr:off', 'Power', # 'Management:on', 'Link', 'Quality=43/70', 'Signal', 'level=-67', 'dBm', 'Rx', 'invalid', # 'nwid:0', 'Rx', 'invalid', 'crypt:0', 'Rx', 'invalid', 'frag:0', 'Tx', 'excessive', 'retries:113', # 'Invalid', 'misc:0', 'Missed', 'beacon:0'] def getWifiStatus(self, res): try: iw = getoutput('/sbin/iwconfig %s' % self.ifcName) fields = iw.split() for field in fields: if field.find('ESSID') != -1: res['essid'] = field.split('"')[1] elif field.find('Frequency') != -1: res['freq'] = field.split(':')[1] elif field.find('Quality') != -1: frac = field.split('=')[1] (n, d) = frac.split('/') q = int(n) * 100 / int(d) res['quality'] = q elif field.find('level') != -1: res['level'] = int(field.split('=')[1]) except: return False return True def getIfcAddress(self, ifc): myIfc = QNetworkInterface.interfaceFromName(ifc) addrList = myIfc.addressEntries() for addr in addrList: if addr.ip().protocol() == QAbstractSocket.IPv4Protocol: return addr.ip() return QHostAddress() def setAuth(self, user='', password=''): self.user = user self.password = password def setSSLCertConfig(self, enabled=False, caCertFile='', clientCertFile='', clientKeyFile=''): self.sslEnabled = enabled if self.sslSupported and self.sslEnabled: self.caCertFile = caCertFile self.clientCertFile = clientCertFile self.clientKeyFile = clientKeyFile self.configureCerts() def get(self, url): self.logger.debug('get url=%s' % url.toString()) request = QNetworkRequest(QUrl(url)) request.setRawHeader("User-Agent", "RATT") if self.sslSupported and self.sslEnabled: request.setSslConfiguration(self.sslConfig) reply = self.mgr.get(request) if self.sslSupported and self.sslEnabled: reply.sslErrors.connect(self.handleSSLErrors) return reply def buildRequest(self, url): request = QNetworkRequest(QUrl(url)) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") return request def buildQuery(self, vars): query = str() for key in vars: value = vars[key] sep = '&' if query != '' else '' query = query + '%s%s=%s' % (sep, key, value) return query def post(self, request, query): self.logger.debug('post url=%s query=%s' % (request.url().toString(), query)) if self.sslSupported and self.sslEnabled: request.setSslConfiguration(self.sslConfig) bytearray = QByteArray() bytearray.append(query) reply = self.mgr.post(request, bytearray) if self.sslSupported and self.sslEnabled: self.mgr.sslErrors.connect(self.handleSSLErrors) return reply def handleSSLErrors(self, reply, errors): for err in errors: self.logger.error('SSL errors:' + err.errorString()) def handleAuthenticationRequired(self, reply, authenticator): if self.user == '' and self.password == '': self.logger.warning( 'authentication required and no user/password set') authenticator.setUser(self.user) authenticator.setPassword(self.password) def configureCerts(self): ## import the private client key privateKeyFile = QFile(self.clientKeyFile) privateKeyFile.open(QIODevice.ReadOnly) privateKey = QSslKey(privateKeyFile, QSsl.Rsa) if privateKey.isNull(): self.logger.warning('SSL private key is null') else: self.sslConfig.setPrivateKey(privateKey) ## import the client certificate certFile = QFile(self.clientCertFile) certFile.open(QIODevice.ReadOnly) cert = QSslCertificate(certFile) self.sslConfig.setLocalCertificate(cert) ## import the self-signed CA certificate caCertFile = QFile(self.caCertFile) caCertFile.open(QIODevice.ReadOnly) caCert = QSslCertificate(caCertFile) ## add self-signed CA cert to the other CA certs caCertList = self.sslConfig.caCertificates() caCertList.append(caCert) self.sslConfig.setCaCertificates(caCertList)
class PastebinClient(QObject): """A client for http://p.cmpl.cc/ using QNetworkAccessManager. Attributes: _nam: The QNetworkAccessManager used. Class attributes: API_URL: The base API URL. Signals: success: Emitted when the paste succeeded. arg: The URL of the paste, as string. error: Emitted when the paste failed. arg: The error message, as string. """ API_URL = 'http://paste.the-compiler.org/api/' success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._nam = QNetworkAccessManager(self) def paste(self, name, title, text, parent=None): """Paste the text into a pastebin and return the URL. Args: name: The username to post as. title: The post title. text: The text to post. parent: The parent paste to reply to. """ data = { 'text': text, 'title': title, 'name': name, } if parent is not None: data['reply'] = parent encoded_data = urllib.parse.urlencode(data).encode('utf-8') create_url = urllib.parse.urljoin(self.API_URL, 'create') request = QNetworkRequest(QUrl(create_url)) request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') reply = self._nam.post(request, encoded_data) if reply.isFinished(): self.on_reply_finished(reply) else: reply.finished.connect(functools.partial( self.on_reply_finished, reply)) def on_reply_finished(self, reply): """Read the data and finish when the reply finished. Args: reply: The QNetworkReply which finished. """ if reply.error() != QNetworkReply.NoError: self.error.emit(reply.errorString()) return try: url = bytes(reply.readAll()).decode('utf-8') except UnicodeDecodeError: self.error.emit("Invalid UTF-8 data received in reply!") return if url.startswith('http://'): self.success.emit(url) else: self.error.emit("Invalid data received in reply!")
class HTTPClient(QObject): """An HTTP client based on QNetworkAccessManager. Intended for APIs, automatically decodes data. Attributes: _nam: The QNetworkAccessManager used. _timers: A {QNetworkReply: QTimer} dict. Signals: success: Emitted when the operation succeeded. arg: The received data. error: Emitted when the request failed. arg: The error message, as string. """ success = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._nam = QNetworkAccessManager(self) self._timers = {} def post(self, url, data=None): """Create a new POST request. Args: url: The URL to post to, as QUrl. data: A dict of data to send. """ if data is None: data = {} encoded_data = urllib.parse.urlencode(data).encode('utf-8') request = HTTPRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') reply = self._nam.post(request, encoded_data) self._handle_reply(reply) def get(self, url): """Create a new GET request. Emits success/error when done. Args: url: The URL to access, as QUrl. """ request = HTTPRequest(url) reply = self._nam.get(request) self._handle_reply(reply) def _handle_reply(self, reply): """Handle a new QNetworkReply.""" if reply.isFinished(): self.on_reply_finished(reply) else: timer = QTimer(self) timer.setInterval(10000) timer.timeout.connect(reply.abort) timer.start() self._timers[reply] = timer reply.finished.connect( functools.partial(self.on_reply_finished, reply)) def on_reply_finished(self, reply): """Read the data and finish when the reply finished. Args: reply: The QNetworkReply which finished. """ timer = self._timers.pop(reply) if timer is not None: timer.stop() timer.deleteLater() if reply.error() != QNetworkReply.NoError: self.error.emit(reply.errorString()) return try: data = bytes(reply.readAll()).decode('utf-8') except UnicodeDecodeError: self.error.emit("Invalid UTF-8 data received in reply!") return self.success.emit(data)
class ClusterApiClient: PRINTER_API_PREFIX = "/api/v1" CLUSTER_API_PREFIX = "/cluster-api/v1" # In order to avoid garbage collection we keep the callbacks in this list. _anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Initializes a new cluster API client. # \param address: The network address of the cluster to call. # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, address: str, on_error: Callable) -> None: super().__init__() self._manager = QNetworkAccessManager() self._address = address self._on_error = on_error ## Get printer system information. # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: url = "{}/system".format(self.PRINTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, PrinterSystemStatus) ## Get the printers in the cluster. # \param on_finished: The callback in case the response is successful. def getPrinters( self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: url = "{}/printers".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrinterStatus) ## Get the print jobs in the cluster. # \param on_finished: The callback in case the response is successful. def getPrintJobs( self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post( self._createEmptyRequest(url), json.dumps({ "to_position": 0, "list": "queued" }).encode()) ## Override print job configuration and force it to be printed. def forcePrintJob(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.put(self._createEmptyRequest(url), json.dumps({ "force": True }).encode()) ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.deleteResource(self._createEmptyRequest(url)) ## Set the state of a print job. def setPrintJobState(self, print_job_uuid: str, state: str) -> None: url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid) # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({ "action": action }).encode()) ## Get the preview image data of a print job. def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: url = QUrl("http://" + self._address + path) request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: Logger.logException("e", "Could not parse the cluster response: %s", err) return status_code, {"errors": [err]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None: try: if isinstance(response, list): results = [model_class(**c) for c in response ] # type: List[ClusterApiClientModel] on_finished_list = cast( Callable[[List[ClusterApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**response) # type: ClusterApiClientModel on_finished_item = cast(Callable[[ClusterApiClientModel], Any], on_finished) on_finished_item(result) except JSONDecodeError: Logger.log("e", "Could not parse response from network: %s", str(response)) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. def _addCallback( self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: def parse() -> None: self._anti_gc_callbacks.remove(parse) # Don't try to parse the reply if we didn't get one if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) is None: return if reply.error() > 0: self._on_error(reply.errorString()) return # If no parse model is given, simply return the raw data in the callback. if not model: on_finished(reply.readAll()) return # Otherwise parse the result and return the formatted data in the callback. status_code, response = self._parseReply(reply) self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)
class TranslatorRequest(QObject): """ Class implementing a synchronous network request handler for translation requests. """ def __init__(self, parent=None): """ Constructor @param parent reference to the parent object (QObject) """ super(TranslatorRequest, self).__init__(parent) self.__contentTypes = { "form": b"application/x-www-form-urlencoded", "json": b"application/json", } self.__networkManager = QNetworkAccessManager(self) self.__networkManager.proxyAuthenticationRequired.connect( proxyAuthenticationRequired) self.__loop = QEventLoop() self.__networkManager.finished.connect(self.__loop.quit) def get(self, requestUrl, extraHeaders=None): """ Public method to issue a GET request. @param requestUrl URL of the request (QUrl) @keyparam extraHeaders list of tuples of additional headers giving header name (string) and header value (string) @return server response (QByteArray) or error message (string) """ request = QNetworkRequest(requestUrl) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) if extraHeaders: for name, value in extraHeaders: request.setRawHeader(name, value) reply = self.__networkManager.get(request) if not self.__loop.isRunning(): self.__loop.exec_() if reply.error() != QNetworkReply.NoError: return reply.errorString(), False else: return reply.readAll(), True def post(self, requestUrl, requestData, dataType="form", extraHeaders=None): """ Public method to issue a POST request. @param requestUrl URL of the request (QUrl) @param requestData data of the request (QByteArray) @keyparam dataType type of the request data (string) @keyparam extraHeaders list of tuples of additional headers giving header name (string) and header value (string) @return tuple of server response (string) and flag indicating success (boolean) """ request = QNetworkRequest(requestUrl) request.setRawHeader(b"User-Agent", b"Mozilla/5.0") request.setRawHeader(b"Content-Type", self.__contentTypes[dataType]) request.setRawHeader(b"Content-Length", QByteArray.number(requestData.size())) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) if extraHeaders: for name, value in extraHeaders: request.setRawHeader(name, value) request.setUrl(requestUrl) reply = self.__networkManager.post(request, requestData) if not self.__loop.isRunning(): self.__loop.exec_() if reply.error() != QNetworkReply.NoError: return reply.errorString(), False else: return str(reply.readAll(), "utf-8", "replace"), True
class CloudApiClient: # The cloud URL to use for this remote cluster. ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) ## Initializes a new cloud API client. # \param account: The user's account object # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: super().__init__() self._manager = QNetworkAccessManager() self._account = account self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] # In order to avoid garbage collection we keep the callbacks in this list. self._anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Gets the account used for the API. @property def account(self) -> Account: return self._account ## Retrieves all the clusters for the user that is currently logged in. # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: url = "{}/clusters".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse) ## Retrieves the status of the given cluster. # \param cluster_id: The ID of the cluster. # \param on_finished: The function to be called after the result is parsed. def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterStatus) ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. # \param on_finished: The function to be called after the result is parsed. def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any] ) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) self._addCallback(reply, on_finished, CloudPrintJobResponse) ## Uploads a print job tool path to the cloud. # \param print_job: The object received after requesting an upload with `self.requestUpload`. # \param mesh: The tool path data to be uploaded. # \param on_finished: The function to be called after the upload is successful. # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. # \param cluster_id: The ID of the cluster. # \param job_id: The ID of the print job. # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) reply = self._manager.post(self._createEmptyRequest(url), b"") self._addCallback(reply, on_finished, CloudPrintResponse) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) access_token = self._account.accessToken if access_token: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code), id=str(time()), http_status="500") Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: if "data" in response: data = response["data"] if isinstance(data, list): results = [model_class(**c) for c in data] # type: List[CloudApiClientModel] on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**data) # type: CloudApiClientModel on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished) on_finished_item(result) elif "errors" in response: self._on_error([CloudError(**error) for error in response["errors"]]) else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either # a list or a single item. # \param model: The type of the model to convert the response to. def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: def parse() -> None: status_code, response = self._parseReply(reply) self._anti_gc_callbacks.remove(parse) return self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)