Fri, 03 Jun 2016 21:14:09 +0200
Bugfixing, Added M400 magic
16 | 1 | """ |
2 | Lasercutter library | |
3 | 2015/2016 by NeoSoft, Malte Bayer | |
4 | Intended to use standalone or implemented in Pronterface/Printrun | |
5 | """ | |
6 | ||
7 | """ | |
8 | LASERCUT SETTINGS | |
9 | TODO: move to printrun settings | |
10 | """ | |
19 | 11 | ENGRAVE_SPEED = 10 * 60 # mm/min |
16 | 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 | ||
19 | 16 | TRAVEL_SPEED = 120 * 60 |
17 | E_FACTOR = 0.5 | |
16 | 18 | |
19 | # BITMAP: | |
20 | DPI = 300 | |
21 | GREY_THRESHOLD = 0 | |
22 | CHANGE_DIRECTION = True | |
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 | ||
36 | ||
37 | from PIL import Image | |
38 | import sys | |
39 | ||
40 | # Imports for SVG | |
41 | import xml.etree.ElementTree as ET | |
42 | import math | |
43 | from svg2gcode import shapes as shapes_pkg | |
44 | from svg2gcode.shapes import point_generator | |
45 | ||
46 | ||
47 | class Lasercutter: | |
48 | """ | |
49 | Lasercutter methods | |
50 | parameters: log = logger function (accepts a string) | |
51 | """ | |
52 | def __init__(self, pronterwindow = None): | |
53 | if pronterwindow: | |
54 | self.pronterwindow = pronterwindow | |
55 | self.log = pronterwindow.log | |
56 | else: | |
57 | self.pronterwindow = None | |
58 | self.log = lambda : None | |
59 | self.log("\nLasercutter library initialized resolution: %f mm per pixel" % MM_PIXEL) | |
60 | if STEPS_PIXEL <= 5: | |
61 | self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % STEPS_PIXEL) | |
62 | ||
63 | ||
64 | def pixel2bit(self, pixel, threshold=128): | |
65 | """Convert the pixel value to a bit.""" | |
66 | # some really weird stuff here ;-P | |
67 | ||
68 | # RGB to greyscale | |
69 | #print pixel | |
70 | #print type(pixel) | |
71 | if isinstance(pixel, tuple): | |
72 | #rgb | |
73 | pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 | |
74 | threshold = 128 | |
75 | if pixel > threshold: | |
76 | return 1 | |
77 | else: | |
78 | return 0 | |
79 | ||
80 | # color palette | |
81 | if pixel <= threshold: | |
82 | return 1 | |
83 | else: | |
84 | return 0 | |
85 | ||
86 | def image2gcode(self, filename): | |
87 | """ | |
88 | Open a image file and get the basic information about it. | |
89 | Then convert it to gcode (replacing the existing gcode buffer contents) | |
90 | """ | |
91 | try: | |
92 | im = Image.open(filename) | |
93 | except: | |
94 | self.log("Unable to open %s" % filename) | |
95 | return False | |
96 | ||
97 | self.log("Converting Image for lasercut:") | |
98 | self.log("File: %s" % filename) | |
99 | self.log("format: %s, mode: %s" % (im.format, im.mode)) | |
100 | width,height = im.size | |
101 | self.log("size: %d x %d pixels" % im.size) | |
102 | ||
103 | pix = im.load() | |
104 | ||
105 | fo = open(filename + ".g", "w") | |
106 | fo.write(""" | |
107 | ; Filename: %s | |
108 | ; GCode generated by bitplotter one-night-quick-hack script (marlin code flavour) | |
109 | ; 2015/2016 by NeoSoft - Malte Bayer | |
110 | ||
111 | G21 ; Metric | |
112 | ; We assume Z is in focus height and laser head is focus at bottom left of image! | |
113 | G92 X0 Y0 E0; set zero position - new origin | |
114 | G90 ; absolute positioning | |
115 | M82 ; Set extruder (laser) to absolute positioning | |
116 | M201 X1000 Y1000 E500 ; Set acceleration | |
117 | M203 X1000 Y1000 Z4 E10 ; Set max feedrate | |
118 | M209 S0 ; disable firmware retraction, we dont want to burn holes... | |
119 | M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod | |
120 | M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! | |
121 | G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED | |
122 | G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED | |
123 | ||
124 | """ % (filename, TRAVEL_SPEED, ENGRAVE_SPEED) ) | |
125 | self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( | |
126 | TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) ) | |
127 | ||
128 | fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % ( | |
129 | im.size[0], im.size[1], DPI, im.size[0]*MM_PIXEL, im.size[1]*MM_PIXEL) ) | |
130 | ||
131 | INVERT_Y = MM_PIXEL * (im.size[1] -1) * (-1) | |
132 | DIR = 1 | |
133 | for X in range(im.size[0]): | |
134 | fo.write("; X=%d printing row: direction %i\n" % (X, DIR)) | |
135 | fo.write("G92 E0\n") | |
136 | E = 0 | |
137 | last_bit = 1 # we engrave on black pixel = 0 | |
138 | START_Y = 0 | |
139 | if DIR > 0: | |
140 | range_start = 0 | |
141 | range_stop = im.size[1] | |
142 | else: | |
143 | range_start = im.size[1] -1 | |
144 | range_stop = -1 | |
145 | ||
146 | for Y in range(range_start, range_stop, DIR): | |
147 | YMM = abs((Y * MM_PIXEL) + INVERT_Y) | |
148 | XMM = X * MM_PIXEL | |
149 | #print "X %d Y %d" % (X, Y) | |
150 | bit = self.pixel2bit(pix[X, Y], GREY_THRESHOLD) | |
151 | if INVERT_CUT: | |
152 | if bit == 0: | |
153 | bit = 1 | |
154 | else: | |
155 | bit = 0 | |
156 | if last_bit == bit: | |
157 | if bit == 1: | |
158 | # nothing to do, | |
159 | continue | |
160 | else: | |
161 | # are we at the end of Y range? | |
162 | #print Y | |
163 | if (Y == (im.size[1] - 1)) or (Y == 0): | |
164 | # draw line | |
165 | if DIR > 0: | |
166 | E = E + MM_PIXEL * (Y - START_Y) | |
167 | else: | |
168 | E = E + MM_PIXEL * (START_Y - Y) | |
169 | fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) | |
170 | else: | |
171 | # bit value has changed! | |
172 | if bit == 0: | |
173 | # jump to start of line to write | |
174 | START_Y = Y | |
175 | fo.write("G0 X%.4f Y%.4f\n" % (XMM, YMM)) | |
176 | else: | |
177 | # end of line to write | |
178 | if DIR > 0: | |
179 | E = E + (MM_PIXEL * (Y - START_Y)) | |
180 | else: | |
181 | E = E + (MM_PIXEL * (START_Y - Y)) | |
182 | fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) | |
183 | last_bit = bit | |
184 | if CHANGE_DIRECTION: | |
185 | DIR = DIR * (-1) # change y direction on every X | |
186 | ||
19 | 187 | fo.write("""M400 ; Wait for all moves to finish |
188 | M571 S0 E0 | |
189 | M42 P28 S0 ; Force laser off! | |
190 | M501 ; undo all settings made | |
191 | """) | |
16 | 192 | |
193 | fo.close() | |
194 | ||
195 | if self.pronterwindow: | |
196 | self.log("") | |
197 | self.pronterwindow.load_gcode_async(filename + '.g') | |
198 | ||
199 | def hpgl2gcode(self, filename): | |
200 | OFFSET_X = 0.0 | |
201 | OFFSET_Y = 0.0 | |
202 | ||
203 | self.log("Converting HPGL plot for lasercut:") | |
204 | self.log("File: %s" % filename) | |
205 | ||
206 | fi = open(filename, "r") | |
207 | fo = open(filename + ".g", "w") | |
208 | ||
209 | ||
210 | fo.write(""" | |
211 | ; Filename: %s | |
212 | ; GCode generated by hpglplotter (marlin code flavour) | |
213 | ||
214 | G21 ; Metric | |
215 | ; We assume Z is in focus height and laser head is focus at bottom left of image! | |
216 | G92 X0 Y0 E0; set zero position - new origin | |
217 | G90 ; absolute positioning | |
218 | M82 ; Set extruder (laser) to absolute positioning | |
219 | M201 X1000 Y1000 E500 ; Set acceleration | |
220 | M203 X1000 Y1000 Z4 E10 ; Set max feedrate | |
221 | M209 S0 ; disable firmware retraction, we dont want to burn holes... | |
222 | M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod | |
223 | M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! | |
224 | G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED | |
225 | G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED | |
226 | ||
227 | """ % (filename, TRAVEL_SPEED, ENGRAVE_SPEED) ) | |
228 | ||
229 | G = "0" | |
230 | LASER_STATE = 0 | |
231 | last_coord = [0.0,0.0] | |
232 | last_cmd = "" | |
233 | ||
234 | for line in fi.readlines(): | |
235 | for action in line.split(";"): | |
236 | action = action.strip() | |
237 | if action != "": | |
238 | cmd = action[:2] | |
239 | if cmd == "PD": | |
240 | LASER_STATE = 1 | |
241 | elif cmd == "PU": | |
242 | LASER_STATE = 0 | |
243 | if last_cmd == "PD": | |
244 | OFFSET_X = coord[0] * -1 | |
245 | OFFSET_Y = coord[1] * -1 | |
246 | fo.write("; PD PU detected, set coord offset %.4f x %.4f mm\n" % (OFFSET_X, OFFSET_Y)) | |
247 | elif cmd == "PA" or cmd == "PR": | |
248 | # TODO: convert relative coordinates to absolute here! | |
249 | coord = action[2:].split(",") | |
250 | coord[0] = (float(coord[0]) + OFFSET_X) * SCALE_FACTOR | |
251 | coord[1] = (float(coord[1]) + OFFSET_Y) * SCALE_FACTOR | |
252 | if LASER_STATE: | |
253 | EN = " E%.4f F%.4f" % ( | |
254 | E_FACTOR * math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]), | |
19 | 255 | ENGRAVE_SPEED * 0.5 ) # 1/2 engraving speed |
16 | 256 | else: |
257 | EN = " F%.4f" % TRAVEL_SPEED | |
258 | ||
259 | fo.write("G%d X%.4f Y%.4f%s\n" % ( | |
260 | LASER_STATE, coord[0], coord[1], EN) ) | |
261 | last_coord = coord | |
262 | elif cmd == "IN": | |
263 | pass | |
264 | elif cmd == "PT": | |
265 | print "Ignoring pen thickness" | |
266 | else: | |
267 | print "UNKNOWN: %s" % action | |
268 | last_cmd = cmd | |
269 | ||
270 | ||
19 | 271 | fo.write("""M400 ; Wait for all moves to finish |
272 | M571 S0 E0 | |
273 | M42 P28 S0 ; Force laser off! | |
274 | M501 ; undo all settings made | |
275 | """) | |
16 | 276 | |
277 | fi.close() | |
278 | fo.close() | |
279 | ||
280 | if self.pronterwindow: | |
281 | self.log("") | |
282 | self.pronterwindow.load_gcode_async(filename + '.g') | |
283 | ||
284 | ||
19 | 285 | def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2): |
16 | 286 | self.log("Generating paths from SVG...") |
287 | ||
18 | 288 | preamble = """ |
289 | ; Filename: %s | |
290 | ; GCode generated by bitplotter one-night-quick-hack script (marlin code flavour) | |
291 | ; 2015/2016 by NeoSoft - Malte Bayer | |
292 | ||
293 | G21 ; Metric | |
294 | ; We assume Z is in focus height and laser head is focus at bottom left of image! | |
295 | G92 X0 Y0 E0; set zero position - new origin | |
296 | G90 ; absolute positioning | |
297 | M82 ; Set extruder (laser) to absolute positioning | |
298 | M201 X1000 Y1000 E500 ; Set acceleration | |
299 | M203 X1000 Y1000 Z4 E10 ; Set max feedrate | |
300 | M209 S0 ; disable firmware retraction, we dont want to burn holes... | |
301 | M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod | |
302 | M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! | |
303 | G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED | |
304 | G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED | |
305 | ||
306 | """ % (filename, TRAVEL_SPEED, ENGRAVE_SPEED) | |
307 | self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( | |
308 | TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) ) | |
309 | ||
19 | 310 | postamble = """M400 ; Wait for all moves to finish |
311 | M571 S0 E0 | |
312 | M42 P28 S0 ; Force laser off! | |
313 | M501 ; undo all settings made | |
314 | """ | |
16 | 315 | shape_preamble = "G92 E0\n" |
19 | 316 | shape_postamble = "" |
18 | 317 | |
16 | 318 | |
319 | """ | |
320 | Used to control the smoothness/sharpness of the curves. | |
321 | Smaller the value greater the sharpness. Make sure the | |
322 | value is greater than 0.1 | |
323 | """ | |
324 | if smoothness < 0.1: smoothness = 0.1 | |
325 | ||
326 | svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) | |
327 | ||
328 | tree = ET.parse(filename) | |
329 | root = tree.getroot() | |
330 | ||
331 | width = root.get('width') | |
332 | height = root.get('height') | |
333 | if width == None or height == None: | |
334 | viewbox = root.get('viewBox') | |
335 | if viewbox: | |
336 | _, _, width, height = viewbox.split() | |
337 | ||
338 | if width == None or height == None: | |
339 | self.log("Unable to get width and height for the svg!") | |
340 | return False | |
341 | ||
342 | width = float(width.replace("px", "")) | |
343 | height = float(height.replace("px", "")) | |
344 | ||
345 | scale_x = bed_max_x / max(width, height) | |
346 | scale_y = bed_max_y / max(width, height) | |
347 | ||
348 | self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y)) | |
349 | ||
350 | fo = open(filename + ".g", "w") | |
351 | fo.write(preamble) | |
352 | ||
353 | for elem in root.iter(): | |
354 | try: | |
355 | _, tag_suffix = elem.tag.split('}') | |
356 | except ValueError: | |
357 | continue | |
358 | ||
359 | if tag_suffix in svg_shapes: | |
360 | shape_class = getattr(shapes_pkg, tag_suffix) | |
361 | shape_obj = shape_class(elem) | |
362 | d = shape_obj.d_path() | |
363 | m = shape_obj.transformation_matrix() | |
364 | ||
365 | if d: | |
19 | 366 | fo.write("M400 ; wait for moves finish, then printing shape: %s\n" % (tag_suffix)) |
16 | 367 | E = 0 |
368 | xo = 0 | |
369 | yo = 0 | |
370 | fo.write(shape_preamble) | |
371 | p = point_generator(d, m, smoothness) | |
372 | start = True | |
373 | for x,y,pen in p: | |
374 | y = height - y | |
375 | xs = scale_x * x | |
376 | ys = scale_y * y | |
19 | 377 | if xo == xs and yo == ys: continue |
378 | ||
16 | 379 | if not pen: start = True |
380 | if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y: | |
381 | if start: | |
19 | 382 | fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, TRAVEL_SPEED)) |
16 | 383 | start = False |
384 | xo = xs | |
385 | yo = ys | |
386 | else: | |
387 | e_distance = math.hypot(xs - xo, ys - yo) | |
388 | xo = xs | |
389 | yo = ys | |
19 | 390 | E = E + (e_distance) |
391 | fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, ENGRAVE_SPEED)) | |
16 | 392 | else: |
393 | self.log("Position outside print dimension: %d, %d" % (xs, ys)) | |
394 | fo.write(shape_postamble) | |
395 | ||
396 | fo.write(postamble) | |
397 | fo.close() | |
398 | ||
399 | if self.pronterwindow: | |
400 | self.log("") | |
401 | self.pronterwindow.load_gcode_async(filename + '.g') | |
402 |