Inhalt von Variablen in separatem Fenster anzeigen

Fragen zu Tkinter.
Antworten
GodsHell
User
Beiträge: 3
Registriert: Montag 1. November 2021, 10:57

Hallo zusammen,
ich habe vor 2 Jahren begonnen eine Winterlandschaft mit Karussells, Verkaufs Buden usw. über einen Raspberry zu steuern. Unter anderem wird auch eine H0 Eisenbahn mit einem Bahnhof angesteuert. Programmiert wurde das ganze mit Python3 und es funktioniert auch alles einwandfrei.
Für die Eisenbahn mit Bahnhof habe ich folgendes Script verwendet:

Code: Alles auswählen

import RPi.GPIO as GPIO
import time
import random

GPIO.setmode(GPIO.BCM)
GPIO.setup(9, GPIO.OUT)
GPIO.setup(13, GPIO.OUT)
GPIO.setup(12, GPIO.OUT)
GPIO.setup(14, GPIO.IN)

try:
    while True:
        #Ausgangssituation
        GPIO.output(12,0) #Bahnhof öffnet, Licht an
        GPIO.output(13,0) #Versorgung DC-Wandler ein
        GPIO.output(9,1) #Zug aus
        print ("Bahnhof geöffnet, bitte einsteigen")
        time.sleep(25) #Fahrgäste steigen ein

        GPIO.output(9,0) #Zug starten
        x = random.randint(3,12) #Generator Rundenanzahl
        print("Der Zug wird",  x,  "Runden fahren") #Gesamtrunden anzeigen
        i = x #Ergebnis nach i verschieben
        print("Zug fährt an")
        time.sleep(0.5) #Initiatorpuffer nach Zug
    
        while i > 0: #warten bis i=0         
            GPIO.input(14) #Initiatorabfrage
            time.sleep(0.06) #Initiatorpuffer vor Zug
            if GPIO.input(14) == 0: #Zug überfährt Initiator
                time.sleep(0.3) #Initiator entprellen
                i = i - 1 #abwärtscounter
                print("Der Zug fährt noch: ",  i,  "Runden") #Zählerstand anzeigen
        GPIO.output(9,1) #Zug stoppen
        print("Zug steht, bitte aussteigen")
        time.sleep(20) #Fahrgäste steigen aus

        GPIO.output(13,1) #Bahnhof schließt, Licht aus
        print("Bahnhof geschlossen")
        time.sleep(20) #Bahnhof geschlossen

            
except KeyboardInterrupt:
    GPIO.cleanup()
Mir ist klar, das einige Passagen umständlich programmiert sind, aber es funktioniert.
Nun versuche ich seit einigen Wochen die "print" Ausgaben in ein Fenster mit tkinter zu bekommen, was mir aber einfach nicht gelingen will. Ich habe bereits mehrere Lösungsansätze aus diversen Foren versucht aber entweder läuft das Programm nicht mehr oder das Fenster öffnet sich nicht bzw der Inhalt der Variablen "X" und "i" wird nicht angezeigt.
Es wäre toll, wenn mir jemand weiterhelfen könnte.
Vielen Dank schon mal.
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@GodsHell: Erst mal zum vorhandenen Programm:

``as`` beim Importieren ist zum umbenennen, `GPIO` wird aber gar nicht umbenannt.

`GPIO.cleanup()` sollte *immer* aufgerufen werden, nicht nur wenn der Benutzer das mit Strg+C abbricht, sondern auch wenn beispielsweise ein Fehler auftritt. Und das Aufsetzen der Pins gehört da auch schon in den ”geschützten” Bereich, denn auch wenn das dort am Anfang schon unterbrochen wird, sollten die bisher gemachten `setup()`\s wieder aufgeräumt werden.

Die Pin-Nummern sind magische Zahlen die über das Programm verteilt sind. Wenn man beispielsweise das was an Pin 12 angeschlossen ist, an einen anderen Pin anschliessen will, muss man alle 12 im Programm suchen und ändern. Alle? Nein, nicht ganz, man muss aufpassen, das man bei dem `randint()`-Aufruf die 12 nicht ändert, denn die steht nicht für den Pin. Deshalb definiert man für solche Zahlen Konstanten. Möglichst mit einem Namen der dem Leser verrät wofür der Pin da ist.

Da habe ich jetzt bei Pin 12 ein Problem, denn an einer Stelle steht ``#Bahnhof öffnet, Licht an`` als Kommentar, bei dem Kommentar ``#Bahnhof schließt, Licht aus`` wird dann aber Pin 13 geschaltet und nicht Pin 12‽ Pin 12 wird überhaupt nur einmal am Schleifenanfang geschaltet, und zwar immer auf den gleichen Wert. Warum steht das dann *in* der Schleife? Und nicht davor?

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Eine ganze Reihe von Kommentaren wird man durch das ersetzen der magischen Zahlen für die Pin-Nummern los.

``i = x`` ist sinnfrei, da sollte man die Rundenzahl gleich an den richtigen Namen binden. Und zwar weder an `i` noch an `x`, sondern an `runden_anzahl`, weil man sich dann wieder einen Kommentar sparen kann, die erklärt was `x` eigentlich bedeuten soll.

Anstelle von ``while`` würde man eine ``for``-Schleife verwenden.

Die erste Abfrage von Pin 14 in der Schleife ist sinnfrei, weil das keinen Effekt hat.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import random
import time

from RPi import GPIO

DC_POWER_SWITCH_PIN = 12  # active low!
TRAIN_POWER_PIN = 9  # active low!
TRAIN_STATION_LIGHT_PIN = 13  # active low!

PROXIMITY_SWITCH_PIN = 14  # active low!


def main():
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(
            [TRAIN_POWER_PIN, DC_POWER_SWITCH_PIN, TRAIN_STATION_LIGHT_PIN],
            GPIO.OUT,
            initial=1,
        )
        GPIO.setup(PROXIMITY_SWITCH, GPIO.IN)

        GPIO.output(DC_POWER_SWITCH_PIN, 0)
        while True:
            GPIO.output(TRAIN_STATION_LIGHT_PIN, 0)
            GPIO.output(TRAIN_POWER_PIN, 1)
            print("Bahnhof geöffnet, bitte einsteigen")
            time.sleep(25)  # Fahrgäste steigen ein.

            GPIO.output(TRAIN_POWER_PIN, 0)
            rounds_total = random.randint(3, 12)
            print("Der Zug wird", rounds_total, "Runden fahren")
            print("Zug fährt an")
            time.sleep(0.5)
            for rounds_left in reversed(range(1, rounds_total)):
                time.sleep(0.06)
                if not GPIO.input(PROXIMITY_SWITCH_PIN):
                    time.sleep(0.3)  # Entprellen.
                    print("Der Zug fährt noch:", rounds_left, "Runden")

            GPIO.output(TRAIN_POWER_PIN, 1)
            print("Zug steht, bitte aussteigen")
            time.sleep(20)  # Fahrgäste steigen aus.

            GPIO.output(TRAIN_STATION_LIGHT_PIN, 1)
            print("Bahnhof geschlossen")
            time.sleep(20)  # Bahnhof geschlossen.

    except KeyboardInterrupt:
        pass
    finally:
        GPIO.cleanup()


if __name__ == "__main__":
    main()
Wenn Du das mit einer GUI verbinden willst, musst Du Python-Grundlagen bis einschliesslich objektorientieter Programmierung (OOP) drauf haben. Denn jede nicht-triviale GUI braucht das. Denn da hast nicht mehr die Kontrolle in so einer grossen linear ablaufenden Funktion, sondern die Hauptschleife des GUI-Rahmenwerks ist das was ständig laufen muss. Für bestimmte Ereignisse kannst Du dann Rückrufe registrieren, die von der Hauptschleife aufgerufen werden wenn das Ereignis eingetreten ist. Ereignisse können Klicks auf Schaltflächen sein, aber auch das eine bestimmte Zeitspanne abgelaufen ist.

Deine Ereignisbehandlung darf dann *kurz* etwas machen, und muss dann wieder zur GUI-Hauptschleife zurückkehren. Du kannst also keine länger laufenden Schleifen haben. Das muss alles in kleine Häppchen in Funktionen oder Methoden aufgeteilt werden. Wenn Funktionen, dann muss man immer alles was die anderen brauchen als Argument(e) durchreichen. Weshalb man nicht wirklich um OOP herum kommt, damit man den Zustand zu einem Objekt zusammenfassen kann.

Bei Tk kann man Rückrufe die nach einer angegebenen Zeit aufgerufen werden sollen mit der `after()`-Methode auf Widgets registrieren.

Alternativ könnte man den bisherigen Code in einem Thread ausführen und mit einer `queue.Queue`, die mit `after()` regelmässig abgefragt wird mit der GUI im Hauptthread kommunizieren. Denn die GUI selbst darf nur aus dem Thread heraus manipuliert werden, in dem dir GUI-Hauptschleife läuft.

Vorher würde ich das aber noch von `RPi.GPIO` auf `gpiozero` umstellen. Das hat eine wesentlich angenehmere API und man kann bei den entsprechenden Objekten in die man die Pins kapselt beim erstellen schon angeben, das die low active sind, und dann kann man die semantisch korrekten Methoden `on()` und `off()` verwenden, statt das man beim lesen immer daran denken muss das 1 aus und 0 an bedeutet.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
GodsHell
User
Beiträge: 3
Registriert: Montag 1. November 2021, 10:57

Hallo blackjack,
vielen Dank zunächst für deine schnelle Antwort. Ich habe in der Tat die Kommentare für die GPIO´s 12 und 13 vertauscht, was aber keinen Einfluss auf die Funktion hat. Ich habe nun einige deiner Verbesserungen übernommen. Die erste Abfrage an GPIO 14 hat folgende Bewandtnis: An diesem Eingang ist ein Festo-Initiator über einen PullDown angeschlossen, der mittels eines Magneten unter dem letzten Waggon aktiviert wird. Leider hat dieser Initiator die dumme Angewohnheit stark zu flackern, und zwar kurz bevor und kurz nachdem der Magnet über ihn hinweg fährt (Undefinierter Zustand). Durch diese Abfrage mit anschließendem "sleep" kann ich das Flackern vor dem eigentlichen Schaltzustand verhindern. Mit dem Anschließenden zweiten "sleep" wird das Flackern nach dem Schaltzustand verhindert. Ohne diese Maßnahmen zählt der Raspi jedes mal zwischen 20 und 30 Schaltvorgänge. Dadurch bleibt der Zug sofort wieder stehen.
Mein eigentliches Problem war ja auch nur das Umleiten der "prints" in ein separates Fenster mit Hilfe von tkinter. Da dies wohl doch wesentlich komplizierter ist als ich dachte, werde ich dieses Vorhaben wieder verwerfen.
Mein Script werde ich aber auf jeden Fall überarbeiten und deine Tipps dabei beherzigen. Vielen Dank für deine Hilfe.
Gruß GodsHell
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@GodsHell: Dann ist die erste Abfrage von Pin 14 ein sinnvoller Kandidat für einen Kommentar. Denn welche Auswirkungen das ausserhalb hat, kann man dem Code nicht ansehen und dann ist so etwas ”überflüssiges” schnell mal wegoptimiert. 🙂
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13123
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich habe das mal auf `gpiozero` umgestrickt und den Schleife in eine Funktion herausgezogen die jetzt auch nicht mehr direkt von `print()` abhängig ist.

Code: Alles auswählen

#!/usr/bin/env python3
import random
import time

from gpiozero import DigitalInputDevice, DigitalOutputDevice

DC_POWER_SWITCH_PIN = 12  # active low!
TRAIN_POWER_PIN = 9  # active low!
TRAIN_STATION_LIGHT_PIN = 13  # active low!

PROXIMITY_SWITCH_PIN = 14  # active low!


def loop(train_station_light, train, proximity_switch, message_callback):
    while True:
        train_station_light.on()
        train.off()
        message_callback("Bahnhof geöffnet, bitte einsteigen")
        time.sleep(25)  # Fahrgäste steigen ein.

        train.on()
        rounds_total = random.randint(3, 12)
        message_callback(f"Der Zug wird {rounds_total} Runden fahren")
        message_callback("Zug fährt an")
        time.sleep(0.5)
        for rounds_left in reversed(range(1, rounds_total)):
            proximity_switch.wait_for_active()
            message_callback(f"Der Zug fährt noch: {rounds_left} Runden")

        train.off()
        message_callback("Zug steht, bitte aussteigen")
        time.sleep(20)  # Fahrgäste steigen aus.

        train_station_light.off()
        message_callback("Bahnhof geschlossen")
        time.sleep(20)  # Bahnhof geschlossen.


def main():
    try:
        train = DigitalOutputDevice(TRAIN_POWER_PIN, active_high=False)
        dc_power = DigitalOutputDevice(DC_POWER_SWITCH_PIN, active_high=False)
        train_station_light = DigitalOutputDevice(
            TRAIN_STATION_LIGHT_PIN, active_high=False
        )
        proximity_switch = DigitalInputDevice(
            PROXIMITY_SWITCH_PIN, active_state=False, bounce_time=0.36
        )
        dc_power.on()
        loop(train_station_light, train, proximity_switch, print)

    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
GodsHell
User
Beiträge: 3
Registriert: Montag 1. November 2021, 10:57

Danke, werde ich am WE testen. Bin jetzt wieder auf Montage.
Gruß GodsHell
Antworten