NetworkedPrinterOutputDevice.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.FileHandler.FileHandler import FileHandler #For typing.
  4. from UM.Logger import Logger
  5. from UM.Scene.SceneNode import SceneNode #For typing.
  6. from cura.CuraApplication import CuraApplication
  7. from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
  8. from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
  9. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
  10. from time import time
  11. from typing import Any, Callable, Dict, List, Optional
  12. from enum import IntEnum
  13. import os # To get the username
  14. import gzip
  15. class AuthState(IntEnum):
  16. NotAuthenticated = 1
  17. AuthenticationRequested = 2
  18. Authenticated = 3
  19. AuthenticationDenied = 4
  20. AuthenticationReceived = 5
  21. class NetworkedPrinterOutputDevice(PrinterOutputDevice):
  22. authenticationStateChanged = pyqtSignal()
  23. def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
  24. super().__init__(device_id = device_id, parent = parent)
  25. self._manager = None # type: Optional[QNetworkAccessManager]
  26. self._last_manager_create_time = None # type: Optional[float]
  27. self._recreate_network_manager_time = 30
  28. self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
  29. self._last_response_time = None # type: Optional[float]
  30. self._last_request_time = None # type: Optional[float]
  31. self._api_prefix = ""
  32. self._address = address
  33. self._properties = properties
  34. self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion())
  35. self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
  36. self._authentication_state = AuthState.NotAuthenticated
  37. # QHttpMultiPart objects need to be kept alive and not garbage collected during the
  38. # HTTP which uses them. We hold references to these QHttpMultiPart objects here.
  39. self._kept_alive_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart]
  40. self._sending_gcode = False
  41. self._compressing_gcode = False
  42. self._gcode = [] # type: List[str]
  43. self._connection_state_before_timeout = None # type: Optional[ConnectionState]
  44. printer_type = self._properties.get(b"machine", b"").decode("utf-8")
  45. printer_type_identifiers = {
  46. "9066": "ultimaker3",
  47. "9511": "ultimaker3_extended",
  48. "9051": "ultimaker_s5"
  49. }
  50. self._printer_type = "Unknown"
  51. for key, value in printer_type_identifiers.items():
  52. if printer_type.startswith(key):
  53. self._printer_type = value
  54. break
  55. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
  56. raise NotImplementedError("requestWrite needs to be implemented")
  57. def setAuthenticationState(self, authentication_state: AuthState) -> None:
  58. if self._authentication_state != authentication_state:
  59. self._authentication_state = authentication_state
  60. self.authenticationStateChanged.emit()
  61. @pyqtProperty(int, notify = authenticationStateChanged)
  62. def authenticationState(self) -> AuthState:
  63. return self._authentication_state
  64. def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
  65. compressed_data = gzip.compress(data_to_append.encode("utf-8"))
  66. self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
  67. QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
  68. # Pretend that this is a response, as zipping might take a bit of time.
  69. # If we don't do this, the device might trigger a timeout.
  70. self._last_response_time = time()
  71. return compressed_data
  72. def _compressGCode(self) -> Optional[bytes]:
  73. self._compressing_gcode = True
  74. ## Mash the data into single string
  75. max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
  76. file_data_bytes_list = []
  77. batched_lines = []
  78. batched_lines_count = 0
  79. for line in self._gcode:
  80. if not self._compressing_gcode:
  81. self._progress_message.hide()
  82. # Stop trying to zip / send as abort was called.
  83. return None
  84. # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
  85. # Compressing line by line in this case is extremely slow, so we need to batch them.
  86. batched_lines.append(line)
  87. batched_lines_count += len(line)
  88. if batched_lines_count >= max_chars_per_line:
  89. file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
  90. batched_lines = []
  91. batched_lines_count = 0
  92. # Don't miss the last batch (If any)
  93. if len(batched_lines) != 0:
  94. file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
  95. self._compressing_gcode = False
  96. return b"".join(file_data_bytes_list)
  97. def _update(self) -> None:
  98. if self._last_response_time:
  99. time_since_last_response = time() - self._last_response_time
  100. else:
  101. time_since_last_response = 0
  102. if self._last_request_time:
  103. time_since_last_request = time() - self._last_request_time
  104. else:
  105. time_since_last_request = float("inf") # An irrelevantly large number of seconds
  106. if time_since_last_response > self._timeout_time >= time_since_last_request:
  107. # Go (or stay) into timeout.
  108. if self._connection_state_before_timeout is None:
  109. self._connection_state_before_timeout = self._connection_state
  110. self.setConnectionState(ConnectionState.closed)
  111. # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
  112. # sleep.
  113. if time_since_last_response > self._recreate_network_manager_time:
  114. if self._last_manager_create_time is None:
  115. self._createNetworkManager()
  116. elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
  117. self._createNetworkManager()
  118. assert(self._manager is not None)
  119. elif self._connection_state == ConnectionState.closed:
  120. # Go out of timeout.
  121. if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
  122. self.setConnectionState(self._connection_state_before_timeout)
  123. self._connection_state_before_timeout = None
  124. def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
  125. url = QUrl("http://" + self._address + self._api_prefix + target)
  126. request = QNetworkRequest(url)
  127. if content_type is not None:
  128. request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
  129. request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
  130. return request
  131. def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
  132. part = QHttpPart()
  133. if not content_header.startswith("form-data;"):
  134. content_header = "form_data; " + content_header
  135. part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
  136. if content_type is not None:
  137. part.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
  138. part.setBody(data)
  139. return part
  140. ## Convenience function to get the username from the OS.
  141. # The code was copied from the getpass module, as we try to use as little dependencies as possible.
  142. def _getUserName(self) -> str:
  143. for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
  144. user = os.environ.get(name)
  145. if user:
  146. return user
  147. return "Unknown User" # Couldn't find out username.
  148. def _clearCachedMultiPart(self, reply: QNetworkReply) -> None:
  149. if reply in self._kept_alive_multiparts:
  150. del self._kept_alive_multiparts[reply]
  151. def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
  152. if self._manager is None:
  153. self._createNetworkManager()
  154. assert (self._manager is not None)
  155. request = self._createEmptyRequest(target)
  156. self._last_request_time = time()
  157. reply = self._manager.put(request, data.encode())
  158. self._registerOnFinishedCallback(reply, on_finished)
  159. def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
  160. if self._manager is None:
  161. self._createNetworkManager()
  162. assert (self._manager is not None)
  163. request = self._createEmptyRequest(target)
  164. self._last_request_time = time()
  165. reply = self._manager.get(request)
  166. self._registerOnFinishedCallback(reply, on_finished)
  167. def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
  168. if self._manager is None:
  169. self._createNetworkManager()
  170. assert (self._manager is not None)
  171. request = self._createEmptyRequest(target)
  172. self._last_request_time = time()
  173. reply = self._manager.post(request, data)
  174. if on_progress is not None:
  175. reply.uploadProgress.connect(on_progress)
  176. self._registerOnFinishedCallback(reply, on_finished)
  177. def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
  178. if self._manager is None:
  179. self._createNetworkManager()
  180. assert (self._manager is not None)
  181. request = self._createEmptyRequest(target, content_type=None)
  182. multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
  183. for part in parts:
  184. multi_post_part.append(part)
  185. self._last_request_time = time()
  186. reply = self._manager.post(request, multi_post_part)
  187. self._kept_alive_multiparts[reply] = multi_post_part
  188. if on_progress is not None:
  189. reply.uploadProgress.connect(on_progress)
  190. self._registerOnFinishedCallback(reply, on_finished)
  191. return reply
  192. def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
  193. post_part = QHttpPart()
  194. post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
  195. post_part.setBody(body_data)
  196. self.postFormWithParts(target, [post_part], on_finished, on_progress)
  197. def _onAuthenticationRequired(self, reply: QNetworkReply, authenticator: QAuthenticator) -> None:
  198. Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
  199. def _createNetworkManager(self) -> None:
  200. Logger.log("d", "Creating network manager")
  201. if self._manager:
  202. self._manager.finished.disconnect(self.__handleOnFinished)
  203. self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
  204. self._manager = QNetworkAccessManager()
  205. self._manager.finished.connect(self.__handleOnFinished)
  206. self._last_manager_create_time = time()
  207. self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
  208. if self._properties.get(b"temporary", b"false") != b"true":
  209. CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
  210. def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
  211. if on_finished is not None:
  212. self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
  213. def __handleOnFinished(self, reply: QNetworkReply) -> None:
  214. # Due to garbage collection, we need to cache certain bits of post operations.
  215. # As we don't want to keep them around forever, delete them if we get a reply.
  216. if reply.operation() == QNetworkAccessManager.PostOperation:
  217. self._clearCachedMultiPart(reply)
  218. if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
  219. # No status code means it never even reached remote.
  220. return
  221. self._last_response_time = time()
  222. if self._connection_state == ConnectionState.connecting:
  223. self.setConnectionState(ConnectionState.connected)
  224. callback_key = reply.url().toString() + str(reply.operation())
  225. try:
  226. if callback_key in self._onFinishedCallbacks:
  227. self._onFinishedCallbacks[callback_key](reply)
  228. except Exception:
  229. Logger.logException("w", "something went wrong with callback")
  230. @pyqtSlot(str, result=str)
  231. def getProperty(self, key: str) -> str:
  232. bytes_key = key.encode("utf-8")
  233. if bytes_key in self._properties:
  234. return self._properties.get(bytes_key, b"").decode("utf-8")
  235. else:
  236. return ""
  237. def getProperties(self):
  238. return self._properties
  239. ## Get the unique key of this machine
  240. # \return key String containing the key of the machine.
  241. @pyqtProperty(str, constant = True)
  242. def key(self) -> str:
  243. return self._id
  244. ## The IP address of the printer.
  245. @pyqtProperty(str, constant = True)
  246. def address(self) -> str:
  247. return self._properties.get(b"address", b"").decode("utf-8")
  248. ## Name of the printer (as returned from the ZeroConf properties)
  249. @pyqtProperty(str, constant = True)
  250. def name(self) -> str:
  251. return self._properties.get(b"name", b"").decode("utf-8")
  252. ## Firmware version (as returned from the ZeroConf properties)
  253. @pyqtProperty(str, constant = True)
  254. def firmwareVersion(self) -> str:
  255. return self._properties.get(b"firmware_version", b"").decode("utf-8")
  256. @pyqtProperty(str, constant = True)
  257. def printerType(self) -> str:
  258. return self._printer_type
  259. ## IP adress of this printer
  260. @pyqtProperty(str, constant = True)
  261. def ipAddress(self) -> str:
  262. return self._address