LegacyUM3OutputDevice.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
  2. from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
  3. from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
  4. from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
  5. from cura.PrinterOutput.NetworkCamera import NetworkCamera
  6. from cura.Settings.ContainerManager import ContainerManager
  7. from cura.Settings.ExtruderManager import ExtruderManager
  8. from UM.Logger import Logger
  9. from UM.Settings.ContainerRegistry import ContainerRegistry
  10. from UM.Application import Application
  11. from UM.i18n import i18nCatalog
  12. from UM.Message import Message
  13. from PyQt5.QtNetwork import QNetworkRequest
  14. from PyQt5.QtCore import QTimer, QCoreApplication
  15. from PyQt5.QtWidgets import QMessageBox
  16. from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
  17. from time import time
  18. import json
  19. import os
  20. i18n_catalog = i18nCatalog("cura")
  21. ## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
  22. # Everything after that firmware uses the ClusterUM3Output.
  23. # The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
  24. #
  25. # Authentication is done in a number of steps;
  26. # 1. Request an id / key pair by sending the application & user name. (state = authRequested)
  27. # 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
  28. # 3. OutputDevice will poll if the button was pressed.
  29. # 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
  30. # 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
  31. class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
  32. def __init__(self, device_id, address: str, properties, parent = None):
  33. super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
  34. self._api_prefix = "/api/v1/"
  35. self._number_of_extruders = 2
  36. self._authentication_id = None
  37. self._authentication_key = None
  38. self._authentication_counter = 0
  39. self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
  40. self._authentication_timer = QTimer()
  41. self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
  42. self._authentication_timer.setSingleShot(False)
  43. self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
  44. # The messages are created when connect is called the first time.
  45. # This ensures that the messages are only created for devices that actually want to connect.
  46. self._authentication_requested_message = None
  47. self._authentication_failed_message = None
  48. self._authentication_succeeded_message = None
  49. self._not_authenticated_message = None
  50. self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
  51. self.setPriority(3) # Make sure the output device gets selected above local file output
  52. self.setName(self._id)
  53. self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
  54. self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
  55. self.setIconName("print")
  56. self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
  57. self._output_controller = LegacyUM3PrinterOutputController(self)
  58. def _onAuthenticationStateChanged(self):
  59. # We only accept commands if we are authenticated.
  60. self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)
  61. if self._authentication_state == AuthState.Authenticated:
  62. self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
  63. elif self._authentication_state == AuthState.AuthenticationRequested:
  64. self.setConnectionText(i18n_catalog.i18nc("@info:status",
  65. "Connected over the network. Please approve the access request on the printer."))
  66. elif self._authentication_state == AuthState.AuthenticationDenied:
  67. self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))
  68. def _setupMessages(self):
  69. self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
  70. "Access to the printer requested. Please approve the request on the printer"),
  71. lifetime=0, dismissable=False, progress=0,
  72. title=i18n_catalog.i18nc("@info:title",
  73. "Authentication status"))
  74. self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
  75. title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
  76. self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
  77. i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
  78. self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
  79. self._authentication_succeeded_message = Message(
  80. i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
  81. title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
  82. self._not_authenticated_message = Message(
  83. i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
  84. title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
  85. self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
  86. None, i18n_catalog.i18nc("@info:tooltip",
  87. "Send access request to the printer"))
  88. self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
  89. def _messageCallback(self, message_id=None, action_id="Retry"):
  90. if action_id == "Request" or action_id == "Retry":
  91. if self._authentication_failed_message:
  92. self._authentication_failed_message.hide()
  93. if self._not_authenticated_message:
  94. self._not_authenticated_message.hide()
  95. self._requestAuthentication()
  96. def connect(self):
  97. super().connect()
  98. self._setupMessages()
  99. global_container = Application.getInstance().getGlobalContainerStack()
  100. if global_container:
  101. self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
  102. self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
  103. def close(self):
  104. super().close()
  105. if self._authentication_requested_message:
  106. self._authentication_requested_message.hide()
  107. if self._authentication_failed_message:
  108. self._authentication_failed_message.hide()
  109. if self._authentication_succeeded_message:
  110. self._authentication_succeeded_message.hide()
  111. self._sending_gcode = False
  112. self._compressing_gcode = False
  113. self._authentication_timer.stop()
  114. ## Send all material profiles to the printer.
  115. def _sendMaterialProfiles(self):
  116. Logger.log("i", "Sending material profiles to printer")
  117. # TODO: Might want to move this to a job...
  118. for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
  119. try:
  120. xml_data = container.serialize()
  121. if xml_data == "" or xml_data is None:
  122. continue
  123. names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
  124. if names:
  125. # There are other materials that share this GUID.
  126. if not container.isReadOnly():
  127. continue # If it's not readonly, it's created by user, so skip it.
  128. file_name = "none.xml"
  129. self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None)
  130. except NotImplementedError:
  131. # If the material container is not the most "generic" one it can't be serialized an will raise a
  132. # NotImplementedError. We can simply ignore these.
  133. pass
  134. def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
  135. if not self.activePrinter:
  136. # No active printer. Unable to write
  137. return
  138. if self.activePrinter.state not in ["idle", ""]:
  139. # Printer is not able to accept commands.
  140. return
  141. if self._authentication_state != AuthState.Authenticated:
  142. # Not authenticated, so unable to send job.
  143. return
  144. self.writeStarted.emit(self)
  145. gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
  146. active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  147. gcode_list = gcode_dict[active_build_plate_id]
  148. if not gcode_list:
  149. # Unable to find g-code. Nothing to send
  150. return
  151. self._gcode = gcode_list
  152. errors = self._checkForErrors()
  153. if errors:
  154. text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
  155. informative_text = i18n_catalog.i18nc("@label",
  156. "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
  157. "Please resolve this issues before continuing.")
  158. detailed_text = ""
  159. for error in errors:
  160. detailed_text += error + "\n"
  161. Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
  162. text,
  163. informative_text,
  164. detailed_text,
  165. buttons=QMessageBox.Ok,
  166. icon=QMessageBox.Critical,
  167. callback = self._messageBoxCallback
  168. )
  169. return # Don't continue; Errors must block sending the job to the printer.
  170. # There might be multiple things wrong with the configuration. Check these before starting.
  171. warnings = self._checkForWarnings()
  172. if warnings:
  173. text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
  174. informative_text = i18n_catalog.i18nc("@label",
  175. "There is a mismatch between the configuration or calibration of the printer and Cura. "
  176. "For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
  177. detailed_text = ""
  178. for warning in warnings:
  179. detailed_text += warning + "\n"
  180. Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
  181. text,
  182. informative_text,
  183. detailed_text,
  184. buttons=QMessageBox.Yes + QMessageBox.No,
  185. icon=QMessageBox.Question,
  186. callback=self._messageBoxCallback
  187. )
  188. return
  189. # No warnings or errors, so we're good to go.
  190. self._startPrint()
  191. # Notify the UI that a switch to the print monitor should happen
  192. Application.getInstance().getController().setActiveStage("MonitorStage")
  193. def _startPrint(self):
  194. Logger.log("i", "Sending print job to printer.")
  195. if self._sending_gcode:
  196. self._error_message = Message(
  197. i18n_catalog.i18nc("@info:status",
  198. "Sending new jobs (temporarily) blocked, still sending the previous print job."))
  199. self._error_message.show()
  200. return
  201. self._sending_gcode = True
  202. self._send_gcode_start = time()
  203. self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
  204. i18n_catalog.i18nc("@info:title", "Sending Data"))
  205. self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
  206. self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
  207. self._progress_message.show()
  208. compressed_gcode = self._compressGCode()
  209. if compressed_gcode is None:
  210. # Abort was called.
  211. return
  212. file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
  213. self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
  214. onFinished=self._onPostPrintJobFinished)
  215. return
  216. def _progressMessageActionTriggered(self, message_id=None, action_id=None):
  217. if action_id == "Abort":
  218. Logger.log("d", "User aborted sending print to remote.")
  219. self._progress_message.hide()
  220. self._compressing_gcode = False
  221. self._sending_gcode = False
  222. Application.getInstance().getController().setActiveStage("PrepareStage")
  223. def _onPostPrintJobFinished(self, reply):
  224. self._progress_message.hide()
  225. self._sending_gcode = False
  226. def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
  227. if bytes_total > 0:
  228. new_progress = bytes_sent / bytes_total * 100
  229. # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
  230. # timeout responses if this happens.
  231. self._last_response_time = time()
  232. if new_progress > self._progress_message.getProgress():
  233. self._progress_message.show() # Ensure that the message is visible.
  234. self._progress_message.setProgress(bytes_sent / bytes_total * 100)
  235. else:
  236. self._progress_message.setProgress(0)
  237. self._progress_message.hide()
  238. def _messageBoxCallback(self, button):
  239. def delayedCallback():
  240. if button == QMessageBox.Yes:
  241. self._startPrint()
  242. else:
  243. Application.getInstance().getController().setActiveStage("PrepareStage")
  244. # For some unknown reason Cura on OSX will hang if we do the call back code
  245. # immediately without first returning and leaving QML's event system.
  246. QTimer.singleShot(100, delayedCallback)
  247. def _checkForErrors(self):
  248. errors = []
  249. print_information = Application.getInstance().getPrintInformation()
  250. if not print_information.materialLengths:
  251. Logger.log("w", "There is no material length information. Unable to check for errors.")
  252. return errors
  253. for index, extruder in enumerate(self.activePrinter.extruders):
  254. # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
  255. if extruder.hotendID == "":
  256. # No Printcore loaded.
  257. errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
  258. if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
  259. # The extruder is by this print.
  260. if extruder.activeMaterial is None:
  261. # No active material
  262. errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
  263. return errors
  264. def _checkForWarnings(self):
  265. warnings = []
  266. print_information = Application.getInstance().getPrintInformation()
  267. if not print_information.materialLengths:
  268. Logger.log("w", "There is no material length information. Unable to check for warnings.")
  269. return warnings
  270. extruder_manager = ExtruderManager.getInstance()
  271. for index, extruder in enumerate(self.activePrinter.extruders):
  272. if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
  273. # The extruder is by this print.
  274. # TODO: material length check
  275. # Check if the right Printcore is active.
  276. variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
  277. if variant:
  278. if variant.getName() != extruder.hotendID:
  279. warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
  280. else:
  281. Logger.log("w", "Unable to find variant.")
  282. # Check if the right material is loaded.
  283. local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
  284. if local_material:
  285. if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
  286. Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
  287. warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
  288. else:
  289. Logger.log("w", "Unable to find material.")
  290. return warnings
  291. def _update(self):
  292. if not super()._update():
  293. return
  294. if self._authentication_state == AuthState.NotAuthenticated:
  295. if self._authentication_id is None and self._authentication_key is None:
  296. # This machine doesn't have any authentication, so request it.
  297. self._requestAuthentication()
  298. elif self._authentication_id is not None and self._authentication_key is not None:
  299. # We have authentication info, but we haven't checked it out yet. Do so now.
  300. self._verifyAuthentication()
  301. elif self._authentication_state == AuthState.AuthenticationReceived:
  302. # We have an authentication, but it's not confirmed yet.
  303. self._checkAuthentication()
  304. # We don't need authentication for requesting info, so we can go right ahead with requesting this.
  305. self.get("printer", onFinished=self._onGetPrinterDataFinished)
  306. self.get("print_job", onFinished=self._onGetPrintJobFinished)
  307. def _resetAuthenticationRequestedMessage(self):
  308. if self._authentication_requested_message:
  309. self._authentication_requested_message.hide()
  310. self._authentication_timer.stop()
  311. self._authentication_counter = 0
  312. def _onAuthenticationTimer(self):
  313. self._authentication_counter += 1
  314. self._authentication_requested_message.setProgress(
  315. self._authentication_counter / self._max_authentication_counter * 100)
  316. if self._authentication_counter > self._max_authentication_counter:
  317. self._authentication_timer.stop()
  318. Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
  319. self.setAuthenticationState(AuthState.AuthenticationDenied)
  320. self._resetAuthenticationRequestedMessage()
  321. self._authentication_failed_message.show()
  322. def _verifyAuthentication(self):
  323. Logger.log("d", "Attempting to verify authentication")
  324. # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
  325. self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
  326. def _onVerifyAuthenticationCompleted(self, reply):
  327. status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
  328. if status_code == 401:
  329. # Something went wrong; We somehow tried to verify authentication without having one.
  330. Logger.log("d", "Attempted to verify auth without having one.")
  331. self._authentication_id = None
  332. self._authentication_key = None
  333. self.setAuthenticationState(AuthState.NotAuthenticated)
  334. elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
  335. # If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
  336. Logger.log("d",
  337. "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
  338. self._authentication_state)
  339. self.setAuthenticationState(AuthState.AuthenticationDenied)
  340. self._authentication_failed_message.show()
  341. elif status_code == 200:
  342. self.setAuthenticationState(AuthState.Authenticated)
  343. def _checkAuthentication(self):
  344. Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
  345. self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
  346. def _onCheckAuthenticationFinished(self, reply):
  347. if str(self._authentication_id) not in reply.url().toString():
  348. Logger.log("w", "Got an old id response.")
  349. # Got response for old authentication ID.
  350. return
  351. try:
  352. data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  353. except json.decoder.JSONDecodeError:
  354. Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
  355. return
  356. if data.get("message", "") == "authorized":
  357. Logger.log("i", "Authentication was approved")
  358. self.setAuthenticationState(AuthState.Authenticated)
  359. self._saveAuthentication()
  360. # Double check that everything went well.
  361. self._verifyAuthentication()
  362. # Notify the user.
  363. self._resetAuthenticationRequestedMessage()
  364. self._authentication_succeeded_message.show()
  365. elif data.get("message", "") == "unauthorized":
  366. Logger.log("i", "Authentication was denied.")
  367. self.setAuthenticationState(AuthState.AuthenticationDenied)
  368. self._authentication_failed_message.show()
  369. def _saveAuthentication(self):
  370. global_container_stack = Application.getInstance().getGlobalContainerStack()
  371. if global_container_stack:
  372. if "network_authentication_key" in global_container_stack.getMetaData():
  373. global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
  374. else:
  375. global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
  376. if "network_authentication_id" in global_container_stack.getMetaData():
  377. global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
  378. else:
  379. global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
  380. # Force save so we are sure the data is not lost.
  381. Application.getInstance().saveStack(global_container_stack)
  382. Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
  383. self._getSafeAuthKey())
  384. else:
  385. Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
  386. self._getSafeAuthKey())
  387. def _onRequestAuthenticationFinished(self, reply):
  388. try:
  389. data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  390. except json.decoder.JSONDecodeError:
  391. Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
  392. self.setAuthenticationState(AuthState.NotAuthenticated)
  393. return
  394. self.setAuthenticationState(AuthState.AuthenticationReceived)
  395. self._authentication_id = data["id"]
  396. self._authentication_key = data["key"]
  397. Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
  398. self._authentication_id, self._getSafeAuthKey())
  399. def _requestAuthentication(self):
  400. self._authentication_requested_message.show()
  401. self._authentication_timer.start()
  402. # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
  403. # give issues.
  404. self._authentication_key = None
  405. self._authentication_id = None
  406. self.post("auth/request",
  407. json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
  408. "user": self._getUserName()}).encode(),
  409. onFinished=self._onRequestAuthenticationFinished)
  410. self.setAuthenticationState(AuthState.AuthenticationRequested)
  411. def _onAuthenticationRequired(self, reply, authenticator):
  412. if self._authentication_id is not None and self._authentication_key is not None:
  413. Logger.log("d",
  414. "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
  415. self._id, self._authentication_id, self._getSafeAuthKey())
  416. authenticator.setUser(self._authentication_id)
  417. authenticator.setPassword(self._authentication_key)
  418. else:
  419. Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
  420. def _onGetPrintJobFinished(self, reply):
  421. status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
  422. if not self._printers:
  423. return # Ignore the data for now, we don't have info about a printer yet.
  424. printer = self._printers[0]
  425. if status_code == 200:
  426. try:
  427. result = json.loads(bytes(reply.readAll()).decode("utf-8"))
  428. except json.decoder.JSONDecodeError:
  429. Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
  430. return
  431. if printer.activePrintJob is None:
  432. print_job = PrintJobOutputModel(output_controller=self._output_controller)
  433. printer.updateActivePrintJob(print_job)
  434. else:
  435. print_job = printer.activePrintJob
  436. print_job.updateState(result["state"])
  437. print_job.updateTimeElapsed(result["time_elapsed"])
  438. print_job.updateTimeTotal(result["time_total"])
  439. print_job.updateName(result["name"])
  440. elif status_code == 404:
  441. # No job found, so delete the active print job (if any!)
  442. printer.updateActivePrintJob(None)
  443. else:
  444. Logger.log("w",
  445. "Got status code {status_code} while trying to get printer data".format(status_code=status_code))
  446. def materialHotendChangedMessage(self, callback):
  447. Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
  448. i18n_catalog.i18nc("@label",
  449. "Would you like to use your current printer configuration in Cura?"),
  450. i18n_catalog.i18nc("@label",
  451. "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
  452. buttons=QMessageBox.Yes + QMessageBox.No,
  453. icon=QMessageBox.Question,
  454. callback=callback
  455. )
  456. def _onGetPrinterDataFinished(self, reply):
  457. status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
  458. if status_code == 200:
  459. try:
  460. result = json.loads(bytes(reply.readAll()).decode("utf-8"))
  461. except json.decoder.JSONDecodeError:
  462. Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
  463. return
  464. if not self._printers:
  465. # Quickest way to get the firmware version is to grab it from the zeroconf.
  466. firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
  467. self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
  468. self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream"))
  469. for extruder in self._printers[0].extruders:
  470. extruder.activeMaterialChanged.connect(self.materialIdChanged)
  471. extruder.hotendIDChanged.connect(self.hotendIdChanged)
  472. self.printersChanged.emit()
  473. # LegacyUM3 always has a single printer.
  474. printer = self._printers[0]
  475. printer.updateBedTemperature(result["bed"]["temperature"]["current"])
  476. printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
  477. printer.updateState(result["status"])
  478. try:
  479. # If we're still handling the request, we should ignore remote for a bit.
  480. if not printer.getController().isPreheatRequestInProgress():
  481. printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
  482. except KeyError:
  483. # Older firmwares don't support preheating, so we need to fake it.
  484. pass
  485. head_position = result["heads"][0]["position"]
  486. printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
  487. for index in range(0, self._number_of_extruders):
  488. temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
  489. extruder = printer.extruders[index]
  490. extruder.updateTargetHotendTemperature(temperatures["target"])
  491. extruder.updateHotendTemperature(temperatures["current"])
  492. material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
  493. if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
  494. # Find matching material (as we need to set brand, type & color)
  495. containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
  496. GUID=material_guid)
  497. if containers:
  498. color = containers[0].getMetaDataEntry("color_code")
  499. brand = containers[0].getMetaDataEntry("brand")
  500. material_type = containers[0].getMetaDataEntry("material")
  501. name = containers[0].getName()
  502. else:
  503. # Unknown material.
  504. color = "#00000000"
  505. brand = "Unknown"
  506. material_type = "Unknown"
  507. name = "Unknown"
  508. material = MaterialOutputModel(guid=material_guid, type=material_type,
  509. brand=brand, color=color, name = name)
  510. extruder.updateActiveMaterial(material)
  511. try:
  512. hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
  513. except KeyError:
  514. hotend_id = ""
  515. printer.extruders[index].updateHotendID(hotend_id)
  516. else:
  517. Logger.log("w",
  518. "Got status code {status_code} while trying to get printer data".format(status_code = status_code))
  519. ## Convenience function to "blur" out all but the last 5 characters of the auth key.
  520. # This can be used to debug print the key, without it compromising the security.
  521. def _getSafeAuthKey(self):
  522. if self._authentication_key is not None:
  523. result = self._authentication_key[-5:]
  524. result = "********" + result
  525. return result
  526. return self._authentication_key