Welcome!
I find your question quite interesting as it requires careful modelling of the different states and transitions between them. I feel your question only partially related to the Raspberry PI and is more specifically related to modelling states, events, state transitions and behaviors which might make some moderators mark it as off-topic.
Anyway, based on my understanding of your requirement, I have written up a python script (not tested at all) which I hope will help you in some way. if you find any errors or require any clarifications, please leave a comment below
import RPi.GPIO as GPIO
import time
import threading
from subprocess import Popen
GPIO_LED = 11
GPIO_SENSORS = [13, 15, 19]
GPIO_BUTTON = 21 # change 21 to match your requirement
GPIO_RELAY = 23 # change 23 to match your requirement
# models the button behavior
# button keeps track of whether it is enabled or not
# it gets enabled when all 3 sensors inputs are low
# it gets disabled when one of the following happens
# one or more sensor inputs goes high
# after being enabled, it was pushed by user which caused a relay be activated, a song played after which the relay was deactivated (see below)
# if the button is physically pushed when disabled, the push is ignored
# if the button is physically pushed when enabled,
# the relay is turned,
# a song is played
# at the end of the song
# relay is turned off
# button gets disabled
# if the button is physically pushed when the song is being played the additional push is ignored
# button state can be reset either internally or externally
# internal reset happens when the background thread launched in response to the button push exits
# external reset happens when one of the sensor state goes high
# internal reset has higher priority than external
# (i.e) if a song was being played in response to the button push and the sensor input goes high
# we wait until the song finishes and then reset the button state
# while the song is being played if sensor inputs transition back to ALL LOW, the button will still
# be in enabled state and have a consistent state
class Button(object):
def __init__(self):
self.enabled = False
self.pushCount = 0
self.thread = None
# ugly workaround since the API does provide a way to setup a callback when a subprocess exits
# we first launch a background thread,
# within which we launch the subprocess
# wait for the subprocess to exit
# execute our callback
# and then exit the background thread
# see http://stackoverflow.com/questions/2581817/python-subprocess-callback-when-cmd-exits
def playInBackground(self):
def threadFunc():
# turn on relay
GPIO.output(GPIO_RELAY, HIGH)
# play some ♬, change to match your requirement
player = subprocess.Popen(["/usr/bin/mpg123", "/home/pi/sound.mp3"])
# wait for player process to finish
player.wait()
# turn off relay
GPIO.output(GPIO_RELAY, LOW)
# reset internal state, True indicates internal reset
self.reset(True)
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
# returns immediately after the thread starts
return thread
def push(self):
# ignore push if disabled
if not self.enabled:
return
# if already pushed the button once, ignore the push
if self.pushCount != 0:
return
self.pushCount = 1
# hold on to the background thread so that the GC doesnt pick it up
self.thread = self.playInBackground()
def reset(self, isInternalReset):
if not isInternalReset: # if external reset
# if we have a background thread running,
# let that thread's exit reset the state
if self.thread is not None:
return
self.thread = None
self.pushCount = 0
self.enabled = False
def enable():
GPIO.output(GPIO_LED, HIGH)
class SystemState(object):
def __init__(self):
self.button = Button()
self.channels = {}
for sensor in GPIO_SENSORS:
self.channels[sensor] = True # should match the GPIO.PUD_UP below
def onSensorFallingEdge(self, channel):
self.channels[channel] = False
if all(not x for x in self.channels):
self.button.enabled = True
def onSensorRaisingEdge(self, channel):
self.channels[channel] = False
if any(x for x in self.channels):
self.button.reset(False) # False indicates external reset
def onButtonPush(self):
button.push()
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(GPIO_LED, GPIO.OUT)
GPIO.setup(GPIO_RELAY, GPIO.OUT)
GPIO.setup(GPIO_SENSORS, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.setup(GPIO_BUTTON, GPIO.IN, pull_up_down = GPIO.PUD_UP)
systemState = SystemState()
def onSensorChanged(channel):
if GPIO.input(channel): # if raising edge
systemState.onSensorRaisingEdge(channel)
else
systemState.onSensorFallingEdge(channel)
GPIO.add_event_detect(SENSORS, GPIO.BOTH, callback=onSensorChanged)
GPIO.add_event_detect(BUTTON, GPIO.FALLING, callback=lambda: systemState.onButtonPush())
try:
while True:
time.sleep(0.1)
GPIO.cleanup()
except KeyboardInterrupt:
GPIO.cleanup()