A completely over-engineered line following robot


Seahorse is a line following robot, the second one I designed and built. The first was Crabby, a nice, fun, and functional line follower. Seahorse builds upon my experience with Crabby with the intention of improving my skills at design, planning, and machining with the secondary goal of possibly building a faster more accurate line follower than the first one. While not breaking any speed records, Seahorse is a smashing success across the board. Lots of lessons learned, design decisions that worked exactly as planned, and a few surprises along the way.

Seahorse uses five phototransistors to measure light reflected off the floor emitted by white LEDs. An Adafruit Pro Trinket 5v controller (like a small Arduino UNO) runs the show by reading the sensors and driving two DC gear motors using differential drive. By invoking a simple calibration routine, Seahorse can handle either light or dark lines.

I find myself continuously amazed with robotics and how much detail you can choose to put into the simplest of creations. This project was characterized by slow, patient progress with an attempt to be thoughtful and deliberate with each step of the way. Surely a robot can be built faster and better but the joy in this hobby is about the journey and not the destination.



custom standoffs

The body is made of two sheets of hobby airplane plywood held together (and apart) by six aluminum standoffs. All other parts of the robot attach with machine screws to either the top sheet (for user interface components) or the bottom sheet (for control, sensor, and drive components).

As a novice machinist I often need to remind myself to go slow and simplify things whenever possible. A small example is drilling the six holes in each piece of plywood for the standoffs. To improve alignment between the top and bottom layers, I clamped them together during drilling yielding perfect alignment later when all the guts were installed. Seems obvious to experienced folks, but each little action like this adds to the overall quality of the end product.

The standoffs are custom built from 3/8” rod, measured to the minimum height to clear the motor mounts and added a few millimeters. Cut six lengths of rod by hand with hacksaw and a little miter box. Filed them by hand to get the lengths as exact as possible. Used my 3/8” drilling fixture to drill holes in either end and tapped with 4-40 machine threads. Making these standoffs was a test of patience. I kept wanting to rush to get through them, but reminded myself to slow down and enjoy it. I easily could buy some off the shelf items or screw together a bunch of nylon standoffs sitting right here, but that wasn’t the plan. The resulting standoffs are great even if the holes are not exactly centered.

gorilla glue strengthens the body

The 4-40 threads on the standoffs are a minor nuisance. Everything else on the robot is metric M3, with 12 4-40 screws thrown in to attach the body sheets to the standoffs. Since then I got a M3 tap, but at the time only 4-40 was available. The nice thing about modular construction is that I can replace the standoffs with M3 without impacting any other part of the robot. Probably won’t do it but nice to know :-)

The only place glue was used is on the very front of the bottom plywood. After I nibbled out the rectangle for the sensor view port, the thin strip of plywood along the front felt feeble. Gorilla glued two craft sticks to top and bottom and it is nice and sturdy. Important because this is the part of the robot that will receive the shock in the event of a collision. The picture shows clamps in place during glue drying.

Light Baffle and Floor Skid

light baffle and floor skid

No, neither the name of a new action movie nor a witche’s brew incantation, but rather the white thing at the front of the robot, wrapping to the bottom and contacting the floor. This serves two purposes. First it is a baffle that blocks some ambient light from hitting the light sensors. In actuality this does not do much because of the way there is yet another baffle under the robot blocking light right around the sensors. Second it is a low friction skid that slides on the floor. When I first ran it without this attached, the wood frame along the carpet floor was a recipe for disaster and stalls. This is made from a cut up chinese food container and attaches by two of the 4-40 screws that also connect the body bottom to the standoffs.

The two circular items are furniture slides which act as the skids when the wheels are mounted in low-robot configuration. When in high-robot config (normal usage), these skids do not touch the floor.

Motor Mounts

motor mount

First I used a sheet of transparency, a box cutter and a sharpie marker to create a motor template to indicate the locations of the motor shaft bearing and mounting screw holes. Lay this over some aluminum angle stock and mark the positions. Flip over for a mirror image to create the other motor mount. Hack saw by hand using miter box, file smooth. Drill out four M3 holes in the bottom to attach the mount to the body, and two 4M holes in the face to attach the motor to the mount (with a bit of wiggle room for alignment).

The motor output shaft/bearing is off center, so I cut two bearing holes into each mount. Using the same two screw holes, I can either mount the motor shaft up high giving a low riding robot, or shaft down low giving a higher up robot body. This played out perfectly as I tested the robot out on different surfaces. On nice smooth tiles, the high-shaft-low-robot position was very nice, but on carpet the body rubbed and caused too much friction with the floor. Very simple to unscrew the motors and flip them over. Again, reinforcing my approach of modular design for flexibility and ease of maintenance.

Wheel Couplers

wheel couplers made from aluminum and lego

As is customary for my robots up to this point, I am using LEGO wheels and custom built wheel couplers, following the style documented in David Cook’s books Robot Building for Beginners and Intermediate Robot Building. This particular set was made using the IRB method. Start by creating a fixture to hold the raw rod for the coupler. Then drill out the two sides with nice alignment of the centers of the motor output shaft and wheel axle. Drill and tap a hole for a set-screw and then epoxy in a Lego axle.

The technique is very nice, but I got impatient with myself and my tools and rushed through part of it. As a result, the alignment of my couplers is not the best. Luckily this does not cause much problem with the low-performance robot. And not luckily but rather by design, if I ever choose to, i can carefully make a new set to replace these without any impact to the rest of the robot. The beauty of modular design. That having been said, this set works well enough I probably will not replace them but rather remember it as a lesson to learn and pay attention to when I make my next robot.

User Interface

cardboard user interface prototype

Cardboard Prototype

To help lay out all the parts, I first made a prototype of the top body plate out of cardboard. The pic is missing the line indicators, but they made their way into the prototype too. You can see pencil outlines for where space is reserved for the motor mounts and standoff placement. In the final construction, I added 0.25 inch between the buttons and the motors to ensure the motherboard’s vertical height (pushing up from the bottom) did not interfere with the button vertical height (dropping down from the top).

Line Indicators

Five indicators are used to show the operator what the robot thinks each sensor is seeing, either background or line. I used NeoPixels from Adafruit instead of normal LEDs for two reasons. First, NeoPixels are individually addressable so 5 can be controlled using a single microcontroller pin. Second, NeoPixels can be set to any color, giving some fun for the kid to pick and choose colors. As adults, we spend all our energy making a functional robot but the real entertainment value for the audience comes from a simple color choice.


LCD display shows three values for each of the five sensors. Row 1 and 3 (or 0 and 2 if you like zero-based indexes) show minimum and maximum values encountered since power-on. Row 2 shows current value. Technically there’s a lag between sensor values and display output because I want my program paying attention to the line and not to showing me some values. Bottom row shows the program mode or instructions during calibration, a battery ADC reading, whether calibration thinks the line is dark or light, and the 0/1 state of both of the buttons (more for debugging the debounce routine but I left it in there).

All values (min/max/current sensor and battery) are ADC values which means integers from 0-1023 in Arduino. To save some space without really hurting anything I map the values down to 0-999 to fit 3 digits. This means my output is a tiny bit skewed from the real values the processor is using but the difference is not enough to change the decisions either I (as the author of the code) or the microcontroller (as the executor of the code) make.


  • Power switch is a DPDT connecting battery positive and negative through the poles. In the off position, both battery terminals are disconnected, totally isolating the LiPo battery from the electronics, allowing charging without physically unplugging the battery connector.

  • Black button is the play/pause button to toggle between RUN and IDLE. Gotta be quick to press it as the fast little robot scoots on by.

  • Red button enters CALIBRATE mode. LCD provides instruction to user to first place the sensors on the line and press red button again. Then LCD instructs to place sensors on the background and press red again to complete calibration and return to IDLE.


internal electronic layout

The electronics for Seahorse are broken into three separate homemade PCBs. All were designed and printed using Robot Room Copper Connection (sadly does not exist anymore) and then exposed to UV sensitized copper clad board using Bronzer, etched in HCL/H2O2, drilled using Dremel drill press and then hand soldered components and jumpers (to fake 2 layers when really only using 1 layer). Daughter boards have connecting wires soldered directly to the board, which are then received via nice connectors on the motherboard. This allows easy reconfiguration and disassembly without wasting too much space, money, precious inventory of connectors.


Seahorse uses a single LiPo 2S 7.4v battery. Raw battery power is provided to three distinct circuits: the Pro Trinket controller, the motor driver, and a voltage regulator that powers everything else. Initial testing shows the battery is draining much faster than with Crabby due to heavier construction, larger motors, faster speed, brighter outputs. There is plenty of space in the battery compartment to upgrade to a higher capacity battery if I can’t get better run times out of this puppy.



Main features of this little beauty are:

  • Adafruit Pro Trinket 5v controller, positioned specifically to allow easy access to the USB for programming. When you first connect the USB, the trinket goes into a brief “looking for incoming program upload” mode. If you miss that, you need to reset the board to get back into programming. To avoid some of this, I ran a jumper out to the edge for the reset pin. Allows me to connect a little button during intense programming sessions, and then remove it so kids with grabby hands wont reset the robot whenever they touch it during play time.

  • SN754410NE Motor Driver, driven using two direction pins plus PWM on the enable for each motor. 6 pins control pins is tough, I could have rigged it to reduce pins but this worked out fine. I’ve burned these chips out in the past with a reverse battery so this board has (A) reverse battery protection MOSFET and (B) solderless socket for the motor driver in the event it needs to be replaced.

  • Voltage Regulator for logic and display components.

  • Test Points for: battery voltage, total current usage, motor current usage.

  • M3 mounting holes. There are 8 of these, only 4 in use. Its goofy but I like to build in an Arduino mounting pattern on my boards, just in case I either need to attach my board to an Arduino, or if I need to throw my board away and want to put an Arduino in its place (I’ll already have the mounting hardware in place).

  • Connectors galore. Polarized ones where it matters and simple straight ones elsewhere.

Line Indicator Board

NeoPixel short circuit

A very simple board with 5 NeoPixels in a 5mm LED package. The image shows why you always test your homemade PCBs with a continuity checker. Truth be told, I visually identified the short before even turning on the multimeter but did test it out after scraping the bridge away with a razor. This guy mounts to the top plate of the robot body using M3 screws and little nylon standoffs. On the PCB, the pixels are perfectly aligned. The holes in the surface of the robot are not perfectly aligned. This was for entertainment value, the kid likes little quirks more than perfect alignment, so I nudged my clamps a bit when drilling two of the holes. Had to cross my fingers when press fitting the PCB into the holes to be sure I wouldn’t apply too much pressure and snap the thing, but it went in like a charm and looks quirky enough to be cute without impacting its ability to inform the user what the line visibility status is.

Sensor Board

sensor board under construction
sensor board final result

The sensor board holds five copies of the following:

  • White LED floor illuminator

  • 10k Variable trimpot resistor to alter the brightness of the white LED

  • TSL257-LF Phototransistor

The white plastic assembly is a Lego brick with its heads cut off and most of its guts cut out. Makes a nice organized baffle allowing each light sensor only to see the narrow region below. This whole setup gets positioned on the floor plate of the robot, looking through a rectangular portal nibbled out of the plywood. M3 screws and nuts keep things in place and allow easy disassembly in the event I need to (A) replace all trimpots with larger value to avoid saturation or (B) debug a funky light sensor (which turned out to be an OK sensor but a funky jumper wire connecting from the board back to the motherboard).

Regulated Circuitry

There is a 5v regulator providing power to the electronic sub systems. Right out of the regulator the power is split to the LCD display and to a MOSFET transistor switch which then connects to the NeoPixels, the floor illuminating LEDs, and the motor controller logic voltage. Using the MOSFET allows the microcontroller to shutoff unnecessary drains when LOW BATTERY condition occurs. I left the LCD connected directly to regulated power rather than through the transistor so the LCD remains powered on and can display LOW BATTERY to the user. This approach is overkill, would have been fine to skip the MOSFET entirely, but I was having fun.

To make sure I sized the regulator appropriately, here is the load being powered by a LM2940CT-5.0 (capable of 1A constant supply):

ComponentCurrentCalculation Method
LCD Display with LED backlight36mAMeasured
TSL257-LF Phototransistor sensors and floor illuminating LEDs100mAMeasured current with brighness turned all the way up, normal case is much less than this, phototransistors are saturated at this brightness.
NeoPixel Line Indicators300mADatasheet max current listing if all 5 are on full brightness, white color. Actual usage is much much less (brightness set to 5 out of 255, very dim but still nicely visible, painful to look at if you turn up to 255)
SN754410NE Motor Driver (logic side)28mADatasheet says up to 70mA, but testing a variety of motor conditions maxed out at 28mA
Total464mAMath - simple addition

Control and Logic

The brains of this operation is an Adafruit Pro Trinket 5v microcontroller board. It is like a small Arduino UNO, and is programmed via the Arduino IDE. I tried to keep the program as simple as possible. Each time through the loop the program will check for user button presses (debounced in software), update the sensor ADC values, adjust the motor speeds and directions. Periodically, it will update the LCD display, check the battery voltage, and update the NeoPixel output.

The algorithm in use as of this writing consists of a few if-else statements.

  • If one of the extreme sensor sees the line, near motor stop, far motor slow forward.
  • If one of the inner-but-not-middle sensors sees the line, near motor slow forward, far motor full forward.
  • If only the middle sensor sees the line, both motors full forward.
  • If no sensor sees the line, two choices:
    • If last sighting was the middle sensor, then both motors slow reverse (so it backs straight up to hopefully find the dead end it just flew off of).
    • If last sighting was to either side, near motor slow reverse, far motor slow forward (rotate back towards the line).

Lots of trial and error to configure what SLOW and FULL speeds mean in terms of PWM duty cycle. The values in the final version work well at a full battery charge on my carpet using the not too sharp turns in the tape on my floor. I could slow it down and have more reliable performance on a wider variety of surfaces if I am ever showing off to family and friends outside of my home.

Parts List

This is more for my amusement than anything else. As I built this little project, was continuously amazed at how many individual parts went into it. Decided to list everything for fun. 245 individual pieces. Not to mention how many tools and supplies go into the creation of even a simple albeit overengineered robot.

Wheels and Couplers2Lego Wheels
2Lego Axles
2Pieces of 3/8” aluminum rod
24-40 set screws
Motors and Mounts2DC gear motors
2Aluminum angle brackets
4M3 motor screws
4M3 motor washers
4M3 motor lock washers
8M3 mount screws
8M3 mount nylon lock nuts
4jumper wires
4jumper wires crimp connector
2jumper wires crimp housing
Body2Airplane Plywood sheets
2Craft wood sticks
63/8” Aluminum rod standoffs
124-40 standoff screws
1Chinese food baffle skid
2Velcro straps to retain the baffle skid
2Furniture slides
Line Indicator Board5NeoPixel 5mm LEDs
1Surface mount 330 ohm resistor
1copper clad board
3jumper wires
3jumper wire crimp connectors
14-position (one is blank) jumper wire crimp housing
4M3 nylon spacer
2M3 screws
Sensor Board5Phototransistors
510K Trimpots
5100 ohm fixed resistors
5White LED
20.1uf capacitors
1copper clad board
7jumper wires
7jumper wires crimp connectors
24-position (one is blank, so total 7 wires) jumper wires crimp housing
2long M3 screws
2M3 nuts
1Highly modified Lego brick
LCD120x4 I2C LCD Module
14-wire Female-Female jumper
4M3 screw
8M3 nylon spacer
Buttons and Power Switch1DPDT power switch with mounting nut
2Pushbutton switch, momentarily closed, with mounting nut (one red, one black)
8jumper wires crimp connectors
8heat shrink tubing
22-position jumper wires crimp housing
14-position jumper wires crimp housing
Motherboard1Copper clad board
4M3 screws
8M3 nylon spacers
92-position header
14-position header
13-position header
17-position header
22-position shorting block jumper
18top-layer jumper wires
2Adafruit A6,A7 access jumpers
24.7k resistors
247k resistors
215k resistors
110k resistor
50.1uf capacitors
1220uf capacitor
110uf capacitor (electrolytic)
110uf capacitor (tantalum)
18x2 solderless DIP socket
1Motor driver
1Voltage regulator
Battery1LiPo S2 7.4v battery