wow.py 8.3 KB

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