Thread mit globaler Temperaturausgabe

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
Finn_h
User
Beiträge: 12
Registriert: Samstag 30. Mai 2020, 08:51

Hallo,

ich habe an meinen Raspberry Pi ein ds18b20 Temperaturfühler angeschlossen. Ich möchte einen Thread starten der mir parallel zum restlichen Programm die Temperatur als globale Variable zur Verfügung stellt. Die aktuelle Temperatur möchte ich dann in einer GUI (benutzte Tkinter) anzeigen. Wenn ich die Temperatur mit random.randint simuliere funktioniert das Programm auch wie gewollt. Sobald ich aber die reale Temperatur auslese (im angefügten Code kommentiert), erhalte ich eine Fehlermeldung, dass die Temperatur nicht bekannt, obwohl ich diese als global deklariert habe.
Über eure Hilfe wäre ich sehr dankbar.
MfG

Code: Alles auswählen

import tkinter as tk
import time
import threading
import random

serialNum = "28-01192139d25e"


def display_Temp():

    current_Temp = str(Temperatur)
    Temp_label["text"] = current_Temp
    my_window.after(5000, display_Temp)


def Temp_find():
    while True:
        global Temperatur
        #location = '/sys/bus/w1/devices/28-01192139d25e/w1_slave'
        #tfile = open(location)
        #text = tfile.read()
        #tfile.close()
        #secondline = text.split("\n")[1]
        #temperaturedata = secondline.split(" ")[9]
        #Temperatur = float(temperaturedata[2:]) / float(1000)
        Temperatur = random.randint(25,30)
        print(Temperatur)
        time.sleep(5)



t1 =threading.Thread(target = Temp_find)
t1.start()


my_window = tk.Tk()
my_window.title("Temperatur")
description_label = tk.Label(my_window, text = "Die aktuelle Temperatur beträgt: ", font=("Arial",20))
Temp_label = tk.Label(my_window, font=("Arial",20))
description_label.grid(row = 0, column = 0)
Temp_label.grid(row = 0 , column = 1)
display_Temp()
my_window.mainloop()
 
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Finn_h: Das Problem kann ich zwar praktisch nicht nachvollziehen, bei mir ”funktioniert” das, aber Du hast da eine „race condition“ wo nebenläufig ein Code läuft der `Temperatur` definiert und ein Code läuft der `Temperatur` in der GUI aktualisiert. Und wenn der Code, der die GUI aktualisiert, schneller zu dem Punkt kommt `Temperatur` zu verwenden als der andere Code zu dem Punkt kommt `Temperatur` das erste mal zu definieren, dann gibt es logischerweise den Fall das man einen `NameError` bekommt.

``global`` definiert keine Variablen, das deklariert die nur als (modul)global. Das heisst das sorgt dafür das die Funktion den Namen nicht lokal, sondern im Modulnamensraum sucht. Da existiert sie aber erst wenn ihr dort auch tatsächlich ein Wert zugewiesen wurde.

Soweit zur Erklärung allerdings verwendet man sowieso kein ``global``. Bei objektorientierter Programmierung (OOP) gibt es da auch keinen Grund zu, und die braucht man im Grunde bei jeder nicht-trivialen GUI. Also vergiss das es ``global`` gibt.

Weitere Anmerkungen zum Code: Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben. Das ist dann der Punkt an dem man bei GUI-Programmierung Closures und/oder Klassen braucht. `display_Temp()` benötigt also mindestens das Label auf dem die Temperatur angezeigt wird als Argument und die Queue über die die Temperatur übermittelt wird. Und die Queue muss auch dem Thread übergeben werden.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Namen sollten keine Abkürzungen oder unsinnige Prä- oder Postfixe enthalten und auch nicht nummeriert werden. `my_` ist beispielsweise unsinnig. Das hat keinerlei Informationsgehalt.

Der Thread braucht gar keinen Namen, da nach dem erstellen und starten nicht mehr auf das Objekt zugegriffen wird.

Man sollte Threads eigentlich immer mit ``daemon=True`` starten, es sei denn man will wirklich, dass das Programm mit dem Thread weiterläuft, auch wenn der Hauptthread beendet ist. Was man gerade bei GUI-Programmen nicht möchte, denn wenn man das Fenster schliesst, läuft dann immer noch der Thread weiter, und wenn man das Programm nicht von einer Konsole aus gestartet hat, dann ist das nur noch über die Prozessverwaltung des Betriebssystems zu beenden. Oder man macht ein Konsolenfenster auf, sucht sich die Prozess-ID heraus, und verwendet das entsprechende Konsolenkommando zum beenden von Prozessen. Beides ist unnötig aufwändig.

`Temp_label` enthält eine Abkürzung die zu allem Überfluss auch sehr häufig für „Temporär” steht. Wenn man `temperature` meint, sollte man das auch schreiben.

`Temp_find` klingt komisch weil die Temperatur ja nicht „gefunden“ wird, sondern gelesen. Ist dann auch von der Reihenfolge der Worte eher so Yoda.

`serialNum` ist eine Konstante, sollte also `SERIAL_NUMBER` heissen, und vielleicht worde es Sinn machen auch noch die Information in den Namen zu packen *wofür* das eine Seriennummer ist.

Und diese Konstante sollte man im Code dann vielleicht auch verwenden.

Dateien sollte man wo möglich mit der ``with``-Anweisung öffnen. Und bei Textdateien immer explizit die Kodierung angeben.

Der `float()`-Aufruf bei der literalen 1000 ist überflüssig.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import queue
import random
import threading
import time
import tkinter as tk
from pathlib import Path

TEMPERATURE_SENSOR_SERIAL_NUMBER = "28-01192139d25e"


def display_temperature(label, temperatures_queue):
    try:
        while True:
            label["text"] = temperatures_queue.get_nowait()
    except temperatures_queue.Empty:
        pass

    label.after(5000, display_temperature, label, temperatures_queue)


def read_temperature(temperature_queue):
    while True:
        # second_line = (
        #     Path(
        #         "/sys/bus/w1/devices",
        #         TEMPERATURE_SENSOR_SERIAL_NUMBER,
        #         "w1_slave",
        #     )
        #     .read_text(encoding="utf-8")
        #     .split("\n")[1]
        # )
        # temperature = float(second_line.split(" ")[9][2:]) / 1000
        # temperature_queue.put(temperature)

        temperature = random.randint(25, 30)
        temperature_queue.put(temperature)
        print(temperature)

        time.sleep(5)


def main():
    window = tk.Tk()
    window.title("Temperatur")

    font = ("Arial", 20)
    tk.Label(window, text="Die aktuelle Temperatur beträgt: ", font=font).grid(
        row=0, column=0
    )
    temperature_label = tk.Label(window, font=font)
    temperature_label.grid(row=0, column=1)

    temperature_queue = queue.Queue()
    threading.Thread(
        target=read_temperature, args=[temperature_queue], daemon=True
    ).start()
    display_temperature(temperature_label, temperature_queue)

    window.mainloop()


if __name__ == "__main__":
    main()
Falls das lesen der Temperatur schnell geht und das auch nicht hängen bleiben kann, könnte man sich den Thread sparen und das in einem `after()`-Rückruf erledigen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Finn_h
User
Beiträge: 12
Registriert: Samstag 30. Mai 2020, 08:51

@__blackjack__
Ich habe mir das bis jetzt alles selbst aus YouTube Videos ohne vernünftige Erklärung zusammengesucht. Daher das unschöne Durcheinander.

Ich danke dir daher für Deine Mühen und Erklärungen. Ich werde versuchen, das umzusetzen.

Noch eine Frage zu Deinem Code:

Ich habe mir diesen rauskopiert und etwas abgewandelt, weil der so bei mir nicht funktioniert hat (ich vermute Dein Code war bis dahin ungetestet?). Das Problem lag wohl in der display_temperature Funktion. Zum einen habe ich die while Schleife und das nowait bei temperatures_queue.get_nowait() entfernt. Gibt es einen Grund warum du dies an der Stelle verwendet hast?

Code: Alles auswählen

def display_temperature(label, temperatures_queue):
    try:
        label["text"] = temperatures_queue.get()
    except temperatures_queue.Empty:
        pass

    label.after(5000, display_temperature, label, temperatures_queue)
MfG
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

get blockiert, das heißt, die GUI friert ein, wenn kein Temperatur gelesen worden ist. Auf der anderen Seite, wenn das Lesen schneller ist als das Darstellen, dann füllt sich die Queue und es werden veraltete Werte dargestellt, daher braucht es das while. Daher muss es so lauten, wie __blackjack__ es geschrieben hat. Es muss nur `queue.Empty` heißen.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ups, da habe ich wohl zu schnell das von der Autovervollständung angebotene `temperatures_queue` ausgewählt. 😳
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten