wow.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #!`/usr/bin/whereis python3`
  2. import datetime
  3. from PIL import Image
  4. # Ignore movement which are larger than this value for:
  5. # - Layer thickness calculation
  6. # - Movement time
  7. # This is mainly to ignore the last movement made after the final layer
  8. # which as the gcode is made would screw the thickness calculation on that
  9. # layer. (There is nothing to know that the layer ended in that specific case)
  10. # TODO: this need to be improved in a way. This is not completely reliable
  11. _IgnoreMovementLargerThan = 15 # in mm
  12. class layer:
  13. """Implement one layer of SLA with all linked parameters"""
  14. def __init__(self, number):
  15. self.thickness = 0
  16. self.exposition_time = 0
  17. self.move_time = 0
  18. self.number = number
  19. self.speed_up = 0
  20. self.speed_down = 0
  21. self.up_distance = 0
  22. self.down_distance = 0
  23. self.exposition = 0
  24. self.data = b""
  25. self.width = 0
  26. self.height = 0
  27. self.image = None
  28. def set_image(self, data, size):
  29. self.width = size[0]
  30. self.height = size[1]
  31. self.image = Image.frombytes("1", size, data, "raw", "1;R")
  32. self.image = self.image.rotate(90, expand=True)
  33. self.data = data # For now keep it until we have a packing function
  34. def update_movetime(self):
  35. if self.speed_up > 0 or self.speed_down > 0:
  36. self.move_time = abs(self.up_distance) / (self.speed_up / 60)
  37. self.move_time += abs(self.up_distance - self.thickness) / (self.speed_down / 60)
  38. else:
  39. # Absurdely huge number to show that there is a problem
  40. # (movement set to 0 is not a wanted value)
  41. self.move_time = 9999999
  42. def get_packed_image(self):
  43. return self.data
  44. def set_exposition(self, value):
  45. self.exposition = value
  46. def _nothing(code, cur_layer):
  47. """Stub for unused gcode command"""
  48. pass
  49. def _g1(code, cur_layer):
  50. """Decode gcode G1 command"""
  51. distance = 0
  52. speed = 0
  53. for param in code:
  54. if param[0] == "Z" or param[0] == "z":
  55. distance = float(param[1:])
  56. if abs(distance) <= _IgnoreMovementLargerThan:
  57. if distance > 0:
  58. cur_layer.up_distance = distance
  59. else:
  60. cur_layer.down_distance = distance
  61. cur_layer.thickness += distance
  62. cur_layer.thickness = round(cur_layer.thickness, 5)
  63. if speed is not 0:
  64. cur_layer.move_time += abs(distance) / (speed / 60)
  65. if distance > 0:
  66. cur_layer.speed_up = speed
  67. else:
  68. cur_layer.speed_down = speed
  69. elif param[0] == "F" or param[0] == "f":
  70. speed = float(param[1:])
  71. if distance is not 0:
  72. cur_layer.move_time += abs(distance) / (speed / 60)
  73. if distance > 0:
  74. cur_layer.speed_up = speed
  75. else:
  76. cur_layer.speed_down = speed
  77. def _g4(code, cur_layer):
  78. """Decode gcode G4 command"""
  79. for param in code:
  80. if param[0] == "S" or param[0] == "s":
  81. value = float(param[1:])
  82. # Ending have a really long pause, don't change layer exposition if it is longer than 120 seconds
  83. if value <= 120:
  84. cur_layer.exposition_time += value
  85. def _m106(code, cur_layer):
  86. """Decode gcode M106 command"""
  87. if cur_layer is not None:
  88. for param in code:
  89. if param[0] == "S" or param[0] == "s":
  90. value = float(param[1:])
  91. if value > 0:
  92. cur_layer.set_exposition(value)
  93. class WowFile:
  94. """"""
  95. _GCodes = {
  96. "G21": _nothing, # Set unit to millimetre
  97. "G91": _nothing, # Set positioning to relative
  98. "M17": _nothing, # Set On/Off all steppers
  99. "M106": _m106, # UV Backlight power
  100. "G28": _nothing, # Move to origin (Homing)
  101. "M18": _nothing, # Move to origin (Homing)
  102. "G1": _g1, # Move
  103. "G4": _g4, # Sleep
  104. }
  105. _Preamble = "G21;\n" \
  106. "G91;\n" \
  107. "M17;\n" \
  108. "M106 S0;\n" \
  109. "G28 Z0;\n" \
  110. ";W:{W};\n" \
  111. ";H:{H};\n"
  112. _Ending = "M106 S0;\n" \
  113. "G1 Z20.0;\n" \
  114. "G4 S300;\n" \
  115. "M18;"
  116. _LayerStart = ";L:{layer:d};\n" \
  117. "M106 S0;\n" \
  118. "G1 Z{up:,g} F{spdu:g};\n" \
  119. "G1 Z{down:,g} F{spdd:g};\n" \
  120. "{{{{\n"
  121. _LayerEnd = "\n" \
  122. "}}}}\n" \
  123. "M106 S{exp:g};\n" \
  124. "G4 S{wait:g};\n"
  125. def _decode(self, code, cur_layer):
  126. splitcode = code.strip(";").split(" ")
  127. if code[0] == 'G' or code[0] == 'g' or code[0] == 'M' or code[0] == 'm':
  128. try:
  129. self._GCodes[splitcode[0]](splitcode, cur_layer)
  130. except KeyError:
  131. raise Exception("Not support gcode found: {code}".format(code=code))
  132. elif code[0] == ';':
  133. if code[1] == "H" or code[1] == "h":
  134. self.Height = int(code.strip(";").split(":")[1], 0)
  135. elif code[1] == "W" or code[1] == "w":
  136. self.Width = int(code.strip(";").split(":")[1], 0)
  137. elif code[1] == "L" or code[1] == "l":
  138. return layer(int(code.strip(";").split(":")[1], 0))
  139. else:
  140. raise Exception("Invalid gcode found: {code}".format(code=code))
  141. return None
  142. def __init__(self, file):
  143. f = open(file, "rb")
  144. self.layers = []
  145. EndOfFile = False
  146. cur_layer = None
  147. while not EndOfFile:
  148. # Read GCode header until we get to the picture ( {{ )
  149. while True:
  150. line = f.readline().decode("ASCII").strip("\n")
  151. if line == "":
  152. EndOfFile = True
  153. break
  154. if line[0:2] == "{{":
  155. break
  156. new_layer = self._decode(line, cur_layer)
  157. if new_layer is not None:
  158. if cur_layer is not None:
  159. self.layers.append(cur_layer)
  160. cur_layer = new_layer
  161. if not EndOfFile:
  162. data = f.read(int(self.Height*self.Width / 8))
  163. cur_layer.set_image(data, (self.Width, self.Height))
  164. # There is a newline at end of image data, and.. we don't care about it -> just discard it.
  165. f.readline()
  166. line = f.readline()
  167. if line != b"}}\n":
  168. raise Exception("File is invalid, }} not found at end of frame")
  169. elif EndOfFile:
  170. # Don't forget to add the last layer!
  171. self.layers.append(cur_layer)
  172. def get_zheight(self):
  173. height = 0
  174. for l in self.layers:
  175. height = round(height + l.thickness, 5)
  176. return height
  177. def get_layer(self, layer_num):
  178. return self.layers[layer_num]
  179. def get_printtime(self, human_readable=False):
  180. exptime = 0
  181. for l in self.layers:
  182. exptime += l.exposition_time
  183. exptime += l.move_time
  184. exptime = round(exptime, 5)
  185. if human_readable:
  186. sec = round(exptime % 60, 0)
  187. mn = round(exptime // 60, 0)
  188. hour = round(mn // 60, 0)
  189. mn = round(mn % 60, 0)
  190. day = round(hour // 24, 0)
  191. hour = round(hour % 24, 0)
  192. out = ""
  193. if day > 0:
  194. out = " {d:g}d".format(d=day)
  195. if hour > 0:
  196. out = " {h:g}hr".format(h=hour)
  197. if mn > 0:
  198. out = out + " {m:g}min".format(m=mn)
  199. if sec > 0:
  200. out = out + " {s:g}s".format(s=sec)
  201. return out.lstrip()
  202. else:
  203. return exptime
  204. def get_layercount(self):
  205. return len(self.layers)
  206. def export_layer(self, layer_num, png_file):
  207. self.layers[layer_num].image.save(png_file, "PNG")
  208. def write_wow(self, filename):
  209. with open(filename, "wb") as f:
  210. # First write file preamble
  211. f.write(self._Preamble.format(H=self.Height, W=self.Width).encode("ascii"))
  212. for l in self.layers:
  213. # Write layer preamble
  214. f.write(self._LayerStart.format(layer=l.number,
  215. up=l.up_distance,
  216. down=round(l.thickness - l.up_distance, 5),
  217. spdu=l.speed_up,
  218. spdd=l.speed_down).encode("ascii"))
  219. # Write layer image
  220. f.write(l.get_packed_image())
  221. # Write layer ending
  222. f.write(self._LayerEnd.format(exp=l.exposition,
  223. wait=l.exposition_time).encode("ascii"))
  224. # Write ending
  225. f.write(self._Ending.encode("ascii"))
  226. if __name__ == "__main__":
  227. wN = WowFile("hollow_out_lcd_w.wow")
  228. print("Height in mm: {h}".format(h=wN.get_zheight()))
  229. print("Approx printing time: {exp}".format(exp=str(datetime.timedelta(seconds=wN.get_printtime()))))
  230. print("layer 1: {exp}".format(exp=wN.layers[1].exposition_time))
  231. print("layer 3: {exp}".format(exp=wN.layers[3].exposition_time))
  232. wN.export_layer(50, "test.png")
  233. #wN.write_wow("test.wow")