Cura/plugins/SliceInfoPlugin/SliceInfo.py
Thomas Karl Pietrowski 8597b5c9be CURA-1923: SliceInfo: Sending data threaded
The idea is to make the sending process, so it won't block the UI until the data is sent or the timeout has reached.
This commit does the following:
* Increasing the timeout to 5s
-> As we are sending our data threaded now this won't hurt. At least for clients in slow networks.
* Moving section for "sending data to info_url" into a seperate thread.
* Starting the thread instead and collecting all threads in a list
* Removing all threads from the list, if they have already finished their work.
* When the SliceInfo Extension gets __del__'d, it will wait for the threads until they finish.

Contributes to CURA-1923
2016-07-25 22:25:26 +02:00

160 lines
No EOL
7.6 KiB
Python

# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Extension import Extension
from UM.Application import Application
from UM.Preferences import Preferences
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Platform import Platform
from UM.Qt.Duration import DurationFormat
from threading import Thread
import platform
import math
import urllib.request
import urllib.parse
import ssl
catalog = i18nCatalog("cura")
class SliceInfoThread(Thread):
data = None
url = None
def __init__(self, url, data):
Thread.__init__(self)
self.url = url
self.data = data
def run(self):
if not self.url or not self.data:
Logger.log("e", "URL or DATA for sending slice info was not set!")
# Submit data
kwoptions = {"data" : self.data,
"timeout" : 5
}
if Platform.isOSX():
kwoptions["context"] = ssl._create_unverified_context()
try:
f = urllib.request.urlopen(self.url, **kwoptions)
Logger.log("i", "Sent anonymous slice info to %s", self.url)
f.close()
except Exception:
Logger.logException("e", "An exception occurred while trying to send slice information")
## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
# no model files are being sent (Just a SHA256 hash of the model).
class SliceInfo(Extension):
info_url = "https://stats.youmagine.com/curastats/slice"
def __init__(self):
super().__init__()
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
Preferences.getInstance().addPreference("info/send_slice_info", True)
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False)
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
self.runningThreads = []
def __del__(self):
for thread in self.threadedReports:
if thread.is_alive():
thread.join() # Wait for threads - shouldn't take much more time than the timeout. See above..
def _removeFinishedThreads(self):
for process in self.runningThreads:
if not process.is_alive():
self.runningThreads.remove(process) # Remove finished threads
def messageActionTriggered(self, message_id, action_id):
self.send_slice_info_message.hide()
Preferences.getInstance().setValue("info/asked_send_slice_info", True)
def _onWriteStarted(self, output_device):
self._removeFinishedThreads()
try:
if not Preferences.getInstance().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Get total material used (in mm^3)
print_information = Application.getInstance().getPrintInformation()
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
# TODO: Send material per extruder instead of mashing it on a pile
material_used = math.pi * material_radius * material_radius * sum(print_information.materialLengths) #Volume of all materials used
# Get model information (bounding boxes, hashes and transformation matrix)
models_info = []
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False):
model_info = {}
model_info["hash"] = node.getMeshData().getHash()
model_info["bounding_box"] = {}
model_info["bounding_box"]["minimum"] = {}
model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z
model_info["bounding_box"]["maximum"] = {}
model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
model_info["transformation"] = str(node.getWorldTransformation().getData())
models_info.append(model_info)
# Bundle the collected data
submitted_data = {
"processor": platform.processor(),
"machine": platform.machine(),
"platform": platform.platform(),
"settings": global_container_stack.serialize(), # global_container with references on used containers
"version": Application.getInstance().getVersion(),
"modelhash": "None",
"printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
"filament": material_used,
"language": Preferences.getInstance().getValue("general/language"),
}
for container in global_container_stack.getContainers():
container_id = container.getId()
try:
container_serialized = container.serialize()
except NotImplementedError:
Logger.log("w", "Container %s could not be serialized!", container_id)
continue
if container_serialized:
submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
else:
Logger.log("i", "No data found in %s to be serialized!", container_id)
# Convert data to bytes
submitted_data = urllib.parse.urlencode(submitted_data)
binary_data = submitted_data.encode("utf-8")
# Sending slice info non-blocking
reportThread = SliceInfoThread(self.info_url, binary_data)
self.runningThreads.append(reportThread)
reportThread.start()
except:
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
Logger.logException("e", "Exception raised while sending slice info") # But we should be notified about these problems of course.