python-3.xraspberry-pigpioraspberry-pi4gpiozero

How to use gpiozero button methods to check when a button is pressed at a certain time


I am currently working on a small project to get started with physical computing with Python - using a raspberry pi. I am trying to make a game using 3 LED's and a push button. The 3 LED's will flash in a certain order for different times and you have to click the button when the amber LED is turned on. So far I have tried this code using gpiozero to recognize when the button is turned on and see if it is was within the time frame that the amber led was on.

Game Code: https://codeshare.io/G7qk1j (If link above doesn't work please let me know)

Code:

def gameEasy():
    print("Level 1 - trial run")
    how_long_to_react = 5
    time_till_started = time.time() + 4
    while time.time() < time_till_started:
        button.when_pressed = None
    button.when_pressed = lambda:clickedRight(ambTime, greenTime, time.time())
    redLed.on()
    time.sleep(2)
    redLed.off()
    amberLed.on()
    ambTime = time.time()
    greenTime = ambTime + how_long_to_react
    time.sleep(how_long_to_react)
    amberLed.off()
    greenLed.on()
    time.sleep(0.1)
    greenLed.off()
    time.sleep(10)

At the moment, to try to get it to work, firstly, the red led will turn on for 2 seconds, then the amber led will turn on (currently for 4 seconds just as a test). When the amber LED turns on, that is when the first time is noted to later be checked to see if the button is clicked within the range. My issue is that if the button is clicked before this variable (ambTime) is calculated, then line 58 will trigger an error:

    button.when_pressed = lambda:clickedRight(ambTime, greenTime, time.time()) 
NameError: free variable 'ambTime' referenced before assignment in enclosing scope

I understand why it's happening, but i'm not sure how to 'block' any button clicks before the amber LED turns on. As you can see, I have a while loop to try to block any commands for the first 4 seconds but it acts as a delay rather than a background loop (so it won't start the next code until after the 4 seconds).

I've noticed with this method - button.when_pressed = lambda:clickedRight(ambTime, greenTime, time.time()) - it is is always checking for the button press, no matter where this line is in the code, so it doesn't matter if I put this line just before amberLed.on()

I hope I have explained this right, this has been troubling me for quite a while so any help would much appreciated. I have tried multiprocessing but got really confused with how to integrate it here, but I am open to any suggestions. I completely understand that you may not be able to test your code so I will be happy to try anything and respond with any issues (if there are any) that come up.

Thank you.


Solution

  • As you have noted; running...

        button.when_pressed = lambda:clickedRight(ambTime, greenTime, time.time())
    

    ...immediately starts a thread that monitors for button clicks, and if your lambda function runs before ambTime is defined, it will fail. The easiest solution is probably to initialize ambTime to some value before setting up the button.when_pressed action, e.g:

    def gameEasy():
        ...
        ambTime = None
        button.when_pressed = lambda:clickedRight(ambTime, greenTime, time.time())
        ...
    

    And in your clickedRight function, explicitly check for to see if ambTime has a valid value. Maybe something like:

    def clickedRight(led_clicked_time, led_after_turnedon_time, button_time):
        if led_clicked_time is not None and (
          button_time >= led_clicked_time and
          button_time <= led_after_turnedon_time
        ):
            print("Yay")
        else:
            print("Missed")
        button.when_pressed = None