Funktionen gleichzeitig ablaufen lassen - Threads ungeeignet?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
KrisenKrieger
User
Beiträge: 5
Registriert: Samstag 15. Oktober 2022, 12:51

Hallo zusammen,

ich bin noch blutiger Anfänger und versuche seit zwei Tagen eine Lösung für mein erstes Projekt zu finden. Habe mich schon durch x Foren gewühlt
aber noch keine Lösung gefunden. Ich wäre Euch sehr dankbar wenn Ihr mir helfen könntet, so langsam bin ich mit meinem Latein am Ende... danke!

Folgende Situation:
Ich habe an einem Pi einen IR-Bewegungssensor angeschlossen. Ebenfalls angeschlossen ist ein 4-fach Relais Modul.
Nun möchte ich dass wenn der IR-Sensor eine Bewegung registriert und seinem GPIO-Port meldet, der Pi zwei der Relais "anzieht" (ebenfalls über GPIO-Ports) und dann unterschiedlich lange hält bevor er sie wieder abfallen lässt.
Dann geht alles wieder in Wartestellung bis wieder eine erneute Bewegung erkannt wird und beide Relais wieder angezogen werden usw.

Natürlich könnte ich die Relais erstmal beide anziehen, dann ein time.sleep setzen, dann das erste Relais abfallen lassen, dann ein time.sleep setzen
und dann das zweite Relais abfallen lassen...
Ich habe mir aber gedacht dass das auch schöner gehen muss - insbesondere wenn mehr als zwei Relais schalten möchte wird das ja irgendwann ein etwas unschöner "Stapel" Programmcode.

Meine Idee war für jedes Relais eine eigene Funktion zu bauen und diese dann aufzurufen wenn sicher der IR-Bewegungsmelder an seinem GPIO-Port meldet.
Soweit so gut, aber wie schaffe ich es zwei (oder mehr) Funktionen gleichzeitig anzustoßen?

Versucht habe ich es mit Threads - funktioniert zwar gut, aber ganz genau nur ein Mal, da ich dann die Meldung bekomme dass jeder Thread nur ein Mal laufen kann.
Ich benötige das allerdings quasi endlos... gibt es eine Möglichkeit das "nur-ein-Mal-Laufen" zu umgehen?

Oder lässt sich vielleicht mit dem "Event Detect" bzw. einem ähnlichen Befehl auch mehr als eine Funktion aufrufen???:
GPIO.add_event_detect(PIRSENSOR_GPIO, GPIO.RISING, callback=FunktionRelais1)

Kann mir das jemand bitte an einem einfachen Beispiel erklären?

Vielen Dank und Euch allen ein schönes WE !!!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da du keinen Code, sondern nur Prosa zeigst, kann man auch nur soviel dazu sagen. Grundsaetzlich sind Threads schon irgendwie geeignet. Genauso grundsaetzlich sind sie das mit Abstand komplexeste Thema in der Softwareentwicklung. Gerade Anfaenger glauben oft, sie haetten damit eine Loesung fuer ihre Probleme, weil jetzt ja vermeintlich alles geht. Wie jemand, der beim reinigen seines Segelboots denkt "fuer das benoetigte Wasser, da bohre ich mir einfach ein Loch in den Rumpf!" - loest erstmal das Problem, aber kreiert einen Sack neuer.

Es wird auch erstaunlich kompliziert, wenn man Zeit-basierte Ereignisse mit anderen, wie hier deinem PIR-Sensor, mischt. Denn unabhaengig von der Implementierung stellen sich hier ja schon eine ganze Reihe von Fragen (die man wegen der wenigen Informationen auch nicht beantworten kann, hier und jetzt): was passiert denn, wenn waehrend der Zeitraeume, welche die Relais angezogen sind, wieder ein PIR-Ereignis auftritt? Verlaengert sich die gesamte Sequenz, oder wird das ignoriert. Gibt es einen Unterschied in dem Fall, dass dieses Ereignis in der Zeit zwischen dem erster Relais schon wieder auf Ruhezustand, aber dem zweiten noch angezogen, auftaucht? Gibt es eine "cooldown"-Periode nach den jeweiligen Schaltzeitpunkten?

Abhaengig davon, wie die Antwort auf all diese Fragen aussieht, kann deine jetzige Loesung im Zweifel exakt das sein, was du suchst - nicht nur funktionierend, sondern auch noch "am schoensten". Denn es ist genau garnix damit gewonnen, irgendwelche Funktionen in Threads auszulagern,wenn man dann Zwecks Synchronisierung eh wieder auf die warten muss.

"mehr als zwei Relais" ist auch nicht klar. Sollen die sich genauso verhalten, wie die anderen? Einfach kaskadiert? Fahren die ganz andere Zyklen, abhaengig von ganz anderen Sensoren? Davon haengt ab, was man da am besten macht.
Benutzeravatar
hyle
User
Beiträge: 96
Registriert: Sonntag 22. Dezember 2019, 23:19
Wohnort: Leipzig

KrisenKrieger hat geschrieben: Samstag 15. Oktober 2022, 13:35 [...]
Meine Idee war für jedes Relais eine eigene Funktion zu bauen und diese dann aufzurufen wenn sicher der IR-Bewegungsmelder an seinem GPIO-Port meldet.
[...]
Warum das denn?

Meine Antwort geht zwar an der eigentlichen Frage "Funktionen gleichzeitig ablaufen lassen" vorbei und ist ggf. auch etwas unorthodox, aber anhand der hier bekannten Infos würde ich einfach etwas fertiges nehmen.
Dafür wäre https://gpiozero.readthedocs.io/en/stab ... l#ledboard (das leds.value betreffend) in einer Funktion IMHO ganz gut geeignet. Das könnte man sich zwar auch selber stricken, aber das Endergebnis wäre ja das selbe.

Btw. Bewegungsmelder kennt gpiozero auch: https://gpiozero.readthedocs.io/en/stab ... ion-sensor
Alles was wir sind ist Sand im Wind Hoschi.
KrisenKrieger
User
Beiträge: 5
Registriert: Samstag 15. Oktober 2022, 12:51

Hallo, vielen Dank schon mal für Eure Antworten! Ich stelle gleich noch meine beiden erfolgreichsten Versuche ein. :-)

Noch kurz zum Hintergrund:
Ich möchte den PI zu Halloween nutzen und quasi eine Geisterbahn bauen. Sobald also ein Kind am Bewegungsmelder vorbei läuft,
sollen verschiedene Aktionen ausgelöst werden. Dieses Jahr wollte ich mich auf eine Nebelmaschine und eine Animatronicpuppe beschränken.
Wenn das gut funktioniert, sollen nächstes Jahr noch zwei weitere Puppen mit eingebunden werden (daher zwei weitere Relais), ggf. auch ein zweiter PIR-Sensor.

Mit nur einer Nebelaschine und einer Puppe mag es dieses Jahr noch gut mit dem Nacheinanderabarbeiten von Befehlen funktionieren. Jetzt aber noch zwei Puppen
und eventuell noch ein zweiter PIR-Sensor... mhh
Wenn es von den zeitlichen Abständen/ Wartezeiten etc. überhaupt machbar sein wird all diese Befehle hintereinander abzuarbeiten ohne das ich an irgendeiner Stelle dann "zu spät komme"
bzw. sich irgendetwas so überschneidet dass der Ablauf nicht sauber ist, wird es irgendwann ziemlich unübersichtlich werden.
Und wenn ich dann im nächsten Jahr neu kombiniere oder etwas altes gegen was neues austausche, muss ich mir wieder den Kopf machen wie ich diesen "Stapel an Befehlen"
sortiere damit die Geisterbahn so abläuft wie sie soll.

Daher dachte ich mir, dass es am übersichtlichsten wäre für jedes Relais eine eigene Funktion zu haben, so dass ich in jeder Funktion Änderungen vornehmen kann ohne dass es die andere
Abläufe beeinträchtigt - mal abgesehen von der Gesamtlaufzeit eines Durchgangs natürlich.
Daher ja, ein wenig synchronisiert werden die Funktionen dann immer ablaufen (alles wartet am Ende auf die am längsten dauernde Funktion).
Den PIR würde ich dann am besten in den Schlaf versetzen.

Ich hoffe mit dieser Erläuterung noch für etwas Aufklärung sorgen.

Viele Grüße und vielen Dank im Voraus!
KrisenKrieger
User
Beiträge: 5
Registriert: Samstag 15. Oktober 2022, 12:51

So, die ganz einfache Variante (hintereinander weg):

import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

IRSENSOR_PIN = 27
GPIO.setup(IRSENSOR_PIN, GPIO.IN)

RELAIS_1_GPIO = 23
GPIO.setup(RELAIS_1_GPIO, GPIO.HIGH)
GPIO.setup(RELAIS_1_GPIO, GPIO.LOW)
GPIO.setup(RELAIS_1_GPIO, GPIO.OUT)

RELAIS_2_GPIO = 22
GPIO.setup(RELAIS_2_GPIO, GPIO.HIGH)
GPIO.setup(RELAIS_2_GPIO, GPIO.LOW)
GPIO.setup(RELAIS_2_GPIO, GPIO.OUT)


def Grusel(channel):

print("!Achtung, Kids kommen!")
GPIO.output(RELAIS_1_GPIO ,GPIO.LOW)
GPIO.output(RELAIS_2_GPIO ,GPIO.LOW)
time.sleep(1)
GPIO.output(RELAIS_1_GPIO ,GPIO.HIGH)
print("Relais 1 aus")
time.sleep(2)
GPIO.output(RELAIS_2_GPIO ,GPIO.HIGH)
print("Relais 2 aus")
print("Warte...")
print(" ")

print("Gestartet...")

try:

GPIO.add_event_detect(IRSENSOR_PIN, GPIO.RISING, callback=Grusel)
while True:
time.sleep(2)


except KeyboardInterrupt:
print("Beende...")
GPIO.cleanup()
KrisenKrieger
User
Beiträge: 5
Registriert: Samstag 15. Oktober 2022, 12:51

Und hier der Versuch mit den beiden Threads:


import RPi.GPIO as GPIO
import time
from threading import Thread

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

IRSENSOR_PIN = 27
GPIO.setup(IRSENSOR_PIN, GPIO.IN)

RELAIS_1_GPIO = 23
GPIO.setup(RELAIS_1_GPIO, GPIO.OUT)
GPIO.setup(RELAIS_1_GPIO, GPIO.HIGH)
GPIO.setup(RELAIS_1_GPIO, GPIO.LOW)

RELAIS_2_GPIO = 22
GPIO.setup(RELAIS_2_GPIO, GPIO.OUT)
GPIO.setup(RELAIS_2_GPIO, GPIO.HIGH)
GPIO.setup(RELAIS_2_GPIO, GPIO.LOW)


def Grusel1():
GPIO.output(RELAIS_1_GPIO ,GPIO.LOW)
time.sleep(1)
GPIO.output(RELAIS_1_GPIO ,GPIO.HIGH)
print("Relais 1 aus")
time.sleep(1)


def Grusel2():
GPIO.output(RELAIS_2_GPIO ,GPIO.LOW)
time.sleep(2)
GPIO.output(RELAIS_2_GPIO ,GPIO.HIGH)
print("Relais 2 aus")
time.sleep(2)


thread_1 = Thread(target=Grusel1)
thread_2 = Thread(target=Grusel2)


def Callbackgesamt(channel):
print("!Achtung, Kids kommen!")
thread_1.start()
thread_2.start()


try:
GPIO.add_event_detect(IRSENSOR_PIN, GPIO.RISING, callback=Callbackgesamt)


except KeyboardInterrupt:

print ("Ende...")

GPIO.cleanup()
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

In der Geisterbahn ist vor allem der Code zum Gruseln. `as` bei `import` ist zum Unbenennen da, GPIO wird aber gar nicht umbenannt.
Warnungen sind dazu da, dass man sie behebt, nicht dass man sie ignoriert. Dazu fehlt aber der Code, dass GPIO.cleanup auch sicher aufgerufen wird.
GPIO.setup soll IN oder OUT verstlegen, das mit HIGH und LOW aufzurufen ist totaler Quatsch.
Eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht mal 5 und dann nur eines.

Um mehrere Aktionen zu synchornisieren benutzt man einen Scheduler:

Code: Alles auswählen

from RPi import GPIO
import sched

IRSENSOR_PIN = 27
RELAIS_1_PIN = 23 
RELAIS_2_PIN = 22 

def initialize():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(IRSENSOR_PIN, GPIO.IN)
    GPIO.setup(RELAIS_1_GPIO, GPIO.OUT)
    GPIO.setup(RELAIS_2_GPIO, GPIO.OUT)

def trigger(pin, scheduler, delay):
    GPIO.output(pin, GPIO.LOW)
    scheduler.enter(delay, 1, GPIO.output, (pin, GPIO.HIGH))

def main():
    try:
        initialize()
        scheduler = sched.scheduler()
        while True:
            timeout = scheduler.run(False)
            channel = GPIO.wait_for_edge(IRSENSOR_PIN, GPIO.RISING, timeout=timeout)
            if channel is not None:
                trigger(RELAIS_1_PIN, scheduler, 1)
                trigger(RELAIS_2_PIN, scheduler, 2)
    finally:
        GPIO.cleanup()

if __name__ == "__main__":
    main()
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Also so wie das klingt, ist es ein eher simples Ding: ein PIR schaltet eine Reihe von Relais in festen Intervallen. Bis die durchgelaufen sind, gibt es keine Neu-Ausloesung.

Der Pi ist eigentlich viel zu dick fuer sowas. Ich bin mir noch nicht mal sicher, dass es dafuer einen Mikro braucht. Wahrscheinlich kann man da mit einer Handvoll Logikgatter und einem NE555 aehnliches erreichen.

Aber wenn wir den Pi mal annehmen, dann empfehle ich dir, das einfach durch zwei getrennte Skripte zu machen. Jedes hat seinen PIR-Pin, und schaltet seine Relais. Beide werden gestartet. Das war's.

Natuerlich ist das nicht die Loesung, die *ich* schreiben wuerde. Ich wuerde wahrscheinlich zu asyncio greifen, wegen des linearen Programmiermodells, das damit moeglich ist. Aber das, genauso wie Sirius3 Loesung, ist einfach zu komplex fuer den Anfaenger, der eignetlich auch nicht wirklich Programmieren lernen will. Sondern nur eine Loesung haben.

Einen Pi Pico dafuer zu nehmen wuerde aber auch preiswert und einfach sein. Einen *pro* Schaltfunktion! Weil auch da diese Verschraenkung der zeitlichen Ereignisse sonst ueberfordert vermute ich mal.
KrisenKrieger
User
Beiträge: 5
Registriert: Samstag 15. Oktober 2022, 12:51

Hallo zusammen,

vielen Dank für die Hilfestellung, vor allem Dir Sirius3, das hat mir enorm weitergeholfen.
Ich wollte mich eigentlich schon früher zurückgemeldet haben, hatte aber in den letzten Tagen nicht viel Zeit.
Nachdem ich mich durch Deinen Code durchgearbeitet hatte, habe ich diesen noch ein wenig erweitert.
Ich würde mich freuen wenn Du oder Ihr da mal einen Blick drauf werfen könntet.
Würde mich interessieren, was man wie noch besser machen könnte - man lernt ja nie aus!

Kurz zur Erläuterung:
Ich habe nun zwei Triggerfunktionen gebaut. Triggerlong um ein Relais für eine gewisse Zeit lang angezogen zu halten,
in diesem Fall um eine Nebelmaschine für die angegebene Dauer Nebel produzieren zu lassen, sowie triggershort mit der man die Animatronic-Puppe(n) aktivieren
aber nach eingestellter Zeit auch wieder deaktivieren kann (einige von den Puppen "jammern" sonst viel zu lang, dem muss mit einem erneuten Trigger dann einen Riegel vorgeschoben werden).

Mit der Variable "aktiv", der Funktion "pause" und einem kleinen if-Block habe ich dann noch eine Art Deaktivierung des IR-Sensors auf Zeit eingebaut,
damit die Kinder nicht beim Verlassen der Auffahrt alles nochmal auslösen. Das ist zwar noch etwas behelfsmäßig, aber eine andere Lsöung sehe ich da erstmal nicht.
Ggf. werde ich es im kommenden Jahr mal mit einem zweiten IR-Sensor ein paar Meter weiter versuchen, so dass ich mit beiden IR-Sensoren zusammen ermitteln kann ob gerade jemand kommt oder geht.

... eine Kleinigkeit habe ich noch auf dem Zettel.
Wie Ihr seht frage ich in den beiden Trigger-Funktionen auch häufig den Zustand der GPIOs ab, um mir eine Art Live-Protokoll anzeigen lassen zu können.
Es ist einfach hilfreich sehen zu können welcher GPIO bzw. welches Relais in welcher Reihenfolge geschaltet wurde.
So wie die Abfrage-Befehle dort jetzt stehen, bekomme ich als Ausgabe die GPIO-Nummer ausgegeben. Schöner und übersichtlicher wäre es den Namen, z.B. RELAIS_4, angezeigt zu bekommen.
Ich weiß ich übergebe den "pin", aber wie verknüpfe ich das wieder mit dem Namen?

Vielen Dank und ein schönes WE !

P.S.: Kann übrigens jemand ein gutes Python-Buch empfehlen?

Code: Alles auswählen


from RPi import GPIO
import sched

IRSENSOR = 27 
RELAIS_1 = 23 
RELAIS_2 = 22 
RELAIS_3 = 12 
RELAIS_4 = 20 

aktiv = 1

def abfrage(pin):
    if GPIO.input(pin) == GPIO.HIGH:
        print ("GPIO - {} - aus!".format(pin))
    elif GPIO.input(pin) == GPIO.LOW:
        print ("GPIO - {} - ein!".format(pin))
    else:
        print ("fehler abfrage")

def initialisieren(): 
    GPIO.setmode(GPIO.BCM) 
    GPIO.setup(IRSENSOR, GPIO.IN) 
    GPIO.setup(RELAIS_1, GPIO.OUT) 
    GPIO.setup(RELAIS_2, GPIO.OUT) 
    GPIO.setup(RELAIS_3, GPIO.OUT) 
    GPIO.setup(RELAIS_4, GPIO.OUT)

def triggerlong(pin, scheduler, delaylong):
    scheduler.enter(0, 1, GPIO.output, (pin, GPIO.LOW))
    scheduler.enter(0.1, 1, abfrage, (pin,))
    scheduler.enter(delaylong, 1, GPIO.output, (pin, GPIO.HIGH)) 
    scheduler.enter(delaylong+0.1, 1, abfrage, (pin,))
    
def triggershort(pin, scheduler, delayshort): 
    scheduler.enter(0, 1, GPIO.output, (pin, GPIO.LOW)) 
    scheduler.enter(0.1, 1, abfrage, (pin,))
    scheduler.enter(0.5, 1, GPIO.output, (pin, GPIO.HIGH)) 
    scheduler.enter(0.6, 1, abfrage, (pin,))
    scheduler.enter(delayshort, 1, GPIO.output, (pin, GPIO.LOW))
    scheduler.enter(delayshort+0.1, 1, abfrage, (pin,))
    scheduler.enter(delayshort+0.5, 1, GPIO.output, (pin, GPIO.HIGH))
    scheduler.enter(delayshort+0.6, 1, abfrage, (pin,))

def pause():
    global aktiv
    aktiv = 1
    
def main():
    global aktiv
    print("Programm gestartet")
    print("=======================================")
    try:
        initialisieren() 
        Zeitplan = sched.scheduler()
        while True: 
            timeout = Zeitplan.run(False)
            channel = GPIO.wait_for_edge(IRSENSOR, GPIO.RISING, timeout=60)
            if channel is not None:
                if GPIO.input(27) == False:
                    pass
                    print("GPIO.input(27) == False")
                if GPIO.input(27) == True  and  aktiv == 1:   
                    triggershort(RELAIS_1, Zeitplan, 8) #23
                    triggerlong(RELAIS_2, Zeitplan, 3) #22
                    triggershort(RELAIS_3, Zeitplan, 5) #12
                    triggershort(RELAIS_4, Zeitplan, 5) #20
                    aktiv = 0
                    Zeitplan.enter(30, 1, pause)
                    print()
                
    finally:
        GPIO.cleanup()

if __name__ == "__main__":
    main()
    




Antworten