CuraEngineBackend.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Backend.Backend import Backend
  4. from UM.Application import Application
  5. from UM.Scene.SceneNode import SceneNode
  6. from UM.Preferences import Preferences
  7. from UM.Signal import Signal
  8. from UM.Logger import Logger
  9. from UM.Qt.Bindings.BackendProxy import BackendState #To determine the state of the slicing job.
  10. from UM.Message import Message
  11. from UM.PluginRegistry import PluginRegistry
  12. from . import ProcessSlicedObjectListJob
  13. from . import ProcessGCodeJob
  14. from . import StartSliceJob
  15. import os
  16. import sys
  17. from PyQt5.QtCore import QTimer
  18. import Arcus
  19. from UM.i18n import i18nCatalog
  20. catalog = i18nCatalog("cura")
  21. class CuraEngineBackend(Backend):
  22. def __init__(self):
  23. super().__init__()
  24. # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
  25. default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", "CuraEngine")
  26. if hasattr(sys, "frozen"):
  27. default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
  28. if sys.platform == "win32":
  29. default_engine_location += ".exe"
  30. default_engine_location = os.path.abspath(default_engine_location)
  31. Preferences.getInstance().addPreference("backend/location", default_engine_location)
  32. self._scene = Application.getInstance().getController().getScene()
  33. self._scene.sceneChanged.connect(self._onSceneChanged)
  34. # Workaround to disable layer view processing if layer view is not active.
  35. self._layer_view_active = False
  36. Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
  37. self._onActiveViewChanged()
  38. self._stored_layer_data = None
  39. # When there are current settings and machine instance is changed, there is no profile changed event. We should
  40. # pretend there is though.
  41. Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveProfileChanged)
  42. self._profile = None
  43. Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
  44. self._onActiveProfileChanged()
  45. self._change_timer = QTimer()
  46. self._change_timer.setInterval(500)
  47. self._change_timer.setSingleShot(True)
  48. self._change_timer.timeout.connect(self.slice)
  49. self._message_handlers["cura.proto.SlicedObjectList"] = self._onSlicedObjectListMessage
  50. self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
  51. self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
  52. self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
  53. self._message_handlers["cura.proto.ObjectPrintTime"] = self._onObjectPrintTimeMessage
  54. self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
  55. self._slicing = False
  56. self._restart = False
  57. self._enabled = True
  58. self._always_restart = True
  59. self._process_layers_job = None #The currently active job to process layers, or None if it is not processing layers.
  60. self._message = None
  61. self.backendQuit.connect(self._onBackendQuit)
  62. self.backendConnected.connect(self._onBackendConnected)
  63. Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
  64. Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
  65. Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onInstanceChanged)
  66. ## Get the command that is used to call the engine.
  67. # This is useful for debugging and used to actually start the engine
  68. # \return list of commands and args / parameters.
  69. def getEngineCommand(self):
  70. active_machine = Application.getInstance().getMachineManager().getActiveMachineInstance()
  71. if not active_machine:
  72. return None
  73. return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", active_machine.getMachineDefinition().getPath(), "-vv"]
  74. ## Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
  75. # \param time The amount of time the print will take.
  76. # \param material_amount The amount of material the print will use.
  77. printDurationMessage = Signal()
  78. ## Emitted when the slicing process starts.
  79. slicingStarted = Signal()
  80. ## Emitted whne the slicing process is aborted forcefully.
  81. slicingCancelled = Signal()
  82. ## Perform a slice of the scene.
  83. def slice(self):
  84. if not self._enabled:
  85. return
  86. if self._slicing:
  87. self._terminate()
  88. if self._message:
  89. self._message.hide()
  90. self._message = None
  91. return
  92. if self._process_layers_job:
  93. self._process_layers_job.abort()
  94. self._process_layers_job = None
  95. if self._profile.hasErrorValue():
  96. Logger.log("w", "Profile has error values. Aborting slicing")
  97. if self._message:
  98. self._message.hide()
  99. self._message = None
  100. self._message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
  101. self._message.show()
  102. return #No slicing if we have error values since those are by definition illegal values.
  103. self.processingProgress.emit(0.0)
  104. self.backendStateChange.emit(BackendState.NOT_STARTED)
  105. if self._message:
  106. self._message.setProgress(-1)
  107. #else:
  108. # self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
  109. # self._message.show()
  110. self._scene.gcode_list = []
  111. self._slicing = True
  112. self.slicingStarted.emit()
  113. job = StartSliceJob.StartSliceJob(self._profile, self._socket)
  114. job.start()
  115. job.finished.connect(self._onStartSliceCompleted)
  116. def _terminate(self):
  117. self._slicing = False
  118. self._restart = True
  119. self.slicingCancelled.emit()
  120. self.processingProgress.emit(0)
  121. Logger.log("d", "Attempting to kill the engine process")
  122. if self._process is not None:
  123. Logger.log("d", "Killing engine process")
  124. try:
  125. self._process.terminate()
  126. Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
  127. self._process = None
  128. #self._createSocket() # Re create the socket
  129. except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
  130. Logger.log("d", "Exception occured while trying to kill the engine %s", str(e))
  131. def _onStartSliceCompleted(self, job):
  132. if job.getError() or job.getResult() != True:
  133. if self._message:
  134. self._message.hide()
  135. self._message = None
  136. return
  137. def _onSceneChanged(self, source):
  138. if type(source) is not SceneNode:
  139. return
  140. if source is self._scene.getRoot():
  141. return
  142. if source.getMeshData() is None:
  143. return
  144. if source.getMeshData().getVertices() is None:
  145. return
  146. self._onChanged()
  147. def _onSocketError(self, error):
  148. super()._onSocketError(error)
  149. self._terminate()
  150. if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
  151. Logger.log("e", "A socket error caused the connection to be reset")
  152. def _onActiveProfileChanged(self):
  153. if self._profile:
  154. self._profile.settingValueChanged.disconnect(self._onSettingChanged)
  155. self._profile = Application.getInstance().getMachineManager().getWorkingProfile()
  156. if self._profile:
  157. self._profile.settingValueChanged.connect(self._onSettingChanged)
  158. self._onChanged()
  159. def _onSettingChanged(self, setting):
  160. self._onChanged()
  161. def _onSlicedObjectListMessage(self, message):
  162. if self._layer_view_active:
  163. self._process_layers_job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(message)
  164. self._process_layers_job.start()
  165. else :
  166. self._stored_layer_data = message
  167. def _onProgressMessage(self, message):
  168. if self._message:
  169. self._message.setProgress(round(message.amount * 100))
  170. self.processingProgress.emit(message.amount)
  171. self.backendStateChange.emit(BackendState.PROCESSING)
  172. def _onSlicingFinishedMessage(self, message):
  173. self.backendStateChange.emit(BackendState.DONE)
  174. self.processingProgress.emit(1.0)
  175. self._slicing = False
  176. if self._message:
  177. self._message.setProgress(100)
  178. self._message.hide()
  179. self._message = None
  180. def _onGCodeLayerMessage(self, message):
  181. self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
  182. def _onGCodePrefixMessage(self, message):
  183. self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
  184. def _onObjectPrintTimeMessage(self, message):
  185. self.printDurationMessage.emit(message.time, message.material_amount)
  186. def _createSocket(self):
  187. super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
  188. ## Manually triggers a reslice
  189. def forceSlice(self):
  190. self._change_timer.start()
  191. def _onChanged(self):
  192. if not self._profile:
  193. return
  194. self._change_timer.start()
  195. def _onBackendConnected(self):
  196. if self._restart:
  197. self._onChanged()
  198. self._restart = False
  199. def _onToolOperationStarted(self, tool):
  200. self._terminate() # Do not continue slicing once a tool has started
  201. self._enabled = False # Do not reslice when a tool is doing its 'thing'
  202. def _onToolOperationStopped(self, tool):
  203. self._enabled = True # Tool stop, start listening for changes again.
  204. def _onActiveViewChanged(self):
  205. if Application.getInstance().getController().getActiveView():
  206. view = Application.getInstance().getController().getActiveView()
  207. if view.getPluginId() == "LayerView":
  208. self._layer_view_active = True
  209. # There is data and we're not slicing at the moment
  210. # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
  211. if self._stored_layer_data and not self._slicing:
  212. self._process_layers_job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(self._stored_layer_data)
  213. self._process_layers_job.start()
  214. self._stored_layer_data = None
  215. else:
  216. self._layer_view_active = False
  217. def _onInstanceChanged(self):
  218. self._terminate()
  219. def _onBackendQuit(self):
  220. if not self._restart and self._process:
  221. Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
  222. self._process = None
  223. self._createSocket()