Some humans find it very, very hard to multi-task. Like drink coffee, eat toast (with baked beans on top), talk and cycle... all at the same time (...and all of this just after waking up, from a bad night's sleep). But this is where robots should be cleverer than humans. They can do many things, at the same time (sort of). Or they do a little bit of each thing so extremely quickly that it seems like they are doing it all simultaneously. But, regardless, we still like to think we are cleverer than robots. And we like to be bossy. So we're going to order them to be better than us... and multi-task.
Let's remind ourselves: We were able to move Rosie by controlling her remotely. Separately, we got her to alert us when there was an object in front of her. But - I hear your say - how do we get her to do both of those things at the same time? Could she, for example, think through all the steps needed to drink her Starbucks latte, eat beans on toast, describe to us the taste (yummy!), and compete in the Tour de France, all at the same time? And how does she keep track of how she's progressing with each task, and work out what to do next when things (obviously) start to go wrong? If she's now drinking beans on coffee, she needs to stop cycling. Right?
Here's the thing. Everything we've been doing so far has been boringly sequential... we progressed through each line of Rosie's Python code embedded deep in her brain, in order, one line at a time. That's so boring. So yesterday. Today, we want to get Parallel. As per the words to this famous song from 1981*:
Let's get parallel, parallel
I want to get parallel
Let's get into parallel
Let me hear your Rosie talk
You will need to have these:
- Raspberry Pi 3 (with motors / wheels), and Raspbian running on SDHC card
- Computer from which you are connecting to the Raspberry Pi remotely
- Ultrasonic range (distance) sensor and 'RGB' LED to plug into Pi's GPIO pins
- Obstacles... lots of them
You will need to have done these things:
- I’ve got Pi(3) brain
- Why-Fi-ght the Wi-Fi?
- Hurrah-ndom inventions
- Don't reinvent the eel
- Achoo! Crash-choo! Episode I
You will need to do this:
- Adapt the Python program to use a thread
- Try and make Rosie crash!
What do you get after all this?
You may not have noticed it, but we were walking (or in the case of Rosie, crashing) into a big problem. And that problem wasn't (for once) Tammy, the cat.We can control Rosie remotely using our keyboard (over SSH). And our grand plan was just to combine that bit of code with our code for the distance sensor. Rosie can then be controlled, but also detect obstacles, at the same time. Easy. We just copy and paste in the new code somewhere before or after the existing code. And we can move onto more interesting things, right?
No way, Jose. Esto no es tan simple. And this is why. The code that takes keyboard input halts, and waits (patiently) on a line of code that requires a human - yeah, that's you - to enter an arrow key before carrying on. So while it's waiting, it won't be able to do any other useful stuff, like check for approaching alien spaceships using the distance sensor. And, likewise, once the motors have been instructed to move forwards, Rosie's application will continue to remain paused, until instructed to stop or change direction, using another keyboard key. There's no background monitoring during these pauses. We don't get our LED colour changes. We don't get our auto-break. That's right... Rosie will continue to crash into Tammy. Booo! It's called Blocking, and we don't like it.
So we think. And we think some more... What we would actually like to do is run two sets of code in parallel; one in the background, constantly checking the distance sensor, and another (the main program) that waits for, and processes, your input. And crucially, the main program should have access to what the other bit of code is reporting back so that it can use that information to help it make decisions.
There are many different techniques in programming to achieve this; but in Python, the two most common solutions involve the use of Processes or Threads - mechanisms that seemingly allow bits of code to run in parallel. There are endless discussions on the Internet about the differences between the two, and why you might use one or the other. But we don't like the sound of processes (we like to make things up as we go along, you know, break the rules) so let's use threads instead.
Confused already? Of course. But is it all worth it?
Yes! Because when it's all working, Rosie appears to be doing a lot more than just accepting our demands. She is thinking for herself in the background, and constantly trying to stop us from making the wrong decisions.
This is simply too much detail:
Let's see some Blocking in action. Temporarily create a file called 'blocking.py' (aren't we being imaginative today?) and place the following lines in the file:touch blocking.py nano blocking.py...creates 'blocking.py' Python application file and open in nano
print("ROSIE: Where would you like to go now?") destination = raw_input() print("ROSIE: Ok, let's go to...") print(destination)
If you now run this, you'll notice that it is waiting for you to enter something.
python blocking.py...runs the 'blocking.py' Python code
It's only once you enter that something (and hit 'Enter') that the code carries on and completes. This makes sense in this case, because carrying on without having the destination from you is pointless. Where would you go?
We can also confirm this by running Python interactively.
Sometimes we don't want to create an application (the .py file) to test our code. We just want to run some Python commands in real-time, and see what it does. There is nothing stopping you from running Python interactively. To do this, from the Linux shell, simply type:
python...launches Python shell
Here, you can run any Python code you like - interactively - to see what happens. Use the function exit() to leave the shell at any time. For example:
message = "Welcome to the world of Python shell" print(message) exit()
Start up the Python shell again. Now, let's just type in the following line from the 'blocking.py' file:
destination = raw_input()
There; we have confirmed that the raw_input() function is Blocking, and that it waits until someone enters something. All very useless, if you have other things that you need your robot to be thinking about and doing in the meantime.
Now, time to meet the latest of your Python friends: Thread. He's a bit difficult. He's moody. He likes to work alone in most cases. But you'll grow to like him. After all, he has special powers; he allows one section of your code to magically run independently. Now would be a great time to ask him to do just that.
Create a new Python file called 'rosie_wheels_with_distance.py'.
touch rosie_wheels_with_distance.py nano rosie_wheels_with_distance.py...creates 'rosie_wheels_with_distance.py' Python application file and open using nano text editor... but you knew this already!
This is the code that hopefully achieves our aims (for now):
Most of the code will look familiar, as it is simply an ugly mash-up of 'rosie_wheels.py' and 'rosie_distance.py'. The main points of interest are listed out below.
First things first. We need the threading module to use... wait for it... threads. If you are using Processes instead, the module would be called 'multiprocessing'. Read up on the differences between threads and processes before choosing which one to use.
from threading import Thread
A new function check_distance() is created. This essentially contains the bulk of our code from 'rosie_distance.py' which checks distance sensor input, and changes Rosie's alert level. It's a busy function: it also does the LED colour changing, and the emergency stopping of motors. We would like the function to be run as a thread, in the background, infinitely, regardless of what we are telling Rosie to do. It's, after all, critical to her safety.
def check_distance(): #...code to check distance, change alert level, change LED colour, stop motors...
A variable to track Rosie's alert level (0 = normal, 1 = warning, 2 = danger) is declared as being 'global'. This is to allow the main program (more on this later) to receive Rosie's alert level from the background thread.
global alert_level
There are other (more sophisticated, and probably correct!) ways to share data between threads, the most common being Queues, but we'll stick to using a global variable for now. But remember, running threads can cause a lot of havoc if you don't keep track of what they are all doing, and to what, and when.
And the rest of the program, borrowed heavily from 'rosie_wheels.py', ends up in the 'main' part of the program. In Python, this is defined using a bizarre bit of code. Don't ask. Just accept that this is how you define the main section of your program which only gets executed when running your program directly.
if __name__ == "__main__": #...bunch of code to do what you want the application to do
Oh, yes. If you want the thread to now start, don't forget to create an instance of it, and map it back to our check_distance() function.
t = Thread(target = check_distance) t.daemon = True t.start()
The rest of the content here is pretty much the same as the original 'rosie_wheels.py' file (which in turn is based on the sample '08_manual_robot_continuous.py' program from GitHub), with the exception of:
if alert_level != 2: rr.forward(0, 0.5) print("ROSIE: I'm moving forwards.\r") else: print("ROSIE: For your safety, forward is currently disabled.\r")
Here, Rosie is simply refusing to follow instructions when the 'UP' key is pressed, if the last alert level provided by the thread, running check_distance() loop, was 'danger' (2). How sensible of her?
And that's it. With a bit of threading, we have made a dramatic improvement to Rosie's IQ. Tell us, who is cleverer now - you or Rosie?
Run the program to find out!
Even more reading:
Python's official documentation on threading module:A reminder that our particular implementation makes use of the following GitHub contributions for lower-level interaction with the hardware that we are using:
Pretty cool! My "Posie" enjoying a Rosie Mind Meld.
ReplyDeletepi@RWPi:~/RWPi $ python posie_wheels_with_distance.py
RRB3__init__
New state: STARTUP
pollBumpers started with 0.100 interval
UltrasonicDistance: readingsPerSec: 10
pollUltrasonicDistance started with 0.100s cycle
UltrasonicDistance: reading thread told to start
Motors: worker thread readingsPerSec: 20
Motors: pollMotors thread started with 0.050000 at 2018-02-19 10:32:38.538830
waiting for threads to start
ROSIE: I'm starting up...
Spoken: I'm starting up
ROSIE: I've started my distance sensor...
ROSIE: Startup complete. Controls enabled.
I've started my distance sensor
-------------------------------------------
Use the arrow keys to move ROSIE
Press CTRL-c to quit the program
-------------------------------------------
ROSIE: I'm turning left.
Spoken: I'm turning left.
ROSIE: Too close! Too close!
Spoken: Too close! Too close!
ROSIE: It's all good.
Spoken: It's all good.
ROSIE: I'm turning right.
Spoken: I'm turning right.
ROSIE: I'm moving forwards.
Spoken: I'm moving forward
ROSIE: I'm going backwards.
Spoken: I'm going backwards.
ROSIE: keyp == 3 detected
RRB3.cancel() called
Motors.cancel() called
Waiting for Motors.control Thread to quit
New state: SHUTDOWN
********* RWPi STATUS *****
2018-02-19 10:33:49 up 55 min, 4 users, load average: 0.02, 0.06, 0.08
battery.volts(): 7.7
battery.hoursOfLifeRemaining(): 8 h 25 m
currentsensor.current_sense(): 481 mA
Processor Temp: 55.8'C
Clock Frequency: 600 MHz
throttled=0x0
irDistance.inInches: 14.7
usDistance.inInches: 33.6
bumpers: NONE
bumpers.cancel() called
UltrasonicDistance.cancel() called
Waiting for UtrasonicDistance.readThread to quit
do_run went false. Stopping pollUltrasonicDistance thread
New state: DONE
myPDALib.PiExit(): PDALib.pi.stop() called
do_run went false. Stopping pollBumpers thread
pi@RWPi:~/RWPi $
("Posie" uses the Pi Droid Alpha interface card that provides 16 digital I/O, 8 analog-digital-conversion, and H-Bridge motor control. http://www.mikronauts.com/raspberry-pi/pi-droid-alpha/)
Posie thinks Rosie is a great robot friend to have.