This post is aimed at relatively new programmers grappling with how to manage multiple simultaneous things on their PiWars robots (for example, reading from a game controller without any lag while also managing a distance sensor that can only be read once a second!)
I’ve had a few questions on Discord and Twitter about how to ‘read buttons on a controller and manage LEDs at the same time’ and similar. These kinds of problems are trickier than they sound – you need to read a game controller fast or you’ll either miss things or respond too late, but you might need to also read sensors slowly or they’ll mis-read. For example, ultrasonic rangefinders can’t be read hundreds of times per second, they need time to settle, to send their ping, and to wait for the response. If you try to read faster than that they’ll return garbage and your robot will run into walls.
So, let’s consider the example of something that needs to be done slowly. To avoid over-complicating things, our first requirement is:
from time import sleep while True: sleep(5) print("five seconds have passed, have a cookie!")
If you run this code you’ll see that it does, indeed, print the message to the console every five seconds. If this is all you needed to do you could walk away happy at this point, but there’s a catch!
Let’s add another requirement…
Ah. This is a bit tricker! If we use sleep() to wait between cookies, we’ll miss out on the milk that has to happen every three seconds. We might be tempted to try something like this:
from time import sleep while True: sleep(3) print("three seconds have passed, drink milk!") sleep(2) print("five seconds have passed, have a cookie!")
…but while this does the right thing the first time around, it quickly goes wrong and we end up with five seconds between each request to drink milk (work this through in your head, or on paper if that’s not obvious!). We need a better option.
It is possible to do this using a mechanism called threading. Threads are a way to tell the computer to do more than one thing at a time, and sometimes this would be the right way to do it. However, you find very quickly that asking the computer to do more than one thing at a time is risky!
I’m not saying you should never use threads, or that they’re in some way bad, but they’re something you should avoid unless you’re very confident you know how to deal with the problems they can introduce. I’ve got twenty years of experience programming for a living and I still have to be extra careful when I use them!
The approach we used in example 1 is a bit like setting a countdown alarm on your stopwatch (or phone, if you’re being fancy). You press the start button, then you wait for the alarm to go off, then you do the thing you were waiting for. This is great when you want to have a very precise delay, because you’re just waiting, but as we’ve discovered this can be a problem in itself if you want to do other things at the same time.
As an alternative, imagine you’re asked to do something every ten minutes. You’re busy doing something else, but there’s a clock on the wall, so every so often you look at the clock (between pages of your book, or rounds of your game, or whatever) and work out whether it’s been ten minutes since you last did the thing. If it hasn’t, you go back to whatever it was you were doing, but if it has you can stop and do the thing. You might not wait exactly ten minutes, but it’ll be close, and, crucially you can do other things while you wait. How might this look in Python? We’ll use the time.time() function which returns the time right now in seconds since a magic date a long time in the past, and we’ll run every ten seconds.
from time import time last_time = time() while True: now = time() if (now - last_time) > 10: print("Doing a thing every ten seconds!") last_time = now
In this code, last_time holds the time we last did the thing (in this case just printing to the console). Line 7 is the important bit – remember that all our times are just numbers of seconds since a magic point in the past (it doesn’t matter as long as it’s always the same point!) so we can get the interval between ‘now’ and ‘last_time’ by just subtracting them. If the result of that subtraction is more than 10 then we’re more than 10 seconds since last_time. If that’s the case, we print a message to the console, and, importantly, set the last_time value to now.
So, how often does this loop run? The answer is ‘very fast indeed’, as most of the time it’s doing nothing. In spite of this, we’re still only going to print that message very ten seconds, and we never had to sit and wait at all. Going back to example 2, where we wanted to print two messages at different rates, we could re-write the above to add another last_time (because we need to track two things) and solve our problem like this:
from time import time last_cookie = time() last_milk = time() while True: now = time() if (now - last_cookie) > 5: print("Five seconds have passed, have a cookie!") last_cookie = now if (now - last_milk) > 3: print("Three seconds have passed, drink milk!") last_milk = now
This works! We get milk and cookies at the right intervals, and if we needed to do anything else it should be obvious by now what you’d need to do.
If you’ve only got a couple of things to manage, the code above is fine. It’s a bit messy though – you’ve got to declare your last time tracking variable for each thing, then you’ve got to check it, then you’ve got to set it later. We can do a bit of Python magic to clean this up and make it simpler to use by wrapping all this up in a new class (don’t worry if this isn’t familiar, the approach above works just fine, this is just a way to make the code more ‘pro’!)
In my robots I’ve needed to do this kind of process a lot, so I did what experienced programmers do and wrapped up the thing I need to code repeatedly into its own little piece of code. This means I don’t have to write out the same thing over and over again, and I’m basically lazy so any chance I get to avoid doing work is a good thing! This is how I’ve done it:
from time import time class IntervalCheck: def __init__(self, interval): self.last_time = None self.interval = interval def should_run(self): now = time() if self.last_time is None or now - self.last_time > self.interval: self.last_time = now return True else: return False cookie_check = IntervalCheck(interval = 5) milk_check = IntervalCheck(interval = 3) while True: if cookie_check.should_run(): print("Five seconds have passed, have a cookie!") if milk_check.should_run(): print("Three seconds have passed, drink milk!")
By using an object (defined by my IntervalCheck class), I can bundle together the state, that is the last_time value, and the specification, that is the interval I want to wait between actions, and hide both away from the rest of my code. I put the check for whether something should run inside the object as well where it can see those values.
This is obviously longer than the previous version, but the key here is that I’ve created something which I can use anywhere in my code where I need this kind of functionality. It’s also nice and clear – when I create the instance of this class for my cookies or my milk I’m telling it at that point how many seconds it should wait. In the earlier example the number of seconds is defined somewhere else – you can imagine that if you had to modify your code a few weeks after you wrote it you might have to hunt around a bit to find that number (and it might not be obvious what the number meant), whereas this way around it’s clear that the ‘5’ and ‘3’ are intervals and associated with the cookies and milk respectively.
By using interval checking we can avoid having to put time.sleep() calls in our code (which cause everything to stop) and also avoid having to use threads (which are complicated and hard to debug). If we want to be fancy we can wrap up this functionality in a new class and use it anywhere we like.
While I’ve used trivial examples (just printing to the console) my robots use exactly this code to do rather more complicated tasks. For example, last year’s robot, Viridia, used an interval check to update its estimated position from sensor readings two times every second, while reading from the controller I was using to drive it as often as possible to avoid lag.
Hopefully this will help you manage your robot’s sensors and processing more easily! Have fun, and feel free to get in touch on Twitter if you need more info.