aboutsummaryrefslogtreecommitdiff
path: root/posts/opening-black-boxes.md
blob: b94416603218219dc894f09208ea8a50c6a7c0d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# On Opening Black Boxes or: How I Learned to Stop Worrying and Love G-Code {#top}

![Baby Yoda, engraved. ([G-code](baby-yoda.nc))](baby-yoda.png)

**TL;DR** PhotoVCarve should not cost $149. I made [my own](https://github.com/built1n/rastercarve).

Recently I've gotten my hands on a 3-axis [ShopBot milling
machine](https://www.shopbottools.com/products/max). For the
uninitiated, a CNC mill is essentially a robotic carving machine --
think "*robot drill*": you put in a piece of wood/foam/aluminum,
program the machine, and out comes a finished piece with the right
patterns cut into it. I had the idea of
[engraving](https://en.wikipedia.org/wiki/Engraving) a raster image
using the machine, and there happens to be a nice piece of software
out there that claims to do just that: Vectric's
[PhotoVCarve](https://www.vectric.com/products/photovcarve).

There's just one problem: PhotoVCarve costs $149. Now, I have no
qualms paying for software when it makes sense to do so, but in this
case, $149 is simply excessive -- especially for a hobbyist. And
besides, just see for yourself in the video below: all PhotoVCarve
does is take an image and draw a bunch of grooves over it -- *nothing
that couldn't be done in a couple lines of Python,* I thought.

[![PhotoVCarve - Engraving Photographs](http://img.youtube.com/vi/krFyBxYwWW8/0.jpg)](https://www.youtube.com/watch?v=krFyBxYwWW8)

## G-Code

The first step in the process was figuring out *how* to control a CNC
machine. Some Googling told me that virtually all machines read
[G-code](https://en.wikipedia.org/wiki/G-code), a sequence of
alphanumeric instructions that command the movement of the tool in 3
dimensions. It looks something like this:

~~~ {.numberLines}
G00 X0 Y0 Z0.2
G01 Z-0.2 F10
G01 X1.0 Y0
~~~

These three commands tell the machine to:

1. Go to (0, 0, 0.2), rapidly (`G00` is "rapid traverse").
2. Go to (0, 0, -0.2), slowly (`G01` commands a slower move than `G00`).
3. Go to (1, 0, 0), slowly.

My program just had to output the right sequence of G-code commands,
which I could then feed into the ShopBot control software. (This was
far simpler than I had originally imagined.)

At this point, one of my flow states kicked in. I sat down, and got to
coding.

## The Program

The development process was surprisingly straightforward -- I put in
perhaps a total of 4 hours from my initial proof-of-concept to the
current viable prototype. There were no major hiccups this time
around, and even though I'm still in the process of learning it,
Python made things *so* much easier than C (or God forbid -- [ARM
assembly](adieu-quake.html#asm-listing)).

The heart of my program is a function,
[`engraveLine`](http://fwei.tk/git/rastercarve/tree/src/rastercarve.py?id=c2de4a3258c3e37d4b49a41d786eef936262f137#n118) (below),
which outputs the G-code to engrave one "groove" across the image. It
takes in a initial position vector on the border of the image, and a
direction vector telling it which way to cut.

~~~ {.python .numberLines}
# Engrave one line across the image. start and d are vectors in the
# output space representing the start point and direction of
# machining, respectively. start should be on the border of the image,
# and d should point INTO the image.
def engraveLine(img_interp, img_size, ppi, start, d, step = LINEAR_RESOLUTION):
    v = start
    d = d / np.linalg.norm(d)

    if not inBounds(img_size, v):
        print("NOT IN BOUNDS (PROGRAMMING ERROR): ", img_size, v, file=sys.stderr)

    moveZ(SAFE_Z)
    moveRapidXY(v[0], v[1])

    first = True

    while inBounds(img_size, v):
        img_x = int(round(v[0] * ppi))
        img_y = int(round(v[1] * ppi))
        x, y = v
        depth = getDepth(getPix(img_interp, img_x, img_y))
        if not first:
            move(x, y, depth)
        else:
            first = False
            moveSlow(x, y, depth)

        v += step * d
    # return last engraved point
    return v - step * d
~~~

After this was written, it was a simple exercise to write a driver
function to call `engraveLine` with the right vectors in the right
sequence -- and that was all it took![^1] (I really wonder how Vectric
manages to charge $149 for this...)

I fired up the program on a test image and fed its output into
ShopBot's excellent G-code previewer. [Success](#top)! I added a
couple of tweaks (getting the lines to cut at an angle was fun) and I
christened the program
[*RasterCarve*](https://github.com/built1n/rastercarve).

The G-code that produced the image at the top of this post is
[here](baby-yoda.nc). Xander Luciano has an excellent online
[simulator](https://ncviewer.com) which can preview this toolpath.

## Conclusion

This was a fun little project that falls into the theme of "gradually
opening up black boxes." G-code, I learned, isn't nearly as hard as it
might seem. It's all too easy to abstract away the details of a
technical process, but sometimes the best way to really understand
something is by opening up the hood and tinkering with it.

[^1]: I'm probably oversimplifying here. There was, in reality, some
neat vector math to figure out just *where* the "border" of the image
would be when the grooves were at an angle.