Intro To Robotics Level B V20314
Intro To Robotics Level B V20314
INTRO TO ROBOTICS
LEVEL B
WORKING WITH SENSORS & INTERMEDIATE PROGRAMMING
Intro to Robotics
Level B: Working with Sensors &
Intermediate Programming
V20314
All contents copyright © 2020 by 42 Development LLC DBA 42 Electronics. All rights
reserved.
When our own children reached an age where they were ready to dive into electronics,
programming, and robotics, we started searching for a great kit to get them started. We were so
disappointed with what we found! The kits available often have components and instruction
booklets for specific projects but do not have any systematic teaching to understand how
components work and interface with each other. Also, there's not much out there for initially
learning to integrate circuits with the Raspberry Pi making it difficult to efficiently build skills to
tackle the many amazing projects available online. Essentially, if you don't already know what
you're doing, the other kits on the market aren't going to do much to improve that situation.
We decided to tackle this project by designing a curriculum written directly to the student which
assumes no background in electronics, programming, or robotics by either the parent or the
child. We start with the most basic concepts and systematically work our way forward to achieve
a thorough understanding.
Level B: Working with Sensors and Intermediate Programming YOU ARE HERE
Our goal is that when a child completes this four-part course they will have:
• A functioning robot and the skills to reconfigure the robot for any number of tasks.
• An extensive set of components to work with.
• The skills to venture out to design projects themselves or complete projects they find
online.
No part of this document may be reproduced or transmitted in any form except with
written permission by the author. This includes email lists or websites.
Families may make as many photocopies of this material as you need for use WITHIN
YOUR OWN FAMILY ONLY.
Schools and Co-ops MAY NOT PHOTOCOPY any portion of the materials. 42
Electronics offers a reprinting license of $10 per student, per course level, per year. If
you would like to repurchase a printing license, please contact 42 Electronics at
[email protected].
Level B is the second in a four-part series making up our Intro to Robotics course. In
this level, your student will continue to learn to build electrical circuits using common
components, learn to write more complex programs in Python, and pull those two skills
together to learn to control the circuits he or she builds with the computer code he or
she writes.
This level lays the ground work for Levels C which move on to adding more complex
audiovisual components such as sound and video, all while continuing to advance their
Python coding skills. In Level D, all these components and coding skills come together
allowing the student to build a mobile robot they will program to gather data, make
decisions, perform tasks, etc.
Our goal in the end, is that your student not only have a fully functional robot but that
they have a full understanding of every component and every line of code. This is
powerful knowledge! The collection of components they will have amassed along with
the knowledge they will have built, will allow them total flexibility to modify the robot,
build other robotic devices they dream up, or take on entirely new projects they may find
online.
Level B: Working with Sensors and Intermediate Programming YOU ARE HERE
OPEN AND GO
This program is written directly to the student and is designed for students (and parents)
with no previous electronics or programming experience. Each lesson will walk you
through step-by-step to teach you to use common electrical circuit components and
common Python commands. Please follow each lesson as written for optimal results.
PREVIOUS EXPERIENCE
Please be sure your student completes all lessons in Level A prior to moving to Level B.
Both the skills and materials used in Level A are necessary for completing Level B.
NEXT LEVELS
Once you have completed Level B, please move onto the next levels:
• Level C: Audiovisual and Advanced Programming
• Level D: Working with Motors and Taking It Mobile
Note to Parents of Middle School Students: The concepts taught in Level B are more
complex than Level A, so parents of younger students should consider significantly
slowing down lesson frequency to perhaps once per week or less. This would allow the
student to complete the lesson more than once if needed and to practice the new skills
prior to moving on to the next lesson. Keep in mind because students are constantly
building on previous skills, there are lots of options for experimenting with new skills in
between lessons to include other skills and components that were taught previously.
Middle school students should aim to complete Levels A and B in 1+ school year (we
strongly encourage moving at your child's individual pace--there's plenty of time to
complete all four levels!)
PARTS KIT
The parts kit for this course shipped within a few days of your order being placed. Once
it arrives, please leave all components in their packaging in the box. This will keep them
safe and clean. You’ll pull them out as needed for each lesson. Each bag is clearly
labeled and each lesson will list which components are needed. A complete list of
included parts and pictures, can be found on page 561.
TIP: If you take good care of the components, they will be reusable to build many
amazing projects long after you complete this course.
REQUIRED EQUIPMENT
In addition to this curriculum and the Level B parts kit purchased with it, your student will
also need the equipment they used in Level A:
• Computer Monitor (a television can also be used provided it has HDMI inputs)
• HDMI cable (likely attached to your computer monitor)
• Keyboard and Mouse
WHAT’S NEXT?
After completing this course, please move on to Level C of this program available at
www.42electronics.com. The next level will continue to build both your electrical and
coding skills as well as add fun new components to your tool box.
YOUNGER STUDENTS
We recommend this course for students in middle school and high school (and for
adults who want to learn how to work with the Raspberry Pi to do amazing projects). It is
possible for a younger student to use this course, but we would recommend the
following:
• Work alongside an adult or older child. Remember, this curriculum is designed
for people who have no previous experience, so your parents shouldn’t be afraid
to join you.
• Move quickly through sections that focus on theoretical background and
mathematical equations. You can always revisit these sections as you get older.
• Keep in mind that Level A is more appropriate for younger students since Levels
B-D teach significantly more complex skills. If your student is not quite ready to
move on to B after A, it’s best to let them continue to experiment with skills
learned in Level A and try again with B in 6-12 months.
• Have the student design a circuit and program with the skills they’ve learned
as a final project.
• Complete multiple levels of this course. Scheduled for 2-3 days a week, four
levels (plus the additional options below) should take approximately one
academic year to complete. Additional levels may be purchased at
www.42electronics.com.
• Have the student read one or more books outlining the historical development
of the fields of electronics or computer science, or a biography of a significant
contributor to the field and write a report. Excellent choices include Nikola
Tesla, Albert Einstein, Ada Lovelace, Charles Babbage, and Alan Turing.
Page 6
OBJECTIVE
Learn administrative file management skills for working in both the Graphical User
Interface (GUI) and the Terminal.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
• Working in the GUI and Terminal Interfaces; Working with the Sudo Command
(Lesson A-9)
The workflows for file related changes in Raspbian are very similar to ones that you
might already be using within the Windows or Mac operating systems. The "desktop"
area referred to below is the screen area that's visible when Raspbian boots up.
CREATING A FOLDER
To create a new folder (also known as a directory) on the desktop, right-click anywhere
on the desktop, highlight Create New, and select Folder from the list. You will be
prompted to name the new folder. Once a name is added and you click OK, the new
folder will be created on the desktop.
You can view the contents of the folder by double clicking on it using the left mouse
button.
You can now click and drag files into your new folder, or even create other folders inside
your folder. A folder inside of another folder is commonly referred to as a sub-folder.
Choosing the right file type for your project is important because it will determine which
program will try to open your file. A text file would be good for a list of project parts
because it will automatically open in the text editor, allowing you to edit your list. You
would not want to create a Python program called program.txt as it would be treated by
the OS as a text file and would open in the text editor. You could view the code however
the text editor has no ability to run Python code.
Owner: This will generally be the user that created the file, this user will have ultimate
permission over what happens in the folder and can include/exclude other users access
based on changing the permission settings.
Group: This setting designates a group of users that can have a special set of
privileges for a file. This group can, for example, be given the right to view a file while
other users outside the group are not allowed to view the file.
View content: Also known as read – this permission designates who is allowed to view
or read the file or folder.
Change content: Also known as modify – this permission designates who is allowed to
change or modify the file or folder content.
Access content: Also known as execute – this permission designates who is allowed
to execute or run the file.
To change permissions, right click on the file and select Properties from the menu. Click
on the Permissions tab and the current permissions will be displayed. In this area you
can change the selections for who can read, write, and execute the file. Click OK to
apply your changes to the permission settings or Cancel to exit without saving.
ROOT USER
One user that overrides all of these permission settings is called root. The root user can
read, modify, and execute any file in the whole system by default. The root user is used
by the Raspbian system to make file system changes behind the scenes. While it is
possible to log in as root, it is not advised. Raspbian, like other types of Linux-based
systems, doesn’t display error messages letting you know that you are about to do
something very bad to your file system.
It's possible that while logged in as root you could delete a folder that is critical for
system operation. Your system would then cease to function. The only way to recover
would be to load a new OS, overwriting all of the previous files on the SD card.
Needless to say, you want to avoid this.
You can also hover over the file, hold down the left mouse button while dragging the file
to the wastebasket, then release the left mouse button while over the wastebasket.
Select Yes when asked for confirmation that you intend for the file to be added to the
wastebasket.
File Manager will open and display the contents of the /home/pi directory
The left pane contains a list of all folders in the file system. This view can also be
referred to as the file tree as subfolders will "branch" out from the main set of folders.
These subfolders can be expanded or collapsed by clicking the + or – symbol next to a
folder. A+ symbol indicates there are subfolders that can be expanded and viewed by
clicking on the + sign. A– sign indicates that subfolders are already expanded, and that
section of the file tree can be collapsed by clicking the – sign.
The right pane contains the contents of any folder that is highlighted in the left pane.
Contents of a folder in the right pane can also be displayed by double-clicking that
folder in the right panel to view its subfolders or files.
The up arrow next to the address bar will take you to the folder above your current
folder. If you're viewing /home/pi/Desktop/files, then clicking the up arrow will take you
to /home/pi/Desktop. Clicking the up arrow again will take you up another level to
/home/pi.
You can access the Terminal program by clicking on in the upper-left menu bar of
your Raspberry Pi.
WORKING DIRECTORY
The working directory is the directory that you're currently working in on the command
line. When first opening Terminal this will be the pi user's home directory or /home/pi.
You can confirm what directory you're currently located in by typing pwd and pressing
the enter key. This command is short for print working directory and it will print your
current location in the file tree.
The ls command is short for the word list. Even though the first character might look
like the number 1 or an upper-case i depending on the font, it’s actually a lower-case L.
CREATING A FILE
To create a file, you can use the Nano command-line text editor:
This will momentarily escalate your user permissions to a root user, create a new file
named robot.py, and open it in Nano. You can make modifications to the file and when
you’re ready to save, hit CTRL-X (control key and x together), press y to save changes,
confirm the filename, and press enter.
You can also use this same command to edit an existing file. If the file already exists
then it will open in Nano. If the file does not exist, then the new file will be created, and it
will open in Nano.
mkdir testing
This will create a new directory called testing within your working directory.
cd Desktop
Capitalization is important, so desktop is not the same as Desktop. If you mistyped the
directory name or tried to go into a directory that doesn't exist, you will receive a "No
such file or directory" error.
If you really know where you're going you can specify multiple folders in one command
You can use the command cd .. to go up one directory. There must be a space
between cd and .. in order for the command to be recognized.
cd ..
Running this command from inside the /home/pi/Desktop folder will take you to the
/home/pi folder. Running it again from there would take you to /home.
There is a shortcut to get back to user pi's home directory or /home/pi. You can use the
command cd ~ to be taken directly to /home/pi from anywhere in the file system.
cd ~
cp lesson.txt lesson2.txt
This will create a copy of lesson.txt, name it lesson2.txt, and drop the file into your
current working directory. This is very handy for making a copy of the file before you
modify the original. For example, let's say you have a program called robot.py and you
want to make some extensive modifications to the code. You could modify your original
file but there won't be a good way to undo the changes if they don't work. You can make
a backup of your original file before modifying robot.py:
cp robot.py robot_original.py
You will now have a perfect copy of your original file. If things go badly with your
changes you can always delete your modified robot.py file, rename robot_original.py
back to its original name, and everything will be back to the way it was before the
changes. We will get into deleting and renaming files in the next few sections.
You can also specify a folder path with the copy command. Let's say that you want to
create the backup file called robot_original.py but you want it to be kept in a subfolder
you created called backups. You would use the following command:
cp robot.py /backups/robot_original.py
This will create a copy of your file and drop it into your backups subfolder.
cp -r Folder_Name New_Folder_Name
The -r argument for the cp command stands for recursive, and it means that the copy
command will be applied to the directory and all of its contents. Without the -r
argument, the cp command will attempt to treat the folder as a regular file, and the
command will generate an error.
It's always good to make backup copies of files before you make big changes, so you
can revert the changes if desired. You might also want to periodically keep backup
copies of important programs you've created on a USB drive for safe keeping. The SD
card storage on the Raspberry Pi is fairly reliable, however unplugging power
mv robot.py new_robot.py
This will "move" the file to the same directory with the new name of new_robot.py. Just
like the copy command, you can specify a path for the source or destination files. If you
wanted to move robot.py from your current folder into a subfolder called finished, you
would use the following command:
mv robot.py /finished/robot.py
The file will be moved, and you will now have to move into the finished directory using
the cd command to view or work on robot.py.
Deleting files can be done with the rm command which is short for remove:
rm robot.py
This will delete the program called robot.py. As stated above, there is no confirmation
for deletion. Once you press enter that file is gone, so be careful.
The command to delete a folder is slightly different. To delete a folder, you must add the
-r argument to the rm command. This lets the rm command know you understand this is
a directory, and its contents will be deleted.
rm -r testing
This command will delete the folder called testing along with any files or subfolders
that it contains.
You can find your current version by using the following command:
cat /etc/os-release
This will report the version of Raspbian you're currently running on the Raspberry Pi.
scrot
This command will create a screenshot of your current desktop view and save it to your
working directory with a name generated using the date/time of the capture. You can
also specify a name for the file as an argument:
scrot file.png
This will take a screenshot and save it to your working directory as file.png.
STEP #1
First, create a new folder on the desktop.
Right click on the desktop and create a new folder named New.
Double click on the new folder and File Manager will open.
Right-click in the new folders content pane and create a new empty file called test.txt.
Double click on the test.txt file and it will be opened in the text area. Add the word test
to the text area and then select File > Save from the menus. Select File > Quit to close
the file.
Right-click on the test file and select Move to Wastebasket from the menu. Click on Yes
when prompted for confirmation.
STEP #6
Close File Manager by clicking on the X icon in the right corner of the top menu bar.
Right-click on the folder and select Move to Wastebasket from the menu. Click on Yes
when prompted for confirmation.
STEP #1
The first step is to open a Terminal window, so you can use the command-line.
Click on the Terminal icon in the upper-left menu bar to open a terminal window.
STEP #2
Confirm the current working directory.
You can do this by typing pwd and pressing enter. This will confirm that you're located in
the /home/pi directory.
STEP #4
Now change directories into the Desktop directory.
Type cd Desktop and press enter. Remember that capitalization is important when
referring to directories. Desktop is not the same as desktop.
Create this folder by typing the command mkdir projects and pressing enter. Since
you are creating this folder inside your Desktop folder, the new folder will also be visible
on your desktop once it is created.
STEP #6
Next, list the contents of your working directory. This can be done by typing ls and
pressing enter. You will now see a directory called projects.
STEP #8
Now, you will create a text file inside your projects folder called test.txt.
Type the command sudo nano test.txt and press enter to create the file and open it
in Nano.
Add here is my text to the text file and press CTRL-X to exit and press y to save
changes. Confirm the filename, and then press enter.
STEP #10
Next, list the contents of your working directory again.
This can be done by typing ls and pressing enter. You should now see your recently
edited test file named test.txt.
Type cd .. to move up one directory and press enter. You can confirm your new
location by the command prompt changing from ~/Desktop/projects to ~/Desktop:
STEP #12
Now that you're back in the Desktop directory, use the command rm -r projects to
remove the projects directory and its contents.
You will be prompted for confirmation prior to deletion of each file since you are running
the rm command as the regular user pi, and not using sudo to run the command as root.
This is a much safer alternative to running every command using sudo.
The folder named projects as well as the text file that it contained have been deleted.
You will also notice that the folder has disappeared from your Desktop.
ANSWER: No, system files are critical to the operation of the Raspbian OS and
you should never delete or rename important system files or folders.
ANSWER: Yes, you can confirm which version you are using with the command
cat /etc/os-release.
In the next lesson, you will learn to work with functions. Functions allow for automation
of tasks and saving space in the code. You will see functions in many projects you work
on going forward.
FUNCTIONS
OBJECTIVE
In this lesson you will learn to work with functions. Functions allow for the automation of
tasks and saving space in the code. You will see functions in many projects you work on
going forward.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
UNDERSTANDING FUNCTIONS
Functions can be used to define a set of commands in a program. You can think of a
function like a list of chores. It would not be very efficient for your parent to tell you to do
every one of your assigned chores. Instead they ask you to do your chores and you
know this means everything on the chore list.
Functions work the same way in a program. You can assign several commands to a
single function and call on that group of commands anytime you need them. This can be
very helpful for keeping your program organized. Since many lines of duplicated code
can be represented by one function, you can call on them as often as you like in your
program, using only one line of code.
def function_name():
command 1
command 2
command 3
In this case, def lets Python know that you are about to define a function,
function_name is the name of our function, and command 1, command 2, and
command 3 will be executed any time this function is called. The function definition must
end in open and close parenthesis and then a colon. Also, the commands contained in
the function must be indented to be considered part of the function.
To call this function in a program you will use function_name(). This is just like using a
variable in your program, in that you must define the function before you try to call it in
your program. If you call the function before defining it, you will have errors. Function
Since programs run from top to bottom, This program will run properly since the
this code will generate an error as the function was defined above, or before,
thing function is called before the the function was called by the program.
program has been told what the
statements make up the thing function.
Technically, both layouts will run without errors. Even on the random side, modules are
imported, and functions are defined, prior to being called by the program. This layout
seems simple enough now, however it becomes very problematic as your program gets
more complex.
Imagine 30 or more lines of code separating each of these groups of code on the
random side. If you receive an error that function thing2 has not been defined, where do
you begin to look? Since the code is not organized in a logical manner, you would have
to search through all the code to find the location where thing 2 is being defined and
figure out the problem with that line of code.
If all imports and function definitions happen at the beginning of your program, you won't
have to look far to find thing2. Keeping all imports and function definitions grouped
together at the beginning of your program ensures they will be available when you call
them later in the program, and they can be easily located if there is an issue.
def setup():
GPIO.setmode(GPIO.BCM)
GPIO.setup(16, GPIO.IN)
GPIO.setup(20, GPIO.OUT)
GPIO.setup(21, GPIO.OUT)
GPIO.output(21, GPIO.OUT)
Later in the program you can call this setup function using setup(). This is a good way
to keep all of your setup related code together for easier modification or troubleshooting
later, if required.
thing()
def thing()
for i in range(0,5):
print('output')
for i in range(0,2):
thing()
The result of this code will be the word output being printed 10 times. The main loop
calls the thing function two times, once when i = 0 initially and again when i = 1. The
main loop stops when i = 2 and the program terminates.
The function is also using a loop to print the word output and it's using i as the loop
counter. The loop continues to print until i = 5 and the loop exits back to the main
program. If the value of i was updated globally to be 5 then the main loop would exit
after only one call to the function, since 5 is greater than 2.
The value of i in the main program and the value of i in the function do not interact due
to global vs. local variables, so 10 copies of output are printed to the console.
NOTE: There are ways to define globally accessible variables from within a
function, however that falls outside the scope of this lesson.
def robot():
print(‘piece of bread’)
print(‘piece of bread’)
Every time you call robot it will make you a peanut butter and jelly sandwich. Someday
you might get tired of peanut butter and jelly, and want some different options, say 20
different options. You could write 20 different functions, each using different names like
robot_pbj or robot_ham, each specifying a different sandwich filling. But there is a
much more efficient way to do this by adding parameters and arguments to the function.
An argument is additional information you can provide when calling a function that can
be used during the execution of that function. The argument is placed inside the
parentheses when calling the function. Continuing with the sandwich example from
above, you could send the robot() function the filling of the sandwich that you would
like by including an argument like robot('salami') or robot('cheese'). This will call
the robot() function while including the argument you supplied. Sending the filling
information from the program when you call the function is referred to as passing an
argument to a function.
The function needs to be configured to accept the argument and this is done with a
parameter. A parameter is added inside the parentheses when a function is defined.
The name of the parameter will determine how the incoming argument will be referred
to while inside the function:
def robot(filling):
The filling variable will now be assigned the value of the inbound argument. Inside
the function you can now use (filling) anywhere that you would like to substitute
your requested ingredient.
print(‘piece of bread’)
print(filling)
print(‘piece of bread’)
The filling parameter has been used to determine what will be included in the middle
of the sandwich.
All that’s left is to send the function your choice of filling, or the argument, when calling
the function. Now you can call the function robot using the argument (turkey), and the
function will substitute the string ‘turkey’ in place of (filling).
robot(‘turkey’)
The output from calling the function robot() using the argument ‘turkey’ will be the
following sandwich being printed to the shell:
piece of bread
turkey
piece of bread
Now the robot can make you any kind of sandwich you want, without having to program
in a function for every possibility of sandwich. You can ask for a robot(‘banana’)
sandwich or a robot(‘ham’) sandwich and the robot will know how to make all of
them.
def print_age(number):
print(number)
print_age(12)
print_age(13)
print_age(14)
print_age will call the function print_age and pass it the value in parenthesis. In this
code, the function will be called three times with a different value each time. The result
will be the values 12, 13, and 14 being printed to the console:
12
13
14
In this example, the function isn’t saving us any lines of code, since you could
accomplish the same output using three print statements. Once the function has
multiple lines of code, or is called very often in a program, it will begin to save you large
amounts of code.
STEP #1
Open Thonny and create a new Python program.
STEP #2
Create the function that will contain the print statement. Name the function abc.
def abc():
STEP #3
Add a print statement that will print Here is the function to the console.
Make sure this is code is indented so it is seen as part of the function. Also be sure to
include the colon after abc().
def abc():
def abc():
STEP #5
Call the function so it can be used to print its own statement. You can do this by using
abc() in the main program:
def abc():
abc()
def abc():
abc()
STEP #7
Run the program. The program will print the following to the console:
This illustrates that the main program is printing one statement, the function is being
called and printing its statement, and then the main program is printing the last
statement.
STEP #1
With Thonny open from the last activity, create a new file.
STEP #2
Create a function that can be accessed by calls from the main program. Create a
function called abc() that contains a print statement that will print Here is the
function.
def abc():
STEP #3
Create a loop that will run 10 times. This can be accomplished by using for i in
range(0,10):. 0 is the starting count and the loop will stop when i = 10.
def abc():
for i in range(0,10):
def abc():
for i in range(0,10):
abc()
STEP #5
Run the program. Here is the function will be printed to the console 10 times.
This illustrates that the main program is calling the function, allowing the function to print
its statement, and returning back to the loop, with i being counted (incremented) each
time the loop runs. When i reaches 10, the loop will exit, and the program will
terminate.
STEP #1
Open Thonny and create a new Python program.
STEP #2
Create a function named hello that will greet each person with their name and their
location. The function will use the parameters name and city that will be passed as
arguments when hello is called. The body of the function needs to include a greeting
that will print "Hello name from city". The print statement will merge the passed
arguments along with the other strings to complete the sentence:
Be sure to include the extra spaces with the words 'Hello ' and ' from ' to properly
space out the sentence. Without them you will get an output like this:
HelloBeckyfromSomewhere
STEP #3
Create a function that will let everyone know what topics will be covered in the class.
Name the function course and use parameters topic1 and topic2 for the arguments
that will be passed when the function is called. The first line of the function will look like
this:
print('Today we will be learning about ' + topic1 + ' and ' + topic2)
STEP #5
Now that the functions have been defined, you can build the main program. All that's left
is to call the functions and pass some arguments. For this example, say you have two
students: Bob from Anytown and Becky from Somewhere. Call the hello function two
times with the information for each student. For the course topics use 'functions' and
'arguments'. Call the course function with those arguments.
print('Today we will be learning about ' + topic1 + ' and ' + topic2)
hello('Bob', 'Anytown')
hello('Becky', 'Somewhere')
course('functions', 'arguments')
Here is the fully assembled program in case you need to troubleshoot anything with
word spacing or arguments:
print('Today we will be learning about ' + topic1 + ' and ' + topic2)
hello('Bob', 'Anytown')
hello('Becky', 'Somewhere')
course('functions', 'arguments')
ANSWER: Defining a function only sets up that function to be used later in the
program. A function must be called from elsewhere within the program before its
block of code will run.
ANSWER: Yes, both function definition names and function call names are case
sensitive. A function defined by def Func1(): must be called using Func1().
Attempting to call this function using func1() with a lowercase f will result
program errors
ANSWER: Module imports and function definitions must happen before they can
be used in a program. Keeping all imports and function definitions near the top of
a program ensures that they will be available throughout the rest of the program
below.
In the next lesson you will continue to make progress in your coding skills. Lesson 3 will
have you work with advanced program layout options as well as learn more advanced
ways to use strings when coding.
OBJECTIVE
In this lesson you will learn to build programs that run until manually interrupted, as well
as several advanced techniques for working with strings in programs.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
One way would be to create a loop that is always true. Something like:
imports_and_function_definitions
end = 0
while end == 0:
print('Still looping')
The variable end will be set equal to zero. The while loop will compare the value of end
to zero, and it will continue to run as long as they are equal. If nothing inside the loop
modifies the value of end, end will continue to equal zero, and the loop will run forever.
This method works, however Python has a better way to make a loop run forever. It's
called a while True: loop. Creating a loop using while True: will make the loop run
forever without any additional variables:
while True:
things_to_do_forever
Since a program like this will loop forever, the only way to exit the program is to use the
Stop button in Thonny or CTRL-C. Holding Control and pressing the c key will send an
interrupt signal to the program, causing it to exit. Some programs that are built like this
will print a message about pressing CTRL-C to exit the program:
imports_and_function_definitions
while True:
things_to_do_forever
This is helpful to let the user know that there is no way to exit the program gracefully,
and that CTRL-C is the only option for ending the program. If exiting the program
gracefully is required for your program, then the next section will be helpful.
The try block will contain the main part of the program. This will be run automatically
once the program has executed any setup code above it in the program. The except
block will not run unless the program encounters an exception. The finally block is not
required, but if used, it should contain any cleanup operations that need to run before
the program exits.
The except block is often configured to catch keyboard interrupts. You can trigger this
behavior when a program is running by pressing CTRL-C on the keyboard whether the
program was launched from the command-line or Thonny. The stop button in Thonny
will only generate an exception for programs that were launched in Thonny.
The finally block is a good place for the GPIO.cleanup() command if your program is
interacting with GPIO pins. This will ensure that all GPIO pins are returned to their
default state before the program exits.
imports_and_function_definitions
try:
main_part_of_program
except:
things_to_do_in_case_of_an_exception
finally:
things_to_do_before_exiting_the_program
The try block will not run repeatedly. Just like the rest of the program, it will only run one
time by default. To get the try block to loop repeatedly, you can combine this format with
a while True: loop:
imports_and_function_definitions
try:
while True:
main_part_of_program_that_will_loop_forever
except:
things_to_do_in_case_of_an_exception
finally:
things_to_do_before_exiting_the_program
Indentation is important for this layout to work properly. Each line of the main part of the
loop must be double-indented or 8 spaces to be considered part of the while True:
loop.
The try block is the only required block for this format. The except and finally
blocks are optional. It may make sense to add an except block, a finally block, or both
based on the needs of your program.
len(test)
This value could be used for all sorts of things like printing an error if a string is not a
certain length or is too long. Those possibilities might look something like this:
if len(test) < 4:
The desired index position can be enclosed in square brackets and used to get a
string's value at that index position. If the string above was named test then:
test[1] will return the value in the string named test, index position 1. The value
returned will be 'h'
Say you have multiple strings in your program that all have a unique digit in position 5.
You might want to only print the unique information at index position 5 and not the rest
of the string:
print(test[5])
This will print the value in index position 5 of the string named test. This value in the
example image of 'thing3' would be '3'.
test[3:5]
This will return index values 3 through 5 of the string named test. Note that the starting
value will be included, but the ending value will not. The result would be the third and
fourth index positions, excluding the fifth, which will return the string 'ng'.
The example above of returning 'ng' isn't very useful, but that all depends on the data. It
might be more useful for something like grabbing a three-digit employee number from a
string, and comparing it to a known value:
id = 'employee042'
if id[8:11] == '042':
print('Access Granted')
This code will grab the index digits 8, 9, and 10 from the string named id and compare it
to the string '042'. If the two are equal, then Access Granted will be printed to the
console. If they are not equal, then nothing will be printed to the console.
id = 'employee0421'
if id[8:] == '042':
print('Access Granted')
The if statement above will check to see if index positions 8 and above are equal to
'042'. In this case '0421' does not equal '042' so Access Granted will not be printed.
If id[8:11] were being used to do this check then only the first three digits of the
employee number would be checked, which would match, and employee0421 would be
given access permission reserved for employee042.
These items are accessed just like the strings in the last section. By using the name of
the list along with the index position of one of its items, you can get the entire value
contained in that position of the list:
things[0] = 'apple'
things[3] = 7
things[2:4] = ['blue', 7]
print(alpha.upper())
print(alpha.lower())
Printing with these case modifications will not modify the original alpha string. You can
save a new uppercase copy of the string by using something like:
upper_case = alpha.upper()
This new variable named upper_case will contain a copy of the sentence that has been
converted to all uppercase letters:
It's much better to refer to people by their actual name rather than calling them "name".
You can personalize this statement during printing by replacing the word name with
someone's actual name:
print(hello.replace('name', 'Bob'))
This print statement will find the string named hello, locate any instances of the word
name, replace those with Bob, and print the new string. Remember that the original
string hello will not actually be modified, only printed with the requested replacements. If
you wanted to save a personalized greeting, you could create a new string:
You now have a personalized greeting saved as a new string that can be used for
whatever you need.
STEP #1
Open Thonny and create a new program.
STEP #2
The time module will need to be imported so you can add a delay between statements
being printed. Import the time module:
import time
STEP #3
Since you need this program to loop forever, create a while True: loop. Don't forget
the colon after True. Also, True must be capitalized to be valid.
import time
while True:
Remember to indent the print statement so it will execute as part of the while True:
loop.
import time
while True:
print('Still looping')
STEP #5
The way the loop is currently configured would print far out too fast to see. Add a one
second time.sleep delay to the loop to slow it down.
Make sure the sleep command is indented so it's executed as part of the while True
loop.
import time
while True:
print('Still looping')
time.sleep(1)
STEP #6
Run the program. You will see Still looping printed to the console once every
second. Since this is an endless loop, this program will run forever. Press the stop
button in Thonny to end the program. The console will print an error message when the
program is stopped manually.
STEP #1
You will be adding the try, except, and finally program blocks to the program from
Activity #1. With the program from Activity #1 open, insert a carriage return above the
while True loop, and add the try: block above the while True loop.
Make sure to add appropriate indentation to the while True loop and its contents. After
the changes it should look like this:
import time
try:
while True:
print('Still looping')
time.sleep(1)
Add an except block below the time.sleep command that will catch keyboard
interrupts.
import time
try:
while True:
print('Still looping')
time.sleep(1)
except KeyboardInterrupt:
This will configure the except block to only execute if a specific type of exception
occurs, a keyboard interrupt exception.
import time
try:
while True:
print('Still looping')
time.sleep(1)
except KeyboardInterrupt:
This will print Stop or CTRL-C was pressed if a keyboard interrupt exception is
encountered.
Add a finally block with a print statement that will let you know when the program is
ending:
import time
try:
while True:
print('Still looping')
time.sleep(1)
except KeyboardInterrupt:
finally:
print('Program is ending')
STEP #5
Run the program. It will print Still looping at one second intervals. Press CTRL-C or
the stop button in Thonny.
The keyboard exception will be triggered, and CTRL-C was pressed will be printed.
After the except block runs the finally block will be triggered and Program is ending
will be printed.
No Python error messages should be printed to the console since the program has
been instructed on what to do in case of a keyboard exception.
STEP #1
In Thonny, create a new program.
Start the program off by adding some user and employee numbers that we can use
throughout the program.
Create three variables named user1 through user3 that contain employee numbers
123, 456, and 789:
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
STEP #2
Create a generic welcome message.
Create a variable called message that contains the string 'Welcome to the company':
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
print(user1[0])
Using the variables e_num1 through e_num3, set each equal to index values 8 through
10 of the corresponding user string value. e_num1 should pull from user1, and so on.
Remember that the ending index value in the range will not be included, so [8:10] will
only include digits 8 and 9. To include 10 your range must be [8:11].
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
print(user1[0])
e_num1 = user1[8:11]
e_num2 = user2[8:11]
e_num3 = user3[8:11]
Create a print statement that will list all three of the employee numbers by printing
'Employee numbers are ' and adding e_num1 through e_num3.
Make sure to include proper punctuation like commas and the word 'and' before the
last value in the list:
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
print(user1[0])
e_num1 = user1[8:11]
e_num2 = user2[8:11]
e_num3 = user3[8:11]
print('Employee numbers are ' + e_num1 + ', ' + e_num2 + ', and ' + e_num3)
For the first message, print 'WELCOME TO THE COMPANY!' in all uppercase using the
.upper() command with the message string as input.
Next, print the message, but replace 'the company' with '42 Electronics':
user1 = 'Employee123'
user2 = 'Employee456'
user3 = 'Employee789'
print(user1[0])
e_num1 = user1[8:11]
e_num2 = user2[8:11]
e_num3 = user3[8:11]
print('Employee numbers are ' + e_num1 + ', ' + e_num2 + ', and ' + e_num3)
print(message.upper())
Welcome to 42 Electronics!
If your output does not look like this, find the code that's generating the problematic
output and make sure it matches the program from Step #6 exactly.
a. x = 0
while x == 0:
keep_doing_this_forever
b. while True:
keep_doing_this_forever
2. Is the finally: block required in a program using the try and except blocks?
3. What command would you use to check if index position 4 of a string named
test is equal to 'z'?
a. x = 0
while x == 0:
keep_doing_this_forever
b. while True:
keep_doing_this_forever
ANSWER: Option B. The while True loop is the better choice as it doesn’t
require the program to look for a specific value to continue to loop.
2. Is the finally: block required in a program using the try and except blocks?
ANSWER: No, only the try block is required. Whether the except and finally
blocks are used just depends on the program and the functionality you require.
3. What command would you use to check if index position 4 of a string named
test is equal to 'z'?
if test[4] == 'z':
This will check index position 4 in the string named test to see if it's equal to z. If
it is, the indented code block below this conditional statement will run.
In the next lesson, you will switch gears a bit and learn to work with pulse width
modification including when and how to use it in programs.
OBJECTIVE
In this lesson, you’ll learn new ways to import modules, as well as how to use Pulse
Width Modulation (PWM) which allows a GPIO pin to behave somewhat like an analog
output.
MATERIALS
**Please note, you received two types of jumper wires in your parts kit: male-to-male
and male-to-female. In Lesson 17 you will use the male-to-female jumper wires. For all
other lessons (including this one) you will use the male-to-male jumper wires.
IMPORT METHODS
Until now, the import methods in your programs have been limited to standard imports
like import time or import RPi.GPIO as GPIO. These will be explained in depth
below as well as a few new import concepts.
import time
This statement will import all functions and information contained in the module named
time. The functions within this module can be called in your program by using the
module_name.function format. Sleep is a function contained within the time module so it
is called by using the time.sleep statement followed by the value in seconds that you
wish to pass to the time.sleep function. For example, time.sleep(1) will execute the
sleep() function within the time module. The value of 1 will be passed to the sleep()
function which will result in a program delay of 1 second.
This statement will import the RPi.GPIO module, just like the first example above, but
the inclusion of as will create an alias, or secondary name, called GPIO for the
RPi.GPIO module. Calls to the RPi.GPIO module can now be simplified to just GPIO.
This is why you will find statements like GPIO.setup or GPIO.output in programs that
import the RPi.GPIO module as GPIO.
This can be handy when a module has a very long name like module_doing_things. If
module_doing_things contained a function called thing1 then calling that function
would require a long statement like module_doing_things.thing1(). Instead you can
give this function an alias on import:
Now that you have created an alias called module1 for the module, you can use
module1.thing1() instead and the program will call the same function, while using a
shorter name.
import time
import random
import os
This is a good way to keep your code organized, but there is a way to combine these
into one line using commas between items:
These are two different ways of importing multiple modules, and Python will treat both
exactly the same. Which format you choose to use is entirely up to you, but you may
encounter either of these formats in programs you find around the internet, so it's good
to understand that they are interchangeable.
Another option to simplify your code is to import specific parts of a module using the
from module import specific_element format. Using the same module and
function names from above, this example would be:
This will make the function named thing1 available as if it was defined as part of your
program. Since it was imported using this method, the module name can be omitted
when the function is called within your program:
thing1()
This can simplify your programs and make it easier to call frequently used functions
without having to include the module name.
Some programs may import everything defined in a library using the asterisk (*):
This will make every function definition available locally in the program without
specifying the module name, however this can cause problems if a function is defined in
your program and also in the module. For example:
If module1.py contains:
def setup():
def setup():
All function definitions in module1.py will be imported and made local using the from
module1 import * statement, including the function called setup. However, your local
program already includes a definition for a function called setup.
When you call the setup function later in the main program using setup(), the definition
from the module will be ignored and your local definition of setup() will be used. This
could cause parts of your program to break if they were relying on setup information that
was intended to be passed in from module1.
If you run into any odd issues when working with programs that import modules, check
that there isn't a conflict of function names between your program and the modules that
you're importing. If there is, you can always rename the functions in your program to
eliminate the conflicts.
While digital signals are very useful, there are some jobs they are not very good at, like
driving speakers. The on/off nature of digital signals do not mix well with the smooth
signal transitions required to drive a speaker. To do this well, you need an analog
signal. You will dive deeper into the differences between analog and digital signals in
Lesson 8.
Unfortunately, the Raspberry Pi does not have the capability to input or output analog
signals using its GPIO pins. The good news is that there are pieces of hardware that
can be paired with the Pi to provide this functionality, and you will explore some of those
in later lessons.
This is similar to Activity #3 in Level A, Lesson 16 where you controlled an LED with a
GPIO pin. The Raspberry Pi can turn GPIO pins on and off so fast that you had to add
delays between turning the LED on or off, to allow the LED time to react to the changes:
These one second delays add enough time for the LED to react to the change in state
on the GPIO pin and turn on or off. Without this delay the GPIO pins can reportedly
switch at speeds up to 70MHz. That means changing states every 0.0000000143
seconds or 14.3 nanoseconds. The LED may be fast enough to begin to turn on, but not
enough electrons will flow through the LED to generate light before the pin is low again.
This results in the LED appearing to stay off despite the GPIO pin going high twice in
this program.
The two states of an LED are fully on and fully off, but if you turn it on and off fast
enough, you can simulate brightness levels in between. This is the basic concept
behind PWM.
Duty cycle is measured in percent and refers to how much time the signal spends high
over a given period of time. The period of time is based on the frequency, measured in
hertz (HZ), which is how many times per second the signal switches between high and
low.
If, over a one second period, a signal transitions only one time from positive to negative,
back to positive, then that signal has a frequency of 1Hz:
If, over a one second period, a signal transitions 10 times from positive to negative, and
back to positive, then that signal has a frequency of 10 Hz:
Now that you have the period of the signal, the PWM duty cycle will determine what
percentage of the time the signal will be high during that period. The duty cycle can be
0% (always low), 100% (always high), or any percentage in between:
The PWM frequency used will be determined by the specific device you are trying to
control. LEDs may work at 30hz, but there might be some noticeable flickering based on
your specific LED, so a higher frequency may be required. The duty cycle can be
adjusted to obtain the desired brightness.
Due to the digital nature of PWM signals, they should not be used to drive standard
speakers. Piezoelectric speakers (piezos for short) are a specially designed type of
speaker with a tiny coil that can move very quickly to produce audio tones. Piezo
speakers are often found on PC motherboards and are used to generate bootup and
system error beeps. Piezos generally sound best with a 50% duty cycle to keep the
high/low timing equal, but the frequency can be adjusted to obtain the desired sound.
Another option is software controlled PWM. Software controlled means that a module or
program is in charge of turning GPIO pins on and off to generate a PWM signal. The
benefit to this is that multiple GPIO pins can be configured as PWM outputs. One
drawback is that software controlled PWM can consume extensive CPU resources, and
depending on the complexity of your program, this additional CPU usage could cause
problems. That being said, lots of programs use software controlled PWM without any
problems, and they run well due to the CPU (Central Processing Unit) resources
available on the Raspberry Pi.
Creates a variable named pwm and set it equal to a command that will let the RPi.GPIO
module know that you intend to use this pin for PWM. The channel is the pin number
and the frequency is the desired frequency of the PWM signal in hertz. Any PWM
commands that refer to this channel will start with pwm. as seen below.
pwm.start(duty_cycle)
Starts the PWM signal on the pin specified in the GPIO.PWM command above.
duty_cycle is the desired percentage of time the signal should be high. This value can
be 0, 100 or anywhere in between.
pwm.ChangeFrequency(new_frequency)
This command can be used to change the frequency of a PWM that is currently running.
new_frequency represents the new value for the PWM frequency in hertz.
pwm.ChangeDutyCycle(duty_cycle)
This command can be used to change the duty cycle of a PWM that is currently running.
duty_cycle represents the new value for the PWM duty cycle.
Stops the PWM output from the pin specified when the variable of pwm was declared.
The pwm variable we selected above can be any variable, as long as it's referenced
when controlling that channel later in the program. If you have multiple PWM pins then
you might want the variables to me more descriptive. If PWM output is required on
GPIO19 and GPIO26 then you might assign descriptive variables like pwm_19 or
pwm_26. The configuration and control commands would then begin with pwm_19 or
pwm_26, depending on the pin you want to control:
Using these commands, you will be able to control multiple devices using PWM signals.
STEP #1
Connect the wedge to breadboard rows 1-20. Connect ground from J3 to ground rail at
N2-3.
LED between E37 and F37. Make sure to insert the cathode (shorter lead) of the LED
into F37 so that it's correctly polarized
STEP #4
You will now test the circuit. Verify all connections are correct per the image in Step #3.
If not already connected, attach the ribbon cable to the wedge and power up the
Raspberry Pi.
GPIO.setmode(GPIO.BCM)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)
STEP #6
The next step will be to test the LED on GPIO26. Add a few lines of code that will push
GPIO26 high, wait one second, and pull it back low. Don't forget to add a cleanup
command at the end of the program.
GPIO.setmode(GPIO.BCM)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)
GPIO.output(26, GPIO.HIGH)
time.sleep(1)
GPIO.output(26, GPIO.LOW)
GPIO.cleanup()
STEP #7
Next, add some code to test the piezo speaker. Since the piezo only makes sound
when it's rapidly transitioning from low to high or from high to low, a simple on/off test
like the LED won't work. You will need to send the piezo a PWM signal to hear any
sound.
Insert the following code between the LED test code and the cleanup command.
Comments have been added to explain each line of PWM code:
GPIO.output(26, GPIO.LOW)
GPIO.cleanup()
GPIO.setmode(GPIO.BCM)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)
GPIO.output(26, GPIO.HIGH)
time.sleep(1)
GPIO.output(26, GPIO.LOW)
GPIO.cleanup()
STEP #1
First, you will need to remove the block of code that is currently controlling the LED on
GPIO26. Remove these lines of code from the program in Activity #1:
GPIO.output(26, GPIO.HIGH)
time.sleep(1)
GPIO.output(26, GPIO.LOW)
STEP #2
Next, you will need to configure pin 26 as a PWM pin. Insert the following code in place
of the old LED code:
GPIO.setup(26, GPIO.OUT)
Run your code to verify that the LED flashes very quickly for two seconds, and then
runs the piezo for .5 seconds.
STEP #3
To demonstrate the effect of dimming the LED, let's modify the values of the LED PWM
signal. Change the frequency from 20Hz to 50Hz so the LED will appear to be on all of
the time. Also change the duty cycle from 10% to 1%. Those settings should result in a
pretty dim LED. Here is the code after the changes:
pwm_26.start(1)
time.sleep(2)
pwm_26.stop()
Run the code to verify the LED is dim as expected. If not, double-check your code for
any problems.
STEP #1
First, simplify the program from Activity #2 by removing the blocks controlling the LED
and piezo. The program should now be only imports, setmode, setups, and cleanup:
GPIO.setmode(GPIO.BCM)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)
GPIO.cleanup()
GPIO.setmode(GPIO.BCM)
GPIO.setup(19, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)
pwm_26.start(0)
GPIO.cleanup()
pwm_26.start(0)
pwm_26.ChangeDutyCycle(b)
time.sleep(0.02)
pwm_26.ChangeDutyCycle(b)
time.sleep(0.02)
GPIO.cleanup()
The first loop will check the value of b. b must fall between 0 and 51 for the loop to run
and each time the loop runs, 1 will be added to b due to the third value at (0, 50, 1).
Each time this loop runs the duty cycle for pwm_26 will be changed to the current value
of b and held for 0.02 seconds by the time.sleep command. Once b reaches 50, the
loop will exit, and the rest of the program will continue.
The second loop is also setup to check the value of b. b must fall between 50 and -1 for
the loop to run and each time the loop runs, 1 will be subtracted from b due to the third
value at (50, -1, -1). Each time this loop runs the duty cycle for pwm_26 will be changed
to the current value of b and held for 0.02 seconds by the time.sleep command. Once
b reaches 0, the loop will exit, and the rest of the program will continue.
2. Write the words piezo and buzzer next to their definition below:
3. What command would be used to import only a function named test from a
module named software?
2. Write the words piezo and buzzer next to their definition below:
ANSWER:
Must be driven by a square wave and sound can be
piezo
adjusted by changing frequency of the signal
Sound will be created as soon as voltage is applied
because it contains internal circuitry for generating buzzer
drive signals
3. What command would be used to import only a function named test from a
module named software?
ANSWER: You would use from software import test to import a function
named test from the module named software.
In the next lesson you will move on to learning to use more complex switches including
writing computer programs to overcome the electrical shortcomings of some switches.
You’ll also learn to use logical operators to control program flow.
OBJECTIVE
In this lesson, you will learn to work with different types of switches and learn to use
programs to overcome the electrical shortcomings of some switches.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
While it's good to understand the basics behind using resistors for pull-up and pull-
down, the Raspberry Pi can help you simplify circuit design by doing the pull-up and
pull-down work for you. The Raspberry Pi contains internal resistors that can be
accessed when setting up a GPIO pin as an input. This will simplify your switch wiring to
a single jumper wire from the GPIO pin to the switch, and another wire from the switch
to 3.3V or ground, depending on whether you want a high or low to trigger the input.
This command will initialize GPIO12 as an input, and configure that input to be internally
pulled up. This input will be an active low, which means this input will remain high until it
is made active by connecting it to ground or low, hence the name "active low". GPIO12
will now only need to be connected to ground to trigger the input, which can be done
very simply using a switch and two wires.
The opposite of active low is active high, which means the input will remain grounded or
low until it is made active by connecting to 3.3V. This can be accomplished by using
GPIO.PUD_DOWN after the pull_up_down parameter:
This command will initialize GPIO20 as an input and configure that input to be internally
pulled down. GPIO20 will now only need to be connected to 3.3V to trigger the input,
which can be done with a switch and two wires.
In a two-position
slide switch the
center pole can
connect to one of the
throws at a time,
based on the position
of the slider:
A slide switch can also be used as a selector between two different inputs. In the
diagram below, a slide switch is connected to two GPIO pins, with the center terminal
attached to ground.
Programming for a slide switch is identical to the programming you've already done for
pushbutton switches. Your program could use these switch inputs to light up different
LEDs, modify the frequency of a piezo buzzer, or anything else you might want to
control.
All mechanical switches will exhibit some level of switch bounce, but it may not be a
problem in their specific application. For example, the light switch in your room has
switch contact bounce but you don't see the light flash on and off multiple times when
you flip the switch on. This is due to the latency in overhead lighting and most other
electronics.
Switch bounce may become a problem depending on how you plan to use the switch
input. If the program just turns on an LED based on switch input, then bounce won't
really be a problem. Extremely rapid on/off LED activity will be masked by the amount of
time it takes the LED to turn on and off, so the LED will appear to turn on and off
smoothly, even though switch bounce is still occurring.
The problem occurs when you have the ability to check a switch very quickly, like the
Raspberry Pi is able to do, thousands of times per second. Imagine you have program
that is supposed to keep track of how often you drink water each day. Each time you
drink a glass of water you press a pushbutton, and the program keeps track of how
many times the switch went from low to high, adding to that count throughout the day.
Pressing the button quickly, only one time, should result in one being added to the
current count. Due to switch bounce, there may be a few extra low to high transitions for
every time the button is pushed, and these will all be logged to the counter.
while True:
if GPIO.input(6) == False:
time.sleep(0.02)
if GPIO.input(6) == False:
water_count = water_count + 1
The first if statement will trigger the very first time GPIO6 goes low. The program will
sleep for 0.05 seconds, and then check GPIO6 again. If GPIO6 is still False, then one
will be added to water_count. If this second check of GPIO6 is True then nothing else
will happen, and the value of water_count will not be changed.
There are a couple of problems with this method. If you somehow managed to press the
button for less than .02 seconds, no water would be recorded, as the second level if
statement would not evaluate as False. Also, since this entire loop will be evaluated
every .02 seconds, you can easily end up with multiple water_count increments for a
single button press, which defeats the purpose of the delay.
You could increase the amount of delay to avoid multiple water_count events being
counted when the button is held down. Increasing this delay will however increase the
amount of time between the first and second checks, which could allow you a greater
chance of pressing the button and releasing without being counted. You could tune this
value to find a balance between quick presses not registering and longer presses
registering more than once, but it may still not give you performance you would like.
while True:
if GPIO.input(6) == False:
water_count = water_count + 1
time.sleep(.1)
The very first time GPIO6 goes low the loop will be triggered. One will be added to
water_count and the program will sleep for .1 seconds before resuming further checks.
This will eliminate the possibility of quick button presses not being counted, and the
delay in this loop can be modified to eliminate single button presses registering more
than one count per press, without affecting the responsiveness of the first button press.
The only problem left with this code is that button presses longer than .1 seconds will
continue to register additional water_count events. This can be fixed by adding a while
loop to hold the program as long as GPIO6 remains low:
while True:
if GPIO.input(6) == False:
water_count = water_count + 1
time.sleep(.1)
pass
time.sleep(0.02)
The current utilization of the CPU is displayed in the top-right corner of the menu bar:
In this image, the CPU utilization is at 25%, meaning 25% of the available processing
power is being used, with 75% still free.
If CPU utilization is too high, no processing power is left over for routine operating
system tasks. By running a loop in your program with no delay, you can easily consume
more system resources than you really need, causing instability in the Raspbian
Operating System.
If you have a loop checking an input in a program, adding a small delay like 0.1 seconds
will free up valuable resources, allowing the OS to complete all the tasks it needs in the
background. Your program will likely run no different, however you will see a big
difference in the CPU utilization number, as well as how much heat is being generated
by your Raspberry Pi.
STEP #1
Make sure your Raspberry Pi is powered off. Using the circuit from Lesson B-4 as a
starting point, add a slide switch connected between these points:
The breadboard circuit is now ready for you to create a program that will use the
switches to control some program functions.
STEP #1
The first step will be to create an empty program that you can use to make a program
that counts button presses. Create a new program in Thonny and save it to your
Desktop as button_counter.
STEP #2
Since this program will interact with GPIO pins and have a delay, you must import the
RPi.GPIO and time modules. Import these modules at the top of your program:
STEP #3
Next, the GPIO pin mode will need to be set to BCM. GPIO13 will also need to be
configured as an active-low input since it's connected to ground. Since this switch does
not have any pull-up resistors attached, this will need to be configured within the
program. Add the following two highlighted lines just below your import code:
GPIO.setmode(GPIO.BCM)
GPIO.setmode(GPIO.BCM)
button_count = 0
STEP #5
Since this program is interacting with GPIO pins, use a try/except loop so the
except: condition can catch keyboard interrupts and run a GPIO.cleanup() before the
program exits. Add the following code to the bottom of your program:
button_count = 0
try:
except KeyboardInterrupt:
GPIO.cleanup()
Your program can now exit smoothly, and without any errors, when the stop button is
pressed in Thonny.
button_count = 0
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
except KeyboardInterrupt:
GPIO.cleanup()
Make sure that the print statement is indented inside the if statement. If not, the value
of button_count will be printed every time the main loop runs, which is very, very
often.
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
time.sleep(0.05)
except KeyboardInterrupt:
Make sure the indentation on this delay is aligned with the if: block above so that it
runs even if the if: block does not get triggered by GPIO13.
GPIO.setmode(GPIO.BCM)
button_count = 0
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #1
You will be adding a delay to the main program to slow down how often the state of
GPIO13 is checked. Use a delay value of 2 seconds and see how the program
responds. Add a time.sleep(2) just below the print statement:
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
time.sleep(2)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
time.sleep(0.2)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #3
Run the program again with the new delay value and press the button a few times. You
will find that a value of 0.2 for the delay is just right for keeping the program responsive
after a button press, as well as eliminating a single press being counted multiple times.
The word "tuning" was used in title of this section because a loop might need slight
timing changes based on how it's causing your program to behave. No delay caused
multiple counts, while 2 full seconds made the program feel unresponsive. For this
specific program, a delay of 0.2 was a good balance between those two to ensure the
most accurate counting performance from the program.
STEP #1
The GPIO pins for the slide switch and LED will need to be configured so they can be
used in this program. GPIO22 will need to be configured as an active-low input (software
pull-up required), and GPIO26 will need to be an output. Using your program from the
last activity as a starting point, make these additions just below the existing GPIO.setup
line:
GPIO.setup(26, GPIO.OUT)
button_count = 0
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
if GPIO.input(22) == False:
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
The time delay from the last lesson can now perform the additional task of keeping the
LED on for 0.2 seconds, which is a good quick flash for an indicator light.
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
if GPIO.input(22) == False:
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
The LED will be turned off every time the main loop runs. If the slide switch is applying
ground to GPIO22 when the main loop runs, then the LED will be turned on, delayed for
0.2 seconds, and then turned off at the end of the main loop.
If the slide switch is not applying a ground to GPIO22 when the main loop runs, the LED
will not turn on, the program will still delay 0.2 seconds, and then the LED will be told to
turn off, even though it's already off. Sending a GPIO.LOW command to a GPIO pin that's
already low will not harm anything. This just ensures that the LED gets turned off at the
end of the main loop, regardless of whether it happens to be on or off.
A copy of the entire program is available on the next page for reference.
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
button_count = 0
try:
while True:
if GPIO.input(13) == False:
button_count = button_count + 1
print(button_count)
if GPIO.input(22) == False:
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
2. Why is switch contact bounce a problem for programs that are counting switch
presses?
ANSWER: Pulling inputs up and down can be done within your program, when
you configure the GPIO pin as an input.
2. Why is switch contact bounce a problem for programs that are counting switch
presses?
ANSWER: If your application for counting switch contact is very accurate like a
computer and can register multiple switch contacts per second, so a single
button press could result in several switch counts, rather than only one being
counted.
ANSWER: An active-low input means that the input must be low to activate the
input. That input will need a ground applied to over come the pull-up and
activate the input in software.
In the next lesson, you will learn to work with logical operators that will allow you to
create programs with more advanced behavior based on multiple GPIO inputs or
variables.
LOGICAL OPERATORS
OBJECTIVE
In this lesson, you will learn to work with logical operators to control program flow as
well as continue to expand your knowledge of using switches.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
Each comparison in the statements above will be evaluated separately. The behavior of
the first statement is very simple because there are only two possibilities:
if x equals '42', this condition will evaluate as True, and the if block will run
if x doesn't equal '42', this condition will evaluate as False, and the if block will not run
The only way to get a True result out of two conditions using and is for both conditions
to evaluate as True. Any other combination of True/False conditions will result in the
and statement evaluating as False, and that block of code being skipped.
The or operator is much more flexible, as it will allow True results if any of the
conditions are True:
The not operator can be used a couple of different ways. It can be used to get the
opposite of a True or False value:
var = False
if not var:
print('Message')
The not operator can be used in another way to modify a variable's True/False value to
be the opposite.
This will take the current True/False value of var and change it to the opposite. If True it
will become False, and if False it will become True. This seems like it might not be very
useful, but you will use this switching in the next section to toggle a variable between
True/False to control the state of an LED.
led = False
old_switch = True
while True:
new_switch = GPIO.input(13)
time.sleep(.2)
GPIO.output(26, led)
old_switch = new_switch
The initial states for two variables are set at the top so they can be used (and reused)
throughout the program. The led variable is set equal to False, so the LED will initially
be off. The old_switch variable is set equal to True so it's initial value will match the
high state of GPIO13. Remember that True is the same as high in Python, so you can
compare True and False to an input to determine if it's high or low.
The while True: loop will run its contents forever, so it's a handy way to check a
switch over and over in a program.
The value of new_switch will be set equal to the value of GPIO13. If GPIO13 is high (the
switch is not pressed) then new_switch will be set to True, and if GPIO13 is low (the
switch is pressed) then new_switch will be set to False.
The if block will only run if new_switch is False and old_switch is True. Any other
pair of conditions like True/True, True/False, or False/False will cause this block to be
If led is currently equal to False, then led = not led will set led to True
If led is currently equal to True, then led = not led will set led to False
The time.sleep(.2) was added to slow down this loop. Without the delay, the contact
bounce within the switch can cause additional button presses to be registered, toggling
the LED unexpectedly. Slowing down the toggling of the led state by adding the delay
fixes that problem.
The GPIO.output statement will switch GPIO26 high or low based on the value of the
led variable:
If led is high, then this statement will run as GPIO.output(26, True) which is
equivalent to GPIO.output(26, GPIO.HIGH).
If led is low, then this statement will run as GPIO.output(26, False) which is
equivalent to GPIO.output(26, GPIO.LOW).
The last line of code will set old_switch equal to the current value of new_switch. This
line is crucial for the program's ability to remember the last state of the switch. This
enables the if block that changes the led state to run only when the button is first
pressed. Holding the pushbutton down will not affect the LED status, as both
new_switch and old_switch will be equal to False during that time. Once the button is
released, new_switch will be True again, and old_switch will be True as well, once
this last line of code sets them equal.
One thing to note about the GPIO.output commands above is that multiple arguments
can be used to turn a GPIO pin on and off. GPIO.HIGH, True, or 1 will all make a GPIO
pin go high:
GPIO.OUTPUT(26, GPIO.HIGH)
GPIO.OUTPUT(26, True)
GPIO.OUTPUT(26, 1)
GPIO.OUTPUT(26, GPIO.LOW)
GPIO.OUTPUT(26, False)
GPIO.OUTPUT(26, 0)
As these lessons progress, you will start to switch to using 0 and 1 to push GPIO pins
high and low. This will be one way that more advanced programs can be slightly
simplified, by using one character instead of 8 for these arguments.
To use the example from the last section, it might be nice to have one switch that turns
the LED on and off, and another switch that ends the program gracefully. Since GPIO13
was being used to toggle the LED, you could use GPIO22 for this theoretical end switch
in the example below.
The main change will be to the while True: loop. That loop will run forever with no
way to exit. Instead you can create a variable called end above the loop and set it to
False:
end = False
Now the while True: statement will be replaced with one that relies on end staying
equal to False:
end = False
This while loop will only continue to run as long as end equals False. Setting end to
True anywhere in the loop will cause the loop to exit, and the rest of the program after
the loop to run. The variable you choose to control the loop does not have to be named
end, but whatever name you choose must be set initially above the loop and referenced
in the while loop condition.
end = False
if GPIO.input(22) == False:
Since GPIO22 is activated, it will evaluate as False and any code inside the if statement
will run. Go ahead and add another line of code to change the value of end to True:
end = False
if GPIO.input(22) == False:
end = True
As long as GPIO22 continues to evaluate as True, this block of code will be skipped,
and the rest of the while loop will run over and over. Adding the GPIO.cleanup code to
the very end of this program will complete the end button functionality.
end = False
if GPIO.input(22) == False:
end = True
GPIO.cleanup()
led = False
old_switch = True
end = False
if GPIO.input(22) == False:
end = True
new_switch = GPIO.input(13)
time.sleep(.2)
GPIO.output(26, led)
old_switch = new_switch
GPIO.cleanup()
The button attached to GPIO13 will switch the LED state, and the button attached to
GPIO22 will cause the while loop to end, the cleanup command to run, before exiting
the program.
The programs above will all use the breadboard circuit that you built in Lesson B-5.
STEP #1
The first step will be to create a new program in Thonny. Create a new program called
logic_pb and save it to the Desktop.
STEP #2
This program will use GPIO pins and the time.sleep command, so those modules will
need to be imported. Import them in the first line of the program:
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
STEP #4
The pushbutton switch and LED are now set up to be used by the program. Next, you
will need a couple of variables to hold the current state of the LED and the last value of
the switch. By using the not logical operator you can toggle the state of an LED
regardless of its current state. Create a variable named led that's equal to False and
old_switch that's equal to False:
GPIO.setup(26, GPIO.OUT)
led = False
old_switch = False
These values will be modified later in the program to control the state of the LED as well
as "remember" the last state of the software toggle switch you are creating for the
pushbutton.
led = False
old_switch = False
try:
except KeyboardInterrupt:
GPIO.cleanup()
STEP #6
You want the program to continually monitor the pushbutton on GPIO13 for presses so
add a while True: loop inside the try: block so it will run over and over:
try:
while True:
except KeyboardInterrupt:
GPIO.cleanup()
try:
while True:
new_switch = GPIO.input(13)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #8
Now that new_switch is storing the current value of GPIO13, you can compare it to the
value of old_switch. If they are both False, then you need to switch the state of led
using the not logical operator, as well as add a time.sleep of 0.2 seconds to make
sure this loop only triggers once each time the pushbutton is pressed:
try:
while True:
new_switch = GPIO.input(13)
time.sleep(0.2)
except KeyboardInterrupt:
try:
while True:
new_switch = GPIO.input(13)
time.sleep(0.2)
GPIO.output(26, led)
old_switch = new_switch
time.sleep(0.05)
except KeyboardInterrupt:
Make sure these additions are indented for the main program loop and not part of the
if block. The if block should only be updated the state of led and inserting a 0.2 second
delay.
GPIO.setmode(GPIO.BCM)
GPIO.setup(13, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(26, GPIO.OUT)
led = False
old_switch = False
try:
while True:
new_switch = GPIO.input(13)
if new_switch == False and old_switch == False:
led = not led
time.sleep(0.2)
GPIO.output(26, led)
old_switch = new_switch
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #1
The first step will be to create a copy of the program from Activity #1 that can be
modified to add these new features. With logic_pb.py open in Thonny, click File and
then Save As. Use a new file name of logic_and_or and save the new file to your
Desktop.
STEP #2
Since this program will make use of the slide switch connected to GPIO22, GPIO22 will
need to be configured as an input with a pull-up. Add the line of code below to the
GPIO.setup lines:
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
try:
while True:
except KeyboardInterrupt:
GPIO.cleanup()
try:
while True:
GPIO.output(26, GPIO.HIGH)
except KeyboardInterrupt:
try:
while True:
GPIO.output(26, GPIO.HIGH)
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
time.sleep(0.2)
except KeyboardInterrupt:
try:
while True:
GPIO.output(26, GPIO.HIGH)
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
time.sleep(0.2)
else:
GPIO.output(26, GPIO.LOW)
except KeyboardInterrupt:
else:
GPIO.output(26, GPIO.LOW)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
GPIO.setmode(GPIO.BCM)
GPIO.setup(13, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(26, GPIO.OUT)
try:
while True:
if GPIO.input(22) == False and GPIO.input(13) == False:
GPIO.output(26, GPIO.HIGH)
elif GPIO.input(22) == False or GPIO.input(13) == False:
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
time.sleep(0.2)
else:
GPIO.output(26, GPIO.LOW)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #1
The first step will be to create a copy of the program from Activity #2 that can be
modified to add these new features. With logic_and_or.py open in Thonny, click File
and then Save As. Use a new file name of logic_end and save the new file to your
Desktop.
STEP #2
You will need to use a variable to hold a value that will determine whether the main loop
continues to run. Let use end as the variable and set it equal to False. Add this line of
code between the GPIO.setup lines and the try: block:
GPIO.setup(26, GPIO.OUT)
end = False
try:
Replace while True: with while end == False: in the main loop:
try:
STEP #4
The last change to the program will be the block of code that gets executed when both
GPIO22 and GPIO13 are False. When this condition is satisfied, you want to set end
equal to True, print Program ending, and run a GPIO.cleanup(). Replace the line of
LED code in the if block with the lines below:
try:
end = True
print('Program ending')
GPIO.cleanup()
You may be wondering why you need another GPIO.cleanup since there is already one
at the end of the program inside the except block. That GPIO.cleanup will only run
when the program is stopped by an exception, which is how you have been stopping the
program, until now.
STEP #5
Run the program. It will behave the same as the program in Activity #2 with the
exception that activating both switches will now end the program instead of turning on
the LED. Here is the entire program for your reference:
GPIO.setmode(GPIO.BCM)
GPIO.setup(13, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(26, GPIO.OUT)
end = False
try:
while end == False:
if GPIO.input(22) == False and GPIO.input(13) == False:
end = True
print('Program ending')
GPIO.cleanup()
elif GPIO.input(22) == False or GPIO.input(13) == False:
GPIO.output(26, GPIO.HIGH)
time.sleep(0.2)
GPIO.output(26, GPIO.LOW)
time.sleep(0.2)
else:
GPIO.output(26, GPIO.LOW)
time.sleep(0.05)
except KeyboardInterrupt:
GPIO.cleanup()
2. Does the variable used to end a program have to be named end, or can you
use variable names like continue or keep_loop_running?
3. Which logical operator can be used to get the opposite of a value that's True or
False?
ANSWER: This table is for an or operator. If it was a table for an and operator,
only the first line with two true conditions would produce a true result. Since
lines two and three containing a single true statement are also true, this is an or
operator table.
2. Does the variable used to end a program have to be named end, or can you
use variable names like continue or keep_loop_running?
ANSWER: You can use any variable you like, as long as the names match in
both the initial assignment, as well as wherever it gets modified later in the
program:
continue = 0
while continue == 0:
if GPIO.input(21) == True:
continue = 1
ANSWER: The not operator can switch a value to its opposite. In the example
of var = not var, the not operator will be applied to the current True/False
value of var, switching it to the opposite of the current value, and saving it as
the new value of var.
In the next lesson, you will learn to work with a 3x4 matrix keypad which provides a
more efficient wiring system for connecting multiple switches.
WORKING WITH A
3X4 MATRIX KEYPAD
OBJECTIVE
In this lesson, you will learn to work with multiple keypad switches wired in a matrix style
configuration. You will also learn to work with global and local variables.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
There are many methods for reducing the number of microcontroller pins required to
interact with a large number of switches, and one very popular method is called matrix
wiring. Matrix wiring means switches are wired together in such a way that each row
and column has its own unique output
wire. Matrix wiring can also be used with
LEDs, which is how they are often wired in
LED cube projects.
At first glance, it may be difficult to visualize how you will determine which specific
switch is pressed, but software is crucial to how this matrix of switches can be read and
used in a program.
If all are low, then no buttons are pressed so move to the next row
Repeat for rows B, C, and D, then start over again with row A
The only thing missing is to assign values to each row/column that match the buttons
that will be pressed. This will allow the software to give you the exact value of the button
that was pressed. This value assignment can be done with lists:
These lists are the reason the columns were numbered 0, 1, and 2, instead of the more
logical 1, 2, 3. The values 0, 1, and 2 correspond directly with the index values in these
rows, therefore column 0 (index 0) for row a will map to the button labeled 1, and so on.
This way the column number can be used to access the exact position in each of these
lists. Back in Lesson B-3 you learned how to return the value from a specific location of
a list, using its index value:
key = row_A[2]
print(key)
This code will respond equally well whether the value of key is set equal to a string or
an integer value. The value of key will be printed in either case.
The problem occurs if you intend to have a more informative print statement that
includes the value of key:
key = row_A[2]
print('The key pressed was ' + key)
This code would work well for any values in the row lists that are stored as strings, but
integer values would cause errors. This is due to the fact that strings and integers are
two different types of objects and cannot be merged into the same print statement.
Storing all values as strings using quotation marks will ensure that either type of print
statement will run properly.
Another type of pushbutton switch called a membrane switch has been developed to
mimic the function of a pushbutton switch, but they are able to be made very thin due to
their construction.
When the switch is pressed, the metal dome or conductive sheet, will come in contact
with the circuit layer, completing a connection between the two sides of the switch. This
will allow current to flow until the switch is no longer pressed, causing the metal dome to
spring back away from the circuit layer, causing the switch to open.
Membrane switches can be found everywhere from the front of your microwave oven to
the keyboard on your computer. Since keyboards usually contain over 100 switches,
manufacturers often opt for membrane-style switches due to lower cost and how much
they simplify the manufacturing process. Some higher end keyboards will separate
pushbutton switches for every key to increase key-press performance for gaming and
other applications.
The connections above will only be correct with the keypad facing up. If the keypad is
flipped over, or face down, then the connections listed above will be reversed.
Each row and column keypad connection must be properly identified in your program. If
the program is checking column 2 on GPIO21, then GPIO21 must be physically
connected to column 2, or the keypad values the program has stored for column 2 will
not match the keys being pressed.
This variable var below is being assigned in the main program and is considered
global. It can interact with objects in both the main program and inside functions:
var = 42
def test():
print(var)
The print statement will have no problem printing the value of the global variable var.
The opposite of this is a local variable. The variable var below is being assigned from
within the function called test.
def test():
var = 42
This variable can be used, printed, and modified in any way you want, as long as it
stays inside this function. Trying to use var in the main program will result in errors that
var has not been defined. This is because the main program is looking for variables with
global scope, and in this case, var has been defined local to the test function.
There is a way to allow local variables to be available at the global level, by using the
global command inside of a function to denote that this variable must be available
globally. Just insert the word global followed by the variable name into your function:
def test():
global var
var = 42
Since space is tight around the wedge, it's best to insert the jumper wires before
inserting the keypad. Insert four jumper wires between the following locations:
Connect the keypad to J18 through J24, making sure that the columns of the keypad
(no black marks) are connected to J18 through J20. This will leave J21 through J24
connected to the row pins.
Please note that to align the three column connections with GPIO16, GPIO20, and
GPIO21, the face of the keypad must be facing away from the wedge. The image above
shows the white label adhesive side of the keypad.
STEP #1
Open a new program in Thonny. This program will use the RPi.GPIO and time modules
and will use BCM pin numbering. Import these modules separated by a comma, and set
the GPIO pin mode to BCM:
STEP #2
This program will use 7 GPIO pins to communicate with the 3x4 matrix. Each of these
would need their own line of code for setup, but there is another way. The inputs (rows)
and outputs (columns) can be grouped together as lists to keep the pins more organized
and save a few lines of code.
Create a list named inputs that contains 12, 25, 24, and 23. Create a list named outputs
that contains 16, 20, and 21.
inputs = [12,25,24,23]
outputs = [16,20,21]
Using the lists from Step #2, set up all input pins as internally pulled down (active high),
and set up all output pins as outputs. Remember to replace the pin numbers in the
setup commands with the appropriate list name.
The setup lines will perform the setup repeatedly for each value present in the lists.
STEP #4
The program will need to know what button value should be assigned to each location in
the matrix. You can do this by creating lists with for each row that contain the button
values.
Using r1, r2, r3, and r4 for variable names, create a list for each row that contains the
buttons in that row, starting with column 0 on the left and moving to column 2 on the
right:
def check():
row = 0
if GPIO.input(12) == True:
row = r1
row = r2
row = r3
row = r4
Use elif statements for the last three to show that these are connected to the first if
statement.
In this case, the four if statements will function exactly the same as using if/elif, but
it's good to get into the habit of connecting related conditions together properly. You
might make a modification to this program later that could result in the statements not
evaluating properly if multiple if statements are used instead of if/elif.
Add an if statement to the bottom of the function that checks to see if row is non-zero. If
so, create a variable named selection that's equal to the row value, using an index
position of [col]. The col variable will be created lower in the program when each
column is made high. Also, inside this if statement, add a print statement that will print
'The key pressed was ' and the value of selection. This must be part of the non-zero
if statement, otherwise the function will try to print selection before a selection has
been made, resulting in errors. Add a time.sleep value of 0.3 at the end of the if block
to keep the same key press from being registered multiple times:
row = r4
if row != 0:
selection = row[col]
time.sleep(0.3)
Make sure that your non-zero check is properly indented so it will run as part of the
check() function. Also, ensure that you use if and not elif for this block. Using elif
will cause this block to be connected to the row selection block. Once a row is selected,
no other elif statements will be evaluated, and the selection / printing block will be
skipped entirely.
Create a while True: loop that contains code to push the following GPIO pins high,
set the column value, run the check() function, and return the GPIO pin low:
while True:
GPIO.output(21,GPIO.HIGH)
col = 0
check()
GPIO.output(21,GPIO.LOW)
GPIO.output(20,GPIO.HIGH)
col = 1
check()
GPIO.output(20,GPIO.LOW)
GPIO.output(16,GPIO.HIGH)
col = 2
check()
GPIO.output(16,GPIO.LOW)
Run the program and press some of the keypad buttons. The buttons should be printed
in the console. Due to the use of the while True: loop, there is no way to gracefully
exit the program, so use the stop button in Thonny when you want to end the program.
If the program is not working as expected, double check the keypad connection and
each step in the program. Do not proceed to Activity #3 until the program is working
properly.
The complete program is listed on the next page for your reference.
GPIO.setmode(GPIO.BCM)
inputs = [12,25,24,23]
outputs = [16,20,21]
GPIO.setup(inputs,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(outputs,GPIO.OUT)
def check():
row = 0
if GPIO.input(12) == True:
row = r1
elif GPIO.input(25) == True:
row = r2
elif GPIO.input(24) == True:
row = r3
elif GPIO.input(23) == True:
row = r4
if row != 0:
selection = row[col]
print('The key pressed was ' + selection)
time.sleep(0.3)
while True:
GPIO.output(21,GPIO.HIGH)
col = 0
check()
GPIO.output(21,GPIO.LOW)
GPIO.output(20,GPIO.HIGH)
col = 1
check()
GPIO.output(20,GPIO.LOW)
GPIO.output(16,GPIO.HIGH)
col = 2
check()
GPIO.output(16,GPIO.LOW)
This will be accomplished by using the octothorpe (pound sign) to modify the value of a
variable named end. The modified end value will cause the main while loop to stop so a
GPIO.cleanup can be executed prior to exiting the program.
Using the program from Activity #2 as a starting point, add a variable named end and
set it equal to 0. Add this line just above the while loop. Also change the while loop
from while True: to be dependent on end being equal to 0:
if row != 0:
selection = row[col]
end = 0
while end == 0:
GPIO.output(21,GPIO.HIGH)
col = 0
check()
GPIO.output(21,GPIO.LOW)
This will allow the while loop to run only when the value of end is 0. If end is changed to
anything but 0, the while loop will no longer run, and any code below it in the program
will run.
Add an if statement to the row non-zero block that checks to see if selection is equal to
'#'. If so, assign the variable end to be global, and set end equal to 1:
if row != 0:
selection = row[col]
time.sleep(0.3)
if selection == '#':
global end
end = 1
The global command will allow the variable named end to be accessed by the entire
program. The new end value of 1 will cause the while loop to stop running and run any
code after it in the program.
STEP #3
The last addition will be a GPIO.cleanup line at the very end of the program that will run
when the while end == 0: loop is no longer True. Add GPIO.cleanup to the end of the
program:
GPIO.cleanup()
The program should run just like it did in Activity #2, but now the program will exit if # is
pressed. If the program is not working as expected, double check each step in this
activity.
The complete program is listed on the next page for your reference.
GPIO.setmode(GPIO.BCM)
inputs = [12,25,24,23]
outputs = [16,20,21]
GPIO.setup(inputs,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(outputs,GPIO.OUT)
def check():
row = 0
if GPIO.input(12) == True:
row = r1
elif GPIO.input(25) == True:
row = r2
elif GPIO.input(24) == True:
row = r3
elif GPIO.input(23) == True:
row = r4
if row != 0:
selection = row[col]
print('The key pressed was ' + selection)
time.sleep(0.3)
if selection == '#':
global end
end = 1
end = 0
while end == 0:
GPIO.output(21,GPIO.HIGH)
col = 0
check()
GPIO.output(21,GPIO.LOW)
GPIO.output(20,GPIO.HIGH)
col = 1
check()
GPIO.cleanup()
2. Which option uses more GPIO pins, separate switches or matrix style switches
with shared column and row connections?
3. Where are some places that membrane type switches might be seen in your
daily life?
ANSWER: No, variables assigned within a function are only accessible within
the function. For a variable to be used outside the function you must add the
global command to the variable inside the function.
2. Which option uses more GPIO pins, separate switches or matrix style switches
with shared column and row connections?
ANSWER: Separate switches use more GPIO pins. You can use less pins by
using a matrix style switch with shared column and row connections.
3. Where are some places that membrane type switches might be seen in your
daily life?
In the next lesson, you will learn to use GitHub to access library files and advanced
techniques for working with lists. You will also learn about the different versions of
Python and the issues that can arise from attempting to run a program in the wrong
version.
GITHUB AND
PYTHON 2 VS PYTHON 3
OBJECTIVE
In this lesson you will learn to use GitHub to access library files. You will also learn
about the different versions of Python and factors you will need to be aware of if you
attempt a project written in a different version than you are currently working with.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
LIBRARY FILES
Different companies sell many devices that interface with the Raspberry Pi, such as
displays, cameras, and all sorts of sensors. Due to the complexity of these devices, it's
not always possible to write a quick Python program that will talk to the device. Some of
these devices require complex methods of communication, but the good news is that
you don't have to worry about that part of the program.
Devices like these have software called libraries (or modules) that have been written to
act as an interpreter for your program. For example, if you want to print this page out
using a printer, you would just click a single print button and a page would come out of
the printer. Behind the scenes, that button press triggered some very complex
communication between your computer and the printer to print that page.
It's much the same with library files. Your program might issue a command like
TEMP_SENSOR.check() which could return a value in Fahrenheit. What you don't see is
the file called TEMP_SENSOR.py that might be hundreds of lines of code built
specifically to talk to the temperature sensor. One line of your program asks
TEMP_SENSOR.py for the temperature and it does all the hard work for you, so your
program can use the temperature value for whatever you like.
These library files need to be available to everyone that might purchase a device, so
most of these library files will be available on a website called GitHub. Some hardware
vendors may host the library files themselves, with links in the product description, but
most libraries are hosted on GitHub.
GitHub is organized into many folders, known as repositories, which you may see called
repos for short. Each top-level repository is named after the user or organization that
created that repository. Most hardware vendors that sell Raspberry Pi components will
have a repository that contains the code to run their products. GitHub organization
pages will be located at https://github.com/ followed by the name of the company. Here
are a few notable GitHub pages:
https://github.com/42electronics
https://github.com/adafruit
https://github.com/sparkfun
These top-level organization folders will contain folders for any hardware that the
organization has made available. The search bar at the top of the page will allow you to
search for the specific name of different hardware devices, while limiting results to only
that organization, or allow results from all of GitHub.
The first step to cloning a library is to connect your Raspberry Pi to the internet. After
that, make sure it's fully up to date by opening a Terminal window and running:
The Pi maintains a list of all installed software packages and their version numbers. The
apt-get update command causes the Pi to go check the internet for the latest version
numbers of all available packages. This lets the Pi know if any of its installed packages
are not the latest version, and if any new packages are to be installed, which version is
the latest available. No packages are modified by the update command, only the list of
currently available package versions.
The library that you need to clone might only be part of the software that you need to
communicate with the new device. Additional software packages could be required, and
if so, these will be mentioned in the tutorial for your specific device.
The next step will be to install any of these additional packages. In the example below
these packages are python-imaging and python-smbus:
The install command will let you know if any of the requested packages are already
installed on your Pi, and whether or not they are the newest version. If a package is not
found on your Pi, you will be told how much space the new package will consume after
installation and asked for confirmation to proceed with the installation.
The next step will be to install the git package. This package is used by the Pi to
communicate with GitHub. It could have been installed with the previous install
command, but some tutorials will call this out as an extra step:
None of these commands so far have been dependent on your location in the directory
structure of the Pi. The update and install commands will function the same from any
directory. The upcoming clone command works differently. It will create a copy of the
remote repository in your current working directory.
Opening Terminal will automatically drop you into /home/pi as your default directory. If
you have moved into another directory during the course of your work, you can get back
to /home/pi using a couple of different commands:
cd /home/pi
or
cd ~
Either of these commands will drop you into /home/pi, regardless of your current
location.
Now that you're in the home directory, you can clone the remote repository. The next
command will use the git package to clone the repository that you need:
The name of the organization or user would replace organization_name and the
actual name of the repository would replace name_of_repository.git. If the
organization is using additional folders to organize their repositories there could be
additional /folder_name/ entries in the path as well.
Also, keep in mind that GitHub is used for many platforms other than the Raspberry Pi
and Python. Just because you find a folder for your specific hardware device does not
mean it's compatible with Raspberry Pi and Python. There are many other hardware
platforms and languages available on GitHub, so prior to installing anything, always
confirm the library is for the correct platform and language that you plan to use.
The best way to ensure library compatibility is to obtain GitHub library links directly from
setup tutorials provided by the hardware vendor. They will often have links on the
website's product page that contain walkthroughs on how to get that device running on
different platforms.
While GitHub is currently the largest host for repositories, you may also find files hosted
on GitLab, BitBucket, SourceForge, or other legitimate repository hosting sites.
The vendor's tutorial will inform you if this additional installation step is required. These
installations are usually simple and only involve moving into the cloned directory and
executing a file named setup.py that can be found inside:
cd cloned_directory
This will fully install the library and any of its files can now be accessed by your
programs.
While already inside the new directory you might want to check to see if it contains any
example programs. These can often be found in a subfolder named examples. These
can be a good way to check out what the modules can do with your new hardware.
Most tutorials will have you run these example programs as a way to quickly get familiar
with the modules and their capabilities.
In online tutorials you may see mention of pip or easy_install. These are alternate
installation methods that some packages support. If one of these installation methods is
required, then the tutorial will walk you through the exact commands to be used to
complete the installation.
Since GitHub is organized in a logical file format, navigation is about the same as
navigating folders on your PC or the Raspberry Pi. The top-level organization or user
folder will be located at:
https://github.com/organization_name
If you aren't sure of the specific URL of the organization you're looking for on GitHub,
you can always use the search available at the top of https://github.com. This will
search GitHub for that term and return all results for any users (organizations),
repositories, code, and much more.
The list of result categories can be clicked on to narrow the displayed results. The
search for "42 Electronics" yields a large amount of results but clicking on the "Users"
result will refine the displayed results to 42 Electronics, as well as a few individual users
whose user profiles fit the search criteria.
Clicking on a folder name in this area will take you inside that folder. Clicking on any
supported type of image, text, or code file will open a viewer within the browser,
allowing you to quickly review these types of files.
Code files are the most interesting thing you will view in this area. The code will open in
a window that will display the entire program with line numbers, colorized markup to aid
in clarity, and the ability to highlight and copy code directly from within this window.
The command line (Terminal) has both versions of Python available using different
commands. Programs can be run in Python 3 by using the command python3 before
the name of the program you want to run. The command line name for Python 2 is
slightly less logical as the command python is used before the name of the program
you want to run. This happened because Python 2 was, at one point, the one and only
Python, so this command made perfect sense.
When Python 3 was released, there were already a lot of programs running version 2
that were using the python command. Python 3 needed a new command to
differentiate it from 2, so python3 was used. To summarize, when on the command
line:
This command information can be applied to the library install from the previous section.
The command sudo python setup.py install was used to install the library. The
use of python in this command means this library was only installed in the Python 2
environment. Attempting to run programs in Python 3 that require files from this library
will result in errors that the library is not installed.
Some libraries are written to support installation in either Python 2, Python 3 or both. If
the library is compatible it can be installed in the Python 3 environment by using the
python3 command:
This will install the library in the Python 3 environment, making it available for any
Python 3 programs that need to access files contained in the library.
Thonny is a great tool for editing programs, but sometimes the best solution is a mix of
Terminal and Thonny. Having both windows open at once, each with their own purpose.
Terminal can be used to install libraries, and quickly execute Python 2 programs using
the python command. Even with Thonny running its Python 3 environment, you can still
open, modify and save Python 2 programs without any problems. As long as you don't
execute the Python 2 program in Thonny, everything will run well. Here is what this
workflow might look like:
Leaving these two windows open will allow you the freedom to quickly execute Python 2
programs using Terminal while using the advanced editing features available in Thonny.
Don’t worry if this sounds a bit confusing. You’ll be practicing running Thonny and
Terminal side by side in the activities for this lesson.
STEP #1
The first step is to ensure your Pi knows the latest version numbers for packages that
are currently available.
Ensure your Pi is connected to the internet and open a Terminal window. Once the
Terminal window is open, type the command sudo apt-get update and press enter:
Your Pi will check the internet and update its internal list with the latest available
package versions.
If any of these packages need to be installed or updated, you will be presented with the
amount of storage space required to make the changes.
Type y followed by enter to confirm the changes. Wait until the installation is fully
complete and you are presented with a command prompt before proceeding to the next
step.
cd ~
NOTE: This step is not needed in this specific instance since you have been
located the home directory since Step #1. This step is good to remember,
however if you happened to start this process in a previous Terminal session that
may not have been located in /home/pi. Performing this step in either instance
will always ensure that your cloned folders end up located in /home/pi.
NOTE: If you didn't enter the ~ (tilde) correctly, you might have accidently
entered the ` (backtick) that shares the same key. If this happened then you are
likely looking at the interactive > prompt, meaning the command line is waiting for
more information. CTRL-C (holding the control key while typing c) will end the
interactive prompt and return you to the main command prompt, so you can try
again.
STEP #4
It's now time to clone the remote repository for the MCP3008. Adafruit maintains a
repository for this IC in their main GitHub repository.
Type the following command to clone the remote MCP3008 repository into your local
/home/pi directory:
If you receive any errors, verify every aspect of the command above is correct.
Capitalization is critical along with every _ and / mark. If the command is correct you will
see status activity related to the download.
Wait until this activity is complete before proceeding to the next step.
cd Adafruit_Python_MCP3008
Again, accuracy is critical. If you receive any messages about a directory not being
found, verify every character in your command is correct. You must be located in this
new directory to execute the setup command in the next step.
STEP #6
This specific library requires the additional install step talked about earlier in this lesson.
Now that you're located inside the new folder containing the library files, you can run the
setup file by running the following command:
This will compile the library elements based on your installed packages and make the
library elements available in the Python 2 environment. This library is also compatible
with Python 3.
Install the library into the Python 3 environment by running the following command:
Both Python 2 and Python 3 will now have access to the files contained in this library.
Leave the Terminal window open as you will use it in the next activity.
STEP #1
For this activity you will use the Terminal window from Activity #1 as a starting point. In
this window you will already be located in the /home/pi/Adafruit_Python_MCP3008
directory.
pwd
The console will report that you are located in the /home/pi/Adafruit_Python_MCP3008
directory. Type ls to list the contents of the current directory:
ls
All files and folders located in the current directory will be listed:
Terminal will color code directories in blue and Python, setup, and text files in gray.
Some other file types may show up in different colors, but none of those file types are
present in this directory.
From the directory listing you can see various setup files, a readme file, and a few
directories.
The Nano command line editor can be used to open the README.md file. Open the file
by running the following command:
This will open the README file in the Nano text editor. Scroll through the file using the
up and down arrows. The installation instructions will look familiar as they are the ones
you just ran to get the library cloned and installed.
Press CTRL-x to exit the Nano editor and you will be brought back to the command line.
Change directories into the examples directory by running the following command:
cd examples
Once inside the examples directory, list the directory contents using the ls command:
ls
The file will open in Nano, so you can view the code. Some of the most important lines
are 13 through 16, where the GPIO pins are configured. These might have to be edited
based on how the IC gets wired to the Raspberry Pi. If, for example, another part of
your project is already using pins 18, 23, 24, or 25, the values on lines 13 through 16 on
this program would need to be modified according to use the new GPIO pin locations.
Don’t be concerned with that for the moment though. You will learn more about this in
the lesson when you wire up the MCP3008 to the Pi and use simpletest.py as a
template for other programs.
Press CTRL-x to exit the Nano editor and close the Terminal window.
STEP #1
The first step to reviewing code on GitHub is to open a web browser window. Ensure
your Pi is connected to the internet and open a web browser window. Using the
browser, go to https://github.com.
STEP #2
Now that you are on GitHub, you can look for a repository.
Type 42 Electronics into the search bar and press enter. Click on Users, then click
on the user named 42 Electronics.
Click on the Repositories tab, then click on the repository named level_a:
STEP #4
Inside level_a you will be presented with the list of folders contained in that repository.
Click on the folder named lesson 17 and you will see the contents of that folder. Click on
the file named Activity3_Four_LEDs_10_Times.py and it will open in your browser:
Scroll down below the license information to view the Python code. You can now see
the lines of code from Lesson 17 of Level A that made four GPIO pins blink 10 times:
This same process can be used to the view the code inside any repository that's publicly
available on GitHub.
Close out the web browser window when you are finished.
A. Python 2 only
B. Python 3 only
C. Both Python 2 and Python 3
D. Neither Python 2 or Python 3
2. Will installing one library cover all hardware that might be connected to a
Raspberry Pi, or will you need to install a library for each piece of hardware you
intend to connect to the Pi?
3. On github.com, can you view the contents of a Python file in your web browser
or do you have to clone the repository locally before being able to view the
contents?
A. Python 2 only
B. Python 3 only
C. Both Python 2 and Python 3
D. Neither Python 2 or Python 3
2. Will installing one library cover all hardware that might be connected to a
Raspberry Pi, or will you need to install a library for each piece of hardware you
intend to connect to the Pi?
ANSWER: Each piece of hardware may have its own library that will need to be
downloaded and installed.
3. On github.com, can you view the contents of a Python file in your web browser or
do you have to clone the repository locally before being able to view the
contents?
In the next lesson, you will learn about analog signal processing with the Raspberry Pi.
OBJECTIVE
In this lesson, you will learn how to interface the Raspberry Pi with analog signals, work
with integrated circuits, and add a few additional electronics concepts to your
knowledge base.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
VOLTAGE DIVIDERS
Back in Lesson A-3 you learned about how voltage is dropped across different
components. Voltage drop is the amount of voltage that a component in the circuit
consumes. The total of the voltage dropped across components will be equal to the
supply voltage. If 3.3V is the supply voltage, then 3.3V will be dropped across all
components in the circuit.
The single 1K ohm resistor makes up the entire resistance between supply and ground,
so it's dropping 100% of 3.3V.
The total resistance of two 1K ohm resistance in series is 2K ohms. Each resistor
makes up one half of the total resistance, so they each drop half of 3.3V, or 1.65V.
The total resistance of three 1K ohm resistors in series is 3K ohms. Each resistor
makes up one third of the total resistance, so they each drop one third of 3.3V, or 1.1V.
The voltage available at the center point can vary drastically by swapping the positions
of the resistors above.
In Activity #3, you will use a voltage divider to create new voltage levels. These voltage
levels will not be 3.3V or ground so an analog input will need to be added to the
Raspberry Pi in order to measure these signal levels.
This capability works well for most projects, but some will require the ability to monitor
an analog signal. Some sensors do not have digital capability and will only output an
analog signal of varying voltage level based on the conditions that sensor is
experiencing, whether it's varying light levels, varying input levels on a microphone, or
whatever else your sensor is meant to sense.
While digital signals are limited to only high and low, an analog signal can travel
anywhere between its supply voltage and ground. This means that an analog device
running on a supply voltage of 3.3V can output 3.1V, 2.3V, .5V, or any other voltage
level between 3.3V and ground. Rapid fluctuations in these analog signal levels can
even be used to create sound waves by driving a speaker.
One way to visualize digital signals versus analog is to think about a driving video
game. Most game controllers have analog inputs that allow for very fine steering and
throttle/brake application. Imagine if all of these switches were digital, only on or off.
Staying on the track would be a lot more difficult if your only options were hard left turn,
go straight, or hard right turn. The same would go for only having throttle and brake
options of 100% acceleration, coast, or 100% braking. Instead of these on/off type
inputs we have nice, smooth control sticks and triggers that allow for varying levels of
steering or throttle/brake based on your input.
Analog signals have many uses, but you may see them used to represent varying
sensor outputs. For example, a light sensor may represent the light value it's sensing by
varying an analog output. 0V might represent complete darkness and 3.3V might
represent full sunlight. Voltage levels in between will present the amount of light present
at the sensor, so a voltage level of .5V would represent a fairly dark light level, and 2.5V
would represent that a fairly bright light is present. Your program could be coded to
respond to these varying voltage levels and complete different tasks based on the
sensor voltage level.
To get analog signals converted into something the Raspberry Pi understands, you
must use another piece of hardware called an Analog to Digital Converter, also known
as an A/D Converter. An A/D Converter is an integrated circuit that will take in analog
signals and convert them into a digital format the Raspberry Pi can process.
ICs can contain large amounts of circuitry and some, like A/D converters, do not require
any additional components to operate. They only need to be supplied with power and
ground and they are ready to start converting analog signals into digital. All the complex
circuitry to complete this task is already inside the IC.
ICs will need to be connected to the rest of the circuit to perform the intended task. For
an A/D converter, the analog inputs would be connected to your analog sensor input,
and the digital output would connect to your Raspberry Pi.
IC DATASHEETS
The IC's datasheet will contain operating specifications like temperature and voltage
requirements, as well as the signal pinout for connecting the IC into a circuit.
The function of each pin will vary based on different models of ICs, so it's important to
make sure you're using a datasheet that matches your specific model. If the datasheet
is not linked in the project tutorial you're using, you can always find it by searching the
internet for the IC's model number followed by the word "datasheet". For example, "555
datasheet" or "MCP3008 datasheet" will show results that include PDF versions of the
datasheets for those ICs.
There may be additional information printed on top of the chip like the manufacturer's
logo, along with other information the manufacturer might use to identify the production
run. If you're searching for a datasheet and not finding any results, try the other
numbers from the top of the IC. As shown in the previous image, different
manufacturers print IC information differently, but the model number will be there
somewhere.
The most important aspect of IC pin numbering is locating pin one. Pin one is used as
the starting point for numbering pins on the IC. Pin one will be to the left of the notch at
one end of the IC. Some ICs also have a dot to indicate the location of pin one.
Once pin one is located, the pins are numbered in a somewhat counter-clockwise
direction. With pin one at the top, the pins will increase in value down the left side of the
IC, resuming at the bottom of the chip, and increasing up the right side of the chip:
Searching for the IC's model number followed by the word "pinout" is a good way to
quickly find images of an IC's pinout. The images will have very short names for the pin
descriptions, so you will likely need to refer to the site supplying the image or the IC's
datasheet to fully understand what the shortened names mean. For example, here is
the pinout for an MCP3008 A/D converter:
When two materials are in contact with each other, electrons can flow from one material
into another. Separating the two materials can leave excess positive or negative charge
in the materials that is looking for a place to equalize by discharging. When this
discharge happens, it's known as static electricity.
You have probably been shocked by touching a door handle at some point in your life.
This is because you may have built up excess charge by walking around on carpet. This
excess charge was looking for somewhere to go and the metal of a door handle is a
good conductor. The intensity of the spark generated can vary based on how much
contact you had with the carpet and the humidity of the room, but often the spark can be
felt, or sometimes even seen.
To see or feel a spark from static electricity means that thousands of volts of charge
were present on your finger. This charge isn’t dangerous to humans as all of that
voltage is only accompanied by a minute amount of current.
While it takes thousands of volts to generate a spark that we can feel or see, ICs can be
damaged by much lower voltage levels. This is because the connections inside the IC
are extremely small and are not generally built to handle thousands of volts flowing
through them. Some ICs are much more static sensitive than others, but it's good to
take precautions as if the IC you are handling might be damaged by ESD.
ICs are less likely to be damaged once they are connected to your circuit and have a
ground path available. ICs are most likely to be damaged when they are not yet
connected in your circuit, so the special handling considerations below should be
followed.
As stated previously, not all ESDs are extremely static sensitive. An ICs sensitivity to
ESD damage will increase as the density of the circuitry inside the IC is increased.
Devices like computer CPUs are extremely sensitive due to the large number of
transistors packed inside, and additional precautions like an ESD wrist strap should be
used when handling devices like CPUs. For hobby-level ICs, like the MCP3008 in this
kit, the four steps listed above will help you avoid damaging any ICs that you plan to use
with Raspberry Pi projects.
Using hardware SPI means that a small part of the BCM2835 processor on the
Raspberry Pi is devoted to handling SPI communication. This method requires you to
enable a setting in the Raspbian OS to "turn on" the hardware SPI portion of the
BCM2835, then hardware SPI must be done via a few specific GPIO pins after the
setting is enabled. Hardware SPI can yield better performance in programs where large
amounts of data are being transferred using SPI, but these increases will not be seen
when creating simple programs like the ones in this level.
Software SPI means that your program uses GPIO pins going high and low very rapidly
to emulate hardware SPI. When software is used to emulate hardware it's often referred
to as "bit banging". The benefits of this method are that no setting changes are required
on the Pi, and any of the configurable GPIO pins can be used for SPI communication. A
library must be installed for your program to understand how to communicate with other
devices using software SPI.
CE – Chip enable allows multiple devices to share CLK, MISO, and MOSI connections
MISO – Master Input Slave Output – communication from the device to the Pi
MOSI – Master Output Slave Input – communication from the Pi to the device
The MCP3008 Analog to Digital Converter is used to gather information from 8 analog
input channels and send it to another device using digital communication. This
converter has a resolution of 10 bits meaning 10 units of ones and zeros will be
available to express the analog voltage level it sees.
On a 3.3V based input voltage, 10 bits of resolution means 1024 steps of voltage
change can be measured between 0V (ground), and 3.3V. Breaking that voltage range
into 1024 steps means that the MCP3008 can resolve voltage changes as small as
0.0032 volts, or 3.2 millivolts. That should be more than accurate enough for any of your
projects.
The MCP3008 can be powered by a supply anywhere between 2.7V and 5.5V, making
it an ideal part for running from the 3.3V the Raspberry Pi's can supply. Another
attractive feature of this part is that it does not require any additional components to
operate, just connections to power, ground, SPI communication, and some analog
voltage levels for inputs.
CLK – GPIO18
MISO – GPIO23
MOSI – GPIO24
CS – GPIO25
STEP #1
In order to make room for the MCP3008, a few components will need to be removed
from the circuit you built in Lesson B-7.
Remove the keypad, slide switch, pushbutton switch, and the jumper wires connecting
to those parts. When completed, your circuit should look like this:
STEP #3
Now that power and ground are available near the bottom of the board, the next step
will be to carefully insert the MCP3008 into the breadboard.
Insert the MCP3008 into breadboard holes E43-50 and F43-50, with pin 1 located in
E43. The black dot on the IC (pin 1) should be near the LED in row 37:
Insert two short jumper wires from the P1 power rail over to G43 and G44. Insert two
short jumper wires from the ground rail N2 over to J45 and J50:
STEP #7
Your circuit is now complete.
Ensure all connections match the image in Step #6 and power up your Raspberry Pi.
STEP #1
The first step to creating your
program will be to open up
Thonny and create a new
program.
STEP #2
This program will need a couple of libraries to run. It will need the time library to add
some pauses to the program, and it will need the Adafruit_MCP3008 library to
communicate with the MCP3008.
Remember the spelling, symbols, and capitalization are important. Every character of
the library name must exactly match the line above, of your program will fail to find the
library.
Create the following list of pin names and numbers in your program:
CLK = 18
MISO = 23
MOSI = 24
CS = 25
Add the line below setting the variable mcp equal to the values the Adafruit library needs
to complete a read of the MCP3008:
CLK = 18
MISO = 23
MOSI = 24
CS = 25
The variable mcp can now be used to greatly simplify the command needed to read
values from the IC.
Create a while True: loop that sets a variable named ch0 equal to mcp.read_adc(0),
prints the value, then pauses for .25 seconds and repeats:
CLK = 18
MISO = 23
MOSI = 24
CS = 25
while True:
ch0 = mcp.read_adc(0)
print(ch0)
time.sleep(.25)
The mcp.read_adc command will return the current MCP3008 value for the channel
number specified between parentheses, in this case channel 0.
You will add more intelligence to gracefully exit this loop in future lessons, but for now
the Stop button in Thonny will be used to end the program.
The value will not vary much from 1023. Remember that the MCP3008 uses 0 to
represent ground and 1023 to represent 3.3. Since channel 0 is tied directly to 3.3V, the
reported value should be right around 1023.
If anything does not run as expected, confirm all of your code matches the last step, and
confirm your circuit is wired properly per Activity #1, Step #6.
STEP #1
The first step will be to power down the Pi to make it safe to add circuitry to the
breadboard. Shut down the Pi and disconnect power before proceeding.
STEP #2
Next, you will add a voltage divider network of three 1K-ohm resistors. As well as
ground and 3.3V, this will make two more analog voltage levels available for
measurement.
P1-57 to A57
D57 to D61
E61 to G61
Install a jumper wire from J61 to N2-61 to complete the path between power and
ground, through these three resistors:
STEP #4
Double-check your voltage divider placement and connections and power up the
Raspberry Pi. Channel 0 is still connected to 3.3V at this time.
Run the read3008.py program to ensure it is still reporting values of around 1023,
representing 3.3V.
With the program still running and reporting values, carefully relocate the input jumper
wire from P1-43 to B57. Your program will start reporting new values around 682.
The voltage divider is cutting the voltage into thirds since it's using three equally sized
resistors. Only one resistor is between our test point and 3.3V. 1.1V is being dropped by
this resistor, so you will measure 2.2V at this point. This will be shown as 2/3 of 1023
counts or around 682.
With the program still running and reporting values, carefully relocate the input jumper
wire from P1-43 to B61. Your program will start reporting new values around 341.
Only 1.1V will be present at this point in the voltage divider, which will be represented
by 1/3 of 1023 counts or around 341.
You will add devices that can create variable analog voltage levels that will be
measured using the MCP3008.
2. Does the Raspberry Pi have the capability to measure analog voltage levels
using its onboard components?
3. When using hardware or software SPI, which is more flexible with respect to
which GPIO pins must be used for communication?
ANSWER: A voltage divider made up of two equal value resistors will each drop
½ of the supply voltage. Since the supply voltage is 5V then each resistor will
drop 2.5V. 2.5V be available at the center- point between the two resistors.
2. Does the Raspberry Pi have the capability to measure analog voltage levels
using its onboard components?
3. When using hardware or software SPI, which is more flexible with respect to
which GPIO pins must be used for communication?
ANSWER: Software SPI is more flexible as it can be done using many of the
GPIO pins. Hardware SPI is less flexible because it must be done using specific
GPIO pins which may already be in use in your project.
In the next lesson, you will learn to work with potentiometers (variable resistors) and
photoresistors (light sensors). You will also learn advanced commands for lists which
will increase the functionality of programs you code.
POTENTIOMETERS,
PHOTOTRANSISTORS, AND
ADVANCED LIST COMMANDS
OBJECTIVE
In this lesson you will learn to use potentiometers (variable resistors), phototransistors
(light sensors), and work with advanced list commands.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
VARIABLE RESISTORS
A variable resistor is a resistor that can change value based on user input. These
devices are also called potentiometers or "pots" for short.
Variable resistors have a rotary knob or slider that changes their internal resistance
when a user moves the knob or slider. They are commonly used in audio equipment for
volume control, but they can show up in other places like lighting dimmer switches and
mechanical joysticks. They can be used any time user input needs to be measured so it
can be processed by a circuit or program.
You can also find potentiometers on the circuit boards inside most electronic equipment.
If an exact voltage or resistance level is needed for a circuit to function properly, a
potentiometer can be added so the circuit can be tuned to operate as intended, without
removing and reinstalling resistors of different values.
In this example, the potentiometer is adjusted about 20% away from one end. Since the
total resistance of the resistive track is 10K-ohms, this leaves 2K-ohms from the wiper
to one end, and 8K-ohms from the wiper to the other end. Using the voltage divider
formula you will find that the 2K-ohm side is dropping .2 or 20% of the 10K-ohm total
resistance. This means that .2 of 3.3V or 0.66V is being dropped by the resistive track
before the wiper. Subtracting this 0.66V from the initial supply of 3.3V results in 2.64V
being available at the wiper. These resistance values only apply to the potentiometer in
this exact position. If the knob is adjusted, these resistance and voltage values would
have to be recalculated.
Pins 1 and 3 are the ends of the resistive track, and pin 2 is connected to the wiper that
moves along the resistive track.
Photo-conductive cells will change their resistance based on the amount of incoming
light. In full darkness the resistance will be highest, and full sunlight their resistance will
be lowest. This resistance change can be used as part of a voltage divider to create an
analog voltage based on the light level the sensor is reading.
The legs of a phototransistor are the emitter and collector, but there is no external
connection for the base. The base is essentially made of photoconductive material and
is fully contained inside the phototransistor. As more light is received, the transistor will
allow more current to flow through the device.
Photoresistors do not have any polarity so they can be installed into a circuit in either
direction. This is not the case for phototransistors. Since phototransistors operate using
a polarized junction, it is important to have the device properly polarized in your circuit
to allow for proper light measurement.
The emitter of the phototransistor must be connected to the ground side of the circuit,
even if it's through one or more resistors. If not, the component will be reversed in the
circuit, and current will not be able to flow through it when light is detected.
In the good examples above, the phototransistor is properly polarized with the collector
toward 3.3V and the emitter toward ground. In the bad examples, the phototransistor is
reversed and will not allow current to flow through the device when light is detected.
The split command can break this string into new values in a list:
new_list = data.split()
In this command data represents the name of the string that will be split, and () will
cause default separators of spaces to be used. The resulting contents of new_list will
now be:
The separator does not need to be spaces. It can be anything as long as it is specified
inside the parentheses. In this example the incoming data is separated by commas:
data = '12,23,34,45,56'
new_list = data.split(',')
What happens if you specify a separator that's not contained in the list? No errors will
occur, but the string won't be properly split up because Python doesn't know where the
splits should occur:
data = '12?23?34?45?56'
new_list = data.split('/')
In this case, the split command is looking for forward slashes in the string to indicate
separation point. Since none are found, the entire contents of the string are moved into
the list, as the first entry in the list:
new_list = ['12?23?34?45?56']
This list isn't very useful, as it isn't split up into different parts as intended. The separator
must always match the one specified after the split command for the split to work
properly.
my_list.append('67')
This will result in the string '67' being added to the end of the list. The new value of
my_list will be:
my_list.index('34')
This index locator will look for the string '34' in the list named my_list and return the
index position if found. If the string is not found in the list, Python will generate an error
stating that information.
Running the code above would return an index value of 2, but that value isn't being
used for anything. You can use this value in your program however you like. You might
set a variable called position_34 equal to the index value that's returned from this
check:
position_34 = my_list.index('34')
Running this code will print the value 2 to the console since this is the index position of
'34' in my_list. Remember to include two parentheses at the end of a nested
statement like this, one to close out the print statement, and one to close out the string
value for the index command.
One thing to note about the index command is how it behaves when the same value is
present more than once in the list. It starts looking through the list from the left, or index
position 0. As soon as it finds your string it will return that value, even if there are more
of that value further down the list:
position_34 = my_list.index('34')
There are multiple instances of the string '34' in my_list. The index command will
start at index position 0 and will run across the first occurrence of '34' in index position
2, so the value of position_34 will now be 2. The rest of the occurrences of '34' will
be ignored.
This won't be a problem as long as your list does not contain duplicate data, but this
behavior is definitely something to keep in mind if it does.
if '45' in my_list:
print('Found 45')
This can be used to determine if the string '45' is part of my_list. If found, then Found
45 will be printed to the console. If '45' is not found in my_list, then the print
statement will be skipped, and the rest of the program will execute.
Another way to use this might be to add an index check if the string is found:
if '45' in my_list:
position_45 = my_list.index('45')
If found, position_45 will be set equal to 3 which is the index position of '45' in
my_list. The value of position_45 can then be used throughout the rest of your
program. If '45' is not found in my_list, then this if statement and its contents will be
skipped, without creating any errors.
print(len(my_list))
This will print the current length of my_list, which is 5, to the console. This command
can also be used to set a variable equal to the length:
my_list_length = len(mylist)
The variable named my_list_length can then be used throughout the rest of your
program to represent the current length of my_list. The value of my_list_length will
only be accurate if no items are added to or removed from my_list. This was a
snapshot of the length when this line of code ran. The value of my_list_length will
need to be updated with the new length if items are added to or removed from my_list.
There are two command that can be used to sort a list. The first command is sort and it
will reorder the contents of your original list. The second command is sorted and it will
only return a resorted list, but will not modify the contents of your original list.
The second line of code runs a sort on my_list and reorders the contents of the
original list. After running the second line of code, the items in my_list will now be in
alphabetical order. Printing the contents of my_list will now print:
This is great, but you might not want to modify the contents of the original list. In that
case, you can use the sorted command to return the resorted contents, without
modifying the original list:
print(sorted(my_list))
This will print the contents of my_list in alphabetical order, without actually modifying
the contents of my_list. You can also use the sorted command to create a second
alphabetized list, while leaving the original list unmodified:
my_list_alpha = sorted(my_list)
These commands can also take some parameters, one of which is reverse. The
reverse parameter will let sort or sorted know that you want the results in reverse, or
descending order:
my_list.sort(reverse=True)
The result of this command will be the reordering of my_list into reverse alphabetical
order. Printing my_list to the console now will result in:
Adding the reverse command to sorted requires a comma after the list name:
This will result in my_list_alpha being created in reverse alphabetical order, while the
contents of my_list remain unmodified.
One interesting behavior of both commands is the way numeric strings are ordered. You
might think these values are already in order, but since they are strings, they are not:
This same list, if converted to integers, will sort as you would expect:
The process for sorting numeric strings in the order you expect is called Natural Sorting
or Natural Sort Order. In the event that your program needs this functionality, while
outside the scope of this lesson, there are many examples of Python code available
online showing various ways to accomplish this task.
The first step is to only run the loop as many times as there are items in the list, which is
equal to the length of my_list or len(my_list). Normally you would specify the stop
value as an integer, but you can substitute len(my_list) to specify that value as the
amount of times the loop should run:
for i in range(len(my_list)):
Now this for loop will once for each item in my_list and then stop. A print statement
can now be added to the for loop that will print the current value of i along with the
value of my_list at index position i:
for i in range(len(my_list)):
print(i, my_list[i])
The first time this loop runs, i will equal 0. The print statement will print 0 along with the
value of my_list at index position 0 which is 'Brad'. i will be incremented by 1 and
will now equal 1. This new value of i will be printed along with the value at my_list[1]
or 'Kelly'. The printing will look like this in the console:
0 Brad
1 Kelly
2 Annie
3 Charlie
4 Dan
my_list.pop()
If the contents of my_list were printed at this point they would no longer include 'Dan'
as the pop() command removed the last item from the list. You can remove other items
by specifying their index position inside the parentheses:
my_list.pop(0)
Printing my_list at this point would show that index position 0 or 'Brad' has been
removed from the list.
my_list[:] = []
This will remove all values from the list and return it back to a completely empty state.
Another method available in Python 3.3 or higher is to use the clear() command:
my_list.clear()
When running programs in Thonny using the default Python environment of 3.5, either
of these commands will work well for clearing the contents of a list.
STEP #1
To make room for the new components, the voltage divider circuit will need to be
removed from your breadboard. You will use circuit from Lesson B-9 as a starting point
for this activity.
Next, remove the voltage divider, and any associated jumper wires from your
breadboard. When completed the circuit should look like this:
The resistive track on the potentiometer will be connected between 3.3V and ground,
and the wiper will be connected to channel 1 on the MCP3008. Make the following
additions to your breadboard:
Connect potentiometer to pins E61, E62, and E63. Align the body of the potentiometer
across the center of the breadboard so that columns A through D are clear for additional
connections.
Connect the phototransistor between P1-55 to A55. The collector should be connected
to P1-55 and the emitter should be connected to A55. This will properly polarize the
sensor.
STEP #4
The circuit is now complete.
Locate the file named read3008.py you saved in Lesson B-9 and double-click to open it
in Thonny. Click File, and then Save As to save a new copy of the file named
read3008_2ch.py to your Desktop.
Just below the line reading channel 0, add a line to read channel 1 using the variable
ch1. Make sure to point the mcp.read_adc command at channel 1 by using (1). The
new line is highlighted below:
while True:
ch0 = mcp.read_adc(0)
ch1 = mcp.read_adc(1)
print(ch0)
time.sleep(.25)
Add ch1 to the existing print statement by using a comma after ch0:
while True:
ch0 = mcp.read_adc(0)
ch1 = mcp.read_adc(1)
print(ch0, ch1)
time.sleep(.25)
Run the program. With the program running, use a flashlight to vary the intensity of light
on the phototransistor and watch its reported value increase and decrease. Turn the
knob on the potentiometer and its reported value will change as well.
NOTE: Be careful when adjusting the potentiometer. The knob will stop when
you get to the end of adjustment range and the legs of the potentiometer can be
damaged if you continue to attempt to rotate the knob past the end of travel.
Press the stop button in Thonny to exit the program when you are finished.
STEP #1
Create a new copy of your program that you can use as a base for your new program.
In Thonny, go to File, then Save As and save the new file as read3008_led.py. This
will leave your original read3008_2ch.py unmodified in case you want to go back and
run it again, or you want to create another copy to use as a base for another program.
STEP #2
Since this program will be using a GPIO pin, the GPIO module must be imported.
Import the GPIO module just below the existing import line for the time and MCP3008
modules:
CLK = 18
Just below the import section, add two lines of code to set the pin mode to BCM and
configure GPIO26 as an output:
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
CLK = 18
Add an if/else block between the print and sleep statements that checks both ch0 and
ch1 for values above 500. If the value is above 500, turn on GPIO26. If not, turn off
GPIO26. The if statement will need to use the or condition to allow either the
phototransistor or the potentiometer to turn on the LED. The additional code is
highlighted in gray below:
while True:
ch0 = mcp.read_adc(0)
ch1 = mcp.read_adc(1)
print(ch0, ch1)
GPIO.output(26, GPIO.HIGH)
else:
GPIO.output(26, GPIO.LOW)
time.sleep(.25)
STEP #5
Run the program.
Your sensor values will still be printed to the console, but now the LED will turn on if
either sensors value goes above 500.
Leave this circuit built as it will be used as a starting point in Lesson B-11.
STEP #1
The first step is to create a new program.
In Thonny, click the green plus button to create a new program. Go to File, then Save
As, and save the file as lists.py:
STEP #2
You will need to create a string to use as the data that will be manipulated by the rest of
the program.
Create a variable named data that contains a single string of the names Tom, Annie,
Zach, Kelly, and Bill, with colons (:) between each name:
data = 'Tom:Annie:Zach:Kelly:Bill'
STEP #3
Unfortunately, this data being in a single string isn't very useful. Break up the string into
a list by using the split() command.
Create a variable called name_list that will contain the names from data, split up
using (:) as a separator:
data = 'Tom:Annie:Zach:Kelly:Bill'
name_list = data.split(':')
Add a print command to print the contents of name_list and run the program. The list
of names will be printed to the console:
data = 'Tom:Annie:Zach:Kelly:Bill'
name_list = data.split(':')
print(name_list)
STEP #5
The horizontal name list is nice, but print the list vertically along with the index value for
each name. You can do this by using the for i in range loop covered earlier in this
lesson.
Add a for loop that will loop through the list, printing both the current value of i, and the
value of that index position in name_list. After adding the for loop, run the program and
both the horizontal list and vertical list with index values will be printed to the console.
data = 'Tom:Annie:Zach:Kelly:Bill'
name_list = data.split(':')
print(name_list)
for i in range(len(name_list)):
print(i, name_list[i])
Create a variable called abc_list and use the sorted command to store an
alphabetized copy of name_list, without modifying the contents of name_list:
data = 'Tom:Annie:Zach:Kelly:Bill'
name_list = data.split(':')
print(name_list)
for i in range(len(name_list)):
print(i, name_list[i])
abc_list = sorted(name_list)
Add a print command that will print the contents of abc_list, and run the program:
data = 'Tom:Annie:Zach:Kelly:Bill'
name_list = data.split(':')
print(name_list)
for i in range(len(name_list)):
print(i, name_list[i])
abc_list = sorted(name_list)
print(abc_list)
The following output will be printed to the console. First, the original list in the order they
were pulled from the string, and then the vertical list with along with index numbers, and
then the alphabetized version of the list.
3. Between sort and sorted, which command modifies the original list, and which
does not?
3. Between sort and sorted, which command modifies the original list, and which
does not?
ANSWER: The command sort will modify the original list. The command
sorted will not modify the original list.
In the next two lessons, you will learn to work with Radio Frequency Identification
(RFID) technology.
RFID SYSTEMS
OBJECTIVE
In this lesson you will learn to work with radio frequency identification (RFID) systems
including how to both read and write RFID tags.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
RFID TECHNOLOGY
RFID stands for radio-frequency identification. This system was originally designed for
powering a device using radio waves. The RFID receiver was powered by a transmitter
sending radio waves at a very specific frequency. The receiver did not contain its own
power source, instead it contained special circuitry to convert the incoming radio waves
into power that could be used to power up additional circuits in the receiver.
Around 1970, new ideas and patents started to emerge from this existing RFID
technology. Proposed use cases involved automated toll collection systems, banking,
security, and medical applications, as well as many others.
RFID technology can now be seen everywhere. Automated toll collection is installed on
many of highways. Most pets have RFID implants that allow for identification of the
animal without any external markings like a collar or tags. Many businesses rely on
RFID cards to manage employee access control, instead of handing out physical keys
to the building.
Active tags contain a battery that can be used to boost the tag's transmission distance.
Some of these tags can be read from hundreds of meters away, but the tags are much
larger than passive tags due to containing a battery, larger antenna, and more radio
amplification circuitry.
Since passive tags don't contain a power source or much circuitry, they can be much
smaller. Some versions are flat stickers that can be stuck to products in a store, and
some are almost as small as a grain of rice and can be implanted under an animal’s
skin.
RFID can be used in many applications, but the underlying technology is the same. The
reader sends out radio waves, and the tag responds with values stored within the tag.
The controller is where access control permissions are managed. It stores all tag or
keycard information, along with what areas or doors that keycard is permitted to access.
The software inside the controller operates much like some of the if/else programs you
have created throughout this course. When a keycard is scanned, it's unique value is
used to check a list of permissions stored inside the controller. For example:
User A is issued card number 123. Card 123 is allowed to access door 1 and door 2.
User B is issued card number 456. Card 456 is only allowed to access door 2.
If card 123 is scanned at door 1 or 2 then access will be granted. If card 456 is scanned
at door 2 then access will be granted, but when scanned at door 1, access will be
denied. This is nice because access to areas can be granted or revoked just be
changing the permissions in the main controller, instead of handing out individual metal
keys, or changing locks.
Although encryption is used to secure data storage and communication on some higher
end cards, most cards are not very secure. These less secure cards will respond to any
reader that requests its data, given that reader is operating in the right frequency range.
The tags in your kit operate in the 13.56MHz range, so they will start transmitting their
stored data anytime they receive a 13.56MHz signal.
This is where the problem occurs. Your card doesn't know the difference between the
RFID reader in your kit and any other 13.56MHz reader it sees. Imagine that you wired
your front door to be conveniently controlled by an RFID lock that only opened for a tag
that sent out the string Open Sesame. Everything is working great and you no longer
have to carry keys, using only the tag to unlock your front door.
Since your tag will respond to any 13.56MHz reader, it's possible that your tag might get
scanned by someone hoping to duplicate your tag in order to access your house.
Someone close enough to your tag in line at a coffee shop could send a 13.56MHz
signal your way, and your tag would respond to them with Open Sesame. They can now
duplicate or clone your card and unlock your front door with their copy of your card.
Your tag won't know that it was scanned and the reader on your front door won't know
the difference, since it's only looking for the string Open Sesame to unlock the door. This
attack is fairly uncommon, but it is made possible by the convenience of RFID. There
are RFID shielding devices available that make this type of attack impossible, but this
security does come at the cost of convenience.
These RFID shielding devices completely enclose your card in a material that radio
waves cannot penetrate. This shielding can be built directly into wallets, purses, or
smaller pouches and can hold a single card or tag. While inside this shielding, your card
cannot be read by an attacker, but it also cannot be read by your front door reader
either. You would need to remove your card from this shielded pouch in order for the
radio waves emitted by the reader to interact with the card, and for the card to send its
data back to the reader.
As with everything, there are trade-offs between security, convenience, and cost. The
benefits of deploying an RFID system would need to be weighed against the possible
cloning and misuse of a card. More secure RFID systems can lessen the risk, if not
eliminate, the possibility of these attacks, but this will require a higher-level system with
encryption that will come at a higher financial cost.
These libraries do not support software SPI, so hardware SPI will be used to
communicate with the reader. Using hardware SPI means that specific GPIO pins will
be used for this communication, and that a setting in the Raspberry Pi will need to be
changed in order to enable hardware SPI. In the activities for this lesson, you will install
this library and change the SPI setting once the reader is wired up.
MIFARE1 S50
MIFARE1 S70
MIFARE Ultralight
MIFARE Pro
MIFARE DESFire
Cards may also list compatibility with ISO14443A, which means they will also work with
this reader. If you plan to use a card not listed above, please do your research to ensure
it will be compatible with the MFRC522 reader before purchasing.
RFID TAGS
Tags can come in many shapes and sizes and can store different amounts of
information. The tags in your kit will hold 1KB (one kilobyte) of information. This is not a
ton of room, but it is enough to store enough information to identify the card with a
reader. Your program can then respond however you would like to the presence of the
card.
The tags and library you will be using in this lesson support the reading of two values:
The card's UID value, or Unique ID, is a unique 12 to 13-digit value that is assigned at
the time the card is manufactured. This value can be read but cannot be modified.
The card's text value is a 48-character field that can store any data you like. This could
be anything from a single letter, number, or character, up to a complex string of all these
combined. This value can be read and written by the MFRC522 reader.
These unused values cannot be left empty, so spaces will be used to fill up the rest of
the characters available. This means that when trying to write a short value like 'card',
the actual value written to the card will be:
'card '
This wont normally be an issue unless you want your program to act on this value when
read back from the card.
This wouldn't normally be a problem unless you want to make an action happen in your
program based on this text value:
Leading spaces refer to spaces that occur before the information in your string:
' card'
Trailing spaces refer to spaces that occur after the information in your string:
'card '
Python has a built-in function to get rid of these extra spaces, and you will learn more
about it in the next section.
short_var = long_var.strip()
In this example, the string long_var will be stripped of all leading and trailing spaces,
and saved as short_var.
print(short_var)
The strip() command will only strip leading and trailing spaces from the string, and
not the one between the words. After stripping and being saved as short_var, the print
command will print 42 Electronics to the console, instead of the longer version with
extra spaces.
You may have noticed that the rstrip() command could also be used to eliminate
trailing whitespace from the text value read from a card. This is true, but strip() will
ensure that any extra spaces, whether before or after the value in the string, are
removed.
#!/usr/bin/env python
or
#!/usr/bin/env python3
In Unix-like operating systems, this line can be used to determine the path to locate the
program that should be used to execute the program, and which program should be
used to run the program. Both examples above specify the path for finding Python as
/usr/bin/env, but they specify different versions of Python. The first example specifies
Python, but not which version. Without modifications to your system, Raspbian will use
Python 2 for a file containing this type of shebang. The second example specifies that
the program must run in Python 3.
These lines of code are only used when the Python version is not specified when
running the program on the command-line. Starting the program using sudo python
program.py or sudo python3 program.py will override this line, and it will only be
treated as any other comment in the program. This is the same behavior we see in
Thonny. The environment setting in Thonny will determine which version of Python is
used to run a file, not the shebang line.
This won't normally be an issue for you since programs so far have been run in either
Thonny using Python 3, or in the command-line using Python 2. This information can be
helpful if you're trying to build a project you found online, and odd errors seem to be
popping up.
Once these libraries are installed, and the SimpleMFRC522 module is imported into
your program, working with the reader becomes very easy:
reader = SimpleMFRC522.SimpleMFRC522()
This line of code will allow you to refer to the reader as reader in your program, instead
of the much longer name above.
This command will read the id number and text value stored on the tag and set them
equal to id and text. These variables can then be printed or used in other ways
throughout your program.
reader.write('card')
The command above can be used to modify the text value stored on the tag. In this
example, card will be written to the tags text value. The id number field is fixed so there
is no command available for changing that value.
That is the extent of the commands that we need to interact with the reader. Using
these two commands you can change the text value of a tag, read the id and text value
of a tag, and then have your program take any actions you would like, based on the tag
data that is presented.
STEP #1
Shut down the Pi and disconnect power before proceeding.
To make room for the new components, the phototransistor, potentiometer, and any
associated components will need to be removed from the breadboard.
Using the circuit from Lesson #10, Activity #2 as a starting point, remove the
phototransistor, potentiometer, resistor, and any associated jumper wires. The circuit
should now look like this:
Gently inserting a small, flat screwdriver under alternating sides of the IC is the safest
way to remove the IC.
Remove the IC and associated jumper wires from the breadboard. If you're at all unsure
on the best way to do this, watch the short video on the Level B Resource Page before
attempting to remove the IC. The breadboard should now look like this:
Install the 8-pin connector of the reader into H49 through H56. The reader should be
oriented such that the main body of the reader is above covering columns A through H.
Double-check all connections with this photo before proceeding to the next step.
Once it's up and running click on the raspberry in the top-left corner, select Preferences,
and then select Raspberry Pi configuration from the bottom of the list.
Reboot your Raspberry Pi to allow the SPI setting change to take effect.
Open a Terminal window by clicking the terminal button in the top menu bar. Once
open, use the command sudo apt-get update to ensure your Pi knows the latest
version numbers of all packages. Once that completes, run sudo apt-get upgrade to
upgrade any required packages to the latest version. Answer y to any questions about
free disk space that will be consumed by the upgrades.
In your existing Terminal window, type the following commands, pressing enter after
each command:
cd ~
cd SPI-Py
The last step is to execute the setup.py install script for python3:
SPI-Py will now be installed for use in the Python 3 environment, which you can run
from within Thonny. If you encounter any errors during this process, start over from the
beginning of this step.
In your existing Terminal window, type the following commands, pressing enter after
each command:
cd ~
If you encounter any errors during this process, start over from the beginning of this
step.
STEP #1
You now have a local copy of the MFRC522-
python repository.
Run read.py by clicking the run button in Thonny. Place the card near the reader and
its id number and text value will be displayed in the console output.
Navigate back to the File Manager and double-click on write.py. Write.py will open as a
new tab in Thonny.
Make sure there are no tags near the reader when you run write.py. The program will
write any tag within range and you want to make sure the tags get programmed
correctly, so you can write a program in Activity #3 that will recognize these values.
You will now write the text value of card to the white card.
If any errors occur, or the program does not run as expected, stop write.py in Thonny
and run it again. Do not proceed to the next step until the white card has successfully
been written with the text value card.
STEP #3
Now that the card has been written you can work on the blue tag. Make sure to hold the
metal keyrings on the tag to keep them from coming into contact with any metal
connections on the circuit board of the reader.
Using the same process in the last step, write a text value of tag to the blue tag.
You now know that the text values loaded correctly and now you can create a program
that can make decisions based on those values.
NOTE: The RFID reader's proper operation relies on good connections to power,
ground, and data lines on the Raspberry Pi. Poor or intermittent connections can
cause the reader not to operate properly. If your reader stops reading or writing
for no reason, carefully remove and reinstall each of the jumper wire connections
in J49 through J56. If the problem with your reader is due to a bad jumper wire
connection, this should fix the problem.
The reader will read a tag very quickly, but if you try to swipe a tag by the reader
extremely fast it is possible to pull the tag away from the reader before it's
finished reading. This will result in a card read error that might look something
like this:
If this happens, just scan the tag again, more slowly, and everything should work
as expected.
STEP #1
Use the read.py program as a
starting point, as it already contains
everything needed to read tags.
NOTE: Programs that need to read RFID tags must be located inside the MFRC-
Python directory, as they will need direct access to the SimpleMFRC522.py and
MFRC522.py files used to communicate with the reader. Attempting to run
programs that require access to these communication files from anywhere else,
will result in Python errors.
Add a while True: loop just below the try: loop. The addition is highlighted below:
reader = SimpleMFRC522.SimpleMFRC522()
try:
while True:
id, text = reader.read()
print(id)
print(text)
time.sleep(.3)
finally:
GPIO.cleanup()
You will notice the program is now reading without exiting after each tag, but it's reading
one tag multiple times because this program can loop very quickly, and the tag might be
near the reader during more than one loop.
Another problem with the program is that there is no longer away to exit the program
gracefully. Press the stop button in Thonny or CTRL-C to stop the program. This will
result in errors because the program was busy communicating with the reader when the
program was ended. You will fix the looping and exit problems in the next step:
Import the time module at the beginning of the program and add a time.sleep(.3) to
the end of the loop. Also, change finally: to except KeyboardInterrupt: which will
allow manually ending the program to trigger the GPIO.cleanup(). Additions and
changes are highlighted below:
#!/usr/bin/python3
reader = SimpleMFRC522.SimpleMFRC522()
try:
while True:
id, text = reader.read()
print(id)
print(text)
time.sleep(.3)
except KeyboardInterrupt:
GPIO.cleanup()
It can now scan tags reliably without duplicating reads and pressing stop in Thonny or
CTRL-C will not generate errors, as GPIO.cleanup() is being triggered before the
program exits.
Replace the two existing print statements with a strip() command that will strip trailing
whitespace from text and save it as the new value of text. Changes highlighted
below:
try:
while True:
id, text = reader.read()
text = text.strip()
time.sleep(.3)
The id number and text value will be read from the card and the text value will be
stripped down to either 'card' or 'tag' based on the tag that was scanned.
Add two if statements directly under the strip() command that will check the value of
text and print ACCESS GRANTED or ACCESS DENIED based on which tag is read:
try:
while True:
id, text = reader.read()
text = text.strip()
if text == 'card':
print('ACCESS GRANTED')
if text == 'tag':
print('ACCESS DENIED')
time.sleep(.3)
Run the program and read both tags. The access messages will be printed each time a
card is scanned to indicate ACCESS GRANTED or ACCESS DENIED.
This code is being kept very simple to illustrate the concept, but the print sections of the
if statements in this program could be replaced with anything you like. By including
GPIO pin setups at the beginning of your program you could light an LED when access
is granted, play a noise through the piezo speaker when access is denied, or any
combination of print statements and GPIO events that you might want.
In Lesson B-12, you will add more advanced program functionality, and circuitry that will
allow for additional notification options.
• Leave this circuit built as it will be used as a starting point in Lesson B-12.
• Save the program to use as a starting point in Lesson B-12.
2. What is the Python command that removes leading and trailing whitespace from
a string?
2. What is the Python command that removes leading and trailing whitespace from
a string?
ANSWER: To remove both leading and trailing spaces, use the strip()
command.
ANSWER: No, not all readers and tags are compatible. It is important to check
for compatibility between RFID readers and RFID tags.
In the next lesson you will continue to work with RFID readers but add more advanced
Python programming techniques to create more complicated program functionality.
OBJECTIVE
In this lesson, you will learn to use advanced Python programming techniques that will
enable you to create more complicated programs.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
Your program does not always need to contain every bit of information needed to
operate. Using the access control system you created in an activity in Lesson B-11 as
an example, you might want to create a text file list containing users and access levels,
that your program can access to determine which cards should be allowed and denied
access. Keeping a separate access list means that you do not have to modify your
program every time a user needs to be granted or denied access.
A very common data format is called .csv which stands for Comma Separated Values.
In this format, each piece of data is separated by a comma. This might be something as
simple as a list of people and their favorite colors:
Annie,blue
Bobby,green
Cindy,yellow
David,red
The program can look up the value for each question and determine which person or
color should be provided as the correct answer.
You may also see data files that include header information that labels each column in
the first line:
Person,Color
Annie,blue
Bobby,green
Cindy,yellow
David,red
This may seem little silly with only two columns, but .csv files can get much more
complicated, and hard to read, as they get larger. The column names can help keep
track of what each value represents,
Person,Color,Age,State,Lesson,Joined,LastLogin
Annie,blue,12,CA,A,2017,27JUN2018
Bobby,green,14,WA,B,2016,2JUL2018
Cindy,yellow,13,NC,A,2018,22JUL2018
David,red,12,FL,B,2018,12FEB2018
Annie, blue, 12
Bobby, green, 14
Cindy, yellow, 13
David, red, 12
If you ask the program to find the person that likes 'green', it won't find anyone when
using this data. This is due to the space that made Bobby's favorite color ' green'
instead of 'green'. Since ' green' does not equal 'green' no match will be found,
and the program will report that none of the people listed likes the color 'green'.
This is why it's very important to understand the format of your data, before attempting
to use it in a program. If another program is creating the .csv file you may not have the
option of removing spaces or other problems you might find. In that case, your program
may need to perform some additional tasks on the data, such as stripping leading
spaces, prior to using the data in your program.
This will open the file named 'file_to_open.txt' in read-only mode due to the 'r' argument
enclosed with the filename. In this example, the data at the end of the command
specifies that the information pulled in from the file will be referred to as data. This data
value can then be used in your program to refer to the input file.
One thing to note about the filename specified in the command above is that there is no
path specified for the input file, such as /home/pi/file_to_open.txt or
/home/pi/file_to_open.txt. The filename without a path will only work if the file is
located in the same directory as your program. Adding this open command
to:/home/pi/MFRC522-python/access_control.py means that the program will be looking
for file_to_open.txt in the /home/pi/MFRC522-python directory. If the file is not
located in that folder, then Python will generate an error, and you will either need to
move your input file inside /home/pi/MFRC522-python, or specify the correct path to
your input file in the open command.
import csv
Now that the csv module has been imported, you can use the csv.reader() command
to read the information contained in data. The parenthesis must contain the name of
the file to be read, in this case, data. The list() command is wrapped around the
csv.reader() command so a list can be created by the output, and saved as a new
variable called values:
values = list(csv.reader(data))
This will read the csv values from the file named data and save them as a list of strings
named values. One thing to note about input files that contain multiple lines is that
each line will be stored as its own list. When there are multiple input lines involved, you
end up with a list of lists, also known as a matrix. Here are some examples of a single
list versus a matrix:
[['Annie','blue','12'],['Bobby','green','14'],['Cindy','yellow','13'],
['David','red','12']]
By stacking each small list on top of each other, you can see the familiar pattern of rows
and columns from the input file:
[['Annie','blue','12'],
['Bobby','green','14'],
['Cindy','yellow','13'],
['David','red','12']]
The information in a matrix can be accessed by using the index of the row and then the
column of the data you want. Just like before when accessing positions of a string, row
index values start at 0 and move to the right. Column index values begin at 0 with the
top line and work their way down:
This way, the program can be used to locate specific values that were pulled in from the
input file.
Consider a program where you want to constantly monitor a button for presses, and
also blink a status light once every 10 seconds:
Check button
Repeat
The LED will flash once every 10 seconds, but this program has a problem with the
button check. The button will be checked initially, and then the program sleeps for 10
seconds before turning on the LED. During this delay, any button presses will not be
registered. Holding the button down during a check would allow the push to be
registered, but this would not be a very user-friendly circuit or program. The good news
is that Python has a way to handle this using multiple program threads.
Start program
Define function that blinks LED every 10 seconds
Check button
The LED can now be slowly flashing in a secondary thread, while the main program
constantly checks the button for presses. To use multithreaded operations, you must
first import the _thread module:
import _thread
Next, a function needs to be created that will define what will happen in the new thread:
define status_led():
while True:
time.sleep(10)
GPIO.output(26, GPIO.HIGH)
time.sleep(.5)
GPIO.output(26, GPIO.LOW)
The function will be running like its own program, so it will need a while True: or some
other type of condition check to keep it running. Otherwise, it will only run through one
time and the thread will stop.
_thread.start_new_thread(status_led, ())
This will send the command start_new_thread to the _thread module. The name of
the thread to start is status_led. The () in this command is similar to calling a function
in a program:
status_led will not call the function and Python will generate an error
This trailing () is still included when starting a new thread, it's just separated from the
function name by a comma.
When running programs using the command line, new threads that are created to loop
continuously, like the status_led example above, terminate as soon as the main
program ends. This is not the case in Integrated Development Environments like
Thonny.
IDEs are convenient to use, but they do not always mimic everything that would happen
when running programs using the command line. This is true when terminating new
threads that were started in your program. Pressing the stop button in Thonny will
terminate the main program, but the secondary thread will continue running in the
background. Pressing the stop button again will terminate this secondary process.
A good way to work around this is to use a conditional loop in your secondary thread.
Instead of creating an endless loop by using while True:, do a variable check like
while end == 0: and modify the value of end in your main program. This way the
value of end will be evaluated each time the new thread runs, and if end is ever
anything but 0, the loop will stop, and the thread will terminate. You could also use this
end value to control your main program loop:
def status_led():
while end == 0:
do LED things
try:
while end == 0:
do main program things
except KeyboardInterrupt:
end = 1
Both the main program and the status_led thread will loop continuously as long as
end equals 0, which is set at the beginning of the program. Pressing CTRL-C or Stop in
Thonny will generate a KeyboardInterrupt, changing the value of end to 1. The main
program will stop, and the new value of end will cause the loop in status_led to
evaluate as False, terminating the secondary thread.
Using the technique above will help you avoid threads continuing to run after the main
program has terminated, when using Thonny.
The strftime command can create a string of the current date and time in whatever
format you like:
time.strftime('%Y-%m-%d %H:%M:%S')
This command will create a string of the current date and time that will be formatted
using the sequence in parentheses. These values are called directives:
%Y = Year %H = Hour
%m = Month %M = Minutes
%d = Day %S = Seconds
Capitalization is very important for these directives, so %Y is not the same as %y. They
both represent the year, but in different formats:
A full list of al the directives available for the strftime command is available at:
https://docs.python.org/3/library/time.html
2018-07-02 15:45:09
end = 0
while True:
print('running')
if end == 0:
break
print('exiting')
The while True: loop would normally run forever, printing 'running' over and over, but the
if statement inside is evaluated each time the loop runs. The very first time the loop
runs, the if statement evaluates as True, and the while loop is broken, allowing the rest
of the program to run. The console output from this program would look like this:
running
exiting
The break statement causes any higher level for or while loop to exit, no matter how
far nested the break is inside the outer loop:
end = 0
while True:
print('running')
if end == 0:
if end == 0:
if end == 0:
break
print('exiting')
STEP #1
The first step will be to create and populate the text file that will contain the tag
information, employee name, and allow/deny information. In order to simplify the file
path needed in the program, you will be placing this file in the same directory as your
access_control.py program, the MFRC522-python directory.
Once inside that folder, right-click on any open space in that window and select Create
New, and then Empty File. When prompted for a name, type access_list.txt and
click OK.
Double-click the access_list.txt file that you just created, and it will open in a new text
editor window. Type the information below into your text file:
card,Employee,allow
tag,Visitor,deny
After entering the text above, select File from the upper menu, and then select Save to
save your changes. You can now use the X button at the top-right to close the file.
STEP #3
You now have a text file that contains the access information. The reading of this new
file needs to be added to your access_control.py program.
import time
import csv
reader = SimpleMFRC522.SimpleMFRC522()
try:
Just above the main try: loop, add a with open statement that opens
access_list.txt in read-only mode, and save the contents of that file as a new
variable called data. Indented on the next line, create a new variable named values
that contains the list output of running csv.reader on the information saved as data.
reader = SimpleMFRC522.SimpleMFRC522()
try:
Your file named access_list.txt will now be opened and converted to a matrix (list of
lists) called values that your program can check against any tags that are scanned.
STEP #5
The text file is now available within your program, but your program is not yet searching
for tag matches in that file. For that you will create a loop that will look for matches
between the scanned tag's text value and any matching entries in the values matrix
data.
Just below the text.strip() command, add a for/in loop that will look through each
line in the matrix called values and temporarily set each line equal to a variable named
search. Add the following line of code just below the text.strip() line.
try:
while True:
id, text = reader.read()
text = text.strip()
for search in values:
if text == 'card':
If a match is found then the program should grab index values [1] and [2] from the
search list, saving them as user and status respectively. The loop must then break to
stop the search, so your user and status values are not overwritten by the upcoming
else statement.
If no match is found, an else: statement will be used to manually assign a user value
of 'Unknown' and a status of 'deny'. This will keep the program from generating
errors if the tag value scanned is corrupt or not found in the text file.
Indented below the for search in values: line of code, add the following if/else
statements to accomplish the behavior outlined above.
The variables user and status are now being set equal to the values in your
access_list.txt file, based on the text value of the tag that is scanned. If the tag
is found, user will store the employee name and status will be equal to either 'allow'
or 'deny' based on your text file. If the tag is not found, user will equal 'Unknown' and
status will equal 'deny'.
Change both if statements to use status instead of text. The status to allow access
must be changed to 'allow', and the status to deny access must be changed to
'deny' so they match the values from the text file.
else:
user = 'Unknown')
status = 'deny'
if status == 'allow':
print('ACCESS GRANTED')
if status == 'deny':
print('ACCESS DENIED')
time.sleep(.3)
#!/usr/bin/python3
reader = SimpleMFRC522.SimpleMFRC522()
try:
while True:
id, text = reader.read()
text = text.strip()
for search in values:
if text in search:
user = search[1]
status = search[2]
break
else:
user = 'Unknown'
status = 'deny'
if status == 'allow':
print('ACCESS GRANTED')
if status == 'deny':
print('ACCESS DENIED')
time.sleep(.3)
except KeyboardInterrupt:
GPIO.cleanup()
Run your program in Thonny. Scanning the card will result in an ACCESS GRANTED
message and scanning the key fob will result in an ACCESS DENIED message.
STEP #1
The first step is to import the _thread module so it can be used to launch the LED
thread.
Add import _thread to the list of imports at the beginning of the program:
STEP #2
Next, the GPIO pin numbering mode must be specified and GPIO26 must be configured
as an output.
Add the GPIO.setmode and GPIO.setup lines just below the import section:
import csv
import _thread
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
reader = SimpleMFRC522.SimpleMFRC522()
Below the GPIO lines, create a function called scanned() that turns the LED on for .1
seconds, then off for .1, and repeat 5 times:
GPIO.setup(26, GPIO.OUT)
def scanned():
for i in range(0,5):
GPIO.output(26, GPIO.HIGH)
time.sleep(.1)
GPIO.output(26, GPIO.LOW)
time.sleep(.1)
reader = SimpleMFRC522.SimpleMFRC522()
STEP #4
The last step is to modify the if status == statements that currently only print the
messages to the console. In addition to printing the message these blocks will also
trigger the scanned() function that will flash the LED.
Add the following command to both if blocks that print the messages:
else:
user = 'Unknown'
status = 'deny'
if status == 'allow':
print('ACCESS GRANTED')
_thread.start_new_thread(scanned, ())
if status == 'deny':
print('ACCESS DENIED')
_thread.start_new_thread(scanned, ())
time.sleep(.3)
#!/usr/bin/python3
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
def scanned():
for i in range(0,5):
GPIO.output(26, GPIO.HIGH)
time.sleep(.1)
GPIO.output(26, GPIO.LOW)
time.sleep(.1)
reader = SimpleMFRC522.SimpleMFRC522()
try:
while True:
id, text = reader.read()
text = text.strip()
for search in values:
if text in search:
user = search[1]
status = search[2]
break
else:
user = 'Unknown'
status = 'deny'
if status == 'allow':
print('ACCESS GRANTED')
_thread.start_new_thread(scanned, ())
if status == 'deny':
print('ACCESS DENIED')
except KeyboardInterrupt:
GPIO.cleanup()
Run your program and read the tags. Reading a tag will now result in the LED flashing
quickly 5 times whenever a tag is read.
STEP #1
In the first activity you stored the employee name of the scanned tag as user. You will
now add that variable to the ACCESS GRANTED and ACCESS DENIED print statements.
Add the strings ' for ' and value to the print messages. Make sure to include spaces
before and after for so the messages are spaced properly:
if status == 'allow':
print('ACCESS GRANTED' + ' for ' + user)
_thread.start_new_thread(scanned, ())
if status == 'deny':
print('ACCESS DENIED' + ' for ' + user)
_thread.start_new_thread(scanned, ())
Run the program and confirm that employee names from the text file are now included
with the messages and that they are formatted properly.
STEP #2
Next, add a print statement that will print the date and time of each scan using the
strftime command. You will use the 2018-08-04 12:23:21 formatting from this lesson.
Add the following print statements below the existing print statements:
if status == 'allow':
print('ACCESS GRANTED' + ' for ' + user)
print(time.strftime('%Y-%m-%d %H:%M:%S'))
_thread.start_new_thread(scanned, ())
if status == 'deny':
print('ACCESS DENIED' + ' for ' + user)
print(time.strftime('%Y-%m-%d %H:%M:%S'))
_thread.start_new_thread(scanned, ())
#!/usr/bin/python3
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
def scanned():
for i in range(0,5):
GPIO.output(26, GPIO.HIGH)
time.sleep(.1)
GPIO.output(26, GPIO.LOW)
time.sleep(.1)
reader = SimpleMFRC522.SimpleMFRC522()
try:
while True:
id, text = reader.read()
text = text.strip()
for search in values:
if text in search:
user = search[1]
status = search[2]
break
else:
user = 'Unknown'
status = 'deny'
if status == 'allow':
print('ACCESS GRANTED' + ' for ' + user)
print(time.strftime('%Y-%m-%d %H:%M:%S'))
_thread.start_new_thread(scanned, ())
if status == 'deny':
except KeyboardInterrupt:
GPIO.cleanup()
Run your program and read the tags. The date and time of the scan will now be printed
just below the DENIED or GRANTED message that includes the employee name.
You may disassemble the circuit as you won’t be needing it for Lesson B-13.
3. What command would you use to print the current date/time in the following
format? Hour:minutes:seconds day/month/year
3. What command would you use to print the current date/time in the following
format? Hour:minutes:seconds day/month/year
In the next lesson, you will learn about level shifting and have an opportunity to work
with infrared sensors.
OBJECTIVE
In this lesson you will learn how to shift signal voltage levels to make components
compatible with the GPIO pins of the Raspberry Pi. You will also learn to work with
infrared obstacle and line sensors.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
The GPIO pins on a Raspberry Pi operate in the range of 0 to 3.3 volts, with 3.3V
representing a high signal. Another popular microcontroller board is called the Arduino
and its GPIO pins are based on a system with a range of 0 to 5 volts. In the case of the
Arduino, a high signal will be 5V.
Generally, devices will use their supply voltage to determine the voltage level used for
communication. If an RFID reader is powered by 3.3V, then it will likely communicate
using 3.3V as a high signal. If an infrared obstacle sensor is powered by 5V then it will
likely communicate using 5V as a high signal. These are only general rules, so always
confirm that a device will not communicate using voltages above 3.3V if you plan to
connect it directly to the GPIO pins of your Raspberry Pi.
A hardware level shifter is an IC or circuit board that converts one voltage level to
another. They are available in many voltage levels, but for these lessons you will be
focusing on 3.3V and 5V. These level shifters can come in a few different varieties:
3.3V/5V Bidirectional – converts both 3.3V and 5V inputs into the opposite output
Bidirectional level shifters are generally more costly since they can convert between
3.3V and 5V in both directions. These devices are used to enable two-way
communication between two devices, one using 3.3V signals and one using 5V signals.
The VCC and GND pins will be connected to 3.3V and Ground. A1 through A8 and B1
through B8 are the configurable input or output pins. Applying 3.3V or Ground to the
DIR and OE pins determines whether As or Bs will be used as inputs. We will be using
A as the inputs and B as the outputs. The datasheet for this part states that to enable
communication in this direction, OE must be low or grounded, and DIR must be high or
3.3V. This is the way we will configure the IC in the activities section so that the A side
can be used as 5V inputs, and the B side will be the 3.3V outputs.
A channel is made up of A and B pins sharing the same number. This means that A1
and B1 are one channel, with A being the input, and B being the output. If a 5V high is
seen at input A1 then the IC will make B1 go to 3.3V to represent a high. A2 feeds to
B2, A3 feeds to B3, and so on.
When shifting signals from 5V down to 3.3V, the IC should be powered by 3.3V. Below
is a diagram of a 74LVC245 being used to connect a 5V sensor to a GPIO pin of the
Raspberry Pi:
The sensor outputs a 5V signal that would damage the Pi if connected directly. The 5V
signal is instead connected to the input of the level shifter. The level shifter takes the 5V
input and converts it to 3.3V. This 3.3V output can then be connected directly to a GPIO
pin of the Raspberry Pi. If the sensor outputs a 5V high, the Pi will see a 3.3V high, and
your program can use this data in any way that you like.
The obstacle sensor included in your kit has an active low output, which means that
when no object is detected the output line will have a high or 5V present. When an
obstacle is detected this output line will go to ground or 0V. Your program must be
coded to understand that high means no object has been detected, and that low
represents that an object is near the sensor.
The sensor contains an infrared emitter and receiver, located next to each other.
The infrared emitter constantly sends out infrared signals, and receiver is constantly
waiting to receive those signals back. If an object is close enough to the sensor, the
emitter signals will bounce off the object and reflect back to the receiver. The sensor
then knows that something is close, and the circuitry on the sensor board will pull the
output pin low, indicating an object is near. If an object is not close enough to reflect the
emitted signals back to the receiver, then the sensor will know that no object is near the
sensor, and the output pin will remain high.
The detection distance will also be affected by the angle at which the emitted infrared
contacts the surface of the object. An object reflecting perpendicular to the sensor will
offer the highest level of reflectance, allowing for the greatest possible detection
distance. The further an angle is from 90-degrees, the lower the amount of reflectance
that surface will offer, which will lower the detection range:
A different type of long range distance sensor can then be mounted on the front of the
robot to provide additional obstacle information. This type of sensor will be discussed in
an upcoming lesson.
The infrared line sensor operates almost exactly like the infrared obstacle sensor. The
only difference is that the emitter/receiver have been relocated from the front to the
bottom of the board. Since the line sensor is always close to the surfaces it’s detecting,
a plastic shroud is installed around and between the emitter and receiver, to limit
emission and detection to just the ends of those components, enhancing accuracy:
The line sensor operates using the infrared properties of different colored objects. Black
objects absorb most of the infrared waves they receive, so they have a low level of
infrared reflectance. White objects reflect almost all the infrared energy they receive, so
they have a high level of infrared reflectance. As colors vary between white and black,
so will their infrared reflectance, lighter colors having higher reflectance, and darker
having lower.
The sensor uses this difference in reflectance to determine if it’s positioned above a
black or white surface. The 5V output pin is then updated to match the color that is
currently being sensed, with low or 0V indicating black and high or 5V indicating white. If
no object is close enough to reflect the infrared, the output will also stay low.
Using two sensors is the most cost-effective way to make a robot reliably follow a line
but continuing to add sensors can enhance the robot’s ability to follow a line quickly.
Some line sensing packages, like the one pictured below made by SunFounder, have
as many 8 infrared sensors:
This can greatly increase your robot’s ability to know the exact position of the line,
however your program will have to be much more complex to process and act on the
data from all 8 sensors. To keep your programming as simple as possible, you will only
be using two line sensors on the robot in Level D.
In this activity, you will install and connect the level shifting IC, connect its output to a
GPIO input, and build an RGB LED circuit. You will also build a small program to test
everything out before adding the IR sensors in Activities #2 and #3.
STEP #1
The circuit for this lesson will be made of almost entirely different components from
previous lessons. The first step will be to ensure the Raspberry Pi is powered down so
you can remove any components from previous lessons. Remove all components from
the breadboard except for the wedge and ribbon cable. When completed your
breadboard should look like this:
D24 to F24
D26 to F26
D27 to F27
These resistors must be very close together due to the length of the legs of the RGB
LED. Inspect the legs of each resistor closely to ensure no legs are touching any others
before proceeding to the next step.
During this step you will also install the ground connection for the RGB LED. Insert a
short jumper wire between breadboard points N1-25 and F25:
With pin 1 located at E32, insert the 74LVC245 across the center of the breadboard in
rows 32 through 41.
Install four short jumper wires between the following locations to supply power and
ground to the IC:
The IC is now connected to power and ground and it has been configured to accept 5V
inputs on the A side and output 3.3V signals on the B side.
The input to the level shifter will now be held high by a direct connection to the 5V
power rail. When disconnected the pull-down resistor will take over and the input will be
grounded.
rgb = [13,19,26]
red = 13
green = 19
blue = 26
GPIO.setmode(GPIO.BCM)
GPIO.setup(rgb, GPIO.OUT)
GPIO.setup(21, GPIO.IN)
try:
while True:
if GPIO.input(21) == False:
GPIO.output(green, GPIO.LOW)
GPIO.output(red, GPIO.HIGH)
else:
GPIO.output(red, GPIO.LOW)
GPIO.output(green, GPIO.HIGH)
time.sleep(.1)
except KeyboardInterrupt:
GPIO.cleanup()
There are no new concepts above, but here is an overview of everything happening in
the program in case you have any questions:
The RPi.GPIO and time modules are imported. A variable called rgb is created that
contains all output pins so they can be setup using only one line. The variables red,
green, and blue are set equal to the GPIO pin numbers that control each of those
colors in the LED. Color names can then be used throughout the program instead of
using the GPIO pins numbers.
The try loop uses a while True: loop to keep checking the input over and over. If the
input on GPIO21 is low or False, then the green element will turn off and red will
illuminate. The else condition will trigger if GPIO21 is high or True during a check. In that
case the red element will turn off and the green element will turn on. The time.sleep
command is added to slow the loop checks down to avoid unnecessarily checking of the
GPIO which can lead to unnecessary load, and heat, on the processor of the Raspberry
Pi.
STEP #10
Run the program. The LED will initially be green to indicate that GPIO21 is high or 3.3V
coming from the level shifter IC.
Carefully disconnect the jumper wire at location P2-37 and the input will go low,
indicated by the LED turning red.
Reconnect the jumper wire to P2-37 and the LED will change back to green to indicate
a high is present on the input.
Remove and reinstall the connection at P2-37 a few more times to ensure the program
and circuit are acting as expected. If so, feel free to proceed to the next activity where
you will reconfigure this circuit to get 5V input from an IR Obstacle sensor.
If the circuit or program is not behaving as outlined above, shut down the Pi and
recheck all wiring and the IC's pin 1 orientation. If everything looks good on the circuitry,
then proceed to reboot the Pi and double-check your program. Do not continue to the
next activity until this program and circuit are behaving as expected.
STEP #1
Save your program and shut down your Raspberry Pi so modifications can be made to
the circuitry on your breadboard.
STEP #2
The circuitry on the IR Obstacle Sensor will be taking care of sending both high and low
signals to the level shifter so the 10K-Ohm pull down resistor on channel 1 is no longer
required. Remove the 10K-Ohm resistor between N1-33 and B33. Relocate one end of
the channel 1 input jumper wire from P2-37 over to B48. This will be used to connect to
the output of the new sensor.
The obstacle sensor will need 5V power and ground connections. For ground, add a
short jumper wire between N1-49 and A49. To supply 5V, connect a long jumper wire
between C50 and P2-43. Your breadboard will now look like this:
The pins on the sensor are labeled OUT, GND, and VCC, from left to right. You will
notice that the previous step had you connect rows 48, 49, and 50 so those signals are
available for the sensor
Check the markings directly above the pins on the sensor, and insert the sensor into the
breadboard in column E so that:
OUT is in E48
GND is in E49
VCC is in E50
• If no obstacle is detected, the sensor output will be low, and the LED will remain
green
• If an obstacle is detected, the sensor output will switch to high, and the LED will
turn red.
Simulate an object by lowering a piece of paper in front of the sensor. At some point
around 3 to 5 cm your object will be detected by the sensor, and the LED will change
state. Your hand may work to trigger the sensor, but the human body is not a very good
reflector of infrared waves, so your hand may need to be even closer to trigger the
sensor.
STEP #5
Power down your Pi in preparation for the next activity where you will add the Infrared
line sensor to the circuit.
STEP #1
With your Pi powered off, add the connections that will be required for the Infrared Line
sensor. Add the following jumper wires to your circuit:
OUT is in E60
VCC is in E61
GND is in E62
This will mean that the LEDs on the line sensor are facing away from you when viewed
with column A closest to you. This is the correct orientation to match the wiring above.
STEP #4
Power on your Raspberry Pi and open the program from Activity #2. You will make
modifications to this program to add the input from GPIO20 as well as program some
additional LED behavior based on this input.
Setup GPIO20 as an input just below the setup line for GPIO21. The additional line is
highlighted below:
GPIO.setup(21, GPIO.IN)
GPIO.setup(20, GPIO.IN)
try:
Add an elif statement below the if statement that will check to see if GPIO20 is low or
False. If so, that block will turn off the green and red elements and turn on the blue
element. Turning off the blue element will also be added to the if block. The code
additions are highlighted below:
try:
while True:
if GPIO.input(21) == False:
GPIO.output(green, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
GPIO.output(red, GPIO.HIGH)
GPIO.output(green, GPIO.LOW)
GPIO.output(red, GPIO.LOW)
GPIO.output(blue, GPIO.HIGH)
else:
GPIO.output(red, GPIO.LOW)
Add a line of code to the else statement that will turn off the blue LED. The addition is
highlighted below:
else:
GPIO.output(red, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
GPIO.output(green, GPIO.HIGH)
time.sleep(.1)
rgb = [13,19,26]
red = 13
green = 19
blue = 26
GPIO.setmode(GPIO.BCM)
GPIO.setup(rgb, GPIO.OUT)
GPIO.setup(21, GPIO.IN)
GPIO.setup(20, GPIO.IN)
try:
while True:
if GPIO.input(21) == False:
GPIO.output(green, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
GPIO.output(red, GPIO.HIGH)
elif GPIO.input(20) == False:
GPIO.output(green, GPIO.LOW)
GPIO.output(red, GPIO.LOW)
GPIO.output(blue, GPIO.HIGH)
else:
GPIO.output(red, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
GPIO.output(green, GPIO.HIGH)
time.sleep(.1)
except KeyboardInterrupt:
GPIO.cleanup()
You can now see how obstacle and line sensors can help a robot, and its program,
interact with the environment around it. Additional obstacle and line sensors can be
added to further enhance this capability.
2. Will an Infrared Obstacle Sensor tell you how far away an object is from the
sensor?
3. Can line sensors be used to make a robot follow a line without human
intervention?
2. Will an Infrared Obstacle Sensor tell you how far away an object is from the
sensor?
ANSWER: No. The obstacle sensor will only tell you once an object appears in
it field of range.
3. Can line sensors be used to make a robot follow a line without human
intervention?
ANSWER: The input from line sensors can allow a program make a robot follow
a line without human intervention.
In the next lesson, you will learn to work with an ultrasonic range sensor to detect
objects and measure the distance to an object.
OBJECTIVE
In this lesson you will work with ultrasonic range finding sensors and learn to
incorporate them into your programs.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
ULTRASONIC RANGEFINDERS
An ultrasonic rangefinder is made up of an ultrasonic emitter, and ultrasonic receiver,
and a small amount of onboard circuitry that is used to drive the emitter and receiver.
The SR04 rangefinder in your kit is a 5 Volt device meaning it requires 5 volts to operate
and its output will be a 5V signal.
Just like the infrared sensors from the previous lesson, the output of this sensor is not
safe to connect directly to your Raspberry Pi. A level shifter must be used on the output
of the SR04 to make it safe for your GPIO pins. The input or trigger pin on the SR04 can
be safely operated by 3.3V, so this pin is safe to connect directly to a GPIO pin.
Sound waves that are audible to the human ear generally fall in the range of 20Hz to
20kHz. Ultrasonic signals are above the 20kHz audible range meaning that, while it's
not possible for us to hear signals in this range, they can be generated and received by
electronic equipment. Most of the signals that you interact with on a daily basis like
mobile phones, garage door openers, and microwave ovens, operate in the ultrasonic
range or above 20kHz.
These signals travel at a very specific speed in air, which is around 343 meters per
second, or 34,300 centimeters per second. This means that if you know the exact
amount of time that a signal took to fly to, bounce off, and fly back from an object, you
can calculate its distance from the Ultrasonic sensor
The rangefinder is sent a trigger signal from your program that tells it to send a very
short ultrasonic pulse of 40kHz out of its emitter. Your program will then start a timer.
When the rangefinder receives a 40kHz signal reflected back by an object, it will switch
its echo pin high to indicate this activity. Your program will use the state of the echo pin
to determine what the counter should do:
Echo pin low – no signal has been received so keep resetting the starting counter value.
Echo pin high – a signal has been detected so keep updating the ending counter value.
Keep in mind that we're not talking about a lot of time elapsing here. For a distance of
around 200 centimeters you're only looking at around 0.01136 seconds of flight time.
This means that the signal was emitted, flew to a surface, was reflected, and flew back,
all in 0.01136 seconds.
You now know the total distance to the object and back, but it would be a little more
helpful to know just the distance to the object. You can determine this by cutting this
value in half:
You have now determined that the object that the ultrasonic rangefinder detected was
194.8 centimeters away from the sensor. Here are some additional flight times with
distance calculations:
if nothing is detected:
keep driving
Using the variable output from a rangefinder as input, the additional controls in a
program like this can allow your robot to be much more successful at avoiding
obstacles.
61.3
61.2
19.8
61.3
61.3
The distance to the object is around 61.3 cm but the sensor received an odd reflection
that accounted for the 19.8 cm reading. This wouldn't normally be too big of a problem,
but your code may tell the robot to stop anytime the distance is less than 20 cm. This
one bad reading could cause your robot to stop even though it's not actually near an
obstacle.
There are many ways to handle these types of distance anomalies in your program.
One way is to calculate the average of the last few ranges to smooth out the distance
data that the robot uses for it's motion decisions. In the next section you will learn to use
a module called NumPy for these calculations.
One of the most simple, but very useful, functions built into NumPy is the mean or
averaging function. Say that you want to average out five distances from the
rangefinder. Normally you would have to add up all five ranges and divide by five to
obtain the average:
average = total / 5
This will take each value in the ranges list and add them together, setting that sum
equal to a variable named total. A variable named average is created and set equal
to total divided by 5. While this does work to obtain an average, there is a much
simpler way using the mean function of NumPy:
numpy.mean(list_to_average)
import numpy
ranges = [61.3, 61.2, 19.8, 61.3, 61.3]
average = numpy.mean(ranges)
This method works and does not rely on the ranges list having exactly five values. The
ranges list could have 25 values and the numpy.mean would average all values present,
without changing any code. This is not true for the manual addition/division method in
the first example.
Using this averaging feature of NumPy can allow you to obtain more reliable distances
from your rangefinding sensor.
An integer can only represent whole numbers that are positive, negative or zero. Some
examples of integers would be 4, 61, -17, 0, 142, -1653. Integers are good, but they
start to get limited when performing mathematical operations like division.
The result of dividing 7 by 2 is 3.5 which is no longer a whole number. If you try to print
the integer value of 7/2, you will print only the whole number, which is 3 and is not very
accurate. The arrows below are used to show the output of running the code:
x = 7 / 2
print(int(x))
>>> 3
This code will print that 7 divided by 2 is 3 which is not exactly correct. By specifying the
float value of x you can accurately print that 7 divided by 2 is 3.5:
x = 7 / 2
print(float(x))
>>> 3.5
Floats can be used to represent any number of decimal places, but they will always
include at least one decimal place. Dividing 6 by 2 will result in a float value of 3.0:
x = 6 / 2
print(float(x))
>>> 3.0
The %s specifies that a string-type variable will be dropped into the statement, and the
string that will replace %s is specified as an argument after the %, which is 'Your name'.
You can also load the name from elsewhere within the program by specifying a variable
name as the argument instead:
In this case, %s specifies that the string value of your_name will be printed into this
statement.
Be careful with integers and strings in this case. An integer value of 14 for your_age will
allow the statement to print correctly. A string value of '14' (notice the quotation marks
turning it into a string value) will cause this print statement to generate an error,
because it's looking for an integer value and it found a string instead.
One really nice thing about % notation when working with %f or floats, is the ability to
specify how many decimal locations you want to print. When dealing with times related
to distance readings, there are a lot of decimal points involved. Without specifying a limit
on the decimal places when printing, you would end up with distances like:
22.597869
23.601007
23.592830
23.599645
23.586015
print('%.1f' % distance)
22.6
23.6
23.6
23.6
23.6
These shortened values will make it much easier to detect changes, even when
watching three or four messages scroll by per second.
STEP #1
The first step will be to remove the IR obstacle and line sensors from the circuit you built
in Lesson B-13. You will leave the 5V to 3.3V level shifter in place as it will be used to
level-shift the Echo output line from the Ultrasonic Sensor.
Remove the IR line sensor and its associated wiring. Also remove the jumper wire
connecting CH1 from the level shifter to GPIO20 as CH1 will not be used in this lesson.
The circuit should look like this when complete:
Remove the IR obstacle sensor but leave its jumper wires in place as they will be
relocated in the next step to connect to the ultrasonic sensor. When completed, your
circuit will look like this:
Add long jumper wire – Input (Trigger) – Add jumper between I19 and F52
The circuit will look like this after your relocations and addition:
J50 – Ground
J51 – Echo
J52 – Trig
J53 – Vcc
If your signals match up, then power up your Raspberry Pi and proceed to the next
lesson where you will create a program that will read ranges from the sensor.
STEP #1
The first step will be to open Thonny and create a new program. Save this program to
the Desktop as ultrasonic.py so it can be re-used in upcoming activities.
STEP #2
You will now import the time and RPi.GPIO modules, and create two variables. Create a
variable called trigger that is equal to 20 and another variable called echo that is
equal to 21:
import time
import RPi.GPIO as GPIO
trigger = 20
echo = 21
Pins GPIO pins 20 and 21 can now be referred to as trigger and echo. This will make it
much easier to follow the program flow as you're writing code.
trigger = 20
echo = 21
GPIO.setmode(GPIO.BCM)
GPIO.setup(trigger,GPIO.OUT)
GPIO.setup(echo,GPIO.IN)
STEP #4
The next part of the program will be a function called range_check() that will be called
any time your program wants the range from the sensor. Here is the basic overview of
what range_check() will do:
This may look like a lot, but most of these items are only one line of code. The first step
in creating the function will be to define it using the name range_check():
GPIO.setup(echo,GPIO.IN)
def range_check():
def range_check():
GPIO.output(trigger, True)
time.sleep(0.00001)
GPIO.output(trigger, False)
start_timer = time.time()
That block of code has sent out the trigger pulse and you will now have to wait for the
echo to return. You will use a while loop to continually reset the value of start_timer
while you’re waiting for the reflected ultrasonic signals. A second while loop will be
used to update the value of stop_timer while the reflected signals are being received:
def range_check():
GPIO.output(trigger, True)
time.sleep(0.00001)
GPIO.output(trigger, False)
Now that the start and stop times have been captured, the only thing left to complete
this function is to calculate elapsed_time by subtracting start_time from stop_time,
calculating the distance in centimeters using your formula of (time * 34300)/2, and
then return that calculated distance back to the main program. Add this block below the
while loops:
elapsed_time = stop_timer‐start_timer
distance = (elapsed_time * 34300)/2
return distance
def range_check():
GPIO.output(trigger, True)
time.sleep(0.00001)
GPIO.output(trigger, False)
elapsed_time = stop_timer‐start_timer
distance = (elapsed_time * 34300)/2
return distance
STEP #5
Now that the range measurement and calculation function is complete, you can build
the rest of the main program. You will use a try/except format for the main program so
you can clean up the GPIO pins in case of a keyboard interrupt or pressing of the stop
button in Thonny.
Inside the try: loop you will use a while True: loop to keep checking range until the
program ends. A variable called distance will be set equal to the value returned from
running the function range_check(). Next, you will print the distance value trimmed
down to 1 decimal place using % notation with the print command. Last, you will add a
.25 second delay to slow down the main loop so it only runs 4 times per second:
try:
while True:
distance = range_check()
print('%.1f' % distance)
time.sleep(.25)
print('%.1f' % distance)
time.sleep(.25)
except KeyboardInterrupt:
GPIO.cleanup()
You now have a complete program for displaying the ranges from an ultrasonic sensor.
Here is the fully assembled program:
import time
import RPi.GPIO as GPIO
trigger = 20
echo = 21
GPIO.setmode(GPIO.BCM)
GPIO.setup(trigger,GPIO.OUT)
GPIO.setup(echo,GPIO.IN)
def range_check():
GPIO.output(trigger, True)
time.sleep(0.00001)
GPIO.output(trigger, False)
elapsed_time = stop_timer‐start_timer
distance = (elapsed_time * 34300)/2
return distance
try:
while True:
distance = range_check()
print('%.1f' % distance)
time.sleep(.25)
except KeyboardInterrupt:
GPIO.cleanup()
STEP #7
Run the program. Move different objects in front of the sensor and watch the distance
reading change as they move. If your program generates errors, double check your
wiring to the sensor and verify your program matches the example above exactly,
including indentation. This is a fairly long program and any variances or typos will cause
it not to function properly.
Do not proceed to the next activity until your circuit and program are reliably displaying
ranges.
STEP #1
For the averaging in this program, you will use NumPy. Using the program from the last
activity as a starting point, import numpy just below the GPIO import. The additional line
has been highlighted below:
import time
import RPi.GPIO as GPIO
import numpy
trigger = 20
echo = 21
STEP #2
You will need a list to store the range distances before they are averaged. Create an
empty list called ranges below the echo and trigger pin assignments:
trigger = 20
echo = 21
ranges = []
GPIO.setmode(GPIO.BCM)
1. Call the range_check() function three times and append result to the ranges list
2. Delay 0.06 seconds between loops to allow sensor to settle between checks
3. Set distance equal to the mean (average) of the values in the ranges list
4. Clear all values from ranges list so no values are present during next run
5. Return the value of distance back to the main program
Add this block of code just below the end of the range_check() function:
def average():
for i in range(0,3):
ranges.append(range_check())
time.sleep(0.06)
distance = numpy.mean(ranges)
ranges.clear()
return distance
try:
Ensure the indentation matches above. You only want the for i in range loop to check
the range three times and add those to ranges. The average, clear, and return lines
should only run once, each time the average() function is called.
try:
while True:
distance = average()
print('%.1f' % distance)
time.sleep(.25)
STEP #5
Run the new program to verify that it prints range information to the console as
expected. Here is the entire program for reference:
import time
import RPi.GPIO as GPIO
import numpy
trigger = 20
echo = 21
ranges = []
GPIO.setmode(GPIO.BCM)
GPIO.setup(trigger,GPIO.OUT)
GPIO.setup(echo,GPIO.IN)
def range_check():
GPIO.output(trigger, True)
time.sleep(0.00001)
GPIO.output(trigger, False)
def average():
for i in range(0,3):
ranges.append(range_check())
time.sleep(0.06)
distance = numpy.mean(ranges)
ranges.clear()
return distance
try:
while True:
distance = average()
print('%.1f' % distance)
time.sleep(.25)
except KeyboardInterrupt:
GPIO.cleanup()
Do not continue to the next activity until this program is working properly. If it is not,
verify all highlighted additions and changes were made correctly.
STEP #1
To use the three GPIO pins connected to the RGB LED, they will have to be set up as
outputs at the beginning of the program. In the interest of shortening the code and
making it as easy to read as possible, create a list called rgb that contains a list of the
RGB pin numbers, as well as set each color name equal to its GPIO pin number. This
will make them easier to keep track of later in the program.
Add these elements just below the module imports at the top of the program:
import numpy
rgb = [13,19,26]
red = 13
green = 19
blue = 26
trigger = 20
Now all three pins can be configured as outputs using the list called rgb, and the each
RGB element can be referred to by its color instead of having to remember its pin
number.
GPIO.setmode(GPIO.BCM)
GPIO.setup(trigger,GPIO.OUT)
GPIO.setup(echo,GPIO.IN)
GPIO.setup(rgb,GPIO.OUT)
def range_check():
STEP #3
Next, you will add an if/elif/else block to the main program that will turn the RGB LED
elements on (1) or off (0) based on the current distance value. Just below the print
statement in the main program block, add an if statement that checks to see if
distance is less than 20, and if so, turn the red element on and the others off:
while True:
distance = average()
print('%.1f' % distance)
if distance < 20:
GPIO.output(red, GPIO.HIGH)
GPIO.output(green, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
time.sleep(.25)
STEP #5
The final step will be to add an else statement that catches any distance greater than
25cm. Since this is an else statement it does not have a condition attached as it runs
automatically in the case that neither of the previous if or elif statements evaluated
as True. Add an else statement that turns the blue element on and the others off:
This program will be reused in Lesson B-18, so ensure the updated program has been
saved to the Desktop as ultrasonic.py.
2. Does NumPy require a specific number of values in a list, or can you use it on
any list of numeric values?
3. Does the ultrasonic sensor supply range information directly, or does it simply
send triggers and echoes, and your program must calculate the time and
distance from the signals received?
2. Does NumPy require a specific number of values in a list, or can you use it on
quantity of numeric values?
ANSWER: The NumPy module can use any quantity of numeric values.
3. Does the ultrasonic sensor supply range information directly, or does it simply
return echoes, and your program must calculate the time and distance from the
signals received?
ANSWER: The Ultrasonic Range Sensor will only return echoes. Your code
must calculate the time and distance based on the signals that are received.
In the next lesson, you will learn a new communication method called I2C and how to
integrate a temperature sensor into your programs.
OBJECTIVE
In this lesson you will learn about a communication method called I2C and how to use a
temperature sensor.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
I2C COMMUNICATION
In I2C, many devices can be connected on one bus by using different addresses to
identify each sensor:
I2C devices generally consist of a connection to power, ground, a clock signal, and a
data bus. The clock signal is generated by the Raspberry Pi and is used to synchronize
communications between the Pi and all I2C connected devices. A single data bus line is
used to convey data between all connected devices.
0x76,39.2
0x77,6.8
0x77,6.9
0x76,39.1
0x77,6.8
Since the sensors share the same data bus, they must identify themselves by attaching
their address to the beginning of any message they send out. This way the Raspberry
Pi knows that any data beginning with 0x76 is coming from the refrigerator and any data
beginning with 0x77 is coming from the freezer.
Your program will need to be programmed to identify each of these I2C addresses. This
may take the form of something like the following near the top of the program:
fridge = 0x76
freezer = 0x77
Variables like this can be used to set the I2C address for sensors throughout your
program. The variables fridge and freezer can then be referenced without needing to
remember the exact I2C address of each device. Your program can then display or
make decisions based on the values coming from each of these sensors.
Some devices do not allow their I2C address to be modified. This is the case for the
BMP280 Temperature/Pressure sensor in your Level B kit. The address for this sensor
is 0x76 and cannot be changed. This means that only one of these devices can be
connected to your I2C bus. If more than one BMP280 sensor was connected, they
would both use 0X76 to communicate, and you would not be able to determine which
sensor a temperature reading came from.
If you wanted to use two of these devices on the same I2C bus, you would need to de-
solder the zero-ohm jumper that is connected between the center and the 0x78
connection, and re-solder it between the center and the 0x7A position. You would then
be able to connect both displays to your I2C bus, accessing one using the 0x78 address
and the other using the 0x7A address.
Select the Enabled radio button next to I2C, click OK, and then reboot the Raspberry Pi.
Once this process is completed, GPIO2 (pin 3) and GPIO3 (pin 5) will be devoted to I2C
communication, and they can no longer be used as standard GPIO pins. Pin 3 will be
configured as the SDA or Serial Data line and pin 5 will be configured as SCL or the
Serial Clock line. You will notice these SDA and SCL designations match the markings
on the wedge for pins 3 and 5:
https://learn.adafruit.com/i2c-addresses/the-list
This list does not contain every single I2C device that's available, but if you're looking
for an I2C device to perform a specific function on your Raspberry Pi, then this list is a
good place to start.
You can use a command called i2cdetect at the command line to display a list of all
connected I2C devices. The i2cdetect command needs a couple of additional
arguments specified when you run the command:
i2cdetect -y 1
The -y argument will automatically answer yes to the Continue? [Y,n] question that
is normally presented when running this command. The 1 specifies which I2C bus
inside the Raspberry Pi you would like to probe. The Raspberry Pi only has a single I2C
bus that is named 1, but this argument is still required when running this command. The
output of this command will look like this when a device is connected to the I2C bus and
is communicating on address 0x76:
The BMP280 can output temperature and barometric pressure data, but not humidity.
Another IC in this same family called the BME280 can output temperature, barometric
pressure, and humidity. When researching information about the BMP280 you may see
complaints that the humidity output is not working, but this is by design. A BME280 must
be used if you would like to use humidity measurements for a project.
Communicating with these devices is very complex, so a driver file is used to simplify
communication. These two ICs are very similar in design, so similar in fact that the
same driver file can be used to communicate with either the BME or BMP versions of
this IC. Matt Hawkins from Raspberrypi-spy.co.uk built a very nice module that
combines everything you need to communicate with these sensors into a single file.
The 3.3V and GND lines can be connected directly to the power rails of the Raspberry
Pi. SCL connects to GPIO pin 5 and SDA connects to GPIO pin 3. As soon as these
connections are made this device will begin sending temperature and barometric
pressure measurements out via I2C.
BMP280 COMMANDS
Since the bme280.py module was written for the BME280, its commands are centered
around this version of the sensor, but it will also communicate well with the BMP280.
The only difference is that when the BMP280 is asked for a humidity reading, it returns
0 instead of the actual humidity reading that a BME280 would return.
The main command to obtain temperature from a BMP280 using this module is:
temperature,pressure,humidity = bme280.readBME280All()
This command will access the module named bme280.py and execute the function
named readBME280All(). This function will return three values back to your program.
The first value returned will be assigned to the variable temperature, the second value
returned will be assigned to pressure, and the third will be assigned to humidity.
Different variable names could be used in your program to represent these values:
or
t,p,h = bme280.readBME280All()
The order that values are returned by the module cannot be changed. It will always
return three values in the order of temperature first, then pressure, and humidity last.
You can then use the values stored in your variables to run calculations, print, or
anything else you would like to do with the values now that they're in your program.
TEMPERATURE CONVERSION
The temperature values returned by the BMP280 will be in Centigrade or C format. If
you would like to convert the value to Fahrenheit once it’s in your program, you can use
the following calculation:
This will take the current Centigrade value stored in temperature, multiply it by 9/5 (or
1.8), and then add 32. This result updates the value of temperature and represents the
Fahrenheit conversion of the Centigrade value reported by the BMP280.
wget https://42electronics.com/switches.png
When typed at the command line, this will download a copy of an image named
switches.png from the 42 Electronics website and save it into your current directory.
Most of these modules cannot be run as a program directly. They do not contain any
kind of main program or loop that will run when executed directly. Some modules, like
bme280.py have the added benefit of being able to be imported into your program, as
well as executed directly.
You can tell if a program offers this executable functionality by the presence of this
block of code:
if __name__=="__main__":
This code checks to see if the program is being directly executed. If the program was
executed directly, in this case by running bme280.py, then this block will evaluate as
True and the code contained in the if block will execute:
def other_things():
print('This program is running as an import')
def main():
print('This program is being executed directly')
if __name__=="__main__":
main()
def main():
(chip_id, chip_version) = readBME280ID()
print("Chip ID : %s" % chip_id)
print("Version : %s" % chip_version)
temperature,pressure,humidity = readBME280All()
print("Temperature : %s C" % temperature)
print("Pressure : %s hpa" % pressure)
print("Humidity : %s %%" % humidity)
This adds an extra level of functionality to a module like bme280.py. When first
connecting to a sensor like the BMP280, you can run this file directly and get some
output from the sensor to ensure it's communicating properly with the Raspberry Pi
using this file. Once that communication is confirmed you can then create a program by
importing this module, and calling the bme280.readBME280All() function from within
your own program.
This command is built into Python and can be used without any additional module
imports. SystemExit() can be used anywhere in your program where you might want
to end the program:
if continue == 'y'
keep_running()
else:
SystemExit()
If continue equals 'y' then the function named keep_running will run. If not, the
SystemExit() will be executed and the program will end.
This command will be used to create a graceful exit for the programs you build in
Activities #3 and #4 of this lesson. An except condition will catch the
KeyboardInterrupt created by pressing CTRL-C or the stop button in Thonny, and
SystemExit() will be used to end the program without errors.
In this activity you will enable the I2C interface on the Raspberry Pi. This interface must
be enabled to communicate with the BMP280 temperature sensor.
STEP #1
With the Raspberry Pi powered up, enter the configuration menu by clicking the
raspberry icon in the top-left corner and selecting Preferences and then Raspberry
Pi Configuration from the menu:
STEP #3
The Raspberry Pi must be rebooted before the I2C setting will take effect. Instead of
rebooting, shut down the Pi so you can safely make changes to the circuit on the
breadboard. After the Pi has completely shut down, proceed to the next activity.
STEP #1
The first step will be to remove the Ultrasonic Range Sensor and 5V to 3.3V level shifter
from the circuit you built in Lesson B-14. Remove these two components and any
jumper wires associated with them. Leave the jumper wire in place between 3.3V and
P1-3 as it will be used later to power the BMP280. With this step completed, your circuit
should look like this:
Open up a new Terminal window and create a new folder called temp_sensor:
mkdir temp_sensor
Next, move into that directory so you can get ready to download the driver file:
cd temp_sensor
STEP #5
You should now be located in /home/pi/temp_sensor and you're ready to download the
bme280.py driver file from our GitHub repository using the wget command. Enter the
following command and the file will be downloaded to your current directory:
wget https://raw.githubusercontent.com/42electronics/level_b/master/bme280.py
Ensure you type the web address exactly as listed above or the download will fail. You
will see the file download progress and confirmation that 'bme280.py' was saved.
Confirm the file is present in your directory by using the ls command to list the contents
of your directory:
i2cdetect -y 1
This will print a list of all I2C devices that the Raspberry Pi sees and your BMP280
should be listed at address 0x76:
If your sensor is not listed, power down the Raspberry Pi and double-check your wiring.
Do not proceed to the next activity until your sensor is reporting in at 0x76 as seen in
the image above.
STEP #1
The driver file can only be accessed by programs within the same folder, so your new
program must also reside in the /home/pi/temp_sensor directory. Open Thonny and
click the green plus to create a new file. Click File and Save As to save the new file.
Ensure the directory is /home/pi/temp_sensor so your program will be in the same
folder as bme280.py:
STEP #2
Now that you've saved your new file in the /home/pi/temp_sensor directory, you’re ready
to start building the program. The first section of the program will need to import the
bme280 and time modules:
import bme280
import time
import bme280
import time
try:
except KeyboardInterrupt:
SystemExit()
Remember to indent the SystemExit() command so it belongs to the except
condition.
STEP #4
The program now needs a main loop so you can check the temperature repeatedly. Add
a while True: loop that will pull in readings from the BMP280 using the bme280.py
driver file:
import bme280
import time
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
except KeyboardInterrupt:
SystemExit()
NOTE: Even though the BMP280 included with your kit will not output humidity
data, the driver file will still send a 0 for this value. Failing to assign this extra
value in your program will result in errors when the program receives sensor
information from bme280.py.
import bme280
import time
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
except KeyboardInterrupt:
SystemExit()
The value stored as temperature will be multiplied by 9/5 and then added to 32. This
new value will then overwrite the old value of temperature.
STEP #6
Now that the temperature has been converted to F, print it to the console. Using %
notation in the print statement, trim the printed value down to a float of two decimal
points by specifying %.2f inside the print statement.
You should also add a time delay of .3 seconds to avoid printing temperature values so
fast that you can't read them:
import bme280
import time
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
print('Temp= %.2f' %temperature)
time.sleep(.3)
except KeyboardInterrupt:
SystemExit()
Press CTRL-C or the stop button in Thonny to end the program. If the program does not
run as expected, confirm every line matches the program in Step #6. Ensure the
program is running as expected before proceeding to the next activity.
STEP #1
You will be making quite a few
additions to the program from the
last activity, so make a new copy
of temp_reading that you can
modify without affecting your
original program. With
temp_reading open , click File
and Save As. Name the new file
temp_led and click Save:
STEP #2
Now that you have a copy of temp_reading saved as temp_led you can start making
modifications to the temp_led program. The RGB LED is connected to three GPIO pins
so you will need to import the GPIO module:
import bme280
import time
import RPi.GPIO as GPIO
try:
while True:
import bme280
import time
import RPi.GPIO as GPIO
red = 13
green = 19
try:
Now you can use the values red and green to reference the pins instead of 13 and 19.
STEP #4
The next step will be to set the GPIO pin mode as BCM and configure GPIO13 and
GPIO19 as outputs. Instead of using the pin numbers for configuring the outputs you
can substitute the variables from the last step:
red = 13
green = 19
GPIO.setmode(GPIO.BCM)
GPIO.setup(red, GPIO.OUT)
GPIO.setup(green, GPIO.OUT)
try:
If the temperature is equal to or above 80 degrees, the green element will turn off and
the red element will turn on. The else will be True for any temperature below 80 and will
turn off the red element and turn on the green.
Insert this block of code between printing the temperature and the time.sleep
delay:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
print('Temp= %.2f' %temperature)
STEP #6
The last modification to the program will be adding a GPIO.cleanup() to the exception
that's caught when CTRL-C or stop in Thonny is pressed. This will keep the program
from generating errors when exiting, and this will also keep the LED from being left on
after the program exits.
except KeyboardInterrupt:
GPIO.cleanup()
SystemExit()
NOTE: You can carefully touch the metal case of the BMP280 IC to increase the
temperature and trigger the LED to change. Take caution to avoid touching the
electrical connections on the circuit board. While there are no voltages present
that could cause injury, introducing static electricity into the electrical connections
could damage the BMP280 IC.
import bme280
import RPi.GPIO as GPIO
import time
red = 13
green = 19
GPIO.setmode(GPIO.BCM)
GPIO.setup(red, GPIO.OUT)
GPIO.setup(green, GPIO.OUT)
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
print('Temp= %.2f' %temperature)
except KeyboardInterrupt:
GPIO.cleanup()
SystemExit()
3. What does the following code mean when you find it in a module file?
if __name__=="__main__":
ANSWER: I2C devices are identified by unique addresses on the I2C bus such
as 0x3C or 0x78.
3. What does the following code mean when you find it in a module file?
if __name__=="__main__":
ANSWER: This block of code checks to see if the program was executed
directly, and if so, the code contained in this if: block will execute. If the file was
imported as a module, this line will evaluate as False, and the code inside this
block will be skipped.
In the next lesson, you will learn to work with another I2C device, an OLED display.
OBJECTIVE
In this lesson you will learn to work with an OLED display to display information from the
program and including data gathered from external sensors.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
The display included in your kit is a 128x64 OLED I2C display. Here are the details:
The horizontal/vertical pixel count will often be referred to as the resolution of the
display, so this display would have a resolution of 128x64. This means that the images
or text it displays must be made up of no more than 128 pixels horizontally and 64
pixels vertically.
Screens that are meant to be viewed at close distances, like mobile phones, must have
fairly high PPI values to make text and images appear smooth. Devices like TVs will
have a much lower PPI value as they are meant to be viewed at much greater distances
than mobile phones.
You can calculate the PPI value of any device by knowing the dimensions of the display
and the number of pixels present. By dividing 128 pixels by .96 inches of screen, you
come up with a PPI of 133.3.
Understanding pixel size and location is important for building programs that will display
images or fonts. Displaying full screen images is fairly simple as they use every pixel on
the display, but when displaying smaller images and text, X and Y position information
must to provided to determine where it will be positioned on the display:
Y positions start at the top of the display and increase as you move down:
The position of any pixel can be specified by combining its X and Y values into the
expression (X,Y). The top left corner of the display is X = 0 and Y = 0 which are
expressed as (0,0). Here are all four corners of the display marked with their X and Y
coordinates:
This may seem very complicated, but you will often only need to specify the starting
pixel position for whatever you want to print, and the driver file for that display will take
care of the rest.
LCD displays work well for many devices, however some backlight modules can require
a fair amount of energy to run, making this type of display problematic for mobile use.
I2C COMMUNICATION
The OLED screen that comes with your Level B kit uses I2C for communication. This
means that using only 3.3V, GND, and two data lines, you can control every pixel on the
display. This is in contrast to other types of displays that may need 10 or more
connections to control even fewer pixels. Using less connections will make it easier to
integrate this display into more of your projects, while keeping required wiring to a
minimum.
You will go through the download and installation of the Adafruit driver package later in
Activity #1, but before that you will need to review some of the commands that will be
used in your program to communicate with the display.
REQUIRED MODULES
Communication with your OLED display from within a Python program requires the
following modules be imported:
import Adafruit_SSD1306
This will import the SSD1306 library from Adafruit that contains information for
communicating with the hardware inside the display.
The Adafruit library requires that the RPi.GPIO library be imported to communicate with
the GPIO pins connected to the display.
PIL stands for Python Imaging Library. It contains code that allows the manipulation of
images and text from within a Python program. In this case the functions Image,
ImageDraw, and ImageFont are being imported from the module named PIL so they
can be used to interact with the display.
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
This line will create a variable named disp that can send the Adafruit_SSD1306 driver
file the exact model of our display which is SSD1306_128_64. This configuration line
also contains the argument rst=None which lets the driver file know that your display
does not require a reset pin.
disp.begin()
This will send the configuration information to the Adafruit_SSD1306 driver file and turn
the display on using those configuration parameters.
width = disp.width
height = disp.height
The driver file was written to accommodate many display sizes which is the reason
these values are variable and being fed from the driver file. Since you are only using the
128x64 display you could also specify width = 128 and height = 64 directly in your
program without causing issues.
Instead of sending every command to the display individually, a display buffer is used to
store up display commands. It's much like printing a word processor file on your printer.
You can get the document exactly the way you want and then print it all at once. The
display buffer operates in the same way. You can fill it up with all the images and text
you would like to display, and once it's ready, you can send the entire contents to the
display, so it will look exactly the way you want.
Your program will create a value called image that will create an image buffer named
Image.new:
This image buffer will hold anything that you would like to send to the display. The
argument of 1 specifies that the image buffer will be 1-bit color, or black and white. The
width and height specify the dimensions of Image.new.
Another variable named draw will be used to send information to the image buffer:
draw = ImageDraw.Draw(image)
This variable will use a function named Draw in an imported module named ImageDraw
to draw images and text within the image buffer. The argument of image specifies that
Different fonts can be specified when printing text to the display. The Raspberry Pi has
a few fonts loaded into /usr/share/fonts/truetype that can be used in your projects. One
of the fonts available in this folder is called FreeSans:
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
This line of code will create a variable named font that will tell the ImageFont module
to use the truetype font named FreeSans.ttf located at the specified path. The
argument of 18 specifies the size of the font to be used, in pixels.
This command will draw a rectangle in the image buffer. The top corner of the rectangle
will start at the coordinate X=0,Y=0 and the values width and height specify the
dimensions of the box, in this case 128 wide by 64 tall. The outline value of 0 means the
rectangle will have no border outline, and the fill value of 0 means that the rectangle will
be black.
Drawing a black rectangle over the entire image buffer will essentially "erase" the entire
image buffer. This is generally done as a first step to clear the image buffer of content
from any previous commands.
This command will print the text Hello World!! in the upper-left corner of the image
buffer starting at pixel 0,0. The font of this text will be determined by the value of the
font variable that was specified earlier in the program. The color of the text will be
white due to the fill value of 255 which represents the 8-bit equivalent of white.
Once you have filled the image buffer with everything you want to display, the following
commands can be used to send the stored image buffer to the screen:
disp.image(image)
This command will load the image buffer stored as image and make it ready to be
displayed.
disp.display()
This command tells the display to show the image from the image buffer. The image on
the screen is only updated when this command runs. Exiting the program after
displaying an image will cause that image to continue displaying even after the program
has exited, until power is removed from the screen. If you wish to clear the display
during your program, or on program exit, you can use the following command:
disp.clear()
This will load a cleared image buffer into the screen, but this will not immediately clear
the screen. Another disp.display() command must be issued to update the screen
with the cleared image buffer. Together the commands to clear the screen would be:
disp.clear()
disp.display()
Using these prior to your program exiting will ensure that the screen is cleared of all
contents before the program exits.
Unicode characters can be specified by their Unicode designator. Here are just a few:
You can use these Unicode characters in your programs by prefacing the Unicode
designator with \u to let the program know the following four digits represent a Unicode
character. For example, here is the code to print Hello:
print('Hello')
If you would like to add a happy face after Hello, you can add \u263A after Hello:
print('Hello \u263A')
The \u263A will be treated as a single character in the print statement and will be
replaced with ☺ when the statement is printed to the console.
Different systems will have varying levels of compatibility with these Unicode
characters. Some programs will support the display of all characters, while others won't
display any of them.
When using a small display like the SSD1306, Unicode character support will depend
on the internal software that's running inside the display. The Raspberry Pi program can
Luckily, the SSD1306 is programmed to display the happy face and quite a few other
Unicode characters.
In this activity you will add the 128x64 OLED display and the slide switch to the circuit
you build in Lesson B-15. You will also download the Adafruit driver files as well as test
the I2C connection to the screen
STEP #1
The Raspberry Pi must be powered off to make changes to the breadboard circuit.
Power down the Raspberry Pi.
GND – C43
VCC – C44
SCL – C45
SDA – C46
Once the Raspberry Pi has fully booted up, open a Terminal window and enter the
command below:
STEP #6
The last step in the installation process for the SSD1306 module is to run the install
script. First, change into the directory by typing the following command:
cd Adafruit_Python_SSD1306
Now that you're in the directory containing the script, all that's left is to run the script
using:
This will make the module available in the Python 3 environment so the programs you
write can run from within Thonny.
i2cdetect -y 1
Both your screen and the temperature sensor should show up as devices connected to
the I2C bus. The temperature sensor will be the device at 0x3c and the screen will be
at 0x76:
Do not proceed to Activity #2 until both devices are reporting in properly when using
i2cdetect. If they are not detected, check wiring and jumper wire connections. If a
connection is bad, then a reboot of the Pi may be required to restore I2C
communication.
STEP #1
In this activity and the next, you will be building a program, and then make modifications
which will allow it to communicate with the temperature sensor. This means that your
second program will need to be in the same folder as the bme280.py driver file, or
inside the temp_sensor directory you created in Lesson B-15.
To eliminate confusion when making modifications to this program in Activity #3, you will
build the initial program in the temp_sensor directory. Your initial program will not need
access to the temperature sensor, but access to the bme280.py file will be required for
Activity #3.
Open Thonny and create a new program. Click File and then Save As to save your new
program. Navigate inside the /home/pi/temp_sensor directory and name your program
display_text:
import time
import Adafruit_SSD1306
import RPi.GPIO as GPIO
from PIL import Image, ImageDraw, ImageFont
STEP #3
Next, you need to let the Adafruit_SSD1306 library know what display type it will be
communicating with when the disp variable is used throughout the program. Configure
the display as a 128x64 display with no reset line needed, and initialize the display with
the disp.begin() command by adding the following lines:
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height))
draw = ImageDraw.Draw(image)
If you have questions about what any of these lines are used for, please refer back to
the subsection on Variables That Simplify Communication with the Display in this
lesson.
STEP #5
Next, you will select the font that will be used for text on the display by creating a
variable named font. When the font variable is used it will let the ImageFont function
from the PIL know that you want to use the FreeSans font at the specified path, in a
height of 18 pixels:
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
try:
except KeyboardInterrupt:
disp.clear()
disp.display()
SystemExit()
STEP #7
The main loop is now ready for some code. Add a while True: loop that will print a
few lines of text. Line 1 of the text will read 'Hello from" and line 2 will read "42
Electronics". Line 3 will be a row of five happy faces using the Unicode character 263A.
Each line will start at position X=0, but the Y value will change so each line is below the
last. Line 1 will use a Y position of 0, line 2 will use a Y position of 22, and line 3 will use
a Y position of 44. This will ensure the lines are properly spaced vertically.
Add a while True: loop inside the existing try: loop that contains the following text
lines:
try:
while True:
draw.text((0, 0), 'Hello from', font=font, fill=255)
draw.text((0, 22), '42 Electronics', font=font, fill=255)
draw.text((0, 44), '\u263A \u263A \u263A \u263A \u263A',
font=font, fill=255)
except KeyboardInterrupt:
NOTE: Although line 3 of the text is split into two lines above, this is due to
display limitations within this document. To execute properly, this will need to be
one continuous line of code in Thonny.
except KeyboardInterrupt:
import time
import Adafruit_SSD1306
import RPi.GPIO as GPIO
from PIL import Image, ImageDraw, ImageFont
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height)) # '1' converts image to 1-bit color
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
try:
while True:
draw.text((0, 0), 'Hello from', font=font, fill=255)
draw.text((0, 22), '42 Electronics', font=font, fill=255)
draw.text((0, 44), '\u263A \u263A \u263A \u263A \u263A',
font=font, fill=255)
disp.image(image)
disp.display()
except KeyboardInterrupt:
disp.clear()
disp.display()
SystemExit()
STEP #1
First, make a new copy of the display_text.py program that you created in the last
lesson. That way you will have the original if you want to run it again or make small
modifications to that program later.
With display_text.py open in Thonny, click File and then Save As. Enter the new
filename as display_info and click the Save button. This will keep the original
display_text.py file intact, while making a new copy called display_info that will contain
all the same code as the original file.
STEP #2
Now that you have a copy of the original file to work with, start adding the code to
display temperature and switch information. The bme280.py module will need to be
imported to communicate with the temperature sensor. Import it just below the rest of
the import block:
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
STEP #4
The setup items are now in place to communicate with the screen, the temperature
sensor, and the slide switch. It's now time to start replacing the lines of code from the
main program loop with new lines that will:
The first step in this process will be to remove everything from the while True: loop
except for two lines at the end that push the image buffer to the screen and display the
image. Those two lines will still be used at the end of our new loop.
When complete, your while True loop should look like this:
try:
while True:
disp.image(image)
disp.display()
except KeyboardInterrupt:
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
disp.image(image)
disp.display()
STEP #6
Since you will be refreshing live data on the screen, you don’t want old data left in the
image buffer interfering with new data. This would result in both old and new text being
displayed at the same time, making any updated areas of the screen unreadable.
You can draw a black rectangle over all the pixels in the image buffer every time the
main loop runs. This will clear out any old pixel data and make the image buffer ready
for new data.
Add the line of code below to draw a black rectangle the width and height of the screen
to clear the image buffer:
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
disp.image(image)
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
if GPIO.input(21) == True:
draw.text((0, 0), "Switch is OFF", font=font, fill=255)
else:
draw.text((0, 0), "Switch is ON", font=font, fill=255)
disp.image(image)
GPIO21 will be checked for it's True (High) or False (Low) status. If GPIO21 is High then
Switch is OFF will be displayed on the first line of the screen. If GPIO21 is not High
then Switch is ON will be displayed on the first line of the screen. The coordinates of
(0,0) will cause either of these lines to be positioned at the top-left of the screen.
Add the following line below the block of code displaying the switch information:
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
if GPIO.input(21) == True:
draw.text((0, 0), "Switch is OFF", font=font, fill=255)
else:
draw.text((0, 0), "Switch is ON", font=font, fill=255)
disp.image(image)
Add the if: block for the alert code just below the temperature code:
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
if GPIO.input(21) == True:
draw.text((0, 0), "Switch is OFF", font=font, fill=255)
else:
draw.text((0, 0), "Switch is ON", font=font, fill=255)
disp.image(image)
NOTE: You can carefully touch the metal case of the BMP280 IC to increase the
temperature and trigger the alert message for temperatures above 80F.
Take caution to avoid touching the electrical connections on the circuit board.
While there are no voltages present that could cause injury, however introducing
static electricity into the electrical connections could damage the BMP280 IC.
If your screen does not display as expected, double-check your program with the code
below:
import time
import Adafruit_SSD1306
import RPi.GPIO as GPIO
from PIL import Image, ImageDraw, ImageFont
import bme280
GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height)) # '1' converts image to 1-bit color
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
try:
while True:
temperature,pressure,humidity = bme280.readBME280All()
temperature = (temperature * 9/5) + 32
if GPIO.input(21) == True:
draw.text((0, 0), "Switch is OFF", font=font, fill=255)
else:
except KeyboardInterrupt:
disp.clear()
disp.display()
SystemExit()
2. In (64, 17), what is the value of X and Y? Approximately where is that coordinate
located on the 128x64 screen below:
3. What changes must be made to the program from Activity #3 to reorder the lines
on the screen to match the layout below?
Temp: 80.28
** ALERT **
Switch is OFF
ANSWER: Text drawn using the draw.text command is sent to the image
buffer.
2. In (64, 17), what is the value of X and Y? Approximately where is that coordinate
located on the 128x64 screen below:
ANSWER: X = 64, Y = 17. The exact position is noted in the image below.
Temp: 80.28
** ALERT **
Switch is OFF
ANSWER: While you could change the order of the lines of code in the
program, it's not necessary. The order the lines are written to the image buffer is
not important, only their Y position. By changing the Y position values of each
line, you can make the lines appear wherever you want on the screen.
if GPIO.input(21) == True:
draw.text((0, 44), "Switch is OFF", font=font, fill=255)
else:
draw.text((0, 44), "Switch is ON", font=font, fill=255)
The switch status will now be displayed at Y=44, the temperature reading at
Y=0, and the ALERT at Y=22.
In the next lesson, you will learn to work with capacitors and a capacitive touch sensor.
CAPACITORS AND
CAPACITIVE TOUCH SENSORS
OBJECTIVE
In this lesson you will learn to use capacitors and a capacitive touch sensor in a circuit.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
• Series and Parallel Circuits, Ohms Law, Forward Voltage (Lesson A-3)
CAPACITORS
Capacitors are used to quickly store and discharge energy into other components. The
amount of energy a capacitor can store is known as its capacitance. Capacitance is
measured in a unit called a Farad, but most of the capacitors you see in projects will be
measured in millionths of Farads or micro-Farads. The symbol used to indicate micro-
Farads is uF.
The capacitance value determines how much energy can be stored in a capacitor. A
higher value indicates that it can store more energy, so a 2200uF capacitor can store
around 10 times more energy than a 220uF capacitor.
Smaller capacitors are not often polarized so they can be used in a circuit in either
direction. Larger capacitors, known as electrolytic, are polarized and must be placed in
the circuit properly to avoid damage to components. The negative of an electrolytic
capacitor will be marked with minus signs and often a large gray band.
The same type of damage could occur during the discharge of a capacitor. If you
connect a full charged capacitor directly to a low GPIO pin, the grounded GPIO pin will
attempt to provide a current path for the charge inside the capacitor. If the current in or
out of a GPIO pin exceeds 16mA, then it could be permanently damaged.
Instead, a current limiting resistor should be placed in series with the capacitor. Using
the supply voltage and the value of the series resistor, you can then calculate the
maximum amount of current that will be allowed to flow in and out of that GPIO pin.
You calculated current using a series resistor with an LED back in Lesson A-3, but
calculating current in a series resistor attached to a capacitor is slightly different. Unlike
an LED, a capacitor will not create a voltage drop in a circuit, so it's Vforward is
Below, an LED drops 2.8V of the 3V supply, leaving only 0.2V to be dropped across the
resistor. Ohms law shows that a 220-Ohm resistor in this configuration will limit the LED
current to around 0.9 mA:
Here is the same circuit, but the LED has been replaced with a capacitor. Since the
capacitor has no Vforward value, or zero, the entire supply voltage will be consumed by
the resistor:
This results in much higher current running through the circuit despite both circuits using
identical supply voltage levels and current limiting resistors. This is why a 1K-Ohm
resistor will be used as a current limiter in the upcoming activities:
This will limit current flowing through the GPIO pin to 3.3mA, which is safely below the
limit of 16mA.
CHARGE TIME
Charge time is the amount of time that it takes a capacitor to fully charge. This value will
be affected by factors like the capacitance value and the voltage level being used to
charge the capacitor.
Assuming a charging voltage of 3.3V, a 2200uF capacitor will take longer to charge than
a smaller 220uF capacitor. The larger capacitor will take longer to charge, but once it's
charged, it can put out much more energy than the smaller capacitor.
A capacitor that is allowed to fully charge will reach very near the voltage it's being
charged with. A capacitor being charged by 3.3V will likely reach somewhere between
3.25 and 3.29V.
This means that it will take 2.2 seconds for a 2200uF capacitor to charge to 63% of the
supply voltage when in series with a 1K-Ohm resistor. To compute the time required to
fully charge the capacitor, this single RC value would be multiplied by 5:
A 2200uF capacitor with a series resistor of 1K-Ohms will take 11 seconds to fully
charge to the supply voltage. Any less time than this and the capacitor will not be fully
charged.
Calculate the full charge time (5RC) on a 220uF capacitor with a 1K-Ohm series
resistor:
You can see from this calculation that a 220uF will fully charge much faster than a
2200uF, in exactly 1/10th the time to be more specific.
While it might be nice to give the 2200uF capacitor a full 11 seconds to charge, as you
can see from the graph above, after about 6 seconds it has charged to 95% capacity.
95% capacity will likely be more than enough for any projects you plan to work on using
capacitors. The chart below shows a capacitor's charge level at each time constant:
Most of the charge is stored during the first couple of RC time constants. After that,
each additional RC time constant adds less and less charge. For this reason, in the
activities for this lesson, you will only allow the capacitor to charge for 6 seconds. The
RC value for a 2200uF/1K-Ohm pair is 2.2 seconds. Allowing the capacitor to charge for
6 seconds is just short of the 3RC value of 6.6 seconds, but this will be more than
enough for the purpose of the upcoming activities.
With resistors wired in series, their total value is all individual values added together:
R1 + R2 + R3 + … = RT
C1 + C2 + C3 + … = CT
The 2200uF will be removed from the circuit and its removal from the total capacitance
will result in a drastic change in the measured discharge time.
This model of CTS can run from 3.3V supplied by your Raspberry Pi. This means that
the output lines from the CTS will not exceed 3.3V, making them safe to connect directly
to GPIO pins on your Pi.
When 3.3V and ground are initially applied to the CTS, it will determine the baseline
capacitance of the four input pads. When you touch a pad after this initialization
process, your skin will change the capacitance that is measured at the pad, and the
output line for that pad will be pushed high.
The CTS uses the baseline measurement to determine when a pad is touched or not. If
you happened to be touching a pad when the CTS first turned on, your skin capacitance
would change the baseline value, and now the CTS can no longer determine if a pad
has been touched. To avoid this problem, ensure that you are not touching the pads
when the CTS is initially powered on.
There is not much to working with the CTS as an input for your programs. It handles the
capacitive touch sensing internally, so you end up with four outputs that switch between
high and low, just like any other switch input.
The capacitive touch sensor, for example, has pins coming out the top of the circuit
board. It would need to be turned over to be inserted into the breadboard, at which point
you could no longer see the signal names, or the touchpad numbers. The male-to-
female jumper wires included in your kit make it possible to connect the CTS to the
breadboard, while still allowing you to read the markings on the top of the circuit board.
As you have seen throughout various projects, GPIO inputs are usually high or low to
trigger different events in a program. Sometimes there are voltages that are slowly
transitioning from high to low, like a capacitor going from charged to discharged.
Connecting this capacitor to a GPIO input through a current limiting resistor, will allow
you to watch how slowly this high-to-low process occurs. The input will start out as a
high since the capacitor is charged, and then as the capacitor slowly discharges, the
GPIO pin will eventually register as a low. In the Raspberry Pi this happens right around
1.4V. Voltage levels above 1.4V will register as high, and anything below 1.4V will
register as low.
The sensing of this high-to-low point will be a key part of the program that you create in
Activity #2 that will time the discharge of capacitors from charged down to 1.4V.
With the jumper wire removed, the circuit no longer has a power source, and the LED
will turn off instantly. With it inserted, the LED circuit will receive power, and the LED will
turn back on.
Carefully remove the 3.3V jumper wire between P1-52 and H51. 3.3V will no longer be
supplied by the Raspberry Pi but the LED will remain on, temporarily running on the
energy stored in the capacitor. You can re-install and remove the jumper wire if you
would like to watch the charge/discharge cycle again.
STEP #6
Power down the Raspberry Pi so additional changes can be made to the circuit.
The circuit will now be modified so the capacitor circuit can be charged by a GPIO pin.
Remove the 1K-Ohm resistor and LED that are in in parallel with the capacitor. Keep
the 1K-Ohm resistor handy as you will be inserting it into a new location in the next step.
220uF capacitor – between H55 and H61. Ensure that negative of capacitor is inserted
into H61
Reinstall 2200uF capacitor between J55 and J61 (negative), after the components
above are installed.
STEP #9
Double-check all component placement and capacitor polarity with the diagram in Step
#5, and then power on the Raspberry Pi. Once the Raspberry Pi is fully booted,
continue to the next activity.
STEP #1
Open Thonny and save a new program to the Desktop called discharge_time. This
program will use the RPi.GPIO and time modules, as well as the BCM pin mode. You
will also use a variable named pin to hold the GPIO pin number that will be used to
measure your capacitors. Set these up at the beginning of the program:
GPIO.setmode(GPIO.BCM)
pin = 20
NOTE: By referring to pin instead of 20 throughout the rest of the program, you
can easily reconfigure this program to use a different pin later just by modifying
this one value. If you used 20 throughout the program, you would have to modify
every instance of this value if you wanted to reconfigure the program to use
another GPIO pin.
STEP #2
Next, use a try/except loop that will catch keyboard exceptions and run a
GPIO.cleanup. The try: loop should also contain a while True: loop so your main
block of code will continue looping. Add the following code to the end of your program:
GPIO.setmode(GPIO.BCM)
pin = 20
try:
while True:
except KeyboardInterrupt:
GPIO.cleanup()
try:
while True:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.HIGH)
time.sleep(3)
except KeyboardInterrupt:
STEP #4
The capacitors have been given time to charge from the 3.3V supplied by GPIO20. It's
now time to switch GPIO20 to an input, store the current time as time_start, and hold
the program in a while loop as long as GPIO20 is still high by using the pass command:
try:
while True:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.HIGH)
time.sleep(3)
GPIO.setup(pin, GPIO.IN)
time_start = time.time()
while GPIO.input(pin) == GPIO.HIGH:
pass
except KeyboardInterrupt:
This will cause the program to stay in the while loop until GPIO20 transitions to a low
state. The pass command is used as a placeholder and tells the program that you do
not wish for anything to happen inside this while loop. You're only using it to delay the
program right up until GPIO transitions low, and then the rest of the program will
execute.
try:
while True:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.HIGH)
time.sleep(3)
GPIO.setup(pin, GPIO.IN)
time_start = time.time()
while GPIO.input(pin) == GPIO.HIGH:
pass
time_stop = time.time()
timer = time_stop - time_start
except KeyboardInterrupt:
STEP #6
Now that the discharge time has been calculated and stored as timer, print the first 3
decimal places of that value using % formatting, along with the word seconds. Add the
print statement below to the end of your main loop:
try:
while True:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.HIGH)
time.sleep(3)
GPIO.setup(pin, GPIO.IN)
time_start = time.time()
while GPIO.input(pin) == GPIO.HIGH:
pass
time_stop = time.time()
timer = time_stop - time_start
print('%.3f seconds' % timer)
except KeyboardInterrupt:
You can drastically reduce the total capacitance by removing the 2200uF capacitor from
the circuit. With your program running and displaying discharge values, remove the
2200uF capacitor and watch what happens to the discharge time.
NOTE: The times reported by your circuit will not match these exactly due to
slight differences in AC power supply voltages and variations in capacitors.
The average values went from around 0.840 seconds with 2420uF of total capacitance
to around 0.084 seconds with only 220uF of total capacitance. This shows you how
much more energy storage capacity is added with the 2200uF capacitor:
With only 220uF of energy storage, the circuit discharged from 3.3V down to around
1.4V in 0.084 seconds. By adding the 2200uF capacitor and increasing the total
capacitance to 2420uF, it took 10x as long to discharge the capacitors from 3.3V down
to 1.4V.
STEP #1
Shut down the Pi and disconnect power before proceeding.
Once the shutdown is complete, remove the capacitor experimentation circuit from your
breadboard. This includes the long jumper wire connected to GPIO20. When
completed, your breadboard should look like this:
Power on your Raspberry Pi and open Thonny. Create a new program called
cap_touch.py and save it to the Desktop.
STEP #4
This program will interact with GPIO pins and will use a time.sleep delay, so the
RPi.GPIO and time modules will need to be imported.
STEP #5
Next, the GPIO pins connected to the touchpad will need to be configured as active-
high inputs (software pull-down required), and the GPIO pins connected to the RGB
LED will need to be configured as outputs.
To make this task easier, use some lists to hold the pin information so setup can be
completed in two lines instead of six. Use a variable named pads to hold [22,27,17]
and a variable named rgb to hold [13,19,26]. Set the pin mode to BCM and use the
lists above to configure the inputs and outputs:
pads = [22,27,17]
rgb = [13,19,26]
GPIO.setmode(GPIO.BCM)
GPIO.setup(pads, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(rgb, GPIO.OUT)
try:
while True:
except KeyboardInterrupt:
GPIO.cleanup()
STEP #7
Now, the main program will need to check each of the pads, and if they've gone high
indicating they've been touched, turn on an element of the RGB LED. U the following
mapping of inputs and outputs:
try:
while True:
while GPIO.input(22) == True:
GPIO.output(13, GPIO.HIGH)
while GPIO.input(27) == True:
GPIO.output(19, GPIO.HIGH)
while GPIO.input(17) == True:
GPIO.output(26, GPIO.HIGH)
except KeyboardInterrupt:
Each touchpad and RGB element combination will need its own while: loop.
try:
while True:
while GPIO.input(22) == True:
GPIO.output(13, GPIO.HIGH)
while GPIO.input(27) == True:
GPIO.output(19, GPIO.HIGH)
while GPIO.input(17) == True:
GPIO.output(26, GPIO.HIGH)
GPIO.output(rgb, GPIO.LOW)
time.sleep(0.1)
except KeyboardInterrupt:
GPIO.cleanup()
pads = [22,27,17]
rgb = [13,19,26]
GPIO.setmode(GPIO.BCM)
GPIO.setup(pads, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(rgb, GPIO.OUT)
try:
while True:
while GPIO.input(22) == True:
GPIO.output(13, GPIO.HIGH)
while GPIO.input(27) == True:
GPIO.output(19, GPIO.HIGH)
while GPIO.input(17) == True:
GPIO.output(26, GPIO.HIGH)
GPIO.output(rgb, GPIO.LOW)
time.sleep(0.1)
except KeyboardInterrupt:
GPIO.cleanup()
2. To yield the greatest total capacitance of two or more capacitors, should they be
connected in series or parallel?
3. At around what voltage does the Raspberry Pi sense a high signal (3.3V)
transitioning to a low (0V)?
ANSWER: No, when first plugged in the capacitive touch sensor must establish
a baseline. If you are touching it at the time it is turned on, that will be the
baseline.
3. At around what voltage does the Raspberry Pi sense a high signal (3.3V)
transitioning to a low (0V)?
ANSWER: At around 1.4V the Raspberry Pi will begin to recognize the signal
as low. Anything above 1.4V will generally be read as high and anything below
1.4V will generally be read as low.
The next lesson is the final lesson for Level B. In that lesson you will learn how to use a
voltage divider to do simple signal level shifting, the absolute value function, and how to
modify a file so it can be used as an import for another program. You will then move on
to the final project for this course.
OBJECTIVE
In this lesson you will learn how to use a voltage divider to do simple signal level
shifting, the absolute value function, and how to modify a file so it can be used as an
import for another program. You will then move on to the final project for this course.
MATERIALS
REVIEW CONCEPTS
If you do not feel comfortable with the following concepts, please review them before
proceeding.
The following voltage divider with two equal value 1K-ohm resistors will cut the supplied
5V signal in half:
The great thing about this circuit is that the 5V signal does not have to come from a
constant supply. It can also be connected to the output from a 5V device, like the
Ultrasonic Range Sensor, whose 5V output is not safe to connect directly to a GPIO pin.
By using the output of the Ultrasonic Range Sensor to supply the input voltage, the
output will be 2.5V which is safe to connect to a GPIO pin.
• 2.5V present means the Ultrasonic Range Sensor is applying 5V to the input
• 0V present means the Ultrasonic Range Sensor is not applying 5V to the input
You learned in Lesson B-17 that a GPIO pin will register as high for any voltage above
1.4V. So a 2.5V level will easily trigger a high in the GPIO pin:
• GPIO high means the Ultrasonic Range Sensor is applying 5V to the input
• GPIO low means the Ultrasonic Range Sensor is not applying 5V to the input
Using a voltage divider can be a very useful way to quickly reduce the voltage from a
sensor, using only two resistors. When using several 5V sensors, it makes more sense
to use a device like the 74LVC245 (Level Shifting) IC instead, due the amount and
complexity of resistors that would be required to make a voltage divider for each
channel.
abs()
abs(-23) becomes 23
abs(42) becomes 42
The second example doesn't seem to be very useful, but what if you don’t know what
the value in the parentheses will be until your program starts running:
abs(x-y)
If x is greater than y then the abs() function will have no effect on the value that this
function outputs. However, if y is greater than x, the result of x-y will be negative and
abs() will strip off the sign from the negative number.
In the upcoming activity, you will build a game that will be taking the difference of two
numbers, without knowing which will be larger. The abs() function will be used to strip
the sign from the result, leaving only the positive value of the difference between the
two values.
def thing1():
x = 1
while True:
print('Hello World')
On import, the imported file would define the function named thing1(), and then get
stuck in the while True: loop, never returning to your program. This is obviously not
ideal, and this is why back in Lesson B-15, you learned about the __name__ variable
that can be used to determine if a file has been imported, or has been executed directly:
if __name__=="__main__":
Any code indented below this if condition will only run when the file was run directly
and will be ignored if the file is being executed as an import to another file. You can
modify the earlier example to allow for both imported and direct execution:
def thing1():
x = 1
if __name__=="__main__":
while True:
print('Hello World')
Importing the file will now result in the code inside the if block being completely ignored,
and when the program is executed directly, it will run from top to bottom, including
everything contained in the if block.
In the following activities, the ultrasonic range sensing program that you created in
Lesson B-14 will be modified so its function definitions can be used for import, without
executing the main program it contains.
if __name__=="__main__":
try:
while True:
distance = average()
print('%.1f' % distance)
if distance < 20:
GPIO.output(red, GPIO.HIGH)
GPIO.output(green, GPIO.LOW)
GPIO.output(blue, GPIO.LOW)
elif 20 <= distance < 25:
GPIO.output(red, GPIO.LOW)
GPIO.output(green, GPIO.HIGH)
GPIO.output(blue, GPIO.LOW)
else:
GPIO.output(red, GPIO.LOW)
GPIO.output(green, GPIO.LOW)
GPIO.output(blue, GPIO.HIGH)
time.sleep(.25)
except KeyboardInterrupt:
GPIO.cleanup()
All of the gray boxes above are additional spaces that were added to realign the code
below the new if block.
STEP #2
Save your updated file so you can use it as an import later when building the game
program in Activity #3.
STEP #1
Shut down the Pi and disconnect power before proceeding.
Once that's done, the first circuit modification will be to get 5V power and ground over to
the P2/N2 power rails so it can be used to power the Ultrasonic Range Sensor. Make
the following two connections using short jumper wires:
The voltage divider will take care of bringing the Echo line from the ultrasonic range
sensor down to a safe level, but you will need to make a few more connections before
you can use it for ranges. Make the following four connections between the points listed
below:
Power the Raspberry Pi on so it can be used to create a program to use with your new
circuit.
The game will ask you to place your hand or another obstacle at a pre-determined
distance from the Ultrasonic Range Sensor. The game will consist of the following
actions:
• The user selects the desired difficulty level using the slide switch. Easy mode
allows for 20cm of error while Hard mode only allows for 10cm. The program
waits 2 seconds before the position of the slide switch determines which difficulty
level will be used for that round.
• The screen displays a random target distance between 20cm and 100cm.
• The user is asked to press capacitive touch pad 1-4 to start the distance capture
process. Each pad represents the number of seconds before the capture occurs,
which can be used to add more difficulty. Less time to get ready before the
capture makes it more difficult.
• The user range is captured and compared to the target distance. The RGB LED
will turn green if the user distance was within 20cm in Easy mode, or 10cm in
Hard mode. Errors above these amounts will result in the RGB LED turning red.
• The program loops back to the beginning.
This program will be the largest you've written yet, but it will borrow large blocks from
programs in previous activities. So, don’t be intimidated! Build the program one block at
a time, just like you would with a shorter program.
STEP #1
The first step will be to open Thonny and create a new program called range_game.py.
Save the file to your Desktop so it will have access to the ultrasonic.py file that you
modified in Activity #1.
As you go through the steps below, save your program often to avoid losing an of your
work.
STEP #3
Next, you will assign the input and output pins using a lot of variables.
This will help you later if you want to modify this program and move an input or output to
another pin. This list will contain pin assignments for the slide switch, RGB elements,
and Capacitive Touch inputs.
The lists for rgb and cap will allow those groups of pins to be configured as inputs and
outputs as a group, using only one line each:
slide = 16
rgb = [13,19,26]
red = 13
green = 19
blue = 26
cap = [22,27,17,4]
cap1 = 22
cap2 = 27
cap3 = 17
cap4 = 4
cap2 = 27
cap3 = 17
cap4 = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(slide, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(rgb, GPIO.OUT)
GPIO.setup(cap, GPIO.IN)
GPIO.output(rgb, GPIO.LOW)
This block of code is pulled directly from the program you created in Lesson B-16,
Activity #2, and each line of code is broken down in the section titled SSD1306 Display
Driver. If you're unsure about anything below, please refer to that section for more
information:
GPIO.setup(cap, GPIO.IN)
GPIO.output(rgb, GPIO.LOW)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height)) # '1' converts image to 1-bit color
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
Instead of using these two lines every time you need to update the display, create a
function named update_display() that can be called every time an update is needed:
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
def update_display():
disp.image(image)
disp.display()
You are now just over 30 lines into the program. Ensure your program matches the
program on the next page before continuing to the next step.
slide = 16
rgb = [13,19,26]
red = 13
green = 19
blue = 26
cap = [22,27,17,4]
cap1 = 22
cap2 = 27
cap3 = 17
cap4 = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(slide, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(rgb, GPIO.OUT)
GPIO.setup(cap, GPIO.IN)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
def update_display():
disp.image(image)
disp.display()
disp.display() pushes the image buffer to display to so it's clear before shutdown
def update_display():
disp.image(image)
disp.display()
try:
while True:
except KeyboardInterrupt:
disp.clear()
disp.display()
GPIO.cleanup()
Any new code in upcoming steps will be inserted in the while True: loop.
Inside this skill_selection loop, you will nest two if/else conditions:
If the slide switch is low or False, then draw a rectangle to clear the display, draw the
strings "Difficulty?" and "Easy" to two lines of the image buffer, push the image
buffer to the screen, and set a variable named skill equal to 2. This variable will be
used later to determine the size of the error window that will result in a green LED.
If the slide switch is not low, the else: block will execute, just like the block above but
"Hard" will be printed to the second line of the display, and skill will be set equal to 1.
try:
while True:
for skill_selection in range(0,20):
if GPIO.input(slide) == False:
draw.rectangle((0,0,width,height), outline=0, fill = 0)
draw.text((0, 0), "Difficulty?", font=font, fill=255)
draw.text((0, 22), "Easy", font=font, fill=255)
update_display()
skill = 2
else:
draw.rectangle((0,0,width,height), outline=0, fill = 0)
draw.text((0, 0), "Difficulty?", font=font, fill=255)
draw.text((0, 22), "Hard", font=font, fill=255)
update_display()
skill = 1
time.sleep(.1)
Make sure your indentation matches the code above. This is crucial for each block of
the program to operate as expected.
Next, show the target distance on the display by using % notation to print 'Target =
%s" %target on the first line of the display. The second and third lines of the display
should read "Press pad" and "to start". An update_display() will be used to
send this new information to the display:
update_display()
skill = 1
time.sleep(.1)
target = random.randint(20,100)
Be careful again with the indentation on this section of the program. It should be at the
same indentation level as the for: loop. You want it to run after, and not as part of the
for: loop.
This can be done by using a while: loop that requires cap1, cap2, cap3, and cap4 all
to be False to keep the loop running. As soon as any of the GPIO pins connected to
those pads goes high, the loop will exit and continue with the rest of the program. A
time.sleep of 0.05 will be added inside the loop to keep the program from using too
many resources while waiting for input from a touchpad:
Even though the while: condition didn't fit on one line in this document, it should be
one continuous line in your code from while all the way to False:. The
time.sleep(0.05) should be indented so it runs each time the while condition is met.
Create some if conditions following the while: loop that will check to see if each pad
is high or True, and assign a variable named delay equal to that pad’s number. If cap1
was pressed then delay = 1, and if cap4 was pressed then delay = 4, etc.
time.sleep(.05)
if GPIO.input(cap1) == True:
delay = 1
if GPIO.input(cap2) == True:
delay = 2
if GPIO.input(cap3) == True:
delay = 3
if GPIO.input(cap4) == True:
delay = 4
Since the while: loop exited you know that one of the pads was pressed. This code will
quickly check each pad and assign the value of delay based on which pad was pressed.
The first two lines of the message will be very similar to the Press pad to start block
of code form Step #9. The first line will display the target distance and the second will
display "Capturing range". The third line of the display will need to be customized
using an if/else block.
If delay is 1 then the third line should be "in %i second" %delay to maintain proper
grammar for the single second. If delay is anything else, then the third line should be
"in %i seconds" %delay to properly display multiple seconds.
At the end of this block you will update the display and insert a delay equal to the value
of the delay variable by using time.sleep(delay):
if GPIO.input(cap4) == True:
delay = 4
Next, you will create a variable named diff that will hold the absolute value of the
random target variable minus the distance returned from the average() function.
This will be expressed as diff = abs(target-distance).
Now you will calculate the range of values that will get a green LED based on the
difficulty that's been selected. Create a variable named window that equals 10 *
skill. This means:
update_display()
time.sleep(delay)
distance = ultrasonic.average()
diff = abs(target-distance)
window = 10 * skill
STEP #14
It's now time to light the LED using the diff and window variables. If diff is smaller
than or equal to window, the user distance is good, so the LED should turn green. If not,
the user distance that round was larger than the window, so the LED should turn red.
This can be accomplished by using a simple if/else block:
distance = ultrasonic.average()
diff = abs(target-distance)
window = 10 * skill
Finally, you will display the target and player distances, along with the difference
between the two, add a 5 second delay so there is time to view the results, and turn off
the LED so it's ready for the next round.
You will use time.sleep(5) for the delay and the rgb list to turn off all LED elements:
else:
GPIO.output(red, GPIO.HIGH)
slide = 16
rgb = [13,19,26]
red = 13
green = 19
blue = 26
cap = [22,27,17,4]
cap1 = 22
cap2 = 27
cap3 = 17
cap4 = 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(slide, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(rgb, GPIO.OUT)
GPIO.setup(cap, GPIO.IN)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None)
disp.begin()
width = disp.width
height = disp.height
image = Image.new('1', (width, height)) # '1' converts image to 1-bit color
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf',18)
def update_display():
disp.image(image)
disp.display()
try:
while True:
for skill_selection in range(0,20):
if GPIO.input(slide) == False:
draw.rectangle((0,0,width,height), outline=0, fill = 0)
draw.text((0, 0), "Difficulty?", font=font, fill=255)
draw.text((0, 22), "Easy", font=font, fill=255)
update_display()
skill = 2
else:
draw.rectangle((0,0,width,height), outline=0, fill = 0)
draw.text((0, 0), "Difficulty?", font=font, fill=255)
draw.text((0, 22), "Hard", font=font, fill=255)
update_display()
skill = 1
time.sleep(.1)
target = random.randint(20,100)
while GPIO.input(cap1)==GPIO.input(cap2)==GPIO.input(cap3)==GPIO.input(cap4)==False:
time.sleep(.05)
if GPIO.input(cap1) == True:
delay = 1
if GPIO.input(cap2) == True:
delay = 2
if GPIO.input(cap3) == True:
delay = 3
if GPIO.input(cap4) == True:
delay = 4
distance = ultrasonic.average()
diff = abs(target-distance)
window = 10 * skill
except KeyboardInterrupt:
disp.clear()
disp.display()
GPIO.cleanup()
If your program is not working as expected, identify which area of the program needs to
be checked. Each block of code is performing a very specific function, so identify what
part of the game is not working properly and check out the block of code that is
controlling that behavior.
If you still can't get your program to work, you can download a copy of this program from
the Level B Resource Page.
2. Can every file be used an import without causing any problems in your main
program?
ANSWER: Signal level shifting can be done with two resistors but if multiple 5V
devices are being used, using an IC for level shifting is recommended.
2. Can every file be used an import without causing any problems in your main
program?
ANSWER: No. Everything in the imported file will run on import unless it's inside
an if __name__=="__main__": condition. Loops or other types of code in
the imported file could cause problems when imported and must be enclosed in
this if: condition to isolate it during your import.
ANSWER: The abs() function will return the absolute value of a variable or
expression.
What’s next?
• Order a copy of Level C of this course to continue to learn to work with more
electronic components and coding commands (available Spring 2019).
• The skills you have gained and the components you have amassed working with
Level A and Level B, have put you in a great position to tackle beginner and
intermediate projects online. We recommend searching for projects that use both
the Raspberry Pi and Python. You are likely to find that you have worked with
much of the code and components that will be called for in the projects, and
those you haven’t, you likely have the skills to figure out.
Page 563
COMPONENTS
OVERVIEW
The supply kit for this course will be shipped to you within several days of your order.
Here are a few things to keep in mind:
• Specific instructions for using all kit components will be included in the lessons.
For a few parts, you’ll find notes in this overview for how to get the part ready to
use, etc.
• Leave all components in their packages until you need them for a lesson. This
keeps them protected.
While all components for circuit building needed for the lessons are included in the kit, if
you wish to purchase extra parts or replacement parts, please visit:
www.42electronics.com/products/replacement-parts.
PIEZO SPEAKER
Piezo Speaker
Qty. 1
Slide Switch
Qty. 1
Qty. 1
Keypad Header
Qty. 1
RFID READER
RFID Reader
Qty. 1
Qty. 2
Analog-to-Digital Convertor
Qty. 1
Qty. 1
PHOTOTRANSISTOR
Phototransistor
Qty. 1
Qty. 1
Qty. 1
Qty. 1
Qty. 1
TEMPERATURE SENSOR
Qty. 1
OLED DISPLAY
Qty. 1
Qty. 1
220uF - Qty. 2
2200uF – Qty. 1
Qty. 8
Qty. 4
Qty. 8
REPLACEMENT PARTS
While all components for circuit building needed for the lessons are included in the kit, if
you need to purchase extra parts or replacement parts, please visit:
www.42electronics.com/products/replacement-parts.
LIST OF MATERIALS
FOR EACH LESSON
LESSON 1
LESSON 2
LESSON 3
LESSON 4
Level B – Quick Reference: List of Materials for Each Lesson Page 571
LESSON 6
LESSON 7
LESSON 8
Level B – Quick Reference: List of Materials for Each Lesson Page 572
LESSON 10
LESSON 11
LESSON 12
Level B – Quick Reference: List of Materials for Each Lesson Page 573
LESSON 14
LESSON 15
Level B – Quick Reference: List of Materials for Each Lesson Page 574
LESSON 17
LESSON 18
Level B – Quick Reference: List of Materials for Each Lesson Page 575