Building a Self-Driving FLL Robot With PID Line Following
A Pybricks-driven FLL competition robot combining a color-sensor line follower, an ultrasonic obstacle check, and a manipulator, with a clean movement API on top.
This is the code for an FLL Challenge robot: a self-driving car built on LEGO SPIKE hardware and programmed in Python with Pybricks. It has to drive, follow a line, notice obstacles, and grab objects on a competition table, and the code is organized so that each of those is a small, named function rather than a tangle.
The Sensor and Motor Layout
The hardware config is the foundation, so it is worth being precise. Two color sensors: one on the front for line following, one on the back as a reference. An ultrasonic distance sensor for obstacle detection. Three motors doing different jobs: a medium motor for steering, a large motor for main propulsion, and a separate manipulator motor for grabbing. Splitting steering and drive into their own motors is what makes the car steer like a car instead of a tank, which matters for following a line smoothly.
Two Line Followers, and Why the Second One Wins
The code ships two line-following implementations, and the difference between them is the real lesson of the project. The first, follow_line_simple, is a basic edge follower: it looks at the sensor, decides it is too far one way, and corrects hard. It works, but it wobbles, because it only ever knows "on the line" or "off the line" and steers at full correction either way.
The second, follow_line_pid, uses a PID controller against a target value, and it is noticeably smoother. Instead of slamming the steering to a fixed angle, it scales the correction to how far off the line the sensor actually is. A small deviation gets a small nudge, a large one gets a large one. On a competition table where a wobbling robot drifts off its path or wastes time, that proportional response is the difference between finishing a run and re-running it.
Obstacles and the Manipulator
On top of movement sits a small obstacle layer: get_distance reads the ultrasonic sensor in millimeters and obstacle_ahead turns that into a simple boolean the driving logic can branch on. The manipulator has its own trio, grab, release, and manipulator_home, with the grab and release angles tuned to the specific build. The troubleshooting notes are refreshingly grounded: if the manipulator does not move, check the motor direction and adjust the grab and release angles.
The takeaway, and the thing worth handing to a student team, is that a clean movement API pays for itself. Once drive_forward, steer_left, follow_line_pid, and grab exist as reliable building blocks, writing a full competition run becomes composing them instead of rewriting motor logic every time. And when a smooth PID follower is one function call away from a jerky simple one, you get to feel exactly why control theory earns its keep.
Have something that needs building, or stabilizing?
These notes are the work log. The paid work runs through Moonshine Labs, my product and engineering studio. Tell us what you're building, or grab a call.
Work with us →