GCodeWriter.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import re # For escaping characters in the settings.
  4. import json
  5. from UM.Mesh.MeshWriter import MeshWriter
  6. from UM.Logger import Logger
  7. from UM.Application import Application
  8. from UM.Settings.InstanceContainer import InstanceContainer
  9. from cura.Machines.ContainerTree import ContainerTree
  10. from UM.i18n import i18nCatalog
  11. from cura.Settings.CuraStackBuilder import CuraStackBuilder
  12. catalog = i18nCatalog("cura")
  13. class GCodeWriter(MeshWriter):
  14. """Writes g-code to a file.
  15. While this poses as a mesh writer, what this really does is take the g-code
  16. in the entire scene and write it to an output device. Since the g-code of a
  17. single mesh isn't separable from the rest what with rafts and travel moves
  18. and all, it doesn't make sense to write just a single mesh.
  19. So this plug-in takes the g-code that is stored in the root of the scene
  20. node tree, adds a bit of extra information about the profiles and writes
  21. that to the output device.
  22. """
  23. version = 3
  24. """The file format version of the serialised g-code.
  25. It can only read settings with the same version as the version it was
  26. written with. If the file format is changed in a way that breaks reverse
  27. compatibility, increment this version number!
  28. """
  29. escape_characters = {
  30. re.escape("\\"): "\\\\", # The escape character.
  31. re.escape("\n"): "\\n", # Newlines. They break off the comment.
  32. re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
  33. }
  34. """Dictionary that defines how characters are escaped when embedded in
  35. g-code.
  36. Note that the keys of this dictionary are regex strings. The values are
  37. not.
  38. """
  39. _setting_keyword = ";SETTING_"
  40. def __init__(self):
  41. super().__init__(add_to_recent_files = False)
  42. self._application = Application.getInstance()
  43. def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
  44. """Writes the g-code for the entire scene to a stream.
  45. Note that even though the function accepts a collection of nodes, the
  46. entire scene is always written to the file since it is not possible to
  47. separate the g-code for just specific nodes.
  48. :param stream: The stream to write the g-code to.
  49. :param nodes: This is ignored.
  50. :param mode: Additional information on how to format the g-code in the
  51. file. This must always be text mode.
  52. """
  53. if mode != MeshWriter.OutputMode.TextMode:
  54. Logger.log("e", "GCodeWriter does not support non-text mode.")
  55. self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
  56. return False
  57. active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
  58. scene = Application.getInstance().getController().getScene()
  59. if not hasattr(scene, "gcode_dict"):
  60. self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
  61. return False
  62. gcode_dict = getattr(scene, "gcode_dict")
  63. gcode_list = gcode_dict.get(active_build_plate, None)
  64. if gcode_list is not None:
  65. has_settings = False
  66. for gcode in gcode_list:
  67. if gcode[:len(self._setting_keyword)] == self._setting_keyword:
  68. has_settings = True
  69. stream.write(gcode)
  70. # Serialise the current container stack and put it at the end of the file.
  71. if not has_settings:
  72. settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
  73. stream.write(settings)
  74. return True
  75. self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
  76. return False
  77. def _serialiseSettings(self, stack):
  78. """Serialises a container stack to prepare it for writing at the end of the g-code.
  79. The settings are serialised, and special characters (including newline)
  80. are escaped.
  81. :param stack: A container stack to serialise.
  82. :return: A serialised string of the settings.
  83. """
  84. container_registry = self._application.getContainerRegistry()
  85. prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
  86. prefix_length = len(prefix)
  87. quality_type = stack.quality.getMetaDataEntry("quality_type")
  88. container_with_profile = stack.qualityChanges
  89. machine_definition_id_for_quality = ContainerTree.getInstance().machines[stack.definition.getId()].quality_definition
  90. if container_with_profile.getId() == "empty_quality_changes":
  91. # If the global quality changes is empty, create a new one
  92. quality_name = container_registry.uniqueName(stack.quality.getName())
  93. quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
  94. container_with_profile = InstanceContainer(quality_id)
  95. container_with_profile.setName(quality_name)
  96. container_with_profile.setMetaDataEntry("type", "quality_changes")
  97. container_with_profile.setMetaDataEntry("quality_type", quality_type)
  98. if stack.getMetaDataEntry("position") is not None: # For extruder stacks, the quality changes should include an intent category.
  99. container_with_profile.setMetaDataEntry("intent_category", stack.intent.getMetaDataEntry("intent_category", "default"))
  100. container_with_profile.setDefinition(machine_definition_id_for_quality)
  101. container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
  102. merged_global_instance_container = InstanceContainer.createMergedInstanceContainer(stack.userChanges, container_with_profile)
  103. # If the quality changes is not set, we need to set type manually
  104. if merged_global_instance_container.getMetaDataEntry("type", None) is None:
  105. merged_global_instance_container.setMetaDataEntry("type", "quality_changes")
  106. # Ensure that quality_type is set. (Can happen if we have empty quality changes).
  107. if merged_global_instance_container.getMetaDataEntry("quality_type", None) is None:
  108. merged_global_instance_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
  109. # Get the machine definition ID for quality profiles
  110. merged_global_instance_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
  111. serialized = merged_global_instance_container.serialize()
  112. data = {"global_quality": serialized}
  113. all_setting_keys = merged_global_instance_container.getAllKeys()
  114. for extruder in stack.extruderList:
  115. extruder_quality = extruder.qualityChanges
  116. if extruder_quality.getId() == "empty_quality_changes":
  117. # Same story, if quality changes is empty, create a new one
  118. quality_name = container_registry.uniqueName(stack.quality.getName())
  119. quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
  120. extruder_quality = InstanceContainer(quality_id)
  121. extruder_quality.setName(quality_name)
  122. extruder_quality.setMetaDataEntry("type", "quality_changes")
  123. extruder_quality.setMetaDataEntry("quality_type", quality_type)
  124. extruder_quality.setDefinition(machine_definition_id_for_quality)
  125. extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
  126. flat_extruder_quality = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder_quality)
  127. # If the quality changes is not set, we need to set type manually
  128. if flat_extruder_quality.getMetaDataEntry("type", None) is None:
  129. flat_extruder_quality.setMetaDataEntry("type", "quality_changes")
  130. # Ensure that extruder is set. (Can happen if we have empty quality changes).
  131. if flat_extruder_quality.getMetaDataEntry("position", None) is None:
  132. flat_extruder_quality.setMetaDataEntry("position", extruder.getMetaDataEntry("position"))
  133. # Ensure that quality_type is set. (Can happen if we have empty quality changes).
  134. if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
  135. flat_extruder_quality.setMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
  136. # Change the default definition
  137. flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality)
  138. extruder_serialized = flat_extruder_quality.serialize()
  139. data.setdefault("extruder_quality", []).append(extruder_serialized)
  140. all_setting_keys.update(flat_extruder_quality.getAllKeys())
  141. # Check if there is any profiles
  142. if not all_setting_keys:
  143. Logger.log("i", "No custom settings found, not writing settings to g-code.")
  144. return ""
  145. json_string = json.dumps(data)
  146. # Escape characters that have a special meaning in g-code comments.
  147. pattern = re.compile("|".join(GCodeWriter.escape_characters.keys()))
  148. # Perform the replacement with a regular expression.
  149. escaped_string = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], json_string)
  150. # Introduce line breaks so that each comment is no longer than 80 characters. Prepend each line with the prefix.
  151. result = ""
  152. # Lines have 80 characters, so the payload of each line is 80 - prefix.
  153. for pos in range(0, len(escaped_string), 80 - prefix_length):
  154. result += prefix + escaped_string[pos: pos + 80 - prefix_length] + "\n"
  155. return result