Zwei while-Schlaufen parallel laufen lassen in Python?

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
Swi_Mo
User
Beiträge: 5
Registriert: Donnerstag 15. September 2016, 08:27

Liebe Insider vom Forum

Ich bin Lehrer und arbeite mit einem Schüler an einem "Torzähler", den wir in einen Tischkicker einbauen wollen. Wir arbeiten...
- mit einem Raspberry Pi
- einem LED-Display von Adafruit
- zwei Druckschaltern (für beide Kickertore)
- und programmieren mit Python

Wir sind Python-Anfänger und versuchen, das nötige Wissen im Selbststudium zu erlangen. Bei folgendem Programmier-Problem kommen wir derzeit nicht weiter:

Der Kern unseres Programms sind Abfragen der beiden Druckschalter, welche ständig laufen sollen. Wird der eine Druckschalter betätigt (bei einem Tor), so soll die eine Variable (Torstand der einen Mannschaft) hochgezählt werden; beim anderen Druckschalter die andere Variable. Die beiden Variabeln werden ständig aktuell im Display angezeigt. Gemäss unserer Logik würden sich dafür zwei parallel laufende while-Schlaufen eignen. Allerdings finden wir keine funktionierende Syntax, wie wir das hinkriegen könnten. Hat jemand Tipps oder hilfreiche Links?

Besten Dank im Voraus!
Swi_Mo
BlackJack

@Swi_Mo: Das einfachste wäre in *einer* ``while``-Schleife beide Schalter zu prüfen. Ansonsten muss man entweder selber anfangen etwas Nebenläufiges zu bauen (`threading`-Modul), oder das verwenden was die Bibliothek bietet, mit der die GPIOs angesprochen werden.

Ein Mittelding wäre selber für die Nebenläufigkeit zu sorgen, aber in den beiden Threads keine ``while``-Schleife zu verwenden, sondern `Button.wait_for_press()` beim `gpiozero`-Package oder `wait_for_edge()` bei `RPi.GPIO`. Das hat den Vorteil, dass diese Bibliotheken das intern effizienter lösen können als tatsächlich CPU Zeit mit warten zu verbrennen („busy waiting“) falls die Hardware Interrupts dafür bietet.

Wenn man die Nebenläufigkeit nicht selber programmieren möchte, bieten sowohl `gpiozero` als auch `RPi.GPIO` die Möglichkeit eine Rückruffunktion für das drücken, loslassen, oder beides zu registrieren. `when_pressed` und `when_released` bei `Button`-Objekten bzw. `add_event_detect()`.

Wenn man die Threads selbst schreibt und mit einer `Queue.Queue` mit dem Hauptthread kommuniziert, kommt man mit Funktionen aus, bei den Rückruffunktionen muss man sich wohl entweder Closures oder objektorientierte Programmierung anschauen wenn das sauber werden soll.

Das hat übrigens alles nichts mit Syntax zu tun. :-)
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Hier mal eine funktionierende Lösung auf die Schnelle. Kann sicherlich optimiert werden ;)

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8

from __future__ import print_function
from RPi import GPIO
import signal
from itertools import count
from functools import partial


PINS = [20, 21] 


def setup_gpio():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(PINS, GPIO.IN)

    
def print_score(counter_team_red, counter_team_blue, pin):
    if pin == 20:
        print('Team Red: {}'.format(counting_goals_team_red(counter_team_red)))
    else:
        print('Team Blue: {}'.format(counting_goals_team_blue(counter_team_blue)))


def counting_goals_team_red(counter_team_red):
    return counter_team_red.next()

    
def counting_goals_team_blue(counter_team_blue):
    return counter_team_blue.next()

def main():
    setup_gpio()
    counter_team_red = count()
    counter_team_blue = count()
    try:
        for pin in PINS:
            GPIO.add_event_detect(pin, GPIO.RISING, callback=partial(print_score, counter_team_red, counter_team_blue), bouncetime=500)
        signal.pause()
    except KeyboardInterrupt:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
BlackJack

@lackschuh: Zwei Verbesserungsvorschläge: Das Aufräumen würde ich in einen ``finally``-Zweig verlegen, denn sonst wird bei anderen Ausnahmen nicht aufgeräumt. Und der Vergleich mit 20 ist sehr magisch, vor allem steht diese literale 20 zweimal im Quelltext.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Kleiner Zusatz noch zum oben Erwähnten:
``count(1)`` mit dem Startwert 1 versehen, da sonst das erste Tor mit 0 angezeigt wird
BlackJack

Eine konzeptionell recht einfache Variante bei der der Spielstand im üblichen A:B-Format ausgegeben werden kann, die `gpiozero` und Rückrufe mit einer Queue verwendet (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8
from __future__ import absolute_import, division, print_function
from functools import partial
from Queue import Queue
from gpiozero import Button

TEAM_A_PIN = 20
TEAM_B_BIN = 21


def main():
    goals = Queue()

    team_a_button = Button(TEAM_A_PIN)
    team_a_button.when_pressed = partial(goals.put, TEAM_A_PIN)

    team_b_button = Button(TEAM_B_BIN)
    team_b_button.when_pressed = partial(goals.put, TEAM_B_BIN)

    try:
        team_a_score = team_b_score = 0
        while True:
            team_pin = goals.get()

            if team_pin == TEAM_A_PIN:
                team_a_score += 1
            elif team_pin == TEAM_B_BIN:
                team_b_score += 1
            else:
                assert False, 'unknown team {0!r}'.format(team_pin)

            print('A:B = {0}:{1}').format(team_a_score, team_b_score)
    except KeyboardInterrupt:
        pass
    finally:
        for device in [team_a_button, team_b_button]:
            device.close()


if __name__ == '__main__':
    main()
Man sieht hier an den Namenspräfixen `team_x_*` ganz gut, dass sich selbst hier schon OOP anbieten würde, weil so ein Team offensichtlich aus mehreren Einzelteilen besteht, die man zu einem Verbundtyp zusammenfassen kann/sollte.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

@BlackJack
Ich kannte das ``gpiozero`` Modul noch gar nicht. Sieht aber auf den ersten Blick interessant aus...

Code: Alles auswählen

Traceback (most recent call last):
  File "taster.py", line 42, in <module>
    main()
  File "taster.py", line 16, in main
    team_a_button.when_pressed = partial(goals.put, TEAM_A_PIN)
  File "/usr/local/lib/python2.7/dist-packages/gpiozero/devices.py", line 159, in __setattr__
    return super(GPIOBase, self).__setattr__(name, value)
  File "/usr/local/lib/python2.7/dist-packages/gpiozero/mixins.py", line 209, in when_activated
    self._when_activated = self._wrap_callback(value)
  File "/usr/local/lib/python2.7/dist-packages/gpiozero/mixins.py", line 283, in _wrap_callback
    'value must be a callable which accepts up to one '
gpiozero.exc.BadEventHandler: value must be a callable which accepts up to one mandatory parameter
BlackJack

@lackschuh: Nachdem ich da in den Quelltext geschaut habe, würde ich sagen das ist die Schuld von `gpiozero`. Die machen da zu viel (indirekte) Typprüfung und schliessen somit `partial`-Objekte aus. Also im Grunde *alle* aufrufbaren Objekte die in C geschrieben sind deren Funktionssignatur man nicht mit `inspect`-Mitteln anschauen kann. Da ist `partial` ja nicht das einzige. Sie haben eine spezielle Prüfung für in Python eingebaute Objekte, denn da geht das auch nicht. Das geht irgendwie gegen „duck typing“. :-(

Edit: Ich hab mal ein Issue dazu aufgemacht: https://github.com/RPi-Distro/python-gp ... issues/436
BlackJack

Die machen diese Magie ja unter anderen um zu sehen ob sie die Rückruffunktion ohne Argumente oder mit einem, nämlich dem Objekt auf dem sie gesetzt wird, aufgerufen werden soll. Dann kommt man ohne `partial()` aus wenn man die Team-Buttons als Vergleich wer das Tor gemacht hat, verwendet (ungetestet und in der Hoffnung das das niemand mit einem Python ausprobiert wo `Queue.put()` in C implementiert ist…):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8
from __future__ import absolute_import, division, print_function
from Queue import Queue
from gpiozero import Button
 
TEAM_A_PIN = 20
TEAM_B_BIN = 21


def make_team_button(queue, pin):
    button = Button(pin)
    button.when_pressed = queue.put
    return button


def main():
    goals = Queue()
 
    team_a_button, team_b_button = buttons = [
        make_team_button(goals, p) for p in [TEAM_A_PIN, TEAM_B_BIN]
    ]
    try:
        team_a_score = team_b_score = 0
        while True:
            team_button = goals.get()
 
            if team_button == team_a_button:
                team_a_score += 1
            elif team_button == team_b_button:
                team_b_score += 1
            else:
                assert False, 'unknown team {0!r}'.format(team_button)
 
            print('A:B = {0}:{1}').format(team_a_score, team_b_score)
    except KeyboardInterrupt:
        pass
    finally:
        for button in buttons:
            button.close()
 
 
if __name__ == '__main__':
    main()
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

Läuft. Allerdings muss ich bei meinen empfindlichen Tastern der Button Klasse noch ``bounce_time=.5`` übergeben, sonst werden zu viele Tore gezählt mit einem Druck.
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

Solangsam lohnt sich eine Team-Klasse

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf-8
from __future__ import absolute_import, division, print_function
from Queue import Queue
from gpiozero import Button
 
TEAM_PINS = [20, 21]

class Team(Button):
    def __init__(self, pin, queue):
        Button.__init__(self, pin)
        self.when_pressed = queue.put
        self.score = 0
 
def main():
    goals = Queue()
    teams = [Team(pin, goals) for pin in TEAM_PINS]
    try:
        while True:
            team = goals.get()
            team.score += 1
            print('A:B = {teams[0].score}:{teams[1].score}').format(teams=teams)
    except KeyboardInterrupt:
        pass
    finally:
        for team in teams:
            team.close()
 
if __name__ == '__main__':
    main()
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

@Sirius3
Zeile 22 `.format` innerhalb der Klammern. Dann funktioniert es problemlos ;)
Swi_Mo
User
Beiträge: 5
Registriert: Donnerstag 15. September 2016, 08:27

Wow!

Ich bedanke mich herzlich für all diese Beiträge. Wir treffen uns bloss wöchentlich zu diesem Projekt und werden damit nächste Woche sicher weiter kommen!
Swi_Mo
User
Beiträge: 5
Registriert: Donnerstag 15. September 2016, 08:27

Wir sind weiterhin dran und werden die oben gemachten Vorschläge testen. Nun beginnen jedoch die Schulferien und wir melden uns bei Bedarf im Oktober wieder.
Swi_Mo
User
Beiträge: 5
Registriert: Donnerstag 15. September 2016, 08:27

Guten Tag allerseits

Unsere zwei Schlaufen laufen inzwischen. Hier ist das aktuelle ganze Programm:

Code: Alles auswählen

#!/usr/bin/python
from Adafruit_7Segment import SevenSegment           # Anschluss des Displays gemaess Dokumentation auf der Adafruit Webseite
import os                          
import time	                 
import RPi.GPIO as GPIO    


GPIO.setmode(GPIO.BCM)
GPIO.cleanup()
GPIO.setwarnings(False) 
GPIO.setup(27,GPIO.IN,pull_up_down=GPIO.PUD_UP)    # Dieses Setup ist von Youtube-Video
GPIO.setup(24,GPIO.IN,pull_up_down=GPIO.PUD_UP)    # Connecting a Push Switch with Raspberry Pi

print"------------------"
print"Programm fuer Tischkicker mit zwei Druckschaltern. Torstands-Ausgabe an Adafruit 7-Segment Display"
print"------------------"

segment = SevenSegment(address=0x70)

Rot = 0
Blau = 0

# Die folgenden 7 Zeilen definieren, wie die beiden Torstaende am Display anzezeigt werden
def aktualisiereAnzeige (Team, Punktestand):
 if Team == "rot":
  segment.writeDigit(0, int(Punktestand / 10))    
  segment.writeDigit(1, Punktestand % 10)
 if Team == "blau":
  segment.writeDigit(3, int(Punktestand / 10))
  segment.writeDigit(4, Punktestand % 10)

aktualisiereAnzeige("rot",Rot)    # Diese 2 Zeilen, damit von Anfang an der Punktestand gezeigt wird.
aktualisiereAnzeige("blau",Blau)

# Hier sind die zwei parallel laufenden Schlaufen
while True:
 if ( GPIO.input(27)!=True ):   # !=True kehrt den Ausgabewert des Schalterzustands um. Ohne dieses Element zaehlen die Torstaende bei nicht gedruecktem Schalter hoch. Druecken der Schalter unterbricht das Hochzaehlen          
  Rot = Rot + 1    
  aktualisiereAnzeige("rot",Rot)
        
 if ( GPIO.input(24)!=True ):
  Blau = Blau + 1
  aktualisiereAnzeige("blau",Blau)
       
 time.sleep(0.10)  
Zuletzt geändert von Anonymous am Montag 12. Dezember 2016, 16:00, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@Swi_Mo: Eine Einrücktiefe von *einem* Leerzeichen pro Ebene ist arg wenig. Konvention sind vier Leerzeichen. Und auch welche nach Kommata und um binäre Operatoren, damit es leichter lesbar ist.

Das `os`-Modul wird importiert, aber nicht verwendet.

`GPIO.cleanup()` also Aufräumen, gehört ans Ende des Programms und es sollte sichergestellt werden, dass das auch tatsächlich aufgerufen wird. Also zum Beispiel auch wenn das Programm durch eine Ausnahme endet. Dann kann man sich auch das ausstellen der Warnungen sparen. Bei Warnungen sollte man überlegen was falsch läuft und sie nicht ignorieren.

Hauptprogramm und Definitionen von Funktionen sollte man nicht vermischen und das Hauptprogramm sollte auch nicht direkt auf Modulebene stehen, sondern auch in einer Funktion. Wenn man das macht, dann fällt auf das die Funktion zur Aktualisierung der Anzeige einfach so auf `segment` zugreift ohne diesen Wert als Argument übergeben zu bekommen. Der Kommentar erscheint überflüssig. Die Angabe 7 Zeilen zu genau und man müsste das immer anpassen wenn man etwas an der Funktion ändert so das sich die Zeilenanzahl ändert. Wenn man schon beschreibt was eine Funktion macht, dann wäre das als Docstring besser aufgehoben als als Kommentar.

In der Funktion wird in den beiden ``if``-Zweigen fast das gleiche gemacht. Die unterscheiden sich nur durch den Index der Ziffern für die Anzeige. Die Bedigungen beeinflussen also eigentlich nur diesen Index. Die beiden Abfragen schliessen sich auch gegenseitig aus, also ist für den zweiten Zweig auch eher ein ``elif`` angebracht. Und dann würde ich noch ein ``else`` hinzufügen das den Fall behandelt das etwas anderes als `team` übergeben wurde.

Der `int()`-Aufruf ist überflüssig. Wenn man Python 3 berücksichtigen möchte, könnte man an der Stelle den Ganzahldivisionsoperator ``//`` verwenden.

Werte die man im Programm mehrfach wiederholt und/oder die man einfach anpassen können möchte, werden am Anfang als Konstanten definiert. Magische Zahlen haben so dann auch gleich einen Namen an dem der Leser erkennt wofür die gut sind.

`GPIO.setup()` kann man auch mehr als einen Pin übergeben.

`Rot` und `Blau` vermitteln nicht wirklich, dass es sich dabei um Punkte handelt.

Der Kommentar mit den parallelen Schlaufen (sagt das tatsächlich jemand so?) ist verwirrend weil da weder zwei Schleifen noch irgend etwas paralleles folgt. Das ist eine Schleife mit sequentieller Abfrage der beiden Taster (oder wirklich Schalter?).

Die Klammern gehören da nicht um die Bedingung und auf literale Wahrheitswerte vergleicht man nicht. Da kommt nur wieder ein Wahrheitswert bei heraus, also kann man entweder gleich den Ausgangswert nehmen, oder wie in diesem Fall seine Negation mit ``not``.

Beim Kommentar fand ich den letzten Satz ein wenig verwirrend. Den sollte man in Bezug zu dem davor geschriebenen setzen. Ausserdem würde ich das was die **input()**-Funktion liefert nicht als *Ausgabewert* bezeichnen.

Da nicht auf die Flanke reagiert wird, sondern wirklich die ganze Zeit mit Zehntelsekunden-Päuschen auf geschlossenen Kontakt geprüft wird, ist das ein bisschen fehleranfällig für zu langes Drücken IMHO.

Code: Alles auswählen

#!/usr/bin/env python
import time
from Adafruit_7Segment import SevenSegment
from RPi import GPIO

ROT, BLAU = 'rot', 'blau'
TEAM_PINS = ROTES_TEAM_PIN, BLAUES_TEAM_PIN = [27, 24]


def aktualisiere_punkte_anzeige(segment, team, punktestand):
    if team == ROT:
        index = 0
    elif team == BLAU:
        index = 3
    else:
        assert False, 'Falscher Wert fuer `team`: {}'.format(team)
    segment.writeDigit(index, punktestand // 10)
    segment.writeDigit(index + 1, punktestand % 10)


def main():
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(TEAM_PINS, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        segment = SevenSegment(address=0x70)

        print '------------------'
        print 'Programm fuer Tischkicker mit zwei Druckschaltern.'
        print 'Torstands-Ausgabe an Adafruit 7-Segment Display'
        print '------------------'

        punkte_rot = 0
        punkte_blau = 0

        aktualisiere_punkte_anzeige(segment, ROT, punkte_rot)
        aktualisiere_punkte_anzeige(segment, BLAU, punkte_blau)

        while True:
            # ``not`` kehrt den Eingabewert des Schalterzustands um.
            # Ohne diese Operation zaehlen die Torstaende bei nicht
            # gedruecktem Schalter hoch, sondern druecken der Schalter 
            # unterbricht das Hochzaehlen.
            if not GPIO.input(ROTES_TEAM_PIN):
                punkte_rot += 1
                aktualisiere_punkte_anzeige(segment, ROT, punkte_rot)

            if not GPIO.input(BLAUES_TEAM_PIN):
                punkte_blau += 1
                aktualisiere_punkte_anzeige(segment, BLAU, punkte_blau)

            time.sleep(0.1)
    finally:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

Oder mit einem Team-Objekt, um die restlichen Code-Wiederholungen zu eliminieren:

Code: Alles auswählen

#!/usr/bin/env python
import time
from Adafruit_7Segment import SevenSegment
from RPi import GPIO

ROTES_TEAM_PIN, BLAUES_TEAM_PIN = [27, 24]

class Team(object):
    def __init__(self, segment, pin, index):
        self.pin = pin
        self.index = index
        self.segment = segment
        self.punkte = 0
        self.aktualisiere_punkte_anzeige()
        GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def aktualisiere_anzeige(self):
        for index, ziffer in enumerate(divmod(self.punkte, 10), self.index):
            self.segment.writeDigit(index, ziffer)
 
    def aktualisiere_punkte(self):
        # ``not`` kehrt den Eingabewert des Schalterzustands um.
        # Ohne diese Operation zaehlen die Torstaende bei nicht
        # gedruecktem Schalter hoch, sondern druecken der Schalter
        # unterbricht das Hochzaehlen.
        if not GPIO.input(self.pin):
            self.punkte += 1
            self.aktualisiere_anzeige()
 
def main():
    try:
        GPIO.setmode(GPIO.BCM)
        segment = SevenSegment(address=0x70)
 
        print '------------------'
        print 'Programm fuer Tischkicker mit zwei Druckschaltern.'
        print 'Torstands-Ausgabe an Adafruit 7-Segment Display'
        print '------------------'
 
        teams = [
            Team(segment, ROTES_TEAM_PIN, 0),
            Team(segment, BLAUES_TEAM_PIN, 3),
        ]
 
        while True:
            for team in teams:
                team.aktualisiere_punkte()
            time.sleep(0.1)
    finally:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Swi_Mo hat geschrieben:Unsere zwei Schlaufen laufen inzwischen.
Ehe sich der Fehler einschlauft: Schlaufe vs. Schleife
Swi_Mo
User
Beiträge: 5
Registriert: Donnerstag 15. September 2016, 08:27

Inzwischen ist unser RaspberryPi-Projekt zu einem Arduino-Projekt geworden und abgeschlossen. :D Unser sehr beschränktes Python-Wissen liess sich problemlos in C++ vom Arduino transferieren. Die beiden Schlaufen laufen inzwischen anstandslos im Tischkicker-Zähler.
Bild
Am Ende der 1. Seite von folgendem Thread gibt es Infos des fertigen Projekts und den verwendeten Code.
http://www.forum-raspberrypi.de/Thread- ... t-geworden

Beste Grüsse!
Antworten