Browse Source

publish laser engraver page. add video-thumb script.

Thomas Buck 1 year ago
parent
commit
ed909ff926
44 changed files with 680 additions and 16 deletions
  1. 595
    15
      input/projects/laser-engraver.md
  2. 37
    1
      macros.py
  3. 12
    0
      static/css/style.css
  4. BIN
      static/img/laser_cut_test_1.jpg
  5. BIN
      static/img/laser_cut_test_1_small.jpg
  6. BIN
      static/img/laser_cut_test_2.jpg
  7. BIN
      static/img/laser_cut_test_2_small.jpg
  8. BIN
      static/img/laser_cut_test_3.jpg
  9. BIN
      static/img/laser_cut_test_3_small.jpg
  10. BIN
      static/img/laser_grid.jpg
  11. BIN
      static/img/laser_grid_small.jpg
  12. BIN
      static/img/laser_pagoda_1.jpg
  13. BIN
      static/img/laser_pagoda_1_small.jpg
  14. BIN
      static/img/laser_pagoda_2.jpg
  15. BIN
      static/img/laser_pagoda_2_small.jpg
  16. BIN
      static/img/laser_pagoda_3.jpg
  17. BIN
      static/img/laser_pagoda_3_small.jpg
  18. BIN
      static/img/laser_power_switch.jpg
  19. BIN
      static/img/laser_power_switch_small.jpg
  20. BIN
      static/img/laser_psu.jpg
  21. BIN
      static/img/laser_psu_small.jpg
  22. BIN
      static/img/laser_side_1.jpg
  23. BIN
      static/img/laser_side_1_small.jpg
  24. BIN
      static/img/laser_side_2.jpg
  25. BIN
      static/img/laser_side_2_small.jpg
  26. BIN
      static/img/laser_tree_1.jpg
  27. BIN
      static/img/laser_tree_1_small.jpg
  28. BIN
      static/img/laser_tree_2.jpg
  29. BIN
      static/img/laser_tree_2_small.jpg
  30. BIN
      static/img/laser_tree_3.jpg
  31. BIN
      static/img/laser_tree_3_small.jpg
  32. BIN
      static/img/laser_tree_segments.mp4
  33. BIN
      static/img/laser_tree_segments_poster.png
  34. BIN
      static/img/laser_tree_segments_thumb.png
  35. BIN
      static/img/laser_tree_test.mp4
  36. BIN
      static/img/laser_tree_test_poster.png
  37. BIN
      static/img/laser_tree_test_thumb.png
  38. BIN
      static/img/laser_x_support.jpg
  39. BIN
      static/img/laser_x_support_small.jpg
  40. BIN
      static/img/laser_y_gantry.jpg
  41. BIN
      static/img/laser_y_gantry_small.jpg
  42. BIN
      static/img/lasergrbl_settings.png
  43. BIN
      static/img/lasergrbl_settings_small.png
  44. 36
    0
      video-thumb

+ 595
- 15
input/projects/laser-engraver.md View File

@@ -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
 %-->

+ 37
- 1
macros.py View File

@@ -214,11 +214,13 @@ def printRobotMenuDeutsch():
214 214
 #     [ "image-link", "description" ],
215 215
 #     [ "image-link", "thumbnail-link", "description" ],
216 216
 #     [ "youtube-link", "thumbnail-link", "description" ],
217
-#     [ "video-link", "mime", "thumbnail-link", "image-link", "description" ]
217
+#     [ "video-link", "mime", "thumbnail-link", "image-link", "description" ],
218
+#     [ "video-link", "mime", "", "", "description" ],
218 219
 # ])
219 220
 
220 221
 # it will also auto-generate thumbnails and resize and strip EXIF from images
221 222
 # using the included web-image-resize script.
223
+# and it can generate video thumbnails and posters with the video-thumb script.
222 224
 
223 225
 def lightgallery_check_thumbnail(link, thumb):
224 226
     # only check local image links
@@ -245,6 +247,33 @@ def lightgallery_check_thumbnail(link, thumb):
245 247
     script = os.path.join(os.getcwd(), 'web-image-resize')
246 248
     os.system(script + ' ' + path)
247 249
 
250
+def lightgallery_check_thumbnail_video(link, thumb, poster):
251
+    # only check local image links
252
+    if not link.startswith('img/'):
253
+        return
254
+
255
+    # generate thumbnail filenames video-thumb will create
256
+    x = link.rfind('.')
257
+    thumb_l = link[:x] + '_thumb.png'
258
+    poster_l = link[:x] + '_poster.png'
259
+
260
+    # only run when desired thumb path matches calculated ones
261
+    if (thumb_l != thumb) or (poster_l != poster):
262
+        return
263
+
264
+    # generate fs path to images
265
+    path = os.path.join(os.getcwd(), 'static', link)
266
+    thumb_p = os.path.join(os.getcwd(), 'static', thumb)
267
+    poster_p = os.path.join(os.getcwd(), 'static', poster)
268
+
269
+    # no need to generate thumb again
270
+    if os.path.exists(thumb_p) or os.path.exists(poster_p):
271
+        return
272
+
273
+    # run video-thumb to generate thumbnail
274
+    script = os.path.join(os.getcwd(), 'video-thumb')
275
+    os.system(script + ' ' + path)
276
+
248 277
 def lightgallery(links):
249 278
     videos = [l for l in links if len(l) == 5]
250 279
     v_i = -1
@@ -274,6 +303,13 @@ def lightgallery(links):
274 303
         elif len(l) == 5:
275 304
             v_i += 1
276 305
             link, mime, thumb, poster, alt = videos[v_i]
306
+            if len(thumb) <= 0:
307
+                x = link.rfind('.')
308
+                thumb = link[:x] + '_thumb.png'
309
+            if len(poster) <= 0:
310
+                x = link.rfind('.')
311
+                poster = link[:x] + '_poster.png'
312
+            lightgallery_check_thumbnail_video(link, thumb, poster)
277 313
             print '<div class="border" data-poster="' + poster + '" data-sub-html="' + alt + '" data-html="#video' + str(v_i) + '"><a href="' + link + '"><img class="pic" src="' + thumb + '"></a></div>'
278 314
         else:
279 315
             raise NameError('Invalid number of arguments for lightgallery')

+ 12
- 0
static/css/style.css View File

@@ -25,6 +25,14 @@ th, td {
25 25
     padding-bottom: 0.2em;
26 26
 }
27 27
 
28
+s {
29
+    text-decoration-thickness: 0.2em;
30
+}
31
+
32
+s:hover {
33
+  text-decoration: none;
34
+}
35
+
28 36
 #nav {
29 37
     border-bottom-style: solid;
30 38
     border-bottom-width: 2px;
@@ -296,6 +304,10 @@ table, th, td, ._sh, .ascii, .border, .releasecard, #index-avatar, .collapsecont
296 304
     border-color: #32CD32;
297 305
 }
298 306
 
307
+s {
308
+    text-decoration-color: #FF0000;
309
+}
310
+
299 311
 .releasecard, .collapse {
300 312
     background-color: #C0C0C0;
301 313
 }

BIN
static/img/laser_cut_test_1.jpg View File


BIN
static/img/laser_cut_test_1_small.jpg View File


BIN
static/img/laser_cut_test_2.jpg View File


BIN
static/img/laser_cut_test_2_small.jpg View File


BIN
static/img/laser_cut_test_3.jpg View File


BIN
static/img/laser_cut_test_3_small.jpg View File


BIN
static/img/laser_grid.jpg View File


BIN
static/img/laser_grid_small.jpg View File


BIN
static/img/laser_pagoda_1.jpg View File


BIN
static/img/laser_pagoda_1_small.jpg View File


BIN
static/img/laser_pagoda_2.jpg View File


BIN
static/img/laser_pagoda_2_small.jpg View File


BIN
static/img/laser_pagoda_3.jpg View File


BIN
static/img/laser_pagoda_3_small.jpg View File


BIN
static/img/laser_power_switch.jpg View File


BIN
static/img/laser_power_switch_small.jpg View File


BIN
static/img/laser_psu.jpg View File


BIN
static/img/laser_psu_small.jpg View File


BIN
static/img/laser_side_1.jpg View File


BIN
static/img/laser_side_1_small.jpg View File


BIN
static/img/laser_side_2.jpg View File


BIN
static/img/laser_side_2_small.jpg View File


BIN
static/img/laser_tree_1.jpg View File


BIN
static/img/laser_tree_1_small.jpg View File


BIN
static/img/laser_tree_2.jpg View File


BIN
static/img/laser_tree_2_small.jpg View File


BIN
static/img/laser_tree_3.jpg View File


BIN
static/img/laser_tree_3_small.jpg View File


BIN
static/img/laser_tree_segments.mp4 View File


BIN
static/img/laser_tree_segments_poster.png View File


BIN
static/img/laser_tree_segments_thumb.png View File


BIN
static/img/laser_tree_test.mp4 View File


BIN
static/img/laser_tree_test_poster.png View File


BIN
static/img/laser_tree_test_thumb.png View File


BIN
static/img/laser_x_support.jpg View File


BIN
static/img/laser_x_support_small.jpg View File


BIN
static/img/laser_y_gantry.jpg View File


BIN
static/img/laser_y_gantry_small.jpg View File


BIN
static/img/lasergrbl_settings.png View File


BIN
static/img/lasergrbl_settings_small.png View File


+ 36
- 0
video-thumb View File

@@ -0,0 +1,36 @@
1
+#!/bin/bash
2
+
3
+if [ -z "$1" ]; then
4
+    echo "No argument supplied"
5
+    exit 1
6
+fi
7
+
8
+sdir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
9
+dir=$(dirname "${1}")
10
+video=$(basename -- "${1}")
11
+ext="${video##*.}"
12
+name="${video%.*}"
13
+thumb="${name}_thumb.png"
14
+poster="${name}_poster.png"
15
+tmp="${name}_tmp.png"
16
+
17
+echo $sdir
18
+
19
+echo "Directory : $dir"
20
+echo "Input file: $video"
21
+echo "Thumbnail : $thumb"
22
+echo "Poster    : $poster"
23
+
24
+cd "$dir"
25
+
26
+echo ffmpeg -i $video -vf "select=eq(n\,0)" -n -update 1 -frames:v 1 $poster
27
+ffmpeg -i $video -vf "select=eq(n\,0)" -n -update 1 -frames:v 1 $poster
28
+
29
+echo convert "$poster" -auto-orient -thumbnail 300x300\> -strip "$tmp"
30
+convert "$poster" -auto-orient -thumbnail 300x300\> -strip "$tmp"
31
+
32
+echo convert $tmp \( $sdir/static/lg/video-play.png -background none -gravity center \) -composite $thumb
33
+convert $tmp \( $sdir/static/lg/video-play.png -background none -gravity center \) -composite $thumb
34
+
35
+echo rm -rf $tmp
36
+rm -rf "$tmp"

Loading…
Cancel
Save