While developing the CodeBot Python with Robots curriculum we're taking on the challenge of giving students real-world tools to address some fundamental CS problems. One that came up very early in the process is multitasking.
As a beginning coder you focus on instructing the computer to do a single task in just the way you intend it to. When we think about algorithms (you do think about them, don't you?) it's usually with a sequential model:
- do task A,
- then proceed to task B,
- and finally, task C,
- then repeat!
After all, computers execute your code one line at a time, sequentially. You have to wait for task A to finish before proceeding to task B.
# Blink LED-0 on and off repeatedly
The code above is a simple example of a blinking one of CodeBot's LEDs on and off in a loop. The sleep() function calls are blocking, which means your next line of code doesn't run until after the sleep() function returns.
The following is another simple program based on sequential blocking code. The most basic level of motor control is to apply power to the motors for a fixed amount of time. (this example doesn't use CodeBot's more advanced wheel encoders for precise movement) Here we wait for each movement in sequence:
# Drive the 4-sides of a "very rough" square
for i in range(4):
# Straight ahead for 1 second
# Rotate right for half a second (approx 90 degree turn)
Blocking code works fine for simple tasks, and it's nice to be able to follow a sequence of operations one-at-a-time. So what's the problem?
Walking and chewing gum at the same time
Just like us humans, robots need to be able to accomplish multiple things simultaneously. We sense what's around us while navigating the world, and respond to whatever new events may arise. Let's see how a robot might handle this in code.
Continuing the example above, what would CodeBot need to do to combine both of the tasks: blinking an LED while at the same time moving in a square?
Your first thought might be to continue writing the code in a blocking fashion with sleep() functions. But it starts getting complicated since we want the LED to blink at exactly a 1-second rate. Mixing the movement code and the blinking code with various sleep times would make the code hard to follow. And woe unto you if someone asks you to change the blink rate - how about 3x per second, oh and now adjust those movement times a little...
So we need a better way to multitask. Preferably one that helps separate the logic of different unrelated tasks we want to perform. There are a number of ways to do this, but for Python with Robots we've chosen an Event Driven programming model.
"Every object persists in its state of rest or uniform motion unless it is compelled to change that state by forces impressed on it."
- Sir Isaac Newton, First Law of Motion
With event-driven programming, your code defines functions to handle the various events you might need to respond to. What are some events CodeBot might care about?
- Line sensor detects a line.
- Proximity sensor detects an object.
- Accelerometer detects a change exceeding a set threshold.
- A scheduled timer has expired.
How can you write event-driven code? Take the Line Sensors for example, and contrast a blocking versus event-driven approach to using them:
To support event-driven code you need an "event dispatcher". For CodeBot we've created one called BotServices. It provides a way to attach "callback functions" - like handle_line_sensors() in the example above - to all sorts of events your 'bot might need to react to.
One common event we all respond to is the passage of time! BotServices supports registering callback functions with millisecond-level resolution via the on_timeout() function. Just tell it the function you'd want it to call, and the number of milliseconds in the future that should happen, and it takes care of it. You can register as many timeout functions as you like... It's like having lots of little programmable alarm clocks, that do your bidding when they go off!
The code below shows how you could blink one of CodeBot's LEDs on and off, like we did in the first example above. This time it uses event-driven coding with BotServices.
# Create the event-dispatching service.
bot = BotServices()
leds.user(0, True) # Turn ON
bot.on_timeout(led_off, 1000) # Schedule turn-OFF after 1 sec
leds.user(0, False) # Turn OFF
bot.on_timeout(led_on, 1000) # Schedule turn-ON after 1 sec
# Turn LED on and schedule led_off for later.
# Start the event-loop, which will call our "callback functions" when needed.
See how each callback function above schedules the next callback to occur? Callback functions, also known as event handlers, are responsible for handling an event by doing whatever is needed in response to it. A callback should do its job as quickly as possible, then return so that further events can be processed. Your callbacks are called from the event-loop, which is where BotServices checks all the sensors and timeouts, etc., and dispatches events.
Writing code in this way makes it easy to add tasks that do different things simultaneously. For example, the following shows the code we could add to make CodeBot drive in a square while still blinking those LEDs at a 1-sec rate. Hey, we're multitasking!
# Define callback functions for square driving!
go_straight() # Start going straight
bot.on_timeout(straight_complete, 1000) # Schedule next move
turn_right() # Start turning right
bot.on_timeout(turn_complete, 500) # Schedule next move
# Start making our square!
Event-Driven code looks quite different than the sequential code you may have written in the past. But really your code is still being run one-step-at-a-time just like always. It’s just that the code that calls your callback functions is down inside bot.loop(), so it seems a bit like magic as your program runs!
In our classroom beta testing so far, we're finding students quickly grasp the event-driven approach. That's awesome, because it's definitely not unique to robotics. Almost all web applications rely heavily on event-driven programming, as do many games, mobile apps and other software we interact with every day.