NetworkedPrinterOutputDevice.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. import gzip
  5. from time import time
  6. from typing import Dict, List, Optional
  7. from enum import IntEnum
  8. from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
  9. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QCoreApplication
  10. from UM.FileHandler.FileHandler import FileHandler
  11. from UM.Scene.SceneNode import SceneNode
  12. from cura.NetworkClient import NetworkClient
  13. from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
  14. class AuthState(IntEnum):
  15. NotAuthenticated = 1
  16. AuthenticationRequested = 2
  17. Authenticated = 3
  18. AuthenticationDenied = 4
  19. AuthenticationReceived = 5
  20. class NetworkedPrinterOutputDevice(PrinterOutputDevice, NetworkClient):
  21. authenticationStateChanged = pyqtSignal()
  22. def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
  23. PrinterOutputDevice.__init__(self, device_id = device_id, parent = parent)
  24. NetworkClient.__init__(self)
  25. self._api_prefix = ""
  26. self._address = address
  27. self._properties = properties
  28. self._authentication_state = AuthState.NotAuthenticated
  29. self._sending_gcode = False
  30. self._compressing_gcode = False
  31. self._gcode = [] # type: List[str]
  32. self._connection_state_before_timeout = None # type: Optional[ConnectionState]
  33. self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
  34. self._recreate_network_manager_time = 30
  35. ## Override creating empty request to compile the full URL.
  36. # Needed to keep NetworkedPrinterOutputDevice backwards compatible after refactoring NetworkClient out of it.
  37. def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
  38. return super()._createEmptyRequest("http://" + self._address + self._api_prefix + target, content_type)
  39. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
  40. raise NotImplementedError("requestWrite needs to be implemented")
  41. def setAuthenticationState(self, authentication_state: AuthState) -> None:
  42. if self._authentication_state != authentication_state:
  43. self._authentication_state = authentication_state
  44. self.authenticationStateChanged.emit()
  45. @pyqtProperty(int, notify = authenticationStateChanged)
  46. def authenticationState(self) -> AuthState:
  47. return self._authentication_state
  48. def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
  49. compressed_data = gzip.compress(data_to_append.encode("utf-8"))
  50. self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
  51. QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
  52. # Pretend that this is a response, as zipping might take a bit of time.
  53. # If we don't do this, the device might trigger a timeout.
  54. self._last_response_time = time()
  55. return compressed_data
  56. def _compressGCode(self) -> Optional[bytes]:
  57. self._compressing_gcode = True
  58. ## Mash the data into single string
  59. max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
  60. file_data_bytes_list = []
  61. batched_lines = []
  62. batched_lines_count = 0
  63. for line in self._gcode:
  64. if not self._compressing_gcode:
  65. self._progress_message.hide()
  66. # Stop trying to zip / send as abort was called.
  67. return None
  68. # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
  69. # Compressing line by line in this case is extremely slow, so we need to batch them.
  70. batched_lines.append(line)
  71. batched_lines_count += len(line)
  72. if batched_lines_count >= max_chars_per_line:
  73. file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
  74. batched_lines = []
  75. batched_lines_count = 0
  76. # Don't miss the last batch (If any)
  77. if len(batched_lines) != 0:
  78. file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
  79. self._compressing_gcode = False
  80. return b"".join(file_data_bytes_list)
  81. def _update(self) -> None:
  82. if self._last_response_time:
  83. time_since_last_response = time() - self._last_response_time
  84. else:
  85. time_since_last_response = 0
  86. if self._last_request_time:
  87. time_since_last_request = time() - self._last_request_time
  88. else:
  89. time_since_last_request = float("inf") # An irrelevantly large number of seconds
  90. if time_since_last_response > self._timeout_time >= time_since_last_request:
  91. # Go (or stay) into timeout.
  92. if self._connection_state_before_timeout is None:
  93. self._connection_state_before_timeout = self._connection_state
  94. self.setConnectionState(ConnectionState.closed)
  95. # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
  96. # sleep.
  97. if time_since_last_response > self._recreate_network_manager_time:
  98. if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
  99. self._createNetworkManager()
  100. assert(self._manager is not None)
  101. elif self._connection_state == ConnectionState.closed:
  102. # Go out of timeout.
  103. if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
  104. self.setConnectionState(self._connection_state_before_timeout)
  105. self._connection_state_before_timeout = None
  106. ## Convenience function to get the username from the OS.
  107. # The code was copied from the getpass module, as we try to use as little dependencies as possible.
  108. def _getUserName(self) -> str:
  109. for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
  110. user = os.environ.get(name)
  111. if user:
  112. return user
  113. return "Unknown User" # Couldn't find out username.
  114. @pyqtSlot(str, result = str)
  115. def getProperty(self, key: str) -> str:
  116. bytes_key = key.encode("utf-8")
  117. if bytes_key in self._properties:
  118. return self._properties.get(bytes_key, b"").decode("utf-8")
  119. else:
  120. return ""
  121. def getProperties(self):
  122. return self._properties
  123. ## Get the unique key of this machine
  124. # \return key String containing the key of the machine.
  125. @pyqtProperty(str, constant = True)
  126. def key(self) -> str:
  127. return self._id
  128. ## The IP address of the printer.
  129. @pyqtProperty(str, constant = True)
  130. def address(self) -> str:
  131. return self._properties.get(b"address", b"").decode("utf-8")
  132. ## Name of the printer (as returned from the ZeroConf properties)
  133. @pyqtProperty(str, constant = True)
  134. def name(self) -> str:
  135. return self._properties.get(b"name", b"").decode("utf-8")
  136. ## Firmware version (as returned from the ZeroConf properties)
  137. @pyqtProperty(str, constant = True)
  138. def firmwareVersion(self) -> str:
  139. return self._properties.get(b"firmware_version", b"").decode("utf-8")
  140. @pyqtProperty(str, constant = True)
  141. def printerType(self) -> str:
  142. return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
  143. ## IP address of this printer
  144. @pyqtProperty(str, constant = True)
  145. def ipAddress(self) -> str:
  146. return self._address
  147. def __handleOnFinished(self, reply: QNetworkReply) -> None:
  148. super().__handleOnFinished(reply)
  149. # Since we got a reply from the network manager we can now be sure we are actually connected.
  150. if self._connection_state == ConnectionState.connecting:
  151. self.setConnectionState(ConnectionState.connected)