As a hobby project to give me something else to think about than work when I'm not at work (and when I am at work) I decided to automate drawing with an Etch A Sketch.
-
Do not modify the Etch A Sketch
I ended up violating this requirement but in an acceptably minor way: I popped the white knobs off and replaced them with synchronous drive pulleys. When building the hardware I realized that I was going to substantially increase the cost and complexity of my project by trying to keep the white plastic knobs in place, and that on the software side there would be no difference.
Most importantly the Etch A Sketch can't be modified to lift up the stylus!
-
The program itself must interpret and draw an image.
I haven't yet implemented this part of the program. It quickly became obvious that this requirement is a giant black-hole and that I could easily get sucked in and then never write the rest of the program. I've written almost everything else and now it is time to go back and work on image processing.
-
The code should be clean, professional, and extensible.
I think I achieved this fairly well! There are a couple small [style inconsistencies](PEP8 http:https://www.python.org/dev/peps/pep-0008/ "PEP8") , but I'll clean those up over time.
The draw.py script is the main script used to coordinate everything. The origin is considered to be the bottom left corner of the Etch A Sketch screen (this is the math/science convention, rather than the graphics convention, sorry). If the stylus is not at (0,0), then you can drive it there using manual.py
To talk to the motors you need Adafruit's RPi.GPIO library library. There are probably other Raspberry Pi GPIO libraries, but I haven't tried them.
Most Linux distros will require super-user privileges to use the GPIO pins on
the Raspberry Pi, so you'll need to run any program that drives a motor via
them with super-use privileges. If you are new to Linux, remember that is done
either by running the su
command to enter super-user mode, or to precede an
entry on the command line with sudo
.
oliver@rpi:~/etch$ sudo python draw.py
Just as with draw.py
, you'll need super-user privileges to run
manual.py
.
At the ?
prompt you can issue commands composed of a direction and a distance.
Directions are u
, r
, d
, and l
and are followed immediately (no space) by
the number of steps to take.
This would draw a square approximately 1cm on a side:
oliver@rpi:~/etch$ sudo python manual.py
? u150
? r150
? d150
? l150
You cannot issue serial commands like ? u50r10u100r90
. But if you would like
to extend manual.py
to do that or anything else I would be
grateful.
This program implements a strategy pattern so all the big ugly code that creates paths is tucked away to keep the rest of the code and the Render() relatively clean.
Paths are computed, scaled to fit the screen and then drawn by connecting the
points in order in with straight lines. The straight lines are computed with
Bressenham's line algorihm.
The algorithm is modified so that rather than returning the coordinates of each
point on the line, it returns a list of Point()
objects where x, y are
-1, 0, 1 and represent single steps by the stepper motor. With the
configuration I have set-up this produces wonderfully smooth lines as each step
is approximately 0.04mm.
-
draw.py coordinates the everything
-
Render()
is defined in render.py and is class thatdraw.py
interacts with to get a list ofPoint()
s as a path and and awidth
andheight
all as data attributes.Render()
has the important method.scale([factor])
wherefactor
is a numeric data type and will be used to scale up or down the path - you'd use this method if you want to scale an image to fit a screen of a given size.Note also that the data attributes
x
andy
don't necessarily correspond with the maximumx
ory
in the.path
attribute. An image may have white space outside of its drawn portion, this allows that white space to exist. -
Point()
is defined in point.py and is an object withx
andy
attributes that are integers (Bressenham's line algorithm only really makes logical sense with integers).__rmul__
is implemented but not__mul__
so ifp
is an instance ofPoint()
, you can say2*p
but notp*2
.- you can add
+
, subtract-
, negate-
, and compare=
twoPoint()
s, but not multiply etc. since I don't know what that would mean - same goes for__lt__()
,__le__()
, etc.
-
Canvas()
is defined in canvas.py and is really just a height and width of the screen to set the maximum distance that the stylus can travel before it won't drive any further. -
Stepper()
is defined in stepper_driver.py. The steppers are driven by powering a sequential pattern of its four coils. Those patterns are defined asforward_sequence
andreverse_sequence
as well as anerror_sequence
that can be sent to the motor to allow the program to continue normal operation but not move the stylus - as when the stylus has run outside of the limits ofCanvas()
. -
step_bressenham()
andcompute_bressenham()
functions are defined in bressenham_functions.py. These aren't particularly long functions, but they are just long enough that pulling them out of draw.py cleaned up the code a lot.compute_bressenham()
returns a list ofPoint()
s representing the two end points and every point connecting them, that list can then be sent tostep_bressenham()
which will return a list of the moves needed to connect the ends of the line as described above.
Implementing your own path generator should be fairly straightforward. A path
generator should inherit
its interface from PathInterface
in the path_interface.py
module, or re-implement the interface. Specifically your path generator object
needs these public data attributes:
self.width()
returns a non-negative integerself.height()
returns a non-negative integerself.path()
returns an ordered list of Point() objects with integer values.
Your path generator then needs to be passed to be set as the
Render object's path_generator
attribute either when initializing
the Render object, or via the Render.set_path_generator()
method.
That's it!
Here is one hardware list that works:
- Raspberry Pi
- some stepper motors with control boards - the 28BYJ-48 5V stepper motor is pretty common and can be bought for a couple bucks with a motor driver included. Don't be tempted to get the 12v version because its gearing is different and it doesn't have enough power.
- Etch A Sketch
- synchronous drive belts and pulleys. I used these:
- A frame to hold it all together I really like MicroRax
- Write a method to center an image on the screen.
- Make a path simulator to output an
.svg
so that new path generators can be quickly tested. - Implement path generators that read an image. (That's why I made this program!)
- Build something to automatically clear the screen.
- Clean up inconsistencies in coding style.
-
If you want a little lesson about driving stepper motors with the Raspberry Pi, then check out Adafruit's lessn
-
Here is where I learned about driving stepper motors, though the code isn't the best.
-
If you are new to the Rasbperry Pi then check out Adafruit's tutorials
If you need a comprehensive introduction then start with their setup of the Raspberry Pi and then do the rest of the tutorials.
-
For more about the Strategy Pattern in Python check out this Stack Exchange discussion