|
@@ -1,25 +1,46 @@
|
1
|
1
|
title: Laser Engraver
|
2
|
|
-description: Marlin based CNC laser engraver with 3D printed parts
|
3
|
|
-x-parent: projects
|
|
2
|
+description: Marlin based CNC laser engraver / cutter with 3D printed parts
|
|
3
|
+parent: projects
|
4
|
4
|
git: https://git.xythobuz.de/thomas/marlin/src/branch/laser-engraver
|
5
|
|
-x-date: 2022-11-11
|
|
5
|
+date: 2022-11-25
|
6
|
6
|
comments: true
|
7
|
7
|
---
|
8
|
8
|
|
9
|
9
|
After dabbling in 3D printing for a couple of years now, I had to level-up my Makers toolkit.
|
10
|
10
|
The next logical step, in my opinion, is laser engraving / laser cutting.
|
11
|
|
-I have to admit, I was initially a bit scared, and still have some respect for the potential hazards posed by the strong and focussed laser beam.
|
12
|
|
-So I decided to go slow and start with a low powered diode laser.
|
|
11
|
+I have to admit I was initially a bit scared and still have some respect for the potential hazards posed by the strong, focussed laser beam.
|
|
12
|
+So I decided to go slow and start with a low powered 2.5W diode laser.
|
13
|
13
|
|
14
|
14
|
⚠️ Be sure to always use proper laser safety goggles when working with such machines! ⚠️
|
15
|
15
|
|
16
|
16
|
<!--%
|
17
|
17
|
lightgallery([
|
18
|
18
|
[ "img/laser_tower_1.jpg", "Frontal view in Ikea Lack tower" ],
|
|
19
|
+ [ "img/laser_tree_test.mp4", "video/mp4", "", "", "Cutting a christmas tree decoration" ],
|
|
20
|
+ [ "img/laser_side_1.jpg", "View from left side" ],
|
|
21
|
+ [ "img/laser_side_2.jpg", "View from right side" ],
|
19
|
22
|
])
|
20
|
23
|
%-->
|
21
|
24
|
|
|
25
|
+### Table Of Contents
|
|
26
|
+
|
|
27
|
+* [Hardware](laser-engraver.html#hardware)
|
|
28
|
+ * [Mechanics](laser-engraver.html#mechanics)
|
|
29
|
+ * [Electronics](laser-engraver.html#electronics)
|
|
30
|
+* [Software](laser-engraver.html#software)
|
|
31
|
+ * [MCU Firmware](laser-engraver.html#mcu_firmware)
|
|
32
|
+ * [Host Software](laser-engraver.html#host_software)
|
|
33
|
+ * [LaserGRBL](laser-engraver.html#lasergrbl)
|
|
34
|
+ * [Inkscape](laser-engraver.html#inkscape)
|
|
35
|
+ * [FreeCAD](laser-engraver.html#freecad)
|
|
36
|
+ * [Working with G-Code](laser-engraver.html#g_code)
|
|
37
|
+* [Cutting Tests](laser-engraver.html#cutting_tests)
|
|
38
|
+* [Future Improvements](laser-engraver.html#future_improvements)
|
|
39
|
+* [Cutting Parameters](laser-engraver.html#cutting_parameters)
|
|
40
|
+* [More Pictures](laser-engraver.html#more_pictures)
|
|
41
|
+
|
22
|
42
|
## Hardware
|
|
43
|
+<a class="anchor" name="hardware"></a>
|
23
|
44
|
|
24
|
45
|
I know I say this in a lot of articles here, probably in an attempt to justify my hoarding of electronic parts. 😳
|
25
|
46
|
But this was even better than most of my previous projects, in the sense that I didn't have to buy any parts at all, except for the 2500mW 450nm laser diode, which I got used from my colleague [Philipp](https://www.phschoen.de/) for just 13€.
|
|
@@ -28,6 +49,7 @@ The steppers, mainboard, display, fans and cables came from my now disassembled
|
28
|
49
|
Everything else came out of my parts bin.
|
29
|
50
|
|
30
|
51
|
### Mechanics
|
|
52
|
+<a class="anchor" name="mechanics"></a>
|
31
|
53
|
|
32
|
54
|
The mechanism is based on the ["Cantilever Laser Engraver" by Meatball](https://www.printables.com/model/213526-cantilever-laser-engraver).
|
33
|
55
|
This in turn is based on the ["Cantilever Laser Engraver" by GeoDave](https://www.thingiverse.com/thing:4605853).
|
|
@@ -40,6 +62,8 @@ Otherwise the length of the axes can be customized freely.
|
40
|
62
|
<!--%
|
41
|
63
|
lightgallery([
|
42
|
64
|
[ "img/laser_frame.jpg", "Frame of laser engraver, made of 2020 rails" ],
|
|
65
|
+ [ "img/laser_y_gantry.jpg", "Close view of Y gantry mechanism" ],
|
|
66
|
+ [ "img/laser_x_support.jpg", "Close view of X axis support wheel" ],
|
43
|
67
|
])
|
44
|
68
|
%-->
|
45
|
69
|
|
|
@@ -56,8 +80,18 @@ I also designed custom endstop mounts using M2.5 heat-melt inserts, as well as a
|
56
|
80
|
The OpenSCAD and STL files for these parts [can be found on my Printables profile](https://www.printables.com/model/314945-cantilever-laser-engraver-fixes).
|
57
|
81
|
|
58
|
82
|
To fit the whole machine comfortably inside my [Ikea Lack tower](ikea-lack.html) I mounted it on a 18mm mutliplex base plate with 540x440mm.
|
|
83
|
+The 3D printed feet of the Y-axis already have holes for countersunk screws, so I simply used wood screws there.
|
|
84
|
+To mount the mainboard PCB and laser heatsink I made 3.2mm holes for M3 screws and M3 stand-offs, drilling with a 6mm drill from the bottom to approximately half the depth of the base plate, so even cylindrical screw heads don't stand out from below the plate.
|
|
85
|
+I also added a power switch with a simple 3D printed mount.
|
|
86
|
+
|
|
87
|
+<!--%
|
|
88
|
+lightgallery([
|
|
89
|
+ [ "img/laser_power_switch.jpg", "Power switch with mount" ],
|
|
90
|
+])
|
|
91
|
+%-->
|
59
|
92
|
|
60
|
93
|
### Electronics
|
|
94
|
+<a class="anchor" name="electronics"></a>
|
61
|
95
|
|
62
|
96
|
As mentioned above I used the electronics, namely mainboard, LCD and fans, from my old 3D printer.
|
63
|
97
|
|
|
@@ -95,9 +129,25 @@ And the ground connection is also the one that's switched, when connecting the l
|
95
|
129
|
So even when the MOSFET was turned off the laser still had a ground connection and was powered through the ground pin of the TTL connector, so it could never fully turn off.
|
96
|
130
|
So if you do the same, make sure to only connect one of the two ground pins!
|
97
|
131
|
|
|
132
|
+The power supply is simply reused from my [Fabrikator Mini](fabrikator-mini.html), it already was placed in the Ikea Lack tower.
|
|
133
|
+To save some space I now mounted it to the top of the next Lack table above the laser, with some strips of perforated metal sheet.
|
|
134
|
+Of course this is kind of problematic, as the rest of the machine is kind of designed to be easily transportable.
|
|
135
|
+I will have to find some kind of small alternative power supply.
|
|
136
|
+
|
|
137
|
+<!--%
|
|
138
|
+lightgallery([
|
|
139
|
+ [ "img/laser_psu.jpg", "12V power supply on underside of Ikea Lack" ],
|
|
140
|
+])
|
|
141
|
+%-->
|
|
142
|
+
|
98
|
143
|
## Software
|
|
144
|
+<a class="anchor" name="software"></a>
|
|
145
|
+
|
|
146
|
+There's a surprisingly large amount of software involved in this project.
|
|
147
|
+All just to move a lamp around a bit 💡🤔
|
99
|
148
|
|
100
|
149
|
### MCU Firmware
|
|
150
|
+<a class="anchor" name="mcu_firmware"></a>
|
101
|
151
|
|
102
|
152
|
Most DIY laser engravers or CNC machines seem to use the [GRBL firmware](https://github.com/gnea/grbl).
|
103
|
153
|
Unfortunately this firmware only runs on Atmega328p MCUs, so I can not use it with the Atmega2560 on the GT2560 mainboard.
|
|
@@ -116,9 +166,14 @@ I also did some small changes to show the current laser power on the LCD status
|
116
|
166
|
These changes can be seen [in this commit](https://git.xythobuz.de/thomas/marlin/commit/58dbdff1d5b6e365bfd5ae4eeb7b42967688c51e).
|
117
|
167
|
I also opened a [pull request](https://github.com/MarlinFirmware/Marlin/pull/25003) to hopefully upstream them.
|
118
|
168
|
|
|
169
|
+Later I learned that Marlin does not allow setting home offsets further than 20mm away from endstops.
|
|
170
|
+So I also [added a custom menu command](https://git.xythobuz.de/thomas/marlin/commit/74cafcd77625003623d3a8a6378a4ff0ac6eda72) that simply executes [`G92`](https://marlinfw.org/docs/gcode/G092.html) `X0 Y0` to set the current position to zero.
|
|
171
|
+Using this I can generate G-Code where the object to cut starts at coordinates `(0, 0)` while being able to position it arbitrarily on the machine itself.
|
|
172
|
+
|
119
|
173
|
My current Marlin configuration for the laser engraver can be found [on my Gitea instance](https://git.xythobuz.de/thomas/marlin/src/branch/laser-engraver).
|
120
|
174
|
|
121
|
175
|
### Host Software
|
|
176
|
+<a class="anchor" name="host_software"></a>
|
122
|
177
|
|
123
|
178
|
Besides the microcontroller firmware, we also need some host software to prepare the G-Code from whatever kind of input file we start out with.
|
124
|
179
|
There are two basic approaches for generating paths for the laser engraver.
|
|
@@ -137,6 +192,7 @@ But it is commercial paid software, not under any free software license, so [it
|
137
|
192
|
Below I will try to document all different free software packages I tried for laser engraving.
|
138
|
193
|
|
139
|
194
|
#### LaserGRBL
|
|
195
|
+<a class="anchor" name="lasergrbl"></a>
|
140
|
196
|
|
141
|
197
|
The first solution I found out about is [LaserGRBL](https://lasergrbl.com/).
|
142
|
198
|
It is a Windows-only program, but at least it is open-source / free software.
|
|
@@ -154,7 +210,7 @@ G1 X23 S150
|
154
|
210
|
</pre>
|
155
|
211
|
|
156
|
212
|
Without further modifications, this only moves the toolhead, while keeping the laser off at all times, because Marlin by default does not process the power-level parts of the G-Code lines.
|
157
|
|
-To get this to work, the "continuous inline power mode" has to be enabled by putting `M3 I` in the G-Code header setting of LaserGRBL.
|
|
213
|
+To get this to work, the "continuous inline power mode" has to be enabled by putting `M3 I` in the G-Code header setting of LaserGRBL, as well as at the end of the "Multiple Passes" G-Code section.
|
158
|
214
|
|
159
|
215
|
<!--%
|
160
|
216
|
lightgallery([
|
|
@@ -197,6 +253,25 @@ For raster input, LaserGRBL only puts the speed in a single G0 move at the begin
|
197
|
253
|
So the speed does not apply to the G1 moves afterwards.
|
198
|
254
|
This case is not handled yet.
|
199
|
255
|
|
|
256
|
+In another vector job, I saw LaserGRBL generate output like the following.
|
|
257
|
+
|
|
258
|
+<pre class="sh_gcode">
|
|
259
|
+G0X3Y2
|
|
260
|
+S255
|
|
261
|
+G1Y5F200
|
|
262
|
+X0.7
|
|
263
|
+Y8.347
|
|
264
|
+X8.25
|
|
265
|
+X8.75Y8.847
|
|
266
|
+Y12.347
|
|
267
|
+</pre>
|
|
268
|
+
|
|
269
|
+This is the very compact GRBL G-Code dialect, which leaves out spaces and also the modes, as long as they don't change.
|
|
270
|
+So to handle that, I remember the last G0/G1 mode, prepending it as needed to lines, while also adding the missing spaces for better human readable output.
|
|
271
|
+
|
|
272
|
+As you can tell there are many different output formats from LaserGRBL and I'm not able to tell when it will generate what.
|
|
273
|
+But my script below should work for all variants I have seen up to now.
|
|
274
|
+
|
200
|
275
|
<pre class="sh_python">
|
201
|
276
|
#!/usr/bin/env python
|
202
|
277
|
|
|
@@ -212,6 +287,17 @@ out_file = sys.argv[2]
|
212
|
287
|
|
213
|
288
|
power = ""
|
214
|
289
|
speed = ""
|
|
290
|
+mode_g0 = True
|
|
291
|
+
|
|
292
|
+def add_spaces(s):
|
|
293
|
+ prev = ' '
|
|
294
|
+ r = ""
|
|
295
|
+ for c in s:
|
|
296
|
+ if str(prev).isdigit() and str(c).isalpha():
|
|
297
|
+ r = r + " "
|
|
298
|
+ r = r + c
|
|
299
|
+ prev = c
|
|
300
|
+ return r
|
215
|
301
|
|
216
|
302
|
with open(in_file, 'r') as fi, open(out_file, 'w') as fo:
|
217
|
303
|
for line in fi:
|
|
@@ -220,14 +306,26 @@ with open(in_file, 'r') as fi, open(out_file, 'w') as fo:
|
220
|
306
|
elif line.startswith("F"):
|
221
|
307
|
speed = " " + line.rstrip()
|
222
|
308
|
else:
|
223
|
|
- s = line.rstrip()
|
|
309
|
+ s = add_spaces(line.rstrip())
|
224
|
310
|
if line.startswith("G1"):
|
|
311
|
+ mode_g0 = False
|
225
|
312
|
s += power
|
226
|
313
|
s += speed
|
227
|
314
|
elif line.startswith("G0"):
|
|
315
|
+ mode_g0 = True
|
228
|
316
|
s += power
|
229
|
317
|
if (power != " S0") and (power != ""):
|
230
|
318
|
print("Warning: G0 move with power not zero!" + power)
|
|
319
|
+ elif line.startswith("X") or line.startswith("Y"):
|
|
320
|
+ if mode_g0:
|
|
321
|
+ s = "G0 " + s
|
|
322
|
+ s += power
|
|
323
|
+ if (power != " S0") and (power != ""):
|
|
324
|
+ print("Warning: G0 move with power not zero!" + power)
|
|
325
|
+ else:
|
|
326
|
+ s = "G1 " + s
|
|
327
|
+ s += power
|
|
328
|
+ s += speed
|
231
|
329
|
s += "\n"
|
232
|
330
|
fo.write(s)
|
233
|
331
|
</pre>
|
|
@@ -245,37 +343,41 @@ For these tests I have simply used LaserGRBL to produce a G-Code file, which I h
|
245
|
343
|
LaserGRBL also can directly connect to the machine via the serial port.
|
246
|
344
|
This has worked for me for a bit, but even though you can set the G-Code flavor to Marlin in the LaserGRBL settings, it does not really work properly.
|
247
|
345
|
You can jog the toolhead but homing is not possible.
|
248
|
|
-I also had to reduce the polling frequency in the LaserGRBL settings to 'Quiet' and disable Soft-Reset to get it to work inside the VirtualBox VM I use to run the program.
|
|
346
|
+I also had to reduce the polling frequency in the LaserGRBL settings to 'Quiet' and disable Soft-Reset to get it to work inside the VirtualBox VM I used at first to run the program.
|
249
|
347
|
|
250
|
|
-Apparently it is also possible to [run LaserGRBL in Wine](https://github.com/arkypita/LaserGRBL/issues/5#issuecomment-873564055) / [with PlayOnLinux](https://github.com/arkypita/LaserGRBL/raw/master/POL_LaserGRBL_setup.sh), but on my Arch system I was not able to get either of these running, due to problems with the required Winetricks.
|
|
348
|
+It is also possible to [run LaserGRBL in Wine](https://github.com/arkypita/LaserGRBL/issues/5#issuecomment-873564055) / [with PlayOnLinux](https://github.com/arkypita/LaserGRBL/raw/master/POL_LaserGRBL_setup.sh).
|
|
349
|
+At first I was not able to run this PlayOnLinux script due to Winetricks downloads failing with invalid checksums.
|
|
350
|
+But after trying a couple of days later, with the exact same script, the installation worked fine.
|
|
351
|
+I had to make the same changes to the settings as described for VirtualBox above.
|
251
|
352
|
|
252
|
|
-Also, for all the above tests with LaserGRBL I have imported raster bitmap images and either rastered them, or vectorized them within LaserGRBLs UI.
|
|
353
|
+Also for all the above tests with LaserGRBL I have imported raster bitmap images and either rastered them, or vectorized them within LaserGRBLs UI.
|
253
|
354
|
With bitmaps it is possible to position the resulting G-Code paths with an offset from the zero position of the machine.
|
254
|
355
|
This is not possible when using LaserGRBL to import SVG vector paths.
|
255
|
356
|
With them, the output will always start at coordinates `(0, 0)`.
|
256
|
|
-You will then have to set the proper offset on the machine itself.
|
|
357
|
+You will then have to set the proper offset on the machine itself, as described in the Marlin section above.
|
257
|
358
|
|
258
|
359
|
#### Inkscape
|
|
360
|
+<a class="anchor" name="inkscape"></a>
|
259
|
361
|
|
260
|
362
|
Inkscape includes the [G-Code Tools Plugin from the russian-language CNC-Club forums](https://www.cnc-club.ru/forum/viewtopic.php?t=35).
|
261
|
363
|
Unfortunately I was not able to find much up-to-date english-language documentation for this.
|
262
|
364
|
It is also relatively complicated and unintuitive.
|
263
|
365
|
|
264
|
|
-TODO
|
265
|
|
-
|
266
|
366
|
I was able to get it to generate G-Code from a path, but not with any laser power instructions yet.
|
|
367
|
+This section will be updated if I have more success in the future.
|
|
368
|
+Until then I'm just using Inkscape to export svg files for LaserGRBL.
|
267
|
369
|
|
268
|
370
|
#### FreeCAD
|
|
371
|
+<a class="anchor" name="freecad"></a>
|
269
|
372
|
|
270
|
373
|
FreeCAD has the [Path Workbench](https://wiki.freecadweb.org/Path_Workbench), which can be used to create G-Code instructions for all kinds of CNC machines.
|
271
|
374
|
It is not really complete and fool-proof yet, unfortunately.
|
272
|
375
|
And it is also not designed for pure 2D machines, like laser engravers, by default.
|
273
|
376
|
|
274
|
|
-TODO
|
275
|
|
-
|
276
|
377
|
I have not yet tested that.
|
277
|
378
|
|
278
|
379
|
#### Working with G-Code
|
|
380
|
+<a class="anchor" name="g_code"></a>
|
279
|
381
|
|
280
|
382
|
One nice feature I saw in LaserGRBL, but was not able to use with Marlin, is the ability to draw the outline of the object to be cut, for positioning of the stock material.
|
281
|
383
|
So I quickly made up a small Python script that analyzes a G-Code file, taking the coordinates from all G1 cutting moves and generating their bounding box, which is then drawn on the lowest possible power setting multiple times.
|
|
@@ -528,13 +630,491 @@ with open(filename, 'w') as f:
|
528
|
630
|
write("G0 X0 Y0 F" + str(speed_g0))
|
529
|
631
|
</pre>
|
530
|
632
|
|
|
633
|
+I like the idea but unfortunately it has some problems in practice.
|
|
634
|
+When changing the focus level of my laser diode, it's very easy to also change the offset in the X and Y axes.
|
|
635
|
+So the grid is not properly aligned for long.
|
|
636
|
+But it still looks nice.
|
|
637
|
+
|
|
638
|
+<!--%
|
|
639
|
+lightgallery([
|
|
640
|
+ [ "img/laser_grid.jpg", "Closer view of grid on base plate" ],
|
|
641
|
+])
|
|
642
|
+%-->
|
|
643
|
+
|
|
644
|
+If you look closely, you can see the first run I did with a power setting of 50 / 255.
|
|
645
|
+This was not strong enough, the lines are only faintly visible.
|
|
646
|
+So I did another run with 100 / 255 over it.
|
|
647
|
+This was enough power for nice visible lines, but the laser offset unfortunately shifted slightly between the runs.
|
|
648
|
+
|
|
649
|
+As I grew tired of removing, flashing and re-inserting of the SD card, I decided to also write a quick little G-Code sender for the serial port.
|
|
650
|
+
|
|
651
|
+<pre class="sh_python">
|
|
652
|
+#!/usr/bin/env python
|
|
653
|
+
|
|
654
|
+import sys
|
|
655
|
+import serial
|
|
656
|
+
|
|
657
|
+if len(sys.argv) < 3:
|
|
658
|
+ print("Usage:")
|
|
659
|
+ print(" " + sys.argv[0] + " /dev/serial/port input.gcode")
|
|
660
|
+ sys.exit(1)
|
|
661
|
+
|
|
662
|
+port_file = sys.argv[1]
|
|
663
|
+in_file = sys.argv[2]
|
|
664
|
+
|
|
665
|
+port = serial.Serial()
|
|
666
|
+port.port = port_file
|
|
667
|
+port.baudrate = 115200
|
|
668
|
+#port.timeout = 0.0
|
|
669
|
+
|
|
670
|
+max_cmd_buffer = 5
|
|
671
|
+counter = 0
|
|
672
|
+
|
|
673
|
+def connect_serial():
|
|
674
|
+ try:
|
|
675
|
+ port.open()
|
|
676
|
+ if port.is_open:
|
|
677
|
+ print("connected to: " + port_file)
|
|
678
|
+ else:
|
|
679
|
+ print("error connecting to " + port_file)
|
|
680
|
+ sys.exit(1)
|
|
681
|
+ except serial.serialutil.SerialException:
|
|
682
|
+ print("error connecting to " + port_file)
|
|
683
|
+ sys.exit(1)
|
|
684
|
+
|
|
685
|
+def wait_for_text(s):
|
|
686
|
+ while True:
|
|
687
|
+ response = port.read_until().decode().strip()
|
|
688
|
+ print("rx w: " + response)
|
|
689
|
+ if response.startswith(s):
|
|
690
|
+ break
|
|
691
|
+ elif response.startswith("Error"):
|
|
692
|
+ print(response)
|
|
693
|
+ print("CNC returned error. aborting.")
|
|
694
|
+ abort_print(False)
|
|
695
|
+ port.close()
|
|
696
|
+ sys.exit(1)
|
|
697
|
+
|
|
698
|
+def abort_print(wait_for_oks = True):
|
|
699
|
+ global counter
|
|
700
|
+
|
|
701
|
+ print("tx: M5")
|
|
702
|
+ port.write("M5\n".encode())
|
|
703
|
+ counter += 1
|
|
704
|
+
|
|
705
|
+ print("tx: G0 X0 Y0 F1000")
|
|
706
|
+ port.write("G0 X0 Y0 F1000\n".encode())
|
|
707
|
+ counter += 1
|
|
708
|
+
|
|
709
|
+ if wait_for_oks:
|
|
710
|
+ while counter > 0:
|
|
711
|
+ wait_for_text("ok")
|
|
712
|
+ counter -= 1
|
|
713
|
+
|
|
714
|
+def send_file(f):
|
|
715
|
+ global counter
|
|
716
|
+
|
|
717
|
+ for line in f:
|
|
718
|
+ if ";" in line:
|
|
719
|
+ line = line.split(";")[0]
|
|
720
|
+
|
|
721
|
+ line = line.strip()
|
|
722
|
+
|
|
723
|
+ if len(line) <= 0:
|
|
724
|
+ print("skipping empty line")
|
|
725
|
+ continue
|
|
726
|
+
|
|
727
|
+ print("tx: " + line)
|
|
728
|
+ tx = line.encode() + b'\n'
|
|
729
|
+ port.write(tx)
|
|
730
|
+
|
|
731
|
+ counter += 1
|
|
732
|
+ print("cnt=" + str(counter))
|
|
733
|
+
|
|
734
|
+ while counter >= max_cmd_buffer:
|
|
735
|
+ response = port.read_until().decode().strip()
|
|
736
|
+ #print("rx: " + response)
|
|
737
|
+ if response.startswith("ok"):
|
|
738
|
+ counter -= 1
|
|
739
|
+ print("cnt=" + str(counter))
|
|
740
|
+ elif response.startswith("Error"):
|
|
741
|
+ print(response)
|
|
742
|
+ print("CNC returned error. aborting.")
|
|
743
|
+ abort_print(False)
|
|
744
|
+ port.close()
|
|
745
|
+ sys.exit(1)
|
|
746
|
+
|
|
747
|
+def main():
|
|
748
|
+ connect_serial()
|
|
749
|
+
|
|
750
|
+ print("waiting for CNC to reset")
|
|
751
|
+ wait_for_text("start")
|
|
752
|
+
|
|
753
|
+ print("auto-homing after reset")
|
|
754
|
+ print("tx: G28")
|
|
755
|
+ port.write("G28\n".encode())
|
|
756
|
+ wait_for_text("ok")
|
|
757
|
+
|
|
758
|
+ try:
|
|
759
|
+ with open(in_file, 'r') as f:
|
|
760
|
+ send_file(f)
|
|
761
|
+
|
|
762
|
+ while counter > 0:
|
|
763
|
+ wait_for_text("ok")
|
|
764
|
+ counter -= 1
|
|
765
|
+ except KeyboardInterrupt:
|
|
766
|
+ print("user interrupt. aborting.")
|
|
767
|
+ abort_print(True)
|
|
768
|
+
|
|
769
|
+ port.close()
|
|
770
|
+
|
|
771
|
+if __name__ == '__main__':
|
|
772
|
+ main()
|
|
773
|
+</pre>
|
|
774
|
+
|
|
775
|
+Of course it's important to use the proper speed, power and number of iterations for each object to cut.
|
|
776
|
+Not enough power or iterations, or too high of a speed, and the object will still be part of the stock material.
|
|
777
|
+Too much power or iterations and not only the object will be cut, but also the base plate below.
|
|
778
|
+
|
|
779
|
+So I first did some tests manually, which is relatively time consuming.
|
|
780
|
+I then made another Python script go generate G-Code for a cutting test, varying speed and number of iterations.
|
|
781
|
+
|
|
782
|
+<pre class="sh_python">
|
|
783
|
+#!/usr/bin/env python
|
|
784
|
+
|
|
785
|
+# general settings
|
|
786
|
+filename = "cut_test.gcode"
|
|
787
|
+pwr = 255
|
|
788
|
+pwr_num = 150
|
|
789
|
+speed_g0 = 3000
|
|
790
|
+speed_g1_num = 500
|
|
791
|
+
|
|
792
|
+# settings of single test shape
|
|
793
|
+width = 10
|
|
794
|
+height = 10
|
|
795
|
+dist = 2
|
|
796
|
+
|
|
797
|
+# range for tests
|
|
798
|
+iterations = [ 5, 10, 15, 20 ]
|
|
799
|
+speeds_g1 = [ 1000, 500, 200 ]
|
|
800
|
+
|
|
801
|
+def drawSquare(w, h, x, y, speed_g1):
|
|
802
|
+ write("G0 X" + str(x) + " Y" + str(y) + " S0 F" + str(speed_g0))
|
|
803
|
+ square = [
|
|
804
|
+ ( x + w, y ),
|
|
805
|
+ ( x + w, y + h ),
|
|
806
|
+ ( x, y + h ),
|
|
807
|
+ ( x, y ),
|
|
808
|
+ ]
|
|
809
|
+ for xp, yp in square:
|
|
810
|
+ write("G1 X" + str(xp) + " Y" + str(yp) + " S" + str(pwr) + " F" + str(speed_g1))
|
|
811
|
+
|
|
812
|
+def drawHeart(w, h, x, y, speed_g1):
|
|
813
|
+ heart = [
|
|
814
|
+ ( 0.0, 5.0 / 8.0 ),
|
|
815
|
+ ( 0.0, 6.0 / 8.0 ),
|
|
816
|
+ ( 0.1, 7.0 / 8.0 ),
|
|
817
|
+ ( 0.2, 8.0 / 8.0 ),
|
|
818
|
+ ( 0.3, 8.0 / 8.0 ),
|
|
819
|
+ ( 0.4, 7.0 / 8.0 ),
|
|
820
|
+ ( 0.5, 6.0 / 8.0 ),
|
|
821
|
+ ( 0.6, 7.0 / 8.0 ),
|
|
822
|
+ ( 0.7, 8.0 / 8.0 ),
|
|
823
|
+ ( 0.8, 8.0 / 8.0 ),
|
|
824
|
+ ( 0.9, 7.0 / 8.0 ),
|
|
825
|
+ ( 1.0, 6.0 / 8.0 ),
|
|
826
|
+ ( 1.0, 5.0 / 8.0 ),
|
|
827
|
+ ( 0.9, 4.0 / 8.0 ),
|
|
828
|
+ ( 0.8, 3.0 / 8.0 ),
|
|
829
|
+ ( 0.7, 2.0 / 8.0 ),
|
|
830
|
+ ( 0.6, 1.0 / 8.0 ),
|
|
831
|
+ ( 0.5, 0.0 / 8.0 ),
|
|
832
|
+ ( 0.4, 1.0 / 8.0 ),
|
|
833
|
+ ( 0.3, 2.0 / 8.0 ),
|
|
834
|
+ ( 0.2, 3.0 / 8.0 ),
|
|
835
|
+ ( 0.1, 4.0 / 8.0 ),
|
|
836
|
+ ( 0.0, 5.0 / 8.0 )
|
|
837
|
+ ]
|
|
838
|
+ write("G0 X" + str(x + heart[0][0] * w) + " Y" + str(y + heart[0][1] * h) + " S0 F" + str(speed_g0))
|
|
839
|
+ for xp, yp in heart:
|
|
840
|
+ write("G1 X" + str(x + xp * w) + " Y" + str(y + yp * h) + " S" + str(pwr) + " F" + str(speed_g1))
|
|
841
|
+
|
|
842
|
+def drawShape(w, h, x, y, speed_g1):
|
|
843
|
+ #drawSquare(w, h, x, y, speed_g1)
|
|
844
|
+ drawHeart(w, h, x, y, speed_g1)
|
|
845
|
+
|
|
846
|
+digits = [
|
|
847
|
+ [
|
|
848
|
+ # 0
|
|
849
|
+ (0.0, 0.0),
|
|
850
|
+ (0.0, 1.0),
|
|
851
|
+ (1.0, 1.0),
|
|
852
|
+ (1.0, 0.0),
|
|
853
|
+ (0.0, 0.0)
|
|
854
|
+ ], [
|
|
855
|
+ # 1
|
|
856
|
+ (0.5, 0.0),
|
|
857
|
+ (0.5, 1.0)
|
|
858
|
+ ], [
|
|
859
|
+ # 2
|
|
860
|
+ (0.0, 1.0),
|
|
861
|
+ (1.0, 1.0),
|
|
862
|
+ (1.0, 0.5),
|
|
863
|
+ (0.0, 0.5),
|
|
864
|
+ (0.0, 0.0),
|
|
865
|
+ (1.0, 0.0),
|
|
866
|
+ ], [
|
|
867
|
+ # 3
|
|
868
|
+ (0.0, 1.0),
|
|
869
|
+ (1.0, 1.0),
|
|
870
|
+ (1.0, 0.5),
|
|
871
|
+ (0.0, 0.5),
|
|
872
|
+ (1.0, 0.5),
|
|
873
|
+ (1.0, 0.0),
|
|
874
|
+ (0.0, 0.0),
|
|
875
|
+ ], [
|
|
876
|
+ # 4
|
|
877
|
+ (0.0, 1.0),
|
|
878
|
+ (0.0, 0.5),
|
|
879
|
+ (1.0, 0.5),
|
|
880
|
+ (1.0, 1.0),
|
|
881
|
+ (1.0, 0.0),
|
|
882
|
+ ], [
|
|
883
|
+ # 5
|
|
884
|
+ (1.0, 1.0),
|
|
885
|
+ (0.0, 1.0),
|
|
886
|
+ (0.0, 0.5),
|
|
887
|
+ (1.0, 0.5),
|
|
888
|
+ (1.0, 0.0),
|
|
889
|
+ (0.0, 0.0),
|
|
890
|
+ ], [
|
|
891
|
+ # 6
|
|
892
|
+ (1.0, 1.0),
|
|
893
|
+ (0.0, 1.0),
|
|
894
|
+ (0.0, 0.0),
|
|
895
|
+ (1.0, 0.0),
|
|
896
|
+ (1.0, 0.5),
|
|
897
|
+ (0.0, 0.5),
|
|
898
|
+ ], [
|
|
899
|
+ # 7
|
|
900
|
+ (0.0, 1.0),
|
|
901
|
+ (1.0, 1.0),
|
|
902
|
+ (1.0, 0.0),
|
|
903
|
+ ], [
|
|
904
|
+ # 8
|
|
905
|
+ (1.0, 0.5),
|
|
906
|
+ (1.0, 1.0),
|
|
907
|
+ (0.0, 1.0),
|
|
908
|
+ (0.0, 0.5),
|
|
909
|
+ (1.0, 0.5),
|
|
910
|
+ (1.0, 0.0),
|
|
911
|
+ (0.0, 0.0),
|
|
912
|
+ (0.0, 0.5),
|
|
913
|
+ ], [
|
|
914
|
+ # 9
|
|
915
|
+ (1.0, 0.5),
|
|
916
|
+ (1.0, 1.0),
|
|
917
|
+ (0.0, 1.0),
|
|
918
|
+ (0.0, 0.5),
|
|
919
|
+ (1.0, 0.5),
|
|
920
|
+ (1.0, 0.0),
|
|
921
|
+ (0.0, 0.0),
|
|
922
|
+ ]
|
|
923
|
+]
|
|
924
|
+
|
|
925
|
+font_w = 1.5
|
|
926
|
+font_h = 3.0
|
|
927
|
+font_d = 0.5
|
|
928
|
+
|
|
929
|
+def draw_digit(f, i, size_x, size_y, off_x, off_y):
|
|
930
|
+ dig = digits[i]
|
|
931
|
+ n = 0
|
|
932
|
+ for p in dig:
|
|
933
|
+ x, y = p
|
|
934
|
+ s = ""
|
|
935
|
+
|
|
936
|
+ if n == 0:
|
|
937
|
+ s += "G0 S0 F" + str(speed_g0)
|
|
938
|
+ else:
|
|
939
|
+ s += "G1 S" + str(pwr_num) + " F" + str(speed_g1_num)
|
|
940
|
+
|
|
941
|
+ s += " X" + str(off_x + size_x * x)
|
|
942
|
+ s += " Y" + str(off_y + size_y * y)
|
|
943
|
+
|
|
944
|
+ print(s)
|
|
945
|
+ f.write(s + "\n")
|
|
946
|
+ n += 1
|
|
947
|
+
|
|
948
|
+ return s
|
|
949
|
+
|
|
950
|
+def draw_number(f, n, x, y):
|
|
951
|
+ if n >= 1000:
|
|
952
|
+ draw_digit(f, int(n / 1000), font_w, font_h, x, y)
|
|
953
|
+ draw_digit(f, int((n / 100)) % 10, font_w, font_h, x + font_d + font_w, y)
|
|
954
|
+ draw_digit(f, int((n / 10) % 10), font_w, font_h, x + 2 * (font_d + font_w), y)
|
|
955
|
+ draw_digit(f, int(n % 10), font_w, font_h, x + 3 * (font_d + font_w), y)
|
|
956
|
+ elif n >= 100:
|
|
957
|
+ draw_digit(f, int(n / 100), font_w, font_h, x, y)
|
|
958
|
+ draw_digit(f, int((n / 10) % 10), font_w, font_h, x + font_d + font_w, y)
|
|
959
|
+ draw_digit(f, int(n % 10), font_w, font_h, x + 2 * (font_d + font_w), y)
|
|
960
|
+ elif n >= 10:
|
|
961
|
+ draw_digit(f, int(n / 10), font_w, font_h, x, y)
|
|
962
|
+ draw_digit(f, int(n % 10), font_w, font_h, x + font_d + font_w, y)
|
|
963
|
+ else:
|
|
964
|
+ draw_digit(f, n, font_w, font_h, x, y)
|
|
965
|
+
|
|
966
|
+with open(filename, 'w') as f:
|
|
967
|
+ def write(s):
|
|
968
|
+ print(s)
|
|
969
|
+ f.write(s + "\n")
|
|
970
|
+
|
|
971
|
+ # header
|
|
972
|
+ #write("G28")
|
|
973
|
+ write("G90")
|
|
974
|
+ write("M3 I")
|
|
975
|
+ write("M3 S0")
|
|
976
|
+ write("")
|
|
977
|
+
|
|
978
|
+ # first line is not having the power applied
|
|
979
|
+ # TODO
|
|
980
|
+ write("G0 X0 Y0 S0 F" + str(speed_g0))
|
|
981
|
+ write("G1 X1 Y0 S1 F" + str(speeds_g1[0]))
|
|
982
|
+ write("G0 X0 Y0 S0 F" + str(speed_g0))
|
|
983
|
+ write("G1 X0 Y1 S1 F" + str(speeds_g1[0]))
|
|
984
|
+ write("G0 X0 Y0 S0 F" + str(speed_g0))
|
|
985
|
+
|
|
986
|
+ max_num_str_len = 4
|
|
987
|
+ str_w = (max_num_str_len * font_w) + ((max_num_str_len - 1) * font_d)
|
|
988
|
+
|
|
989
|
+ write("; iterations")
|
|
990
|
+ for i in range(0, len(iterations)):
|
|
991
|
+ write("; " + str(iterations[i]))
|
|
992
|
+ draw_number(f, iterations[i], (width + str_w) / 2.0 + dist + i * (width + dist), 0)
|
|
993
|
+ write("")
|
|
994
|
+
|
|
995
|
+ write("; speeds")
|
|
996
|
+ for i in range(0, len(speeds_g1)):
|
|
997
|
+ write("; " + str(speeds_g1[i]))
|
|
998
|
+ draw_number(f, speeds_g1[i], 0, (height + font_h) / 2.0 + dist + i * (height + dist))
|
|
999
|
+
|
|
1000
|
+ for i in range(0, len(speeds_g1)):
|
|
1001
|
+ for j in range(0, len(iterations)):
|
|
1002
|
+ write("; speed=" + str(speeds_g1[i]) + " n=" + str(iterations[j]))
|
|
1003
|
+ for k in range(0, iterations[j]):
|
|
1004
|
+ drawShape(width, height, str_w + dist + j * (width + dist), font_h + dist + i * (height + dist), speeds_g1[i])
|
|
1005
|
+ write("")
|
|
1006
|
+
|
|
1007
|
+ # footer
|
|
1008
|
+ write("M5")
|
|
1009
|
+ write("G0 X0 Y0 F" + str(speed_g0))
|
|
1010
|
+</pre>
|
|
1011
|
+
|
|
1012
|
+I'm drawing digits again, like in the Grid G-Code generator above.
|
|
1013
|
+It seems I need to create some kind of library or so if I continue this route of generating G-Code.
|
|
1014
|
+
|
|
1015
|
+<!--%
|
|
1016
|
+lightgallery([
|
|
1017
|
+ [ "img/laser_cut_test_1.jpg", "Test sheet being cut" ],
|
|
1018
|
+ [ "img/laser_cut_test_3.jpg", "Finished test run" ],
|
|
1019
|
+])
|
|
1020
|
+%-->
|
|
1021
|
+
|
|
1022
|
+In the pictures above, the 3x3 grid of hearts on the right was made with the laser slightly out of focus.
|
|
1023
|
+It was not able to cut through in this run, even with 30 iterations.
|
|
1024
|
+I also still had the text not aligned properly.
|
|
1025
|
+
|
|
1026
|
+The 3x1 and 3x3 runs on the left side were made after properly focussing the laser and fixing the text alignment.
|
|
1027
|
+Here 200mm/min with 10 iterations was already enough to cut through the 3mm plywood.
|
|
1028
|
+
|
|
1029
|
+## Cutting Tests
|
|
1030
|
+<a class="anchor" name="cutting_tests"></a>
|
|
1031
|
+
|
|
1032
|
+For the first 'real' test I decided to cut the assembly jig for the [Pagoda antenna](https://www.maartenbaert.be/quadcopters/antennas/pagoda-antenna/#design-files) by Maarten Baert.
|
|
1033
|
+The files are available in dxf and svg format.
|
|
1034
|
+I was able to easily import the svg file in LaserGRBL to create G-Code, afterwards converting it to the proper format using my script described above.
|
|
1035
|
+
|
|
1036
|
+<!--%
|
|
1037
|
+lightgallery([
|
|
1038
|
+ [ "img/laser_pagoda_1.jpg", "Front side of the Pagoda jig sheet" ],
|
|
1039
|
+ [ "img/laser_pagoda_2.jpg", "Back side of the Pagoda jig sheet" ],
|
|
1040
|
+ [ "img/laser_pagoda_3.jpg", "Assembled Pagoda antenna jig" ],
|
|
1041
|
+])
|
|
1042
|
+%-->
|
|
1043
|
+
|
|
1044
|
+As in one of my previous tests I used 200mm/min with 10 iterations.
|
|
1045
|
+But this was not quite enough to get the parts out easily.
|
|
1046
|
+In hindsight I should have used at least 15 iterations.
|
|
1047
|
+But the result was still usable, even with only 10 iterations.
|
|
1048
|
+
|
|
1049
|
+Next my SO requested some christmas decoration, so I decided to attempt to cut this [Christmas Tree Ornament](https://www.thingiverse.com/thing:43041) by TomKeddie on Thingiverse.
|
|
1050
|
+It is made for 4.8mm plywood, so I scaled it down by a factor of `3.0 / 4.8 = 0.625`.
|
|
1051
|
+The design comes as a dxf file, so I imported that in Inkscape and then exported an svg file from that.
|
|
1052
|
+This I then imported in LaserGRBL, generated G-Code and converted that using the script above.
|
|
1053
|
+
|
|
1054
|
+<!--%
|
|
1055
|
+lightgallery([
|
|
1056
|
+ [ "img/laser_tree_test.mp4", "video/mp4", "", "", "Cutting a christmas tree decoration" ],
|
|
1057
|
+ [ "img/laser_tree_segments.mp4", "video/mp4", "", "", "Segmented lines while cutting" ],
|
|
1058
|
+])
|
|
1059
|
+%-->
|
|
1060
|
+
|
|
1061
|
+As you can tell from the second video, what should be single continuous lines has instead sometimes been split up into many small path segments, which LaserGRBL is cutting in non-optimal ordering.
|
|
1062
|
+I have not yet found a way to easily clean these paths up.
|
|
1063
|
+
|
|
1064
|
+<!--%
|
|
1065
|
+lightgallery([
|
|
1066
|
+ [ "img/laser_tree_1.jpg", "Front side of christmas tree sheet" ],
|
|
1067
|
+ [ "img/laser_tree_2.jpg", "Back side of christmas tree sheet" ],
|
|
1068
|
+ [ "img/laser_tree_3.jpg", "Finished christmas tree ornament" ],
|
|
1069
|
+])
|
|
1070
|
+%-->
|
|
1071
|
+
|
|
1072
|
+The end-result does not seem to be really affected by this however, although with 200mm/min and 15 iterations the parts were not removable as easily as I hoped.
|
|
1073
|
+So I think I either have to adjust the focus better or go up to 20 iterations for the 3mm birch plywood.
|
|
1074
|
+
|
|
1075
|
+## Future Improvements
|
|
1076
|
+<a class="anchor" name="future_improvements"></a>
|
|
1077
|
+
|
|
1078
|
+I didn't expect it to be this bad, but cutting wood really produces a noticeable amount of smoke and the smell of burnt wood.
|
|
1079
|
+So in the long run I will have to add some kind of air filtration system to my Ikea Lack tower.
|
|
1080
|
+Also I now better understand the need for Air Assist on laser cutters.
|
|
1081
|
+My small fans don't help much in keeping the light-path free of smoke and soot.
|
|
1082
|
+But I will hopefully get away with running it for a while without these additions.
|
|
1083
|
+
|
|
1084
|
+Another obvious improvement is the addition of a Raspberry Pi and a Webcam, to allow remote monitoring of long-running jobs.
|
|
1085
|
+I also came across the [LaserWeb project](https://laserweb.yurl.ch/) which seems to be similar in spirit to OctoPrint for 3D printers, but also with the ability of processing vector and bitmap files from within the web interface.
|
|
1086
|
+This would be a very useful addition and solve a lot of the software workflow problems illustrated above.
|
|
1087
|
+Also theres more than enough room left on my base plate for a SBC.
|
|
1088
|
+
|
|
1089
|
+## Cutting Parameters
|
|
1090
|
+<a class="anchor" name="cutting_parameters"></a>
|
|
1091
|
+
|
|
1092
|
+Here are the results of all cutting tests I've made up to now.
|
|
1093
|
+I will update this table as soon as I gather new data 🧑🔬
|
|
1094
|
+
|
|
1095
|
+<!--%
|
|
1096
|
+tableHelper([ "align-right", "align-right", "align-right", "align-right", "align-right", "align-right", "align-right", "align-left", "align-left" ],
|
|
1097
|
+ [ "Type", "Material", "Thickness<br><span style=\"font-size: small;\">(in mm)</span>", "Power<br><span style=\"font-size: small;\">(in W)</span>", "PWM<br><span style=\"font-size: small;\">(out of 255)</span>", "Speed<br><span style=\"font-size: small;\">(in mm/min)</span>", "Iterations", "Remarks", "Date" ], [
|
|
1098
|
+ [ "<s>Plywood</s>", "<s>Birch</s>", "<s>3</s>", "<s>2.5</s>", "<s>255</s>", "<s>200</s>", "<s>5</s>", "<s>Not possible to remove.</s>", "<s>2022-11-24</s>" ],
|
|
1099
|
+ [ "Plywood", "Birch", "3", "2.5", "255", "200", "10", "Difficult to remove.", "2022-11-24" ],
|
|
1100
|
+ [ "Plywood", "Birch", "3", "2.5", "255", "200", "15", "Easily removable.", "2022-11-24" ],
|
|
1101
|
+ [ "Plywood", "Birch", "3", "2.5", "255", "200", "20", "Freely fell out.", "2022-11-24" ],
|
|
1102
|
+ [ "<s>Plywood</s>", "<s>Birch</s>", "<s>3</s>", "<s>2.5</s>", "<s>255</s>", "<s>500</s>", "<s>15</s>", "<s>Not possible to remove.</s>", "<s>2022-11-24</s>" ],
|
|
1103
|
+ [ "Plywood", "Birch", "3", "2.5", "255", "500", "20", "Difficult to remove.", "2022-11-24" ],
|
|
1104
|
+ [ "Plywood", "Birch", "3", "2.5", "255", "200", "15", "Difficult to remove.", "2022-11-25" ],
|
|
1105
|
+ ]
|
|
1106
|
+)
|
|
1107
|
+%-->
|
|
1108
|
+
|
531
|
1109
|
## More Pictures
|
|
1110
|
+<a class="anchor" name="more_pictures"></a>
|
532
|
1111
|
|
533
|
1112
|
<div class="collapse">Some more photographs I didn't use above.</div>
|
534
|
1113
|
<div class="collapsecontent">
|
535
|
1114
|
<!--%
|
536
|
1115
|
lightgallery([
|
537
|
1116
|
[ "img/laser_tower_2.jpg", "Frontal view in Ikea Lack tower" ],
|
|
1117
|
+ [ "img/laser_cut_test_2.jpg", "Finished test run" ],
|
538
|
1118
|
[ "img/laser_gt2560.jpg", "GT2560 mainboard of laser engraver" ],
|
539
|
1119
|
])
|
540
|
1120
|
%-->
|