Select modes(programs) with GPIO buttons

Hello
I’ve been struggling with this for a long time, and about to give up.

Here’s what I’m trying to do:

I have a Raspberry Pi Zero 2 W and I will connect one button to GPIO 2 and another button to GPIO 3. The buttons will act as a next/previous function.

The device I’m designing consist of a small display and the next/previous buttons will basically set what is being displayed on the screen. At the moment I have 3 different modes to display on the screen. They all work individually.

Mode1. A python code that displays a specific image, lets call it mode1.py

cd /home/pi/project/mode1
python3 mode1.py

Mode2. A screensaver with random images using fbi:

cd /home/pi/project/mode2
sudo fbi -T 1 -a --noverbose -blend 1000 -t 5 *

Mode3. A video loop with VLC:

cd /home/pi/project/mode3
sudo vlc --no-video-title-show -L -f loop1.mp4

I would like to add Mode4 and more later.

This program I need help with should automatically start on Mode 1, and when Next is pressed Mode 1 is killed and Mode 2 starts, and so on… Previous button works the same but backwards.

I need help with writing this code. At the moment the only code I have is where I select Mode with separate buttons for each mode, but that is not what I want and it’s not working well. I keep having problems killing the instances. I’m not sure if I should show that code since it is a mess.

Please help, my knowledge in programming is very limited.

Have you tried using the subprocess module? I’m not familiar with the RaspberryPi Zero W, so I don’t know how capable it is.

Hello,

what you first need to do is to set up / configure the edge detection circuitry for each GPIO pin so that they each detect when a pin has been pressed (rising / falling edge, etc.). I would also recommend reviewing the schematic to ensure the GPIO pin circuits are ideal for edge detection. In the following sketch, I included what is an ideal edge detection circuit. The resistor pulls up the voltage to VCC (circuit operating voltage) and the capacitor keeps the voltage constant at that pin. The switch pin should be tied to the juncture of the GPIO pin, resistor, and capacitor.

edge_circuitry

You also have to set up an ISR for each GPIO pin such that when an edge (disturbance caused by the user pressing the button) has been detected on their respective pin, their ISR is automatically called. Inside each ISR, you include the code where you either increment or decrement the value of the global variable that keeps tabs on the current active mode.

You will also need a selection statement. If you are writing this in C, use the case statement. Else, if in Python, you can use the match statement (See PEP 636).

Here is a very basic high-level view of the code written in Python for simplicity - port it to the actual language (C?) that you’ll be using. But there is enough here to get you started:

# Define modes
MODE_1, MODE_2, MODE_3, MODE_4 = 0, 1, 2, 3
active_mode = MODE_1  # Initialize mode

while True:

    match active_mode:

        case MODE_1:
        # include function to run

        case MODE_2:
        # include function to run

        case MODE_3:
        # include function to run

        case MODE_4:
        # include function to run

# You will have to read the device's datasheet / documentation on how to 
# properly set up the ISR  (correct syntax, etc.)

# ISR - assigned to GPIO 2 for NEXT
def isr_next_mode_GPIO2():

    global active_mode
    active_mode += 1

    if active_mode == 4:
        active_mode = 0

# ISR - assigned to GPIO 3 for PREVIOUS
def isr_previous_mode_GPIO3():

    global active_mode
    active_mode -= 1
    
    if active_mode == -1:
        active_mode = 3

Thank you. I want the code to be in Python.
Unfortunately I don’t know how to continue with the code example you provided.

How would the While True function look like? Do I use if, elif, and else?

Is there anything I need to import?

In my old program I used this to get the GPIO to work

from gpiozero import Button

Here’s what I had previously.
I created a shell that would start each mode and another shell that would stop it.

mode1_start.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode1
sudo /usr/bin/python3 /home/pi/project/mode1/mode1.py

mode1_stop.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode1
sudo pkill -f -9 mode1.py

mode2_start.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode2
sudo fbi -T 1 -a --noverbose -blend 1000 -t 5 *

mode2_stop.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode2
sudo kill $(pgrep fbi)

mode3_start.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode3
sudo vlc --no-video-title-show -L -f mode3.mp4

mode3_stop.sh

PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/home/pi/.local/bin
cd /home/pi/project/mode3
sudo kill $(pgrep vlc)

And here’s the program, mode.py

from gpiozero import Button
from time import sleep
import subprocess
button1 = Button(2) 
button2 = Button(3)
button3 = Button(4) 
while True:
    if button1.is_pressed:
       subprocess.run("sudo /bin/bash /home/pi/project/mode1/start.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode2/stop.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode3/stop.sh", shell=True)
       sleep(0.25)
       continue

    elif button2.is_pressed:
       subprocess.run("sudo /bin/bash /home/pi/project/mode1/stop.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode2/start.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode3/stop.sh", shell=True)
       sleep(0.25)
       continue

    elif button3.is_pressed:
       subprocess.run("sudo /bin/bash /home/pi/project/mode1/stop.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode2/stop.sh", shell=True)
       subprocess.run("sudo /bin/bash /home/pi/project/mode3/start.sh", shell=True)
       sleep(0.25)
       continue

    else:
       continue

This kind-of works. If I press button 1, the correct program starts, but it doesnt close when I press button 2 or 3. But if I manually run the mode2/stop.sh then it closes. For some reason the program can’t close it.

If I press button 2 then mode2 starts, and if I press button 1 or 3 then mode2 closes and the other modes open. The call for the stop of mode 1 and 3 doesnt seem to work in the program.

But this is not really the function I wanted. I want only two buttons, Next/Previous.

I’m completely lost :slight_smile:

Try subprocess.Popen.

To start a task:

task = subprocess.Popen("sudo /bin/bash /home/pi/project/mode1/start.sh", shell=True)

and to kill the task:

task.terminate()

Incidentally, you don’t need the continue. You can do just:

while True:
    if first_condition:
        # first action
        ...
    elif second_condition:
        # second action
        ...
    elif third_condition:
        # third action
        ...

While True is not a function. It is a conditional loop. The True makes it an infinite loop unless of course a condition is met and forces it to exit (you define the condition).

I don’t have experience working with Rasberry Pi modules or using Python to control them. However, I did create a very similar application on a board that I designed using C where I had to detect a disturbance on a pin and take some action every time that an edge was detected.

Therefore, if you are using Python for controlling the Rasberry Pi module, you may use the while loop as I have provided. The match selection statement is an alternative to the if, elif, and else conditional selection statements (reference PEP 636 or google a related tutorial). It comes down to preference and application suitability.

You do not need to do this. The program should take care of this automatically. You should power up the board, and press either button. Depending the button pressed, the correct function (mode) should run.

I would recommend transferring the code of each mode into its own separate function. For example, if you define a function for each mode:

def mode_1():
    # include algorithm / body of statements for this mode here

def mode_2():
    # include algorithm / body of statements for this mode here

def mode_3():
    # include algorithm / body of statements for this mode here

def mode_4():
    # include algorithm / body of statements for this mode here

These are the functions that should be included in the match conditional statement options. So, depending on the variable keeping tabs on the user selection, that function (process) will run until the a button is pressed again.

As you clearly stated in your original post, there should only be two buttons: previous and next. Not one for each mode as you are currently doing.

Perhaps what you should do is define only two subprocesses: One for incrementing the variable and one for decrementing it. That is, the subprocesses should not be tied to any particular mode. The modes should be variable value dependent and selected in the match selection statement.

this is not working, how do I define the task? Now my program just runs all the modes at the same time.
Im completely lost :frowning:

Something like this perhaps:

from gpiozero import Button
from subprocess import Popen
from time import sleep

def start_task(mode):
    if mode == 1:
        return Popen("python3 mode1.py", cwd="/home/pi/project/mode1", shell=True)
    elif mode == 2:
        return Popen("fbi -T 1 -a --noverbose -blend 1000 -t 5 *", cwd="/home/pi/project/mode2", shell=True)
    elif mode == 3:
        return Popen("vlc --no-video-title-show -L -f loop1.mp4", cwd="/home/pi/project/mode3", shell=True)
    else:
        return None

previous_button = Button(2)
next_button = Button(3)
currently_pressed = None
current_mode = 1
current_task = start_task(current_mode)

while True:
    if previous_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = previous_button

            if current_mode == 1:
                current_mode = 3
            else:
                current_mode -= 1

            current_task.terminate()
            current_task = start_task(current_mode)
    elif next_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = next_button

            if current_mode == 3:
                current_mode = 1
            else:
                current_mode += 1

            current_task.terminate()
            current_task = start_task(current_mode)
    else:
        currently_pressed = None

    sleep(0.25)

Thank you, but unfortunately it doesn’t seem to work.

When I start the program mode1 starts. But when I press the next or previous buttons the current mode doesn’t terminate, but if I manually terminate it then I see that the other mode had started but it was hidden behind mode1.

Why would the terminate command not work?

This seems like similar problem as I had in previous program, no matter what method I used I was always unable to kill the process, could it be a permissions issue?

Do you have any idea what could be wrong?

Does this help?

raspberry pi - How do I terminate my subprocesses in Python - Stack Overflow

Basically, if the terminate() method doesn’t work, try the kill() method instead.

current_task.kill() does not work either

OK, here’s something else:

I’ve tried this on a RaspberryPi 3:

from subprocess import PIPE, Popen
from time import sleep

print('Starting VLC')
p = Popen('exec vlc', stdout=PIPE, shell=True)

print('Waiting 10 seconds')
sleep(10)

print('Ending VLC')
p.kill()

print('Done!')
sleep(10)

It seems to work OK.

I’m not sure how to implement this. This is what I have now and doesn’t work:
Where do I declare the mode(command)?

rom subprocess import PIPE, Popen
from gpiozero import Button
from time import sleep

how do I define these here --> mode1 = Popen("python3 mode1.py", stdout=PIPE, cwd="/home/pi/project/mode1", shell=True)
how do I define these here --> mode2 = Popen("fbi -T 1 -a --noverbose -blend 1000 -t 5 *", stdout=PIPE, cwd="/home/pi/project/mode2/images", shell=True)
how do I define these here --> mode3 = Popen("vlc --no-video-title-show -L -f loop1.mp4", stdout=PIPE, cwd="/home/pi/project/mode3", shell=True)

def start_task(mode):
    if mode == 1:
        print('Starting Mode1')
        #mode1 = Popen("python3 moon_start.py", stdout=PIPE, cwd="/home/pi/project/mode1", shell=True)
        return Popen("python3 moon_start.py", stdout=PIPE, cwd="/home/pi/project/mode1", shell=True)
        mode2.kill()
        mode3.kill()
    elif mode == 2:
        print('Starting Mode2')
        #mode2 = Popen("fbi -T 1 -a --noverbose -blend 1000 -t 5 *", stdout=PIPE, cwd="/home/pi/project/mode2/images", shell=True)
        return Popen("fbi -T 1 -a --noverbose -blend 1000 -t 5 *", stdout=PIPE, cwd="/home/pi/project/mode2/images", shell=True)
        mode1.kill()
        mode3.kill()
    elif mode == 3:
        print('Starting Mode3')
        #mode3 = Popen("vlc --no-video-title-show -L -f loop1.mp4", stdout=PIPE, cwd="/home/pi/project/mode3", shell=True)
        return Popen("vlc --no-video-title-show -L -f loop1.mp4", stdout=PIPE, cwd="/home/pi/project/mode3", shell=True)
        mode1.kill()
        mode2.kill()
    else:
        return None

previous_button = Button(2)
next_button = Button(4)
currently_pressed = None
current_mode = 1
current_task = start_task(current_mode)

while True:
    if previous_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = previous_button

            if current_mode == 1:
                current_mode = 3
            else:
                current_mode -= 1

            current_task.terminate()
            current_task = start_task(current_mode)
    elif next_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = next_button

            if current_mode == 3:
                current_mode = 1
            else:
                current_mode += 1

            current_task.terminate()
            current_task = start_task(current_mode)
    else:
        currently_pressed = None

    sleep(0.25)

What are the variables mode1, mode2 and mode3 supposed to do? Why did you add them?

In my previous code, I had current_mode and current_task. current_mode said which mode you were in (1, 2 or 3) and current_task was the ‘handle’ to the subprocess that was running that mode’s code.

When changing mode, you would update current_mode, kill the current task (current_task.kill()), and start the new task, putting the handle to the new subprocess in current_task.

The problem was that current_task.kill() wasn’t killing the task.

The solution, as far as I can tell, is to prefix the command line you pass to Popen with "exec " and set the stdout to PIPE, as in my last code.

Therefore:

from gpiozero import Button
from subprocess import Popen, PIPE
from time import sleep

def start_task(mode):
    if mode == 1:
        return Popen("exec python3 mode1.py", cwd="/home/pi/project/mode1", shell=True, stdout=PIPE)
    elif mode == 2:
        return Popen("exec fbi -T 1 -a --noverbose -blend 1000 -t 5 *", cwd="/home/pi/project/mode2", shell=True, stdout=PIPE)
    elif mode == 3:
        return Popen("exec vlc --no-video-title-show -L -f loop1.mp4", cwd="/home/pi/project/mode3", shell=True, stdout=PIPE)
    else:
        return None

previous_button = Button(2)
next_button = Button(3)
currently_pressed = None
current_mode = 1
current_task = start_task(current_mode)

while True:
    if previous_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = previous_button

            if current_mode == 1:
                current_mode = 3
            else:
                current_mode -= 1

            current_task.terminate()
            current_task = start_task(current_mode)
    elif next_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = next_button

            if current_mode == 3:
                current_mode = 1
            else:
                current_mode += 1

            current_task.terminate()
            current_task = start_task(current_mode)
    else:
        currently_pressed = None

    sleep(0.25)

Thank you! This is a big improvement, the only thing not working now is killing mode2 and it seems that the window system crashes when I try to go past mode2. Maybe this does not work well with fbi?

You might want to test each of the modes individually to see whether you can start and then kill it.

Does it need the stdout=PIPE part in Popen?

Does the command line passed to Popen need the "sudo" that you had in your original code? Does it need the "exec"? Does it need both, i.e. "exec sudo"?

Does the terminate method or the kill method work?

If some of them don’t work with any of the possibilities, then you have a problem!

Hello,

what if you replaced the .terminate function with your own explicit function for terminating the previous mode - the antithesis to the start_task function.

def end_task(previous_mode):

    match previous_mode:

        case 1:
            # Instructions to terminate mode 1

        case 2:
            # Instructions to terminate mode 2

        case 3
            # Instructions to terminate mode 3

        case _:
            pass

This functionn would be used to replace this line:

current_task.terminate()

with:

end_task(previous_mode)

You will need to add the new variable previous_mode as part of your script, however, to keep track of the previous running mode since as soon as you enter either the 2nd or 3rd if statement, the current_mode variable gets updated. You would need to assign this variable prior to updating the current_mode variable when either of the buttons are pressed.

previous_mode = current_mode

I am not familiar with how to kill a running script in a Rasberry pi module (maybe someone can chime in here). But one instruction that I have seen online is:

pkill -f mode1.py  # for killing mode1.py script

Once you determine the correct instruction to kill a script, you can apply it to the other scripts and populate the end_task function with these instructions for every value of previous_mode in the match selection statement body.

I was able to isolate the problem to fbi, I installed feh instead and now everything works perfect! And I figured out how to add more modes.

Here’s the final code and a schematic for those interested. Thanks everyone for the help and especially @MRAB :star: :star: :star: :star: :star:

from gpiozero import Button
from subprocess import Popen, PIPE
from time import sleep

def start_task(mode):
    if mode == 1:
        return Popen("exec python3 mode.py", cwd="/home/pi/project/mode1", shell=True, stdout=PIPE)
    elif mode == 2:
        return Popen("exec feh -Z -x -F -Y -B -q -D 5 *", cwd="/home/pi/project/mode2/images", shell=True)
    elif mode == 3:
        return Popen("exec cvlc --no-video-title-show -L -f mode3.mp4", cwd="/home/pi/project/mode3", shell=True, stdout=PIPE)
    elif mode == 4:
        return Popen("exec cvlc --no-video-title-show -L -f mode4.mp4", cwd="/home/pi/project/mode4", shell=True, stdout=PIPE)
    else:
        return None

previous_button = Button(2)
next_button = Button(4)
currently_pressed = None
current_mode = 1
current_task = start_task(current_mode)

while True:
    if previous_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = previous_button

            if current_mode == 1:
                current_mode = 4
            else:
                current_mode -= 1

            current_task.terminate()
            current_task = start_task(current_mode)
    elif next_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = next_button

            if current_mode == 4:
                current_mode = 1
            else:
                current_mode += 1

            current_task.terminate()
            current_task = start_task(current_mode)
    else:
        currently_pressed = None

    sleep(0.25)

Doing it this way means there’s less work to do to add new modes. Just add to the TASKS list.

from gpiozero import Button
from subprocess import Popen, PIPE
from time import sleep

# For each task, what is the command line, what is the directory, and should stdout should be PIPE?
TASKS = [
    ("exec python3 mode.py", "/home/pi/project/mode1", True),
    ("exec feh -Z -x -F -Y -B -q -D 5 *", "/home/pi/project/mode2/images", False),
    ("exec cvlc --no-video-title-show -L -f mode3.mp4", "/home/pi/project/mode3", True),
    ("exec cvlc --no-video-title-show -L -f mode4.mp4", "/home/pi/project/mode4", True),
]

NUM_TASKS = len(TASKS)

def start_task(mode):
    command_line, directory, use_pipe = TASKS[mode]
    
    if use_pipe:
        return Popen(command_line, cwd=directory, shell=True, stdout=PIPE)
    else:
        return Popen(command_line, cwd=directory, shell=True)

previous_button = Button(2)
next_button = Button(4)
currently_pressed = None
current_mode = 0 # Start mode at 0 because it makes the code slightly simpler.
current_task = start_task(current_mode)

while True:
    if previous_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = previous_button
            current_mode = (current_mode - 1) % NUM_TASKS

            current_task.terminate()
            current_task = start_task(current_mode)
    elif next_button.is_pressed:
        if currently_pressed is None:
            currently_pressed = next_button
            current_mode = (current_mode + 1) % NUM_TASKS

            current_task.terminate()
            current_task = start_task(current_mode)
    else:
        currently_pressed = None

    sleep(0.25)