printrun-src/printrun/laser.py

changeset 22
4c9bb8f93ae8
parent 21
8551b89bd05e
child 23
e18b2a4ef561
equal deleted inserted replaced
21:8551b89bd05e 22:4c9bb8f93ae8
4 Intended to use standalone or implemented in Pronterface/Printrun 4 Intended to use standalone or implemented in Pronterface/Printrun
5 """ 5 """
6 6
7 """ 7 """
8 LASERCUT SETTINGS 8 LASERCUT SETTINGS
9 TODO: move to printrun settings 9 Will be overridden from pronterface settings
10 """ 10 """
11 ENGRAVE_SPEED = 10 * 60 # mm/min
12 # 30mm/min works for wood (regulate the output power to something between 10-30%)
13 # 30mm/min for black anodized aluminum to get a light engraving @ 100% power
14 # 10mm/min for black anodized aluminum to get more "silver" @ 100% power
15
16 TRAVEL_SPEED = 120 * 60
17 E_FACTOR = 0.5 11 E_FACTOR = 0.5
18 12
19 # BITMAP: 13 from PIL import Image
20 DPI = 300 14 import sys
21 GREY_THRESHOLD = 0 15 import math
22 CHANGE_DIRECTION = True 16
23 INVERT_CUT = True
24
25 """
26 STATIC DEFINITIONS
27 DO NOT CHANGE WORLD's RULES!
28 """
29 INCH = 25.4 # mm
30 MM_PIXEL = round(INCH / DPI, 4)
31 STEPS_PIXEL = MM_PIXEL * 80 # mine is 80 steps/mm on XY
32
33 # FOR HPGL:
34 SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units
35 17
36 # GENERAL HEADER AND FOOTER GCODE 18 # GENERAL HEADER AND FOOTER GCODE
37 GCODE_HEAD = """ 19 GCODE_HEAD = """
38 ; GCode generated by laser.py pronterface library (marlin code flavour) 20 ; GCode generated by laser.py pronterface library (marlin code flavour)
39 ; 2015/2016 by NeoSoft - Malte Bayer 21 ; 2015/2016 by NeoSoft - Malte Bayer
46 M201 X1000 Y1000 E1000 ; Set acceleration 28 M201 X1000 Y1000 E1000 ; Set acceleration
47 M203 X1000 Y1000 Z4 E1000 ; Set max feedrate 29 M203 X1000 Y1000 Z4 E1000 ; Set max feedrate
48 M209 S0 ; disable firmware retraction, we dont want to burn holes... 30 M209 S0 ; disable firmware retraction, we dont want to burn holes...
49 M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod 31 M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod
50 M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! 32 M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement!
51 G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED 33 """
52 G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED
53
54 """ % (TRAVEL_SPEED, ENGRAVE_SPEED)
55 34
56 GCODE_FOOT = """G0 X0 Y0 F%.4f 35 GCODE_FOOT = """G0 X0 Y0 F%.4f
57 M400 ; Wait for all moves to finish 36 M400 ; Wait for all moves to finish
58 M571 S0 E0 37 M571 S0 E0
59 M42 P28 S0 ; Force laser off! 38 M42 P28 S0 ; Force laser off!
60 M501 ; undo all settings made 39 M501 ; undo all settings made
61 """ % (TRAVEL_SPEED) 40 """ % (100*60)
62 41
63 from PIL import Image 42
64 import sys 43 class LasercutterSettings:
65 44 """
66 # Imports for SVG 45 Default settings object
67 import xml.etree.ElementTree as ET 46 """
68 import math 47 def __init__(self):
69 from svg2gcode import shapes as shapes_pkg 48 self.lc_engrave_speed = 10
70 from svg2gcode.shapes import point_generator 49 # 30mm/min works for wood (regulate the output power to something between 10-30%)
71 50 # 30mm/min for black anodized aluminum to get a light engraving @ 100% power
51 # 10mm/min for black anodized aluminum to get more "silver" @ 100% power
52 self.lc_travel_speed = 120
53
54 # BITMAP:
55 self.lc_bitmap_speed_factor = 1.0
56 self.lc_dpi = 300
57 self.lc_grey_threshold = 0
58 self.lc_change_dir = True
59 self.lc_invert_cut = True
60
61 # HPGL:
62 self.lc_hpgl_speed_factor = 1.0
63
64 # SVG:
65 self.lc_svg_speed_factor = 1.0
72 66
73 class Lasercutter: 67 class Lasercutter:
74 """ 68 """
75 Lasercutter methods 69 Lasercutter methods
76 parameters: log = logger function (fuction has to accept a string) 70 parameters: log = logger function (fuction has to accept a string)
77 """ 71 """
78 def __init__(self, pronterwindow = None): 72 def __init__(self, pronterwindow = None):
79 if pronterwindow: 73 if pronterwindow:
80 self.pronterwindow = pronterwindow 74 self.pronterwindow = pronterwindow
75 self.settings = pronterwindow.settings
81 #self.log = pronterwindow.log 76 #self.log = pronterwindow.log
82 self.log = self.log_print 77 self.log = self.log_print
83 self.pronterwindow.clear_log(None) 78 self.pronterwindow.clear_log(None)
84 else: 79 else:
85 self.pronterwindow = None 80 self.pronterwindow = None
81 self.settings = LasercutterSettings()
86 self.log = lambda : None 82 self.log = lambda : None
87 self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (DPI, MM_PIXEL)) 83
88 if STEPS_PIXEL <= 5: 84 # STATIC DEFINITIONS, DO NOT CHANGE WORLD's RULES!
89 self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % STEPS_PIXEL) 85 self.INCH = 25.4 # mm
86 self.MM_PIXEL = round(self.INCH / self.settings.lc_dpi, 4)
87 self.STEPS_PIXEL = self.MM_PIXEL * 80 # mine is 80 steps/mm on XY
88
89 self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (self.settings.lc_dpi, self.MM_PIXEL))
90 if self.STEPS_PIXEL <= 5:
91 self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % self.STEPS_PIXEL)
90 self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( 92 self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % (
91 TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) ) 93 self.settings.lc_travel_speed, self.settings.lc_engrave_speed) )
92 self.log("") 94 self.log("")
93 95
94 def log_print(self, msg): 96 def log_print(self, msg):
95 print(msg) 97 print(msg)
96 98
97 99
98 def pixel2bit(self, pixel, threshold=128): 100 def pixel2bit(self, pixel):
99 """Convert the pixel value to a bit.""" 101 """Convert the pixel value to a bit."""
100 # some really weird stuff here ;-P 102 # some really weird stuff here ;-P
101 103
102 # RGB to greyscale 104 # RGB to greyscale
103 #print pixel 105 #print pixel
104 #print type(pixel) 106 #print type(pixel)
105 if isinstance(pixel, tuple): 107 if isinstance(pixel, tuple):
106 #rgb 108 #rgb
107 pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 109 pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140
108 threshold = 128 110 if pixel > self.settings.lc_grey_threshold:
109 if pixel > threshold:
110 return 1 111 return 1
111 else: 112 else:
112 return 0 113 return 0
113 114
114 # color palette 115 # color palette
115 if pixel <= threshold: 116 # TODO: get the grey value of the palette index instead of using pixel which is the palette index?
117 if pixel <= self.settings.lc_grey_threshold:
116 return 1 118 return 1
117 else: 119 else:
118 return 0 120 return 0
119 121
120 def image2gcode(self, filename): 122 def image2gcode(self, filename):
138 140
139 fo = open(filename + ".g", "w") 141 fo = open(filename + ".g", "w")
140 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) 142 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD))
141 143
142 fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % ( 144 fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % (
143 im.size[0], im.size[1], DPI, im.size[0]*MM_PIXEL, im.size[1]*MM_PIXEL) ) 145 im.size[0], im.size[1], self.settings.lc_dpi, im.size[0] * self.MM_PIXEL, im.size[1] * self.MM_PIXEL) )
144 146
145 INVERT_Y = MM_PIXEL * (im.size[1] -1) * (-1) 147 INVERT_Y = self.MM_PIXEL * (im.size[1] -1) * (-1)
146 DIR = 1 148 DIR = 1
149 travel_speed = self.settings.lc_travel_speed * 60
150 engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_bitmap_speed_factor
147 for X in range(im.size[0]): 151 for X in range(im.size[0]):
148 fo.write("; X=%d printing row: direction %i\n" % (X, DIR)) 152 # TODO: Skip empty rows!!!
153 fo.write("M400 ; X=%d printing row: direction %i\n" % (X, DIR))
149 fo.write("G92 E0\n") 154 fo.write("G92 E0\n")
150 E = 0 155 E = 0
151 last_bit = 1 # we engrave on black pixel = 0 156 last_bit = 1 # we engrave on black pixel = 0
152 START_Y = 0 157 START_Y = 0
153 if DIR > 0: 158 if DIR > 0:
156 else: 161 else:
157 range_start = im.size[1] -1 162 range_start = im.size[1] -1
158 range_stop = -1 163 range_stop = -1
159 164
160 for Y in range(range_start, range_stop, DIR): 165 for Y in range(range_start, range_stop, DIR):
161 YMM = abs((Y * MM_PIXEL) + INVERT_Y) 166 YMM = abs((Y * self.MM_PIXEL) + INVERT_Y)
162 XMM = X * MM_PIXEL 167 XMM = X * self.MM_PIXEL
163 #print "X %d Y %d" % (X, Y) 168 #print "X %d Y %d" % (X, Y)
164 bit = self.pixel2bit(pix[X, Y], GREY_THRESHOLD) 169 bit = self.pixel2bit(pix[X, Y])
165 if INVERT_CUT: 170 if self.settings.lc_invert_cut:
166 if bit == 0: 171 if bit == 0:
167 bit = 1 172 bit = 1
168 else: 173 else:
169 bit = 0 174 bit = 0
170 if last_bit == bit: 175 if last_bit == bit:
175 # are we at the end of Y range? 180 # are we at the end of Y range?
176 #print Y 181 #print Y
177 if (Y == (im.size[1] - 1)) or (Y == 0): 182 if (Y == (im.size[1] - 1)) or (Y == 0):
178 # draw line 183 # draw line
179 if DIR > 0: 184 if DIR > 0:
180 E = E + MM_PIXEL * (Y - START_Y) 185 E = E + self.MM_PIXEL * (Y - START_Y)
181 else: 186 else:
182 E = E + MM_PIXEL * (START_Y - Y) 187 E = E + self.MM_PIXEL * (START_Y - Y)
183 fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) 188 fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed))
184 else: 189 else:
185 # bit value has changed! 190 # bit value has changed!
186 if bit == 0: 191 if bit == 0:
187 # jump to start of line to write 192 # jump to start of line to write
188 START_Y = Y 193 START_Y = Y
189 fo.write("G0 X%.4f Y%.4f\n" % (XMM, YMM)) 194 fo.write("G0 X%.4f Y%.4f F%.4f\n" % (XMM, YMM, travel_speed))
190 else: 195 else:
191 # end of line to write 196 # end of line to write
192 if DIR > 0: 197 if DIR > 0:
193 E = E + (MM_PIXEL * (Y - START_Y)) 198 E = E + (self.MM_PIXEL * (Y - START_Y))
194 else: 199 else:
195 E = E + (MM_PIXEL * (START_Y - Y)) 200 E = E + (self.MM_PIXEL * (START_Y - Y))
196 fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) 201 fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed))
197 last_bit = bit 202 last_bit = bit
198 if CHANGE_DIRECTION: 203 if self.settings.lc_change_dir:
199 DIR = DIR * (-1) # change y direction on every X 204 DIR = DIR * (-1) # change y direction on every X
200 205
201 fo.write(GCODE_FOOT) 206 fo.write(GCODE_FOOT)
202 fo.close() 207 fo.close()
203 208
204 if self.pronterwindow: 209 if self.pronterwindow:
205 self.log("") 210 self.log("")
206 self.pronterwindow.load_gcode_async(filename + '.g') 211 self.pronterwindow.load_gcode_async(filename + '.g')
207 212
208 def hpgl2gcode(self, filename): 213 def hpgl2gcode(self, filename):
214 # FOR HPGL:
215 SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units
209 OFFSET_X = 0.0 216 OFFSET_X = 0.0
210 OFFSET_Y = 0.0 217 OFFSET_Y = 0.0
211 218
212 self.log("Converting HPGL plot for lasercut:") 219 self.log("Converting HPGL plot for lasercut:")
213 self.log("File: %s" % filename) 220 self.log("File: %s" % filename)
218 225
219 G = "0" 226 G = "0"
220 LASER_STATE = 0 227 LASER_STATE = 0
221 last_coord = [0.0,0.0] 228 last_coord = [0.0,0.0]
222 last_cmd = "" 229 last_cmd = ""
230
231 travel_speed = self.settings.lc_travel_speed * 60
232 engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_hpgl_speed_factor
233
223 234
224 for line in fi.readlines(): 235 for line in fi.readlines():
225 for action in line.split(";"): 236 for action in line.split(";"):
226 action = action.strip() 237 action = action.strip()
227 if action != "": 238 if action != "":
240 coord[0] = (float(coord[0]) + OFFSET_X) * SCALE_FACTOR 251 coord[0] = (float(coord[0]) + OFFSET_X) * SCALE_FACTOR
241 coord[1] = (float(coord[1]) + OFFSET_Y) * SCALE_FACTOR 252 coord[1] = (float(coord[1]) + OFFSET_Y) * SCALE_FACTOR
242 if LASER_STATE: 253 if LASER_STATE:
243 EN = " E%.4f F%.4f" % ( 254 EN = " E%.4f F%.4f" % (
244 E_FACTOR * math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]), 255 E_FACTOR * math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]),
245 ENGRAVE_SPEED * 0.5 ) # 1/2 engraving speed 256 engrave_speed)
246 else: 257 else:
247 EN = " F%.4f" % TRAVEL_SPEED 258 EN = " F%.4f" % travel_speed
248 259
249 fo.write("G%d X%.4f Y%.4f%s\n" % ( 260 fo.write("G%d X%.4f Y%.4f%s\n" % (
250 LASER_STATE, coord[0], coord[1], EN) ) 261 LASER_STATE, coord[0], coord[1], EN) )
251 last_coord = coord 262 last_coord = coord
252 elif cmd == "IN": 263 elif cmd == "IN":
263 274
264 if self.pronterwindow: 275 if self.pronterwindow:
265 self.log("") 276 self.log("")
266 self.pronterwindow.load_gcode_async(filename + '.g') 277 self.pronterwindow.load_gcode_async(filename + '.g')
267 278
268
269 def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2): 279 def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2):
270 self.log("Generating paths from SVG...") 280 # Imports for SVG
271 281 import xml.etree.ElementTree as ET
272 shape_preamble = "G92 E0\n" 282 from svg2gcode import shapes as shapes_pkg
273 shape_postamble = "" 283 from svg2gcode.shapes import point_generator
274 284
275 """ 285 self.log("Generating paths from SVG, alternative lib (outlines only)...")
276 Used to control the smoothness/sharpness of the curves.
277 Smaller the value greater the sharpness. Make sure the
278 value is greater than 0.1
279 """
280 if smoothness < 0.1: smoothness = 0.1 286 if smoothness < 0.1: smoothness = 0.1
281
282 svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) 287 svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'])
283
284 tree = ET.parse(filename) 288 tree = ET.parse(filename)
285 root = tree.getroot() 289 root = tree.getroot()
286 290
287 width = root.get('width') 291 width = root.get('width')
288 height = root.get('height') 292 height = root.get('height')
293 297
294 if width == None or height == None: 298 if width == None or height == None:
295 self.log("Unable to get width and height for the svg!") 299 self.log("Unable to get width and height for the svg!")
296 return False 300 return False
297 301
298 width = float(width.replace("px", "")) 302 width = float(width.replace("px", "").replace("pt", ""))
299 height = float(height.replace("px", "")) 303 height = float(height.replace("px", "").replace("pt", ""))
300 304
301 scale_x = bed_max_x / max(width, height) 305 scale_x = bed_max_x / max(width, height)
302 scale_y = bed_max_y / max(width, height) 306 scale_y = bed_max_y / max(width, height)
303 307
304 self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y)) 308 self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y))
305 309
306 fo = open(filename + ".g", "w") 310 fo = open(filename + ".g", "w")
307 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) 311 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD))
312
313 travel_speed = self.settings.lc_travel_speed * 60
314 engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_svg_speed_factor
308 315
309 for elem in root.iter(): 316 for elem in root.iter():
310 try: 317 try:
311 _, tag_suffix = elem.tag.split('}') 318 _, tag_suffix = elem.tag.split('}')
312 except ValueError: 319 except ValueError:
317 shape_obj = shape_class(elem) 324 shape_obj = shape_class(elem)
318 d = shape_obj.d_path() 325 d = shape_obj.d_path()
319 m = shape_obj.transformation_matrix() 326 m = shape_obj.transformation_matrix()
320 327
321 if d: 328 if d:
322 fo.write("M400 ; wait for moves finish, then printing shape: %s\n" % (tag_suffix)) 329 fo.write("M400 ; start %s\n" % (tag_suffix))
330 fo.write("G92 E0\n")
323 E = 0 331 E = 0
324 xo = 0 332 xo = 0
325 yo = 0 333 yo = 0
326 fo.write(shape_preamble)
327 p = point_generator(d, m, smoothness) 334 p = point_generator(d, m, smoothness)
328 start = True 335 start = True
329 for x,y,pen in p: 336 for x,y,pen in p:
330 y = height - y 337 y = height - y
331 xs = scale_x * x 338 xs = scale_x * x
333 if xo == xs and yo == ys: continue 340 if xo == xs and yo == ys: continue
334 341
335 if not pen: start = True 342 if not pen: start = True
336 if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y: 343 if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y:
337 if start: 344 if start:
338 fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, TRAVEL_SPEED)) 345 fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, travel_speed))
339 start = False 346 start = False
340 xo = xs 347 xo = xs
341 yo = ys 348 yo = ys
342 object_xs = xs 349 object_xs = xs
343 object_ys = ys 350 object_ys = ys
344 else: 351 else:
345 e_distance = math.hypot(xs - xo, ys - yo) 352 e_distance = math.hypot(xs - xo, ys - yo)
346 xo = xs 353 xo = xs
347 yo = ys 354 yo = ys
348 E = E + (e_distance) 355 E = E + (e_distance)
349 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, ENGRAVE_SPEED)) 356 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, engrave_speed))
350 else: 357 else:
351 self.log("Position outside print dimension: %d, %d" % (xs, ys)) 358 self.log("Position outside print dimension: %d, %d" % (xs, ys))
352 if shape_obj.xml_node.get('fill'): 359 if shape_obj.xml_node.get('fill'):
353 # Close the polygon 360 # Close the polygon
354 e_distance = math.hypot(object_xs - xo, object_ys - yo) 361 e_distance = math.hypot(object_xs - xo, object_ys - yo)
355 E = E + (e_distance) 362 E = E + (e_distance)
356 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f ; Close the object polygon\n" % (object_xs, object_ys, E * E_FACTOR, ENGRAVE_SPEED)) 363 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f ; Close the object polygon\n" % (object_xs, object_ys, E * E_FACTOR, engrave_speed))
357 print "connecting polycommon path end to start" 364 print "connecting filled path end to start"
358
359 fo.write(shape_postamble)
360 365
361 fo.write(GCODE_FOOT) 366 fo.write(GCODE_FOOT)
362 fo.close() 367 fo.close()
363 368
364 if self.pronterwindow: 369 if self.pronterwindow:

mercurial