0

I’m trying to use a button, in this case an Amazon Dash Button, to trigger two functions - i.e. push once it activates one function, push again activates another.

I have tried setting a state to the button and then changing the state from False to True, but the while loop simply seems to repeat triggering the action to turn on and off.

I suspect this is because the loop is going to the start, seeing the state is set to False by default and then going from there. However, I’m stuck when trying to figure out where I need to put the state so that it works.

EDIT: Update to show full code below and to add more context:

So the Python script is running on a Pi and the idea is that I will push an Amazon Dash Button to toggle some radio controlled plug sockets (Energenie Pimote), but my problem is that the button push triggers the sockets to come on, but then they go into an endless loop of turning off and back on again every 5 seconds. Here's the code:

#!/usr/bin/env python

import RPi.GPIO as GPIO
import time
import requests
import logging
import urllib
import httplib
import os
import threading

logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *

def both_on():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(11, GPIO.OUT)
    GPIO.setup(15, GPIO.OUT)
    GPIO.setup(16, GPIO.OUT)
    GPIO.setup(13, GPIO.OUT)
    GPIO.setup(18, GPIO.OUT)
    GPIO.setup(22, GPIO.OUT)
    GPIO.output (22, False)
    GPIO.output (18, False)
    GPIO.output (11, False)
    GPIO.output (15, False)
    GPIO.output (16, False)
    GPIO.output (13, False)
    GPIO.output (11, True)
    GPIO.output (15, True)
    GPIO.output (16, False)
    GPIO.output (13, True)
    time.sleep(0.1)
    GPIO.output (22, True)
    time.sleep(0.25)
    GPIO.output (22, False)
    GPIO.cleanup()

def both_off():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(11, GPIO.OUT)
    GPIO.setup(15, GPIO.OUT)
    GPIO.setup(16, GPIO.OUT)
    GPIO.setup(13, GPIO.OUT)
    GPIO.setup(18, GPIO.OUT)
    GPIO.setup(22, GPIO.OUT)
    GPIO.output (22, False)
    GPIO.output (18, False)
    GPIO.output (11, False)
    GPIO.output (15, False)
    GPIO.output (16, False)
    GPIO.output (13, False)
    GPIO.output (11, True)
    GPIO.output (15, True)
    GPIO.output (16, False)
    GPIO.output (13, False)
    time.sleep(0.1)
    GPIO.output (22, True)
    time.sleep(0.25)
    GPIO.output (22, False)
    GPIO.cleanup()

def button_pressed_dash():
    global lastClick
    lastClick = time.time()
    print("button pressed")

def state_machine():
    print("State Machine Go...")
    STATE1 = 1
    STATE2 = 2
    INITIAL_STATE = STATE1
    state = INITIAL_STATE
    global lastClick
    lastClick = 0 # wait for next button click
    print("Last click set")
    while True:
        print("While true loop...")
        ###### state definitions and transitions
        if state == STATE1:  # STATE1 actions and transitions
           if lastClick != 0:
               print("On!")
               both_on()
               lastClick = 0  # we handled click
               state = STATE2  # goto STATE2
        elif state == STATE2:  # STATE2 actions and transitions
           if lastClick != 0:
               print("Off...")
               both_off()
               lastClick = 0  # we handled click
               state = INITIAL_STATE
        ##### transitions common to all states
        if time.time() - lastClick > 5:
            print("Check time")
            lastClick = 0  # timeout
            state = INITIAL_STATE
            print("State = "+str(state))
            print("Last Click = "+str(lastClick))
        time.sleep(1)

def udp_filter(pkt):
    print("UDP filter start")
    options = pkt[DHCP].options
    for option in options:
        if isinstance(option, tuple):
            if 'requested_addr' in option:
                mac_to_action[pkt.src]()
                print("UDP done")
                break

mac_to_action = {'xx:xx:xx:xx:xx:xx' : button_pressed_dash} # Add your Amazon Dash Button's MAC address in lowercase
print("Button Done")
mac_id_list = list(mac_to_action.keys())
# create separate thread for state machine
stateMachineThread = threading.Timer(0.1, state_machine)
stateMachineThread.daemon = True
stateMachineThread.start()

print("ID List Done")
sniff(prn=udp_filter, store=0, filter="udp", lfilter=lambda d: d.src in mac_id_list)
print("Sniff done")

#state_machine()
print("--END--")

So, when I run this script on my Pi, the script works but pushing the dash button triggers the sockets (great), but they then enter an endless loop of turning off and on.

Hopefully the last update - nearly there!

The code is now working albeit only triggering the both_on() with each press, but the idea is to use one dash button to toggle a switch on and off. So, I press the button once - the lights turn on. I press the button a second time, the lights turn off and repeat. The plan is to leave the script running constantly and I can toggle a socket on or off with a button push.

At the moment, when I push the button only both_on() is called every time. I need it to call both_off() on the second push and vice versa.

The timeout is just to allow the switch to work properly as it cannot be toggled too quickly.

Hopefully this explains it all now!

closed as off-topic by joan, Milliways, Aurora0001, Gene, tlhIngan Jul 19 '18 at 15:54

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "This question does not appear to be specific to the Raspberry Pi within the scope defined in the help center." – joan, Milliways, Aurora0001, Gene, tlhIngan
If this question can be reworded to fit the rules in the help center, please edit the question.

2

State machines are awesome for programming problems like this. The trouble with designing with "if" statements alone is that they tend to force you unnaturally into thinking only in binary decisions. State machines are rich and versatile. A state machine can have as many states and transitions between those states as you wish and can be constructed using "if" statements.

Abstractly, your project might be represented as a state machine having these states:

STATE1: waiting for button press to activate both_on()
STATE2: waiting for button press to activate both_off()

Simple, right? Notice that the states are numbered so that adding a new state is easy. In your code, you have socket_on. I propose we use another variable called state instead. Using a variable state lets us represent as many states as we wish (e.g., state = 42). But let's stick with two states for now--you will no doubt add more later as you have more fun.

But these two states aren't sufficient. A state machine also has transitions, and transitions can get complicated. Let's take a stab at understanding the transitions.

STATE1 transitions: if button pressed then goto STATE2
STATE2 transitions: if button pressed then goto STATE1

But that's not enough, right? Your code has the notion of a 5 second timeout. Oh dear. Now life gets complicated. First, let's NOT use sleep(). We want our program to be aware, alive, nimble and responsive, not dead asleep. So let's use time(), which returns the current time immediately. Perhaps we store that time in a variable lastClick = time.time(). And now we can can create code that uses that variable.

I don't write Python. I write Javascript, which won't help you. Instead, here's a sketch that may help:

lastClick = 0
def state_machine():
    STATE1 = 1
    STATE2 = 2
    INITIAL_STATE = STATE1
    DEBOUNCE_SECONDS = 1
    state = INITIAL_STATE 
    global lastClick
    lastClick = 0 # wait for next button click
    while True:
        elapsed = time.time() - lastClick
        ###### state definitions and transitions
        if state == STATE1:  # STATE1 actions and transitions
           if lastClick != 0 and elapsed > DEBOUNCE_SECONDS:
               both_on()
               lastClick = 0  # we handled click
               state = STATE2  # goto STATE2
        elif state == STATE2:  # STATE2 actions and transitions
           if lastClick != 0 and elapsed > DEBOUNCE_SECONDS:
               both_off()
               lastClick = 0  # we handled click
               state = INITIAL_STATE  
        ##### transitions common to all states
        time.sleep(0.1)  # let the processor do other stuff

Now that's a LOT of code. But as you read it, you'll notice that it follows a certain design pattern that you can easily extend to do whatever you want with a button or even two buttons or three. The design pattern includes an infinite loop (!) that takes over the Pi while your program thread is running. To be good citizens, we need to allow the Pi to do other stuff, which is what the sleep(0.1) does--it allows other stuff to run for 100 milliseconds.

And are we done? Not quite.

We need one more important piece of code. When the button is clicked, do this and ONLY this:

global lastClick
lastClick = time.time()

Finally, we need to invoke our state_machine(). In a simple program, we could just call state_machine() at the end of the program. But our life is going to get messy...

Unfortunately, we are hacking an Amazon Dash button (which creates all sorts of problems). This presents a challenge of its own in that we need a sniff() call to check the Dash button. Sadly, the sniff() call ALSO runs an infinite loop. We can only have one infinite loop in each program thread. This means that we need to create a new thread for our state machine. Our two threads (i.e., state machine and sniff) will work in parallel. Change the end of your program as follows:

# mac_id_list line ...
import threading # put this at top of program if Python complains

# create separate thread for state machine
stateMachineThread = new threading.Timer(0.1, state_machine)
stateMachineThread.start()

# main thread will check Amazon Dash
sniff(prn=udp_filter, store=0, filter="udp", lfilter=lambda d: d.src in mac_id_list)

NOTE: When creating a state machine, the best coding construct is actually a switch statement.Amusingly, Python seems to have an aversion to switch statements.

Let's work on this together and see where it leads. State machines and Raspberry Pi's work really well together.

  • Comments are not for extended discussion; this conversation has been moved to chat. – Ghanima Jul 3 '18 at 20:20
  • @Ghanima thank you. The last time I tried that the OP took days to find the chat. I think we'll be fine. :D – OyaMist Jul 3 '18 at 20:36
  • @Ghanima it wouldn’t let me move the comments to a chat as I’m new here. Now all the useful stuff is gone. This seems counterintuitive! Also, I use the mobile app for Stack Exchange which doesn’t have the chat feature! Apart from OyaMistAeroponics, and a few others, this whole place seems full of pedants and jobsworths. – Wesley A Jul 4 '18 at 7:08
  • I am sorry you feel that way. The proper way for the "useful stuff" however is to include that into the answer(s) not bury it within twenty comments. To be honest that is what I would consider intuitive. I furthermore think that the claim that the mobile app does not provide the chat feature is not true. The mobile version does have its shortcomings but it should open the chat as linked above - it at least does at my phone. – Ghanima Jul 4 '18 at 10:32
  • @WesleyA the StackExchange software tortures moderators with review queues. I myself have to wade through two. Our lengthy conversation is no doubt triggering alerts which drive moderators batty. Out of consideration for them, we should roll over to a chat room. If that is infeasible, just email me at karl@oyamist.com. StackExchange is trying to change their software for the better, but it's tricky. – OyaMist Jul 4 '18 at 14:20

Not the answer you're looking for? Browse other questions tagged or ask your own question.