Skip to content

A line-following robot has exactly one thing it can sense that matters: how far it has drifted sideways from the line. Everything the controller does is built on turning that single number into a steering command.


Let yy be the robot’s lateral offset from the line, and 00 be where it wants to be. The error is simply how far off it is:

e(t)=0y(t)e(t) = 0 - y(t)

A proportional (P) controller reacts to that error in the most direct way possible — it steers harder the further off the line the robot is, and it does nothing once the error is zero:

u(t)=Kpe(t)u(t) = K_p \, e(t)

Here u(t)u(t) is the steering command and KpK_p is the proportional gain — the one knob this controller has. That command doesn’t move the robot sideways directly; it changes the robot’s heading, which then carries it back toward the line as it drives forward:

θ(t)=θ(tΔt)+u(t)Δt,θ[θmax, θmax]\theta(t) = \theta(t - \Delta t) + u(t)\,\Delta t, \quad \theta \in [-\theta_{max},\ \theta_{max}] y(t)=y(tΔt)+vsin(θ(t))Δty(t) = y(t - \Delta t) + v \sin\big(\theta(t)\big)\,\Delta t

The heading is clamped to ±θmax\pm\theta_{max} because a real steering mechanism has a physical limit — the robot cannot instantly point straight sideways no matter how large the error is.


Notice that heading integrates the steering command rather than jumping straight to it. That integration is inertia: once the robot has turned toward the line, it keeps that heading even after the error shrinks, so it overshoots, has to correct back the other way, overshoots again, and so on. A pure P controller has no term that anticipates or damps this — that’s the job of the D term in a full PID controller — so on its own it tends to oscillate around the line rather than settle smoothly onto it.


Drag the slider to change KpK_p and watch the same robot, starting at the same offset, respond differently. The live readout below the canvas shows the exact numbers behind the animation at each instant.

error m
heading°
steering cmd °/s

steering command = Kp × error

This is the same model implemented in gui.py — download it and run python3 gui.py to play with it as a desktop Tkinter app instead of in the browser.