Build A Robot With A Raspberry Pi
Build A Robot With A Raspberry Pi
The 1kOhm resistors and the LEDs are not needed for the car, but they make
it easier to understand the principles.
Pi
The Raspberry Pi is the heart of the whole system. A third generation Pi is
used here. A Pi 4 is not required for a simple robot.
It is a complete computer. The inhibition threshold for many is certainly the
fact that the operating system used is not Windows, but a Linux offshoot. But
that's really not a problem, after installation. You can find the programs and
settings you need relatively quickly because there is a graphical user
interface. We proceed step by step with the installations.
Don’t panic!
SD-Card
The SD card serves as storage. It should be ordered with an adapter for a PC
slot. Then you can easily plug it into a laptop and copy the code. 8 GB is
enough for this project, but 16 GB is better.
The housing
I am using the official Pi case. But that is not necessary. It just looks nicer.
Wires
For those who enjoy soldering, go ahead! But inexpensive jumper wires or
cable jumpers are available. These allow you to set up your experiments and
then easily dismantle them again.
Pin Breadboard
This is also not necessary. You can plug everything together with cables.
However, you quickly lose an overview of what you are building. And since
these boards are numbered and lettered, they are great for documentation. I
will make use of it below. But as I said, it works without it.
GPIO Extension Board
This 40-pin extension board is available so that you do not always have to
fiddle around with the Pi itself. In contrast to the Pi, it is even labelled. You
plug this onto the pins and then connect the cables outside of the computer.
This component is also not required. (By the way, GPIO stands for General
Purpose Input / Output. These are the pins on the Pi that can be used either as
input, output or for pulse width modulation. An explanation of this will
follow.)
The end of the cable leading to the Pi is a little too high for the housing to be
closed with the GPIO extension board installed. The upper strain relief can be
levered out with a knife. But proceed carefully so you do not damage it. Then
you can completely close the housing again.
Chassis with motors
This component kept me from building for a long time because I did not
know how to connect the motors with the gears and the wheels and then fix
everything. But you can buy one cheaply. A chassis with three wheels is used
here. There is also a variant with four drive wheels. But it is hard to go
around the corner with it (I have tested one). The parts are delivered loosely.
Therefore, a soldering iron is required (only for this step). You can borrow it
if necessary. Also nice is a chassis that contains a small “table” for the Pi
which you can then attach the Pi to with screws. There are various variants of
the chassis available.
Important
A mouse, keyboard, and monitor (HDMI connection) are required for the
initial start-up. You can also borrow these components, as a VNC connection
will be used later.
The current for the Pi during the setup can come from the power bank or a
corresponding charging cable. The USB port on the PC is often sufficient.
Install the software on the Pi
You probably already have the software you need installed on the Pi. If this is
not the case, I will show you how you can do it now. Unfortunately, the
following steps are as boring as they are tedious. And when you are done
with it, there is still nothing. It is like buying a new computer only to find that
nothing is installed, and the device is currently useless. I calculate two to two
and a half hours for the annoying installation. But you cannot get around that.
Once it is finished, you can do a few small experiments quickly if the
components from the list are available.
SD-Card
First, an empty SD card is required.
A program is needed to cleanse this of all data and to format it. I use the free
SD Card Formatter software. This program is available free of charge here:
https://www.sdcard.org/downloads/formatter/eula_windows/index.html
Do not type! A Google search for "sdcard.org" is sufficient. Then click on
downloads and select Windows or Mac.
After installation, insert the SD card into the computer, start the program and
select the format (the option to be selected is Quick format). Formatting only
takes a few seconds.
Now the operating system must be copied to the card. The Pi does not have
its own internal memory for this. The programs, the data and the operating
system are later all together on this card.
NOOBS and the operating system
NOOBS is the installer for the operating system. Little time is required for
the download and the subsequent installation.
The installer is available for free on the site:
https://www.raspberrypi.org/downloads/noobs/
Do not download NOOBS Lite here, download NOOBS. Save the zip file on
your own computer and unzip it at any point (for example: C:\noobs) (extract
all). This data must now be moved to the SD card. Simply mark everything
and copy it to the SD card. This is packed over 2 GB, so it will take a few
minutes to download and copy. On the Pi it will be about 3 GB.
Now the card can be plugged into the Raspberry Pi. A mouse, keyboard, and
screen (HDMI connection) are required for the initial start-up. The current
can come from the power bank (1.5Amps or 2Amps output are both fine) or a
corresponding charging cable. Later we will establish a connection from our
own PC so that only the power supply is necessary. However, the
components are required for initial commissioning because there is no Wi-Fi
connection yet. From the Pi 3 you can connect to the Internet via Wi-Fi. This
is useful if you want to let the Pi drive around. Otherwise a LAN cable would
be quite annoying.
Now turn on the power!
A rainbow color image appears briefly and then you must configure various
things.
Here you select the Raspbian Full operating system.
If you use an 8GB card, the memory will not be sufficient. In this case, read
on.
Important: Find the right keyboard below.
I changed my language to German. But you do not need to.
Before the installation you select the Wi-Fi network. A list of the available
networks is offered. Choose your network and enter your Wi-Fi password.
A problem with the Wi-Fi password may be caused by an unexpected
keyboard layout.
Other operating systems appear that can be installed. Among other things,
Raspberry Pi OS is listed, but you should only use it if you only have 8GB
available. This means that all the examples from this book work perfectly,
but you will miss a few pre-installed programs, such as Mathematica.
Otherwise, we stay with Raspbian Full. This is already on the SD card from
NOOBS and is also the official version.
Now click on Install.
There is another warning that we confirm with yes.
Now everything is unpacked and installed. It will take a few minutes. During
this time, you can already see what to expect in a preview. There is a lot of
software on the Pi: Python and Java development tools, the Libri package
(which is like Office), Excel and typing programs. Even Mathematica is
preinstalled. There is also a browser and a few small games. But you should
not expect a great performance from these programs. After all, it is a quite
simple computer that does not have a lot of computing power. When they
landed on the moon, however, the astronauts had significantly less.
At the end we get the message: “Operating system(s) installed successfully”
In the first dialog click on “Next” and then select the country settings.
And now a new password must be assigned. If you do not enter one, the
password is »raspberry«. However, this is not recommended because many
services will then output error messages. So, enter a new password here
twice.
Hint: I recommend writing it down or at least remembering it.
This password will be necessary later for the remote connection from another
PC, and if you want to install new programs.
Then there is a message about a black frame around the screen. In that case,
check This screen shows a black border around the desktop.
We can skip the following network question because we have already set up
the Wi-Fi connection.
The software is updated in the next dialog. It can take a while. The computer
must now be restarted for the changes to take effect.
Rainbow colors again. You can now try a few of the games. Play a little with
word processing. Or let Mathematica solve a few math problems. The web
browser opens Chromium instead of Chrome. That is what it is called on
Linux computers. The default search engine is DuckDuckGo. But that can
easily be changed.
If the rainbow screen keeps reappearing, the voltage is probably insufficient.
What we now need for our work is a development environment for the
Python programming language. Usually this is not installed.
IDE for Python
A development environment is required to program the inputs and outputs. I
like to use VS Code myself. But that can only be uploaded to the Pi under
certain circumstances. Since the programs that we will need are very
compact, a simple development environment is sufficient. I have had a few
problems with Thonny, so I recommend the IDE for Python. Sometimes this
IDE is installed. And sometimes not. You can find Python 3 (IDLE) via:
You can call up the file manager (top left, third symbol) via the menu and go
directly to the home directory.
Here we create a new folder with the name “test”.
Which pin?
A special feature of this little computer is (among other things) the set of 40
pins on the top right. These are combined into a connector named J8 (you
have to look very carefully).
We need two connections for the first test. In the first column we take
connections 2 and 5. The correct numbering is different, but we will come to
that later.
GPIO means general purpose input / output. These can be used to implement
inputs and outputs. They are very flexible, but also require that you tell the Pi
how you want to use the corresponding pin. There are 25 of them that are
wildly distributed in functionality. A detailed overview will come later.
Hardware
We need a resistor (around 1k Ohm) and an LED. The long leg of the LED is
the positive pole (anode). This is crucial because it does not light up if you
connect it the wrong way.
These electronic components must now be connected to the cables and to the
pins of the Pi.
With a breadboard it would look like the following picture. However, this is
only one of many possibilities.
It is also good to know which holes on the board are connected
“underground”.
The right side shows an alternative way to connect the LED, but with one
additional cable.
A little program
To create a program, we open the Python Editor (Raspberry →
Development → Python 3 (IDLE)). Via Ctrl + n (or File → New File) you
open an empty document. In this new document (right) you enter the program
code shown.
Conclusion
I admit we have not launched a moon rocket yet. The LED could have been
made to glow with a battery. And next comes a rather dry chapter about
GPIOs. But at the end of the next section the LED will flash, and you will get
an understanding of what you have just programmed.
GPIO
The inputs and outputs are required to send data from the computer to the
environment and vice versa. Usually this is a 3.3 Volts signal. But there are
also a few other options. Of these, we will only use what is known as pulse
width modulation. I will explain that later, but to put it simply, they are
adjustable output signals between 0 and 3.3 Volts. This is what you need to
change the speed of the motor.
The designers seem to have rolled the dice when distributing the GPIOs.
Without a plan, there is little chance of finding the right numbers.
If you connect the LED to the outputs GPIO2 or GPIO3, it lights up weakly
in the off state. This is not the case with GPIO4.
The numbering starts at 2 and ends at 26. So only 25 of the 40 pins are
available for signals. That could be a problem when you have a larger project.
But it is enough for the experiments in this book.
The ID_SD and ID_SC are reserved for extensions that are plugged in
(HAT). We will not use these either.
The Terminal
This overview can also be displayed on the Pi. We use a terminal window for
this.
A terminal can be opened via Ctrl + Alt + t (t for terminal). You can also take
the third symbol to the right of the raspberry.
Here you can see the username and the operating system (pi@raspberry) and
the current directory path (~). The tilde is an abbreviation for /home/pi/. We
are in the “home” directory of the user “pi.” Our new folder “test” is also
located here.
gpio
To display the current status of the hardware and the GPIO software, enter
the following:
gpio -v
But you already know that. The overview of the pins is more interesting. To
do this, enter the following command:
gpio readall
In the middle you can see the physical numbering. You can also find the
exact names of the pins. The numbers from the figure above are on the left
and right on the outside. There you can also find the GPIO numbers 0 and 1.
In most programming languages, the indexing starts at 0. But a test with the
LED shows that these do not work. They are for the I²C bus. We will not use
that. So, the GPIOs start at 2.
Some GPIO Commands
You can decide in the Python program whether you want to use BCM coding
or the physical numbers. For this you have to set the mode.
Either
GPIO.setmode (GPIO.BOARD)
or
GPIO.setmode (GPIO.BCM)
In the following, I will use BCM because it is easy to find on the extension
board. This has to be communicated to the Pi once at the beginning of the
program.
To get this working, you load a module into your own program. A module is
a pre-created program that allows you to use a few functions. To load the
GPIO module there is the command:
import RPi.GPIO as GPIO
Import RPi.GPIO would suffice. But then you would always have to write
RPi.GPIO in front of each command. It is better to choose a short but
meaningful identifier.
“as GPIO” follows. Instead of GPIO you could also write “as RobinHood”. It
is just a name you choose. But any code that you might find on the internet
will be easier to reference and adapt if you use “GPIO.”
Input or output
For a start, the GPIO pins will need to be configured as either input or output.
We will add pulse width modulation later.
Inputs
While »OUT« can be programmed quite easily, »IN« is a bit more
complicated. The information from the button must be read out. Usually you
do something with it. A little later we want to use this information with a
simple program and a switch to make the LED shine. A switch is just a
connection that is either open or closed. We do not need any hardware for
that. It is sufficient to connect two cables.
Usually you add a resistor in between, a pull down or pull up resistor. The Pi
can do this via software. This makes the command for the input a little
longer:
GPIO.setup (2, GPIO.IN, pull_up_down = GPIO.PUD_UP)
You can query whether the switch is closed or open with a simple command.
The result is saved in a variable. This corresponds to a brain cell that has a
name (status) and can remember whether the input is open or closed.
status = GPIO.input (2)
This status can then be checked with a query.
Clean up
When the program has run completely or is terminated due to an abort, the
inputs and outputs still have to be tidied up. Otherwise the Pi will mess up
and send a warning. It does not interfere with the process, but it is more
elegant to clean up again at the end. There is a clean-up command for this:
GPIO.cleanup()
Wiring
We leave the LED as before. Now there are two more cables. To protect
against incorrect connection, there is again a resistor in between.
We deliberately leave the GPIO 4 to GND route open in the first attempt.
Later, this connection will be closed, using a pin and a socket (male and
female). It does not matter which GND connection we use. Fortunately, there
are quite a few of them.
With the breadboard you can save yourself a cable. The resistor for the
switch goes to the right GRD row in this picture. To switch, pull or plug the
lower / green cable.
A little program
This program is not yet earth-shattering either. But it does show how easy it
is to read information. How to open a new file has already been described.
One more time, to be on the safe side: (Raspberry → Development →
Python 3 (IDLE)). Then Ctrl + n (or File → New File)
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(2, GPIO.OUT)
GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.output(2, GPIO.input(4))
time.sleep(5)
GPIO.cleanup()
The highlight is:
GPIO.output (2, GPIO.input (4))
Here the return value from input 4 is placed directly on output 2.
We can now start the code sequence (in the program window with F5). If
everything is wired properly and the program has been entered correctly, the
lamp should now light up. For five seconds.
Now you connect the loose ends and call the program again. This time the
LED stays off.
Short explanation:
If the connection is open, there is a HIGH signal at GPIO 4. This is passed to
GPIO 2 and the LED is on.
If you close the connection, there is the same potential at GPIO 4 as at GND
(i.e. 0 V), and that corresponds to LOW. Therefore, the LED is then off.
It was quite a hassle to program all of this (almost) without Python. A short
course follows in a few chapters to help you understand the basics. But now
we want to get rid of the periphery. And we do that with a VNC connection
between a computer and the Pi.
VNC Connection
Usually there is already a PC or laptop at home. Therefore, it is inconvenient
for such a small mini PC to have all that stuff on the desk. In the following
section we will establish a connection from the PC/laptop to the Pi so that we
can control it remotely.
IP Addresses
An important question to start with: Can my laptop access the Internet from
home?
The answer is no.
Only the router can do this. Otherwise you would quickly run out of IP
addresses that are available worldwide. The home router is in a Class C
network. What does that mean? This means that it can manage up to 254
devices at home. When my PC wants to access the Internet, it connects to the
router via the gateway. And then it sends the data. To make this work in the
other direction, my computer in the home network needs an IP address.
Otherwise the router would not find the PC again and would not know what
to do with the answer.
The router's gateway can usually be found on the router or in an info card on
the router. This is roughly:
192.168.0.1
or as with me 192.168.2.1
I have seen 192.168.178.1 as well.
If you enter this address in the browser, you can connect to the router. Some
providers also have a descriptive name, such as http://speedport.ip
Every device, such as a mobile phone, tablet, laptop, or Raspberry Pi,
receives a unique address from the router automatically. We now want to
search for the address for the Pi and then make it permanent. Often the router
will assign the address to another device the next time. However, this
behavior is undesirable for our experiments.
IP address of the Pi
To do this, we open a terminal again (Ctrl + Alt + t or via the raspberry
menu).
Here you enter “ifconfig”. If you are familiar with PC: Do not confuse it with
ipconfig!
Wow! This is too much text! We are only looking for one line. Therefore, we
use the search function. Enter the command again with a pipe. grep is the
search command and looks for the string in the quotation marks in front of
the "|". grep means global/regular expression print. So: find a regular
expression and display it.
ifconfig | grep “inet 192”
Enter the Pi's IPv4 address and press Enter. Authenticate so that not everyone
at home can access the device. Then click on “Continue”. Incidentally, this
does not work from outside your home network. Username and password are
the Pi's login information (i.e.: “pi” and your password). There is a check
mark for Remember password. It is worth it.
The connection is usually quick, and then you can see the Pi's screen. Now
you can separate the additional monitor, mouse, and keyboard.
When restarting the Pi, the resolution is usually set to the minimum because
no monitor can be found. You can set that permanently.
Select the Display tab and then click on Set resolution.
Now is exactly the time to learn a few things in the Python programming
language.
Python Part 1
I am starting from scratch here. If you are familiar with Python, then you can
skip the chapter. How to open a new window and start a program, however,
you should know now.
Print and variables
Variables are used in all programming languages. These are small memories
that can store information. The assignment is always from right to left. The
right value is written into the left memory (into the variable).
Memory ☐ value
“Value” can also be a math problem (4 + 7).
Normally you must define whether these are yes/no values, whole numbers,
floating point numbers or text. Python does not care. The names of the
memories can be chosen freely, but they must consist of letters, numbers, or
the underscore. A leading number is not allowed. They cannot be reserved
words either.
my_money = 563.34
income = 2204.45
age = 40
name = "Martin"
male = True
myDict = {"name": "Peter", "age": 30}
What can you do with variables? You can calculate with the numbers, and
you can output the texts, for example.
incomePerYear = income * 12
print ("My data:")
print (name)
print (incomePerYear)
print (my_money)
print (male)
print (myDict)
The whole program looks like this:
my_money = 563.34
income = 2204.45
age = 40
name = "Martin"
male = True
myDict={"name":"Peter","age":30}
incomePerYear = income * 12
print("My data:")
print(name)
print(incomePerYear)
print(my_money)
print(male)
print(myDict)
The print command is pretty clever. It can output text as well as numbers and
Boolean expressions (True / False). But not both together.
print ("My annual income:" + incomePerYear)
This does not work!
Internally, Python knows that there are two different formats here (namely
string and float).
But you can convert numbers into text (string). The str() command is used for
this.
print ("My annual income:" + str (incomePerYear))
With the "+" strings are combined if Python has recognized strings. You can
do this also with a comma in the print command. Then a space is
automatically inserted.
Loops
If something needs to be done repeatedly, you can use loops. They are
available in every programming language. The most common two are “for”
and “while” loops.
For
If I want a text, four times, I can do it this way:
for i in [0,1,2,3]:
print("Pi")
There are a few things to keep in mind. All program parts that form a block
are preceded by a colon at the end of the previous line. If you enter the colon
in the editor and then press Enter, the cursor automatically jumps to the next
line and then indents 4 spaces (this is not so easy to recognize here with this
font). Python uses this to identify a block. Other programming languages use
the curly braces {} for this.
We take a closer look at the first line because it takes some time getting used
to it, even for people who have programmed for years. “for” is the keyword
that initiates a counting loop. Then a variable follows. Again, it can have any
name. However, i is often used. If i is already used elsewhere use j, k, l, ...
Then comes the keyword “in”. That makes the line easier to read. "For each i
in the array:"
Finally, an array or field must be specified. When variables contain several
values, one speaks of an array. This can be recognized by the square brackets.
There are numerous functions in Python that do things with or create arrays.
It would be annoying to have a loop from 0 to 1000 and have to write all the
numbers into the array. It is easier to do with the range command. The first
parameter is the start value, the second the number of elements in the array.
for i in range(0,4):
print("Pi", i)
The variable i can also be used in the block, as shown in the example above.
While
Sometimes you do not know how many times to repeat a loop. It can depend
on a condition, for example. Here you will use while loops. The condition is
in parentheses (which is not required, but easier to read) after the while
keyword. As long as this is true, the block is executed. Here, too, there is the
colon that introduces the block. In this case the block is two lines long.
i=1
while (i<256):
print("Pi",i)
i*=(i+2)
The variable i must already be defined so that the condition can be checked
successfully (without - you get an error message). We set the value in the first
line to 1. The print command is already known. It gets a little more
complicated in the last line. It is shorthand because programmers are pretty
lazy. It actually says:
i = i * (i + 2)
If you put the operator in front of the equal sign, you save an i. This also
works with addition, subtraction, and division.
So i + = 3 means: i = i + 3
If you run the program, the numbers 1, 3, 15 and 255 are displayed.
Why?
In the first pass, i = 1.
The math problem is then: i ☐ 1 * (1 + 2)
In the second pass i is 3
The math problem is then: i ☐ 3 * (3 + 2)
In the third iteration, i is 15
...
At some point the condition is no longer met and the loop ends.
Conditions
Very often you need conditions. For example: stop when the traffic light is
red, drive when it is green. There is the “if” command for this, which often
comes along with an “else”. We already had a condition in the while loop.
This is just a question that will be answered with either “True” or “False”. A
block follows the “if” condition. There is also an optional “else” block if the
condition was not met. The parentheses are not required in Python. I write
them because I am used to it.
color="red"
if (color == "green"):
print("GO!")
Too bad.
Unfortunately, nothing happens there. By the way, if you compare something
after if (or while), you need two equal signs (==). If you only use one, it is an
assignment. This is a very common mistake, not just among beginners. An
assignment is … not evaluated to anything. But not False. So, Python
interprets this as True.
To get an output, you add an else branch.
color="red"
if (color== "green"):
print("GO!")
else:
print("STOP!")
What if yellow is added now? There is an “elif” branch for this. A
combination of “if” and “else”.
color="red"
if (color== "green"):
print("GO!")
elif (color =="yellow"):
print("WAIT!")
else:
print("STOP!")
We have programmed a traffic light. However, it is always red. At the end of
the chapter we will put everything together and control the traffic lights via
the keyboard.
A little program
Now we will read in the keys r, y and g and, depending on the input, print the
color of the traffic light as text.
Python cannot easily recognize keystrokes. For this we must import a module
(readchar).
If the program does not run just read on!
import readchar as rc
import time
c=""
while (c!="e"):
if (c == "r"):
print("red")
elif(c == "y"):
print("yellow")
elif(c == "g"):
print("green")
c=rc.readchar()
time.sleep(2)
I have saved my program under test/readchar_test.py.
If you start the program now, you get another error message. Great!
Traceback (most recent call last):
File "/home/pi/test/readchar_test.py", line 12, in <module>
c=rc.readchar()
File "/home/pi/.local/lib/python3.7/site-
packages/readchar/readchar_linux.py", line 11, in readchar
fd = sys.stdin.fileno()
io.UnsupportedOperation: fileno
Unfortunately, the readchar command cannot be executed in the editor.
In all honesty: If, as a beginner, I had to find out all of this on my own, I
would throw the Pi in a bin. Unfortunately, there will always be things that
lead to errors. But you are not the first to make mistakes, and the internet is
full of people who love to help and forgive beginners.
In Linux, almost everything works in the terminal. And that is where the
keyboard input works, too. But how do you start a Python program in the
terminal?
Start Python in the terminal
To do this, open a terminal with Ctrl + Alt + t or via the menu at the top left.
You are then automatically in your home directory (~). If you have created
the “test” folder as recommended, you can now start the program with:
python3 test/readchar_test.py
I named my program readchar_test.py. You may have used a different name.
The python3 command executes the following Python program. If it is
annoying to always write “test/”, you can switch to the test directory. This is
possible with the change directory command. Because it is used so often, it
has been abbreviated to “cd”.
cd test
changes to the test directory. You can see that in the line in which the cursor
is flashing.
Now let us open a terminal. The copied link cannot be used directly. The
appropriate git command is still missing. Namely git clone.
git clone your_link_from_the_clipboard
Now everything will be copied to a new folder under your home directory.
The new folder has the same name as the online repository. But this only
works if this folder does not yet exist. Otherwise an error message appears.
If everything is ok, the online directory will be copied to your computer.
Currently only the readme.txt file is in it. It is an exact copy (a clone) of the
online repository. Now let us add something.
Upload
The term “upload” is not used in git. But that is actually what we are going to
do now.
First you create a small Python program and save it in your repository. What
you write in there is not relevant at this point. The program only needs to be
on the Pi in the newly created folder. Your folder will be called differently,
but for the sake of simplicity I will call it “dummy”. The program (I call it
test.py) must be saved in the dummy folder.
Under commits you can see all changes that you have uploaded with “git
push”.
With
git status
you can display on the Pi whether you still have programs that still need to be
“committed” or that still need to be “pushed”. If the files are red (and not
green) you still need to add them with “git add”.
Check out
We now want to turn back time and remove a faulty program.
To do this, we write another program called test2.py and push it to github
again. It is a good practice to add, commit, and push.
If you want to have the previous/last status on your computer again (i.e.
before you have created and uploaded test2.py), you need the HASH code of
the appropriate commit. You can find it on github.com under the commits on
the right-hand side. A HASH code is a long, unique sequence of letters and
numbers. We need the code from "comment".
Only a few characters are displayed. The real HASH code is longer. You can
copy the code using the copy symbol. You then have to insert it on the Pi
behind git checkout. Do not worry, we will turn it back in a moment.
git checkout copy_HASH_Code_here
If you now look into your folder (for example with the file manager or in the
terminal with the command “ls -lah”), you will see that test2.py is no longer
there. We have restored the old state.
The code is traced on a branch. The main branch is called master. This is the
current end of all uploaded data.
Download
If several people are working on a project, you can use
git pull
to get the current status from github. But only if you have previously
connected to the repository on github.com using git clone ...
Conclusion
There is a lot more you can do with git. If you want to write complex
programs, this is what you should study.
Get the robot code
This is for readers who do not work with git, but just want to avoid typing. I
will not print all the program lines later, but only refer to the code from the
repository.
Open a new terminal!
We now want to clone the data into a folder called “code”. To be able to
clone the repository, the code folder must not yet exist. If you already have
such a directory, you can delete it completely (but this should not be
necessary at this point) with
rm -R code
rm stands for remove, the parameter -R means that the directory will be
deleted completely recursively. And “code” is the name of the directory to be
deleted.
Then you can clone my repository.
git clone https://github.com/Pi-Robot/code.git
Now the typing is over. At least for you.
Python part 2
In the second part of the Python course I will explain comments, exceptions,
functions, the import command and how to open files. Finally, there is a
small program with which you can control the LED via a switch or the
keyboard.
Comments
If you look at the written code again after a week, you often do not know
what you were thinking. With difficult loops and branches, this is sometimes
the case on the next day. Therefore, commenting on the code is
recommended.
In Python, simple comments start with a "#".
# print hello to the screen
print("hello")
The comment appears in striking orange in the editor.
If you want to comment out a larger area, you can do so with three quotation
marks:
"""
print("nothing")
print("nothing")
"""
Commenting with three quotation marks is normally used for docstrings.
This commented out area is colored green by the editor.
Exceptions
Sometimes code crashes and throws out an error message. Error messages are
good, since they help the programmer to know what went wrong. Sometimes,
however, this error is expected, and one would like to react to it without an
error message. A program crash during operation should be avoided.
try:
number=123
print("Hello" + number)
except:
print("Something went wrong")
finally:
print("Call this always at the end")
In the example above, an attempt is made to combine text with a number. We
already know that this does not work with a plus (but with a comma).
Therefore, it is enclosed in a try block. If an exception occurs, the “except”
block is executed. You can add a clean-up function (finally block) that is
always executed.
The “finally” block does not have to be used.
We will use it when we want to react to the termination of a non-ending code
(endless while loop).
The little program can be found under code/course/exception.py
Functions
In every programming language there are functions, i.e. blocks that fulfill a
specific task and are often called with parameters. As an example, I will show
a function that prints the status of a Boolean variable.
def showState(state):
if (state == True):
print(“On”)
else:
print(“Off”)
We call a function with the function name and the appropriate parameter, but
only after it has been defined. Python processes the code "live" from top to
bottom. If the function is called too early, the definition has not yet been
made and an error message appears.
showState(True)
This line then writes “On” in the shell. You can find the associated code
under code/course/function.py.
Importing modules
Some functions can be grouped into modules. These can then be imported.
We have already done this with RPi.GPIO, “time“, and “readchar”. But you
can also create your own modules. That makes the program more compact
later. Unfortunately, not always easier because you can no longer see what is
happening in the modules. Meaningful names are useful when naming the
functions.
For a test I change the program function.py slightly and save it under
function2.py. I only removed the two function calls.
We will then write in a new program.
import functions2 as fc
fc.showState (True)
It is that easy to import your own functions. The program can be found under
code/course/import.py. It does nothing else than printing "On". But the call of
the actual print command is no longer in import.py but in function2.py.
Open files
Many programs can read in a state (for example the current document) and
save it again. There are write and read functions for this. In Python this is
complicated or at least notated in an unusual way, but it is quite compact.
with open ("data.json", "r") as data:
states = data.read ()
The file is opened with open (filename, read “r” | write “w” | append “a”).
The parameter “r” is the standard value, so it can be omitted. The “with as”
command moves the result to a variable (data). It is important to note that the
open command does not read the file yet, but only prepares it for reading.
The actual reading is done with the read() command.
In this example I am reading in a JSON file. But this can also be any other
file. There is a reason why I use JSON here. This special format with curly
braces and square brackets can be easily imported with Python and then
processed further. It resembles a dictionary. Or an array that contains several
dictionaries.
Square brackets are arrays (fields) and curly braces are objects/dictionaries.
And there are key-value pairs in the objects, first an identifier or key and then
the associated value. If an object has further key-value pairs, these are
separated from each other by commas.
[
{
"pin":2,
"io":"out"
},
{
"pin":4,
"io":"in"
}
]
With only a little imagination, you can recognize two objects, each with the
properties pin and io. And the two objects are in an array.
import json
with open("data.json") as data:
text = data.read()
states = json.loads(text)
print("Pin: " + str(states[0]['pin']))
print("In/Out: "+ states[0]['io'])
print("Pin: " + str(states[1]['pin']))
print("In/Out: "+ states[1]['io'])
print("\nREAD IN LOOP")
for state in states:
print("Pin: " + str(state['pin']))
print("In/Out: "+ state['io'])
The program can be found under code/course/open_json.py.
To interpret JSON, you import the json module. Parsing (converting an object
to a dictionary) is done with the loads() command. The rest of the code then
displays the two dictionaries that have been read. Once for each dictionary –
first individually and then in a for loop to demonstrate different ways.
The objects read are then in the “states” variable as an array. The first
dictionary (states[0]) contains an array with the variables 'pin' and 'io'. The
inner array has no indices here (numbers from 0 to ...) but can be addressed
directly via the identifier or key. The value 2 from “pin: 2” is read out via
states[0]["pin"].
In the for loop, the “states” array becomes a “state” object in each pass. This
corresponds to states[0] in the first pass and states[1] in the second. You can
use “state” to access ["pin"] and ["io"] directly.
We will use a similar JSON object later to configure all inputs and outputs at
a central point. Only a few more parameters will be necessary.
A little program
So that you can see what the individual commands and structures are used
for, we will now write a program to switch the LED on and off using a switch
(two cables that are connected). The program can be aborted with Ctrl + c.
This key combination is common to exit Linux programs.
First, an endless loop is required so that the program does not end after a few
seconds. You can do this with a condition that is always True. The following
lines are not actual program code but are intended to show how it works.
while (True):
PROGRAM BLOCK
We will intercept the termination condition (Ctrl + c) with try-except so that
the GPIOs are cleaned up again. The corresponding error is called
KeyboardInterrupt.
try:
while (True):
PROGRAM BLOCK
except KeyboardInterrupt:
GPIO_CLEANUP
And now the program for switching the LED:
import pin
pin.load()
try:
while (True):
status = pin.In("switch")
if (status == 0 ):
pin.Out("led1", 1)
else:
pin.Out("led1", 0)
except KeyboardInterrupt:
print("End")
finally:
pin.cleanup()
The code is available at code/course/lightLED.py
That looks very compact. But where are the GPIOs set? This is done in the
new module pin. A JSON file is read there, and we have two functions for
reading in and reading out GPIOs. The associated configuration file looks
like this:
{
"led1": {
"pin": 2,
"io": "out"
},
"switch": {
"pin": 4,
"io": "in"}
}
code/course/config.json
The structure has changed to make it easier to interpret the names of the pins.
Using this identifier at the beginning of the object, it is easier to assign inputs
and outputs. For example, with:
pin.Out("led1", 1)
The real magic takes place in the pin module. There are still three things that
have not yet been explained.
The module consists of reading the config.json file and contain the In and
Out functions. Functions usually start with lowercase letters, but “in” is a
reserved word.
Here is the module, which can be found in code/course/pin.py:
import json
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
pinItems = {}
def load():
try:
with open("config.json", "r") as file:
items = json.loads(file.read())
except:
print("Error loading config.json")
exit(0)
global pinItems
pinItems = items;
for name, item in items.items():
pin = item["pin"]
if(item["io"] == "in"):
GPIO.setup(pin, GPIO.IN, pull_up_down = GPIO.PUD_UP)
if(item["io"] == "out"):
GPIO.setup(pin, GPIO.OUT)
def In(name):
global pinItems
return GPIO.input(pinItems[name]["pin"])
def Out(name,state):
global pinItems
GPIO.output(pinItems[name]["pin"], state)
def cleanup():
GPIO.cleanup()
The following positions are new:
pinItems = {}
This is what is known as a dictionary. This allows you to assign text
identifiers to an array. For example: pinItem["switch"] and then
pinItem["led1"].
global pinItems
Variables are only valid in the block in which they were defined. They are
initially not known in subordinate functions. If you want to access a higher-
level variable, you first have to say “global“ in the function. Without the
global, pinItems would be a new variable which, for example, would only be
known in the def In(name): function.
return
Functions can not only receive values, but also return them. In this case (def
In(name):) the HIGH/LOW state of the input is returned.
Now I am going to explain the different parts of the program because this is
the most fundamental part of the whole book. Once this has been understood,
the rest can be done.
import RPi.GPIO as GPIO
GPIO.setmode (GPIO.BCM)
This imports the RPi.GPIO module and with setmode() we say that the pins
are read according to the BCM regulation (there is also BOARD).
The load() function opens the JSON file and converts the text into
objects/dictionaries using json.loads(). These are then saved as a dictionary in
the pinItems variable.
At this point, you can read out the associated dictionary with
pinItems["led1"]. If you need the number of the pin, you can find it out using
pin = pinItems["led1"]["pin"]
In order to be able to access them, there is a function for dictionaries called
items(). This returns the identifier (name) and the content (item).
for name, item in items.items():
In the first run of the loop there is {"pin": 2, "io": "out"} in the variable
“item”. "Led1" is in “name”, but we do not need it here.
A distinction is then made whether it is an input or an output. The appropriate
GPIO.setup command is called accordingly. Now all necessary information is
in pinItems, and the GPIO configuration is complete. You can now get back
on it again and again and no longer have to worry about GPIO.
The two following functions In() and Out() either return the value of an input
or write it based on the name from the JSON file.
Hence a call works like:
pin.Out ("led1", 1)
“Out” gets the pinItem object with the reference "led1" and reads the “pin”
from it. It then assigns the value “state” to this pin.
We need the pin.cleanup() function so that an import of RPi.GPIO is no
longer necessary in the main program.
Have you had enough of blinking LEDs now? What you have learned so far
is hardly suitable for building a robot.
Motors
Yes, it is!
These are prerequisites so that you can control the motors. Comfortably.
Assemble motors
The motors that come with the chassis are usually not mounted. A red and a
black wire should be soldered to each motor. If you connect these cables to
the battery block (insert batteries first), the motor will turn.
Yeah, something finally happens!
A third hand is usually missing when soldering. Here you can help yourself
with adhesive tape to fix the cables.
Activate the motor
If you swap the cables, the motor turns the other way around. If you were to
reduce the voltage, the motor would turn more slowly.
We need three states for turn left, turn right and off. Furthermore, you have to
be able to adjust the voltage with the Pi.
The small minicomputer cannot supply 6 volts and a corresponding current
for the motors. Hence the battery pack and a controller are needed. The
L298N turns signals (very little electricity) into the control for the motor (lots
of electricity). Here is a sketch of the principle.
The three required states can be implemented with two lines because four
different states can be achieved by combining HIGH and LOW (see table
below).
The speed information supplies a voltage between 0V and 3.3V, achieved
through pulse width modulation. This is a trick you use to create a certain
voltage by having the Pi turn the output on and off very quickly. I will show
you how to do that. But in a simplified way, you can imagine that you can
freely set the output voltage as a percentage between 0V and 3.3V.
The following figure shows an L298N with the wiring for a motor. The
following applies: The blue sockets, where you must screw the wires tight,
carry a lot of current. The pins at the bottom right hardly carry any current.
These pins are the signal lines.
On the left there are the connections OUT1 and OUT2 (they are also labeled
from the bottom). The first motor is connected there. The three blue sockets
show the labels +12V, GND and +5V. But that does not mean that you can
choose whether to connect 5V or 12V. The controller is always fed with the
two left connections. But if you need 5V for something else (for example for
the Pi), you can grab the voltage there.
And now to the signal lines. The ENA and ENB often have a plug connection
first. You can remove them. In1 and In2 are the forward and backward
information. And the speed information is connected to ENA. This is it for
motor one.
Manage the controller with the Pi
We have already seen that the vehicle can turn. Now this should happen with
a Python program. For this we first give the full voltage (GPIO.HIGH) to
ENA. How to adjust this, and thus change the speed, is coming soon. Now
the program should first turn the motor forward and then backward for a few
seconds. We already have the tools to do this. The new hurdle is wiring the
connections. And you must know the combination for forward, backward and
stop. There is another difficulty: braking. But since we have a gearbox in the
motors, we will not use that.
In1 In2 Motor
LOW LOW brake
LOW HIGH forward
HIGH LOW backward
HIGH HIGH brake
The "Off" state is achieved by a 0V signal at ENA, which seems logical.
We only need three outputs named ena, in1 and in2 for the program. A JSON
file to read in the configuration was already available in the previous chapter.
The associated reading function has been expanded a little so that we can
read in any file name now. At the end of the new pin.py file, there is also the
function for pulse width modulation. That will come later. We will now use a
new folder. The program code can be found under code/motor/.
This time we are going to use pins 14, 15, and 18.
in1 = 14
in2 = 15
ena = 18
This information is stored in the file code/motor/config1.json
The program code/motor/motor1.py then looks like this:
import pin
import time
pin.load("config1.json")
print("forward")
pin.Out("in1",1)
pin.Out("in2",0)
pin.Out("ena",1)
time.sleep(3)
print("backward")
pin.Out("in1",0)
pin.Out("in2",1)
pin.Out("ena",1)
time.sleep(3)
pin.Out("ena",0)
print("stop")
pin.cleanup()
Incidentally, I started with four motors and an elaborate chassis, only to find
out that it is next to impossible not to drive straight. Three wheels are the
better choice.
The block for going forward or backward is already quite compact.
pin.Out ("in1", 1)
pin.Out ("in2", 0)
pin.Out ("ena", 1)
But you can easily imagine that you can summarize this as a function with a
descriptive name. In addition to the pin module, there will also be a “control”
module in which there will be simple functions for driving.
Pulse width modulation (PWM)
First imagine that you can set an output signal between 0V and 3.3V to
control the speed of the motor.
Technique
With pulse width modulation, an output is switched on and off very quickly.
The switching frequency can be adjusted. Likewise, the ratio of off to on (the
"width"). If the signal is 80% off and only 20% on and the whole thing
changes back and forth very quickly, then you only see 20% of the 3.3V at
the output, because the subsequent system does not notice the change that
fast. We also believe that movies come out of the television and not - as in
reality - a new picture every 1/60 of a second. Our eyes are too sluggish for
that. It is the same with electronics if the frequency is high enough. An
idealized signal looks something like this:
Two milliseconds for one cycle correspond to 500Hz. This is the unit that can
be entered in the Python program.
The signal repeats itself continuously until a new command comes in.
How to program
We are not the first to need PWM, so the procedure for the RPi.GPIO module
is quite simple.
For initialization you have to write that it is a PWM output, and then the
frequency is required. However, here you get an object back that can perform
several functions. We will then need three of these. Setting the percentage,
starting, and switching off.
GPIO.setup (pin, GPIO.OUT)
pwm = GPIO.PWM (pin, 500)
It is imperative that the pin is defined as an output. The following line then
activates the PWM. The number of the pin must also be specified here. The
following number is the frequency in Hertz (500Hz). “pwm” is just an
identifier that can be anything. This can then be used to call the functions of
the object.
The PWM is initialized with
pwm.start(0)
The function to stop is then
pwm.stop()
Setting the percentage is
pwm.ChangeDutyCycle (50)
where 50 is the percentage. The possible range of values is from 0 to 100.
(101 or above leads to an error.)
How it is used in the module pin
We just want to enter it in the JSON configuration file and not worry about
the initialization. One function that controls the motor should suffice.
{
"in1": {
"pin": 14,
"io": "out"
},
"in2": {
"pin": 15,
"io": "out"},
"ena": {
"pin": 18,
"io": "pwm"}
}
The only thing that has changed here is the text after "io". It is now "pwm".
And now the changes in the module “pin”.
For the frequency we always use 500Hz as the standard value.
global pinItems
for name, item in items.items():
pin = item["pin"]
if(item["io"] == "in"):
GPIO.setup(pin, GPIO.IN, pull_up_down = GPIO.PUD_UP)
if(item["io"] == "out"):
GPIO.setup(pin, GPIO.OUT)
if(item["io"] == "pwm"):
GPIO.setup(pin, GPIO.OUT)
pinItems[name]=GPIO.PWM(pin,500)
This is an excerpt from the load() function in the code/motor/pin.py module.
All PWM pins are configured in the file as described above.
Now the level function is added. This is similar to the functions for In() and
Out(), but a value of the desired level must be set (the percentage of PWM).
Up to about 50% nothing happens, except that the motor starts beeping
around 500Hz, which is due to the PWM. If you set a frequency other than
500Hz in the pin module, you will hear a different tone. The threshold (in the
graphic at 50%) depends on the voltage. The battery pack supplies approx.
6V. There are also packs with 5 batteries, in which case the threshold is
lower.
It is important to know that rechargeable batteries supply less than 1.6 volts.
Only about 1.4 volts. Four rechargeable batteries are not enough for the
motors. You should use a battery pack with 5 or 6 compartments.
It will always be difficult to keep the motor running slowly. You can get
around that with a regulator and an H206. I will get to that later.
The car
At this point we have everything we need for a remote controlled (Wi-Fi) car.
We can send signals to the Pi using the keyboard, and the L298N and the
signal lines will drive the wheels. But I would like to add a Python module
(control.py) to make things easier (after developing the control module).
There will be a function that controls the right and left wheel in just one line.
The car should be steered using the W, A, S and D keys. W and S for forward
and backward, A and D for left and right. Every other button brakes the car.
This is especially important because the little speedster drives quickly.
The module control
I would like to have a function that controls the left and right wheels at the
same time. In the shape
control.move(left, right)
To keep this easy, you have to tell this module which signal lines are used for
the motors. We need another JSON file. Alternatively, we can write even
more information into the existing one. An initialization function is required
in any case. I choose to extend the existing file. There will be a new key for
control and the values can be
right1
right2
rightPower
left1
left2
leftPower
These identifiers are fixed, while the “name” keys ("in1", "in2", ...) can be
freely selected. For the right side the new config.json looks like this:
{
"in1": {
"pin": 14,
"io": "out",
"control":"right1"
},
"in2": {
"pin": 15,
"io": "out",
"control":"right2"
},
"ena": {
"pin": 18,
"io": "pwm",
"control":"rightPower"
}
}
There is the logical connection between the pins and the desired function.
When control.move(20, 60) is called, the information for left and right is read
out. The right value (e.g. 60) is then used to generate and set all required data
for the right side.
Test
To test the whole thing, you have to connect another motor. For this we use
GPIOs 25, 8 and 7.
in3 = 7
in4 = 8
enb = 25
You can find this in the file code/car/config1.json.
The corresponding lines are assigned on the L298N. Most of the time is spent
making sure that the “forward” command causes the wheels to turn in the
right direction. The front and back of this robot are relative. If it does not fit,
you can either turn the signal lines (simple) or swap the motor cables
(complicated).
The actual test program is now very plain. You can find it in code/car/car1.py
import pin
import control
import time
pin.load("config1.json")
control.load("config1.json")
control.move(50,-60)
time.sleep(3)
control.move(-100,90)
time.sleep(3)
pin.cleanup()
Our two modules pin and control are now included. The configuration file
must be read in twice, once into the pin module and once into the control
module. Now you can control the two motors with the command
control.move(). Since all GPIO-specific things are done in the modules, the
program is now very short.
And now all together
Now we want to control all the motors and let the car drive. It is time for my
desk to be tidied up again.
In my version, I mounted the L298N at the front. To fix it, I screwed it to the
chassis. In the version with the Pi table, you still have a few plastic screws
and can mount the controller with two sleeves so that it sits firmly.
I write this in a few lines, but it was a lot of fiddling. That must have taken an
hour. And when I tried to check it with code/car/car1.py, it did not work!
Because I forgot the ground compensation line. Perfect! I mentioned it above.
Now everything is wired. To switch off the battery voltage, you can either
install the provided switch or remove a battery from the battery pack. We
should run the test program again. If the motors do not turn in the same
direction (namely to where you define "forward") you can swap the signal
lines.
If it does not work
If the motor is not turning at all, then maybe the ground compensation line is
missing (a classic). Or the pins on the Pi do not match the configuration in
config1.json.
If there is just one motor that does not want to move, it is certainly due to the
wrong wiring. If one of the motors only turns in one direction, ENA or ENB
is probably on a signal line.
Is the red LED on the L298N lit?
Are the motor cables connected correctly?
Otherwise you must check all cables for the corresponding motor again.
It is very unlikely that one of the components is defective, but of course it can
happen.
Let´s go!
Now is the time to assemble everything and create a new program with
keyboard input. We have already read in a key in a previous program. And
the line for driving the engines is now also quite manageable. All we have to
do now is figure out how to respond to the various keys. One possibility
would be to carry out a specific action for a defined time on each key. For
example: w is one second forward.
Whatever we decide, there must be a branch (if, elif, else) after a key has
been read.
c=rc.readchar()
if (c == “w”):
# code for w
elif (c == “d”):
#code for d
…
It certainly makes sense to install an emergency button if the car threatens to
hit an obstacle. Since a certain key cannot be found quickly enough in such
an emergency, we simply take all the other keys and request in the else
branch that the car should stop.
else:
# stop the car
I choose the following option here (based on my son's suggestion). Driving
right and left should fall back to driving straight ahead when you no longer
press the button. The more you press, the greater the curvature. Forward and
backward are retained and can be increased by pressing the corresponding
key several times. Once the car drives, all you have to do is choose left and
right.
To do this, we need a timer that keeps the program waiting so that the
changes do not happen too quickly. A few milliseconds are enough.
Unfortunately …
However, the readchar module is unsuitable for implementing the desired
functionality. It waits until a character has been entered. However,
calculations should be carried out permanently, namely the automatic turning
back of the "steering wheel". It will not work that way. But how do other
programs do that? A few Python games are preinstalled on your Pi, for
example this soccer game (Raspbian Full), which I do not understand. It
reacts to keys and at the same time the opponents keep running (probably the
reason why I always lose).
Fortunately …
Programming Python games is of course not easy, but the programmer is
given several tools that make it manageable even for beginners. It is always
essential to have a separate window that waits for inputs or mouse
movements or clicks (events). We make use of that.
We need two imports
import pygame
from pygame import *
Then the game has to be initialized. Already with this line, a friendly message
from the pygame community and an associated link appear after the program
starts.
pygame.init()
Now you have to create a window, which receives the input focus (the active
window). The size is arbitrary since we do not want to write a game. But
Pygame will not work without the window.
size = width, height = 600, 400
screen = pygame.display.set_mode(size)
These are all just prerequisites to enable us to respond to key events. The
actual event processing comes now.
if event.type == KEYDOWN and event.key == K_ESCAPE:
GPIO.cleanup()
pygame.display.quit()
sys.exit()
elif event.type == KEYDOWN and event.key == K_w:
motorRight += 10
motorLeft += 10
Here you can see that the program is aborted when the ESC key is pressed,
and when the w key (K_w) has been pressed, the value for the motor speed is
increased by 10%.
With this change it is no longer necessary to start the program in the terminal.
This can now also be done from the IDLE editor. The whole program
code/car/car2.py looks like this.
import pin
import control
import time
import sys, pygame
from pygame.locals import *
pygame.init()
pin.load("config1.json")
control.load("config1.json")
motorRight = 0
motorLeft = 0
size = width, height = 600,400
screen = pygame.display.set_mode(size)
while(True):
time.sleep(0.2)
for event in pygame.event.get():
if event.type == KEYDOWN and event.key == K_ESCAPE:
pin.cleanup()
pygame.display.quit()
sys.exit()
elif event.type == KEYDOWN and event.key == K_w:
motorRight += 10
motorLeft += 10
elif event.type == KEYDOWN and event.key == K_s:
motorRight -= 10
motorLeft -= 10
elif event.type == KEYDOWN and event.key == K_d:
motorRight += 10
motorLeft -= 10
elif event.type == KEYDOWN and event.key == K_a:
motorRight -= 10
motorLeft += 10
elif event.type == KEYDOWN:
motorRight = 0
motorLeft = 0
if(motorLeft>motorRight):
motorLeft-=1
motorRight+=1
if(motorLeft<motorRight):
motorLeft+=1
motorRight-=1
motorLeft=min(motorLeft,100)
motorLeft=max(motorLeft,-100)
motorRight =min(motorRight,100)
motorRight =max(motorRight,-100)
control.move(motorLeft,motorRight)
In the first part, all keys are queried. If the values of the motor speed for left
and right differ, this is compensated in each run of the endless loop (the
events are much faster). Finally, you have to make sure that the value for the
speed does not exceed 100.
In this code not a single pin is assigned in the main program. This is all done
in the configuration area.
A server
Using the keyboard of a laptop or PC is sometimes a bit cumbersome. We
will now try this with the mobile phone.
VNC works on the phone too, but it is a bit bulky. We want to build a small
website that can be used to control the robot.
Web servers are super complicated unless you are using Python. There are
several modules there that make things relatively easy. A common module
with many tips on the Internet is “flask”. With just a few lines we will now
build a simple website.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def showIndex():
return "INDEX"
if __name__ == "__main__":
app.run()
The program code can be found under code/server/web1.py.
After starting the program, you will find the information about what you must
enter in the Pi browser (next to the raspberry in the menu) to open the page.
That should be http://127.0.0.1:5000. By the way, there is a colon between 1
and 5000 (this is called a port). And https does not work. It must be http.
Wow, that says "INDEX"!
With just seven lines of code, a server was created that creates a website.
Admittedly, it is simple and only consists of the word "INDEX".
The execution of the server is to be terminated with "Ctrl + c". This can also
be found in the note.
What is happening? The import is of course required, and the app is
initialized in the second line. The object is initialized with the __name__
variable, which is part of the Python standard.
The important part is the three following lines. The route is specified with
@app.route("/"). Everything that begins with @ is called annotation. This
contains the paths to the websites in the browser. It just looks like it is a
directory (sometimes it actually is). If you were to enter @app.route("/ test"),
you would have to specify this part in the browser (http://127.0.0.1:5000/test)
in order to call up the function under the "@".
Important:
It does not matter what the function is called. But a descriptive name would
make sense. If you enter via the main route ("/"), this function is carried out
and returns the text "INDEX".
The last two lines start the program if it was started as the main program and
thus not loaded as a module. From now on the server listens to port 5000.
This is the standard port of flask.
And other computers?
Unfortunately, the host (127.0.0.1) is fixed. This is the IP address of the
current computer. Always.
Fortunately, you can configure it so that you can also access it from all other
computers in your own home network. To do this, expand the app.run()
command.
app.run(host="0.0.0.0")
If you start the server again, you can reach it from all other PCs and
telephones at home using the Pi's IP address. For me it is 192.168.2.110. The
last two numbers are surely different for you.
If you enter http://192.168.2.110:5000 in the mobile phone browser, you will
also see the word INDEX.
You can also change the port.
app.run(host="0.0.0.0", port=8080)
If you want.
See code/server/web2.py.
Respond to requests
We are already responding to the request for the route "/" and returning text.
Now let us return a full webpage that has a couple of buttons. If you click on
one of the buttons, another route will be called up.
Writing the HTML website in the Python code quickly makes things
confusing. Therefore, we load the page from an HTML file (index1.html)
which is also in the code/server folder. Now for the first time we are loading
something other than JSON files. But it works the same way. We already
discussed reading files above.
filename = "index1.html"
@app.route("/")
def showIndex():
with open(filename, "r") as file:
return file.read()
A basic structure of a website (code/server/index1.html) looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-
scale=1.0" />
<title>Car remote control</title>
</head>
<body>
</body>
</html>
I have been programming websites for a very long time, but I still get the
basic structure from the Internet by searching for “html scaffolding”.
HTML pages consist of a header and a body. In the header you can activate
the font set, store information to help search engines, enter the title of the
page (car remote control) and reload scripts and styles. Except for the title,
none of it is displayed.
The user of the website can see the body. Since there is nothing between the
<body> tags, the page is empty. We now put a few buttons between the
<body> tags.
<button onclick="window.location.href='/w';">
Forward
</button>
<br>
<button onclick="window.location.href='/s';">
Backward
</button>
<br>
<button onclick="window.location.href='/stop';">
STOP
</button>
Three buttons appear. These react to a click (onclick), and then call a function
that fetches a new page.
window.location.href = '/s';
If the “http” is missing, the link is relative. In this case,
http://192.168.2.110:5000/s is called.
The Flask server does not yet react to this route. We change that with a
second function in code/server/web3.py.
@app.route("/<direction>")
def moveTo(direction):
print(direction)
with open(filename, "r") as file:
return file.read()
It is still incredibly little code. After “route” there is now ("/<direction>").
This means that everything after the first slash is immediately written into the
variable “direction”, because of the angle bracket. The “direction” identifier
is arbitrary again, but it will be accessed in the following. The function (the
function name is also arbitrary) receives the parameter “direction”, and you
can work with it. In this example we do nothing other than simply print the
text after “/”. There is no filter here, and when you go to the page in the
browser you can now enter anything. Finally, the website is read in again and
returned to the browser.
For example, if you try a few of the buttons, you get the following output:
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
w
192.168.2.100 - - [31/May/2020 14:46:33] "GET /w HTTP/1.1" 200 -
favicon.ico
192.168.2.100 - - [31/May/2020 14:46:33] "GET /favicon.ico HTTP/1.1" 200
-
s
192.168.2.100 - - [31/May/2020 14:46:33] "GET /s HTTP/1.1" 200 -
favicon.ico
192.168.2.100 - - [31/May/2020 14:46:33] "GET /favicon.ico HTTP/1.1" 200
-
stop
192.168.2.100 - - [31/May/2020 14:46:34] "GET /stop HTTP/1.1" 200 -
favicon.ico
192.168.2.100 - - [31/May/2020 14:46:34] "GET /favicon.ico HTTP/1.1" 200
–
The routes w, s and stop were written to the screen with the print command.
The program can be ended with "Ctrl + c".
With a little imagination, you can now guess that the Pi's motors could be
controlled instead of the print command. We will do that in the next section.
A slightly nicer website
Websites can be designed in any way you want. I do not want to clutter the
code here, but I want to enhance the buttons. This is usually done with CSS
(Cascading Style Sheets). That would be the third language (Python, HTML
and CSS) that we are using here.
In addition, we will no longer print the inputs, but let the robot drive.
In my first version of this book I created a rather complex website at this
point. You will be introduced to the matter a little more slowly since I have
now inserted a simpler page. This is composed of five blocks with which you
can drive forwards, backwards, left, and right. And of course, stop.
Some Python
We find the required components in the modules pin and control. The website
has been expanded to include the new button design. Except for the design,
you should be able to do it yourself. But I know that you want to test quickly
whether it works. And there is surely a little laziness involved. I am making
this code available again and explaining it.
First comes the Python part. It is only slightly longer than before because we
use the pin and control modules. There are no longer any restrictions on
plus/minus 100. The HTML page will only send legal values.
from flask import Flask
import sys
import pin
import control
app = Flask(__name__)
filename = "index2_simple.html"
pin.load("config1.json")
control.load("config1.json")
@app.route("/")
def init():
with open(filename, "r") as file:
return file.read()
@app.route("/move/<left>/<right>")
def moveTo(left,right):
control.move(int(left),int(right))
with open(filename, "r") as file:
return file.read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
The changes can mainly be found in the moveTo() function and in the route
@app.route ("/move/<left>/<right>"). The website is called here when “...
/move/75/75” is requested. Or any other number for left and right. And these
values are sent to the function for driving the robot. It is almost too easy ...
Website
... if it were not for the website. The attentive reader can see in the code (I
would have missed it too) that the index2_simple.html file is now being
loaded. This is the easy version. In index2.html there is a more complex one.
If everything works, then the two versions should look like this:
In the following, I will explain a bit of CSS and JavaScript (the fourth
programming language).
CSS
I will not explain everything in detail, style sheets are too extensive for that.
First off, a short list of some of the most common CSS code and what it
means.
CSS Meaning
position:relative; The element is moved relative
to the previous position.
position:absolute; The element is absolutely
moved to the parent element.
At the top left, the coordinate
is 0, 0
color: The color of the text.
backgound-color: The color of the background
left: and top: The left position and the
distance from the top. This
corresponds to x and y
coordinates, but y points
downwards. You must specify
the unit. This can be px (pixel)
or % (of the higher-level
element). For example: top:
45px;
width: and height: The width and height of the
element.
font-size: That is the font size. font-size:
14px; can be read easily. 6px is
pretty small.
margin-left: The left margin (px/%) outside
until the element is shown.
There are also: margin-top,
margin-right, ...
padding-left: Inner left margin (px/%) until
the content (e.g. text) begins.
About the color:
The computer combines colors together from red, green, and blue
components. There are two options
#RGB
or
#RRGGBB
The numbers for R, G and B go from 0 to 15 in the first possibility, with 10
to 15 being replaced by A to F. So #0F0 would be 100% green.
In the second example the numbers go from 0 to 255. So, from 00 to FF.
#0000FF is 100% blue.
All together in a small example.
<div
style="position: absolute;
left:100px;
top:10px;
width:100px;
height:20px;
padding:10px;
background-color:#668866;
color:#cccccc;
font-family:arial;
font-size:16px;"
onclick="window.location.href='/move/80/80';"
>
FORWARD
</div>
This creates a light green rectangle with a gray font of the size 100px x 20px.
The padding makes it 10 px wider in each direction. The upper left corner is
at x = 100px and y = 10px.
Class
If you have similar texts in the styles, you can write them in a style block
<style> </style> and give them a unique name. Then it is sufficient to call
this name with class = “css_name“.
<style>
.test_container{
position:absolute;
left:200px;
top:100px;
width:100px;
height:100px;
background-color: #00cccc;
}
.test_content{
position: relative;;
margin-left:20px;
margin-top:40px;
width:40px;
padding-left:20px;
background-color: #fff;
color:#ff0000;
font-size: 10px;
}
</style>
<div class="test_container">
<div class="test_content">Hallo</div>
</div>
This looks like this:
An even nicer website
The wheel from the second example (code/server/web4.py and
code/server/index2.html) was created with pure CSS, it is not an image.
Operation is simple; depending on where you touch the pane, the robot
moves forwards, to the right and so on. The further you are from the center,
the faster the robot runs. Values less than 30% (where the robot would not
drive anyway, only beep) do not occur here.
Again, we are using DIVS. You can do anything with it. But they do not have
shape or color until you put one on them. It is a kind of container in which
you can put other elements. They are very flexible. If you only see the word
<div> in the code, you do not yet know what it is used for. The block in the
HTML code that controls the movements looks like this:
<body>
<div class="pieContainer">
<div class="circleContainer"><div class="segment segmentColor1" >
</div></div>
<div class="circleContainer turn1"><div class="segment
segmentColor2"></div></div>
<div class="circleContainer turn2"><div class="segment
segmentColor3"></div></div>
<div class="circleContainer turn3"><div class="segment
segmentColor4"></div></div>
<div class="circleContainer turn4"><div class="segment
segmentColor5"></div></div>
<div class="circleContainer turn5"><div class="segment
segmentColor6"></div></div>
<div class="circleContainer turn6"><div class="segment
segmentColor7"></div></div>
<div class="circleContainer turn7"><div class="segment
segmentColor8"></div></div>
<div class="pieBackground" onclick="getPosition(event)"></div>
<div class="innerCircle" onclick="move(0,0);">
<div class="content"><b>STOP</b></div>
</div>
</div>
</body>
That is very confusing. Therefore, we dissect the whole code.
On the outside we have
<div class = "pieContainer">
...
</div>
which embraces the whole structure. This will later become a rectangle that
we can move. This means that all internal elements are also moved. Inside are
various elements (again divs). There are eight colored pie slices. An example:
<div class="circleContainer turn1">
<div class="segment segmentColor2"></div>
</div>
Each piece of pie consists of a “segment” and a “circleContainer”. The
segment is a triangle (but you cannot see it that way) and the circleContainer
cuts off the outer edge in a circle. “segmentColor2” sets a color (color
number 2) and “turn1” rotates the piece by 22.5 °. There is a total of eight
segmentColorN and seven (the first segment is not rotated) turnN classes.
This will be the stop circle in the middle:
<div class="innerCircle" onclick="move(0,0);">
<div class="content"><b>STOP</b></div>
</div>
This is the first time you see a function. move(0,0) sets both motors to zero.
This function can be found later in the JavaScript code.
If divs are on top of each other in the same place, the last one is always on
top. That is why there is an invisible div between the pie pieces and the stop
circle, which reacts to click events and then calls a function. The pie pieces
do not react to anything, they are only there for the look.
<div class="pieBackground" onclick="getPosition(event)">
</div>
It is difficult to be confronted with so much new stuff. But if you take a little
time and try out the examples yourself, you will figure it out. The simple
variant under index2_simple.html is sufficient in most cases anyway.
Hopefully, it is clear at this point how to make more pie pieces (just keep
turning and using a different color). That is all just to get some color on the
screen. For something to happen, you need JavaScript.
JavaScript
JavaScript is a scripting language that has nothing to do with Java. It is so
freely parameterizable that some programmers would wish it were not. For
this reason, a superset (TypeScript) was developed that checks types for
compatibility. I prefer JavaScript.
We already saw the command for calling a page (URL) in the last chapter.
This has now been embedded in a function, and the target address now
matches our flask server.
function move(left, right){
window.location.href='/move/'+left+'/'+right;
}
This function is called for example with
move(0,0);
which causes the robot to stop. This function is called if you tap or click on
the large <div> in the middle.
It is a little more laborious to query all other combinations on the pie pieces.
You could assign a function to each pixel, but that would be very tedious, and
programmers are not known for their writing skills. We need a concept to
make things easier. This usually (as in this case) means that an outsider has to
think for a long time before he understands what is going on in this code at
getPosition(e).
I start by reading out the x, y coordinates. That is already the first hurdle,
because the function for reading out the clicked position always returns the
pixels from the top left, from the browser window. e.clientX and e.clientY
provide the left and top data.
I built in the values +/- 70 so that you can see more quickly that it is a sine
function depending on the angle. It is rotated by 45°.
In almost all programming languages, the function “atan2” is used to build an
angle from the x-y values. This is an extension of the arctangent function,
with which you can find the angle from x-y pairs.
Math.atan2(y, x)
I do not know why they always swap x and y.
var angle = Math.atan2(y,x) ; // rad
It should be noted that the angle is obtained in radians (from -Pi to + Pi). For
the left motor, turn the sine function by + Pi/4, i.e. 45 °.
var left = Math.sin(angle + Math.PI/4); // +45° +Math.PI/4
var right = -Math.cos(angle + Math.PI/4);
The negative cosine function is used for the right motor.
Unfortunately, that is not yet entirely working. At 90 ° (i.e. forwards) the
function delivers too little and at 45 ° too much. This is now superimposed
with a waveform to get rid of this this error. The cosine function used
oscillates four times faster than a normal one, and the amplitude is slightly
less than 0.414.
var corr = 1 + 0.35 * Math.cos(4*angle);
I admit this is not entirely trivial. All in all, the function looks like this:
function getPosition(e){
var rect = e.target.getBoundingClientRect();
var radius = 165;
var x = (e.clientX - rect.left)-radius;
var y = -((e.clientY - rect.top)-radius);
var angle = Math.atan2(y,x) ; // rad
var left = Math.sin(angle + Math.PI/4); // +45° +Math.PI/4
var right = -Math.cos(angle + Math.PI/4); //
var corr = 1 + 0.35 * Math.cos(4*angle);
var power = corr * 100 * Math.sqrt(x * x + y * y) / radius;
move(Math.floor(power*left),Math.floor(power*right));
}
The program can be found under code/server/web4.py and
code/server/index2.html.
Try out
You realize very quickly that driving straight ahead does not always mean
driving straight ahead. The motors turn at different speeds with the same
voltage, so that you go either left or right.
In my model, the battery was too heavy so that the shopping cartwheel, which
was supposed to turn freely, did not turn freely. With a little shift in weight, I
was able to solve the problem. One more reason for a small battery.
It is enough if you have maybe six directions and a big stop button. You can
certainly expand the simple version of the website on your own and add more
buttons at useful places. The complicated example is supposed to show that
there are no limits to the design.
There is another behavior that is very annoying. The constant reloading of the
entire HTML page. The amount of data is still manageable here, but problems
can arise with more complex pages. Therefore, we will be sending commands
in the background shortly.
Ajax
We had the problem of the website reloading constantly on the mobile phone.
Other websites (such as Facebook, Reddit or Amazon) reload information
without the whole page being refreshed. This happens in the background and
the concept used is called AJaX. AJaX stands for Asynchronous JavaScript
and XML.
AJAX is a JavaScript function that sends data from the website to the server
without the browser's reload function being aware of it. You can even react to
the server's response, but we do not need that for the car. It is enough to send
information.
Since we are not using the Internet Explorer, the JavaScript code to send is
simple.
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "/move/50/50", true);
xhttp.send();
XMLHttpRequest () is an object that can be used to send data to the server.
We use GET in the open function (i.e. fetch), although we do not want to
fetch anything. With the request (/mov /50/50) alone, we can achieve what
we want. What the server replies is irrelevant.
We change the move() function in the file code/server/index2.html and then
name the whole thing code/server/index3.html
function move(left, right){
var href = '/move/'+left+'/'+right;
var xhttp = new XMLHttpRequest();
xhttp.open("GET", href, true);
xhttp.send();
}
The moveTo() function in the flask server no longer needs to return the entire
website. This can be found under code/server/web5.py. Only the filename
(index3.html) is changed and the moveTo() function returns "".
from flask import Flask
import sys
import pin
import control
import os
app = Flask(__name__)
print(__name__)
filename = "index3.html"
pin.load("config1.json")
control.load("config1.json")
@app.route("/")
def init():
with open(filename, "r") as file:
return file.read()
@app.route("/move/<left>/<right>")
def moveTo(left,right):
print(left,right)
control.move(int(left),int(right))
return ''
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Automatic start of the program
It would be useful if you did not always have to go through the VNC
connection and the editor. The website and the server should start when you
turn on the Pi.
The Raspberry Pi runs on a Linux operating system. On Windows computers
there are services. The Linux version of a service is called a “daemon”. These
are programs that run constantly in the background. For example, there is a
program that does nothing but wait for a key to be pressed. We now want to
create such a background service or daemon. Daemon comes from “disk and
execution monitor.”
This means that we are now announcing the web5.py program to the
operating system as a daemon. A few changes are necessary for this, which
can be found on web6.py.
The path is no longer correct if we call the program from the operating
system level. Then the files config.json and index3.html can no longer be
found. You can now either specify the entire path when calling up or adjust
the working directory. We will do the latter. These are the lines that need to
be added:
import os
os.chdir("/home/pi/code/server/")
currentDirectory=os.getcwd()
print(currentDirectory)
Actually, os.chdir() would be sufficient. For a better understanding, the
current directory is also read and printed.
And now you can create the control code for the daemon. Then we start the
service.
This is not trivial.
First, we open an operating system editor (nano) with sudo rights via the
terminal. Unfortunately, the normal editor does not work.
sudo nano /lib/systemd/system/web6.service
A comment on sudo. This is a command that gives us all rights to install
programs or change something in the operating system for several minutes.
This should be handled carefully. A command containing sudo always
includes a user query. After pressing “Enter” you need your password again.
The nano editor comes from the Stone Age and is difficult to use for normal
people. The following code must be entered there (can also be found under
/code/service/daemon.txt):
[Unit]
Description=Web server
After=multi-user.target
[Service]
ExecStart=/usr/bin/python3 /home/pi/code/server/web6.py
[Install]
WantedBy=multi-user.target
Save with Ctrl + o
Exit Nano-Editor with Ctrl + x
A brief explanation of this, which is not enough to understand systemd (the
management of the daemons). This template could also be used to start
another service if you change the Python program (here web6.py).
The value after “After” indicates when the service should be started. There
are different run levels. 0 would be “Computer is off”. Then a few more
follow. Run Level 4 corresponds to the state that the user interface has come
up completely. You can also write the identifier multi-user.target for this.
ExecStart specifies what should be executed with which program. So here
web6.py is started with python3.
The Install section determines when the service will be installed.
Now a few more Linux commands follow to activate it. First the computer
must be restarted. This can be done either via Raspberry → Shutdown or
enter in the terminal:
sudo reboot
When the Pi is up again, we need a terminal. There we check the status of the
service. Sudo rights are also required for this.
sudo service web6 status
web6 is the name of our service. If you have saved this under a different
name, you have to use the part in front of “.service“ (from web6.service).
Here you can see that the service is loaded but not active (dead). In front of
web6.service you can see a small point. Only when it is green everything is
okay.
If you do not see a prompt, in most cases you come back with “q” for quit.
So, type q once.
With the service command you can not only query the status, but also start
(start) or end (stop) the service.
IMPORTANT:
The flask server is no longer allowed to run. Only one program can run on
one port (5000) at a time. Otherwise a warning will appear, and the program
will not start. So, if you have not finished the web5.py program, now is the
right time to do so. But normally at this point you should have just restarted
the Pi.
We start web6:
sudo service web6 start
It will take a few seconds and then the service will start. It is recommended to
check the status again. If a program error occurred during execution, you can
see it here.
sudo service web6 status
The green dot is there so everything is fine. In the event of an error, you can
see the Python error messages here.
Even without IDLE and starting the program with F5, the server is now
active.
For this to remain active when you switch the computer off and on, two more
steps are required
sudo systemctl daemon-reload
And
sudo systemctl enable web6.service
If the Pi starts again, the service is still active. A VNC terminal is no longer
required.
H206
Perhaps you have already wondered why the black disc is mounted on the
other side of the wheel. This is a code disk with which you can measure the
speed with an accuracy of 1/20 of a revolution. But how?
With an H206. This is a component that emits light and checks on the
opposite side whether the light is arriving. If there is a change from light to
dark, the component delivers an impulse (on the Pi input). The H206 can be
inserted directly from the top into the existing slot in the chassis. You can
even attach it with a screw on one side. This is where problems arise if you
have a chassis with a table for the Pi, because the structure is in the way.
However, this can be solved with a little manual skill.
To do this, we need to know roughly how many pulses per second come from
the wheel when the level is 100%. From this we will later form a straight line
to the origin. We write a small program that prints this information
(code/h206/wheelspeed.py).
import RPi.GPIO as GPIO
import pin
import control
import time
pin.load("config1.json")
control.load("config1.json")
lastTime=0
count=0
def counter(channel):
global lastTime, count
if(count==0):
lastTime=int(round(time.time()*1000))
count+=1
if(count >= 40):
delta=int(round(time.time()*1000)) - lastTime
print(count/delta, count, delta)
count=0
GPIO.add_event_detect(4, GPIO.BOTH, callback=counter)
control.move(100,0)
time.sleep(5)
pin.cleanup()
A few details about the program. The counter() function needs a parameter
(channel), which is not required for the calculation. Instead of the falling edge
(GPIO.FALLING), I evaluate both edges here to receive twice the amount of
information (GPIO.BOTH). The signals come so quickly that I decided not to
use bouncing.
When the counter (count) is zero, I log the timestamp. Then the function
waits until 40 signals have been received. I read the time again and then
calculate the signals per interval. For level 100 I get something about 0.12,
and the time for one cycle is 0.33 seconds. With a wheel diameter of around
6.5 cm, the car moves around 60 cm per second. I measured that on the table
with a free turning wheel, so without friction. In fact, it is a little less (0.11)
but still way too fast for my taste.
The regulator
For a well-functioning regulator, you would need a lot more signals per
revolution. But we work with what we have. In principle it will run in such a
way that the regulator (a piece of software) tries to achieve the specified
speed. It will drive a little too fast and then brake again immediately
afterwards. It is more of a “stop and go”. If you had more information per
revolution, it would go more smoothly.
Regulators work with the “set” minus “actual value” difference, which is
often called an error.
error = setPoint - velocity
A factor (pFactor) is then applied to this error and returned to the motor. So
that you do not start with a big mistake right away, there is an operating
point. The value that has to be entered in order to achieve the required speed.
But without a regulator that does not work because of the friction.
workingPoint = 100 * setPoint / 0.11
I calculated the value for 100% for my car at around 0.11. The value for the
level is calculated as follows:
left = workingPoint + error * pFactor
If we assume the target value (setPoint) 0.02 (the car normally does not move
at this value) and for pFactor 2000, the result is for the case that the car is still
stationary:
left = 18.2 + (0.02-0.0) * 2000
left = 58.2
When the motor then starts to turn, the error becomes smaller and the level
value for the motor also decreases.
What if a value of 58.2 is not enough to start the motor? There is an integral
part for this. This adds up the error over time. Sounds complicated, but it can
be achieved by a simple addition. In each cycle (0.001 seconds) the error is
multiplied by a factor (iFactor) and then added to a memory (iPart). At some
point the level is high enough to set the wheel in motion.
This results in the following equation for the controller:
iPart = iPart + error * iFactor
pPart = error * pFactor
left = workingPoint + pPart + iPart
This is called a PI-controller. The P stands for the proportional and the I for
the integral part.
Now you have to make sure that the value for left is always between 0 and
100 (when turning backwards the H206 counter would still think that the car
is moving forward). If the signal has not changed compared to the last one,
the actual value (velocity) must be set to zero. The event only detects
movements, but not a standstill. A program that slowly turns the robot in a
circle for a second looks like this (code/h206/controller.py):
import RPi.GPIO as GPIO
import pin
import control
import time
pin.load("config1.json")
control.load("config1.json")
lastTime=0
iPart = 0
iFactor = 30
pFactor = 4000
velocity = 0
lastVelocity = 0
lastTime = 0
setPoint=0.01 # set the desired value here
def controller(setPoint):
global iPart, ifactor, pFactor, velocity, lastVelocity
workingPoint = (setPoint / 0.11) * 100 # max speed = 0.11
if lastVelocity == velocity:
velocity = 0
delta = setPoint - velocity
lastVelocity = velocity
iPart += iFactor * delta
if(iPart<0):
iPart=0
pPart = pFactor * delta
left = workingPoint + pPart + iPart
left=max(min(left,100),0)
return left
def counter(channel):
global lastTime, velocity
if lastTime != 0:
delta = (time.time()-lastTime)*1000
velocity = 1 / delta
lastTime = time.time()
GPIO.add_event_detect(4, GPIO.BOTH, callback=counter)
i=0
while i<1000:
time.sleep(0.001)
left = controller(setPoint) # setpoint
control.move(left,0)
i+=1
pin.cleanup()
If you enjoy tinkering, you can write a better program with a regulation of
both wheels for the robot and then send it to me. I will then make it available
in the code/customer folder for everyone on github.com.
I have optimized a little, and now you can barely see the “stop and go”.
However, you cannot drive as slowly as you like. With values below 0.008
for the setPoint it was not possible to move the car.
That was all about the robot car. Now we turn to the stepper motors.
Control a stepper motor
The constant turning of motors is very convenient when driving. But there are
also tasks that require precise positioning. Stepper motors can be used for
such an application. The proposed stepper motor 28BYJ-48 is briefly
explained below. It has four poles (outside) that are positioned around a rotor
(inside) with eight slots. Each pole can be transformed into a magnetic north
or south pole with the control. We now only consider the north poles (the
south pole is always opposite). As you can see, the poles are slightly offset on
the right and left. If you switch to the right pole after the upper pole, the
motor rotates by 11.25° due to the magnetic effect. This is called a step. With
four steps the rotor has turned 45°. You need 32 steps for the rotor to turn
completely around its axis.
One can imagine that the holding force (torque) of such motors is extremely
low. This is why a gearbox is usually connected. This is also the case with the
present motor. The gear ratio is 64:1. This means that you have to perform 32
x 64 steps (2048) to rotate by 360°. If you only take one step, you have to
look very carefully to notice a change.
Most stepper motors have two settings: very accurate and extremely accurate.
If you want it to be even more precise, you can operate the motor in the so-
called half-step process.
Two poles are then activated simultaneously between the first and second
step.
The control board has six pins that we will use. These are the power supply
(5V and GND) and four pins for the four poles.
The 5 volts and the ground line can be taken from the L298N or directly from
the Pi. Pins 1N1 to 1N4 are used to control the poles. To do this, simply set
one of the four lines to HIGH and all others to LOW in one-step operation.
The only important thing is the order. You can use an array for this.
queue = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]]
These values are then queried in a loop and sent to the four outputs. The only
question is in which direction you want to turn and how far. 360° would be
2048 steps, 180° 1024, ...
A function
The code is compact, but quickly becomes confusing if you install two or
three motors. It is also not practical to write the target value in the code.
A function will now be created that can manage several stepper motors and
rotate absolutely or relatively in both directions. This is again stored in the
configuration file.
stepperID: n,
stepper1N: n
The ID indicates the number of the stepper motor and 1N the pole. The new
configuration file (code/stepper/config2.json) looks like this:
{
"1N1":{
"pin":5,
"io":"out",
"stepperID":0,
"stepper1N":0
},
"1N2":{
"pin":6,
"io":"out",
"stepperID":0,
"stepper1N":1
},
"1N3":{
"pin":13,
"io":"out",
"stepperID":0,
"stepper1N":2
},
"1N4":{
"pin":19,
"io":"out",
"stepperID":0,
"stepper1N":3
}
}
The main program should be so clear that you can interpret it immediately.
This is the code from code/stepper/step2.py:
import pin
import stepper
pin.load("config2.json")
stepper.load("config2.json")
stepper.rel(0,512)
stepper.rel(0,-256)
stepper.abs(0,0)
pin.cleanup()
A quarter turn forward, an eighth turn back and then to the absolute position
0. So back to the starting point.
“rel” stands for relative and “abs” for absolute.
As is so often the case, simple controls that can be seen are often linked to
complex controls in the background. The imported module stepper.py is a
combination of control.py and the last program step1.py. Importing the
configuration file is similar to the motor control. And the stepper.rel()
function contains everything from the step1.py program, whereby the global
variables and arrays are imported.
A new feature is the creation of arrays which again contain arrays. It is easier
to do in other programming languages, but it is always a challenge. I am now
dividing this into, three parts. Import, the rel() function, and the abs()
function. The whole code can be found under code/stepper/ stepper.py.
import json
import pin
import time
stepperCount = 6
stepper = [[] for f in range(stepperCount)]
state = [ 0 for f in range(stepperCount)]
position = [ 0 for f in range(stepperCount)]
def load(filename):
try:
with open(filename, "r") as file:
items = json.loads(file.read())
except:
print("Error loading",filename)
exit(0)
global stepper
global state
global position
for name,item in items.items():
if((item["stepperID"] or item["stepperID"]==0)\
and (item["stepper1N"] or item["stepper1N"]==0)):
ID = item["stepperID"]
n = item["stepper1N"]
try:
stepper[ID][n]=name
except:
stepper[ID]=[ [] for f in range(5)]
stepper[ID][n]=name
state[ID]=0
position[ID]=0
With stepperCount it is determined that we want to control a maximum of 6
stepper motors.
stepper = [[] for f in range (stepperCount)]
This creates an array of length 6 (from 0 to 5). You also often find:
stepper = [list () for f in range (stepperCount)]
It is identical. We have already described loading and converting to an object.
In line
if (item ["stepperID"] and item ["stepper1N"]):
we check whether both values are present: The number of the stepper motor
(if we have several) and the pin assignment. Then we just have to save the
name so that it can be accessed later with pin.Out().
stepper[ID][n] = pin
But the array behind stepper[ID] doesn't even exist in the first run. Therefore,
the try/except block is used.
try:
stepper[ID][n] = pin
except:
stepper[ID] = [[] for f in range(4)]
stepper[ID][n] = pin
state[ID] = 0
position[ID] = 0
If the assignment fails, Python continues in the except branch and creates a
new field for stepper[ID]. At the same time, the state and the current position
are initialized with zero. In a real project, you could also enter a default value
for the start position here.
With large machines, the stepper motor moves in a defined direction until a
limit switch is found. Then the engine stops and sets the counter to zero. The
limit switch determines the start position. Sometimes it is even more
complicated so that the starting point is very precise.
Now comes the rel() function:
def rel(ID,counts):
global stepper
global state
global position
destination = position[ID] + counts
queue = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]
while(position[ID] != destination):
for i in range(0,4):
pinName=stepper[ID][i]
value=queue[state[ID]][i]
pin.Out(pinName,value)
time.sleep(0.002)
if(counts>0):
position[ID] += 1
state[ID] += 1
else:
position[ID] -= 1
state[ID] -= 1
state[ID] %= 4
The global variables have to be read, and then the target position (destination)
is calculated absolutely by adding the relative position (counts) to the last
(position[ID]). The queue indicates the order in the single-step process.
A while loop until the position is reached follows. Here you may see a weak
point in the code. Only one motor can be running at a time. With robots,
however, all axes move simultaneously.
The name for pin.Out() is read from stepper[ID] [i] and the current state of
the pole (queue[state[ID]][i]) is determined.
If all outputs are set, the motor is given 2 milliseconds to reach the new
position.
The increment depends on the sign of the transferred variable counts. So, you
can turn backwards or forwards.
The calculation of the next state (state) is new. This should only take values
from 0 to 3. And when the maximum is exceeded when counting (3), the
value should continue at 0. Like a clock (after 11 comes 0). The modulo
operator is used for this in many programs. This indicates the remainder of a
division. 5 divided by 4 would be 1. The technical symbol is the percent sign
(%).
print(18% 4)
writes a 2 on the screen. Because 18/2 = 4 with remainder 2. This also works
for negative numbers. After 0 in the negative direction comes the 3 again.
print (-1% 4) # returns 3
There is a program code/stepper/modulo.py that displays the numbers for -10
to 10. So that you can see that it works.
And now comes the last function abs().
def abs(ID, dest):
counts = dest - position[ID]
rel(ID,counts)
This is very short, as it only calls the rel() function with the correct
parameters (counts). You should avoid creating code twice.
Summary
With these possibilities you can do a lot. If you connected the wheels with
two stepper motors, you could build a car that drives very slowly and
precisely. Or you could raise and lower something if you know a little about
mechanics. Owning a 3D printer is an advantage here. However, it is not yet
possible to run the motors in parallel. That would be necessary if several axes
(robot arm) should move at the same time. That is what the last section is
about, and it becomes even more difficult.
Simultaneous driving
If several stepper motors are to run at the same time, two conditions must be
met. On the one hand, it would be desirable if the motors arrive at
approximately the same time, and on the other hand, the commands for the
1N pins for all stepper motors must come at the same time or at least
alternate.
The first condition can be influenced separately for each motor via the
waiting time. And the second by bypassing the while loop. If the destination
has not yet been reached, a function is constantly called that checks whether a
motor has to do a step or not. Then you jump back to the main program so
that you can also do other tasks. In principle, this is how threads work, i.e.
parallel processes. You can also watch a video on the computer and write a
text at the same time. Because the individual processes share the memory and
only do something for a few milliseconds.
The threading module is available in python3 for this purpose. And to be
honest: This is anything but trivial. But I will try to explain it in small steps.
Classes
Over three decades ago, someone came up with the idea of introducing
classes. Since they still exist, it was probably a good idea. Before that, the
code was fragmented. With classes you have the possibility to collect things
that belong together in one place. A class is a kind of construction plan if you
want to create a new object (house, car, robot). Therefore, the terms classes
and object-oriented programming (OOP) are inseparable.
When do things belong together?
An example:
A car can drive and has a color (And a few more features and functions but
let us not make it too complicated). We are now packing the driving and the
paint into the blueprint for a new car. If you have such a blueprint, you can
quickly create a new car. And another one. And another one …
Mini-Class
We start with the color.
class Car:
color = "green"
Now we build a new car with the blueprint (class) and read out the color.
car1 = Car():
print(car1.color)
We can also set the color:
car1.color = "red"
See code/robot/class1.py.
Initialize classes
In Python there is a built-in function (__init__) that is called whenever a class
is initialized. This can receive parameters, and in most cases, these are also
saved. The unusual thing is the ”self” variable. This is comparable to the this-
pointer in JavaScript (if you know it). This variable is not transferred to the
initialization function, but it must be specified when defining the function. I
find this confusing.
If you want to transfer the color and the current mileage during initialization,
this function looks like this:
class Car:
def __init__(self, color, mileage):
self.color = color
self.mileage = mileage
See code/robot/class2.py.
A new car can now be created with parameters. But only with two instead of
three. You have to remember that! This way, you can now add color and
mileage to a new car.
car1 = Car("purple", 0)
print(car1.color)
car1.color = "red"
print(car1.color)
The query and the assignment happen exactly as in the previous example.
For the sake of simplicity, you can remember: Everything that was defined
internally with “self.” can be addressed directly from the outside with the
new car (car1.color or car1.mileage).
Init
The class definition (class myThread) receives a parameter
(threading.Thread) and is defined separately within the class.
threading.Thread .__ init __ (self)
Otherwise it looks like a normal class from the previous examples. In this
case, the name, a counter, and a waiting time are transferred to the myThread
class. The new variable of type myThread is initialized with these parameters.
thread1 = myThread("Big-Thread", 50, 0.25)
Run
myThread also has a function run(), which has to be named exactly “run” to
work. From the outside - i.e. from the main program - this function is
activated via thread1.start(), which then searches for run() and starts the
thread. Another function (printCount()) is called in the run() function. This
runs until the counter is zero and, after the delay, prints the text with the
current counter status. The special thing is that despite the while loop, there is
no waiting for the function to finish. You can imagine that this loop is
running on a completely different computer. Like parallel.
End of program
If you run the program (code/robot/class4.py) and only start the two threads
(thread1 and thread2), the main program will not be terminated. That is not
always desired. There are several ways to prevent this from happening.
The simplest is to add join().
thread1.join()
thread2.join()
This means that the main program continues after the threads finish running.
But the main program is blocked.
Another possibility: You run a loop in the main program that takes a little
longer than the time that the longest thread needs. For example, a loop with a
timer.
for i in range (40):
print ("Simple Counter", i)
time.sleep (0.4)
If you start this variant, you can easily see that the main program and the two
threads are all running in parallel, because they all write their texts on the
screen when their waiting time has expired. Simple counter, Big thread, and
Small thread alternate. Please note that the time in the main program must be
long enough.
Another great option is to monitor the threads. In a loop you can check at
regular intervals whether the thread is still running. The isAlive() function is
used for this. We check the thread that lasts longer:
while thread1.isAlive():
time.sleep(0.1)
All variants (#test 1 to #test 3) can be found in class4.py. They are just
commented out.
We only need one thread to control the stepper motors. We will use the third
method (isAlive()) to check the end of the thread. Once again, these complex
structures are outsourced to a module (robotthread).
We have moved a long way away from the subject of “simultaneous driving”.
Now we are going to come back to that using what we just learned.
The module robotthread
Here we will now apply what we tested in class4.py. The main RobotThread
class consists of __init __() and run(). The function run() will now not call a
print command, but it decides whether one or more of a maximum of six
stepper motors must do a step or not. In addition, there are functions to start
the thread and to check the “I'm still alive” signal (sign of live or SoL).
import threading
import time
global thread1
class RobotThread (threading.Thread):
def __init__(self, name, stepper, delta):
threading.Thread.__init__(self)
self.name = name
self.delta = delta
self.stepper = stepper
def run(self):
self.setRobotPosition(self.delta)
def setRobotPosition(self, delta):
pointer=0
while pointer < 100000:
time.sleep(0.002)
pointer += delta
for i in range(6):
if self.stepper.motor[i]:
self.stepper.motor[i].setPosition(pointer)
def start(stepper,time):
delta=(0.002*100000)/time
global thread1
thread1 = RobotThread("Robot-Thread", stepper, delta)
thread1.start()
def SoL():
global thread1
return thread1.isAlive()
Almost everything has already been explained before, now it is only in one
module.
The timeline
What is new is the concept of a virtual guide axis. This is used for
simultaneous driving. You can think of it as a timeline that runs from zero to
100,000 in this case. Let us say we have two stepper motors. One should do
1024 steps and the other only 512. But they should arrive at the same time.
For example, after 10 seconds.
The 10 seconds only apply to the timeline. The counter should have reached
100,000 after exactly 10 seconds, in a linear way. As you can see in
setRobotPosition(), the test function (stepper one step further or stop) is
called every 2 milliseconds. The delta (the step size per cycle) for the
timeline was previously calculated in the start() function. In our example they
are
0.002 * 100000/10 = 20
20 units that the pointer travels every 2 milliseconds until it reaches 100,000.
Then it is checked whether the motor[i] exists. If this is the case, the pointer
is passed to the function motor[i].setPosition(). As you can see right away,
there a decision is made as to whether the motor must run or not.
class Motor:
def __init__(self,position):
self.run = False
self.start= position
self.position = position
self.end = position
self.factor = 0
self.dir = 1
self.outputs = ['','','','']
self.state = 0 # index of the queue
def setOutput(self,index,name):
if index>3 or index<0:
print("stepper1N index out of range")
exit
self.outputs[index] = name
def setDestination(self,destination):
self.start=self.end
self.end += destination
print("Start:",self.start," End: ", self.end) # debug
self.factor = (self.end-self.start) / 100000
if self.factor <0:
self.dir =-1
else:
self.dir = 1
if self.start != self.end:
self.run = True
else:
self.run = False
def setPosition(self,pointer):
if self.run:
destPosition = self.start + round(self.factor * pointer)
if self.position != destPosition:
self.position += self.dir
self.state += self.dir
self.state %= 4
self.step()
def step(self):
queue = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]
for i in range(4):
pinName=self.outputs[i] # index starts with 0
value=queue[self.state][i]
pin.Out(pinName,value) # set the output pin
def load(filename):
try:
with open(filename, "r") as file:
items = json.loads(file.read())
except:
print("Error loading",filename)
exit(0)
global stepper
for name,item in items.items():
if((item["stepperID"] or item["stepperID"]==0)\
and (item["stepper1N"] or item["stepper1N"]==0)):
ID=item["stepperID"]
n = item["stepper1N"]
try:
motor[ID].setOutput(n,name)
except:
motor[ID] = Motor(0) # new class
motor[ID].setOutput(n,name)
This is the longest module so far, and I will explain it now. Instead of many
arrays, there is now only one (motor) that can hold six motors (of the type
Motor).
__init__
The following properties are defined in the Motor class.
self.run = False
self.start= position
self.position = position
self.end = position
self.factor = 0
self.dir = 1
self.inputs = ['','','','']
self.state = 0 # index of the queue
run Indicator whether the engine should run.
start!= end → True, otherwise False
start Starting position
position Actual position
end Target position
factor Multiplier for the pointer to determine the
current position of the motor
dir Forward (1) or backward (-1)
outputs The pin identifiers of the outputs for the
stepper motor
state Index in the queue
Now the functions of the Motor class.
setOutput
Here the transferred identifiers (for the pin module) are placed in the correct
position in the self.outputs[] array. Only values between 0 and 3 are allowed.
setDestination
The start and target positions are set here. The start position is automatically
the previous end position. The end position is relative. The transferred value
is added to the end position. Now you can determine the factor: the difference
divided by 100,000. The direction (dir) is also calculated from the sign of the
factor. And if the start and end positions are different, the run flag is set.
Instead of the linear factors, one can also imagine more complicated
functions. For example, if you want to drive a circle, a sine and cosine
function would appear at this point.
setPosition
It is checked whether the position calculated from pointer and factor matches
the current one. If it does not, two things happen. First the mathematical
position in the class is set (either +1 or -1) and the status of the queue is
changed, and then a step command is sent to the motor.
At this point I want to mention that there is no guarantee that the motor will
actually do a step. The program has no information about the real position of
the motor. If the load is too great or the angular impulses are too fast, the
motor can "lose steps".
step
Here all four outputs are set based on the state.
Martin Stottmeister
Small-time privateers battle death cults and demons at the edge of the
Solar System!
Brian Zaks had enough on his plate: developing his skills as a spacecraft pilot
and combat maneuvers specialist for Elwood’s Privateers–a small-time
Martian defense company–and helping Evvie Evans, fellow privateer and his
ex-lover, manage her nocturnal hallucinations of a raven-headed demon.
When Elwood’s “treasure hunting” hobby uncovered the lost secrets of an
ancient cult, Brian and Evvie were thrust into the center of an interplanetary
conflict between the cult’s present-day branches that threatened to upend
human civilization.
Uriel’s Revenge is a horror-tinged science fiction adventure. Read it today
and escape to the Cliptic!
...expertly paced action sci-fi with intelligence and heart with a slow-drip of
tension that propels the story forward. Add likable characters and some
romantic tension, and you have an excellent, different debut sci-fi novel from
a writer worth your time and attention. Definitely recommended. —
Alexander Hellene, author of A Traitor to Dreams and The Last Ancestor