Lesson 3 of 5 · 24 min
Python for robots
A robot program is a loop that repeats three steps: read the world, decide, act. Python is good at the middle step, because deciding is mostly about data and logic, which is where the language shines. This lesson covers the parts of Python that show up in nearly every robot, and each idea comes with code you can run right here in the browser, with no hardware.
Functions: name your ideas
A function gives a name to one idea, such as "convert a raw reading to centimetres". Once named, you can test it alone, reuse it, and fix it in one place. Robot code that has grown without functions turns into one long loop where nobody can find the bug.
Prefer small functions that take values in and give values back, with no hidden global variables. A function like that can be tested on your laptop without a robot, which is exactly what the runners in this lesson do.
Data structures that robots actually use
- list: an ordered, changeable sequence, such as the last ten readings.
- tuple: a fixed group like
(x, y)or(left_speed, right_speed). Returning two motor speeds as a tuple keeps them together. - dict: names to values, such as
"FORWARD": (0.6, 0.6)mapping a state to wheel speeds. A dict lookup replaces a long chain ofifstatements. - deque: from
collections, a list with a maximum length. Adding a new item pushes the oldest one out, which is ideal for a sliding window of recent readings.
Filtering noisy sensors
Real sensors are noisy. An ultrasonic sensor might read 50.2, 49.1, 51.4 cm while the wall is perfectly still. If your robot reacts to every wobble, it will twitch. A moving average reduces noise by averaging the last N readings. With noise of standard deviation s, averaging N independent readings reduces it to about s / sqrt(N), so 4 readings halve the noise.
The price is lag: the filtered value trails behind real changes by roughly N/2 samples. Pick N as small as the noise allows.
Change the 35.0 spike to something else and watch how far it drags the average. A single bad reading still pulls the output up for 4 samples. That is a weakness of averaging, and you will see a better tool for it shortly.
A simple state machine
Robots do different things at different times: drive, turn, stop. A state machine makes this explicit. You keep one variable that holds the current state, and a function that decides the next state from the current one plus the sensor input.
The key design trick is hysteresis: use different thresholds to enter and leave a state. Below, the robot turns when something is closer than 20 cm but only drives again once the way is clear beyond 35 cm. Without that gap, a reading hovering around 20 cm would flip the state back and forth every cycle.
Notice that readings of 22, 28 and 30 do not cause a switch back to FORWARD. That is hysteresis doing its job.
Units: the quiet source of bugs
Many robot bugs are unit bugs: centimetres treated as metres, degrees fed to a function that wants radians. The fix is to convert at the edge of your program and keep the rest in one consistent unit.
An ultrasonic sensor measures the echo time, and the sound travels to the wall and back, so the distance is half of speed times time. The speed of sound also depends on temperature: v = 331.3 + 0.606 x T metres per second, with T in degrees Celsius.
Between 0 and 30 degrees the speed changes by more than 5 percent, which is close to 3 cm of error at 50 cm. Whether that matters depends on your robot.
Simulating a sensor
You can develop most of your logic without the sensor. Write a fake one that behaves like the real thing: Gaussian noise around a true value, plus the occasional bad reading. Then test your filter against it.
A median filter handles bad readings better than an average. It sorts the window and takes the middle value, so one wild value is simply ignored.
On the glitch samples the mean jumps to around 120 cm while the median stays near 50. If your robot used the mean, it would think the path was clear and drive into the wall.
The real loop, with timing
On the robot, the same functions plug into a loop that reads, decides and acts at a steady rate. A naive time.sleep(0.05) at the end of each cycle drifts, because it adds 50 ms on top of however long the work took. A fixed schedule using time.monotonic() stays on time:
import time
from gpiozero import DistanceSensor
sensor = DistanceSensor(echo=24, trigger=23) # echo needs a voltage divider
PERIOD = 0.05 # 20 Hz control loop
next_tick = time.monotonic()
try:
while True:
distance_cm = sensor.distance * 100 # gpiozero reports metres
# filter, choose state, set motors here
next_tick += PERIOD
time.sleep(max(0, next_tick - time.monotonic()))
finally:
pass # stop the motors here, even if the program crashes
The try and finally block matters: if your code crashes mid-run, finally still executes, so you get a chance to stop the motors instead of leaving them spinning.
Check yourself
Why does the state machine use 20 cm to start turning but 35 cm to resume driving?
Check yourself
One reading in a window of five is a wild 400 cm glitch and the rest are about 50 cm. Which filter output is closest to the truth?