Sonntag, 17. Dezember 2017

Tkinter und Threads

Der Raspberry Pi ist ein recht populärer Minirechner, besonders für Hobbybastler und Leute, die gerne mit elektrischen Schaltung, welche vom Rechner gesteuert werden, herumspielen.
In Supportforen taucht öfters die Frage auf, wie man Messwerte von Sensoren am besten in graphischen Oberfläche (GUI) visualisiert werden können. Da Tkinter bei Python standardmäßig an Bord ist, wird dieses gerne eingesetzt (auch, wenn Qt oder GTK+ leistungsfähiger sind).

Das grundsätzliche Problem: Das Auslesen von Sensoren kann, je nach Sensor, mehr oder minder lang dauern. Liest man den Sensor über eine Funktion in der GUI aus, kann es sein, dass das Warten auf den Wert des Sensor die GUI bzw. deren Mainloop blockiert. Dies kann man recht einfach dadurch um gehen, dass das Auslesen der Sensoren in einen eigenen Thread auszulesen und die Werte über eine Queue an die GUI zu senden.

Der entsprechende Code kann z.B. wie folgt aussehen:

import tkinter as tk
from queue import Queue
from threading import Thread, Event
from time import sleep
from random import random

def generate_data(my_queue, event):
    while not event.is_set():
        data ={}
        data['value_sensor_1'] = random()
        data['value_sensor_2'] = random()
        my_queue.put(data)        
        sleep(0.7)
      

class Application(tk.Frame):
    def __init__(self, my_queue, thread_kill_event, master=None):
        super().__init__(master)
        self.my_queue = my_queue        
        self.thread_kill_event = thread_kill_event
        self.pack()
        self.create_widgets()
        self.update_labels()

    def create_widgets(self):
        self.label_sensor_1 = tk.Label(self, bg='purple', height=5, width=10, 
                                       text='Sensor 1')
        self.label_sensor_1.pack(side='left')
        self.label_sensor_2 = tk.Label(self, bg='yellow', height=5, width=10,
                                       text='Sensor 2')
        self.label_sensor_2.pack(side='right')
        self.quit = tk.Button(self, text='QUIT', fg='red',
                              command=self.exit_cleanup)
        self.quit.pack(side='bottom')

    def update_labels(self):
        self.master.after(500, self.update_labels)
        if not self.my_queue.empty():
            data = self.my_queue.get()
            if data['value_sensor_1'] < 0.5:
                self.label_sensor_1['bg'] = 'purple'
            else:
                self.label_sensor_1['bg'] = 'pink'
            if data['value_sensor_2'] < 0.5:
                self.label_sensor_2['bg'] = 'yellow'
            else:
                self.label_sensor_2['bg'] = 'blue'

    def exit_cleanup(self):
        self.thread_kill_event.set()
        self.master.destroy()

def main():
    my_queue = Queue()
    my_event = Event()
    my_thread = Thread(target=generate_data, args=(my_queue, my_event, ))
    my_thread.start()
    root = tk.Tk()
    app = Application(my_queue, my_event, master=root)
    app.master.title('TKinter Thread Demo')
    app.mainloop()

if __name__ == '__main__':
    main()

Von Prinzip her ist das eigentlich recht einfach:
Die Funktion generate_data , welche in einem eigenen Thread läuft, produziert alle 0,7 Sekunden einen zufälligen Wert zwischen 0 und 1 für zwei fiktive Sensoren, schreibt die Werte in ein Dictionary, welches dann in die Queue "geschoben" wird. Die 0,7 Sekunden Wartezeit "simulieren" dabei dir Trägheit des Sensors.
Die Klasse Application ist die eigentliche Tkinter-Anwendung. Diese besteht nur aus zwei Labeln und einem Quit-Button.
Die Methode update_labels ist die, welche die Werte aus der Queue holt und in Abhängigkeit vom Wert data['value_sensor_1'] bzw. data['value_sensor_2'] die Farbe des jeweiligen Labels ändert. Wichtig ist die Zeile

self.master.after(500, self.update_labels)

mit der festgelegt wird, dass die Methode alle 500 Millisekunden aufgerufen wird, so dass die Labels periodisch aktualisiert werden.
In der Funktion exit_cleanup wird dann noch der Thread sauber beendet, indem ein Event an die Funktion generate_data gesendet wird, worauf hin diese sich beendet, dann wird noch das Tkinter Fenster gekillt.

Wie zu sehen ist, ist das Entkoppeln von Messwerterfassungen und Messwertvisualisierung nicht weiter schwierig. Hat man mehrere verschiedene Sensoren, lässt sich das obige Beispiel auch einfach auf mehrere Threads erweitern.