Student guide · Page 1 of 6
Start here
Plan the turret before you draw it
This guide is for student teams taking on the RoboChargers laser maze challenge. It gives you hints for building a turret that steers a laser dot through a maze. It does not hand you a design. Choosing the layout, the gears and the code is your team's job, and it is where most of the learning is.
Every page mixes five kinds of content. Tips tell you what to do and why. Key ideas are the few things worth remembering. Try it boxes are short experiments. Common mistakes show where teams lose time. Work it out boxes are questions that only your team can answer, because the answer depends on your parts, your room and your choices.
What the turret has to do
Your turret aims one laser dot with two servos. One servo swings the dot left and right. That motion is called pan. The other swings it up and down. That is tilt. Your program works out a route through the maze, and the turret has to walk the dot along it without touching a wall.
Touching a wall costs points, and a slower run earns a smaller time bonus. Read the scoring rules for the exact numbers, then decide as a team which matters more to you.
Your kit
| Part | What it does | Find out before you design |
|---|---|---|
| 270° servo (two) | Turns each axis | Body size, mounting holes, where the output shaft sits, the horn's hole pattern, its datasheet |
| Bearings | Let a part turn freely while carrying its weight | Inside diameter, outside diameter, width |
| Laser pointer | Makes the dot | Body diameter and length, where the wires or button are, its voltage |
| Servo power supply | Powers the two servos | Its voltage and how much current it can give |
| Webcam | Takes the photo your program reads | See the Maze solver page |
| Arduino Uno or Nano | Turns your computer's commands into servo signals | Which pins you will use |
| Laptop | Runs your main program and the tools for writing code | Your own or a college laptop. Which software is already installed on it? |
| Screws and nuts | Hold it together | Thread size, head size, the lengths you have |
| 3D printed parts and gears | Everything else | You design these |
Two programs, two languages
Your project is two separate programs, running on two machines, and they are usually written in two different languages.
| On your laptop | On the Arduino | |
|---|---|---|
| What it does | Takes the photo, reads the maze, finds the route and decides where the dot should go | Receives short messages and turns them into servo pulses and laser on or off |
| Language | Your choice. The examples in this guide use Python | C++, with extra commands that Arduino adds |
| Where you write it | Any code editor | The Arduino IDE, a free program you install on the laptop |
| How it runs | You start it on the laptop each time | You upload it to the board once. It then runs by itself whenever the board has power |
| Guide page | Maze solver | Arduino and servos |
How much angle do you need?
A 270° servo can turn three quarters of a full circle. The maze takes up far less than that. Before you design anything, find out how much.
Measure the projected maze and the distance from the turret to the wall. Then:
angle needed = 2 × arctan( (maze size ÷ 2) ÷ distance to wall )
Use the maze's width for pan and its height for tilt. Make sure your calculator is in degrees.
Worked example, with made-up numbers. A maze is 1.5 m wide and 1.07 m tall. The turret sits 2.0 m from the wall, level with the middle of the maze.
- Pan: 2 × arctan(0.75 ÷ 2.0) = 41°
- Tilt: 2 × arctan(0.535 ÷ 2.0) = 30°
Hint: for question 4
Redraw the figure with the turret half as far away. Compare the two triangles. There is something to gain and something to lose, and the next section is about the gain.
How small a step do you need?
The dot has to stay inside a corridor. So the real question is how many different places the dot can stop inside one maze cell.
A servo is told where to go by a pulse, measured in microseconds (µs). The Arduino page explains this. For now you need two facts from your servo's datasheet:
- Travel and pulse range. A typical 270° servo spreads its 270° over pulses from 500 to 2500 µs. That is 270 ÷ 2000 = 0.135° for each microsecond.
- Dead band. A servo ignores a change smaller than its dead band, often 2 to 5 µs. The dead band times the degrees per microsecond is the smallest change the servo can respond to. Treat it as a best case.
With a 3 µs dead band, that best-case step is about 0.4°. In the made-up example, one cell of a 21-cell-wide maze is about 2° wide. So even a perfect servo could tell apart only about five positions across a whole cell.
This table is for the example's numbers: a 270° servo, a 3 µs dead band and a 2° cell.
| Gear ratio | Range at the laser | Best-case step | Best-case steps across a 2° cell |
|---|---|---|---|
| 1:1 (no gears) | 270° | 0.41° | 5 |
| 2:1 | 135° | 0.20° | 10 |
| 3:1 | 90° | 0.14° | 15 |
| 4:1 | 67.5° | 0.10° | 20 |
| 5:1 | 54° | 0.08° | 25 |
Step size is only half of precision. The other half is anything that lets the laser move when the servo has not.
Choosing a layout
There is more than one good turret. Most designs are built from two ways of driving an axis, and you can use a different one for pan and for tilt.
| Direct drive | Geared, on a bearing | |
|---|---|---|
| Parts to design | Few | More |
| Step size | The servo's own | Divided by the gear ratio |
| What carries the weight | The servo's output shaft | The bearing |
| Looseness inside the servo | Reaches the laser at full size | Divided by the gear ratio |
| New looseness | None | The gap between your printed teeth |
| Time to a first test | An afternoon | Several prints |
These choices can be mixed. A direct-drive axis can still rest on a bearing. A geared axis still pushes sideways on the servo's shaft, at the point where the teeth meet.
Whatever you choose, turrets that point well tend to share a few features:
- The laser sits close to where the two axes cross. Then turning one axis does not slide the beam sideways as well as turning it.
- The moving parts are light and stiff. Less weight means less wobble after each move.
- The base cannot shift. Once you have matched the turret to the camera, a nudge of one millimetre spoils the match.
- Wires have slack. A tight cable pulls the laser off target as the axes turn.
- Every screw can be reached with the turret assembled.
Bearings
A bearing lets one part turn inside another with almost no friction and almost no wobble. It has an outer ring, an inner ring and balls between them. One ring is held still and the other turns.
A servo's output shaft is made to twist a load. It can take only a small load pushing it sideways or tipping it. How much depends on the servo, so check the datasheet. A bearing is made for exactly those loads.
How a bearing seat works
- The outer ring presses into a round printed pocket. The shaft or bolt goes through the inner ring.
- Whatever sits on the bearing touches the inner ring only. A washer or a small raised shoulder does this.
- The pocket needs a lip, so the bearing cannot slide straight through.
- A printed pocket comes out a little smaller than you drew it. The OrcaSlicer page shows how to find out by how much.
Common sizes
The code is stamped on the side of the bearing. Measure yours to confirm.
| Code | Inside × outside × width (mm) |
|---|---|
| 623 | 3 × 10 × 4 |
| 624 | 4 × 13 × 5 |
| 625 | 5 × 16 × 5 |
| 608 | 8 × 22 × 7 |
| 6805 | 25 × 37 × 7 |
| 6806 | 30 × 42 × 7 |
A way to work
Take one question at a time. Build the smallest thing that answers it, test it, write down what you measured, then move on. Do not print the whole turret and switch it on for the first time at the end. When it misses, you will not know which part is to blame.
A good order is one where each step proves one thing:
- A bare servo moves when your code tells it to. See the Arduino page.
- Your printer makes a hole the size you need. See the fit test on the OrcaSlicer page.
- One pair of printed gears turns smoothly. See the Gears page.
- One axis points at the same spot twice. Measure how far apart the two dots land.
- Both axes together cover the whole maze.
- The turret and your program agree about where the dot is.
Words used on this page
| Word | Meaning |
|---|---|
| Pan | Turning left and right about an upright axis |
| Tilt | Tipping up and down about a level axis |
| Axis | The imaginary line a part turns around |
| Servo | A motor that turns to an angle you ask for and holds it |
| Horn | The plastic or metal arm or disc that fits on a servo's output shaft |
| Dead band | The smallest change in signal a servo will respond to |
| Gear ratio | How many turns of the small gear make one turn of the large one |
| Bearing | A ring that lets a shaft turn freely while holding it in place |
| Repeatable | Landing in the same place every time, for the same command |
The other pages
Split the work. One person can start on the maze photo, another on the Arduino page and a third in Onshape.
| Page | Read it when |
|---|---|
| Maze solver | You are taking the photo and turning it into data. Start this early |
| Onshape | You are ready to draw parts |
| Gears | You are deciding whether to gear an axis, and how |
| OrcaSlicer | Before your first print |
| Arduino and servos | You are wiring a servo and writing its code. Start this early |
Student guide · Page 2 of 6
Maze solver
From a photo to data you can search
This page gives hints for the software half of the challenge: getting a usable photo of the maze, and turning that photo into data your program can search. It does not tell you how to find the route. That part is yours.
This page has three kinds of content. Tips tell you what to do and why. Code blocks are small working pieces you can copy, each explained line by line. Questions are the parts you must work out as a team. Key terms such as "threshold" and "slice" are explained in the word list at the end.
Taking a good picture
A clean photo makes every later step easier. Most "my code does not work" problems on this project start with the picture, not the code.
What the camera gives your program
Your eyes see a maze. Your program sees only brightness numbers. It has no idea what a wall is. All it can ever know is "this spot is bright" or "this spot is dark".
So anything that changes brightness changes what your program believes. Glare makes floor look like wall. A dim projector makes walls look like floor. A camera that brightens the picture by itself shifts every number at once.
Every tip below has the same goal: keep the numbers steady, and keep the walls clearly brighter than the floor.
The checklist
-
Get everything in the frame. The whole maze and every ArUco tag must be visible, with a little space around them. Your code finds the maze from the tags. A tag cut off at the edge of the photo usually means your program cannot find the maze.
-
Laser off first. Switch the laser off before you take the photo. If you cannot switch it yet, block the beam with a piece of matte card held close to the laser. A bright dot looks just like a piece of wall.
-
Lock the camera settings. Auto-exposure means the camera keeps changing how much light it lets in, to keep the picture looking "nice". Autofocus means it keeps re-adjusting sharpness. Both change the picture while you are not looking, and either one can break a brightness test that worked a minute ago. Turn them off in the webcam's own settings if you can.
-
Set the focus by hand. If the webcam has a focus ring or a focus slider, adjust it until the edges of the tags look crisp. Blurry tags are usually the first thing to fail.
-
Mount the camera. Use a tripod, a clamp or tape. A hand-held photo is slightly blurred, and blur smears thin walls into the floor. Also, once your program has measured anything from a photo, a bumped camera makes those measurements wrong.
-
Face the wall as squarely as you can. Some tilt is fine, and you can correct for it in code. A steep angle squashes the far cells into very few pixels, and no code can bring back detail that was never captured.
-
Watch the light. If the walls look washed out, dim the room lights. Look for glare from the projector on a shiny wall. A quick test: take one photo with the room lights on and one with them off, and compare.
-
Use enough pixels. A maze 21 cells wide in a photo 1280 pixels wide gives fewer than 60 pixels per cell. The walls are then only a few pixels thick, and a little blur can make one vanish. Move the camera closer or use a higher resolution, as long as everything stays in the frame.
-
Skip the first frames. A webcam adjusts itself for a moment after it starts, and it can hand you an old frame from its queue. Read and throw away a few frames before the one you keep.
-
Look at the photo yourself. Save it to a file, open it and zoom in on the smallest cells. If you cannot clearly see every wall and tag, your program cannot either.
When the photo is the problem
| What you see in the photo | Likely cause | What to try |
|---|---|---|
| A tag is missing or cut off | Camera too close, or aimed off-centre | Move the camera back or re-aim it |
| Walls look grey, not bright | Room too bright, or exposure too low | Dim the lights or raise the exposure |
| A bright patch or streak across the maze | Glare from the projector or a lamp | Shift the camera a little to one side |
| Everything is slightly fuzzy | Out of focus, or the camera moved | Set the focus by hand and mount the camera |
| Two photos of the same maze differ in brightness | Auto-exposure is still on | Turn it off in the webcam settings |
| The far side of the maze looks squashed | Camera angle too steep | Move the camera closer to square-on |
Code you will need: getting a picture and reading it
These are building blocks in Python with OpenCV. They each do one small job. Putting them together is your work.
Set up Python
The examples on this page are in Python, with a library called OpenCV that handles pictures. Any language that runs on your laptop is allowed. OpenCV also exists for C++ and Java. If you choose another language, the ideas here still apply, but the code will look different.
This is the laptop's program. The Arduino's program is a separate thing in a different language, covered on the Arduino and servos page.
- Open a terminal. On Windows, search the Start menu for Terminal or PowerShell. On a Mac, open the Terminal app.
- Check for Python. Type
python --versionand press Enter. On a Mac, typepython3 --version. You want an answer that starts withPython 3. - Install it if it is missing. Get it from python.org/downloads. On Windows it is also in the Microsoft Store. On a college laptop, ask the club before you install anything.
- Install the libraries. In the terminal, type
python -m pip install opencv-python numpy. On a Mac, start withpython3instead ofpython. - Write and run a program. Use any code editor. Save your program with a name ending in
.py, then run it from the terminal withpython name.py, in the folder where you saved it.
From OpenCV 4.7 onwards the ArUco tag tools are part of the standard package, so the older contrib package is not needed.
In Python, # starts a comment, and the spaces at the start of a line matter: indented lines belong to the if or for above them.
Grab one frame and save it
import cv2
import numpy as np
cap = cv2.VideoCapture(0) # 0 = first camera. Try 1 if you get the laptop's built-in one
if not cap.isOpened():
raise SystemExit("No camera found. Is it plugged in? Is another program using it?")
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
for _ in range(10): # throw away the first few frames
ok, frame = cap.read()
ok, frame = cap.read() # ok is False if the camera did not answer
cap.release()
if not ok:
raise SystemExit("The camera opened but did not send a picture.")
cv2.imwrite("maze_photo.png", frame) # save it so you can look at it
What each part does:
-
import cv2loads OpenCV.import numpy as nploads NumPy, the library OpenCV uses to hold pictures. -
cv2.VideoCapture(0)opens a camera and gives you an object,cap, for talking to it. The number picks which camera. -
The two
cap.setlines ask for a picture size. The camera may refuse and give you a different size, so checkframe.shapeafterwards. -
cap.read()returns two things.okis True or False: did it work?frameis the picture. Checkokbefore you useframe. -
raise SystemExit("...")stops the program and prints your message. A clear message here is far better than a confusing crash three steps later, when the real cause is hard to trace. -
The
forloop reads ten frames and ignores them. This gives the camera time to settle and empties its queue of old frames. -
cap.release()hands the camera back. Until you do this, other programs cannot use it. -
cv2.imwritesaves the picture. Use.png, which keeps every pixel exactly. A.jpgfile is smaller because it slightly smudges fine detail, and thin walls are fine detail.
On Windows, cv2.VideoCapture(0, cv2.CAP_DSHOW) often opens the camera faster.
frame = cv2.imread("maze_photo.png")
if frame is None:
raise SystemExit("Could not open maze_photo.png. Check the name and the folder.")
If the file name is wrong, cv2.imread does not complain. It quietly returns None, which is why the check is there. Without it, the next line crashes with a message about NoneType that says nothing about files.
A picture is a grid of numbers
frame.shape # (720, 1280, 3) -> 720 rows, 1280 columns, 3 colour numbers
frame[90, 500] # one pixel -> [blue, green, red], each from 0 to 255
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray[90, 500] # one number -> 0 is black, 255 is white
gray[100:200, 300:400] # a rectangle: rows 100 to 199, columns 300 to 399
gray[100:200, 300:400].mean() # the average brightness of that rectangle
Some notes on those lines:
-
frame.shapelists rows first, then columns. The picture is 1280 wide and 720 tall, but the shape says(720, 1280, 3). -
Counting starts at 0. The top-left pixel is
frame[0, 0]and the bottom-right one isframe[719, 1279]. Row numbers grow downward. -
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)makes a greyscale copy: one brightness number per pixel. Walls are bright and floor is dark whatever their colour, so brightness is all you need to find walls. One number per pixel is also easier to work with than three. -
gray[100:200, 300:400]is called a slice. The first number is included and the last is not. So100:200means rows 100 up to 199, which is 100 rows. -
.mean()averages every number in the slice. An average over a patch is steadier than a single pixel, because one odd pixel barely moves it.
That last line is more useful than it looks. "How bright is this small patch?" is the main question you will ask the picture.
Seeing a wall in the numbers
You can also print a single row of pixels. This strip runs from left to right across one wall, in a flattened photo (flattening is covered in the next section). The numbers are an example of what to expect, not values to copy:
flat[90, 48:73]
# [ 31 30 33 32 31 31 35 31 41 85 146 163 164 164 149 88 43 31 39 32 31 32 35 35 29]
The floor is around 30. The wall is the bump in the middle, rising to about 164. Notice it is not a sharp step. The wall fades in and out over a pixel or two, because every camera blurs a little.
The same kind of strip across an edge with no wall is flat:
flat[90, 108:133]
# [33 31 32 32 34 35 36 33 32 33 33 35 34 33 37 33 31 33 35 38 35 33 32 34 32]
Notice the floor is not perfectly even either. It wobbles between 31 and 38. That wobble is called noise, and every camera has it.
Your numbers will be different, because your camera, projector and room are different. Print strips like these from your own photo before you write any wall-finding code.
Three traps that catch everyone
-
Row comes first. You write
gray[row, column], which isgray[y, x]. Most other OpenCV functions want points as(x, y). Mixing these up gives you a maze turned on its side. -
Colours are blue, green, red. OpenCV stores them as BGR, not RGB.
-
Pixel numbers wrap around. Pixels are stored as whole numbers from 0 to 255. Add 200 and 100 and you get 44, not 300. Before adding or subtracting pixels, make a separate decimal copy:
gray_f = gray.astype(np.float32).
Why do pixel numbers wrap? You can check what kind of numbers a picture holds with gray.dtype. A fresh photo says uint8. That means whole numbers from 0 to 255, each stored in one byte. There is no room for 300 or for -5, so the result rolls over, like a car's mileage counter going past its last digit. The copy made by astype(np.float32) can hold decimals, negative numbers, and numbers bigger than 255.
Finding the maze in the photo
The five ArUco tags are your landmarks. OpenCV can find them for you and tell you exactly where each one is.
An ArUco tag is a black square with a pattern of black and white blocks inside, a bit like a very simple QR code. The pattern stands for a number, called the tag's ID. Tags are designed so a computer can find them even when they are small, tilted or a little blurry.
Each tag has a white border around it. The detector needs that border to pick out the black square, so keep it in the frame and do not let glare wash it out.
| Tag ID | Where it is |
|---|---|
| 0 | Outside the maze, beside the upper-left corner |
| 1 | Outside the maze, beside the upper-right corner |
| 2 | Outside the maze, beside the lower-right corner |
| 17 | Inside the Start cell |
| 8 | Inside the Finish cell |
How big is a tag?
This catches many teams. The white border makes the whole tag one cell wide on the practice maps, but the corners your program receives belong to the black square inside it, which is smaller.
| Tag | Width with its white border | Width of the black square |
|---|---|---|
| 0, 1 and 2 (beside the corners) | 1 cell | 0.75 of a cell |
| 17 (Start) and 8 (Finish) | 0.88 of a cell | 0.66 of a cell |
These sizes come from the practice-map generator on the competition page. Confirm them against the map you are given before you rely on them.
Detecting the tags
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
detector = cv2.aruco.ArucoDetector(dictionary, cv2.aruco.DetectorParameters())
corners, ids, _ = detector.detectMarkers(gray) # gray must be the original 0 to 255 picture
if ids is None:
raise SystemExit("No tags found. Look at the photo: focus, glare, or tags out of frame?")
found = {} # tag ID -> its 4 corners
for tag_corners, tag_id in zip(corners, ids.flatten()):
pts = tag_corners.reshape(4, 2) # 4 corners of the black square, each (x, y)
found[int(tag_id)] = pts
centre = pts.mean(axis=0) # (x, y) of the middle of the tag
print(tag_id, centre)
missing = [tag for tag in (0, 1, 2, 17, 8) if tag not in found]
if missing:
raise SystemExit(f"Tags not found: {missing}")
What each part does:
-
A "dictionary" here means a family of tags.
DICT_4X4_50is the family where each tag has a 4 by 4 grid of blocks and there are 50 different IDs. The detector only finds tags from the family you name. -
detectMarkersreturns three things.cornersis a list with one entry for each tag it found.idsholds the ID numbers, in the same order. The third is a list of shapes that looked like tags but were not. The name_is a common way to say "I am ignoring this". -
zip(corners, ids.flatten())walks through both lists together, so each tag's corners stay paired with its ID. -
reshape(4, 2)tidies one tag's corners into 4 rows of(x, y). -
pts.mean(axis=0)averages the four corners. The average of a square's four corners is its centre. In a tilted photo this is off by a fraction of a pixel, which is far too small to matter here. -
found[int(tag_id)] = ptsstores each tag's corners under its ID, so you can ask forfound[17]later. -
The last three lines name which tags are missing. "Tags not found: [2]" points you straight at the lower-right corner of the photo.
Things to watch:
-
The tags come back in no particular order. Do not assume the first one is tag 0. That is why the code stores them by ID.
-
Each corner is an
(x, y)pixel position. Note the order: x first here. -
The four corners come in the tag's own order: its top-left first, then clockwise. For an upright tag, that matches what you see. If the camera is mounted sideways or upside down, the tag's "top-left" is no longer the photo's top-left.
-
When no tags are found,
idsisNone, not an empty list. Without the first check, the loop would crash with a message aboutNoneType. -
To see what was found, draw it:
cv2.aruco.drawDetectedMarkers(frame, corners, ids), then save the picture. -
This needs OpenCV 4.7 or newer.
Flattening a tilted photo
A camera at an angle sees the maze as a lopsided four-sided shape. If you know where the maze's four corners are in the photo, OpenCV can redraw it as a perfect rectangle, as if seen straight on.
This redrawing is called a perspective transform. It needs four pairs of matching points: where each corner is in the photo, and where you want that corner to be in the new picture.
S = 60 # pixels per cell in the flat picture (you choose)
w, h = 7, 5 # cells across and down
photo_corners = np.float32([ul, ur, lr, ll]) # the maze's 4 corners in the photo, each (x, y)
flat_corners = np.float32([[0, 0], [w * S, 0], [w * S, h * S], [0, h * S]])
H = cv2.getPerspectiveTransform(photo_corners, flat_corners)
flat = cv2.warpPerspective(gray, H, (w * S, h * S))
What each part does:
-
Ssets the size of the flat picture. Each cell becomesSbySpixels. Too small and thin walls disappear. Making it bigger costs only a little time. A value close to the size of a cell in your photo is a sensible start. -
photo_cornersandflat_cornersmust list the corners in the same order. Here it is upper-left, upper-right, lower-right, lower-left. Swap two and the maze comes out twisted or mirrored. -
np.float32is there because these two functions insist on that type of number. -
cv2.getPerspectiveTransformreturnsH, a small table of nine numbers. You do not need to understand the nine numbers. Think ofHas a recipe for moving any point from the photo to the flat picture. -
cv2.warpPerspectiveuses the recipe to build the flat picture. Its last argument is the new picture's size as(width, height). That is across first, the opposite of.shape.
Save flat with cv2.imwrite and look at it. The outer wall should run right along the four edges of the picture. If it does not, your corners are off.
In the flat picture every cell is the same size. The cell in column i and row j covers rows j * S to (j + 1) * S and columns i * S to (i + 1) * S. That makes it easy to look at any cell or any edge you like.
The same H also converts single points. This tells you where a tag's centre lands in the flat picture:
spot = cv2.perspectiveTransform(np.float32([[centre]]), H)[0][0] # (x, y) in the flat picture
The double brackets in [[centre]] are not a typo. This function wants its points wrapped in a list inside a list. The [0][0] at the end unwraps the answer again.
Suppose that with S = 60 the centre of tag 17 lands at about (390, 270) in the flat picture. Which column and row is that?
Things you have to work out
The snippet above needs ul, ur, lr, ll, w and h. Nobody hands you those.
Hint: where could more known points come from?
There are two places to look. First, each tag gives you four corners, not just one centre. Once you know where a tag's centre sits in cell units, the tag size table tells you where each of its corners sits. Three tags make twelve known points. Second, the outer wall has a real corner at the lower left, where two bright lines meet.
Look up cv2.findHomography. It does the same job as cv2.getPerspectiveTransform but accepts more than four pairs of points.
On a Hard maze the tags are small, so their corners are less exact. How would you test which method gives better corners for your camera?
From picture to grid: questions to answer
A flat picture is still just pixels. Your program has to turn it into facts about walls. There is no code in this section on purpose. Work through these questions as a team.
A good way to work: take one question, write a few lines that print or draw the answer, check it against the photo by eye, then move on. Do not write the whole wall reader and run it for the first time at the end. When it fails, you will not know which part is wrong.
Ways to store the maze
There is no single right way to hold a maze in memory. Here are four that work, each shown on the same small example so you can compare them.
A data structure is a way of arranging information in memory so your program can find what it needs. The same maze can be arranged in many ways, just as the same list of club members can be sorted by name or by year. Each arrangement makes some questions easy to ask and others awkward.
The options use five ordinary Python building blocks:
| Python type | Looks like | What it is |
|---|---|---|
| List | [1, 0, 1] |
An ordered row of things. row[0] is the first one |
| List of lists | [[1, 0], [0, 1]] |
A table. table[j][i] means row j, then column i |
| Tuple | (2, 0) |
A fixed pair, such as a cell's column and row |
| Dictionary | {(2, 0): "something"} |
A lookup. Give it a key, get back what is stored under that key |
| Set | {"N", "W"} |
A bag of things. The only question you ask is "is this in it?" |
In Python, 1 and 0 can stand in for True and False. The tables below use 1 and 0 because they line up neatly.
The example maze is 3 cells wide and 2 cells tall. A cell is named (i, j): column i counted from the left, row j counted from the top. Both start at 0.
Thick lines are walls. Dashed lines are edges you can step across. Every option below describes exactly this maze.
Whichever option you pick, your solver will keep asking one question: from this cell, can I step to that one? Under each option is how you would ask whether you can step right from cell (1, 0) to cell (2, 0). In the drawing, you can.
Option A: two tables of walls
Store the walls themselves. Vertical walls go in one table and horizontal walls in another.
# vwall[j][i] is 1 if there is a wall on the LEFT side of cell (i, j)
vwall = [[1, 1, 0, 1],
[1, 0, 1, 1]]
# hwall[j][i] is 1 if there is a wall on the TOP side of cell (i, j)
hwall = [[1, 1, 1],
[0, 0, 0],
[1, 1, 1]]
vwall[0][2] # 0 -> no wall on the left side of cell (2, 0), so you can step right from (1, 0)
The tables are different shapes. A maze 3 cells wide has 4 vertical lines, so vwall has one extra column. hwall has one extra row for the same reason. The right side of a cell is the left side of the next cell over.
How to read it. Take the top row of vwall, which is [1, 1, 0, 1]. Walk along the top row of the drawing from left to right and look at each upright line in turn. The outer wall: 1. The line between (0, 0) and (1, 0) is a wall: 1. The line between (1, 0) and (2, 0) is dashed: 0. The outer wall again: 1. Do the same for the second row, and for hwall, until you believe every number.
How you would fill it. One edge of the photo at a time. Each wall-or-open answer goes into exactly one slot, so nothing is stored twice.
The four moves. To step right from cell (i, j) you check vwall[j][i + 1]. What do you check to step left? Up? Down? Work them out on the drawing before you write any code.
Option B: four wall flags for each cell
Store one entry per cell that says which of its four sides have walls.
# walls[(i, j)] is the set of sides of cell (i, j) that have a wall
walls = {
(0, 0): {"N", "E", "W"}, (1, 0): {"N", "W"}, (2, 0): {"N", "E"},
(0, 1): {"S", "W"}, (1, 1): {"E", "S"}, (2, 1): {"E", "S", "W"},
}
"E" in walls[(1, 0)] # False -> no wall on the east side of (1, 0), so you can step right
N, E, S and W are compass directions on the picture: N is up, E is right, S is down and W is left. Cell (1, 0) has a wall above it and a wall on its left, so its entry is {"N", "W"}.
Every inside wall appears twice. The wall between (0, 0) and (1, 0) is "E" in cell (0, 0) and also "W" in cell (1, 0). If your program records one and forgets the other, the maze disagrees with itself. You could walk through that wall in one direction but not the other.
How you would fill it. Visit each cell and test its four sides. Or test each edge once and write the answer into both cells that share it.
A more compact version packs the four flags into one number per cell. Give each side a value (N = 1, E = 2, S = 4, W = 8) and add up the sides that have walls:
N, E, S, W = 1, 2, 4, 8
cells = [[11, 9, 3], # 11 = N + E + W, 9 = N + W, 3 = N + E
[12, 6, 14]]
cells[0][1] & E # 0 -> no east wall on cell (1, 0). Note the order: cells[row][column]
The & asks "is this flag part of the number?" Look up "bit flags" if you want to use this version.
How the numbers work. There is only one way to make 11 from the values 1, 2, 4 and 8, and that is 8 + 2 + 1. So 11 means walls on W, E and N, and no wall on S. Written in binary, 11 is 1011, and each digit is one flag.
11 & 2 gives 2, because the E flag is part of 11. 9 & 2 gives 0, because it is not part of 9. In an if, Python treats 0 as False and any other number as True.
Option C: a list of neighbours for each cell
Forget the walls. Store, for each cell, the cells you can step to from it.
open_to = {
(0, 0): [(0, 1)],
(1, 0): [(2, 0), (1, 1)],
(2, 0): [(1, 0), (2, 1)],
(0, 1): [(0, 0), (1, 1)],
(1, 1): [(0, 1), (1, 0)],
(2, 1): [(2, 0)],
}
(2, 0) in open_to[(1, 0)] # True -> you can step right
Computer scientists call this a graph. It is worth looking that word up.
How to read it. Each line says "from this cell, you can step to these cells". Cell (0, 0) has one neighbour, so it is a dead end. Cell (1, 0) has two.
Every opening appears twice. (0, 1) is in the list for (0, 0), and (0, 0) is in the list for (0, 1). If you can step one way, you must be able to step back.
How you would fill it. Not straight from the photo. You need to know where the walls are first. So this option is usually built from one of the others.
Option D: a grid of blocks
Make a bigger grid where walls take up squares too. Each square is either solid (1) or open (0). A maze w by h becomes a grid 2w + 1 wide and 2h + 1 tall.
grid = [[1, 1, 1, 1, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 1, 1]]
# cell (i, j) lives at grid[2*j + 1][2*i + 1]
grid[1][4] # 0 -> the square between cells (1, 0) and (2, 0) is open, so you can step right
How to read it. There are three kinds of square:
-
A square with an odd row and an odd column is a cell. Cells are always 0.
-
A square between two cells is an edge. It is 1 if there is a wall there and 0 if it is open.
-
A square with an even row and an even column is a corner post, where walls meet. Posts are always 1.
Cell (1, 0) is at grid[1][3] and cell (2, 0) is at grid[1][5]. The square between them is grid[1][4]. So a step from one cell to the next moves two squares, and the square you pass over must be 0.
How you would fill it. Start with every square set to 1. Set every cell square to 0. Then set an edge square to 0 wherever the photo shows no wall.
This one has a nice bonus. You can print it and see the maze:
for row in grid:
print("".join("#" if block else " " for block in row))
#######
# # #
# # # #
# # #
#######
How they compare
| Option | What you store | Good at | Watch out for |
|---|---|---|---|
| A. Two wall tables | One entry per edge | Matches how you read the photo, one edge at a time. Each wall is stored once | The two tables have different shapes. Easy to be off by one |
| B. Flags per cell | Four sides for every cell | Everything about one cell is in one place | Every inside wall is stored twice, once from each side. The two copies must agree |
| C. Neighbour lists | Where you can go from each cell | Answers the solver's question directly | Hard to spot a mistake by eye. You still have to read the walls first |
| D. Block grid | One square per cell, wall and corner | Easy to print and check. A move is just "is the next square a 0?" | Twice the size in each direction. Positions are in squares, not cells, so you convert back at the end |
You are not limited to one. A common plan is to read the photo into one form because it is easy to fill, then convert to another because it is easy to search. Converting between them is a good exercise.
There are other choices too. For example, the walls could be a list of line segments, each (x1, y1, x2, y2). That is handy for drawing the maze. How easy would it be to ask "can I step right?" with it?
Choosing one
Talk these through as a team before you write code:
Do not choose for speed or memory. Even a Hard maze is only 21 by 15, which is 315 cells. Any of these options handles that instantly. Choose the one your team finds clearest.
Check your maze before you try to solve it
A solver given a wrong maze gives a wrong route, and it will not tell you why. Make sure the maze in memory matches the maze on the wall first.
-
Start with a maze you typed by hand. Type the small example above into your chosen structure. One teammate can build the solver against it while another works on the camera.
-
Draw it back. Make your program draw what it read, either printed as text or as lines on top of the photo with
cv2.line. Compare it with the real maze by eye. This is the best bug-finder you will have. -
Is the outer wall solid? There should be no gap anywhere around the outside.
-
Count the openings. The practice maps have exactly one route between any two cells. A maze like that always has exactly
w * h - 1openings between cells. The example has 6 cells and 5 openings. If your count is off, a wall was misread. -
Can you leave Start? Can you enter Finish? Each needs at least one open side.
-
Write down your conventions. Is it
(column, row)or[row][column]? Does row 0 mean the top? Put the answer in a comment at the top of your code and stick to it. Mixing these up is the most common bug on this project. -
Test on more than one maze. Try every difficulty and a few different map codes. Code that works on one photo often fails on the next.
Why counting the openings works
Imagine building the maze by knocking holes in walls. You start in one cell. Each new hole lets you reach exactly one new cell. To reach all w * h cells you need w * h - 1 holes.
One hole more than that would join two cells that were already connected. That makes a loop, and a loop means two routes. One hole fewer would leave a cell sealed off. So the count is a quick test. A wrong count means at least one wall was misread. A right count is a good sign, not proof. It also cannot tell you which wall is wrong. Drawing the maze back is how you find that.
Drawing on a picture
These lines draw on a copy of the flat picture and save it, so you can check your program's answers by eye:
check = cv2.cvtColor(flat, cv2.COLOR_GRAY2BGR) # a colour copy, so the drawing can be in colour
cv2.line(check, (60, 60), (60, 120), (0, 255, 0), 2) # green line from (60, 60) to (60, 120), 2 pixels thick
cv2.circle(check, (390, 270), 6, (0, 0, 255), -1) # filled red dot at (390, 270), radius 6
cv2.imwrite("check.png", check)
-
Points are
(x, y), across first, and must be whole numbers. -
Colours are
(blue, green, red). So(0, 255, 0)is green and(0, 0, 255)is red. -
A thickness of
-1means "fill the shape in".
One useful check: draw a line on every edge your program called a wall. If the lines sit on top of every real wall and nowhere else, your wall reader works.
What is left for you
Once the maze is stored and checked, you still have to find the route from Start to Finish. Some questions to get you going:
Useful terms to look up: breadth-first search, depth-first search, flood fill and wall follower. Each is a different way to search a maze. Pick one your whole team can explain.
In one sentence each:
-
Breadth-first search spreads out from Start one step at a time, in every direction at once.
-
Depth-first search follows one corridor as far as it goes, then backs up to the last junction and tries another way.
-
Flood fill writes a number in every cell saying how many steps it is from one end of the maze.
-
Wall follower keeps one "hand" on a wall and walks. It is simple, but the route can be much longer than it needs to be, and on a maze with loops it can circle forever. The practice maps have no loops. Do you want your program to depend on that?
Whichever you choose, test it first on the small hand-typed maze from this page. You already know what the right answer looks like there.
Words used on this page
| Word | Meaning |
|---|---|
| ArUco tag | A square black-and-white marker that a computer can find in a photo and read as a number |
| Cell | One square of the maze, named by its column and row, (i, j) |
| Data structure | A way of arranging information in memory so a program can use it |
| Edge | The line between two neighbouring cells, where a wall may or may not be |
| Exposure | How much light the camera lets in for each picture |
| Frame | One picture from a camera |
| Graph | A set of things (here, cells) and the connections between them |
| Greyscale | A picture with one brightness number per pixel and no colour |
| Noise | Small random wobbles in pixel brightness that every camera adds |
| Perspective transform | Redrawing a tilted view of a flat surface as if seen straight on |
| Pixel | One tiny square of a digital picture |
| Slice | A rectangle cut out of a picture, written picture[row1:row2, col1:col2] |
| Threshold | A cut-off value: above it means one thing, below it means another |
Student guide · Page 3 of 6
Onshape
Drawing parts you can print
Onshape is the CAD program you will draw your parts in. This page teaches the tool on small practice parts. It does not show you the turret's parts, because those depend on the layout your team chose.
Set up
Onshape runs in a browser tab, so it works on almost any laptop or Chromebook and saves your work by itself.
- Create an account through Onshape for Education and pick the Student plan, which is free for students (Onshape education plans).
- Click Create, then Document, and give it your team's name.
- Open the document menu (top left) and set Workspace units to millimetre and degree.
- Look at the tabs along the bottom. A Part Studio is where you model parts. An Assembly is where you join parts with moving joints.
- Click Share and add your teammates. Everyone can edit the same document at the same time.
| To do this | Do this |
|---|---|
| Rotate the view | Drag with the right mouse button |
| Pan | Drag with the middle button |
| Zoom | Scroll. Press F to fit everything on screen |
| Look straight at a sketch | N |
| Start a sketch / start an extrude | Shift+S / Shift+E |
| Line, circle, dimension | L, C, D while sketching |
| Any other tool | Hover over its icon to see its name and shortcut |
Sketch, constrain, extrude
Almost every printed part is a flat outline pushed into a solid, then cut by more outlines. Learn this loop and you can draw most of a turret.
- Click Sketch and pick a plane (Top, Front or Right), or a flat face of a part you already have.
- Draw the rough shape. Start from the origin, the dot in the middle, so the part stays centred.
- Add dimensions (D) and constraints such as equal, parallel and symmetric until every line turns black.
- Click the green tick to finish the sketch.
- Click Extrude. New makes a new part, Add grows one, Remove cuts into one.
- Repeat on a face of the solid.
- Add Fillet and Chamfer last.
Hint: when a sketch will not turn black
Grab a blue point and drag it. The way it moves shows you which dimension or constraint is missing. If it turns red instead, you have added one rule too many. Delete the last one.
Variables: one number, many features
A variable is a named number. You type the name into a dimension in place of the number. When the number changes, every feature that uses it changes with it.
Create one with the Variable feature in the toolbar, or in the variable table panel on the right edge of the screen. Then type # and the name in any dimension box. Dimensions accept sums, such as #bearing_od + #fit_press.
A variable made with the Variable feature only works in features that come after it in the feature list. Drag your variables to the top of the list.
| Kind of variable | Example | Why it earns a name |
|---|---|---|
| A measured kit size | #bearing_od = 22 mm |
Used in several places. You may be given a different bearing |
| A fit clearance | #fit_press = 0.1 mm |
You will change it after your fit test |
| A choice you might revisit | #wall = 2.4 mm |
One edit thickens every wall |
| Something worked out from others | #a = #b + 2 * #wall |
Keeps parts matched to each other |
Model the kit parts first
Before you design anything printed, build simple models of the servo, the bearing and the laser. Then you can draw each pocket and hole around the real thing, and an assembly will show you clashes before the printer does.
For the servo:
- Measure the body, the mounting tabs and their holes, and where the output shaft sits.
- Sketch the body on the Top plane and extrude it to its height.
- Add the tabs and cut their holes.
- Add a cylinder for the output shaft, in the measured position, and a disc for the horn with its holes.
- Rename the part
Servo (reference)and give it a different colour, so nobody prints it by mistake.
Some makers publish a 3D model of their servo; the RDS3218 product page lists one (DSSERVO RDS3218). You can bring a file in with the + at the bottom left, then Import. Check it against your calipers before you trust it.
To place a second servo, use Transform with Copy part ticked. To draw around a reference part, use the Use tool (U) inside a sketch. It copies an edge of the servo into your sketch, so your pocket follows the servo if the servo changes.
Designing parts that print well
A 3D printer builds a part in flat layers from the bed upward. That one fact explains most of the rules below.
Holes shrink, so add clearance
These are common starting values for a 0.4 mm nozzle. Each is what you draw in CAD, not what you will measure on the print. Unless it says "each side", the value is added to the diameter or the full width. Your printer has its own numbers, and you find them with the fit test on the OrcaSlicer page.
| Joint | Start with |
|---|---|
| A bearing pressed into a pocket | Draw the pocket 0.1 mm larger than the bearing. It prints at about the bearing's size or a touch under, which is why it grips |
| A servo body in a pocket | 0.25 mm of gap on each side |
| Two printed parts that slide or turn | 0.3 to 0.5 mm of gap |
| An M3 screw passing through | 3.4 mm hole |
| An M3 screw cutting its own thread in plastic | 2.8 mm hole |
Slopes, overhangs and bridges
- Give every part one large flat face to sit on the bed.
- Keep slopes at 45° or steeper. The angle is measured up from the bed: a wall is 90° and a flat ceiling is 0°. The 45° figure is a common rule of thumb, and a well-cooled printer can manage less. On an edge that faces the bed, use a chamfer. A rounded fillet starts almost flat and prints badly.
- Print round holes with the hole pointing up. A hole on its side has a flat ceiling at the top.
- Chamfer the bed-side edge of pockets. The first layer is squashed and spreads outward by a fraction of a millimetre, which blocks a tight fit.
- Make walls at least 1.2 mm thick, and thicker where a screw or bearing pushes on them.
- Split awkward parts. Two simple pieces bolted together often print better than one piece that needs supports.
Assemblies: move it before you print it
An Assembly lets you join your parts with moving joints and drag them through their travel on screen.
- Click the + at the bottom left and choose Create Assembly.
- Click Insert and add parts from your Part Studio.
- Right-click the part that stays still and choose Fix.
- Join parts that are bolted together with a Fastened mate. Small targets called mate connectors appear at the centres of holes and faces as you hover.
- Join parts that turn with a Revolute mate.
- Open a revolute mate and set Limits to the range you want. Then drag the part.
- A Gear relation links two revolute mates by a ratio, so turning one turns the other.
- Check for clashes. Open Show analysis tools at the bottom right, choose Interference detection, and select the parts. Anything that overlaps shows in red (Onshape help: interference detection).
Dragging parts and looking is not proof that they clear. Run the check with each axis at both ends of its travel and in the middle.
Export for the slicer
- Create a version first, so the file you print matches a saved point in the design.
- Find the part in the Parts list at the bottom left of the Part Studio.
- Right-click it and choose Export.
- Choose STEP, then export. The file lands in your downloads folder.
- Put the part name and version in the file name:
bracket_v3.step.
STEP keeps curves as true curves, and OrcaSlicer opens it directly. If you need STL or 3MF instead, set the units to millimetre and the resolution to Fine. A coarse STL turns round pockets into polygons.
Words used on this page
| Word | Meaning |
|---|---|
| Sketch | A flat drawing made of lines, arcs and the rules that fix them |
| Constraint | A rule in a sketch, such as "these two lines are equal" |
| Extrude | Push a sketch into a solid, or cut with it |
| Part Studio | The tab where parts are modelled |
| Assembly | The tab where parts are joined so they can move |
| Mate | A joint between two parts in an assembly |
| Variable | A named number you can use in any dimension |
| Clearance | The gap you leave on purpose so parts fit |
| Chamfer / fillet | A flat cut across an edge / a rounded edge |
| STEP | A file format that keeps exact shapes |
Student guide · Page 4 of 6
Gears
Trading range for precision
Gears are optional. A turret with none can work. This page gives you the vocabulary, the three formulas, the steps for making a gear pair in Onshape, and the questions that decide whether gears are worth it for your team.
Do you need gears?
Gears let you trade something you have too much of for something you have too little of. Your servo turns 270°. The maze covers a small part of that. A gear reduction shrinks the range at the laser and shrinks every step by the same amount.
| Gearing down gives you | Gearing down costs you |
|---|---|
| Finer steps: the servo's best-case step is divided by the ratio | A new source of looseness: the gap between printed teeth is not divided by anything |
| Less servo looseness at the laser: play inside the servo is divided by the ratio too | More parts to design, print and line up |
| The servo can sit beside the axis, so a bearing can carry the load | Slower movement at the laser, by the same ratio |
Gear words and three formulas
| Term | What it means | Formula |
|---|---|---|
| Teeth, z | Number of teeth on one gear | |
| Module, m | Tooth size, in millimetres | |
| Pitch diameter | The circle where two gears roll on each other | m × z |
| Outside diameter | Tip to tip. This is what calipers measure | m × (z + 2) |
| Centre distance | How far apart the two axes must be | m × (z pinion + z gear) ÷ 2 |
| Ratio | Turns of the pinion for one turn of the gear | z gear ÷ z pinion |
| Tooth thickness | Width of one tooth at the pitch circle | 3.14 × m ÷ 2 |
| Pressure angle | The slope of the tooth's side. 20° is standard | |
| Backlash | The small gap between meshing teeth |
The curved side of a gear tooth is called an involute. You do not draw it by hand. The Onshape gear tool makes it for you.
Choosing your numbers
You have three numbers to choose: the ratio, the module and the pinion's tooth count. The gear's tooth count and every size then follow from the formulas. Each choice pulls against the others.
Ratio. This comes from your own table. A bigger ratio gives finer steps and less range.
Module. A bigger module gives thicker, stronger teeth that are easier to print, and a bigger gear. A 0.4 mm nozzle draws lines about 0.4 mm wide, and a tooth needs several lines across it to be solid. Use the tooth thickness formula to see how many lines fit. As a rough guide, module 1 is about the smallest that prints well on a 0.4 mm nozzle. Printers differ, so test yours.
Pinion teeth. Fewer teeth make a smaller pinion and a smaller gear. With the usual 20° pressure angle, a pinion with fewer than about 17 teeth needs the base of each tooth cut away before the gears can mesh properly. This is called undercut. It is slight at 15 teeth, which is acceptable for the light loads here. It grows quickly below about 12 teeth, where the teeth get weak and the motion gets rough.
Worked example. This shows the method. It is not a recommendation. Suppose a team picks a ratio of 4:1, module 1.5 and a 15-tooth pinion.
- Gear teeth: 15 × 4 = 60
- Pinion pitch diameter: 1.5 × 15 = 22.5 mm. Gear pitch diameter: 1.5 × 60 = 90 mm
- Centre distance: 1.5 × (15 + 60) ÷ 2 = 56.25 mm
- Range at the laser: 270° ÷ 4 = 67.5°
- Tooth thickness: 3.14 × 1.5 ÷ 2 = 2.4 mm, about six printed lines
A 15-tooth pinion has a little undercut. Zoom in on the base of its teeth in Onshape, and check on a test print that the pair turns without rubbing there.
Making a gear pair in Onshape
Onshape has no gear button in its standard toolbar. You add a custom feature called Spur Gear once, then use it like any other tool.
Add the Spur Gear feature
- In a Part Studio, click Add custom features at the right-hand end of the toolbar (Onshape help: custom features).
- Search for
Spur Gearand choose the one published by Onshape. - Click the feature to add it. Its icon now sits in your toolbar.
If the search finds nothing, your account may block public documents. Ask a club officer to open the Spur Gear document and share a copy with your team, then paste that copy's link into the search box.
Set up the centres
- Make variables for your module, your two tooth counts and the centre distance. Let Onshape work out the centre distance from the formula.
- Start a sketch on the plane the gears will lie in.
- Place a point where the pinion's centre will be.
- Place a second point, and dimension it one centre distance from the first.
- Finish the sketch.
Make the two gears
- Click Spur Gear. Set the input type to Module and enter your module variable.
- Enter the pinion's tooth count. Leave the pressure angle at 20°.
- Enter the gear depth. This is how thick the gear is.
- For the location, pick the pinion's centre point.
- Find the backlash setting and enter a small value. See the backlash section below before you choose it.
- Accept, then repeat for the large gear at the second point, with the same module, pressure angle and backlash.
The current version of the feature includes backlash, helical and double-helix options (Onshape forum: Spur Gear update).
Turn the blanks into parts
The tool gives you plain toothed discs. Everything else is ordinary sketching: a hub, bolt holes, a bearing pocket, a cut to remove teeth you do not need.
Joining gears to the servo and the axis
A servo's output shaft has a ring of tiny ridges called a spline. Many standard-size servos have 25 ridges on a shaft under 6 mm across, and some have 24 or another number (servo datasheet with spline size). Each ridge is far smaller than a 0.4 mm nozzle can form, so a printed copy of the spline does not grip.
Your servo came with horns that already fit the spline.
Hint: one approach some builders use
A strip of ordinary paper between the teeth while you tighten leaves a small, even gap when you pull it out.
Backlash
Backlash is how far the laser can rock while the servo holds still. You see it as the dot landing in a different place depending on which direction it arrived from.
Where it comes from
- The gap between your printed teeth.
- Looseness inside the servo's own gears, and between the horn and the spline.
- A bearing that is loose in its pocket or on its shaft.
- Parts that bend: thin arms, a wobbly base.
Measure it
Reducing it
There is no single fix. These questions each point at one.
Printing gears
The OrcaSlicer page explains the settings. These are the points that matter most for gears, and why.
- Print them flat, with the axis pointing up. Every tooth then has the same shape.
- Get the outline right. On a gear printed flat, the nozzle draws the tooth's outline, and that outline is the same on every layer. Thinner layers do not make it more accurate. A printer with its flow calibrated, so that lines are not too fat, and a slower outer wall both do.
- Make the teeth solid. Use enough wall loops that each tooth is filled with wall lines.
- Deal with the first layer. It is squashed against the bed and spreads outward, so teeth are fatter at the bottom. A small chamfer in CAD, or a slicer setting, fixes it.
- Watch the seam. The bump where each layer starts and stops should not sit on the side of a tooth that meshes.
| What you feel | What it suggests |
|---|---|
| Turns freely, with a slight rock | A good mesh |
| Tight all the way round, or will not go on | Teeth printed fat, or centres too close |
| Tight in one place, loose in another | A gear is off-centre, or there is a bump on one tooth |
| Loose, with a lot of rock | Too much backlash, or centres too far apart |
Words used on this page
| Word | Meaning |
|---|---|
| Pinion | The smaller gear of a pair. Here, the one on the servo |
| Module | Tooth size in millimetres. Both gears must match |
| Pitch circle | The imaginary circle on each gear where the two roll together |
| Centre distance | The distance between the two gears' axes |
| Reduction | Gearing that makes the output turn less than the input |
| Sector gear | A gear with teeth on only part of its edge |
| Spline | The ring of fine ridges on a servo's output shaft |
| Backlash | The play between meshing teeth |
| Mesh | How two gears' teeth fit together |
Student guide · Page 5 of 6
OrcaSlicer
Turning a model into a good print
OrcaSlicer turns your 3D model into the layer-by-layer instructions a printer follows. This page teaches the tool and shows what each important setting changes. It gives ranges, not final answers. Your printer and your parts decide the numbers, and short test prints are how you find them.
Set up
Every print job uses three presets, shown as three dropdowns on the left: Printer, Filament and Process. The Process preset holds the settings you will change most.
- Download OrcaSlicer from the official project, or use the copy already on the computer you print from (OrcaSlicer wiki and downloads).
- In the setup wizard, pick your printer model, its nozzle size and your filament. PLA is a good choice for this project. It is stiff and prints close to size.
- Pick a stock process to start from, such as
0.20mm Standard. - Switch on Advanced mode at the top of the Process panel. Several settings on this page are hidden without it.
- Drag your STEP file onto the plate.
- Work left to right: Prepare, then Slice plate, then Preview, then send the job to the printer.
Orientation comes first
Which way up a part sits on the bed decides how strong it is, how round its holes are and whether it needs supports. Choose it before you touch any other setting.
Select the part, use Lay on face (F), then click the face that should touch the bed.
Four things to weigh:
- Do not let the load pull layers apart. A print splits between layers far more easily than across them. A long arm is strongest when its layers run along its length.
- Round things print best pointing up. A hole or gear with its axis upright comes out round.
- A big flat face on the bed sticks well and needs no supports.
- Surfaces that must fit should not sit on supports. A pocket that rested on support material comes out rough.
These four often disagree. A part might want one orientation for strength and another for a round hole.
Settings that matter
Start from a stock process and change as little as you can. Each setting below is a trade. The ranges are typical for PLA and a 0.4 mm nozzle.
| Setting | Where to find it | Typical range | What it trades |
|---|---|---|---|
| Layer height | Quality | 0.12 to 0.28 mm | Thinner layers give smoother slopes and take longer |
| Wall loops | Strength | 2 to 5 | More walls give a stronger, heavier, slower part |
| Top and bottom shell layers | Strength | 3 to 5 | More layers give stiffer flat faces |
| Sparse infill density | Strength | 10 to 40% | More infill gives more stiffness and more time |
| Precise wall | Quality, Precision | On or off | Adjusts wall spacing for sizes closer to the drawing |
| Elephant foot compensation | Quality, Precision | 0 to 0.2 mm | Shrinks the squashed first layer |
| X-Y hole compensation | Quality, Precision | From your fit test | Grows or shrinks every hole |
| X-Y contour compensation | Quality, Precision | From your fit test | Grows or shrinks every outside edge |
| Seam position | Quality, Seam | Aligned, Back, Nearest, Random | Where each layer's start-and-stop bump goes |
| Brim | Others | On or off | Extra grip on the bed, and an edge to trim off afterwards |
A layer count means different things at different layer heights. Three top layers at 0.12 mm make a skin 0.36 mm thick. Three at 0.28 mm make 0.84 mm. OrcaSlicer also has shell thickness settings that set a minimum in millimetres, which is the safer way to think.
The setting names and what they do are from the OrcaSlicer documentation (precision settings).
Supports and the preview
The best support is the one you designed out. If a part still needs support after you have tried every orientation, keep it away from the surfaces that have to fit.
- Tick Enable support in the Support section of the process.
- Tree (auto) suits curved overhangs. Normal (auto) suits flat ones.
- On build plate only stops supports from starting on the model's own surfaces, so they can only grow up from the bed (OrcaSlicer support settings). A hole or pocket that opens onto the bed can still fill with support. To keep a fit surface clean, use a support blocker or the painting tool, then check the preview.
- The support painting tool in the top toolbar lets you choose exactly which faces get support.
- A flat ceiling across a short gap is called a bridge. As a rough guide, bridges up to about 20 mm often print without support. Cooling and speed change this, so test one.
Always read the preview
After slicing, drag the vertical slider through the layers. The preview shows you exactly what the printer will do.
Find your printer's fits
A printed hole comes out a little smaller than the drawing, and by a different amount on every printer. You cannot look the number up. You have to measure it.
A fit test is a small, quick print with the same feature repeated at several sizes. You try your real bearing, servo or screw in each one and see which feels right. Then you put that size into your Onshape variables.
Both compensation values act on each side of a feature. A hole that measures 0.2 mm too small across needs 0.1 mm of hole compensation (hole compensation walkthrough).
OrcaSlicer also has a built-in test piece. In the Prepare tab, right-click the plate, choose Add Handy Model, then pick the Orca Tolerance Test. You print it, measure it with calipers and use the result to set the two compensation values (OrcaSlicer tolerance calibration). Menus move between versions, so if yours looks different, check that page.
The Calibration menu has temperature, flow rate and pressure advance tests. If your prints look rough or over-filled, ask whoever looks after the printer whether these have been run for your filament.
When a print goes wrong
| What you see | Likely cause | What to try |
|---|---|---|
| A bearing starts into its pocket, then jams near the bed side | First layer spread outward | Elephant foot compensation, or a chamfer on that edge |
| Every hole is too small | Normal for this kind of printing | More clearance in CAD. If everything is off equally, hole compensation |
| Gears are tight all the way round | Teeth printed fat, or centres too close | Check the first layer and the flow. Check the centre distance |
| Gears are tight at one spot each turn | A gear is off-centre, or there is a seam bump on a tooth | Check how the gear is located. Move the seam |
| Teeth break off | Teeth hollow inside, or gear printed on edge | More wall loops. Print flat |
| An arm snaps along a layer line | Layers run across the load | Change the orientation, or thicken it |
| Corners lift off the bed | Poor first-layer grip | Clean the plate. Add a brim to that part |
| A rough, drooping underside | Overhang too shallow, with no support | Re-orient, add a chamfer, or enable supports |
| Fine hairs between features | Damp filament or nozzle too hot | Dry the filament. Lower the temperature a little |
| A round pocket looks faceted | Coarse STL export | Export STEP, or STL at Fine resolution |
Words used on this page
| Word | Meaning |
|---|---|
| Slicer | A program that cuts a 3D model into layers and plans the printer's path |
| G-code | The list of instructions the printer follows |
| Layer height | How thick each printed layer is |
| Wall loop | One line of plastic around the outline of a layer |
| Infill | The sparse pattern that fills the inside of a part |
| Overhang | Part of a layer with nothing underneath it |
| Bridge | A flat span printed across a gap, held at both ends |
| Support | Throwaway material printed to hold up an overhang |
| Seam | The spot on each layer where the nozzle starts and stops |
| Elephant foot | The first layer spreading wider than the layers above |
Student guide · Page 6 of 6
Arduino and servos
Making a servo go where you say
This page explains how an Arduino tells a servo where to go. It gives you small pieces of working code, each explained line by line. It does not give you the program that drives your turret. Putting the pieces together, and deciding what your computer says to your Arduino, is your work.
Before you start: the Arduino IDE
An Arduino program is called a sketch. You write it on your laptop in a free program called the Arduino IDE. The IDE also checks the sketch for mistakes and sends it to the board over the USB cable, which is called uploading. After that, the sketch runs by itself whenever the Arduino has power, even with the laptop unplugged.
Sketches are written in C++, with extra commands that Arduino adds, such as pinMode and delay. This is a different language from the Python used on the Maze solver page. The Start here page has a short table of the differences.
Set it up and prove it works
Do this before you wire anything. It proves the laptop, the cable, the IDE and the board all work together.
- Install Arduino IDE 2 from the Arduino software page. On a college laptop, check first whether it is already installed.
- Plug the Arduino into the laptop with a USB data cable. Some cables only carry power for charging, and with one of those the board never shows up.
- Open the IDE. Click the board selector at the top of the window, then choose your board and its port. If the IDE offers to install a board package, say yes. The same choices are under Tools, Board and Tools, Port.
- Open File, Examples, 01.Basics, Blink.
- Click Upload, the arrow button. After a few seconds, a small light on the board should blink once a second.
- Change both
delay(1000)lines todelay(200)and upload again. The light should blink faster. You have now edited a program and watched the board obey it.
These steps follow Arduino's own instructions (Arduino help: upload a sketch).
How to read a sketch
void setup() { ... }runs once, when the board powers up or restarts.void loop() { ... }runs after that, again and again, for as long as the board has power.- Every instruction ends with a semicolon. Leaving one out is the most common error.
- Curly brackets
{ }group lines together. //starts a comment. The board ignores everything after it on that line.- A new variable needs its kind stated first:
int us = 1500;makes a whole number calledus. - Verify, the tick button, checks a sketch without uploading it. Error messages appear at the bottom of the window. Fix the first one first, because later ones are often caused by it.
What a servo is
A hobby servo is a small motor, a set of gears, a position sensor and a control board in one box. You tell it an angle. Its own board runs the motor until the sensor says the shaft is at that angle, then holds it there.
It has three wires:
| Wire colour | Job |
|---|---|
| Red | Power, usually 5 to 6 V |
| Brown or black | Ground |
| Orange, yellow or white | Signal from the Arduino |
The signal
The signal is a short pulse of voltage, repeated about 50 times a second. The width of the pulse is the command. Width is measured in microseconds (µs). There are a million microseconds in a second.
Those three numbers are typical for a 270° servo (DS3218 specifications, DS3235-270 datasheet). Yours may differ, so check its datasheet.
Pulse explorer
Drag the slider to change the pulse width and watch where an ideal servo would point. This shows the idea only. Your real servo will be a little different, which is why you measure it.
Two more facts follow from how the signal works:
- A servo ignores very small changes. Datasheets call this the dead band, and list 2 to 5 µs for servos of this kind. It is the smallest change the servo notices at all. What the dot really does is usually worse, because of friction and looseness, and you have to measure it.
- This is not
analogWrite(). The Arduino Servo library makes these pulses with a timer, and can use any digital pin (Makeability Lab: servo motors).
Wiring and power
Two rules prevent most servo problems. Power the servos from their own supply, not from the Arduino's 5V pin. And join all the grounds together.
| Connection | Goes to |
|---|---|
| Each servo's signal wire | Its own Arduino digital pin |
| Both servo power wires (red) | Positive of a separate supply |
| Both servo ground wires | Negative of that supply |
| Supply negative | Also to an Arduino GND pin |
| Arduino | USB cable to the computer |
Wire colours vary between servo makers, so check yours against the table at the top of this page.
Why a separate supply
A servo draws a burst of current each time it starts to move, and far more if it is blocked. Datasheets list about 0.8 to 1.1 A for a blocked 9 g micro servo and 1.8 to 2.6 A for a standard-size DS3218 (micro 270° servo datasheet, DS3218 specifications). A standard USB port is built to supply about 0.5 A, and the Arduino's own parts pass on even less. That is nowhere near enough.
A large capacitor (470 to 1000 µF) across the servo supply, close to the servos, smooths the shortest bursts. Its stripe marks the negative leg, and its voltage rating must be higher than your supply's voltage.
A capacitor only fills in dips that last a few thousandths of a second. It cannot make up for a supply that is too small.
Why the grounds must be joined
A signal is a voltage measured against ground. If the servo and the Arduino do not share a ground, the servo has nothing to measure the pulse against. It twitches, or ignores you.
Building block: move one servo
Get one bare servo moving on the bench before any mechanism exists. Wire it as above, with nothing on the horn, and upload this.
#include <Servo.h>
Servo pan;
void setup() {
pan.writeMicroseconds(1500); // choose the centre before the signal starts
pan.attach(9, 500, 2500); // pin, shortest pulse, longest pulse (microseconds)
}
void loop() {
for (int us = 1200; us <= 1800; us += 5) { // sweep one way
pan.writeMicroseconds(us);
delay(20);
}
for (int us = 1800; us >= 1200; us -= 5) { // and back
pan.writeMicroseconds(us);
delay(20);
}
}
What each part does:
#include <Servo.h>loads the Servo library. It comes with the Arduino IDE.Servo pan;makes one servo object. You will want a second one for the other axis.pan.writeMicroseconds(1500);sets the pulse width. Written beforeattach, it decides where the servo goes first.pan.attach(9, 500, 2500);starts the pulses on pin 9, and sets the shortest and longest pulse the library will send.- Each
forloop changes the pulse by 5 µs, waits 20 ms, and repeats.delay(20)waits 20 thousandths of a second.
Two details catch people out:
- Always give
attachthe range. Without it the library limits pulses to 544 to 2400 µs, and a 270° servo never reaches the ends of its travel (Servo.attach reference). - Write before you attach. When
attachruns, the pulses start at the last value written, or at the middle if nothing was. Writing first means you choose that first target. It does not make the start gentle. A servo cannot report where it is resting, so it still snaps to the target from wherever it was. Power up with the laser off and the mechanism clear.
Angles and microseconds
The Servo library has two ways to command a servo. write(angle) takes degrees. writeMicroseconds(us) takes a pulse width.
write() assumes a 180° servo. It spreads the numbers 0 to 180 across whatever pulse range you gave attach. On a 270° servo, write(180) sends the longest pulse, and the servo turns 270°. The number is not the angle. It also only takes whole numbers, so each step is about 11 µs, or 1.5° of real movement.
Worked example. A 270° servo over 2000 µs moves 0.135° for each microsecond, so one degree is 7.41 µs. To turn that servo 40° past centre, add 40 × 7.41 = 296 µs to 1500, giving 1796 µs.
Building block: listen to the computer
The Arduino and your computer talk over the USB cable using serial: a stream of characters, one after another. Both ends must agree on the speed, called the baud rate.
This sketch reads one line of text and sends it straight back. It does nothing useful, and that is the point. It proves the link works before anything depends on it.
void setup() {
Serial.begin(115200); // open the link at 115200 baud
}
void loop() {
if (Serial.available()) { // has anything arrived?
String line = Serial.readStringUntil('\n'); // read up to the end of the line
line.trim(); // remove stray spaces and line endings
Serial.print("got: ");
Serial.println(line); // send it back
}
}
What each part does:
Serial.begin(115200)opens the link. The number must match the one on the computer's side.Serial.available()says how many characters are waiting. Zero counts as false, so theifskips when nothing has arrived.Serial.readStringUntil('\n')collects characters until it sees a newline, the invisible character your keyboard sends for Enter.line.trim()removes spaces and leftover line-ending characters from both ends.Serial.printlnsends text back, with a newline after it.
Pulling numbers out of a line
A line arrives as text. Your servos need numbers.
int a = 0, b = 0;
int found = sscanf(line.c_str(), "%d %d", &a, &b); // found = how many numbers it managed to read
"%d %d"is a pattern: a whole number, a space, another whole number.&aand&bsay where to put the two numbers.foundis 2 if both were read. Check it before you useaandb.
For a single number, line.toInt() is enough. It returns 0 when the text is not a number, so hello and 0 give the same answer.
Keeping a number in range
int safe = constrain(value, 1000, 2000); // never below 1000, never above 2000
constrain returns value unless it is outside the two limits, in which case it returns the nearer limit.
This suits a real number that is slightly too large. Is it also the right response to a garbled message? A scrambled line that reads as 0 would be clamped to your lower limit, and the turret would swing there. The design questions below come back to this.
The computer's end
This is the laptop's half, shown in Python. The pyserial library opens the same link. Install it once by typing python -m pip install pyserial in a terminal, as described on the Maze solver page. If your laptop program is in another language, look for that language's serial port library.
import serial
import time
port = serial.Serial("COM3", 115200, timeout=1) # on a Mac or Linux the name looks like /dev/ttyUSB0
time.sleep(2) # the Arduino restarts when the port opens. Give it time
port.write(b"hello\n") # send one line. The b means "as raw bytes"
print(port.readline()) # read one line back
port.close()
Designing your own messages
Your computer and your Arduino need a shared language. Nobody hands you one. A message can be as simple as two numbers on a line.
Limits and smooth moves
Soft limits
A servo pushed against a hard stop draws its full blocked current and gets hot. Your turret has places it cannot go: a gear running off its teeth, a cradle hitting an arm. A soft limit is a line in your code that refuses to send the servo there.
Small steps
Ask a servo to jump a long way and it runs there at full speed, then overshoots and wobbles. Ask it for many small steps, one every 20 ms or so, and you decide the speed.
From a maze position to two pulse widths
Your solver will produce a route in maze cells. Your servos want microseconds. Something in your program has to translate one into the other, and it has to be right for your turret, in your room, on the day.
This is the hardest open problem on the hardware side, and there is more than one way to solve it. The run rules limit what you may do during a run and during the calibration time before it, so read them before you choose.
Useful terms to look up: interpolation, lookup table, calibration.
Switching the laser
One Arduino pin can turn the laser on and off. The pin has to be set up as an output first.
const int LASER_PIN = 7;
void setup() {
pinMode(LASER_PIN, OUTPUT); // make the pin able to drive something
digitalWrite(LASER_PIN, LOW); // start with the laser off
}
After that, digitalWrite(LASER_PIN, HIGH) switches the laser on and LOW switches it off.
How you wire it depends on the laser you were given.
| Your laser | How it can be switched |
|---|---|
| A laser module with two wires, marked for 5 V | Through a small transistor, as below |
| A module on a small board with three pins | Power the board from 5 V and GND, and connect its signal pin to the Arduino pin. Test whether HIGH or LOW switches it on, because boards differ |
| A battery pointer with a push button | Ask the club before you modify it. Until it can be switched, block the beam with a piece of matte card when it should be "off" |
| A bare laser diode: a small metal can with no circuit board | Do not connect it to 5 V. It needs a driver circuit that limits its current. Ask the club |
An Arduino pin can supply only a small current, and a laser module can need more. A transistor lets a small current from the pin switch a larger one. For a small module, a common NPN type such as the 2N2222 with a 1 kΩ resistor between the pin and its base is a usual starting point.
Two things to check when you build it:
- The legs. A transistor's three legs are called E, B and C, and their order differs from one part to another. Look up the pinout for the exact transistor you have.
- The ground. The Arduino's GND must join the transistor's E leg, as in the figure. Without that wire, the pin's signal has nothing to push against.
Optional: a 10 kΩ resistor from the Arduino pin to GND holds the laser off during the moment the Arduino restarts.
Laser safety
The full list is in the laser safety box on the Start here page. The points that matter most while you are wiring and coding:
- Never look into the beam, and never point it at anyone.
- Keep the laser off while your face is near the turret. Make "laser off" the first thing your sketch does.
- "Off" means powered off, or blocked by a piece of matte card. Aiming it somewhere else is not off.
- Watch for shiny surfaces near the maze wall that could send the beam back into the room.
When it does not work
| What you see | Likely cause | What to try |
|---|---|---|
| The Arduino restarts or USB drops out when a servo moves | Servos powered through the Arduino | Give the servos their own supply |
| A servo twitches at random or ignores commands | Grounds not joined, or noisy power | Connect supply negative to Arduino GND. Add the capacitor |
| A servo hums and gets warm while standing still | It is pushing against a stop or a tight gear mesh | Tighten your soft limits. Loosen the mesh |
| A servo never reaches the ends of its travel | attach called without a range |
Give attach the shortest and longest pulse |
| A servo jumps when the power comes on | It is moving from where it rested to its first target | This is normal. Choose the first target by writing before attach, and rest the turret near that position before power-up |
| Nothing comes back over serial | Wrong baud rate or line ending, or the port is in use | Match the baud rate. Close the Serial Monitor |
| Upload fails on a Nano | Some Nano boards need a different setting | Try Tools, Processor, ATmega328P (Old Bootloader). Ask about the CH340 driver |
| The dot lands differently depending on direction | Backlash | See the Gears page |
| The dot shakes while holding still | Parts flexing, or too much weight on the axis | Stiffen the holder. Lighten what moves |
Words used on this page
| Word | Meaning |
|---|---|
| Pulse | A short burst of voltage on the signal wire |
| Microsecond (µs) | One millionth of a second |
| Dead band | The smallest change in pulse width a servo responds to. A best case, not a promise |
| Sketch | An Arduino program, written in C++ |
| Arduino IDE | The free program used to write a sketch and upload it to the board |
| Upload | Sending a sketch from the laptop to the board over USB |
| Library | Ready-made code you can use, such as Servo.h |
| Serial | Sending characters one at a time over a cable |
| Baud rate | The speed of a serial link. Both ends must match |
| Newline | The invisible character that ends a line, written \n |
| Ground (GND) | The shared zero-volt reference every signal is measured against |
| Soft limit | A limit enforced by code, to stop a mechanism reaching a hard stop |
| Calibration | Measuring how your real hardware behaves, so your code can allow for it |