The code for this project may be found in Github. To check out the code, run the following commands:
git clone https://github.com/alfmel/rpi_leds.git cd rpi_leds git checkout part2
In Part 1 I learned how to turn an LED on and off. Not something really exciting. For this next part I want to hook up several LEDs and turn them on and off in a sequence, and if possible, more than one sequence. Because I'll be working with object-oriented code and trying to explain its merits, this will be a long post.
Taking what I learned in Part 1, it wouldn't be very difficult to turn on multiple LEDs. I would need a way to define the sequence (like an array) and a loop to iterate over the sequence. Since it would be easy to define LEDs that need to be turned on during the sequence, it would also be nice to know which pins are associated with LEDs.
Since the turning of LEDs on and off requires timers, I will create a class that will handle the timer and sequence loop. That's right, I will be writing Object-Oriented Python Code (no need to be scared). In order to run asynchronously (that is, I can start the sequence in one part of the program, go do something else, and turn it off later) the class will also have to handle the creation of its own thread.
Don't worry, writing object-oriented, multi-threaded Python code may sound intimidating at first, but it's not too bad. Also, the class is completely reusable. That means you can use the class and not have to worry about threading. That's why we write classes: to create modular pieces of code we can reuse while reducing unintended side-effects.
The Explorer pHAT Kit came with several LEDs of several colors. I decided to grab an LED of each color and hook them up to the Pi Zero. There are four different colors, so I'll need four GPIO pins and a ground pin. Looking at the pin layout, physical pins 32, 34, 36, 38 and 40 are the perfect candidates. All of them are GPIO pins (with the exception of pin 34 which is ground) and they are all right next to each other. And with the multi-color wires, I can color coordinate!
Now, let me explain the code. Let me start by showing you led_sequences.py. This is the executable part of one of the scripts I've written to run the LED sequences. This script creates several sequences, and runs them one by one. You can move from one sequence to another by hitting
Defining the LEDs in Use
Lines 12-15 define the pins I'll be using. Notice that I am defining variables with the pin numbers. The variable names are simply the colors of the LEDs. If I were defining more than one red LED, I would have to name my variables red1, red2, etc. I could also call my variables led1, led2, etc. but then I would have to think which one is which. By naming the variables the color of the LED, it will be really easy to write a sequence.
Defining the Sequence Time Delay
In line 9 I define a variable that has the delay between parts of the sequence. The delay is specified in seconds. Here the delay is set to 0.25, meaning 250ms or 1/4 of a second.
Defining a sequence
Before I define a sequence I need to create an array that will define the LEDs that will belong to a sequence. In this case, I add all my LEDs (line 32).
Defining the sequence requires a simple list of LED pin numbers that should be turned on at a given time. For example defining
[red, blue, red] will turn on red, blue, then red again. The sequence would then repeat. The list can accept either a single pin, a list of pins to turn on several LEDs on at once, or an empty list to signify that no LEDs should be on. On led_sequences.py lines 18-26 I define several sequences, each with a name. (I used the OrderedDict class to ensure the order of the sequences stayed in the order I defined them).
How the Sequence Class Works
Before I describe the class, I want to remind you, the reader, why we use classes in programming. Classes are programming structures that contain both functionality and state. They are self-contained. For the Sequence class, the state consists of:
- A reference to the GPIO module. (Requiring dependent modules in this way is called Dependency Injection. It allows us to replace the implementation of GPIO at runtime, which means the code can be more easily tested.)
- The sequence time delay
- The LEDs in use
- The LED sequence
Internally the class also keeps track of the thread that will turn the LEDs on and off. But because it is internal (private in Object-Oriented Programming talk), the consumer of the class doesn't have to worry about it. The class hides (encapsulates) this complexity. In other words, you, as the consumer of the class, don't have to worry about how the class works. You just worry about being able to talk to it.
Now let's look at the Sequence class. The class has the following public methods (what you use to communicate with the class):
set_delay(delay)Set the duration of each element of the sequence in seconds
set_pins(pins)Set the pins that will be used during the sequence, passing the pin numbers as an array
set_sequence(sequence)Set the LED sequence
start()Once everything is in place, this will start the sequence
stop()Stops the running sequence
Testing the pi.sequence Class
The constructor (the method or function called when the class is instantiated into an object) requires the GPIO reference I discussed earlier. This allows us to pass a test double or a mock library that can help us with testing. Test doubles have the same functions and methods that the real class has, but they work differently. One advantage of using mock functions is that we can validate (or assert) they were called properly.
In lines 8-13 of test_sequence.py I define a mock GPIO class that I will pass during testing. Then, in the individual tests, I assert that the methods are called with the proper values, in this case, making sure the right LED pins are turned on and off (see lines 35-46.
To run the tests, simply execute the following command from the command line in the project's parent directory:
python -m unittest discover
Putting It All Together
Now that I've explained how the code works, it's time to make it all work.
- On line 30 of led_sequences.py I instantiate the class (into an object), passing the real GPIO module.
- On line 31 I set the delay I defined in line 9.
- On line 32 I set the LED pins I'll be using
- On line 36 I iterate (loop) through the sequences I defined in lines 18-25.
- On line 37 I set the current sequence
- On line 38 I start the sequence
- On line 39 I display the name of the current sequence and wait for the user to hit Enter.
- When the user hits Enter line 40 executes and the sequence stops. If there are still more sequences in the loop, I move on to the next one and run the next sequence. If there are no more sequences, the program exits.
The result can be seen below. (The sequence appears choppy because of the frame rate reduction of the animated GIF.)
Running More Than One Sequence
Because I used a class to run the sequence, I should be able to run multiple sequences at once simply by creating separate instances (or copies) of the class. (Each instance of a class is called an object, by the way, and each instance will have its own state, without conflicting with any other instances). led_double_sequences.py does exactly that. On lines 8 and 9 I define two different delays. On line 18 I create one sequence with two of the LEDs, and on line 19 I define a second sequence with the other two LEDs. On lines 24-27 I set up one instance of the Sequence class (sequence_a) and in lines 29-32 I set up the second instance with different values (sequence_b). Then on lines 36-37 I start both sequences. Because each instance of the Sequence class creates its own thread, they can run at the same time. (Again, the rate reduction in the animated GIF doesn't quite show it perfectly.)
And there you have it! Thanks to Object-Oriented Programming principles, it is now easy for me to create a light sequence with different LEDs hooked up to different GPIO pins and have them run independently of what my program might be doing.