SliceInfo.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from cura.CuraApplication import CuraApplication
  4. from cura.Settings.ExtruderManager import ExtruderManager
  5. from UM.Extension import Extension
  6. from UM.Application import Application
  7. from UM.Preferences import Preferences
  8. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  9. from UM.Message import Message
  10. from UM.i18n import i18nCatalog
  11. from UM.Logger import Logger
  12. import time
  13. from UM.Qt.Duration import DurationFormat
  14. from .SliceInfoJob import SliceInfoJob
  15. import platform
  16. import math
  17. import urllib.request
  18. import urllib.parse
  19. import json
  20. catalog = i18nCatalog("cura")
  21. ## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
  22. # The data is only sent when the user in question gave permission to do so. All data is anonymous and
  23. # no model files are being sent (Just a SHA256 hash of the model).
  24. class SliceInfo(Extension):
  25. info_url = "https://stats.ultimaker.com/api/cura"
  26. def __init__(self):
  27. super().__init__()
  28. Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
  29. Preferences.getInstance().addPreference("info/send_slice_info", True)
  30. Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
  31. if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
  32. self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
  33. lifetime = 0,
  34. dismissable = False,
  35. title = catalog.i18nc("@info:title", "Collecting Data"))
  36. self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
  37. description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
  38. self.send_slice_info_message.addAction("Disable", name = catalog.i18nc("@action:button", "Disable"), icon = None,
  39. description = catalog.i18nc("@action:tooltip", "Don't allow Cura to send anonymized usage statistics. You can enable it again in the preferences."), button_style = Message.ActionButtonStyle.LINK)
  40. self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
  41. self.send_slice_info_message.show()
  42. ## Perform action based on user input.
  43. # Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
  44. def messageActionTriggered(self, message_id, action_id):
  45. Preferences.getInstance().setValue("info/asked_send_slice_info", True)
  46. if action_id == "Disable":
  47. CuraApplication.getInstance().showPreferences()
  48. self.send_slice_info_message.hide()
  49. def _onWriteStarted(self, output_device):
  50. try:
  51. if not Preferences.getInstance().getValue("info/send_slice_info"):
  52. Logger.log("d", "'info/send_slice_info' is turned off.")
  53. return # Do nothing, user does not want to send data
  54. global_container_stack = Application.getInstance().getGlobalContainerStack()
  55. print_information = Application.getInstance().getPrintInformation()
  56. data = dict() # The data that we're going to submit.
  57. data["time_stamp"] = time.time()
  58. data["schema_version"] = 0
  59. data["cura_version"] = Application.getInstance().getVersion()
  60. active_mode = Preferences.getInstance().getValue("cura/active_mode")
  61. if active_mode == 0:
  62. data["active_mode"] = "recommended"
  63. else:
  64. data["active_mode"] = "custom"
  65. definition_changes = global_container_stack.definitionChanges
  66. machine_settings_changed_by_user = False
  67. if definition_changes.getId() != "empty":
  68. # Now a definition_changes container will always be created for a stack,
  69. # so we also need to check if there is any instance in the definition_changes container
  70. if definition_changes.getAllKeys():
  71. machine_settings_changed_by_user = True
  72. data["machine_settings_changed_by_user"] = machine_settings_changed_by_user
  73. data["language"] = Preferences.getInstance().getValue("general/language")
  74. data["os"] = {"type": platform.system(), "version": platform.version()}
  75. data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")}
  76. # add extruder specific data to slice info
  77. data["extruders"] = []
  78. extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
  79. extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
  80. for extruder in extruders:
  81. extruder_dict = dict()
  82. extruder_dict["active"] = ExtruderManager.getInstance().getActiveExtruderStack() == extruder
  83. extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""),
  84. "type": extruder.material.getMetaData().get("material", ""),
  85. "brand": extruder.material.getMetaData().get("brand", "")
  86. }
  87. extruder_position = int(extruder.getMetaDataEntry("position", "0"))
  88. if len(print_information.materialLengths) > extruder_position:
  89. extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
  90. extruder_dict["variant"] = extruder.variant.getName()
  91. extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")
  92. extruder_settings = dict()
  93. extruder_settings["wall_line_count"] = extruder.getProperty("wall_line_count", "value")
  94. extruder_settings["retraction_enable"] = extruder.getProperty("retraction_enable", "value")
  95. extruder_settings["infill_sparse_density"] = extruder.getProperty("infill_sparse_density", "value")
  96. extruder_settings["infill_pattern"] = extruder.getProperty("infill_pattern", "value")
  97. extruder_settings["gradual_infill_steps"] = extruder.getProperty("gradual_infill_steps", "value")
  98. extruder_settings["default_material_print_temperature"] = extruder.getProperty("default_material_print_temperature", "value")
  99. extruder_settings["material_print_temperature"] = extruder.getProperty("material_print_temperature", "value")
  100. extruder_dict["extruder_settings"] = extruder_settings
  101. data["extruders"].append(extruder_dict)
  102. data["quality_profile"] = global_container_stack.quality.getMetaData().get("quality_type")
  103. data["models"] = []
  104. # Listing all files placed on the build plate
  105. for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()):
  106. if node.callDecoration("isSliceable"):
  107. model = dict()
  108. model["hash"] = node.getMeshData().getHash()
  109. bounding_box = node.getBoundingBox()
  110. model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
  111. "y": bounding_box.minimum.y,
  112. "z": bounding_box.minimum.z},
  113. "maximum": {"x": bounding_box.maximum.x,
  114. "y": bounding_box.maximum.y,
  115. "z": bounding_box.maximum.z}}
  116. model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")}
  117. extruder_position = node.callDecoration("getActiveExtruderPosition")
  118. model["extruder"] = 0 if extruder_position is None else int(extruder_position)
  119. model_settings = dict()
  120. model_stack = node.callDecoration("getStack")
  121. if model_stack:
  122. model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
  123. model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))
  124. # Mesh modifiers;
  125. model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
  126. model_settings["cutting_mesh"] = model_stack.getProperty("cutting_mesh", "value")
  127. model_settings["support_mesh"] = model_stack.getProperty("support_mesh", "value")
  128. model_settings["anti_overhang_mesh"] = model_stack.getProperty("anti_overhang_mesh", "value")
  129. model_settings["wall_line_count"] = model_stack.getProperty("wall_line_count", "value")
  130. model_settings["retraction_enable"] = model_stack.getProperty("retraction_enable", "value")
  131. # Infill settings
  132. model_settings["infill_sparse_density"] = model_stack.getProperty("infill_sparse_density", "value")
  133. model_settings["infill_pattern"] = model_stack.getProperty("infill_pattern", "value")
  134. model_settings["gradual_infill_steps"] = model_stack.getProperty("gradual_infill_steps", "value")
  135. model["model_settings"] = model_settings
  136. data["models"].append(model)
  137. print_times = print_information.printTimes()
  138. data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
  139. "support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
  140. "infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),
  141. "total": int(print_information.currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))}
  142. print_settings = dict()
  143. print_settings["layer_height"] = global_container_stack.getProperty("layer_height", "value")
  144. # Support settings
  145. print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
  146. print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))
  147. # Platform adhesion settings
  148. print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")
  149. # Shell settings
  150. print_settings["wall_line_count"] = global_container_stack.getProperty("wall_line_count", "value")
  151. print_settings["retraction_enable"] = global_container_stack.getProperty("retraction_enable", "value")
  152. # Prime tower settings
  153. print_settings["prime_tower_enable"] = global_container_stack.getProperty("prime_tower_enable", "value")
  154. # Infill settings
  155. print_settings["infill_sparse_density"] = global_container_stack.getProperty("infill_sparse_density", "value")
  156. print_settings["infill_pattern"] = global_container_stack.getProperty("infill_pattern", "value")
  157. print_settings["gradual_infill_steps"] = global_container_stack.getProperty("gradual_infill_steps", "value")
  158. print_settings["print_sequence"] = global_container_stack.getProperty("print_sequence", "value")
  159. data["print_settings"] = print_settings
  160. # Send the name of the output device type that is used.
  161. data["output_to"] = type(output_device).__name__
  162. # Convert data to bytes
  163. binary_data = json.dumps(data).encode("utf-8")
  164. # Sending slice info non-blocking
  165. reportJob = SliceInfoJob(self.info_url, binary_data)
  166. reportJob.start()
  167. except Exception:
  168. # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
  169. # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
  170. Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.